@charcuterie/ui 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +9 -0
  2. package/dist/Dialog/Dialog.d.ts.map +1 -1
  3. package/dist/Dialog/Dialog.js +3 -1
  4. package/dist/Dialog/Dialog.js.map +1 -1
  5. package/dist/Field/Field.d.ts.map +1 -1
  6. package/dist/Field/Field.js +1 -1
  7. package/dist/Field/Field.js.map +1 -1
  8. package/dist/Field/FieldGroup.d.ts.map +1 -1
  9. package/dist/Field/FieldGroup.js +1 -1
  10. package/dist/Field/FieldGroup.js.map +1 -1
  11. package/dist/Header/Header.d.ts +52 -0
  12. package/dist/Header/Header.d.ts.map +1 -0
  13. package/dist/Header/Header.js +55 -0
  14. package/dist/Header/Header.js.map +1 -0
  15. package/dist/Main/Main.d.ts +113 -0
  16. package/dist/Main/Main.d.ts.map +1 -0
  17. package/dist/Main/Main.js +123 -0
  18. package/dist/Main/Main.js.map +1 -0
  19. package/dist/Rail/Rail.d.ts +61 -0
  20. package/dist/Rail/Rail.d.ts.map +1 -0
  21. package/dist/Rail/Rail.js +64 -0
  22. package/dist/Rail/Rail.js.map +1 -0
  23. package/dist/Shell/Shell.d.ts +112 -0
  24. package/dist/Shell/Shell.d.ts.map +1 -0
  25. package/dist/Shell/Shell.js +116 -0
  26. package/dist/Shell/Shell.js.map +1 -0
  27. package/dist/Shell/contentWidth.d.ts +63 -0
  28. package/dist/Shell/contentWidth.d.ts.map +1 -0
  29. package/dist/Shell/contentWidth.js +74 -0
  30. package/dist/Shell/contentWidth.js.map +1 -0
  31. package/dist/Shell/shellContext.d.ts +36 -0
  32. package/dist/Shell/shellContext.d.ts.map +1 -0
  33. package/dist/Shell/shellContext.js +32 -0
  34. package/dist/Shell/shellContext.js.map +1 -0
  35. package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -1
  36. package/dist/SortableTableHeader/SortableTableHeader.js +7 -1
  37. package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -1
  38. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  39. package/dist/Tooltip/Tooltip.js +5 -1
  40. package/dist/Tooltip/Tooltip.js.map +1 -1
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +4 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  46. package/dist/mountStory.testHelpers.js +56 -0
  47. package/dist/mountStory.testHelpers.js.map +1 -1
  48. package/dist/viewport.testHelpers.d.ts +31 -0
  49. package/dist/viewport.testHelpers.d.ts.map +1 -0
  50. package/dist/viewport.testHelpers.js +34 -0
  51. package/dist/viewport.testHelpers.js.map +1 -0
  52. package/package.json +2 -2
  53. package/src/Dialog/Dialog.tsx +17 -1
  54. package/src/Field/Field.tsx +10 -2
  55. package/src/Field/FieldGroup.tsx +4 -2
  56. package/src/Header/Header.mdx +72 -0
  57. package/src/Header/Header.stories.tsx +221 -0
  58. package/src/Header/Header.test.tsx +122 -0
  59. package/src/Header/Header.tsx +118 -0
  60. package/src/Main/Main.mdx +149 -0
  61. package/src/Main/Main.stories.tsx +164 -0
  62. package/src/Main/Main.test.tsx +167 -0
  63. package/src/Main/Main.tsx +174 -0
  64. package/src/Rail/Rail.mdx +75 -0
  65. package/src/Rail/Rail.stories.tsx +177 -0
  66. package/src/Rail/Rail.test.tsx +150 -0
  67. package/src/Rail/Rail.tsx +102 -0
  68. package/src/Shell/Shell.mdx +186 -0
  69. package/src/Shell/Shell.stories.tsx +185 -0
  70. package/src/Shell/Shell.test.tsx +267 -0
  71. package/src/Shell/Shell.tsx +170 -0
  72. package/src/Shell/contentWidth.ts +89 -0
  73. package/src/Shell/shellContext.ts +41 -0
  74. package/src/SortableTableHeader/SortableTableHeader.tsx +6 -1
  75. package/src/Tooltip/Tooltip.tsx +5 -1
  76. package/src/index.ts +16 -0
  77. package/src/mountStory.testHelpers.ts +63 -0
  78. package/src/shell.storyHelpers.tsx +258 -0
  79. package/src/sourceRules.test.ts +17 -1
  80. package/src/viewport.testHelpers.ts +47 -0
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"mountStory.testHelpers.d.ts","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;AAEvC;;;;;GAKG;AACH,KAAK,cAAc,GAAG;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE;QACb,aAAa,EAAE,WAAW,CAAA;KAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB,CAAA;AAkDD,eAAO,MAAM,UAAU,GACrB,OAAO,cAAc,KACpB,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,WAAW,CAAA;CAC3B,CAwBA,CAAA"}
1
+ {"version":3,"file":"mountStory.testHelpers.d.ts","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;AAEvC;;;;;GAKG;AACH,KAAK,cAAc,GAAG;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE;QACb,aAAa,EAAE,WAAW,CAAA;KAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB,CAAA;AA+GD,eAAO,MAAM,UAAU,GACrB,OAAO,cAAc,KACpB,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,WAAW,CAAA;CAC3B,CA0BA,CAAA"}
@@ -46,6 +46,61 @@ import { within } from "storybook/test";
46
46
  * triggers still open from inside `canvas`.
