@helloimjolopez-pathway/pathway-tokens 5.4.0 → 7.0.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,43 +2,105 @@
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
6
+ > of the 267 names you may use, and what to link in what order.
7
+
5
8
  ## Using the tokens
6
9
 
7
- The Pathway token library is published to npm and updated automatically on every token sync.
10
+ **The colour contract is 169 names. Your full working vocabulary is 267.**
11
+
12
+ That is the number worth knowing, because adding up every declaration in the token
13
+ folder gives 966 and the retired `tokens.css` used to emit 2,338. Neither is the
14
+ contract. The difference is that primitives ship but are not named, and the modes share
15
+ one name set rather than each having their own.
16
+
17
+ | | Count | Name these? |
18
+ |---|---|---|
19
+ | Colour (`themes/light.css`, `themes/midnight.css` — same names) | **169** | Yes |
20
+ | Type scale (`type.css`) | 41 | Yes |
21
+ | Spacing, radii, borders (`layout.css`) | 40 | Yes |
22
+ | Motion (`motion.css`) | 17 | Yes |
23
+ | Sheet and TopNav padding (`layout-responsive.css`) | 6 | Yes |
24
+ | Breakpoints (`breakpoints.css`) | 5 | Yes |
25
+ | **Working vocabulary** | **267** | |
26
+ | Raw ramps (`primitives.css`) | 350 | **No** — but you must load it |
27
+ | Component metrics (`layout-contextual.css`) | 26 | This repo's components |
28
+
29
+ **The CSS lives in [`src/tokens/`](src/tokens/), and that folder has its own README**
30
+ covering load order, theming, the primitives question and the traps. Read it before
31
+ wiring anything up. The published package carries the same guidance in `dist/README.md`,
32
+ plus `dist/contract.json` listing every consumable name if you want to lint against it.
33
+
34
+ ### Install
8
35
 
9
36
  ```bash
10
37
  npm install @helloimjolopez-pathway/pathway-tokens
11
38
  ```
12
39
 
13
- ```js
14
- // CSS custom properties — import once in your app entry point
15
- import "@helloimjolopez-pathway/pathway-tokens/css";
40
+ NuGet: `Pathway.DesignTokens`.
16
41
 
17
- // JS token object
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
45
+ // and the page renders unstyled with no console error.
46
+ import "@helloimjolopez-pathway/pathway-tokens/primitives.css";
47
+ import "@helloimjolopez-pathway/pathway-tokens/themes/light.css";
48
+ import "@helloimjolopez-pathway/pathway-tokens/themes/midnight.css";
49
+ import "@helloimjolopez-pathway/pathway-tokens/type.css";
50
+ import "@helloimjolopez-pathway/pathway-tokens/layout.css";
51
+ import "@helloimjolopez-pathway/pathway-tokens/layout-responsive.css";
52
+ import "@helloimjolopez-pathway/pathway-tokens/motion.css";
53
+ import "@helloimjolopez-pathway/pathway-tokens/breakpoints.css";
54
+
55
+ // JS token object, and the raw DTCG JSON for tooling
18
56
  import tokens from "@helloimjolopez-pathway/pathway-tokens";
19
-
20
- // Raw DTCG JSON (for tooling)
21
57
  import tokenJson from "@helloimjolopez-pathway/pathway-tokens/json";
58
+ import contract from "@helloimjolopez-pathway/pathway-tokens/contract.json";
22
59
  ```
23
60
 
24
- Without a bundler — link the CSS directly:
61
+ There is deliberately no single `/css` entry point. One combined file was what
62
+ `tokens.css` used to be: it emitted every token times every mode with the mode baked
63
+ into the property name, 2,338 custom properties, and it is the reason this system read
64
+ as too granular to adopt. Retired 2026-09-03 and not coming back.
65
+
66
+ ### Without a bundler
25
67
 
26
68
  ```html
27
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/tokens.css" />
69
+ <!-- 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" />
28
78
  ```
29
79
 
30
- Once loaded, use the custom properties anywhere:
80
+ `@main` tracks the latest. Pin a tag instead if you need a fixed version.
81
+
82
+ ### Using a token
31
83
 
32
84
  ```css
33
- .my-component {
34
- background: var(--semantic-color-light-mode-fill-static-brand-base);
35
- color: var(--semantic-color-light-mode-text-static-primary-inverse);
85
+ .card {
86
+ background: var(--semantic-color-fill-surface-sheet);
87
+ color: var(--semantic-color-foreground-static-neutral-strong);
88
+ border: var(--semantic-layout-units-borderwidth-base) solid
89
+ var(--semantic-color-stroke-static-neutral-base);
90
+ border-radius: var(--semantic-layout-units-cornerradius-medium);
36
91
  }
37
92
  ```
