# The YouSpot design system

This is the constitution and the router: the rules that don't change, and pointers to
the files where the values live. It deliberately restates **no** token values and **no**
component inventories, because those drift within days of being copied. When this document and
a source file disagree, the source file wins; fix this document.

Engineering rules for the workspace (branching, gates, bun-vs-npm, plugins) are in
`WORKSPACE.md`. Verbal brand (voice, naming, writing style) is in
`docs/product/brand/`.

## Principles

1. **A component may reference only contract tokens.** Semantic names like
   `bg-primary`, `text-muted-foreground`, `border-border`, `rounded-lg`, `shadow-card`,
   `font-heading`, `bg-accent-soft`, `bg-chrome`. Never a Tailwind palette class
   (`bg-blue-600`), never raw hex or a color function, never a `dark:` variant.
2. **No hand-rolled `<style>` block in a `frontend/` component.** State and
   breakpoints are variants on the element (`hover:`, `sm:`), not selectors you write.
   A stylesheet in a template string is also where the rules above stop applying:
   /pricing kept its palette that way, and hand-writing `.pricing-cta-dark:hover`
   tripped `check:tokens` on its own class name. Declaring custom properties for a
   subtree and `@keyframes` are the two things Tailwind cannot express; those belong
   in a real stylesheet, and are why the rule is scoped to the ratcheted tree.
3. **Dark mode is the theme's job.** Components style one resting state; the theme's
   `.dark` block reassigns the variables. Toggling the `dark` class on `<html>` is the
   entire mechanism.
4. **Brand values live in exactly one file**, `packages/youspot-theme/themes/youspot.css`.
   The private `--ys-*` OKLCH palette inside it is an implementation detail: nothing
   outside that file (code, specs, docs) may cite a `--ys-*` name. Cite the contract name.
5. **React Aria + `tailwind-variants`, never CVA.** Render props spread into `tv()` so
   `isDisabled` / `isPending` / `isSelected` are variant keys. No Chakra, no Convex in
   new work.
6. **One animation system per element.** `motion` presets (`@youspot/ui/motion`) for
   presence and layout; `tw-animate-css` + React Aria `isEntering`/`isExiting` for
   overlays. Never both on one element. Durations and easings come from the theme.
7. **Components are framework-free.** They take an injected `LinkComponent` and a data
   provider; they never import a router or call an API. Generative-UI cards render only
   what the host's server-side sanitizer emitted. An envelope attribute is a reference
   to fetch, never inline data to display.
8. **Compose before inventing.** Check `packages/youspot-ui/src/index.ts` and the studio
   gallery first; a new component is the last resort, and it enters through the workflow
   below, not ad hoc.
9. **The viewport is a variant, never state.** A component may not measure the width to
   decide what to render. The server has no viewport, so a `matchMedia` answer is wrong
   for one frame on every phone. `useIsNarrow` initialised `false`, which meant the
   sidebar painted the desktop chrome and swapped it after hydration. Render both
   presentations and let `lg:` / `max-lg:` pick. What a subtree legitimately needs is not
   the viewport but its own position (which surface am I in), and that is React context:
   see `useSidebarSurface`. Preference queries (`prefers-reduced-motion`,
   `prefers-color-scheme`) are not this; they change no layout and have no wrong answer
   to give the server.

## Sources of truth

| File                                                                                   | Owns                                                                                                  |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `packages/youspot-theme/contract.css`                                                  | The vocabulary: every semantic token a component may use                                              |
| `packages/youspot-theme/themes/youspot.css`                                            | The values: OKLCH palette, light/dark, radii, elevation, fonts, motion, v5 brand artwork as data-URIs |
| `packages/youspot-theme/src/index.ts`                                                  | `motionTokens`: numeric durations/easings for the `motion` library                                    |
| `packages/youspot-ui/src/index.ts`                                                     | The public component surface (primitives, CRM composites, AI elements, generative UI, motion presets) |
| `packages/youspot-ui/src/genui/specs.ts`                                               | The generative-UI envelope contract (`DEFAULT_SPECS`, sanitizer allowlist, prompt spec)               |
| `apps/youspot-studio/src/lib/inventory.ts`                                             | Catalog metadata: component list, token groups, slugs                                                 |
| `apps/youspot-studio/src/stories/`                                                     | The story system: every component's live, controllable proof                                          |
| `docs/product/brand/`                                                                  | Voice, naming, writing style                                                                          |
| `plugins/youspot-toolkit/skills/youspot-ui-new-component/references/retokenize-map.md` | The palette-class → contract-token mapping used when re-tokenizing registry files                     |

Typography, for the record of where to look rather than what it is: body and titles are
the system sans (`font-sans` / `font-heading`), display copy is Georgia (`font-display`),
the handwriting accent is Caveat (`font-accent`). The assignments live in
`themes/youspot.css`; anything citing other faces is stale.

The studio (`apps/youspot-studio`, port 3300) is the proof surface: the gallery, the
`/themes` light/dark specimen page, the live token reference with copyable classes, the
genui playground, and the shadcn registry (`bunx shadcn@latest add @youspot/<slug>`,
generated by `apps/youspot-studio/scripts/build-registry.ts`).

## Jurisdiction

Governed by this system today:

- `packages/youspot-theme`, `packages/youspot-ui`, `packages/youspot-og`,
  `packages/youspot-email` (email colors are generated from the theme and drift-gated)
