# Development Guideline — `@godxjp/ui`

How to work **on** the design system (not just with it). If you only consume the framework from the app, read [the ten consumer rules](./CONSUMER-RULES.md) instead — this file is for people **editing `packages/godx-ui/`**.

---

## 0. What this package IS — and the boundary it must keep

`@godxjp/ui` is **the** UI framework for every godx surface (admin, agency portal, handheld). It is shared, versioned infrastructure: one change here ripples to every screen and every consumer. Two consequences:

1. **Editing it needs explicit session permission** — the hard gate in [CLAUDE.md](../CLAUDE.md) (MANDATORY: read the component skill before touching UI). Treat the package as off-limits by default. 2. **It is generic and presentational only.** The framework knows about _tokens, layout, accessibility, and interaction_ — never about the app's data, routes, language files, or business rules.

### The hard boundary — consumer-layer concerns MUST NOT leak in

A component in this package may **not** import or assume any of the following. These belong to the **consumer** (the app), which composes framework primitives around them:

| ❌ Never inside `packages/godx-ui/`                              | ✅ Where it belongs                                                 |
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| App i18n — `useTranslation`, `t()`, `resources/js/i18n`          | App passes already-translated strings in as props                   |
| Inertia — `router`, `usePage`, `<Form>`, `@inertiajs/*`          | App wires navigation/submit; pass `onClick`/`href`/`onSubmit` props |
| Wayfinder routes — `@/routes`, `@/actions`, `.url()`             | App resolves the URL, passes a plain string                         |
| Business entities / domain logic (Item, Packing, shipment rules) | App-level component in `resources/js/components/**`                 |
| App copy / labels / placeholder text baked into the component    | Comes in via props; framework ships no product wording              |
| Raw colors / hex / `hsl(...)` literals, `dark:` overrides        | Semantic tokens (`var(--…)`, `bg-primary`, …) — see §3              |
| A consumer's theme values redefined inside a component           | Theme lives in `src/tokens/` (the framework default ships it)       |

> **Litmus test:** if a component can't render in a Storybook-style preview with
> plain props and zero app context, it does **not** belong in this package. Keep it
> app-level (`resources/js/components/admin/…`) and have it _compose_ framework
> primitives. See the **`godx-ui-component-placement`** skill for the full decision.

The framework ships its **own theme** (colors, fonts, type scale, wa-iro palette) in `src/tokens/foundation.css`, so consumers import `@godxjp/ui/styles` and need **zero** extra theme config. Do not push theme decisions back onto the consumer.

---

## 1. Architecture — the layers, bottom-up

```
src/tokens/        Design tokens (the single source of values)
  foundation.css   :root + .dark — colors, fonts, type scale, spacing, radius, wa-iro
  primitives/*.css Per-domain primitive tokens (card, table, control, badge, …)
  base.css         Coordinates the token layer
src/styles/        CSS that styles components by [data-slot] + density.css (the density knob)
  index.css        Entry: @import fontsource → tailwindcss → @theme (token→utility map) → layout css
src/components/    React components, grouped (data-display, data-entry, layout, …)
src/props/         Prop type system — vocabulary/ (atomic) + components/ + registry.ts
src/lib/           cn(), control-styles, variants — shared helpers
examples/          *.preview.tsx — Storybook-style stories (preview app)
docs/primitives/   <component>/index.tsx demo + examples/ + generated .md
preview/           The preview app (vite) on :6008 that renders examples + docs
```

**Token → utility flow:** a value is defined once as a CSS var in `tokens/foundation.css` (e.g. `--primary`), mapped to a Tailwind utility in the `@theme` block of `styles/index.css` (`--color-primary: hsl(var(--primary))`), and consumed as `bg-primary` / `hsl(var(--primary))`. Never skip a layer with a literal.

### The component pattern — markup emits slots, CSS owns styling

Components render semantic structure and `data-slot` / `data-*` flags; **the spacing, padding, and chrome live in `src/styles/*-layout.css`**, keyed on those slots. This keeps density and theming centralized.

```tsx
// component: emits slots + flags only
<div data-slot="card" data-variant={variant} data-density={density} />
```

```css
/* styles/card-layout.css: owns the look */
[data-slot="card"] {
  border: 1px solid hsl(var(--card-border));
  border-radius: var(--card-radius);
}
[data-slot="card"][data-density="tight"] {
  --card-space-inset: var(--space-3);
}
```

