# Composition pattern vs Framework component — the decision contract

> **Cardinal rule:** before ANY new thing is added to `src/components/`, it MUST pass the
> **Framework-Component Test** below — _all_ criteria, no exceptions. If it fails even one, it is a
> **composition pattern**: build it from existing primitives + tokens in the app/showcase, and do
> **not** add it to the framework. **When in doubt, compose.** The framework stays lean; brand- and
> screen-specific UI lives where it belongs — in the consumer app (or a `docs/` showcase), as a
> composition of real primitives configured by tokens.

This document defines the two concepts precisely, gives the mandatory test, and lists worked examples so the call is never a matter of taste.

---

## 1. The two concepts

### Framework component (`src/components/**`)

A **reusable, behavior-bearing primitive** that ships in the `@godxjp/ui` bundle and is imported by many consumer apps. It encapsulates interaction, state, accessibility and a controlled API that must NOT be re-implemented per app. Examples: `Button`, `Select`, `DataTable`, `Dialog`, `Calendar`, `Switch`, `Combobox` (= `Select showSearch`), `StatCard`.

### Composition pattern (consumer app, or a `docs/` showcase)

A **specific arrangement of existing primitives** for a specific screen, domain or brand — a dashboard layout, a marketing **Hero**, a **Navbar**, a **Footer**, a pricing section, a settings page. It has **no reusable behavior of its own**: it is layout + content + token configuration over real components. It is brand/design-specific and is **expressible today** by composing `Card`, `Button`, `Text`, `ResponsiveGrid`, `Flex`, `Badge`, `Avatar`, … + token overrides (incl. scoped `[data-tenant]` / per-region role scoping).

> A Claude Design's marketing landing page (Hero / Services / CTA / Footer) is the canonical
> composition case: it is reproduced 100% from **token configuration + real primitives**, with zero
> new framework components. See `docs/showcase/acme-website.tsx`.

---

## 2. The Framework-Component Test (MANDATORY — all 7 must pass)

Before creating OR proposing a framework component, evaluate **every** criterion and record the verdict. **Allowed only if ALL are `PASS`.** Any `FAIL` ⇒ it is a composition pattern; use existing components instead.

| #                                                                 | Criterion                                                                                                                                | PASS means                                                            | FAIL means (→ compose)                                                                                         |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **C1. Universal, not design-specific**                            | Needed by many apps across domains; not a recreation of one design's block.                                                              | A generic capability (a select, a table, a date picker).              | "ACME Hero", "AcmePricingTable", "this dashboard's header".                                                    |
| **C2. Encapsulates reusable BEHAVIOR**                            | Owns non-trivial state / keyboard / focus / ARIA that must not be re-implemented per app.                                                | Listbox navigation, dialog focus-trap, table sort/select, async load. | Pure static layout/visual arrangement (a hero, a footer, a banner).                                            |
| **C3. Not expressible by composing existing primitives + tokens** | You genuinely cannot build it from `Card`/`Button`/`Text`/`Grid`/`Flex`/… + token overrides.                                             | A new interaction primitive with no existing equivalent.              | You _can_ build it today from primitives + tokens (then you must).                                             |
| **C4. Single responsibility + controlled-vocabulary API**         | One job; maps to `value`/`defaultValue`/`onValueChange`, `size ∈ xs\|sm\|md\|lg`, `tone`, etc.; generalizes beyond one screen.           | `Select`, `Switch`, `Pagination`.                                     | A grab-bag "Section"/"Block"/"Layout" with a bespoke, screen-shaped API.                                       |
| **C5. Fully token-themeable, zero baked brand**                   | Appearance is 100% token-driven; works for every brand with no code change.                                                              | Reads `hsl(var(--…))` / semantic tokens only.                         | Needs baked colors / raw hex / per-brand styling to look right.                                                |
| **C6. Earns the international contract**                          | Broad reuse justifies the full i18n (`t()`/`Intl`) + WAI-ARIA APG + WCAG 2.2 AA + RTL + a11y-test + MCP-catalog cost.                    | A control everyone uses and that needs correct ARIA.                  | A one-off where that cost is pure overhead.                                                                    |
| **C7. Named pattern + neutral defaults**                          | A pattern named in at least TWO of WAI-ARIA APG, antd, MUI, shadcn (or an ISO/CLDR need), whose defaults encode no one product's policy. | `Select` (APG combobox · antd · MUI · shadcn), `Calendar` (CLDR).     | A screen-shaped invention no reference names ⇒ compose; one product's policy baked in ⇒ that product's preset. |

