@juwel-development/design-system 2.1.1 → 3.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 (40) hide show
  1. package/README.md +31 -0
  2. package/dist/design-system.js +246 -170
  3. package/dist/index.css +1 -1
  4. package/dist/types/Arrangement/Cluster/Cluster.d.ts +50 -0
  5. package/dist/types/Arrangement/Stack/Stack.d.ts +46 -0
  6. package/dist/types/Display/Figure/Figure.d.ts +21 -4
  7. package/dist/types/Display/Rail/Rail.d.ts +6 -0
  8. package/dist/types/Display/Typography/H1/H1.d.ts +7 -1
  9. package/dist/types/Display/Typography/Note/Note.d.ts +35 -0
  10. package/dist/types/Interaction/Input/Input.d.ts +5 -0
  11. package/dist/types/Interaction/Link/Link.d.ts +4 -4
  12. package/dist/types/Interaction/TextArea/TextArea.d.ts +5 -0
  13. package/dist/types/Layout/Form/Form.d.ts +8 -4
  14. package/dist/types/Layout/Header/Header.d.ts +20 -1
  15. package/dist/types/Layout/Hero/Hero.d.ts +9 -7
  16. package/dist/types/Layout/Section/Section.d.ts +7 -0
  17. package/dist/types/Theme/Palette.d.ts +55 -6
  18. package/dist/types/index.d.ts +3 -0
  19. package/package.json +2 -6
  20. package/src/Arrangement/Cluster/Cluster.tsx +80 -0
  21. package/src/Arrangement/Stack/Stack.tsx +75 -0
  22. package/src/Display/Figure/Figure.tsx +48 -7
  23. package/src/Display/Rail/Rail.tsx +6 -0
  24. package/src/Display/Typography/H1/H1.tsx +17 -7
  25. package/src/Display/Typography/Note/Note.tsx +56 -0
  26. package/src/Interaction/Button/Button.tsx +4 -1
  27. package/src/Interaction/Input/Input.tsx +25 -6
  28. package/src/Interaction/Link/Link.tsx +9 -5
  29. package/src/Interaction/TextArea/TextArea.tsx +25 -6
  30. package/src/Layout/Form/Form.tsx +24 -8
  31. package/src/Layout/Header/Header.tsx +37 -4
  32. package/src/Layout/Hero/Hero.tsx +10 -6
  33. package/src/Layout/PageHead/PageHead.tsx +2 -0
  34. package/src/Layout/Section/Section.tsx +11 -0
  35. package/src/Theme/Palette.ts +63 -14
  36. package/src/Theme/renderTokens.ts +18 -1
  37. package/src/index.ts +3 -0
  38. package/src/tokens.css +20 -9
  39. package/src/tokens.dark.css +16 -5
  40. package/src/tokens.light.css +16 -5
@@ -8,8 +8,9 @@ export type FormState = 'idle' | 'sending' | 'sent' | 'failed';
8
8
  // The one recipe, and it paints the note only (issue #6): `state` selects the note's tone - muted
9
9
  // while the form stands, success on the outcome, error on the failure. Region selection is the
10
10
  // NOTE_ROLE map below, not classes, so this axis never grows a layout job. Colours are semantic