47
47
  */
48
48
  const mountedCanvases = [];
49
+ /**
50
+ * One frame past the mount, so post-mount effects have applied
51
+ * before anything reads the DOM.
52
+ *
53
+ * `storybook-addon-pseudo-states` applies its forced
54
+ * `:hover`/`:focus`/`:active` classes **a tick after render**, not
55
+ * during it — `vrtCapture.mjs` documents the same window and closes
56
+ * it with a settle, after "a real flake seen on `*--all-states`
57
+ * stories".
58
+ *
59
+ * This suite has the identical window and never closed it, and the
60
+ * consequence is worse here than a pixel diff: the a11y addon's
61
+ * `afterEach` runs axe the moment `run()` resolves, so a story
62
+ * whose forced state lands late is audited **un-forced**, and one
63
+ * whose state lands early is audited **hovered**. That is a
64
+ * contrast check reading `intent-accent-solid` on one run and
65
+ * `intent-accent-solid-hover` on the next — which is precisely the
66
+ * shape of `ButtonLink`'s intermittent `color-contrast` failure in
67
+ * CI, reported against `#6A64F0`, the daylight `solidHover`.
68
+ *
69
+ * Two frames rather than a fixed delay: React flushes effects and
70
+ * the addon applies classes within a frame, so this is a barrier
71
+ * tied to the browser's own scheduling instead of a millisecond
72
+ * count guessed against one machine. `vrtCapture` can afford its
73
+ * 400ms because it takes ~700 shots; this runs before every one of
74
+ * 222 tests.
75
+ */
76
+ /**
77
+ * Captured at module load, **before any test can replace it**.
78
+ *
79
+ * `Toast.test.tsx` stubs `requestAnimationFrame` and *holds* the
80
+ * callbacks, deliberately, to prove that a dismiss arriving during
81
+ * the enter frame is not undone by it. A settle that called the
82
+ * live `globalThis.requestAnimationFrame` would hand its own
83
+ * continuation to that stub and never be released — which is
84
+ * exactly what happened: three Toast tests went from passing to a
85
+ * flat 15s timeout.
86
+ *
87
+ * Binding the native one keeps a true frame barrier while staying
88
+ * immune to a stub, and it does not disturb the test doing the
89
+ * stubbing: the component's frames still go to its stub, only this
90
+ * helper's do not.
91
+ */
92
+ const requestNativeFrame = globalThis.requestAnimationFrame?.bind(globalThis);
93
+ const settlePostMountEffects = () => new Promise((resolve) => {
94
+ if (!requestNativeFrame) {
95
+ globalThis.setTimeout(resolve, 0);
96
+ return;
97
+ }
98
+ requestNativeFrame(() => {
99
+ requestNativeFrame(() => {
100
+ resolve();
101
+ });
102
+ });
103
+ });
49
104
  export const mountStory = async (story) => {
50
105
  for (const previous of mountedCanvases.splice(0)) {
51
106
  previous.remove();
@@ -57,6 +112,7 @@ export const mountStory = async (story) => {
57
112
  document.body.append(canvasElement);
58
113
  mountedCanvases.push(canvasElement);
59
114
  await story.run({ canvasElement });
115
+ await settlePostMountEffects();
60
116
  return {
61
117
  body: within(document.body),
62
118
  canvas: within(canvasElement),
@@ -1 +1 @@
1
- {"version":3,"file":"mountStory.testHelpers.js","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAgBvC;;;;;;;;;;GAUG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,eAAe,GAAkB,EAAE,CAAA;AAEzC,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,KAAqB,EAKpB,EAAE;IACH,KAAK,MAAM,QAAQ,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,QAAQ,CAAC,MAAM,EAAE,CAAA;IACnB,CAAC;IAED,KAAK,MAAM,UAAU,IAAI,QAAQ,CAAC,gBAAgB,CAChD,2BAA2B,CAC5B,EAAE,CAAC;QACF,UAAU,CAAC,MAAM,EAAE,CAAA;IACrB,CAAC;IAED,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAEnD,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IAEnC,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAEnC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,CAAC,CAAA;IAElC,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC;QAC7B,aAAa;KACd,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"mountStory.testHelpers.js","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAgBvC;;;;;;;;;;GAUG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,eAAe,GAAkB,EAAE,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,kBAAkB,GACtB,UAAU,CAAC,qBAAqB,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;AAEpD,MAAM,sBAAsB,GAAG,GAAG,EAAE,CAClC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;IAC5B,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QAEjC,OAAM;IACR,CAAC;IAED,kBAAkB,CAAC,GAAG,EAAE;QACtB,kBAAkB,CAAC,GAAG,EAAE;YACtB,OAAO,EAAE,CAAA;QACX,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,KAAqB,EAKpB,EAAE;IACH,KAAK,MAAM,QAAQ,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,QAAQ,CAAC,MAAM,EAAE,CAAA;IACnB,CAAC;IAED,KAAK,MAAM,UAAU,IAAI,QAAQ,CAAC,gBAAgB,CAChD,2BAA2B,CAC5B,EAAE,CAAC;QACF,UAAU,CAAC,MAAM,EAAE,CAAA;IACrB,CAAC;IAED,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAEnD,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IAEnC,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAEnC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,CAAC,CAAA;IAElC,MAAM,sBAAsB,EAAE,CAAA;IAE9B,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC;QAC7B,aAAa;KACd,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Resize the browser and **wait until the page agrees**.
3
+ *
4
+ * `page.viewport()` resolves when the driver has asked for the
5
+ * resize, not when the tester frame has been laid out at the new
6
+ * size. On the first resize in a file that gap is real: the
7
+ * assertion runs against the *previous* width, so a
8
+ * horizontal-scroll test at 390px silently measures a 1440px page
9
+ * and reports a number nobody can explain. It is a race, so it
10
+ * fails intermittently — which is the worst possible property for
11
+ * the one gate that says the shell does not scroll sideways.
12
+ *
13
+ * Polling `innerWidth` is the honest wait: it is the same number
14
+ * the media queries and the layout resolved against, so when it
15
+ * matches, the thing under test really is at the width the test
16
+ * claims.
17
+ */
18
+ export declare const setViewport: ({ height, width, }: {
19
+ height: number;
20
+ width: number;
21
+ }) => Promise<void>;
22
+ /** A phone in portrait — the width plex-channels scrolls at. */
23
+ export declare const PHONE: {
24
+ height: number;
25
+ width: number;
26
+ };
27
+ export declare const DESKTOP: {
28
+ height: number;
29
+ width: number;
30
+ };
31
+ //# sourceMappingURL=viewport.testHelpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.testHelpers.d.ts","sourceRoot":"","sources":["../src/viewport.testHelpers.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GAAU,oBAG/B;IACD,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd,KAAG,OAAO,CAAC,IAAI,CAgBf,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,KAAK;;;CAA8B,CAAA;AAEhD,eAAO,MAAM,OAAO;;;CAA+B,CAAA"}
@@ -0,0 +1,34 @@
1
+ import { page } from "@vitest/browser/context";
2
+ /**
3
+ * Resize the browser and **wait until the page agrees**.
4
+ *
5
+ * `page.viewport()` resolves when the driver has asked for the
6
+ * resize, not when the tester frame has been laid out at the new
7
+ * size. On the first resize in a file that gap is real: the
8
+ * assertion runs against the *previous* width, so a
9
+ * horizontal-scroll test at 390px silently measures a 1440px page
10
+ * and reports a number nobody can explain. It is a race, so it
11
+ * fails intermittently — which is the worst possible property for
12
+ * the one gate that says the shell does not scroll sideways.
13
+ *
14
+ * Polling `innerWidth` is the honest wait: it is the same number
15
+ * the media queries and the layout resolved against, so when it
16
+ * matches, the thing under test really is at the width the test
17
+ * claims.
18
+ */
19
+ export const setViewport = async ({ height, width, }) => {
20
+ await page.viewport(width, height);
21
+ const deadline = Date.now() + 2000;
22
+ while (globalThis.innerWidth !== width) {
23
+ if (Date.now() > deadline) {
24
+ throw new Error(`The viewport never reached ${width}px — it is ${globalThis.innerWidth}px. Every width assertion after this point would be measuring the wrong page.`);
25
+ }
26
+ await new Promise((resolve) => {
27
+ globalThis.requestAnimationFrame(resolve);
28
+ });
29
+ }
30
+ };
31
+ /** A phone in portrait — the width plex-channels scrolls at. */
32
+ export const PHONE = { height: 844, width: 390 };
33
+ export const DESKTOP = { height: 900, width: 1440 };
34
+ //# sourceMappingURL=viewport.testHelpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport.testHelpers.js","sourceRoot":"","sources":["../src/viewport.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAA;AAE9C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,EAChC,MAAM,EACN,KAAK,GAIN,EAAiB,EAAE;IAClB,MAAM,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAElC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;IAElC,OAAO,UAAU,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CACb,8BAA8B,KAAK,cAAc,UAAU,CAAC,UAAU,+EAA+E,CACtJ,CAAA;QACH,CAAC;QAED,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC5B,UAAU,CAAC,qBAAqB,CAAC,OAAO,CAAC,CAAA;QAC3C,CAAC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC,CAAA;AAED,gEAAgE;AAChE,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;AAEhD,MAAM,CAAC,MAAM,OAAO,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcuterie/ui",
3
- "version": "2.8.0",
3
+ "version": "2.10.0",
4
4
  "description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@charcuterie/logic": "1.2.0",
52
- "@charcuterie/tokens": "1.2.0",
52
+ "@charcuterie/tokens": "1.3.0",
53
53
  "@floating-ui/react": "^0.27.20",
54
54
  "@tanstack/react-virtual": "^3.13.12"
55
55
  },
@@ -119,7 +119,23 @@ export const Dialog = ({
119
119
  ) : null}
120
120
  </header>
121
121
 
122
- <div className="min-h-0 flex-1 overflow-auto p-4">
122
+ {/*
123
+ `tabIndex={0}` is load-bearing, not decoration. This region
124
+ scrolls, and a scrollable region that cannot be focused cannot
125
+ be scrolled by anyone driving the keyboard — there is nothing
126
+ to put the caret in and no way to reach the overflow. axe
127
+ gates it as `scrollable-region-focusable` (serious).
128
+
129
+ It only surfaced once the body actually overflowed, which is
130
+ why it could land green: the rule fires on a region that is
131
+ scrollable *right now*, so a dialog whose content happened to
132
+ fit stayed silent.
133
+ */}
134
+ <div
135
+ className="min-h-0 flex-1 overflow-auto p-4"
136
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: a scroll container must be focusable or a keyboard user can see the pane and cannot scroll it — axe's `scrollable-region-focusable`, which fails this component without the line. Same call, and the same wording, as `LogViewer`'s scroll pane. The rule is right about `<div>`s in general and wrong about scrollers.
137
+ tabIndex={0}
138
+ >
123
139
  {children}
124
140
  </div>
125
141
 
@@ -202,9 +202,17 @@ export const Field = ({
202
202
 
203
203
  {clonedControl}
204
204
 
205
+ {/*
206
+ Both of these are `text-sm`, not `text-xs`, since 2026-08-10.
207
+ A field description is the instruction for the input directly
208
+ above it, and an error is the one thing the user has to read
209
+ to get unstuck — neither survives being the smallest text on
210
+ the page. `aria-describedby` already ties them to the control;
211
+ the size is what decides whether a sighted user reads them.
212
+ */}
205
213
  {description === undefined ? null : (
206
214
  <p
207
- className="text-content-secondary text-xs"
215
+ className="text-content-secondary text-sm"
208
216
  id={descriptionId}
209
217
  >
210
218
  {description}
@@ -213,7 +221,7 @@ export const Field = ({
213
221
 
214
222
  {error === undefined ? null : (
215
223
  <p
216
- className="text-intent-danger-content text-xs"
224
+ className="text-intent-danger-content text-sm"
217
225
  id={errorId}
218
226
  >
219
227
  {error}
@@ -116,9 +116,11 @@ export const FieldGroup = ({
116
116
 
117
117
  {children}
118
118
 
119
+ {/* `text-sm` since 2026-08-10, matching `Field` — same reasoning,
120
+ and the two must not disagree about how big a description is. */}
119
121
  {description === undefined ? null : (
120
122
  <p
121
- className="text-content-secondary text-xs"
123
+ className="text-content-secondary text-sm"
122
124
  id={descriptionId}
123
125
  >
124
126
  {description}
@@ -127,7 +129,7 @@ export const FieldGroup = ({
127
129
 
128
130
  {error === undefined ? null : (
129
131
  <p
130
- className="text-intent-danger-content text-xs"
132
+ className="text-intent-danger-content text-sm"
131
133
  id={errorId}
132
134
  >
133
135
  {error}
@@ -0,0 +1,72 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as HeaderStories from "./Header.stories";
4
+
5
+ <Meta of={HeaderStories} />
6
+
7
+ # Header
8
+
9
+ The `<header>` landmark, the app's name, and the controls that belong to the frame rather
10
+ than to the page. Ten repos hand-roll this row; two of them wrote the same string.
11
+
12
+ <Canvas of={HeaderStories.Default} />
13
+
14
+ ## Props
15
+
16
+ <Controls />
17
+
18
+ <Canvas of={HeaderStories.AllStates} />
19
+
20
+ ## `isSticky` sets `position` **and** `z-index`
21
+
22
+ That pairing is the entire reason the prop is a boolean rather than two class names.
23
+
24
+ mux-magic's `PageHeader` is documented as a sticky header and sets only `z-index`
25
+ (`PageHeader.tsx:173`). It never sets `position: sticky`, so it scrolls away like any other
26
+ block and the z-index it carries has nothing to stack against — it is a flex-column shell
27
+ wearing a sticky header's clothes. The CSS is valid, the intent is stated in a comment, and
28
+ nobody scrolls a component in a review.
29
+
30
+ <Canvas of={HeaderStories.AllVariants} />
31
+
32
+ The z-index is `--layer-sticky` (100) from the token scale, never a hand-picked number. The
33
+ fleet's headers currently sit at `z-40` (mail-sifter, points-market), `z-50` (rip-deck),
34
+ `z-10` (spoolbuddy) and `z-[100]` (mux-magic) with no ordering guarantee between a header
35
+ and anything else. On the scale, `sticky` is below `dropdown` (200), `modal` (400) and
36
+ `toast` (500) — so a `Menu` opened *from* the header paints over it, by construction.
37
+
38
+ <Canvas of={HeaderStories.NotSticky} />
39
+
40
+ ## It invents no colours
41
+
42
+ `bg-surface-raised/85`, `backdrop-blur-md`, `border-border-subtle`. The fleet's copy of this
43
+ line is `bg-surface-base/90` — the header at *the same colour as the page behind it*,
44
+ leaning entirely on the blur to separate the two, so it disappears against a page that has
45
+ not been scrolled. `raised` means *more separated from base*, which is the role this
46
+ element actually wants.
47
+
48
+ ## The width comes from `Shell`
49
+
50
+ <Canvas of={HeaderStories.Responsive} />
51
+
52
+ The `<header>` is full-bleed — the border and the translucent fill run edge to edge — and
53
+ only its **inner row** is capped. That is what stops a constrained header from reading as a
54
+ floating island on an ultrawide, and it is the half points-market got right while its
55
+ `<main>` went uncapped.
56
+
57
+ The cap itself comes from `Shell`'s `contentWidth` through context, so the title always
58
+ sits directly above the content it names. Passing `contentWidth` here overrides it for the
59
+ rare case where they genuinely differ.
60
+
61
+ ## Slots, in render order
62
+
63
+ `heading` → `children` → `actions`. Every one is optional: a header with only actions is
64
+ image-viewer's `TitleBar`, one with only a heading is board-games'. `children` is the middle
65
+ — a search field, a breadcrumb, a status pill — and `actions` is pushed to the far end with
66
+ `ms-auto`, so it stays at the end in RTL too.
67
+
68
+ <Canvas of={HeaderStories.Interactive} />
69
+
70
+ `headingLevel` defaults to `1`, because on most pages the app's name *is* the document's
71
+ top-level heading. An app whose page title is its `h1` sets `headingLevel={2}` here rather
72
+ than leaving two of them.
@@ -0,0 +1,221 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { Badge } from "../Badge/Badge.tsx"
4
+ import { Button } from "../Button/Button.tsx"
5
+ import {
6
+ StoryCell,
7
+ StorySection,
8
+ } from "../board.storyHelpers.tsx"
9
+ import { Main } from "../Main/Main.tsx"
10
+ import { Shell } from "../Shell/Shell.tsx"
11
+ import {
12
+ HeaderSchemeToggle,
13
+ PageContent,
14
+ ScrollFiller,
15
+ } from "../shell.storyHelpers.tsx"
16
+ import { Header } from "./Header.tsx"
17
+
18
+ const meta = {
19
+ title: "Components/Header",
20
+ component: Header,
21
+ parameters: { layout: "fullscreen" },
22
+ argTypes: {
23
+ headingLevel: {
24
+ control: { type: "radio" },
25
+ options: [1, 2],
26
+ },
27
+ },
28
+ args: {
29
+ headingLevel: 1,
30
+ isSticky: true,
31
+ },
32
+ } satisfies Meta<typeof Header>
33
+
34
+ export default meta
35
+
36
+ type Story = StoryObj<typeof meta>
37
+
38
+ /**
39
+ * The fleet's header, as one component: the app's name, the
40
+ * controls that belong to the frame, a real `<header>` landmark,
41
+ * and a fill that is `surface-raised` rather than the page's own
42
+ * colour.
43
+ */
44
+ export const Default: Story = {
45
+ args: { heading: "Rip Deck" },
46
+ render: (headerProps) => (
47
+ <Shell>
48
+ <Header
49
+ {...headerProps}
50
+ actions={<HeaderSchemeToggle />}
51
+ />
52
+
53
+ <Main>
54
+ <PageContent />
55
+ </Main>
56
+ </Shell>
57
+ ),
58
+ }
59
+
60
+ /**
61
+ * The three slots, in the order they render: the heading, the
62
+ * middle (`children` — a search field, a breadcrumb, a status
63
+ * pill), and `actions`, which is pushed to the far end.
64
+ *
65
+ * Every one is optional. A header with only actions is the
66
+ * image-viewer's `TitleBar`; a header with only a heading is
67
+ * board-games'.
68
+ */
69
+ export const AllStates: Story = {
70
+ render: (headerProps) => (
71
+ <StorySection title="Ten repos hand-roll this row. These are the four shapes they hand-roll it into.">
72
+ <StoryCell align="stretch" label="heading only">
73
+ <Header {...headerProps} heading="Board Games" />
74
+ </StoryCell>
75
+
76
+ <StoryCell align="stretch" label="heading + actions">
77
+ <Header
78
+ {...headerProps}
79
+ actions={<HeaderSchemeToggle />}
80
+ heading="Points Market"
81
+ />
82
+ </StoryCell>
83
+
84
+ <StoryCell
85
+ align="stretch"
86
+ label="heading + middle + actions"
87
+ >
88
+ <Header
89
+ {...headerProps}
90
+ actions={<HeaderSchemeToggle />}
91
+ heading="Mail Sifter"
92
+ >
93
+ <Badge intent="info">412 unread</Badge>
94
+ </Header>
95
+ </StoryCell>
96
+
97
+ <StoryCell align="stretch" label="actions only">
98
+ <Header {...headerProps}>
99
+ <Button appearance="ghost" size="sm">
100
+ Open
101
+ </Button>
102
+ </Header>
103
+ </StoryCell>
104
+ </StorySection>
105
+ ),
106
+ }
107
+
108
+ /**
109
+ * **`isSticky` sets `position` and `z-index` together, and that
110
+ * is the point.** mux-magic's `PageHeader` is documented as a
111
+ * sticky header and sets only the z-index, so it scrolls away
112
+ * like any other block — valid CSS, stated intent, no effect.
113
+ *
114
+ * Scroll the panel below. The z-index is `--layer-sticky` (100)
115
+ * from the token scale, so a `Menu` opened from the header — at
116
+ * `--layer-modal` (400) — still paints over it.
117
+ */
118
+ export const AllVariants: Story = {
119
+ render: (headerProps) => (
120
+ <div className="h-96 overflow-y-auto border border-border-subtle">
121
+ <Shell>
122
+ <Header
123
+ {...headerProps}
124
+ actions={<HeaderSchemeToggle />}
125
+ heading="Scroll me"
126
+ />
127
+
128
+ <Main>
129
+ <PageContent />
130
+
131
+ <ScrollFiller />
132
+ </Main>
133
+ </Shell>
134
+ </div>
135
+ ),
136
+ }
137
+
138
+ /**
139
+ * `isSticky={false}` — the header scrolls away with the page, and
140
+ * takes its z-index with it rather than leaving a stacking
141
+ * context nobody asked for. board-games' `AppShell.tsx` is this
142
+ * one: the same structure as mail-sifter's and points-market's,
143
+ * minus `sticky`.
144
+ */
145
+ export const NotSticky: Story = {
146
+ args: { heading: "Board Games", isSticky: false },
147
+ render: (headerProps) => (
148
+ <div className="h-96 overflow-y-auto border border-border-subtle">
149
+ <Shell>
150
+ <Header
151
+ {...headerProps}
152
+ actions={<HeaderSchemeToggle />}
153
+ />
154
+
155
+ <Main>
156
+ <PageContent />
157
+
158
+ <ScrollFiller />
159
+ </Main>
160
+ </Shell>
161
+ </div>
162
+ ),
163
+ }
164
+
165
+ /**
166
+ * The width contract. **The `<header>` is full-bleed and only its
167
+ * inner row is capped**, which is what stops a constrained header
168
+ * from looking like a floating island on an ultrawide — and the
169
+ * cap it uses is `Shell`'s, so the title always sits directly
170
+ * above the content it names.
171
+ *
172
+ * points-market is the app where those two disagree: its header
173
+ * row is capped at 80rem and its `<main>` is not capped at all.
174
+ */
175
+ export const Responsive: Story = {
176
+ render: (headerProps) => (
177
+ <Shell contentWidth="sm">
178
+ <Header
179
+ {...headerProps}
180
+ actions={<HeaderSchemeToggle />}
181
+ heading="Narrow by contract"
182
+ />
183
+
184
+ <Main>
185
+ <PageContent />
186
+ </Main>
187
+ </Shell>
188
+ ),
189
+ }
190
+
191
+ /**
192
+ * What an agent reaches for. The header is a landmark, so
193
+ * `getByRole("banner")` scopes every query inside it — which is
194
+ * how one of the three "Settings" buttons on a page becomes
195
+ * addressable.
196
+ */
197
+ export const Interactive: Story = {
198
+ args: { heading: "Rip Deck" },
199
+ render: (headerProps) => (
200
+ <Shell>
201
+ <Header
202
+ {...headerProps}
203
+ actions={
204
+ <>
205
+ <Button appearance="outline" size="sm">
206
+ Settings
207
+ </Button>
208
+
209
+ <HeaderSchemeToggle />
210
+ </>
211
+ }
212
+ />
213
+
214
+ <Main>
215
+ <Button appearance="outline" size="sm">
216
+ Settings
217
+ </Button>
218
+ </Main>
219
+ </Shell>
220
+ ),
221
+ }