Prefer this over hardcoding Tailwind padding inside the component. Use Tailwind utility classes for **one-off layout** (flex/grid/gap), not for re-theming.

### Density

One knob — `.ui-density-{compact,default,comfortable}` in `styles/density.css` — retunes `--phi-unit`, control heights, and table row heights together. Components read the resulting tokens; never branch on density in component JS.

### The `ui/` layer

`src/components/ui/*` are thin **re-exports** of the canonical implementation (`export * from "../data-display/card"`). They exist for shadcn-style import paths. Edit the canonical file under its group; the `ui/` path follows automatically.

---

## 2. Adding or changing a component (after you have §0 permission)

Work in this order; only advance when the previous step genuinely can't express the need:

1. **Use** an existing primitive. 2. **Compose** primitives (Card + FormField + Flex…). 3. **Extend** an existing component — add a prop/slot (e.g. `labelAddon`, `accent`). Prefer this: one more prop beats one more component. 4. **Create** a new component — last resort.

Document the decision (which promotion criteria it met) so review can check it — see [COMPOSITION-VS-COMPONENT.md](./COMPOSITION-VS-COMPONENT.md).

---

## 3. Rules for framework code

- **Semantic tokens only.** No raw hex / `rgb()` / `hsl()` literals, no palette utilities (`bg-blue-500`), no `dark:` overrides — tokens adapt automatically. Structural literals (a `3px` accent stripe, `1px` borders) are fine; _color/size scale_ values come from tokens. - **No app coupling** (the §0 boundary table above). - **Props live in `src/props/`** — atomic concepts in `vocabulary/`, per-component interfaces in `components/`. Check `registry.ts` + `PROP_ALIASES_FORBIDDEN` before inventing a prop name.

---

## 4. Every component change ships its story + docs

A change isn't done until its documentation reflects it:

1. **Preview story** — `examples/<group>/<Component>.preview.tsx` (Storybook-style). New props get a story (see `examples/data-display/Card.preview.tsx`: Surfaces / Density / AccentEdges). 2. **Docs demo** — `docs/primitives/<group>/<component>/index.tsx` shows the new capability; `examples/` holds focused per-feature demos. 3. **Regenerate props docs** — `pnpm docs:sync-primitives` regenerates the `.md` from source. Run it after prop changes so the tables stay accurate.

> Docs are mid-migration to the `<component>/index.tsx (+ examples/)` shape. Do **not**
> resurrect flat `docs/primitives/<component>.tsx` demos — they were dead orphans and
> were removed.

---

## 5. Verify before finishing

```bash
pnpm lint                 # eslint — self-contained flat config
pnpm typecheck            # tsc --noEmit
pnpm vitest run src/components/<group>/__tests__ --maxWorkers=2   # ONLY what you touched
pnpm test                 # FULL suite — CI only, never from an agent loop
pnpm preview:build        # integration test: examples + docs must build — at most once, pre-PR
pnpm audit                # godxjp-ui-audit — 0 errors for touched files
pnpm check:mcp-sync       # MCP registry ↔ library export drift guard
pnpm check:frame-axe      # WCAG 2.2 AA over every frame — LOCAL ONLY, never in CI (FRAME-A11Y-CI.md)
```

`pnpm verify` and `pnpm verify:release` run these together (verify:release also builds) — **both include the full suite, so both belong to CI.** Locally, run them at most once immediately before opening a PR, never inside an edit loop and never while other agents are working on the same machine. It needs `pnpm exec playwright install chromium` once locally; see [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) for how to run/scope it, read the evidence, and regenerate its baseline after an accessibility fix.

### Reproducing one screen at one width — the frame routes

