@juwel-development/design-system 3.11.0 → 4.1.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 (70) hide show
  1. package/README.md +16 -3
  2. package/dist/design-system.js +1094 -546
  3. package/dist/styles.css +2 -0
  4. package/dist/styles.dark.css +2 -0
  5. package/dist/styles.light.css +2 -0
  6. package/dist/types/Arrangement/ColumnLayout/ColumnLayout.d.ts +38 -18
  7. package/dist/types/Display/Callout/Callout.d.ts +31 -0
  8. package/dist/types/Display/Icon/Icon.d.ts +20 -24
  9. package/dist/types/Display/Meter/Meter.d.ts +5 -6
  10. package/dist/types/Display/Rule/Rule.d.ts +22 -0
  11. package/dist/types/Display/Table/Table.d.ts +57 -7
  12. package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +1 -1
  13. package/dist/types/Display/Typography/H1/H1.d.ts +1 -1
  14. package/dist/types/Display/Typography/H2/H2.d.ts +1 -1
  15. package/dist/types/Display/Typography/H3/H3.d.ts +1 -1
  16. package/dist/types/Display/Typography/H4/H4.d.ts +1 -1
  17. package/dist/types/Display/Typography/H5/H5.d.ts +1 -1
  18. package/dist/types/Display/Typography/H6/H6.d.ts +1 -1
  19. package/dist/types/Display/Typography/Note/Note.d.ts +16 -3
  20. package/dist/types/Display/Typography/P/P.d.ts +15 -2
  21. package/dist/types/Display/Typography/Prose/Prose.d.ts +14 -6
  22. package/dist/types/Interaction/Button/Button.d.ts +10 -4
  23. package/dist/types/Interaction/Slider/Slider.d.ts +3 -0
  24. package/dist/types/Interaction/Stepper/Stepper.d.ts +59 -0
  25. package/dist/types/Interaction/Stepper/StepperCompositionError.d.ts +3 -0
  26. package/dist/types/Interaction/Switch/Switch.d.ts +20 -0
  27. package/dist/types/Interaction/Tabs/Tabs.d.ts +24 -3
  28. package/dist/types/Layout/Dialog/Dialog.d.ts +11 -3
  29. package/dist/types/Layout/Form/FormConfigurationError.d.ts +3 -0
  30. package/dist/types/Layout/Pane/Pane.d.ts +57 -0
  31. package/dist/types/Layout/Screen/Screen.d.ts +31 -0
  32. package/dist/types/Layout/Screen/ScreenBodyAllocation.d.ts +1 -0
  33. package/dist/types/Layout/Shell/Shell.d.ts +39 -0
  34. package/dist/types/Layout/Sidebar/Sidebar.d.ts +16 -2
  35. package/dist/types/Theme/Palette.d.ts +11 -4
  36. package/dist/types/index.d.ts +22 -1
  37. package/package.json +18 -8
  38. package/src/Arrangement/ColumnLayout/ColumnLayout.tsx +86 -28
  39. package/src/Display/Callout/Callout.tsx +64 -0
  40. package/src/Display/Icon/Icon.tsx +53 -13
  41. package/src/Display/Meter/Meter.tsx +12 -13
  42. package/src/Display/Rule/Rule.tsx +31 -0
  43. package/src/Display/Table/Table.tsx +189 -24
  44. package/src/Display/Typography/Note/Note.tsx +27 -11
  45. package/src/Display/Typography/P/P.tsx +29 -4
  46. package/src/Display/Typography/Prose/Prose.tsx +44 -19
  47. package/src/Interaction/Button/Button.tsx +11 -4
  48. package/src/Interaction/Slider/Slider.tsx +3 -5
  49. package/src/Interaction/Stepper/Stepper.tsx +307 -0
  50. package/src/Interaction/Stepper/StepperCompositionError.ts +6 -0
  51. package/src/Interaction/Switch/Switch.tsx +64 -0
  52. package/src/Interaction/Tabs/Tabs.tsx +44 -10
  53. package/src/Layout/Dialog/Dialog.tsx +19 -6
  54. package/src/Layout/Form/Form.tsx +2 -1
  55. package/src/Layout/Form/FormConfigurationError.ts +6 -0
  56. package/src/Layout/Pane/Pane.tsx +272 -0
  57. package/src/Layout/Screen/Screen.tsx +61 -0
  58. package/src/Layout/Screen/ScreenBodyAllocation.ts +5 -0
  59. package/src/Layout/Shell/Shell.tsx +124 -0
  60. package/src/Layout/Sidebar/Sidebar.tsx +31 -7
  61. package/src/Theme/Palette.ts +17 -6
  62. package/src/Theme/renderTokens.ts +61 -12
  63. package/src/index.ts +22 -2
  64. package/src/styles.css +5 -6
  65. package/src/styles.dark.css +5 -6
  66. package/src/styles.light.css +5 -6
  67. package/src/tokens.css +54 -9
  68. package/src/tokens.dark.css +51 -8
  69. package/src/tokens.light.css +49 -6
  70. package/dist/index.css +0 -2