11
- // tokens re-pointed by `.dark`, so no variant carries a `dark:` class.
12
- const form = cva('text-sm', {
11
+ // tokens re-pointed by `.dark`, so no variant carries a `dark:` class. The face joins the size in
12
+ // the base, so no state can disagree with it (docs/adr/0004, #90).
13
+ const form = cva('font-secondary text-small', {
13
14
  variants: {
14
15
  state: {
15
16
  idle: 'text-muted',
@@ -37,8 +38,12 @@ interface IFormProps {
37
38
  children?: ReactNode;
38
39
  /** The actions row - the consumer supplies its own submit Button. */
39
40
  actions?: ReactNode;
40
- /** Standing note in `idle`/`sending`; the outcome message in `sent`/`failed`. */
41
- note?: string;
41
+ /** Standing note in `idle`/`sending`; the outcome message in `sent`/`failed`. Inline content: a
42
+ * sentence, with a Link or emphasis inside it. Form renders the paragraph the note sits in and
43
+ * owns its ARIA role, so a node that brings its own block element - a second `<p>` - is unnested
44
+ * by the parser and the text leaves the region carrying that role. A node that renders nothing
45
+ * (`null`, `false`, an omitted prop) is no note: the paragraph is not rendered at all. */
46
+ note?: ReactNode;
42
47
  testId?: string;
43
48
  }
44
49
 
@@ -51,8 +56,8 @@ interface IFormProps {
51
56
  * `sent` drops the fields and actions so a completed submission cannot be resubmitted, while the
52
57
  * `<form>` itself is retained in every state so the DOM shape is stable across a runtime change.
53
58
  *
54
- * The note's text is always the consumer's; Form chooses only its element and ARIA role. `sent` is
55
- * the outcome message, so a `sent` with no `note` is a programmer error and throws.
59
+ * The note's content is always the consumer's; Form chooses only its element and ARIA role. `sent`
60
+ * is the outcome message, so a `sent` with no `note` is a programmer error and throws.
56
61
  */
57
62
  export const Form: FunctionComponent<IFormProps> = ({
58
63
  action,
@@ -63,7 +68,16 @@ export const Form: FunctionComponent<IFormProps> = ({
63
68
  note,
64
69
  testId,
65
70
  }) => {
66
- if (state === 'sent' && !note) {
71
+ // Absence, not falsiness: a ReactNode may be falsy and still render (`0` renders as `"0"`), so a
72
+ // truthiness test would both refuse a legitimate note in `sent` and drop a paragraph React had
73
+ // written a value into. Absence is React's own set of nothing-to-render nodes rather than
74
+ // `undefined` alone, because `note={showPrivacy && <>...</>}` hands over `false` when the line is
75
+ // off - and an empty `<p>` is still a flex item, so it would cost a region gap and, in `sent`, a
76
+ // status region announcing nothing.
77
+ const hasNote =
78
+ note !== undefined && note !== null && typeof note !== 'boolean';
79
+
80
+ if (state === 'sent' && !hasNote) {
67
81
  throw new Error(
68
82
  'Form in the `sent` state must be given a `note` - it is the outcome message.',
69
83
  );
@@ -87,12 +101,14 @@ export const Form: FunctionComponent<IFormProps> = ({
87
101
  {children}
88
102
  </div>
89
103
  )}
104
+ {/* Not pinned against Stack (#75) as the two columns above are: the actions row runs along the
105
+ other axis, which is Cluster's arrangement (#76) and not a stack with a different gap. */}
90
106
  {showFields && actions && (
91
107
  <div className={'flex flex-row gap-[var(--space-stack)]'}>
92
108
  {actions}
93
109
  </div>
94
110
  )}
95
- {note && (
111
+ {hasNote && (
96
112
  <p role={noteRole} className={form({ state })}>
97
113
  {note}
98
114
  </p>
@@ -2,7 +2,7 @@ import type { VariantProps } from 'class-variance-authority';
2
2
  import { cva } from 'class-variance-authority';
3
3
  import type { FunctionComponent, ReactNode } from 'react';
4
4
 
5
- // One recipe on the <header>. It sets the label type role - the "small grotesk, letter-spaced, muted"
5
+ // The recipe on the <header>. It sets the label type role - the "small grotesk, letter-spaced, muted"
6
6
  // the issue described, whose "never grows past 1rem" was a role wearing a number, so no size literal
7
7
  // appears here (#14). The shell's air is one value in every direction: --space-region above, below and
8
8
  // (on the nav) between, --gutter across, so the bar aligns with every inset Section. The current-page
@@ -15,7 +15,7 @@ import type { FunctionComponent, ReactNode } from 'react';
15
15
  const header = cva(
16
16
  [
17
17
  'flex items-baseline justify-between',
18
- 'font-secondary text-label tracking-label text-muted',
18
+ 'font-secondary text-label leading-label tracking-label text-muted',
19
19
  'py-[var(--space-region)] px-[var(--gutter)]',
20
20
  '[&_[aria-current=page]]:text-foreground',
21
21
  ].join(' '),
@@ -30,6 +30,20 @@ const header = cva(
30
30
  },
31
31
  );
32
32
 
33
+ // Not a second recipe - the standing slot has nothing to vary, and the standard allows a component
34
+ // one cva() (design-system-components.md §4), which is the bar's own above. A named constant beside
35
+ // the nav's inline class string, so the comment has something to sit on.
36
+ //
37
+ // The height floor is the nav's own line box, written from the two tokens the recipe above sets the
38
+ // header from, so the declared floor and the rendered line cannot drift (#81). `shrink-0` because an
39
+ // explicit min-width replaces a flex item's automatic minimum - without it the bar squeezes the slot
40
+ // below its content and wraps the place name the floor exists to keep on one line.
41
+ const standingSlot = [
42
+ 'inline-flex shrink-0 items-center',
43
+ 'min-h-[calc(var(--text-label)*var(--leading-label))]',
44
+ 'min-w-[var(--standing-min-width)]',
45
+ ].join(' ');
46
+
33
47
  export interface IHeaderProps extends VariantProps<typeof header> {
34
48
  /** The standing link: a place name, a mark, or a home link. The consumer supplies the whole anchor -
35
49
  * Header renders no link of its own. A place name uses `<Link treatment="quiet" href="/">…</Link>`; a
@@ -49,7 +63,14 @@ export interface IHeaderProps extends VariantProps<typeof header> {
49
63
  *
50
64
  * @Guarantees — enforced on every render
51
65
  * - The header is set at the label role and carries no size of its own: `font-secondary text-label
52
- * tracking-label text-muted`, so it never grows past that role whatever the page font size.
66
+ * leading-label tracking-label text-muted`, so it never grows past that role whatever the page font size.
67
+ * - The nav's line box is the library's own rather than the consuming document's: the header leads
68
+ * itself at the label role, so the height it declares for its slot is the height it renders.
69
+ * - The standing slot is floored, never fixed: its minimum height is that same line box - the label
70
+ * role's size times its leading - its minimum width is `--standing-min-width`, and its contents are
71
+ * centred. Standing content inside both floors leaves the bar the same height from route to route,
72
+ * so a place name on one page and a mark on another do not move the shell; content past either floor
73
+ * grows the slot, on one line, and is never clamped or wrapped.
53
74
  * - A nav item marked `aria-current="page"` renders at `foreground` whatever supplied it - the treatment
54
75
  * keys on the attribute, not on `Link`, so it holds against a bare anchor or any component.
55
76
  * - The shell's air is one value above, below and between: `--space-region` vertically and between nav
@@ -57,6 +78,18 @@ export interface IHeaderProps extends VariantProps<typeof header> {
57
78
  * - It is never sticky and needs no JavaScript: there is no `sticky` variant and nothing to hydrate.
58
79
  * - Omitting `navName` emits no `aria-label` at all, not an empty one.
59
80
  *
81
+ * @CallerMustEnsure — the component cannot see these and does not check them
82
+ * - A mark that should *fill* the standing slot is given a **definite width** by whoever placed it -
83
+ * `width: var(--standing-min-width)` on the element wrapping it, so the mark and the floor move
84
+ * together when a theme re-points the token. Measured against a `viewBox`-only SVG, the common
85
+ * shape: it has no intrinsic width to fill from, and `width: 100%` cannot resolve against the slot's
86
+ * indefinite basis - so a mark told to fill that way renders at its intrinsic width instead (300px in
87
+ * a slot floored at 120px, against 120px on the place-name route) and the floor stops governing,
88
+ * which is the jump between routes the floor exists to remove. A definite width holds at any bar
89
+ * width; `flex: 1 1 0; min-width: 0` only collapses the mark when the bar is already out of room, so
90
+ * it is not the rule to reach for. The library cannot apply either rule for you: the same width on a
91
+ * place name would clamp the name and wrap it.
92
+ *
60
93
  * @UXGuidelines
61
94
  * - Name the nav with `navName` once the page has more than one navigation landmark - a footer nav will
62
95
  * be the second, and two unnamed navs are indistinguishable to a screen-reader user.
@@ -73,7 +106,7 @@ export const Header: FunctionComponent<IHeaderProps> = ({
73
106
  testId,
74
107
  }) => (
75
108
  <header className={header({ edge })} data-testid={testId}>
76
- <div>{standing}</div>
109
+ <div className={standingSlot}>{standing}</div>
77
110
  <nav
78
111
  aria-label={navName}
79
112
  className={'flex flex-wrap items-baseline gap-[var(--space-region)]'}
@@ -2,6 +2,8 @@ import type { VariantProps } from 'class-variance-authority';
2
2
  import { cva } from 'class-variance-authority';
3
3
  import type { FunctionComponent, ReactNode } from 'react';
4
4
 
5
+ // Not pinned against Stack (#75): the fold height is this component's own, so the base legitimately
6
+ // differs and an equality spec would only forbid a difference that is the point of the component.
5
7
  // One recipe on a plain <div>. It holds the fold - a minimum height taller than its content, so leftover
6
8
  // space is guaranteed and how the slot sits in it is a decision, not a default (#17). With no className
7
9
  // escape hatch, whatever place picks is final, so it is a variant: center is the conventional hero and the
@@ -52,12 +54,14 @@ export interface IHeroProps extends VariantProps<typeof hero> {
52
54
  * bleed, the band, the gutter and the region. `Hero` imports no component and adds none of these.
53
55
  *
54
56
  * @UXGuidelines
55
- * - Cap the lead at `--measure-display`, not at a caption's width, and apply it to your own lead - not
56
- * here, since capping the slot would cap the mark too. Measured on the lockup that prompted this: at
57
- * 24ch a one-sentence lead set five lines and became a second block competing with the mark; at 36ch it
58
- * sets three and reads as the mark's caption. This is the one place where copy length is a layout
59
- * parameter - a hero whose lead sentence can vary in length needs its measure chosen against the real
60
- * sentence, not a rule of thumb.
57
+ * - The lead is capped at `--measure-display`, and `H1` is what carries the cap - place one in the slot
58
+ * and the bound comes with it (#87). The slot itself stays uncapped, because capping it would cap the
59
+ * mark too. Measured on the lockup that prompted this: at 24ch a one-sentence lead set five lines and
60
+ * became a second block competing with the mark; at 36ch, where the token sits, it sets three and reads
61
+ * as the mark's caption. This is the one place where copy length is a layout parameter, and the token
62
+ * is one value for the whole product - so a hero whose lead runs longer than that measurement assumed
63
+ * is a reason to re-measure `--measure-display` against the real sentence, which moves every `H1` with
64
+ * it, and never a reason to cap that one hero by hand.
61
65
  * - A hero built on a wide lockup is a desktop bet by construction. A 15-character lockup can only be as
62
66
  * wide as the phone, so at 390px the scale contrast between mark and lead is capped by the viewport
63
67
  * rather than chosen - roughly 2:1, against roughly 6:1 at 1440.
@@ -10,6 +10,8 @@ import type { FunctionComponent, ReactNode } from 'react';
10
10
  // literal appears. That tracking is the title role's, not this component's (#57): H2 binds to the same
11
11
  // role and carries the same correction, so the role reads one way wherever it is rendered. The stack gap is --space-stack, the sibling gap Prose uses. Colours are semantic
12
12
  // tokens re-pointed by `.dark`, so no `dark:` class.
13
+ // Not pinned against Stack (#75): the band and the gutter are the head's own page job, so the base
14
+ // legitimately differs and there is no equality to hold - a Stack carries neither.
13
15
  const pageHead = cva(
14
16
  'flex flex-col gap-[var(--space-stack)] py-[var(--space-band)] px-[var(--gutter)]',
15
17
  );
@@ -16,6 +16,10 @@ import type { FunctionComponent, ReactNode } from 'react';
16
16
  // `.dark`, so no `dark:` class.
17
17
  const section = cva(
18
18
  [
19
+ // Anchor, not placement: a consumer hangs its own decoration off the join, and cannot get an anchor
20
+ // by wrapping the section without costing two joins. No offset and no z-index, so the layout is
21
+ // unchanged and no stacking context forms - decoration at z-index:-1 must still escape to an outer one.
22
+ 'relative',
19
23
  'py-[var(--space-band)]',
20
24
  '[[data-section]+&]:border-t [[data-section]+&]:border-solid [[data-section]+&]:border-rule',
21
25
  ].join(' '),
@@ -51,10 +55,17 @@ export interface ISectionProps extends VariantProps<typeof section> {
51
55
  * - A `bleed="full"` section drops the gutter and runs edge to edge; the vertical band is kept on both
52
56
  * bleed variants, and the join spans the full width of either.
53
57
  * - An unnamed section is not a landmark: it emits no `aria-label` and is inert to assistive technology.
58
+ * - The section is a positioning context, under either bleed, so a consumer may place absolutely-positioned
59
+ * decoration against its edges - including the join above it, the one edge only a section knows. It sets
60
+ * no offset and no `z-index`, so it is not a stacking context and decoration at a negative `z-index`
61
+ * still resolves against an outer one.
54
62
  *
55
63
  * @CallerMustEnsure
56
64
  * - Where `name` is given it matches the section's visible heading. The component labels the region with
57
65
  * that string because it cannot reach the heading's id to reference it with `aria-labelledby` instead.
66
+ * - A descendant meant to position against an ancestor *outside* the section is given its own positioned
67
+ * wrapper, since the section is now the nearer positioned ancestor and takes the anchor. The
68
+ * re-anchoring is silent - no error and no warning, only a descendant that lands somewhere else.
58
69
  *
59
70
  * @UXGuidelines
60
71
  * - On a page with no borrowed proof - no logos, no testimonials, no credits - a gap between blocks reads
@@ -39,15 +39,36 @@ export type PaletteTokens = {
39
39
  * keeps it near `surface` rather than a mid grey. */
40
40
  backing: string;
41
41
 
42
- /** The main call-to-action fill. */
42
+ /** The main call-to-action fill. A filled control draws no border, so the fill is the only thing
43
+ * separating the control from the surface - what `controlBorder` is for an unfilled one.
44
+ * Constraint (WCAG 2.2 SC 1.4.11): at least 3:1 against `surface` in the same theme,
45
+ * `primaryHover` included, since a hovered control has to stay identifiable too. A second
46
+ * constraint governs this value and `primaryHover` from the other side, stated on
47
+ * `primaryForeground` - the ink they carry - rather than restated here. */
43
48
  primary: string;
44
49
  primaryHover: string;
45
- /** Text and icons drawn on top of `primary`. */
50
+ /** Text and icons drawn on top of `primary`. Constraint (WCAG 2.2 SC 1.4.3): at least 4.5:1
51
+ * against both `primary` and `primaryHover` in the same theme, hover included, since a hovered
52
+ * control has to stay readable too. Button text is normal-weight at the `body` role, so it takes
53
+ * the text threshold rather than the 3:1 large-text allowance. This is what makes the ink follow
54
+ * the theme where the fills' own roles do not: a fill light enough to clear 3:1 against a dark
55
+ * surface is too light to carry near-white text, so the ink sits at the *surface's* end of the
56
+ * neutral range in each theme. Not required against `disabled` or `disabledHover` - a disabled
57
+ * control is an inactive user interface component, which SC 1.4.3 exempts. */
46
58
  primaryForeground: string;
47
59
 
48
- /** The alternative action fill, for choices that sit beside a primary one. */
60
+ /** The alternative action fill, for choices that sit beside a primary one. Filled like `primary`
61
+ * and so identified the same way. Constraint (WCAG 2.2 SC 1.4.11): at least 3:1 against
62
+ * `surface` in the same theme, `secondaryHover` included. A second constraint governs this value
63
+ * and `secondaryHover` from the other side, stated on `secondaryForeground`. */
49
64
  secondary: string;
50
65
  secondaryHover: string;
66
+ /** Text and icons drawn on top of `secondary`. Constraint (WCAG 2.2 SC 1.4.3): at least 4.5:1
67
+ * against both `secondary` and `secondaryHover` in the same theme, hover included, since a
68
+ * hovered control has to stay readable too. Not required against `disabled` or `disabledHover` -
69
+ * a disabled control is an inactive user interface component, which SC 1.4.3 exempts. Why the
70
+ * threshold is 4.5 and not 3, and why this makes the ink follow the theme: see
71
+ * `primaryForeground`, which carries the same rule for the other ramp. */
51
72
  secondaryForeground: string;
52
73
 
53
74
  /** The tone a control takes when it cannot be interacted with - its fill, or its border when
@@ -75,6 +96,25 @@ export type PaletteTokens = {
75
96
  info: string;
76
97
  };
77
98
 
99
+ /**
100
+ * The ink follows the theme, and that is what set the eight fill and ink values across both sets
101
+ * (issue #93). Solve the window a fill has to sit in - at least 3:1 against its surface (#78) and
102
+ * at least 4.5:1 against the ink drawn on it - and it comes out lopsided: a near-white ink is
103
+ * unbounded above in light and leaves a window just 1.26x wide in dark, and a near-black ink is the
104
+ * exact inverse. The reason is structural - in a dark theme a fill has to be light enough to
105
+ * separate from a near-black surface, and a light fill wants dark text - so pinning the ink
106
+ * near-white in both themes asks a dark fill to be both at once. That 1.26x has to hold two values,
107
+ * rest and hover, where what shipped before it stepped 1.35x and 1.23x here and 1.35x and 1.48x in
108
+ * dark. Letting the ink invert instead puts every value on a stock ramp step, with hover steps of
109
+ * 1.25x and 1.27x here and 1.56x and 1.48x in dark.
110
+ *
111
+ * Two routes were rejected, and they are what a reader arriving here is most likely to re-propose:
112
+ * - Keep one near-white ink in both themes and move the fills. It clears 4.5:1, but caps the dark
113
+ * hover at that same 1.26x - gutting the state #78 constrained hover in order to keep.
114
+ * - Give each ramp its own ink, the same in both themes. It clears 4.5:1 only against pure black:
115
+ * against `#0f172a`, the dark set's own `surface`, sky-600 lands at 4.36 and fails. It also buys
116
+ * a light theme with white text on one button and black on the one beside it.
117
+ */
78
118
  export const light: PaletteTokens = {
79
119
  surface: '#ffffff',
80
120
  foreground: '#0f172a',
@@ -84,12 +124,12 @@ export const light: PaletteTokens = {
84
124
  rule: '#808fa3',
85
125
  backing: '#f1f5f9',
86
126
 
87
- primary: '#8b5cf6',
88
- primaryHover: '#7c3aed',
127
+ primary: '#7c3aed',
128
+ primaryHover: '#6d28d9',
89
129
  primaryForeground: '#f8fafc',
90
130
 
91
- secondary: '#0ea5e9',
92
- secondaryHover: '#0284c7',
131
+ secondary: '#0369a1',
132
+ secondaryHover: '#075985',
93
133
  secondaryForeground: '#f8fafc',
94
134
 
95
135
  disabled: '#94a3b8',
@@ -106,10 +146,19 @@ export const light: PaletteTokens = {
106
146
  };
107
147
 
108
148
  /**
109
- * Dark reverses the ramp the fills are drawn from: `primary` takes the step that light uses for
110
- * its hover, and hovering steps *up* into light. That is what the components' old
149
+ * Dark reverses the direction the fills step: it sits higher up the ramp than light does and hovers
150
+ * *up* into lighter still, where light sits lower and hovers down. That is what the components' old
111
151
  * `dark:bg-primary-600 dark:hover:bg-primary-500` pair encoded, moved here so it is stated once
112
- * for the whole system instead of repeated per component.
152
+ * for the whole system instead of repeated per component. The two sets no longer share a step, since
153
+ * each theme's pair has to clear its own ink as well as its own surface (issue #93).
154
+ *
155
+ * The ink inverts with the ramp: slate-950 under these lighter fills, where light takes slate-50
156
+ * under its darker ones. The pair is the two ends of the one neutral ramp the rest of the palette is
157
+ * already built from - `#f8fafc` is slate-50, `surface` slate-900, `muted` slate-500 - rather than a
158
+ * new colour arriving for a single job. `#020617` is also the lightest slate step that still admits
159
+ * violet-500 and sky-600, which is what leaves this set's `secondary` pair unmoved: slate-900 draws
160
+ * 4.22 and 4.36 against them and fails. Why the ink follows the theme at all, and the two routes
161
+ * rejected in getting here: see the light set.
113
162
  */
114
163
  export const dark: PaletteTokens = {
115
164
  surface: '#0f172a',
@@ -120,13 +169,13 @@ export const dark: PaletteTokens = {
120
169
  rule: '#5b6a80',
121
170
  backing: '#1e293b',
122
171
 
123
- primary: '#7c3aed',
124
- primaryHover: '#8b5cf6',
125
- primaryForeground: '#f8fafc',
172
+ primary: '#8b5cf6',
173
+ primaryHover: '#a78bfa',
174
+ primaryForeground: '#020617',
126
175
 
127
176
  secondary: '#0284c7',
128
177
  secondaryHover: '#0ea5e9',
129
- secondaryForeground: '#f8fafc',
178
+ secondaryForeground: '#020617',
130
179
 
131
180
  disabled: '#475569',
132
181
  disabledHover: '#334155',
@@ -61,6 +61,14 @@ const CONTROL_MIN_WIDTH = `:root {
61
61
  --control-min-width: 10.5rem;
62
62
  }`;
63
63
 
64
+ /* The shell's standing slot claims a width, and that is what this names: how much of the bar the
65
+ slot takes, never how wide any drawing is - a slot role, so Brandmark keeps setting no dimension of
66
+ its own (.out-of-scope/brandmark-size-vocabulary.md). Not a colour and no Tailwind namespace, so it
67
+ sits in :root beside the control minimum width. 7.5rem is the one attested value, measured in #81. */
68
+ const STANDING_MIN_WIDTH = `:root {
69
+ --standing-min-width: 7.5rem;
70
+ }`;
71
+
64
72
  /* The library's underline is not a colour: like the focus-ring dimensions it lives in :root only,
65
73
  never @theme inline. Not prose's alone - every Link treatment that draws a line draws it from these.
66
74
  Constraint: --underline-thickness-hover > --underline-thickness (the thicken on hover is the only
@@ -102,11 +110,17 @@ const TYPOGRAPHY = `@theme {
102
110
  subtitle was rejected in ADR 0004 - it welds the lede's size to H3's and its leading to a head's. */
103
111
  --text-lede: 1.25rem; /* 20px */
104
112
  --leading-lede: 1.4;
105
- --text-body: 1.0625rem; /* 17px */
113
+ --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) */
106
114
  --leading-body: 1.6;
107
115
  --text-small: 0.9375rem; /* 15px floor: below it tabular figures stop comparing column to column */
108
116
  --text-label: 0.8125rem; /* 13px floor: below it the tracking reads as damage, not a device */
109
117
 
118
+ /* The label role's leading, declared standalone and deliberately not paired into text-label's
119
+ utility: pairing would move every existing label site's line box at once, silently. A component
120
+ whose own height is measured against its labels reads leading-label explicitly (Header #81). 1.5
121
+ is the line height SC 1.4.12 expects text to survive, so a user stylesheet applying it moves nothing. */
122
+ --leading-label: 1.5;
123
+
110
124
  /* Two quantities on one property that must not collapse: --tracking-label is a fixed letter-spaced
111
125
  style, --tracking-optical a correction that varies with size. The names say which is which, so the
112
126
  distinction survives without the ADR in hand. Scope: the title role and above - H1, H2 and the page
@@ -225,6 +239,9 @@ ${RADIUS}
225
239
  /* The control minimum width is not a colour either, and sits in :root beside radius. */
226
240
  ${CONTROL_MIN_WIDTH}
227
241
 
242
+ /* The standing slot's minimum width is not a colour either, and sits in :root beside the control one. */
243
+ ${STANDING_MIN_WIDTH}
244
+
228
245
  /* The library's underline dimensions are not colours either, and sit in :root beside radius. They are
229
246
  not prose's alone: every Link treatment that draws a line draws it from these (docs/adr/0006). */
230
247
  ${UNDERLINE}
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import './styles.css';
2
2
 
3
+ export { Cluster } from 'Arrangement/Cluster/Cluster';
4
+ export { Stack } from 'Arrangement/Stack/Stack';
3
5
  export { Brandmark } from 'Display/Brandmark/Brandmark';
4
6
  export { Checklist } from 'Display/Checklist/Checklist';
5
7
  export { DefinitionList } from 'Display/DefinitionList/DefinitionList';
@@ -13,6 +15,7 @@ export { H3 } from 'Display/Typography/H3/H3';
13
15
  export { H4 } from 'Display/Typography/H4/H4';
14
16
  export { H5 } from 'Display/Typography/H5/H5';
15
17
  export { H6 } from 'Display/Typography/H6/H6';
18
+ export { Note } from 'Display/Typography/Note/Note';
16
19
  export { P } from 'Display/Typography/P/P';
17
20
  export { Prose } from 'Display/Typography/Prose/Prose';
18
21
  export { Button } from 'Interaction/Button/Button';
package/src/tokens.css CHANGED
@@ -17,11 +17,11 @@
17
17
  --color-control-border: #64748b;
18
18
  --color-rule: #808fa3;
19
19
  --color-backing: #f1f5f9;
20
- --color-primary: #8b5cf6;
21
- --color-primary-hover: #7c3aed;
20
+ --color-primary: #7c3aed;
21
+ --color-primary-hover: #6d28d9;
22
22
  --color-primary-foreground: #f8fafc;
23
- --color-secondary: #0ea5e9;
24
- --color-secondary-hover: #0284c7;
23
+ --color-secondary: #0369a1;
24
+ --color-secondary-hover: #075985;
25
25
  --color-secondary-foreground: #f8fafc;
26
26
  --color-disabled: #94a3b8;
27
27
  --color-disabled-hover: #64748b;
@@ -41,12 +41,12 @@
41
41
  --color-control-border: #94a3b8;
42
42
  --color-rule: #5b6a80;
43
43
  --color-backing: #1e293b;
44
- --color-primary: #7c3aed;
45
- --color-primary-hover: #8b5cf6;
46
- --color-primary-foreground: #f8fafc;
44
+ --color-primary: #8b5cf6;
45
+ --color-primary-hover: #a78bfa;
46
+ --color-primary-foreground: #020617;
47
47
  --color-secondary: #0284c7;
48
48
  --color-secondary-hover: #0ea5e9;
49
- --color-secondary-foreground: #f8fafc;
49
+ --color-secondary-foreground: #020617;
50
50
  --color-disabled: #475569;
51
51
  --color-disabled-hover: #334155;
52
52
  --color-focus-ring: var(--color-ring, #cbd5e1);
@@ -110,6 +110,11 @@
110
110
  --control-min-width: 10.5rem;
111
111
  }
112
112
 
113
+ /* The standing slot's minimum width is not a colour either, and sits in :root beside the control one. */
114
+ :root {
115
+ --standing-min-width: 7.5rem;
116
+ }
117
+
113
118
  /* The library's underline dimensions are not colours either, and sit in :root beside radius. They are
114
119
  not prose's alone: every Link treatment that draws a line draws it from these (docs/adr/0006). */
115
120
  :root {
@@ -144,11 +149,17 @@
144
149
  subtitle was rejected in ADR 0004 - it welds the lede's size to H3's and its leading to a head's. */
145
150
  --text-lede: 1.25rem; /* 20px */
146
151
  --leading-lede: 1.4;
147
- --text-body: 1.0625rem; /* 17px */
152
+ --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) */
148
153
  --leading-body: 1.6;
149
154
  --text-small: 0.9375rem; /* 15px floor: below it tabular figures stop comparing column to column */
150
155
  --text-label: 0.8125rem; /* 13px floor: below it the tracking reads as damage, not a device */
151
156
 
157
+ /* The label role's leading, declared standalone and deliberately not paired into text-label's
158
+ utility: pairing would move every existing label site's line box at once, silently. A component
159
+ whose own height is measured against its labels reads leading-label explicitly (Header #81). 1.5
160
+ is the line height SC 1.4.12 expects text to survive, so a user stylesheet applying it moves nothing. */
161
+ --leading-label: 1.5;
162
+
152
163
  /* Two quantities on one property that must not collapse: --tracking-label is a fixed letter-spaced
153
164
  style, --tracking-optical a correction that varies with size. The names say which is which, so the
154
165
  distinction survives without the ADR in hand. Scope: the title role and above - H1, H2 and the page
@@ -12,12 +12,12 @@
12
12
  --color-control-border: #94a3b8;
13
13
  --color-rule: #5b6a80;
14
14
  --color-backing: #1e293b;
15
- --color-primary: #7c3aed;
16
- --color-primary-hover: #8b5cf6;
17
- --color-primary-foreground: #f8fafc;
15
+ --color-primary: #8b5cf6;
16
+ --color-primary-hover: #a78bfa;
17
+ --color-primary-foreground: #020617;
18
18
  --color-secondary: #0284c7;
19
19
  --color-secondary-hover: #0ea5e9;
20
- --color-secondary-foreground: #f8fafc;
20
+ --color-secondary-foreground: #020617;
21
21
  --color-disabled: #475569;
22
22
  --color-disabled-hover: #334155;
23
23
  --color-focus-ring: var(--color-ring, #cbd5e1);
@@ -81,6 +81,11 @@
81
81
  --control-min-width: 10.5rem;
82
82
  }
83
83
 
84
+ /* The standing slot's minimum width is not a colour either, and sits in :root beside the control one. */
85
+ :root {
86
+ --standing-min-width: 7.5rem;
87
+ }
88
+
84
89
  /* The library's underline dimensions are not colours either, and sit in :root beside radius. They are
85
90
  not prose's alone: every Link treatment that draws a line draws it from these (docs/adr/0006). */
86
91
  :root {
@@ -115,11 +120,17 @@
115
120
  subtitle was rejected in ADR 0004 - it welds the lede's size to H3's and its leading to a head's. */
116
121
  --text-lede: 1.25rem; /* 20px */
117
122
  --leading-lede: 1.4;
118
- --text-body: 1.0625rem; /* 17px */
123
+ --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) */
119
124
  --leading-body: 1.6;
120
125
  --text-small: 0.9375rem; /* 15px floor: below it tabular figures stop comparing column to column */
121
126
  --text-label: 0.8125rem; /* 13px floor: below it the tracking reads as damage, not a device */
122
127
 
128
+ /* The label role's leading, declared standalone and deliberately not paired into text-label's
129
+ utility: pairing would move every existing label site's line box at once, silently. A component
130
+ whose own height is measured against its labels reads leading-label explicitly (Header #81). 1.5
131
+ is the line height SC 1.4.12 expects text to survive, so a user stylesheet applying it moves nothing. */
132
+ --leading-label: 1.5;
133
+
123
134
  /* Two quantities on one property that must not collapse: --tracking-label is a fixed letter-spaced
124
135
  style, --tracking-optical a correction that varies with size. The names say which is which, so the
125
136
  distinction survives without the ADR in hand. Scope: the title role and above - H1, H2 and the page
@@ -12,11 +12,11 @@
12
12
  --color-control-border: #64748b;
13
13
  --color-rule: #808fa3;
14
14
  --color-backing: #f1f5f9;
15
- --color-primary: #8b5cf6;
16
- --color-primary-hover: #7c3aed;
15
+ --color-primary: #7c3aed;
16
+ --color-primary-hover: #6d28d9;
17
17
  --color-primary-foreground: #f8fafc;
18
- --color-secondary: #0ea5e9;
19
- --color-secondary-hover: #0284c7;
18
+ --color-secondary: #0369a1;
19
+ --color-secondary-hover: #075985;
20
20
  --color-secondary-foreground: #f8fafc;
21
21
  --color-disabled: #94a3b8;
22
22
  --color-disabled-hover: #64748b;
@@ -81,6 +81,11 @@
81
81
  --control-min-width: 10.5rem;
82
82
  }
83
83
 
84
+ /* The standing slot's minimum width is not a colour either, and sits in :root beside the control one. */
85
+ :root {
86
+ --standing-min-width: 7.5rem;
87
+ }
88
+
84
89
  /* The library's underline dimensions are not colours either, and sit in :root beside radius. They are
85
90
  not prose's alone: every Link treatment that draws a line draws it from these (docs/adr/0006). */
86
91
  :root {
@@ -115,11 +120,17 @@
115
120
  subtitle was rejected in ADR 0004 - it welds the lede's size to H3's and its leading to a head's. */
116
121
  --text-lede: 1.25rem; /* 20px */
117
122
  --leading-lede: 1.4;
118
- --text-body: 1.0625rem; /* 17px */
123
+ --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) */
119
124
  --leading-body: 1.6;
120
125
  --text-small: 0.9375rem; /* 15px floor: below it tabular figures stop comparing column to column */
121
126
  --text-label: 0.8125rem; /* 13px floor: below it the tracking reads as damage, not a device */
122
127
 
128
+ /* The label role's leading, declared standalone and deliberately not paired into text-label's
129
+ utility: pairing would move every existing label site's line box at once, silently. A component
130
+ whose own height is measured against its labels reads leading-label explicitly (Header #81). 1.5
131
+ is the line height SC 1.4.12 expects text to survive, so a user stylesheet applying it moves nothing. */
132
+ --leading-label: 1.5;
133
+
123
134
  /* Two quantities on one property that must not collapse: --tracking-label is a fixed letter-spaced
124
135
  style, --tracking-optical a correction that varies with size. The names say which is which, so the
125
136
  distinction survives without the ADR in hand. Scope: the title role and above - H1, H2 and the page