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

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