Touch is a panel setting, and the rail grows with it
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m9s
Playwright Tests / test-playwright (2, 2) (push) Failing after 11s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m28s
Compose Smoke Test / test-compose (push) Failing after 11s
Playwright Tests / merge-reports (push) Failing after 2m19s
Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m9s
Playwright Tests / test-playwright (2, 2) (push) Failing after 11s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m28s
Compose Smoke Test / test-compose (push) Failing after 11s
Playwright Tests / merge-reports (push) Failing after 2m19s
It described the wrong object. A dashboard is a document that may hang on a
hallway tablet and in a desk browser at the same time, and only one of those
has fingers on it — so the flag moves off `DashboardDef.settings` and onto
`PanelDef` as a plain bool, ticked in the Panels dialog. `useCanvasRoot` takes
it as an argument rather than reading the document, and `/panel/{id}` is the
only surface with a panel to ask.
Dropping the message binding with it is deliberate: nothing drove it, and a
flow deciding whether a screen has fingers on it was never the point. A stored
`settings.touch` is inert rather than migrated, which `_check_settings`
skipping unknown names already guaranteed.
The rail was the other half. It had no touch behaviour at all and its 40px
buttons met neither branch of the 44/32 rule. `[data-touch] .dui-rail{-item}`
in `ui/core/core.css` spends the padding and the gap on the buttons instead,
so they reach the 44px target and the rail comes out taller at exactly the
same width — `RAIL_INSET` never moves, and the arrangement under it does not
either.
Also closes the panels-dialog icon gap: `DashboardSummary` carries the `icon`
now, so the dialog draws each assigned dashboard's rail glyph beside its
checkbox. `initials()` went from three identical copies in the looks to one in
`Dashboard/icons.ts`, so the dialog and the rail fall back the same way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va7ExQDtuwKN7kNpHhWWNQ
This commit is contained in:
@@ -156,6 +156,7 @@ export function CanvasSurface({
|
||||
dashboard,
|
||||
dots,
|
||||
rail,
|
||||
touch,
|
||||
children,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
@@ -163,6 +164,8 @@ export function CanvasSurface({
|
||||
dots?: boolean
|
||||
/** The dashboard-switching rail, drawn on the panel rather than beside it. */
|
||||
rail?: React.ReactNode
|
||||
/** Whether the panel this canvas hangs on is touched rather than pointed at. */
|
||||
touch?: boolean
|
||||
children: (scale: number) => React.ReactNode
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
@@ -183,7 +186,7 @@ export function CanvasSurface({
|
||||
|
||||
const { width, height } = canvasOf(dashboard)
|
||||
const scale = Math.min(box.width / width, box.height / height)
|
||||
const root = useCanvasRoot(dashboard)
|
||||
const root = useCanvasRoot(dashboard, touch)
|
||||
const area = areaOf(dashboard, Boolean(rail))
|
||||
|
||||
return (
|
||||
@@ -191,7 +194,7 @@ export function CanvasSurface({
|
||||
{/* Measured first: a guessed scale would place the whole panel once and
|
||||
then move it. */}
|
||||
{scale > 0 ? (
|
||||
<LookProvider dashboard={dashboard}>
|
||||
<LookProvider dashboard={dashboard} touch={touch}>
|
||||
<div
|
||||
data-look={root["data-look"]}
|
||||
data-touch={root["data-touch"]}
|
||||
|
||||
@@ -22,6 +22,7 @@ export function PanelSurface({
|
||||
dashboard,
|
||||
stacked,
|
||||
rail,
|
||||
touch,
|
||||
}: {
|
||||
dashboard: Dashboard
|
||||
/** A landscape arrangement scaled onto a phone comes out at about a fifth of
|
||||
@@ -29,6 +30,8 @@ export function PanelSurface({
|
||||
stacked?: boolean
|
||||
/** The way between this panel's dashboards, drawn on the canvas itself. */
|
||||
rail?: React.ReactNode
|
||||
/** Whether the screen this hangs on is touched. Stated by the panel route. */
|
||||
touch?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="relative size-full">
|
||||
@@ -37,12 +40,12 @@ export function PanelSurface({
|
||||
) : (
|
||||
// The panel's own surface, scaled to fit. No dots: nothing is being
|
||||
// arranged here.
|
||||
<CanvasSurface dashboard={dashboard} rail={rail}>
|
||||
<CanvasSurface dashboard={dashboard} rail={rail} touch={touch}>
|
||||
{() => <DashboardView dashboard={dashboard} rail={Boolean(rail)} />}
|
||||
</CanvasSurface>
|
||||
)}
|
||||
{/* Outside the canvas, so it needs the look stated for it. */}
|
||||
<LookProvider dashboard={dashboard}>
|
||||
<LookProvider dashboard={dashboard} touch={touch}>
|
||||
<LockNotice dashboard={dashboard} />
|
||||
</LookProvider>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type PanelsConfig,
|
||||
PanelsService,
|
||||
} from "@/client"
|
||||
import { initials, resolveIcon } from "@/components/Dashboard/icons"
|
||||
import {
|
||||
dashboardsQueryOptions,
|
||||
panelsQueryOptions,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import useAuth from "@/hooks/useAuth"
|
||||
import useCustomToast from "@/hooks/useCustomToast"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -120,6 +122,7 @@ export function PanelsDialog() {
|
||||
dashboards={known.map((dashboard) => ({
|
||||
name: dashboard.name,
|
||||
title: dashboard.title || dashboard.name,
|
||||
icon: dashboard.icon ?? "",
|
||||
}))}
|
||||
onChange={(next) => replace(panel.id, next)}
|
||||
onRemove={() =>
|
||||
@@ -196,7 +199,7 @@ function PanelRow({
|
||||
* links — worth seeing — with the writes turned off rather than a 403.
|
||||
*/
|
||||
canEdit: boolean
|
||||
dashboards: { name: string; title: string }[]
|
||||
dashboards: { name: string; title: string; icon: string }[]
|
||||
onChange: (next: PanelDef) => void
|
||||
onRemove: () => void
|
||||
}) {
|
||||
@@ -294,6 +297,25 @@ function PanelRow({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<label
|
||||
htmlFor={`touch-${panel.id}`}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<span className="flex-1">Touch friendly</span>
|
||||
<InfoTip label="Touch friendly">
|
||||
Bigger controls and a bigger rail, and nothing that only happens on
|
||||
hover. Set here rather than on a dashboard because it describes the
|
||||
screen: the same dashboard may also hang somewhere with a mouse.
|
||||
</InfoTip>
|
||||
<Switch
|
||||
id={`touch-${panel.id}`}
|
||||
checked={panel.touch === true}
|
||||
disabled={!canEdit}
|
||||
data-testid={`panel-touch-${panel.id}`}
|
||||
onCheckedChange={(touch) => onChange({ ...panel, touch })}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{dashboards.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No dashboards to assign yet.
|
||||
@@ -303,6 +325,10 @@ function PanelRow({
|
||||
{dashboards.map((dashboard) => {
|
||||
const position = assigned.indexOf(dashboard.name)
|
||||
const id = `assign-${panel.id}-${dashboard.name}`
|
||||
// What this dashboard draws on the rail. Shown rather than set: the
|
||||
// icon belongs to the document, and this dialog has no draft to put
|
||||
// a change into.
|
||||
const Glyph = resolveIcon(dashboard.icon)
|
||||
return (
|
||||
<label
|
||||
key={dashboard.name}
|
||||
@@ -316,6 +342,16 @@ function PanelRow({
|
||||
disabled={!canEdit}
|
||||
onCheckedChange={() => toggle(dashboard.name)}
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex size-5 shrink-0 items-center justify-center text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{Glyph ? (
|
||||
<Glyph className="size-4" />
|
||||
) : (
|
||||
initials(dashboard.title)
|
||||
)}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{dashboard.title}</span>
|
||||
{position >= 0 ? (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -100,6 +100,20 @@ export function resolveIcon(name: string): LucideIcon | null {
|
||||
return CircleHelp
|
||||
}
|
||||
|
||||
/**
|
||||
* Two letters off a title, so a rail of four reads as four different things.
|
||||
*
|
||||
* The fallback wherever a glyph is drawn from a name that may be empty: the
|
||||
* rail in all three looks, and the panels dialog showing what the rail will
|
||||
* draw.
|
||||
*/
|
||||
export function initials(label: string): string {
|
||||
const words = label.split(/[\s_-]+/).filter(Boolean)
|
||||
if (words.length === 0) return "?"
|
||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
||||
return (words[0][0] + words[1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* What an icon may be tinted, by name.
|
||||
*
|
||||
|
||||
@@ -1521,7 +1521,6 @@ function PalettePicker({
|
||||
/** How a setting's own value reads in the "nothing is driving it" line. */
|
||||
function valueLabel(name: SettingName, value: unknown): string {
|
||||
if (name === "locked") return value === true ? "read-only" : "editable"
|
||||
if (name === "touch") return value === true ? "touch friendly" : "pointer"
|
||||
if (name === "look") return lookOf(value)
|
||||
if (name === "background") return value ? "that image" : "no image"
|
||||
if (name === "palette") {
|
||||
@@ -1560,7 +1559,6 @@ export function DashboardPanel({
|
||||
const locked = settingOf(dashboard, "locked")
|
||||
const look = settingOf(dashboard, "look")
|
||||
const background = settingOf(dashboard, "background")
|
||||
const touch = settingOf(dashboard, "touch")
|
||||
const paletteSetting = settingOf(dashboard, "palette")
|
||||
const palette = parsePalette(paletteSetting.value)
|
||||
|
||||
@@ -1765,28 +1763,6 @@ export function DashboardPanel({
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection
|
||||
title="Touch"
|
||||
help="Bigger controls, and nothing that only happens on hover, for a panel that is touched rather than pointed at. A phone gets this anyway; a wall panel cannot say so for itself."
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 text-sm">
|
||||
Touch friendly
|
||||
<Switch
|
||||
checked={touch.value === true}
|
||||
aria-label="Touch friendly"
|
||||
data-testid="dashboard-touch"
|
||||
onCheckedChange={(value) =>
|
||||
setSetting("touch", { ...touch, value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SettingBinding
|
||||
name="touch"
|
||||
setting={touch}
|
||||
onChange={(setting) => setSetting("touch", setting)}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection
|
||||
title="Lock"
|
||||
help="Locked, the controls are still shown but stop publishing, and the surface says so. This is a read-only surface, not a permission: what a paired screen may reach is decided by its own credential."
|
||||
|
||||
@@ -40,8 +40,6 @@ export const SETTING_DTYPES = {
|
||||
palette: "list",
|
||||
/** The URL of an image drawn under the widgets. */
|
||||
background: "str",
|
||||
/** Bigger controls, for a panel that is touched rather than pointed at. */
|
||||
touch: "bool",
|
||||
} as const satisfies Record<string, string>
|
||||
|
||||
/** The settings this build actually wires up. */
|
||||
@@ -119,11 +117,6 @@ export function useDashboardBackground(
|
||||
return typeof value === "string" ? value.trim() : ""
|
||||
}
|
||||
|
||||
/** Whether this dashboard is drawn for a finger rather than a pointer. */
|
||||
export const useDashboardTouch = (
|
||||
dashboard: DashboardDef_Output | undefined,
|
||||
): boolean => useSetting(dashboard, "touch") === true
|
||||
|
||||
/**
|
||||
* The class that themes a dashboard's own surface, or `""` to follow the app.
|
||||
*
|
||||
|
||||
@@ -56,6 +56,33 @@
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/*
|
||||
* The rail grows into the column it already has rather than taking a wider one.
|
||||
*
|
||||
* `RAIL_INSET` reserves the same 72px either way — the arrangement must not
|
||||
* move because a screen was told it has fingers on it — so the padding and the
|
||||
* gap pay for the buttons, and the rail comes out taller rather than wider.
|
||||
* 48px of width less two 2px margins is the 44px target the guidelines ask for.
|
||||
*
|
||||
* Geometry here, paint in each look: the sizes are Tailwind literals on the
|
||||
* three `Rail`s, which this beats because the file lands unlayered.
|
||||
*/
|
||||
[data-touch] .dui-rail {
|
||||
padding: 0.125rem;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
[data-touch] .dui-rail-item {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
[data-touch] .dui-rail-item svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pulled up half a step, so the title reads from the centre of the corner
|
||||
* radius rather than from below it — and the body, which is what anyone is
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* Which look is being drawn, and what the canvas it is drawn on carries.
|
||||
*
|
||||
* A dashboard states its look, its colours and whether it is touched on one
|
||||
* element — the canvas root — and everything below reads them from there:
|
||||
* the tokens by inheritance, the look and the touch flag through this context.
|
||||
* A dashboard states its look and its colours, and the panel it hangs on says
|
||||
* whether it is touched, on one element — the canvas root — and everything
|
||||
* below reads them from there: the tokens by inheritance, the look and the
|
||||
* touch flag through this context.
|
||||
*
|
||||
* The style is carried in the context as well as on the element, because a
|
||||
* menu is portalled to `body` and lands outside the canvas. A surface that
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
useDashboardLook,
|
||||
useDashboardPalette,
|
||||
useDashboardTheme,
|
||||
useDashboardTouch,
|
||||
} from "../../settings"
|
||||
import { rolesOf, tokenStyle } from "./theme"
|
||||
|
||||
@@ -47,13 +47,21 @@ export const useLook = () => useContext(LookContext)
|
||||
* for a dashboard that follows the device: the sets state their own colours
|
||||
* per theme, and a chart canvas has to be told which one it is drawing in.
|
||||
* Restating the app's own resolved theme changes nothing when they agree.
|
||||
*
|
||||
* `touch` is passed in rather than read off the document, because it belongs to
|
||||
* the screen rather than to the dashboard: the same document may hang on a
|
||||
* hallway tablet and on a desk browser. Only `/panel/{id}` has a panel to ask,
|
||||
* so everywhere else takes the default — a phone still gets the finger-sized
|
||||
* ladder from the width query in `core.css`.
|
||||
*/
|
||||
export function useCanvasRoot(dashboard: DashboardDef_Output | undefined) {
|
||||
export function useCanvasRoot(
|
||||
dashboard: DashboardDef_Output | undefined,
|
||||
touch = false,
|
||||
) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const stated = useDashboardTheme(dashboard)
|
||||
const roles = rolesOf(useDashboardPalette(dashboard))
|
||||
const look = useDashboardLook(dashboard)
|
||||
const touch = useDashboardTouch(dashboard)
|
||||
return {
|
||||
className: stated || (resolvedTheme === "dark" ? "dark" : "light"),
|
||||
style: (roles ? tokenStyle(roles) : {}) as React.CSSProperties,
|
||||
@@ -72,12 +80,15 @@ export function useCanvasRoot(dashboard: DashboardDef_Output | undefined) {
|
||||
*/
|
||||
export function LookProvider({
|
||||
dashboard,
|
||||
touch,
|
||||
children,
|
||||
}: {
|
||||
dashboard: DashboardDef_Output | undefined
|
||||
/** Whether the panel this is drawn on is touched. See `useCanvasRoot`. */
|
||||
touch?: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const root = useCanvasRoot(dashboard)
|
||||
const root = useCanvasRoot(dashboard, touch)
|
||||
return (
|
||||
<LookContext.Provider
|
||||
value={{
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ICONS } from "../../icons"
|
||||
import { ICONS, initials } from "../../icons"
|
||||
import type {
|
||||
BackdropProps,
|
||||
FrameProps,
|
||||
@@ -110,21 +110,13 @@ export function Frame({
|
||||
)
|
||||
}
|
||||
|
||||
/** Two letters off the title, so a rail of four reads as four different things. */
|
||||
function initials(label: string): string {
|
||||
const words = label.split(/[\s_-]+/).filter(Boolean)
|
||||
if (words.length === 0) return "?"
|
||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
||||
return (words[0][0] + words[1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
export function Rail({ entries }: RailProps) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid={TESTID.rail}
|
||||
className={cn(
|
||||
"fx-rail pointer-events-auto absolute left-3 top-1/2 z-10 flex max-h-[calc(100%-1.5rem)] w-12 -translate-y-1/2 flex-col items-center gap-1 p-1",
|
||||
"dui-rail fx-rail pointer-events-auto absolute left-3 top-1/2 z-10 flex max-h-[calc(100%-1.5rem)] w-12 -translate-y-1/2 flex-col items-center gap-1 p-1",
|
||||
// As tall as what it carries, centred in the column it reserves: a
|
||||
// rail of two stretched to the height of the panel is mostly empty
|
||||
// pill. More dashboards than the panel is tall still scroll, but no
|
||||
@@ -142,7 +134,7 @@ export function Rail({ entries }: RailProps) {
|
||||
aria-label={entry.label}
|
||||
aria-current={entry.active ? "page" : undefined}
|
||||
data-testid={`panel-rail-${entry.name}`}
|
||||
className="fx-rail-item relative flex size-10 shrink-0 items-center justify-center text-xs font-medium"
|
||||
className="dui-rail-item fx-rail-item relative flex size-10 shrink-0 items-center justify-center text-xs font-medium"
|
||||
>
|
||||
{entry.active ? (
|
||||
<motion.span
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ICONS } from "../../icons"
|
||||
import { ICONS, initials } from "../../icons"
|
||||
import type {
|
||||
BackdropProps,
|
||||
FrameProps,
|
||||
@@ -143,21 +143,13 @@ export function Frame({
|
||||
)
|
||||
}
|
||||
|
||||
/** Two letters off the title, so a rail of four reads as four different things. */
|
||||
function initials(label: string): string {
|
||||
const words = label.split(/[\s_-]+/).filter(Boolean)
|
||||
if (words.length === 0) return "?"
|
||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
||||
return (words[0][0] + words[1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
export function Rail({ entries }: RailProps) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid={TESTID.rail}
|
||||
className={cn(
|
||||
"gl-surface gl-rail pointer-events-auto absolute left-3 top-1/2 z-10 flex max-h-[calc(100%-1.5rem)] w-12 -translate-y-1/2 flex-col items-center gap-1 p-1",
|
||||
"dui-rail gl-surface gl-rail pointer-events-auto absolute left-3 top-1/2 z-10 flex max-h-[calc(100%-1.5rem)] w-12 -translate-y-1/2 flex-col items-center gap-1 p-1",
|
||||
// As tall as what it carries, centred in the column it reserves: a
|
||||
// rail of two stretched to the height of the panel is mostly empty
|
||||
// pill. More dashboards than the panel is tall still scroll, but no
|
||||
@@ -176,7 +168,7 @@ export function Rail({ entries }: RailProps) {
|
||||
aria-current={entry.active ? "page" : undefined}
|
||||
data-testid={`panel-rail-${entry.name}`}
|
||||
className={cn(
|
||||
"gl-pressable relative flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-medium",
|
||||
"dui-rail-item gl-pressable relative flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-medium",
|
||||
entry.active ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ICONS } from "../../icons"
|
||||
import { ICONS, initials } from "../../icons"
|
||||
import type {
|
||||
BackdropProps,
|
||||
FrameProps,
|
||||
@@ -109,21 +109,13 @@ export function Frame({
|
||||
)
|
||||
}
|
||||
|
||||
/** Two letters off the title, so a rail of four reads as four different things. */
|
||||
function initials(label: string): string {
|
||||
const words = label.split(/[\s_-]+/).filter(Boolean)
|
||||
if (words.length === 0) return "?"
|
||||
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
||||
return (words[0][0] + words[1][0]).toUpperCase()
|
||||
}
|
||||
|
||||
export function Rail({ entries }: RailProps) {
|
||||
return (
|
||||
<nav
|
||||
aria-label="Dashboards on this panel"
|
||||
data-testid={TESTID.rail}
|
||||
className={cn(
|
||||
"m3-rail pointer-events-auto absolute left-3 top-1/2 z-10 flex max-h-[calc(100%-1.5rem)] w-12 -translate-y-1/2 flex-col items-center gap-1 p-1",
|
||||
"dui-rail m3-rail pointer-events-auto absolute left-3 top-1/2 z-10 flex max-h-[calc(100%-1.5rem)] w-12 -translate-y-1/2 flex-col items-center gap-1 p-1",
|
||||
// As tall as what it carries, centred in the column it reserves: a
|
||||
// rail of two stretched to the height of the panel is mostly empty
|
||||
// pill. More dashboards than the panel is tall still scroll, but no
|
||||
@@ -142,7 +134,7 @@ export function Rail({ entries }: RailProps) {
|
||||
aria-current={entry.active ? "page" : undefined}
|
||||
data-testid={`panel-rail-${entry.name}`}
|
||||
className={cn(
|
||||
"m3-pressable relative flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-medium",
|
||||
"dui-rail-item m3-pressable relative flex size-10 shrink-0 items-center justify-center rounded-full text-xs font-medium",
|
||||
entry.active
|
||||
? "text-card-foreground"
|
||||
: "text-muted-foreground",
|
||||
|
||||
Reference in New Issue
Block a user