# design-sync notes — tvu-react-pilot → claude.ai/design

Target project: **TVU UX Design System** (`406a39da-1681-4edf-ae26-826cf3c33c9c`).

## Source shape & why the setup looks like this

- The sync source is **`react-pilot/`** — 32 React wrappers under `src/wrappers/*.tsx`. Each renders a `<tvu-*>` custom element and passes props as DOM properties via `ref` + `useEffect`. It is a **private pilot**: no library build, no barrel, no `.d.ts` output, `package.json` has no `main`/`module`/`exports`/`types`.
- The web components come from the repo's compiled **`dist-wc/tvu-web-components.js`** (aliased `@tvu/wc` in `react-pilot/vite.config.ts`). Importing it runs `register-*.js`'s 32 top-level `customElements.define()` calls — **self-registering side effect**, so nothing needs to call a register function.
- The WCs are **Vue `defineCustomElement` → shadow DOM**; component styles are inlined into each shadow root. So the light-DOM `styles.css` only needs the **`:root` design tokens** (custom properties inherit into shadow roots) + `@font-face`. This is confirmed by `react-pilot/styles.css`, whose only import is the token file.

## Converter wiring (see config.json)

- **`entry` = `react-pilot/ds-sync/entry.tsx`** — a hand-written barrel: `import '../../dist-wc/tvu-web-components.js'` (WC registration) + `export * from '../src/wrappers/<Name>'` ×32. App.tsx/main.tsx are deliberately NOT bundled (main.tsx renders on import). `--entry` at this file makes PKG_DIR resolve to `react-pilot/`.
- **`componentSrcMap`** pins all 32 explicitly. Required because with `--entry` set (not synth) and no `.d.ts`, `exportedNames` is empty; the package shape doesn't filter by it, but the explicit map is the authoritative component list.
- **`cssEntry` = `ds-sync/tokens.css`** — a COPY of `../src/tokens/variables.css`, placed inside react-pilot because `cssEntry` is bound to PKG_DIR (a `../` path outside the package is rejected). Its `@font-face` `url('./fonts/...')` resolves to `ds-sync/fonts/` (also copied). The 231 `:root` tokens ship via this file into the styles.css closure.
- **`.d.ts` are emitted**, not shipped: `react-pilot/tsconfig.dts.json` → `tsc -p` → `react-pilot/types/*.d.ts` (32 + `types.d.ts`). `findTypesRoot` picks up `types/`. **Without this the props degrade to `{[key:string]: unknown}`** — the converter's dts extraction reads `.d.ts` only, never `.tsx`.

## Known / accepted

- **`[FONT_MISSING] Microsoft YaHei`** (`--font-family-zh`) — a system CJK fallback family, not a brand font we own. Suppressed via `runtimeFontPrefixes: ["Microsoft YaHei"]`; renders with system CJK fallback. The real brand font (DigitalNumbers, TopBar digits) DOES ship (2 `@font-face` → `fonts/`).
- Components group under **`general/`** (srcDir is `src/wrappers`, so the group segment is empty → `general`). Cosmetic; the DS pane groups by it.

## Known render warns (triaged legitimate)

- **`[GRID_OVERFLOW]` → `cardMode: "column"`** on the wide components: Chart, MenuList, Message, Notification, Pagination, Steps, StepItem, Tab, TabItem, TabList, TopBar. These render wider than a grid cell, so they're pinned to one-story-per-row via `cfg.overrides`. Column cards can't re-flag `wide` — expected, not new. (MenuList added 2026-07-02: its `Horizontal` nav story is ~520px wide.)
- **UserMenu renders only the avatar trigger in a static card.** The account panel (header + actions[MenuList] + language toggle + sign-out) is `v-if="open"`, opened by an avatar click — an interaction-driven state that does NOT render statically (same posture as PopupBox's body / Tooltip's hover). The avatar variants (initials/image, brand + color overrides) are the statically-verifiable affordance; the panel renders live in a real app. Graded good for the avatar trigger. If a richer static card is ever wanted, add a `defaultOpen`-style prop to the component (source change).
- **PopupBox** `cardMode: "single"` (overlay modal needs its own viewport).

## Component authoring notes (folded from batch learnings — all 35 authored, all cells graded good)

- **META-02 (2026-07-02) added Logo / MenuList / UserMenu** (code-first, no Figma component-set; divergence `logo-menulist-usermenu-code-first-2026-07-02`). Authored previews: Logo (Brand + Sizes), MenuList (Horizontal nav underline + Vertical active-checkmark/disabled), UserMenu (avatar variants — panel is interaction-driven, see Known render warns). All graded good.