> **Heuristic shortcuts** (a single one is usually enough to send it to composition):
>
> - "Could I build this **right now** from existing primitives + token overrides?" → **yes ⇒ compose** (C3).
> - "Is the name a **proper noun / screen name / brand block** (Hero, Navbar, Footer, Pricing,
>   DashboardHeader)?" → **yes ⇒ compose** (C1).
> - "Does it own **any real behavior**, or is it just **layout + content + color**?" → layout only ⇒ **compose** (C2).

### 2.1 The admission rule — usage decides the TIER, never the verdict (v32, #1205 decision B4)

C7 used to read "earns its bundle cost — used across most apps". That made usage the verdict, and
usage in our own repos measures **migration exposure, not worldwide need**: a date-range picker
nobody in GoDX happens to import yet is still a standard form primitive every adopter expects. The
rule that replaced it (R4, agreed by both reviewers; JEV 0.87–0.89):

A **core** component must

1. **own reusable behaviour** — keyboard, focus, ARIA, timing, measurement — **measured in the
   source**, not claimed in the tagline (C2);
2. **match a pattern named in at least two of** WAI-ARIA APG, antd, MUI, shadcn, or an ISO/CLDR
   need (C7);
3. **not be composable** from primitives plus tokens — if it is, it is a recipe in `docs/` (C3);
4. **have neutral defaults** — a component that encodes one product's policy (its roles, its
   branches, its identity mark) belongs in that product's preset, e.g. `@godxjp/ui/themes/godx`
   (C5 + C7).

Then **usage picks the tier**, it never removes a component on its own:

| Passes the rule, and …                         | Ships from                       | Notes                                                                                                                                                                                    |
| ---------------------------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| imported by consumers                          | `@godxjp/ui` (core)              | The default agent context; the full contract.                                                                                                                                            |
| **no consumer imports it yet**                 | **`@godxjp/ui/lab`**             | Same gates as core (tests, a11y, i18n, tokens) and semver'd; left out of the default MCP listings. **Promoted** to core once imported by **≥ 2 repos**; **demotion is never automatic**. |
| a cohesive family with its own audience (chat) | its own package (`@godxjp/chat`) | Composes the kit's public API only.                                                                                                                                                      |
| fails the rule                                 | —                                | A recipe in `docs/`, or the product's preset.                                                                                                                                            |

### Decision flow

```
new UI need
  │
  ├─ Does an existing primitive already cover it? ───────────── yes → USE IT (no new code)
  │        (Select = searchable/async; AppShell/PageContainer = chrome; DataTable = grids)
  │
  ├─ Run the Framework-Component Test (C1–C7).
  │        ├─ ALL pass → it MAY be a framework component; §2.1 picks core vs `/lab` by usage
  │        │             → follow the full `godxjp-ui-component` contract (i18n/a11y/vocab/
  │        │               tokens/test/MCP/registry) before merging into src/components/.
  │        └─ ANY fail → it is a COMPOSITION PATTERN
  │                      → build it from existing primitives + tokens in the app (or a docs/
  │                        showcase). Resolve any visual gap with a TOKEN, never a baked value.
  │                        NEVER add it to src/components/.
  │
  └─ If a token is missing to express it → ADD THE TOKEN (extensibility), not a component.
```

---

## 3. Worked examples

