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)