@juwel-development/design-system 1.0.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.
Files changed (61) hide show
  1. package/dist/design-system.js +541 -36
  2. package/dist/index.css +1 -1
  3. package/dist/types/Display/Brandmark/Brandmark.d.ts +50 -0
  4. package/dist/types/Display/Checklist/Checklist.d.ts +26 -0
  5. package/dist/types/Display/DefinitionList/DefinitionList.d.ts +41 -0
  6. package/dist/types/Display/Figure/Figure.d.ts +52 -0
  7. package/dist/types/Display/Rail/Rail.d.ts +54 -0
  8. package/dist/types/Display/Table/Table.d.ts +61 -0
  9. package/dist/types/Display/Typography/Eyebrow/Eyebrow.d.ts +26 -0
  10. package/dist/types/Display/Typography/H1/H1.d.ts +27 -0
  11. package/dist/types/Display/Typography/H2/H2.d.ts +24 -0
  12. package/dist/types/Display/Typography/H3/H3.d.ts +23 -0
  13. package/dist/types/Display/Typography/H4/H4.d.ts +22 -0
  14. package/dist/types/Display/Typography/H5/H5.d.ts +23 -0
  15. package/dist/types/Display/Typography/H6/H6.d.ts +23 -0
  16. package/dist/types/Display/Typography/P/P.d.ts +25 -0
  17. package/dist/types/Display/Typography/Prose/Prose.d.ts +42 -0
  18. package/dist/types/Interaction/Button/Button.d.ts +7 -3
  19. package/dist/types/Interaction/Input/Input.d.ts +29 -0
  20. package/dist/types/Interaction/Link/Link.d.ts +27 -0
  21. package/dist/types/Interaction/TextArea/TextArea.d.ts +25 -0
  22. package/dist/types/Layout/Footer/Footer.d.ts +34 -0
  23. package/dist/types/Layout/Form/Form.d.ts +31 -0
  24. package/dist/types/Layout/Header/Header.d.ts +41 -0
  25. package/dist/types/Layout/Hero/Hero.d.ts +46 -0
  26. package/dist/types/Layout/PageHead/PageHead.d.ts +36 -0
  27. package/dist/types/Layout/Section/Section.d.ts +38 -0
  28. package/dist/types/Theme/Palette.d.ts +25 -6
  29. package/dist/types/index.d.ts +25 -1
  30. package/package.json +1 -1
  31. package/src/Display/.gitkeep +0 -0
  32. package/src/Display/Brandmark/Brandmark.tsx +97 -0
  33. package/src/Display/Checklist/Checklist.tsx +75 -0
  34. package/src/Display/DefinitionList/DefinitionList.tsx +90 -0
  35. package/src/Display/Figure/Figure.tsx +116 -0
  36. package/src/Display/Rail/Rail.tsx +108 -0
  37. package/src/Display/Table/Table.tsx +191 -0
  38. package/src/Display/Typography/Eyebrow/Eyebrow.tsx +46 -0
  39. package/src/Display/Typography/H1/H1.tsx +46 -0
  40. package/src/Display/Typography/H2/H2.tsx +43 -0
  41. package/src/Display/Typography/H3/H3.tsx +41 -0
  42. package/src/Display/Typography/H4/H4.tsx +40 -0
  43. package/src/Display/Typography/H5/H5.tsx +41 -0
  44. package/src/Display/Typography/H6/H6.tsx +41 -0
  45. package/src/Display/Typography/P/P.tsx +38 -0
  46. package/src/Display/Typography/Prose/Prose.tsx +91 -0
  47. package/src/Interaction/Button/Button.tsx +16 -9
  48. package/src/Interaction/Input/Input.tsx +103 -0
  49. package/src/Interaction/Link/Link.tsx +70 -0
  50. package/src/Interaction/TextArea/TextArea.tsx +93 -0
  51. package/src/Layout/.gitkeep +0 -0
  52. package/src/Layout/Footer/Footer.tsx +60 -0
  53. package/src/Layout/Form/Form.tsx +102 -0
  54. package/src/Layout/Header/Header.tsx +84 -0
  55. package/src/Layout/Hero/Hero.tsx +73 -0
  56. package/src/Layout/PageHead/PageHead.tsx +79 -0
  57. package/src/Layout/Section/Section.tsx +79 -0
  58. package/src/Theme/Palette.ts +38 -12
  59. package/src/Theme/renderTokens.ts +198 -2
  60. package/src/index.ts +25 -1
  61. package/src/tokens.css +126 -9
@@ -1,10 +1,12 @@
1
1
  import type { VariantProps } from 'class-variance-authority';
2
- import type { FunctionComponent, PropsWithChildren } from 'react';
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
- export interface IButtonProps extends VariantProps<typeof button>, PropsWithChildren {
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<PropsWithChildren<IButtonProps>>;
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
- secondaryRing: string;
38
- /** Fill for controls that cannot be interacted with. */
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
- /** Focus ring for surfaces that have no fill of their own, such as a ghost button. */
44
- ring: string;
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;
@@ -1,5 +1,29 @@
1
1
  import './styles.css';
2
- export type { IButtonProps } from 'Interaction/Button/Button';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juwel-development/design-system",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Shared design system: tokens and components.",
6
6
  "license": "MIT",
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;