@juwel-development/design-system 3.9.0 → 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.
- package/README.md +9 -298
- package/dist/design-system.js +1043 -589
- package/dist/index.css +1 -1
- package/dist/types/Arrangement/ColumnLayout/ColumnLayout.d.ts +92 -0
- package/dist/types/Arrangement/ColumnLayout/ColumnLayoutCompositionError.d.ts +3 -0
- package/dist/types/Arrangement/ColumnLayout/ColumnLayoutConfigurationError.d.ts +3 -0
- package/dist/types/Arrangement/FieldRow/FieldRow.d.ts +87 -0
- package/dist/types/Arrangement/FieldRow/FieldRowCompositionError.d.ts +3 -0
- package/dist/types/Arrangement/FieldRow/FieldRowConfigurationError.d.ts +3 -0
- package/dist/types/Arrangement/FieldRow/IControlEdge.d.ts +6 -0
- package/dist/types/Arrangement/FieldRow/alignControlEdges.d.ts +7 -0
- package/dist/types/Display/Box/Box.d.ts +41 -0
- package/dist/types/Display/Brandmark/Brandmark.d.ts +1 -1
- package/dist/types/Display/DefinitionList/DefinitionList.d.ts +59 -9
- package/dist/types/Display/DefinitionList/DefinitionListConfigurationError.d.ts +3 -0
- package/dist/types/Display/Figure/Figure.d.ts +2 -2
- package/dist/types/Display/Icon/Icon.d.ts +26 -0
- package/dist/types/Display/Table/Table.d.ts +118 -8
- package/dist/types/Display/Table/TableConfigurationError.d.ts +3 -0
- package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +1 -1
- package/dist/types/Display/Typography/H1/H1.d.ts +3 -2
- package/dist/types/Display/Typography/H2/H2.d.ts +3 -2
- package/dist/types/Display/Typography/H3/H3.d.ts +3 -2
- package/dist/types/Display/Typography/H4/H4.d.ts +3 -2
- package/dist/types/Display/Typography/H5/H5.d.ts +3 -2
- package/dist/types/Display/Typography/H6/H6.d.ts +3 -2
- package/dist/types/Display/Typography/Note/Note.d.ts +1 -1
- package/dist/types/Display/Typography/P/P.d.ts +3 -2
- package/dist/types/Display/Typography/Prose/Prose.d.ts +1 -1
- package/dist/types/Interaction/Button/Button.d.ts +23 -3
- package/dist/types/Interaction/Slider/Slider.d.ts +9 -4
- package/dist/types/Interaction/Tabs/Tabs.d.ts +31 -6
- package/dist/types/Layout/Header/Header.d.ts +67 -8
- package/dist/types/Layout/ScrollContainer/ScrollContainer.d.ts +37 -0
- package/dist/types/Layout/Section/Section.d.ts +1 -1
- package/dist/types/Layout/Sidebar/Sidebar.d.ts +4 -0
- package/dist/types/Theme/Palette.d.ts +31 -7
- package/dist/types/index.d.ts +6 -0
- package/package.json +1 -1
- package/src/Arrangement/ColumnLayout/ColumnLayout.tsx +249 -0
- package/src/Arrangement/ColumnLayout/ColumnLayoutCompositionError.ts +8 -0
- package/src/Arrangement/ColumnLayout/ColumnLayoutConfigurationError.ts +6 -0
- package/src/Arrangement/FieldRow/FieldRow.tsx +313 -0
- package/src/Arrangement/FieldRow/FieldRowCompositionError.ts +6 -0
- package/src/Arrangement/FieldRow/FieldRowConfigurationError.ts +6 -0
- package/src/Arrangement/FieldRow/IControlEdge.ts +6 -0
- package/src/Arrangement/FieldRow/alignControlEdges.ts +35 -0
- package/src/Display/Box/Box.tsx +77 -0
- package/src/Display/Checklist/Checklist.tsx +1 -1
- package/src/Display/DefinitionList/DefinitionList.tsx +164 -22
- package/src/Display/DefinitionList/DefinitionListConfigurationError.ts +6 -0
- package/src/Display/Icon/Icon.tsx +63 -0
- package/src/Display/Table/Table.tsx +434 -44
- package/src/Display/Table/TableConfigurationError.ts +6 -0
- package/src/Display/Typography/H1/H1.tsx +3 -2
- package/src/Display/Typography/H2/H2.tsx +3 -2
- package/src/Display/Typography/H3/H3.tsx +3 -2
- package/src/Display/Typography/H4/H4.tsx +3 -2
- package/src/Display/Typography/H5/H5.tsx +3 -2
- package/src/Display/Typography/H6/H6.tsx +3 -2
- package/src/Display/Typography/P/P.tsx +3 -2
- package/src/Display/Typography/Prose/Prose.tsx +3 -3
- package/src/Interaction/Button/Button.tsx +45 -17
- package/src/Interaction/Input/Input.tsx +1 -1
- package/src/Interaction/MultiSelect/MultiSelect.tsx +1 -1
- package/src/Interaction/NumberInput/NumberInput.tsx +1 -1
- package/src/Interaction/Select/Select.tsx +1 -1
- package/src/Interaction/Slider/Slider.tsx +34 -18
- package/src/Interaction/Tabs/Tabs.tsx +38 -11
- package/src/Interaction/TextArea/TextArea.tsx +1 -1
- package/src/Layout/Dialog/Dialog.tsx +4 -3
- package/src/Layout/Header/Header.tsx +139 -39
- package/src/Layout/PageHead/PageHead.tsx +6 -5
- package/src/Layout/ScrollContainer/ScrollContainer.tsx +149 -0
- package/src/Layout/Sidebar/Sidebar.tsx +8 -4
- package/src/Theme/Palette.ts +37 -9
- package/src/Theme/renderTokens.ts +101 -5
- package/src/index.ts +6 -0
- package/src/tokens.css +68 -4
- package/src/tokens.dark.css +66 -4
- package/src/tokens.light.css +64 -2
|
@@ -2,8 +2,14 @@ import type { VariantProps } from 'class-variance-authority';
|
|
|
2
2
|
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
3
|
declare const header: (props?: ({
|
|
4
4
|
edge?: "none" | "rule" | null | undefined;
|
|
5
|
+
mode?: "navigation" | "statusAction" | null | undefined;
|
|
5
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
-
|
|
7
|
+
interface IHeaderShellProps extends Omit<VariantProps<typeof header>, 'mode'> {
|
|
8
|
+
testId?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The navigation bar: the mode every existing caller is in, and the default. A standing link and a
|
|
11
|
+
* `<nav>`, and never a status or an action slot - the other shape is `IHeaderStatusActionProps`. */
|
|
12
|
+
export interface IHeaderNavigationProps extends IHeaderShellProps {
|
|
7
13
|
/** The standing link: a place name, a mark, or a home link. The consumer supplies the whole anchor -
|
|
8
14
|
* Header renders no link of its own. A place name uses `<Link treatment="quiet" href="/">…</Link>`; a
|
|
9
15
|
* mark uses `<Link treatment="graphic" href="/"><Brandmark …/></Link>`, whose `graphic` treatment
|
|
@@ -13,15 +19,45 @@ export interface IHeaderProps extends VariantProps<typeof header> {
|
|
|
13
19
|
navName?: string;
|
|
14
20
|
/** The nav links. */
|
|
15
21
|
children?: ReactNode;
|
|
16
|
-
|
|
22
|
+
status?: never;
|
|
23
|
+
action?: never;
|
|
17
24
|
}
|
|
25
|
+
/** The status/action bar: a bar that reports and acts rather than navigates, for a product whose
|
|
26
|
+
* shell carries a readout and a control and no links. It has no standing link and no `<nav>` - a
|
|
27
|
+
* button is not navigation - and the two shapes cannot be mixed: the compiler rejects a call that
|
|
28
|
+
* hands this bar a standing link, a nav name or nav children. */
|
|
29
|
+
export interface IHeaderStatusActionProps extends IHeaderShellProps {
|
|
30
|
+
/** The readout - a date, a balance, a short note, or a `Cluster` of them - at the start edge, first
|
|
31
|
+
* in reading order. A plain `div` with no role, no name and no live region: what it holds carries its
|
|
32
|
+
* own semantics, and whether a change is announced is the consumer's decision, made by wrapping its
|
|
33
|
+
* own live region. Long and unbroken wording wraps inside the slot. Omitted, nothing renders. */
|
|
34
|
+
status?: ReactNode;
|
|
35
|
+
/** The control - a `Button`, or a `Cluster` of them - at the end edge, last in reading and keyboard
|
|
36
|
+
* order, on whichever line it lands when the bar breaks. A plain `div` like `status`. Header never
|
|
37
|
+
* decides whether the control is available: pass it disabled, or withhold it (`{ready && <Button/>}`
|
|
38
|
+
* renders no box). Omitted, nothing renders. */
|
|
39
|
+
action?: ReactNode;
|
|
40
|
+
standing?: never;
|
|
41
|
+
navName?: never;
|
|
42
|
+
children?: never;
|
|
43
|
+
}
|
|
44
|
+
export type IHeaderProps = IHeaderNavigationProps | IHeaderStatusActionProps;
|
|
18
45
|
/**
|
|
19
|
-
* The shell's top edge
|
|
20
|
-
*
|
|
46
|
+
* The shell's top edge, at the label type role, in one of two mutually exclusive modes. The
|
|
47
|
+
* **navigation bar** - the default, and what every existing caller renders - is a standing link and a
|
|
48
|
+
* single `<nav>`. The **status/action bar** is a readout at the start edge and a control at the end
|
|
49
|
+
* edge, both plain boxes with no landmark of their own and no `<nav>` anywhere. The props type is a
|
|
50
|
+
* union of the two shapes, so a call that mixes them does not compile. Either bar arranges nothing
|
|
51
|
+
* beyond its slots and works with no hydration.
|
|
21
52
|
*
|
|
22
53
|
* @Guarantees — enforced on every render
|
|
23
54
|
* - The header is set at the label role and carries no size of its own: `font-secondary text-label
|
|
24
55
|
* leading-label tracking-label text-muted`, so it never grows past that role whatever the page font size.
|
|
56
|
+
* - The shell's air is one value above, below and between: `--space-region` vertically, between the
|
|
57
|
+
* nav's items and between the status/action bar's slots along a line, `--gutter` across, so the bar
|
|
58
|
+
* aligns with every inset `Section`. It is never sticky and needs no JavaScript.
|
|
59
|
+
*
|
|
60
|
+
* The navigation bar, unchanged by #125:
|
|
25
61
|
* - The nav's line box is the library's own rather than the consuming document's: the header leads
|
|
26
62
|
* itself at the label role, so the height it declares for its slot is the height it renders.
|
|
27
63
|
* - The standing slot is floored, never fixed: its minimum height is that same line box - the label
|
|
@@ -31,10 +67,24 @@ export interface IHeaderProps extends VariantProps<typeof header> {
|
|
|
31
67
|
* grows the slot, on one line, and is never clamped or wrapped.
|
|
32
68
|
* - A nav item marked `aria-current="page"` renders at `foreground` whatever supplied it - the treatment
|
|
33
69
|
* keys on the attribute, not on `Link`, so it holds against a bare anchor or any component.
|
|
34
|
-
* - The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
70
|
+
* - The standing slot and the `<nav>` are the bar's two children whatever the caller passes, the nav
|
|
71
|
+
* flush with the end content edge and absorbing narrowing by wrapping its links. Omitting `navName`
|
|
72
|
+
* emits no `aria-label` at all, not an empty one.
|
|
73
|
+
*
|
|
74
|
+
* The status/action bar:
|
|
75
|
+
* - It renders no `<nav>`, no standing slot, no role, no name and no live region of its own. Each
|
|
76
|
+
* filled slot is one `div` directly inside the banner; an omitted or withheld slot (`false`, `null`)
|
|
77
|
+
* renders no box and reserves no space, so a status-only bar starts with its readout and an action-only
|
|
78
|
+
* bar is the control alone at the end edge.
|
|
79
|
+
* - Reading and keyboard order is status, then action, and the DOM order is the same at every width.
|
|
80
|
+
* - With both fitting, the readout sits at the start content edge and the control at the end content
|
|
81
|
+
* edge on one line. When they cannot share a line, the control drops below the readout and stays
|
|
82
|
+
* flush with the end edge; a readout then takes the whole line and wraps inside it, unbroken wording
|
|
83
|
+
* included, so the bar never widens the page and never clips.
|
|
84
|
+
* - Breaking is CSS alone: a width change re-flows the same elements, so descendant state and focus
|
|
85
|
+
* survive it.
|
|
86
|
+
* - What the consumer passes keeps its semantics: a heading stays a heading, a live region stays one, a
|
|
87
|
+
* disabled control stays disabled. Header adds, removes and announces nothing.
|
|
38
88
|
*
|
|
39
89
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
40
90
|
* - A mark that should *fill* the standing slot is given a **definite width** by whoever placed it -
|
|
@@ -47,6 +97,10 @@ export interface IHeaderProps extends VariantProps<typeof header> {
|
|
|
47
97
|
* width; `flex: 1 1 0; min-width: 0` only collapses the mark when the bar is already out of room, so
|
|
48
98
|
* it is not the rule to reach for. The library cannot apply either rule for you: the same width on a
|
|
49
99
|
* place name would clamp the name and wrap it.
|
|
100
|
+
* - Set the type of what fills a status/action slot: the bar sets the label role, so a bare string in
|
|
101
|
+
* `status` reads as a label, while a `P` or a `Note` wears its own role and a `Button` its own.
|
|
102
|
+
* - Whether a changing readout interrupts is yours to decide: wrap your own live region inside `status`
|
|
103
|
+
* where a change must be announced, and leave it out where it must not. Header takes neither side.
|
|
50
104
|
*
|
|
51
105
|
* @UXGuidelines
|
|
52
106
|
* - Name the nav with `navName` once the page has more than one navigation landmark - a footer nav will
|
|
@@ -55,6 +109,11 @@ export interface IHeaderProps extends VariantProps<typeof header> {
|
|
|
55
109
|
* `aria-current`, so set `current` on the `Link` and style nothing yourself.
|
|
56
110
|
* - The nav does not collapse into a menu: a disclosure needs JavaScript, so on a narrow viewport the
|
|
57
111
|
* items wrap. A mobile menu is out of scope, not a follow-up.
|
|
112
|
+
* - A bar that reports and acts is a status/action bar, never a navigation bar with a button among its
|
|
113
|
+
* links: a Balance line or a Continue button inside a `<nav>` is announced as navigation. A product
|
|
114
|
+
* whose shell needs links as well as a control has two bars, not one.
|
|
115
|
+
* - Several readouts or several actions go in a `Cluster` inside the slot: `gap="stack"` keeps them one
|
|
116
|
+
* group, and the Cluster wraps inside the slot before the bar runs out of room.
|
|
58
117
|
*/
|
|
59
118
|
export declare const Header: FunctionComponent<IHeaderProps>;
|
|
60
119
|
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { type FunctionComponent, type ReactNode } from 'react';
|
|
3
|
+
declare const scrollContainer: (props?: ({
|
|
4
|
+
axis?: "both" | "horizontal" | "vertical" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export interface IScrollContainerProps extends VariantProps<typeof scrollContainer> {
|
|
7
|
+
/** The group's accessible name while it can be scrolled. Required: a tab stop with no name is a
|
|
8
|
+
* mystery to a screen reader. The consuming app words it, usually after the heading above. */
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
testId?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Makes overflowing content reachable along the chosen axes, within the space its parent allocates.
|
|
15
|
+
* It owns the scrolling; the consumer owns the content and the allocation of space.
|
|
16
|
+
*
|
|
17
|
+
* @Guarantees — enforced on every render
|
|
18
|
+
* - Scrolls only an enabled axis and only once content overflows it; overflow on a disabled axis is
|
|
19
|
+
* clipped. Content wraps as it would anywhere else.
|
|
20
|
+
* - Keyboard-reachable - a named `group` with a tab stop - exactly while an enabled axis overflows,
|
|
21
|
+
* static content included; no stop and no group while everything fits. Scrollability is re-read on
|
|
22
|
+
* resize and on content change without moving focus.
|
|
23
|
+
* - Native scrolling: scrollbars, wheel, touch and the browser's own arrow/page keys on the focused
|
|
24
|
+
* container. No key of a control inside it is intercepted and focus is never trapped.
|
|
25
|
+
* - Invents no bound: no height, width or viewport unit of its own. The only space it adds is the
|
|
26
|
+
* focus ring's room around its content, so a focusable child flush with its edge - a Table's
|
|
27
|
+
* scroll region, a button - keeps a visible ring instead of having it clipped at the edge.
|
|
28
|
+
*
|
|
29
|
+
* @CallerMustEnsure
|
|
30
|
+
* - The parent allocates finite space on every axis the container should scroll - a sized box, or a
|
|
31
|
+
* flex/grid item allowed to shrink (`flex: 1 1 0; min-height: 0` in a column). Under an unbounded
|
|
32
|
+
* parent it simply grows with its content, as any block would.
|
|
33
|
+
* - Content fits a disabled axis. Clipping is not a way to hide essential content or controls.
|
|
34
|
+
* - `ariaLabel` is wording the viewer would recognise, typically the heading above the content.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ScrollContainer: FunctionComponent<IScrollContainerProps>;
|
|
37
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { VariantProps } from 'class-variance-authority';
|
|
2
2
|
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
3
|
declare const section: (props?: ({
|
|
4
|
-
bleed?: "
|
|
4
|
+
bleed?: "inset" | "full" | null | undefined;
|
|
5
5
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
6
|
export interface ISectionProps extends VariantProps<typeof section> {
|
|
7
7
|
/** Names the section as a region a screen-reader user can jump to. Omit for an ordinary section: an
|
|
@@ -41,6 +41,10 @@ export interface ISidebarContentProps {
|
|
|
41
41
|
* - Activating the active entry or an inert one emits nothing; no render emits anything. Entries are
|
|
42
42
|
* non-submitting `type="button"` buttons with native Tab/Enter/Space behaviour - no tabs/menu model.
|
|
43
43
|
* - An inert entry stays visible but muted and disabled, so Tab skips it and activation is inert too.
|
|
44
|
+
* - A label is rendered in full inside its entry, in either arrangement: a phrase wraps at its spaces
|
|
45
|
+
* and a word wider than the track breaks within itself. Nothing is truncated, renamed or hidden
|
|
46
|
+
* behind a tooltip, no horizontal scrolling is introduced, and a translated label neither widens
|
|
47
|
+
* the 12rem track nor displaces the content.
|
|
44
48
|
* - At and above 64rem the nav is a fixed 12rem track, sticky at the top of the scrolling area with no
|
|
45
49
|
* assumed top-bar offset, capped to the screen/scrolling-area height with independent entry scrolling.
|
|
46
50
|
* Content sits beside it in `minmax(0,1fr)`, so wide content cannot displace the track.
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Light and dark are two complete sets of the same roles rather than a set of `dark:` overrides
|
|
11
11
|
* scattered through the components. A component therefore carries no dark-mode classes at all:
|
|
12
|
-
* swapping the `.dark` class re-points the variables underneath it.
|
|
12
|
+
* swapping the `.dark` class re-points the variables underneath it. Complete means `Required`: the
|
|
13
|
+
* two roles the type marks optional are optional for a consumer's palette object, never here.
|
|
13
14
|
*
|
|
14
15
|
* The values below are still depot-tracker's brand (primary is its violet). They are carried over
|
|
15
16
|
* so nothing changed visually during the extraction - a starting point to replace, not a decision.
|
|
@@ -113,8 +114,28 @@ export type PaletteTokens = {
|
|
|
113
114
|
* constraint stated on `success`. */
|
|
114
115
|
warning: string;
|
|
115
116
|
/** The error status tone. Carries the general status-tone contract and 4.5:1-against-`surface`
|
|
116
|
-
* constraint stated on `success`, plus the depletion-path constraint stated on `meterFill`.
|
|
117
|
+
* constraint stated on `success`, plus the depletion-path constraint stated on `meterFill`. It is
|
|
118
|
+
* also Button's destructive fill (#119), identified like `primary` by the fill alone: at least 3:1
|
|
119
|
+
* against `surface` in the same theme, `errorHover` included - which the 4.5:1 text floor already
|
|
120
|
+
* clears - and constrained from the other side by the ink it carries, stated on `errorForeground`.
|
|
121
|
+
* See docs/adr/0011-status-tones-are-general-roles.md, Amendments. */
|
|
117
122
|
error: string;
|
|
123
|
+
/** The destructive fill's hover step. Constraint (WCAG 2.2 SC 1.4.11): at least 3:1 against
|
|
124
|
+
* `surface` in the same theme, and at least 4.5:1 against `errorForeground`, since a hovered
|
|
125
|
+
* control has to stay identifiable and readable too. Not a status tone: it carries no text of its
|
|
126
|
+
* own and the status-tone text floor does not apply to it. Optional in the type so a palette
|
|
127
|
+
* object written before #119 keeps compiling: the shipped stylesheet declares `--color-error-hover`,
|
|
128
|
+
* and a theme that omits the role inherits that default - which pairs with the shipped `error`,
|
|
129
|
+
* so a theme that re-points `error` re-points this too. */
|
|
130
|
+
errorHover?: string;
|
|
131
|
+
/** Text and icons drawn on top of `error` and `errorHover`. Constraint (WCAG 2.2 SC 1.4.3): at
|
|
132
|
+
* least 4.5:1 against both in the same theme, hover included. Optional in the type for the same
|
|
133
|
+
* reason as `errorHover`, with the same obligation: the shipped default is the ink for the
|
|
134
|
+
* shipped `error`. The ink inverts with the theme as `primaryForeground` does, and in light it is
|
|
135
|
+
* pure white rather than slate-50 because the shipped `error` sits exactly on the 4.5:1 floor
|
|
136
|
+
* against white (4.501:1) and slate-50 measures 4.30:1 - under it. Not required against
|
|
137
|
+
* `disabled`, which SC 1.4.3 exempts. */
|
|
138
|
+
errorForeground?: string;
|
|
118
139
|
/** The informational status tone. Carries the general status-tone contract and
|
|
119
140
|
* 4.5:1-against-`surface` constraint stated on `success`. */
|
|
120
141
|
info: string;
|
|
@@ -138,7 +159,7 @@ export type PaletteTokens = {
|
|
|
138
159
|
* against `#0f172a`, the dark set's own `surface`, sky-600 lands at 4.36 and fails. It also buys
|
|
139
160
|
* a light theme with white text on one button and black on the one beside it.
|
|
140
161
|
*/
|
|
141
|
-
export declare const light: PaletteTokens
|
|
162
|
+
export declare const light: Required<PaletteTokens>;
|
|
142
163
|
/**
|
|
143
164
|
* Dark reverses the direction the fills step: it sits higher up the ramp than light does and hovers
|
|
144
165
|
* *up* into lighter still, where light sits lower and hovers down. That is what the components' old
|
|
@@ -150,8 +171,11 @@ export declare const light: PaletteTokens;
|
|
|
150
171
|
* under its darker ones. The pair is the two ends of the one neutral ramp the rest of the palette is
|
|
151
172
|
* already built from - `#f8fafc` is slate-50, `surface` slate-900, `muted` slate-500 - rather than a
|
|
152
173
|
* new colour arriving for a single job. `#020617` is also the lightest slate step that still admits
|
|
153
|
-
* violet-500
|
|
154
|
-
*
|
|
155
|
-
*
|
|
174
|
+
* violet-500: slate-900 draws 4.22 against it and fails. The `secondary` pair sits one ramp step
|
|
175
|
+
* above where it first shipped - sky-500 at rest, sky-400 on hover, where it was sky-600 and sky-500 -
|
|
176
|
+
* because #119 made `secondary` an ink as well as a fill: the outlined Button draws its text and edge
|
|
177
|
+
* in it, and sky-600 measures 4.36:1 against this surface, under the 4.5:1 text floor (now 6.44:1
|
|
178
|
+
* and 8.33:1, both still clearing `secondaryForeground` at 7.28:1 and 9.42:1). Why the ink follows
|
|
179
|
+
* the theme at all, and the two routes rejected in getting here: see the light set.
|
|
156
180
|
*/
|
|
157
|
-
export declare const dark: PaletteTokens
|
|
181
|
+
export declare const dark: Required<PaletteTokens>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import './styles.css';
|
|
2
2
|
export { Cluster } from 'Arrangement/Cluster/Cluster';
|
|
3
|
+
export { ColumnLayout } from 'Arrangement/ColumnLayout/ColumnLayout';
|
|
4
|
+
export { FieldRow } from 'Arrangement/FieldRow/FieldRow';
|
|
3
5
|
export { Stack } from 'Arrangement/Stack/Stack';
|
|
6
|
+
export { Box } from 'Display/Box/Box';
|
|
4
7
|
export { Brandmark } from 'Display/Brandmark/Brandmark';
|
|
5
8
|
export { Checklist } from 'Display/Checklist/Checklist';
|
|
6
9
|
export { Collection } from 'Display/Collection/Collection';
|
|
7
10
|
export { DefinitionList } from 'Display/DefinitionList/DefinitionList';
|
|
8
11
|
export { Figure } from 'Display/Figure/Figure';
|
|
12
|
+
export { Icon } from 'Display/Icon/Icon';
|
|
9
13
|
export { Meter } from 'Display/Meter/Meter';
|
|
10
14
|
export { Rail } from 'Display/Rail/Rail';
|
|
15
|
+
export type { TableColumnAllocation } from 'Display/Table/Table';
|
|
11
16
|
export { Table } from 'Display/Table/Table';
|
|
12
17
|
export { Eyebrow } from 'Display/Typography/Eyebrow/Eyebrow';
|
|
13
18
|
export { H1 } from 'Display/Typography/H1/H1';
|
|
@@ -37,6 +42,7 @@ export { Form } from 'Layout/Form/Form';
|
|
|
37
42
|
export { Header } from 'Layout/Header/Header';
|
|
38
43
|
export { Hero } from 'Layout/Hero/Hero';
|
|
39
44
|
export { PageHead } from 'Layout/PageHead/PageHead';
|
|
45
|
+
export { ScrollContainer } from 'Layout/ScrollContainer/ScrollContainer';
|
|
40
46
|
export { Section } from 'Layout/Section/Section';
|
|
41
47
|
export { Sidebar } from 'Layout/Sidebar/Sidebar';
|
|
42
48
|
export type { PaletteTokens } from 'Theme/Palette';
|
package/package.json
CHANGED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
type CSSProperties,
|
|
6
|
+
createContext,
|
|
7
|
+
Fragment,
|
|
8
|
+
type FunctionComponent,
|
|
9
|
+
isValidElement,
|
|
10
|
+
type ReactElement,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
useContext,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { ColumnLayoutCompositionError } from './ColumnLayoutCompositionError';
|
|
15
|
+
import { ColumnLayoutConfigurationError } from './ColumnLayoutConfigurationError';
|
|
16
|
+
|
|
17
|
+
// A wrapping flex row that aligns its tracks to the top. The gap variant also publishes the chosen
|
|
18
|
+
// role as `--column-layout-gap`, which the threshold below reads, so the switch and the paint take
|
|
19
|
+
// the role from one declaration. Nothing here measures: the arrangement needs no JavaScript.
|
|
20
|
+
const columnLayout = cva('flex flex-wrap items-start', {
|
|
21
|
+
variants: {
|
|
22
|
+
gap: {
|
|
23
|
+
stack:
|
|
24
|
+
'gap-[var(--space-stack)] [--column-layout-gap:var(--space-stack)]',
|
|
25
|
+
region:
|
|
26
|
+
'gap-[var(--space-region)] [--column-layout-gap:var(--space-region)]',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: { gap: 'region' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// The all-or-one switch, after Heydon Pickering's "Holy Albatross": `100%` is the Root's width W
|
|
33
|
+
// and the threshold T sits on the Root, so W >= T clamps the basis to 0 (one line, grown by weight)
|
|
34
|
+
// and W < T amplifies the shortfall past 100% (a line each). The factor turns a 1/64px shortfall,
|
|
35
|
+
// Chrome's layout grain, into a full switch; `0px` and `100%` are the two states, not measurements.
|
|
36
|
+
const columnLayoutColumn = cva(
|
|
37
|
+
'min-w-0 grow-[var(--column-layout-weight)] basis-[clamp(0px,(var(--column-layout-threshold)-100%)*1000000,100%)]',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// React's CSSProperties is closed over known properties; the two custom properties the recipes
|
|
41
|
+
// read are declared here so the style objects stay typed without an assertion.
|
|
42
|
+
type ColumnLayoutRootStyle = CSSProperties & {
|
|
43
|
+
'--column-layout-threshold': string;
|
|
44
|
+
};
|
|
45
|
+
type ColumnLayoutColumnStyle = CSSProperties & {
|
|
46
|
+
'--column-layout-weight': number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** The name of a CSS custom property, as written in a stylesheet: `--main-column-min-width`. */
|
|
50
|
+
type MinWidthToken = `--${string}`;
|
|
51
|
+
|
|
52
|
+
type Track = { weight: number; minWidth: MinWidthToken };
|
|
53
|
+
|
|
54
|
+
// Granted only to a Column the Root counted: the Root wraps each direct Column child in it, and a
|
|
55
|
+
// Column revokes it for its own descendants. Anything else is outside the composition.
|
|
56
|
+
const ColumnLayoutMembership = createContext<boolean>(false);
|
|
57
|
+
|
|
58
|
+
// The ident grammar CSS gives a custom property name, restricted to ASCII so a `var()`, a length,
|
|
59
|
+
// a space or a brace can never ride in on the name.
|
|
60
|
+
const TOKEN_NAME = /^--[A-Za-z0-9_-]+$/;
|
|
61
|
+
|
|
62
|
+
const toTrack = (
|
|
63
|
+
element: ReactElement<IColumnLayoutColumnProps>,
|
|
64
|
+
index: number,
|
|
65
|
+
): Track => {
|
|
66
|
+
const { weight, minWidth } = element.props;
|
|
67
|
+
if (!Number.isFinite(weight) || weight <= 0) {
|
|
68
|
+
throw new ColumnLayoutConfigurationError(
|
|
69
|
+
`column ${index + 1} needs a positive finite weight (got ${weight})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!TOKEN_NAME.test(minWidth)) {
|
|
73
|
+
throw new ColumnLayoutConfigurationError(
|
|
74
|
+
`column ${index + 1} needs a custom-property name such as --main-column-min-width for minWidth (got ${JSON.stringify(minWidth)})`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return { weight, minWidth };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// The documented threshold, `(n - 1) * g + max(m_i * S / w_i)`, left to the browser to resolve
|
|
81
|
+
// so a theme re-pointing a minimum - or the gap - moves it with no script in between.
|
|
82
|
+
const thresholdOf = (tracks: readonly Track[]): string => {
|
|
83
|
+
const total = tracks.reduce((sum, track) => sum + track.weight, 0);
|
|
84
|
+
const shares = tracks.map(
|
|
85
|
+
(track) => `var(${track.minWidth}) * ${total / track.weight}`,
|
|
86
|
+
);
|
|
87
|
+
return `calc(${tracks.length - 1} * var(--column-layout-gap) + max(${shares.join(', ')}))`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const isFragment = (
|
|
91
|
+
node: ReactNode,
|
|
92
|
+
): node is ReactElement<{ children?: ReactNode }> =>
|
|
93
|
+
isValidElement(node) && node.type === Fragment;
|
|
94
|
+
|
|
95
|
+
export interface IColumnLayoutRootProps
|
|
96
|
+
extends VariantProps<typeof columnLayout> {
|
|
97
|
+
/** The columns, as `ColumnLayout.Column` elements: direct children, or arrays and fragments of
|
|
98
|
+
* them. A conditional that renders nothing reserves nothing. */
|
|
99
|
+
children?: ReactNode;
|
|
100
|
+
testId?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface IColumnLayoutColumnProps {
|
|
104
|
+
/** This column's share of the width left after the gaps, relative to its siblings' weights: a
|
|
105
|
+
* positive finite number. `2` beside `1` is a two-thirds/one-third arrangement. */
|
|
106
|
+
weight: number;
|
|
107
|
+
/** The name of the consumer's custom property holding this column's minimum readable width,
|
|
108
|
+
* such as `--main-column-min-width`. A token name, never a length or a `var()`: the consumer
|
|
109
|
+
* declares it in the theme with a nonnegative CSS length. */
|
|
110
|
+
minWidth: MinWidthToken;
|
|
111
|
+
/** The column's content. Rendered unmodified: the column imposes no anatomy. */
|
|
112
|
+
children?: ReactNode;
|
|
113
|
+
testId?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const isColumn = (
|
|
117
|
+
node: ReactNode,
|
|
118
|
+
): node is ReactElement<IColumnLayoutColumnProps> =>
|
|
119
|
+
isValidElement(node) && node.type === ColumnLayoutColumn;
|
|
120
|
+
|
|
121
|
+
const collectColumns = (
|
|
122
|
+
children: ReactNode,
|
|
123
|
+
): ReactElement<IColumnLayoutColumnProps>[] =>
|
|
124
|
+
Children.toArray(children).flatMap((child) => {
|
|
125
|
+
if (isFragment(child)) {
|
|
126
|
+
return collectColumns(child.props.children);
|
|
127
|
+
}
|
|
128
|
+
return isColumn(child) ? [child] : [];
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Marks every counted Column and nothing else, keeping React's own keys for each position so a
|
|
132
|
+
// conditional column appearing later neither remounts its siblings nor moves their focus.
|
|
133
|
+
const markColumns = (children: ReactNode): ReactNode =>
|
|
134
|
+
Children.map(children, (child) => {
|
|
135
|
+
if (isFragment(child)) {
|
|
136
|
+
return <Fragment>{markColumns(child.props.children)}</Fragment>;
|
|
137
|
+
}
|
|
138
|
+
if (isColumn(child)) {
|
|
139
|
+
return (
|
|
140
|
+
<ColumnLayoutMembership.Provider value={true}>
|
|
141
|
+
{child}
|
|
142
|
+
</ColumnLayoutMembership.Provider>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return child;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const ColumnLayoutRoot: FunctionComponent<IColumnLayoutRootProps> = ({
|
|
149
|
+
gap,
|
|
150
|
+
children,
|
|
151
|
+
testId,
|
|
152
|
+
}) => {
|
|
153
|
+
const tracks = collectColumns(children).map(toTrack);
|
|
154
|
+
const style: ColumnLayoutRootStyle | undefined =
|
|
155
|
+
tracks.length === 0
|
|
156
|
+
? undefined
|
|
157
|
+
: { '--column-layout-threshold': thresholdOf(tracks) };
|
|
158
|
+
return (
|
|
159
|
+
<div className={columnLayout({ gap })} style={style} data-testid={testId}>
|
|
160
|
+
{markColumns(children)}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const ColumnLayoutColumn: FunctionComponent<IColumnLayoutColumnProps> = ({
|
|
166
|
+
weight,
|
|
167
|
+
children,
|
|
168
|
+
testId,
|
|
169
|
+
}) => {
|
|
170
|
+
const isCounted = useContext(ColumnLayoutMembership);
|
|
171
|
+
if (!isCounted) {
|
|
172
|
+
throw new ColumnLayoutCompositionError();
|
|
173
|
+
}
|
|
174
|
+
const style: ColumnLayoutColumnStyle = { '--column-layout-weight': weight };
|
|
175
|
+
return (
|
|
176
|
+
<div className={columnLayoutColumn()} style={style} data-testid={testId}>
|
|
177
|
+
<ColumnLayoutMembership.Provider value={false}>
|
|
178
|
+
{children}
|
|
179
|
+
</ColumnLayoutMembership.Provider>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* An arrangement of weighted columns that becomes one column when the space it is given cannot
|
|
186
|
+
* satisfy every column's minimum at the requested proportions. It owns the arrangement and nothing
|
|
187
|
+
* else - no landmark, no band, no join, no gutter, no fill - and takes no outer space, so whatever
|
|
188
|
+
* holds it owns the rhythm around it. `Stack`'s `split` keeps its own viewport-keyed contract; this
|
|
189
|
+
* one answers to the width of its holder (docs/adr/0008, Amendments).
|
|
190
|
+
*
|
|
191
|
+
* @Guarantees — enforced on every render
|
|
192
|
+
* - `Root` renders a `div` with no landmark role, no heading and no margin. Each `Column` is a
|
|
193
|
+
* `div` directly inside it, in the order written, so DOM, reading and keyboard order are the
|
|
194
|
+
* order of the children in both arrangements, and no column is ever remounted merely because
|
|
195
|
+
* the arrangement changed: resizing keeps every column's state and focus.
|
|
196
|
+
* - In the horizontal arrangement the gaps are taken off the Root's content width and the rest is
|
|
197
|
+
* 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.
|
|
199
|
+
* - The row holds only while every proportional share is at least its own minimum. The moment one
|
|
200
|
+
* falls short, every column takes a line of its own and fills the Root's width - a column
|
|
201
|
+
* 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, no share is clamped, nothing is redistributed to
|
|
203
|
+
* postpone the switch. For n columns, gap g, total weight S and minimums m_i the row fits at and
|
|
204
|
+
* above `(n - 1) * g + max(m_i * S / w_i)`.
|
|
205
|
+
* - The space measured is the Root's own width, never the viewport, so two instances on one page
|
|
206
|
+
* switch independently, and a narrow holder on a wide screen stacks.
|
|
207
|
+
* - Every minimum is read from the theme as a CSS length each time layout runs, so re-pointing a
|
|
208
|
+
* token - in a theme class, a media query, or a scope around the Root - moves the threshold
|
|
209
|
+
* with it, and a font-relative length moves it as the type does.
|
|
210
|
+
* - `gap` selects which space role separates the columns, across the row and between the stacked
|
|
211
|
+
* lines alike: `region` (the default), the gap between groups of blocks, or `stack`, the gap
|
|
212
|
+
* between siblings within one block. Nothing else.
|
|
213
|
+
* - 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.
|
|
215
|
+
* Allocations follow every change of columns, weights, gap, holder width or theme.
|
|
216
|
+
* - A column adds no scrolling, truncation or overflow treatment of its own and does not widen its
|
|
217
|
+
* track for its content: the content keeps whatever wrapping or scrolling contract it has.
|
|
218
|
+
* - Every value it emits is a role the token layer or the consumer's theme already names; the
|
|
219
|
+
* only literals are the switch's two states.
|
|
220
|
+
* - A non-positive or non-finite `weight`, or a `minWidth` that is not a custom-property name,
|
|
221
|
+
* throws {@link ColumnLayoutConfigurationError}; a `Column` that is not a direct child of a
|
|
222
|
+
* `Root` - loose, nested in another column, or reached through a wrapping component - throws
|
|
223
|
+
* {@link ColumnLayoutCompositionError}. Both are programmer errors, raised loud and early.
|
|
224
|
+
*
|
|
225
|
+
* @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, as a valid nonnegative
|
|
227
|
+
* CSS length: `24rem`, `20em`, `40ch`, `320px`. A `rem` resolves against the document root, an
|
|
228
|
+
* `em` or `ch` against the Root's inherited type. A missing or invalid token is not a
|
|
229
|
+
* responsive configuration: the switch has nothing to compare and the columns size from their
|
|
230
|
+
* content instead.
|
|
231
|
+
* - The holder gives the Root a definite width, as any block does. Inside a shrink-to-fit frame
|
|
232
|
+
* there is no width to measure the shares against.
|
|
233
|
+
* - Content that cannot wrap - an unbroken string, a fixed-width control - needs its own overflow
|
|
234
|
+
* contract, as `Table.Root` has; the column will not widen to hold it.
|
|
235
|
+
* - `Root`'s children are `Column`s. Anything else placed beside them is rendered as given but is
|
|
236
|
+
* not a column: it takes no weight, counts for no gap or threshold, and sits in the row as
|
|
237
|
+
* content of its own width, which breaks the proportions. Content belongs inside a `Column`.
|
|
238
|
+
*
|
|
239
|
+
* @UXGuidelines
|
|
240
|
+
* - A minimum is the width below which the column's content stops being readable or operable -
|
|
241
|
+
* the narrowest a comparison table can be scanned, the narrowest a summary's labels keep their
|
|
242
|
+
* lines - not the width the designer would like. The stack is the readable fallback.
|
|
243
|
+
* - Weights are structural relationships: `2` and `1` say the table is the subject and the summary
|
|
244
|
+
* its support. A ratio chosen to hit a pixel width is a measurement, and the theme owns those.
|
|
245
|
+
*/
|
|
246
|
+
export const ColumnLayout = {
|
|
247
|
+
Root: ColumnLayoutRoot,
|
|
248
|
+
Column: ColumnLayoutColumn,
|
|
249
|
+
} as const;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export class ColumnLayoutCompositionError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super(
|
|
4
|
+
'ColumnLayout.Column must be a direct child of ColumnLayout.Root (arrays and fragments are fine; a wrapping component or a nested Column is not)',
|
|
5
|
+
);
|
|
6
|
+
this.name = 'ColumnLayoutCompositionError';
|
|
7
|
+
}
|
|
8
|
+
}
|