@helloimjolopez-pathway/pathway-tokens 11.1.0 → 11.3.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Design system for Ministry Brands Amplify. Tokens + components + specifications, all versioned in one place, with Storybook deployed from `main`.
4
4
 
5
- > **Looking for the tokens?** → **[TOKENS.md](TOKENS.md)** — where the CSS lives, which
5
+ > **Looking for the tokens?** → **[TOKENS.md](TOKENS.md)**, where the CSS lives, which
6
6
  > of the 284 names you may use, and what to link in what order.
7
7
 
8
8
  ## Using the tokens
@@ -16,14 +16,14 @@ one name set rather than each having their own.
16
16
 
17
17
  | | Count | Name these? |
18
18
  |---|---|---|
19
- | Colour (`themes/light.css`, `themes/midnight.css` — same names) | **186** | Yes |
19
+ | Colour (`themes/light.css`, `themes/midnight.css`, same names) | **186** | Yes |
20
20
  | Type scale (`type.css`) | 41 | Yes |
21
21
  | Spacing, radii, borders (`layout.css`) | 40 | Yes |
22
22
  | Motion (`motion.css`) | 17 | Yes |
23
23
  | Sheet and TopNav padding (`layout-responsive.css`) | 6 | Yes |
24
24
  | Breakpoints (`breakpoints.css`) | 5 | Yes |
25
25
  | **Working vocabulary** | **284** | |
26
- | Raw ramps (`primitives.css`) | 350 | **No** — but you must load it |
26
+ | Raw ramps (`primitives.css`) | 350 | **No**, but you must load it |
27
27
  | Component metrics (`layout-contextual.css`) | 26 | This repo's components |
28
28
 
29
29
  **The CSS lives in [`src/tokens/`](src/tokens/), and that folder has its own README**
@@ -40,8 +40,7 @@ npm install @helloimjolopez-pathway/pathway-tokens
40
40
  NuGet: `Pathway.DesignTokens`.
41
41
 
42
42
  ```js
43
- // Every stylesheet is a named subpath export. primitives.css MUST come first —
44
- // the themes resolve through it, so without it all colour resolves to nothing
43
+ // Every stylesheet is a named subpath export. primitives.css MUST come first, // the themes resolve through it, so without it all colour resolves to nothing
45
44
  // and the page renders unstyled with no console error.
46
45
  import "@helloimjolopez-pathway/pathway-tokens/primitives.css";
47
46
  import "@helloimjolopez-pathway/pathway-tokens/themes/light.css";
@@ -67,14 +66,14 @@ as too granular to adopt. Retired 2026-09-03 and not coming back.
67
66
 
68
67
  ```html
69
68
  <!-- primitives first: the themes reference these via var(), so this must load -->
70
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/primitives.css" />
71
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/themes/light.css" />
72
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/themes/midnight.css" />
73
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/type.css" />
74
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/layout.css" />
75
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/layout-responsive.css" />
76
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/motion.css" />
77
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/breakpoints.css" />
69
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/primitives.css" />
70
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/themes/light.css" />
71
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/themes/midnight.css" />
72
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/type.css" />
73
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/layout.css" />
74
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/layout-responsive.css" />
75
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/motion.css" />
76
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@helloimjolopez-pathway/pathway-tokens/dist/breakpoints.css" />
78
77
  ```
79
78
 
80
79
  `@main` tracks the latest. Pin a tag instead if you need a fixed version.
@@ -92,8 +91,7 @@ as too granular to adopt. Retired 2026-09-03 and not coming back.
92
91
  ```
93
92
 
94
93
  One name carries both modes. Set `data-theme="midnight"` on `<html>` to flip the page,
95
- or on any element to flip just that subtree. Never write a mode into a property name —
96
- `--semantic-color-light-mode-*` resolves to nothing.
94
+ or on any element to flip just that subtree. Never write a mode into a property name, `--semantic-color-light-mode-*` resolves to nothing.
97
95
 
98
96
  ### Can I use a primitive?
99
97
 
@@ -129,7 +127,7 @@ pathway-ds/
129
127
  ├── scripts/ Token sync/audit/fix + component build scripts