38
93
 
39
- Full token reference: [Storybook → Tokens](https://helloimjolopez-collab.github.io/pathway-ds/storybook/?path=/docs/tokens-primitives-color--docs) · [Token architecture](tokens/README.md) · [Agent brief](tokens/agent-brief.md)
94
+ 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.
97
+
98
+ ### Can I use a primitive?
40
99
 
41
- ---
100
+ Yes, and nothing stops you. But `Primitive: Color` has exactly one mode, so a primitive
101
+ holds one value forever and will **not** respond to `data-theme`. Reach for one only
102
+ when you specifically want a fixed value. Full reasoning in
103
+ [`src/tokens/README.md`](src/tokens/README.md).
42
104
 
43
105
  ## Layout
44
106
 
@@ -57,7 +119,7 @@ pathway-ds/
57
119
  │ ├── figma-export/ Raw Figma export (sync target)
58
120
  │ └── pathway-design-tokens.json W3C DTCG format
59
121
  ├── src/
60
- │ ├── tokens/ Built output (tokens.css + tokens.js)
122
+ │ ├── tokens/ Built output (the 8-file contract + tokens.js)
61
123
  │ └── stories/ Storybook sources
62
124
  │ ├── Primitives/ Token primitives (Color, Typography, Units)
63
125
  │ ├── Semantics/ Semantic tokens (light/dark, layout, typography)
@@ -65,7 +127,6 @@ pathway-ds/
65
127
  ├── .storybook/
66
128
  ├── .github/workflows/ Token sync · component sync · Storybook deploy
67
129
  ├── scripts/ Token sync/audit/fix + component build scripts
68
- └── storybook/ Built Storybook output (deployed by CI)
69
130
  ```
70
131
 
71
132
  ## Governance — where things live
@@ -74,7 +135,7 @@ pathway-ds/
74
135
  |---|---|
75
136
  | Published token package | [`@helloimjolopez-pathway/pathway-tokens`](https://www.npmjs.com/package/@helloimjolopez-pathway/pathway-tokens) |
76
137
  | Token values | [`tokens/pathway-design-tokens.json`](tokens/pathway-design-tokens.json) (source-of-truth, synced from Figma) |
77
- | Built CSS consumed by Storybook | [`src/tokens/tokens.css`](src/tokens/tokens.css) (auto-regenerated by Style Dictionary) |
138
+ | Built CSS consumed by Storybook | the eight files in [`src/tokens/`](src/tokens/) (auto-regenerated by Style Dictionary) |
78
139
  | Component geometry, tokens, accessibility rules | [`components/<name>/<name>-spec.md`](components/) |
79
140
  | Component reference implementation | [`components/<name>/<name>.html`](components/) |
80
141
  | How a component appears in Storybook | [`src/stories/Library/<Name>/`](src/stories/Library/) |
@@ -87,7 +148,7 @@ Three workflows in `.github/workflows/`:
87
148
 
88
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`.
89
150
  2. **`sync-component.yml`** — fires when `components/sidenav/sidenav.html` changes. Regenerates the `sidenav-figmamake.html` variant and commits it.
90
- 3. **`deploy-storybook.yml`** — fires when `tokens/`, `src/`, `.storybook/`, `components/`, `docs/`, or `style-dictionary.config.js` changes. Rebuilds Style Dictionary + Storybook and commits the built site to `/storybook/` on `main`. GitHub Pages serves that folder.
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.)
91
152
 
92
153
  Source of truth is always the repo on `main`; Storybook is a downstream artifact.
