ADR-016: Frontend design system standard: shadcn luma preset + sidebar-07
Context
Before PR #155, the @aucert/ui package had accumulated several anti-patterns:
- Custom typography overrides on shadcn primitives. Code was placing
text-xs uppercase tracking-wider text-muted-foregroundonCardTitle, overriding the preset's defaultfont-heading text-base font-medium. This caused visual inconsistency and made future preset refreshes painful. - Tailwind v3 config pattern.
tailwind.config.tswith apresets: []array was the prior approach, which blocked the CSS-only v4 migration path. - No canonical layout block. Each app composed its own sidebar or top-nav shell, leading to divergent navigation patterns across the console and internal tools.
- Fragmented token vocabulary. Teams used
--accent,--surface,--text-primaryfrom prior iterations of the design system rather than the luma standard token set.
PR #155 migrated @aucert/ui to the shadcn luma preset (b2pl3ZuLI), Tailwind CSS v4, react-hook-form + zod for form handling, and the sidebar-07 block as the canonical layout. The internal Astra dashboard now runs on this stack.
A standard must be codified so that all future frontend work — console, new apps, and internal tools — follows the same path without rediscovering the same trade-offs.
Decision
Theme: shadcn luma preset b2pl3ZuLI
The shadcn luma preset b2pl3ZuLI (luma style, mist base, green theme, Inter font, default radius, pointer cursor on buttons) is the canonical theme for all Aucert frontend surfaces. To refresh the theme or regenerate primitives after a shadcn version bump, run:
pnpm dlx shadcn@latest apply --preset b2pl3ZuLI --yes
This is a mechanical operation, not a bespoke theming exercise. Do not hand-craft component styles that duplicate what the preset provides.
Primitives: shadcn components from @aucert/ui
All UI primitives come from @aucert/ui (which re-exports the luma-generated shadcn components). Consumer apps must not install their own copy of shadcn primitives.
Use shadcn compound APIs as intended — compose Card + CardHeader + CardTitle + CardDescription + CardContent rather than nesting raw <div> wrappers.
No custom typography overrides on shadcn primitives. Let the preset defaults render. CardTitle's default is font-heading text-base font-medium — do not override it with utility classes on the consumer side. If the preset's default is wrong, fix the preset (re-run shadcn apply), not the consumer.
CSS configuration: Tailwind v4 CSS-only
Theming is CSS-only via @theme inline {} in frontend/packages/ui/src/theme/globals.css. There is no tailwind.config.ts or presets: array. Consumer apps import the shared theme with:
@import "@aucert/ui/theme/globals.css";
Consumer apps must also add a @source directive scanning the @aucert/ui package so Tailwind v4 compiles utilities used inside the package:
@source "../../packages/ui/src/**";
Without this directive, utility classes inside @aucert/ui silently fail to compile in the consumer app's build.
Token vocabulary
Use luma standard tokens for all styling. These are the authoritative names:
| Use case | Correct token | Incorrect (old) |
|---|---|---|
| Page background | --background | --surface |
| Default text | --foreground | --text-primary |
| Card surface | --card / --card-foreground | --surface-card |
| Muted highlight | --accent / --accent-foreground | --highlight |
| De-emphasized text | --muted-foreground | --text-secondary |
| Primary action | --primary / --primary-foreground | --brand |
Aucert extension tokens (--surface-raised, --border-hover, --color-success, etc.) exist only for semantic use cases that luma has no equivalent for. Do not add new extension tokens when a luma token exists.
Layout pattern: sidebar-07 block
The sidebar-07 block is the canonical layout pattern for all apps. It uses the SidebarProvider / AppSidebar / SidebarInset composition from @aucert/ui. Do not build new top-nav shells or custom sidebar composites.
The sidebar-07 block requires that SidebarProvider wraps children in TooltipProvider. Failing to do so causes runtime crashes in the sidebar's collapsed tooltip rendering.
Form handling: react-hook-form + zod
- Validated forms (any form with user-facing validation): use the
Form+FormField+FormItem+FormLabel+FormControl+FormMessageprimitive chain withreact-hook-form+zodresolver. - Simple non-validated dialog labels (read-only labels, display-only inputs, inline dialogs that do not submit to an API): use
LabeledFieldfrom@aucert/ui/composites.LabeledFieldis a back-compat shim — it wrapslabel+inputwithout RHF and is safe in Server Components.
Do not add a custom form wrapper on top of the primitives; the Form chain already handles layout, accessibility, and error display.
Dark/light theming: next-themes
Use next-themes ThemeProvider in the app root. Token resolution is handled by globals.css :root and .dark selectors. Consumer apps must list next-themes as a direct dependency in their package.json (pnpm phantom-dep policy).
Alternatives considered
| Option | Pros | Cons |
|---|---|---|
| shadcn luma preset + Tailwind v4 (chosen) | Official shadcn theming path; CSS-only; preset refresh is one CLI command; no tailwind.config.ts drift | @source directive required in every consumer; preset regeneration copies over any hand-edits |
| Tailwind v3 + custom config preset | Familiar; no @source required | v3 is now legacy; presets: pattern diverges from shadcn upstream; theme refresh is manual diff |
| Custom design tokens from scratch | Full control; no upstream coupling | Enormous initial investment; loses shadcn community tooling; requires ongoing maintenance of primitives |
| Radix Themes (alternative to shadcn) | All-in-one; simpler CSS | Not shadcn-compatible; different component API surface; would require rewriting all existing composites |
Consequences
What becomes easier
- Theme refresh is mechanical. Re-run
shadcn apply --preset b2pl3ZuLIto pick up upstream preset changes. No manual diff. - New apps follow a clear path. Start with
@aucert/ui, importglobals.css, add@source, use sidebar-07. No architectural exploration required. - Consistency across surfaces. Console and internal tools share the same visual language without custom per-app overrides.
- Future preset changes are low-risk. Because consumer code does not override primitive typography, a preset regeneration does not conflict with consumer styles.
What becomes harder
- Deviating from the preset requires justification. Intentional visual differences from the preset must be captured in an ADR amendment, not a one-line utility class.
@sourcedirective must be added to every new consumer app. This is a build configuration step that fails silently (Tailwind does not error; utilities simply don't compile). See Gotchas infrontend/.context/FRONTEND.md.
Risks
- shadcn preset URL stability. The preset ID
b2pl3ZuLIis a shadcn customizer permalink. If shadcn changes its URL schema, the permalink may break. Mitigation: store the full component source in@aucert/ui/primitives/so a broken permalink is not a blocker — regeneration is a convenience, not a requirement. - Luma token gaps. As new UI patterns emerge, luma may not have a token for every use case. Mitigation: add Aucert extension tokens sparingly and document them in
DESIGN_LIBRARY.md.
References
- PR #155 — shadcn luma preset migration: Tailwind v4 + sidebar-07 + react-hook-form
frontend/packages/ui/.context/DESIGN_LIBRARY.md— component inventory and theming referencefrontend/.context/FRONTEND.md— frontend stack and conventions- Tailwind CSS v4 docs
- shadcn customizer — preset
b2pl3ZuLI - sidebar-07 block
Changelog
| Date | Change | Author |
|---|---|---|
| 2026-05-14 | Initial decision captured | Vivek + Claude |