@godxjp/ui 28.7.0 → 28.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/components/data-display/index.d.ts +2 -0
  2. package/dist/components/data-display/index.js +2 -0
  3. package/dist/components/data-display/marquee.d.ts +16 -0
  4. package/dist/components/data-display/marquee.js +155 -0
  5. package/dist/components/general/reveal.d.ts +23 -2
  6. package/dist/components/general/reveal.js +37 -7
  7. package/dist/components/general/typography.d.ts +4 -1
  8. package/dist/components/general/typography.js +14 -1
  9. package/dist/components/layout/affix.d.ts +86 -0
  10. package/dist/components/layout/affix.js +187 -0
  11. package/dist/components/layout/index.d.ts +4 -0
  12. package/dist/components/layout/index.js +4 -0
  13. package/dist/components/layout/legal-document-shell.js +4 -3
  14. package/dist/components/layout/masonry.d.ts +74 -0
  15. package/dist/components/layout/masonry.js +214 -0
  16. package/dist/components/layout/page-container.js +5 -20
  17. package/dist/components/navigation/anchor.d.ts +64 -0
  18. package/dist/components/navigation/anchor.js +284 -0
  19. package/dist/components/navigation/index.d.ts +4 -0
  20. package/dist/components/navigation/index.js +4 -0
  21. package/dist/components/navigation/mega-menu.d.ts +21 -0
  22. package/dist/components/navigation/mega-menu.js +526 -0
  23. package/dist/contracts/measurement.json +1 -1
  24. package/dist/i18n/messages/en.json +517 -0
  25. package/dist/i18n/messages/ja.json +513 -0
  26. package/dist/i18n/messages/vi.json +513 -0
  27. package/dist/lib/hooks.d.ts +68 -0
  28. package/dist/lib/hooks.js +52 -0
  29. package/dist/lib/platform.d.ts +14 -0
  30. package/dist/lib/platform.js +10 -1
  31. package/dist/lib/utils.d.ts +1 -1
  32. package/dist/lib/utils.js +3 -2
  33. package/dist/props/components/data-display.prop.d.ts +95 -1
  34. package/dist/props/components/general.prop.d.ts +47 -3
  35. package/dist/props/components/layout.prop.d.ts +194 -0
  36. package/dist/props/components/navigation.prop.d.ts +263 -0
  37. package/dist/props/registry.d.ts +359 -4
  38. package/dist/props/registry.js +472 -3
  39. package/dist/props/vocabulary/index.d.ts +1 -1
  40. package/dist/props/vocabulary/interaction.prop.d.ts +39 -2
  41. package/dist/styles/control.css +5 -6
  42. package/dist/styles/data-display-layout.css +2 -1
  43. package/dist/styles/density.css +4 -0
  44. package/dist/styles/layout.css +79 -0
  45. package/dist/styles/motion.css +121 -1
  46. package/dist/styles/navigation-layout.css +397 -1
  47. package/dist/styles/shell-layout.css +3 -0
  48. package/dist/styles/text-layout.css +52 -4
  49. package/dist/tokens/base.css +5 -0
  50. package/dist/tokens/components/affix.css +7 -0
  51. package/dist/tokens/components/anchor.css +17 -0
  52. package/dist/tokens/components/control.css +3 -3
  53. package/dist/tokens/components/form.css +1 -1
  54. package/dist/tokens/components/marquee.css +7 -0
  55. package/dist/tokens/components/masonry.css +6 -0
  56. package/dist/tokens/components/mega-menu.css +62 -0
  57. package/dist/tokens/components/shell.css +3 -0
  58. package/dist/tokens/foundation.css +11 -0
  59. package/dist/tokens/semantic/layout.css +7 -0
  60. package/docs/COMPOSITION-VS-COMPONENT.md +19 -1
  61. package/docs/DESIGN-AUTHORITY.md +99 -18
  62. package/docs/FRAME-COVERAGE-REPORT.md +7 -2
  63. package/docs/data-display/marquee.tsx +254 -0
  64. package/docs/foundation/_theme-editor-scope.ts +222 -0
  65. package/docs/foundation/density.tsx +12 -2
  66. package/docs/foundation/spacing.tsx +5 -0
  67. package/docs/foundation/theme-editor.tsx +645 -0
  68. package/docs/general/activity.tsx +65 -0
  69. package/docs/general/reveal.tsx +290 -22
  70. package/docs/general/typography.tsx +91 -1
  71. package/docs/layout/affix.tsx +209 -0
  72. package/docs/layout/masonry.tsx +291 -0
  73. package/docs/navigation/anchor.tsx +285 -0
  74. package/docs/navigation/mega-menu-panel.tsx +86 -0
  75. package/docs/navigation/mega-menu.tsx +254 -0
  76. package/docs/roadmap/website-components.md +779 -0
  77. package/docs/showcase/acme-website.tsx +75 -39
  78. package/docs/showcase/futurelastic-web.tsx +91 -49
  79. package/docs/showcase/marketing-page.tsx +885 -0
  80. package/docs/showcase/table-footer-totals.tsx +12 -2
  81. package/docs/showcase/theme-customization.tsx +1259 -0
  82. package/package.json +5 -3
  83. package/scripts/brand-accent.generated.mjs +27 -0
  84. package/scripts/ui-audit.mjs +66 -0
  85. package/scripts/visual-audit-rules.mjs +46 -2
