@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.
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/marquee.d.ts +16 -0
- package/dist/components/data-display/marquee.js +155 -0
- package/dist/components/general/reveal.d.ts +23 -2
- package/dist/components/general/reveal.js +37 -7
- package/dist/components/general/typography.d.ts +4 -1
- package/dist/components/general/typography.js +14 -1
- package/dist/components/layout/affix.d.ts +86 -0
- package/dist/components/layout/affix.js +187 -0
- package/dist/components/layout/index.d.ts +4 -0
- package/dist/components/layout/index.js +4 -0
- package/dist/components/layout/legal-document-shell.js +4 -3
- package/dist/components/layout/masonry.d.ts +74 -0
- package/dist/components/layout/masonry.js +214 -0
- package/dist/components/layout/page-container.js +5 -20
- package/dist/components/navigation/anchor.d.ts +64 -0
- package/dist/components/navigation/anchor.js +284 -0
- package/dist/components/navigation/index.d.ts +4 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/navigation/mega-menu.d.ts +21 -0
- package/dist/components/navigation/mega-menu.js +526 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +517 -0
- package/dist/i18n/messages/ja.json +513 -0
- package/dist/i18n/messages/vi.json +513 -0
- package/dist/lib/hooks.d.ts +68 -0
- package/dist/lib/hooks.js +52 -0
- package/dist/lib/platform.d.ts +14 -0
- package/dist/lib/platform.js +10 -1
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +3 -2
- package/dist/props/components/data-display.prop.d.ts +95 -1
- package/dist/props/components/general.prop.d.ts +47 -3
- package/dist/props/components/layout.prop.d.ts +194 -0
- package/dist/props/components/navigation.prop.d.ts +263 -0
- package/dist/props/registry.d.ts +359 -4
- package/dist/props/registry.js +472 -3
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +39 -2
- package/dist/styles/control.css +5 -6
- package/dist/styles/data-display-layout.css +2 -1
- package/dist/styles/density.css +4 -0
- package/dist/styles/layout.css +79 -0
- package/dist/styles/motion.css +121 -1
- package/dist/styles/navigation-layout.css +397 -1
- package/dist/styles/shell-layout.css +3 -0
- package/dist/styles/text-layout.css +52 -4
- package/dist/tokens/base.css +5 -0
- package/dist/tokens/components/affix.css +7 -0
- package/dist/tokens/components/anchor.css +17 -0
- package/dist/tokens/components/control.css +3 -3
- package/dist/tokens/components/form.css +1 -1
- package/dist/tokens/components/marquee.css +7 -0
- package/dist/tokens/components/masonry.css +6 -0
- package/dist/tokens/components/mega-menu.css +62 -0
- package/dist/tokens/components/shell.css +3 -0
- package/dist/tokens/foundation.css +11 -0
- package/dist/tokens/semantic/layout.css +7 -0
- package/docs/COMPOSITION-VS-COMPONENT.md +19 -1
- package/docs/DESIGN-AUTHORITY.md +99 -18
- package/docs/FRAME-COVERAGE-REPORT.md +7 -2
- package/docs/data-display/marquee.tsx +254 -0
- package/docs/foundation/_theme-editor-scope.ts +222 -0
- package/docs/foundation/density.tsx +12 -2
- package/docs/foundation/spacing.tsx +5 -0
- package/docs/foundation/theme-editor.tsx +645 -0
- package/docs/general/activity.tsx +65 -0
- package/docs/general/reveal.tsx +290 -22
- package/docs/general/typography.tsx +91 -1
- package/docs/layout/affix.tsx +209 -0
- package/docs/layout/masonry.tsx +291 -0
- package/docs/navigation/anchor.tsx +285 -0
- package/docs/navigation/mega-menu-panel.tsx +86 -0
- package/docs/navigation/mega-menu.tsx +254 -0
- package/docs/roadmap/website-components.md +779 -0
- package/docs/showcase/acme-website.tsx +75 -39
- package/docs/showcase/futurelastic-web.tsx +91 -49
- package/docs/showcase/marketing-page.tsx +885 -0
- package/docs/showcase/table-footer-totals.tsx +12 -2
- package/docs/showcase/theme-customization.tsx +1259 -0
- package/package.json +5 -3
- package/scripts/brand-accent.generated.mjs +27 -0
- package/scripts/ui-audit.mjs +66 -0
- 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.
|