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

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,76 @@ 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.4
11
+
12
+ The strip's first link says whose work it leads to, and the footer's sentence holds to the measure.
13
+ Every optional prop takes `undefined`, the callout takes the props of a `<div>` and is padded as
14
+ steps and code blocks are, and every empty slot is read alike. The sheets show a pointer on what
15
+ takes a click, mark an invalid field in forced colors and keep a token's name whole in a table.
16
+
17
+ - **Page chrome.** The strip's first link reads "More work by Jason Poteet", where it read "A demo
18
+ by Jason Poteet", which fits a demo and the kit's own site alike and says where the link goes. A
19
+ page that writes the strip by hand changes the link's words itself. The footer's sentence holds
20
+ to the measure, while its rule still runs the page's width.
21
+ - **Types.** Every optional prop of every component takes `undefined`, as if it were left out, so a
22
+ wrapper that passes on a prop it may not have been given compiles under
23
+ `exactOptionalPropertyTypes`. Only the types widen, so nothing that compiled before breaks.
24
+ - **Callouts.** `Callout` takes the props of a `<div>` but `style`, `role`, the `title` attribute,
25
+ inner HTML and the live attributes, among them an `id`, a page's class after the kit's, and a
26
+ `ref` that holds the callout drawn, so a page can link to an error summary, focus it after a form
27
+ fails, or lay it out. Its props are exported as `CalloutProps`. Its message takes its role from
28
+ `live` alone. A tone or a heading level the types refuse, as plain JavaScript can pass, draws news
29
+ and a paragraph. A callout is padded 0.75rem by 1rem, from 0.5rem by 0.75rem, as steps and code
30
+ blocks are.
31
+ - **Empty slots.** Every slot a component leaves out when it is empty reads an empty string, or a
32
+ list or a fragment that holds only false, null or empty strings, as nothing. The strip's switches
33
+ and the footer's additions now read an empty string so too. A field's hint and error and a
34
+ callout's title, message and actions now read such a list or fragment as nothing, where before
35
+ they drew an empty paragraph, which a hint or an error also listed in `aria-describedby`.
36
+ - **Pointers.** `base.css` shows a pointer on a disclosure's summary and on a checkbox or a radio
37
+ with the label that holds it, and the arrow on one that is unavailable. The label's pointer reads
38
+ `:has()`, so in Firefox 120 only the box or the dot shows it.
39
+ - **Fields.** In forced colors, where an invalid control's red edge is gone, it gains a dashed line
40
+ around its edge in the system's text color, `CanvasText`, which moves nothing as the error comes
41
+ and goes. With focus, the ring takes its place.
42
+ - **Tables.** Code in a table cell within its frame stays on one line, so a token's name never
43
+ breaks after `--dk-`, and a long expression scrolls the frame as a wide table does.
44
+ - **Docs.** The README shows the strip's switch as a kit button and an unavailable button with the
45
+ text that says why, says why a field uses `disabled` where a button stays in the tab order, and
46
+ says the strip's and the footer's link props take only URLs the demo's author writes.
47
+
48
+ ## 0.1.0-next.3
49
+
50
+ The page chrome holds still and shows where a reader is, the controls a page moves focus to take a
51
+ `ref`, and an empty live callout draws nothing. The strip keeps room for a demo's switches before
52
+ they arrive, the footer marks the current page as the strip does, the link to the current page
53
+ stands out by more than its color, and the skip link stays hidden wherever it sits.
54
+
55
+ - **Page chrome.** `PortfolioStrip` takes `toolsWaiting`, which keeps room for one row of the kit's
56
+ controls while a demo's switches are on their way, so nothing below the strip moves when they
57
+ arrive. The section that holds the room is left unnamed, so it is no landmark until the switches
58
+ fill it. A page without React writes `.dk-strip-room` in the unnamed section, and names the
59
+ section when it fills it.
60
+ - **Page chrome.** `DemoFooter` takes `current`, as `PortfolioStrip` does, and set to `about` marks
61
+ the link to how it works as the current page, for the footer on a demo's explainer. In the strip
62
+ or the footer, the link marked as the current page stands in the page's text at the heading's
63
+ weight, so a sighted reader sees that "How it works" leads where they already are. It keeps its
64
+ underline, and the weight holds in forced colors, where the color does not.
65
+ - **Page chrome.** The skip link waits in its own place as the visually hidden class does, one pixel
66
+ clipped to nothing, rather than 10rem above it, so one inside a positioned box lower on the page
67
+ no longer shows over what is above the box or takes its clicks. It shows where it did when a
68
+ keyboard focuses it.
69
+ - **Buttons.** `Button`'s props take a `ref`, which holds the `<button>` drawn, so a page that
70
+ moves focus to a button, such as the one that opened a panel when the panel closes, uses the
71
+ kit's component. React 19 already passed the ref through, and only the type refused it.
72
+ - **Fields.** `InputField` and `SelectField` take a `ref` in the same way, which holds the
73
+ `<input>` or the `<select>` drawn, so a page that moves focus to a field, such as a form that
74
+ fails, uses the kit's component rather than finding the control by its id.
75
+ - **Callouts.** A live callout whose message holds nothing at all, with no actions, draws nothing,
76
+ no ground, padding or edge, and stays in the page so a screen reader reads what fills it. A page
77
+ no longer needs a holder of its own to hide one it mounts before it has words. The rule uses
78
+ `:has()`, which Firefox has from 121, so in Firefox 120 such a callout still draws its ground.
79
+
10
80
  ## 0.1.0-next.2
11
81
 
12
82
  The page chrome every demo draws, and buttons, text fields, selects and callouts, styled by
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, the measure and column widths, a
58
+ 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, its list of work and the GitHub
77
+ profile the chrome links to. It has no component for the explainer or the pieces in it, which a
78
+ page writes by hand. It takes React `^19.3.0` as an optional peer, which a page that writes the
79
+ 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,38 +104,71 @@ 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
- The strip holds a navigation landmark named Portfolio, with three links in this order: whose demo
111
- it is, how it works and the source. The strip on the demo's explainer marks the explainer's link
112
- with `aria-current="page"`. A demo with switches of its own puts them in a section after the links,
113
- named for them, and a demo with none leaves the section out.
114
+ A skip link can also sit inside a box of the page's own, such as one at the top of a column of
115
+ filters that leads past them to the results. It waits in its own place as the visually hidden class
116
+ does, one pixel clipped to nothing, so it draws nothing and takes no click wherever the box starts
117
+ on the page. A box that is positioned holds it, and it shows at the top of that box, inset as it is
118
+ at the top of a page, when a keyboard reaches it.
119
+
120
+ The strip holds a navigation landmark named Portfolio, with links in this order: the author's other
121
+ work, which names who made the demo, how it works and the source. It reads "More work by Jason
122
+ Poteet", which fits a demo and the kit's own site alike. The strip on the demo's explainer marks the
123
+ explainer's link with `aria-current="page"`. A demo with switches of its own puts them in a section
124
+ after the links, named for them, and a demo with none leaves the section out.
125
+
126
+ <!-- example: strip -->
114
127
 
