@juwel-development/design-system 3.11.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/design-system.js +1094 -546
- 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 +57 -0
- package/dist/types/Layout/Screen/Screen.d.ts +31 -0
- package/dist/types/Layout/Screen/ScreenBodyAllocation.d.ts +1 -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 +22 -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 +272 -0
- package/src/Layout/Screen/Screen.tsx +61 -0
- package/src/Layout/Screen/ScreenBodyAllocation.ts +5 -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 +22 -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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import {
|
|
3
|
+
type FunctionComponent,
|
|
4
|
+
type ReactNode,
|
|
5
|
+
useLayoutEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
const shell = cva(
|
|
11
|
+
'h-dvh min-w-0 overflow-clip grid grid-rows-[auto_minmax(0,1fr)]',
|
|
12
|
+
);
|
|
13
|
+
const shellBody = cva(
|
|
14
|
+
'min-h-0 min-w-0 overflow-auto overscroll-contain p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))] scroll-p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))] outline-focus-ring focus-visible:outline focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-[calc(-1*var(--focus-ring-width))]',
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
mode: { console: '[&>div]:h-full', page: '[&>div]:min-h-full' },
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: { mode: 'console' },
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Geometry is read only to expose a keyboard stop; CSS alone allocates the height.
|
|
24
|
+
const useScrollableBody = (body: {
|
|
25
|
+
current: HTMLDivElement | null;
|
|
26
|
+
}): boolean => {
|
|
27
|
+
const [isScrollable, setIsScrollable] = useState(true);
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
const element = body.current;
|
|
30
|
+
if (!element) return;
|
|
31
|
+
const measure = () =>
|
|
32
|
+
setIsScrollable(
|
|
33
|
+
element.scrollHeight > element.clientHeight ||
|
|
34
|
+
element.scrollWidth > element.clientWidth ||
|
|
35
|
+
element === element.ownerDocument.activeElement,
|
|
36
|
+
);
|
|
37
|
+
measure();
|
|
38
|
+
const resize =
|
|
39
|
+
typeof ResizeObserver === 'undefined'
|
|
40
|
+
? undefined
|
|
41
|
+
: new ResizeObserver(measure);
|
|
42
|
+
const observeContent = () => {
|
|
43
|
+
resize?.disconnect();
|
|
44
|
+
resize?.observe(element);
|
|
45
|
+
for (const child of element.querySelectorAll('*')) resize?.observe(child);
|
|
46
|
+
measure();
|
|
47
|
+
};
|
|
48
|
+
observeContent();
|
|
49
|
+
const mutations = new MutationObserver(observeContent);
|
|
50
|
+
mutations.observe(element, {
|
|
51
|
+
childList: true,
|
|
52
|
+
subtree: true,
|
|
53
|
+
characterData: true,
|
|
54
|
+
});
|
|
55
|
+
window.addEventListener('resize', measure);
|
|
56
|
+
element.addEventListener('blur', measure);
|
|
57
|
+
return () => {
|
|
58
|
+
resize?.disconnect();
|
|
59
|
+
mutations.disconnect();
|
|
60
|
+
window.removeEventListener('resize', measure);
|
|
61
|
+
element.removeEventListener('blur', measure);
|
|
62
|
+
};
|
|
63
|
+
}, [body]);
|
|
64
|
+
return isScrollable;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface IShellProps extends VariantProps<typeof shellBody> {
|
|
68
|
+
/** Consumer-owned top bar; its content determines its height. */
|
|
69
|
+
bar: ReactNode;
|
|
70
|
+
/** Body content; receives the remaining height in console mode. */
|
|
71
|
+
children: ReactNode;
|
|
72
|
+
/** The body scroll group's name, exposed only while overflowing or focused. */
|
|
73
|
+
ariaLabel: string;
|
|
74
|
+
testId?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A viewport-owning frame with a content-height bar and the remaining height for its body.
|
|
79
|
+
*
|
|
80
|
+
* @Guarantees
|
|
81
|
+
* - CSS allocates both tracks, including when bar content wraps. Header is unchanged.
|
|
82
|
+
* - Console (default) passes a finite height to its child; page lets body content grow.
|
|
83
|
+
* Both keep overflow inside the body, with the bar visible and document still.
|
|
84
|
+
* - Console body scrolling is a reachability fallback for content that cannot fit; bounded
|
|
85
|
+
* panels normally own their scrolling. Page deliberately scrolls the body as a whole.
|
|
86
|
+
* - Overflow makes the body a named, keyboard-reachable group, never a landmark. Removing
|
|
87
|
+
* overflow does not relocate focus. Native focus reveal and scroll keys remain intact.
|
|
88
|
+
* - Body padding reserves the shared focus ring's room, with an inset ring on the body itself.
|
|
89
|
+
*
|
|
90
|
+
* @CallerMustEnsure
|
|
91
|
+
* - Place one Shell at the viewport origin, with no outside margins, padding or siblings that
|
|
92
|
+
* add document extent. This is the application frame, not a nested fullscreen section.
|
|
93
|
+
* - The bar must leave usable body space. An arbitrarily tall bar cannot fit every viewport.
|
|
94
|
+
* - Supply the main landmark inside the body (inside Sidebar.Content when using Sidebar).
|
|
95
|
+
* - Intermediate consumer wrappers must preserve the allocated height for bounded panels;
|
|
96
|
+
* wrappers with height: 100% and min-height: 0, or display: contents, can pass it through.
|
|
97
|
+
* - Use Sidebar.Root fill in a console; below 64rem its list stacks and the body scrolls.
|
|
98
|
+
* - Name the body in the viewer's language. Content and its panel arrangements remain yours.
|
|
99
|
+
*/
|
|
100
|
+
export const Shell: FunctionComponent<IShellProps> = ({
|
|
101
|
+
bar,
|
|
102
|
+
children,
|
|
103
|
+
ariaLabel,
|
|
104
|
+
mode,
|
|
105
|
+
testId,
|
|
106
|
+
}) => {
|
|
107
|
+
const body = useRef<HTMLDivElement>(null);
|
|
108
|
+
const isScrollable = useScrollableBody(body);
|
|
109
|
+
return (
|
|
110
|
+
<div className={shell()} data-testid={testId}>
|
|
111
|
+
<div>{bar}</div>
|
|
112
|
+
{/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: the group and its name are conditional together */}
|
|
113
|
+
<div
|
|
114
|
+
ref={body}
|
|
115
|
+
className={shellBody({ mode })}
|
|
116
|
+
role={isScrollable ? 'group' : undefined}
|
|
117
|
+
aria-label={isScrollable ? ariaLabel : undefined}
|
|
118
|
+
tabIndex={isScrollable ? 0 : undefined}
|
|
119
|
+
>
|
|
120
|
+
<div>{children}</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cva } from 'class-variance-authority';
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
2
|
import type { FunctionComponent, ReactNode, RefCallback } from 'react';
|
|
3
3
|
import { Children, createContext, isValidElement, useContext } from 'react';
|
|
4
4
|
import type { Subject } from 'rxjs';
|
|
@@ -12,10 +12,20 @@ const sidebarRoot = cva(
|
|
|
12
12
|
'grid gap-[var(--space-region)] lg:grid-cols-[12rem_minmax(0,1fr)]',
|
|
13
13
|
'[&>div:first-child]:border-solid [&>div:first-child]:border-border [&>div:first-child]:border-b [&>div:first-child]:pb-[var(--space-stack)]',
|
|
14
14
|
'lg:[&>div:first-child]:border-b-0 lg:[&>div:first-child]:border-r lg:[&>div:first-child]:pb-0 lg:[&>div:first-child]:pr-[var(--space-stack)]',
|
|
15
|
-
'lg:[&>div:first-child>nav]:sticky lg:[&>div:first-child>nav]:top-0 lg:[&>div:first-child>nav]:
|
|
15
|
+
'lg:[&>div:first-child>nav]:sticky lg:[&>div:first-child>nav]:top-0 lg:[&>div:first-child>nav]:overflow-y-auto',
|
|
16
16
|
'[&>div:first-child>nav]:p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))] lg:[&>div:first-child>nav]:scroll-p-[calc(var(--focus-ring-width)+var(--focus-ring-offset))]',
|
|
17
17
|
'[&>div:first-child>nav>ul]:flex [&>div:first-child>nav>ul]:flex-col [&>div:first-child>nav>ul]:gap-[var(--space-stack)]',
|
|
18
18
|
].join(' '),
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
fill: {
|
|
22
|
+
false:
|
|
23
|
+
'lg:[&>div:first-child>nav]:max-h-[min(100dvh,var(--sidebar-scrollport-height,100dvh))]',
|
|
24
|
+
true: 'min-h-full lg:h-full lg:min-h-0 lg:grid-rows-[minmax(0,1fr)] lg:[&>div]:min-h-0 lg:[&>div]:h-full lg:[&>div:first-child>nav]:max-h-full',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: { fill: false },
|
|
28
|
+
},
|
|
19
29
|
);
|
|
20
30
|
|
|
21
31
|
// A percentage max-height resolves against the content-driven grid cell, not its scrollport;
|
|
@@ -38,9 +48,13 @@ const observeScrollport: RefCallback<HTMLElement> = (navigation) => {
|
|
|
38
48
|
),
|
|
39
49
|
);
|
|
40
50
|
if (scrollport) {
|
|
51
|
+
const scrollportStyle = getComputedStyle(scrollport);
|
|
52
|
+
const padding =
|
|
53
|
+
(Number.parseFloat(scrollportStyle.paddingTop) || 0) +
|
|
54
|
+
(Number.parseFloat(scrollportStyle.paddingBottom) || 0);
|
|
41
55
|
navigation.style.setProperty(
|
|
42
56
|
'--sidebar-scrollport-height',
|
|
43
|
-
`${scrollport.clientHeight}px`,
|
|
57
|
+
`${Math.max(0, scrollport.clientHeight - padding)}px`,
|
|
44
58
|
);
|
|
45
59
|
} else {
|
|
46
60
|
navigation.style.removeProperty('--sidebar-scrollport-height');
|
|
@@ -96,7 +110,10 @@ const SidebarSelectionContext = createContext<SidebarSelection | undefined>(
|
|
|
96
110
|
undefined,
|
|
97
111
|
);
|
|
98
112
|
|
|
99
|
-
export interface ISidebarRootProps {
|
|
113
|
+
export interface ISidebarRootProps extends VariantProps<typeof sidebarRoot> {
|
|
114
|
+
/** Pass a finite parent height through to Content at 64rem and above, using CSS alone.
|
|
115
|
+
* Below 64rem the enclosing scrollable body owns the stacked list and content. */
|
|
116
|
+
fill?: VariantProps<typeof sidebarRoot>['fill'];
|
|
100
117
|
/** The key of the active entry. The caller supplies a key identifying one non-inert entry;
|
|
101
118
|
* Sidebar renders what it is given and never selects a fallback for an invalid key. */
|
|
102
119
|
active: string;
|
|
@@ -166,12 +183,13 @@ const SidebarRoot: FunctionComponent<ISidebarRootProps> = ({
|
|
|
166
183
|
onSelect$,
|
|
167
184
|
children,
|
|
168
185
|
testId,
|
|
186
|
+
fill,
|
|
169
187
|
}) => {
|
|
170
188
|
const parts = Children.toArray(children).filter(isValidElement);
|
|
171
189
|
return (
|
|
172
|
-
<div className={sidebarRoot()} data-testid={testId}>
|
|
190
|
+
<div className={sidebarRoot({ fill })} data-testid={testId}>
|
|
173
191
|
<div>
|
|
174
|
-
<nav ref={observeScrollport} aria-label={label}>
|
|
192
|
+
<nav ref={fill ? undefined : observeScrollport} aria-label={label}>
|
|
175
193
|
<SidebarSelectionContext value={{ active, onSelect$ }}>
|
|
176
194
|
<ul>{parts.filter((part) => part.type === SidebarItem)}</ul>
|
|
177
195
|
</SidebarSelectionContext>
|
|
@@ -204,11 +222,17 @@ const SidebarRoot: FunctionComponent<ISidebarRootProps> = ({
|
|
|
204
222
|
* assumed top-bar offset, capped to the screen/scrolling-area height with independent entry scrolling.
|
|
205
223
|
* Content sits beside it in `minmax(0,1fr)`, so wide content cannot displace the track.
|
|
206
224
|
* - Below 64rem the whole list lies above the content in normal flow - no stickiness, no cap, no
|
|
207
|
-
* drawer.
|
|
225
|
+
* drawer. Without fill, Sidebar grows with its content.
|
|
226
|
+
* - With fill, a finite parent height passes through Root and Content at 64rem and above;
|
|
227
|
+
* navigation is capped to that allocated height by CSS, with no scrollport measurement.
|
|
228
|
+
* Below 64rem the list stacks and the enclosing body scrolls. The DOM never switches.
|
|
208
229
|
* - It separates the tracks itself (the region space role and a `border` hairline) but gives the
|
|
209
230
|
* content track no padding and no landmark - the consumer owns everything inside `Content`.
|
|
210
231
|
*
|
|
211
232
|
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
233
|
+
* - Fill requires a parent with a finite height and a scrollable enclosing body for narrow
|
|
234
|
+
* viewports or oversized fixed content. Consumer wrappers inside Content preserve that bound.
|
|
235
|
+
* It works with any bounded parent; no Shell identity, context or offset is required.
|
|
212
236
|
* - Entry keys are unique and `active` names one non-inert entry. Sidebar corrects nothing.
|
|
213
237
|
* - Items and the one Content are direct children of `Root` - Root assembles only what it can see,
|
|
214
238
|
* and an Item rendered outside a Root has no selection to derive its treatment from.
|
package/src/Theme/Palette.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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
12
|
* swapping the `.dark` class re-points the variables underneath it. Complete means `Required`: the
|
|
13
|
-
*
|
|
13
|
+
* roles the type marks optional are optional for a consumer's palette object, never here.
|
|
14
14
|
*
|
|
15
15
|
* The values below are still depot-tracker's brand (primary is its violet). They are carried over
|
|
16
16
|
* so nothing changed visually during the extraction - a starting point to replace, not a decision.
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
export type PaletteTokens = {
|
|
19
19
|
/** Page and panel background. */
|
|
20
20
|
surface: string;
|
|
21
|
+
/** Selected Table row surface, distinct from resting and hover surfaces. Keep foreground, muted
|
|
22
|
+
* link and status inks at 4.5:1; marker, rule and focusRing at 3:1 in each scheme.
|
|
23
|
+
* Optional for existing palette objects; CSS overrides inherit the shipped default. Consumers
|
|
24
|
+
* composing other inks or controls on this surface must ensure their contrast too. */
|
|
25
|
+
selectedSurface?: string;
|
|
26
|
+
/** Error ink/fill on a selected surface: 4.5:1 against selectedSurface and errorForeground. */
|
|
27
|
+
selectedError?: string;
|
|
21
28
|
/** Default text colour on `surface`. */
|
|
22
29
|
foreground: string;
|
|
23
30
|
/** De-emphasised text - captions, help text, placeholders. */
|
|
@@ -42,11 +49,11 @@ export type PaletteTokens = {
|
|
|
42
49
|
|
|
43
50
|
/** The themed veil behind a Dialog, marking the page beneath as present but unavailable. The one
|
|
44
51
|
* role whose alpha is part of the colour - the page must show through, which a solid hex cannot
|
|
45
|
-
* say - so it is an `rgb(r g b / a)` value, not a hex.
|
|
46
|
-
*
|
|
52
|
+
* say - so it is an `rgb(r g b / a)` value, not a hex. Constraint: compositing over surface
|
|
53
|
+
* must reduce its relative luminance by at least 50%; modality never depends on the treatment.
|
|
47
54
|
*
|
|
48
55
|
* Migration (added with Dialog): a consumer constructing its own `PaletteTokens` object must
|
|
49
|
-
* add this role - `rgb(15 23 42 / 0.5)` is the
|
|
56
|
+
* add this role - `rgb(15 23 42 / 0.5)` is the light default. Spreading `light`/`dark` and
|
|
50
57
|
* overriding is unaffected, and a theme that only overrides the generated custom properties
|
|
51
58
|
* in CSS inherits the default. */
|
|
52
59
|
scrim: string;
|
|
@@ -170,6 +177,8 @@ export type PaletteTokens = {
|
|
|
170
177
|
*/
|
|
171
178
|
export const light: Required<PaletteTokens> = {
|
|
172
179
|
surface: '#ffffff',
|
|
180
|
+
selectedSurface: '#f8fafc',
|
|
181
|
+
selectedError: '#cd2d7d',
|
|
173
182
|
foreground: '#0f172a',
|
|
174
183
|
muted: '#64748b',
|
|
175
184
|
border: '#e2e8f0',
|
|
@@ -225,6 +234,8 @@ export const light: Required<PaletteTokens> = {
|
|
|
225
234
|
*/
|
|
226
235
|
export const dark: Required<PaletteTokens> = {
|
|
227
236
|
surface: '#0f172a',
|
|
237
|
+
selectedSurface: '#151e30',
|
|
238
|
+
selectedError: '#f48fb1',
|
|
228
239
|
foreground: '#f8fafc',
|
|
229
240
|
muted: '#94a3b8',
|
|
230
241
|
border: '#334155',
|
|
@@ -232,7 +243,7 @@ export const dark: Required<PaletteTokens> = {
|
|
|
232
243
|
rule: '#5b6a80',
|
|
233
244
|
backing: '#1e293b',
|
|
234
245
|
|
|
235
|
-
scrim: 'rgb(
|
|
246
|
+
scrim: 'rgb(0 0 0 / 0.6)',
|
|
236
247
|
|
|
237
248
|
meterFill: '#f8fafc',
|
|
238
249
|
meterTrack: '#334155',
|
|
@@ -246,7 +257,7 @@ export const dark: Required<PaletteTokens> = {
|
|
|
246
257
|
secondaryForeground: '#020617',
|
|
247
258
|
|
|
248
259
|
disabled: '#475569',
|
|
249
|
-
disabledHover: '#
|
|
260
|
+
disabledHover: '#64748b',
|
|
250
261
|
|
|
251
262
|
focusRing: '#cbd5e1',
|
|
252
263
|
|
|
@@ -96,14 +96,20 @@ const TYPOGRAPHY = `@theme {
|
|
|
96
96
|
--font-primary: inherit;
|
|
97
97
|
--font-secondary: inherit;
|
|
98
98
|
|
|
99
|
+
/* Visual emphasis for body copy and annotations (#135), independent of family and size. */
|
|
100
|
+
--font-weight-strong: 700;
|
|
101
|
+
|
|
99
102
|
/* Type roles, not a scale: one role per job, so a component asks for what its text is, never a rung.
|
|
100
103
|
display > title > subtitle are the three heading steps H1-H3 bind to; H4-H6 share body and split
|
|
101
104
|
by weight, so there is no fourth role. display is the hero and must out-scale every subpage head. */
|
|
102
|
-
|
|
105
|
+
/* At 1280, 1440 and 1920 CSS px (16px root), size(w / 2) / size(w) must be >= 0.75.
|
|
106
|
+
At 200% zoom the viewport halves, so physical growth is 2 * that ratio: at least 1.5x.
|
|
107
|
+
The rem terms preserve growth; at 1280px these sizes stay within 2px of the old vw clamps. */
|
|
108
|
+
--text-display: clamp(3rem, 3rem + 3.125vw, 6rem);
|
|
103
109
|
--leading-display: 1.05;
|
|
104
|
-
--text-title: clamp(2.25rem,
|
|
110
|
+
--text-title: clamp(2.25rem, 2.25rem + 2.25vw, 4.25rem);
|
|
105
111
|
--leading-title: 1.1;
|
|
106
|
-
--text-subtitle: clamp(1.5rem,
|
|
112
|
+
--text-subtitle: clamp(1.5rem, 1.5rem + 1vw, 2.25rem);
|
|
107
113
|
--leading-subtitle: 1.2;
|
|
108
114
|
|
|
109
115
|
/* The reading block's opening paragraph (Prose #21): sized above body and below subtitle, led
|
|
@@ -112,6 +118,10 @@ const TYPOGRAPHY = `@theme {
|
|
|
112
118
|
subtitle was rejected in ADR 0004 - it welds the lede's size to H3's and its leading to a head's. */
|
|
113
119
|
--text-lede: 1.25rem; /* 20px */
|
|
114
120
|
--leading-lede: 1.4;
|
|
121
|
+
|
|
122
|
+
/* Whole correspondence blocks, independently of opening ledes (Prose #128). */
|
|
123
|
+
--text-letter: 1.25rem;
|
|
124
|
+
--leading-letter: 1.8;
|
|
115
125
|
--text-body: 1.0625rem; /* 17px, floored at 16px: below it iOS Safari zooms the viewport when a control set at this role takes focus (#92) */
|
|
116
126
|
--leading-body: 1.6;
|
|
117
127
|
--text-small: 0.9375rem; /* 15px floor: below it tabular figures stop comparing column to column */
|
|
@@ -126,7 +136,7 @@ const TYPOGRAPHY = `@theme {
|
|
|
126
136
|
/* The Dialog title role: the top heading of an independent transitory task, deliberately outside
|
|
127
137
|
the page heading ladder (docs/adr/0005). Ships subtitle's values while staying an independent
|
|
128
138
|
role, so a consumer re-points Dialog titles without moving every H3 (docs/adr/0004, Amendments). */
|
|
129
|
-
--text-dialog-title: clamp(1.5rem,
|
|
139
|
+
--text-dialog-title: clamp(1.5rem, 1.5rem + 1vw, 2.25rem);
|
|
130
140
|
--leading-dialog-title: 1.2;
|
|
131
141
|
|
|
132
142
|
/* Two quantities on one property that must not collapse: --tracking-label is a fixed letter-spaced
|
|
@@ -147,6 +157,9 @@ const FAMILY_ROLES = `@utility font-heading {
|
|
|
147
157
|
font-family: var(--font-heading, var(--font-primary));
|
|
148
158
|
}
|
|
149
159
|
|
|
160
|
+
@utility font-letter {
|
|
161
|
+
font-family: var(--font-letter, var(--font-primary));
|
|
162
|
+
}
|
|
150
163
|
@utility font-body {
|
|
151
164
|
font-family: var(--font-body, var(--font-primary));
|
|
152
165
|
}
|
|
@@ -161,6 +174,7 @@ const FAMILY_ROLES = `@utility font-heading {
|
|
|
161
174
|
the action column, in rem (docs/adr/0008, Amendments, #97); 20rem clears --control-min-width. */
|
|
162
175
|
const MEASURE = `:root {
|
|
163
176
|
--measure: 66ch;
|
|
177
|
+
--measure-letter: 60ch;
|
|
164
178
|
--measure-display: 36ch;
|
|
165
179
|
--measure-wide: 72ch;
|
|
166
180
|
--measure-action: 20rem;
|
|
@@ -218,6 +232,20 @@ const DEFINITION_SPACING = `:root {
|
|
|
218
232
|
initial-value: 1px;
|
|
219
233
|
}`;
|
|
220
234
|
|
|
235
|
+
// Resolve the fill row's width shortfall before its columns query the horizontal state.
|
|
236
|
+
const COLUMN_LAYOUT_FIT = `@property --column-layout-shortfall {
|
|
237
|
+
syntax: "<length>";
|
|
238
|
+
inherits: false;
|
|
239
|
+
initial-value: 1px;
|
|
240
|
+
}`;
|
|
241
|
+
|
|
242
|
+
/* Callout's leading mark and separation from its content (#131). The mark stays visible at a
|
|
243
|
+
positive CSS length; its inset accepts a nonnegative CSS length and follows inherited type. */
|
|
244
|
+
const CALLOUT_MARK = `:root {
|
|
245
|
+
--callout-mark-width: 0.25rem;
|
|
246
|
+
--space-callout-inset: 1em;
|
|
247
|
+
}`;
|
|
248
|
+
|
|
221
249
|
/* Box's inner padding (#117): one role on all four sides, distinct from a sibling gap, a region's
|
|
222
250
|
air or a page band, so a brand tunes every Box at once and no Box gains a padding prop (ADR 0008,
|
|
223
251
|
the token-role test). In em like --space-collection-item, so it tracks the inherited type.
|
|
@@ -286,6 +314,16 @@ const TICK = `:root {
|
|
|
286
314
|
--tick-thickness: 1px;
|
|
287
315
|
}`;
|
|
288
316
|
|
|
317
|
+
/* Step geometry is independently themeable: positive marker size and line thicknesses,
|
|
318
|
+
nonnegative insets. The marker size must fit the consumer's font and ordinal count. */
|
|
319
|
+
const STEPPER = `:root {
|
|
320
|
+
--step-marker-size: 2em;
|
|
321
|
+
--step-inset-inline: 1em;
|
|
322
|
+
--step-inset-block: 0.5em;
|
|
323
|
+
--step-connector-thickness: 1px;
|
|
324
|
+
--step-selection-thickness: 2px;
|
|
325
|
+
}`;
|
|
326
|
+
|
|
289
327
|
/* Tab label insets separate adjacent controls and give the label vertical breathing room.
|
|
290
328
|
These are two axis-specific jobs, not a selectable scale; stack/region/band describe other jobs.
|
|
291
329
|
Like those spacing roles, em tracks the label type. Non-negative values preserve the hit area. */
|
|
@@ -320,6 +358,13 @@ const SLIDER = `:root {
|
|
|
320
358
|
--slider-thumb-size: 1.5rem;
|
|
321
359
|
}`;
|
|
322
360
|
|
|
361
|
+
/* Switch geometry is independent of choice markers: its track holds two thumb positions.
|
|
362
|
+
Both lengths must be positive; track width derives from the thumb so positions stay distinct. */
|
|
363
|
+
const SWITCH = `:root {
|
|
364
|
+
--switch-thumb-size: 1rem;
|
|
365
|
+
--switch-track-inset: 0.125rem;
|
|
366
|
+
}`;
|
|
367
|
+
|
|
323
368
|
/* The choice marker's two dimensions are not colours: like the tick they live in :root only,
|
|
324
369
|
never @theme inline, so a brand re-points the marker without Choices gaining a prop. Sized in
|
|
325
370
|
rem like the tick and the slider thumb. Constraints: both > 0 (a zero dot erases the one cue
|
|
@@ -363,13 +408,8 @@ const declarations = (
|
|
|
363
408
|
.map((name) => ` --color-${toKebabCase(name)}: ${value(name)};`)
|
|
364
409
|
.join('\n');
|
|
365
410
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
// test checks. See docs/adr/0002-focus-ring-token-contract.md.
|
|
369
|
-
const raw = (tokens: Required<PaletteTokens>) => (name: string) => {
|
|
370
|
-
const value = tokens[name as keyof PaletteTokens];
|
|
371
|
-
return name === 'focusRing' ? `var(--color-ring, ${value})` : value;
|
|
372
|
-
};
|
|
411
|
+
const raw = (tokens: Required<PaletteTokens>) => (name: string) =>
|
|
412
|
+
tokens[name as keyof PaletteTokens];
|
|
373
413
|
const reference = () => (name: string) => `var(--color-${toKebabCase(name)})`;
|
|
374
414
|
|
|
375
415
|
// Only the combined tokens.css carries the dark set; this gates it behind .dark and defines dark:.
|
|
@@ -406,7 +446,7 @@ ${UNDERLINE}
|
|
|
406
446
|
utilities, so it is not carried in @theme inline with the palette. */
|
|
407
447
|
${TYPOGRAPHY}
|
|
408
448
|
|
|
409
|
-
/* The heading, body and control family roles are utilities with a fallback to the primary face, not
|
|
449
|
+
/* The heading, body, letter and control family roles are utilities with a fallback to the primary face, not
|
|
410
450
|
@theme variables, so a theme re-pointing either name at any scope reaches the text beneath it. */
|
|
411
451
|
${FAMILY_ROLES}
|
|
412
452
|
|
|
@@ -433,9 +473,14 @@ ${COLLECTION_SPACING}
|
|
|
433
473
|
/* DefinitionList's item air and column minimums sit in :root beside the collection role (#123). */
|
|
434
474
|
${DEFINITION_SPACING}
|
|
435
475
|
|
|
476
|
+
${COLUMN_LAYOUT_FIT}
|
|
477
|
+
|
|
436
478
|
/* Box's inner padding, measured against inherited type, beside Collection's. */
|
|
437
479
|
${BOX_INSET}
|
|
438
480
|
|
|
481
|
+
/* Callout marks an edge without enclosing or arranging its content. */
|
|
482
|
+
${CALLOUT_MARK}
|
|
483
|
+
|
|
439
484
|
/* Table's cell insets, one pair per density, beside Box's. */
|
|
440
485
|
${TABLE_CELL_INSET}
|
|
441
486
|
|
|
@@ -459,11 +504,15 @@ ${TABLE_SELECTION_MARKER}
|
|
|
459
504
|
|
|
460
505
|
${TAB_INSETS}
|
|
461
506
|
|
|
507
|
+
${STEPPER}
|
|
508
|
+
|
|
462
509
|
${SLIDER}
|
|
463
510
|
|
|
464
511
|
/* The choice marker's dimensions are not colours either, and sit in :root beside the slider block. */
|
|
465
512
|
${CHOICE_MARKER}
|
|
466
513
|
|
|
514
|
+
${SWITCH}
|
|
515
|
+
|
|
467
516
|
/* The meter track's thickness is not a colour either, and sits in :root beside the tab blocks. */
|
|
468
517
|
${METER_TRACK}
|
|
469
518
|
|
package/src/index.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import './styles.css';
|
|
2
|
-
|
|
3
1
|
export { Cluster } from 'Arrangement/Cluster/Cluster';
|
|
4
2
|
export { ColumnLayout } from 'Arrangement/ColumnLayout/ColumnLayout';
|
|
3
|
+
export { ColumnLayoutCompositionError } from 'Arrangement/ColumnLayout/ColumnLayoutCompositionError';
|
|
4
|
+
export { ColumnLayoutConfigurationError } from 'Arrangement/ColumnLayout/ColumnLayoutConfigurationError';
|
|
5
5
|
export { FieldRow } from 'Arrangement/FieldRow/FieldRow';
|
|
6
|
+
export { FieldRowCompositionError } from 'Arrangement/FieldRow/FieldRowCompositionError';
|
|
7
|
+
export { FieldRowConfigurationError } from 'Arrangement/FieldRow/FieldRowConfigurationError';
|
|
6
8
|
export { Stack } from 'Arrangement/Stack/Stack';
|
|
7
9
|
export { Box } from 'Display/Box/Box';
|
|
8
10
|
export { Brandmark } from 'Display/Brandmark/Brandmark';
|
|
11
|
+
export { Callout } from 'Display/Callout/Callout';
|
|
9
12
|
export { Checklist } from 'Display/Checklist/Checklist';
|
|
10
13
|
export { Collection } from 'Display/Collection/Collection';
|
|
11
14
|
export { DefinitionList } from 'Display/DefinitionList/DefinitionList';
|
|
15
|
+
export { DefinitionListConfigurationError } from 'Display/DefinitionList/DefinitionListConfigurationError';
|
|
12
16
|
export { Figure } from 'Display/Figure/Figure';
|
|
13
17
|
export { Icon } from 'Display/Icon/Icon';
|
|
14
18
|
export { Meter } from 'Display/Meter/Meter';
|
|
19
|
+
export { MeterConfigurationError } from 'Display/Meter/MeterConfigurationError';
|
|
15
20
|
export { Rail } from 'Display/Rail/Rail';
|
|
21
|
+
export { Rule } from 'Display/Rule/Rule';
|
|
16
22
|
export type { TableColumnAllocation } from 'Display/Table/Table';
|
|
17
23
|
export { Table } from 'Display/Table/Table';
|
|
24
|
+
export { TableConfigurationError } from 'Display/Table/TableConfigurationError';
|
|
18
25
|
export { Eyebrow } from 'Display/Typography/Eyebrow/Eyebrow';
|
|
19
26
|
export { H1 } from 'Display/Typography/H1/H1';
|
|
20
27
|
export { H2 } from 'Display/Typography/H2/H2';
|
|
@@ -27,24 +34,37 @@ export { P } from 'Display/Typography/P/P';
|
|
|
27
34
|
export { Prose } from 'Display/Typography/Prose/Prose';
|
|
28
35
|
export { Button } from 'Interaction/Button/Button';
|
|
29
36
|
export { Choices } from 'Interaction/Choices/Choices';
|
|
37
|
+
export { ChoicesCompositionError } from 'Interaction/Choices/ChoicesCompositionError';
|
|
30
38
|
export { Input } from 'Interaction/Input/Input';
|
|
31
39
|
export { Link } from 'Interaction/Link/Link';
|
|
32
40
|
export { MultiSelect } from 'Interaction/MultiSelect/MultiSelect';
|
|
41
|
+
export { MultiSelectCompositionError } from 'Interaction/MultiSelect/MultiSelectCompositionError';
|
|
33
42
|
export { NumberInput } from 'Interaction/NumberInput/NumberInput';
|
|
34
43
|
export { Select } from 'Interaction/Select/Select';
|
|
35
44
|
export { Slider } from 'Interaction/Slider/Slider';
|
|
45
|
+
export { SliderConfigurationError } from 'Interaction/Slider/SliderConfigurationError';
|
|
46
|
+
export { Stepper } from 'Interaction/Stepper/Stepper';
|
|
47
|
+
export { StepperCompositionError } from 'Interaction/Stepper/StepperCompositionError';
|
|
48
|
+
export { Switch } from 'Interaction/Switch/Switch';
|
|
36
49
|
export { Tabs } from 'Interaction/Tabs/Tabs';
|
|
50
|
+
export { TabsCompositionError } from 'Interaction/Tabs/TabsCompositionError';
|
|
37
51
|
export { TextArea } from 'Interaction/TextArea/TextArea';
|
|
38
52
|
export { Cover } from 'Layout/Cover/Cover';
|
|
39
53
|
export { Dialog } from 'Layout/Dialog/Dialog';
|
|
54
|
+
export { DialogCompositionError } from 'Layout/Dialog/DialogCompositionError';
|
|
55
|
+
export { DialogNamingError } from 'Layout/Dialog/DialogNamingError';
|
|
40
56
|
export { Footer } from 'Layout/Footer/Footer';
|
|
41
57
|
export type { FormState } from 'Layout/Form/Form';
|
|
42
58
|
export { Form } from 'Layout/Form/Form';
|
|
59
|
+
export { FormConfigurationError } from 'Layout/Form/FormConfigurationError';
|
|
43
60
|
export { Header } from 'Layout/Header/Header';
|
|
44
61
|
export { Hero } from 'Layout/Hero/Hero';
|
|
45
62
|
export { PageHead } from 'Layout/PageHead/PageHead';
|
|
63
|
+
export { Pane } from 'Layout/Pane/Pane';
|
|
64
|
+
export { Screen } from 'Layout/Screen/Screen';
|
|
46
65
|
export { ScrollContainer } from 'Layout/ScrollContainer/ScrollContainer';
|
|
47
66
|
export { Section } from 'Layout/Section/Section';
|
|
67
|
+
export { Shell } from 'Layout/Shell/Shell';
|
|
48
68
|
export { Sidebar } from 'Layout/Sidebar/Sidebar';
|
|
49
69
|
export type { PaletteTokens } from 'Theme/Palette';
|
|
50
70
|
export { dark, light } from 'Theme/Palette';
|
package/src/styles.css
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/* Component-level styles for the design system. */
|
|
2
|
-
@import "tailwindcss";
|
|
2
|
+
@import "tailwindcss" source(none);
|
|
3
3
|
@import "./tokens.css";
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@source ".";
|
|
5
|
+
/* Only component code contributes utilities to the published stylesheets. */
|
|
6
|
+
@source "./**/*.tsx";
|
|
7
|
+
@source not "./**/*.spec.tsx";
|
|
8
|
+
@source not "./**/*.stories.tsx";
|
package/src/styles.dark.css
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/* Component-level styles for the design system, dark theme only (issue #62). */
|
|
2
|
-
@import "tailwindcss";
|
|
2
|
+
@import "tailwindcss" source(none);
|
|
3
3
|
@import "./tokens.dark.css";
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@source ".";
|
|
5
|
+
/* Only component code contributes utilities to the published stylesheets. */
|
|
6
|
+
@source "./**/*.tsx";
|
|
7
|
+
@source not "./**/*.spec.tsx";
|
|
8
|
+
@source not "./**/*.stories.tsx";
|
package/src/styles.light.css
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/* Component-level styles for the design system, light theme only (issue #62). */
|
|
2
|
-
@import "tailwindcss";
|
|
2
|
+
@import "tailwindcss" source(none);
|
|
3
3
|
@import "./tokens.light.css";
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@source ".";
|
|
5
|
+
/* Only component code contributes utilities to the published stylesheets. */
|
|
6
|
+
@source "./**/*.tsx";
|
|
7
|
+
@source not "./**/*.spec.tsx";
|
|
8
|
+
@source not "./**/*.stories.tsx";
|