@@ -0,0 +1,124 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
+ import {
3
+ type FunctionComponent,
4
+ type ReactNode,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+
10
+ const shell = cva(
11
+ 'h-dvh min-w-0 overflow-clip grid grid-rows-[auto_minmax(0,1fr)]',
12
+ );
13
+ const shellBody = cva(
14
+ 'min-h-0 min-w-0 overflow-auto overscroll-contain p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))] scroll-p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))] outline-focus-ring focus-visible:outline focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-[calc(-1*var(--focus-ring-width))]',
15
+ {
16
+ variants: {
17
+ mode: { console: '[&>div]:h-full', page: '[&>div]:min-h-full' },
18
+ },
19
+ defaultVariants: { mode: 'console' },
20
+ },
21
+ );
22
+
23
+ // Geometry is read only to expose a keyboard stop; CSS alone allocates the height.
24
+ const useScrollableBody = (body: {
25
+ current: HTMLDivElement | null;
26
+ }): boolean => {
27
+ const [isScrollable, setIsScrollable] = useState(true);
28
+ useLayoutEffect(() => {
29
+ const element = body.current;
30
+ if (!element) return;
31
+ const measure = () =>
32
+ setIsScrollable(
33
+ element.scrollHeight > element.clientHeight ||
34
+ element.scrollWidth > element.clientWidth ||
35
+ element === element.ownerDocument.activeElement,
36
+ );
37
+ measure();
38
+ const resize =
39
+ typeof ResizeObserver === 'undefined'
40
+ ? undefined
41
+ : new ResizeObserver(measure);
42
+ const observeContent = () => {
43
+ resize?.disconnect();
44
+ resize?.observe(element);
45
+ for (const child of element.querySelectorAll('*')) resize?.observe(child);
46
+ measure();
47
+ };
48
+ observeContent();
49
+ const mutations = new MutationObserver(observeContent);
50
+ mutations.observe(element, {
51
+ childList: true,
52
+ subtree: true,
53
+ characterData: true,
54
+ });
55
+ window.addEventListener('resize', measure);
56
+ element.addEventListener('blur', measure);
57
+ return () => {
58
+ resize?.disconnect();
59
+ mutations.disconnect();
60
+ window.removeEventListener('resize', measure);
61
+ element.removeEventListener('blur', measure);
62
+ };
63
+ }, [body]);
64
+ return isScrollable;
65
+ };
66
+
67
+ export interface IShellProps extends VariantProps<typeof shellBody> {
68
+ /** Consumer-owned top bar; its content determines its height. */
69
+ bar: ReactNode;
70
+ /** Body content; receives the remaining height in console mode. */
71
+ children: ReactNode;
72
+ /** The body scroll group's name, exposed only while overflowing or focused. */
73
+ ariaLabel: string;
74
+ testId?: string;
75
+ }
76
+
77
+ /**
78
+ * A viewport-owning frame with a content-height bar and the remaining height for its body.
79
+ *
80
+ * @Guarantees
81
+ * - CSS allocates both tracks, including when bar content wraps. Header is unchanged.
82
+ * - Console (default) passes a finite height to its child; page lets body content grow.
83
+ * Both keep overflow inside the body, with the bar visible and document still.
84
+ * - Console body scrolling is a reachability fallback for content that cannot fit; bounded
85
+ * panels normally own their scrolling. Page deliberately scrolls the body as a whole.
86
+ * - Overflow makes the body a named, keyboard-reachable group, never a landmark. Removing
87
+ * overflow does not relocate focus. Native focus reveal and scroll keys remain intact.
88
+ * - Body padding reserves the shared focus ring's room, with an inset ring on the body itself.
89
+ *
90
+ * @CallerMustEnsure
91
+ * - Place one Shell at the viewport origin, with no outside margins, padding or siblings that
92
+ * add document extent. This is the application frame, not a nested fullscreen section.
93
+ * - The bar must leave usable body space. An arbitrarily tall bar cannot fit every viewport.
94
+ * - Supply the main landmark inside the body (inside Sidebar.Content when using Sidebar).
95
+ * - Intermediate consumer wrappers must preserve the allocated height for bounded panels;
96
+ * wrappers with height: 100% and min-height: 0, or display: contents, can pass it through.
97
+ * - Use Sidebar.Root fill in a console; below 64rem its list stacks and the body scrolls.
98
+ * - Name the body in the viewer's language. Content and its panel arrangements remain yours.
99
+ */
100
+ export const Shell: FunctionComponent<IShellProps> = ({
101
+ bar,
102
+ children,
103
+ ariaLabel,
104
+ mode,
105
+ testId,
106
+ }) => {
107
+ const body = useRef<HTMLDivElement>(null);
108
+ const isScrollable = useScrollableBody(body);
109
+ return (
110
+ <div className={shell()} data-testid={testId}>
111
+ <div>{bar}</div>
112
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: the group and its name are conditional together */}
113
+ <div
114
+ ref={body}
115
+ className={shellBody({ mode })}
116
+ role={isScrollable ? 'group' : undefined}
117
+ aria-label={isScrollable ? ariaLabel : undefined}
118
+ tabIndex={isScrollable ? 0 : undefined}
119
+ >
120
+ <div>{children}</div>
121
+ </div>
122
+ </div>
123
+ );
124
+ };
@@ -1,4 +1,4 @@
1
- import { cva } from 'class-variance-authority';
1
+ import { cva, type VariantProps } from 'class-variance-authority';
2
2
  import type { FunctionComponent, ReactNode, RefCallback } from 'react';
