@juwel-development/design-system 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/design-system.js +541 -36
- package/dist/index.css +1 -1
- package/dist/types/Display/Brandmark/Brandmark.d.ts +50 -0
- package/dist/types/Display/Checklist/Checklist.d.ts +26 -0
- package/dist/types/Display/DefinitionList/DefinitionList.d.ts +41 -0
- package/dist/types/Display/Figure/Figure.d.ts +52 -0
- package/dist/types/Display/Rail/Rail.d.ts +54 -0
- package/dist/types/Display/Table/Table.d.ts +61 -0
- package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +26 -0
- package/dist/types/Display/Typography/H1/H1.d.ts +27 -0
- package/dist/types/Display/Typography/H2/H2.d.ts +24 -0
- package/dist/types/Display/Typography/H3/H3.d.ts +23 -0
- package/dist/types/Display/Typography/H4/H4.d.ts +22 -0
- package/dist/types/Display/Typography/H5/H5.d.ts +23 -0
- package/dist/types/Display/Typography/H6/H6.d.ts +23 -0
- package/dist/types/Display/Typography/P/P.d.ts +25 -0
- package/dist/types/Display/Typography/Prose/Prose.d.ts +42 -0
- package/dist/types/Interaction/Button/Button.d.ts +7 -3
- package/dist/types/Interaction/Input/Input.d.ts +29 -0
- package/dist/types/Interaction/Link/Link.d.ts +27 -0
- package/dist/types/Interaction/TextArea/TextArea.d.ts +25 -0
- package/dist/types/Layout/Footer/Footer.d.ts +34 -0
- package/dist/types/Layout/Form/Form.d.ts +31 -0
- package/dist/types/Layout/Header/Header.d.ts +41 -0
- package/dist/types/Layout/Hero/Hero.d.ts +46 -0
- package/dist/types/Layout/PageHead/PageHead.d.ts +36 -0
- package/dist/types/Layout/Section/Section.d.ts +38 -0
- package/dist/types/Theme/Palette.d.ts +25 -6
- package/dist/types/index.d.ts +25 -1
- package/package.json +1 -1
- package/src/Display/.gitkeep +0 -0
- package/src/Display/Brandmark/Brandmark.tsx +97 -0
- package/src/Display/Checklist/Checklist.tsx +75 -0
- package/src/Display/DefinitionList/DefinitionList.tsx +90 -0
- package/src/Display/Figure/Figure.tsx +116 -0
- package/src/Display/Rail/Rail.tsx +108 -0
- package/src/Display/Table/Table.tsx +191 -0
- package/src/Display/Typography/Eyebrow/Eyebrow.tsx +46 -0
- package/src/Display/Typography/H1/H1.tsx +46 -0
- package/src/Display/Typography/H2/H2.tsx +43 -0
- package/src/Display/Typography/H3/H3.tsx +41 -0
- package/src/Display/Typography/H4/H4.tsx +40 -0
- package/src/Display/Typography/H5/H5.tsx +41 -0
- package/src/Display/Typography/H6/H6.tsx +41 -0
- package/src/Display/Typography/P/P.tsx +38 -0
- package/src/Display/Typography/Prose/Prose.tsx +91 -0
- package/src/Interaction/Button/Button.tsx +15 -10
- package/src/Interaction/Input/Input.tsx +103 -0
- package/src/Interaction/Link/Link.tsx +70 -0
- package/src/Interaction/TextArea/TextArea.tsx +93 -0
- package/src/Layout/.gitkeep +0 -0
- package/src/Layout/Footer/Footer.tsx +60 -0
- package/src/Layout/Form/Form.tsx +102 -0
- package/src/Layout/Header/Header.tsx +84 -0
- package/src/Layout/Hero/Hero.tsx +73 -0
- package/src/Layout/PageHead/PageHead.tsx +79 -0
- package/src/Layout/Section/Section.tsx +79 -0
- package/src/Theme/Palette.ts +38 -12
- package/src/Theme/renderTokens.ts +179 -2
- package/src/index.ts +25 -1
- package/src/tokens.css +113 -9
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
-
import type { FunctionComponent,
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
3
|
import type { Subject } from 'rxjs';
|
|
4
4
|
declare const button: (props?: ({
|
|
5
5
|
variant?: "primary" | "secondary" | "ghost" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
-
|
|
7
|
+
interface IButtonProps extends VariantProps<typeof button> {
|
|
8
|
+
/** Optional: an icon-only button renders none, and names itself with `ariaLabel` instead. */
|
|
9
|
+
children?: ReactNode;
|
|
8
10
|
onClick$?: Subject<void>;
|
|
9
11
|
disabled?: boolean;
|
|
10
12
|
testId?: string;
|
|
@@ -28,11 +30,13 @@ export interface IButtonProps extends VariantProps<typeof button>, PropsWithChil
|
|
|
28
30
|
* - Provide visual feedback on hover/active states
|
|
29
31
|
* - Ensure sufficient touch target size (minimum 44x44px) for mobile users
|
|
30
32
|
* - Position primary actions on the right for multi-button layouts
|
|
33
|
+
* - A submit button's busy state is a label swap ("Send" to "Sending…"), never a spinner: it costs
|
|
34
|
+
* nothing to render server-side and keeps a Form's `sending` state driver-agnostic
|
|
31
35
|
*
|
|
32
36
|
* @Accessibility
|
|
33
37
|
* - Ensure adequate color contrast (4.5:1 minimum ratio)
|
|
34
38
|
* - Provide focus styles for keyboard navigation
|
|
35
39
|
* - Use appropriate ARIA attributes when needed
|
|
36
40
|
*/
|
|
37
|
-
export declare const Button: FunctionComponent<
|
|
41
|
+
export declare const Button: FunctionComponent<IButtonProps>;
|
|
38
42
|
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { type FunctionComponent } from 'react';
|
|
3
|
+
import type { Subject } from 'rxjs';
|
|
4
|
+
declare const input: (props?: ({
|
|
5
|
+
variant?: "text" | "email" | "url" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
interface IInputProps extends VariantProps<typeof input> {
|
|
8
|
+
/** Always rendered and associated with the control; never replaced by the placeholder. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** How the surrounding form reads the value on submit. */
|
|
11
|
+
name: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
invalid?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
defaultValue?: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
autocomplete?: 'name' | 'email' | 'url' | 'organization' | 'tel' | 'off';
|
|
18
|
+
hint?: string;
|
|
19
|
+
errorMessage?: string;
|
|
20
|
+
onInput$?: Subject<string>;
|
|
21
|
+
testId?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A labelled single-line text control. Its value is uncontrolled - the form reads it by `name` on
|
|
25
|
+
* submit - so it works with JavaScript disabled. Ids are minted internally, so the prop surface
|
|
26
|
+
* stays closed and the label/hint/error associations survive with no hydration.
|
|
27
|
+
*/
|
|
28
|
+
export declare const Input: FunctionComponent<IInputProps>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
declare const link: (props?: ({
|
|
4
|
+
treatment?: "prose" | "quiet" | "label-link" | "graphic" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
interface ILinkProps extends VariantProps<typeof link> {
|
|
7
|
+
/** Where the link points. Carried on the anchor, so the link navigates with JavaScript disabled. */
|
|
8
|
+
href: string;
|
|
9
|
+
/** The link text. */
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
/** Opens in a new tab and severs the opener together - `target="_blank"` implies the `rel`, so
|
|
12
|
+
* neither half is settable alone. */
|
|
13
|
+
external?: boolean;
|
|
14
|
+
/** Marks this link as the current page for assistive technology (`aria-current="page"`). Semantics
|
|
15
|
+
* only: any visual current-page treatment belongs to the Header, not here. */
|
|
16
|
+
current?: boolean;
|
|
17
|
+
testId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A link in one of four treatments. `prose` for running text (told apart by its underline, never by
|
|
21
|
+
* hue), `quiet` for standing navigation (muted, and foreground with an underline on hover),
|
|
22
|
+
* `label-link` for a link acting as a label (inherits its colour, underlines on hover, sets no type
|
|
23
|
+
* of its own), and `graphic` for an anchor whose child is not text (paints nothing, so a mark keeps
|
|
24
|
+
* its own colour). It renders a plain `<a>`, so it works with no hydration.
|
|
25
|
+
*/
|
|
26
|
+
export declare const Link: FunctionComponent<ILinkProps>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'react';
|
|
2
|
+
import type { Subject } from 'rxjs';
|
|
3
|
+
interface ITextAreaProps {
|
|
4
|
+
/** Always rendered and associated with the control; never replaced by the placeholder. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** How the surrounding form reads the value on submit. */
|
|
7
|
+
name: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
invalid?: boolean;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
defaultValue?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
autocomplete?: 'name' | 'email' | 'url' | 'organization' | 'tel' | 'off';
|
|
14
|
+
hint?: string;
|
|
15
|
+
errorMessage?: string;
|
|
16
|
+
onInput$?: Subject<string>;
|
|
17
|
+
testId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A labelled multi-line text control. Its value is uncontrolled - the form reads it by `name` on
|
|
21
|
+
* submit - so it works with JavaScript disabled. Ids are minted internally, so the prop surface
|
|
22
|
+
* stays closed and the label/hint/error associations survive with no hydration.
|
|
23
|
+
*/
|
|
24
|
+
export declare const TextArea: FunctionComponent<ITextAreaProps>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
declare const footer: (props?: ({
|
|
4
|
+
edge?: "none" | "rule" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export interface IFooterProps extends VariantProps<typeof footer> {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
testId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The shell's bottom edge: a `<footer>` that owns the `contentinfo` landmark, the small type, the shell's
|
|
12
|
+
* vertical air and the gutter, and its own top rule - and nothing about what is inside it. It is a frame,
|
|
13
|
+
* not an arrangement: `children` are placed directly in the footer with no wrapper and no `<nav>`.
|
|
14
|
+
*
|
|
15
|
+
* @Guarantees — enforced on every render
|
|
16
|
+
* - The footer is set at the `small` role and carries no size of its own.
|
|
17
|
+
* - Children are placed unmodified, directly inside the `<footer>`, with no wrapping element.
|
|
18
|
+
* - The top edge is the caller's choice - `edge="rule"` (the default) or `edge="none"` - and when drawn is
|
|
19
|
+
* always the page's single hairline weight, in the `rule` colour Section's join uses.
|
|
20
|
+
* - It is never sticky or fixed and needs no JavaScript.
|
|
21
|
+
*
|
|
22
|
+
* @CallerMustEnsure
|
|
23
|
+
* - The footer is a direct child of the page, not nested inside `main`, `article`, `aside`, `nav` or a
|
|
24
|
+
* `Section`. Nesting silently demotes it out of the `contentinfo` landmark, and the component cannot
|
|
25
|
+
* detect this.
|
|
26
|
+
*
|
|
27
|
+
* @UXGuidelines
|
|
28
|
+
* - Supply your own `<nav aria-label="…">` inside the slot if the footer navigates - a footer's contents
|
|
29
|
+
* are frequently not navigation, so this component declares no nav landmark over them. Where the footer
|
|
30
|
+
* does navigate, name `Header`'s nav with `navName` too: two unnamed navigation landmarks are
|
|
31
|
+
* indistinguishable to a screen-reader user.
|
|
32
|
+
*/
|
|
33
|
+
export declare const Footer: FunctionComponent<IFooterProps>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
2
|
+
/** Which of the four states the page has put the form in. Owned by the page, never by Form: the
|
|
3
|
+
* component renders the state it is given and never decides which one it is in. */
|
|
4
|
+
export type FormState = 'idle' | 'sending' | 'sent' | 'failed';
|
|
5
|
+
interface IFormProps {
|
|
6
|
+
/** Where the submission goes. Passed straight to the form element; Form makes no decision about it. */
|
|
7
|
+
action: string;
|
|
8
|
+
method?: 'get' | 'post';
|
|
9
|
+
state?: FormState;
|
|
10
|
+
/** The fields. */
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
/** The actions row - the consumer supplies its own submit Button. */
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
/** Standing note in `idle`/`sending`; the outcome message in `sent`/`failed`. */
|
|
15
|
+
note?: string;
|
|
16
|
+
testId?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The container around a set of fields: fields, an actions row, a note. It renders a real `<form>`
|
|
20
|
+
* carrying `action` and `method`, so a native submission works with no JavaScript, and it renders
|
|
21
|
+
* the `state` it is given without ever owning a submission, a transport or validation timing - both
|
|
22
|
+
* a server-rendered driver and a hydrated island express every state through the same props.
|
|
23
|
+
*
|
|
24
|
+
* `sent` drops the fields and actions so a completed submission cannot be resubmitted, while the
|
|
25
|
+
* `<form>` itself is retained in every state so the DOM shape is stable across a runtime change.
|
|
26
|
+
*
|
|
27
|
+
* The note's text is always the consumer's; Form chooses only its element and ARIA role. `sent` is
|
|
28
|
+
* the outcome message, so a `sent` with no `note` is a programmer error and throws.
|
|
29
|
+
*/
|
|
30
|
+
export declare const Form: FunctionComponent<IFormProps>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
declare const header: (props?: ({
|
|
4
|
+
edge?: "none" | "rule" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export interface IHeaderProps extends VariantProps<typeof header> {
|
|
7
|
+
/** The standing link: a place name, a mark, or a home link. The consumer supplies the whole anchor -
|
|
8
|
+
* Header renders no link of its own. A place name uses `<Link treatment="quiet" href="/">…</Link>`; a
|
|
9
|
+
* mark uses `<Link treatment="graphic" href="/"><Brandmark …/></Link>`, whose `graphic` treatment
|
|
10
|
+
* paints nothing over a `currentColor` mark. */
|
|
11
|
+
standing?: ReactNode;
|
|
12
|
+
/** Names the nav for assistive technology. Omit unless the page has more than one nav. */
|
|
13
|
+
navName?: string;
|
|
14
|
+
/** The nav links. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
testId?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The shell's top edge: a standing link and a nav, at the label type role. It renders the banner
|
|
20
|
+
* landmark and a single `<nav>`, arranging nothing beyond the two slots, so it works with no hydration.
|
|
21
|
+
*
|
|
22
|
+
* @Guarantees — enforced on every render
|
|
23
|
+
* - The header is set at the label role and carries no size of its own: `font-secondary text-label
|
|
24
|
+
* tracking-label text-muted`, so it never grows past that role whatever the page font size.
|
|
25
|
+
* - A nav item marked `aria-current="page"` renders at `foreground` whatever supplied it - the treatment
|
|
26
|
+
* keys on the attribute, not on `Link`, so it holds against a bare anchor or any component.
|
|
27
|
+
* - The shell's air is one value above, below and between: `--space-region` vertically and between nav
|
|
28
|
+
* items, `--gutter` across, so the bar aligns with every inset `Section`.
|
|
29
|
+
* - It is never sticky and needs no JavaScript: there is no `sticky` variant and nothing to hydrate.
|
|
30
|
+
* - Omitting `navName` emits no `aria-label` at all, not an empty one.
|
|
31
|
+
*
|
|
32
|
+
* @UXGuidelines
|
|
33
|
+
* - Name the nav with `navName` once the page has more than one navigation landmark - a footer nav will
|
|
34
|
+
* be the second, and two unnamed navs are indistinguishable to a screen-reader user.
|
|
35
|
+
* - Nav links use `Link`'s `quiet` treatment. The current-page treatment is applied here from
|
|
36
|
+
* `aria-current`, so set `current` on the `Link` and style nothing yourself.
|
|
37
|
+
* - The nav does not collapse into a menu: a disclosure needs JavaScript, so on a narrow viewport the
|
|
38
|
+
* items wrap. A mobile menu is out of scope, not a follow-up.
|
|
39
|
+
*/
|
|
40
|
+
export declare const Header: FunctionComponent<IHeaderProps>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
declare const hero: (props?: ({
|
|
4
|
+
place?: "center" | "start" | "between" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export interface IHeroProps extends VariantProps<typeof hero> {
|
|
7
|
+
/** The fold's one opaque slot. Rendered unmodified: a poster composes a mark, an `H1` lead and a foot
|
|
8
|
+
* here; another brand composes something else. `Hero` imposes no anatomy. */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
testId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The first screen's frame: a plain container that holds at least the fold height and places one opaque
|
|
14
|
+
* slot within it. It renders no heading and no landmark - a hero's lead is an `<H1>` the consumer places
|
|
15
|
+
* in the slot, and the `Section` around it owns the region, the bleed, the band and the gutter. A hero
|
|
16
|
+
* varies in its structure where a footer varies only in its contents, so there is no shared anatomy to
|
|
17
|
+
* model and the opaque slot is the honest answer.
|
|
18
|
+
*
|
|
19
|
+
* @Guarantees — enforced on every render
|
|
20
|
+
* - It is at least `--fold-height` tall and never taller by construction: a `min-height` floor, not a
|
|
21
|
+
* fixed height, so content longer than the fold grows the frame rather than overflowing it.
|
|
22
|
+
* - `children` render unmodified; the component adds nothing to and strips nothing from them, and sets no
|
|
23
|
+
* margin, max-width, colour or heading of its own.
|
|
24
|
+
* - It owns no bleed, band or gutter and emits no `<section>` and no landmark role - the enclosing
|
|
25
|
+
* `Section` carries those.
|
|
26
|
+
* - `place` positions the slot in the leftover space: `center` (the default), `start` or `between`; the
|
|
27
|
+
* parts are gapped with `--space-stack`.
|
|
28
|
+
* - It is never sticky or fixed and needs no JavaScript, so it renders identically server-side.
|
|
29
|
+
*
|
|
30
|
+
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
31
|
+
* - The hero sits inside a `Section` - `<Section bleed="full"><Hero>…</Hero></Section>` - which owns the
|
|
32
|
+
* bleed, the band, the gutter and the region. `Hero` imports no component and adds none of these.
|
|
33
|
+
*
|
|
34
|
+
* @UXGuidelines
|
|
35
|
+
* - Cap the lead at `--measure-display`, not at a caption's width, and apply it to your own lead - not
|
|
36
|
+
* here, since capping the slot would cap the mark too. Measured on the lockup that prompted this: at
|
|
37
|
+
* 24ch a one-sentence lead set five lines and became a second block competing with the mark; at 36ch it
|
|
38
|
+
* sets three and reads as the mark's caption. This is the one place where copy length is a layout
|
|
39
|
+
* parameter - a hero whose lead sentence can vary in length needs its measure chosen against the real
|
|
40
|
+
* sentence, not a rule of thumb.
|
|
41
|
+
* - A hero built on a wide lockup is a desktop bet by construction. A 15-character lockup can only be as
|
|
42
|
+
* wide as the phone, so at 390px the scale contrast between mark and lead is capped by the viewport
|
|
43
|
+
* rather than chosen - roughly 2:1, against roughly 6:1 at 1440.
|
|
44
|
+
*/
|
|
45
|
+
export declare const Hero: FunctionComponent<IHeroProps>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
2
|
+
export interface IPageHeadProps {
|
|
3
|
+
/** The page title - the head's one scale event, rendered as the page's `h1` at the title role. */
|
|
4
|
+
title: ReactNode;
|
|
5
|
+
/** The standfirst under the title: a lede-role paragraph at a measure slightly wider than the
|
|
6
|
+
* reading column. Omit for a bare title. */
|
|
7
|
+
lede?: ReactNode;
|
|
8
|
+
/** The muted small-print paragraph under the lede, held to the reading measure. Omit where the head
|
|
9
|
+
* is title and lede only. */
|
|
10
|
+
intro?: ReactNode;
|
|
11
|
+
testId?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A subpage's opening: one scale event, then small print. It renders a `<header>` carrying the page's
|
|
15
|
+
* `h1`, an optional lede standfirst and an optional muted intro, full-bleed over the vertical band and
|
|
16
|
+
* the gutter. It arranges the three slots and owns their type roles; the consumer supplies the copy.
|
|
17
|
+
*
|
|
18
|
+
* @Guarantees — enforced on every render
|
|
19
|
+
* - The title is an `h1` at the `title` role - one rung below the hero's `display` - so it can never
|
|
20
|
+
* out-scale the homepage hero: the cap is the `display > title` token relationship (ADR 0004/0005),
|
|
21
|
+
* not two `clamp()`s ordered by luck, and no size literal is set here.
|
|
22
|
+
* - The head is full-bleed: it carries `--space-band` and `--gutter` but takes no `max-width` and joins
|
|
23
|
+
* no grid, so it opts out of the index column and spans the full content width.
|
|
24
|
+
* - The lede is the `lede` role at `--measure-wide`, one measure wider than the reading column; the
|
|
25
|
+
* intro is the `small` role, muted, at `--measure`. Each measure is on the text, never on the head.
|
|
26
|
+
* - Omitting `lede` or `intro` renders that paragraph not at all.
|
|
27
|
+
* - It is never sticky or fixed and needs no JavaScript.
|
|
28
|
+
*
|
|
29
|
+
* @CallerMustEnsure — the component cannot see these and does not check them
|
|
30
|
+
* - The head sits inside the page's main content (a `main`, `article` or `section`), not as a top-level
|
|
31
|
+
* child of `body`, so its `<header>` is the page head and not a second `banner` beside the shell's
|
|
32
|
+
* `Header` (#14).
|
|
33
|
+
* - This head opens a subpage. The homepage hero is `PosterFold` (#17), which renders at the `display`
|
|
34
|
+
* role; reach for that where the page is the poster, not for this.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PageHead: FunctionComponent<IPageHeadProps>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
declare const section: (props?: ({
|
|
4
|
+
bleed?: "full" | "inset" | null | undefined;
|
|
5
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
6
|
+
export interface ISectionProps extends VariantProps<typeof section> {
|
|
7
|
+
/** Names the section as a region a screen-reader user can jump to. Omit for an ordinary section: an
|
|
8
|
+
* unnamed section is inert to assistive technology, which is the right default. Two or three named
|
|
9
|
+
* regions on a page is navigation; six is landmark noise, which is why this is opt-in. */
|
|
10
|
+
name?: string;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
testId?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The page's structural unit: a band carrying its own vertical air and, unless it bleeds, the gutter.
|
|
16
|
+
* It arranges nothing inside itself - what it owns is the join to the section before it, which is why
|
|
17
|
+
* it is a composable of the page rather than a container of its contents.
|
|
18
|
+
*
|
|
19
|
+
* @Guarantees — enforced on every render
|
|
20
|
+
* - Sections abut and never gap: the component sets no margin, and the join is the separation. A rule
|
|
21
|
+
* appears only between two sections - drawn when the preceding sibling also carries data-section, so
|
|
22
|
+
* never above the first section nor beneath a header before it.
|
|
23
|
+
* - A `bleed="full"` section drops the gutter and runs edge to edge; the vertical band is kept on both
|
|
24
|
+
* bleed variants, and the join spans the full width of either.
|
|
25
|
+
* - An unnamed section is not a landmark: it emits no `aria-label` and is inert to assistive technology.
|
|
26
|
+
*
|
|
27
|
+
* @CallerMustEnsure
|
|
28
|
+
* - Where `name` is given it matches the section's visible heading. The component labels the region with
|
|
29
|
+
* that string because it cannot reach the heading's id to reference it with `aria-labelledby` instead.
|
|
30
|
+
*
|
|
31
|
+
* @UXGuidelines
|
|
32
|
+
* - On a page with no borrowed proof - no logos, no testimonials, no credits - a gap between blocks reads
|
|
33
|
+
* as missing content, while air inside the type reads as care. So the levers on a page's rhythm are the
|
|
34
|
+
* measure, the leading and the band, never a space between sections. This is why Section offers no gap
|
|
35
|
+
* and no margin.
|
|
36
|
+
*/
|
|
37
|
+
export declare const Section: FunctionComponent<ISectionProps>;
|
|
38
|
+
export {};
|
|
@@ -23,25 +23,44 @@ export type PaletteTokens = {
|
|
|
23
23
|
muted: string;
|
|
24
24
|
/** Hairlines and dividers. */
|
|
25
25
|
border: string;
|
|
26
|
+
/** The boundary of a control with no fill of its own, so it is the only thing separating the
|
|
27
|
+
* control from the surface. Constraint (WCAG 2.2 SC 1.4.11): at least 3:1 against `surface`
|
|
28
|
+
* in the same theme. */
|
|
29
|
+
controlBorder: string;
|
|
30
|
+
/** The structural rule that marks where a block's structure is, weightier than `border`'s hairline
|
|
31
|
+
* dividers and lighter than `controlBorder`'s control edge. It serves three consumers: a table's
|
|
32
|
+
* heavier top line above the first row, a checklist's tick, and a page section's join to the
|
|
33
|
+
* section before it. A mid-neutral, so it structures without boxing. Constraint: at least 3:1
|
|
34
|
+
* against `surface` in the same theme. */
|
|
35
|
+
rule: string;
|
|
36
|
+
/** The plate behind content that has not painted - an image still loading, or one that failed and
|
|
37
|
+
* is showing its alt text. Constraint: at least 4.5:1 against `foreground` in the same theme,
|
|
38
|
+
* because a failed image renders its alt text on this plate and that text must stay legible. That
|
|
39
|
+
* keeps it near `surface` rather than a mid grey. */
|
|
40
|
+
backing: string;
|
|
26
41
|
/** The main call-to-action fill. */
|
|
27
42
|
primary: string;
|
|
28
43
|
primaryHover: string;
|
|
29
44
|
/** Text and icons drawn on top of `primary`. */
|
|
30
45
|
primaryForeground: string;
|
|
31
|
-
/** Focus ring for primary surfaces - lighter than the fill so it reads against it. */
|
|
32
|
-
primaryRing: string;
|
|
33
46
|
/** The alternative action fill, for choices that sit beside a primary one. */
|
|
34
47
|
secondary: string;
|
|
35
48
|
secondaryHover: string;
|
|
36
49
|
secondaryForeground: string;
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
/** The tone a control takes when it cannot be interacted with - its fill, or its border when
|
|
51
|
+
* the fill is transparent. */
|
|
39
52
|
disabled: string;
|
|
40
53
|
/** Kept distinct from `disabled` so a disabled control still absorbs hover rather than
|
|
41
54
|
* appearing to respond to it. */
|
|
42
55
|
disabledHover: string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
56
|
+
/** The one focus ring, drawn by every focusable primitive regardless of variant - a focus ring
|
|
57
|
+
* states keyboard position, not the control's importance. Constraint (WCAG 2.2 SC 1.4.11): at
|
|
58
|
+
* least 3:1 against `surface` in the same theme. */
|
|
59
|
+
focusRing: string;
|
|
60
|
+
/** The colour of a prose link. A link is told apart by its underline, never by hue, so this
|
|
61
|
+
* carries the text threshold, not the 3:1 the ring takes. Constraint (WCAG 2.2 SC 1.4.3): at
|
|
62
|
+
* least 4.5:1 against `surface` in the same theme. */
|
|
63
|
+
link: string;
|
|
45
64
|
/** Status colours. Not yet consumed by a component - they complete the role set so the
|
|
46
65
|
* first Alert or Toast has names to reach for instead of inventing them. */
|
|
47
66
|
success: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import './styles.css';
|
|
2
|
-
export
|
|
2
|
+
export { Brandmark } from 'Display/Brandmark/Brandmark';
|
|
3
|
+
export { Checklist } from 'Display/Checklist/Checklist';
|
|
4
|
+
export { DefinitionList } from 'Display/DefinitionList/DefinitionList';
|
|
5
|
+
export { Figure } from 'Display/Figure/Figure';
|
|
6
|
+
export { Rail } from 'Display/Rail/Rail';
|
|
7
|
+
export { Table } from 'Display/Table/Table';
|
|
8
|
+
export { Eyebrow } from 'Display/Typography/Eyebrow/Eyebrow';
|
|
9
|
+
export { H1 } from 'Display/Typography/H1/H1';
|
|
10
|
+
export { H2 } from 'Display/Typography/H2/H2';
|
|
11
|
+
export { H3 } from 'Display/Typography/H3/H3';
|
|
12
|
+
export { H4 } from 'Display/Typography/H4/H4';
|
|
13
|
+
export { H5 } from 'Display/Typography/H5/H5';
|
|
14
|
+
export { H6 } from 'Display/Typography/H6/H6';
|
|
15
|
+
export { P } from 'Display/Typography/P/P';
|
|
16
|
+
export { Prose } from 'Display/Typography/Prose/Prose';
|
|
3
17
|
export { Button } from 'Interaction/Button/Button';
|
|
18
|
+
export { Input } from 'Interaction/Input/Input';
|
|
19
|
+
export { Link } from 'Interaction/Link/Link';
|
|
20
|
+
export { TextArea } from 'Interaction/TextArea/TextArea';
|
|
21
|
+
export { Footer } from 'Layout/Footer/Footer';
|
|
22
|
+
export type { FormState } from 'Layout/Form/Form';
|
|
23
|
+
export { Form } from 'Layout/Form/Form';
|
|
24
|
+
export { Header } from 'Layout/Header/Header';
|
|
25
|
+
export { Hero } from 'Layout/Hero/Hero';
|
|
26
|
+
export { PageHead } from 'Layout/PageHead/PageHead';
|
|
27
|
+
export { Section } from 'Layout/Section/Section';
|
|
4
28
|
export type { PaletteTokens } from 'Theme/Palette';
|
|
5
29
|
export { dark, light } from 'Theme/Palette';
|
package/package.json
CHANGED
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
// The whole recipe is one class: `inline-flex`. An SVG inside a plain inline span sits on the text
|
|
6
|
+
// baseline and reserves descender space beneath it - a silent few-pixel misalignment nobody sees
|
|
7
|
+
// until a mark is measured against a nav's right edge. `inline-flex` removes it. The component paints
|
|
8
|
+
// nothing else: it sets no colour and no dimension, so there is no `dark:` class and no token here.
|
|
9
|
+
//
|
|
10
|
+
// `cut` selects only the `data-cut` attribute - each option carries no class of its own, the way
|
|
11
|
+
// `Input`'s `variant` selects only the control's `type`. Its real job is being required: a caller
|
|
12
|
+
// cannot place a mark without stating which cut it is, so "has the reduction row been done?" is asked
|
|
13
|
+
// at every call site. There is deliberately no default variant.
|
|
14
|
+
const brandmark = cva('inline-flex', {
|
|
15
|
+
variants: {
|
|
16
|
+
cut: { full: '', compact: '' },
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export interface IBrandmarkProps extends VariantProps<typeof brandmark> {
|
|
21
|
+
/**
|
|
22
|
+
* What the mark says. Required so the decision is forced at the call site. Empty string is a
|
|
23
|
+
* legitimate value - a mark beside the product name already set as text - and is not rejected.
|
|
24
|
+
*/
|
|
25
|
+
name: string;
|
|
26
|
+
/** The consumer's inline SVG. */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
testId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Names a consumer-supplied inline mark and makes its cut explicit. It hosts no asset: the SVG is the
|
|
33
|
+
* consumer's, passed as `children` and rendered unmodified. A brand mark is text that has stopped
|
|
34
|
+
* being text - drawn as outlines, it carries no accessible name of its own - so naming it is the job,
|
|
35
|
+
* and the one thing a consumer most reliably gets wrong.
|
|
36
|
+
*
|
|
37
|
+
* `role="img"` makes the element a leaf in the accessibility tree, so nothing inside the SVG - a stray
|
|
38
|
+
* `<title>`, `<text>` or `<desc>` - contributes, and the mark is announced exactly once.
|
|
39
|
+
*
|
|
40
|
+
* @Guarantees — enforced on every render
|
|
41
|
+
* - A named mark (`name` non-empty) renders `role="img"` and `aria-label={name}`, announced exactly
|
|
42
|
+
* once and never through its SVG's own contents.
|
|
43
|
+
* - A decorative mark (`name=""`) renders `aria-hidden="true"` with no `role` and no `aria-label`, so
|
|
44
|
+
* it leaves the accessibility tree entirely - an unnamed `role="img"` would be worse than absent.
|
|
45
|
+
* - The cut in use is always stated and always readable from the DOM, via required `cut` -> `data-cut`.
|
|
46
|
+
* - `children` renders unmodified; the component adds nothing to and strips nothing from the SVG, and
|
|
47
|
+
* sets no colour, dimension, margin or link of its own.
|
|
48
|
+
*
|
|
49
|
+
* @UXGuidelines
|
|
50
|
+
* - One mark is not one asset - do the reduction row before you accept a mark. Measured on the lockup
|
|
51
|
+
* that prompted this: a 15-character full lockup is illegible at 32px, while its accent dot survives
|
|
52
|
+
* to 17px. A single SVG used at every size is the default and it is usually wrong. Expect at least
|
|
53
|
+
* two cuts out of any mark, which is why `cut` is required. There is no width-based swapping and no
|
|
54
|
+
* threshold token: a threshold is one wordmark's measurement, and keeping both cuts in the DOM to
|
|
55
|
+
* swap in CSS doubles the payload of the element inlined to keep LCP off the network. The component
|
|
56
|
+
* renders whichever single cut it is given; which slot it is in is known by whoever places it.
|
|
57
|
+
* - The sanctioned accent, and how narrow it is. A fill-only colour that fails 3:1 as text may sit
|
|
58
|
+
* inside a letterform only where it carries no information and the glyph reads without it - a lifted
|
|
59
|
+
* `i`-dot qualifies because the `i` keeps its stem. The library sets no fill inside a consumer's
|
|
60
|
+
* mark and cannot check this; it is guidance to whoever draws the mark, not a token constraint.
|
|
61
|
+
* - Inline the SVG rather than requesting it where the mark is the largest object on the first screen
|
|
62
|
+
* and therefore the LCP element - inlining means LCP depends on no font and no network round-trip.
|
|
63
|
+
*/
|
|
64
|
+
export const Brandmark: FunctionComponent<IBrandmarkProps> = ({
|
|
65
|
+
name,
|
|
66
|
+
cut,
|
|
67
|
+
children,
|
|
68
|
+
testId,
|
|
69
|
+
}) => {
|
|
70
|
+
// The two paths differ in markup, not just in an attribute, so they are two branches: an empty
|
|
71
|
+
// `name` must leave the accessibility tree, and a `span` carrying `aria-label` with no `role` is
|
|
72
|
+
// both wrong and rejected by the a11y lint. `role="img"` is what makes `aria-label` valid here.
|
|
73
|
+
if (name === '') {
|
|
74
|
+
return (
|
|
75
|
+
<span
|
|
76
|
+
aria-hidden={'true'}
|
|
77
|
+
data-cut={cut}
|
|
78
|
+
className={brandmark({ cut })}
|
|
79
|
+
data-testid={testId}
|
|
80
|
+
>
|
|
81
|
+
{children}
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<span
|
|
88
|
+
role={'img'}
|
|
89
|
+
aria-label={name}
|
|
90
|
+
data-cut={cut}
|
|
91
|
+
className={brandmark({ cut })}
|
|
92
|
+
data-testid={testId}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</span>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { cva } from 'class-variance-authority';
|
|
2
|
+
import type { FunctionComponent, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
// The marker is a rule, not a glyph - and this is worth writing down because it outlives this one
|
|
5
|
+
// component and there is no ADR carrying it. A checkbox character, dingbat or icon is an *invented
|
|
6
|
+
// graphic*, and in a design language built from 1px rules and type an invented graphic is the thing
|
|
7
|
+
// that reads as borrowed. So the tick is the structural hairline already on the page, doing one more
|
|
8
|
+
// job - and it is unconditional: no prop removes it, reshapes it or swaps it. A future primitive
|
|
9
|
+
// needing a marker for some other job has nowhere else to find this stance; it is here on purpose.
|
|
10
|
+
|
|
11
|
+
// The ul: an unstyled list. `list-none` strips the browser marker (so no bullet is ever drawn) and
|
|
12
|
+
// resets its default indent; `role="list"` is restored in the markup because `list-style: none`
|
|
13
|
+
// silently drops list semantics in Safari/VoiceOver. Colours are semantic tokens re-pointed by
|
|
14
|
+
// `.dark`, so no selector carries a `dark:` class.
|
|
15
|
+
const checklistRoot = cva('m-0 list-none p-0');
|
|
16
|
+
|
|
17
|
+
// The li: body copy in `foreground` - items are content, not annotation, so they are not muted. The
|
|
18
|
+
// tick is drawn as a `::before` pseudo-element, the only route to a rule: CSS `::marker` styles just
|
|
19
|
+
// colour, font and content, so it cannot draw a line, and a `✓` in the content would be announced on
|
|
20
|
+
// every single item. The li is a flex row so the tick sits in a left gutter with the text hanging
|
|
21
|
+
// indented beside it; `before:mt-[0.7em]` drops the rule to the optical middle of the first line. Its
|
|
22
|
+
// colour is the `rule` role - a table's top rule and a list's tick are one job, a 1px mark at the
|
|
23
|
+
// weight where structure becomes visible - its length and thickness the two named tick tokens, no
|
|
24
|
+
// literal here. Hairlines sit *between* items in the `border` colour, with no rule above the first or
|
|
25
|
+
// below the last, so the list is open at both ends - deliberately unlike a closed list, which closes
|
|
26
|
+
// at the foot. The between-item gap reads `--space-stack`, split above and below the hairline so it
|
|
27
|
+
// sits centred in the gap.
|
|
28
|
+
const checklistItem = cva(
|
|
29
|
+
[
|
|
30
|
+
'flex items-start gap-3 font-primary text-body leading-body text-foreground',
|
|
31
|
+
"before:mt-[0.7em] before:h-[var(--tick-thickness)] before:w-[var(--tick-length)] before:shrink-0 before:bg-[var(--color-rule)] before:content-['']",
|
|
32
|
+
'[&:not(:first-child)]:mt-[var(--space-stack)] [&:not(:first-child)]:border-border [&:not(:first-child)]:border-t [&:not(:first-child)]:border-solid [&:not(:first-child)]:pt-[var(--space-stack)]',
|
|
33
|
+
].join(' '),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
interface IChecklistRootProps {
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
testId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IChecklistItemProps {
|
|
42
|
+
children?: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ChecklistRoot: FunctionComponent<IChecklistRootProps> = ({
|
|
46
|
+
children,
|
|
47
|
+
testId,
|
|
48
|
+
}) => (
|
|
49
|
+
// biome-ignore lint/a11y/noRedundantRoles: list-style:none silently strips the ul's list semantics in Safari/VoiceOver; the explicit role restores them (issue #13)
|
|
50
|
+
<ul className={checklistRoot()} role={'list'} data-testid={testId}>
|
|
51
|
+
{children}
|
|
52
|
+
</ul>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const ChecklistItem: FunctionComponent<IChecklistItemProps> = ({
|
|
56
|
+
children,
|
|
57
|
+
}) => <li className={checklistItem()}>{children}</li>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A list of things to confirm or prepare, composed from its two members. Each item is hairline-separated
|
|
61
|
+
* and marked by a short rule sitting in the left indent - never a checkbox, bullet, dingbat or icon.
|
|
62
|
+
* This is a reading device, not a form control: it renders no interactive checkbox and owns no state.
|
|
63
|
+
*
|
|
64
|
+
* @Guarantees — enforced on every render
|
|
65
|
+
* - `Root` renders a `ul` carrying `role="list"`; `Item` renders an `li`. Works with JavaScript off.
|
|
66
|
+
* - The marker is a `::before` rule reading `--color-rule`, `--tick-length` and `--tick-thickness` - no
|
|
67
|
+
* glyph, character, dingbat, icon or `::marker` content, and nothing is announced on each item.
|
|
68
|
+
* - Items render at the body type role in `foreground`; they are content, not annotation.
|
|
69
|
+
* - Hairlines in `border` separate items, with no rule above the first or below the last, so the list
|
|
70
|
+
* is open at both ends. The between-item gap reads `--space-stack`.
|
|
71
|
+
*/
|
|
72
|
+
export const Checklist = {
|
|
73
|
+
Root: ChecklistRoot,
|
|
74
|
+
Item: ChecklistItem,
|
|
75
|
+
} as const;
|