- **Every preview wraps content in a dark `Surface`** (`background: var(--bg-layer1)`). MANDATORY — the DS is dark-default and preview cards render on a white body; without it, dark-theme controls are invisible. Copied verbatim from `Button.tsx`. Do NOT `import React` (jsx is automatic).
- **PopupBox** = overlay modal SHELL (backdrop + title + close + footer render). Its **slotted body content does NOT render in static preview** (Vue CE `<Teleport>` + default-slot projection limitation) — it renders live in a real app. Graded good for the shell; the empty body is an environmental limitation, not a preview defect. Per design rule, PopupBox is for large custom-content modals; **confirmations/alerts use `Notification form="dialog"`** (see `docs/CLAUDE_DESIGN_RULES.md` §3).
- **Chart** was fixed at source: `src/canonical/Chart.vue` now STATICALLY imports `BaseChart` (was `defineAsyncComponent(() => import(...))`). The async-component code-split didn't survive single-file bundling (esbuild IIFE left a runtime `import()` for the ECharts chunk → 404 → empty render, in the design tool too). Static import inlines ECharts into `dist-wc` → renders everywhere. Trade-off accepted: ECharts is eager in the WC bundle.
- **Prop gotchas (verified against `react-pilot/src/wrappers/types.ts`)**:
  - Inputs (Input/InputBoxFilled/SelectBox*) disable via `enable="off"`, NOT a bool. InputNumber alone uses a real `disabled` bool + `property1` stepper enum.
  - `FormItem.type` `'Label & checkbox'` is lowercase-`c`; the other types are Title-cased. Do not "correct" it.
  - `Rating.value` is a **string** `'1'..'5'`, not a number.
  - `Switch.status` is three-state `off/on/**live**`; `live` renders red (TVU-specific).
  - `CheckBox.status` `some` = indeterminate (green filled square).
  - `DropDownListSelect` renders an OPEN inline panel; items need all 5 keys `{label,value,checked,active,disabled}`.
  - `Slider` needs an explicit width wrapper or it collapses; `size` is `M/S` (no L). SelectBox* `size` is `M/L` (no XL).
  - `BreadcrumbItemState.Default` is capitalized (others lowercase). Tab/Step type enums accept both cases.
  - Context-required subcomponents authored inside parents: BreadcrumbItem→Breadcrumb, TabList/TabItem→Tab, StepItem→Steps.
- **Cosmetic quirks (not bugs)**: TabItem explicit-children labels/active are driven by the parent Tab context (for a guaranteed-distinct disabled cell use `TabList items=[{...,disabled:true}]`); Steps circle numerals reflect internal index accounting, not `current`; Breadcrumb renders a leading separator before the first item.
- Interactive-only states (hover/focus/open-dropdown/`ux="hover"`/SelectBox `multi select`/`editable`) are NOT authored — they don't render statically. Static states cover the rest.

## Re-sync risks (what can silently go stale)

- **`react-pilot/ds-sync/tokens.css` + `ds-sync/fonts/` + `react-pilot/types/` are DERIVED copies.** If `src/tokens/variables.css`, the fonts, or any wrapper/`types.ts` change, they must be refreshed BEFORE the converter runs, or the sync ships stale tokens / stale prop contracts:
  1. `cp src/tokens/variables.css react-pilot/ds-sync/tokens.css`
  2. `cp src/tokens/fonts/DigitalNumbers-Regular.{woff,ttf} react-pilot/ds-sync/fonts/`
  3. `cd react-pilot && ./node_modules/.bin/tsc -p tsconfig.dts.json`
- **`dist-wc/tvu-web-components.js` is the compiled WC bundle.** If WC source changed, rebuild it (`pnpm build:wc`) before syncing — the converter bundles it verbatim.
- **Chart depends on a static import in `src/canonical/Chart.vue`** (`import BaseChart from '../components/Chart/Chart.vue'`). If anyone reverts it to `defineAsyncComponent(() => import(...))`, the code-split ECharts chunk won't survive the single-file bundle and Chart renders empty again (in the design tool too). Keep it static, or re-solve the bundling. Always `pnpm build:wc` after touching it.
- `types.ts` is auto-generated from `src/web-components/components.config.ts`. The emitted `.d.ts` inherit whatever it declares; prop-contract accuracy is only as good as that generator.
