A calm, warm, deeply-accessible product surface.
Four independently-versioned packages under core/design/ — tokens, icons, primitives, and this documentation site — shared by member-app, workbench, and every future Doro surface.
Introduction
The system is lifted directly from member-app/app/globals.css so there is zero drift between the runtime theme and design review. Tokens are HSL CSS custom properties; components are shadcn primitives wrapped in Doro-specific behavior. Everything flows through a single Tailwind preset.
Principles
- Calm over clever. Warm cream grounds, one accent, breathing room.
- Accessible by default. WCAG AA on every token pair; focus visible everywhere.
- Shadcn-native. Upgradeable primitives; Doro customizations live in wrappers.
- One source of truth. Tokens live in one file, consumed by app & docs alike.
Versioning
Each package in core/design/* is versioned independently through release-please, identical to every other package under core/. Consumers link via file: dependencies — same pattern as @doro/core-data and @doro/core-lib.
Packages
Four sibling packages. Dependency order flows left to right: tokens → icons → ui → docs.
currentColor. Depends on tokens.Color
All surfaces are driven by HSL CSS custom properties declared in @doro/design-tokens/tokens.css. Toggle light/dark in the sidebar — every swatch below reflects the current theme.
Semantic tokens
Chart palette
Ordered categorical colors for data viz. Avoid using --primary for decorative non-interactive chart elements.
Typography
Fraunces for editorial display. Inter Tight for interface. JetBrains Mono for code. Fluid size ramp, never smaller than 13px in product UI.
Spacing & radius
4px base grid, Tailwind-default. Radius tokens flow from --radius (0.75rem) — bump that one variable to reshape the system.
Spacing scale
14px28px312px416px624px832px1248px1664px2496pxRadius
0.5rem0.625rem0.75remSafe-area spacing
Device-safe insets exposed as Tailwind utilities — pt-safe-top, pb-safe-bottom, etc. Critical for member-app running on notched devices.
Motion
Short, purposeful motion. Duration under 300ms unless there's narrative reason. All motion is auto-disabled by prefers-reduced-motion.
| Token | Duration | Easing | Use |
|---|---|---|---|
motion.instant | 80ms | ease-out | Hover color shifts, focus |
motion.snap | 140ms | ease-out | Button states, tab indicator |
motion.swift | 200ms | cubic-bezier(.33,1,.68,1) | Card & popover reveal |
motion.settle | 300ms | cubic-bezier(.33,1,.68,1) | Sheet, dialog |
accordion-down/up | 200ms | ease-out | Disclosure |
bottom-nav-pulse | 1350ms | ease-out | Mobile tab attention peak (once per session) |
Forms
Input, Textarea, Label, Checkbox, Radio, Switch. All bound to --ring for focus outline — WCAG AA against the warm background.
Feedback
Alerts, badges, progress. Low-noise states — we don't surface "success" toasts for everyday writes.
Badges
Progress
Data display
Cards, avatars, simple tables. The dashboard layout in member-app is composed entirely from these primitives.
shadcn primitives
The full roster exported from @doro/design-ui/components/ui. Each has a matching Storybook story; tokens flow through the same HSL variables.
Overlays
Dialog· modal + form confirmationsSheet· side drawer (nav, filters)Popover· lightweight inline panelsTooltip· hover hints onlyDropdownMenu· actions on a triggerCommand· ⌘K palette
Form
Input·Textarea·LabelSelect(Radix, keyboard-full)Checkbox·RadioGroup·SwitchSlider·Calendar+DoroDatePicker
Structure
Card·Separator·ScrollAreaTabs·AccordionTableprimitivesBreadcrumb·Pagination
Status
Alert·Badge·ProgressToast+ToasterruntimeSkeletonloadingAvatarwith fallback
Accordion
Command palette
Calendar & Date picker
Built on react-day-picker. The DoroDatePicker wrapper composes Popover + Calendar for the common single-date case.
import {DoroDatePicker} from '@doro/design-ui';
<DoroDatePicker
value={date}
onChange={setDate}
placeholder="Appointment date"
/>
Sheet (side drawer)
Slider & Separator
Separator between clusters of content, not as a decorative line inside cards.Adding more primitives
cd core/design/ui npx shadcn@latest add <primitive> # writes components/ui/<name>.tsx # Then: # 1. add to components/ui/index.ts # 2. write a story under stories/ # 3. bump package.json deps if the primitive pulls new Radix packages
Monorepo structure
The design system lives alongside other core/* packages. Apps are siblings of core/, not nested.
Consuming packages
Three lines of wiring — package.json, next.config.mjs, tailwind.config.ts.
1. Link as file: dependencies
// member-app/package.json
{
"dependencies": {
"@doro/design-tokens": "file:../core/design/tokens",
"@doro/design-icons": "file:../core/design/icons",
"@doro/design-ui": "file:../core/design/ui"
}
}
2. Transpile in Next
// member-app/next.config.mjs
export default {
transpilePackages: [
'@doro/design-tokens',
'@doro/design-icons',
'@doro/design-ui',
],
};
3. Extend Tailwind with the preset
// member-app/tailwind.config.ts
import designPreset from '@doro/design-tokens/tailwind-preset';
export default {
presets: [designPreset],
content: [
'./app/**/*.{ts,tsx}',
'../core/design/ui/src/**/*.{ts,tsx}',
],
};
4. Import the base CSS once
// member-app/app/layout.tsx import '@doro/design-tokens/tokens.css'; import '@doro/design-tokens/styles.css';
5. Use primitives
import {Button, Card, CardTitle} from '@doro/design-ui';
import {IconCheck} from '@doro/design-icons';
export function Confirm() {
return (
<Card>
<CardTitle>All set</CardTitle>
<Button><IconCheck /> Done</Button>
</Card>
);
}
shadcn upgrade strategy
shadcn components live inside @doro/design-ui, not vendored into every app. The rules below keep them upgradeable.
Where files live
Add a primitive
cd core/design/ui npx shadcn@latest add dropdown-menu
Then export from components/ui/index.ts and add a Storybook story.
Customize without forking
// components/doro-button.tsx
import {Button, type ButtonProps} from './ui/button.js';
export function DoroButton(props: ButtonProps) {
return <Button className="doro-warm-shadow" {...props} />;
}
When shadcn updates
Re-run npx shadcn@latest add <component>. The primitive file is replaced; your doro-* wrapper stays intact. Review the diff, bump the design-ui package version, and merge.
Release flow
Independent versioning via release-please. Identical to every other core/* package.
- Commit with conventional-commit messages targeting a design package:
feat(design-ui): add DropdownMenu primitive release-pleaseopens a PR bumping that package's version and updating itsCHANGELOG.md.- Merging tags a release —
design-ui-v0.3.0,design-tokens-v0.2.1, etc. - Cloud Build triggers on the tag pattern and deploys the package's artifact (Storybook for
ui, static site fordocs). Token and icon releases are library-only — no deploy. - Consuming apps update their
file:link and re-runnpm install. Build-gating in the app's CI catches breaking API drift before merge.
Accessibility
WCAG 2.2 AA is the floor, not the ceiling. Every token pair below is computed live from @doro/design-tokens. Pass / fail is marked against WCAG AA for normal text (4.5:1).
Key contrast pairs (computed live)
| Pair | Light | Dark | Required |
|---|
Known issue · primary button contrast
White text on the current warm primary (hsl(32 65% 48%)) measures 3.17:1 in light and 2.74:1 in dark — below the 4.5 AA threshold for normal text. It passes the 3.0 bar for large text (18pt+ or 14pt bold), so headline-scale usage is compliant, but default 14px button labels are not. Three options on the table, ranked by disruption:
- Darken primary to
hsl(28 70% 36%)(≈4.7:1 with white). Preserves warmth; shifts slightly redder. - Swap foreground to near-black —
hsl(28 40% 12%)on primary reads ≈8.2:1. Breaks the "colored button = white text" convention. - Restrict primary to large text only; use
--secondaryfor default-size CTAs. Smallest visual change; requires lint rule indesign-ui.
Pending design decision. Ticket: DORO-TBD.
Rules we enforce
- Focus ring uses
--ringwithoutline-offset; neveroutline: nonewithout a replacement. - All interactive elements reach 44px hit targets on mobile.
- Motion longer than 300ms is gated behind
prefers-reduced-motion: no-preference. - Labels are associated with controls via
htmlFor, never proximity-only. - Icon-only buttons carry
aria-label; Storybook has an a11y addon that fails stories missing one.
Contributing
See core/design/CONTRIBUTING.md for the long version. The short version:
- Does the change need new tokens? If yes, PR
tokens/first. - Is this an unmodified shadcn primitive? Use the CLI — don't hand-author.
- Is this a Doro-specific tweak to a shadcn primitive? Wrap it in a
doro-*component. - Every new primitive or wrapper ships with a Storybook story and, where it affects composition, a section here in
design-docs. - Run
task verifyfromcore/design/before opening the PR.