@juwel-development/design-system 3.9.1 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +9 -298
  2. package/dist/design-system.js +1026 -582
  3. package/dist/index.css +1 -1
  4. package/dist/types/Arrangement/ColumnLayout/ColumnLayout.d.ts +92 -0
  5. package/dist/types/Arrangement/ColumnLayout/ColumnLayoutCompositionError.d.ts +3 -0
  6. package/dist/types/Arrangement/ColumnLayout/ColumnLayoutConfigurationError.d.ts +3 -0
  7. package/dist/types/Arrangement/FieldRow/FieldRow.d.ts +87 -0
  8. package/dist/types/Arrangement/FieldRow/FieldRowCompositionError.d.ts +3 -0
  9. package/dist/types/Arrangement/FieldRow/FieldRowConfigurationError.d.ts +3 -0
  10. package/dist/types/Arrangement/FieldRow/IControlEdge.d.ts +6 -0
  11. package/dist/types/Arrangement/FieldRow/alignControlEdges.d.ts +7 -0
  12. package/dist/types/Display/Box/Box.d.ts +41 -0
  13. package/dist/types/Display/Brandmark/Brandmark.d.ts +1 -1
  14. package/dist/types/Display/DefinitionList/DefinitionList.d.ts +59 -9
  15. package/dist/types/Display/DefinitionList/DefinitionListConfigurationError.d.ts +3 -0
  16. package/dist/types/Display/Figure/Figure.d.ts +2 -2
  17. package/dist/types/Display/Icon/Icon.d.ts +26 -0
  18. package/dist/types/Display/Table/Table.d.ts +118 -8
  19. package/dist/types/Display/Table/TableConfigurationError.d.ts +3 -0
  20. package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +1 -1
  21. package/dist/types/Display/Typography/H1/H1.d.ts +3 -2
  22. package/dist/types/Display/Typography/H2/H2.d.ts +3 -2
  23. package/dist/types/Display/Typography/H3/H3.d.ts +3 -2
  24. package/dist/types/Display/Typography/H4/H4.d.ts +3 -2
  25. package/dist/types/Display/Typography/H5/H5.d.ts +3 -2
  26. package/dist/types/Display/Typography/H6/H6.d.ts +3 -2
  27. package/dist/types/Display/Typography/Note/Note.d.ts +1 -1
  28. package/dist/types/Display/Typography/P/P.d.ts +3 -2
  29. package/dist/types/Display/Typography/Prose/Prose.d.ts +1 -1
  30. package/dist/types/Interaction/Button/Button.d.ts +23 -3
  31. package/dist/types/Interaction/Tabs/Tabs.d.ts +31 -6
  32. package/dist/types/Layout/Header/Header.d.ts +67 -8
  33. package/dist/types/Layout/ScrollContainer/ScrollContainer.d.ts +37 -0
  34. package/dist/types/Layout/Section/Section.d.ts +1 -1
  35. package/dist/types/Layout/Sidebar/Sidebar.d.ts +4 -0
  36. package/dist/types/Theme/Palette.d.ts +31 -7
  37. package/dist/types/index.d.ts +6 -0
  38. package/package.json +1 -1
  39. package/src/Arrangement/ColumnLayout/ColumnLayout.tsx +249 -0
  40. package/src/Arrangement/ColumnLayout/ColumnLayoutCompositionError.ts +8 -0
  41. package/src/Arrangement/ColumnLayout/ColumnLayoutConfigurationError.ts +6 -0
  42. package/src/Arrangement/FieldRow/FieldRow.tsx +313 -0
  43. package/src/Arrangement/FieldRow/FieldRowCompositionError.ts +6 -0
  44. package/src/Arrangement/FieldRow/FieldRowConfigurationError.ts +6 -0
  45. package/src/Arrangement/FieldRow/IControlEdge.ts +6 -0
  46. package/src/Arrangement/FieldRow/alignControlEdges.ts +35 -0
  47. package/src/Display/Box/Box.tsx +77 -0
  48. package/src/Display/Checklist/Checklist.tsx +1 -1
  49. package/src/Display/DefinitionList/DefinitionList.tsx +164 -22
  50. package/src/Display/DefinitionList/DefinitionListConfigurationError.ts +6 -0
  51. package/src/Display/Icon/Icon.tsx +63 -0
  52. package/src/Display/Table/Table.tsx +434 -44
  53. package/src/Display/Table/TableConfigurationError.ts +6 -0
  54. package/src/Display/Typography/H1/H1.tsx +3 -2
  55. package/src/Display/Typography/H2/H2.tsx +3 -2
  56. package/src/Display/Typography/H3/H3.tsx +3 -2
  57. package/src/Display/Typography/H4/H4.tsx +3 -2
  58. package/src/Display/Typography/H5/H5.tsx +3 -2
  59. package/src/Display/Typography/H6/H6.tsx +3 -2
  60. package/src/Display/Typography/P/P.tsx +3 -2
  61. package/src/Display/Typography/Prose/Prose.tsx +3 -3
  62. package/src/Interaction/Button/Button.tsx +45 -17
  63. package/src/Interaction/Input/Input.tsx +1 -1
  64. package/src/Interaction/MultiSelect/MultiSelect.tsx +1 -1
  65. package/src/Interaction/NumberInput/NumberInput.tsx +1 -1
  66. package/src/Interaction/Select/Select.tsx +1 -1
  67. package/src/Interaction/Tabs/Tabs.tsx +38 -11
  68. package/src/Interaction/TextArea/TextArea.tsx +1 -1
  69. package/src/Layout/Dialog/Dialog.tsx +4 -3
  70. package/src/Layout/Header/Header.tsx +139 -39
  71. package/src/Layout/PageHead/PageHead.tsx +6 -5
  72. package/src/Layout/ScrollContainer/ScrollContainer.tsx +149 -0
  73. package/src/Layout/Sidebar/Sidebar.tsx +8 -4
  74. package/src/Theme/Palette.ts +37 -9
  75. package/src/Theme/renderTokens.ts +101 -5
  76. package/src/index.ts +6 -0
  77. package/src/tokens.css +68 -4
  78. package/src/tokens.dark.css +66 -4
  79. package/src/tokens.light.css +64 -2