115
128
  ```html
116
129
  <div class="dk-strip">
117
130
  <nav class="dk-strip-links" aria-label="Portfolio">
118
- <a href="https://jasonsdevspace.com/#work">A demo by Jason Poteet</a>
131
+ <a href="https://jasonsdevspace.com/#work">More work by Jason Poteet</a>
119
132
  <a href="./about/">How it works</a>
120
133
  <a href="https://github.com/jasonsdevspace/example">Source on GitHub</a>
121
134
  </nav>
122
135
  <section class="dk-strip-tools" aria-label="Demo">
123
- <button type="button">Tour</button>
136
+ <button type="button" class="dk-button">Tour</button>
124
137
  </section>
125
138
  </div>
126
139
  ```
127
140
 
141
+ A demo whose switches are still on their way, drawn by a script that has not run yet, keeps their
142
+ room so nothing below the strip moves when they come. The section is drawn with no name and holds
143
+ only the room, with no text, so it is no landmark and a screen reader finds nothing in it. When the
144
+ switches come they take the room's place in the same section, and the section takes its name.
145
+
146
+ <!-- example: stripWaiting -->
147
+
148
+ ```html
149
+ <section class="dk-strip-tools"><span class="dk-strip-room"></span></section>
150
+ ```
151
+
152
+ The room holds the height of one row of the kit's buttons or fields, beside the links on a wide
153
+ screen and on a row of their own on a phone. Switches of another height, or more than fit on one
154
+ row, still move what is below them when they come.
155
+
128
156
  On a wide screen the strip is one 2.5rem row over a 1px rule, in the small text, with its links
129
157
  1.5rem apart and the switches on the right. At 48rem wide or less the text steps down to the
130
158
  caption size, the links close to 1rem apart and wrap where they must, and the switches take a row
131
159
  of their own below them.
132
160
 
133
161
  The footer is one paragraph that says whose project the demo is, with the author's name linking the
134
- portfolio, and links how it works, the source, the rest of the work and the license. Anything the
135
- demo adds follows the license, as sentences of its own. Every link in the chrome keeps its
136
- underline, and in the footer's sentence the underline is what tells a link from the text around it.
162
+ portfolio, and links how it works, the source, the rest of the work and the license. The footer on
163
+ the demo's explainer marks the link to how it works with `aria-current="page"`, as the strip does.
164
+ Anything the demo adds follows the license, as sentences of its own. The sentence holds to the
165
+ measure, as prose does, while the footer's rule runs the page's width. Every link in the chrome
166
+ keeps its underline, and in the footer's sentence the underline is what tells a link from the text
167
+ around it. The link marked as the current page, in the strip or the footer, stands in the page's
168
+ text at the heading's weight, so it is told from the links around it by more than its color, in
169
+ forced colors too, and still reads as a link.
170
+
171
+ <!-- example: footer -->
137
172
 
138
173
  ```html
139
174
  <footer class="dk-footer">
@@ -154,18 +189,14 @@ is another width sets `--dk-column` to it, as it sets a color role.
154
189
  A page with React draws the same markup from the components.
155
190
 
156
191
  ```tsx
157
- import { DemoFooter, PortfolioStrip, SkipLink } from '@jasonsdevspace/design-kit/react';
192
+ import { Button, DemoFooter, PortfolioStrip, SkipLink } from '@jasonsdevspace/design-kit/react';
158
193
 
159
194
  const source = 'https://github.com/jasonsdevspace/example';
160
195
 