130
128
  ```
131
129
 
132
- ## Governance — where things live
130
+ ## Governance: where things live
133
131
 
134
132
  | To change… | Where |
135
133
  |---|---|
@@ -146,15 +144,15 @@ pathway-ds/
146
144
 
147
145
  Three workflows in `.github/workflows/`:
148
146
 
149
- 1. **`sync-tokens.yml`** — fires when `tokens/figma-export/**` changes. Normalises the Figma export into the W3C DTCG JSON and commits `tokens/pathway-design-tokens.json`.
150
- 2. **`sync-component.yml`** — fires when `components/sidenav/sidenav.html` changes. Regenerates the `sidenav-figmamake.html` variant and commits it.
151
- 3. **`deploy-pages.yml`** — fires on every push to `main`. Builds Style Dictionary and Storybook fresh and deploys a clean `_site` artifact through the official Pages actions. Nothing is committed back to `main`. (`deploy-storybook.yml` is the retired predecessor, kept manual-only; it committed 14MB of build output per push.)
147
+ 1. **`sync-tokens.yml`**, fires when `tokens/figma-export/**` changes. Normalises the Figma export into the W3C DTCG JSON and commits `tokens/pathway-design-tokens.json`.
148
+ 2. **`sync-component.yml`**, fires when `components/sidenav/sidenav.html` changes. Regenerates the `sidenav-figmamake.html` variant and commits it.
149
+ 3. **`deploy-pages.yml`**, fires on every push to `main`. Builds Style Dictionary and Storybook fresh and deploys a clean `_site` artifact through the official Pages actions. Nothing is committed back to `main`. (`deploy-storybook.yml` is the retired predecessor, kept manual-only; it committed 14MB of build output per push.)
152
150
 
153
151
  Source of truth is always the repo on `main`; Storybook is a downstream artifact.
154
152
 
155
153
  ## Typography (brand fonts)
156
154
 
157
- Pathway uses **Red Hat Text** (all UI text) and **Red Hat Display** (headings H1–H3 only). Both are open-source Google Fonts.
155
+ Pathway uses **Red Hat Text** (all UI text) and **Red Hat Display** (headings H1H3 only). Both are open-source Google Fonts.
158
156
 
159
157
  **Load via CDN (web/prototypes):**
160
158
 
@@ -170,6 +168,176 @@ Pathway uses **Red Hat Text** (all UI text) and **Red Hat Display** (headings H1
170
168
 
171
169
  Licensed under the [SIL Open Font License](https://scripts.sil.org/OFL). Free for any use.
172
170
 
171
+ ## The components
172
+
173
+ Everything in the library, with the four things you need for each: what it is
174
+ specified as, what implements it, where to see it running, and which Figma node
175
+ it was measured from. This table is generated from
176
+ [`components/manifest.json`](components/manifest.json) by
177
+ `npm run build-component-index`, so it cannot drift from the registry.
178
+
179
+ <!-- BEGIN component-index (generated by scripts/build-component-index.js) -->
180
+
181
+ **19 components with their own page, 2 building blocks that only exist nested.**
182
+
183
+ | Component | Spec | Module | Storybook | Figma node | Code Connect files |
184
+ |---|---|---|---|---|---|
185
+ | **SideNav** | [spec](components/sidenav/sidenav-spec.md) | [`sidenav.jsx`](components/sidenav/sidenav.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-sidenav--docs) | `40004059:1375` | 9 |
186
+ | **Scrollable** | [spec](components/scrollbar/scrollbar-spec.md) | [`scrollbar.jsx`](components/scrollbar/scrollbar.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-scrollbar--docs) | repo only, not in Figma | 0 |
187
+ | **Checkbox** | [spec](components/checkbox/checkbox-spec.md) | [`checkbox.jsx`](components/checkbox/checkbox.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-checkbox--docs) | `40002324:54532` | 1 |
188
+ | **OrgSwitcher** | [spec](components/org-switcher/org-switcher-spec.md) | [`org-switcher.jsx`](components/org-switcher/org-switcher.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-orgswitcher--docs) | `40006819:14583` | 2 |
189
+ | **TopNav.Global** | [spec](components/top-nav/top-nav-spec.md) | [`top-nav.jsx`](components/top-nav/top-nav.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-topnav--docs) | `40007067:6508` | 4 |
190
+ | **Spinner** | [spec](components/spinner/spinner-spec.md) | [`spinner.jsx`](components/spinner/spinner.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-spinner--docs) | `40006622:50003` | 0 |
191
+ | **Badge** | [spec](components/badge/badge-spec.md) | [`badge.jsx`](components/badge/badge.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-badge--docs) | `40017025:86804` | 0 |
192
+ | **Button** | [spec](components/button/button-spec.md) | [`button.jsx`](components/button/button.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-button--docs) | `40003293:93741` | 3 |
193
+ | **Search** | [spec](components/search/search-spec.md) | [`search.jsx`](components/search/search.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-global-search-top-nav--docs) | `40007095:4048` | 1 |
194
+ | **NavShell** | [spec](components/nav-shell/nav-shell-spec.md) | composed, see its stories | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-navshell--docs) | 3 sets on page `Navigation: Top Nav, Navigation: Side Nav, Page Template` | 0 |
195
+ | **Widget** | [spec](components/widget/widget-spec.md) | [`widget.jsx`](components/widget/widget.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-widget--docs) | `40009622:39702` | 2 |
196
+ | **KPI Tile** | [spec](components/kpi-tile/kpi-tile-spec.md) | [`kpi-tile.jsx`](components/kpi-tile/kpi-tile.jsx)<br>[`kpi-number-trend.jsx`](components/kpi-tile/kpi-number-trend.jsx)<br>[`mini-chart.jsx`](components/kpi-tile/mini-chart.jsx)<br>[`bar-chart.jsx`](components/kpi-tile/bar-chart.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-kpi-tile--docs) | 7 sets on page `40009862:10309` | 9 |
197
+ | **Dashboard** | [spec](components/dashboard/dashboard-spec.md) | [`dashboard.jsx`](components/dashboard/dashboard.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-dashboard--docs) | no Figma component | 0 |
198
+ | **Financial KPIs** | none | [`financial-kpis.jsx`](components/financial-kpis/financial-kpis.jsx) | none | no Figma component | 0 |
199
+ | **Icon** | [spec](components/icon/icon-spec.md) | [`icon.jsx`](components/icon/icon.jsx)<br>[`action-icon.jsx`](components/icon/action-icon.jsx)<br>[`display-icon.jsx`](components/icon/display-icon.jsx)<br>[`featured-icon.jsx`](components/icon/featured-icon.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-action-icon--docs) | `48:1153` | 4 |
200
+ | **Dropdown** | [spec](components/dropdown/dropdown-spec.md) | [`dropdown.jsx`](components/dropdown/dropdown.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-dropdown--docs) | `40005568:4207` | 3 |
201
+ | **Mini Chart** | [spec](components/kpi-tile/mini-chart-spec.md) | [`mini-chart.jsx`](components/kpi-tile/mini-chart.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-mini-chart--docs) | `40009415:27919` | 1 |
202
+ | **Bar Chart** | [spec](components/kpi-tile/bar-chart-spec.md) | [`bar-chart.jsx`](components/kpi-tile/bar-chart.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-bar-chart--docs) | `40009417:13029` | 3 |
203
+ | **Page Template** | [spec](components/page-template/page-template-spec.md) | [`page-template.jsx`](components/page-template/page-template.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-page-template--docs) | `40009709:26016` | 0 |
204
+
205
+ Building blocks. Each is mapped for Code Connect and specced, but has no page
206
+ of its own because it only ever appears as a nested instance inside the
207
+ component above it.
208
+
209
+ | Component | Spec | Module | Storybook | Figma node | Code Connect files |
210
+ |---|---|---|---|---|---|
211
+ | **ModuleIcon** | [spec](components/module-icon/module-icon-spec.md) | [`module-icon.jsx`](components/module-icon/module-icon.jsx) | nested only | `40006876:42134` | 1 |
212
+ | **TopNav.Actions** | [spec](components/top-nav-actions/top-nav-actions-spec.md) | [`top-nav.jsx`](components/top-nav/top-nav.jsx) | [open](https://potential-bassoon-voz982n.pages.github.io/storybook/?path=/docs/library-topnav--docs) | `40007082:7313` | 1 |
213
+
214
+ <!-- END component-index -->
215
+
216
+ ### What is checked, and what is not
217
+
218
+ Two suites. `npm run build-dist` runs the token-integrity checks, and
219
+ `npm run check-tokens` runs the documentation and story checks, so both fail a
220
+ build rather than relying on someone noticing.
221
+
222
+ In `build-dist`:
223
+
224
+ | Check | What it catches |
225
+ |---|---|
226
+ | `check-demo-tokens` | a document naming a token that does not exist |
227
+ | `check-token-refs` | a reference to the retired `--semantic-color-light-mode-*` form |
228
+ | `check-token-names` | a name that is not kebab-case lowercase |
229
+ | `check-token-object-refs` | a `T`/`L` object key referenced but never defined |
230
+ | `check-ladder-order` | a ladder whose names disagree with its resolved luminance |
231
+ | `check-state-distinctness` | an interaction state indistinguishable from another state in its group |
232
+ | `check-variant-distinctness` | a component prop that has stopped changing anything |
233
+ | `check-fill-foreground-pairs` | a fill with no paired `On Subtle` / `On Strong` foreground, or the reverse |
234
+ | `check-motion-coverage` | a state change with no transition, or a partial one |
235
+ | `fix-stale-doc-hex --check` | a hand-copied hex in a doc that no longer matches its token |
236
+ | `check-story-yield` | a token docs page rendering less content than its floor |
237
+
238
+ In `check-tokens`, additionally:
239
+
240
+ | Check | What it catches |
241
+ |---|---|
242
+ | `build-component-index --check` | the component table above out of step with the manifest, and a Storybook link in it that is not in the build |
243
+ | `check-spec-stories` | a spec whose Storybook section names a story that does not exist, or omits one that does |
244
+ | `check-demo-links` | a standalone demo linking a stylesheet or script that is not served where the page is served. All twelve demos linked `../../src/tokens/*.css`, which resolves in the repo and NOWHERE ELSE: the built Storybook mounts `components/` only, and Pages copies `components/` while putting the token contract at `/tokens/`. Verified against the live site on 2026-10-08, `/src/tokens/primitives.css` was 404. Every `StandaloneDemo` story and every published demo URL had been loading nine 404s and rendering on fallback colours rather than Pathway's. `pathway-sidenav.html` was worse: it loaded `../../dist/pathway-sidenav.js`, which is gitignored and never published, so that page had no script at all and rendered nothing. |
245
+ | `check-component-tables` | a component that keeps its measurements as data naming a key its lookup object does not define. `KPI_TILE_SPECS` named `gapChart03`, `L` had no such key, so the value was undefined, React dropped the style and CSS fell back to no gap. Nothing threw and no story was blank. |
246
+
247
+ And the ones that need a built Storybook, so they are their own command,
248
+ `npm run verify-storybook`:
249
+
250
+ | Check | What it catches |
251
+ |---|---|
252
+ | `check-syntax` | a component or story file that does not parse. Redundant with the build and worth it anyway: it parses all 54 files in about a second, where the build takes three minutes and reports the same error as a WARN line far above the failure |
253
+ | `check-stories-render` | a story that renders NOTHING while the build stays green |
254
+ | `check-mdx-refs` | a docs page referencing a story that does not exist, which renders an empty block, and a `<Controls>` pointed at a story with no argTypes, which renders an empty table under a heading promising controls |
255
+ | `check-accessible-names` | a button with no accessible name, resolved the way a browser does: `aria-label`, `aria-labelledby`, text content with `aria-hidden` subtrees removed, then `title` |
256
+
257
+ **That last one matters more than it sounds.** A module-level error in a CSF
258
+ file blanks every story in that file and webpack compiles it happily. It has
259
+ happened three times here: a leftover `storyName` assignment after a rename
260
+ blanked all nine Global Search stories, a dangling `BRAND_AXIS` reference
261
+ blanked more, and `useState` used without being imported blanked the Widget
262
+ Glance configurations story. The check loads all 107 stories in headless Chrome
263
+ and fails on an empty root. It was verified by breaking a story on purpose: the
264
+ build reported no errors and the check reported the blank page.
265
+
266
+ **`check-accessible-names` found 101 on its first run,** all of them Buttons.
267
+ Every labelled Pathway Button announced as just "button": `aria-label` was set
268
+ only when `showText` was false, on the rule that a button showing its text does
269
+ not need a label. True of an ordinary button, false here, because the text sat
270
+ inside `pw-button__container`, which carried `aria-hidden="true"`, so the name
271
+ computation skipped it. The only named case was the icon-only one, which is
272
+ backwards. Fixed by removing that `aria-hidden` rather than adding a label, so
273
+ the name comes from the visible text and cannot drift from it.
274
+
275
+ **Standalone demos are generated now, for the three newest components.**
276
+ `scripts/build-demos.js` bundles the real module with esbuild and writes the
277
+ page around it, so Widget, KPI Tile and Dashboard have a hands-on resize demo
278
+ that cannot fall behind the component. The twelve older demos are hand-written
279
+ and each says, at the top of its script block, "Mirrors `<name>.jsx` exactly.
280
+ Keep the two in step": a second implementation of a shipped component, kept
281
+ correct by memory. Converting them is a separate job. Their token links were
282
+ repointed at `components/_demo/tokens/` so they at least render.
283
+
284
+ **What is NOT checked, and why it matters.**
285
+
286
+ - **Code Connect verifies nothing.** It publishes a code snippet to a Figma
287
+ component. It never compares a token value, a measurement or a layout, and it
288
+ flows repo to Figma only. A component can be fully mapped and completely
289
+ wrong.
290
+ - **The token checks confirm that a name RESOLVES, not that it is the right
291
+ name.** `check-fill-foreground-pairs` proves every fill HAS a paired
292
+ foreground; nothing measures the pair a component actually uses against a
293
+ contrast ratio per mode. Button's Primary and Negative labels sat on the mono
294
+ anchor at 3.04:1 and 2.85:1 in Midnight while every check passed.
295
+ - **Nothing can verify a Figma node id from here.** Only Figma knows whether an
296
+ id resolves and what kind of thing it is, and the MCP is interactively
297
+ authenticated, so this needs an agent session.
298
+ `npm run check-figma-nodes` does the half that can be automated: it lists
299
+ every id the repo cites, where each is cited, and which are cited as a set.
300
+
301
+ So drift between Figma and this repo is the normal state, not the exception,
302
+ and only measurement finds it. A sweep on 2026-10-07 found four defects of that
303
+ class that had each survived months: two cited ids that do not exist in the
304
+ file at all, Search's node pointing at a 2594x1680 layout frame rather than a
305
+ component, and NavShell's pointing at a variant rather than its set.
306
+
307
+ A second sweep on 2026-10-12 found three more. NavShell cited `ScreenTemplate`
308
+ as though it owned it: there is no NavShell set in the file, so the manifest now
309
+ names the three sets it composes and says so. `icon-spec.md` held the whole
310
+ 14-section spec twice, 645 lines, the second copy still describing the retired
311
+ single `Icon` Storybook page. The Page Template page's second
312
+ `Page Template` frame is the Midnight Mode showcase: instances of the one
313
+ `ScreenTemplate` set under a `Semantic: Color` override to `Amplify Dark`, one
314
+ of them also overridden to `Tablet 798pt`.
315
+
316
+ That last one is in this list as a defect of MINE, not of the file. On
317
+ 2026-10-12 the manifest and a commit message both called those four nodes
318
+ duplicate component sets. They are one set and two instances of it. The error
319
+ came from comparing bound fill variables, which are identical across modes
320
+ because a variable is the same variable in every mode, and never reading
321
+ `explicitVariableModes`, which is the only thing that marks a mode showcase.
322
+ When two nodes look identical in Figma, read the mode override before
323
+ concluding anything.
324
+
325
+ ### Walking through one
326
+
327
+ Take Widget as the shape of it. Open [`components/widget/widget-spec.md`](components/widget/widget-spec.md)
328
+ for the anatomy, the token table, the size ladder and the grid; open the
329
+ Storybook page for the same thing running, with a Playground, a size ladder at
330
+ the exact Figma boxes, and two token listings that resolve against the live
331
+ document; open [`components/widget/widget.jsx`](components/widget/widget.jsx)
332
+ for the implementation, where every measurement carries the Figma node it came
333
+ from. The Code Connect file next to it is what makes the Figma component offer
334
+ this code, and it publishes repo to Figma only.
335
+
336
+ Widget, KPI Tile and Dashboard are three components, not three views of one.
337
+ A Widget is a container that does not know what it holds. A KPI Tile is a card
338
+ in its own right. Both nest the same building block, `KpiNumberAndTrend`, which
339
+ has no card of its own and gets one from whatever nests it.
340
+
173
341
  ## Component-to-token map
174
342
 
175
343
  `tokens/component-token-map.json` maps every component element to its CSS variable. Use this file to answer "which token does X use?" without reading specs.
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Scrollable — Pathway Design System
2
+ * Scrollable, Pathway Design System
3
3
  *
4
4
  * A reusable overlay-scrollbar wrapper. Hides the native OS scrollbar entirely and renders
5
5
  * one custom liquid-glass thumb so scrolling looks and behaves IDENTICALLY on macOS, Windows,
6
- * iOS, and Android (native scrollbars can't be made consistent — this sidesteps them).
6
+ * iOS, and Android (native scrollbars can't be made consistent: this sidesteps them).
7
7
  *
8
8
  * Behaviour (see components/scrollbar/scrollbar-spec.md):
9
9
  * - Thumb height is PROPORTIONAL to content: (viewport / total) × track, floored at 28px.
10
10
  * - Reveal: appears while scrolling, fades ~900ms after; also appears when the MOUSE is near
11
11
  * the right-edge bar (so it can be grabbed). It does NOT appear from hovering the panel.
12
12
  * - Drag: MOUSE only, via a wider invisible grab strip (the 6px thumb alone is hard to hit).
13
- * - Touch: passive — the bar just shows position; you scroll by swiping content. The grab
13
+ * - Touch: passive: the bar just shows position; you scroll by swiping content. The grab
14
14
  * strip is disabled on touch devices via a `(hover: none)` rule.
15
15
  * - Keyboard / a11y: the content scrolls natively (arrows, PageUp/Down, Home/End, Space); the
16
16
  * thumb is decorative (`aria-hidden`) and never the only scroll mechanism.
@@ -20,16 +20,17 @@
20
20
  */
