@jasonsdevspace/design-kit 0.1.0-next.1 → 0.1.0-next.2

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/CHANGELOG.md CHANGED
@@ -7,6 +7,59 @@ keeping a copy. Each version from `0.1.0-next.1` on is staged on npm from a tag
7
7
  workflow, with provenance, goes live when it is promoted, and has a release on GitHub with the
8
8
  entry below as its notes.
9
9
 
10
+ ## 0.1.0-next.2
11
+
12
+ The page chrome every demo draws, and buttons, text fields, selects and callouts, styled by
13
+ `components.css` and drawn by the components in `./react`, with the state of each control carried
14
+ by a native or ARIA attribute. The explainer page, with its steps, panels, tables and code blocks,
15
+ is styled too, as markup a page writes by hand.
16
+
17
+ - **React.** `./react` is new, and takes React `^19.3.0` as an optional peer, which a page that
18
+ writes the markup itself never needs.
19
+ - **Page chrome.** `SkipLink`, `PortfolioStrip` and `DemoFooter` draw the skip link, the strip
20
+ above the page that says whose demo it is, and the footer below it, and `PORTFOLIO_URL`,
21
+ `PORTFOLIO_WORK_URL` and `GITHUB_PROFILE_URL` are the links they share. The components draw
22
+ markup alone, so a page loads `components.css` itself.
23
+ - **Buttons.** `Button` draws `.dk-button`, a plain `type="button"` unless a page gives another
24
+ type, with `.dk-button-accent` for the one action a view leads with. A toggle carries
25
+ `aria-pressed`, and a button whose panel is open `aria-expanded`, drawn alike. `off` marks a
26
+ button `aria-disabled` rather than `disabled`, so it keeps its place in the tab order, and a press
27
+ of it runs no click handler of the page's and submits no form. A button and a field's control
28
+ stand 2.25rem tall, and 2.75rem at 48rem wide or less.
29
+ - **Fields.** `InputField` and `SelectField` draw `.dk-field`, its label, a hint and an error when
30
+ given, and the control, `.dk-input` or a native `.dk-select`. The error sits above the control,
31
+ starts with a hidden "Error: " and marks the control `aria-invalid`. A control's text is never
32
+ under 16px.
33
+ - **Callouts.** `Callout` draws `.dk-callout` in the info tone on the highlight, or
34
+ `.dk-callout-success` or `.dk-callout-danger`, which start with a hidden word that says the tone.
35
+ It takes a role only when the page passes `live`.
36
+ - **Explainer pages.** `.dk-explainer-header` and `.dk-explainer` lay out the page that says how a
37
+ demo works, its text starting where the strip's links start and held to the measure, in plain
38
+ sections that open on a rule, a number and a heading a jump leaves in sight. `.dk-steps` draws
39
+ steps one after another. `./react` draws none of it.
40
+ - **Panels, tables and code.** `.dk-panel` sets a part of a page apart on the card ground, with no
41
+ tone and no role. A table in `.dk-table-scroll` and a block of `.dk-code` scroll inside
42
+ themselves, each a region named by the title above it that the keyboard reaches at every width,
43
+ so the page never scrolls sideways.
44
+ - **Code in running text.** `base.css` sets `code` at `--dk-text-code`, a step under the text
45
+ around it, and breaks a long name where it must. In a block it takes the block's size.
46
+ - **Base.** The border box now reaches a page's `::before` and `::after`, which it missed before,
47
+ since a pseudo-element listed inside a `:where()` matches nothing. The cut to motion reaches
48
+ those too, and a dialog's `::backdrop` and each part of a view transition. A `::before` or
49
+ `::after` that sets a width or height beside padding or a border now draws that size including
50
+ them, so one sized for the content box draws smaller.
51
+ - **Weight.** Every rule for these pieces, the resting look and the explainer's layout included,
52
+ sits in one `:where()`, so a state wins over the resting look by its order in the sheet, and a
53
+ page's own rule outside a layer wins over both.
54
+ - **Tokens.** `--dk-measure` changes from 42rem to 34rem, for every page that reads it. At 42rem a
55
+ line of body text ran past 80 characters, the most WCAG 1.4.8 suggests, and at 34rem the browser
56
+ tests hold a line of the lead and of body text to 80 or fewer. `--dk-text-code` is new, at 0.9em,
57
+ so code sizes to the text around it. The dark value of `--dk-control` is `#7d7d7d`, from
58
+ `#7a7a7a`. A button or field inside an info callout drew its edge at 2.99 to 1 on the highlight in
59
+ the dark theme, under the 3 to 1 a control's edge needs. The accent on the success and danger
60
+ grounds, and a control's edge on the highlight and on each callout's ground, are now measured as
61
+ well.
62
+
10
63
  ## 0.1.0-next.1
11
64
 
12
65
  Nothing in the code, the styles or the types changed. This is the first version the release workflow
package/README.md CHANGED
@@ -1,27 +1,33 @@
1
1
  # @jasonsdevspace/design-kit
