diff --git a/frontend/src/components/Dashboard/DashboardView.tsx b/frontend/src/components/Dashboard/DashboardView.tsx index 06abb5d..bb94d40 100644 --- a/frontend/src/components/Dashboard/DashboardView.tsx +++ b/frontend/src/components/Dashboard/DashboardView.tsx @@ -312,6 +312,10 @@ export const isPlaced = (widgets: WidgetDef[]) => * is for — so its widgets are one arrangement rather than a stack of headed * grids. Pages and sections left the schema in 7ff29ca; a document written * the old way is flattened on read. + * + * Drawn below a `LookProvider` mounted by whatever owns the canvas root — the + * surface, the editor, or the panel's stacked branch — rather than mounting a + * second one here, which could only ever restate what that one already says. */ export function DashboardView({ dashboard, @@ -366,14 +370,10 @@ export function DashboardView({ ) - return ( - - {editing ? ( - grid - ) : ( - {grid} - )} - + return editing ? ( + grid + ) : ( + {grid} ) } diff --git a/frontend/src/components/Dashboard/PanelSurface.tsx b/frontend/src/components/Dashboard/PanelSurface.tsx index b4648d9..c0da0f6 100644 --- a/frontend/src/components/Dashboard/PanelSurface.tsx +++ b/frontend/src/components/Dashboard/PanelSurface.tsx @@ -45,7 +45,12 @@ export function PanelSurface({ return (
{stacked ? ( - + // Nothing scales a stack, so there is no canvas element here to state + // the look on: this branch mounts what `CanvasSurface` mounts for the + // other one, touch included. + + + ) : ( // The panel's own surface, scaled to fit. No dots: nothing is being // arranged here. diff --git a/frontend/src/components/Dashboard/settings.tsx b/frontend/src/components/Dashboard/settings.tsx index 3d00362..3882d8e 100644 --- a/frontend/src/components/Dashboard/settings.tsx +++ b/frontend/src/components/Dashboard/settings.tsx @@ -146,12 +146,16 @@ export function useDashboardBackground( * whether the surface is light or dark is a fact about the palette rather than * a second setting that could disagree with it. The class still goes on, * because a chart canvas and the shadcn pieces inside the tile read it. + * + * The roles are passed in rather than resolved here: the caller needs them for + * the tokens anyway, and reading them twice subscribes to the palette's + * message twice. */ export function useDashboardTheme( dashboard: DashboardDef_Output | undefined, + roles: Roles | null, ): "" | "light" | "dark" { const value = useSetting(dashboard, "theme") - const roles = rolesOf(useDashboardPalette(dashboard)) if (roles) return isLight(roles.background) ? "light" : "dark" return value === "dark" || value === "light" ? value : "" } diff --git a/frontend/src/components/Dashboard/ui/core/look.tsx b/frontend/src/components/Dashboard/ui/core/look.tsx index 8449fa5..eb009ad 100644 --- a/frontend/src/components/Dashboard/ui/core/look.tsx +++ b/frontend/src/components/Dashboard/ui/core/look.tsx @@ -62,8 +62,8 @@ export function useCanvasRoot( touch = false, ) { const { resolvedTheme } = useTheme() - const stated = useDashboardTheme(dashboard) const roles = useDashboardRoles(dashboard) + const stated = useDashboardTheme(dashboard, roles) const look = useDashboardLook(dashboard) // The tokens are handed to every widget through the context below, so they // have to outlive a render: a fresh object here is a re-render of the whole diff --git a/frontend/src/components/UserSettings/RemoteAccess.tsx b/frontend/src/components/UserSettings/RemoteAccess.tsx index 1b4ec3f..6b68c24 100644 --- a/frontend/src/components/UserSettings/RemoteAccess.tsx +++ b/frontend/src/components/UserSettings/RemoteAccess.tsx @@ -138,21 +138,16 @@ export function RemoteAccess() {

- + {status.enrolled ? "Re-pair" : "Connect"} +
) diff --git a/frontend/src/components/ui/loading-button.tsx b/frontend/src/components/ui/loading-button.tsx index ea78b4a..6ed4359 100644 --- a/frontend/src/components/ui/loading-button.tsx +++ b/frontend/src/components/ui/loading-button.tsx @@ -1,39 +1,9 @@ import { Slot, Slottable } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import type { VariantProps } from "class-variance-authority" +import { buttonVariants } from "@/components/ui/button" import { FluksioLoader } from "@/components/ui/fluksio-loader" import { cn } from "@/lib/utils" -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-full text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-none hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-none hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-none hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-none hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-full gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-full px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -) - export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { @@ -41,6 +11,12 @@ export interface ButtonProps loading?: boolean } +/** + * A button that draws the brand loader beside its label while it waits. + * + * Wears `Button`'s own variants rather than a copy of them: a copy drifts, and + * a call site asking for one it never grew loses it without saying so. + */ function LoadingButton({ className, loading = false, @@ -64,4 +40,4 @@ function LoadingButton({ ) } -export { buttonVariants, LoadingButton } +export { LoadingButton } diff --git a/frontend/tests/widgets.spec.ts b/frontend/tests/widgets.spec.ts index a6f5e84..b6bb699 100644 --- a/frontend/tests/widgets.spec.ts +++ b/frontend/tests/widgets.spec.ts @@ -302,16 +302,20 @@ async function openPanel(page: Page, name = dashboardName) { ) } +/** The Load tile, scoped: the panel carries a second bar (the unbound one). */ +const loadTile = (page: Page) => + page.getByTestId("widget-frame").filter({ hasText: "Load" }) + test("a bar draws a row per reading", async ({ page }) => { await openPanel(page) // The panel's bar is stored the way a bar was written before it had rows — // one reading with a second nested in it — so drawing two rows is also what // proves that shape is still read. - const rows = page.getByTestId("bar-row") + const rows = loadTile(page).getByTestId("bar-row") await expect(rows).toHaveCount(2) - const fills = page.getByTestId("bar-fill") + const fills = loadTile(page).getByTestId("bar-fill") const width = (index: number) => fills .nth(index) @@ -355,11 +359,17 @@ test("a reading over its scale fills the track and no more", async ({ // bounding box is not clipped by an ancestor's overflow. Where it settles is // what "fills the track and no more" is about, so that is what is measured: // flush with the track, neither short of it nor over. - const track = page.getByTestId("bar-row").nth(1).locator(".dui-bar-track") + const track = loadTile(page) + .getByTestId("bar-row") + .nth(1) + .locator(".dui-bar-track") await expect .poll( async () => { - const fill = await page.getByTestId("bar-fill").nth(1).boundingBox() + const fill = await loadTile(page) + .getByTestId("bar-fill") + .nth(1) + .boundingBox() const rail = await track.boundingBox() return Math.round(fill!.width - rail!.width) }, @@ -705,7 +715,7 @@ for (const scheme of ["light", "dark"] as const) { ) // Every widget still draws, whichever set is asked to draw it. await expect(page.getByTestId("widget-frame")).toHaveCount(TILES) - await expect(page.getByTestId("bar-row")).toHaveCount(2) + await expect(loadTile(page).getByTestId("bar-row")).toHaveCount(2) await page.screenshot({ path: `screenshots/widgets/${scheme}-${look}.png`, @@ -738,7 +748,7 @@ for (const scheme of ["light", "dark"] as const) { expect(Math.round(ground.width)).toBe(Math.round(canvas.width)) expect(Math.round(ground.height)).toBe(Math.round(canvas.height)) - const primary = await page + const primary = await loadTile(page) .getByTestId("bar-fill") .first() .evaluate((el) => getComputedStyle(el).backgroundColor)