3
3
  import { Children, createContext, isValidElement, useContext } from 'react';
4
4
  import type { Subject } from 'rxjs';
@@ -12,10 +12,20 @@ const sidebarRoot = cva(
12
12
  'grid gap-[var(--space-region)] lg:grid-cols-[12rem_minmax(0,1fr)]',
13
13
  '[&>div:first-child]:border-solid [&>div:first-child]:border-border [&>div:first-child]:border-b [&>div:first-child]:pb-[var(--space-stack)]',
14
14
  'lg:[&>div:first-child]:border-b-0 lg:[&>div:first-child]:border-r lg:[&>div:first-child]:pb-0 lg:[&>div:first-child]:pr-[var(--space-stack)]',
15
- 'lg:[&>div:first-child>nav]:sticky lg:[&>div:first-child>nav]:top-0 lg:[&>div:first-child>nav]:max-h-[min(100dvh,var(--sidebar-scrollport-height,100dvh))] lg:[&>div:first-child>nav]:overflow-y-auto',
15
+ 'lg:[&>div:first-child>nav]:sticky lg:[&>div:first-child>nav]:top-0 lg:[&>div:first-child>nav]:overflow-y-auto',
16
16
  '[&>div:first-child>nav]:p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))] lg:[&>div:first-child>nav]:scroll-p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))]',
17
17
  '[&>div:first-child>nav>ul]:flex [&>div:first-child>nav>ul]:flex-col [&>div:first-child>nav>ul]:gap-[var(--space-stack)]',
18
18
  ].join(' '),
19
+ {
20
+ variants: {
21
+ fill: {
22
+ false:
23
+ 'lg:[&>div:first-child>nav]:max-h-[min(100dvh,var(--sidebar-scrollport-height,100dvh))]',
24
+ true: 'min-h-full lg:h-full lg:min-h-0 lg:grid-rows-[minmax(0,1fr)] lg:[&>div]:min-h-0 lg:[&>div]:h-full lg:[&>div:first-child>nav]:max-h-full',
25
+ },
26
+ },
27
+ defaultVariants: { fill: false },
28
+ },
19
29
  );
20
30
 
21
31
  // A percentage max-height resolves against the content-driven grid cell, not its scrollport;
@@ -38,9 +48,13 @@ const observeScrollport: RefCallback<HTMLElement> = (navigation) => {
38
48
  ),
39
49
  );