2
2
 
3
- Design tokens, base styles and a theme script for the demos on
4
- [jasonsdevspace.com](https://jasonsdevspace.com). Every rule sits in a cascade layer of the kit's
5
- own, so a page's own rule outside a layer wins over every rule of the kit's but the cut to motion,
6
- without any extra weight. A page with layers of its own has to name where the kit's go among them,
7
- in the order the section on layers below gives. The script sets the theme a visitor chose on the
8
- portfolio before the page paints.
3
+ Design tokens, base styles, a theme script, the page chrome, buttons, fields and callouts, and the
4
+ explainer page, for the demos on [jasonsdevspace.com](https://jasonsdevspace.com). Every rule sits
5
+ in a cascade layer of the kit's own, so a page's own rule outside a layer wins over every rule of
6
+ the kit's but the cut to motion, without any extra weight. A page with layers of its own has to
7
+ name where the kit's go among them, in the order the section on layers below gives. The script
8
+ sets the theme a visitor chose on the portfolio before the page paints. The components are the
9
+ page chrome a demo draws around its page, a skip link, a strip and a footer, and the buttons,
10
+ text fields, selects and callouts on the page itself, as markup a sheet styles and as React
11
+ components that draw it. The explainer that says how a demo works, with its steps, panels, tables
12
+ and code blocks, is markup a page writes by hand, with no component.
9
13
 
10
14
  ```sh
11
15
  npm install @jasonsdevspace/design-kit
12
16
  ```
13
17
 
14
- The package loads on Node 22.12 or later, the lowest Node 22 that Vite 8 runs on, and takes Vite 8
15
- or later as an optional peer for its plugin. Prereleases are under npm's `next` tag, as
16
- `@jasonsdevspace/design-kit@next`. What changed in each version is in
18
+ The package loads on Node 22.12 or later, the lowest Node 22 that Vite 8 runs on. It takes Vite 8
19
+ or later as an optional peer for its plugin, and React `^19.3.0` as an optional peer for its
20
+ components. Prereleases are under npm's `next` tag, as `@jasonsdevspace/design-kit@next`. What
21
+ changed in each version is in
17
22
  [CHANGELOG.md](https://github.com/jasonsdevspace/design-kit/blob/main/packages/design-kit/CHANGELOG.md).
18
23
 
19
- A page loads both sheets, tokens first, through a bundler that resolves package exports, such as
24
+ A page loads the sheets, tokens first, through a bundler that resolves package exports, such as
20
25
  Vite.
21
26
 
22
27
  ```css
23
28
  @import '@jasonsdevspace/design-kit/tokens.css';
24
29
  @import '@jasonsdevspace/design-kit/base.css';
30
+ @import '@jasonsdevspace/design-kit/components.css';
25
31
  ```
26
32
 
27
33
  A page built with Vite takes the theme script from the plugin, and marks where it goes in its head.
@@ -46,16 +52,29 @@ export default defineConfig({ plugins: [themeScriptPlugin()] });
46
52
  `--dk-surface`, `--dk-raised`, `--dk-fg`, `--dk-muted`, `--dk-line`, `--dk-control`,
47
53
  `--dk-accent`, `--dk-on-accent`, `--dk-highlight`, `--dk-success`, `--dk-success-bg`,
48
54
  `--dk-danger`, `--dk-danger-bg` and the scrim, `--dk-scrim`. Beside them are steps of space, text
49
- size and corners, two line heights, three weights, a sans and a mono font stack, the focus ring's
50
- width and offset, a field's height and inline padding, the size of a touch target, the height of
51
- the portfolio's strip, the measure and column widths, a shadow and a short duration.
55
+ size and corners, a code size in em that follows the text around it, two line heights, three
56
+ weights, a sans and a mono font stack, the focus ring's width and offset, a field's height and
57
+ inline padding, the size of a touch target, the height of the portfolio's strip, the measure
58
+ and column widths, a shadow and a short duration.
52
59
  - `@jasonsdevspace/design-kit/base.css` gives the page its type and colors, one focus ring in the
53
- accent for every link and control the keyboard reaches, the `.dk-visually-hidden` class for a
54
- label a screen reader reads and the eye does not, and less motion for a visitor who asks for it.
55
- It loads after the tokens, since the tokens' first statement declares the kit's layer order.
60
+ accent for every link and control the keyboard reaches, code in running text a step under the
61
+ text around it, the `.dk-visually-hidden` class for a label a screen reader reads and the eye
62
+ does not, and less motion for a visitor who asks for it. It loads after the tokens, since the
63
+ tokens' first statement declares the kit's layer order.
64
+ - `@jasonsdevspace/design-kit/components.css` styles the page chrome, the skip link, the strip and
65
+ the footer, the buttons, fields and callouts, and the explainer page with its steps, panels,
66
+ tables and code blocks, by the classes in the markup the sections below give. It loads after
67
+ the tokens too.
56
68
  - `@jasonsdevspace/design-kit/theme` exports `THEME_KEY`, the storage key the portfolio saves a
57
69
  visitor's theme under, `THEMES`, the two themes the script sets, their type `Theme`, and
58
70
  `themeScript`, the script itself as a string to put inline.
71
+ - `@jasonsdevspace/design-kit/react` exports `SkipLink`, `PortfolioStrip` and `DemoFooter`, which
72
+ draw the chrome's markup, and `Button`, `InputField`, `SelectField` and `Callout`, which draw the
73
+ markup of the buttons, fields and callouts, with the types `ButtonProps`, `InputFieldProps`,
74
+ `SelectFieldProps` and `CalloutTone`. It also exports `PORTFOLIO_URL`, `PORTFOLIO_WORK_URL` and
75
+ `GITHUB_PROFILE_URL`, the portfolio, its list of work and the GitHub profile the chrome links to.
76
+ It has no component for the explainer or the pieces in it, which a page writes by hand. It takes
77
+ React `^19.3.0` as an optional peer, which a page that writes the markup itself never needs.
59
78
  - `@jasonsdevspace/design-kit/vite` exports `themeScriptPlugin()`, the Vite plugin that puts the
60
79
  script inline in place of `THEME_PLACEHOLDER`, the comment `<!-- theme-script -->`, and
61
80
  `themeScriptHash`, the script's hash as a Content Security Policy source. It runs in Node, and
@@ -71,12 +90,479 @@ import { themeScript } from '@jasonsdevspace/design-kit/theme';
71
90
  const head = `<script>${themeScript}</script>`;
72
91
  ```
73
92
 
93
+ ## Page chrome
94
+
95
+ The page chrome a demo draws around its page is a skip link, a strip above the page that says whose
96
+ demo it is, and a footer below it. `components.css` styles the markup below, which a page without
97
+ React writes by hand and the components in `./react` draw. The components ship no stylesheet and
98
+ set no `style` attribute, so a page loads `components.css` itself, and a page whose policy allows
99
+ styles only from its own origin, `style-src 'self'`, still draws them.
100
+
101
+ The skip link is the keyboard's first stop, out of sight until it takes focus, and then at the
102
+ column's inset, where the strip's links start. Following it moves focus to the page's main content,
103
+ which has a tabindex of -1 so it can take focus.
104
+
105
+ ```html
106
+ <a class="dk-skip" href="#main-content">Skip to content</a>
107
+ <main id="main-content" tabindex="-1">...</main>
108
+ ```
109
+
110
+ The strip holds a navigation landmark named Portfolio, with three links in this order: whose demo
111
+ it is, how it works and the source. The strip on the demo's explainer marks the explainer's link
112
+ with `aria-current="page"`. A demo with switches of its own puts them in a section after the links,
113
+ named for them, and a demo with none leaves the section out.
114
+
115
+ ```html
116
+ <div class="dk-strip">
117
+ <nav class="dk-strip-links" aria-label="Portfolio">
118
+ <a href="https://jasonsdevspace.com/#work">A demo by Jason Poteet</a>
119
+ <a href="./about/">How it works</a>
120
+ <a href="https://github.com/jasonsdevspace/example">Source on GitHub</a>
121
+ </nav>
122
+ <section class="dk-strip-tools" aria-label="Demo">
123
+ <button type="button">Tour</button>
124
+ </section>
125
+ </div>
126
+ ```
127
+
128
+ On a wide screen the strip is one 2.5rem row over a 1px rule, in the small text, with its links
129
+ 1.5rem apart and the switches on the right. At 48rem wide or less the text steps down to the
130
+ caption size, the links close to 1rem apart and wrap where they must, and the switches take a row
131
+ of their own below them.
132
+
133
+ The footer is one paragraph that says whose project the demo is, with the author's name linking the
134
+ portfolio, and links how it works, the source, the rest of the work and the license. Anything the
135
+ demo adds follows the license, as sentences of its own. Every link in the chrome keeps its
136
+ underline, and in the footer's sentence the underline is what tells a link from the text around it.
137
+
138
+ ```html
139
+ <footer class="dk-footer">
140
+ <p>
141
+ Example is a portfolio project by <a href="https://jasonsdevspace.com/">Jason Poteet</a>.
142
+ <a href="./about/">How it works</a>, in plain words. The
143
+ <a href="https://github.com/jasonsdevspace/example">source</a> and
144
+ <a href="https://github.com/jasonsdevspace">more work</a> are on GitHub, under the
145
+ <a href="https://github.com/jasonsdevspace/example/blob/main/LICENSE">MIT license</a>.
146
+ </p>
147
+ </footer>
148
+ ```
149
+
150
+ The strip and the footer run their rules the width of the page and line their content up with the
151
+ page's column, `--dk-column`, 72rem wide by default and centered, inset by 1rem. A page whose column
152
+ is another width sets `--dk-column` to it, as it sets a color role.
153
+
154
+ A page with React draws the same markup from the components.
155
+
156
+ ```tsx
157
+ import { DemoFooter, PortfolioStrip, SkipLink } from '@jasonsdevspace/design-kit/react';
158
+
159
+ const source = 'https://github.com/jasonsdevspace/example';
160
+
161
+ export const Page = () => (
162
+ <>
163
+ <SkipLink />
164
+ <PortfolioStrip
165
+ about="./about/"
166
+ source={source}
167
+ tools={<button type="button">Tour</button>}
168
+ />
169
+ <main id="main-content" tabIndex={-1}>
170
+ <h1>Example</h1>
171
+ </main>
172
+ <DemoFooter name="Example" about="./about/" source={source} />
173
+ </>
174
+ );
175
+ ```
176
+
177
+ - `SkipLink` leads to `main-content` unless `target` names another id, and its children replace
178
+ the words "Skip to content".
179
+ - `PortfolioStrip` takes `about`, the demo's explainer, and `source`, its repository. `current`
180
+ set to `about` marks the explainer's link as the current page. `tools` holds the demo's own
181
+ switches, and `toolsLabel` names their landmark, `Demo` unless given. With `tools` undefined,
182
+ null or false the strip draws no section for them, so a demo can pass `hasTour && <Tour />`.
183
+ - `DemoFooter` takes `name`, the demo's name as the sentence opens with it, `about` and `source`.
184
+ `license` is the link to the license, `LICENSE` on the main branch of `source` unless given, with
185
+ any trailing slash on `source` dropped first. `licenseName` is the license's name in the
186
+ sentence, `MIT license` unless given. Its children follow the license, and children that are
187
+ undefined, null or false add nothing.
188
+
189
+ ## Buttons, fields and callouts
190
+
191
+ `components.css` styles a button, a text field, a select and a callout by the markup below, which a
192
+ page without React writes by hand and the components in `./react` draw. A control's state is the
193
+ native or ARIA attribute a screen reader already reads, such as `aria-pressed` or `aria-invalid`,
194
+ never a class of the kit's, so what a visitor sees and what a screen reader says come from one
195
+ attribute. Every rule for these pieces sits in one `:where()`, the resting look included, so each
196
+ weighs nothing and a state wins over the resting look by coming after it in the sheet. A resting
197
+ look written as a bare class would outweigh every state. A page's own rule outside a layer still
198
+ wins over all of them.
199
+
200
+ ### Buttons
201
+
202
+ ```html
203
+ <button type="button" class="dk-button">Filters</button>
204
+ <button type="button" class="dk-button dk-button-accent">Save rule</button>
205
+ <button type="button" class="dk-button" aria-pressed="true">Members only</button>
206
+ <button type="button" class="dk-button" aria-disabled="true">Previous</button>
207
+ ```
208
+
209
+ A button is `type="button"` unless the page gives it another type, so it never submits a form by
210
+ surprise. `dk-button-accent` fills the one action a view leads with in the accent. It is a
211
+ class, since it is a kind of button rather than a state, and an accent button is never a toggle. A
212
+ toggle that stands alone carries `aria-pressed`, `"true"` or `"false"`, and keeps the same label in
213
+ both states, since the attribute already says which it is. An action carries no `aria-pressed` at
214
+ all. A button with `aria-expanded="true"`, whose panel is open, is drawn as a pressed one.
215
+
216
+ A button that is off carries `aria-disabled="true"` rather than `disabled`. A disabled button that
217
+ has focus drops it, and leaves the tab order, while an aria-disabled one stays where the keyboard
218
+ can reach it and find out why. The attribute does nothing by itself, so a page that writes this
219
+ markup makes its own click handler check `aria-disabled` and return early, calling
220
+ `preventDefault()` so a submit button does not submit. The sheet draws a `disabled` button, and one
221
+ inside a disabled fieldset, the same as an off one, and an accent button that is off keeps its
222
+ class and is drawn as any off button is. A page says why a button is off in text beside it. A
223
+ form's submit button is better left on while the form is incomplete, so a press shows the errors
224
+ rather than doing nothing.
225
+
226
+ A button is 2.25rem tall, `--dk-field-height`, as tall as a field, and 2.75rem, `--dk-target`, at
227
+ 48rem wide or less, and it is never narrower than it is tall. It has no ground of its own, so it
228
+ takes the ground of whatever it sits on, a callout included. A link with the class looks the same.
229
+ Where a pointer can hover, the edge turns to the accent under it, and the accent button underlines
230
+ its words instead. Pressed is the highlight with an edge twice as thick in the accent, and its words
231
+ in the page's text, an accent button's included. Off is the muted text on no ground with an edge in
232
+ the line color, rather than a fade, so its words keep a ratio the palette measures, and an off
233
+ toggle that is on keeps the thicker edge, in the line color. In forced colors a pressed button
234
+ takes the system's `Highlight`, an off one `GrayText` on the page's ground, accent or not, and an
235
+ off toggle that is on an edge in `Highlight`.
236
+
237
+ ### Fields
238
+
239
+ ```html
240
+ <div class="dk-field">
241
+ <label class="dk-field-label" for="email">Email</label>
242
+ <p class="dk-field-hint" id="email-hint">Where the receipt goes</p>
243
+ <p class="dk-field-error" id="email-error">
244
+ <span class="dk-visually-hidden">Error: </span>Enter an email address
245
+ </p>
246
+ <input
247
+ class="dk-input"
248
+ id="email"
249
+ type="email"
250
+ aria-describedby="email-hint email-error"
251
+ aria-invalid="true"
252
+ />
253
+ </div>
254
+
255
+ <div class="dk-field">
256
+ <label class="dk-field-label" for="sort">Sort by</label>
257
+ <select class="dk-select" id="sort">
258
+ <option value="">Choose an order</option>
259
+ <option value="price">Price</option>
260
+ </select>
261
+ </div>
262
+ ```
263
+
264
+ A field is its label, a hint when it has one, an error when it has one, and then the control,
265
+ `.dk-input` or `.dk-select`. The error sits above the control, so a phone's keyboard or an open list
266
+ never covers it, and the hint stays while the error shows, since it often says how to put the error
267
+ right. The error starts with a visually hidden "Error: ", so it is told from the hint by more than
268
+ its color. `aria-describedby` lists only what is drawn, the hint and then the error, followed by any
269
+ description of the page's own, and is left off when there is none. `aria-invalid="true"` is there
270
+ only while an error is drawn.
271
+
272
+ The error is not live, since a form that fails in several fields at once would speak over itself.
273
+ The page says when errors appear, and on a submit that fails it moves focus to the first invalid
274
+ control or to an error summary. The kit does no validation, so the page decides when to show an
275
+ error, best after a submit is tried rather than while someone is typing.
276
+
277
+ A control is as tall as a button, at both widths, and its text is never under 16px, so a phone does
278
+ not zoom the page when the field is tapped. It sits on the page's own ground, so it reads as a well
279
+ on a card. The hint and the error are in the caption size, the error in the danger color, and an
280
+ invalid control's edge in the danger color too. A disabled control is drawn on the surface ground
281
+ in the muted text. The select is a native `<select>`, its caret drawn as a background, and in forced
282
+ colors the browser's own arrow comes back. A select that shows its options as a list, with
283
+ `multiple` or a `size`, is left as the browser draws it, with no caret.
284
+
285
+ ### Callouts
286
+
287
+ ```html
288
+ <div class="dk-callout dk-callout-danger">
289
+ <div class="dk-callout-body">
290
+ <p class="dk-callout-title">
291
+ <span class="dk-visually-hidden">Error: </span>The products could not be loaded
292
+ </p>
293
+ <p>Check the file, then load it again.</p>
294
+ </div>
295
+ <div class="dk-callout-actions">
296
+ <button type="button" class="dk-button">Try again</button>
297
+ </div>
298
+ </div>
299
+ ```
300
+
301
+ A callout is `dk-callout` alone for news, on the highlight, or adds `dk-callout-success` or
302
+ `dk-callout-danger` for something that went right or wrong, on that color's own ground. The words
303
+ stay in the page's text color in every tone. A success or danger callout starts its title, or its
304
+ message when it has no title, with a visually hidden "Success: " or "Error: ", so the tone is not
305
+ told by color alone. The title is a paragraph, or a heading from `h2` to `h6` where the callout is a
306
+ section a visitor would jump to, and opens `.dk-callout-body`, the message. The actions, when there
307
+ are some, follow the message, outside it.
308
+
309
+ A callout has no role unless the page gives it one, whatever its tone. A message on the page when it
310
+ loads gets none, since a live region created with its text already in it is read unreliably across
311
+ screen readers and interrupts the reading of the page's title. A message that arrives after the page
312
+ has loaded takes `role="status"`, read when the screen reader is next idle, or `role="alert"`, read
313
+ at once, on `.dk-callout-body`, so the title and the tone's word are read with the message and the
314
+ actions are not. The page puts a live callout on the page empty first and fills it after, which is
315
+ when a screen reader reads it. A page filling a success or danger callout puts the hidden word in
316
+ with the message, as `<span class="dk-visually-hidden">Success: </span>Saved.`, since the word is
317
+ part of what is read. An empty live callout still draws its ground and padding, so a page places it
318
+ where an empty band reads as space, or leaves it out of the layout's flow until it is filled.
319
+
320
+ A callout's edge is transparent, and shows only in forced colors, where the ground is dropped and
321
+ the edge marks where the callout starts and ends.
322
+
323
+ ### From React
324
+
325
+ ```tsx
326
+ import { useState } from 'react';
327
+
328
+ import { Button, Callout, InputField, SelectField } from '@jasonsdevspace/design-kit/react';
329
+
330
+ type Props = { readonly error?: string; readonly saved?: string };
331
+
332
+ export const Checkout = ({ error, saved }: Props) => {
333
+ const [gift, setGift] = useState(false);
334
+ return (
335
+ <form>
336
+ <InputField label="Email" hint="Where the receipt goes" error={error} type="email" />
337
+ <SelectField label="Delivery">
338
+ <option value="standard">Standard</option>
339
+ <option value="express">Express</option>
340
+ </SelectField>
341
+ <Button pressed={gift} onClick={() => setGift(!gift)}>
342
+ Gift wrap
343
+ </Button>
344
+ <Button type="submit" accent>
345
+ Place order
346
+ </Button>
347
+ <Callout tone="success" live="status">
348
+ {saved}
349
+ </Callout>
350
+ </form>
351
+ );
352
+ };
353
+ ```
354
+
355
+ - `Button` takes the props of a `<button>` but `style`, `disabled`, `aria-disabled` and
356
+ `aria-pressed`, and is `type="button"` unless `type` says otherwise. `accent` adds
357
+ `dk-button-accent`. `pressed` writes `aria-pressed`, `false` included, and left undefined makes
358
+ the button an action. The types refuse `accent` and `pressed` together. `off` writes
359
+ `aria-disabled="true"` and puts a click handler that calls `preventDefault()` in place of
360
+ `onClick`, so the page's click handler does not run and a submit button does not submit. Any
361
+ other handler the page passes, for keys or a pointer, still runs, and checks `off` itself. A
362
+ `className` follows the kit's classes.
363
+ - `InputField` takes `label`, and `hint` and `error`, each drawn only when given, an empty string,
364
+ null or false drawing nothing. `id` is the control's, made with React's `useId` unless given, and
365
+ a page gives one when something links to the field, such as an error summary. Every other prop
366
+ goes to the `<input>` but `style` and `aria-invalid`. A `className` follows `dk-input`, and an
367
+ `aria-describedby` of the page's own is listed after the hint and the error.
368
+ - `SelectField` takes the same props, with its children as the options of the `<select>`.
369
+ - `Callout` takes `tone`, `info` unless given, `success` or `danger`, and `title`, drawn at the
370
+ start of the message as a paragraph or, with `headingLevel` from 2 to 6, as that heading, and as
371
+ a paragraph for any other level. `actions` are drawn after the message. `prefix` is the word
372
+ read before the title or the message, the tone's own unless given, and an empty string drops it.
373
+ `live`, `status` or `alert`, puts that role on the message and draws the message even when it is
374
+ empty. A callout takes no other props, so a page that needs a class
375
+ or an id on one puts it on an element around it.
376
+
377
+ ## Explainer pages
378
+
379
+ An explainer is the page that says how a demo works, the one the strip's "How it works" link
380
+ leads to. `components.css` styles its layout and the steps, panels, tables and code blocks it
381
+ holds, by the markup below, which every page writes by hand. `./react` draws none of it, since
382
+ none of these pieces has logic to hide. Every rule for them sits in one `:where()`, the layout
383
+ included, so where the layout and a piece set inside it both set something, the later rule in
384
+ the sheet wins, and a page's own rule outside a layer wins over both. A layout written as a bare
385
+ class would outweigh every piece set inside it.
386
+
387
+ ### The page
388
+
389
+ ```html
390
+ <a class="dk-skip" href="#main-content">Skip to content</a>
391
+ <div class="dk-strip">...</div>
392
+ <header class="dk-explainer-header"><a href="../">Example</a></header>
393
+ <main id="main-content" tabindex="-1" class="dk-explainer">
394
+ <h1>How Example works</h1>
395
+ <p class="dk-explainer-lead">What the demo does, in a sentence or two.</p>
396
+ <p><a class="dk-button dk-button-accent" href="../">Open the demo</a></p>
397
+ <section>
398
+ <p class="dk-explainer-number" aria-hidden="true">01</p>
399
+ <h2 id="order">What happens when an order comes in</h2>
400
+ <p>...</p>
401
+ <h3>What usually goes wrong</h3>
402
+ <p>...</p>
403
+ </section>
404
+ </main>
405
+ <footer class="dk-footer">...</footer>
406
+ ```
407
+
408
+ The header holds a link back to the demo, outside `main`, so the skip link passes it and reading
409
+ starts at the h1. `main` holds the h1, the lead, the one action as an accent button, and then
410
+ the sections. Each section holds its number, hidden from a screen reader since its heading says
411
+ what the section is, then an h2 with an id, then its text, with an h3 for each part of it.
412
+
413
+ A section is a plain `<section>`, with no `aria-labelledby` or `aria-label`. A named section becomes
414
+ a region landmark, and the page's landmarks stay the strip's navigation, the demo's switches when it
415
+ has some, the header, the main content, the footer and the frames that scroll. A reader moves
416
+ between sections by their headings. The h2 ids are a contract other pages link to, so a page keeps
417
+ an id when it rewords its heading. An in-page link needs no tabindex on the heading, since a jump to
418
+ a fragment moves where the keyboard goes next. Each h2 has a scroll margin, so a jump stops short of
419
+ it and leaves the section's number and rule in sight above it. The kit draws no table of contents,
420
+ since an explainer is short enough to move through by its headings.
421
+
422
+ The header and the explainer start their text at the column's inset, where the strip's links start,
423
+ and every part of the explainer holds to `--dk-measure`, 34rem, no more than 80 characters a line in
424
+ the body size. Each part is spaced from the one before it, so a list, a table, a panel and a code
425
+ block space alike. The h1 is the display size and the lead a step over the body. A section opens on
426
+ a rule in the line color, with 2rem of space above and below it, then its number in the muted
427
+ caption size, its heading in the title size, and its text, with an h3 at the body size.
428
+
429
+ ### Steps
430
+
431
+ ```html
432
+ <ol class="dk-steps" role="list">
433
+ <li>
434
+ <strong class="dk-steps-title">The order names a product.</strong>
435
+ <span class="dk-steps-note">And how many of it.</span>
436
+ </li>
437
+ <li>
438
+ <span class="dk-steps-arrow" aria-hidden="true">↓</span>
439
+ <strong class="dk-steps-title">It waits its turn.</strong>
440
+ <span class="dk-steps-note">Behind any order for the same product.</span>
441
+ </li>
442
+ </ol>
443
+ ```
444
+
445
+ Steps a demo takes one after another, each in a box of its own with an edge in the line color and
446
+ large corners, in the small text. A step's title is at medium weight, with its note in the muted
447
+ text on a line below. Every step after the first opens with an arrow, a span holding ↓ that the
448
+ sheet sets in the gap above the step, hidden from a screen reader. It is a span because a
449
+ component's selectors allow nothing after a `:where()` but a focus pseudo-class, and a
450
+ pseudo-element cannot sit inside one. The list keeps `role="list"`, since WebKit with VoiceOver
451
+ drops a list's meaning when its markers are gone.
452
+
453
+ ### Panels
454
+
455
+ ```html
456
+ <div class="dk-panel">
457
+ <p class="dk-panel-title">How the numbers were taken</p>
458
+ <p>...</p>
459
+ </div>
460
+ ```
461
+
462
+ A panel sets a part of a page apart on the card ground, `--dk-surface`, with an edge in the line
463
+ color and large corners. The edge is what draws it, since the card ground is close to the page's,
464
+ and it stays in forced colors. A panel is padded 1.5rem, and 1rem at 48rem wide or less, and what it
465
+ holds is spaced 0.75rem from the one before it, a step closer than an explainer's parts. Its title
466
+ is a paragraph, or a heading one level under its section's where a reader would jump to it, and
467
+ inside an explainer it steps down to the body size.
468
+
469
+ A panel differs from a callout in having no tone, no hidden word and no live role, and in being
470
+ at the body size rather than the small text. It has no role at all. It is not an `aside`, since
471
+ inside `main` an aside becomes a complementary landmark, and a box in the reading flow is not
472
+ complementary to it. Nor is it a named `section`, which would be a region landmark, or a
473
+ `figure`.
474
+
475
+ ### Tables
476
+
477
+ ```html
478
+ <p class="dk-table-title" id="bench-title">Orders a second under each strategy</p>
479
+ <div class="dk-table-scroll" role="region" aria-labelledby="bench-title" tabindex="0">
480
+ <table class="dk-table" aria-labelledby="bench-title">
481
+ <thead>
482
+ <tr>
483
+ <th scope="col">Scenario</th>
484
+ <th scope="col" class="dk-table-number">Orders a second</th>
485
+ <th scope="col" class="dk-table-number">Slowest 1%, ms</th>
486
+ </tr>
487
+ </thead>
488
+ <tbody>
489
+ <tr>
490
+ <th scope="row">Hot product, row lock</th>
491
+ <td class="dk-table-number">700</td>
492
+ <td class="dk-table-number">48.5</td>
493
+ </tr>
494
+ </tbody>
495
+ </table>
496
+ </div>
497
+ ```
498
+
499
+ The title is a paragraph above the frame, never a `<caption>`, since a caption takes the table's
500
+ width and scrolls off a phone with it. The one title names both the frame and the table. Its id is
501
+ unique on the page, and both `aria-labelledby` attributes point to it. The frame is a region with a
502
+ tabindex of 0 at every width, so a keyboard can reach it and scroll it with the arrow keys. A page
503
+ written by hand cannot tell when its table overflows, which changes with zoom and text size, so the
504
+ frame is a region whether or not it scrolls, which costs one tab stop on a wide screen where it does
505
+ not. A frame holds to the measure as the prose does, so a table wider than 34rem scrolls on a wide
506
+ screen too.
507
+
508
+ A column's header is `th scope="col"`, the cell that names a row is `th scope="row"`, and every
509
+ cell of a numeric column, its header included, is `.dk-table-number`. A table is never stacked
510
+ into cards on a phone, since `display: block` on a table's parts drops its table semantics in
511
+ WebKit. It scrolls in its frame instead.
512
+
513
+ A table is in the small text, with each row ruled in the line color. Its column headers are the
514
+ muted text at medium weight, and a row's header is at medium weight too. The outer cells keep no
515
+ padding on their outer side, so the table's text lines up with the prose around it. Figures sit
516
+ to the right and never wrap, while text in a cell wraps and keeps its first line level with the
517
+ rest of its row. A header gives its unit in words a screen reader says well, "Slowest 1%, ms"
518
+ rather than "p99 ms".
519
+
520
+ ### Code blocks
521
+
522
+ ```html
523
+ <p class="dk-code-title" id="lock-title">The statement that holds the stock</p>
524
+ <pre
525
+ class="dk-code"
526
+ role="region"
527
+ aria-labelledby="lock-title"
528
+ tabindex="0"
529
+ translate="no"
530
+ ><code>UPDATE stock SET held = held + 1
531
+ WHERE product_id = $1 AND held &lt; on_hand;</code></pre>
532
+ ```
533
+
534
+ A block of code keeps its lines as written and scrolls inside itself, since wrapping would break its
535
+ indentation and show line breaks the code does not have. It is a named region with a tabindex of 0,
536
+ for the same reasons as a table's frame. Its title says what the code is for, such as "Run it
537
+ yourself", rather than naming its language. `translate="no"` keeps a browser's translation of the
538
+ page off the identifiers. A block never takes `lang` for its programming language, since `lang`
539
+ names a human language. The code starts right after `<code>`, since a line break there shows as a
540
+ blank first line, and a `<` or `&` in it is written `&lt;` or `&amp;`.
541
+
542
+ A block sits on the card ground, or on the page's own ground inside a panel, so it reads as set
543
+ into what holds it. Its edge is transparent, and shows only in forced colors. The kit ships no
544
+ script, so a block has no copy button.
545
+
546
+ ### Code in running text
547
+
548
+ `base.css` sets code in running text on every page, not only an explainer, at `--dk-text-code`,
549
+ 0.9em, so it follows the size of the text around it, a step under it since a mono face looks
550
+ larger at the same size. It breaks anywhere it must, with `overflow-wrap: anywhere`, since a long
551
+ name has no spaces to break at and would push a narrow column wider. Inside a `pre` it takes the
552
+ block's size and keeps its lines whole. It has no ground of its own, and in a link it keeps the
553
+ link's underline.
554
+
74
555
  ## Layers and palettes
75
556
 
76
557
  The tokens declare `dk.tokens`, `dk.base` and `dk.components`, in that order, and each sheet fills
77
558
  its own. A page's own rule outside a layer wins over every rule of the kit's but the cut to motion,
78
- however little it weighs and wherever its sheet loads. Inside the layers, every selector but the
79
- visually hidden class is one `:where()`, followed at most by a focus pseudo-class.
559
+ however little it weighs and wherever its sheet loads. Inside the layers, every selector is either
560
+ one of the kit's own classes, such as `.dk-strip` or `.dk-visually-hidden`, or one `:where()`,
561
+ followed at most by a focus pseudo-class. The base alone follows a `:where()` with a pseudo-element,
562
+ so the border box reaches a page's `::before` and `::after`, and the cut to motion reaches those, a
563
+ dialog's `::backdrop` and each part of a view transition, since a pseudo-element inside a `:where()`
564
+ matches nothing. That adds the weight of one element, which counts for nothing against a page's own
565
+ rule outside a layer.
80
566
 
81
567
  A page with layers of its own names where `dk` goes among them. A layer takes its place where its
82
568
  name first appears, so a page whose layers load before the kit's sheets, without naming `dk`, puts
@@ -103,11 +589,13 @@ color in both themes.
103
589
 
104
590
  The default palette is measured in both themes, against the pairings it is meant for. Text, muted
105
591
  text, the accent, and the success and danger colors pass 4.5 to 1 on the page, a card and anything
106
- raised, and a control's edge passes 3 to 1 on each of those. Text on the accent passes 4.5 to 1, as
107
- do text and muted text on the highlight and on each callout's ground, the accent on the highlight,
108
- and the success and danger colors on their own grounds. The focus ring is drawn in the accent, so
109
- it is held as a link is. A pairing outside that list, such as a line against its ground, carries no
110
- ratio, and a page that sets its own palette answers for its own pairings.
592
+ raised, and a control's edge passes 3 to 1 on each of those. Text on the accent passes 4.5 to 1.
593
+ Text, muted text and the accent pass 4.5 to 1 on the highlight and on the success and danger grounds
594
+ as well, and a control's edge passes 3 to 1 on them, so a button or a field keeps its edge inside a
595
+ callout of any tone. The success and danger colors pass 4.5 to 1 on their own grounds. The focus
596
+ ring is drawn in the accent, so it is held as a link is. A pairing outside that list, such as a line
597
+ against its ground, carries no ratio, and a page that sets its own palette answers for its own
598
+ pairings.
111
599
 
112
600
  ## The theme contract
113
601