@juwel-development/design-system 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/design-system.js +237 -126
- package/dist/index.css +1 -1
- package/dist/types/Display/Meter/Meter.d.ts +59 -0
- package/dist/types/Display/Meter/MeterConfigurationError.d.ts +3 -0
- package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +5 -2
- package/dist/types/Display/Typography/H1/H1.d.ts +5 -2
- package/dist/types/Display/Typography/H2/H2.d.ts +5 -2
- package/dist/types/Display/Typography/H3/H3.d.ts +5 -2
- package/dist/types/Display/Typography/H4/H4.d.ts +5 -2
- package/dist/types/Display/Typography/H5/H5.d.ts +5 -2
- package/dist/types/Display/Typography/H6/H6.d.ts +5 -2
- package/dist/types/Display/Typography/Note/Note.d.ts +5 -2
- package/dist/types/Display/Typography/P/P.d.ts +5 -2
- package/dist/types/Display/Typography/Prose/Prose.d.ts +6 -3
- package/dist/types/Interaction/Slider/Slider.d.ts +41 -0
- package/dist/types/Interaction/Slider/SliderConfigurationError.d.ts +3 -0
- package/dist/types/Theme/Palette.d.ts +22 -2
- package/dist/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/Display/Meter/Meter.tsx +136 -0
- package/src/Display/Meter/MeterConfigurationError.ts +6 -0
- package/src/Display/Typography/Eyebrow/Eyebrow.tsx +12 -2
- package/src/Display/Typography/H1/H1.tsx +12 -2
- package/src/Display/Typography/H2/H2.tsx +12 -2
- package/src/Display/Typography/H3/H3.tsx +12 -2
- package/src/Display/Typography/H4/H4.tsx +12 -2
- package/src/Display/Typography/H5/H5.tsx +12 -2
- package/src/Display/Typography/H6/H6.tsx +12 -2
- package/src/Display/Typography/Note/Note.tsx +12 -2
- package/src/Display/Typography/P/P.tsx +12 -2
- package/src/Display/Typography/Prose/Prose.tsx +13 -3
- package/src/Interaction/Slider/Slider.tsx +125 -0
- package/src/Interaction/Slider/SliderConfigurationError.ts +6 -0
- package/src/Theme/Palette.ts +32 -5
- package/src/Theme/renderTokens.ts +23 -0
- package/src/index.ts +2 -0
- package/src/tokens.css +19 -3
- package/src/tokens.dark.css +14 -0
- package/src/tokens.light.css +17 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { CSSProperties, FunctionComponent } from 'react';
|
|
3
|
+
import { MeterConfigurationError } from './MeterConfigurationError';
|
|
4
|
+
|
|
5
|
+
// The track is the whole capacity: square corners, no radius token - Meter is not a control
|
|
6
|
+
// (docs/adr/0003). The one-pixel `rule` line identifies the capacity against the surface;
|
|
7
|
+
// `meterTrack` behind it makes the scale visible without boxing (docs/adr/0010). Deliberately no
|
|
8
|
+
// transition class: the library's one motion belongs to state changes on controls, not a display.
|
|
9
|
+
const meterTrack = cva(
|
|
10
|
+
'w-full h-[var(--meter-track-thickness)] bg-meter-track border border-solid border-rule',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
// The fill is a block box sized by --meter-level, the normalized share the component computes;
|
|
14
|
+
// block layout places it at inline-start in both LTR and RTL, no physical offset. Depleting reads
|
|
15
|
+
// the same share into color-mix: `meterFill` at max, `error` at min, linearly through OKLab - a
|
|
16
|
+
// perceptual space, so no threshold (docs/adr/0010). Both endpoints stay tokens a brand re-points.
|
|
17
|
+
const meterFill = cva('h-full w-[calc(var(--meter-level)*100%)]', {
|
|
18
|
+
variants: {
|
|
19
|
+
treatment: {
|
|
20
|
+
neutral: 'bg-meter-fill',
|
|
21
|
+
depleting:
|
|
22
|
+
'bg-[color-mix(in_oklab,var(--color-meter-fill)_calc(var(--meter-level)*100%),var(--color-error))]',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
treatment: 'neutral',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// React's CSSProperties is closed over known properties; the one custom property the recipes read
|
|
31
|
+
// is declared here so the style object stays typed without an assertion.
|
|
32
|
+
type MeterLevelStyle = CSSProperties & { '--meter-level': number };
|
|
33
|
+
|
|
34
|
+
export interface IMeterProps extends VariantProps<typeof meterFill> {
|
|
35
|
+
/** The current level. Must lie inside `[min, max]`; an outside value throws
|
|
36
|
+
* {@link MeterConfigurationError} rather than clamping - a wrong number must not render as a
|
|
37
|
+
* plausible level. */
|
|
38
|
+
value: number;
|
|
39
|
+
/** The bottom of the scale. Defaults to `0`. */
|
|
40
|
+
min?: number;
|
|
41
|
+
/** The top of the scale. Must exceed `min`. */
|
|
42
|
+
max: number;
|
|
43
|
+
/** The meter's accessible name - what the level measures. Required, and never rendered visibly:
|
|
44
|
+
* the component shows a filled track and nothing else. Its wording is the consumer's. */
|
|
45
|
+
label: string;
|
|
46
|
+
/** Optional `aria-valuetext`, so a screen reader hears the consumer's wording ("strained")
|
|
47
|
+
* instead of a bare number. Omitted, the numeric value is announced. */
|
|
48
|
+
valueText?: string;
|
|
49
|
+
testId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A read-only display of one numeric level within a bounded scale, shown as a filled share of a
|
|
54
|
+
* whole: a short track, a fill from inline-start, no text. It states an amount - not an operation's
|
|
55
|
+
* completion, and unlike a Slider it cannot be operated.
|
|
56
|
+
*
|
|
57
|
+
* @Guarantees — enforced on every render
|
|
58
|
+
* - The root carries `role="meter"`, `aria-valuenow`/`aria-valuemin`/`aria-valuemax`, the `label`
|
|
59
|
+
* as its accessible name, and `aria-valuetext` exactly when `valueText` is supplied. It is not
|
|
60
|
+
* focusable, and the visual fill is hidden from the accessibility tree.
|
|
61
|
+
* - The filled share is `(value - min) / (max - min)` of the container and begins at logical
|
|
62
|
+
* inline-start under both LTR and RTL direction.
|
|
63
|
+
* - Neutral paints the fill with `meterFill`; `depleting` maps the normalized level linearly
|
|
64
|
+
* through OKLab from `meterFill` at the maximum to `error` at the minimum - no threshold, no
|
|
65
|
+
* wording, no icon (docs/adr/0010-meter-depletion-is-a-treatment.md).
|
|
66
|
+
* - A value change lands instantly: no width or colour animation.
|
|
67
|
+
* - The track fills its container, reads `--meter-track-thickness`, keeps square corners (no
|
|
68
|
+
* radius token - Meter is not a control), and draws a persistent one-pixel `rule` line that
|
|
69
|
+
* identifies the whole capacity against the surface.
|
|
70
|
+
* - Invalid numeric configuration throws {@link MeterConfigurationError}: non-finite `value`,
|
|
71
|
+
* `min` or `max`, `min >= max`, or a value outside the inclusive range. Nothing is clamped.
|
|
72
|
+
* - No visible wording of its own, no numeric readout, no caption slot, no `className`/`style`
|
|
73
|
+
* passthrough.
|
|
74
|
+
*
|
|
75
|
+
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
76
|
+
* - `label` carries meaningful wording; the component renders it to assistive technology only and
|
|
77
|
+
* does not inspect it.
|
|
78
|
+
* - When `depleting` is selected, surrounding visible content explains why nearing the minimum
|
|
79
|
+
* matters - the colour walk reinforces a stated consequence and must not be its only carrier.
|
|
80
|
+
*
|
|
81
|
+
* @UXGuidelines
|
|
82
|
+
* - Place any visible caption or readout yourself with typography; the Meter deliberately carries
|
|
83
|
+
* none, so what the level measures stays the consumer's wording.
|
|
84
|
+
* - Reach for `depleting` only where a low value is genuinely more erroneous - some quantities
|
|
85
|
+
* become safer toward their minimum, which is why the treatment is opt-in.
|
|
86
|
+
*/
|
|
87
|
+
export const Meter: FunctionComponent<IMeterProps> = ({
|
|
88
|
+
value,
|
|
89
|
+
min = 0,
|
|
90
|
+
max,
|
|
91
|
+
treatment,
|
|
92
|
+
label,
|
|
93
|
+
valueText,
|
|
94
|
+
testId,
|
|
95
|
+
}) => {
|
|
96
|
+
if (
|
|
97
|
+
!Number.isFinite(value) ||
|
|
98
|
+
!Number.isFinite(min) ||
|
|
99
|
+
!Number.isFinite(max)
|
|
100
|
+
) {
|
|
101
|
+
throw new MeterConfigurationError(
|
|
102
|
+
`value, min and max must be finite numbers (got value=${value}, min=${min}, max=${max})`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (min >= max) {
|
|
106
|
+
throw new MeterConfigurationError(
|
|
107
|
+
`min must be less than max (got min=${min}, max=${max})`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (value < min || value > max) {
|
|
111
|
+
throw new MeterConfigurationError(
|
|
112
|
+
`value must lie within [min, max] (got value=${value}, min=${min}, max=${max})`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const style: MeterLevelStyle = {
|
|
117
|
+
'--meter-level': (value - min) / (max - min),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
// biome-ignore lint/a11y/useSemanticElements: the native <meter> paints its own optimum/low/high bands the token contract cannot reach, and its fill is unstylable cross-browser; the ARIA meter pattern on a div carries the same semantics with the library's paint (#105).
|
|
122
|
+
<div
|
|
123
|
+
role={'meter'}
|
|
124
|
+
aria-valuenow={value}
|
|
125
|
+
aria-valuemin={min}
|
|
126
|
+
aria-valuemax={max}
|
|
127
|
+
aria-label={label}
|
|
128
|
+
aria-valuetext={valueText}
|
|
129
|
+
data-testid={testId}
|
|
130
|
+
className={meterTrack()}
|
|
131
|
+
style={style}
|
|
132
|
+
>
|
|
133
|
+
<div aria-hidden={'true'} className={meterFill({ treatment })} />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
@@ -10,7 +10,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
10
10
|
// semantic token re-pointed by `.dark`, so no variant carries a `dark:` class.
|
|
11
11
|
const eyebrow = cva('font-secondary text-label tracking-label font-medium', {
|
|
12
12
|
variants: {
|
|
13
|
-
color: {
|
|
13
|
+
color: {
|
|
14
|
+
foreground: 'text-foreground',
|
|
15
|
+
muted: 'text-muted',
|
|
16
|
+
success: 'text-success',
|
|
17
|
+
warning: 'text-warning',
|
|
18
|
+
error: 'text-error',
|
|
19
|
+
info: 'text-info',
|
|
20
|
+
},
|
|
14
21
|
},
|
|
15
22
|
defaultVariants: { color: 'muted' },
|
|
16
23
|
});
|
|
@@ -28,12 +35,15 @@ interface IEyebrowProps extends VariantProps<typeof eyebrow> {
|
|
|
28
35
|
* @Guarantees — enforced on every render
|
|
29
36
|
* - Renders a `p`, reading `--font-secondary`, sized by `--text-label` and tracked by
|
|
30
37
|
* `--tracking-label`, at weight 500.
|
|
31
|
-
* - `color` selects
|
|
38
|
+
* - `color` selects `muted` (default), `foreground`, `success`, `warning`, `error` or `info`;
|
|
39
|
+
* nothing else paints text. A status tone changes colour only and adds no announcement semantics.
|
|
32
40
|
* - Sets neither `font-variant-caps` nor `font-variant-numeric` under any prop.
|
|
33
41
|
*
|
|
34
42
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
35
43
|
* - This is **not a form label**: it renders no `htmlFor` and labels no control. A labelled control
|
|
36
44
|
* uses `Input`/`TextArea`, which label themselves.
|
|
45
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
46
|
+
* announcement behavior required when that status changes.
|
|
37
47
|
*/
|
|
38
48
|
export const Eyebrow: FunctionComponent<IEyebrowProps> = ({
|
|
39
49
|
children,
|
|
@@ -12,7 +12,14 @@ const h1 = cva(
|
|
|
12
12
|
'font-primary text-display leading-display tracking-optical max-w-[var(--measure-display)]',
|
|
13
13
|
{
|
|
14
14
|
variants: {
|
|
15
|
-
color: {
|
|
15
|
+
color: {
|
|
16
|
+
foreground: 'text-foreground',
|
|
17
|
+
muted: 'text-muted',
|
|
18
|
+
success: 'text-success',
|
|
19
|
+
warning: 'text-warning',
|
|
20
|
+
error: 'text-error',
|
|
21
|
+
info: 'text-info',
|
|
22
|
+
},
|
|
16
23
|
},
|
|
17
24
|
defaultVariants: { color: 'foreground' },
|
|
18
25
|
},
|
|
@@ -34,7 +41,8 @@ interface IH1Props extends VariantProps<typeof h1> {
|
|
|
34
41
|
* because bigger type wants fewer characters per line (docs/adr/0004). The bound is the recipe's,
|
|
35
42
|
* not a caller's: the level fixes the role and the role fixes the measure, so there is no `measure`
|
|
36
43
|
* prop to select between roles (docs/adr/0008). It holds under every `color`.
|
|
37
|
-
* - `color` selects
|
|
44
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
45
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
38
46
|
*
|
|
39
47
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
40
48
|
* - This is an ordinary page title, and it is also a hero's lead - `display` is the hero role, so a
|
|
@@ -44,6 +52,8 @@ interface IH1Props extends VariantProps<typeof h1> {
|
|
|
44
52
|
* - A subpage head is the exception: `PageHead` renders its own `h1` at the `title` role, the one
|
|
45
53
|
* sanctioned escape valve from level-fixes-role (docs/adr/0005). Reach for it where it fits.
|
|
46
54
|
* - Heading levels descend without skipping — an `h1` is followed by an `h2`, never an `h3`.
|
|
55
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
56
|
+
* announcement behavior required when that status changes.
|
|
47
57
|
*/
|
|
48
58
|
export const H1: FunctionComponent<IH1Props> = ({
|
|
49
59
|
children,
|
|
@@ -9,7 +9,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
9
9
|
// Colour is a semantic token re-pointed by `.dark`, so no variant carries a `dark:` class.
|
|
10
10
|
const h2 = cva('font-primary text-title leading-title tracking-optical', {
|
|
11
11
|
variants: {
|
|
12
|
-
color: {
|
|
12
|
+
color: {
|
|
13
|
+
foreground: 'text-foreground',
|
|
14
|
+
muted: 'text-muted',
|
|
15
|
+
success: 'text-success',
|
|
16
|
+
warning: 'text-warning',
|
|
17
|
+
error: 'text-error',
|
|
18
|
+
info: 'text-info',
|
|
19
|
+
},
|
|
13
20
|
},
|
|
14
21
|
defaultVariants: { color: 'foreground' },
|
|
15
22
|
});
|
|
@@ -27,10 +34,13 @@ interface IH2Props extends VariantProps<typeof h2> {
|
|
|
27
34
|
* - Reads `--font-primary`, sized by `--text-title`, led by `--leading-title` and optically corrected
|
|
28
35
|
* by `--tracking-optical` — the title role is the smallest role that carries it, so `H3` and below
|
|
29
36
|
* take none.
|
|
30
|
-
* - `color` selects
|
|
37
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
38
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
31
39
|
*
|
|
32
40
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
33
41
|
* - Heading levels descend without skipping — an `h2` sits under an `h1`, not under an `h3`.
|
|
42
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
43
|
+
* announcement behavior required when that status changes.
|
|
34
44
|
*/
|
|
35
45
|
export const H2: FunctionComponent<IH2Props> = ({
|
|
36
46
|
children,
|
|
@@ -8,7 +8,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
8
8
|
// class.
|
|
9
9
|
const h3 = cva('font-primary text-subtitle leading-subtitle', {
|
|
10
10
|
variants: {
|
|
11
|
-
color: {
|
|
11
|
+
color: {
|
|
12
|
+
foreground: 'text-foreground',
|
|
13
|
+
muted: 'text-muted',
|
|
14
|
+
success: 'text-success',
|
|
15
|
+
warning: 'text-warning',
|
|
16
|
+
error: 'text-error',
|
|
17
|
+
info: 'text-info',
|
|
18
|
+
},
|
|
12
19
|
},
|
|
13
20
|
defaultVariants: { color: 'foreground' },
|
|
14
21
|
});
|
|
@@ -25,10 +32,13 @@ interface IH3Props extends VariantProps<typeof h3> {
|
|
|
25
32
|
* @Guarantees — enforced on every render
|
|
26
33
|
* - Renders an `h3`; its outline level and the subtitle role are one choice, not two (docs/adr/0005).
|
|
27
34
|
* - Reads `--font-primary`, sized by `--text-subtitle` and led by `--leading-subtitle`.
|
|
28
|
-
* - `color` selects
|
|
35
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
36
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
29
37
|
*
|
|
30
38
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
31
39
|
* - Heading levels descend without skipping — an `h3` sits under an `h2`, not under an `h1`.
|
|
40
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
41
|
+
* announcement behavior required when that status changes.
|
|
32
42
|
*/
|
|
33
43
|
export const H3: FunctionComponent<IH3Props> = ({
|
|
34
44
|
children,
|
|
@@ -8,7 +8,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
8
8
|
// token re-pointed by `.dark`, so no variant carries a `dark:` class.
|
|
9
9
|
const h4 = cva('font-primary text-body leading-body font-bold', {
|
|
10
10
|
variants: {
|
|
11
|
-
color: {
|
|
11
|
+
color: {
|
|
12
|
+
foreground: 'text-foreground',
|
|
13
|
+
muted: 'text-muted',
|
|
14
|
+
success: 'text-success',
|
|
15
|
+
warning: 'text-warning',
|
|
16
|
+
error: 'text-error',
|
|
17
|
+
info: 'text-info',
|
|
18
|
+
},
|
|
12
19
|
},
|
|
13
20
|
defaultVariants: { color: 'foreground' },
|
|
14
21
|
});
|
|
@@ -24,10 +31,13 @@ interface IH4Props extends VariantProps<typeof h4> {
|
|
|
24
31
|
* @Guarantees — enforced on every render
|
|
25
32
|
* - Renders an `h4`; its outline level and the body role are one choice, not two (docs/adr/0005).
|
|
26
33
|
* - Reads `--font-primary`, sized by `--text-body`, and is bold so it stands apart from a paragraph.
|
|
27
|
-
* - `color` selects
|
|
34
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
35
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
28
36
|
*
|
|
29
37
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
30
38
|
* - Heading levels descend without skipping — an `h4` sits under an `h3`, not under an `h2`.
|
|
39
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
40
|
+
* announcement behavior required when that status changes.
|
|
31
41
|
*/
|
|
32
42
|
export const H4: FunctionComponent<IH4Props> = ({
|
|
33
43
|
children,
|
|
@@ -8,7 +8,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
8
8
|
// token re-pointed by `.dark`, so no variant carries a `dark:` class.
|
|
9
9
|
const h5 = cva('font-primary text-body leading-body font-semibold', {
|
|
10
10
|
variants: {
|
|
11
|
-
color: {
|
|
11
|
+
color: {
|
|
12
|
+
foreground: 'text-foreground',
|
|
13
|
+
muted: 'text-muted',
|
|
14
|
+
success: 'text-success',
|
|
15
|
+
warning: 'text-warning',
|
|
16
|
+
error: 'text-error',
|
|
17
|
+
info: 'text-info',
|
|
18
|
+
},
|
|
12
19
|
},
|
|
13
20
|
defaultVariants: { color: 'foreground' },
|
|
14
21
|
});
|
|
@@ -25,10 +32,13 @@ interface IH5Props extends VariantProps<typeof h5> {
|
|
|
25
32
|
* @Guarantees — enforced on every render
|
|
26
33
|
* - Renders an `h5`; its outline level and the body role are one choice, not two (docs/adr/0005).
|
|
27
34
|
* - Reads `--font-primary`, sized by `--text-body`, semibold so it stands apart from a paragraph.
|
|
28
|
-
* - `color` selects
|
|
35
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
36
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
29
37
|
*
|
|
30
38
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
31
39
|
* - Heading levels descend without skipping — an `h5` sits under an `h4`, not under an `h3`.
|
|
40
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
41
|
+
* announcement behavior required when that status changes.
|
|
32
42
|
*/
|
|
33
43
|
export const H5: FunctionComponent<IH5Props> = ({
|
|
34
44
|
children,
|
|
@@ -8,7 +8,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
8
8
|
// token re-pointed by `.dark`, so no variant carries a `dark:` class.
|
|
9
9
|
const h6 = cva('font-primary text-body leading-body font-medium', {
|
|
10
10
|
variants: {
|
|
11
|
-
color: {
|
|
11
|
+
color: {
|
|
12
|
+
foreground: 'text-foreground',
|
|
13
|
+
muted: 'text-muted',
|
|
14
|
+
success: 'text-success',
|
|
15
|
+
warning: 'text-warning',
|
|
16
|
+
error: 'text-error',
|
|
17
|
+
info: 'text-info',
|
|
18
|
+
},
|
|
12
19
|
},
|
|
13
20
|
defaultVariants: { color: 'foreground' },
|
|
14
21
|
});
|
|
@@ -25,10 +32,13 @@ interface IH6Props extends VariantProps<typeof h6> {
|
|
|
25
32
|
* @Guarantees — enforced on every render
|
|
26
33
|
* - Renders an `h6`; its outline level and the body role are one choice, not two (docs/adr/0005).
|
|
27
34
|
* - Reads `--font-primary`, sized by `--text-body`, medium so it stands apart from a paragraph.
|
|
28
|
-
* - `color` selects
|
|
35
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
36
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
29
37
|
*
|
|
30
38
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
31
39
|
* - Heading levels descend without skipping — an `h6` sits under an `h5`, not under an `h4`.
|
|
40
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
41
|
+
* announcement behavior required when that status changes.
|
|
32
42
|
*/
|
|
33
43
|
export const H6: FunctionComponent<IH6Props> = ({
|
|
34
44
|
children,
|
|
@@ -11,7 +11,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
11
11
|
// the page owns the rhythm around it. Colour is re-pointed by `.dark`, so no variant carries `dark:`.
|
|
12
12
|
const note = cva('font-secondary text-small', {
|
|
13
13
|
variants: {
|
|
14
|
-
color: {
|
|
14
|
+
color: {
|
|
15
|
+
foreground: 'text-foreground',
|
|
16
|
+
muted: 'text-muted',
|
|
17
|
+
success: 'text-success',
|
|
18
|
+
warning: 'text-warning',
|
|
19
|
+
error: 'text-error',
|
|
20
|
+
info: 'text-info',
|
|
21
|
+
},
|
|
15
22
|
},
|
|
16
23
|
defaultVariants: { color: 'foreground' },
|
|
17
24
|
});
|
|
@@ -30,7 +37,8 @@ export interface INoteProps extends VariantProps<typeof note> {
|
|
|
30
37
|
*
|
|
31
38
|
* @Guarantees — enforced on every render
|
|
32
39
|
* - Renders a `p`, reading `--font-secondary` and sized by `--text-small`.
|
|
33
|
-
* - `color` selects
|
|
40
|
+
* - `color` selects `foreground` (default), `muted`, `success`, `warning`, `error` or `info`;
|
|
41
|
+
* nothing else paints text. A status tone changes colour only.
|
|
34
42
|
* - Emits no tracking, no font-weight, no measure and no margin under any prop.
|
|
35
43
|
* - Carries no ARIA role and no live region under any prop.
|
|
36
44
|
*
|
|
@@ -44,6 +52,8 @@ export interface INoteProps extends VariantProps<typeof note> {
|
|
|
44
52
|
* architecture standard's one-way dependency rule is why `Prose` restates `P`'s utilities.
|
|
45
53
|
* - Where the annotation is a form's status message, `Form` owns `role="status"`/`role="alert"` by
|
|
46
54
|
* state; a `Note` announces nothing.
|
|
55
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
56
|
+
* announcement behavior required when that status changes.
|
|
47
57
|
*/
|
|
48
58
|
export const Note: FunctionComponent<INoteProps> = ({
|
|
49
59
|
children,
|
|
@@ -7,7 +7,14 @@ import type { FunctionComponent, ReactNode } from 'react';
|
|
|
7
7
|
// P owns no reading measure - that belongs to whatever owns the reading column (Prose #21).
|
|
8
8
|
const p = cva('font-primary text-body leading-body', {
|
|
9
9
|
variants: {
|
|
10
|
-
color: {
|
|
10
|
+
color: {
|
|
11
|
+
foreground: 'text-foreground',
|
|
12
|
+
muted: 'text-muted',
|
|
13
|
+
success: 'text-success',
|
|
14
|
+
warning: 'text-warning',
|
|
15
|
+
error: 'text-error',
|
|
16
|
+
info: 'text-info',
|
|
17
|
+
},
|
|
11
18
|
},
|
|
12
19
|
defaultVariants: { color: 'foreground' },
|
|
13
20
|
});
|
|
@@ -23,13 +30,16 @@ interface IPProps extends VariantProps<typeof p> {
|
|
|
23
30
|
*
|
|
24
31
|
* @Guarantees — enforced on every render
|
|
25
32
|
* - Renders a `p`, reading `--font-primary`, sized by `--text-body` and led by `--leading-body`.
|
|
26
|
-
* - `color` selects
|
|
33
|
+
* - `color` selects `foreground`, `muted`, `success`, `warning`, `error` or `info`; nothing else
|
|
34
|
+
* paints text. A status tone changes colour only and adds no announcement semantics.
|
|
27
35
|
*
|
|
28
36
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
29
37
|
* - Where line length matters, place the paragraph inside whatever bounds the reading measure; `P`
|
|
30
38
|
* does not constrain its own width.
|
|
31
39
|
* - For a paragraph inside a reading column, reach for `Prose.Body`, which is measure-bounded by its
|
|
32
40
|
* `Prose.Root`; `P` is for a paragraph with no reading column around it.
|
|
41
|
+
* - Status-toned content communicates its status without relying on colour. If a change needs to be
|
|
42
|
+
* announced, the caller owns that behavior; selecting a tone does not create a status event.
|
|
33
43
|
*/
|
|
34
44
|
export const P: FunctionComponent<IPProps> = ({ children, color, testId }) => (
|
|
35
45
|
<p className={p({ color })} data-testid={testId}>
|
|
@@ -20,7 +20,14 @@ const proseLede = cva('font-primary text-lede leading-lede text-foreground');
|
|
|
20
20
|
// semantic token re-pointed by `.dark`, so no variant carries a `dark:` class.
|
|
21
21
|
const proseBody = cva('font-primary text-body leading-body', {
|
|
22
22
|
variants: {
|
|
23
|
-
color: {
|
|
23
|
+
color: {
|
|
24
|
+
foreground: 'text-foreground',
|
|
25
|
+
muted: 'text-muted',
|
|
26
|
+
success: 'text-success',
|
|
27
|
+
warning: 'text-warning',
|
|
28
|
+
error: 'text-error',
|
|
29
|
+
info: 'text-info',
|
|
30
|
+
},
|
|
24
31
|
},
|
|
25
32
|
defaultVariants: { color: 'foreground' },
|
|
26
33
|
});
|
|
@@ -76,12 +83,15 @@ const ProseTail: FunctionComponent<IProseTailProps> = ({ children }) => (
|
|
|
76
83
|
* `--measure`, setting no font-size so the `ch` measure resolves against inherited body type.
|
|
77
84
|
* - `Root` stacks its children on `--space-stack` and takes no outer margin: the page owns the
|
|
78
85
|
* rhythm around the block, and the air the block wants is at its edges and inside the type.
|
|
79
|
-
* - `Lede` renders a `p` at the lede role; `Body` at the body role with a
|
|
80
|
-
* `color`; `Tail` at the small
|
|
86
|
+
* - `Lede` renders a fixed-foreground `p` at the lede role; `Body` at the body role with a
|
|
87
|
+
* `foreground`, `muted`, `success`, `warning`, `error` or `info` `color`; `Tail` at the small
|
|
88
|
+
* role, always muted. A status tone changes colour only and adds no announcement semantics.
|
|
81
89
|
*
|
|
82
90
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
83
91
|
* - Use `Prose.Body` for a paragraph inside this reading column; for a paragraph with no reading
|
|
84
92
|
* column around it - in a form, a card, a table cell - use `P` instead.
|
|
93
|
+
* - Status-toned content communicates its status without relying on colour. The caller owns any
|
|
94
|
+
* announcement behavior required when that status changes.
|
|
85
95
|
*/
|
|
86
96
|
export const Prose = {
|
|
87
97
|
Root: ProseRoot,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent } from 'react';
|
|
3
|
+
import type { Subject } from 'rxjs';
|
|
4
|
+
import { SliderConfigurationError } from './SliderConfigurationError';
|
|
5
|
+
|
|
6
|
+
// The track and thumb are vendor pseudo-elements, so every dimension on them needs a token name -
|
|
7
|
+
// no consumer selector reaches them (ADR 0004's test; the tick precedent). The track takes
|
|
8
|
+
// `controlBorder`: the one thing separating an unfilled control from the surface, >=3:1 against it.
|
|
9
|
+
// The thumb takes `foreground`, a solid mark drawn on the surface like the tab marker; disabled
|
|
10
|
+
// paints both in the disabled role, the non-operable statement every control makes. WebKit does
|
|
11
|
+
// not centre the thumb on the track, hence the computed negative margin; Firefox does, but draws a
|
|
12
|
+
// default thumb border WebKit does not, hence border-none on its thumb alone.
|
|
13
|
+
const slider = cva(
|
|
14
|
+
[
|
|
15
|
+
'block h-[var(--slider-thumb-size)] w-full cursor-pointer appearance-none bg-transparent',
|
|
16
|
+
'disabled:cursor-not-allowed',
|
|
17
|
+
'outline-focus-ring outline-offset-[var(--focus-ring-offset)] focus-visible:outline focus-visible:outline-[length:var(--focus-ring-width)]',
|
|
18
|
+
'[&::-webkit-slider-runnable-track]:h-[var(--slider-track-thickness)] [&::-webkit-slider-runnable-track]:rounded-[var(--radius-control)] [&::-webkit-slider-runnable-track]:bg-control-border',
|
|
19
|
+
'[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-[var(--slider-thumb-size)] [&::-webkit-slider-thumb]:rounded-[var(--radius-control)] [&::-webkit-slider-thumb]:bg-foreground',
|
|
20
|
+
'[&::-webkit-slider-thumb]:mt-[calc((var(--slider-track-thickness)-var(--slider-thumb-size))/2)]',
|
|
21
|
+
'[&:disabled::-webkit-slider-runnable-track]:bg-disabled [&:disabled::-webkit-slider-thumb]:bg-disabled',
|
|
22
|
+
'[&::-moz-range-track]:h-[var(--slider-track-thickness)] [&::-moz-range-track]:rounded-[var(--radius-control)] [&::-moz-range-track]:bg-control-border',
|
|
23
|
+
'[&::-moz-range-thumb]:size-[var(--slider-thumb-size)] [&::-moz-range-thumb]:rounded-[var(--radius-control)] [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:bg-foreground',
|
|
24
|
+
'[&:disabled::-moz-range-track]:bg-disabled [&:disabled::-moz-range-thumb]:bg-disabled',
|
|
25
|
+
].join(' '),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export interface ISliderProps {
|
|
29
|
+
/** The operating range's lower bound - interaction geometry, not a content rule (ADR 0009). */
|
|
30
|
+
min: number;
|
|
31
|
+
/** The operating range's upper bound. */
|
|
32
|
+
max: number;
|
|
33
|
+
/**
|
|
34
|
+
* The increment a movement produces. Defaults to 1: whole-step movement. Keeping `value` and
|
|
35
|
+
* `max` on the step grid is the caller's obligation - Slider neither rejects nor repairs an
|
|
36
|
+
* off-grid value, and the native control's own conduct stays observable: inside a surrounding
|
|
37
|
+
* form, misalignment counts as a `stepMismatch` against that form's validity.
|
|
38
|
+
*/
|
|
39
|
+
step?: number;
|
|
40
|
+
/** The current value. Controlled - the consumer holds it and passes it back in. */
|
|
41
|
+
value: number;
|
|
42
|
+
/**
|
|
43
|
+
* Emits the new value on every native input event. Required, not optional: the control is
|
|
44
|
+
* controlled and takes no part in form submission, so a consumer that did not listen here
|
|
45
|
+
* could never read a value at all.
|
|
46
|
+
*/
|
|
47
|
+
onInput$: Subject<number>;
|
|
48
|
+
/** The control's accessible name. */
|
|
49
|
+
label: string;
|
|
50
|
+
/**
|
|
51
|
+
* How the value is announced, in the consumer's wording ("$60 a week"). Left out, assistive
|
|
52
|
+
* technology reads the bare number: the library ships no wording of its own.
|
|
53
|
+
*/
|
|
54
|
+
valueText?: string;
|
|
55
|
+
/** The explicit non-operable state: native disabled conduct, no emissions, disabled paint. */
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
testId?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Structural misconfiguration is a programmer error, so it fails fast - no clamping, no
|
|
61
|
+
// normalization, no substitute emission. Step-grid alignment is deliberately not checked: that is
|
|
62
|
+
// the caller's obligation, documented on `step`, and native constraint behaviour stays observable.
|
|
63
|
+
const assertOperatingRange = (
|
|
64
|
+
range: Pick<Required<ISliderProps>, 'min' | 'max' | 'step' | 'value'>,
|
|
65
|
+
): void => {
|
|
66
|
+
for (const [name, given] of Object.entries(range)) {
|
|
67
|
+
if (!Number.isFinite(given)) {
|
|
68
|
+
throw new SliderConfigurationError(
|
|
69
|
+
`\`${name}\` must be a finite number, got ${given}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (range.min >= range.max) {
|
|
74
|
+
throw new SliderConfigurationError(
|
|
75
|
+
`\`min\` (${range.min}) must be below \`max\` (${range.max})`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (range.step <= 0) {
|
|
79
|
+
throw new SliderConfigurationError(
|
|
80
|
+
`\`step\` (${range.step}) must be positive`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (range.value < range.min || range.value > range.max) {
|
|
84
|
+
throw new SliderConfigurationError(
|
|
85
|
+
`\`value\` (${range.value}) must lie within [${range.min}, ${range.max}]`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A control that sets one numeric value by moving one thumb along a fixed, visible operating
|
|
92
|
+
* range. Controlled: the consumer holds the value and passes it back in, and a value it does not
|
|
93
|
+
* pass back is never adopted. It renders the value nowhere - the consumer sets any figures beside
|
|
94
|
+
* it with typography - and it fills its container's width the way `Input` does. `disabled` is the
|
|
95
|
+
* explicit non-operable state.
|
|
96
|
+
*/
|
|
97
|
+
export const Slider: FunctionComponent<ISliderProps> = ({
|
|
98
|
+
min,
|
|
99
|
+
max,
|
|
100
|
+
step = 1,
|
|
101
|
+
value,
|
|
102
|
+
onInput$,
|
|
103
|
+
label,
|
|
104
|
+
valueText,
|
|
105
|
+
disabled,
|
|
106
|
+
testId,
|
|
107
|
+
}) => {
|
|
108
|
+
assertOperatingRange({ min, max, step, value });
|
|
109
|
+
return (
|
|
110
|
+
<input
|
|
111
|
+
type={'range'}
|
|
112
|
+
className={slider()}
|
|
113
|
+
min={min}
|
|
114
|
+
max={max}
|
|
115
|
+
step={step}
|
|
116
|
+
value={value}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
aria-label={label}
|
|
119
|
+
aria-valuetext={valueText}
|
|
120
|
+
data-testid={testId}
|
|
121
|
+
// React's onChange rides the native input event, so every movement emits through here.
|
|
122
|
+
onChange={(event) => onInput$.next(event.currentTarget.valueAsNumber)}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
};
|