dowel-ui 0.20.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ A dowel is the hidden peg that joins two boards so the seam does not show. That
11
11
  **[Documentation](https://lacodda.github.io/dowel/)** — what everything is and why it is that way.
12
12
  **[The stand](https://lacodda.github.io/dowel/stand/)** — every component, live, in either theme and in the accent of any product of the line.
13
13
 
14
- **Status:** v0.20.2 - the theme, the scales, an accent per product, forty-five components - overlays, menus, the command palette and the three ways of saying something happened - and the gates each one passes: axe, the keyboard, a dependency budget and a picture in both themes. Every component has a page of its own on the stand, which remembers the theme and the accent you left it in. Components install from a versioned registry, the docs are served in the form an agent reads, and `dowel check` tells a project what stands between it and the vocabulary. Two products of the line live on it. See the [roadmap](#roadmap).
14
+ **Status:** v0.21.0 - the theme, the scales, an accent per product, fifty-one components - overlays, menus, the command palette, the table and the three ways of saying something happened - and the gates each one passes: axe, the keyboard, a dependency budget and a picture in both themes. Every component has a page of its own on the stand, which remembers the theme and the accent you left it in. Components install from a versioned registry, the docs are served in the form an agent reads, and `dowel check` tells a project what stands between it and the vocabulary. Two products of the line live on it. See the [roadmap](#roadmap).
15
15
 
16
16
  ## The theme
17
17
 
@@ -98,16 +98,19 @@ snapshot the cross-references point into the same snapshot, so a component and
98
98
  the sibling it reuses are the pair that shipped together. See
99
99
  [installing from the registry](https://lacodda.github.io/dowel/guides/registry/).
100
100
 
101
- Forty-five of them so far. The everyday ones - Button, Input, Textarea, Panel,
101
+ Fifty-one of them so far. The everyday ones - Button, Input, Textarea, Panel,
102
102
  Badge, Chip, Kbd, Spinner, Truncate and Copyable; Field and the three controls
103
103
  that answer a question - Checkbox, RadioGroup and Switch; five for a number or
104
104
  a judgement - NumberField, Slider, RatingScale, DurationField and
105
- PasswordField; five for a date or a time - Calendar, DatePicker,
105
+ PasswordField; the harder half of a form - TagInput, FileDrop, ColorField,
106
+ ActionBar and SaveState; five for a date or a time - Calendar, DatePicker,
106
107
  DateRangePicker, TimeField and the `calendar-math` under them; the six that
107
108
  float above the page - Dialog, ConfirmDialog, Drawer, Popover, PreviewCard and
108
109
  Tooltip; four for choosing something - Menu, ContextMenu, Select and Combobox;
109
110
  three for finding it - SearchField, CommandPalette and the `useShortcut` behind
110
- them; and three for saying that something happened: Toast, Alert and Banner.
111
+ them; three for saying that something happened - Toast, Alert and Banner; and
112
+ six for showing rows of data: Table, Pagination, PageSize, NumberFormat,
113
+ RelativeTime and the `table-sort` with no React in it.
111
114
 
112
115
  Those last three are the ones products keep confusing, so each page names all
113
116
  four options: a **toast** goes away, an **alert** is still true after a reload,
@@ -0,0 +1,10 @@
1
+ /*
2
+ * furca - terracotta (paired with olive) #D9704A, the colour of its mark (fr).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D9704A;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * hilvan - crimson #D64550, the colour of its mark (hv).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D64550;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * rhapsod - field green #6CB14E, the colour of its mark (rh).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #6CB14E;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * rigger - iris (paired with lagoon) #8A62F0, the colour of its mark (rr).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #8A62F0;
10
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ * scheda - terracotta (paired with sea) #D9704A, the colour of its mark (sc).
3
+ *
4
+ * Import after the theme. Everything else - the hover shade, the soft fill,
5
+ * the focus ring, the colour of text on an accent fill, and the trace of the
6
+ * hue the greys carry - is derived from this one value.
7
+ */
8
+ :root {
9
+ --accent-base: #D9704A;
10
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"line.d.ts","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,WAAW,EAe9C,CAAA;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEjE"}
1
+ {"version":3,"file":"line.d.ts","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,YAAY,EAAE,SAAS,WAAW,EA+B9C,CAAA;AAED,kDAAkD;AAClD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEjE"}
package/dist/line.js CHANGED
@@ -27,6 +27,22 @@ export const lineProducts = [
27
27
  { name: 'dowel', code: 'dw', accent: '#E8862D', colorName: 'signal amber' },
28
28
  { name: 'lyrn', code: 'ln', accent: '#6D7BF2', colorName: 'indigo' },
29
29
  { name: 'austeris', code: 'au', accent: '#C25BD9', colorName: 'orchid' },
30
+ { name: 'rhapsod', code: 'rh', accent: '#6CB14E', colorName: 'field green' },
31
+ { name: 'hilvan', code: 'hv', accent: '#D64550', colorName: 'crimson' },
32
+ /*
33
+ * The last three marks are two-colour: the registry's palette of single hues
34
+ * ran out at hilvan, and a product now owns either one hue or a pair drawn as
35
+ * one diagonal gradient.
36
+ *
37
+ * The theme takes the first colour of the pair and nothing else. A gradient
38
+ * is a property of the mark — the tile, the code, the banner rule — and an
39
+ * accent is a flat colour a button is filled with, a focus ring is drawn in
40
+ * and a link is set in. Carrying the pair into the vocabulary would make
41
+ * every one of those a gradient, which is not what the pair is for.
42
+ */
43
+ { name: 'scheda', code: 'sc', accent: '#D9704A', colorName: 'terracotta (paired with sea)' },
44
+ { name: 'furca', code: 'fr', accent: '#D9704A', colorName: 'terracotta (paired with olive)' },
45
+ { name: 'rigger', code: 'rr', accent: '#8A62F0', colorName: 'iris (paired with lagoon)' },
30
46
  ];
31
47
  /** The product with this name, or `undefined`. */
32
48
  export function lineProduct(name) {
package/dist/line.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"line.js","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACzE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACnE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE;IACpE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE;IAC3E,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACpE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;CACzE,CAAA;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC9D,CAAC"}
1
+ {"version":3,"file":"line.js","sourceRoot":"","sources":["../src/line.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAaH,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACzE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACrE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IAClE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;IACnE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACtE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE;IACpE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACrE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE;IAC3E,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACpE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;IACxE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE;IAC5E,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE;IACvE;;;;;;;;;;OAUG;IACH,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,8BAA8B,EAAE;IAC5F,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,gCAAgC,EAAE;IAC7F,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,2BAA2B,EAAE;CAC1F,CAAA;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC9D,CAAC"}
@@ -215,6 +215,76 @@
215
215
  }
216
216
  ]
217
217
  },
218
+ {
219
+ "name": "accent-rhapsod",
220
+ "type": "registry:file",
221
+ "title": "rhapsod accent",
222
+ "description": "field green #6CB14E - the colour of the rhapsod mark (rh). Import after the theme.",
223
+ "files": [
224
+ {
225
+ "path": "dowel/accents/rhapsod.css",
226
+ "target": "~/dowel/accents/rhapsod.css",
227
+ "type": "registry:file",
228
+ "content": "/*\n * rhapsod - field green #6CB14E, the colour of its mark (rh).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #6CB14E;\n}\n"
229
+ }
230
+ ]
231
+ },
232
+ {
233
+ "name": "accent-hilvan",
234
+ "type": "registry:file",
235
+ "title": "hilvan accent",
236
+ "description": "crimson #D64550 - the colour of the hilvan mark (hv). Import after the theme.",
237
+ "files": [
238
+ {
239
+ "path": "dowel/accents/hilvan.css",
240
+ "target": "~/dowel/accents/hilvan.css",
241
+ "type": "registry:file",
242
+ "content": "/*\n * hilvan - crimson #D64550, the colour of its mark (hv).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #D64550;\n}\n"
243
+ }
244
+ ]
245
+ },
246
+ {
247
+ "name": "accent-scheda",
248
+ "type": "registry:file",
249
+ "title": "scheda accent",
250
+ "description": "terracotta (paired with sea) #D9704A - the colour of the scheda mark (sc). Import after the theme.",
251
+ "files": [
252
+ {
253
+ "path": "dowel/accents/scheda.css",
254
+ "target": "~/dowel/accents/scheda.css",
255
+ "type": "registry:file",
256
+ "content": "/*\n * scheda - terracotta (paired with sea) #D9704A, the colour of its mark (sc).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #D9704A;\n}\n"
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ "name": "accent-furca",
262
+ "type": "registry:file",
263
+ "title": "furca accent",
264
+ "description": "terracotta (paired with olive) #D9704A - the colour of the furca mark (fr). Import after the theme.",
265
+ "files": [
266
+ {
267
+ "path": "dowel/accents/furca.css",
268
+ "target": "~/dowel/accents/furca.css",
269
+ "type": "registry:file",
270
+ "content": "/*\n * furca - terracotta (paired with olive) #D9704A, the colour of its mark (fr).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #D9704A;\n}\n"
271
+ }
272
+ ]
273
+ },
274
+ {
275
+ "name": "accent-rigger",
276
+ "type": "registry:file",
277
+ "title": "rigger accent",
278
+ "description": "iris (paired with lagoon) #8A62F0 - the colour of the rigger mark (rr). Import after the theme.",
279
+ "files": [
280
+ {
281
+ "path": "dowel/accents/rigger.css",
282
+ "target": "~/dowel/accents/rigger.css",
283
+ "type": "registry:file",
284
+ "content": "/*\n * rigger - iris (paired with lagoon) #8A62F0, the colour of its mark (rr).\n *\n * Import after the theme. Everything else - the hover shade, the soft fill,\n * the focus ring, the colour of text on an accent fill, and the trace of the\n * hue the greys carry - is derived from this one value.\n */\n:root {\n --accent-base: #8A62F0;\n}\n"
285
+ }
286
+ ]
287
+ },
218
288
  {
219
289
  "name": "action-bar",
220
290
  "type": "registry:ui",
@@ -223,7 +293,7 @@
223
293
  "dependencies": [
224
294
  "@base-ui/react",
225
295
  "class-variance-authority",
226
- "dowel-ui@^0.20.2"
296
+ "dowel-ui@^0.21.0"
227
297
  ],
228
298
  "registryDependencies": [],
229
299
  "files": [
@@ -242,7 +312,7 @@
242
312
  "description": "A message that stays on the screen, in the flow of the page, about the thing next to it: this field could not be saved, this profile has no axes yet, this export is out of date.",
243
313
  "dependencies": [
244
314
  "class-variance-authority",
245
- "dowel-ui@^0.20.2"
315
+ "dowel-ui@^0.21.0"
246
316
  ],
247
317
  "registryDependencies": [],
248
318
  "files": [
@@ -261,7 +331,7 @@
261
331
  "description": "A small piece of state attached to something else: a count, a status, a label. It is not a button and never was - if it can be clicked it is a Chip.",
262
332
  "dependencies": [
263
333
  "class-variance-authority",
264
- "dowel-ui@^0.20.2"
334
+ "dowel-ui@^0.21.0"
265
335
  ],
266
336
  "registryDependencies": [],
267
337
  "files": [
@@ -280,7 +350,7 @@
280
350
  "description": "A strip across the top of the application, about the application: you are offline, this build is a preview, your licence expires on Friday, a new version is ready to install.",
281
351
  "dependencies": [
282
352
  "class-variance-authority",
283
- "dowel-ui@^0.20.2"
353
+ "dowel-ui@^0.21.0"
284
354
  ],
285
355
  "registryDependencies": [],
286
356
  "files": [
@@ -300,7 +370,7 @@
300
370
  "dependencies": [
301
371
  "@base-ui/react",
302
372
  "class-variance-authority",
303
- "dowel-ui@^0.20.2"
373
+ "dowel-ui@^0.21.0"
304
374
  ],
305
375
  "registryDependencies": [],
306
376
  "files": [
@@ -334,7 +404,7 @@
334
404
  "title": "Calendar",
335
405
  "description": "The sums live next door in `calendar-math`, which has no React in it; this is the grid that draws them and the keyboard that moves around it.",
336
406
  "dependencies": [
337
- "dowel-ui@^0.20.2"
407
+ "dowel-ui@^0.21.0"
338
408
  ],
339
409
  "registryDependencies": [
340
410
  "https://lacodda.github.io/dowel/r/calendar-math.json"
@@ -355,7 +425,7 @@
355
425
  "description": "The interesting part is the words. A checkbox on its own is a nine-pixel target that says nothing; wired to a label it is the whole row, and the row is what a finger and a pointer both aim at. So the label is part of the component rather than something a caller remembers to add - the commonest bug in a hand-rolled checkbox is a `<label>` that is next to the input instead of tied to it, which looks identical and does nothing.",
356
426
  "dependencies": [
357
427
  "@base-ui/react",
358
- "dowel-ui@^0.20.2"
428
+ "dowel-ui@^0.21.0"
359
429
  ],
360
430
  "registryDependencies": [],
361
431
  "files": [
@@ -374,7 +444,7 @@
374
444
  "description": "A badge you can act on: a filter that can be removed, a tag with a count, a selected value in a field. The difference from a Badge is entirely about whether something happens when you click it - and if something does, that part is a real `<button>` with a real label, not a decorative cross.",
375
445
  "dependencies": [
376
446
  "class-variance-authority",
377
- "dowel-ui@^0.20.2"
447
+ "dowel-ui@^0.21.0"
378
448
  ],
379
449
  "registryDependencies": [],
380
450
  "files": [
@@ -392,7 +462,7 @@
392
462
  "title": "Color-field",
393
463
  "description": "Picking a colour for something the product stores: a tag, a project, a calendar. Note what that is *not* - it is not choosing the appearance of the interface. The theme decides that, from one accent, and a field that let a reader repaint the chrome would undo the argument the whole system rests on.",
394
464
  "dependencies": [
395
- "dowel-ui@^0.20.2"
465
+ "dowel-ui@^0.21.0"
396
466
  ],
397
467
  "registryDependencies": [
398
468
  "https://lacodda.github.io/dowel/r/input.json"
@@ -414,7 +484,7 @@
414
484
  "dependencies": [
415
485
  "@base-ui/react",
416
486
  "class-variance-authority",
417
- "dowel-ui@^0.20.2"
487
+ "dowel-ui@^0.21.0"
418
488
  ],
419
489
  "registryDependencies": [
420
490
  "https://lacodda.github.io/dowel/r/input.json",
@@ -437,7 +507,7 @@
437
507
  "dependencies": [
438
508
  "@base-ui/react",
439
509
  "class-variance-authority",
440
- "dowel-ui@^0.20.2"
510
+ "dowel-ui@^0.21.0"
441
511
  ],
442
512
  "registryDependencies": [
443
513
  "https://lacodda.github.io/dowel/r/combobox.json",
@@ -460,7 +530,7 @@
460
530
  "dependencies": [
461
531
  "@base-ui/react",
462
532
  "class-variance-authority",
463
- "dowel-ui@^0.20.2"
533
+ "dowel-ui@^0.21.0"
464
534
  ],
465
535
  "registryDependencies": [],
466
536
  "files": [
@@ -479,7 +549,7 @@
479
549
  "description": "The same list of actions as Menu, opened the other way round: by right click, or by a long press on a touch screen, over an *area* rather than from a button. So the trigger is not a control - it is the region the menu belongs to, a row, a canvas, a file tile - and it renders a `<div>`.",
480
550
  "dependencies": [
481
551
  "@base-ui/react",
482
- "dowel-ui@^0.20.2"
552
+ "dowel-ui@^0.21.0"
483
553
  ],
484
554
  "registryDependencies": [
485
555
  "https://lacodda.github.io/dowel/r/menu.json"
@@ -499,7 +569,7 @@
499
569
  "title": "Copyable",
500
570
  "description": "Any text that someone will eventually want to copy - an id, a path, a hash, a token - copied with one click. The rule comes from nitid: if a value is worth showing, it is worth being able to take away, and selecting a monospaced id by hand is a small daily tax.",
501
571
  "dependencies": [
502
- "dowel-ui@^0.20.2"
572
+ "dowel-ui@^0.21.0"
503
573
  ],
504
574
  "registryDependencies": [],
505
575
  "files": [
@@ -517,7 +587,7 @@
517
587
  "title": "Date-picker",
518
588
  "description": "The trigger is a button rather than a text input, and that is the decision worth stating. A typable date field has to answer \"what does `03/04/26` mean\" in a locale it cannot be sure of, and it answers wrong for half the world; a button showing the date spelled out has no such question. Where typing genuinely matters - a birth date, forty years back - the calendar is the wrong control anyway and a product should reach for a plain field.",
519
589
  "dependencies": [
520
- "dowel-ui@^0.20.2"
590
+ "dowel-ui@^0.21.0"
521
591
  ],
522
592
  "registryDependencies": [
523
593
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -540,7 +610,7 @@
540
610
  "title": "Date-range-picker",
541
611
  "description": "The interesting part is the state between them. After the first click there is a start and no end, and that is not an incomplete range to be hidden or a range of one day - it is the normal middle of the interaction, and the calendar has to show it: the first day marked, the days under the pointer shading as the reader moves, the popup staying open. Products that skip it end up with a picker that seems to do nothing until the second click.",
542
612
  "dependencies": [
543
- "dowel-ui@^0.20.2"
613
+ "dowel-ui@^0.21.0"
544
614
  ],
545
615
  "registryDependencies": [
546
616
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -565,7 +635,7 @@
565
635
  "dependencies": [
566
636
  "@base-ui/react",
567
637
  "class-variance-authority",
568
- "dowel-ui@^0.20.2"
638
+ "dowel-ui@^0.21.0"
569
639
  ],
570
640
  "registryDependencies": [],
571
641
  "files": [
@@ -585,7 +655,7 @@
585
655
  "dependencies": [
586
656
  "@base-ui/react",
587
657
  "class-variance-authority",
588
- "dowel-ui@^0.20.2"
658
+ "dowel-ui@^0.21.0"
589
659
  ],
590
660
  "registryDependencies": [],
591
661
  "files": [
@@ -603,7 +673,7 @@
603
673
  "title": "Duration-field",
604
674
  "description": "The alternative is what products keep building: two number boxes labelled \"hours\" and \"minutes\", which means two tab stops, two validations, and a reader who has to divide 90 minutes in their head before typing. Here they write `1h 30m`, or `90m`, or `1.5h`, and it means the same thing.",
605
675
  "dependencies": [
606
- "dowel-ui@^0.20.2"
676
+ "dowel-ui@^0.21.0"
607
677
  ],
608
678
  "registryDependencies": [
609
679
  "https://lacodda.github.io/dowel/r/input.json"
@@ -624,7 +694,7 @@
624
694
  "description": "Every form is the same four parts repeated: a name for the control, the control, sometimes a hint, and sometimes an error. Written by hand each time, they drift - the label loses its `htmlFor`, the hint is a `<div>` no screen reader mentions, the error appears in red and is announced by nothing at all. This is that arrangement, once.",
625
695
  "dependencies": [
626
696
  "@base-ui/react",
627
- "dowel-ui@^0.20.2"
697
+ "dowel-ui@^0.21.0"
628
698
  ],
629
699
  "registryDependencies": [],
630
700
  "files": [
@@ -642,7 +712,7 @@
642
712
  "title": "File-drop",
643
713
  "description": "A place to put files: drag them onto it, or press it and pick them. It takes files and hands them over - it does not upload them. Where they go, with which credentials, retried how - that is the product's transport, and a primitive that owned it would be wrong for every product whose upload does not look like the one it guessed.",
644
714
  "dependencies": [
645
- "dowel-ui@^0.20.2"
715
+ "dowel-ui@^0.21.0"
646
716
  ],
647
717
  "registryDependencies": [],
648
718
  "files": [
@@ -660,7 +730,7 @@
660
730
  "title": "Input",
661
731
  "description": "A single-line field. It is a plain `<input>` with the line's clothes on, so everything a browser gives an input for free - autofill, spellcheck, the right keyboard on a phone, `type=\"email\"` validation - still works.",
662
732
  "dependencies": [
663
- "dowel-ui@^0.20.2"
733
+ "dowel-ui@^0.21.0"
664
734
  ],
665
735
  "registryDependencies": [],
666
736
  "files": [
@@ -678,7 +748,7 @@
678
748
  "title": "Kbd",
679
749
  "description": "A key, as printed in a menu or a hint: `Ctrl` `K`. It is a `<kbd>` element because that is what the element is for - a screen reader announces it as keyboard input rather than reading a stray capital letter.",
680
750
  "dependencies": [
681
- "dowel-ui@^0.20.2"
751
+ "dowel-ui@^0.21.0"
682
752
  ],
683
753
  "registryDependencies": [],
684
754
  "files": [
@@ -698,7 +768,7 @@
698
768
  "dependencies": [
699
769
  "@base-ui/react",
700
770
  "class-variance-authority",
701
- "dowel-ui@^0.20.2"
771
+ "dowel-ui@^0.21.0"
702
772
  ],
703
773
  "registryDependencies": [],
704
774
  "files": [
@@ -717,7 +787,7 @@
717
787
  "description": "A number typed into a text input is a string that happens to look like a number, and every product then writes the same four fixes: strip the letters, clamp to a range, round to a step, and decide what an empty box means. This is those four, once, plus the stepper - because a value with a small range is faster nudged than typed.",
718
788
  "dependencies": [
719
789
  "@base-ui/react",
720
- "dowel-ui@^0.20.2"
790
+ "dowel-ui@^0.21.0"
721
791
  ],
722
792
  "registryDependencies": [
723
793
  "https://lacodda.github.io/dowel/r/input.json"
@@ -731,6 +801,64 @@
731
801
  }
732
802
  ]
733
803
  },
804
+ {
805
+ "name": "number-format",
806
+ "type": "registry:ui",
807
+ "title": "Number-format",
808
+ "description": "Two things, and the second is the reason this is a component rather than a call to `toLocaleString` at each site.",
809
+ "dependencies": [
810
+ "dowel-ui@^0.21.0"
811
+ ],
812
+ "registryDependencies": [],
813
+ "files": [
814
+ {
815
+ "path": "ui/number-format.tsx",
816
+ "target": "@ui/number-format.tsx",
817
+ "type": "registry:ui",
818
+ "content": "import type { HTMLAttributes } from 'react'\nimport { cn } from 'dowel-ui'\n\n/*\n * A number, written the way the reader's language writes numbers.\n *\n * Two things, and the second is the reason this is a component rather than a\n * call to `toLocaleString` at each site.\n *\n * **The separators are the reader's.** A thousand is `1,000` here, `1 000`\n * there and `1.000` somewhere else, and the last one is the same string\n * another reader would read as one. `Intl` knows this and a product does not\n * have to.\n *\n * **The figures line up.** `tabular-nums` makes every digit the same width, so\n * a column of numbers has its digits above each other and the eye can compare\n * lengths without reading. Without it a proportional font gives `1` less room\n * than `8`, the column ripples, and the only way to tell 9,999 from 10,000 is\n * to count. This is the part that gets left out, because it looks fine in the\n * one number a developer tries it on and only fails in a column - which is\n * exactly where numbers live.\n *\n * `Intl.NumberFormat` covers plain numbers, currency, percentages and units\n * with the same options object, so those are not four components. What it does\n * not cover is a \"compact\" number that must not lose meaning - `1.2M` is a\n * choice about how much precision the reader is owed, and it is made by the\n * caller, in `notation`.\n *\n * No locale is defaulted. `undefined` means the reader's own, which is what a\n * product almost always wants; passing `'en-US'` to be safe is how a German\n * reader is shown American separators for the life of the product.\n */\n\n/* `style` belongs to both halves of these props and means opposite things:\n * `'currency'` to `Intl`, a CSS object to the DOM. The `Intl` one wins, since\n * it is the option a number component is actually asked for; inline styles are\n * given up in exchange, which costs nothing here - a primitive's appearance is\n * the theme's business, and `className` is still there. */\nexport interface NumberFormatProps\n extends Omit<HTMLAttributes<HTMLSpanElement>, 'children' | 'style'>,\n Intl.NumberFormatOptions {\n /* Required, and a number rather than `number | null`. A row with no value\n * shows whatever the product says absence looks like - a dash, a word, an\n * empty cell - and that is a decision about the data, not about formatting.\n * Accepting `null` here would put a default answer to it inside a primitive,\n * and the default would be wrong wherever absence means something. */\n value: number\n /** The reader's own by default. */\n locale?: string | string[]\n}\n\n/** Format a number without rendering it. For a `title`, an `aria-label`, a\n * CSV, or anywhere the string is needed rather than an element. */\nexport function formatNumber(\n value: number,\n locale?: string | string[],\n options?: Intl.NumberFormatOptions,\n): string {\n return new Intl.NumberFormat(locale, options).format(value)\n}\n\nexport function NumberFormat({\n value,\n locale,\n className,\n // Everything `Intl.NumberFormat` understands, pulled out of the props so\n // what remains can go on the element. Listed rather than inferred, because\n // the two sets overlap - `style` is a valid option and a valid DOM\n // attribute, and spreading `style: 'currency'` onto a `<span>` is a runtime\n // error the type system will not catch.\n style,\n currency,\n currencyDisplay,\n currencySign,\n unit,\n unitDisplay,\n notation,\n compactDisplay,\n signDisplay,\n useGrouping,\n minimumIntegerDigits,\n minimumFractionDigits,\n maximumFractionDigits,\n minimumSignificantDigits,\n maximumSignificantDigits,\n numberingSystem,\n ...props\n}: NumberFormatProps) {\n const formatted = formatNumber(value, locale, {\n style,\n currency,\n currencyDisplay,\n currencySign,\n unit,\n unitDisplay,\n notation,\n compactDisplay,\n signDisplay,\n useGrouping,\n minimumIntegerDigits,\n minimumFractionDigits,\n maximumFractionDigits,\n minimumSignificantDigits,\n maximumSignificantDigits,\n numberingSystem,\n })\n\n return (\n <span className={cn('tabular-nums', className)} {...props}>\n {formatted}\n </span>\n )\n}\n"
819
+ }
820
+ ]
821
+ },
822
+ {
823
+ "name": "page-size",
824
+ "type": "registry:ui",
825
+ "title": "Page-size",
826
+ "description": "Its own file rather than a part of `Pagination`, because the two are needed apart often enough: a list that scrolls for ever wants \"how many to load at a time\" and no page buttons, and a table with a fixed page size wants the buttons and no choice. Together they were also over the size gate, which asked the right question.",
827
+ "dependencies": [
828
+ "dowel-ui@^0.21.0"
829
+ ],
830
+ "registryDependencies": [
831
+ "https://lacodda.github.io/dowel/r/select.json"
832
+ ],
833
+ "files": [
834
+ {
835
+ "path": "ui/page-size.tsx",
836
+ "target": "@ui/page-size.tsx",
837
+ "type": "registry:ui",
838
+ "content": "import type { ReactNode } from 'react'\nimport { cn } from 'dowel-ui'\nimport { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from './select'\n\n/*\n * How many rows to a page.\n *\n * Its own file rather than a part of `Pagination`, because the two are needed\n * apart often enough: a list that scrolls for ever wants \"how many to load at\n * a time\" and no page buttons, and a table with a fixed page size wants the\n * buttons and no choice. Together they were also over the size gate, which\n * asked the right question.\n *\n * A `Select`, because the line has no native `<select>` anywhere - and this is\n * the control most likely to reintroduce one, since it is three numbers in a\n * box and looks like the case where it would not matter.\n */\n\nexport interface PageSizeProps {\n pageSize: number\n options?: number[]\n onPageSizeChange: (pageSize: number) => void\n /** Names the control for a screen reader, e.g. \"Rows per page\". */\n label: string\n /** The visible label, when there is room for one. */\n children?: ReactNode\n className?: string\n}\n\nexport function PageSize({\n pageSize,\n options = [10, 25, 50, 100],\n onPageSizeChange,\n label,\n children,\n className,\n}: PageSizeProps) {\n return (\n <div className={cn('flex items-center gap-2 text-sm text-dim', className)}>\n {children}\n <Select\n value={String(pageSize)}\n onValueChange={(value: unknown) => onPageSizeChange(Number(value))}\n >\n <SelectTrigger aria-label={label} className=\"w-auto min-w-16\">\n <SelectValue />\n </SelectTrigger>\n <SelectPopup>\n {options.map((option) => (\n <SelectItem key={option} value={String(option)}>\n {option}\n </SelectItem>\n ))}\n </SelectPopup>\n </Select>\n </div>\n )\n}\n"
839
+ }
840
+ ]
841
+ },
842
+ {
843
+ "name": "pagination",
844
+ "type": "registry:ui",
845
+ "title": "Pagination",
846
+ "description": "The arithmetic is exported separately from the component for the same reason `table-sort` is a file of its own: a product that pages on the server needs the page numbers and not the buttons, and computing them a second time in a different place is how the two disagree about where the last page ends.",
847
+ "dependencies": [
848
+ "dowel-ui@^0.21.0"
849
+ ],
850
+ "registryDependencies": [
851
+ "https://lacodda.github.io/dowel/r/button.json"
852
+ ],
853
+ "files": [
854
+ {
855
+ "path": "ui/pagination.tsx",
856
+ "target": "@ui/pagination.tsx",
857
+ "type": "registry:ui",
858
+ "content": "import { cn } from 'dowel-ui'\nimport { Button } from './button'\n\n/*\n * Paging through a list, and choosing how much of it to see at once.\n *\n * The arithmetic is exported separately from the component for the same reason\n * `table-sort` is a file of its own: a product that pages on the server needs\n * the page numbers and not the buttons, and computing them a second time in a\n * different place is how the two disagree about where the last page ends.\n *\n * Two things this deliberately does not do.\n *\n * It does not fetch, and it does not slice. It is told the page and the total\n * and it says which page was asked for - because whether paging means an\n * `OFFSET` or an `Array.prototype.slice` is the product's business, and a\n * component that guesses gets it wrong for the other one.\n *\n * It does not show every page number. A list of forty pages drawn in full is a\n * row of forty targets nobody aims at; `pageWindow` gives first, last, the\n * neighbourhood of the current page, and a gap where the rest were.\n *\n * How many rows to a page is next door, in `PageSize`. They are two controls\n * that usually sit together and are needed apart often enough to be separate:\n * a list that scrolls for ever wants \"how many to load at a time\" and no page\n * buttons, and a table with a fixed page size wants the buttons and no choice.\n */\n\n/** A step in the row of pages: a page to go to, or the gap where pages were\n * left out. The gap is not a page and is never clickable - rendering it as a\n * disabled button gives a keyboard three stops that lead nowhere. */\nexport type PageStep = number | 'gap'\n\n/** How many pages a list of this size has. At least one: a list with nothing\n * in it is on page 1 of 1, not page 1 of 0, and the second reads as broken. */\nexport function pageCount(total: number, pageSize: number): number {\n if (pageSize <= 0) return 1\n return Math.max(1, Math.ceil(total / pageSize))\n}\n\n/** The row of pages to draw: first, last, the current page and its\n * neighbours, with a gap standing in for the rest.\n *\n * `around` is how many pages sit either side of the current one. The row keeps\n * a steady width as the reader pages through it, because a row that grows and\n * shrinks moves the buttons under the pointer.\n */\nexport function pageWindow(page: number, pages: number, around = 1): PageStep[] {\n const total = Math.max(1, pages)\n const current = Math.min(Math.max(1, page), total)\n\n // How many places the row has: first, last, a gap at each end, and the\n // neighbourhood between them. This is the number the row always fills.\n const width = around * 2 + 5\n if (total <= width) {\n return Array.from({ length: total }, (_, index) => index + 1)\n }\n\n /* The row is built to a fixed number of places, and that is the whole\n * difficulty. A window of a fixed number of *pages* looks right in the\n * middle and comes up short at the ends, where one of the gaps is not\n * needed: the row loses a place, and the \"next\" button moves as you use it.\n *\n * So the run of middle pages is grown to whatever fills the row - which\n * depends on how many gaps there are, which depends on where the run sits.\n * Rather than iterate towards that (it oscillates on the pages either side\n * of an end, where dropping a gap moves the run and moving the run brings\n * the gap back), each possible number of gaps is tried and the one that\n * describes itself is kept.\n */\n for (const gaps of [2, 1, 0]) {\n // Places for pages: the row, less the two ends, less the gaps.\n const run = width - 2 - gaps\n if (run > total - 2) continue\n // Centre the run on the current page, then slide it inside the ends. The\n // clamp is what pushes the neighbourhood away from an edge rather than\n // clipping it there.\n let from = Math.min(Math.max(2, current - Math.floor((run - 1) / 2)), total - run)\n\n // A gap that stands for a single page is a lie that costs a click: `1 … 3`\n // hides only page 2, which the row had room for. The fix is to slide the\n // run onto the end rather than to drop the page: leaving it out silently\n // is worse than the gap, because then the row says 1 is followed by 3 and\n // gives no sign that anything is missing.\n if (from === 3) from = 2\n if (from + run - 1 === total - 2) from += 1\n const to = from + run - 1\n\n const before = from > 2\n const after = to < total - 1\n if (Number(before) + Number(after) !== gaps) continue\n\n const steps: PageStep[] = [1]\n if (before) steps.push('gap')\n for (let page = from; page <= to; page += 1) steps.push(page)\n if (after) steps.push('gap')\n steps.push(total)\n return steps\n }\n\n // Unreachable for `total > width`: with no gaps the run covers every middle\n // page, which is the case handled above. Kept so the function has an answer\n // for every input rather than an implicit `undefined`.\n return Array.from({ length: total }, (_, index) => index + 1)\n}\n\n/** Which rows this page holds: `[from, to]`, counting from one, for the \"1-20\n * of 97\" a reader is shown. `to` is clamped to the total, so the last page\n * says what it actually holds rather than what a full page would. */\nexport function pageRange(page: number, pageSize: number, total: number): [number, number] {\n if (total <= 0) return [0, 0]\n const from = (page - 1) * pageSize + 1\n return [from, Math.min(page * pageSize, total)]\n}\n\nexport interface PaginationProps {\n page: number\n pageSize: number\n total: number\n onPageChange: (page: number) => void\n /** How many pages either side of the current one. */\n around?: number\n /** What a screen reader hears for the region and its buttons. Required\n * rather than defaulted: a default here would be English shipped inside a\n * primitive, and these are the only words the control has. */\n labels: PaginationLabels\n className?: string\n}\n\nexport interface PaginationLabels {\n /** Names the whole control, e.g. \"Pages\". */\n region: string\n previous: string\n next: string\n /** Names one page button. Given the number, because \"Page 3\" is a sentence\n * only the product's language can build. */\n page: (page: number) => string\n}\n\nexport function Pagination({\n page,\n pageSize,\n total,\n onPageChange,\n around = 1,\n labels,\n className,\n}: PaginationProps) {\n const pages = pageCount(total, pageSize)\n const current = Math.min(Math.max(1, page), pages)\n const steps = pageWindow(current, pages, around)\n\n return (\n <nav aria-label={labels.region} className={cn('flex items-center gap-1', className)}>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label={labels.previous}\n disabled={current <= 1}\n onClick={() => onPageChange(current - 1)}\n >\n ‹\n </Button>\n\n {steps.map((step, index) =>\n step === 'gap' ? (\n // Not a button, and not `aria-hidden` either: the gap is real\n // information - there are pages here that are not shown - and a\n // reader that skips it hears a jump from 2 to 40 with no reason.\n <span\n // The index is the key on purpose: two gaps in a row are\n // indistinguishable, and there is never more than one at each end.\n key={`gap-${index}`}\n className=\"px-1 text-dim select-none\"\n >\n …\n </span>\n ) : (\n <Button\n key={step}\n variant={step === current ? 'primary' : 'ghost'}\n size=\"sm\"\n // The current page is announced, not only coloured. `aria-current`\n // rather than `aria-selected`, which belongs to a listbox.\n aria-current={step === current ? 'page' : undefined}\n aria-label={labels.page(step)}\n onClick={() => onPageChange(step)}\n className=\"tabular-nums\"\n >\n {step}\n </Button>\n ),\n )}\n\n <Button\n variant=\"ghost\"\n size=\"sm\"\n aria-label={labels.next}\n disabled={current >= pages}\n onClick={() => onPageChange(current + 1)}\n >\n ›\n </Button>\n </nav>\n )\n}\n"
859
+ }
860
+ ]
861
+ },
734
862
  {
735
863
  "name": "panel",
736
864
  "type": "registry:ui",
@@ -738,7 +866,7 @@
738
866
  "description": "The raised surface everything else sits on. It is the one place a screen gets its structure from, so it stays deliberately plain: a ground, a hairline, a corner.",
739
867
  "dependencies": [
740
868
  "class-variance-authority",
741
- "dowel-ui@^0.20.2"
869
+ "dowel-ui@^0.21.0"
742
870
  ],
743
871
  "registryDependencies": [],
744
872
  "files": [
@@ -756,7 +884,7 @@
756
884
  "title": "Password-field",
757
885
  "description": "The reveal is the whole component, and it is not a convenience. A masked field is the only one in a form where a typo cannot be seen, so people either paste (fine) or type slowly and get it wrong anyway; the toggle is what turns an unverifiable field into a checkable one, and it is why long passphrases became usable at all.",
758
886
  "dependencies": [
759
- "dowel-ui@^0.20.2"
887
+ "dowel-ui@^0.21.0"
760
888
  ],
761
889
  "registryDependencies": [
762
890
  "https://lacodda.github.io/dowel/r/input.json"
@@ -778,7 +906,7 @@
778
906
  "dependencies": [
779
907
  "@base-ui/react",
780
908
  "class-variance-authority",
781
- "dowel-ui@^0.20.2"
909
+ "dowel-ui@^0.21.0"
782
910
  ],
783
911
  "registryDependencies": [],
784
912
  "files": [
@@ -798,7 +926,7 @@
798
926
  "dependencies": [
799
927
  "@base-ui/react",
800
928
  "class-variance-authority",
801
- "dowel-ui@^0.20.2"
929
+ "dowel-ui@^0.21.0"
802
930
  ],
803
931
  "registryDependencies": [],
804
932
  "files": [
@@ -818,7 +946,7 @@
818
946
  "dependencies": [
819
947
  "@base-ui/react",
820
948
  "class-variance-authority",
821
- "dowel-ui@^0.20.2"
949
+ "dowel-ui@^0.21.0"
822
950
  ],
823
951
  "registryDependencies": [],
824
952
  "files": [
@@ -836,7 +964,7 @@
836
964
  "title": "Rating-scale",
837
965
  "description": "Generalised from kilna, where it is how a work is scored on each of its axes. The shape is a row of marks rather than stars: stars carry a meaning of their own - a review, a public verdict - and this is as often \"how hard was this\" or \"how finished is it\" as it is \"how good\".",
838
966
  "dependencies": [
839
- "dowel-ui@^0.20.2"
967
+ "dowel-ui@^0.21.0"
840
968
  ],
841
969
  "registryDependencies": [],
842
970
  "files": [
@@ -848,13 +976,31 @@
848
976
  }
849
977
  ]
850
978
  },
979
+ {
980
+ "name": "relative-time",
981
+ "type": "registry:ui",
982
+ "title": "Relative-time",
983
+ "description": "The relative-time primitive.",
984
+ "dependencies": [
985
+ "dowel-ui@^0.21.0"
986
+ ],
987
+ "registryDependencies": [],
988
+ "files": [
989
+ {
990
+ "path": "ui/relative-time.tsx",
991
+ "target": "@ui/relative-time.tsx",
992
+ "type": "registry:ui",
993
+ "content": "import { useState, type TimeHTMLAttributes } from 'react'\nimport { cn } from 'dowel-ui'\n\n/*\n * \"3 minutes ago\", in the reader's language, over a date they can still read.\n *\n * The rule the component is built on: **relative time is a convenience, never\n * the only copy of the fact.** \"Last week\" is quicker to read than a date and\n * useless the moment the reader needs to say when something actually happened\n * - and they cannot get it back, because the page has thrown it away. So the\n * element is a `<time dateTime=…>` with the exact moment in its `title`: the\n * machine-readable value stays, hovering shows the real date, and copying the\n * text still yields something a person can act on.\n *\n * `Intl.RelativeTimeFormat` writes the phrase, which is the whole reason there\n * is no date library here. \"yesterday\", \"3 недели назад\", \"in 2 months\" - the\n * plural rules and the special words for the nearest units are the part a\n * hand-written version gets wrong first, and it gets it wrong only in the\n * languages its author does not read.\n *\n * What `Intl` does not decide is which unit to use: it is told \"3\" and\n * \"weeks\". Choosing between them is `relativeParts` below, and it is the one\n * piece of arithmetic here.\n *\n * Deliberately not self-updating. A component that re-renders every second to\n * keep \"2 minutes ago\" honest costs a timer per instance - a table of fifty\n * rows is fifty timers - to correct a number nobody is watching change. A\n * product that needs it re-renders the list on its own schedule; the `now`\n * prop is there so it can, and so tests are not written against the clock.\n */\n\n/** The thresholds, largest unit first: how many seconds it takes to earn one,\n * and the unit `Intl` should be handed.\n *\n * A month is 30 days and a year is 365. Both are approximations, and they are\n * the right ones: this is a phrase saying roughly how long ago, and a reader\n * who needs the exact interval is reading the date in the `title` instead. */\nconst UNITS: [seconds: number, unit: Intl.RelativeTimeFormatUnit][] = [\n [60 * 60 * 24 * 365, 'year'],\n [60 * 60 * 24 * 30, 'month'],\n [60 * 60 * 24 * 7, 'week'],\n [60 * 60 * 24, 'day'],\n [60 * 60, 'hour'],\n [60, 'minute'],\n [1, 'second'],\n]\n\n/** How long ago, as a count and a unit: what `Intl.RelativeTimeFormat` needs.\n *\n * The sign is the one `Intl` wants - negative for the past - and the count is\n * rounded towards zero. Rounding to nearest is the tempting alternative and\n * says \"in 1 hour\" 31 minutes before the meeting; truncating never claims more\n * time has passed than has. */\nexport function relativeParts(\n from: Date | number | string,\n now: Date | number = Date.now(),\n): [value: number, unit: Intl.RelativeTimeFormatUnit] {\n const then = new Date(from).getTime()\n const seconds = (then - new Date(now).getTime()) / 1000\n const magnitude = Math.abs(seconds)\n\n for (const [size, unit] of UNITS) {\n if (magnitude >= size) return [Math.trunc(seconds / size), unit]\n }\n // Under a second either way. Zero seconds rather than the smallest unit,\n // because `Intl` turns that into \"now\" in every language it knows.\n return [0, 'second']\n}\n\n/** The phrase alone, for a `title`, an `aria-label` or a string. */\nexport function formatRelative(\n from: Date | number | string,\n now: Date | number = Date.now(),\n locale?: string | string[],\n options?: Intl.RelativeTimeFormatOptions,\n): string {\n const [value, unit] = relativeParts(from, now)\n return new Intl.RelativeTimeFormat(locale, { numeric: 'auto', ...options }).format(value, unit)\n}\n\nexport interface RelativeTimeProps extends Omit<TimeHTMLAttributes<HTMLTimeElement>, 'title'> {\n /** The moment being described. A `Date`, epoch milliseconds, or an ISO\n * string - whichever the data already holds. */\n value: Date | number | string\n /** What counts as now. Given rather than read from the clock so a list can\n * re-render on its own schedule, and so a test is not written against the\n * time it runs at. */\n now?: Date | number\n locale?: string | string[]\n /** `'auto'` by default, which is what produces \"yesterday\" rather than \"1\n * day ago\" where the language has a word for it. Pass `'always'` for a\n * column where every row should read the same way. */\n numeric?: Intl.RelativeTimeFormatOptions['numeric']\n /** How the exact moment is written in the `title`. The reader's own format\n * by default. */\n titleOptions?: Intl.DateTimeFormatOptions\n}\n\nexport function RelativeTime({\n value,\n now,\n locale,\n numeric = 'auto',\n titleOptions = { dateStyle: 'medium', timeStyle: 'short' },\n className,\n ...props\n}: RelativeTimeProps) {\n /* Read once, at mount, rather than in a default argument.\n *\n * `now = Date.now()` in the parameter list is the obvious spelling and is\n * impure: it is evaluated on every render, so \"now\" moves whenever the\n * parent happens to re-render and the phrase changes for reasons that have\n * nothing to do with this component. Caught by `react-hooks/purity`, and\n * worth keeping caught - a component that deliberately does not update\n * itself must not update itself by accident either.\n *\n * With `now` given, the state is initialised and never read, which is what a\n * product paging a list wants: every row is measured from the same moment. */\n const [mountedAt] = useState(() => Date.now())\n const moment = new Date(value)\n const invalid = Number.isNaN(moment.getTime())\n\n if (invalid) {\n // A date that is not a date renders as nothing rather than as \"Invalid\n // Date\", which is a string no reader can do anything with and which looks\n // like a value. The `<time>` element with no `dateTime` says the same to a\n // machine: there is no moment here.\n return <time className={className} {...props} />\n }\n\n return (\n <time\n dateTime={moment.toISOString()}\n // The fact itself, kept. The phrase above it is the convenience.\n title={new Intl.DateTimeFormat(locale, titleOptions).format(moment)}\n className={cn('whitespace-nowrap', className)}\n {...props}\n >\n {formatRelative(moment, now ?? mountedAt, locale, { numeric })}\n </time>\n )\n}\n"
994
+ }
995
+ ]
996
+ },
851
997
  {
852
998
  "name": "save-state",
853
999
  "type": "registry:ui",
854
1000
  "title": "Save-state",
855
1001
  "description": "The quiet line beside a field that saves itself: \"saving…\", then a tick that fades. It exists because a form without a Save button has to say what it did anyway - otherwise the reader is left guessing whether their edit survived, and the usual answer to that guess is to press Ctrl+S at a page that has no such thing.",
856
1002
  "dependencies": [
857
- "dowel-ui@^0.20.2"
1003
+ "dowel-ui@^0.21.0"
858
1004
  ],
859
1005
  "registryDependencies": [
860
1006
  "https://lacodda.github.io/dowel/r/spinner.json"
@@ -874,7 +1020,7 @@
874
1020
  "title": "Search-field",
875
1021
  "description": "An Input that knows it is a search box, which is three small things the products kept not doing:\n * - a magnifier, so the field is recognisable before it is read; - a way to clear it that is not \"select all and delete\" - and one that a keyboard can reach, which a decorative `<span>` cannot; - the shortcut that focuses it, shown in the field rather than learned.",
876
1022
  "dependencies": [
877
- "dowel-ui@^0.20.2"
1023
+ "dowel-ui@^0.21.0"
878
1024
  ],
879
1025
  "registryDependencies": [
880
1026
  "https://lacodda.github.io/dowel/r/input.json",
@@ -898,7 +1044,7 @@
898
1044
  "dependencies": [
899
1045
  "@base-ui/react",
900
1046
  "class-variance-authority",
901
- "dowel-ui@^0.20.2"
1047
+ "dowel-ui@^0.21.0"
902
1048
  ],
903
1049
  "registryDependencies": [
904
1050
  "https://lacodda.github.io/dowel/r/input.json"
@@ -935,7 +1081,7 @@
935
1081
  "description": "The case for it over a NumberField is that the number does not matter much: a volume, an opacity, a weight in a search filter. Where the exact figure does matter, a slider is a worse field with more pixels - it cannot be typed into, it cannot be pasted into, and it has no state for \"empty\".",
936
1082
  "dependencies": [
937
1083
  "@base-ui/react",
938
- "dowel-ui@^0.20.2"
1084
+ "dowel-ui@^0.21.0"
939
1085
  ],
940
1086
  "registryDependencies": [],
941
1087
  "files": [
@@ -954,7 +1100,7 @@
954
1100
  "description": "Something is happening and the answer has not arrived. It carries no text of its own - what is loading is the product's word, not the system's - but it does have to say *something* to a screen reader, or a page that is busy is silently identical to a page that is empty.",
955
1101
  "dependencies": [
956
1102
  "class-variance-authority",
957
- "dowel-ui@^0.20.2"
1103
+ "dowel-ui@^0.21.0"
958
1104
  ],
959
1105
  "registryDependencies": [],
960
1106
  "files": [
@@ -973,7 +1119,7 @@
973
1119
  "description": "The difference from Checkbox is not how it looks, and getting it wrong is the commonest mistake in the pair. A checkbox is an answer collected now and submitted later, with the rest of the form; a switch is a setting that applies the moment it moves. Put a switch in a form with a Save button and the reader cannot tell whether anything happened - they flipped it, and nothing said so.",
974
1120
  "dependencies": [
975
1121
  "@base-ui/react",
976
- "dowel-ui@^0.20.2"
1122
+ "dowel-ui@^0.21.0"
977
1123
  ],
978
1124
  "registryDependencies": [],
979
1125
  "files": [
@@ -985,6 +1131,43 @@
985
1131
  }
986
1132
  ]
987
1133
  },
1134
+ {
1135
+ "name": "table-sort",
1136
+ "type": "registry:ui",
1137
+ "title": "Table-sort",
1138
+ "description": "Split out of the Table for the reason `calendar-math` was split out of the Calendar: these are the sums, and the component is the thing that draws them. A product sorting its own rows - on the server, in a worker, before the data ever reaches a component - imports this and nothing else.",
1139
+ "dependencies": [],
1140
+ "registryDependencies": [],
1141
+ "files": [
1142
+ {
1143
+ "path": "ui/table-sort.tsx",
1144
+ "target": "@ui/table-sort.tsx",
1145
+ "type": "registry:ui",
1146
+ "content": "/*\n * How a column is ordered, with no React in it.\n *\n * Split out of the Table for the reason `calendar-math` was split out of the\n * Calendar: these are the sums, and the component is the thing that draws\n * them. A product sorting its own rows - on the server, in a worker, before\n * the data ever reaches a component - imports this and nothing else.\n *\n * No table library, deliberately. The obvious choice here is TanStack Table,\n * and it would be the first dependency a product has to install beyond Base\n * UI. What it offers is a model of columns, pages and sorting state; what the\n * line's one real table needed was the rule below, which the model does not\n * have. So the model is the part that is written here, and it is small.\n *\n * The rule, and the reason this file exists at all:\n *\n * **Absence sorts last, whichever way the column points.**\n *\n * A row with no value in this column is not the smallest - it is unknown, and\n * the two are different facts. Rank absence with the rest and flip the sign,\n * and a descending sort floats every empty row to the top: the reader asks for\n * \"highest first\" and is handed the rows that have no value at all. This is\n * measured rather than assumed - it is what the first version of the line's\n * catalogue did, and the fix is the shape below, where presence is settled\n * before the direction is applied.\n *\n * It is the same fact `RatingScale` is built on: not judged yet is a state,\n * not a zero. A table that sorts them together loses it on the first click.\n */\n\n/** Which way a column points. */\nexport type SortDirection = 'asc' | 'desc'\n\n/** A column, and which way it points. `column` is the caller's own key - the\n * id it gave the column, not an index, so reordering columns cannot silently\n * change what is sorted. */\nexport interface Sort<Column extends string = string> {\n column: Column\n direction: SortDirection\n}\n\n/** What can be compared. `null` and `undefined` both mean absent - a product\n * gets whichever its data uses, and having to normalise them before sorting\n * is the kind of step that gets forgotten in one branch of a switch. */\nexport type SortValue = string | number | boolean | null | undefined\n\n/** Reads the value of a column out of a row.\n *\n * Given rather than inferred: a column id is not always a field name, and a\n * sortable column is often a computed one - a duration that is stored as two\n * timestamps, a name that sorts by surname. */\nexport type SortAccessor<Row, Column extends string = string> = (\n row: Row,\n column: Column,\n) => SortValue\n\nexport interface SortOptions<Row> {\n /** The tiebreaker: what decides the order of rows that compare equal.\n *\n * Without one, `Array.prototype.sort` is stable and therefore leaves ties in\n * input order - which sounds fine until the input is re-fetched and arrives\n * in a different order, and a column of ties reshuffles under the reader\n * with no click. Pass the row's id. */\n tiebreak?: (row: Row) => SortValue\n /** The locale text is compared in. Passed to `Intl.Collator`, so `ä` sorts\n * where the reader expects rather than after `z`. */\n locale?: string\n}\n\n/** Order the rows. Returns a new array; the input is not touched, because a\n * component that sorts its own prop in place mutates the caller's state. */\nexport function sortRows<Row, Column extends string = string>(\n rows: readonly Row[],\n sort: Sort<Column>,\n accessor: SortAccessor<Row, Column>,\n options: SortOptions<Row> = {},\n): Row[] {\n const sign = sort.direction === 'asc' ? 1 : -1\n // One collator for the whole sort rather than one `localeCompare` per\n // comparison: building it is the expensive half, and a sort of n rows asks\n // for it n log n times.\n const collator = new Intl.Collator(options.locale)\n\n return [...rows].sort((a, b) => {\n const left = accessor(a, sort.column)\n const right = accessor(b, sort.column)\n\n // Presence first, and outside the sign. This is the whole point of the\n // file: absence is last in both directions.\n const absent = presence(left, right)\n if (absent !== 0) return absent\n\n const ranked = compareValues(left, right, collator) * sign\n if (ranked !== 0) return ranked\n\n if (options.tiebreak) {\n const tied = compareValues(options.tiebreak(a), options.tiebreak(b), collator)\n // Deliberately not multiplied by the sign. The tiebreaker is there to\n // make the order stable, and an order that reverses with the column is\n // not stable - the rows that tie would swap places on every click.\n if (tied !== 0) return tied\n }\n return 0\n })\n}\n\n/** Whether a value counts as absent.\n *\n * Both spellings of nothing, and neither `0` nor `''` nor `false`, which are\n * values a row genuinely has.\n *\n * `NaN` counts too, and that is not tidiness. Subtraction with it returns\n * `NaN`, which `sort` reads as \"these two are equal\" - so a row whose number\n * is not a number takes whatever place the input happened to give it, and the\n * order changes when the data is re-fetched. A number that is not a number is\n * not a small number; it is unknown, which is what this file already has a\n * place for. */\nexport function isAbsent(value: SortValue): boolean {\n if (value === null || value === undefined) return true\n return typeof value === 'number' && Number.isNaN(value)\n}\n\n/** Which of the two lacks a value; absent sorts last, always. */\nfunction presence(left: SortValue, right: SortValue): number {\n const leftHas = !isAbsent(left)\n const rightHas = !isAbsent(right)\n if (leftHas === rightHas) return 0\n return leftHas ? -1 : 1\n}\n\n/** Compare two present values of the same column.\n *\n * Numbers by subtraction, text by collator, and booleans as false-then-true.\n * A column whose values are of mixed type compares as text: that is a data\n * problem the table cannot fix, and ordering by `String` at least gives the\n * same answer twice. */\nfunction compareValues(left: SortValue, right: SortValue, collator: Intl.Collator): number {\n if (isAbsent(left) || isAbsent(right)) return presence(left, right)\n\n // NaN never reaches here: `isAbsent` counts it as absent, so `presence`\n // settles it above, outside the sign.\n if (typeof left === 'number' && typeof right === 'number') return left - right\n if (typeof left === 'boolean' && typeof right === 'boolean') {\n return Number(left) - Number(right)\n }\n return collator.compare(String(left), String(right))\n}\n\n/** What clicking a column heading does.\n *\n * Three states rather than two, and the third is the reason this is a function\n * and not `direction === 'asc' ? 'desc' : 'asc'`: clicking a *different*\n * column starts it ascending rather than inheriting the direction of the one\n * before. Inheriting is what a two-state toggle does, and it means the first\n * click on a new column can hand back an order nobody asked for. */\nexport function toggleSort<Column extends string>(\n sort: Sort<Column>,\n column: Column,\n): Sort<Column> {\n if (sort.column !== column) return { column, direction: 'asc' }\n return { column, direction: sort.direction === 'asc' ? 'desc' : 'asc' }\n}\n\n/** What a column heading announces: `ascending`, `descending`, or nothing.\n *\n * The value belongs in `aria-sort` on the `<th>`, and only on the one that is\n * sorted - `aria-sort=\"none\"` on every other heading is allowed by the spec\n * and read out by some screen readers on every cell, which turns a table into\n * a recital. `undefined` removes the attribute. */\nexport function ariaSort(\n sort: Sort<string> | undefined,\n column: string,\n): 'ascending' | 'descending' | undefined {\n if (!sort || sort.column !== column) return undefined\n return sort.direction === 'asc' ? 'ascending' : 'descending'\n}\n"
1147
+ }
1148
+ ]
1149
+ },
1150
+ {
1151
+ "name": "table",
1152
+ "type": "registry:ui",
1153
+ "title": "Table",
1154
+ "description": "Parts rather than a `columns` prop, and that is the decision worth stating: a `<DataTable columns={…} rows={…} />` is quicker to write for the first table and then owns every cell in the product forever. The moment one column needs a Badge, another a link, and a third the row's own menu, the prop grows a `render` for each - at which point it is JSX with extra steps, spelt in a shape only this component understands.",
1155
+ "dependencies": [
1156
+ "class-variance-authority",
1157
+ "dowel-ui@^0.21.0"
1158
+ ],
1159
+ "registryDependencies": [
1160
+ "https://lacodda.github.io/dowel/r/table-sort.json"
1161
+ ],
1162
+ "files": [
1163
+ {
1164
+ "path": "ui/table.tsx",
1165
+ "target": "@ui/table.tsx",
1166
+ "type": "registry:ui",
1167
+ "content": "import type { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from 'dowel-ui'\nimport { ariaSort, type Sort } from './table-sort'\n\n/*\n * A table of rows, and the heading that reorders them.\n *\n * Parts rather than a `columns` prop, and that is the decision worth stating:\n * a `<DataTable columns={…} rows={…} />` is quicker to write for the first\n * table and then owns every cell in the product forever. The moment one column\n * needs a Badge, another a link, and a third the row's own menu, the prop\n * grows a `render` for each - at which point it is JSX with extra steps, spelt\n * in a shape only this component understands.\n *\n * So the parts are the ones HTML already has, dressed: a caller writes\n * `<TableRow>` and `<TableCell>` and puts whatever it likes inside. What is\n * bought by having them here rather than in the product is that every table of\n * the line has the same row height, the same heading, the same hairline and\n * the same behaviour when a column is sorted - and that a heading that sorts\n * announces it, which is the part hand-rolled tables get wrong.\n *\n * The sorting arithmetic is next door in `table-sort`, with no React in it -\n * a product that sorts on the server imports that and never this.\n *\n * `<TableScroll>` exists because a table cannot scroll itself: `overflow` on a\n * `<table>` does nothing, so the wrapper is not decoration but the only place\n * a scrollbar can live. It is also what makes the sticky heading work, since\n * `position: sticky` needs a scroll container to stick inside.\n */\n\nexport const tableVariants = cva('w-full border-collapse text-left text-sm', {\n variants: {\n density: {\n /* Comfortable, and the default: a row a finger can hit, for a table a\n * person reads. */\n base: '[&_td]:py-2.5 [&_th]:py-2.5',\n /* For a table a person scans - a log, a ledger, a list of a thousand\n * things. Half the padding, same font: shrinking the text too is how a\n * dense table becomes an unreadable one. */\n dense: '[&_td]:py-1.5 [&_th]:py-1.5',\n },\n },\n defaultVariants: { density: 'base' },\n})\n\nexport interface TableProps\n extends HTMLAttributes<HTMLTableElement>,\n VariantProps<typeof tableVariants> {}\n\nexport function Table({ density, className, ...props }: TableProps) {\n return <table className={cn(tableVariants({ density }), className)} {...props} />\n}\n\n/** The scroll container a table needs, and the one a sticky heading sticks in.\n *\n * The scrollbar is the theme's - an overlay that takes no room in the layout,\n * so a table that grows past the fold does not shift the column beside it. */\nexport function TableScroll({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n return <div className={cn('w-full overflow-x-auto', className)} {...props} />\n}\n\nexport interface TableHeadProps extends HTMLAttributes<HTMLTableSectionElement> {\n /** Keep the heading in view while the body scrolls under it.\n *\n * Off by default. A sticky heading needs a container with a height to stick\n * inside; switched on by default it would silently do nothing in the common\n * case - a table that scrolls with the page - and look broken in the other. */\n sticky?: boolean\n}\n\nexport function TableHead({ sticky = false, className, ...props }: TableHeadProps) {\n return (\n <thead\n className={cn(\n 'text-xs text-dim',\n // The heading needs its own ground when it is sticky: transparent, it\n // would have the body's rows sliding visibly beneath its text.\n sticky && 'sticky top-0 z-sticky bg-bg',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport function TableBody({ className, ...props }: HTMLAttributes<HTMLTableSectionElement>) {\n return <tbody className={cn('', className)} {...props} />\n}\n\nexport interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {\n /** The row the reader has picked out - the one open in the panel beside the\n * table, the one a keyboard cursor is on. Not a hover state. */\n selected?: boolean\n}\n\nexport function TableRow({ selected = false, className, ...props }: TableRowProps) {\n return (\n <tr\n // Announced rather than only drawn: a row picked out by colour alone is\n // a row nobody using a screen reader knows about.\n aria-selected={selected || undefined}\n className={cn(\n 'border-b border-line transition-colors last:border-b-0',\n selected ? 'bg-accent-soft' : 'hover:bg-soft',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> {\n /** Right-align and use the lining figures. For a column of numbers, and the\n * reason it is a prop rather than a class the caller adds: a column of\n * numbers that is not aligned is the most common defect in a table, and the\n * one nobody files a bug about. */\n numeric?: boolean\n}\n\nexport function TableCell({ numeric = false, className, ...props }: TableCellProps) {\n return (\n <td\n className={cn('px-3 align-middle', numeric && 'text-right tabular-nums', className)}\n {...props}\n />\n )\n}\n\nexport interface TableHeaderProps extends ThHTMLAttributes<HTMLTableCellElement> {\n numeric?: boolean\n}\n\n/** A plain heading, for a column that does not sort. */\nexport function TableHeader({ numeric = false, className, ...props }: TableHeaderProps) {\n return (\n <th\n scope=\"col\"\n className={cn(\n 'px-3 font-medium',\n numeric && 'text-right tabular-nums',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport interface TableSortHeaderProps extends Omit<TableHeaderProps, 'onClick'> {\n /** This column's id - the key the accessor sorts by. */\n column: string\n /** What is sorted now, across the whole table. */\n sort: Sort | undefined\n onSortChange: (column: string) => void\n}\n\n/** A heading that reorders the table when clicked.\n *\n * A real `<button>` inside the `<th>`, not a click handler on the cell: the\n * cell is not focusable, gets no keyboard, and announces nothing. This is the\n * part a hand-rolled table almost always gets wrong, and it is invisible to\n * everyone who reorders with a mouse.\n *\n * The arrow is `aria-hidden`, because `aria-sort` on the cell already says\n * which way the column points - a reader would otherwise hear the direction\n * twice, once as a word and once as a character nobody meant to publish. */\nexport function TableSortHeader({\n column,\n sort,\n onSortChange,\n numeric = false,\n className,\n children,\n ...props\n}: TableSortHeaderProps) {\n const active = sort?.column === column\n const direction = sort?.direction\n\n return (\n <th\n scope=\"col\"\n // Only on the sorted column. `aria-sort=\"none\"` on every other heading\n // is valid and gets announced by some readers on every cell.\n aria-sort={ariaSort(sort, column)}\n className={cn('px-3 font-medium', numeric && 'tabular-nums', className)}\n {...props}\n >\n <button\n type=\"button\"\n onClick={() => onSortChange(column)}\n className={cn(\n 'group -mx-1 inline-flex w-[calc(100%+0.5rem)] cursor-pointer items-center gap-1 rounded-sm px-1',\n 'transition-colors hover:text-text',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',\n active && 'text-text',\n numeric && 'justify-end',\n )}\n >\n {children}\n <span\n aria-hidden\n className={cn(\n 'text-2xs transition-opacity',\n // The arrow of the unsorted column appears on hover rather than\n // never: a heading that gives no sign it can be clicked is one\n // nobody clicks. It stays out of the way until the pointer is on\n // it, so a table of eight columns is not a row of eight arrows.\n active ? 'opacity-100' : 'opacity-0 group-hover:opacity-40',\n )}\n >\n {direction === 'desc' && active ? '▼' : '▲'}\n </span>\n </button>\n </th>\n )\n}\n\n/** What the table says when it has no rows.\n *\n * A row spanning every column rather than a block below the table, so the\n * heading stays where it is and the table does not change shape between having\n * rows and not. `colSpan` is required: without it the message sits under the\n * first column and the rest of the header hangs over nothing. */\nexport interface TableEmptyProps extends TdHTMLAttributes<HTMLTableCellElement> {\n colSpan: number\n}\n\nexport function TableEmpty({ colSpan, className, ...props }: TableEmptyProps) {\n return (\n <tr>\n <td\n colSpan={colSpan}\n className={cn('px-3 py-10 text-center text-sm text-dim', className)}\n {...props}\n />\n </tr>\n )\n}\n"
1168
+ }
1169
+ ]
1170
+ },
988
1171
  {
989
1172
  "name": "tag-input",
990
1173
  "type": "registry:ui",
@@ -992,7 +1175,7 @@
992
1175
  "description": "Free text turned into a list: type a word, press Enter, it becomes a chip.",
993
1176
  "dependencies": [
994
1177
  "class-variance-authority",
995
- "dowel-ui@^0.20.2"
1178
+ "dowel-ui@^0.21.0"
996
1179
  ],
997
1180
  "registryDependencies": [
998
1181
  "https://lacodda.github.io/dowel/r/chip.json",
@@ -1013,7 +1196,7 @@
1013
1196
  "title": "Textarea",
1014
1197
  "description": "A multi-line field that can grow with what is typed into it, which is the only interesting part: a fixed box makes someone scroll inside a scroll, and a box that grows without limit pushes the button they are trying to reach off the screen. `autoResize` grows it; `maxRows` says when to stop and let it scroll after all.",
1015
1198
  "dependencies": [
1016
- "dowel-ui@^0.20.2"
1199
+ "dowel-ui@^0.21.0"
1017
1200
  ],
1018
1201
  "registryDependencies": [
1019
1202
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1033,7 +1216,7 @@
1033
1216
  "title": "Time-field",
1034
1217
  "description": "No donor for this one: neither product of the line had a time field, so this is written from the same shape as DurationField, and for the same reason. Anything a person plausibly types is accepted - `9`, `9:30`, `930`, `9.30`, `9pm`, `21:30` - and what comes back is always `HH:MM`.",
1035
1218
  "dependencies": [
1036
- "dowel-ui@^0.20.2"
1219
+ "dowel-ui@^0.21.0"
1037
1220
  ],
1038
1221
  "registryDependencies": [
1039
1222
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1055,7 +1238,7 @@
1055
1238
  "dependencies": [
1056
1239
  "@base-ui/react",
1057
1240
  "class-variance-authority",
1058
- "dowel-ui@^0.20.2"
1241
+ "dowel-ui@^0.21.0"
1059
1242
  ],
1060
1243
  "registryDependencies": [],
1061
1244
  "files": [
@@ -1075,7 +1258,7 @@
1075
1258
  "dependencies": [
1076
1259
  "@base-ui/react",
1077
1260
  "class-variance-authority",
1078
- "dowel-ui@^0.20.2"
1261
+ "dowel-ui@^0.21.0"
1079
1262
  ],
1080
1263
  "registryDependencies": [],
1081
1264
  "files": [
@@ -1093,7 +1276,7 @@
1093
1276
  "title": "Truncate",
1094
1277
  "description": "Text that does not fit, cut with an ellipsis - and, importantly, still readable in full: the element carries its own text as a `title`, so hovering shows what was cut. Every product wrote the one-line version of this and none of them remembered the title.",
1095
1278
  "dependencies": [
1096
- "dowel-ui@^0.20.2"
1279
+ "dowel-ui@^0.21.0"
1097
1280
  ],
1098
1281
  "registryDependencies": [],
1099
1282
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dowel-ui",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "The lacodda line design system: theme tokens and React primitives, distributed as a shadcn-compatible registry.",
5
5
  "license": "MIT",
6
6
  "type": "module",