memos/web/src/components/ui/README.md
johnnyjoygh c031b77c83 chore: add a quiet button variant and one ring-free focus for kit
buttons

The calendar shipped its own "quiet control" grammar as a page-local set
of class strings: 28px tall, 13px text, muted ink that darkens under a
light hover wash, ring-free outline focus, and the accent fill reserved
for a pressed or current state. The same look was hand-rolled elsewhere
with drifting values (/65 ink, /50 washes, ring focus) as className
overrides on the kit's Button, against the kit policy.

The grammar now lives in the kit as `variant="quiet"`, with the accent
fill keyed off `aria-pressed`, `aria-current` and `data-popup-open`, so
a
control that is on, current, or holding a popup open fills itself. A
`size="icon-compact"` names the 28px square the sidebar already used for
its compose control; `sm` already had the height for labelled chips.

Kit buttons had no focus treatment of their own, so sites that needed
one
bolted `focus-visible:ring-*` overrides onto the Button. The base now
carries a 2px inset outline from a new `ui/focus.ts` constant that raw
controls (calendar grid cells, the reaction chip) import too, and the
calendar's controls.ts goes away.

With both in place the hand-rolled sites collapse onto the variant: the
calendar's header, month picker and day panel; the statistics month
navigator; the sidebar's section actions and compose and mobile menu
controls; the memo card's trailing actions and its action menu, which no
longer takes a className; the share-image dialog's footer; the memo
outline; and the inactive underline tab. The month title fills while its
picker is open and the outline's current heading fills like every other
current thing. Tests follow the classes they assert.
2026-09-06 22:43:21 +08:00

3.3 KiB

UI Kit (components/ui)

shadcn/ui primitives ("new-york" style, zinc base, lucide icons, CSS variables). This folder is the single source of styling truth. Read this before adding a component or reaching for className on a kit component.

The one rule: use props, not className

Consumers style kit components through variant / size / shape props only — do not pass className to add colors, sizing, borders, or hover states.

  • Need a look that no prop provides, but it recurs? Add a variant here.
  • Need layout/positioning (margins, absolute)? Put it on a wrapper element.
  • The element is genuinely bespoke (data-driven background, glass/blur, a contextual one-off)? Don't fight the kit — use a raw HTML element with its own classes. It is not a kit component.

Color comes from the semantic OKLCH tokens documented in ../../themes/COLOR_GUIDE.md (--primary, --muted-foreground, --success, --warning, …). Never hardcode gray-500 / amber-50 palette classes.

Variant catalog

Component variant size / shape Notes
Button default · destructive · outline · secondary · ghost · link · quiet size: default · sm · lg · icon · icon-compact · icon-sm Every variant carries the kit's ring-free keyboard focus (FOCUS_VISIBLE_OUTLINE_CLASSES from ui/focus.ts; raw controls import the same constant). quiet is local: 13px muted ink that darkens under a bg-muted/60 hover wash, and the accent fill only for aria-pressed, aria-current or data-popup-open. Use it via buttonVariants({ variant: "quiet" }) on raw <button>/<a> elements too (calendar controls, memo outline, card actions). icon-compact (size-7) is the 28px square matching sm's height; icon-sm (size-6) is for dense icon buttons. Auto-sizes unsized svg children to size-4.
Badge default · secondary · destructive · outline · warning shape: default · pill warning + shape are local additions. pill = rounded-full.
Tabs — variant: segmented · underline Local component (not upstream shadcn). Context-based Tabs/TabsList/TabsTrigger, no Radix. underline relies on the consumer's own divider.
Dialog — size: sm · default · lg · xl · 2xl · full Local size variants + auto-focus prevention on open.
Select — size: xs · sm · default Local size prop on the trigger.
DropdownMenu item: default · destructive — Local destructive item variant + a sub-menu hover-delay hook.

Each cva component also exports its variant types, e.g. ButtonVariant, ButtonSize, BadgeVariant, BadgeShape, TabsVariant — use these instead of re-deriving VariantProps.

Layering (z-index tiers)

Defined once as Tailwind tokens in themes/default.css (--z-index-*) and used by the primitives — do not hardcode z-[60]-style literals:

Utility Value Used by
z-overlay 50 Dialog / Sheet overlay + content
z-dropdown 60 Select / DropdownMenu / Popover content
z-tooltip 70 Tooltip content + arrow

A dropdown inside a dialog needs z-dropdown (already the default) so it renders above the overlay; a tooltip sits above everything.