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

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,69 @@ 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.5
11
+
12
+ The strip's first link is the author's name and leads to the portfolio. A demo can name its
13
+ switches' width, so the strip wraps the same before they arrive as after, and on a wide screen the
14
+ switches keep to the strip's right edge.
15
+
16
+ - **Page chrome.** The strip's first link reads "Jason Poteet" and leads to the portfolio's home
17
+ page, `PORTFOLIO_URL`, where it read "More work by Jason Poteet" and led to the portfolio's list
18
+ of work. It matches the footer's own "Jason Poteet", which leads to the same place, and no longer
19
+ shares "more work" with the footer's link to the GitHub profile. A page that writes the strip by
20
+ hand changes the link's words and where it leads itself. `PORTFOLIO_WORK_URL` is still exported,
21
+ for a page that links to the list of work.
22
+ - **Page chrome.** A new token, `--dk-strip-tools`, holds the width of a demo's switches in the
23
+ strip, in the strip's own text size, such as `:root { --dk-strip-tools: 20em; }`. It is `auto`, so
24
+ nothing changes for a demo that leaves it alone. A demo whose switches arrive after the page sets
25
+ it at least as wide as they are, and on a wide screen the room the strip keeps for them and the
26
+ section that holds them both take that width, the switches packed at its right end, so the strip
27
+ wraps before they arrive just as it does after. On a phone the switches keep a row of their own at
28
+ its full width.
29
+ - **Page chrome.** On a wide screen the switches keep to the strip's right edge, on the links' row
30
+ or on a row of their own where they wrap below it, where before a wrapped section started at the
31
+ left. Where the switches wrap within their section, its last row sits at the right too. On a phone
32
+ they still start at the left. A page that writes the strip by hand gets this from the sheet, with
33
+ no change to its markup.
34
+
35
+ ## 0.1.0-next.4
36
+
37
+ The strip's first link says whose work it leads to, and the footer's sentence holds to the measure.
38
+ Every optional prop takes `undefined`, the callout takes the props of a `<div>` and is padded as
39
+ steps and code blocks are, and every empty slot is read alike. The sheets show a pointer on what
40
+ takes a click, mark an invalid field in forced colors and keep a token's name whole in a table.
41
+
42
+ - **Page chrome.** The strip's first link reads "More work by Jason Poteet", where it read "A demo
43
+ by Jason Poteet", which fits a demo and the kit's own site alike and says where the link goes. A
44
+ page that writes the strip by hand changes the link's words itself. The footer's sentence holds
45
+ to the measure, while its rule still runs the page's width.
46
+ - **Types.** Every optional prop of every component takes `undefined`, as if it were left out, so a
47
+ wrapper that passes on a prop it may not have been given compiles under
48
+ `exactOptionalPropertyTypes`. Only the types widen, so nothing that compiled before breaks.
49
+ - **Callouts.** `Callout` takes the props of a `<div>` but `style`, `role`, the `title` attribute,
50
+ inner HTML and the live attributes, among them an `id`, a page's class after the kit's, and a
51
+ `ref` that holds the callout drawn, so a page can link to an error summary, focus it after a form
52
+ fails, or lay it out. Its props are exported as `CalloutProps`. Its message takes its role from
53
+ `live` alone. A tone or a heading level the types refuse, as plain JavaScript can pass, draws news
54
+ and a paragraph. A callout is padded 0.75rem by 1rem, from 0.5rem by 0.75rem, as steps and code
55
+ blocks are.
56
+ - **Empty slots.** Every slot a component leaves out when it is empty reads an empty string, or a
57
+ list or a fragment that holds only false, null or empty strings, as nothing. The strip's switches
58
+ and the footer's additions now read an empty string so too. A field's hint and error and a
59
+ callout's title, message and actions now read such a list or fragment as nothing, where before
60
+ they drew an empty paragraph, which a hint or an error also listed in `aria-describedby`.
61
+ - **Pointers.** `base.css` shows a pointer on a disclosure's summary and on a checkbox or a radio
62
+ with the label that holds it, and the arrow on one that is unavailable. The label's pointer reads
63
+ `:has()`, so in Firefox 120 only the box or the dot shows it.
64
+ - **Fields.** In forced colors, where an invalid control's red edge is gone, it gains a dashed line
65
+ around its edge in the system's text color, `CanvasText`, which moves nothing as the error comes
66
+ and goes. With focus, the ring takes its place.
67
+ - **Tables.** Code in a table cell within its frame stays on one line, so a token's name never
68
+ breaks after `--dk-`, and a long expression scrolls the frame as a wide table does.
69
+ - **Docs.** The README shows the strip's switch as a kit button and an unavailable button with the
70
+ text that says why, says why a field uses `disabled` where a button stays in the tab order, and
71
+ says the strip's and the footer's link props take only URLs the demo's author writes.
72
+
10
73
  ## 0.1.0-next.3
11
74
 
12
75
  The page chrome holds still and shows where a reader is, the controls a page moves focus to take a
package/README.md CHANGED
@@ -1,15 +1,11 @@
1
1
  # @jasonsdevspace/design-kit