| Thing                                                   | C1  | C2  | C3  | C4  | C5  | C6  | C7  | Verdict                                                                                                            |
| ------------------------------------------------------- | --- | --- | --- | --- | --- | --- | --- | ------------------------------------------------------------------------------------------------------------------ |
| `Select` (incl. search/async)                           | ✅  | ✅  | ✅  | ✅  | ✅  | ✅  | ✅  | **Framework component**                                                                                            |
| `DataTable`, `Dialog`, `Calendar`, `Switch`, `Combobox` | ✅  | ✅  | ✅  | ✅  | ✅  | ✅  | ✅  | **Framework component**                                                                                            |
| `StatCard` (+ `icon`)                                   | ✅  | ➖  | ✅  | ✅  | ✅  | ✅  | ✅  | **Framework component** (a reusable KPI tile with a stable API + tokens)                                           |
| `ServiceLauncherCard`                                   | ➖  | ❌  | ❌  | ✅  | ✅  | ➖  | ➖  | **Composition by the test — RETAINED as a recorded exception** (gh#814)                                            |
| `MegaMenu`                                              | ✅  | ✅  | ✅  | ✅  | ✅  | ✅  | ✅  | **Framework component, `@godxjp/ui/lab` tier** (no consumer yet, §2.1) — the instructive contrast with Hero, below |
| Marketing **Hero**                                      | ❌  | ❌  | ❌  | ❌  | ❌  | ❌  | ❌  | **Composition** — section, static, composable, brand-specific                                                      |
| **Navbar** / **Footer**                                 | ❌  | ❌  | ❌  | ❌  | ❌  | ❌  | ❌  | **Composition** — layout of `Text`/`Button`/`Flex`                                                                 |
| **PricingTable** / feature grid                         | ❌  | ❌  | ❌  | ❌  | ❌  | ❌  | ❌  | **Composition** — `ResponsiveGrid` + `Card`                                                                        |
| Dashboard **page layout**                               | ❌  | ❌  | ❌  | ❌  | ➖  | ❌  | ❌  | **Composition** — `AppShell` + `PageContainer` + `ResponsiveGrid`                                                  |
| "Icon medallion"                                        | ❌  | ❌  | ❌  | ❌  | ✅  | ❌  | ❌  | **Composition** — `Avatar` (square) + a Lucide glyph                                                               |
| **Section** / **Band** (full-bleed + measured column)   | ❌  | ❌  | ❌  | ❌  | ✅  | ❌  | ❌  | **Composition** — `<section>` + `Flex measure` + `pad`; see §3.1                                                   |
| `TenantTheme` (customer hex at runtime)                 | ✅  | ➖  | ❌  | ❌  | ✅  | ❌  | ➖  | **Composition + a published FUNCTION** — `tenantTheme()`; see §3.2                                                 |

`✅ pass · ❌ fail · ➖ borderline`. **StatCard** is the instructive borderline: C2 is weak (it owns little behavior), but it is a universal KPI tile with a controlled API, fully tokenized, broadly reused — so it earns its place. A **Hero** fails six of seven; it is unambiguously a composition.

**MegaMenu is the instructive PASS, and it is in this table because it sits next to Navbar.** A
Navbar fails every criterion; a megamenu is the same bar with behaviour bolted through it, and that
one difference flips the verdict. **C2** is the whole case: a roving tabindex across the top-level
items, `aria-expanded`/`aria-controls` per trigger, hover intent (a diagonal pointer path toward the
panel must not close it), `Escape` closing and returning focus to its trigger, Tab-out and
outside-click closing, and close-on-route-change. None of that is layout. **C3** is the one people
get wrong: `DropdownMenu` looks like the answer and is the WRONG primitive, not an awkward one — it
is react-aria-components `Menu`, i.e. `role="menu"` / `role="menuitem"`, which announces a set of
site links as a desktop application menu and takes Tab out of the widget. The APG says so itself in
its Disclosure Navigation example. A recipe over `DropdownMenu` would therefore ship the classic
megamenu accessibility defect, which is exactly the case C3 is asking about. **C7 passes** under
the v32 rule — APG names the Disclosure Navigation pattern and antd ships `Menu mode="horizontal"`
with popup panels. What the old C7 ("used across most apps") marked ➖ is now a TIER question, not a
verdict: no GoDX repo imports it yet, so it ships from `@godxjp/ui/lab` (§2.1) until two do.

**ServiceLauncherCard is the ONE recorded exception, and it is recorded so that it stays one.** It shipped before the test was run against it, and when the test was run it came back with two hard FAILs: **C2** — no state, no keyboard handling, no focus management, its only ARIA three static attributes — and **C3** — its own imports are `Card` + `CardContent` + `Badge` + a Lucide glyph, so "could I build this right now from primitives?" is yes. By §2 that makes it a composition pattern. It is kept anyway because `src/components/layout/app-launcher.tsx` consumes it: it is an internal building block of a component that **does** pass, so the question was never "should it exist" but "should it be PUBLIC", and removing a public export is breaking. Keeping it public was the cheaper call and the ledger lives on gh#814 and at the top of `src/components/data-display/service-launcher-card.tsx`. **Consumers should compose `Card` + `Badge` for a service tile** — reach for `ServiceLauncherCard` only to match `AppLauncher`'s own tiles — and this row is not precedent for adding another static tile to `src/components/`.

### 3.1 "Full-bleed outside, measured column inside" — the case that was answered with a PROP (gh#839)

This row exists because the question it settles was asked the other way round, and the right answer
turned out to be neither "add a component" nor "do nothing".

**A `Section` component is refused, and it is refused for Hero's reasons.** It owns no state, no
keyboard, no focus and no ARIA a `<section aria-labelledby>` does not already have (**C2**); it is
`<section>` + `Flex` + padding + a max-width, i.e. buildable today (**C3**); its API would be the
grab-bag `band` / `measure` / `glow` / `surface` / `bleed` that **C4** names by name; and no
reference library names a "band" component — it is a page section, not a pattern (**C7**). One
criterion of seven passes. Nothing about that is close.

**But "do nothing" was also refused, and that is the part worth remembering.** The doctrine's own
remedy for a composition is a TOKEN (§4.3), and `--page-measure-wide` shipped in 28.8.0 — so on
paper the gap was already closed. It was not. A token is only closed when something can READ it,
and three unrelated pages had each hand-written the identical four declarations to do so:
`docs/showcase/marketing-page.tsx` as an inline `style` constant, `acme-website.tsx` as `.tx-shell`,
`futurelastic-web.tsx` as `.fl-shell` — one shape, three spellings, two of them page-local CSS
classes on the pages whose entire claim is that they need none. `docs/TOKENS.md` calls a value that
appears in more than one place tier 1, and the same logic reads on a SHAPE.

**The third move is a prop on a primitive that already passes the test.** `Flex measure` (narrow |
medium | wide) centres the box and caps it at `var(--page-measure-*)`; it adds no gutter, because
`pad` owns that. A full-bleed band is now `<section>` — carrying the edge-to-edge paint — around one
`<Flex direction="col" measure="wide" pad={{ inline: 6, block: 20 }}>`. Measured on
`marketing-page.tsx`: seven bands, 7 inline `style` spreads and 3 `CSSProperties` constants gone,
the metric that page defends (0 bespoke classes, 0 raw px/rem literals) unchanged at 0/0, and the
geometry byte-identical (1440: section 1440px, column 1152px, band 96/80px, gutter 24px).

**`PageContainer measure="wide"` stays refused, and for the original reason.** A marketing page is
full-bleed `<section>`s; `PageContainer` owns page padding and a header/toolbar/footer scaffold, so
the two pages that are the prop's proof still cannot consume it. The prop went to the primitive
those pages CAN put inside a `<section>`, which is the distinction the earlier deferral was missing
rather than a reversal of it.

**The rule this leaves behind:** when a composition repeats a shape, ask whether an existing
primitive is missing ONE AXIS before you ask whether a new component is missing. A component that
fails C1–C7 and a prop that closes the same gap are not the same proposal, and the test above is
only asking about the first.

### 3.2 `<TenantTheme>` — the case that was answered with a FUNCTION (gh#861, gh#868)

Two consumer repos asked for the same wrapper, in the same words: `<TenantTheme primary={hex}>`,
so that one region of a page can wear a **customer's** brand colour at runtime. One repo had
already hand-rolled it and shipped it to production; the other deliberately had not, because
copying it would have been the third private version of the same thing.

| #                                                    | verdict                                          | why                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ---------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **C1** universal                                     | **PASS**                                         | Multi-tenant branding is not one design's block; two unrelated repos hit it independently, and a third (`AppSettingPicker kind="brand"`) already covers the PRESET axis, which is what makes the arbitrary-hex axis missing rather than duplicated.                                                                                                                                                                                                                                                 |
| **C2** owns reusable behaviour                       | ➖ **borderline, and it is the interesting one** | There IS behaviour that must not be re-derived per consumer — hex → HSL triplet, and the WCAG contrast rule that picks the label. But it is arithmetic, not interaction: no state, no keyboard, no focus, no ARIA, nothing a `<div>` does. C2 asks for behaviour a COMPONENT must own; a pure function owns this better, because it is then callable from a server (Platform already computes its pair at write time), from a `<style>` block with no hydration flash, and from a test with no DOM. |
| **C3** not expressible from primitives + tokens      | **FAIL**                                         | Once the arithmetic is published, the wrapper is `<div data-tenant={slug} style={tenantTheme(hex).vars}>` — a scoped token override, which §4.3 of this document names as the remedy for a composition. `docs/showcase/theme-customization.tsx` had already recorded the same verdict for the same shape.                                                                                                                                                                                           |
| **C4** single responsibility + controlled vocabulary | **FAIL**                                         | The requested API is `primary` / `secondary` / `primaryForeground` / `tenant`. There is no `value`/`defaultValue`/`onValueChange`, no `size`, no `tone` — it is a configuration carrier with a screen-shaped API, which is the shape C4 names by name.                                                                                                                                                                                                                                              |
| **C5** token-themeable, zero baked brand             | **PASS** (vacuously)                             | It IS the theming mechanism; it bakes nothing.                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| **C6** earns the international contract              | **FAIL**                                         | It renders no text, has no role, no keyboard and no ARIA, so there is nothing for `t()` or the APG to say. The accessibility content of this feature is CONTRAST, and that lives in the computation and its tests — not in a component contract.                                                                                                                                                                                                                                                    |
| **C7** named pattern + neutral defaults              | ➖                                               | The need is universal and the defaults are neutral, but no reference names a theming WRAPPER component — antd's ConfigProvider and MUI's ThemeProvider configure tokens, which is what the published function does. A React wrapper around a `<div>` is not the pattern.                                                                                                                                                                                                                            |

**Three hard FAILs. Verdict: composition pattern — plus a published function.** `tenantTheme(hex)`
in `@godxjp/ui/app` returns the declarations (`--primary`, `--primary-foreground`, `--ring`, and
the hover / pressed steps), the label it chose, the WCAG 2.2 ratio it achieves and whether that
clears AA. `hexToHsl` / `contrastRatio` / `relativeLuminance` ship beside it. The `<div>` lives in
the consumer app; `docs/showcase/tenant-brand-color.tsx` is the worked screen.

**The rule this leaves behind, and it is the twin of §3.1.** §3.1 says: when a composition repeats
a SHAPE, ask whether a primitive is missing one axis before you ask whether a component is missing.
This says: when a composition repeats a CALCULATION, ask whether the package is missing a FUNCTION.
A wrapper component and a published function are not the same proposal, and C1–C7 is only asking
about the first. The tell is C6 — if there is no text, no role and no keyboard for the
international contract to apply to, the thing you are holding is probably not a component.

---

## 4. How to build a composition pattern correctly

1. **Real primitives only.** Compose `Card`/`Button`/`Text`/`ResponsiveGrid`/`Flex`/`Avatar`/… — never hand-roll a control or fake a primitive with a styled `<div>`. 2. **Sections are layout, not components.** A `<section>`/`<header>`/`<footer>` wrapper that arranges primitives is allowed — it is plain layout, not a faked primitive. 3. **Resolve every visual gap with a TOKEN.** A navy hero, a glass card, a brand glow, display type → a token override (global `:root`, scoped `[data-tenant]`, or **per-region role scoping** `[data-tenant] .region { --card: navy; --foreground: white; … }`). If the token doesn't exist, **add the token to the framework** (extensibility), never bake a value into the composition. 4. **Live in the app, or a `docs/` showcase** — never `src/components/`.

> The whole point of the token-extensibility work (scoped theming, glow / tint / gradient / focus,
> display type, dual-font, role-scoping) is so that **a composition pattern can reach 100% design
> fidelity from tokens alone** — which is exactly why almost nothing new needs to become a framework
> component.

---

## 5. The mandatory gate (enforced)

This contract is wired into the build/skill gates:

- **`godxjp-ui-component` skill, Gate 0** — the FIRST gate before writing any component is the Framework-Component Test.
