dowel-ui 0.16.0 → 0.17.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 +9 -7
- package/dist/registry.json +127 -29
- package/package.json +1 -1
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.
|
|
14
|
+
**Status:** v0.17.0 - the theme, the scales, an accent per product, thirty-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. 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,13 +98,15 @@ 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
|
-
Thirty of them so far. The everyday ones - Button, Input, Textarea, Panel,
|
|
101
|
+
Thirty-five 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
|
-
that answer a question - Checkbox, RadioGroup and Switch;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
that answer a question - Checkbox, RadioGroup and Switch; five for a number or
|
|
104
|
+
a judgement - NumberField, Slider, RatingScale, DurationField and
|
|
105
|
+
PasswordField; the six that float above the page - Dialog, ConfirmDialog,
|
|
106
|
+
Drawer, Popover, PreviewCard and Tooltip; four for choosing something - Menu,
|
|
107
|
+
ContextMenu, Select and Combobox; three for finding it - SearchField,
|
|
108
|
+
CommandPalette and the `useShortcut` behind them; and three for saying that
|
|
109
|
+
something happened: Toast, Alert and Banner.
|
|
108
110
|
|
|
109
111
|
Those last three are the ones products keep confusing, so each page names all
|
|
110
112
|
four options: a **toast** goes away, an **alert** is still true after a reload,
|
package/dist/registry.json
CHANGED
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
"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.",
|
|
223
223
|
"dependencies": [
|
|
224
224
|
"class-variance-authority",
|
|
225
|
-
"dowel-ui@^0.
|
|
225
|
+
"dowel-ui@^0.17.0"
|
|
226
226
|
],
|
|
227
227
|
"registryDependencies": [],
|
|
228
228
|
"files": [
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
"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.",
|
|
242
242
|
"dependencies": [
|
|
243
243
|
"class-variance-authority",
|
|
244
|
-
"dowel-ui@^0.
|
|
244
|
+
"dowel-ui@^0.17.0"
|
|
245
245
|
],
|
|
246
246
|
"registryDependencies": [],
|
|
247
247
|
"files": [
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"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.",
|
|
261
261
|
"dependencies": [
|
|
262
262
|
"class-variance-authority",
|
|
263
|
-
"dowel-ui@^0.
|
|
263
|
+
"dowel-ui@^0.17.0"
|
|
264
264
|
],
|
|
265
265
|
"registryDependencies": [],
|
|
266
266
|
"files": [
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
"dependencies": [
|
|
281
281
|
"@base-ui/react",
|
|
282
282
|
"class-variance-authority",
|
|
283
|
-
"dowel-ui@^0.
|
|
283
|
+
"dowel-ui@^0.17.0"
|
|
284
284
|
],
|
|
285
285
|
"registryDependencies": [],
|
|
286
286
|
"files": [
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
"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.",
|
|
300
300
|
"dependencies": [
|
|
301
301
|
"@base-ui/react",
|
|
302
|
-
"dowel-ui@^0.
|
|
302
|
+
"dowel-ui@^0.17.0"
|
|
303
303
|
],
|
|
304
304
|
"registryDependencies": [],
|
|
305
305
|
"files": [
|
|
@@ -318,7 +318,7 @@
|
|
|
318
318
|
"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.",
|
|
319
319
|
"dependencies": [
|
|
320
320
|
"class-variance-authority",
|
|
321
|
-
"dowel-ui@^0.
|
|
321
|
+
"dowel-ui@^0.17.0"
|
|
322
322
|
],
|
|
323
323
|
"registryDependencies": [],
|
|
324
324
|
"files": [
|
|
@@ -338,7 +338,7 @@
|
|
|
338
338
|
"dependencies": [
|
|
339
339
|
"@base-ui/react",
|
|
340
340
|
"class-variance-authority",
|
|
341
|
-
"dowel-ui@^0.
|
|
341
|
+
"dowel-ui@^0.17.0"
|
|
342
342
|
],
|
|
343
343
|
"registryDependencies": [
|
|
344
344
|
"https://lacodda.github.io/dowel/r/input.json",
|
|
@@ -361,7 +361,7 @@
|
|
|
361
361
|
"dependencies": [
|
|
362
362
|
"@base-ui/react",
|
|
363
363
|
"class-variance-authority",
|
|
364
|
-
"dowel-ui@^0.
|
|
364
|
+
"dowel-ui@^0.17.0"
|
|
365
365
|
],
|
|
366
366
|
"registryDependencies": [
|
|
367
367
|
"https://lacodda.github.io/dowel/r/combobox.json",
|
|
@@ -384,7 +384,7 @@
|
|
|
384
384
|
"dependencies": [
|
|
385
385
|
"@base-ui/react",
|
|
386
386
|
"class-variance-authority",
|
|
387
|
-
"dowel-ui@^0.
|
|
387
|
+
"dowel-ui@^0.17.0"
|
|
388
388
|
],
|
|
389
389
|
"registryDependencies": [],
|
|
390
390
|
"files": [
|
|
@@ -403,7 +403,7 @@
|
|
|
403
403
|
"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>`.",
|
|
404
404
|
"dependencies": [
|
|
405
405
|
"@base-ui/react",
|
|
406
|
-
"dowel-ui@^0.
|
|
406
|
+
"dowel-ui@^0.17.0"
|
|
407
407
|
],
|
|
408
408
|
"registryDependencies": [
|
|
409
409
|
"https://lacodda.github.io/dowel/r/menu.json"
|
|
@@ -423,7 +423,7 @@
|
|
|
423
423
|
"title": "Copyable",
|
|
424
424
|
"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.",
|
|
425
425
|
"dependencies": [
|
|
426
|
-
"dowel-ui@^0.
|
|
426
|
+
"dowel-ui@^0.17.0"
|
|
427
427
|
],
|
|
428
428
|
"registryDependencies": [],
|
|
429
429
|
"files": [
|
|
@@ -443,7 +443,7 @@
|
|
|
443
443
|
"dependencies": [
|
|
444
444
|
"@base-ui/react",
|
|
445
445
|
"class-variance-authority",
|
|
446
|
-
"dowel-ui@^0.
|
|
446
|
+
"dowel-ui@^0.17.0"
|
|
447
447
|
],
|
|
448
448
|
"registryDependencies": [],
|
|
449
449
|
"files": [
|
|
@@ -463,7 +463,7 @@
|
|
|
463
463
|
"dependencies": [
|
|
464
464
|
"@base-ui/react",
|
|
465
465
|
"class-variance-authority",
|
|
466
|
-
"dowel-ui@^0.
|
|
466
|
+
"dowel-ui@^0.17.0"
|
|
467
467
|
],
|
|
468
468
|
"registryDependencies": [],
|
|
469
469
|
"files": [
|
|
@@ -475,6 +475,26 @@
|
|
|
475
475
|
}
|
|
476
476
|
]
|
|
477
477
|
},
|
|
478
|
+
{
|
|
479
|
+
"name": "duration-field",
|
|
480
|
+
"type": "registry:ui",
|
|
481
|
+
"title": "Duration-field",
|
|
482
|
+
"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.",
|
|
483
|
+
"dependencies": [
|
|
484
|
+
"dowel-ui@^0.17.0"
|
|
485
|
+
],
|
|
486
|
+
"registryDependencies": [
|
|
487
|
+
"https://lacodda.github.io/dowel/r/input.json"
|
|
488
|
+
],
|
|
489
|
+
"files": [
|
|
490
|
+
{
|
|
491
|
+
"path": "ui/duration-field.tsx",
|
|
492
|
+
"target": "@ui/duration-field.tsx",
|
|
493
|
+
"type": "registry:ui",
|
|
494
|
+
"content": "import { useState, type Ref } from 'react'\nimport { cn } from 'dowel-ui'\nimport { fieldClasses } from './input'\n\n/*\n * DurationField - a length of time, typed the way people say it.\n *\n * The alternative is what products keep building: two number boxes labelled\n * \"hours\" and \"minutes\", which means two tab stops, two validations, and a\n * reader who has to divide 90 minutes in their head before typing. Here they\n * write `1h 30m`, or `90m`, or `1.5h`, and it means the same thing.\n *\n * The value is **minutes**, a plain number. Not a string, not a Duration\n * object: the products of this line store durations as minutes already, and\n * a field whose value has to be parsed by its caller has moved the problem\n * rather than solved it.\n *\n * Parsing is deliberately generous and formatting is strict. Anything a\n * person plausibly types is accepted - `2h`, `2 h`, `2:30`, `150`, `2h30`,\n * with or without spaces - and what comes back on blur is always the one\n * canonical spelling. That asymmetry is the whole design: being strict on\n * input means rejecting people, being loose on output means the column of\n * values never lines up.\n *\n * Empty is `null`, like NumberField: \"no duration\" and \"zero minutes\" are\n * different answers, and a task with no estimate is not a task estimated at\n * nothing.\n */\n\n/** Minutes from whatever was typed, or `null` for empty, or `undefined` when\n * it cannot be read as a duration at all.\n *\n * Exported because the parsing *is* the component - a test that goes through\n * the DOM checks React's state handling, and what needs checking is this. */\nexport function parseDuration(text: string): number | null | undefined {\n const input = text.trim().toLowerCase()\n if (input === '') return null\n\n // `2:30` - the clock spelling, which is unambiguous and worth taking.\n const clock = /^(\\d+):([0-5]?\\d)$/.exec(input)\n if (clock) return Number(clock[1]) * 60 + Number(clock[2])\n\n // A bare number is minutes. `90` is an hour and a half, not ninety hours:\n // the field is most often used for something that takes minutes, and a\n // reader typing hours writes the `h`.\n if (/^\\d+(?:[.,]\\d+)?$/.test(input)) return Math.round(Number(input.replace(',', '.')))\n\n /* The general form: any number of `<number><unit>` pairs. A decimal is\n * allowed on the hours (`1.5h`) because people write it, and a comma counts\n * as a decimal point because half the world uses one. */\n const pattern = /(\\d+(?:[.,]\\d+)?)\\s*([hm])/g\n let total = 0\n let matched = false\n let consumed = 0\n\n for (const match of input.matchAll(pattern)) {\n matched = true\n consumed += match[0].length\n const amount = Number(match[1]!.replace(',', '.'))\n total += match[2] === 'h' ? amount * 60 : amount\n }\n\n // Everything that is not whitespace has to have been part of a pair -\n // otherwise `1h banana` would quietly parse as an hour.\n if (!matched || consumed !== input.replace(/\\s+/g, '').length) return undefined\n\n return Math.round(total)\n}\n\n/** The one spelling a duration is written back as: `1h 30m`, `45m`, `2h`.\n *\n * Zero is `0m` rather than blank, because a duration of zero is an answer -\n * blank is what `null` renders as. */\nexport function formatDuration(minutes: number): string {\n const whole = Math.max(0, Math.round(minutes))\n const hours = Math.floor(whole / 60)\n const rest = whole % 60\n if (hours === 0) return `${rest}m`\n if (rest === 0) return `${hours}h`\n return `${hours}h ${rest}m`\n}\n\nexport interface DurationFieldProps {\n /** Minutes, or `null` for empty. */\n value: number | null\n onValueChange: (value: number | null) => void\n /** What a reader sees before they type. A duration in the canonical\n * spelling is the best hint there is, so this is the product's to give. */\n placeholder?: string\n disabled?: boolean\n readOnly?: boolean\n required?: boolean\n name?: string\n id?: string\n ref?: Ref<HTMLInputElement>\n 'aria-label'?: string\n 'aria-describedby'?: string\n className?: string\n}\n\nexport function DurationField({\n value,\n onValueChange,\n className,\n ref,\n ...props\n}: DurationFieldProps) {\n /* The field holds text while it is being typed and a number the rest of the\n * time. Without the local copy, typing `1h 3` would reformat under the\n * cursor after every keystroke - the classic controlled-input-with-parsing\n * bug, where the field fights the person using it. */\n const [text, setText] = useState(() => (value === null ? '' : formatDuration(value)))\n const [editing, setEditing] = useState(false)\n\n /* The `value` this box last saw from outside, adjusted during render rather\n * than in an effect - React's own pattern for a state that has to follow a\n * prop, and the one the `set-state-in-effect` rule points at.\n *\n * The comparison has to be against what was last *seen*, not against\n * `value`. After a commit the two differ in exactly the case that must be\n * left alone: `commit` writes `1h 30m` and tells the parent, the parent\n * still holds `null` for a tick, and comparing with `value` would clear the\n * box under the reader. Asking \"has the outside changed?\" answers it. */\n const [seen, setSeen] = useState<number | null>(value)\n\n if (value !== seen) {\n setSeen(value)\n // A value that changed while someone is typing is remembered, not shown:\n // reformatting under the cursor is the bug the local copy exists for.\n if (!editing) setText(value === null ? '' : formatDuration(value))\n }\n\n const commit = () => {\n setEditing(false)\n const parsed = parseDuration(text)\n if (parsed === undefined) {\n // Unreadable: put back what the value actually is rather than leaving\n // the box saying something the form does not believe.\n setText(value === null ? '' : formatDuration(value))\n return\n }\n setText(parsed === null ? '' : formatDuration(parsed))\n if (parsed !== value) onValueChange(parsed)\n }\n\n return (\n <input\n {...props}\n ref={ref}\n type=\"text\"\n inputMode=\"text\"\n value={text}\n onFocus={() => setEditing(true)}\n onChange={(event) => setText(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n commit()\n }\n }}\n className={cn(fieldClasses, 'h-9 tabular-nums', className)}\n />\n )\n}\n"
|
|
495
|
+
}
|
|
496
|
+
]
|
|
497
|
+
},
|
|
478
498
|
{
|
|
479
499
|
"name": "field",
|
|
480
500
|
"type": "registry:ui",
|
|
@@ -482,7 +502,7 @@
|
|
|
482
502
|
"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.",
|
|
483
503
|
"dependencies": [
|
|
484
504
|
"@base-ui/react",
|
|
485
|
-
"dowel-ui@^0.
|
|
505
|
+
"dowel-ui@^0.17.0"
|
|
486
506
|
],
|
|
487
507
|
"registryDependencies": [],
|
|
488
508
|
"files": [
|
|
@@ -500,7 +520,7 @@
|
|
|
500
520
|
"title": "Input",
|
|
501
521
|
"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.",
|
|
502
522
|
"dependencies": [
|
|
503
|
-
"dowel-ui@^0.
|
|
523
|
+
"dowel-ui@^0.17.0"
|
|
504
524
|
],
|
|
505
525
|
"registryDependencies": [],
|
|
506
526
|
"files": [
|
|
@@ -518,7 +538,7 @@
|
|
|
518
538
|
"title": "Kbd",
|
|
519
539
|
"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.",
|
|
520
540
|
"dependencies": [
|
|
521
|
-
"dowel-ui@^0.
|
|
541
|
+
"dowel-ui@^0.17.0"
|
|
522
542
|
],
|
|
523
543
|
"registryDependencies": [],
|
|
524
544
|
"files": [
|
|
@@ -538,7 +558,7 @@
|
|
|
538
558
|
"dependencies": [
|
|
539
559
|
"@base-ui/react",
|
|
540
560
|
"class-variance-authority",
|
|
541
|
-
"dowel-ui@^0.
|
|
561
|
+
"dowel-ui@^0.17.0"
|
|
542
562
|
],
|
|
543
563
|
"registryDependencies": [],
|
|
544
564
|
"files": [
|
|
@@ -550,6 +570,27 @@
|
|
|
550
570
|
}
|
|
551
571
|
]
|
|
552
572
|
},
|
|
573
|
+
{
|
|
574
|
+
"name": "number-field",
|
|
575
|
+
"type": "registry:ui",
|
|
576
|
+
"title": "Number-field",
|
|
577
|
+
"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.",
|
|
578
|
+
"dependencies": [
|
|
579
|
+
"@base-ui/react",
|
|
580
|
+
"dowel-ui@^0.17.0"
|
|
581
|
+
],
|
|
582
|
+
"registryDependencies": [
|
|
583
|
+
"https://lacodda.github.io/dowel/r/input.json"
|
|
584
|
+
],
|
|
585
|
+
"files": [
|
|
586
|
+
{
|
|
587
|
+
"path": "ui/number-field.tsx",
|
|
588
|
+
"target": "@ui/number-field.tsx",
|
|
589
|
+
"type": "registry:ui",
|
|
590
|
+
"content": "import type { ReactNode } from 'react'\nimport { NumberField as Base } from '@base-ui/react/number-field'\nimport { cn } from 'dowel-ui'\nimport { fieldClasses } from './input'\n\n/*\n * NumberField - a number, and the two ways of changing it.\n *\n * A number typed into a text input is a string that happens to look like a\n * number, and every product then writes the same four fixes: strip the\n * letters, clamp to a range, round to a step, and decide what an empty box\n * means. This is those four, once, plus the stepper - because a value with a\n * small range is faster nudged than typed.\n *\n * Base UI carries the parts that are genuinely hard: the arrow keys with\n * PageUp/PageDown for the large step, the parse of what a person actually\n * types (spaces, a comma for a decimal point, a pasted currency string), and\n * `Intl.NumberFormat` for how it reads back. That last one matters more than\n * it looks: a number field that shows `1234.5` where the reader writes\n * `1 234,5` is a field they have to translate in their head.\n *\n * `unit` is ours, and it is a label rather than part of the value. Putting\n * \"px\" inside the input makes it something to parse and something to delete\n * by accident; beside the input it is a caption that cannot be typed into.\n * The value stays a number.\n *\n * Empty is `null`, not zero. \"No number\" and \"the number zero\" are different\n * facts - a price of nothing and no price yet - and a field that returns 0 for\n * an empty box makes them the same the moment it is saved.\n */\n\nexport interface NumberFieldProps {\n value?: number | null\n defaultValue?: number\n onValueChange?: (value: number | null) => void\n min?: number\n max?: number\n /** What the arrows change it by. */\n step?: number\n /** What PageUp and PageDown change it by, when a single step is too slow. */\n largeStep?: number\n /** How the number reads: `Intl.NumberFormat` options, so a currency or a\n * percentage is a prop rather than a wrapper. */\n format?: Intl.NumberFormatOptions\n /** Which conventions `format` follows. Left alone it is the reader's own,\n * which is nearly always right; a product states one only when the figure\n * belongs to a place rather than to a person - a price in a fixed market. */\n locale?: Intl.LocalesArgument\n /** What the number is in - `px`, `kg`, `%`. A caption beside the field, not\n * part of the value. */\n unit?: ReactNode\n /** Hide the stepper. For a field with a wide range, where the buttons are\n * an invitation to click sixty times. */\n hideStepper?: boolean\n disabled?: boolean\n readOnly?: boolean\n required?: boolean\n name?: string\n placeholder?: string\n 'aria-label'?: string\n className?: string\n}\n\n/** The stepper's two buttons. Square, the height of the field, and marked\n * `aria-hidden` because the input they belong to already announces its value\n * and its range - a screen reader hearing \"increase, decrease\" as separate\n * controls learns nothing it did not have. */\nconst stepperButton = cn(\n 'flex w-7 shrink-0 items-center justify-center text-dim',\n 'transition-colors hover:bg-soft hover:text-text',\n 'disabled:pointer-events-none disabled:opacity-50',\n)\n\nexport function NumberField({\n unit,\n hideStepper = false,\n className,\n placeholder,\n 'aria-label': ariaLabel,\n ...props\n}: NumberFieldProps) {\n return (\n <Base.Root {...props} className={cn('inline-flex items-center gap-2', className)}>\n <Base.Group\n className={cn(\n fieldClasses,\n 'flex h-9 items-stretch overflow-hidden p-0',\n // The group carries the field's clothes, so the focus ring belongs\n // to the whole control rather than to the bare input inside it.\n 'focus-within:outline-2 focus-within:outline-offset-0 focus-within:outline-accent',\n )}\n >\n {!hideStepper && (\n <Base.Decrement className={cn(stepperButton, 'border-r border-line')} aria-hidden>\n <svg viewBox=\"0 0 16 16\" className=\"size-3.5\">\n <path d=\"M4 8h8\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </Base.Decrement>\n )}\n\n <Base.Input\n placeholder={placeholder}\n aria-label={ariaLabel}\n className={cn(\n 'w-full min-w-0 bg-transparent px-2.5 text-sm text-text placeholder:text-faint',\n 'outline-none',\n // Figures line up in a column, which is the whole reason a number\n // is in a field of its own.\n 'tabular-nums',\n hideStepper ? 'text-left' : 'text-center',\n )}\n />\n\n {!hideStepper && (\n <Base.Increment className={cn(stepperButton, 'border-l border-line')} aria-hidden>\n <svg viewBox=\"0 0 16 16\" className=\"size-3.5\">\n <path d=\"M8 4v8M4 8h8\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </Base.Increment>\n )}\n </Base.Group>\n\n {unit !== undefined && <span className=\"shrink-0 text-xs text-dim\">{unit}</span>}\n </Base.Root>\n )\n}\n"
|
|
591
|
+
}
|
|
592
|
+
]
|
|
593
|
+
},
|
|
553
594
|
{
|
|
554
595
|
"name": "panel",
|
|
555
596
|
"type": "registry:ui",
|
|
@@ -557,7 +598,7 @@
|
|
|
557
598
|
"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.",
|
|
558
599
|
"dependencies": [
|
|
559
600
|
"class-variance-authority",
|
|
560
|
-
"dowel-ui@^0.
|
|
601
|
+
"dowel-ui@^0.17.0"
|
|
561
602
|
],
|
|
562
603
|
"registryDependencies": [],
|
|
563
604
|
"files": [
|
|
@@ -569,6 +610,26 @@
|
|
|
569
610
|
}
|
|
570
611
|
]
|
|
571
612
|
},
|
|
613
|
+
{
|
|
614
|
+
"name": "password-field",
|
|
615
|
+
"type": "registry:ui",
|
|
616
|
+
"title": "Password-field",
|
|
617
|
+
"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.",
|
|
618
|
+
"dependencies": [
|
|
619
|
+
"dowel-ui@^0.17.0"
|
|
620
|
+
],
|
|
621
|
+
"registryDependencies": [
|
|
622
|
+
"https://lacodda.github.io/dowel/r/input.json"
|
|
623
|
+
],
|
|
624
|
+
"files": [
|
|
625
|
+
{
|
|
626
|
+
"path": "ui/password-field.tsx",
|
|
627
|
+
"target": "@ui/password-field.tsx",
|
|
628
|
+
"type": "registry:ui",
|
|
629
|
+
"content": "import { useId, useState, type Ref } from 'react'\nimport { cn } from 'dowel-ui'\nimport { fieldClasses } from './input'\n\n/*\n * PasswordField - a password, and the button that shows it.\n *\n * The reveal is the whole component, and it is not a convenience. A masked\n * field is the only one in a form where a typo cannot be seen, so people\n * either paste (fine) or type slowly and get it wrong anyway; the toggle is\n * what turns an unverifiable field into a checkable one, and it is why long\n * passphrases became usable at all.\n *\n * What it costs is a moment where the password is on the screen, so the\n * component states its two rules rather than leaving them to each product:\n *\n * - it always starts masked, and there is no prop to start it revealed;\n * - revealing is the reader's own action, never a default and never\n * something a form can turn on for them.\n *\n * The button is a real button with a real name, and the name changes with the\n * state - \"Show password\" / \"Hide password\". That is what a screen reader\n * announces, and it is the one place the component needs words, so they are\n * required props. A default here would ship English inside a primitive.\n *\n * `autoComplete` is not defaulted either. The right value is the product's\n * to know: `current-password` on a login, `new-password` on a sign-up, and\n * getting it wrong is how a password manager fills the wrong box.\n */\n\nexport interface PasswordFieldProps {\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n /** What the reveal button is called while the password is hidden. */\n showLabel: string\n /** ...and while it is showing. */\n hideLabel: string\n /** `current-password` for a login, `new-password` for a sign-up. */\n autoComplete?: string\n placeholder?: string\n disabled?: boolean\n readOnly?: boolean\n required?: boolean\n name?: string\n id?: string\n ref?: Ref<HTMLInputElement>\n 'aria-label'?: string\n 'aria-describedby'?: string\n className?: string\n}\n\nexport function PasswordField({\n value,\n defaultValue,\n onValueChange,\n showLabel,\n hideLabel,\n className,\n disabled,\n ref,\n ...props\n}: PasswordFieldProps) {\n /* Always false to begin with. Deliberately local state with no prop to set\n * it: a password that arrives on screen without the reader asking is the\n * one failure this component must not have. */\n const [revealed, setRevealed] = useState(false)\n const inputId = useId()\n\n return (\n <div\n className={cn(\n fieldClasses,\n 'flex h-9 items-stretch overflow-hidden p-0',\n 'focus-within:outline-2 focus-within:outline-offset-0 focus-within:outline-accent',\n className,\n )}\n >\n <input\n {...props}\n ref={ref}\n id={props.id ?? inputId}\n type={revealed ? 'text' : 'password'}\n value={value}\n defaultValue={defaultValue}\n disabled={disabled}\n onChange={(event) => onValueChange?.(event.target.value)}\n className={cn(\n 'w-full min-w-0 bg-transparent px-2.5 text-sm text-text placeholder:text-faint',\n 'outline-none disabled:cursor-not-allowed',\n )}\n />\n\n <button\n type=\"button\"\n // Not a submit button, and not in the tab order ahead of the field it\n // belongs to - it sits after the input, which is where Tab reaches it.\n onClick={() => setRevealed((was) => !was)}\n disabled={disabled}\n aria-label={revealed ? hideLabel : showLabel}\n aria-pressed={revealed}\n aria-controls={props.id ?? inputId}\n className={cn(\n 'flex w-9 shrink-0 items-center justify-center text-dim',\n 'transition-colors hover:bg-soft hover:text-text',\n 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent',\n 'disabled:pointer-events-none disabled:opacity-50',\n )}\n >\n {revealed ? (\n /* An eye with a stroke through it: hiding is the action offered\n * while the password is visible. */\n <svg viewBox=\"0 0 20 20\" className=\"size-4\" fill=\"none\" aria-hidden>\n <path\n d=\"M4 4l12 12M8.5 8.7a2 2 0 002.8 2.8M6.3 6.4C4.4 7.5 3 9.2 2.5 10c1.2 2.2 4 5 7.5 5 1.3 0 2.5-.4 3.5-1M9 5.1c.3 0 .7-.1 1-.1 3.5 0 6.3 2.8 7.5 5-.3.5-.8 1.3-1.6 2.1\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n />\n </svg>\n ) : (\n <svg viewBox=\"0 0 20 20\" className=\"size-4\" fill=\"none\" aria-hidden>\n <path\n d=\"M2.5 10C3.7 7.8 6.5 5 10 5s6.3 2.8 7.5 5c-1.2 2.2-4 5-7.5 5s-6.3-2.8-7.5-5z\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n />\n <circle cx=\"10\" cy=\"10\" r=\"2.2\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n </svg>\n )}\n </button>\n </div>\n )\n}\n"
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
},
|
|
572
633
|
{
|
|
573
634
|
"name": "popover",
|
|
574
635
|
"type": "registry:ui",
|
|
@@ -577,7 +638,7 @@
|
|
|
577
638
|
"dependencies": [
|
|
578
639
|
"@base-ui/react",
|
|
579
640
|
"class-variance-authority",
|
|
580
|
-
"dowel-ui@^0.
|
|
641
|
+
"dowel-ui@^0.17.0"
|
|
581
642
|
],
|
|
582
643
|
"registryDependencies": [],
|
|
583
644
|
"files": [
|
|
@@ -597,7 +658,7 @@
|
|
|
597
658
|
"dependencies": [
|
|
598
659
|
"@base-ui/react",
|
|
599
660
|
"class-variance-authority",
|
|
600
|
-
"dowel-ui@^0.
|
|
661
|
+
"dowel-ui@^0.17.0"
|
|
601
662
|
],
|
|
602
663
|
"registryDependencies": [],
|
|
603
664
|
"files": [
|
|
@@ -617,7 +678,7 @@
|
|
|
617
678
|
"dependencies": [
|
|
618
679
|
"@base-ui/react",
|
|
619
680
|
"class-variance-authority",
|
|
620
|
-
"dowel-ui@^0.
|
|
681
|
+
"dowel-ui@^0.17.0"
|
|
621
682
|
],
|
|
622
683
|
"registryDependencies": [],
|
|
623
684
|
"files": [
|
|
@@ -629,13 +690,31 @@
|
|
|
629
690
|
}
|
|
630
691
|
]
|
|
631
692
|
},
|
|
693
|
+
{
|
|
694
|
+
"name": "rating-scale",
|
|
695
|
+
"type": "registry:ui",
|
|
696
|
+
"title": "Rating-scale",
|
|
697
|
+
"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\".",
|
|
698
|
+
"dependencies": [
|
|
699
|
+
"dowel-ui@^0.17.0"
|
|
700
|
+
],
|
|
701
|
+
"registryDependencies": [],
|
|
702
|
+
"files": [
|
|
703
|
+
{
|
|
704
|
+
"path": "ui/rating-scale.tsx",
|
|
705
|
+
"target": "@ui/rating-scale.tsx",
|
|
706
|
+
"type": "registry:ui",
|
|
707
|
+
"content": "import { useId, type KeyboardEvent } from 'react'\nimport { cn } from 'dowel-ui'\n\n/*\n * RatingScale - a judgement on a short scale, and the absence of one.\n *\n * Generalised from kilna, where it is how a work is scored on each of its\n * axes. The shape is a row of marks rather than stars: stars carry a meaning\n * of their own - a review, a public verdict - and this is as often \"how hard\n * was this\" or \"how finished is it\" as it is \"how good\".\n *\n * The part worth keeping from the donor, and the reason this is not a Slider\n * with a small range: **not judged yet is a state, not a zero.** \"I have not\n * scored this\" and \"I scored it nothing\" are different facts, and a control\n * that collapses them makes the difference unrecoverable the moment it is\n * saved. So `value` is `number | undefined`, clicking the current mark clears\n * it, and Backspace does the same from the keyboard.\n *\n * One tab stop, arrows within it - the arrangement a radio group has. The\n * marks are not buttons: the container is the control, and a `<button>` inside\n * an element with `role=\"slider\"` is a nested interactive control - axe calls\n * it out, and it is right, because assistive technology is not promised to\n * announce or reach the inner one. The donor had them as `aria-hidden`\n * buttons, which hides them from a reader without making them stop being\n * controls.\n *\n * So a mark is a plain element that happens to accept a click. Everything\n * that makes the control usable - the tab stop, the keyboard, the announced\n * value - belongs to the container, and the pointer is served by the marks.\n */\n\nexport interface RatingScaleProps {\n /** How many marks. */\n scale: number\n /** The score, or `undefined` for not judged yet. */\n value: number | undefined\n onValueChange: (value: number | undefined) => void\n /** What is being judged. Required: a bare row of marks names nothing, and\n * this is the only thing a screen reader has to go on. */\n label: string\n /** What a screen reader hears in place of a number when nothing is chosen.\n * Required rather than defaulted, because a default here would be English\n * shipped inside a primitive. */\n emptyLabel: string\n disabled?: boolean\n className?: string\n}\n\nexport function RatingScale({\n scale,\n value,\n onValueChange,\n label,\n emptyLabel,\n disabled = false,\n className,\n}: RatingScaleProps) {\n const id = useId()\n const marks = Math.max(1, Math.round(scale))\n\n const clamp = (next: number) => Math.min(Math.max(next, 0), marks)\n\n const step = (delta: number) => {\n if (disabled) return\n // From nothing, a step forward lands on the first mark and a step back on\n // the last - so either arrow starts scoring rather than doing nothing.\n const next = value === undefined ? (delta > 0 ? 1 : marks) : value + delta\n onValueChange(clamp(next))\n }\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (disabled) return\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n event.preventDefault()\n step(1)\n break\n case 'ArrowLeft':\n case 'ArrowDown':\n event.preventDefault()\n step(-1)\n break\n case 'Home':\n event.preventDefault()\n onValueChange(0)\n break\n case 'End':\n event.preventDefault()\n onValueChange(marks)\n break\n case 'Backspace':\n case 'Delete':\n // The keyboard's way back to not judged. Without it the state is\n // reachable only by clicking the mark that is already chosen.\n event.preventDefault()\n onValueChange(undefined)\n break\n default:\n break\n }\n }\n\n return (\n <div\n id={id}\n role=\"slider\"\n tabIndex={disabled ? -1 : 0}\n aria-label={label}\n aria-valuemin={0}\n aria-valuemax={marks}\n aria-valuenow={value}\n aria-valuetext={value === undefined ? emptyLabel : String(value)}\n aria-disabled={disabled || undefined}\n onKeyDown={onKeyDown}\n className={cn(\n 'flex gap-[3px] rounded-md',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',\n disabled && 'cursor-not-allowed opacity-50',\n className,\n )}\n >\n {Array.from({ length: marks }, (_, index) => {\n const mark = index + 1\n const filled = value !== undefined && mark <= value\n return (\n <span\n key={mark}\n // Deliberately not a button. The container is the control; a\n // nested one would be a second interactive element inside a\n // `slider`, which assistive technology is not promised to handle.\n aria-hidden\n // Clicking the mark already chosen clears the score. That is the\n // pointer's way back to not judged, and it is why a rating is not\n // a five-option radio group.\n onClick={() => {\n if (disabled) return\n onValueChange(value === mark ? undefined : mark)\n }}\n className={cn(\n 'h-[22px] flex-1 rounded-sm transition-colors',\n disabled ? 'cursor-not-allowed' : 'cursor-pointer',\n filled ? 'bg-accent' : 'bg-soft',\n !disabled && (filled ? 'hover:bg-accent-2' : 'hover:bg-line-2'),\n )}\n />\n )\n })}\n </div>\n )\n}\n"
|
|
708
|
+
}
|
|
709
|
+
]
|
|
710
|
+
},
|
|
632
711
|
{
|
|
633
712
|
"name": "search-field",
|
|
634
713
|
"type": "registry:ui",
|
|
635
714
|
"title": "Search-field",
|
|
636
715
|
"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.",
|
|
637
716
|
"dependencies": [
|
|
638
|
-
"dowel-ui@^0.
|
|
717
|
+
"dowel-ui@^0.17.0"
|
|
639
718
|
],
|
|
640
719
|
"registryDependencies": [
|
|
641
720
|
"https://lacodda.github.io/dowel/r/input.json",
|
|
@@ -659,7 +738,7 @@
|
|
|
659
738
|
"dependencies": [
|
|
660
739
|
"@base-ui/react",
|
|
661
740
|
"class-variance-authority",
|
|
662
|
-
"dowel-ui@^0.
|
|
741
|
+
"dowel-ui@^0.17.0"
|
|
663
742
|
],
|
|
664
743
|
"registryDependencies": [
|
|
665
744
|
"https://lacodda.github.io/dowel/r/input.json"
|
|
@@ -689,6 +768,25 @@
|
|
|
689
768
|
}
|
|
690
769
|
]
|
|
691
770
|
},
|
|
771
|
+
{
|
|
772
|
+
"name": "slider",
|
|
773
|
+
"type": "registry:ui",
|
|
774
|
+
"title": "Slider",
|
|
775
|
+
"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\".",
|
|
776
|
+
"dependencies": [
|
|
777
|
+
"@base-ui/react",
|
|
778
|
+
"dowel-ui@^0.17.0"
|
|
779
|
+
],
|
|
780
|
+
"registryDependencies": [],
|
|
781
|
+
"files": [
|
|
782
|
+
{
|
|
783
|
+
"path": "ui/slider.tsx",
|
|
784
|
+
"target": "@ui/slider.tsx",
|
|
785
|
+
"type": "registry:ui",
|
|
786
|
+
"content": "import { Slider as Base } from '@base-ui/react/slider'\nimport { cn } from 'dowel-ui'\n\n/*\n * Slider - a value picked by position, and a range picked by two.\n *\n * The case for it over a NumberField is that the number does not matter much:\n * a volume, an opacity, a weight in a search filter. Where the exact figure\n * does matter, a slider is a worse field with more pixels - it cannot be\n * typed into, it cannot be pasted into, and it has no state for \"empty\".\n *\n * Range is the same component with an array. That is Base UI's arrangement\n * and it is the right one: a range slider is not a second control but the\n * same track with two thumbs, and splitting them would double the styling\n * and let the two drift apart. `value={[10, 40]}` is a range; `value={30}` is\n * a single.\n *\n * The parts are separate for a reason worth knowing. The Control is the whole\n * hit area - much taller than the visible track, so a pointer does not have\n * to find four pixels - while the Track is what is drawn, and the Indicator\n * is the filled part behind the thumb. Making the drawn track the hit area is\n * the commonest way a slider ends up hard to grab.\n */\n\nexport interface SliderProps {\n /** A number for one thumb, an array for a range. */\n value?: number | readonly number[]\n defaultValue?: number | readonly number[]\n /* `readonly` because that is what Base UI hands over, and narrowing it here\n * only moves the cast into every caller. */\n onValueChange?: (value: number | readonly number[]) => void\n /** Fires once when the drag ends, for the expensive thing a product does\n * not want to run on every pixel of movement. */\n onValueCommitted?: (value: number | readonly number[]) => void\n min?: number\n max?: number\n step?: number\n /** How close the thumbs of a range may come, in steps. */\n minStepsBetweenValues?: number\n /** How the value reads when it is shown: `Intl.NumberFormat` options. */\n format?: Intl.NumberFormatOptions\n orientation?: 'horizontal' | 'vertical'\n /** Show the current value beside the track. Off by default: on a row of\n * settings the numbers are noise, and where the figure matters a\n * NumberField is the better control. */\n showValue?: boolean\n /** What each thumb is called, by index.\n *\n * The thumb is the control - a hidden `<input type=\"range\">` - and\n * `aria-label` on the root names the *group* around it, which leaves every\n * thumb unnamed. axe reports it for a single slider as loudly as for a\n * range. Without this prop the group's own label is used for each thumb,\n * which is right for one and merely adequate for two: a range wants\n * \"Lowest price\" and \"Highest price\", not the same word twice. */\n getThumbLabel?: (index: number) => string\n disabled?: boolean\n name?: string\n 'aria-label'?: string\n className?: string\n}\n\nexport function Slider({ showValue = false, getThumbLabel, className, ...props }: SliderProps) {\n const vertical = props.orientation === 'vertical'\n\n /* Every thumb needs a name of its own, so the group's label is the fallback\n * rather than nothing. A slider whose only name sits on the group is one\n * where a reader lands on the control and is told a number. */\n const groupLabel = props['aria-label']\n const nameThumb = getThumbLabel ?? (groupLabel === undefined ? undefined : () => groupLabel)\n\n /* One thumb per value. Base UI addresses them by index, so a range needs as\n * many as the array is long - a single Thumb on a range renders one handle\n * that moves the first value and leaves the second unreachable. Read from\n * whichever of the two props is present, because the component is useful\n * both controlled and not. */\n const current = props.value ?? props.defaultValue ?? 0\n const thumbCount = Array.isArray(current) ? current.length : 1\n\n return (\n <Base.Root\n {...props}\n className={cn(\n 'flex items-center gap-3',\n vertical && 'h-40 flex-col',\n !vertical && 'w-full',\n className,\n )}\n >\n <Base.Control\n className={cn(\n // The hit area, deliberately larger than what is drawn: a four-pixel\n // target is a four-pixel target however pretty the track is.\n 'flex touch-none items-center',\n vertical ? 'h-full w-5 justify-center' : 'h-5 w-full flex-1',\n )}\n >\n <Base.Track\n className={cn(\n 'relative rounded-full bg-soft',\n vertical ? 'h-full w-1.5' : 'h-1.5 w-full',\n )}\n >\n <Base.Indicator className={cn('rounded-full bg-accent', vertical ? 'w-full' : 'h-full')} />\n {Array.from({ length: thumbCount }, (_, index) => (\n <Base.Thumb\n key={index}\n index={index}\n getAriaLabel={nameThumb}\n className={cn(\n 'size-4 rounded-full bg-accent shadow-lift',\n 'transition-[box-shadow,transform] duration-quick ease-out',\n 'hover:scale-110',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',\n 'data-[dragging]:scale-110',\n )}\n />\n ))}\n </Base.Track>\n </Base.Control>\n\n {showValue && (\n <Base.Value className=\"shrink-0 text-xs tabular-nums text-dim\" />\n )}\n </Base.Root>\n )\n}\n"
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
},
|
|
692
790
|
{
|
|
693
791
|
"name": "spinner",
|
|
694
792
|
"type": "registry:ui",
|
|
@@ -696,7 +794,7 @@
|
|
|
696
794
|
"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.",
|
|
697
795
|
"dependencies": [
|
|
698
796
|
"class-variance-authority",
|
|
699
|
-
"dowel-ui@^0.
|
|
797
|
+
"dowel-ui@^0.17.0"
|
|
700
798
|
],
|
|
701
799
|
"registryDependencies": [],
|
|
702
800
|
"files": [
|
|
@@ -715,7 +813,7 @@
|
|
|
715
813
|
"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.",
|
|
716
814
|
"dependencies": [
|
|
717
815
|
"@base-ui/react",
|
|
718
|
-
"dowel-ui@^0.
|
|
816
|
+
"dowel-ui@^0.17.0"
|
|
719
817
|
],
|
|
720
818
|
"registryDependencies": [],
|
|
721
819
|
"files": [
|
|
@@ -733,7 +831,7 @@
|
|
|
733
831
|
"title": "Textarea",
|
|
734
832
|
"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.",
|
|
735
833
|
"dependencies": [
|
|
736
|
-
"dowel-ui@^0.
|
|
834
|
+
"dowel-ui@^0.17.0"
|
|
737
835
|
],
|
|
738
836
|
"registryDependencies": [
|
|
739
837
|
"https://lacodda.github.io/dowel/r/input.json"
|
|
@@ -755,7 +853,7 @@
|
|
|
755
853
|
"dependencies": [
|
|
756
854
|
"@base-ui/react",
|
|
757
855
|
"class-variance-authority",
|
|
758
|
-
"dowel-ui@^0.
|
|
856
|
+
"dowel-ui@^0.17.0"
|
|
759
857
|
],
|
|
760
858
|
"registryDependencies": [],
|
|
761
859
|
"files": [
|
|
@@ -775,7 +873,7 @@
|
|
|
775
873
|
"dependencies": [
|
|
776
874
|
"@base-ui/react",
|
|
777
875
|
"class-variance-authority",
|
|
778
|
-
"dowel-ui@^0.
|
|
876
|
+
"dowel-ui@^0.17.0"
|
|
779
877
|
],
|
|
780
878
|
"registryDependencies": [],
|
|
781
879
|
"files": [
|
|
@@ -793,7 +891,7 @@
|
|
|
793
891
|
"title": "Truncate",
|
|
794
892
|
"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.",
|
|
795
893
|
"dependencies": [
|
|
796
|
-
"dowel-ui@^0.
|
|
894
|
+
"dowel-ui@^0.17.0"
|
|
797
895
|
],
|
|
798
896
|
"registryDependencies": [],
|
|
799
897
|
"files": [
|
package/package.json
CHANGED