2
2
 
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.
3
+ Design tokens, base styles, a theme script, page chrome, buttons, fields and callouts, and an
4
+ explainer layout, for the demos on [jasonsdevspace.com](https://jasonsdevspace.com). The styles are
5
+ plain CSS in cascade layers, so a page's own rules outside a layer win without extra weight. The
6
+ theme script sets the theme a visitor chose on the portfolio before the page paints. The chrome and
7
+ the controls come as markup a sheet styles and as React components that draw it. The explainer is
8
+ markup a page writes by hand.
13
9
 
14
10
  ```sh
15
11
  npm install @jasonsdevspace/design-kit
@@ -21,6 +17,10 @@ components. Prereleases are under npm's `next` tag, as `@jasonsdevspace/design-k
21
17
  changed in each version is in
22
18
  [CHANGELOG.md](https://github.com/jasonsdevspace/design-kit/blob/main/packages/design-kit/CHANGELOG.md).
23
19
 
20
+ A page with cascade layers of its own must name where the kit's layers go, as [Layers and
21
+ palettes](#layers-and-palettes) describes. The cut to motion for a visitor who asks for less holds
22
+ over a page's ordinary styles, as that section explains.
23
+
24
24
  A page loads the sheets, tokens first, through a bundler that resolves package exports, such as
25
25
  Vite.
26
26
 
@@ -52,15 +52,16 @@ export default defineConfig({ plugins: [themeScriptPlugin()] });
52
52
  `--dk-surface`, `--dk-raised`, `--dk-fg`, `--dk-muted`, `--dk-line`, `--dk-control`,
53
53
  `--dk-accent`, `--dk-on-accent`, `--dk-highlight`, `--dk-success`, `--dk-success-bg`,
54
54
  `--dk-danger`, `--dk-danger-bg` and the scrim, `--dk-scrim`. Beside them are steps of space, text
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.
55
+ size and corners, a code size in em that follows the text around it, line heights, weights, a sans
56
+ and a mono font stack, the focus ring's width and offset, a field's height and inline padding, the
57
+ size of a touch target, the height of the portfolio's strip and the width of a demo's switches in
58
+ it, `auto` until a demo names one, the measure and column widths, a shadow and a short duration.
59
59
  - `@jasonsdevspace/design-kit/base.css` gives the page its type and colors, one focus ring in the
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.
60
+ accent for every link and control the keyboard reaches, code in running text a step under the text
61
+ around it, the `.dk-visually-hidden` class for a label a screen reader reads and the eye does not,
62
+ a pointer on a disclosure's summary and on a box or a dot with the label that holds it, and less
63
+ motion for a visitor who asks for it. It loads after the tokens, since the tokens' first statement
64
+ declares the kit's layer order.
64
65
  - `@jasonsdevspace/design-kit/components.css` styles the page chrome, the skip link, the strip and
65
66
  the footer, the buttons, fields and callouts, and the explainer page with its steps, panels,
66
67
  tables and code blocks, by the classes in the markup the sections below give. It loads after
@@ -71,10 +72,11 @@ export default defineConfig({ plugins: [themeScriptPlugin()] });
71
72
  - `@jasonsdevspace/design-kit/react` exports `SkipLink`, `PortfolioStrip` and `DemoFooter`, which
72
73
  draw the chrome's markup, and `Button`, `InputField`, `SelectField` and `Callout`, which draw the
73
74
  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.
75
+ `SelectFieldProps`, `CalloutProps` and `CalloutTone`. It also exports `PORTFOLIO_URL`,
76
+ `PORTFOLIO_WORK_URL` and `GITHUB_PROFILE_URL`: the portfolio and the GitHub profile, which the
77
+ chrome links to, and the portfolio's list of work, for a page that links to it. It has no
78
+ component for the explainer or the pieces in it, which a page writes by hand. It takes React
79
+ `^19.3.0` as an optional peer, which a page that writes the markup itself never needs.
78
80
  - `@jasonsdevspace/design-kit/vite` exports `themeScriptPlugin()`, the Vite plugin that puts the
79
81
  script inline in place of `THEME_PLACEHOLDER`, the comment `<!-- theme-script -->`, and
80
82
  `themeScriptHash`, the script's hash as a Content Security Policy source. It runs in Node, and
@@ -102,31 +104,36 @@ The skip link is the keyboard's first stop, out of sight until it takes focus, a
102
104
  column's inset, where the strip's links start. Following it moves focus to the page's main content,
103
105
  which has a tabindex of -1 so it can take focus.
104
106
 
107
+ <!-- example: skipLink -->
108
+
105
109
  ```html
106
110
  <a class="dk-skip" href="#main-content">Skip to content</a>
107
111
  <main id="main-content" tabindex="-1">...</main>
108
112
  ```
109
113
 
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
114
+ A skip link can also sit inside a box of the page's own, such as one at the top of a column of
111
115
  filters that leads past them to the results. It waits in its own place as the visually hidden class
112
116
  does, one pixel clipped to nothing, so it draws nothing and takes no click wherever the box starts
113
117
  on the page. A box that is positioned holds it, and it shows at the top of that box, inset as it is
114
118
  at the top of a page, when a keyboard reaches it.
115
119
 
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
+ The strip holds a navigation landmark named Portfolio, with links in this order: the author's name,
121
+ which leads to the portfolio as the footer's does, how it works and the source. The name reads
122
+ "Jason Poteet", which fits a demo and the kit's own site alike. The strip on the demo's explainer
123
+ marks the explainer's link with `aria-current="page"`. A demo with switches of its own puts them in
124
+ a section after the links, named for them, and a demo with none leaves the section out.
125
+
126
+ <!-- example: strip -->
120
127
 
121
128
  ```html
122
129
  <div class="dk-strip">
123
130
  <nav class="dk-strip-links" aria-label="Portfolio">
124
- <a href="https://jasonsdevspace.com/#work">A demo by Jason Poteet</a>
131
+ <a href="https://jasonsdevspace.com/">Jason Poteet</a>
125
132
  <a href="./about/">How it works</a>
126
133
  <a href="https://github.com/jasonsdevspace/example">Source on GitHub</a>
127
134
  </nav>
128
135
  <section class="dk-strip-tools" aria-label="Demo">
129
- <button type="button">Tour</button>
136
+ <button type="button" class="dk-button">Tour</button>
130
137
  </section>
131
138
  </div>
132
139
  ```
@@ -136,13 +143,19 @@ room so nothing below the strip moves when they come. The section is drawn with
136
143
  only the room, with no text, so it is no landmark and a screen reader finds nothing in it. When the
137
144
  switches come they take the room's place in the same section, and the section takes its name.
138
145
 
146
+ <!-- example: stripWaiting -->
147
+
139
148
  ```html
140
149
  <section class="dk-strip-tools"><span class="dk-strip-room"></span></section>
141
150
  ```
142
151
 
143
152
  The room holds the height of one row of the kit's buttons or fields, beside the links on a wide
144
153
  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.
154
+ row, still move what is below them when they come. On a wide screen the room has no width of its
155
+ own, so a demo whose switches would make the strip wrap where the room did not names their width in
156
+ `--dk-strip-tools`, in the strip's own text size, such as `:root { --dk-strip-tools: 20em; }`. The
157
+ room and the switches then take that width, and the strip wraps before they come as it does after.
158
+ On a phone the switches have a row of their own and the width is left aside.
146
159
 
147
160
  On a wide screen the strip is one 2.5rem row over a 1px rule, in the small text, with its links
148
161
  1.5rem apart and the switches on the right. At 48rem wide or less the text steps down to the
@@ -152,11 +165,14 @@ of their own below them.
152
165
  The footer is one paragraph that says whose project the demo is, with the author's name linking the
153
166
  portfolio, and links how it works, the source, the rest of the work and the license. The footer on
154
167
  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.
168
+ Anything the demo adds follows the license, as sentences of its own. The sentence holds to the
169
+ measure, as prose does, while the footer's rule runs the page's width. Every link in the chrome
170
+ keeps its underline, and in the footer's sentence the underline is what tells a link from the text
171
+ around it. The link marked as the current page, in the strip or the footer, stands in the page's
172
+ text at the heading's weight, so it is told from the links around it by more than its color, in
173
+ forced colors too, and still reads as a link.
174
+
175
+ <!-- example: footer -->
160
176
 
161
177
  ```html
162
178
  <footer class="dk-footer">
@@ -177,18 +193,14 @@ is another width sets `--dk-column` to it, as it sets a color role.
177
193
  A page with React draws the same markup from the components.
178
194
 
179
195
  ```tsx
180
- import { DemoFooter, PortfolioStrip, SkipLink } from '@jasonsdevspace/design-kit/react';
196
+ import { Button, DemoFooter, PortfolioStrip, SkipLink } from '@jasonsdevspace/design-kit/react';
181
197
 
182
198
  const source = 'https://github.com/jasonsdevspace/example';
183
199
 
184
200
  export const Page = () => (
185
201
  <>
186
202
  <SkipLink />
187
- <PortfolioStrip
188
- about="./about/"
189
- source={source}
190
- tools={<button type="button">Tour</button>}
191
- />
203
+ <PortfolioStrip about="./about/" source={source} tools={<Button>Tour</Button>} />
192
204
  <main id="main-content" tabIndex={-1}>
193
205
  <h1>Example</h1>
194
206
  </main>
@@ -199,18 +211,23 @@ export const Page = () => (
199
211
 
200
212
  - `SkipLink` leads to `main-content` unless `target` names another id, and its children replace
201
213
  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.
214
+ - `PortfolioStrip` takes `about`, the demo's explainer, and `source`, its repository. `current` set
215
+ to `about` marks the explainer's link as the current page. `tools` holds the demo's own switches,
216
+ and `toolsLabel` names their landmark, `Demo` unless given. With `tools` undefined, null, false,
217
+ an empty string, or a list or a fragment of only those, the strip draws no section for them, so a
218
+ demo can pass `hasTour && <Tour />`. `toolsWaiting` keeps the room of one row of the kit's
219
+ controls while `tools` draws nothing, for a page drawn before the switches can be, in the same
220
+ section unnamed until they come.
208
221
  - `DemoFooter` takes `name`, the demo's name as the sentence opens with it, `about` and `source`.
209
222
  `current` set to `about` marks the explainer's link as the current page, as on the strip.
210
223
  `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.
224
+ any trailing slash on `source` dropped first. `licenseName` is the license's name in the sentence,
225
+ `MIT license` unless given. Its children follow the license, and children that are undefined,
226
+ null, false, an empty string, or a list or a fragment of only those, add nothing.
227
+
228
+ The strip's and the footer's `about`, `source` and `license` go into their links unchecked, so each
229
+ is a URL the demo's author writes, never one taken from the page's address or a visitor's input,
230
+ since one taken from either could send a visitor anywhere.
214
231
 
215
232
  ## Buttons, fields and callouts
216
233
 
@@ -225,11 +242,16 @@ wins over all of them.
225
242
 
226
243
  ### Buttons
227
244
 
245
+ <!-- example: buttons -->
246
+
228
247
  ```html
229
248
  <button type="button" class="dk-button">Filters</button>
230
249
  <button type="button" class="dk-button dk-button-accent">Save rule</button>
231
250
  <button type="button" class="dk-button" aria-pressed="true">Members only</button>
232
- <button type="button" class="dk-button" aria-disabled="true">Previous</button>
251
+ <button aria-describedby="previous-why" type="button" class="dk-button" aria-disabled="true">
252
+ Previous
253
+ </button>
254
+ <p id="previous-why" class="dk-field-hint">This is the first page.</p>
233
255
  ```
234
256
 
235
257
  A button is `type="button"` unless the page gives it another type, so it never submits a form by
@@ -239,15 +261,16 @@ toggle that stands alone carries `aria-pressed`, `"true"` or `"false"`, and keep
239
261
  both states, since the attribute already says which it is. An action carries no `aria-pressed` at
240
262
  all. A button with `aria-expanded="true"`, whose panel is open, is drawn as a pressed one.
241
263
 
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
264
+ An unavailable button, `off` in React, carries `aria-disabled="true"` rather than `disabled`. A
265
+ disabled button that has focus drops it, and leaves the tab order, while an aria-disabled one stays
266
+ where the keyboard can reach it and find out why. The attribute does nothing by itself, so a page
267
+ that writes this markup makes its own click handler check `aria-disabled` and return early, calling
246
268
  `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.
269
+ inside a disabled fieldset, the same as an off one, and an accent button that is off keeps its class
270
+ and is drawn as any off button is. A page says why a button is off in text beside it, such as a
271
+ `.dk-field-hint` paragraph the button's `aria-describedby` names, a class that works beside any
272
+ control. A form's submit button is better left on while the form is incomplete, so a press shows the
273
+ errors rather than doing nothing.
251
274
 
252
275
  A button is 2.25rem tall, `--dk-field-height`, as tall as a field, and 2.75rem, `--dk-target`, at
253
276
  48rem wide or less, and it is never narrower than it is tall. It has no ground of its own, so it
@@ -262,22 +285,24 @@ off toggle that is on an edge in `Highlight`.
262
285
 
263
286
  ### Fields
264
287
 
288
+ <!-- example: fields -->
289
+
265
290
  ```html
266
291
  <div class="dk-field">
267
292
  <label class="dk-field-label" for="email">Email</label>
268
293
  <p class="dk-field-hint" id="email-hint">Where the receipt goes</p>
269
294
  <p class="dk-field-error" id="email-error">
270
- <span class="dk-visually-hidden">Error: </span>Enter an email address
295
+ <span class="dk-visually-hidden">Error: </span>
296
+ Enter an email address
271
297
  </p>
272
298
  <input
273
299
  class="dk-input"
274
- id="email"
275
300
  type="email"
301
+ id="email"
276
302
  aria-describedby="email-hint email-error"
277
303
  aria-invalid="true"
278
304
  />
279
305
  </div>
280
-
281
306
  <div class="dk-field">
282
307
  <label class="dk-field-label" for="sort">Sort by</label>
283
308
  <select class="dk-select" id="sort">
@@ -303,24 +328,31 @@ error, best after a submit is tried rather than while someone is typing.
303
328
  A control is as tall as a button, at both widths, and its text is never under 16px, so a phone does
304
329
  not zoom the page when the field is tapped. It sits on the page's own ground, so it reads as a well
305
330
  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.
331
+ invalid control's edge in the danger color too. In forced colors, where that color is gone, an
332
+ invalid control gains a dashed line around its edge in `CanvasText`, which moves nothing as the
333
+ error comes and goes, and gives way to the focus ring when it has focus. A disabled control is drawn
334
+ on the surface ground in the muted text. The select is a native `<select>`, its caret drawn as a
335
+ background, and in forced colors the browser's own arrow comes back. A select that shows its options
336
+ as a list, with `multiple` or a `size`, is left as the browser draws it, with no caret. A field a
337
+ visitor cannot change uses `disabled`, unlike a button, which stays in the tab order when it is off.
338
+ The field's hint says why it cannot be changed, and a screen reader reads that hint wherever it
339
+ announces the field. A button's reason sits beside it in text its `aria-describedby` names, read
340
+ when the keyboard reaches the button.
310
341
 
311
342
  ### Callouts
312
343
 
344
+ <!-- example: callout -->
345
+
313
346
  ```html
314
347
  <div class="dk-callout dk-callout-danger">
315
348
  <div class="dk-callout-body">
316
349
  <p class="dk-callout-title">
317
- <span class="dk-visually-hidden">Error: </span>The products could not be loaded
350
+ <span class="dk-visually-hidden">Error: </span>
351
+ The products could not be loaded
318
352
  </p>
319
353
  <p>Check the file, then load it again.</p>
320
354
  </div>
321
- <div class="dk-callout-actions">
322
- <button type="button" class="dk-button">Try again</button>
323
- </div>
355
+ <div class="dk-callout-actions"><button type="button" class="dk-button">Try again</button></div>
324
356
  </div>
325
357
  ```
326
358
 
@@ -382,6 +414,9 @@ export const Checkout = ({ error, saved }: Props) => {
382
414
  };
383
415
  ```
384
416
 
417
+ Every optional prop of every component takes `undefined` as if it were left out, so a wrapper that
418
+ passes on a prop it may not have been given compiles under `exactOptionalPropertyTypes` too.
419
+
385
420
  - `Button` takes the props of a `<button>` but `style`, `disabled`, `aria-disabled` and
386
421
  `aria-pressed`, and is `type="button"` unless `type` says otherwise. `accent` adds
387
422
  `dk-button-accent`. `pressed` writes `aria-pressed`, `false` included, and left undefined makes
@@ -392,11 +427,12 @@ export const Checkout = ({ error, saved }: Props) => {
392
427
  `className` follows the kit's classes. A `ref` holds the `<button>` drawn, for a page that moves
393
428
  focus to it, such as the button that opened a panel when the panel closes.
394
429
  - `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.
430
+ null, false, or a list or a fragment of only those drawing nothing. `id` is the control's, made
431
+ with React's `useId` unless given, and a page gives one when something links to the field, such as
432
+ an error summary. Every other prop goes to the `<input>` but `style` and `aria-invalid`. A
433
+ `className` follows `dk-input`, and an `aria-describedby` of the page's own is listed after the
434
+ hint and the error. A `ref` holds the `<input>` drawn, for a page that moves focus to the field,
435
+ such as a form that fails.
400
436
  - `SelectField` takes the same props, with its children as the options of the `<select>` and its
401
437
  `ref` holding the `<select>`.
402
438
  - `Callout` takes `tone`, `info` unless given, `success` or `danger`, and `title`, drawn at the
@@ -405,8 +441,11 @@ export const Checkout = ({ error, saved }: Props) => {
405
441
  before the title or the message, the tone's own unless given, and an empty string drops it.
406
442
  `live`, `status` or `alert`, puts that role on the message and draws the message even when it is
407
443
  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.
444
+ until it is filled. Every other prop goes to the callout's `<div>` but `style`, `role` and the
445
+ `title` attribute, inner HTML and the live attributes, since its message takes its role from
446
+ `live` alone, so a page gives it an `id`, such as one an error summary is linked by, and a
447
+ `className`, which follows the kit's. A `ref` holds the `<div>` drawn, for a page that moves focus
448
+ to it, as to a summary of what went wrong in a form that fails.
410
449
 
411
450
  ## Explainer pages
412
451
 
@@ -420,6 +459,8 @@ class would outweigh every piece set inside it.
420
459
 
421
460
  ### The page
422
461
 
462
+ <!-- example: explainerPage -->
463
+
423
464
  ```html
424
465
  <a class="dk-skip" href="#main-content">Skip to content</a>
425
466
  <div class="dk-strip">
@@ -468,11 +509,13 @@ caption size, its heading in the title size, and its text, with an h3 at the bod
468
509
 
469
510
  ### Steps
470
511
 
512
+ <!-- example: steps -->
513
+
471
514
  ```html
472
515
  <ol class="dk-steps" role="list">
473
516
  <li>
474
517
  <strong class="dk-steps-title">The order names a product.</strong>
475
- <span class="dk-steps-note">And how many of it.</span>
518
+ <span class="dk-steps-note">It says how many it wants.</span>
476
519
  </li>
477
520
  <li>
478
521
  <span class="dk-steps-arrow" aria-hidden="true">↓</span>
@@ -492,10 +535,15 @@ drops a list's meaning when its markers are gone.
492
535
 
493
536
  ### Panels
494
537
 
538
+ <!-- example: panel -->
539
+
495
540
  ```html
496
541
  <div class="dk-panel">
497
542
  <p class="dk-panel-title">How the numbers were taken</p>
498
- <p>...</p>
543
+ <p>
544
+ Each strategy ran three times on the same laptop with the same orders, and the table shows
545
+ the middle run.
546
+ </p>
499
547
  </div>
500
548
  ```
501
549
 
@@ -514,6 +562,8 @@ complementary to it. Nor is it a named `section`, which would be a region landma
514
562
 
515
563
  ### Tables
516
564
 
565
+ <!-- example: table -->
566
+
517
567
  ```html
518
568
  <p class="dk-table-title" id="bench-title">Orders a second under each strategy</p>
519
569
  <div class="dk-table-scroll" role="region" aria-labelledby="bench-title" tabindex="0">
@@ -552,13 +602,16 @@ WebKit. It scrolls in its frame instead.
552
602
 
553
603
  A table is in the small text, with each row ruled in the line color. Its column headers are the
554
604
  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".
605
+ padding on their outer side, so the table's text lines up with the prose around it. Figures sit to
606
+ the right and never wrap, while text in a cell wraps and keeps its first line level with the rest of
607
+ its row. Code in a cell stays on one line, hyphens and spaces alike, so a token's name never breaks
608
+ after `--dk-`, and a long expression scrolls the frame as a wide table does. A header gives its unit
609
+ in words a screen reader says well, "Slowest 1%, ms" rather than "p99 ms".
559
610
 
560
611
  ### Code blocks
561
612
 
613
+ <!-- example: code -->
614
+
562
615
  ```html
563
616
  <p class="dk-code-title" id="lock-title">The statement that holds the stock</p>
564
617
  <pre
@@ -586,11 +639,11 @@ script, so a block has no copy button.
586
639
  ### Code in running text
587
640
 
588
641
  `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.
642
+ 0.9em, so it follows the size of the text around it, a step under it since a mono face looks larger
643
+ at the same size. It breaks anywhere it must, with `overflow-wrap: anywhere`, since a long name has
644
+ no spaces to break at and would push a narrow column wider, except in a table cell, as Tables
645
+ describes. Inside a `pre` it takes the block's size and keeps its lines whole. It has no ground of
646
+ its own, and in a link it keeps the link's underline.
594
647
 
595
648
  ## Layers and palettes
596
649
 
@@ -655,7 +708,8 @@ from the package rather than keeping a copy.
655
708
 
656
709
  The colors are written with `light-dark()`, which sets the floor: Chrome and Edge 123, Firefox 120
657
710
  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.
711
+ Firefox 120 it draws its ground until it is filled, and a label that holds a checkbox or a radio
712
+ shows no pointer there, though the box or the dot does. The kit's browser tests run in Chromium.
659
713
 
660
714
  ## License
661
715
 
package/dist/base.css CHANGED
@@ -114,6 +114,28 @@
114
114
  accent-color: var(--dk-accent);
115
115
  }
116
116
 
117
+ /*
118
+ * What takes a click shows a pointer: a disclosure's summary, and a box or a dot with the label
119
+ * that holds it, which ticks it from anywhere on its words. One that is unavailable keeps the
120
+ * arrow. A label for a text field only moves focus to it, so it keeps the browser's own.
121
+ */
122
+ :where(
123
+ summary,
124
+ input[type='checkbox'],
125
+ input[type='radio'],
126
+ label:has(> input[type='checkbox'], > input[type='radio'])
127
+ ) {
128
+ cursor: pointer;
129
+ }
130
+
131
+ :where(
132
+ input[type='checkbox']:disabled,
133
+ input[type='radio']:disabled,
134
+ label:has(> input[type='checkbox']:disabled, > input[type='radio']:disabled)
135
+ ) {
136
+ cursor: default;
137
+ }
138
+
117
139
  /*
118
140
  * Out of sight but still read by a screen reader, as for a label the eye takes from what is
119
141
  * around it. It keeps one pixel, since a screen reader may skip an element with no size at all.
@@ -81,14 +81,22 @@
81
81
 
82
82
  /*
83
83
  * The switches wrap where larger text leaves no room, and stand at least as tall as the links'
84
- * row.
84
+ * row, against the strip's right edge, on the links' row or on one of their own where they
85
+ * wrap below it. A demo whose switches arrive after the page names their width, at least as
86
+ * wide as they are, in `--dk-strip-tools`, in the strip's own text size, so the room kept for
87
+ * them and the switches that fill it are one width, and a strip too narrow for the links and
88
+ * the switches wraps before they come as it does after. Without it, the section is as wide as
89
+ * what it holds.
85
90
  */
86
91
  .dk-strip-tools {
87
92
  display: flex;
88
93
  flex-wrap: wrap;
89
94
  align-items: center;
95
+ justify-content: flex-end;
90
96
  gap: var(--dk-space-3);
97
+ inline-size: var(--dk-strip-tools);
91
98
  min-height: var(--dk-strip);
99
+ margin-inline-start: auto;
92
100
  }
93
101
 
94
102
  /* The exit after a long scroll, its rule the width of the page and its text in the column. */
@@ -100,7 +108,9 @@
100
108
  font-size: var(--dk-text-small);
101
109
  }
102
110
 
111
+ /* The footer sentence holds to the measure, as prose does, while its rule runs full width. */
103
112
  :where(.dk-footer p) {
113
+ max-width: var(--dk-measure);
104
114
  margin: 0;
105
115
  }
106
116
 
@@ -135,7 +145,8 @@
135
145
 
136
146
  /*
137
147
  * On a phone the links are smaller and closer, and wrap to rows of their own where they must,
138
- * with the switches on a row below them.
148
+ * with the switches on a row below them, at its start and its full width whatever width a demo
149
+ * names.
139
150
  */
140
151
  @media (max-width: 48rem) {
141
152
  .dk-strip {
@@ -148,6 +159,9 @@
148
159
 
149
160
  .dk-strip-tools {
150
161
  flex-basis: 100%;
162
+ justify-content: flex-start;
163
+ inline-size: auto;
164
+ margin-inline-start: 0;
151
165
  padding-block: 0 var(--dk-space-2);
152
166
  }
153
167
 
@@ -350,7 +364,7 @@
350
364
  :where(.dk-callout) {
351
365
  display: grid;
352
366
  gap: var(--dk-space-2);
353
- padding: var(--dk-space-2) var(--dk-space-3);
367
+ padding: var(--dk-space-3) var(--dk-space-4);
354
368
  border: 1px solid transparent;
355
369
  border-radius: var(--dk-radius);
356
370
  background: var(--dk-highlight);
@@ -440,6 +454,22 @@
440
454
  padding-inline-end: var(--dk-field-inline);
441
455
  background-image: none;
442
456
  }
457
+
458
+ /*
459
+ * An invalid field loses its red edge to the system's colors, so it gains a dashed line
460
+ * around its edge in the text color, which keeps its contrast in every theme and moves
461
+ * nothing as the error comes and goes. With focus, the ring takes its place, so a
462
+ * keyboard sees where it landed.
463
+ */
464
+ :where(.dk-input[aria-invalid='true'], .dk-select[aria-invalid='true']) {
465
+ outline: var(--dk-ring-width) dashed CanvasText;
466
+ outline-offset: 1px;
467
+ }
468
+
469
+ :where(.dk-input[aria-invalid='true'], .dk-select[aria-invalid='true']):focus-visible {
470
+ outline: var(--dk-ring-width) solid var(--dk-accent);
471
+ outline-offset: var(--dk-ring-offset);
472
+ }
443
473
  }
444
474
 
445
475
  /*
@@ -643,8 +673,8 @@
643
673
  }
644
674
 
645
675
  /* Code in a cell keeps its name whole, so the frame scrolls rather than the name breaking. */
646
- :where(.dk-table code) {
647
- overflow-wrap: normal;
676
+ :where(.dk-table-scroll .dk-table code) {
677
+ white-space: nowrap;
648
678
  }
649
679
 
650
680
  /*
package/dist/react.d.ts CHANGED
@@ -11,35 +11,36 @@ export declare const GITHUB_PROFILE_URL = "https://github.com/jasonsdevspace";
11
11
  */
12
12
  export declare const SkipLink: ({ target, children, }: {
13
13
  /** The id of the element the link leads to. */
14
- readonly target?: string;
14
+ readonly target?: string | undefined;
15
15
  readonly children?: ReactNode;
16
16
  }) => import("react").JSX.Element;
17
17
  /** A page of the demo's that the chrome links to, and can mark as the one it sits on. */
18
18
  type ChromePage = 'about';
19
19
  /**
20
- * Whose demo this is, above the page, with three links together on the left. It is a navigation
20
+ * Whose demo this is, above the page, with three links together on the left: the author's name,
21
+ * which leads to the portfolio as the footer's does, how it works and the source. It is a navigation
21
22
  * landmark in a plain div rather than a header, so the demo's own header stays the page's one
22
23
  * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
23
24
  * named for them, and while they are on their way the strip keeps their room in that section,
24
25
  * unnamed and so no landmark.
25
26
  */
26
27
  export declare const PortfolioStrip: ({ about, source, current, tools, toolsLabel, toolsWaiting, }: {
27
- /** The demo's explainer, relative to the page or absolute. */
28
+ /** The demo's explainer, relative to the page or absolute, written by the demo's author. */
28
29
  readonly about: string;
29
- /** The demo's repository. */
30
+ /** The demo's repository, written by the demo's author. */
30
31
  readonly source: string;
31
32
  /** The link that is the page the strip sits on, marked as the current page. */
32
- readonly current?: ChromePage;
33
+ readonly current?: ChromePage | undefined;
33
34
  /** The demo's own switches, drawn beside the links. */
34
35
  readonly tools?: ReactNode;
35
36
  /** The name a screen reader gives the switches' landmark. */
36
- readonly toolsLabel?: string;
37
+ readonly toolsLabel?: string | undefined;
37
38
  /**
38
39
  * The switches are on their way, so the strip keeps the room of one row of the kit's controls
39
40
  * in their section, empty and unnamed, so no landmark, until `tools` holds them and the section
40
41
  * takes its name.
41
42
  */
42
- readonly toolsWaiting?: boolean;
43
+ readonly toolsWaiting?: boolean | undefined;
43
44
  }) => import("react").JSX.Element;
44
45
  /**
45
46
  * The exit after a long scroll: whose project this is, how it works, where the source and the rest
@@ -48,16 +49,19 @@ export declare const PortfolioStrip: ({ about, source, current, tools, toolsLabe
48
49
  export declare const DemoFooter: ({ name, about, source, current, license, licenseName, children, }: {
49
50
  /** The demo's name, as the footer's sentence opens with it. */
50
51
  readonly name: string;
51
- /** The demo's explainer, relative to the page or absolute. */
52
+ /** The demo's explainer, relative to the page or absolute, written by the demo's author. */
52
53
  readonly about: string;
53
- /** The demo's repository. */
54
+ /** The demo's repository, written by the demo's author. */
54
55
  readonly source: string;
55
56
  /** The link that is the page the footer sits on, marked as the current page. */
56
- readonly current?: ChromePage;
57
- /** The demo's license, the LICENSE file on the repository's main branch unless given. */
58
- readonly license?: string;
57
+ readonly current?: ChromePage | undefined;
58
+ /**
59
+ * The demo's license, written by the demo's author, or `LICENSE` on the main branch of `source`
60
+ * unless given.
61
+ */
62
+ readonly license?: string | undefined;
59
63
  /** What the license is called, as the footer's sentence names it. */
60
- readonly licenseName?: string;
64
+ readonly licenseName?: string | undefined;
61
65
  /** What the demo adds after the license, as sentences of its own. */
62
66
  readonly children?: ReactNode;
63
67
  }) => import("react").JSX.Element;
@@ -70,14 +74,14 @@ type NativeButton = Omit<ComponentPropsWithRef<'button'>, 'style' | 'disabled' |
70
74
  */
71
75
  export type ButtonProps = NativeButton & {
72
76
  /** Off, but still in the tab order, so a keyboard keeps its place and finds out why. */
73
- readonly off?: boolean;
77
+ readonly off?: boolean | undefined;
74
78
  } & ({
75
79
  readonly accent: true;
76
- readonly pressed?: never;
80
+ readonly pressed?: undefined;
77
81
  } | {
78
- readonly accent?: false;
82
+ readonly accent?: false | undefined;
79
83
  /** Whether the toggle is on. False still marks it a toggle, undefined an action. */
80
- readonly pressed?: boolean;
84
+ readonly pressed?: boolean | undefined;
81
85
  });
82
86
  /**
83
87
  * A button, of type button unless a page gives another, so it never submits a form by surprise.
@@ -95,7 +99,7 @@ type FieldParts = {
95
99
  /** What is wrong with the value, drawn only when given, which marks the control invalid. */
96
100
  readonly error?: ReactNode;
97
101
  /** The control's id, made with useId unless a page gives one, as it does for a link to it. */
98
- readonly id?: string;
102
+ readonly id?: string | undefined;
99
103
  };
100
104
  export type InputFieldProps = FieldParts & Omit<ComponentPropsWithRef<'input'>, 'style' | 'id' | 'aria-invalid' | 'children'>;
101
105
  /**
@@ -113,25 +117,31 @@ export declare const SelectField: (props: SelectFieldProps) => import("react").J
113
117
  /** What a callout is about: news, something that went right, or something that went wrong. */
114
118
  export type CalloutTone = 'info' | 'success' | 'danger';
115
119
  /**
116
- * A message set apart from the page, with an optional title before it and actions after it. It has
117
- * no role unless the page gives `live`, whatever its tone, since a message on the page when it
118
- * loads is read in its place. A live message is drawn even when empty, so the page can mount it
119
- * first and fill it after, which is when a screen reader reads it. The role sits on the message,
120
- * which holds the title and the tone's word too, so they are read with it, and the actions are
121
- * not.
120
+ * A callout's own props, beside a `<div>`'s own, its `id`, a page's class, drawn after the kit's,
121
+ * and its `ref` among them, which holds the callout drawn for a page that moves focus to it, such
122
+ * as a summary of what went wrong in a form. Its `title` is the callout's, not the attribute.
122
123
  */
123
- export declare const Callout: ({ tone, title, headingLevel, live, prefix, actions, children, }: {
124
- readonly tone?: CalloutTone;
124
+ export type CalloutProps = Omit<ComponentPropsWithRef<'div'>, 'style' | 'role' | 'title' | 'children' | 'dangerouslySetInnerHTML' | 'aria-live' | 'aria-atomic' | 'aria-relevant'> & {
125
+ readonly tone?: CalloutTone | undefined;
125
126
  /** What the callout is about, drawn before its message. */
126
127
  readonly title?: ReactNode;
127
128
  /** The title's heading level, where the callout is a section a visitor would jump to. */
128
- readonly headingLevel?: 2 | 3 | 4 | 5 | 6;
129
+ readonly headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined;
129
130
  /** A message a screen reader reads when it changes, politely or at once. */
130
- readonly live?: "status" | "alert";
131
+ readonly live?: 'status' | 'alert' | undefined;
131
132
  /** The word read before the title or the message, the tone's own unless given. */
132
- readonly prefix?: string;
133
+ readonly prefix?: string | undefined;
133
134
  /** Buttons or links that act on the message. */
134
135
  readonly actions?: ReactNode;
135
136
  readonly children?: ReactNode;
136
- }) => import("react").JSX.Element;
137
+ };
138
+ /**
139
+ * A message set apart from the page, with an optional title before it and actions after it. It has
140
+ * no role unless the page gives `live`, whatever its tone, since a message on the page when it
141
+ * loads is read in its place. A live message is drawn even when empty, so the page can mount it
142
+ * first and fill it after, which is when a screen reader reads it. The role sits on the message,
143
+ * which holds the title and the tone's word too, so they are read with it, and the actions are
144
+ * not.
145
+ */
146
+ export declare const Callout: ({ tone, title, headingLevel, live, prefix, actions, className, children, ...native }: CalloutProps) => import("react").JSX.Element;
137
147
  export {};
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useId } from 'react';
2
+ import { Children, Fragment, isValidElement, useId, } from 'react';
3
3
  /*
4
4
  * The page chrome a demo draws around its page, a skip link, a strip above the page that says
5
5
  * whose demo it is and a footer below it, and the buttons, fields and callouts on the page itself.
@@ -8,8 +8,15 @@ import { useId } from 'react';
8
8
  * page whose policy allows styles only from its own origin draws it. A control's state is a
9
9
  * native or ARIA attribute, never a class of the kit's.
10
10
  */
11
- /** Whether a slot holds something to draw: a demo's `cond && <X />` leaves false or null. */
12
- const drawn = (node) => node !== undefined && node !== null && node !== false;
11
+ /**
12
+ * Whether a slot holds something to draw: a demo's `cond && <X />` leaves false or null, an empty
13
+ * string draws nothing either, and nor does a list or a fragment of only those. Every slot a
14
+ * component leaves out when it is empty is read the same way. A number draws, 0 included, as React
15
+ * draws it, so a page that writes `count && <X />` writes `count > 0 && <X />` instead.
16
+ */
17
+ const filled = (node) => Children.toArray(node).some((child) => isValidElement(child) && child.type === Fragment
18
+ ? filled(child.props.children)
19
+ : child !== '');
13
20
  /** Where the portfolio is, and the section of it that lists the work. */
14
21
  export const PORTFOLIO_URL = 'https://jasonsdevspace.com/';
15
22
  export const PORTFOLIO_WORK_URL = 'https://jasonsdevspace.com/#work';
@@ -24,20 +31,19 @@ export const SkipLink = ({ target = 'main-content', children = 'Skip to content'
24
31
  /** The mark a link to `page` takes when it is the page the chrome sits on, and none otherwise. */
25
32
  const currentAs = (current, page) => current === page ? 'page' : undefined;
26
33
  /**
27
- * Whose demo this is, above the page, with three links together on the left. It is a navigation
34
+ * Whose demo this is, above the page, with three links together on the left: the author's name,
35
+ * which leads to the portfolio as the footer's does, how it works and the source. It is a navigation
28
36
  * landmark in a plain div rather than a header, so the demo's own header stays the page's one
29
37
  * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
30
38
  * named for them, and while they are on their way the strip keeps their room in that section,
31
39
  * unnamed and so no landmark.
32
40
  */
33
- export const PortfolioStrip = ({ about, source, current, tools, toolsLabel = 'Demo', toolsWaiting = false, }) => (_jsxs("div", { className: "dk-strip", children: [_jsxs("nav", { className: "dk-strip-links", "aria-label": "Portfolio", children: [_jsx("a", { href: PORTFOLIO_WORK_URL, children: "A demo by Jason Poteet" }), _jsx("a", { href: about, "aria-current": currentAs(current, 'about'), children: "How it works" }), _jsx("a", { href: source, children: "Source on GitHub" })] }), drawn(tools) ? (_jsx("section", { className: "dk-strip-tools", "aria-label": toolsLabel, children: tools })) : toolsWaiting ? (_jsx("section", { className: "dk-strip-tools", children: _jsx("span", { className: "dk-strip-room" }) })) : null] }));
41
+ export const PortfolioStrip = ({ about, source, current, tools, toolsLabel = 'Demo', toolsWaiting = false, }) => (_jsxs("div", { className: "dk-strip", children: [_jsxs("nav", { className: "dk-strip-links", "aria-label": "Portfolio", children: [_jsx("a", { href: PORTFOLIO_URL, children: "Jason Poteet" }), _jsx("a", { href: about, "aria-current": currentAs(current, 'about'), children: "How it works" }), _jsx("a", { href: source, children: "Source on GitHub" })] }), filled(tools) ? (_jsx("section", { className: "dk-strip-tools", "aria-label": toolsLabel, children: tools })) : toolsWaiting ? (_jsx("section", { className: "dk-strip-tools", children: _jsx("span", { className: "dk-strip-room" }) })) : null] }));
34
42
  /**
35
43
  * The exit after a long scroll: whose project this is, how it works, where the source and the rest
36
44
  * of the work are, and the license, with anything the demo adds of its own after them.
37
45
  */
38
- export const DemoFooter = ({ name, about, source, current, license = `${source.endsWith('/') ? source.slice(0, -1) : source}/blob/main/LICENSE`, licenseName = 'MIT license', children, }) => (_jsx("footer", { className: "dk-footer", children: _jsxs("p", { children: [name, " is a portfolio project by ", _jsx("a", { href: PORTFOLIO_URL, children: "Jason Poteet" }), ".", ' ', _jsx("a", { href: about, "aria-current": currentAs(current, 'about'), children: "How it works" }), ", in plain words. The ", _jsx("a", { href: source, children: "source" }), " and", ' ', _jsx("a", { href: GITHUB_PROFILE_URL, children: "more work" }), " are on GitHub, under the", ' ', _jsx("a", { href: license, children: licenseName }), ".", drawn(children) ? _jsxs(_Fragment, { children: [" ", children] }) : null] }) }));
39
- /** Whether a slot holds words to draw, an empty string being none. */
40
- const filled = (node) => drawn(node) && node !== '';
46
+ export const DemoFooter = ({ name, about, source, current, license = `${source.endsWith('/') ? source.slice(0, -1) : source}/blob/main/LICENSE`, licenseName = 'MIT license', children, }) => (_jsx("footer", { className: "dk-footer", children: _jsxs("p", { children: [name, " is a portfolio project by ", _jsx("a", { href: PORTFOLIO_URL, children: "Jason Poteet" }), ".", ' ', _jsx("a", { href: about, "aria-current": currentAs(current, 'about'), children: "How it works" }), ", in plain words. The ", _jsx("a", { href: source, children: "source" }), " and", ' ', _jsx("a", { href: GITHUB_PROFILE_URL, children: "more work" }), " are on GitHub, under the", ' ', _jsx("a", { href: license, children: licenseName }), ".", filled(children) ? _jsxs(_Fragment, { children: [" ", children] }) : null] }) }));
41
47
  /**
42
48
  * A list of names an attribute holds, such as the classes a component draws, its own first and a
43
49
  * page's after them, or the ids that describe a control, with any left out or empty dropped.
@@ -112,6 +118,12 @@ const TONE_WORDS = {
112
118
  };
113
119
  /** The element a callout's title is drawn as, at each heading level a page may give it. */
114
120
  const HEADINGS = { 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' };
121
+ /**
122
+ * A tone or a heading level the types refuse, as a page in plain JavaScript can pass, draws news
123
+ * and a paragraph, rather than a class the sheet has no rule for or an element React refuses.
124
+ */
125
+ const toneOf = (tone) => Object.hasOwn(TONE_WORDS, tone) ? tone : 'info';
126
+ const titleOf = (level) => level !== undefined && Object.hasOwn(HEADINGS, level) ? HEADINGS[level] : 'p';
115
127
  /**
116
128
  * A message set apart from the page, with an optional title before it and actions after it. It has
117
129
  * no role unless the page gives `live`, whatever its tone, since a message on the page when it
@@ -120,11 +132,12 @@ const HEADINGS = { 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' };
120
132
  * which holds the title and the tone's word too, so they are read with it, and the actions are
121
133
  * not.
122
134
  */
123
- export const Callout = ({ tone = 'info', title, headingLevel, live, prefix = TONE_WORDS[tone], actions, children, }) => {
124
- // A level the types refuse, as a page in plain JavaScript can pass, draws a paragraph.
125
- const Title = (headingLevel === undefined ? undefined : HEADINGS[headingLevel]) ?? 'p';
135
+ export const Callout = ({ tone = 'info', title, headingLevel, live, prefix, actions, className, children, ...native }) => {
136
+ const kind = toneOf(tone);
137
+ const spoken = prefix ?? TONE_WORDS[kind];
138
+ const Title = titleOf(headingLevel);
126
139
  const titled = filled(title);
127
140
  const said = filled(children);
128
- const word = prefix === '' ? null : _jsx("span", { className: "dk-visually-hidden", children: prefix });
129
- return (_jsxs("div", { className: spaced('dk-callout', tone !== 'info' && `dk-callout-${tone}`), children: [titled || said || live !== undefined ? (_jsxs("div", { className: "dk-callout-body", role: live, children: [titled ? (_jsxs(Title, { className: "dk-callout-title", children: [word, title] })) : null, !titled && said ? word : null, said ? children : null] })) : null, filled(actions) ? _jsx("div", { className: "dk-callout-actions", children: actions }) : null] }));
141
+ const word = spoken === '' ? null : _jsx("span", { className: "dk-visually-hidden", children: spoken });
142
+ return (_jsxs("div", { ...native, role: undefined, style: undefined, dangerouslySetInnerHTML: undefined, "aria-live": undefined, "aria-atomic": undefined, "aria-relevant": undefined, className: spaced('dk-callout', kind !== 'info' && `dk-callout-${kind}`, className), children: [titled || said || live !== undefined ? (_jsxs("div", { className: "dk-callout-body", role: live, children: [titled ? (_jsxs(Title, { className: "dk-callout-title", children: [word, title] })) : null, !titled && said ? word : null, said ? children : null] })) : null, filled(actions) ? _jsx("div", { className: "dk-callout-actions", children: actions }) : null] }));
130
143
  };
package/dist/tokens.css CHANGED
@@ -100,6 +100,11 @@
100
100
  --dk-target: 2.75rem;
101
101
  /* The height of the portfolio strip's row, and of each link in it. */
102
102
  --dk-strip: 2.5rem;
103
+ /*
104
+ * The width of a demo's switches in the strip, in the strip's own text size, which a demo
105
+ * whose switches arrive after the page names, so the room kept for them holds it too.
106
+ */
107
+ --dk-strip-tools: auto;
103
108
 
104
109
  /*
105
110
  * The width a page of reading holds to, no more than 80 characters a line in the body size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonsdevspace/design-kit",
3
- "version": "0.1.0-next.3",
3
+ "version": "0.1.0-next.5",
4
4
  "description": "Design tokens, base styles, a theme script, page chrome, buttons, fields, callouts and an explainer layout for the demos on jasonsdevspace.com.",
5
5
  "keywords": [
6
6
  "design-tokens",