Three gates and every bug report start here, so the addressing is written down rather than guessed
(godx-jp/id#639 burned three attempts on `?frame=…`, which is not a thing):

```
pnpm preview                                  # :6008
http://localhost:6008/isolate/<id>            # the demo ALONE, at the real viewport — measure here
http://localhost:6008/frame/<id>              # the same demo inside the device-preset chrome
```

`<id>` is the demo's path under `docs/`, minus `.tsx`, with `/` turned into `-`:
`docs/layout/topbar.tsx` → `layout-topbar`, `docs/data-entry/date-picker.tsx` → `data-entry-date-picker`.
A file may override it with a `slug` in its frontmatter. Both routes accept
`?dir=rtl&density=compact&theme=dark&locale=ja`; `/frame/**` additionally takes `?preset=`/`?w=`/`?h=`/`?zoom=`.

For an axe measurement at a given width, drive `/isolate/<id>` with Playwright at that viewport —
that is what `scripts/topbar-collision-visual.mjs` and the other `test:visual:*` gates do.

All gates are **self-contained** — no internal/external tooling package required. The eslint, prettier, and vitest setup live in the package (`eslint.config.js`, `prettier.config.mjs`, `vitest.config.ts`, `src/test/`), so a fresh checkout can lint/type-check/test without anything beyond the declared devDependencies.

The app side additionally runs **`npm run ui:audit`** (the design-system linter) and must report 0 errors for touched files.

---

## 6. Releasing — the lib and its MCP, in lockstep

This repo publishes **two packages** that must agree: `@godxjp/ui` (the browser component library, root `package.json`) and `@godxjp/ui-mcp` (the Node MCP server that tells agents how to use it, `mcp/`).

### The lockstep contract (issue #140)

A consumer that installs `@godxjp/ui@16.10.x` but points its agent at `@godxjp/ui-mcp@16.7.x` gets prop/token/pattern guidance for a build it never installed. To make that impossible, the two packages carry **mutual, machine-readable compatibility metadata**:

- root `package.json` → `"godxUiMcp": "<mcp version>"` — the catalog version this library ships with.
- `mcp/package.json` → `"version"` (identical to the library) **and** `"godxUiCompatibility": "<maj>.<min>.x"` — the UI minor this catalog describes.
- the MCP exposes it at runtime: `serverInfo.version` == package version, the `godx-ui://compatibility` resource, and the `check_compatibility` tool (an agent calls it with the app's installed `@godxjp/ui` version to get a match/mismatch verdict).

- **Lockstep guard** (`pnpm check:mcp-lockstep` → `scripts/check-release-lockstep.mjs`) — asserts ui.version == mcp.version, ui.godxUiMcp == mcp.version, and mcp.godxUiCompatibility covers the UI version. Any split fails CI. - **Catalog drift guard** (`pnpm check:mcp-sync` / `check:mcp-orphans`) — every component the MCP catalogs must still be a real library export, and vice-versa. - **Packed-artifact guard** (`release-integrity` CI, step 4) — packs both tarballs and re-asserts lockstep on the **packed** manifests, i.e. the bytes that would actually ship.

### Coordinated release (`pnpm release`)

```bash
pnpm release --ui minor --mcp sync   # bump ui, republish mcp at the SAME new version — the norm
pnpm release --mcp sync              # mcp-only fix at the current ui version
```

`--ui <bump>` **requires** `--mcp sync` (the tool refuses a ui-only release) so the two packages can never split. On a bump the tool refreshes both compatibility fields, runs `verify:release` (incl. all three guards) before publishing the lib, `npm publish`es each package, re-runs the lockstep check as a final fail-closed gate, then commits the version bumps.

### Recovery from a partial publish

If a run publishes `@godxjp/ui` but the `@godxjp/ui-mcp` publish fails (npm hiccup, expired token), the two are momentarily split on npm. Recover:

1. Fix the cause (e.g. re-auth `npm whoami`), keep the working tree at the just-published version. 2. Re-run **only** the MCP half at that same version: `pnpm --dir mcp build && npm --prefix mcp version <ui-version> --allow-same-version && npm --prefix mcp publish --access public`. 3.

npm versions are immutable, so you can never "fix" a bad published version in place — you can only publish the matching partner at the same number, or move both forward with a fresh `pnpm release --ui patch --mcp sync`. Never hand-publish one package and forget the other.

---

## See also

- [README](../README.md) — overview, component groups, consumer setup.
- [CONSUMER-RULES.md](./CONSUMER-RULES.md) — the ten rules every consumer follows (the audit enforces them).
- `docs/TOKENS.md`, `docs/SPACING.md`, `docs/PROPS-VOCABULARY.md`, `docs/PROPS-REGISTRY.md`.
- [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y (**local only, never in CI**) + geometry + coverage gates.
- **`godx-ui-component-placement`** skill — decide whether a component belongs here.