21
21
 
22
22
  import React, { useRef, useState, useEffect, useCallback } from "react";
23
+ import { useReducedMotion } from "../_shared/use-reduced-motion.jsx";
23
24
 
24
25
  // ─── TOKENS ──────────────────────────────────────────────────────────────────
25
- // Every colour + unit resolves through a SEMANTIC Pathway token — no primitives,
26
+ // Every colour + unit resolves through a SEMANTIC Pathway token: no primitives,
26
27
  // no hardcoded hex/px. Motion uses the --motion-* tokens (durations/easings from design-system-spec §2).
27
28
  export const SCROLL = {
28
- thumbWidth: "var(--semantic-layout-units-padding-xtight)", // 6px — visible thumb thickness
29
+ thumbWidth: "var(--semantic-layout-units-padding-xtight)", // 6px: visible thumb thickness
29
30
  thumbRadius: "var(--semantic-layout-units-cornerradius-full)", // fully-rounded pill
30
- thumbMin: 28, // px — min thumb length (grab-target floor); JS layout math
31
- gutter: "var(--semantic-layout-units-padding-xxxtight)", // 2px — inset from the right edge
32
- grabZone: 16, // px — invisible mouse grab strip (wider than the 6px thumb so it's catchable)
31
+ thumbMin: 28, // px, min thumb length (grab-target floor); JS layout math
32
+ gutter: "var(--semantic-layout-units-padding-xxxtight)", // 2px, inset from the right edge
33
+ grabZone: 16, // px, invisible mouse grab strip (wider than the 6px thumb so it's catchable)
33
34
  // THE LIGHT RUNGS ARE A DELIBERATE CHOICE BY THE DESIGN OWNER. Do not raise
34
35
  // them again without asking Jo first.
35
36
  //
@@ -46,7 +47,7 @@ export const SCROLL = {
46
47
  // Scrim/Faint and Scrim/Light resolve to Cool Neutral 800, so the thumb was
47
48
  // drawing a COOL grey over warm surfaces and reading as a cold bar against
48
49
  // them. Reported 2026-10-02. The comment below has always said "light warm
49
- // overlay" — the value simply did not match it.
50
+ // overlay": the value simply did not match it.
50
51
  //
51
52
  // Scrim is also the wrong family: it is the modal backdrop, where a cool
52
53
  // near-black is right. The thumb is an overlay on a surface, and under the
@@ -61,7 +62,7 @@ export const SCROLL = {
61
62
  thumbEdge: "inset 0 0 0 0.5px color-mix(in srgb, var(--semantic-color-fill-static-neutral-faint) 35%, transparent)",
62
63
  // Fade OUT gracefully, but appear INSTANTLY. Animating opacity on the way in
63
64
  // multiplied the thumb's own alpha by the in-flight opacity, so during the
64
- // 200ms glide-in it rendered as low as 1.18:1 — below the floor at the exact
65
+ // 200ms glide-in it rendered as low as 1.18:1, below the floor at the exact
65
66
  // moment the user is looking for it. Only the fade-out is animated now.
66
67
  // Asymmetric fade: snappy decelerate glide-IN, graceful fade-OUT. Each entry
67
68
  // names every property it is responsible for, because the element swaps
@@ -94,12 +95,13 @@ if (typeof document !== "undefined" && !document.getElementById("pds-scrollable-
94
95
  "--pw-scrollbar-thumb-rest:var(--primitive-color-warm-neutral-100-16);" +
95
96
  "--pw-scrollbar-thumb-hover:var(--primitive-color-warm-neutral-100-24)}" +
96
97
  ".pds-scrollable__view{scrollbar-width:none;-ms-overflow-style:none}" +
97
- ".pds-scrollable__view::-webkit-scrollbar{width:0;height:0;display:none}" +
98
+ ".pds-scrollable__view:-webkit-scrollbar{width:0;height:0;display:none}" +
98
99
  "@media (hover: none){.pds-scrollable__grab{pointer-events:none!important}}";
99
100
  document.head.appendChild(s);
100
101
  }
101
102
 
102
103
  export function Scrollable({ children, className = "", style = {}, viewClassName = "", viewStyle = {} }) {
104
+ const noMotion = useReducedMotion();
103
105
  const viewRef = useRef(null);
104
106
  const dragRef = useRef(null);
105
107
  const idleRef = useRef(null);
@@ -140,7 +142,7 @@ export function Scrollable({ children, className = "", style = {}, viewClassName
140
142
  const onScroll = () => { recompute(); wake(); };
141
143
 
142
144
  // Edge-proximity reveal (mouse): show the bar when the cursor is within grabZone of the right
143
- // edge, so it can be grabbed — but NOT from hovering the rest of the panel.
145
+ // edge, so it can be grabbed: but NOT from hovering the rest of the panel.
144
146
  const onMouseMove = (e) => {
145
147
  const rect = e.currentTarget.getBoundingClientRect();
146
148
  if (rect.right - e.clientX <= SCROLL.grabZone) {
@@ -194,7 +196,7 @@ export function Scrollable({ children, className = "", style = {}, viewClassName
194
196
 
195
197
  {thumb.show && (
196
198
  <>
197
- {/* Visual thumb — decorative, never intercepts pointer events. */}
199
+ {/* Visual thumb, decorative, never intercepts pointer events. */}
198
200
  <div
199
201
  aria-hidden="true"
200
202
  style={{
@@ -203,11 +205,11 @@ export function Scrollable({ children, className = "", style = {}, viewClassName
203
205
  background: hot ? SCROLL.thumbHover : SCROLL.thumbRest,
204
206
  backdropFilter: SCROLL.thumbBlur, WebkitBackdropFilter: SCROLL.thumbBlur,
205
207
  boxShadow: SCROLL.thumbEdge,
206
- opacity: active ? 1 : 0, transition: active ? SCROLL.fadeIn : SCROLL.fadeOut,
208
+ opacity: active ? 1 : 0, transition: noMotion ? "none" : (active ? SCROLL.fadeIn : SCROLL.fadeOut),
207
209
  pointerEvents: "none", zIndex: 5,
208
210
  }}
209
211
  />
210
- {/* Invisible mouse grab strip — wider than the thumb so it's catchable; disabled on
212
+ {/* Invisible mouse grab strip, wider than the thumb so it's catchable; disabled on
211
213
  touch via the (hover:none) rule so finger-swipes scroll the content beneath. */}
212
214
  <div
213
215
  aria-hidden="true"