40
50
  if (scrollport) {
51
+ const scrollportStyle = getComputedStyle(scrollport);
52
+ const padding =
53
+ (Number.parseFloat(scrollportStyle.paddingTop) || 0) +
54
+ (Number.parseFloat(scrollportStyle.paddingBottom) || 0);
41
55
  navigation.style.setProperty(
42
56
  '--sidebar-scrollport-height',
43
- `${scrollport.clientHeight}px`,
57
+ `${Math.max(0, scrollport.clientHeight - padding)}px`,
44
58
  );
45
59
  } else {
46
60
  navigation.style.removeProperty('--sidebar-scrollport-height');
@@ -96,7 +110,10 @@ const SidebarSelectionContext = createContext<SidebarSelection | undefined>(
96
110
  undefined,
97
111
  );
98
112
 
99
- export interface ISidebarRootProps {
113
+ export interface ISidebarRootProps extends VariantProps<typeof sidebarRoot> {
114
+ /** Pass a finite parent height through to Content at 64rem and above, using CSS alone.
115
+ * Below 64rem the enclosing scrollable body owns the stacked list and content. */
116
+ fill?: VariantProps<typeof sidebarRoot>['fill'];
100
117
  /** The key of the active entry. The caller supplies a key identifying one non-inert entry;
101
118
  * Sidebar renders what it is given and never selects a fallback for an invalid key. */
102
119
  active: string;
@@ -166,12 +183,13 @@ const SidebarRoot: FunctionComponent<ISidebarRootProps> = ({
166
183
  onSelect$,
167
184
  children,
168
185
  testId,
186
+ fill,
169
187
  }) => {
170
188
  const parts = Children.toArray(children).filter(isValidElement);
171
189
  return (
172
- <div className={sidebarRoot()} data-testid={testId}>
190
+ <div className={sidebarRoot({ fill })} data-testid={testId}>
173
191
  <div>
174
- <nav ref={observeScrollport} aria-label={label}>
192
+ <nav ref={fill ? undefined : observeScrollport} aria-label={label}>
175
193
  <SidebarSelectionContext value={{ active, onSelect$ }}>
176
194
  <ul>{parts.filter((part) => part.type === SidebarItem)}</ul>
177
195
  </SidebarSelectionContext>
@@ -204,11 +222,17 @@ const SidebarRoot: FunctionComponent<ISidebarRootProps> = ({
204
222
  * assumed top-bar offset, capped to the screen/scrolling-area height with independent entry scrolling.
205
223
  * Content sits beside it in `minmax(0,1fr)`, so wide content cannot displace the track.
206
224
  * - Below 64rem the whole list lies above the content in normal flow - no stickiness, no cap, no
207
- * drawer. Sidebar sets no viewport-height minimum and grows with its content.
225
+ * drawer. Without fill, Sidebar grows with its content.
226
+ * - With fill, a finite parent height passes through Root and Content at 64rem and above;
227
+ * navigation is capped to that allocated height by CSS, with no scrollport measurement.
228
+ * Below 64rem the list stacks and the enclosing body scrolls. The DOM never switches.
208
229
  * - It separates the tracks itself (the region space role and a `border` hairline) but gives the
209
230
  * content track no padding and no landmark - the consumer owns everything inside `Content`.
210
231
  *
211
232
  * @CallerMustEnsure — the component cannot see these and does not check them
233
+ * - Fill requires a parent with a finite height and a scrollable enclosing body for narrow
234
+ * viewports or oversized fixed content. Consumer wrappers inside Content preserve that bound.
235
+ * It works with any bounded parent; no Shell identity, context or offset is required.
212
236
  * - Entry keys are unique and `active` names one non-inert entry. Sidebar corrects nothing.
213
237
  * - Items and the one Content are direct children of `Root` - Root assembles only what it can see,
214
238
  * and an Item rendered outside a Root has no selection to derive its treatment from.
@@ -10,7 +10,7 @@
10
10
  * Light and dark are two complete sets of the same roles rather than a set of `dark:` overrides
11
11
  * scattered through the components. A component therefore carries no dark-mode classes at all:
12
12
  * swapping the `.dark` class re-points the variables underneath it. Complete means `Required`: the
13
- * two roles the type marks optional are optional for a consumer's palette object, never here.
13
+ * roles the type marks optional are optional for a consumer's palette object, never here.
14
14
  *
15
15
  * The values below are still depot-tracker's brand (primary is its violet). They are carried over
16
16
  * so nothing changed visually during the extraction - a starting point to replace, not a decision.
@@ -18,6 +18,13 @@
18
18
  export type PaletteTokens = {
19
19
  /** Page and panel background. */
20
20
  surface: string;
21
+ /** Selected Table row surface, distinct from resting and hover surfaces. Keep foreground, muted
22
+ * link and status inks at 4.5:1; marker, rule and focusRing at 3:1 in each scheme.
23
+ * Optional for existing palette objects; CSS overrides inherit the shipped default. Consumers
24
+ * composing other inks or controls on this surface must ensure their contrast too. */
25
+ selectedSurface?: string;
26
+ /** Error ink/fill on a selected surface: 4.5:1 against selectedSurface and errorForeground. */
27
+ selectedError?: string;
21
28
  /** Default text colour on `surface`. */
22
29
  foreground: string;
23
30
  /** De-emphasised text - captions, help text, placeholders. */
@@ -42,11 +49,11 @@ export type PaletteTokens = {
42
49
 
43
50
  /** The themed veil behind a Dialog, marking the page beneath as present but unavailable. The one
44
51
  * role whose alpha is part of the colour - the page must show through, which a solid hex cannot
45
- * say - so it is an `rgb(r g b / a)` value, not a hex. Both shipped themes carry the same value;
46
- * the Dialog's modality never depends on the treatment, so no contrast constraint applies.
52
+ * say - so it is an `rgb(r g b / a)` value, not a hex. Constraint: compositing over surface
53
+ * must reduce its relative luminance by at least 50%; modality never depends on the treatment.
47
54
  *
48
55
  * Migration (added with Dialog): a consumer constructing its own `PaletteTokens` object must
49
- * add this role - `rgb(15 23 42 / 0.5)` is the shipped value. Spreading `light`/`dark` and
56
+ * add this role - `rgb(15 23 42 / 0.5)` is the light default. Spreading `light`/`dark` and
50
57
  * overriding is unaffected, and a theme that only overrides the generated custom properties
51
58
  * in CSS inherits the default. */
52
59
  scrim: string;
@@ -170,6 +177,8 @@ export type PaletteTokens = {
170
177
  */
171
178
  export const light: Required<PaletteTokens> = {
172
179
  surface: '#ffffff',
180
+ selectedSurface: '#f8fafc',
181
+ selectedError: '#cd2d7d',
173
182
  foreground: '#0f172a',
174
183
  muted: '#64748b',
175
184
  border: '#e2e8f0',
@@ -225,6 +234,8 @@ export const light: Required<PaletteTokens> = {
225
234
  */
226
235
  export const dark: Required<PaletteTokens> = {
227
236
  surface: '#0f172a',
237
+ selectedSurface: '#151e30',
238
+ selectedError: '#f48fb1',
228
239
  foreground: '#f8fafc',
229
240
  muted: '#94a3b8',
230
241
  border: '#334155',
@@ -232,7 +243,7 @@ export const dark: Required<PaletteTokens> = {
232
243
  rule: '#5b6a80',
233
244
  backing: '#1e293b',
234
245
 
235
- scrim: 'rgb(15 23 42 / 0.5)',
246
+ scrim: 'rgb(0 0 0 / 0.6)',
236
247
 
237
248
  meterFill: '#f8fafc',
238
249
  meterTrack: '#334155',
@@ -246,7 +257,7 @@ export const dark: Required<PaletteTokens> = {
246
257
  secondaryForeground: '#020617',
247
258
 
248
259
  disabled: '#475569',
249
- disabledHover: '#334155',
260
+ disabledHover: '#64748b',
250
261
 
251
262
  focusRing: '#cbd5e1',
252
263
 
@@ -96,14 +96,20 @@ const TYPOGRAPHY = `@theme {
96
96
  --font-primary: inherit;
97
97
  --font-secondary: inherit;
98
98
 
99
+ /* Visual emphasis for body copy and annotations (#135), independent of family and size. */
100
+ --font-weight-strong: 700;
101
+
99
102
  /* Type roles, not a scale: one role per job, so a component asks for what its text is, never a rung.
100
103
  display > title > subtitle are the three heading steps H1-H3 bind to; H4-H6 share body and split
101
104
  by weight, so there is no fourth role. display is the hero and must out-scale every subpage head. */
102
- --text-display: clamp(3rem, 7vw, 6rem);
105
+ /* At 1280, 1440 and 1920 CSS px (16px root), size(w / 2) / size(w) must be >= 0.75.
106
+ At 200% zoom the viewport halves, so physical growth is 2 * that ratio: at least 1.5x.
107
+ The rem terms preserve growth; at 1280px these sizes stay within 2px of the old vw clamps. */
108
+ --text-display: clamp(3rem, 3rem + 3.125vw, 6rem);
103
109
  --leading-display: 1.05;
104
- --text-title: clamp(2.25rem, 5vw, 4.25rem);
110
+ --text-title: clamp(2.25rem, 2.25rem + 2.25vw, 4.25rem);
105
111
  --leading-title: 1.1;
106
- --text-subtitle: clamp(1.5rem, 3vw, 2.25rem);
112
+ --text-subtitle: clamp(1.5rem, 1.5rem + 1vw, 2.25rem);
107
113
  --leading-subtitle: 1.2;
108
114
 
109
115
  /* The reading block's opening paragraph (Prose #21): sized above body and below subtitle, led
@@ -112,6 +118,10 @@ const TYPOGRAPHY = `@theme {
112
118
  subtitle was rejected in ADR 0004 - it welds the lede's size to H3's and its leading to a head's. */
113
119
  --text-lede: 1.25rem; /* 20px */
114
120
  --leading-lede: 1.4;
121
+
122
+ /* Whole correspondence blocks, independently of opening ledes (Prose #128). */
123
+ --text-letter: 1.25rem;
124
+ --leading-letter: 1.8;
115
125
  --text-body: 1.0625rem; /* 17px, floored at 16px: below it iOS Safari zooms the viewport when a control set at this role takes focus (#92) */
116
126
  --leading-body: 1.6;
117
127
  --text-small: 0.9375rem; /* 15px floor: below it tabular figures stop comparing column to column */
@@ -126,7 +136,7 @@ const TYPOGRAPHY = `@theme {
126
136
  /* The Dialog title role: the top heading of an independent transitory task, deliberately outside
127
137
  the page heading ladder (docs/adr/0005). Ships subtitle's values while staying an independent
128
138
  role, so a consumer re-points Dialog titles without moving every H3 (docs/adr/0004, Amendments). */
129
- --text-dialog-title: clamp(1.5rem, 3vw, 2.25rem);
139
+ --text-dialog-title: clamp(1.5rem, 1.5rem + 1vw, 2.25rem);
130
140
  --leading-dialog-title: 1.2;
131
141
 
132
142
  /* Two quantities on one property that must not collapse: --tracking-label is a fixed letter-spaced
@@ -147,6 +157,9 @@ const FAMILY_ROLES = `@utility font-heading {
147
157
  font-family: var(--font-heading, var(--font-primary));
148
158
  }
149
159
 
160
+ @utility font-letter {
161
+ font-family: var(--font-letter, var(--font-primary));
162
+ }
150
163
  @utility font-body {
151
164
  font-family: var(--font-body, var(--font-primary));
152
165
  }
@@ -161,6 +174,7 @@ const FAMILY_ROLES = `@utility font-heading {
161
174
  the action column, in rem (docs/adr/0008, Amendments, #97); 20rem clears --control-min-width. */
162
175
  const MEASURE = `:root {
163
176
  --measure: 66ch;
177
+ --measure-letter: 60ch;
164
178
  --measure-display: 36ch;
165
179
  --measure-wide: 72ch;
166
180
  --measure-action: 20rem;
@@ -218,6 +232,20 @@ const DEFINITION_SPACING = `:root {
218
232
  initial-value: 1px;
219
233
  }`;
220
234
 
235
+ // Resolve the fill row's width shortfall before its columns query the horizontal state.
236
+ const COLUMN_LAYOUT_FIT = `@property --column-layout-shortfall {
237
+ syntax: "<length>";
238
+ inherits: false;
239
+ initial-value: 1px;
240
+ }`;
241
+
242
+ /* Callout's leading mark and separation from its content (#131). The mark stays visible at a
243
+ positive CSS length; its inset accepts a nonnegative CSS length and follows inherited type. */
244
+ const CALLOUT_MARK = `:root {
245
+ --callout-mark-width: 0.25rem;
246
+ --space-callout-inset: 1em;
247
+ }`;
248
+
221
249
  /* Box's inner padding (#117): one role on all four sides, distinct from a sibling gap, a region's
222
250
  air or a page band, so a brand tunes every Box at once and no Box gains a padding prop (ADR 0008,
223
251
  the token-role test). In em like --space-collection-item, so it tracks the inherited type.
@@ -286,6 +314,16 @@ const TICK = `:root {
286
314
  --tick-thickness: 1px;
287
315
  }`;
288
316
 
317
+ /* Step geometry is independently themeable: positive marker size and line thicknesses,
318
+ nonnegative insets. The marker size must fit the consumer's font and ordinal count. */
319
+ const STEPPER = `:root {
320
+ --step-marker-size: 2em;
321
+ --step-inset-inline: 1em;
322
+ --step-inset-block: 0.5em;
323
+ --step-connector-thickness: 1px;
324
+ --step-selection-thickness: 2px;
325
+ }`;
326
+
289
327
  /* Tab label insets separate adjacent controls and give the label vertical breathing room.
290
328
  These are two axis-specific jobs, not a selectable scale; stack/region/band describe other jobs.
291
329
  Like those spacing roles, em tracks the label type. Non-negative values preserve the hit area. */
@@ -320,6 +358,13 @@ const SLIDER = `:root {
320
358
  --slider-thumb-size: 1.5rem;
321
359
  }`;
322
360
 
361
+ /* Switch geometry is independent of choice markers: its track holds two thumb positions.
362
+ Both lengths must be positive; track width derives from the thumb so positions stay distinct. */
363
+ const SWITCH = `:root {
364
+ --switch-thumb-size: 1rem;
365
+ --switch-track-inset: 0.125rem;
366
+ }`;
367
+
323
368
  /* The choice marker's two dimensions are not colours: like the tick they live in :root only,
324
369
  never @theme inline, so a brand re-points the marker without Choices gaining a prop. Sized in
325
370
  rem like the tick and the slider thumb. Constraints: both > 0 (a zero dot erases the one cue
@@ -363,13 +408,8 @@ const declarations = (
363
408
  .map((name) => ` --color-${toKebabCase(name)}: ${value(name)};`)
364
409
  .join('\n');
365
410
 
366
- // The library no longer declares --color-ring but still reads it as a fallback, so a consumer's
367
- // existing value keeps working; the plain hex in the palette is the default and what the contrast
368
- // test checks. See docs/adr/0002-focus-ring-token-contract.md.
369
- const raw = (tokens: Required<PaletteTokens>) => (name: string) => {
370
- const value = tokens[name as keyof PaletteTokens];
371
- return name === 'focusRing' ? `var(--color-ring, ${value})` : value;
372
- };
411
+ const raw = (tokens: Required<PaletteTokens>) => (name: string) =>
412
+ tokens[name as keyof PaletteTokens];
373
413
  const reference = () => (name: string) => `var(--color-${toKebabCase(name)})`;
374
414
 
375
415
  // Only the combined tokens.css carries the dark set; this gates it behind .dark and defines dark:.
@@ -406,7 +446,7 @@ ${UNDERLINE}
406
446
  utilities, so it is not carried in @theme inline with the palette. */
407
447
  ${TYPOGRAPHY}
408
448
 
409
- /* The heading, body and control family roles are utilities with a fallback to the primary face, not
449
+ /* The heading, body, letter and control family roles are utilities with a fallback to the primary face, not
410
450
  @theme variables, so a theme re-pointing either name at any scope reaches the text beneath it. */
411
451
  ${FAMILY_ROLES}
412
452
 
@@ -433,9 +473,14 @@ ${COLLECTION_SPACING}
433
473
  /* DefinitionList's item air and column minimums sit in :root beside the collection role (#123). */
434
474
  ${DEFINITION_SPACING}
435
475
 
476
+ ${COLUMN_LAYOUT_FIT}
477
+
436
478
  /* Box's inner padding, measured against inherited type, beside Collection's. */
437
479
  ${BOX_INSET}
438
480
 
481
+ /* Callout marks an edge without enclosing or arranging its content. */
482
+ ${CALLOUT_MARK}
483
+
439
484
  /* Table's cell insets, one pair per density, beside Box's. */
440
485
  ${TABLE_CELL_INSET}
441
486
 
@@ -459,11 +504,15 @@ ${TABLE_SELECTION_MARKER}
459
504
 
460
505
  ${TAB_INSETS}
461
506
 
507
+ ${STEPPER}
508
+
462
509
  ${SLIDER}
463
510
 
464
511
  /* The choice marker's dimensions are not colours either, and sit in :root beside the slider block. */
465
512
  ${CHOICE_MARKER}
466
513
 
514
+ ${SWITCH}
515
+
467
516
  /* The meter track's thickness is not a colour either, and sits in :root beside the tab blocks. */
468
517
  ${METER_TRACK}
469
518
 
package/src/index.ts CHANGED
@@ -1,20 +1,27 @@
1
- import './styles.css';
2
-
3
1
  export { Cluster } from 'Arrangement/Cluster/Cluster';
4
2
  export { ColumnLayout } from 'Arrangement/ColumnLayout/ColumnLayout';
3
+ export { ColumnLayoutCompositionError } from 'Arrangement/ColumnLayout/ColumnLayoutCompositionError';
4
+ export { ColumnLayoutConfigurationError } from 'Arrangement/ColumnLayout/ColumnLayoutConfigurationError';
5
5
  export { FieldRow } from 'Arrangement/FieldRow/FieldRow';
6
+ export { FieldRowCompositionError } from 'Arrangement/FieldRow/FieldRowCompositionError';
7
+ export { FieldRowConfigurationError } from 'Arrangement/FieldRow/FieldRowConfigurationError';
6
8
  export { Stack } from 'Arrangement/Stack/Stack';
7
9
  export { Box } from 'Display/Box/Box';
8
10
  export { Brandmark } from 'Display/Brandmark/Brandmark';
11
+ export { Callout } from 'Display/Callout/Callout';
9
12
  export { Checklist } from 'Display/Checklist/Checklist';
10
13
  export { Collection } from 'Display/Collection/Collection';
11
14
  export { DefinitionList } from 'Display/DefinitionList/DefinitionList';
15
+ export { DefinitionListConfigurationError } from 'Display/DefinitionList/DefinitionListConfigurationError';
12
16
  export { Figure } from 'Display/Figure/Figure';
13
17
  export { Icon } from 'Display/Icon/Icon';
14
18
  export { Meter } from 'Display/Meter/Meter';
19
+ export { MeterConfigurationError } from 'Display/Meter/MeterConfigurationError';
15
20
  export { Rail } from 'Display/Rail/Rail';
21
+ export { Rule } from 'Display/Rule/Rule';
16
22
  export type { TableColumnAllocation } from 'Display/Table/Table';
17
23
  export { Table } from 'Display/Table/Table';
24
+ export { TableConfigurationError } from 'Display/Table/TableConfigurationError';
18
25
  export { Eyebrow } from 'Display/Typography/Eyebrow/Eyebrow';
19
26
  export { H1 } from 'Display/Typography/H1/H1';
20
27
  export { H2 } from 'Display/Typography/H2/H2';
@@ -27,24 +34,37 @@ export { P } from 'Display/Typography/P/P';
27
34
  export { Prose } from 'Display/Typography/Prose/Prose';
28
35
  export { Button } from 'Interaction/Button/Button';
29
36
  export { Choices } from 'Interaction/Choices/Choices';
37
+ export { ChoicesCompositionError } from 'Interaction/Choices/ChoicesCompositionError';
30
38
  export { Input } from 'Interaction/Input/Input';
31
39
  export { Link } from 'Interaction/Link/Link';
32
40
  export { MultiSelect } from 'Interaction/MultiSelect/MultiSelect';
41
+ export { MultiSelectCompositionError } from 'Interaction/MultiSelect/MultiSelectCompositionError';
33
42
  export { NumberInput } from 'Interaction/NumberInput/NumberInput';
34
43
  export { Select } from 'Interaction/Select/Select';
35
44
  export { Slider } from 'Interaction/Slider/Slider';
45
+ export { SliderConfigurationError } from 'Interaction/Slider/SliderConfigurationError';
46
+ export { Stepper } from 'Interaction/Stepper/Stepper';
47
+ export { StepperCompositionError } from 'Interaction/Stepper/StepperCompositionError';
48
+ export { Switch } from 'Interaction/Switch/Switch';
36
49
  export { Tabs } from 'Interaction/Tabs/Tabs';
50
+ export { TabsCompositionError } from 'Interaction/Tabs/TabsCompositionError';
37
51
  export { TextArea } from 'Interaction/TextArea/TextArea';
38
52
  export { Cover } from 'Layout/Cover/Cover';
39
53
  export { Dialog } from 'Layout/Dialog/Dialog';
54
+ export { DialogCompositionError } from 'Layout/Dialog/DialogCompositionError';
55
+ export { DialogNamingError } from 'Layout/Dialog/DialogNamingError';
40
56
  export { Footer } from 'Layout/Footer/Footer';
41
57
  export type { FormState } from 'Layout/Form/Form';
42
58
  export { Form } from 'Layout/Form/Form';
59
+ export { FormConfigurationError } from 'Layout/Form/FormConfigurationError';
43
60
  export { Header } from 'Layout/Header/Header';
44
61
  export { Hero } from 'Layout/Hero/Hero';
45
62
  export { PageHead } from 'Layout/PageHead/PageHead';
63
+ export { Pane } from 'Layout/Pane/Pane';
64
+ export { Screen } from 'Layout/Screen/Screen';
46
65
  export { ScrollContainer } from 'Layout/ScrollContainer/ScrollContainer';
47
66
  export { Section } from 'Layout/Section/Section';
67
+ export { Shell } from 'Layout/Shell/Shell';
48
68
  export { Sidebar } from 'Layout/Sidebar/Sidebar';
49
69
  export type { PaletteTokens } from 'Theme/Palette';
50
70
  export { dark, light } from 'Theme/Palette';
package/src/styles.css CHANGED
@@ -1,9 +1,8 @@
1
1
  /* Component-level styles for the design system. */
2
- @import "tailwindcss";
2
+ @import "tailwindcss" source(none);
3
3
  @import "./tokens.css";
4
4
 
5
- /* Scan the component sources explicitly so their utilities are generated no matter
6
- which host imports this stylesheet. Tailwind's automatic content detection is
7
- anchored to the consumer and skips node_modules, so a host pulling the components
8
- in through a package would otherwise render them unstyled. */
9
- @source ".";
5
+ /* Only component code contributes utilities to the published stylesheets. */
6
+ @source "./**/*.tsx";
7
+ @source not "./**/*.spec.tsx";
8
+ @source not "./**/*.stories.tsx";
@@ -1,9 +1,8 @@
1
1
  /* Component-level styles for the design system, dark theme only (issue #62). */
2
- @import "tailwindcss";
2
+ @import "tailwindcss" source(none);
3
3
  @import "./tokens.dark.css";
4
4
 
5
- /* Scan the component sources explicitly so their utilities are generated no matter
6
- which host imports this stylesheet. Tailwind's automatic content detection is
7
- anchored to the consumer and skips node_modules, so a host pulling the components
8
- in through a package would otherwise render them unstyled. */
9
- @source ".";
5
+ /* Only component code contributes utilities to the published stylesheets. */
6
+ @source "./**/*.tsx";
7
+ @source not "./**/*.spec.tsx";
8
+ @source not "./**/*.stories.tsx";
@@ -1,9 +1,8 @@
1
1
  /* Component-level styles for the design system, light theme only (issue #62). */
2
- @import "tailwindcss";
2
+ @import "tailwindcss" source(none);
3
3
  @import "./tokens.light.css";
4
4
 
5
- /* Scan the component sources explicitly so their utilities are generated no matter
6
- which host imports this stylesheet. Tailwind's automatic content detection is
7
- anchored to the consumer and skips node_modules, so a host pulling the components
8
- in through a package would otherwise render them unstyled. */
9
- @source ".";
5
+ /* Only component code contributes utilities to the published stylesheets. */
6
+ @source "./**/*.tsx";
7
+ @source not "./**/*.spec.tsx";
8
+ @source not "./**/*.stories.tsx";