Skip to main content

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-foreground on CardTitle, overriding the preset's default font-heading text-base font-medium. This caused visual inconsistency and made future preset refreshes painful.
  • Tailwind v3 config pattern. tailwind.config.ts with a presets: [] 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-primary from 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 caseCorrect tokenIncorrect (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 + FormMessage primitive chain with react-hook-form + zod resolver.
  • Simple non-validated dialog labels (read-only labels, display-only inputs, inline dialogs that do not submit to an API): use LabeledField from @aucert/ui/composites. LabeledField is a back-compat shim — it wraps label + input without 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

OptionProsCons
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 presetFamiliar; no @source requiredv3 is now legacy; presets: pattern diverges from shadcn upstream; theme refresh is manual diff
Custom design tokens from scratchFull control; no upstream couplingEnormous initial investment; loses shadcn community tooling; requires ongoing maintenance of primitives
Radix Themes (alternative to shadcn)All-in-one; simpler CSSNot 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 b2pl3ZuLI to pick up upstream preset changes. No manual diff.
  • New apps follow a clear path. Start with @aucert/ui, import globals.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.
  • @source directive 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 in frontend/.context/FRONTEND.md.

Risks

  • shadcn preset URL stability. The preset ID b2pl3ZuLI is 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 reference
  • frontend/.context/FRONTEND.md — frontend stack and conventions
  • Tailwind CSS v4 docs
  • shadcn customizer — preset b2pl3ZuLI
  • sidebar-07 block

Changelog

DateChangeAuthor
2026-05-14Initial decision capturedVivek + Claude