161
196
  export const Page = () => (
162
197
  <>
163
198
  <SkipLink />
164
- <PortfolioStrip
165
- about="./about/"
166
- source={source}
167
- tools={<button type="button">Tour</button>}
168
- />
199
+ <PortfolioStrip about="./about/" source={source} tools={<Button>Tour</Button>} />
169
200
  <main id="main-content" tabIndex={-1}>
170
201
  <h1>Example</h1>
171
202
  </main>
@@ -176,15 +207,23 @@ export const Page = () => (
176
207
 
177
208
  - `SkipLink` leads to `main-content` unless `target` names another id, and its children replace
178
209
  the words "Skip to content".
179
- - `PortfolioStrip` takes `about`, the demo's explainer, and `source`, its repository. `current`
180
- set to `about` marks the explainer's link as the current page. `tools` holds the demo's own
181
- switches, and `toolsLabel` names their landmark, `Demo` unless given. With `tools` undefined,
182
- null or false the strip draws no section for them, so a demo can pass `hasTour && <Tour />`.
210
+ - `PortfolioStrip` takes `about`, the demo's explainer, and `source`, its repository. `current` set
211
+ to `about` marks the explainer's link as the current page. `tools` holds the demo's own switches,
212
+ and `toolsLabel` names their landmark, `Demo` unless given. With `tools` undefined, null, false,
213
+ an empty string, or a list or a fragment of only those, the strip draws no section for them, so a
214
+ demo can pass `hasTour && <Tour />`. `toolsWaiting` keeps the room of one row of the kit's
215
+ controls while `tools` draws nothing, for a page drawn before the switches can be, in the same
216
+ section unnamed until they come.
183
217
  - `DemoFooter` takes `name`, the demo's name as the sentence opens with it, `about` and `source`.
218
+ `current` set to `about` marks the explainer's link as the current page, as on the strip.
184
219
  `license` is the link to the license, `LICENSE` on the main branch of `source` unless given, with
185
- any trailing slash on `source` dropped first. `licenseName` is the license's name in the
186
- sentence, `MIT license` unless given. Its children follow the license, and children that are
187
- undefined, null or false add nothing.
220
+ any trailing slash on `source` dropped first. `licenseName` is the license's name in the sentence,
221
+ `MIT license` unless given. Its children follow the license, and children that are undefined,
222
+ null, false, an empty string, or a list or a fragment of only those, add nothing.
223
+
224
+ The strip's and the footer's `about`, `source` and `license` go into their links unchecked, so each
225
+ is a URL the demo's author writes, never one taken from the page's address or a visitor's input,
226
+ since one taken from either could send a visitor anywhere.
188
227
 
189
228
  ## Buttons, fields and callouts
190
229
 
@@ -199,11 +238,16 @@ wins over all of them.
199
238
 
200
239
  ### Buttons
201
240
 
241
+ <!-- example: buttons -->
242
+
202
243
  ```html
203
244
  <button type="button" class="dk-button">Filters</button>
204
245
  <button type="button" class="dk-button dk-button-accent">Save rule</button>
205
246
  <button type="button" class="dk-button" aria-pressed="true">Members only</button>
206
- <button type="button" class="dk-button" aria-disabled="true">Previous</button>
247
+ <button aria-describedby="previous-why" type="button" class="dk-button" aria-disabled="true">
248
+ Previous
249
+ </button>
250
+ <p id="previous-why" class="dk-field-hint">This is the first page.</p>
207
251
  ```
208
252
 
209
253
  A button is `type="button"` unless the page gives it another type, so it never submits a form by
@@ -213,15 +257,16 @@ toggle that stands alone carries `aria-pressed`, `"true"` or `"false"`, and keep
213
257
  both states, since the attribute already says which it is. An action carries no `aria-pressed` at
214
258
  all. A button with `aria-expanded="true"`, whose panel is open, is drawn as a pressed one.
215
259
 
216
- A button that is off carries `aria-disabled="true"` rather than `disabled`. A disabled button that
217
- has focus drops it, and leaves the tab order, while an aria-disabled one stays where the keyboard
218
- can reach it and find out why. The attribute does nothing by itself, so a page that writes this
219
- markup makes its own click handler check `aria-disabled` and return early, calling
260
+ An unavailable button, `off` in React, carries `aria-disabled="true"` rather than `disabled`. A
261
+ disabled button that has focus drops it, and leaves the tab order, while an aria-disabled one stays
262
+ where the keyboard can reach it and find out why. The attribute does nothing by itself, so a page
263
+ that writes this markup makes its own click handler check `aria-disabled` and return early, calling
220
264
  `preventDefault()` so a submit button does not submit. The sheet draws a `disabled` button, and one
221
- inside a disabled fieldset, the same as an off one, and an accent button that is off keeps its
222
- class and is drawn as any off button is. A page says why a button is off in text beside it. A
223
- form's submit button is better left on while the form is incomplete, so a press shows the errors
224
- rather than doing nothing.
265
+ inside a disabled fieldset, the same as an off one, and an accent button that is off keeps its class
266
+ and is drawn as any off button is. A page says why a button is off in text beside it, such as a
267
+ `.dk-field-hint` paragraph the button's `aria-describedby` names, a class that works beside any
268
+ control. A form's submit button is better left on while the form is incomplete, so a press shows the
269
+ errors rather than doing nothing.
225
270
 
226
271
  A button is 2.25rem tall, `--dk-field-height`, as tall as a field, and 2.75rem, `--dk-target`, at
227
272
  48rem wide or less, and it is never narrower than it is tall. It has no ground of its own, so it
@@ -236,22 +281,24 @@ off toggle that is on an edge in `Highlight`.
236
281
 
237
282
  ### Fields
238
283
 
284
+ <!-- example: fields -->
285
+
239
286
  ```html
240
287
  <div class="dk-field">
241
288
  <label class="dk-field-label" for="email">Email</label>
242
289
  <p class="dk-field-hint" id="email-hint">Where the receipt goes</p>
243
290
  <p class="dk-field-error" id="email-error">
244
- <span class="dk-visually-hidden">Error: </span>Enter an email address
291
+ <span class="dk-visually-hidden">Error: </span>
292
+ Enter an email address
245
293
  </p>
246
294
  <input
247
295
  class="dk-input"
248
- id="email"
249
296
  type="email"
297
+ id="email"
250
298
  aria-describedby="email-hint email-error"
251
299
  aria-invalid="true"
252
300
  />
253
301
  </div>
254
-
255
302
  <div class="dk-field">
256
303
  <label class="dk-field-label" for="sort">Sort by</label>
257
304
  <select class="dk-select" id="sort">
@@ -277,24 +324,31 @@ error, best after a submit is tried rather than while someone is typing.
277
324
  A control is as tall as a button, at both widths, and its text is never under 16px, so a phone does
278
325
  not zoom the page when the field is tapped. It sits on the page's own ground, so it reads as a well
279
326
  on a card. The hint and the error are in the caption size, the error in the danger color, and an
280
- invalid control's edge in the danger color too. A disabled control is drawn on the surface ground
281
- in the muted text. The select is a native `<select>`, its caret drawn as a background, and in forced
282
- colors the browser's own arrow comes back. A select that shows its options as a list, with
283
- `multiple` or a `size`, is left as the browser draws it, with no caret.
327
+ invalid control's edge in the danger color too. In forced colors, where that color is gone, an
328
+ invalid control gains a dashed line around its edge in `CanvasText`, which moves nothing as the
329
+ error comes and goes, and gives way to the focus ring when it has focus. A disabled control is drawn
330
+ on the surface ground in the muted text. The select is a native `<select>`, its caret drawn as a
331
+ background, and in forced colors the browser's own arrow comes back. A select that shows its options
332
+ as a list, with `multiple` or a `size`, is left as the browser draws it, with no caret. A field a
333
+ visitor cannot change uses `disabled`, unlike a button, which stays in the tab order when it is off.
334
+ The field's hint says why it cannot be changed, and a screen reader reads that hint wherever it
335
+ announces the field. A button's reason sits beside it in text its `aria-describedby` names, read
336
+ when the keyboard reaches the button.
284
337
 
285
338
  ### Callouts
286
339
 
340
+ <!-- example: callout -->
341
+
287
342
  ```html
288
343
  <div class="dk-callout dk-callout-danger">
289
344
  <div class="dk-callout-body">
290
345
  <p class="dk-callout-title">
291
- <span class="dk-visually-hidden">Error: </span>The products could not be loaded
346
+ <span class="dk-visually-hidden">Error: </span>
347
+ The products could not be loaded
292
348
  </p>
293
349
  <p>Check the file, then load it again.</p>
294
350
  </div>
295
- <div class="dk-callout-actions">
296
- <button type="button" class="dk-button">Try again</button>
297
- </div>
351
+ <div class="dk-callout-actions"><button type="button" class="dk-button">Try again</button></div>
298
352
  </div>
299
353
  ```
300
354
 
@@ -314,8 +368,12 @@ at once, on `.dk-callout-body`, so the title and the tone's word are read with t
314
368
  actions are not. The page puts a live callout on the page empty first and fills it after, which is
315
369
  when a screen reader reads it. A page filling a success or danger callout puts the hidden word in
316
370
  with the message, as `<span class="dk-visually-hidden">Success: </span>Saved.`, since the word is
317
- part of what is read. An empty live callout still draws its ground and padding, so a page places it
318
- where an empty band reads as space, or leaves it out of the layout's flow until it is filled.
371
+ part of what is read. A live callout whose message holds nothing at all, not even a space, a line
372
+ break or the tone's word, and that has no actions draws nothing, no ground, padding or edge, until
373
+ it is filled, so a page puts it where its words will go with no holder of its own to hide it. It
374
+ still takes its place in the flow, so a margin or a gap around it stays. The rule reads the message
375
+ with `:has()`, which Firefox has from 121, so in Firefox 120 an empty live callout still draws its
376
+ ground.
319
377
 
320
378
  A callout's edge is transparent, and shows only in forced colors, where the ground is dropped and
321
379
  the edge marks where the callout starts and ends.
@@ -352,6 +410,9 @@ export const Checkout = ({ error, saved }: Props) => {
352
410
  };
353
411
  ```
354
412
 
413
+ Every optional prop of every component takes `undefined` as if it were left out, so a wrapper that
414
+ passes on a prop it may not have been given compiles under `exactOptionalPropertyTypes` too.
415
+
355
416
  - `Button` takes the props of a `<button>` but `style`, `disabled`, `aria-disabled` and
356
417
  `aria-pressed`, and is `type="button"` unless `type` says otherwise. `accent` adds
357
418
  `dk-button-accent`. `pressed` writes `aria-pressed`, `false` included, and left undefined makes
@@ -359,20 +420,28 @@ export const Checkout = ({ error, saved }: Props) => {
359
420
  `aria-disabled="true"` and puts a click handler that calls `preventDefault()` in place of
360
421
  `onClick`, so the page's click handler does not run and a submit button does not submit. Any
361
422
  other handler the page passes, for keys or a pointer, still runs, and checks `off` itself. A
362
- `className` follows the kit's classes.
423
+ `className` follows the kit's classes. A `ref` holds the `<button>` drawn, for a page that moves
424
+ focus to it, such as the button that opened a panel when the panel closes.
363
425
  - `InputField` takes `label`, and `hint` and `error`, each drawn only when given, an empty string,
364
- null or false drawing nothing. `id` is the control's, made with React's `useId` unless given, and
365
- a page gives one when something links to the field, such as an error summary. Every other prop
366
- goes to the `<input>` but `style` and `aria-invalid`. A `className` follows `dk-input`, and an
367
- `aria-describedby` of the page's own is listed after the hint and the error.
368
- - `SelectField` takes the same props, with its children as the options of the `<select>`.
426
+ null, false, or a list or a fragment of only those drawing nothing. `id` is the control's, made
427
+ with React's `useId` unless given, and a page gives one when something links to the field, such as
428
+ an error summary. Every other prop goes to the `<input>` but `style` and `aria-invalid`. A
429
+ `className` follows `dk-input`, and an `aria-describedby` of the page's own is listed after the
430
+ hint and the error. A `ref` holds the `<input>` drawn, for a page that moves focus to the field,
431
+ such as a form that fails.
432
+ - `SelectField` takes the same props, with its children as the options of the `<select>` and its
433
+ `ref` holding the `<select>`.
369
434
  - `Callout` takes `tone`, `info` unless given, `success` or `danger`, and `title`, drawn at the
370
- start of the message as a paragraph or, with `headingLevel` from 2 to 6, as that heading, and as
371
- a paragraph for any other level. `actions` are drawn after the message. `prefix` is the word
372
- read before the title or the message, the tone's own unless given, and an empty string drops it.
435
+ start of the message as a paragraph or, with `headingLevel` from 2 to 6, as that heading, and as a
436
+ paragraph for any other level. `actions` are drawn after the message. `prefix` is the word read
437
+ before the title or the message, the tone's own unless given, and an empty string drops it.
373
438
  `live`, `status` or `alert`, puts that role on the message and draws the message even when it is
374
- empty. A callout takes no other props, so a page that needs a class
375
- or an id on one puts it on an element around it.
439
+ empty, and a live callout whose message draws nothing, with no title or actions, draws nothing
440
+ until it is filled. Every other prop goes to the callout's `<div>` but `style`, `role` and the
441
+ `title` attribute, inner HTML and the live attributes, since its message takes its role from
442
+ `live` alone, so a page gives it an `id`, such as one an error summary is linked by, and a
443
+ `className`, which follows the kit's. A `ref` holds the `<div>` drawn, for a page that moves focus
444
+ to it, as to a summary of what went wrong in a form that fails.
376
445
 
377
446
  ## Explainer pages
378
447
 
@@ -386,9 +455,15 @@ class would outweigh every piece set inside it.
386
455
 
387
456
  ### The page
388
457
 
458
+ <!-- example: explainerPage -->
459
+
389
460
  ```html
390
461
  <a class="dk-skip" href="#main-content">Skip to content</a>
391
- <div class="dk-strip">...</div>
462
+ <div class="dk-strip">
463
+ <nav class="dk-strip-links" aria-label="Portfolio">
464
+ ... <a href="./" aria-current="page">How it works</a> ...
465
+ </nav>
466
+ </div>
392
467
  <header class="dk-explainer-header"><a href="../">Example</a></header>
393
468
  <main id="main-content" tabindex="-1" class="dk-explainer">
394
469
  <h1>How Example works</h1>
@@ -402,7 +477,9 @@ class would outweigh every piece set inside it.
402
477
  <p>...</p>
403
478
  </section>
404
479
  </main>
405
- <footer class="dk-footer">...</footer>
480
+ <footer class="dk-footer">
481
+ <p>... <a href="./" aria-current="page">How it works</a>, in plain words. ...</p>
482
+ </footer>
406
483
  ```
407
484
 
408
485
  The header holds a link back to the demo, outside `main`, so the skip link passes it and reading
@@ -428,11 +505,13 @@ caption size, its heading in the title size, and its text, with an h3 at the bod
428
505
 
429
506
  ### Steps
430
507
 
508
+ <!-- example: steps -->
509
+
431
510
  ```html
432
511
  <ol class="dk-steps" role="list">
433
512
  <li>
434
513
  <strong class="dk-steps-title">The order names a product.</strong>
435
- <span class="dk-steps-note">And how many of it.</span>
514
+ <span class="dk-steps-note">It says how many it wants.</span>
436
515
  </li>
437
516
  <li>
438
517
  <span class="dk-steps-arrow" aria-hidden="true">↓</span>
@@ -452,10 +531,15 @@ drops a list's meaning when its markers are gone.
452
531
 
453
532
  ### Panels
454
533
 
534
+ <!-- example: panel -->
535
+
455
536
  ```html
456
537
  <div class="dk-panel">
457
538
  <p class="dk-panel-title">How the numbers were taken</p>
458
- <p>...</p>
539
+ <p>
540
+ Each strategy ran three times on the same laptop with the same orders, and the table shows
541
+ the middle run.
542
+ </p>
459
543
  </div>
460
544
  ```
461
545
 
@@ -474,6 +558,8 @@ complementary to it. Nor is it a named `section`, which would be a region landma
474
558
 
475
559
  ### Tables
476
560
 
561
+ <!-- example: table -->
562
+
477
563
  ```html
478
564
  <p class="dk-table-title" id="bench-title">Orders a second under each strategy</p>
479
565
  <div class="dk-table-scroll" role="region" aria-labelledby="bench-title" tabindex="0">
@@ -512,13 +598,16 @@ WebKit. It scrolls in its frame instead.
512
598
 
513
599
  A table is in the small text, with each row ruled in the line color. Its column headers are the
514
600
  muted text at medium weight, and a row's header is at medium weight too. The outer cells keep no
515
- padding on their outer side, so the table's text lines up with the prose around it. Figures sit
516
- to the right and never wrap, while text in a cell wraps and keeps its first line level with the
517
- rest of its row. A header gives its unit in words a screen reader says well, "Slowest 1%, ms"
518
- rather than "p99 ms".
601
+ padding on their outer side, so the table's text lines up with the prose around it. Figures sit to
602
+ the right and never wrap, while text in a cell wraps and keeps its first line level with the rest of
603
+ its row. Code in a cell stays on one line, hyphens and spaces alike, so a token's name never breaks
604
+ after `--dk-`, and a long expression scrolls the frame as a wide table does. A header gives its unit
605
+ in words a screen reader says well, "Slowest 1%, ms" rather than "p99 ms".
519
606
 
520
607
  ### Code blocks
521
608
 
609
+ <!-- example: code -->
610
+
522
611
  ```html
523
612
  <p class="dk-code-title" id="lock-title">The statement that holds the stock</p>
524
613
  <pre
@@ -546,11 +635,11 @@ script, so a block has no copy button.
546
635
  ### Code in running text
547
636
 
548
637
  `base.css` sets code in running text on every page, not only an explainer, at `--dk-text-code`,
549
- 0.9em, so it follows the size of the text around it, a step under it since a mono face looks
550
- larger at the same size. It breaks anywhere it must, with `overflow-wrap: anywhere`, since a long
551
- name has no spaces to break at and would push a narrow column wider. Inside a `pre` it takes the
552
- block's size and keeps its lines whole. It has no ground of its own, and in a link it keeps the
553
- link's underline.
638
+ 0.9em, so it follows the size of the text around it, a step under it since a mono face looks larger
639
+ at the same size. It breaks anywhere it must, with `overflow-wrap: anywhere`, since a long name has
640
+ no spaces to break at and would push a narrow column wider, except in a table cell, as Tables
641
+ describes. Inside a `pre` it takes the block's size and keeps its lines whole. It has no ground of
642
+ its own, and in a link it keeps the link's underline.
554
643
 
555
644
  ## Layers and palettes
556
645
 
@@ -614,7 +703,9 @@ from the package rather than keeping a copy.
614
703
  ## Browser support
615
704
 
616
705
  The colors are written with `light-dark()`, which sets the floor: Chrome and Edge 123, Firefox 120
617
- and Safari 17.5. The kit's browser tests run in Chromium.
706
+ and Safari 17.5. An empty live callout draws nothing by `:has()`, which Firefox has from 121, so in
707
+ Firefox 120 it draws its ground until it is filled, and a label that holds a checkbox or a radio
708
+ shows no pointer there, though the box or the dot does. The kit's browser tests run in Chromium.
618
709
 
619
710
  ## License
620
711
 
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.
@@ -12,13 +12,22 @@
12
12
  @layer dk.components {
13
13
  /*
14
14
  * Skip to content, out of sight until it takes focus, and then over the strip at the column's
15
- * inset, where the strip's links start.
15
+ * inset, where the strip's links start. It waits in its own place as the visually hidden
16
+ * class does, one pixel clipped to nothing, rather than moved above it, so one inside a
17
+ * positioned box of the page's own draws nothing, takes no click and covers nothing a screen
18
+ * reader's touch finds wherever the box starts, and shows at the top of that box once a
19
+ * keyboard reaches it.
16
20
  */
17
21
  .dk-skip {
18
22
  position: absolute;
19
23
  left: calc(max(0px, (100% - var(--dk-column)) / 2) + var(--dk-space-4));
20
- top: -10rem;
21
- padding: var(--dk-space-2) var(--dk-space-4);
24
+ top: var(--dk-space-4);
25
+ width: 1px;
26
+ height: 1px;
27
+ padding: 0;
28
+ overflow: hidden;
29
+ clip-path: inset(50%);
30
+ white-space: nowrap;
22
31
  border-radius: var(--dk-radius);
23
32
  background: var(--dk-accent);
24
33
  color: var(--dk-on-accent);
@@ -26,7 +35,11 @@
26
35
  }
27
36
 
28
37
  :where(.dk-skip):focus-visible {
29
- top: var(--dk-space-4);
38
+ width: auto;
39
+ height: auto;
40
+ padding: var(--dk-space-2) var(--dk-space-4);
41
+ overflow: visible;
42
+ clip-path: none;
30
43
  }
31
44
 
32
45
  /*
@@ -87,14 +100,16 @@
87
100
  font-size: var(--dk-text-small);
88
101
  }
89
102
 
103
+ /* The footer sentence holds to the measure, as prose does, while its rule runs full width. */
90
104
  :where(.dk-footer p) {
105
+ max-width: var(--dk-measure);
91
106
  margin: 0;
92
107
  }
93
108
 
94
109
  /*
95
110
  * The chrome's links step back in the muted text, and come forward to the page's text when
96
- * pointed at or focused. Every one keeps its underline, which in the footer's sentence is what
97
- * tells a link from the text around it.
111
+ * pointed at or focused, as the link to the current page always is. Every one keeps its
112
+ * underline, which in the footer's sentence is what tells a link from the text around it.
98
113
  */
99
114
  :where(.dk-strip-links a, .dk-footer a) {
100
115
  color: var(--dk-muted);
@@ -103,12 +118,23 @@
103
118
  :where(
104
119
  .dk-strip-links a:hover,
105
120
  .dk-strip-links a:focus-visible,
121
+ .dk-strip-links a[aria-current='page'],
106
122
  .dk-footer a:hover,
107
- .dk-footer a:focus-visible
123
+ .dk-footer a:focus-visible,
124
+ .dk-footer a[aria-current='page']
108
125
  ) {
109
126
  color: var(--dk-fg);
110
127
  }
111
128
 
129
+ /*
130
+ * The link to the page a reader is on stands in the page's text, as above, and at the heading's
131
+ * weight, so it is told from the links around it by more than its color, which forced colors
132
+ * replace. It keeps its underline, so it still reads as a link.
133
+ */
134
+ :where(.dk-strip-links a[aria-current='page'], .dk-footer a[aria-current='page']) {
135
+ font-weight: var(--dk-weight-strong);
136
+ }
137
+
112
138
  /*
113
139
  * On a phone the links are smaller and closer, and wrap to rows of their own where they must,
114
140
  * with the switches on a row below them.
@@ -126,6 +152,15 @@
126
152
  flex-basis: 100%;
127
153
  padding-block: 0 var(--dk-space-2);
128
154
  }
155
+
156
+ /*
157
+ * The room of switches on their way stands as tall as one of the kit's controls at the
158
+ * target, which is taller than the strip's row, so their row keeps the height the switches
159
+ * give it. On a wide screen the strip's row is taller than a control, and holds alone.
160
+ */
161
+ .dk-strip-room {
162
+ min-height: var(--dk-target);
163
+ }
129
164
  }
130
165
 
131
166
  /*
@@ -317,7 +352,7 @@
317
352
  :where(.dk-callout) {
318
353
  display: grid;
319
354
  gap: var(--dk-space-2);
320
- padding: var(--dk-space-2) var(--dk-space-3);
355
+ padding: var(--dk-space-3) var(--dk-space-4);
321
356
  border: 1px solid transparent;
322
357
  border-radius: var(--dk-radius);
323
358
  background: var(--dk-highlight);
@@ -333,6 +368,18 @@
333
368
  background: var(--dk-danger-bg);
334
369
  }
335
370
 
371
+ /*
372
+ * A callout whose message holds nothing, not even a space, and that has no actions draws
373
+ * nothing, no ground, padding or edge. That is a live callout a page mounts before it has
374
+ * words, and it stays in the page so a screen reader reads what fills it. It comes after the
375
+ * tones, so it wins over each of them by its order.
376
+ */
377
+ :where(.dk-callout:has(> .dk-callout-body:empty):not(:has(> .dk-callout-actions))) {
378
+ padding: 0;
379
+ border-width: 0;
380
+ background: none;
381
+ }
382
+
336
383
  :where(.dk-callout-title) {
337
384
  margin: 0;
338
385
  font-size: inherit;
@@ -395,6 +442,22 @@
395
442
  padding-inline-end: var(--dk-field-inline);
396
443
  background-image: none;
397
444
  }
445
+
446
+ /*
447
+ * An invalid field loses its red edge to the system's colors, so it gains a dashed line
448
+ * around its edge in the text color, which keeps its contrast in every theme and moves
449
+ * nothing as the error comes and goes. With focus, the ring takes its place, so a
450
+ * keyboard sees where it landed.
451
+ */
452
+ :where(.dk-input[aria-invalid='true'], .dk-select[aria-invalid='true']) {
453
+ outline: var(--dk-ring-width) dashed CanvasText;
454
+ outline-offset: 1px;
455
+ }
456
+
457
+ :where(.dk-input[aria-invalid='true'], .dk-select[aria-invalid='true']):focus-visible {
458
+ outline: var(--dk-ring-width) solid var(--dk-accent);
459
+ outline-offset: var(--dk-ring-offset);
460
+ }
398
461
  }
399
462
 
400
463
  /*
@@ -598,8 +661,8 @@
598
661
  }
599
662
 
600
663
  /* Code in a cell keeps its name whole, so the frame scrolls rather than the name breaking. */
601
- :where(.dk-table code) {
602
- overflow-wrap: normal;
664
+ :where(.dk-table-scroll .dk-table code) {
665
+ white-space: nowrap;
603
666
  }
604
667
 
605
668
  /*
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ButtonHTMLAttributes, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes } from 'react';
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
2
  /** Where the portfolio is, and the section of it that lists the work. */
3
3
  export declare const PORTFOLIO_URL = "https://jasonsdevspace.com/";
4
4
  export declare const PORTFOLIO_WORK_URL = "https://jasonsdevspace.com/#work";
@@ -11,60 +11,76 @@ 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
+ /** A page of the demo's that the chrome links to, and can mark as the one it sits on. */
18
+ type ChromePage = 'about';
17
19
  /**
18
20
  * Whose demo this is, above the page, with three links together on the left. It is a navigation
19
21
  * landmark in a plain div rather than a header, so the demo's own header stays the page's one
20
22
  * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
21
- * named for them.
23
+ * named for them, and while they are on their way the strip keeps their room in that section,
24
+ * unnamed and so no landmark.
22
25
  */
23
- export declare const PortfolioStrip: ({ about, source, current, tools, toolsLabel, }: {
24
- /** The demo's explainer, relative to the page or absolute. */
26
+ export declare const PortfolioStrip: ({ about, source, current, tools, toolsLabel, toolsWaiting, }: {
27
+ /** The demo's explainer, relative to the page or absolute, written by the demo's author. */
25
28
  readonly about: string;
26
- /** The demo's repository. */
29
+ /** The demo's repository, written by the demo's author. */
27
30
  readonly source: string;
28
31
  /** The link that is the page the strip sits on, marked as the current page. */
29
- readonly current?: "about";
32
+ readonly current?: ChromePage | undefined;
30
33
  /** The demo's own switches, drawn beside the links. */
31
34
  readonly tools?: ReactNode;
32
35
  /** The name a screen reader gives the switches' landmark. */
33
- readonly toolsLabel?: string;
36
+ readonly toolsLabel?: string | undefined;
37
+ /**
38
+ * The switches are on their way, so the strip keeps the room of one row of the kit's controls
39
+ * in their section, empty and unnamed, so no landmark, until `tools` holds them and the section
40
+ * takes its name.
41
+ */
42
+ readonly toolsWaiting?: boolean | undefined;
34
43
  }) => import("react").JSX.Element;
35
44
  /**
36
45
  * The exit after a long scroll: whose project this is, how it works, where the source and the rest
37
46
  * of the work are, and the license, with anything the demo adds of its own after them.
38
47
  */
39
- export declare const DemoFooter: ({ name, about, source, license, licenseName, children, }: {
48
+ export declare const DemoFooter: ({ name, about, source, current, license, licenseName, children, }: {
40
49
  /** The demo's name, as the footer's sentence opens with it. */
41
50
  readonly name: string;
42
- /** The demo's explainer, relative to the page or absolute. */
51
+ /** The demo's explainer, relative to the page or absolute, written by the demo's author. */
43
52
  readonly about: string;
44
- /** The demo's repository. */
53
+ /** The demo's repository, written by the demo's author. */
45
54
  readonly source: string;
46
- /** The demo's license, the LICENSE file on the repository's main branch unless given. */
47
- readonly license?: string;
55
+ /** The link that is the page the footer sits on, marked as the current page. */
56
+ readonly current?: ChromePage | undefined;
57
+ /**
58
+ * The demo's license, written by the demo's author, or `LICENSE` on the main branch of `source`
59
+ * unless given.
60
+ */
61
+ readonly license?: string | undefined;
48
62
  /** What the license is called, as the footer's sentence names it. */
49
- readonly licenseName?: string;
63
+ readonly licenseName?: string | undefined;
50
64
  /** What the demo adds after the license, as sentences of its own. */
51
65
  readonly children?: ReactNode;
52
66
  }) => import("react").JSX.Element;
53
- type NativeButton = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style' | 'disabled' | 'aria-disabled' | 'aria-pressed'>;
67
+ type NativeButton = Omit<ComponentPropsWithRef<'button'>, 'style' | 'disabled' | 'aria-disabled' | 'aria-pressed'>;
54
68
  /**
55
69
  * A button's own props: the accent for the one action a view leads with, `pressed` for a toggle
56
- * that stands alone, and `off`. The accent is never a toggle, so the two do not go together.
70
+ * that stands alone, and `off`, beside a `<button>`'s own, its `ref` among them, which holds the
71
+ * button drawn for a page that moves focus to it. The accent is never a toggle, so the two do not
72
+ * go together.
57
73
  */
58
74
  export type ButtonProps = NativeButton & {
59
75
  /** Off, but still in the tab order, so a keyboard keeps its place and finds out why. */
60
- readonly off?: boolean;
76
+ readonly off?: boolean | undefined;
61
77
  } & ({
62
78
  readonly accent: true;
63
- readonly pressed?: never;
79
+ readonly pressed?: undefined;
64
80
  } | {
65
- readonly accent?: false;
81
+ readonly accent?: false | undefined;
66
82
  /** Whether the toggle is on. False still marks it a toggle, undefined an action. */
67
- readonly pressed?: boolean;
83
+ readonly pressed?: boolean | undefined;
68
84
  });
69
85
  /**
70
86
  * A button, of type button unless a page gives another, so it never submits a form by surprise.
@@ -82,36 +98,49 @@ type FieldParts = {
82
98
  /** What is wrong with the value, drawn only when given, which marks the control invalid. */
83
99
  readonly error?: ReactNode;
84
100
  /** The control's id, made with useId unless a page gives one, as it does for a link to it. */
85
- readonly id?: string;
101
+ readonly id?: string | undefined;
86
102
  };
87
- export type InputFieldProps = FieldParts & Omit<InputHTMLAttributes<HTMLInputElement>, 'style' | 'id' | 'aria-invalid' | 'children'>;
88
- /** A text input with its label, hint and error, the input taking every other prop given. */
103
+ export type InputFieldProps = FieldParts & Omit<ComponentPropsWithRef<'input'>, 'style' | 'id' | 'aria-invalid' | 'children'>;
104
+ /**
105
+ * A text input with its label, hint and error, the input taking every other prop given, its `ref`
106
+ * among them, which holds the input drawn for a page that moves focus to it.
107
+ */
89
108
  export declare const InputField: (props: InputFieldProps) => import("react").JSX.Element;
90
- export type SelectFieldProps = FieldParts & Omit<SelectHTMLAttributes<HTMLSelectElement>, 'style' | 'id' | 'aria-invalid'>;
91
- /** A native select with its label, hint and error, its children the options. */
109
+ export type SelectFieldProps = FieldParts & Omit<ComponentPropsWithRef<'select'>, 'style' | 'id' | 'aria-invalid'>;
110
+ /**
111
+ * A native select with its label, hint and error, its children the options, and the select taking
112
+ * every other prop given, its `ref` among them, which holds the select drawn for a page that moves
113
+ * focus to it.
114
+ */
92
115
  export declare const SelectField: (props: SelectFieldProps) => import("react").JSX.Element;
93
116
  /** What a callout is about: news, something that went right, or something that went wrong. */
94
117
  export type CalloutTone = 'info' | 'success' | 'danger';
95
118
  /**
96
- * A message set apart from the page, with an optional title before it and actions after it. It has
97
- * no role unless the page gives `live`, whatever its tone, since a message on the page when it
98
- * loads is read in its place. A live message is drawn even when empty, so the page can mount it
99
- * first and fill it after, which is when a screen reader reads it. The role sits on the message,
100
- * which holds the title and the tone's word too, so they are read with it, and the actions are
101
- * not.
119
+ * A callout's own props, beside a `<div>`'s own, its `id`, a page's class, drawn after the kit's,
120
+ * and its `ref` among them, which holds the callout drawn for a page that moves focus to it, such
121
+ * as a summary of what went wrong in a form. Its `title` is the callout's, not the attribute.
102
122
  */
103
- export declare const Callout: ({ tone, title, headingLevel, live, prefix, actions, children, }: {
104
- readonly tone?: CalloutTone;
123
+ export type CalloutProps = Omit<ComponentPropsWithRef<'div'>, 'style' | 'role' | 'title' | 'children' | 'dangerouslySetInnerHTML' | 'aria-live' | 'aria-atomic' | 'aria-relevant'> & {
124
+ readonly tone?: CalloutTone | undefined;
105
125
  /** What the callout is about, drawn before its message. */
106
126
  readonly title?: ReactNode;
107
127
  /** The title's heading level, where the callout is a section a visitor would jump to. */
108
- readonly headingLevel?: 2 | 3 | 4 | 5 | 6;
128
+ readonly headingLevel?: 2 | 3 | 4 | 5 | 6 | undefined;
109
129
  /** A message a screen reader reads when it changes, politely or at once. */
110
- readonly live?: "status" | "alert";
130
+ readonly live?: 'status' | 'alert' | undefined;
111
131
  /** The word read before the title or the message, the tone's own unless given. */
112
- readonly prefix?: string;
132
+ readonly prefix?: string | undefined;
113
133
  /** Buttons or links that act on the message. */
114
134
  readonly actions?: ReactNode;
115
135
  readonly children?: ReactNode;
116
- }) => import("react").JSX.Element;
136
+ };
137
+ /**
138
+ * A message set apart from the page, with an optional title before it and actions after it. It has
139
+ * no role unless the page gives `live`, whatever its tone, since a message on the page when it
140
+ * loads is read in its place. A live message is drawn even when empty, so the page can mount it
141
+ * first and fill it after, which is when a screen reader reads it. The role sits on the message,
142
+ * which holds the title and the tone's word too, so they are read with it, and the actions are
143
+ * not.
144
+ */
145
+ export declare const Callout: ({ tone, title, headingLevel, live, prefix, actions, className, children, ...native }: CalloutProps) => import("react").JSX.Element;
117
146
  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';
@@ -21,20 +28,21 @@ export const GITHUB_PROFILE_URL = 'https://github.com/jasonsdevspace';
21
28
  * content, which has a tabindex of -1 so it can take focus.
22
29
  */
23
30
  export const SkipLink = ({ target = 'main-content', children = 'Skip to content', }) => (_jsx("a", { className: "dk-skip", href: `#${target}`, children: children }));
31
+ /** The mark a link to `page` takes when it is the page the chrome sits on, and none otherwise. */
32
+ const currentAs = (current, page) => current === page ? 'page' : undefined;
24
33
  /**
25
34
  * Whose demo this is, above the page, with three links together on the left. It is a navigation
26
35
  * landmark in a plain div rather than a header, so the demo's own header stays the page's one
27
36
  * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
28
- * named for them.
37
+ * named for them, and while they are on their way the strip keeps their room in that section,
38
+ * unnamed and so no landmark.
29
39
  */
30
- export const PortfolioStrip = ({ about, source, current, tools, toolsLabel = 'Demo', }) => (_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": current === 'about' ? 'page' : undefined, children: "How it works" }), _jsx("a", { href: source, children: "Source on GitHub" })] }), !drawn(tools) ? null : (_jsx("section", { className: "dk-strip-tools", "aria-label": toolsLabel, children: tools }))] }));
40
+ 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: "More work by 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] }));
31
41
  /**
32
42
  * The exit after a long scroll: whose project this is, how it works, where the source and the rest
33
43
  * of the work are, and the license, with anything the demo adds of its own after them.
34
44
  */
35
- export const DemoFooter = ({ name, about, source, 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, 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] }) }));
36
- /** Whether a slot holds words to draw, an empty string being none. */
37
- const filled = (node) => drawn(node) && node !== '';
45
+ 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] }) }));
38
46
  /**
39
47
  * A list of names an attribute holds, such as the classes a component draws, its own first and a
40
48
  * page's after them, or the ids that describe a control, with any left out or empty dropped.
@@ -80,14 +88,21 @@ const useField = ({ id, hint, error }, own) => {
80
88
  * fields at once would speak over itself, so the page says when an error appears.
81
89
  */