- `apps/youspot-studio`
- `chrome-extension`
- `frontend/`: youspot.com **is a consumer**. `frontend/app/globals.css` imports the
  contract and theme, `@source`s the package, and `layout.tsx` sets
  `data-brand="youspot"`. New and rewritten frontend surfaces use `@youspot/ui`
  primitives and contract tokens.

Legacy, tolerated but frozen. Do not extend, migrate on touch:

- The hardcoded CSS below the imports in `frontend/app/globals.css`, the Chakra pages,
  and `frontend/lib/crm/theme.ts` (a literal color map kept only for Chakra/inline
  compat; when its values drift from the theme, the theme is right).
- Flask/Jinja pages under `backend/templates/` and `backend/static/`.
  [Historical Flask app design](docs/architecture/flask-app-design.md) documents that
  world; it is a historical record of the Flask app-page system, not guidance for
  anything new. New user-facing pages go to the NextJS frontend regardless.

## Consuming the system

In a CSS entry (order matters; the `@source` path is relative to the CSS file):

```css
@import "tailwindcss";
@import "@youspot/theme/contract.css";
@import "@youspot/theme/themes/youspot.css";
@import "tw-animate-css";
@plugin "tailwindcss-react-aria-components";
@source "../node_modules/@youspot/ui/src";
```

Set `data-brand="youspot"` on `<html>`; add `dark` for dark mode. Two silent failure
modes to know: without the `@source`, Tailwind generates no classes for the package's
components; without the React Aria plugin, state variants (`selected:`, `pressed:`,
`invalid:` …) parse and do nothing, so components render permanently in their resting
state. Durations use the parenthesized-var form (`duration-(--motion-base)`); in
`motion` code use the presets and wrap the app in `<MotionConfig reducedMotion="user">`.

## Adding or changing a component

The step-by-step ships as `/youspot-toolkit:youspot-ui-new-component`; the shape is:

1. `bunx shadcn@latest add @react-aria/tailwind-<name>` (Adobe's official registry).
2. **Re-tokenize**: replace every palette class and `dark:` override with a contract
   token, using the retokenize map above.
3. **De-alias**: rewrite `@/lib/...` / `@/components/ui/...` imports to relative paths;
   inside a library package the alias resolves against the consuming app's tsconfig.
4. **Export** it from `packages/youspot-ui/src/index.ts`, add its entry to the studio's
   `inventory.ts`, and write its story (the inventory test fails the build if a
   component has no story).
5. `bun run check:tokens`, then look at the studio's `/themes` page in both modes. The
   grep catches palette leaks; only eyes catch a component that is token-clean and
   still visually broken.

A feature-level design goes in `docs/features/<slug>/design-spec.md` (the
`design-spec` skill has the template). Specs cite contract tokens by name, never
`--ys-*` values and never hex.

## Enforcement

| Rule                                                       | Gate                                                                                                                                                                                                                                   |
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Contract tokens only (ui, og, studio, extension)           | `bun run check:tokens`, which rejects palette classes, black/white utilities, `dark:`, raw hex/color functions                                                                                                                         |
| Contract tokens in `frontend/` (ratcheted)                 | same script. Legacy violators are frozen in `scripts/check-tokens-baseline.json`; any new file, or a new violation outside the baseline, fails; a baselined file that goes clean must leave the list (`--update-baseline` rewrites it) |
| No width media query in JS (everywhere, baseline included) | same script. The `viewport measured in JS` rule is _sticky_: it was added with zero violations, so the legacy baseline has no debt to forgive and does not excuse it                                                                   |
| Email templates carry no raw colors                        | `bun run check:email`                                                                                                                                                                                                                  |
| OG brand assets match the theme                            | `bun run check:brand-tokens`                                                                                                                                                                                                           |
| Verbal brand (naming)                                      | `bun run check:brand`                                                                                                                                                                                                                  |
| `motionTokens` track `themes/youspot.css`                  | `packages/youspot-theme/test/motion-tokens.test.ts`                                                                                                                                                                                    |
| Motion preset fallbacks track the theme                    | `packages/youspot-ui/test/motion-presets.test.ts`                                                                                                                                                                                      |
| GenUI specs match the registry and sanitizer               | `packages/youspot-ui/test/genui*.test.ts`                                                                                                                                                                                              |
| Every component has a story, every story a component       | `apps/youspot-studio/test/inventory.test.ts`                                                                                                                                                                                           |
| Inventory entries ↔ `@youspot/ui` exports agree            | same file. A documented component must be exported, a new component export must be documented                                                                                                                                          |
| Studio registry generation still builds                    | the `Build` step in workspace CI (`bun run build` runs the studio prebuild)                                                                                                                                                            |
| All of the above on any PR touching a governed surface     | `.github/workflows/workspace-ci.yml`                                                                                                                                                                                                   |
| React quality across frontend/studio/ui/email/og           | `.github/workflows/react-doctor.yml`                                                                                                                                                                                                   |

Two lists are declared debt, allowed only to shrink: the frontend legacy baseline in
`scripts/check-tokens-baseline.json` (the frozen pre-token files), and
`UNDOCUMENTED_EXPORTS` in the studio inventory test (compound parts and re-exports with
no inventory entry of their own). Neither accepts new entries: a new file uses contract
tokens, a new component export gets documented.

A baselined file is forgiven its colour debt, not granted a licence. Mark a rule `sticky`
in `scripts/check-tokens.ts` when you add it with no violations anywhere: there is nothing
to amnesty, and without it the 123 frozen files would be the one place the new rule does
not apply.

A gate you haven't watched fail is not a gate: when adding one, plant the violation,
confirm the non-zero exit, remove it.
