@helloimjolopez-pathway/pathway-tokens 6.0.0 → 7.0.1

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,26 +2,68 @@
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
69
  <!-- primitives first: the themes reference these via var(), so this must load -->
@@ -30,23 +72,35 @@ Without a bundler — link the CSS directly:
30
72
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/themes/midnight.css" />
31
73
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/type.css" />
32
74
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/layout.css" />
33
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/layout-contextual.css" />
75
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/layout-responsive.css" />
34
76
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/motion.css" />
35
77
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/helloimjolopez-collab/pathway-ds@main/src/tokens/breakpoints.css" />
36
78
  ```
37
79
 
38
- 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
39
83
 
40
84
  ```css
41
- .my-component {
42
- background: var(--semantic-color-light-mode-fill-static-brand-base);
43
- 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);
44
91
  }
45
92
  ```
46
93
 
47
- 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?
48
99
 
49
- ---
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).
50
104
 
51
105
  ## Layout
52
106
 
@@ -30,13 +30,28 @@ export const SCROLL = {
30
30
  thumbMin: 28, // px — min thumb length (grab-target floor); JS layout math
31
31
  gutter: "var(--semantic-layout-units-padding-xxxtight)", // 2px — inset from the right edge
32
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
33
+ // WHY THESE TWO RUNGS AND NOT THE FAINTER ONES (2026-09-16):
34
+ //
35
+ // Rest was Scrim/Faint (black 16%), which composites to 1.4:1 against both
36
+ // canvas and sheet. The AA floor for a non-text UI component is 3:1, so a 6px
37
+ // pill at 1.4:1 sat right at the threshold of perception: one person saw it
38
+ // and another, on a brighter display, reported the scrollbar as missing
39
+ // entirely. That was a real report, not a misconfiguration.
40
+ //
41
+ // Measured over #fafafa: Faint 16% = 1.4:1, Light 24% = 1.68:1,
42
+ // Subtle 50% = 3.4:1, Base 70% = 6.57:1. Subtle is the lightest rung that
43
+ // clears the floor, so rest takes it and hover takes Base, which keeps the
44
+ // hover step legible as a step.
45
+ thumbRest: "var(--semantic-color-scrim-subtle)", // black 50% — 3.4:1, the lightest rung clearing 3:1
46
+ thumbHover: "var(--semantic-color-scrim-base)", // black 70% — 6.57:1, hover/drag
35
47
  thumbBlur: "blur(8px) saturate(180%)",
36
48
  // Hairline glass edge: semantic white at 35% via color-mix.
37
49
  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)",
50
+ // Fade OUT gracefully, but appear INSTANTLY. Animating opacity on the way in
51
+ // multiplied the thumb's own alpha by the in-flight opacity, so during the
52
+ // 200ms glide-in it rendered as low as 1.18:1 — below the floor at the exact
53
+ // moment the user is looking for it. Only the fade-out is animated now.
54
+ fadeIn: "background var(--motion-duration-3) var(--motion-easing-standard)",
40
55
  fadeOut: "opacity var(--motion-duration-5) var(--motion-easing-standard)",
41
56
  idleHideMs: 500, // hide the thumb this long after scrolling stops / mouse leaves the bar
42
57
  };
@@ -62,7 +62,7 @@ export const T = {
62
62
  navHover: c("fill-action-selection-hover"),
63
63
  navActive: c("fill-action-selection-selected"),
64
64
  navTrail: c("fill-action-selection-trail"),
65
- infoSubtle: c("stroke-static-neutral-subtle"), // container right border
65
+ infoSubtle: c("stroke-static-neutral-base"), // container right border
66
66
  },
67
67
  surface: { navLight: c("fill-surface-sheet") },
68
68
  // Label, icon and chevron all resolve to the SAME token per state. A nav item
@@ -73,8 +73,8 @@ export const T = {
73
73
  navHover: c("foreground-action-secondary-hover"),
74
74
  navActive: c("foreground-action-secondary-pressed"),
75
75
  navDisabled: c("foreground-action-disabled"),
76
- secondary: c("foreground-static-neutral-subtle"), // PopoverMenu.SectionLabel
77
- secondarySubtle: c("foreground-static-neutral-subtle"), // NavSectionLabel
76
+ secondary: c("foreground-static-neutral-base"), // PopoverMenu.SectionLabel
77
+ secondarySubtle: c("foreground-static-neutral-base"), // NavSectionLabel
78
78
  },
79
79
  icon: {
80
80
  navBase: c("foreground-action-secondary-rest"),
@@ -85,7 +85,7 @@ export const T = {
85
85
  },
86
86
  // The active-item stripe takes the loud brand blue straight from the Action
87
87
  // tier rather than owning a token of its own.
88
- indicator: c("fill-action-primary-pressed"),
88
+ indicator: c("fill-action-primary-strong-pressed"),
89
89
  radius: u("cornerradius-medium", 8),
90
90
  };
91
91
 
@@ -310,7 +310,7 @@ export function SideNavTooltip({ label, anchorRect, onMouseEnter, onMouseLeave }
310
310
 
311
311
  // ─── SectionLabel ─────────────────────────────────────────────────────────────
312
312
  // Figma node 40006794-5977 — building block used in CollapsedPopover headers.
313
- // Tokens: Foreground/Static/Neutral/Light (#7b7b7b), Label/Section/Small/Semibold
313
+ // Tokens: Foreground/Static/Neutral/Base (#313131), Label/Section/Small/Semibold
314
314
  // (11px / 600 / 16px / 0.6px letter-spacing, uppercase)
315
315
  // Padding: Padding/Tight (12px left), Padding/XTight (8px vertical)
316
316
  export function SectionLabel({ label }) {
@@ -381,7 +381,7 @@ export function PopoverRow({ item, onClick, activeId }) {
381
381
  export function CollapseButton({ isSidebarCollapsed, onToggle, collapseIcon, expandIcon }) {
382
382
  const [h, setH] = useState(false);
383
383
  const labelColor = h ? T.text.navHover : T.text.navBase;
384
- const actionIconColor = T.icon.actionSecondary; // #6b6b6b — always static, no hover change
384
+ const actionIconColor = T.foreground.actionSecondary; // #6b6b6b — always static, no hover change
385
385
  return (
386
386
  <div style={{ width: "100%" }}>
387
387
  <div style={{ height: 1, backgroundColor: T.fill.infoSubtle, marginBottom: L.collapseGap }} />
@@ -431,7 +431,7 @@ export function CollapseButton({ isSidebarCollapsed, onToggle, collapseIcon, exp
431
431
  // ─── SideNav (top-level container) ────────────────────────────────────────────
432
432
  // ── NavSectionLabel ──────────────────────────────────────────────────────────
433
433
  // Optional in-nav section heading. Figma: SideNav.SectionLabel (node 40006794:5975).
434
- // Tokens: Label/Section/Small/Semibold (11px/600/16px/0.6px), Foreground/Static/Neutral/Subtle (#606060)
434
+ // Tokens: Label/Section/Small/Semibold (11px/600/16px/0.6px), Foreground/Static/Neutral/Base (#606060)
435
435
  // Container: h-[40px], pl-[4px], pr-[4px], py-[8px]
436
436
  // In collapsed rail: hidden — replaced by a Divider (rendered by SideNav itself, see §2.3)
437
437
  // ── NavHeader ────────────────────────────────────────────────────────────────
@@ -441,7 +441,7 @@ export function CollapseButton({ isSidebarCollapsed, onToggle, collapseIcon, exp
441
441
  // Hidden on mobile (<768px) — TopNav hamburger is the sole toggle there.
442
442
  export function NavHeader({ isSidebarCollapsed, onToggle, collapseIcon, expandIcon }) {
443
443
  const [h, setH] = useState(false);
444
- const actionIconColor = T.icon.actionSecondary; // #6b6b6b — always static
444
+ const actionIconColor = T.foreground.actionSecondary; // #6b6b6b — always static
445
445
  return (
446
446
  <div style={{ width: "100%", flexShrink: 0 }}>
447
447
  <div onClick={onToggle} onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)}
@@ -471,7 +471,7 @@ export function NavHeader({ isSidebarCollapsed, onToggle, collapseIcon, expandIc
471
471
  </div>
472
472
  )}
473
473
  </div>
474
- {/* Divider — Stroke/Static/Neutral/Subtle, py-[2px] in Figma */}
474
+ {/* Divider — Stroke/Static/Neutral/Base, py-[2px] in Figma */}
475
475
  <div style={{ padding: "2px 0" }}>
476
476
  <div style={{ height: 1, backgroundColor: T.fill.infoSubtle }} />
477
477
  </div>
@@ -570,6 +570,34 @@ export function SideNav({
570
570
  hideCollapseButton = false,
571
571
  defaultExpanded = {},
572
572
  className = "",
573
+ // ── SLOTS ─────────────────────────────────────────────────────────────────
574
+ // Three insertion points, mirroring the slot structure in Figma. Added
575
+ // 2026-09-10 at the user's explicit request (which is what the §1.1 Figma
576
+ // exemption requires before SideNav changes).
577
+ //
578
+ // WHY SLOTS RATHER THAN MORE PROPS: the `items` array can express anything
579
+ // item-SHAPED - a label, an icon, nesting, a disabled state. It cannot
580
+ // express a module-specific widget: a support button, a plan-usage meter, a
581
+ // "New" badge component a team already owns. Without a slot those teams have
582
+ // exactly one option, which is to stop using the component and rebuild the
583
+ // nav. That is the drift this component exists to prevent, so the escape
584
+ // hatch has to be inside it.
585
+ //
586
+ // headerEnd after the collapse toggle in the pinned header. This is the
587
+ // common case: a module name, a small action. In Figma the
588
+ // header is a slot that can hold the toggle alone, or the
589
+ // toggle plus a label or button.
590
+ // children inside the scroll region, after the items. Anything that
591
+ // scrolls with the list.
592
+ // footer PINNED below the scroll region, so it stays put while the
593
+ // list scrolls. Buttons live here.
594
+ //
595
+ // Each is null by default and renders NOTHING when null - no wrapper, no
596
+ // padding, no border. An always-rendered empty wrapper would shift the
597
+ // layout of every nav that does not use the slot.
598
+ headerEnd = null,
599
+ children = null,
600
+ footer = null,
573
601
  }) {
574
602
  const [expanded, setExpanded] = useState(defaultExpanded);
575
603
  const [popoverId, setPopoverId] = useState(null);
@@ -670,10 +698,31 @@ export function SideNav({
670
698
  }}>
671
699
  {/* NavHeader — TOP of nav, PINNED (flexShrink:0) so it stays visible while the
672
700
  item list scrolls beneath it. Hidden on mobile (<768px). */}
673
- {!hideCollapseButton && (
701
+ {(!hideCollapseButton || headerEnd) && (
674
702
  <div style={{ flexShrink: 0 }}>
675
- <NavHeader isSidebarCollapsed={collapsed}
676
- onToggle={() => onCollapseChange && onCollapseChange(!collapsed)} />
703
+ {!hideCollapseButton && (
704
+ <NavHeader isSidebarCollapsed={collapsed}
705
+ onToggle={() => onCollapseChange && onCollapseChange(!collapsed)} />
706
+ )}
707
+ {/* headerEnd slot. Hidden on the 72px rail: the rail is only wide
708
+ enough for a centred icon, so arbitrary header content would
709
+ either overflow it or force it wider. It fades rather than
710
+ unmounting, on the same curve as the section labels, so the
711
+ header does not jump when the rail toggles. */}
712
+ {headerEnd && (
713
+ <div style={{
714
+ opacity: collapsed ? 0 : 1,
715
+ maxHeight: collapsed ? 0 : 200,
716
+ overflow: "hidden",
717
+ pointerEvents: collapsed ? "none" : "auto",
718
+ padding: collapsed ? 0 : `0 ${L.navPadH}`,
719
+ transition:
720
+ "opacity var(--motion-duration-4) var(--motion-easing-standard), " +
721
+ "max-height var(--motion-duration-5) var(--motion-easing-spring)",
722
+ }}>
723
+ {headerEnd}
724
+ </div>
725
+ )}
677
726
  </div>
678
727
  )}
679
728
 
@@ -734,9 +783,31 @@ export function SideNav({
734
783
  </div>
735
784
  )}
736
785
 
786
+ {/* Body slot — scrolls with the item list, so it sits INSIDE
787
+ Scrollable and above the spacer. Use this for content that belongs
788
+ with the items; use `footer` for anything that must stay visible. */}
789
+ {children && <div style={{ flexShrink: 0 }}>{children}</div>}
790
+
737
791
  {/* Bottom spacer — fills remaining height */}
738
792
  <div style={{ flex: 1, minHeight: L.menuPadB }} />
739
793
  </Scrollable>
794
+
795
+ {/* Footer slot — OUTSIDE Scrollable and flexShrink:0, so it is pinned to
796
+ the bottom of the nav and stays put while the list scrolls. The
797
+ divider only appears when the slot has content, so a nav without a
798
+ footer is unchanged. Padding drops to the rail's narrower inset when
799
+ collapsed rather than hiding: a footer button is usually an icon
800
+ button that still works at 72px, unlike arbitrary header content. */}
801
+ {footer && (
802
+ <div style={{
803
+ flexShrink: 0,
804
+ borderTop: `0.5px solid ${T.fill.infoSubtle}`,
805
+ padding: collapsed ? `8px ${L.navColPadH}` : `8px ${L.navPadH}`,
806
+ transition: "padding var(--motion-duration-6) var(--motion-easing-emphasized)",
807
+ }}>
808
+ {footer}
809
+ </div>
810
+ )}
740
811
  </nav>
741
812
  );
742
813
  }
@@ -775,7 +846,7 @@ function ChevUp({ size = 10, color }) {
775
846
  // right_panel_open — used on the collapse button when sidebar is EXPANDED (click to collapse)
776
847
  // Token: Foreground/Action/Secondary/Rest (#6b6b6b)
777
848
  // Figma component: SideBar Expand/Collapse, Type=Collapse, node 40006793:3783
778
- function RightPanelOpenIcon({ size = 12, color = T.icon.actionSecondary }) {
849
+ function RightPanelOpenIcon({ size = 12, color = T.foreground.actionSecondary }) {
779
850
  return (
780
851
  <svg width={size} height={size} viewBox="0 0 12 12" fill="none">
781
852
  <path d="M5.66667 7.86667V4.13333C5.66667 3.97778 5.6 3.87222 5.46667 3.81667C5.33333 3.76111 5.21111 3.78889 5.1 3.9L3.46667 5.53333C3.33333 5.66667 3.26667 5.82222 3.26667 6C3.26667 6.17778 3.33333 6.33333 3.46667 6.46667L5.1 8.1C5.21111 8.21111 5.33333 8.23889 5.46667 8.18333C5.6 8.12778 5.66667 8.02222 5.66667 7.86667ZM1.33333 12C0.966667 12 0.652778 11.8694 0.391667 11.6083C0.130556 11.3472 0 11.0333 0 10.6667V1.33333C0 0.966667 0.130556 0.652778 0.391667 0.391667C0.652778 0.130556 0.966667 0 1.33333 0H10.6667C11.0333 0 11.3472 0.130556 11.6083 0.391667C11.8694 0.652778 12 0.966667 12 1.33333V10.6667C12 11.0333 11.8694 11.3472 11.6083 11.6083C11.3472 11.8694 11.0333 12 10.6667 12H1.33333ZM8.66667 10.6667H10.6667V1.33333H8.66667V10.6667ZM7.33333 10.6667V1.33333H1.33333V10.6667H7.33333Z" fill={color} />
@@ -786,7 +857,7 @@ function RightPanelOpenIcon({ size = 12, color = T.icon.actionSecondary }) {
786
857
  // left_panel_open — used on the collapse button when sidebar is COLLAPSED (click to expand)
787
858
  // Token: Foreground/Action/Secondary/Rest (#6b6b6b)
788
859
  // Figma component: SideBar Expand/Collapse, Type=Expand, node 40006793:3783
789
- function LeftPanelOpenIcon({ size = 12, color = T.icon.actionSecondary }) {
860
+ function LeftPanelOpenIcon({ size = 12, color = T.foreground.actionSecondary }) {
790
861
  return (
791
862
  <svg width={size} height={size} viewBox="0 0 12 12" fill="none">
792
863
  <path d="M6.33333 4.13333V7.86667C6.33333 8.02222 6.4 8.12778 6.53333 8.18333C6.66667 8.23889 6.78889 8.21111 6.9 8.1L8.53333 6.46667C8.66667 6.33333 8.73333 6.17778 8.73333 6C8.73333 5.82222 8.66667 5.66667 8.53333 5.53333L6.9 3.9C6.78889 3.78889 6.66667 3.76111 6.53333 3.81667C6.4 3.87222 6.33333 3.97778 6.33333 4.13333ZM1.33333 12C0.966667 12 0.652778 11.8694 0.391667 11.6083C0.130556 11.3472 0 11.0333 0 10.6667V1.33333C0 0.966667 0.130556 0.652778 0.391667 0.391667C0.652778 0.130556 0.966667 0 1.33333 0H10.6667C11.0333 0 11.3472 0.130556 11.6083 0.391667C11.8694 0.652778 12 0.966667 12 1.33333V10.6667C12 11.0333 11.8694 11.3472 11.6083 11.6083C11.3472 11.8694 11.0333 12 10.6667 12H1.33333ZM3.33333 10.6667V1.33333H1.33333V10.6667H3.33333ZM4.66667 10.6667H10.6667V1.33333H4.66667V10.6667Z" fill={color} />
package/dist/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Pathway design tokens
2
+
3
+ **The colour contract is 169 names.** Your full working vocabulary is
4
+ **278**. If you add up every declaration in this folder you get
5
+ 827 — that number is not the contract, and the table below says why.
6
+
7
+ ## Link these, in this order
8
+
9
+ ```html
10
+ <link rel="stylesheet" href="primitives.css">
11
+ <link rel="stylesheet" href="themes/light.css">
12
+ <link rel="stylesheet" href="themes/midnight.css">
13
+ <link rel="stylesheet" href="type.css">
14
+ <link rel="stylesheet" href="layout.css">
15
+ <link rel="stylesheet" href="motion.css">
16
+ <link rel="stylesheet" href="layout-responsive.css">
17
+ <link rel="stylesheet" href="breakpoints.css">
18
+ ```
19
+
20
+ `primitives.css` must come first. Every semantic token resolves through it, so if it is
21
+ missing all colour resolves to nothing and the page renders unstyled **with no console
22
+ error**.
23
+
24
+ ## The files
25
+
26
+ | File | Names | Safe to name? | Must load? | What it is |
27
+ |---|---|---|---|---|
28
+ | `primitives.css` | 352 | **no** | yes | Raw colour ramps. Load it; do not name it. |
29
+ | `themes/light.css` | 169 | yes | yes | Colour contract, Light Mode. |
30
+ | `themes/midnight.css` | 169 | yes | yes | Colour contract, Midnight Mode. Same names as light. |
31
+ | `type.css` | 41 | yes | yes | Type scale. Compose from these. |
32
+ | `layout.css` | 40 | yes | yes | Spacing, radii, border widths. |
33
+ | `layout-contextual.css` | 28 | **no** | **no** | Per-component metrics. This repo's components use these. |
34
+ | `motion.css` | 17 | yes | yes | Durations and easings. |
35
+ | `layout-responsive.css` | 6 | yes | yes | Sheet and TopNav padding. The only layout tokens with media queries. |
36
+ | `breakpoints.css` | 5 | yes | yes | Breakpoint values. |
37
+
38
+ Each file repeats this in its own header.
39
+
40
+ ## Can I use a primitive?
41
+
42
+ Yes. Nothing stops you and nothing should. But `Primitive: Color` has exactly **one
43
+ mode** and `themes/midnight.css` declares **zero** primitives, so a primitive holds one
44
+ value forever:
45
+
46
+ ```css
47
+ color: var(--primitive-color-brand-400); /* stays this blue in Midnight */
48
+ color: var(--semantic-color-fill-action-primary-strong-rest); /* flips with the theme */
49
+ ```
50
+
51
+ Use one when you specifically want a value that does not respond to the theme.
52
+
53
+ ## Theming
54
+
55
+ One name, two values, resolved by selector. Set `data-theme="midnight"` on `<html>`
56
+ to flip the page, or on any element to flip just that subtree — it composes both ways,
57
+ so a light island can sit inside a dark island.
58
+
59
+ Never put a mode in a property name. `--semantic-color-light-mode-*` came from
60
+ `tokens.css`, retired 2026-09-03, and resolves to nothing.
61
+
62
+ ## Type
63
+
64
+ No composite styles and no `.pw-type-*` classes. Name five properties:
65
+
66
+ ```css
67
+ font-family: var(--semantic-type-family-brand);
68
+ font-size: var(--semantic-type-font-size-m);
69
+ font-weight: var(--semantic-type-weight-semibold);
70
+ line-height: var(--semantic-type-line-height-m-single);
71
+ letter-spacing: var(--semantic-type-letter-spacing-compact);
72
+ ```
73
+
74
+ ## Layout units carry their unit
75
+
76
+ ```css
77
+ border-radius: var(--semantic-layout-units-cornerradius-small); /* correct */
78
+ border-radius: calc(var(--semantic-layout-units-cornerradius-small) * 1px); /* WRONG */
79
+ ```
80
+
81
+ The second is `calc(4px * 1px)`, which is not a length, so the browser drops it silently.
82
+
83
+ ## Machine-readable
84
+
85
+ `contract.json` lists every consumable name plus the per-file counts, so you can lint
86
+ against it if you want the boundary enforced.
87
+
88
+ ## Motion is not in Figma
89
+
90
+ The Variables panel has zero motion variables; `motion.css` is generated from
91
+ `docs/design-system-spec.md` §2. Anyone rebuilding tokens from a Figma export gets no
92
+ motion at all and no warning — consume the package instead.
93
+
94
+ ---
95
+
96
+ Repo: https://github.com/helloimjolopez-collab/pathway-ds
@@ -1,11 +1,18 @@
1
- /**
2
- * Do not edit directly, this file was auto-generated.
3
- */
1
+ /*
2
+ ==========================================================================
3
+ PATHWAY DESIGN TOKENS — CONTRACT
4
+ ==========================================================================
4
5
 
6
+ The breakpoint values. Name these freely.
7
+
8
+ Declares 5 custom properties.
9
+ Generated — do not edit. Run `npm run build-tokens`.
10
+ ==========================================================================
11
+ */
5
12
  :root {
13
+ --breakpoints-desktop-1920px: 1920px;
6
14
  --breakpoints-desktop-1440px: 1440px;
7
15
  --breakpoints-tablet-1024px: 1024px;
8
16
  --breakpoints-tablet-768px: 768px;
9
17
  --breakpoints-mobile-393px: 393px;
10
- --breakpoints-desktop-1920px: 1920px;
11
18
  }