@@ -1,50 +1,161 @@
1
+ import type { VariantProps } from 'class-variance-authority';
1
2
  import { cva } from 'class-variance-authority';
2
- import type { FunctionComponent, ReactNode } from 'react';
3
+ import type { CSSProperties, FunctionComponent, ReactNode } from 'react';
4
+ import { DefinitionListConfigurationError } from './DefinitionListConfigurationError';
3
5
 
4
- // Rules are the layout. Each Item owns its two-track grid and its hairlines. The term track is a fixed
5
- // width, so per-item grids line up without a subgrid. At `lg` (64rem) a second `dt` is pinned to the
6
- // term column so it cannot auto-place into the description column and break the row silently. Colours
7
- // are semantic tokens re-pointed by `.dark`, so no class carries a `dark:` prefix.
6
+ // The dl is the inline-size container its items measure, and the one place density lands: it pads
7
+ // its items from the role of its treatment, so Item, Term and Description carry no density of
8
+ // their own and nothing is passed down (docs/agents/standards/design-system-components.md,
9
+ // "Compound components": a Root styles the descendants it owns through its one recipe).
10
+ const definitionList = cva('@container', {
11
+ variants: {
12
+ density: {
13
+ comfortable: '[&>div]:py-[var(--space-definition-item)]',
14
+ compact: '[&>div]:py-[var(--space-definition-item-compact)]',
15
+ },
16
+ },
17
+ defaultVariants: { density: 'comfortable' },
18
+ });
19
+
20
+ // Each item shares the list's tracks. Below the fit threshold the term track is full width.
21
+ // The registered shortfall resolves on the item and inherits as a length into its description.
22
+ // tan(atan2(length, 1px)) converts the 0px/1px switch to a number for grid-column-start (2/1).
23
+ // Dense placement fills column two beside the first term, or the next full row when stacked;
24
+ // it preserves DOM reading order and supports several terms without counting them in JavaScript.
8
25
  const item = cva(
9
26
  [
10
- 'grid gap-[var(--space-stack)] py-6',
11
- 'border-b border-solid border-border first:border-t',
12
- 'lg:grid-cols-[16rem_minmax(0,1fr)] lg:items-baseline lg:gap-x-12',
13
- 'lg:[&>dt]:col-start-1 lg:[&>dd]:col-start-2 lg:[&>dd]:row-start-1',
27
+ 'grid grid-flow-row-dense gap-[var(--space-stack)] border-b border-solid border-border first:border-t',
28
+ '[--definition-stack:clamp(0px,(var(--definition-threshold)-100%)*1000000,100%)]',
29
+ '[--definition-shortfall:clamp(0px,(var(--definition-threshold)-100cqi)*1000000,1px)]',
30
+ 'grid-cols-[max(calc((100%-var(--space-definition-column))*var(--definition-term-share)),var(--definition-stack))_minmax(0,1fr)]',
31
+ 'gap-x-[max(0px,var(--space-definition-column)-var(--definition-stack))]',
32
+ '[&>dt]:col-start-1 [&>dt]:self-baseline [&>dd]:col-span-full [&>dd]:self-baseline',
33
+ '[&>dd]:col-start-[calc(2-tan(atan2(var(--definition-shortfall),1px)))]',
14
34
  ].join(' '),
15
35
  );
