File
Blob: src/client/components/ui/button-classes.ts
| 1 | // ADR: tokens live in a separate `.ts` (not in `button.tsx`) so the component |
| 2 | // module stays component-only for React Fast Refresh — mixed exports force |
| 3 | // full reloads on token edits. They're shared between <Button> and the small |
| 4 | // set of CTAs that render <a href> for top-level navigation (currently the |
| 5 | // OIDC sign-in hand-offs in client/components/auth/{login,invite}-page.tsx), |
| 6 | // which need native link semantics (cmd/ctrl-click new-tab, hover URL |
| 7 | // preview, link role) that a <button onClick=location.assign> would lose. |
| 8 | // Callers MUST consume these tokens — hand-rolling button classes inline is |
| 9 | // how the primary base drifts a rung lighter (accent-500 vs the canonical |
| 10 | // accent-600). For anything other than top-level navigation, use <Button>. |
| 11 | |
| 12 | export type ButtonVariant = "primary" | "secondary" | "danger" | "ghost" | "subtle"; |
| 13 | export type ButtonSize = "xs" | "sm" | "md"; |
| 14 | |
| 15 | export const BUTTON_BASE_CLASSES = |
| 16 | "inline-flex items-center justify-center gap-2 font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"; |
| 17 | |
| 18 | export const BUTTON_DISABLED_CLASSES = "disabled:cursor-not-allowed disabled:opacity-50"; |
| 19 | |
| 20 | export const BUTTON_VARIANT_CLASSES: Record<ButtonVariant, string> = { |
| 21 | primary: "bg-accent-600 text-white hover:bg-accent-500 active:scale-[0.98] transition", |
| 22 | secondary: |
| 23 | "border border-zinc-700/60 bg-zinc-800/60 text-zinc-300 hover:bg-zinc-700/60 hover:text-zinc-100 active:scale-[0.98] transition", |
| 24 | danger: |
| 25 | "border border-red-500/20 bg-red-500/10 text-red-400 hover:bg-red-500/20 hover:text-red-300 active:scale-[0.98] transition", |
| 26 | ghost: "text-zinc-400 hover:bg-zinc-700 hover:text-zinc-100 active:scale-[0.97] transition", |
| 27 | // Subtle metadata affordance (icon + faded label). Icon stays at full opacity; |
| 28 | // children render through a fading <span> in <Button>. |
| 29 | subtle: "group text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300 transition", |
| 30 | }; |
| 31 | |
| 32 | export const BUTTON_SIZE_CLASSES: Record<ButtonSize, string> = { |
| 33 | xs: "rounded-md px-2 py-1 text-xs", |
| 34 | sm: "rounded-lg px-3 py-1.5 text-xs", |
| 35 | md: "rounded-xl px-4 py-2.5 text-sm", |
| 36 | }; |
| 37 | |
| 38 | export const BUTTON_ICON_ONLY_SIZE_CLASSES: Record<ButtonSize, string> = { |
| 39 | xs: "rounded-md p-1", |
| 40 | sm: "rounded-md p-1.5", |
| 41 | md: "rounded-lg p-2", |
| 42 | }; |