Collapse the button-variant fork and the canvas's duplicate work

LoadingButton kept its own copy of buttonVariants, which had drifted past
`brand` and the icon sizes; it imports Button's now, so the Connect /
Re-pair button can show the loader instead of swapping its own label.

DashboardView mounted a second LookProvider inside the one CanvasSurface
already gives it and dropped `touch` on the way, so every widget on a
touch panel read `useLook().touch` as false. The provider is now mounted
once, by whatever owns the canvas root.

useCanvasRoot resolved the palette twice per render — once for the theme
class, once for the tokens — so useDashboardTheme takes the roles the
caller already has.

The bar assertions in widgets.spec.ts are scoped to the Load tile rather
than the page, which held only because the unbound spare bar draws no
rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfgHWr7AQDiLsTQVu7hpKB
This commit is contained in:
2026-09-14 15:29:40 +02:00
co-authored by Claude Opus 5
parent 853fd8f720
commit 79cb3f881a
7 changed files with 50 additions and 60 deletions
+9 -33
View File
@@ -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<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
@@ -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 }