16
36
 
17
37
  // The term at the subtitle role - a step below title (docs/adr/0005 fixes the role, no size prop), so
18
- // terms never tie with the heading introducing the list. Foreground, and no measure cap.
19
- const term = cva('font-primary text-subtitle leading-subtitle text-foreground');
38
+ // terms never tie with the heading introducing the list. Foreground, and no measure cap. The term
39
+ // is sized like a heading and is not one: with its description it is the reading matter, so both
40
+ // take the body face, never the heading face (docs/adr/0004, #120).
41
+ const term = cva(
42
+ 'font-body text-subtitle leading-subtitle text-foreground wrap-break-word',
43
+ );
20
44
 
21
45
  // The description at the body role, muted, capped at the reading measure in both layout modes.
22
46
  const description = cva(
23
- 'font-primary text-body leading-body text-muted max-w-[var(--measure)]',
47
+ 'font-body text-body leading-body text-muted max-w-[var(--measure)] wrap-break-word',
24
48
  );
25
49
 
26
- interface IDefinitionListRootProps {
50
+ /** The name of a CSS custom property, as written in a stylesheet: `--summary-term-min-width`. */
51
+ type MinWidthToken = `--${string}`;
52
+
53
+ /** One column's allocation: its share of the row relative to the other's, and the theme token that
54
+ * holds its minimum readable width. Named only through `Root`'s props. */
55
+ type ColumnAllocation = { weight: number; minWidth: MinWidthToken };
56
+
57
+ const TERM_COLUMN: ColumnAllocation = {
58
+ weight: 1,
59
+ minWidth: '--definition-term-min-width',
60
+ };
61
+ const DESCRIPTION_COLUMN: ColumnAllocation = {
62
+ weight: 2,
63
+ minWidth: '--definition-description-min-width',
64
+ };
65
+
66
+ // The ident grammar CSS gives a custom property name, restricted to ASCII so a `var()`, a length,
67
+ // a space or a brace can never ride in on the name.
68
+ const TOKEN_NAME = /^--[A-Za-z0-9_-]+$/;
69
+
70
+ const validateColumn = (
71
+ name: 'termColumn' | 'descriptionColumn',
72
+ column: ColumnAllocation,
73
+ ): ColumnAllocation => {
74
+ if (!Number.isFinite(column.weight) || column.weight <= 0) {
75
+ throw new DefinitionListConfigurationError(
76
+ `${name} needs a positive finite weight (got ${column.weight})`,
77
+ );
78
+ }
79
+ if (!TOKEN_NAME.test(column.minWidth)) {
80
+ throw new DefinitionListConfigurationError(
81
+ `${name} needs a custom-property name such as --summary-term-min-width for minWidth (got ${JSON.stringify(column.minWidth)})`,
82
+ );
83
+ }
84
+ return column;
85
+ };
86
+
87
+ // The documented threshold, `g + max(m_i * S / w_i)` with the column gap as g, left to the browser
88
+ // to resolve so a theme re-pointing a minimum - or the gap - moves it with no script in between.
89
+ const thresholdOf = (
90
+ termColumn: ColumnAllocation,
91
+ descriptionColumn: ColumnAllocation,
92
+ ): string => {
93
+ const total = termColumn.weight + descriptionColumn.weight;
94
+ return `calc(var(--space-definition-column) + max(var(${termColumn.minWidth}) * ${total / termColumn.weight}, var(${descriptionColumn.minWidth}) * ${total / descriptionColumn.weight}))`;
95
+ };
96
+
97
+ // React's CSSProperties is closed over known properties; the two custom properties the item
98
+ // recipe reads are declared here so the style object stays typed without an assertion.
99
+ type DefinitionListRootStyle = CSSProperties & {
100
+ '--definition-threshold': string;
101
+ '--definition-term-share': string;
102
+ };
103
+
104
+ export interface IDefinitionListRootProps
105
+ extends VariantProps<typeof definitionList> {
106
+ /** The term column's share of the row and its minimum readable width, shared by every item.
107
+ * Defaults to weight `1` and the library's `--definition-term-min-width`; a consumer's own
108
+ * token is declared in the theme with a nonnegative CSS length, as ColumnLayout's are. */
109
+ termColumn?: ColumnAllocation;
110
+ /** The description column's share and minimum, likewise. Defaults to weight `2` and
111
+ * `--definition-description-min-width`. */
112
+ descriptionColumn?: ColumnAllocation;
27
113
  children?: ReactNode;
28
114
  testId?: string;
29
115
  }
30
116
 
31
- interface IDefinitionListItemProps {
117
+ export interface IDefinitionListItemProps {
32
118
  children?: ReactNode;
33
119
  testId?: string;
34
120
  }
35
121
 
36
- interface IDefinitionListTermProps {
122
+ export interface IDefinitionListTermProps {
37
123
  children?: ReactNode;
38
124
  }
39
125
 
40
- interface IDefinitionListDescriptionProps {
126
+ export interface IDefinitionListDescriptionProps {
41
127
  children?: ReactNode;
42
128
  }
43
129
 
44
130
  const DefinitionListRoot: FunctionComponent<IDefinitionListRootProps> = ({
131
+ density,
132
+ termColumn = TERM_COLUMN,
133
+ descriptionColumn = DESCRIPTION_COLUMN,
45
134
  children,
46
135
  testId,
47
- }) => <dl data-testid={testId}>{children}</dl>;
136
+ }) => {
137
+ const termAllocation = validateColumn('termColumn', termColumn);
138
+ const descriptionAllocation = validateColumn(
139
+ 'descriptionColumn',
140
+ descriptionColumn,
141
+ );
142
+ const style: DefinitionListRootStyle = {
143
+ '--definition-threshold': thresholdOf(
144
+ termAllocation,
145
+ descriptionAllocation,
146
+ ),
147
+ '--definition-term-share': `calc(${termAllocation.weight} / ${termAllocation.weight + descriptionAllocation.weight})`,
148
+ };
149
+ return (
150
+ <dl
151
+ className={definitionList({ density })}
152
+ style={style}
153
+ data-testid={testId}
154
+ >
155
+ {children}
156
+ </dl>
157
+ );
158
+ };
48
159
 
49
160
  const DefinitionListItem: FunctionComponent<IDefinitionListItemProps> = ({
50
161
  children,
@@ -70,17 +181,48 @@ const DefinitionListDescription: FunctionComponent<
70
181
  * `Description` a `<dd>`.
71
182
  *
72
183
  * @Guarantees — enforced on every render
73
- * - Renders semantic `dl`/`div`/`dt`/`dd`, and works with JavaScript off.
184
+ * - Renders semantic `dl`/`div`/`dt`/`dd` at either density, and works with JavaScript off: the
185
+ * arrangement is a stylesheet rule, so nothing is measured, reordered or remounted, and resizing
186
+ * keeps every descendant's state, focus and keyboard order.
74
187
  * - The term is fixed to the subtitle type role and exposes no size prop or variant (docs/adr/0005).
75
- * - The description is body, muted, and capped at `--measure`; the term carries no measure cap.
188
+ * The description is body, muted, and capped at `--measure`; the term carries no measure cap.
189
+ * Density changes neither: `compact` pads each item from `--space-definition-item-compact` where
190
+ * `comfortable` (the default) pads from `--space-definition-item`, and nothing else moves.
76
191
  * - A hairline sits above the first item and below every item, in `border`, and the block closes at
77
192
  * the foot. No card, box, fill, icon or bullet - it reads from the rules alone.
78
- * - Single column below 64rem with the term above its description; two columns at and above it with a
79
- * fixed term track and baseline-aligned rows. The switch is a media query, not a prop.
193
+ * - Every item shares one allocation: the term column and the description column each take a share
194
+ * of the width left after the column gap in proportion to their weights, `1` and `2` unless the
195
+ * caller says otherwise. The row holds while both shares are at least their minimum readable
196
+ * width; the moment one falls short, every item in the list puts its terms above its description
197
+ * at the full width, separated by the stack role. There is no in-between, and the minimum decides
198
+ * the switch rather than flooring a width. For column gap g, total weight S and minimums m_i the
199
+ * row fits at and above `g + max(m_i * S / w_i)`.
200
+ * - The width measured is the list's own, never the viewport, so a narrow list on a wide screen
201
+ * stacks while a wide one beside it keeps its columns, and a theme that re-points a minimum -
202
+ * at any scope - moves the threshold with it.
203
+ * - A long phrase or an unbroken value wraps inside its column; no content is truncated, nothing
204
+ * overlaps, and the list never widens past its holder.
205
+ * - A non-positive or non-finite `weight`, or a `minWidth` that is not a custom-property name,
206
+ * throws {@link DefinitionListConfigurationError}: a programmer error, raised loud and early.
80
207
  *
81
- * @UXGuidelines
208
+ * @CallerMustEnsure — the component cannot see these and does not check them
82
209
  * - Several `Term`s may share one `Description`; keep them inside one `Item` so the term column
83
210
  * stays intact.
211
+ * - A `minWidth` token the caller names is declared, on the list or an ancestor of it, as a valid
212
+ * nonnegative CSS length. The library's own two defaults are declared in every token stylesheet.
213
+ * A missing or invalid token is not a responsive configuration: the threshold has nothing to
214
+ * compare and the list stays stacked at every width.
215
+ * - The holder gives the list a definite width, as any block, grid track or Dialog content region
216
+ * does. Inside a shrink-to-fit frame an inline-size container contributes no width of its own.
217
+ *
218
+ * @UXGuidelines
219
+ * - Choose `compact` for a fact list - short values beside their labels in a panel or a content
220
+ * Dialog - and `comfortable` for a glossary. The choice is what the list holds, never how much
221
+ * air a page wants.
222
+ * - A minimum is the width below which a column's content stops being readable - the narrowest
223
+ * the labels keep their lines - not the width the designer would like; the stack is the readable
224
+ * fallback. Weights are structural: `1` and `3` say the value is the subject and the term its
225
+ * label, and a ratio chosen to hit a pixel width is a measurement, which the theme owns.
84
226
  */
85
227
  export const DefinitionList = {
86
228
  Root: DefinitionListRoot,
@@ -0,0 +1,6 @@
1
+ export class DefinitionListConfigurationError extends Error {
2
+ constructor(reason: string) {
3
+ super(`DefinitionList configuration is invalid: ${reason}`);
4
+ this.name = 'DefinitionListConfigurationError';
5
+ }
6
+ }
@@ -0,0 +1,63 @@
1
+ import { cva } from 'class-variance-authority';
2
+ import type { FunctionComponent } from 'react';
3
+
4
+ // The glyph is sized in `em` and stroked in `currentColor`, so it takes the size and colour of the
5
+ // text it sits in and needs no colour or size prop. Baseline at -0.125em is what seats a 1em square
6
+ // beside lowercase letters; the inline-box exists so an icon reads as a word in its line.
7
+ const icon = cva('inline-block shrink-0 align-[-0.125em]');
8
+
9
+ // Each name selects a drawing and nothing else: the three sort indicators are told apart by their
10
+ // arrows, not by colour or size, so they survive forced-colors mode and a monochrome print.
11
+ const drawings = {
12
+ sort: [
13
+ 'M5.5 13V3',
14
+ 'M3 5.5l2.5-2.5L8 5.5',
15
+ 'M10.5 3v10',
16
+ 'M8 10.5l2.5 2.5 2.5-2.5',
17
+ ],
18
+ 'sort-ascending': ['M8 13V3', 'M4 7l4-4 4 4'],
19
+ 'sort-descending': ['M8 3v10', 'M4 9l4 4 4-4'],
20
+ } as const;
21
+
22
+ export interface IIconProps {
23
+ /** Which drawing. A name selects a shape, never a state or a behaviour. */
24
+ name: keyof typeof drawings;
25
+ testId?: string;
26
+ }
27
+
28
+ /**
29
+ * A reusable visual glyph: the three sort indicators a consumer composes inside a header cell's
30
+ * plain Button, or beside any text that names what the icon reinforces.
31
+ *
32
+ * @Guarantees — enforced on every render
33
+ * - Hidden from assistive technology and out of the tab order: it adds no stop and no spoken name.
34
+ * - Scales with the surrounding font-size and takes the surrounding text colour.
35
+ * - The three drawings differ in shape, so the ordering is never carried by colour alone.
36
+ *
37
+ * @CallerMustEnsure
38
+ * - The control or text beside it carries the meaning: a `Button` label, a header cell's `ariaSort`.
39
+ * An icon standing alone says nothing to a screen reader.
40
+ */
41
+ export const Icon: FunctionComponent<IIconProps> = ({ name, testId }) => (
42
+ <svg
43
+ aria-hidden={true}
44
+ focusable={false}
45
+ viewBox={'0 0 16 16'}
46
+ width={'1em'}
47
+ height={'1em'}
48
+ className={icon()}
49
+ data-testid={testId}
50
+ >
51
+ {drawings[name].map((segment) => (
52
+ <path
53
+ key={segment}
54
+ d={segment}
55
+ fill={'none'}
56
+ stroke={'currentColor'}
57
+ strokeWidth={1.5}
58
+ strokeLinecap={'round'}
59
+ strokeLinejoin={'round'}
60
+ />
61
+ ))}
62
+ </svg>
63
+ );