82
90
  const Field = ({ parts, field, children, }) => (_jsxs("div", { className: "dk-field", children: [_jsx("label", { className: "dk-field-label", htmlFor: field.id, children: parts.label }), field.hint ? (_jsx("p", { className: "dk-field-hint", id: `${field.id}-hint`, children: parts.hint })) : null, field.error ? (_jsxs("p", { className: "dk-field-error", id: `${field.id}-error`, children: [_jsx("span", { className: "dk-visually-hidden", children: "Error: " }), parts.error] })) : null, children] }));
83
- /** A text input with its label, hint and error, the input taking every other prop given. */
91
+ /**
92
+ * A text input with its label, hint and error, the input taking every other prop given, its `ref`
93
+ * among them, which holds the input drawn for a page that moves focus to it.
94
+ */
84
95
  export const InputField = (props) => {
85
96
  const { label: _label, hint: _hint, error: _error, id: _id, className, ...rest } = props;
86
97
  const { 'aria-describedby': own, ...native } = rest;
87
98
  const field = useField(props, own);
88
99
  return (_jsx(Field, { parts: props, field: field, children: _jsx("input", { className: spaced('dk-input', className), ...native, ...field.wired }) }));
89
100
  };
90
- /** A native select with its label, hint and error, its children the options. */
101
+ /**
102
+ * A native select with its label, hint and error, its children the options, and the select taking
103
+ * every other prop given, its `ref` among them, which holds the select drawn for a page that moves
104
+ * focus to it.
105
+ */
91
106
  export const SelectField = (props) => {
92
107
  const { label: _label, hint: _hint, error: _error, id: _id, className, ...rest } = props;
93
108
  const { 'aria-describedby': own, children, ...native } = rest;
@@ -102,6 +117,12 @@ const TONE_WORDS = {
102
117
  };
103
118
  /** The element a callout's title is drawn as, at each heading level a page may give it. */
104
119
  const HEADINGS = { 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' };
120
+ /**
121
+ * A tone or a heading level the types refuse, as a page in plain JavaScript can pass, draws news
122
+ * and a paragraph, rather than a class the sheet has no rule for or an element React refuses.
123
+ */
124
+ const toneOf = (tone) => Object.hasOwn(TONE_WORDS, tone) ? tone : 'info';
125
+ const titleOf = (level) => level !== undefined && Object.hasOwn(HEADINGS, level) ? HEADINGS[level] : 'p';
105
126
  /**
106
127
  * A message set apart from the page, with an optional title before it and actions after it. It has
107
128
  * no role unless the page gives `live`, whatever its tone, since a message on the page when it
@@ -110,11 +131,12 @@ const HEADINGS = { 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' };
110
131
  * which holds the title and the tone's word too, so they are read with it, and the actions are
111
132
  * not.
112
133
  */
113
- export const Callout = ({ tone = 'info', title, headingLevel, live, prefix = TONE_WORDS[tone], actions, children, }) => {
114
- // A level the types refuse, as a page in plain JavaScript can pass, draws a paragraph.
115
- const Title = (headingLevel === undefined ? undefined : HEADINGS[headingLevel]) ?? 'p';
134
+ export const Callout = ({ tone = 'info', title, headingLevel, live, prefix, actions, className, children, ...native }) => {
135
+ const kind = toneOf(tone);
136
+ const spoken = prefix ?? TONE_WORDS[kind];
137
+ const Title = titleOf(headingLevel);
116
138
  const titled = filled(title);
117
139
  const said = filled(children);
118
- const word = prefix === '' ? null : _jsx("span", { className: "dk-visually-hidden", children: prefix });
119
- 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] }));
140
+ const word = spoken === '' ? null : _jsx("span", { className: "dk-visually-hidden", children: spoken });
141
+ 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] }));
120
142
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonsdevspace/design-kit",
3
- "version": "0.1.0-next.2",
3
+ "version": "0.1.0-next.4",
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",