@@ -0,0 +1,779 @@
1
+ # Website & marketing surfaces — the plan
2
+
3
+ > **Status:** proposed · 2026-09-21 · branched from `main` at `76076266`
4
+ >
5
+ > **Contract:** `docs/COMPOSITION-VS-COMPONENT.md` (GATE 0) · `docs/DESIGN-AUTHORITY.md` (antd is the
6
+ > standard) · `docs/SPACING.md` · `docs/TOKENS.md` · `.claude/skills/godxjp-ui-component/SKILL.md`.
7
+ > **Antd reference version: `antd@6.6.5`** (read from the repo's `package.json` on 2026-09-21, and
8
+ > every API table below was fetched from `ant-design/ant-design@master` on that date — not recalled).
9
+ > Pin the version when re-auditing, per `docs/roadmap/parity-backlog.md`.
10
+ >
11
+ > This document decides **what belongs where**. It adds no component and writes no code. Its only
12
+ > claim is that the two halves below — a small set of behaviour-bearing components, and a set of
13
+ > compositions whose missing pieces are TOKENS and PROPS — together close the website gap without
14
+ > smuggling a `Hero` into `src/components/`.
15
+
16
+ ---
17
+
18
+ ## 0. Re-deriving every number in this document
19
+
20
+ ```bash
21
+ # catalog shape (§1)
22
+ python3 -c "import json,glob,collections; g=collections.Counter(json.load(open(f))['group'] for f in glob.glob('agent/components/*.json')); print(sum(g.values()), g.most_common())"
23
+ # what the marketing showcases had to hand-write (§5)
24
+ grep -c 'font-size: [0-9.]*rem' docs/showcase/acme-website.tsx docs/showcase/futurelastic-web.tsx
25
+ # components that publicly own scroll position (§4.1)
26
+ grep -rln 'IntersectionObserver' src/components/ | grep -v __tests__
27
+ # the display type ramp, and who reads it
28
+ grep -rn 'font-size-display\|font-size-5xl\|font-size-4xl\|font-size-3xl' src/ | grep -v tokens/foundation.css
29
+ ```
30
+
31
+ ---
32
+
33
+ ## 1. The measured gap
34
+
35
+ **165 catalogued components** (`agent/components/*.json`, at `76076266`):
36
+
37
+ | group | n | share |
38
+ | ------------ | --- | ------ |
39
+ | data-display | 45 | 27.3 % |
40
+ | data-entry | 45 | 27.3 % |
41
+ | layout | 29 | 17.6 % |
42
+ | feedback | 20 | 12.1 % |
43
+ | general | 14 | 8.5 % |
44
+ | navigation | 9 | 5.5 % |
45
+ | providers | 3 | 1.8 % |
46
+
47
+ `data-entry + data-display` = **90 / 165 = 54.5 %**. The owner's "55 % form-and-table" is confirmed
48
+ to a tenth of a point. Two sharper numbers matter more than that one, because they name the missing
49
+ _capability_ rather than the missing _category_:
50
+
51
+ 1. **Navigation is 5.5 % of the library, and none of it is site navigation.** All nine entries
52
+ (`AppSettingPicker`, `AppSettingToggle`, `Conversations`, `DropdownMenu`, `FilterBar`,
53
+ `Pagination`, `Steps`, `Tabs`, `Toolbar`) are in-app chrome. There is no horizontal site nav, no
54
+ nav panel, no in-page section nav.
55
+ 2. **Almost nothing in the library reacts to scroll position, and what does is private.**
56
+ `grep -rln IntersectionObserver src/components/` returns exactly **one non-test file**:
57
+ `src/components/layout/page-container.tsx`, where `useRevealOnScroll` (`:28-44`) powers
58
+ `PageContainer footerReveal="onScroll"` and is not exported. The only public scroll-aware API in
59
+ 165 components is `FloatButton.BackTop` (`visibilityHeight`, `target`). A website is built out of
60
+ scroll-position behaviour; the library has one instance of it and keeps the machinery private.
61
+
62
+ **The marketing showcases already exist and already measure the cost.** `docs/showcase/acme-website.tsx`
63
+ and `docs/showcase/futurelastic-web.tsx` are complete landing pages built to the doctrine — real
64
+ primitives, token configuration, zero new components. That is the good news, and the receipts are the
65
+ bad news:
66
+
67
+ | showcase | bespoke CSS classes | CSS declarations | raw `px`/`rem` literals |
68
+ | ------------------ | ------------------- | ---------------- | ----------------------- |
69
+ | `acme-website` | 26 | 97 | 47 |
70
+ | `futurelastic-web` | 32 | 123 | 77 |
71
+
72
+ **58 bespoke classes and 124 raw length literals**, and the two lists overlap almost exactly:
73
+ `shell`, `section`, `display`, `h2`, `lead`, `eyebrow`, `navbar`, `navbar-inner`, `brand`, `gold`,
74
+ `medallion`, `footer-grid`, `footer-bottom`, plus a hand-written radial `glow` in both. When two
75
+ independent brands hand-write the same thirteen classes, that is not brand styling. That is a
76
+ **missing token and a missing prop**, which is precisely what §4 of the doctrine says to fix:
77
+
78
+ > _"Resolve every visual gap with a TOKEN … If the token doesn't exist, **add the token to the
79
+ > framework** (extensibility), never bake a value into the composition."_
80
+
81
+ So the gap is **not** "there is no Hero". The gap is:
82
+
83
+ - **(a)** four or five pieces of genuine scroll/overlay **behaviour** that do not exist at all, and
84
+ - **(b)** a marketing **token + prop surface** so thin that every composition re-invents it.
85
+
86
+ ---
87
+
88
+ ## 2. The doctrine, and one place I think it is wrong
89
+
90
+ §2 of `COMPOSITION-VS-COMPONENT.md` is binding and this plan does not ask to relax it: all seven
91
+ criteria, or it is a composition. Everything in §5 below stays out of `src/components/`, under its
92
+ own name and under any other name — a `Hero` called `Banner`, `Section`, `MarketingBlock` or
93
+ `LandingShell` is the same refused component with the evidence filed off.
94
+
95
+ Two corrections I do ask for, in writing, because the plan leans on both.
96
+
97
+ ### 2.1 The §3 worked-examples table scores `❌ ×7`, and that is not true of C5/C6
98
+
99
+ `Marketing Hero`, `Navbar`/`Footer` and `PricingTable` are each recorded as failing **all seven**
100
+ criteria. C5 is _"fully token-themeable, zero baked brand"_ and C6 is _"earns the international
101
+ contract"_. A `❌` on C5 for a Hero reads as "a hero cannot be expressed from tokens" — which
102
+ contradicts §4 of the same document, and is disproved by `docs/showcase/acme-website.tsx`, whose
103
+ entire purpose was proving that it can. C6 is likewise not a failure: a marketing page owes heading
104
+ order, `lang`, RTL and `Intl` exactly like any other page; what it does not owe is a _component's_
105
+ ARIA contract.
106
+
107
+ The verdicts are right. The ledger is sloppy, and a sloppy ledger teaches the next author to score
108
+ every cell `❌` once they know the answer — which is how a real `C2` PASS gets buried. **Proposed
109
+ edit** (verdicts unchanged):
110
+
111
+ | row | C1 | C2 | C3 | C4 | C5 | C6 | C7 | verdict |
112
+ | ------------------------------- | --- | --- | --- | --- | --- | --- | --- | --------------- |
113
+ | Marketing **Hero** | ❌ | ❌ | ❌ | ❌ | ✅ | ➖ | ❌ | **Composition** |
114
+ | **Navbar** / **Footer** | ❌ | ❌ | ❌ | ❌ | ✅ | ➖ | ❌ | **Composition** |
115
+ | **PricingTable** / feature grid | ❌ | ❌ | ❌ | ❌ | ✅ | ➖ | ❌ | **Composition** |
116
+
117
+ C1/C2/C3/C4/C7 are the criteria that decide these, and they decide them decisively. That is a
118
+ stronger position, not a weaker one: it says the rejection rests on _behaviour and universality_,
119
+ the two things §2 actually cares about, rather than on an unbelievable clean sweep.
120
+
121
+ (The `PricingTable` row also has a stale note — "`ResponsiveGrid` + `Card`". Since it was written the
122
+ library shipped `FeatureList`, which is the included/limited/excluded tier list. Pricing is now
123
+ _more_ composable than the row says, not less.)
124
+
125
+ ### 2.2 C7 says "bundle cost" but the package is not bundled
126
+
127
+ `tsup.config.ts` sets `bundle: false` with one output file per source module, precisely so that
128
+ "per-component imports tree-shake perfectly". A component no consumer imports therefore costs a
129
+ consumer **zero bytes**. C7 as literally written ("earns its bundle cost … worth shipping to _every_
130
+ consumer") measures something the build no longer does.
131
+
132
+ This is not academic: `docs/roadmap/parity-backlog.md` defers antd's `Image` preview (the lightbox)
133
+ with the reason _"pass on merit but fail C7 (bundle cost)"_. If C7 means bytes, that reason has
134
+ expired. If C7 means **maintenance surface, API surface and the i18n/a11y/MCP contract per
135
+ component** — which is the real and quite sufficient cost — then the deferral stands and should be
136
+ restated in those terms. I recommend restating C7 as _"earns its maintenance and contract cost"_
137
+ and leaving the `Image` deferral in place on the restated ground (§4.6).
138
+
139
+ ---
140
+
141
+ ## 3. The whole plan in one table
142
+
143
+ **Framework components (C1–C7 all PASS) — 5 definite, 2 conditional:**
144
+
145
+ | # | thing | antd source | what makes it C2/C3 | verdict |
146
+ | ------------------------------------ | ----------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------- |
147
+ | [4.1](#41-affix) | sticky / shrinking header on scroll | **`Affix`** | scroll-threshold state + a placeholder that prevents the reflow jump | **Component** |
148
+ | [4.2](#42-anchor) | scrollspy anchor nav | **`Anchor`** | which section is current, from scroll + hash, with the ink indicator | **Component** |
149
+ | [4.3](#43-menu-mode--horizontal) | MegaMenu | **`Menu` `mode="horizontal"`** | disclosure-navigation: roving triggers, shared viewport, hover intent, Escape | **Component** (riskiest) |
150
+ | [4.4](#44-reveal-gains-onview) | scroll-reveal | — (extend the existing `Reveal`) | enter-viewport trigger; `Reveal` today fires on mount only | **Extend, do not add** |
151
+ | [4.5](#45-marquee) | marquee / ticker | — (`react-fast-marquee` is the prior art) | measured cloning + the **WCAG 2.2.2 pause control** a hand-roll always omits | **Component** |
152
+ | [4.6](#46-conditional-image-preview) | lightbox / gallery | **`Image` + `Image.PreviewGroup`** | zoom/pan/rotate, focus trap, group paging | **Conditional — deferred** |
153
+ | [4.7](#47-conditional-countup) | animated counter | — (antd has none; `react-countup` prior art) | rAF tween + `Intl` + one polite announcement, not sixty | **Conditional** |
154
+
155
+ **Compositions (a `docs/` showcase + the named token, never `src/components/`):**
156
+
157
+ | thing | built from | missing piece |
158
+ | --------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
159
+ | Hero | `Flex` · `ResponsiveGrid`+`Item` · `Heading` · `Text` · `Button` · `AspectRatio` · `Reveal` | display type **prop**, band spacing, `.ui-brand-glow` adoption |
160
+ | Navbar | `Topbar` · `Logo` · `Button` · `Menu`(4.3) · `Sheet` · `Affix`(4.1) | `--topbar-background-alpha`, `--topbar-backdrop-blur-size` |
161
+ | Footer | `ResponsiveGrid columns={12}` + `Item span` · `Separator` · `Text link` · `Logo` | none — **document `ResponsiveGrid.Item`** |
162
+ | Pricing table | `ResponsiveGrid` · `Card accent` · `FeatureList` · `Badge` · `Segmented` · `Button` | none |
163
+ | Feature grid | `ResponsiveGrid` · `Card` · `Avatar`+glyph · `Heading` · `Text` | none |
164
+ | CTA band | `Card variant` + per-region role scoping · `Button` · `Heading` | band spacing; `--gradient-brand` needs a call site |
165
+ | Testimonial / quotes | `Card` · `Avatar` · `Text` · `Carousel` | none |
166
+ | Logo wall | `Flex wrap` / `ResponsiveGrid` · `Thumbnail` · optionally `Marquee`(4.5) | none |
167
+ | Section band / shell | `PageContainer` · `Flex` | `--phi-p3/p4`, `--space-band*`, `--page-measure-wide` |
168
+ | "Icon medallion" | `Avatar` (square) + Lucide glyph | none (already ruled, §3 of the doctrine) |
169
+ | BorderBeam (antd 6.4) | a themed CSS class in the consumer stylesheet | none — see §8 |
170
+
171
+ **Already exists — do NOT build (the anti-duplication list):**
172
+
173
+ | you were about to build | it is already | where |
174
+ | ---------------------------- | --------------------------------------------- | ---------------------------------------------------------------- |
175
+ | back-to-top button | `FloatButton.BackTop` | `visibilityHeight`, `target` — antd's own API |
176
+ | slider / testimonial rotator | `Carousel` | Embla; `CarouselDots`, auto-disabling arrows |
177
+ | pricing feature ticks | `FeatureList` | included / limited / excluded glyphs |
178
+ | monthly↔yearly switch | `Segmented` | with `count`/`overflowCount` |
179
+ | FAQ | `Accordion` | (fix its hardcoded `<h3>` — parity-backlog P1) |
180
+ | entrance animation | `Reveal` | staggered fade-up on the motion tokens |
181
+ | hero halo | `.ui-brand-glow` + `--brand-glow-*` | `src/styles/layout.css:125` |
182
+ | hero display type | `--font-size-display` / `3xl` / `4xl` / `5xl` | `src/tokens/foundation.css:513-521` |
183
+ | contact form | `Form` + `Field` + `useZodForm` | already the richest part of the library |
184
+ | media frame | `AspectRatio`, `Thumbnail`, `CardCover` | |
185
+ | masonry gallery | **in flight** — `Masonry`, another agent | `docs/roadmap/list-masonry.md` §2 — **not touched by this plan** |
186
+ | long virtual feed | **in flight** — `List` (antd `Listy`) | `docs/roadmap/list-masonry.md` §1 |
187
+
188
+ ---
189
+
190
+ ## 4. Framework components — ledgers and antd APIs
191
+
192
+ ### 4.1 `Affix`
193
+
194
+ **antd has it, by that name.** `antd@6.6.5` `Affix`, fetched 2026-09-21:
195
+
196
+ | antd prop | type | default |
197
+ | -------------- | ------------------------------------- | -------------- |
198
+ | `offsetTop` | `number` | `0` |
199
+ | `offsetBottom` | `number` | – |
200
+ | `target` | `() => Window \| HTMLElement \| null` | `() => window` |
201
+ | `onChange` | `(affixed?: boolean) => void` | – |
202
+
203
+ #### GATE 0 ledger
204
+
205
+ | # | criterion | verdict | why |
206
+ | --- | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
207
+ | C1 | Universal | **PASS** | A sticky toolbar over a long table, a sticky filter rail, a sticky form footer, a sticky site header. Admin needs this as much as marketing does. |
208
+ | C2 | Reusable behaviour | **PASS** | Observes a scroll container against a threshold, and — the part every hand-roll gets wrong — renders a **placeholder of the measured size** so the page does not jump when the element pins. Re-measures on resize. Emits `onChange`. |
209
+ | C3 | Not composable | **PASS** | `position: sticky` alone cannot report "am I pinned", so the shrink/condense state is unreachable, and it cannot pin against a scrolling ancestor that is not the nearest one. Both showcases wrote `position: sticky` and got no state out of it. |
210
+ | C4 | Single job + vocabulary | **PASS** | One job. antd's `offsetTop`/`offsetBottom` become logical `offsetBlockStart` / `offsetBlockEnd` (the `left`→`start` precedent in DESIGN-AUTHORITY); `target` keeps antd's lazy-getter shape, exactly as `FloatButton.BackTop` already spells it. |
211
+ | C5 | Token-themeable | **PASS** | It sets geometry, not paint. `--affix-inset-block-start`, `--affix-z-index`. |
212
+ | C6 | Earns the contract | **PASS** | A pinned header must not cover the focused element (`scroll-margin-block-start`) and must not trap `Skip to content`. That is an accessibility contract, not a style. |
213
+ | C7 | Earns its cost | **PASS** | Small, and it is the substrate for 4.2 and for the sticky Navbar composition. |
214
+
215
+ **ALL PASS → framework component.**
216
+
217
+ **Fix upstream while porting.** `PageContainer`'s private `useRevealOnScroll` (`page-container.tsx:28-44`)
218
+ is two thirds of this component, locked inside one consumer. Extract it, let `Affix` own it, and let
219
+ `footerReveal="onScroll"` read the shared implementation — otherwise the library ships the behaviour
220
+ twice and tests it once.
221
+
222
+ **Deliberately NOT ported:** nothing. Do port `onChange`; the shrinking header depends on it.
223
+
224
+ **Reduced motion:** `Affix` itself animates nothing. The _shrink_ is a composition: a
225
+ `data-affixed` attribute plus a height/padding transition on `--duration-fast`/`--ease-standard`.
226
+ Under `prefers-reduced-motion: reduce` the header **snaps** to the condensed size — it still
227
+ condenses, it just does not tween. It must never fade or disappear.
228
+
229
+ ---
230
+
231
+ ### 4.2 `Anchor`
232
+
233
+ **antd has it, by that name.** `antd@6.6.5` `Anchor`, fetched 2026-09-21:
234
+
235
+ | antd prop | type | default |
236
+ | ---------------------------- | --------------------------------------------------------------- | ------------------------ |
237
+ | `items` | `{ key, href, title, target, children }[]` | – |
238
+ | `direction` | `vertical \| horizontal` | `vertical` |
239
+ | `affix` | `boolean \| Omit<AffixProps,'offsetTop'\|'target'\|'children'>` | `true` |
240
+ | `bounds` | `number` | `5` |
241
+ | `getContainer` | `() => HTMLElement` | `() => window` |
242
+ | `getCurrentAnchor` | `(activeLink: string) => string` | – |
243
+ | `offsetTop` · `targetOffset` | `number` | `0` · – |
244
+ | `showInkInFixed` | `boolean` | `false` |
245
+ | `replace` | `boolean` | `false` |
246
+ | `onChange` · `onClick` | `(currentActiveLink) => void` · `(e, link) => void` | – |
247
+ | `items[].children` | `AnchorItem[]` | – (one level of nesting) |
248
+
249
+ Note antd's own `affix` prop takes `AffixProps` — **`Anchor` is specified on top of `Affix`**, which
250
+ is why 4.1 comes first in the ordering.
251
+
252
+ #### GATE 0 ledger
253
+
254
+ | # | criterion | verdict | why |
255
+ | --- | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
256
+ | C1 | Universal | **PASS** | A long settings page, a legal document, API docs, a marketing page's section nav. `LegalDocumentShell` exists here and has **no** table of contents. |
257
+ | C2 | Reusable behaviour | **PASS** | Resolving "which section is current" from scroll position with a bounds tolerance, keeping the hash in sync without fighting the back button, moving an ink indicator, and smooth-scrolling with an offset that clears a pinned header. |
258
+ | C3 | Not composable | **PASS** | Nothing in the library observes section visibility. `NavList activeId` takes the answer as a prop — it does not compute it. |
259
+ | C4 | Single job + vocabulary | **PASS** | `items` matches `NavList`/`Breadcrumb`. `getCurrentAnchor` stays (it is antd's controlled escape hatch); `offsetTop`/`targetOffset` become logical `offsetBlockStart`/`targetOffsetBlockStart`. |
260
+ | C5 | Token-themeable | **PASS** | `--anchor-ink-width`, `--anchor-ink-color`, `--anchor-item-height`, `--anchor-gap`. |
261
+ | C6 | Earns the contract | **PASS** | `<nav>` + `aria-current="location"` (not `"page"` — it is a fragment of the current page), and the scroll must not steal focus. Everyone gets this wrong. |
262
+ | C7 | Earns its cost | **PASS** | Small; every documentation and marketing page wants one. |
263
+
264
+ **ALL PASS → framework component.**
265
+
266
+ **Deviations to write down:** physical→logical offsets (above); `direction` stays antd's word since
267
+ `Separator`/`Flex` already spell orientation that way — check against `check:prop-vocabulary` before
268
+ committing to it. `showInkInFixed` is antd's fix for its own default; keep the behaviour, and if the
269
+ name survives review keep the name.
270
+
271
+ **Reduced motion:** the scroll on click uses `behavior: "smooth"` normally and `"auto"` under
272
+ `prefers-reduced-motion: reduce` — it still jumps to the section, instantly. The ink indicator
273
+ transitions on `--duration-fast`; reduced motion **snaps** it to the active item. The current item is
274
+ never conveyed by motion alone.
275
+
276
+ ---
277
+
278
+ ### 4.3 `Menu` (`mode="horizontal"`) — the MegaMenu
279
+
280
+ **antd has it, and it is not called MegaMenu.** In antd a megamenu is `Menu mode="horizontal"` whose
281
+ `SubMenuType` renders a custom panel through `popupRender`. `antd@6.6.5` `Menu`, fetched 2026-09-21:
282
+
283
+ | antd prop | type | default |
284
+ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------- | -------------------------- |
285
+ | `items` | `ItemType[]` | – |
286
+ | `mode` | `vertical \| horizontal \| inline` | `vertical` |
287
+ | `selectedKeys` / `defaultSelectedKeys` | `string[]` | – |
288
+ | `openKeys` / `defaultOpenKeys` | `string[]` | – |
289
+ | `triggerSubMenuAction` | `hover \| click` | `hover` |
290
+ | `subMenuOpenDelay` / `subMenuCloseDelay` | `number` (seconds) | `0` / `0.1` |
291
+ | `overflowedIndicator` | `ReactNode` | `<EllipsisOutlined />` |
292
+ | `expandIcon` · `forceSubMenuRender` | `ReactNode \| fn` · `boolean` | – · `false` |
293
+ | `popupRender` | `function` | – (**the megamenu panel**) |
294
+ | `selectable` · `multiple` | `boolean` | `true` · `false` |
295
+ | `theme` | `light \| dark` | `light` |
296
+ | `onClick`/`onSelect`/`onDeselect`/`onOpenChange` | `function` | – |
297
+ | `SubMenuType` | `{ key, label, icon, children, popupClassName, popupOffset, popupRender, onTitleClick, disabled }` | |
298
+ | `MenuItemType` | `{ key, label, icon, extra, title, danger, disabled }` | |
299
+
300
+ **Behavioural prior art for the implementation** (not for the name): Radix `NavigationMenu` —
301
+ `Root(value/defaultValue/onValueChange/delayDuration=200/skipDelayDuration=300/orientation)`, `List`,
302
+ `Item(value)`, `Trigger`, `Content(onEscapeKeyDown/onPointerDownOutside/forceMount)`, `Link(active/onSelect)`,
303
+ `Indicator`, `Viewport`, `Sub`. Its accessibility note is the important part: it follows the **W3C
304
+ disclosure-navigation** pattern and deliberately does **not** use `menu`/`menubar` roles, which "are
305
+ often considered unnecessary for website navigation".
306
+
307
+ > ⚠️ **Do not add `@radix-ui/react-navigation-menu`.** `scripts/check-radix-surface.mjs` is a ratchet:
308
+ > _"a declared package with no baseline entry → red"_, and the library is mid-migration onto
309
+ > `react-aria-components`. Radix is the **pattern** reference here, not the dependency. RAC 1.21.1
310
+ > (already installed) ships `Disclosure`, `DisclosureGroup`, `Toolbar`, `Popover`, `NavigationTree`
311
+ > — no navigation-menu — so the disclosure-nav composition has to be assembled from those plus
312
+ > `react-aria`'s hover/focus utilities. **Budget for that**; it is the reason this item is ranked
313
+ > riskiest.
314
+
315
+ #### GATE 0 ledger
316
+
317
+ | # | criterion | verdict | why |
318
+ | --- | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
319
+ | C1 | Universal | **PASS** | Every public-facing product has a top nav; so does every admin with more than one product area. `AppLauncher` already fakes part of this with a launchpad. |
320
+ | C2 | Reusable behaviour | **PASS** | Roving focus across triggers; hover-intent open/close delays; one shared viewport so panels cross-fade instead of stacking; Escape closes and returns focus; **Tab moves out of the whole nav, not through 40 hidden links**; overflow collapse; RTL arrow inversion. |
321
+ | C3 | Not composable | **PASS** | `DropdownMenu` is the **wrong** primitive, not merely an awkward one: it is `role="menu"`, whose children must be `menuitem`s with menu keyboard semantics. A row of them announces a desktop application menubar for a set of links, and cannot host a panel of headings, links and images. |
322
+ | C4 | Single job + vocabulary | **PASS** | `items` (as `Breadcrumb`/`NavList`), `open`/`defaultOpen`/`onOpenChange` for the panel (the house overlay triad, replacing antd's `openKeys` array), `value`/`defaultValue`/`onValueChange` for the current section. |
323
+ | C5 | Token-themeable | **PASS** | `--menu-item-height`, `--menu-panel-background`, `--menu-panel-shadow`, `--menu-ink-color`. antd's `theme: light \| dark` is **not** ported — this library inverts by role scoping, as the acme showcase's navy region does. |
324
+ | C6 | Earns the contract | **PASS** | This is a component whose entire risk is ARIA and keyboard. It is exactly what C6 exists to pay for. |
325
+ | C7 | Earns its cost | **PASS** | Not small, and worth it: it is the single largest thing standing between this library and a website. |
326
+
327
+ **ALL PASS → framework component.** But see the ordering (§7): it goes last of the definites, on its
328
+ own issue, because it is the only item here that can fail on its own merits.
329
+
330
+ **The name is a real decision, and it needs the owner.** DESIGN-AUTHORITY says take antd's name, so
331
+ the component is **`Menu`**. But antd's `Menu` also covers `vertical` and `inline`, which are
332
+ `Sidebar` and `NavList` here — porting all three modes would duplicate two shipped components, and
333
+ "do not duplicate what exists" is parity ground-rule 3. **Recommendation:** ship `Menu` with
334
+ `mode?: "horizontal"` as the only member today, with the catalog entry stating plainly that
335
+ `vertical` → `NavList` and `inline` → `Sidebar`, so the union can grow later without a rename. The
336
+ alternative — a house name like `NavMenu` — is a deviation from DESIGN-AUTHORITY and must not be
337
+ taken silently.
338
+
339
+ **Reduced motion:** panel open/close is an opacity+transform on `--duration-fast`/`--ease-standard`.
340
+ Under reduced motion the panel **appears and disappears instantly**; nothing about open/closed is
341
+ carried by the animation. The indicator that slides between triggers snaps. Hover-intent delays are
342
+ _timing_, not motion, and stay — they are what stops the nav flickering under a moving pointer.
343
+
344
+ ---
345
+
346
+ ### 4.4 `Reveal` gains `on="view"` — extend, do not add
347
+
348
+ **`Reveal` already exists** (`general`, `delay: 0..6`, `asChild`) and is the official entrance
349
+ primitive. It animates **on mount**: `src/styles/motion.css` is a plain CSS `animation … both` with
350
+ `data-reveal-delay` steps. There is no viewport trigger anywhere in the library.
351
+
352
+ **A second component would be the duplication this repo keeps paying to delete.** The verdict is
353
+ therefore an extension, and the ledger is the extension's:
354
+
355
+ | # | criterion | verdict | why |
356
+ | --- | ----------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
357
+ | C1 | Universal | **PASS** | Long pages of any kind, not only marketing. |
358
+ | C2 | Reusable behaviour | **PASS** | One shared `IntersectionObserver` with a threshold and a once-latch, plus an SSR/jsdom-safe fallback that renders content **visible**. |
359
+ | C3 | Not composable | **PASS** | No primitive observes viewport entry (§1, measurement 2). |
360
+ | C4 | Single job + vocabulary | **PASS** | Adds `on?: "mount" \| "view"` (default `"mount"`, so nothing changes), plus `once?: boolean` and `amount?: "some" \| "all" \| number`. |
361
+ | C5 | Token-themeable | **PASS** | Unchanged: `--reveal-distance`, `--reveal-stagger-step`, `--duration-slow`, `--ease-emphasized`. |
362
+ | C6 | Earns the contract | **PASS** | The failure mode is content that never becomes visible. That is a WCAG failure, and it belongs behind one tested implementation. |
363
+ | C7 | Earns its cost | **PASS** | One file, one observer. |
364
+
365
+ **antd has nothing.** Nearest prior art: Motion's `useInView(ref, { root, margin, once, amount, initial })`
366
+ — **borrow `once` and `amount` verbatim**, including `amount: "some" | "all" | number`. Do **not**
367
+ add `motion`/`framer-motion` as a dependency: the library has no animation runtime today (`package.json`
368
+ has none) and this needs ~20 lines of `IntersectionObserver`.
369
+
370
+ **Reduced motion:** unchanged and non-negotiable — under `prefers-reduced-motion: reduce` the
371
+ animation is dropped and the content renders **final, fully visible, in place**. With `on="view"`
372
+ there is one extra rule: the observer must not gate _visibility_, only the animation, so a browser
373
+ with no `IntersectionObserver` (and jsdom) shows everything. Never `opacity: 0` as the resting state
374
+ in the stylesheet.
375
+
376
+ ---
377
+
378
+ ### 4.5 `Marquee`
379
+
380
+ **antd has nothing.** Verified by listing the 83 component directories of `ant-design/ant-design@master`
381
+ on 2026-09-21: no `marquee`, no `ticker`. Nearest prior art, **`react-fast-marquee`** (1.5k★):
382
+
383
+ | prior-art prop | type | default |
384
+ | ---------------------------------------------- | --------------------------------------- | -------------------------- |
385
+ | `play` · `pauseOnHover` · `pauseOnClick` | `boolean` | `true` · `false` · `false` |
386
+ | `direction` | `left \| right \| up \| down` | `left` |
387
+ | `speed` | `number` (px/s) | `50` |
388
+ | `delay` · `loop` | `number` · `number` (`0` = ∞) | `0` · `0` |
389
+ | `autoFill` | `boolean` | `false` |
390
+ | `gradient` · `gradientColor` · `gradientWidth` | `boolean` · `string` · `number\|string` | `false` · `white` · `200` |
391
+ | `onFinish` · `onCycleComplete` · `onMount` | `() => void` | – |
392
+
393
+ Its README documents **no accessibility or reduced-motion behaviour at all**, which is the argument
394
+ for owning this rather than telling consumers to install it.
395
+
396
+ #### GATE 0 ledger
397
+
398
+ | # | criterion | verdict | why |
399
+ | --- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
400
+ | C1 | Universal | **PASS** | Logo walls, announcement tickers, status strips, "now processing" rails. Weaker than `Affix`, but not marketing-only. |
401
+ | C2 | Reusable behaviour | **PASS** | Measuring content, cloning enough copies to fill the track seamlessly (`autoFill`), keeping the clones out of the accessibility tree, pausing on hover **and on focus**, and honouring an explicit play/pause control. |
402
+ | C3 | Not composable | **PASS** | Nothing loops content. A CSS-only marquee cannot know how many copies it needs, and breaks at every width. |
403
+ | C4 | Single job + vocabulary | **PASS** | `direction: "start" \| "end"` (logical, not `left`/`right`), `speed` as a token ordinal rather than raw px/s, `play`/`defaultPlay`/`onPlayChange` for the triad, `pauseOnHover`. |
404
+ | C5 | Token-themeable | **PASS** | `--marquee-gap-inline`, `--marquee-mask-width` for the edge fade (a mask, not antd-style `gradientColor`, which cannot follow a themed background). |
405
+ | C6 | Earns the contract | **PASS** | **This is the whole case.** WCAG 2.2.2 (Pause, Stop, Hide) makes a pause mechanism a conformance requirement for content that moves for more than five seconds. Every hand-rolled marquee omits it. One framework component with a built-in, labelled pause control is how the library stops shipping that defect. |
406
+ | C7 | Earns its cost | **PASS** | Tiny; CSS transform plus a measurement. |
407
+
408
+ **ALL PASS → framework component**, with one gate of its own: **it does not merge without the pause
409
+ control and the `prefers-reduced-motion` test.** If the pause control is cut, the component is a
410
+ liability and should not exist.
411
+
412
+ **Reduced motion:** under `prefers-reduced-motion: reduce` the marquee **does not animate**. The
413
+ track renders as a static, horizontally scrollable row — every item still reachable by keyboard and
414
+ by scroll, nothing hidden, no clones. The cycle duration is `--marquee-interval`, declared in
415
+ `foundation.css` beside `--activity-interval`, following the precedent recorded there (component
416
+ token names must carry a geometry/colour property word and there is none for a duration).
417
+
418
+ ---
419
+
420
+ ### 4.6 Conditional — `Image` / `Image.PreviewGroup` (lightbox)
421
+
422
+ **antd has it, and the correct name is `Image`, not `Lightbox`.** `antd@6.6.5`, fetched 2026-09-21:
423
+
424
+ | `Image` | type | default |
425
+ | ---------------------------------- | -------------------------------------- | ------- |
426
+ | `src` · `alt` · `width` · `height` | `string` · `string` · `string\|number` | – |
427
+ | `preview` | `boolean \| PreviewType` | `true` |
428
+ | `placeholder` · `fallback` | `PlaceholderType` · `string` | – |
429
+ | `onError` | `(event) => void` | – |
430
+
431
+ | `PreviewType` / `PreviewGroupType` | type | default |
432
+ | ------------------------------------------------------------ | ------------------------------------------- | ------------------ |
433
+ | `open` · `onOpenChange` | `boolean` · `(open) => void` | – |
434
+ | `movable` · `wheel` · `focusTrap` | `boolean` | `true` |
435
+ | `minScale` · `maxScale` · `scaleStep` | `number` | `1` · `50` · `0.5` |
436
+ | `mask` | `boolean \| { enabled?, blur?, closable? }` | `true` |
437
+ | `imageRender` · `actionsRender` · `closeIcon` · `cover` | render props | – |
438
+ | `getContainer` · `rootClassName` | – | – |
439
+ | `onTransform` | `{ transform, action }` | – |
440
+ | group-only: `current` · `countRender` · `onChange` · `items` | | – |
441
+
442
+ **The ledger passes on merit** — `parity-backlog.md` already found that, and I agree: C2 (zoom, pan,
443
+ wheel scaling, focus trap, group paging) and C3 (`Dialog` + `Thumbnail` gives you a big picture in a
444
+ box and nothing else) are clear PASSes. It was deferred on **C7**, and §2.2 above argues C7's stated
445
+ reason (bytes) no longer matches the build.
446
+
447
+ **Verdict: stay deferred, on the restated C7 (maintenance + contract cost), with an explicit
448
+ re-decision trigger.** Re-open it when **two or more** of the planned showcase pages need a real
449
+ viewer — a product gallery and a case-study page would do it — and decide it then with that number
450
+ in hand. Until then the existing catalog note stands: **no hand-rolled lightboxes**. If it is built,
451
+ note that `react-aria-components@1.21.1` already ships `SharedElementTransition` / `SharedElement(name)`
452
+ — the thumbnail→full-size zoom, in a dependency the library already has.
453
+
454
+ **Reduced motion:** the open transition is a scale+fade on `--duration-base`; under reduced motion
455
+ the viewer **appears instantly** at full size. Zoom and pan are user-driven and stay — they are
456
+ direct manipulation, not decorative motion.
457
+
458
+ ---
459
+
460
+ ### 4.7 Conditional — `CountUp` (animated counter)
461
+
462
+ **antd has nothing for this, and the near-miss is a trap.** `Statistic.Timer` (5.25.0+) has
463
+ `type: 'countdown' | 'countup'`, but it counts **elapsed time** against `value` as a timestamp with a
464
+ `HH:mm:ss` format — it is not a number tween. `Statistic` itself is static (`value`, `precision`,
465
+ `decimalSeparator`, `groupSeparator`, `prefix`, `suffix`, `formatter`, `loading`). Nearest prior art,
466
+ **`react-countup`** (2k★): `start=0`, `end`, `duration=2`, `decimals=0`, `separator`, `decimal='.'`,
467
+ `prefix`, `suffix`, `useEasing=true`, `easingFn`, `formattingFn`, `enableScrollSpy`, `scrollSpyDelay`,
468
+ `scrollSpyOnce`, `preserveValue=false`, `onStart`, `onEnd`, plus a `useCountUp` hook.
469
+
470
+ | # | criterion | verdict | why |
471
+ | --- | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
472
+ | C1 | Universal | **➖** | Honestly: it is a marketing flourish. An admin KPI that animates is usually a bug. antd's deliberate absence is a data point against. |
473
+ | C2 | Reusable behaviour | **PASS** | rAF tween with an easing curve, cancel-on-unmount, resume on value change, and the announcement discipline. |
474
+ | C3 | Not composable | **PASS** | Nothing tweens a number. |
475
+ | C4 | Single job + vocabulary | **PASS** | `value` / `from` / `duration` (token ordinal), `format` through `Intl.NumberFormat` — **never** `react-countup`'s `separator`/`decimal` strings, which are exactly the locale bug `NumberInput` is being fixed for (parity-backlog P0 #1). |
476
+ | C5 | Token-themeable | **PASS** | Inherits `Text`; `tabular` already exists so digits do not jitter. |
477
+ | C6 | Earns the contract | **PASS** | A hand-roll announces sixty intermediate values to a screen reader. The correct behaviour — render the **final** value in the accessibility tree and animate only the visual text — is not obvious and should be written once. |
478
+ | C7 | Earns its cost | **➖** | Tiny to build; the cost is one more public API to keep. |
479
+
480
+ **Verdict: conditional.** Two `➖`s on C1/C7 mean this is not a 7/7 PASS today. **Trigger:** build it
481
+ only if, after the showcase in §7 step 3 exists, **at least two** pages want it; otherwise a static
482
+ `Text tabular` is the right answer and this stays unbuilt. Name it `CountUp` (prior-art name; antd
483
+ has none to defer to) and place it in `general` beside `Reveal`, not in `data-display` — it is a
484
+ motion primitive, not a statistic.
485
+
486
+ **Reduced motion:** under `prefers-reduced-motion: reduce` there is no tween at all — the final value
487
+ renders immediately. Under every setting, assistive tech sees the final value from the first frame.
488
+
489
+ ---
490
+
491
+ ### 4.8 Recorded, not touched
492
+
493
+ - **`Masonry`** — ALREADY BEING PORTED by another agent; spec and GATE 0 ledger (7/7 PASS) in
494
+ `docs/roadmap/list-masonry.md` §2. **This plan touches nothing about it.** Recorded here only so
495
+ that a gallery or card-board section of a website page is built on it and not re-invented.
496
+ - **`List`** (antd `Listy`) — same file, §1. A long feed, virtualized.
497
+ - **`Watermark`, `Popconfirm`, `Notification`, `Empty`, `Result`, `Spin`, `Rate`, `Tour`** — already
498
+ ruled out (or deferred) by `docs/roadmap/parity-backlog.md`. Not re-litigated here.
499
+
500
+ ---
501
+
502
+ ## 5. Compositions — the primitives, and the missing token
503
+
504
+ Every row below is a **composition pattern**: it lives in a consumer app or a `docs/` showcase, built
505
+ from real primitives and configured by tokens. The only framework work each one generates is in the
506
+ right-hand column.
507
+
508
+ ### 5.1 Hero
509
+
510
+ **Primitives:** `Flex` · `ResponsiveGrid` + `ResponsiveGrid.Item span` · `Heading` · `Text` ·
511
+ `Button` · `Badge` (eyebrow) · `AspectRatio`/`Thumbnail` · `Card` · `Reveal` · `.ui-brand-glow`.
512
+
513
+ **Missing — and it is not a component:**
514
+
515
+ 1. **The display type ramp has tokens but no prop surface.** `--font-size-display` (54px),
516
+ `--font-size-3xl` (≈28), `--4xl` (≈42), `--5xl` exist at `foundation.css:513-521`. The only thing
517
+ in `src/` that reads any of them is `--centered-shell-landing-heading-size`. `Heading` takes
518
+ `level: 1|2|3|4` → `--heading-h1…h4`, whose **top is ≈20px**; `Text size` tops out at `2xl` (≈22px).
519
+ There is no way to render a 42px headline through the public API. Both showcases therefore wrote
520
+ their own `.tx-display` / `.fl-display` class with a raw `font-size`. **Fix: extend the existing
521
+ size vocabulary** — `Text size` gains `3xl | 4xl | 5xl`, and `Heading` gains the same `size`
522
+ override alongside `level` (level keeps owning the semantic tag; size owns the ramp). No new
523
+ token, no new component.
524
+ 2. **Band spacing** — see 5.9.
525
+ 3. **`.ui-brand-glow` is shipped and unused.** `src/styles/layout.css:125` plus `--brand-glow`,
526
+ `--brand-glow-size`, `--brand-glow-position`, `--brand-glow-color`, `--brand-glow-alpha` do exactly
527
+ what `.tx-glow-tr`, `.tx-glow-bl`, `.fl-hero-glow` and `.fl-cta-glow` hand-wrote. **Fix is
528
+ documentation plus one showcase that uses it** — the class needs a docs page, not a token.
529
+
530
+ ### 5.2 Navbar
531
+
532
+ **Primitives:** `Topbar` (an explicitly "PURE SLOT" bar with `start`/`center`/`end`) · `Logo` ·
533
+ `Button` · `Menu` (§4.3) · `Sheet` (the mobile drawer) · `Affix` (§4.1) · `Separator`.
534
+
535
+ **Missing tokens** — both showcases hand-wrote the identical glass bar
536
+ (`position: sticky; top: 0; z-index: 30; background: hsl(var(--background) / .85); backdrop-filter: blur(10px)`):
537
+
538
+ | token | file | note |
539
+ | ----------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- |
540
+ | `--topbar-background-alpha` | `src/tokens/components/shell.css` | default `100%`; the translucency of a pinned bar |
541
+ | `--topbar-backdrop-blur-size` | `src/tokens/components/shell.css` | default `0px`; precedent `--app-launcher-launchpad-backdrop-blur-size` |
542
+ | `--affix-inset-block-start` · `--affix-z-index` | `components/affix.css` · `semantic/layout.css` | z-index has no legal component property word; precedent `--overlay-z-index: 50` |
543
+
544
+ `--topbar-height`, `--topbar-inset`, `--topbar-gap`, `--topbar-gradient` already exist.
545
+
546
+ ### 5.3 Footer
547
+
548
+ **Primitives:** `ResponsiveGrid columns={12}` + `ResponsiveGrid.Item span` · `Separator` ·
549
+ `Text link` · `Logo` · `Flex`.
550
+
551
+ **Missing: nothing, but one documentation defect.** Both showcases hand-wrote
552
+ `grid-template-columns: 1.4fr 1fr 1fr 1fr` because `ResponsiveGrid columns` looks like it only does
553
+ equal columns. **`ResponsiveGrid.Item` with `span` ships today** (`responsive-grid.tsx:125-145`) and a
554
+ 12-column grid with `span={5}/{3}/{2}/{2}` is the asymmetric footer. It is invisible in the MCP
555
+ catalog — already logged as catalog drift in `parity-backlog.md`. **Fix: catalog the existing API.**
556
+
557
+ ### 5.4 Pricing table
558
+
559
+ **Primitives:** `ResponsiveGrid` · `Card accent` + `CardHeader`/`CardContent`/`CardFooter` ·
560
+ `FeatureList` (included / limited / excluded — this _is_ the tier list) · `Badge` ("most popular";
561
+ antd's `Badge.Ribbon` is a composition here by parity ground-rule 2) · `Segmented` (monthly↔yearly,
562
+ with its `count` pill) · `Button` · `Text tabular` for the price.
563
+
564
+ **Missing: nothing.** This is the strongest evidence that the doctrine is right — the single most
565
+ requested "marketing component" needs **zero** new framework surface once §5.9 lands.
566
+
567
+ ### 5.5 Feature grid · 5.6 CTA band · 5.7 Testimonials · 5.8 Logo wall
568
+
569
+ | pattern | primitives | missing |
570
+ | ------------ | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
571
+ | Feature grid | `ResponsiveGrid` · `Card` · `Avatar` square + Lucide glyph (the doctrine's "icon medallion") · `Heading` · `Text` | nothing |
572
+ | CTA band | `Card variant` + per-region role scoping · `Heading` · `Text` · `Button` · `.ui-brand-glow` | band spacing (5.9); **`--gradient-brand` / `--gradient-hero` / `--gradient-glow` exist with no call site** — give them one documented consumer, as `.ui-brand-glow` has |
573
+ | Testimonials | `Card` · `Avatar` · `Text` · `Carousel` + `CarouselDots` · `Rating` | nothing |
574
+ | Logo wall | `Flex wrap` / `ResponsiveGrid` · `Thumbnail` · optionally `Marquee` (§4.5) | nothing — third-party marks are images; de-emphasis (`grayscale`) is a brand decision for the consumer stylesheet, not a framework token |
575
+
576
+ ### 5.9 The section band and the page shell — the one real token gap
577
+
578
+ Both showcases hand-wrote a shell (`max-width: 1200px` / `1140px`, `padding-inline: 2rem`) and a band
579
+ rhythm (`padding-block: 5rem` / `5.5rem` / `6rem`).
580
+
581
+ **Why they had to:**
582
+
583
+ - The φ ladder **stops at φ²**: `--phi-unit: var(--space-4)` (16px) → `--phi-p1` ≈ 26px, `--phi-p2` ≈ 42px
584
+ (`foundation.css:694-699`). The numeric scale stops at `--space-12` (48px). A marketing band is
585
+ 64–110px. **There is no step to reach for**, so authors type `5rem`.
586
+ - `--page-measure-narrow` (42rem) and `--page-measure-medium` (48rem) exist, and `PageContainer measure`
587
+ accepts `"default" | "narrow" | "medium"`. There is no wide/marketing measure.
588
+
589
+ **Proposed tokens:**
590
+
591
+ | token | tier / file | value | why |
592
+ | ------------------------------------ | -------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------- |
593
+ | `--phi-p3` · `--phi-p4` | `src/tokens/foundation.css` | `calc(--phi-p2 × φ)` ≈ 68px · `× φ` ≈ 110px | extend the existing ladder by two steps rather than inventing a parallel marketing scale |
594
+ | `--space-band` · `--space-band-hero` | `src/tokens/semantic/layout.css` | `var(--phi-p3)` · `var(--phi-p4)` | the section rhythm, named beside `--space-section` / `--space-stack-*` |
595
+ | `--page-measure-wide` | `src/tokens/semantic/layout.css` | `72rem` (1152px outer → 1104px surface) | + `"wide"` on `PageContainer measure` |
596
+
597
+ > **A decision for the owner, not for me.** φ³ ≈ 68px and φ⁴ ≈ 110px do not equal the 80/88/96px the
598
+ > showcases used — those were eyeballed. Snapping marketing bands onto the house ladder is the
599
+ > principled answer and it will visibly change both showcases. The alternative is a marketing-only
600
+ > unit (`--phi-unit-band`), which is a second scale and should be refused unless the ladder genuinely
601
+ > cannot carry it. **Do not name these `--space-band-*` plural without checking `--band-height-*`
602
+ > first** — `band` already means "control height" in this repo (`foundation.css`, rule #24), and a
603
+ > second meaning for the same word is the kind of drift `check:prop-vocabulary` exists to stop. If
604
+ > that collision worries the reviewer, `--space-section-band` / `--space-section-hero` are the
605
+ > conflict-free spellings.
606
+
607
+ **RESOLVED in gh#831 — and the φ half of the proposal above was REFUSED, on measurement.** What
608
+ shipped:
609
+
610
+ | token | tier / file | value |
611
+ | ----------------------------------------------------------- | --------------------------------- | -------------------------------------------------- |
612
+ | `--space-20` · `--space-24` | `src/tokens/foundation.css` | 80px · 96px — Carbon `$spacing-11` / `$spacing-12` |
613
+ | `--space-section-band` · `--space-section-hero` | `src/tokens/semantic/layout.css` | `var(--space-20)` · `var(--space-24)` |
614
+ | `--page-measure-wide` | `src/tokens/semantic/layout.css` | `72rem` (1152px outer → 1104px surface) |
615
+ | `--topbar-background-alpha` · `--topbar-backdrop-blur-size` | `src/tokens/components/shell.css` | `initial` · `initial` — read by `.ui-topbar` |
616
+
617
+ `--phi-p3` / `--phi-p4` were NOT minted. Three pieces of evidence, all of them already inside this
618
+ repository, say the φ ladder is not the generator of this scale and must not become one:
619
+
620
+ 1. `docs/DESIGN-AUTHORITY.md` (accepted 2026-09-07) assigns **spacing** to **IBM Carbon**, and says
621
+ in as many words that "Carbon keeps geometry (the spacing steps, the 4px grid)".
622
+ 2. `src/tokens/__tests__/carbon-scale-alignment.test.ts` **enforces** it: every `--space-*` step
623
+ must be a Carbon step or a recorded divergence, and must sit on the 4px grid. Carbon's scale
624
+ already contains 64 / 80 / 96 / 160 — the whole marketing band range. φ³ = 67.8px and
625
+ φ⁴ = 109.7px are on neither the 4px nor the 8px grid, and at `--scaling: 0.92` they are 62.4px
626
+ and 100.9px.
627
+ 3. `src/tokens/semantic/layout.css` has said since it was written that the semantic steps read the
628
+ linear scale and **not** φ, "because mixing the two left an incoherent density rhythm". That
629
+ experiment was already run here once.
630
+
631
+ The φ ladder also has **zero** consumers in `src/` — `--phi-p1` / `--phi-p2` appear only in
632
+ `docs/`, and `docs/foundation/spacing.tsx` labels `--space-stack-lg` as "= `--phi-p1`" when it is
633
+ `var(--space-6)` = 24px against φ¹ = 25.9px. It is a description of the scale, not its generator,
634
+ so extending it would have extended a label.
635
+
636
+ Industry prior art agrees and was checked before the refusal: Tailwind v4 replaced its ladder with
637
+ `calc(var(--spacing) * N)`, purely additive; Primer (`--base-size-96/112/128`) and Polaris
638
+ (`--p-space-2400/2800/3200`) both end their scales with **+16px flat** steps, the _smallest_ ratios
639
+ anywhere in those scales; Atlassian states "every space token is a multiple of this base unit" and
640
+ caps layout spacing at 80px; Material 3 does not scale section spacing at all (a flat 24dp pane
641
+ spacer from Expanded through Extra-large); and `utopia-core` computes type with
642
+ `Math.pow(scale, step)` and space with `base * multiplier` **in the same file**. No system in the
643
+ sample generates spacing from φ. Geometric ratios are a TYPE-scale tool.
644
+
645
+ `PageContainer measure="wide"` was deliberately NOT added with the token. A marketing page is
646
+ full-bleed `<section>`s with a centred inner column; `PageContainer` owns page padding and a
647
+ header/toolbar/footer scaffold, so neither showcase can consume it — the prop would have shipped
648
+ with no call site in the two pages that are its proof, which is the tier-2 mistake `docs/TOKENS.md`
649
+ warns about. The token stands on its own as the vocabulary a composition caps its shell with, the
650
+ way `--gradient-hero` does.
651
+
652
+ ---
653
+
654
+ ## 6. The animation and motion story
655
+
656
+ ### 6.1 What exists (and is simply undocumented)
657
+
658
+ | exists | where | used by |
659
+ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
660
+ | `--duration-fast\|base\|slow` (150/250/500ms) | `foundation.css:716-718` | widely |
661
+ | `--ease-standard\|emphasized\|decelerate\|accelerate` | `foundation.css:719-722` | widely |
662
+ | `--duration-loop` (1400ms) + `--activity-interval` + `--activity-stagger-step` | `foundation.css:735-740` | `Activity` |
663
+ | `--reveal-distance` (10px) + `--reveal-stagger-step` (60ms) | `foundation.css:723-726` | `Reveal` |
664
+ | `.ui-reveal` entrance + `.ui-activity` loops, both with a reduced-motion block | `src/styles/motion.css` — the one motion file | `Reveal`, `Activity` |
665
+ | `prefers-reduced-motion` handling | **13 stylesheets** + `src/props/**` | dialog, sheet, shell, tabs, alert, card, float-button, text, navigation, data-display, layout, actions, motion |
666
+ | view-transition style shared elements | `react-aria-components@1.21.1` `SharedElementTransition` / `SharedElement` | nothing yet |
667
+
668
+ **The motion tier is in better shape than the component tier.** It has one file, one naming
669
+ convention, a documented reason for every knob, and reduced-motion coverage in thirteen stylesheets.
670
+ Nothing here needs rebuilding.
671
+
672
+ ### 6.2 What is actually missing
673
+
674
+ | missing | why it matters for a website | the fix |
675
+ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
676
+ | **a viewport trigger** | every website animates on scroll; `Reveal` only fires on mount | §4.4 — `on="view"` |
677
+ | **a scroll-position state** | sticky/shrink headers, scrollspy; the machinery is private in `PageContainer` | §4.1, §4.2 |
678
+ | **an ambient/loop surface beyond `Activity`** | a ticker or logo marquee has no home | §4.5 — `--marquee-interval` beside `--activity-interval` |
679
+ | **a documented motion page** | `--reveal-*`, `--duration-loop` and `.ui-brand-glow` are invisible to consumers; they hand-roll instead (measured: 4 hand-written radial gradients across 2 showcases) | a `docs/` motion page, not code |
680
+
681
+ **What is NOT missing:** durations, easings, a stagger model, reduced-motion plumbing, or an
682
+ animation runtime. **Do not add `framer-motion`/`motion`.** The library has no animation dependency
683
+ today and every proposal above is CSS plus one `IntersectionObserver`.
684
+
685
+ ### 6.3 The reduced-motion contract, in one table
686
+
687
+ The rule for all of it: **reduced motion snaps, it never hides.** No proposal may leave content
688
+ invisible, unreachable or unannounced when motion is off.
689
+
690
+ | proposal | normal | `prefers-reduced-motion: reduce` |
691
+ | ------------------ | --------------------------------------- | ------------------------------------------------------------------ |
692
+ | `Affix` | header condenses over `--duration-fast` | condenses **instantly**; still pinned, still condensed |
693
+ | `Anchor` | smooth scroll + sliding ink | `behavior: "auto"` jump; ink **snaps**; `aria-current` unchanged |
694
+ | `Menu` panel | fade/slide in `--duration-fast` | appears and disappears **instantly** |
695
+ | `Reveal on="view"` | fade-up from `--reveal-distance` | **no animation**; content final and fully visible, no layout shift |
696
+ | `Marquee` | continuous loop at `--marquee-interval` | **no motion**; a static, scrollable row with every item reachable |
697
+ | `CountUp` | rAF tween | final value **immediately**; a11y tree always has the final value |
698
+ | `Image` preview | scale+fade open | opens **instantly**; user-driven zoom/pan unaffected |
699
+
700
+ ---
701
+
702
+ ## 7. Ordering
703
+
704
+ Sequenced by _what unblocks the most_ first and _what can fail_ last, not by appetite.
705
+
706
+ | # | item | why here | risk |
707
+ | --- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
708
+ | 1 | **Display type prop surface** — `Text size: 3xl\|4xl\|5xl`, `Heading size` | Cheapest thing here and it unblocks **every** composition in §5. The tokens already exist; this is a union extension plus catalog + prop-registry entries. Deletes 4 hand-rolled classes across 2 showcases on day one. | low |
709
+ | 2 | **Marketing tokens** — `--phi-p3/p4`, `--space-band*`, `--page-measure-wide` + `measure="wide"` | The other half of what every composition needs, and it needs the owner's ruling on the φ steps (§5.9) — so ask early, not after five pages are built on eyeballed values. | low, one decision |
710
+ | 3 | **A marketing composition showcase** — hero, feature grid, pricing, testimonial, logo wall, CTA, footer, on 1+2 | This is the item that converts the plan into evidence. It proves 1+2, it produces the acceptance screenshots, and it is what decides §4.6 and §4.7 by measurement instead of opinion. Nothing after this should start before it exists. | low |
711
+ | 4 | **`Affix`** | Highest behavioural leverage per line. Small, antd-named, and two thirds already written privately inside `PageContainer` — extracting it fixes a duplication instead of creating one. `Anchor` is specified on top of it, so it must come first. | medium |
712
+ | 5 | **`Anchor`** | Depends on 4 (antd's own `affix` prop takes `AffixProps`). Also closes a shipped gap that is not marketing at all: `LegalDocumentShell` has no table of contents. | medium |
713
+ | 6 | **`Reveal on="view"`** | One file. Deliberately **after** the showcase so it is tuned against real pages rather than a demo — and so it is obvious whether the pages need it at all. | low |
714
+ | 7 | **`Menu mode="horizontal"`** | The biggest, the riskiest, and the only item that can fail on its merits: no Radix (ratchet), no RAC navigation-menu, so the disclosure-nav has to be assembled. Give it its own issue and its own reviewer, and start it only when 1–6 are landed, so a failure here costs nothing else. | **high** |
715
+ | 8 | **`Marquee`** — conditional on the logo wall in 3 wanting motion | Cheap, but only real if a page needs it. Ships with the pause control or not at all. | low |
716
+ | 9 | **`CountUp` / `Image` preview** — deferred, with the triggers in §4.6/§4.7 | Decide with a number from 3, not with an opinion now. | — |
717
+
718
+ **Batching.** Per the repo's standing rule, 1–3 land as one batch, and 4–7 as one PR each — these
719
+ are public-API changes, so each needs `pnpm ship:surface`, the MCP catalog entry, and the
720
+ `godxjp-ui-mcp-catalog-sync` follow-map.
721
+
722
+ ---
723
+
724
+ ## 8. What I recommend NOT doing
725
+
726
+ 1. **No `Hero`, `Navbar`, `Footer`, `PricingTable`, `Testimonials`, `LogoWall`, `FeatureGrid` or
727
+ `CTASection` in `src/components/` — under any name.** The doctrine's §3 decides four of these
728
+ explicitly, and §5 above shows the rest are `ResponsiveGrid` + `Card` + `FeatureList` + `Button`.
729
+ Also refuse the renames: `Banner`, `Section`, `Block`, `MarketingShell`, `LandingShell` are the
730
+ same refused component. `ServiceLauncherCard` is the ONE recorded exception and §3 says in as many
731
+ words that it is not precedent.
732
+ 2. **No `Parallax`.** antd has nothing. The behaviour is a scroll-linked transform, which CSS now
733
+ expresses natively behind `@supports (animation-timeline: scroll())`, so **C3 fails**. Worse, its
734
+ reduced-motion behaviour is to _not exist_ — a component whose entire value must be switched off
735
+ for the users most at risk from it (WCAG 2.3.3; vestibular triggers) is not a framework component.
736
+ A brand that wants depth gets `.ui-brand-glow` and a tinted section.
737
+ 3. **No `BorderBeam`, even though antd 6.4.0 ships one.** antd's is a decorative moving beam along a
738
+ border (`color`, `count=1`, `duration=6`, `lineWidth=1px`, `size=100`, `outset`). Parity ground-rule
739
+ 2 is explicit that a missing antd component is not automatically a framework component, and this one
740
+ owns no behaviour (C2 ❌) and is a themed CSS class in a consumer stylesheet (C3 ❌). If the owner
741
+ wants it for parity's sake, that is a deliberate override of GATE 0 and should be recorded as one.
742
+ 4. **No new animation dependency.** Not `framer-motion`, not `motion`, not `gsap`, not
743
+ `react-fast-marquee` or `react-countup` as runtime deps. They are prior art for the **API**; the
744
+ implementations here are CSS plus `IntersectionObserver`. The library ships zero animation runtime
745
+ today and should keep shipping zero.
746
+ 5. **No `@radix-ui/react-navigation-menu`.** `check:radix-surface` fails on any Radix package without
747
+ a baseline entry, and the library is migrating _off_ Radix. Borrow the pattern, not the package.
748
+ 6. **No "marketing theme" preset or second design language.** Per-region role scoping plus
749
+ `[data-tenant]` overrides already reach 100% fidelity — `acme-website.tsx` and `futurelastic-web.tsx`
750
+ are the proof. A second preset would be a fork of the theme layer.
751
+ 7. **No re-litigating `Watermark`, `Tour`, `Popconfirm`, `Empty`, `Result`, `Spin`.**
752
+ `parity-backlog.md` ruled on them. Overturning a ruling needs new evidence, and a website page is
753
+ not new evidence for a watermark.
754
+ 8. **Do not touch `Masonry` or `List`.** Another agent owns them (`docs/roadmap/list-masonry.md`).
755
+ 9. **No reading-progress bar, no scroll-driven section counter, no cookie banner, no newsletter
756
+ block.** Each is `Progress`/`Text`/`Form` plus a scroll listener the `Affix`/`Anchor` work already
757
+ makes available, and each is page furniture with a domain in it.
758
+
759
+ ---
760
+
761
+ ## 9. Definition of done (per item in §7)
762
+
763
+ **For a framework component** (4.1, 4.2, 4.3, 4.5, and 4.4's extension): source + group `index.ts`
764
+ export · `XProp` (+ `as XProps`) in the group's `*.prop.ts` **and** `src/props/registry.ts` · a token
765
+ file plus its `@import` in `src/tokens/base.css` · i18n keys in **en/vi/ja** · `@testing-library/user-event`
766
+ behaviour tests beside the component · an `mcp/src/data/components.ts` entry including the
767
+ "deliberately not ported" list · a real-screen `docs/` page with its `/isolate/**` frame · the
768
+ reduced-motion row of §6.3 asserted by a test.
769
+
770
+ **For a composition** (§5): a `docs/showcase/*.tsx` page built from real primitives, with the number
771
+ of bespoke CSS classes it needed recorded in its header comment — that count is the measurement that
772
+ says whether the token work in §7 step 2 actually worked. The target is that a third marketing brand
773
+ needs **fewer than 10** bespoke classes, against the 26 and 32 measured today.
774
+
775
+ **Gates.** `pnpm typecheck && pnpm lint && pnpm run audit && pnpm check:prop-vocabulary &&
776
+ pnpm check:mcp-sync && pnpm check:mcp-orphans && pnpm check:token-tiers && pnpm check:control-sizing &&
777
+ pnpm check:example-imports && pnpm check:doc-prop-existence`, then **only** the touched test files
778
+ (`pnpm vitest run src/components/<group>/__tests__ --maxWorkers=2`). `pnpm test` and a bare
779
+ `pnpm vitest run` are **forbidden**; `pnpm check:frame-axe` is local-only and on request.