/** * Shared class fragments used across component variants. * * These exist so "what a focused control looks like" or "what a disabled * control looks like" is decided once, not re-typed in every `cva()` call. A * component that needs to deviate should still compose from here and override * the specific utility rather than starting from scratch. */ /** Keyboard focus ring. Uses `focus-visible` only — never on mouse click. */ export const focusRing = 'outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring'; /** * CBAR's colour palettes. Each entry is the class that sets the `--ctl-*` role * variables (defined in `src/styles/theme.css`); a variant then reads those * roles without knowing which palette is active. * * This is what keeps the two axes independent: 7 palettes × 5 variants would be * 35 cva compound entries per component, and is 12 entries instead. */ export const colorPalettes = { /** CBAR's institutional navy. The default for every control. */ primary: 'palette-primary', /** Turquoise. */ secondary: 'palette-secondary', /** Cyan. CBAR's Figma variable for this ramp is `brand/tertiary`. */ tertiary: 'palette-tertiary', /** Neutral grey, for controls that should not carry brand colour. */ black: 'palette-black', red: 'palette-red', green: 'palette-green', yellow: 'palette-yellow', /** * @deprecated The pre-rename spelling of {@link colorPalettes.tertiary}. The * kit followed CBAR's published *style* name ("Third") until the Figma * *variables* became readable and turned out to say `brand/tertiary`. Both * classes resolve to the same eight roles in theme.css; this one is removed * in the next major. */ third: 'palette-third', } as const; export type ColorPalette = keyof typeof colorPalettes; /** * CBAR's five fill treatments. Each reads the active palette's roles, so the * same five strings serve Button, IconButton, Badge, Alert and Avatar. * * Hover follows one rule across every palette, taken from CBAR's `state=hover` * variants: a solid fill darkens one ramp step, a tinted fill deepens one step, * and the two transparent treatments pick up the tint. */ export const controlVariants = { /** Filled. One per view, ideally. */ solid: 'bg-(--ctl-solid) text-(--ctl-solid-fg) hover:bg-(--ctl-solid-hover)', /** Tinted background, no outline. */ subtle: 'bg-(--ctl-subtle) text-(--ctl-fg) hover:bg-(--ctl-subtle-hover)', /** Tinted background with an outline — the heaviest non-solid treatment. */ surface: 'border bg-(--ctl-subtle) text-(--ctl-fg) border-(--ctl-border) hover:bg-(--ctl-subtle-hover)', /** Outline only, transparent until hovered. */ outline: 'border border-(--ctl-border) text-(--ctl-fg) hover:bg-(--ctl-subtle)', /** Lowest emphasis — no fill, no outline. */ ghost: 'text-(--ctl-fg) hover:bg-(--ctl-subtle)', } as const; export type ControlVariant = keyof typeof controlVariants; /** Consistent treatment for a disabled *action* (button, menu item, tab). */ export const disabledState = 'disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50'; /** * Disabled treatment for a *form field*. Unlike {@link disabledState} it keeps * pointer events so the cursor can communicate why the field is inert. */ export const disabledField = 'disabled:cursor-not-allowed disabled:opacity-50'; /** Applied when a field fails validation (`aria-invalid`). */ export const invalidState = 'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40'; /** * Icon behaviour every control shares. Deliberately carries no size — a control * whose glyph scales with its own ladder picks a rung from {@link iconSizes} * per size instead, which a single base rule could not express. */ export const iconReset = '[&_svg]:pointer-events-none [&_svg]:shrink-0'; /** * Sizes an icon rendered inside a control, so callers never set it manually. * * CBAR draws Button's icon at 16px on `xs` and 24px from `md` up; every other * control in the file draws one icon size regardless of its own scale. The * `:not([class*='size-'])` guard is what lets a caller opt out with `size-5`. */ export const iconSizes = { /** 16px — menus, tabs, selects, and Button's `xs`/`sm`. */ base: "[&_svg:not([class*='size-'])]:size-4", /** 24px — Button from `md` up. */ lg: "[&_svg:not([class*='size-'])]:size-6", } as const; /** * Reset plus the fixed 16px rule — what a control composes when its icon does * not track its size. Button is the exception and applies the two halves * separately. */ export const iconSizing = `${iconReset} ${iconSizes.base}`; /** * Button's height ladder, straight from CBAR's Button component set: * 32 / 40 / 48 / 56px. * * `sm` (36px) is the one rung CBAR does not draw for Button — it jumps from xs * to md. It is filled in here at the height CBAR's IconButton uses for `sm`, so * a small button and a small icon button still line up. */ export const controlHeights = { xs: 'h-8', sm: 'h-9', md: 'h-10', lg: 'h-12', xl: 'h-14', } as const; export type ControlSize = keyof typeof controlHeights; /** * Form fields run on their own ladder — 32 / 36 / 44 / 56px — because CBAR * draws them taller than buttons from `md` up (44 against 40, 56 against 48). * * That mismatch is CBAR's, kept deliberately rather than normalised: a Button * and an Input of the same named size will not be the same height. Reach for * `controlHeights` on both if you need a form row to align. */ export const fieldHeights = { xs: 'h-8', sm: 'h-9', md: 'h-11', lg: 'h-14', } as const; export type FieldSize = keyof typeof fieldHeights;