@juwel-development/design-system 3.11.0 → 4.0.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 +16 -3
- package/dist/design-system.js +1029 -540
- package/dist/styles.css +2 -0
- package/dist/styles.dark.css +2 -0
- package/dist/styles.light.css +2 -0
- package/dist/types/Arrangement/ColumnLayout/ColumnLayout.d.ts +38 -18
- package/dist/types/Display/Callout/Callout.d.ts +31 -0
- package/dist/types/Display/Icon/Icon.d.ts +20 -24
- package/dist/types/Display/Meter/Meter.d.ts +5 -6
- package/dist/types/Display/Rule/Rule.d.ts +22 -0
- package/dist/types/Display/Table/Table.d.ts +57 -7
- package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +1 -1
- package/dist/types/Display/Typography/H1/H1.d.ts +1 -1
- package/dist/types/Display/Typography/H2/H2.d.ts +1 -1
- package/dist/types/Display/Typography/H3/H3.d.ts +1 -1
- package/dist/types/Display/Typography/H4/H4.d.ts +1 -1
- package/dist/types/Display/Typography/H5/H5.d.ts +1 -1
- package/dist/types/Display/Typography/H6/H6.d.ts +1 -1
- package/dist/types/Display/Typography/Note/Note.d.ts +16 -3
- package/dist/types/Display/Typography/P/P.d.ts +15 -2
- package/dist/types/Display/Typography/Prose/Prose.d.ts +14 -6
- package/dist/types/Interaction/Button/Button.d.ts +10 -4
- package/dist/types/Interaction/Slider/Slider.d.ts +3 -0
- package/dist/types/Interaction/Stepper/Stepper.d.ts +59 -0
- package/dist/types/Interaction/Stepper/StepperCompositionError.d.ts +3 -0
- package/dist/types/Interaction/Switch/Switch.d.ts +20 -0
- package/dist/types/Interaction/Tabs/Tabs.d.ts +24 -3
- package/dist/types/Layout/Dialog/Dialog.d.ts +11 -3
- package/dist/types/Layout/Form/FormConfigurationError.d.ts +3 -0
- package/dist/types/Layout/Pane/Pane.d.ts +55 -0
- package/dist/types/Layout/Shell/Shell.d.ts +39 -0
- package/dist/types/Layout/Sidebar/Sidebar.d.ts +16 -2
- package/dist/types/Theme/Palette.d.ts +11 -4
- package/dist/types/index.d.ts +21 -1
- package/package.json +18 -8
- package/src/Arrangement/ColumnLayout/ColumnLayout.tsx +86 -28
- package/src/Display/Callout/Callout.tsx +64 -0
- package/src/Display/Icon/Icon.tsx +53 -13
- package/src/Display/Meter/Meter.tsx +12 -13
- package/src/Display/Rule/Rule.tsx +31 -0
- package/src/Display/Table/Table.tsx +189 -24
- package/src/Display/Typography/Note/Note.tsx +27 -11
- package/src/Display/Typography/P/P.tsx +29 -4
- package/src/Display/Typography/Prose/Prose.tsx +44 -19
- package/src/Interaction/Button/Button.tsx +11 -4
- package/src/Interaction/Slider/Slider.tsx +3 -5
- package/src/Interaction/Stepper/Stepper.tsx +307 -0
- package/src/Interaction/Stepper/StepperCompositionError.ts +6 -0
- package/src/Interaction/Switch/Switch.tsx +64 -0
- package/src/Interaction/Tabs/Tabs.tsx +44 -10
- package/src/Layout/Dialog/Dialog.tsx +19 -6
- package/src/Layout/Form/Form.tsx +2 -1
- package/src/Layout/Form/FormConfigurationError.ts +6 -0
- package/src/Layout/Pane/Pane.tsx +159 -0
- package/src/Layout/Shell/Shell.tsx +124 -0
- package/src/Layout/Sidebar/Sidebar.tsx +31 -7
- package/src/Theme/Palette.ts +17 -6
- package/src/Theme/renderTokens.ts +61 -12
- package/src/index.ts +21 -2
- package/src/styles.css +5 -6
- package/src/styles.dark.css +5 -6
- package/src/styles.light.css +5 -6
- package/src/tokens.css +54 -9
- package/src/tokens.dark.css +51 -8
- package/src/tokens.light.css +49 -6
- package/dist/index.css +0 -2
|
@@ -19,6 +19,10 @@ import { ColumnLayoutConfigurationError } from './ColumnLayoutConfigurationError
|
|
|
19
19
|
// the role from one declaration. Nothing here measures: the arrangement needs no JavaScript.
|
|
20
20
|
const columnLayout = cva('flex flex-wrap items-start', {
|
|
21
21
|
variants: {
|
|
22
|
+
fill: {
|
|
23
|
+
false: '',
|
|
24
|
+
true: 'h-full min-h-0 content-start [--column-layout-shortfall:clamp(0px,(var(--column-layout-threshold)-100cqi)*1000000,1px)] [&>div]:[@container_style(--column-layout-shortfall:0px)]:h-full [&>div]:[@container_style(--column-layout-shortfall:0px)]:min-h-0',
|
|
25
|
+
},
|
|
22
26
|
gap: {
|
|
23
27
|
stack:
|
|
24
28
|
'gap-[var(--space-stack)] [--column-layout-gap:var(--space-stack)]',
|
|
@@ -26,30 +30,38 @@ const columnLayout = cva('flex flex-wrap items-start', {
|
|
|
26
30
|
'gap-[var(--space-region)] [--column-layout-gap:var(--space-region)]',
|
|
27
31
|
},
|
|
28
32
|
},
|
|
29
|
-
defaultVariants: { gap: 'region' },
|
|
33
|
+
defaultVariants: { gap: 'region', fill: false },
|
|
30
34
|
});
|
|
31
35
|
|
|
36
|
+
// A fill frame supplies the query width without imposing block-size containment. The row's
|
|
37
|
+
// registered shortfall resolves against it, so style queries release the columns when stacked.
|
|
38
|
+
const columnLayoutFrame = cva(
|
|
39
|
+
'h-full min-h-0 min-w-0 [container-type:inline-size]',
|
|
40
|
+
);
|
|
41
|
+
|
|
32
42
|
// The all-or-one switch, after Heydon Pickering's "Holy Albatross": `100%` is the Root's width W
|
|
33
43
|
// and the threshold T sits on the Root, so W >= T clamps the basis to 0 (one line, grown by weight)
|
|
34
44
|
// and W < T amplifies the shortfall past 100% (a line each). The factor turns a 1/64px shortfall,
|
|
35
45
|
// Chrome's layout grain, into a full switch; `0px` and `100%` are the two states, not measurements.
|
|
36
46
|
const columnLayoutColumn = cva(
|
|
37
|
-
'min-w-0 grow-[var(--column-layout-weight)] basis-[clamp(0px,(var(--column-layout-threshold)-100%)*1000000,100%)]',
|
|
47
|
+
'min-w-0 grow-[calc(var(--column-layout-weight)/var(--column-layout-growth-unit))] basis-[clamp(0px,(var(--column-layout-threshold)-100%)*1000000,100%)] max-w-[var(--column-layout-maximum)]',
|
|
38
48
|
);
|
|
39
49
|
|
|
40
|
-
// React's CSSProperties is closed over known properties; the
|
|
50
|
+
// React's CSSProperties is closed over known properties; the custom properties the recipes
|
|
41
51
|
// read are declared here so the style objects stay typed without an assertion.
|
|
42
52
|
type ColumnLayoutRootStyle = CSSProperties & {
|
|
43
53
|
'--column-layout-threshold': string;
|
|
54
|
+
'--column-layout-growth-unit': number;
|
|
44
55
|
};
|
|
45
56
|
type ColumnLayoutColumnStyle = CSSProperties & {
|
|
46
57
|
'--column-layout-weight': number;
|
|
58
|
+
'--column-layout-maximum': string;
|
|
47
59
|
};
|
|
48
60
|
|
|
49
61
|
/** The name of a CSS custom property, as written in a stylesheet: `--main-column-min-width`. */
|
|
50
|
-
type
|
|
62
|
+
type WidthToken = `--${string}`;
|
|
51
63
|
|
|
52
|
-
type Track = { weight: number; minWidth:
|
|
64
|
+
type Track = { weight: number; minWidth: WidthToken; maxWidth?: WidthToken };
|
|
53
65
|
|
|
54
66
|
// Granted only to a Column the Root counted: the Root wraps each direct Column child in it, and a
|
|
55
67
|
// Column revokes it for its own descendants. Anything else is outside the composition.
|
|
@@ -63,7 +75,7 @@ const toTrack = (
|
|
|
63
75
|
element: ReactElement<IColumnLayoutColumnProps>,
|
|
64
76
|
index: number,
|
|
65
77
|
): Track => {
|
|
66
|
-
const { weight, minWidth } = element.props;
|
|
78
|
+
const { weight, minWidth, maxWidth } = element.props;
|
|
67
79
|
if (!Number.isFinite(weight) || weight <= 0) {
|
|
68
80
|
throw new ColumnLayoutConfigurationError(
|
|
69
81
|
`column ${index + 1} needs a positive finite weight (got ${weight})`,
|
|
@@ -74,7 +86,12 @@ const toTrack = (
|
|
|
74
86
|
`column ${index + 1} needs a custom-property name such as --main-column-min-width for minWidth (got ${JSON.stringify(minWidth)})`,
|
|
75
87
|
);
|
|
76
88
|
}
|
|
77
|
-
|
|
89
|
+
if (maxWidth !== undefined && !TOKEN_NAME.test(maxWidth)) {
|
|
90
|
+
throw new ColumnLayoutConfigurationError(
|
|
91
|
+
`column ${index + 1} needs a custom-property name such as --support-column-max-width for maxWidth (got ${JSON.stringify(maxWidth)})`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return { weight, minWidth, maxWidth };
|
|
78
95
|
};
|
|
79
96
|
|
|
80
97
|
// The documented threshold, `(n - 1) * g + max(m_i * S / w_i)`, left to the browser to resolve
|
|
@@ -87,6 +104,12 @@ const thresholdOf = (tracks: readonly Track[]): string => {
|
|
|
87
104
|
return `calc(${tracks.length - 1} * var(--column-layout-gap) + max(${shares.join(', ')}))`;
|
|
88
105
|
};
|
|
89
106
|
|
|
107
|
+
// Flex factors below one leave surplus unallocated, even after other columns freeze at caps.
|
|
108
|
+
const growthUnitOf = (tracks: readonly Track[]): number =>
|
|
109
|
+
tracks.some((track) => track.maxWidth !== undefined)
|
|
110
|
+
? Math.min(1, ...tracks.map((track) => track.weight))
|
|
111
|
+
: 1;
|
|
112
|
+
|
|
90
113
|
const isFragment = (
|
|
91
114
|
node: ReactNode,
|
|
92
115
|
): node is ReactElement<{ children?: ReactNode }> =>
|
|
@@ -94,6 +117,9 @@ const isFragment = (
|
|
|
94
117
|
|
|
95
118
|
export interface IColumnLayoutRootProps
|
|
96
119
|
extends VariantProps<typeof columnLayout> {
|
|
120
|
+
/** Accept the parent's allocated height and pass it to horizontal columns. Unsized parents
|
|
121
|
+
* and stacked columns retain content height; the holder owns stacked overflow. */
|
|
122
|
+
fill?: VariantProps<typeof columnLayout>['fill'];
|
|
97
123
|
/** The columns, as `ColumnLayout.Column` elements: direct children, or arrays and fragments of
|
|
98
124
|
* them. A conditional that renders nothing reserves nothing. */
|
|
99
125
|
children?: ReactNode;
|
|
@@ -107,7 +133,11 @@ export interface IColumnLayoutColumnProps {
|
|
|
107
133
|
/** The name of the consumer's custom property holding this column's minimum readable width,
|
|
108
134
|
* such as `--main-column-min-width`. A token name, never a length or a `var()`: the consumer
|
|
109
135
|
* declares it in the theme with a nonnegative CSS length. */
|
|
110
|
-
minWidth:
|
|
136
|
+
minWidth: WidthToken;
|
|
137
|
+
/** Optional horizontal cap, named by a theme custom property. Stacked columns ignore it.
|
|
138
|
+
* The theme must supply a nonnegative CSS length at least as large as minWidth; resolved
|
|
139
|
+
* lengths are not read or validated by the component. */
|
|
140
|
+
maxWidth?: WidthToken;
|
|
111
141
|
/** The column's content. Rendered unmodified: the column imposes no anatomy. */
|
|
112
142
|
children?: ReactNode;
|
|
113
143
|
testId?: string;
|
|
@@ -147,6 +177,7 @@ const markColumns = (children: ReactNode): ReactNode =>
|
|
|
147
177
|
|
|
148
178
|
const ColumnLayoutRoot: FunctionComponent<IColumnLayoutRootProps> = ({
|
|
149
179
|
gap,
|
|
180
|
+
fill,
|
|
150
181
|
children,
|
|
151
182
|
testId,
|
|
152
183
|
}) => {
|
|
@@ -154,16 +185,25 @@ const ColumnLayoutRoot: FunctionComponent<IColumnLayoutRootProps> = ({
|
|
|
154
185
|
const style: ColumnLayoutRootStyle | undefined =
|
|
155
186
|
tracks.length === 0
|
|
156
187
|
? undefined
|
|
157
|
-
: {
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
: {
|
|
189
|
+
'--column-layout-threshold': thresholdOf(tracks),
|
|
190
|
+
'--column-layout-growth-unit': growthUnitOf(tracks),
|
|
191
|
+
};
|
|
192
|
+
const row = (
|
|
193
|
+
<div
|
|
194
|
+
className={columnLayout({ gap, fill })}
|
|
195
|
+
style={style}
|
|
196
|
+
data-testid={testId}
|
|
197
|
+
>
|
|
160
198
|
{markColumns(children)}
|
|
161
199
|
</div>
|
|
162
200
|
);
|
|
201
|
+
return fill ? <div className={columnLayoutFrame()}>{row}</div> : row;
|
|
163
202
|
};
|
|
164
203
|
|
|
165
204
|
const ColumnLayoutColumn: FunctionComponent<IColumnLayoutColumnProps> = ({
|
|
166
205
|
weight,
|
|
206
|
+
maxWidth,
|
|
167
207
|
children,
|
|
168
208
|
testId,
|
|
169
209
|
}) => {
|
|
@@ -171,7 +211,13 @@ const ColumnLayoutColumn: FunctionComponent<IColumnLayoutColumnProps> = ({
|
|
|
171
211
|
if (!isCounted) {
|
|
172
212
|
throw new ColumnLayoutCompositionError();
|
|
173
213
|
}
|
|
174
|
-
const style: ColumnLayoutColumnStyle = {
|
|
214
|
+
const style: ColumnLayoutColumnStyle = {
|
|
215
|
+
'--column-layout-weight': weight,
|
|
216
|
+
'--column-layout-maximum':
|
|
217
|
+
maxWidth === undefined
|
|
218
|
+
? 'none'
|
|
219
|
+
: `max(var(${maxWidth}), clamp(0px, (var(--column-layout-threshold) - 100%) * 1000000, 100%))`,
|
|
220
|
+
};
|
|
175
221
|
return (
|
|
176
222
|
<div className={columnLayoutColumn()} style={style} data-testid={testId}>
|
|
177
223
|
<ColumnLayoutMembership.Provider value={false}>
|
|
@@ -189,45 +235,57 @@ const ColumnLayoutColumn: FunctionComponent<IColumnLayoutColumnProps> = ({
|
|
|
189
235
|
* one answers to the width of its holder (docs/adr/0008, Amendments).
|
|
190
236
|
*
|
|
191
237
|
* @Guarantees — enforced on every render
|
|
192
|
-
* - `Root` renders
|
|
193
|
-
* `div` directly inside
|
|
238
|
+
* - `Root` renders plain containers with no landmark role, no heading and no margin. Each
|
|
239
|
+
* `Column` is a `div` directly inside the row, so DOM, reading and keyboard order are the
|
|
194
240
|
* order of the children in both arrangements, and no column is ever remounted merely because
|
|
195
241
|
* the arrangement changed: resizing keeps every column's state and focus.
|
|
196
242
|
* - In the horizontal arrangement the gaps are taken off the Root's content width and the rest is
|
|
197
243
|
* divided in proportion to the weights: `2`, `1`, `1` is one half and two quarters of it. The
|
|
198
|
-
* columns align at the top and keep their own heights.
|
|
244
|
+
* columns align at the top and keep their own heights by default (`fill={false}`). Optional
|
|
245
|
+
* maximums cap each allocation and redistribute surplus by weight among columns still able
|
|
246
|
+
* to grow. If all are capped, unused width stays at the trailing edge with unchanged gaps.
|
|
247
|
+
* - With `fill`, Root accepts its parent's allocated height and every horizontal column takes
|
|
248
|
+
* that height, bounding a filling Pane, Table or ScrollContainer. An internal inline-size
|
|
249
|
+
* frame supplies the width query; columns remain direct children of the row.
|
|
250
|
+
* - In an unsized parent, `fill` invents no bound: columns retain content height. When stacked,
|
|
251
|
+
* columns return to content height and the holder owns scrolling. No column scrolls itself.
|
|
199
252
|
* - The row holds only while every proportional share is at least its own minimum. The moment one
|
|
200
253
|
* falls short, every column takes a line of its own and fills the Root's width - a column
|
|
201
254
|
* narrower than its minimum included: the minimum decides the switch and never floors a width.
|
|
202
|
-
* There is no in-between: no column wraps alone
|
|
203
|
-
*
|
|
204
|
-
* above `(n - 1) * g + max(m_i * S / w_i)`.
|
|
255
|
+
* There is no in-between: no column wraps alone. Caps and redistribution never postpone
|
|
256
|
+
* the switch; stacked columns ignore their maximums. For n columns, gap g, total weight S and
|
|
257
|
+
* minimums m_i the row fits at and above `(n - 1) * g + max(m_i * S / w_i)`.
|
|
205
258
|
* - The space measured is the Root's own width, never the viewport, so two instances on one page
|
|
206
259
|
* switch independently, and a narrow holder on a wide screen stacks.
|
|
207
|
-
* - Every
|
|
208
|
-
* token - in a theme class, a media query, or a scope around the Root -
|
|
209
|
-
*
|
|
260
|
+
* - Every bound is read from the theme as a CSS length each time layout runs, so re-pointing a
|
|
261
|
+
* token - in a theme class, a media query, or a scope around the Root - updates allocation.
|
|
262
|
+
* Minimums move the threshold; maximums change caps. Font-relative bounds follow the type.
|
|
210
263
|
* - `gap` selects which space role separates the columns, across the row and between the stacked
|
|
211
264
|
* lines alike: `region` (the default), the gap between groups of blocks, or `stack`, the gap
|
|
212
265
|
* between siblings within one block. Nothing else.
|
|
213
266
|
* - Only rendered columns count: a conditional that renders nothing reserves neither width nor
|
|
214
|
-
* gap, a single column fills the width, and an empty Root holds no tracks and no gaps.
|
|
267
|
+
* gap, a single uncapped column fills the width, and an empty Root holds no tracks and no gaps.
|
|
215
268
|
* Allocations follow every change of columns, weights, gap, holder width or theme.
|
|
216
269
|
* - A column adds no scrolling, truncation or overflow treatment of its own and does not widen its
|
|
217
270
|
* track for its content: the content keeps whatever wrapping or scrolling contract it has.
|
|
218
271
|
* - Every value it emits is a role the token layer or the consumer's theme already names; the
|
|
219
272
|
* only literals are the switch's two states.
|
|
220
|
-
* - A non-positive or non-finite `weight`, or a
|
|
273
|
+
* - A non-positive or non-finite `weight`, or a bound that is not a custom-property name,
|
|
221
274
|
* throws {@link ColumnLayoutConfigurationError}; a `Column` that is not a direct child of a
|
|
222
275
|
* `Root` - loose, nested in another column, or reached through a wrapping component - throws
|
|
223
276
|
* {@link ColumnLayoutCompositionError}. Both are programmer errors, raised loud and early.
|
|
224
277
|
*
|
|
225
278
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
226
|
-
* - Every `minWidth` token is declared, on the Root or an ancestor of it,
|
|
227
|
-
* CSS length: `24rem`, `20em`, `40ch`, `320px`. A `rem` resolves against
|
|
228
|
-
* `em` or `ch` against the Root's inherited type.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
279
|
+
* - Every `minWidth` and supplied `maxWidth` token is declared, on the Root or an ancestor of it,
|
|
280
|
+
* as a valid nonnegative CSS length: `24rem`, `20em`, `40ch`, `320px`. A `rem` resolves against
|
|
281
|
+
* the document root, an `em` or `ch` against the Root's inherited type. Missing or invalid
|
|
282
|
+
* bounds are outside this contract and can invalidate the switch or cap.
|
|
283
|
+
* - Maximums must be at least their column's minimum in every scope and responsive state.
|
|
284
|
+
* Percentages are outside the contract. Resolved lengths are not read by the component;
|
|
285
|
+
* maximum below minimum has no guaranteed runtime exception. A valid maximum below the
|
|
286
|
+
* original share at the threshold applies immediately, with its surplus redistributed.
|
|
287
|
+
* - With `fill`, the holder supplies a definite height to bound content and owns overflow for
|
|
288
|
+
* the stacked fallback. Filling children own their overflow; fixed content must fit the bound.
|
|
231
289
|
* - The holder gives the Root a definite width, as any block does. Inside a shrink-to-fit frame
|
|
232
290
|
* there is no width to measure the shares against.
|
|
233
291
|
* - Content that cannot wrap - an unbroken string, a fixed-width control - needs its own overflow
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
const callout = cva(
|
|
5
|
+
'min-w-0 border-s-[length:var(--callout-mark-width)] border-solid ps-[var(--space-callout-inset)] text-foreground [overflow-wrap:anywhere] forced-colors:border-s-[CanvasText]',
|
|
6
|
+
{
|
|
7
|
+
variants: {
|
|
8
|
+
tone: {
|
|
9
|
+
warning: 'border-warning',
|
|
10
|
+
info: 'border-info',
|
|
11
|
+
success: 'border-success',
|
|
12
|
+
error: 'border-error',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: { tone: 'warning' },
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export interface ICalloutProps extends VariantProps<typeof callout> {
|
|
20
|
+
/** Content and its arrangement belong to the consumer. */
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
/** Optional accessible group name, never rendered as visible text. Empty means unnamed. */
|
|
23
|
+
name?: string;
|
|
24
|
+
testId?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A status-toned leading-edge mark beside consumer-owned content, without a surrounding box.
|
|
29
|
+
* @Guarantees
|
|
30
|
+
* - Supports warning (default), info, success and error through the theme's status tones.
|
|
31
|
+
* - The logical leading border follows writing direction; forced colours use CanvasText.
|
|
32
|
+
* - Only the leading edge is inset; no fill, outer space, layout, clipping or fixed size is added.
|
|
33
|
+
* - Named content is a group; unnamed content has no role. Neither adds focus or announcements.
|
|
34
|
+
* - Children keep their order, arrangement and native keyboard behaviour.
|
|
35
|
+
* @CallerMustEnsure
|
|
36
|
+
* - State the status and its consequence in visible text or a named Icon; colour is supplementary.
|
|
37
|
+
* - Keep the optional name consistent with any visible heading. Own any live announcements.
|
|
38
|
+
* - Keep tones at least 3:1 against the surrounding surface; the shared 4.5:1 text floor clears it.
|
|
39
|
+
* - Theme --callout-mark-width is a positive CSS length; --space-callout-inset is nonnegative.
|
|
40
|
+
* - Content with intrinsic sizing owns its overflow; Callout wraps prose but never clips it.
|
|
41
|
+
* @UXGuidelines
|
|
42
|
+
* - Mark a block needing attention; the consumer owns its text, controls and status meaning.
|
|
43
|
+
*/
|
|
44
|
+
export const Callout: FunctionComponent<ICalloutProps> = ({
|
|
45
|
+
children,
|
|
46
|
+
tone,
|
|
47
|
+
name,
|
|
48
|
+
testId,
|
|
49
|
+
}) =>
|
|
50
|
+
name === undefined || name === '' ? (
|
|
51
|
+
<div className={callout({ tone })} data-testid={testId}>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
) : (
|
|
55
|
+
// biome-ignore lint/a11y/useSemanticElements: a content group must not acquire fieldset form semantics or intrinsic minimum sizing
|
|
56
|
+
<div
|
|
57
|
+
className={callout({ tone })}
|
|
58
|
+
role="group"
|
|
59
|
+
aria-label={name}
|
|
60
|
+
data-testid={testId}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
@@ -2,7 +2,7 @@ import type { VariantProps } from 'class-variance-authority';
|
|
|
2
2
|
import { cva } from 'class-variance-authority';
|
|
3
3
|
import type { FunctionComponent } from 'react';
|
|
4
4
|
|
|
5
|
-
// The glyph is sized in `em` and
|
|
5
|
+
// The glyph is sized in `em` and drawn in `currentColor`, so it takes the size and colour of the
|
|
6
6
|
// text it sits in and needs no size prop. Baseline at -0.125em is what seats a 1em square beside
|
|
7
7
|
// lowercase letters; the inline-box exists so an icon reads as a word in its line. `color` has no
|
|
8
8
|
// default on purpose (docs/adr/0011, the #126 amendment): absent, no class competes with the text.
|
|
@@ -27,9 +27,13 @@ const icon = cva(
|
|
|
27
27
|
const bubble =
|
|
28
28
|
'M3.5 2h9a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7.25L4 13.75V11h-.5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z';
|
|
29
29
|
|
|
30
|
-
// Each name selects a drawing and nothing else: drawings are told apart by their
|
|
30
|
+
// Each name selects a drawing and nothing else: drawings are told apart by their shapes, not by
|
|
31
31
|
// colour or size, so they survive forced-colors mode and a monochrome print.
|
|
32
32
|
const drawings = {
|
|
33
|
+
plus: ['M3 8h10', 'M8 3v10'],
|
|
34
|
+
right: ['M3 8h10', 'M9 4l4 4-4 4'],
|
|
35
|
+
remove: ['M3 8h10'],
|
|
36
|
+
dot: ['M8 5a3 3 0 1 0 0 6a3 3 0 1 0 0-6z'],
|
|
33
37
|
sort: [
|
|
34
38
|
'M5.5 13V3',
|
|
35
39
|
'M3 5.5l2.5-2.5L8 5.5',
|
|
@@ -49,9 +53,7 @@ const drawings = {
|
|
|
49
53
|
'bubble-tick': [bubble, 'M5.75 6.75l1.5 1.5 3-3.5'],
|
|
50
54
|
} as const;
|
|
51
55
|
|
|
52
|
-
export interface
|
|
53
|
-
/** Which drawing. A name selects a shape, never a state or a behaviour. */
|
|
54
|
-
name: keyof typeof drawings;
|
|
56
|
+
export interface IIconPartProps extends VariantProps<typeof icon> {
|
|
55
57
|
/**
|
|
56
58
|
* The accessible name that makes the icon a status mark (CONTEXT.md). Give it only where the icon
|
|
57
59
|
* stands alone for a state; absent or empty, the icon stays hidden from assistive technology.
|
|
@@ -60,10 +62,19 @@ export interface IIconProps extends VariantProps<typeof icon> {
|
|
|
60
62
|
testId?: string;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
export interface IIconProps extends IIconPartProps {
|
|
66
|
+
/** Legacy selector, retained alongside the compound drawing components. */
|
|
67
|
+
name: Exclude<keyof typeof drawings, 'plus' | 'right' | 'remove'>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface IIconDrawingProps extends IIconPartProps {
|
|
71
|
+
name: keyof typeof drawings;
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
75
|
+
* Seven drawings: a filled dot, three sort indicators, a bin, and a bubble holding an exclamation
|
|
76
|
+
* mark or a tick. Decorative beside the control or text that names it, or - given a `label` - a
|
|
77
|
+
* status mark that stands alone for an item's state.
|
|
67
78
|
*
|
|
68
79
|
* @Guarantees — enforced on every render
|
|
69
80
|
* - Out of the tab order in both forms: it adds no stop.
|
|
@@ -72,15 +83,18 @@ export interface IIconProps extends VariantProps<typeof icon> {
|
|
|
72
83
|
* without a name is never rendered.
|
|
73
84
|
* - Scales with the surrounding font-size and takes the surrounding text colour unless `color`
|
|
74
85
|
* selects `muted` or a status tone. The tone changes colour only: no role, name or announcement.
|
|
75
|
-
* - Every drawing differs in shape
|
|
86
|
+
* - Every drawing differs in shape; the dot is filled, the other drawings are outlined.
|
|
87
|
+
* - Inline glyphs align with the first line of the text beside them.
|
|
76
88
|
*
|
|
77
89
|
* @CallerMustEnsure
|
|
78
90
|
* - An unlabelled icon sits beside what carries the meaning: a `Button` label or `ariaLabel`, a
|
|
79
91
|
* header cell's `ariaSort`. Inside a named control it stays unlabelled, or the name is said twice.
|
|
80
92
|
* - A status mark's `label` states the status in the reader's language; the library words nothing.
|
|
81
|
-
* - The two forms of one state use two drawings, never one drawing in two colours.
|
|
93
|
+
* - The two forms of one state use two drawings, never one drawing in two colours. When absence
|
|
94
|
+
* is the other form of a dot, the dot must not be the only carrier: give the state a name or
|
|
95
|
+
* a second cue, such as stronger text weight. A standalone dot still requires a label.
|
|
82
96
|
*/
|
|
83
|
-
|
|
97
|
+
const IconDrawing: FunctionComponent<IIconDrawingProps> = ({
|
|
84
98
|
name,
|
|
85
99
|
label,
|
|
86
100
|
color,
|
|
@@ -102,8 +116,8 @@ export const Icon: FunctionComponent<IIconProps> = ({
|
|
|
102
116
|
<path
|
|
103
117
|
key={segment}
|
|
104
118
|
d={segment}
|
|
105
|
-
fill={'none'}
|
|
106
|
-
stroke={'currentColor'}
|
|
119
|
+
fill={name === 'dot' ? 'currentColor' : 'none'}
|
|
120
|
+
stroke={name === 'dot' ? 'none' : 'currentColor'}
|
|
107
121
|
strokeWidth={1.5}
|
|
108
122
|
strokeLinecap={'round'}
|
|
109
123
|
strokeLinejoin={'round'}
|
|
@@ -111,3 +125,29 @@ export const Icon: FunctionComponent<IIconProps> = ({
|
|
|
111
125
|
))}
|
|
112
126
|
</svg>
|
|
113
127
|
);
|
|
128
|
+
|
|
129
|
+
// All parts share the same SVG renderer and recipe; only their drawing is fixed.
|
|
130
|
+
const partOf =
|
|
131
|
+
(name: keyof typeof drawings): FunctionComponent<IIconPartProps> =>
|
|
132
|
+
({ label, color, testId }) => (
|
|
133
|
+
<IconDrawing name={name} label={label} color={color} testId={testId} />
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const LegacyIcon: FunctionComponent<IIconProps> = ({
|
|
137
|
+
name,
|
|
138
|
+
label,
|
|
139
|
+
color,
|
|
140
|
+
testId,
|
|
141
|
+
}) => <IconDrawing name={name} label={label} color={color} testId={testId} />;
|
|
142
|
+
|
|
143
|
+
/** Legacy named drawings and five compound action drawings, all sharing em sizing, currentColor,
|
|
144
|
+
* optional status tones, label and testId. Parts take no name. Leave parts decorative inside
|
|
145
|
+
* Buttons and supply the action-specific ariaLabel on the Button itself. Remove is a minus, not bin.
|
|
146
|
+
*/
|
|
147
|
+
export const Icon = Object.assign(LegacyIcon, {
|
|
148
|
+
Plus: partOf('plus'),
|
|
149
|
+
Up: partOf('sort-ascending'),
|
|
150
|
+
Down: partOf('sort-descending'),
|
|
151
|
+
Right: partOf('right'),
|
|
152
|
+
Remove: partOf('remove'),
|
|
153
|
+
});
|
|
@@ -31,10 +31,15 @@ const meterFill = cva('h-full w-[calc(var(--meter-level)*100%)]', {
|
|
|
31
31
|
// is declared here so the style object stays typed without an assertion.
|
|
32
32
|
type MeterLevelStyle = CSSProperties & { '--meter-level': number };
|
|
33
33
|
|
|
34
|
+
const filledShare = (value: number, min: number, max: number): number => {
|
|
35
|
+
if (value <= min) return 0;
|
|
36
|
+
if (value >= max) return 1;
|
|
37
|
+
return (value - min) / (max - min);
|
|
38
|
+
};
|
|
39
|
+
|
|
34
40
|
export interface IMeterProps extends VariantProps<typeof meterFill> {
|
|
35
|
-
/** The current level.
|
|
36
|
-
*
|
|
37
|
-
* plausible level. */
|
|
41
|
+
/** The current level. Off-range values clamp the visual fill to the nearest bound;
|
|
42
|
+
* `aria-valuenow` retains the raw value. */
|
|
38
43
|
value: number;
|
|
39
44
|
/** The bottom of the scale. Defaults to `0`. */
|
|
40
45
|
min?: number;
|
|
@@ -58,8 +63,8 @@ export interface IMeterProps extends VariantProps<typeof meterFill> {
|
|
|
58
63
|
* - The root carries `role="meter"`, `aria-valuenow`/`aria-valuemin`/`aria-valuemax`, the `label`
|
|
59
64
|
* as its accessible name, and `aria-valuetext` exactly when `valueText` is supplied. It is not
|
|
60
65
|
* focusable, and the visual fill is hidden from the accessibility tree.
|
|
61
|
-
* - The filled share is `(value - min) / (max - min)
|
|
62
|
-
* inline-start under both LTR and RTL direction.
|
|
66
|
+
* - The filled share is `(value - min) / (max - min)`, clamped to `[0, 1]`, and begins at logical
|
|
67
|
+
* inline-start under both LTR and RTL direction. `aria-valuenow` retains the raw value.
|
|
63
68
|
* - Neutral paints the fill with `meterFill`; `depleting` maps the normalized level linearly
|
|
64
69
|
* through OKLab from `meterFill` at the maximum to `error` at the minimum - no threshold, no
|
|
65
70
|
* wording, no icon (docs/adr/0010-meter-depletion-is-a-treatment.md).
|
|
@@ -68,7 +73,7 @@ export interface IMeterProps extends VariantProps<typeof meterFill> {
|
|
|
68
73
|
* radius token - Meter is not a control), and draws a persistent one-pixel `rule` line that
|
|
69
74
|
* identifies the whole capacity against the surface.
|
|
70
75
|
* - Invalid numeric configuration throws {@link MeterConfigurationError}: non-finite `value`,
|
|
71
|
-
* `min` or `max`, `min >= max
|
|
76
|
+
* `min` or `max`, or `min >= max`.
|
|
72
77
|
* - No visible wording of its own, no numeric readout, no caption slot, no `className`/`style`
|
|
73
78
|
* passthrough.
|
|
74
79
|
*
|
|
@@ -107,14 +112,8 @@ export const Meter: FunctionComponent<IMeterProps> = ({
|
|
|
107
112
|
`min must be less than max (got min=${min}, max=${max})`,
|
|
108
113
|
);
|
|
109
114
|
}
|
|
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
115
|
const style: MeterLevelStyle = {
|
|
117
|
-
'--meter-level': (value
|
|
116
|
+
'--meter-level': filledShare(value, min, max),
|
|
118
117
|
};
|
|
119
118
|
|
|
120
119
|
return (
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent } from 'react';
|
|
3
|
+
|
|
4
|
+
const rule = cva(
|
|
5
|
+
'w-full shrink-0 border-t border-solid border-border forced-colors:border-[CanvasText]',
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export interface IRuleProps {
|
|
9
|
+
testId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A decorative horizontal hairline between two blocks of one arrangement.
|
|
14
|
+
*
|
|
15
|
+
* @Guarantees
|
|
16
|
+
* - One CSS pixel in the theme's border role, spanning its holder's width.
|
|
17
|
+
* - Forced colours use CanvasText so the hairline survives the user's palette.
|
|
18
|
+
* - Always hidden from assistive technology, with no role, content or focus stop.
|
|
19
|
+
* - No margin or padding: the surrounding arrangement owns space on both sides.
|
|
20
|
+
*
|
|
21
|
+
* @CallerMustEnsure
|
|
22
|
+
* - Place it between blocks in a column Stack; Stack's gap supplies the space role.
|
|
23
|
+
* - Rule is never a semantic separator. A thematic break that must be conveyed to
|
|
24
|
+
* assistive technology requires semantic markup instead of this component.
|
|
25
|
+
*
|
|
26
|
+
* @UXGuidelines
|
|
27
|
+
* - Use Collection for a semantic list with rules between items; Rule creates no list.
|
|
28
|
+
*/
|
|
29
|
+
export const Rule: FunctionComponent<IRuleProps> = ({ testId }) => (
|
|
30
|
+
<div aria-hidden={'true'} className={rule()} data-testid={testId} />
|
|
31
|
+
);
|