AlertDialogAction was a plain <Button> — only AlertDialogCancel wrapped the primitive's Close — so confirming ran the action but never dismissed the dialog. Found in Trippy, where "Clear conversation" cleared the chat and left the modal sitting on screen; the same component is in every Crema app. Delete-style dialogs hide it by accident: the row they act on unmounts out from under the open dialog and takes it with it. Any confirm that leaves its target on screen (a rewrite, a regenerate, a clear) just gets stuck. Fixed at the component, so every call site gets it. The action still runs — Base UI merges its own close handler with the caller's onClick — and async work carries on behind the closed dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
components/
Component layers in this project.
ui/ shadcn primitives — token-driven, reskinnable per design system
forms/ composed form widgets
data/ data display (tables, filters, empty states)
layout/ app shell, page chrome, navigation wrappers
marketing/ landing and marketing blocks
[system]/ design-system-specific components (e.g. m3/, apple/)
The one rule
Custom components import from ui/, never the reverse.
ui/ is the primitive layer. It must stay reskinnable by swapping tokens in
app/themes/*.css alone. If a component can't be expressed that way (M3
ripple, Apple segmented control, etc.), it belongs in a system-specific
folder — not ui/ and not the shared folders above.
Tokens, not values
Every custom component should reference semantic tokens:
- Colors:
bg-primary,text-muted-foreground,border-border - Radius:
rounded-md,rounded-lg - Fonts:
font-sans,font-heading
Hardcoded hex, oklch, or px values are a bug — they break theming.