93
154
 
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Scrollable — Pathway Design System
3
+ *
4
+ * A reusable overlay-scrollbar wrapper. Hides the native OS scrollbar entirely and renders
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).
7
+ *
8
+ * Behaviour (see components/scrollbar/scrollbar-spec.md):
9
+ * - Thumb height is PROPORTIONAL to content: (viewport / total) × track, floored at 28px.
10
+ * - Reveal: appears while scrolling, fades ~900ms after; also appears when the MOUSE is near
11
+ * the right-edge bar (so it can be grabbed). It does NOT appear from hovering the panel.
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
14
+ * strip is disabled on touch devices via a `(hover: none)` rule.
15
+ * - Keyboard / a11y: the content scrolls natively (arrows, PageUp/Down, Home/End, Space); the
16
+ * thumb is decorative (`aria-hidden`) and never the only scroll mechanism.
17
+ *
18
+ * Usage:
19
+ * <Scrollable style={{ flex: 1, minHeight: 0 }}>…tall content…</Scrollable>
20
+ */
21
+
22
+ import React, { useRef, useState, useEffect, useCallback } from "react";
23
+
24
+ // ─── TOKENS ──────────────────────────────────────────────────────────────────
25
+ // Every colour + unit resolves through a SEMANTIC Pathway token — no primitives,
26
+ // no hardcoded hex/px. Motion uses the --motion-* tokens (durations/easings from design-system-spec §2).
27
+ export const SCROLL = {
28
+ thumbWidth: "var(--semantic-layout-units-padding-xtight)", // 6px — visible thumb thickness
29
+ 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)
33
+ thumbRest: "var(--semantic-color-scrim-faint)", // black 16% — faint overlay (rest)
34
+ thumbHover: "var(--semantic-color-scrim-light)", // black 30% — hover/drag
35
+ thumbBlur: "blur(8px) saturate(180%)",
36
+ // Hairline glass edge: semantic white at 35% via color-mix.
37
+ thumbEdge: "inset 0 0 0 0.5px color-mix(in srgb, var(--semantic-color-fill-static-neutral-faint) 35%, transparent)",
38
+ // Asymmetric fade, motion-token driven: snappy appear (200ms decelerate glide-in), graceful fade-out (380ms).
39
+ fadeIn: "opacity var(--motion-duration-3) var(--motion-easing-decelerate), background var(--motion-duration-3) var(--motion-easing-standard)",
40
+ fadeOut: "opacity var(--motion-duration-5) var(--motion-easing-standard)",
41
+ idleHideMs: 500, // hide the thumb this long after scrolling stops / mouse leaves the bar
42
+ };
43
+
44
+ // Injected once: hide the native bar on the opt-in class, and disable the mouse grab strip on
45
+ // touch devices so finger-swipes scroll the content underneath (the thumb stays a passive indicator).
46
+ if (typeof document !== "undefined" && !document.getElementById("pds-scrollable-base")) {
47
+ const s = document.createElement("style");
48
+ s.id = "pds-scrollable-base";
49
+ s.textContent =
50
+ ".pds-scrollable__view{scrollbar-width:none;-ms-overflow-style:none}" +
51
+ ".pds-scrollable__view::-webkit-scrollbar{width:0;height:0;display:none}" +
52
+ "@media (hover: none){.pds-scrollable__grab{pointer-events:none!important}}";
53
+ document.head.appendChild(s);
54
+ }
55
+
56
+ export function Scrollable({ children, className = "", style = {}, viewClassName = "", viewStyle = {} }) {
57
+ const viewRef = useRef(null);
58
+ const dragRef = useRef(null);
59
+ const idleRef = useRef(null);
60
+ const nearRef = useRef(false); // mouse currently near the right-edge bar
61
+ const [thumb, setThumb] = useState({ h: 0, top: 0, show: false });
62
+ const [active, setActive] = useState(false); // thumb visible (scrolling, near-bar, or dragging)
63
+ const [hot, setHot] = useState(false); // mouse over / dragging the grab strip
64
+
65
+ // Thumb height is PROPORTIONAL to content; floored at thumbMin. Position maps the floored
66
+ // thumb across the full track so it still reaches top and bottom exactly.
67
+ const recompute = useCallback(() => {
68
+ const el = viewRef.current;
69
+ if (!el) return;
70
+ const { scrollHeight, clientHeight, scrollTop } = el;
71
+ if (scrollHeight <= clientHeight + 1) { setThumb(t => (t.show ? { ...t, show: false } : t)); return; }
72
+ const h = Math.max(SCROLL.thumbMin, (clientHeight / scrollHeight) * clientHeight);
73
+ const top = (scrollTop / (scrollHeight - clientHeight)) * (clientHeight - h);
74
+ setThumb({ h, top, show: true });
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ recompute();
79
+ const el = viewRef.current;
80
+ if (!el || typeof ResizeObserver === "undefined") return;
81
+ const ro = new ResizeObserver(recompute);
82
+ ro.observe(el);
83
+ Array.from(el.children).forEach(c => ro.observe(c));
84
+ return () => ro.disconnect();
85
+ }, [children, recompute]);
86
+
87
+ const scheduleHide = () => {
88
+ clearTimeout(idleRef.current);
89
+ idleRef.current = setTimeout(() => {
90
+ if (!dragRef.current && !nearRef.current) setActive(false);
91
+ }, SCROLL.idleHideMs);
92
+ };
93
+ const wake = () => { setActive(true); scheduleHide(); }; // reveal while scrolling
94
+ const onScroll = () => { recompute(); wake(); };
95
+
96
+ // Edge-proximity reveal (mouse): show the bar when the cursor is within grabZone of the right
97
+ // edge, so it can be grabbed — but NOT from hovering the rest of the panel.
98
+ const onMouseMove = (e) => {
99
+ const rect = e.currentTarget.getBoundingClientRect();
100
+ if (rect.right - e.clientX <= SCROLL.grabZone) {
101
+ nearRef.current = true; setActive(true); clearTimeout(idleRef.current);
102
+ } else if (nearRef.current) {
103
+ nearRef.current = false; scheduleHide();
104
+ }
105
+ };
106
+ const onMouseLeave = () => { nearRef.current = false; if (!dragRef.current) scheduleHide(); };
107
+
108
+ // Drag (mouse only). The grab strip carries this; the visual thumb is decorative.
109
+ const onGrabDown = (e) => {
110
+ e.preventDefault();
111
+ const el = viewRef.current;
112
+ if (!el) return;
113
+ dragRef.current = { startY: e.clientY, startTop: el.scrollTop };
114
+ setHot(true); setActive(true); clearTimeout(idleRef.current);
115
+ const onMove = (ev) => {
116
+ const { scrollHeight, clientHeight } = el;
117
+ const h = Math.max(SCROLL.thumbMin, (clientHeight / scrollHeight) * clientHeight);
118
+ const ratio = (scrollHeight - clientHeight) / (clientHeight - h);
119
+ el.scrollTop = dragRef.current.startTop + (ev.clientY - dragRef.current.startY) * ratio;
120
+ };
121
+ const onUp = () => {
122
+ dragRef.current = null; setHot(false);
123
+ document.removeEventListener("mousemove", onMove);
124
+ document.removeEventListener("mouseup", onUp);
125
+ wake();
126
+ };
127
+ document.addEventListener("mousemove", onMove);
128
+ document.addEventListener("mouseup", onUp);
129
+ };
130
+
131
+ const barTop = thumb.top, barH = thumb.h;
132
+
133
+ return (
134
+ <div
135
+ className={`pds-scrollable ${className}`}
136
+ style={{ position: "relative", minHeight: 0, ...style }}
137
+ onMouseMove={onMouseMove}
138
+ onMouseLeave={onMouseLeave}
139
+ >
140
+ <div
141
+ ref={viewRef}
142
+ className={`pds-scrollable__view ${viewClassName}`}
143
+ onScroll={onScroll}
144
+ style={{ height: "100%", overflowY: "auto", overflowX: "hidden", ...viewStyle }}
145
+ >
146
+ {children}
147
+ </div>
148
+
149
+ {thumb.show && (
150
+ <>
151
+ {/* Visual thumb — decorative, never intercepts pointer events. */}
152
+ <div
153
+ aria-hidden="true"
154
+ style={{
155
+ position: "absolute", top: barTop, right: SCROLL.gutter,
156
+ width: SCROLL.thumbWidth, height: barH, borderRadius: SCROLL.thumbRadius,
157
+ background: hot ? SCROLL.thumbHover : SCROLL.thumbRest,
158
+ backdropFilter: SCROLL.thumbBlur, WebkitBackdropFilter: SCROLL.thumbBlur,
159
+ boxShadow: SCROLL.thumbEdge,
160
+ opacity: active ? 1 : 0, transition: active ? SCROLL.fadeIn : SCROLL.fadeOut,
161
+ pointerEvents: "none", zIndex: 5,
162
+ }}
163
+ />
164
+ {/* Invisible mouse grab strip — wider than the thumb so it's catchable; disabled on
165
+ touch via the (hover:none) rule so finger-swipes scroll the content beneath. */}
166
+ <div
167
+ aria-hidden="true"
168
+ className="pds-scrollable__grab"
169
+ onMouseDown={onGrabDown}
170
+ onMouseEnter={() => setHot(true)}
171
+ onMouseLeave={() => setHot(false)}
172
+ style={{
173
+ position: "absolute", top: barTop, right: 0,
174
+ width: SCROLL.grabZone, height: barH,
175
+ pointerEvents: active ? "auto" : "none", zIndex: 6, background: "transparent",
176
+ }}
177
+ />
178
+ </>
179
+ )}
180
+ </div>
181
+ );
182
+ }
183
+
184
+ export default Scrollable;