dowel-ui 0.33.0 → 0.34.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.
@@ -307,7 +307,7 @@
307
307
  "title": "AboutPlate",
308
308
  "description": "The about-plate primitive.",
309
309
  "dependencies": [
310
- "dowel-ui@^0.33.0"
310
+ "dowel-ui@^0.34.0"
311
311
  ],
312
312
  "registryDependencies": [
313
313
  "https://lacodda.github.io/dowel/r/product-mark.json"
@@ -328,7 +328,7 @@
328
328
  "description": "A stack of collapsible sections that share one rule about how many can be open at once - an FAQ, the groups in a long settings page, the filters in a sidebar. Each section is `Collapsible`'s shape (a header, a trigger, a panel) repeated inside a group that also decides `single` or `multiple`, which is the one thing a lone `Collapsible` has no opinion about because it has nothing to be exclusive with.",
329
329
  "dependencies": [
330
330
  "@base-ui/react",
331
- "dowel-ui@^0.33.0"
331
+ "dowel-ui@^0.34.0"
332
332
  ],
333
333
  "registryDependencies": [
334
334
  "https://lacodda.github.io/dowel/r/collapsible.json"
@@ -350,7 +350,7 @@
350
350
  "dependencies": [
351
351
  "@base-ui/react",
352
352
  "class-variance-authority",
353
- "dowel-ui@^0.33.0"
353
+ "dowel-ui@^0.34.0"
354
354
  ],
355
355
  "registryDependencies": [],
356
356
  "files": [
@@ -369,7 +369,7 @@
369
369
  "description": "The shape everyone recognises: weeks as columns, weekdays as rows, time running left to right, and a value carried by how dark a square is. Two products of the line asked for it by name before it existed.",
370
370
  "dependencies": [
371
371
  "class-variance-authority",
372
- "dowel-ui@^0.33.0"
372
+ "dowel-ui@^0.34.0"
373
373
  ],
374
374
  "registryDependencies": [
375
375
  "https://lacodda.github.io/dowel/r/activity-weeks.json"
@@ -389,7 +389,7 @@
389
389
  "title": "ActivityLegend",
390
390
  "description": "Separate from the grid because a caller showing three grids on one screen wants one legend, and because the words in it are the product's.",
391
391
  "dependencies": [
392
- "dowel-ui@^0.33.0"
392
+ "dowel-ui@^0.34.0"
393
393
  ],
394
394
  "registryDependencies": [
395
395
  "https://lacodda.github.io/dowel/r/activity-heatmap.json",
@@ -427,7 +427,7 @@
427
427
  "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.",
428
428
  "dependencies": [
429
429
  "class-variance-authority",
430
- "dowel-ui@^0.33.0"
430
+ "dowel-ui@^0.34.0"
431
431
  ],
432
432
  "registryDependencies": [],
433
433
  "files": [
@@ -446,7 +446,7 @@
446
446
  "description": "The frame a product draws once and then never thinks about: a bar across the top, a rail down the left, and the screen in the corner they leave. It is three boxes and a grid, which is exactly why every product wrote its own - and why every one of them got the same two things wrong before getting them right.",
447
447
  "dependencies": [
448
448
  "class-variance-authority",
449
- "dowel-ui@^0.33.0"
449
+ "dowel-ui@^0.34.0"
450
450
  ],
451
451
  "registryDependencies": [],
452
452
  "files": [
@@ -465,7 +465,7 @@
465
465
  "description": "A person, in the space of a word. Every screen that lists people needs one, and the three things that go wrong with it are always the same:\n * - **The picture fails to load** and a broken-image glyph appears where a face was. The fallback is not a nicety; it is the state this component spends most of its life in, because half the people in any list have no picture at all.",
466
466
  "dependencies": [
467
467
  "class-variance-authority",
468
- "dowel-ui@^0.33.0"
468
+ "dowel-ui@^0.34.0"
469
469
  ],
470
470
  "registryDependencies": [],
471
471
  "files": [
@@ -484,7 +484,7 @@
484
484
  "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.",
485
485
  "dependencies": [
486
486
  "class-variance-authority",
487
- "dowel-ui@^0.33.0"
487
+ "dowel-ui@^0.34.0"
488
488
  ],
489
489
  "registryDependencies": [],
490
490
  "files": [
@@ -503,7 +503,7 @@
503
503
  "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.",
504
504
  "dependencies": [
505
505
  "class-variance-authority",
506
- "dowel-ui@^0.33.0"
506
+ "dowel-ui@^0.34.0"
507
507
  ],
508
508
  "registryDependencies": [],
509
509
  "files": [
@@ -522,7 +522,7 @@
522
522
  "description": "Bars rather than a line, and the distinction is the data's not the drawing's: a line says the value exists between the points, a column says each period is its own sum. Hours worked in a week is a sum - there is no \"Wednesday afternoon\" reading between two weeks - so it is a column.",
523
523
  "dependencies": [
524
524
  "class-variance-authority",
525
- "dowel-ui@^0.33.0"
525
+ "dowel-ui@^0.34.0"
526
526
  ],
527
527
  "registryDependencies": [],
528
528
  "files": [
@@ -534,6 +534,47 @@
534
534
  }
535
535
  ]
536
536
  },
537
+ {
538
+ "name": "big-calendar-item",
539
+ "type": "registry:ui",
540
+ "title": "BigCalendarItem",
541
+ "description": "One thing on a day of a month view, in one line: a release, a lesson, a payment that falls due.",
542
+ "dependencies": [
543
+ "@base-ui/react",
544
+ "dowel-ui@^0.34.0"
545
+ ],
546
+ "registryDependencies": [],
547
+ "files": [
548
+ {
549
+ "path": "ui/big-calendar-item.tsx",
550
+ "target": "@ui/big-calendar-item.tsx",
551
+ "type": "registry:ui",
552
+ "content": "import { createContext, useContext, type CSSProperties, type HTMLAttributes, type ReactNode } from 'react'\nimport { useRender } from '@base-ui/react/use-render'\nimport { cn } from 'dowel-ui'\n\n/*\n * BigCalendarItem.\n *\n * One thing on a day of a month view, in one line: a release, a lesson, a\n * payment that falls due.\n *\n * One line is the rule the month rests on. BigCalendar folds what a day has\n * no room for into a count, and the count is arithmetic - lines of twenty\n * pixels, four apart - so an item that wraps to two lines on a narrow day\n * breaks the sum and pushes the last line out of the day. The words truncate\n * instead; the whole of them is in `title`, or wherever the product puts a\n * hover card.\n *\n * The colour is the item's own, not the product's: the work it belongs to, the\n * category it is in. Any CSS colour is taken - `var(--series-3)` for a\n * category, the work's own colour where a product keeps one - and drawn as a\n * soft fill with ink leaning towards the theme's text, so the same colour\n * reads on the dark theme and the light one. Without a colour the item is\n * neutral.\n *\n * Two states change how it is drawn, because both are said by kilna's month\n * every day: `tentative` is a plan rather than a booking - a dashed outline,\n * so a suggestion never reads as a fact - and `done` has happened, drawn\n * quieter and still openable, and still readable: a weaker fill and dim ink,\n * not a faded line.\n *\n * It stands outside the month as well: the queue of things waiting for a date\n * beside it, and the copy that travels under the pointer while one is dragged,\n * are the same line.\n */\n\n/**\n * Whether a line stands in the day the month's keyboard is on: `true` there,\n * `false` in every other day, `null` outside a month.\n *\n * The month is one tab stop and the arrows walk its days; Tab from a day goes\n * into that day and then out of the month. So only the lines of the day under\n * the cursor are in the tab order - a month holding forty releases is not\n * forty tab stops. BigCalendarItem reads this itself; a product drawing its\n * own line reads it for the line's `tabIndex`.\n */\nexport const BigCalendarDayContext = createContext<boolean | null>(null)\n\nexport interface BigCalendarItemProps extends Omit<HTMLAttributes<HTMLElement>, 'color'> {\n /** The item's own colour - any CSS colour, `var(--series-3)` for a\n * category. Neutral without one. */\n color?: string\n /** A plan rather than a booking: a dashed outline. */\n tentative?: boolean\n /** It has happened: drawn quieter, still pressable. */\n done?: boolean\n /** Before the words - a cover, a dot. */\n start?: ReactNode\n /** After the words - the glyphs that mark it: a kind, a lock, a status.\n * They do not shrink; the words truncate. */\n end?: ReactNode\n /** The element to draw: the product's link, a hover card's trigger.\n * Without it the item is a `<button>` when it has `onClick`, and a `<div>`\n * when it has nothing to do. */\n render?: useRender.RenderProp\n}\n\nconst slot = 'flex shrink-0 items-center gap-0.5 [&_svg:not([class*=size-])]:size-3'\n\n/* Quieter is a weaker fill and an ink halfway from the text to dim - never\n * opacity. kilna drew a released chip at 60% and its words measured 2.7:1 in\n * the light theme, 1.7:1 on a day of the next month, which was dimmed again;\n * the series mixed into `--dim` alone still came to 3.55. Halfway clears AA\n * in every series of every product (`tests/palettes.test.ts` holds it). */\nconst QUIET_INK = 'color-mix(in oklab, var(--dim), var(--text))'\n\nfunction tint(color: string | undefined, tentative: boolean, done: boolean): CSSProperties | undefined {\n if (tentative) return color === undefined ? undefined : { borderColor: color }\n if (color === undefined) return done ? { color: QUIET_INK } : undefined\n return {\n background: `color-mix(in oklab, ${color} ${done ? 15 : 30}%, transparent)`,\n color: `color-mix(in oklab, ${color} 30%, ${done ? QUIET_INK : 'var(--text)'})`,\n }\n}\n\nexport function BigCalendarItem({\n color,\n tentative = false,\n done = false,\n start,\n end,\n render,\n className,\n style,\n children,\n onClick,\n ...props\n}: BigCalendarItemProps) {\n const pressable = onClick !== undefined\n const inCursorDay = useContext(BigCalendarDayContext)\n return useRender({\n render,\n defaultTagName: pressable ? 'button' : 'div',\n props: {\n ...(render === undefined && pressable ? { type: 'button' } : {}),\n ...(inCursorDay === false ? { tabIndex: -1 } : {}),\n onClick,\n 'data-tentative': tentative ? '' : undefined,\n 'data-done': done ? '' : undefined,\n className: cn(\n // `target-min`: the line is twenty pixels and the gap four, so the\n // invisible target reaches the floor of twenty-four and stops exactly\n // where the next line's begins.\n 'target-min flex h-5 w-full min-w-0 shrink-0 items-center gap-1 rounded-sm px-1 text-left text-xs',\n 'focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent',\n tentative\n ? 'border border-dashed border-line-2 font-medium text-dim'\n : cn(\n done ? 'font-medium' : 'font-semibold',\n color === undefined && (done ? 'bg-softer' : 'bg-soft text-text'),\n ),\n pressable && 'cursor-pointer hover:inset-ring hover:inset-ring-line-2',\n className,\n ),\n style: { ...tint(color, tentative, done), ...style },\n children: (\n <>\n {start === undefined ? null : <span className={slot}>{start}</span>}\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n {end === undefined ? null : <span className={slot}>{end}</span>}\n </>\n ),\n ...props,\n },\n })\n}\n"
553
+ }
554
+ ]
555
+ },
556
+ {
557
+ "name": "big-calendar",
558
+ "type": "registry:ui",
559
+ "title": "BigCalendar",
560
+ "description": "A month at a time, with what is on each day of it, filling the box it is given.",
561
+ "dependencies": [
562
+ "dowel-ui@^0.34.0"
563
+ ],
564
+ "registryDependencies": [
565
+ "https://lacodda.github.io/dowel/r/big-calendar-item.json",
566
+ "https://lacodda.github.io/dowel/r/calendar-math.json",
567
+ "https://lacodda.github.io/dowel/r/scroll-area.json"
568
+ ],
569
+ "files": [
570
+ {
571
+ "path": "ui/big-calendar.tsx",
572
+ "target": "@ui/big-calendar.tsx",
573
+ "type": "registry:ui",
574
+ "content": "import {\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type KeyboardEvent,\n type MouseEvent,\n type ReactNode,\n} from 'react'\nimport { cn, useLocale } from 'dowel-ui'\nimport { BigCalendarDayContext } from './big-calendar-item'\nimport { firstDayOfWeek, keyStep, monthWeeks, parts, today, weekdayNames, type IsoDate } from './calendar-math'\nimport { ScrollArea } from './scroll-area'\n\n/*\n * BigCalendar.\n *\n * A month at a time, with what is on each day of it, filling the box it is\n * given.\n *\n * The question a month view is read for is not \"what is on the 14th\" - a\n * list answers that - but \"is that week empty\". So the month holds its box:\n * the weeks share the height, a day never grows past its share, and what a\n * day has no room for folds into a count. The count opens the day in place,\n * at the same height with its lines scrolling inside it, so a busy Friday does\n * not push the following week off the screen. One day open at a time: several\n * and the grid stops being a month at a glance, which is the only reason the\n * days fold.\n *\n * What a day holds is the product's: `renderDay` returns its lines, one node\n * each - BigCalendarItem draws one - and the grid decides how many show. The\n * sum is exact rather than estimated, because a line is twenty pixels and two\n * are four apart: the grid measures one day's room and divides.\n *\n * Only the weeks the month touches are drawn, four to six, and the days of the\n * neighbouring months in them are real days on a quieter ground - a release\n * can be dragged onto the 31st of August from the September page. Today is\n * filled with the accent rather than coloured: on a grid of thirty-five cells\n * a coloured digit is not where the eye starts.\n *\n * The month's name and the arrows that turn it are not here. They belong to\n * the screen's head, in one row with its filters and actions, which is where\n * kilna moved them; the grid asks for a new month only when the keyboard walks\n * off its edge.\n *\n * The keyboard is the Calendar's: the grid is one tab stop, the arrows move a\n * cursor through the days, PageUp and PageDown turn the month, and Enter\n * presses the day. Tab from a day goes into it - its add button, then its\n * lines - and then out of the month, because the lines of every other day are\n * out of the tab order: a month holding forty releases is not forty stops.\n *\n * Dragging is the product's. Every day carries `data-day`, so whatever library\n * moves things finds the day under the pointer with `dayOf`, and the handlers\n * go on the calendar itself rather than on forty-two cells. `dropTarget` lights\n * the day something would land on, and `dropHint` says what landing there\n * would mean.\n */\n\n/** How tall a line of a day is, in pixels: BigCalendarItem's `h-5`. */\nexport const LINE_HEIGHT = 20\n\n/** The gap between two lines, in pixels: the day's `gap-1`. With the line it\n * makes the pitch of twenty-four, the floor of a pointer target. */\nexport const LINE_GAP = 4\n\n/* What a day is assumed to hold before it has been measured - the first frame,\n * and every test, since jsdom lays nothing out. Three lines are what a week\n * is at its least. */\nconst UNMEASURED_LINES = 3\n\n/** How many lines fit in `height` pixels of a day. */\nexport function linesThatFit(height: number): number {\n if (!Number.isFinite(height) || height <= 0) return 0\n return Math.floor((height + LINE_GAP) / (LINE_HEIGHT + LINE_GAP))\n}\n\n/** What a day of `count` lines draws in `lines` of room: the first `shown`,\n * then `more` folded into a count. The count takes a line of its own, so a day\n * that cannot show everything gives up one more than the overflow - five in\n * four lines is three and \"2 more\", never four and a count cut off at the\n * foot of the day. */\nexport function foldDay(count: number, lines: number): { shown: number; more: number } {\n if (count <= lines) return { shown: count, more: 0 }\n const shown = Math.max(lines - 1, 0)\n return { shown, more: count - shown }\n}\n\n/** The day an element is in, or `null` - for the target of a drop, or for\n * `document.elementFromPoint` under a pointer that is carrying something. */\nexport function dayOf(target: EventTarget | null): IsoDate | null {\n if (!(target instanceof Element)) return null\n return target.closest<HTMLElement>('[data-day]')?.dataset.day ?? null\n}\n\ntype AddDay =\n | {\n /** Add something on this day: a plus in the corner of the day under\n * the pointer, and the first stop of Tab from the day the cursor is on. */\n onAddDay: (date: IsoDate) => void\n /** Names the plus. Required with it: an icon with no name announces\n * nothing. */\n addDayLabel: string\n }\n | { onAddDay?: undefined; addDayLabel?: undefined }\n\nexport type BigCalendarProps = AddDay &\n Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onSelect'> & {\n /** Any day of the month to show. */\n month: IsoDate\n /** Asked for when the keyboard walks off the grid; any day of the new\n * month. Without it the cursor stops at the edge. */\n onMonthChange?: (month: IsoDate) => void\n /** What a day holds, one node per line, each with a `key`. */\n renderDay: (date: IsoDate) => readonly ReactNode[]\n /** The words of the count a full day folds into, given how many are\n * folded: \"3 more\". */\n moreLabel: (count: number) => string\n /** The words that fold an open day again. */\n fewerLabel: string\n /** Pressing a day - a click on its ground, or Enter on the cursor. */\n onDaySelect?: (date: IsoDate) => void\n /** The day something being carried would land on. */\n dropTarget?: IsoDate | null\n /** What landing on `dropTarget` would mean, said at the foot of it. */\n dropHint?: ReactNode\n /** Names the months and the weekdays, and picks the first day of the week:\n * `en-GB` starts on Monday, `en-US` on Sunday, `en-u-fw-mon` on Monday\n * whatever the region. The application's language by default. */\n locale?: string\n }\n\n/* The controls inside a day, which press themselves; the day's own press is\n * for its ground. */\nconst CONTROL = 'button, a, input, select, textarea, [role=\"button\"]'\n\nexport function BigCalendar({\n month,\n onMonthChange,\n renderDay,\n moreLabel,\n fewerLabel,\n onDaySelect,\n onAddDay,\n addDayLabel,\n dropTarget = null,\n dropHint,\n locale,\n className,\n onKeyDown,\n 'aria-label': ariaLabel,\n ...props\n}: BigCalendarProps) {\n const language = useLocale(locale)\n const start = firstDayOfWeek(language)\n const weeks = useMemo(() => monthWeeks(month, language), [month, language])\n const names = useMemo(() => weekdayNames(language, start), [language, start])\n const format = useMemo(\n () => ({\n heading: new Intl.DateTimeFormat(language, { month: 'long', year: 'numeric' }),\n day: new Intl.DateTimeFormat(language, { dateStyle: 'full' }),\n }),\n [language],\n )\n const asDate = (date: IsoDate) => new Date(parts(date).year, parts(date).month - 1, parts(date).day)\n\n const shown = parts(month)\n const inMonth = (date: IsoDate) => parts(date).year === shown.year && parts(date).month === shown.month\n const days = weeks.flat()\n const now = today()\n const id = useId()\n\n /* The keyboard's cursor. When the product turns the month with its own\n * arrows the old cursor is off the grid, and the keyboard starts again on\n * today if today is in this month, or on the day `month` names. */\n const [cursor, setCursor] = useState<IsoDate>(now)\n const active = days.includes(cursor) ? cursor : inMonth(now) ? now : month\n\n const grid = useRef<HTMLDivElement>(null)\n const refocus = useRef(false)\n useEffect(() => {\n if (!refocus.current) return\n refocus.current = false\n grid.current?.querySelector<HTMLElement>(`[data-day=\"${active}\"]`)?.focus()\n })\n\n /* How many lines a day has room for. Every day is the same height, so one\n * measurement answers for all of them, taken again whenever the box\n * changes. jsdom has no ResizeObserver, and lays nothing out to measure. */\n const [lines, setLines] = useState(UNMEASURED_LINES)\n useEffect(() => {\n const element = grid.current\n if (element === null || typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(() => {\n const room = element.querySelector<HTMLElement>('[data-day-lines]')\n if (room !== null) setLines(linesThatFit(room.clientHeight))\n })\n observer.observe(element)\n return () => observer.disconnect()\n }, [])\n\n const [open, setOpen] = useState<IsoDate | null>(null)\n\n const moveTo = (date: IsoDate) => {\n setCursor(date)\n refocus.current = true\n }\n\n const keyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n onKeyDown?.(event)\n if (event.defaultPrevented) return\n\n // Escape folds the open day from anywhere inside it.\n if (event.key === 'Escape' && open !== null && dayOf(event.target) === open) {\n event.preventDefault()\n setOpen(null)\n moveTo(open)\n return\n }\n\n // The rest belongs to the day itself, not a line inside it: a line may\n // want the arrows.\n const from = (event.target as HTMLElement).getAttribute('data-day')\n if (from === null) return\n if ((event.key === 'Enter' || event.key === ' ') && onDaySelect) {\n event.preventDefault()\n onDaySelect(from)\n return\n }\n\n const next = keyStep(event.key, from, start)\n if (next === undefined) return\n event.preventDefault()\n if (!days.includes(next)) {\n // Off the grid. The month is the product's, so it is asked to turn.\n if (!onMonthChange) return\n onMonthChange(next)\n }\n moveTo(next)\n }\n\n const pressDay = (date: IsoDate) => (event: MouseEvent<HTMLDivElement>) => {\n const control = (event.target as Element).closest(CONTROL)\n if (control !== null && event.currentTarget.contains(control)) return\n onDaySelect?.(date)\n }\n\n return (\n <div\n ref={grid}\n role=\"grid\"\n aria-label={ariaLabel ?? format.heading.format(asDate(month))}\n onKeyDown={keyDown}\n className={cn(\n 'flex min-h-0 min-w-0 flex-col overflow-hidden rounded-lg border border-line bg-raise',\n className,\n )}\n {...props}\n >\n <div role=\"rowgroup\">\n <div role=\"row\" className=\"grid grid-cols-7 border-b border-line\">\n {names.map((name) => (\n <div key={name} role=\"columnheader\" className=\"caption truncate px-2 py-1.5\">\n {name}\n </div>\n ))}\n </div>\n </div>\n\n <div role=\"rowgroup\" className=\"grid min-h-0 flex-1 auto-rows-fr\">\n {weeks.map((week, row) => (\n <div key={week[0]} role=\"row\" className=\"grid min-h-0 grid-cols-7\">\n {week.map((date, column) => {\n const items = renderDay(date)\n const fold = foldDay(items.length, lines)\n const isOpen = open === date && fold.more > 0\n const outside = !inMonth(date)\n const target = dropTarget === date\n\n return (\n <div\n key={date}\n role=\"gridcell\"\n data-day={date}\n data-outside={outside ? '' : undefined}\n data-drop-target={target ? '' : undefined}\n aria-current={date === now ? 'date' : undefined}\n // The date and what the day holds. Named from its whole\n // content, every day would also say \"Add a release on this\n // day\", thirty-five times over.\n aria-labelledby={`${id}${date}n ${id}${date}l`}\n tabIndex={date === active ? 0 : -1}\n onFocus={() => setCursor(date)}\n onClick={onDaySelect && pressDay(date)}\n className={cn(\n 'group/day relative flex min-h-0 min-w-0 flex-col gap-1 overflow-hidden p-1 transition-colors',\n 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent',\n column < 6 && 'border-r border-line',\n row < weeks.length - 1 && 'border-b border-line',\n outside && 'bg-softer',\n onDaySelect && 'cursor-pointer hover:bg-soft',\n target && 'bg-accent-soft hover:bg-accent-soft',\n )}\n >\n {/* The day in full for whoever cannot see the grid: \"14\" on\n * its own is not a date. */}\n <span id={`${id}${date}n`} className=\"sr-only\">\n {format.day.format(asDate(date))}\n </span>\n\n <div className=\"flex h-5 shrink-0 items-center justify-between gap-1\">\n <span\n aria-hidden\n className={cn(\n 'rounded-xs px-1 font-mono text-2xs tabular-nums',\n date === now\n ? 'bg-accent font-semibold text-on-accent'\n : outside\n ? 'text-faint'\n : 'text-dim',\n )}\n >\n {parts(date).day}\n </span>\n\n {/* Under the pointer, and on the day the keyboard is in: a\n * plus on every cell is thirty-five plus signs to read\n * past. Transparent rather than absent, so the row does\n * not reflow as the pointer crosses the month. */}\n {onAddDay && (\n <button\n type=\"button\"\n tabIndex={date === active ? 0 : -1}\n aria-label={addDayLabel}\n title={addDayLabel}\n onClick={() => onAddDay(date)}\n className={cn(\n 'target-min flex size-5 items-center justify-center rounded-sm text-dim opacity-0 transition-opacity',\n 'hover:bg-soft hover:text-text group-hover/day:opacity-100 group-focus-within/day:opacity-100',\n 'focus-visible:outline-2 focus-visible:outline-accent',\n )}\n >\n <svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\" aria-hidden>\n <path d=\"M8 3v10M3 8h10\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" />\n </svg>\n </button>\n )}\n </div>\n\n <BigCalendarDayContext.Provider value={date === active}>\n {isOpen ? (\n <ScrollArea label={format.day.format(asDate(date))} orientation=\"vertical\" className=\"flex-1\">\n <div id={`${id}${date}l`} className=\"flex flex-col gap-1\">\n {items}\n <Fold onClick={() => { setOpen(null); moveTo(date) }}>{fewerLabel}</Fold>\n </div>\n </ScrollArea>\n ) : (\n <div\n // The room `lines` is measured from: the same in every day.\n data-day-lines\n id={`${id}${date}l`}\n className=\"flex min-h-0 flex-1 flex-col gap-1 overflow-hidden\"\n >\n {items.slice(0, fold.shown)}\n {fold.more > 0 && (\n <Fold onClick={() => { setOpen(date); moveTo(date) }}>{moreLabel(fold.more)}</Fold>\n )}\n </div>\n )}\n </BigCalendarDayContext.Provider>\n\n {/* Over the foot of the day rather than under its lines: the\n * day does not grow, and the hint is only up while something\n * is carried over it. */}\n {target && dropHint != null && (\n <p className=\"pointer-events-none absolute inset-x-1 bottom-1 rounded-sm bg-raise px-1 py-0.5 text-2xs leading-tight text-dim shadow-lift\">\n {dropHint}\n </p>\n )}\n </div>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n}\n\n/** The line that folds a day or opens it: a line's height, so the sum holds. */\nfunction Fold({ onClick, children }: { onClick: () => void; children: string }) {\n const inCursorDay = useContext(BigCalendarDayContext)\n return (\n <button\n type=\"button\"\n tabIndex={inCursorDay === false ? -1 : 0}\n onClick={onClick}\n className={cn(\n 'target-min h-5 shrink-0 self-start rounded-sm px-1 text-xs text-dim hover:text-text',\n 'focus-visible:outline-2 focus-visible:outline-accent',\n )}\n >\n {children}\n </button>\n )\n}\n"
575
+ }
576
+ ]
577
+ },
537
578
  {
538
579
  "name": "breadcrumbs",
539
580
  "type": "registry:ui",
@@ -541,7 +582,7 @@
541
582
  "description": "Where you are, as a trail rather than as a word. On a list screen the screen's own name is enough; on something opened from it, it is not - \"Works\" says nothing about which work, and the way back to the list is otherwise the browser's back button alone, which a desktop window does not visibly have.",
542
583
  "dependencies": [
543
584
  "@base-ui/react",
544
- "dowel-ui@^0.33.0"
585
+ "dowel-ui@^0.34.0"
545
586
  ],
546
587
  "registryDependencies": [],
547
588
  "files": [
@@ -561,7 +602,7 @@
561
602
  "dependencies": [
562
603
  "@base-ui/react",
563
604
  "class-variance-authority",
564
- "dowel-ui@^0.33.0"
605
+ "dowel-ui@^0.34.0"
565
606
  ],
566
607
  "registryDependencies": [],
567
608
  "files": [
@@ -585,7 +626,7 @@
585
626
  "path": "ui/calendar-math.tsx",
586
627
  "target": "@ui/calendar-math.tsx",
587
628
  "type": "registry:ui",
588
- "content": "/*\n * The arithmetic a calendar runs on, with no React in it.\n *\n * Split out of the Calendar because the size gate asked the right question:\n * the file was two and a half times over its ceiling, and the reason was that\n * it held two things - the sums, and the grid that draws them. These are the\n * sums, and they are what DatePicker, DateRangePicker and any product doing\n * its own date work import.\n *\n * Everything here takes and returns `YYYY-MM-DD`, and never a `Date`. A\n * birthday has no timezone; a release date has no hour. Put one in a `Date`\n * and it becomes a moment - and moments cross midnight when they are\n * serialised, which is how a date reaches a server a day early. The string is\n * what a database column holds and what JSON carries.\n *\n * `Date` appears inside, in two places only: to ask `Intl` for a name, and to\n * add days. Both are wrapped here, so no caller ever holds one.\n *\n * No date library, deliberately. `react-day-picker` is good and would bring\n * `date-fns` and `@date-fns/tz` behind it - the first heavy dependency in a\n * set that is otherwise Base UI or nothing. `Intl` already knows the part a\n * library would be consulted for: which day the week starts on here, and what\n * the months are called. The rest is the arithmetic below, and it is only\n * hard when a date is stored as a moment.\n */\n\n/** A calendar date: `2026-09-02`. Not a moment - no time, no zone. */\nexport type IsoDate = string\n\n/** Whether a string is a calendar date this component can work with, and one\n * that actually exists. `2026-02-31` parses arithmetically and is not a day. */\nexport function isIsoDate(value: string): value is IsoDate {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false\n const [year, month, day] = value.split('-').map(Number) as [number, number, number]\n if (month < 1 || month > 12 || day < 1) return false\n return day <= daysInMonth(year, month)\n}\n\n/** How many days that month has. The leap rule in full, because the\n * hundred-year exception is the part that gets left out. */\nexport function daysInMonth(year: number, month: number): number {\n if (month === 2) {\n const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0\n return leap ? 29 : 28\n }\n return [4, 6, 9, 11].includes(month) ? 30 : 31\n}\n\n/** Today, as a calendar date in the reader's own timezone.\n *\n * Deliberately not `new Date().toISOString().slice(0, 10)`, which is the\n * common spelling and is wrong: that converts to UTC first, so anyone east of\n * Greenwich late in the evening gets tomorrow. */\nexport function today(): IsoDate {\n const now = new Date()\n return format(now.getFullYear(), now.getMonth() + 1, now.getDate())\n}\n\nfunction format(year: number, month: number, day: number): IsoDate {\n return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`\n}\n\ninterface Parts {\n year: number\n month: number\n day: number\n}\n\nexport function parts(date: IsoDate): Parts {\n const [year, month, day] = date.split('-').map(Number) as [number, number, number]\n return { year, month, day }\n}\n\n/** The same date shifted by whole days. Goes through a `Date` at noon rather\n * than midnight: a shift over a daylight-saving boundary at midnight can land\n * on the same calendar day it started from. */\nexport function addDays(date: IsoDate, days: number): IsoDate {\n const { year, month, day } = parts(date)\n const moved = new Date(year, month - 1, day, 12)\n moved.setDate(moved.getDate() + days)\n return format(moved.getFullYear(), moved.getMonth() + 1, moved.getDate())\n}\n\n/** The same day-of-month in another month, clamped when it does not exist\n * there: a step back from 31 March lands on 28 February, not on 3 March. */\nexport function addMonths(date: IsoDate, months: number): IsoDate {\n const { year, month, day } = parts(date)\n const zero = year * 12 + (month - 1) + months\n const nextYear = Math.floor(zero / 12)\n const nextMonth = (zero % 12) + 1\n return format(nextYear, nextMonth, Math.min(day, daysInMonth(nextYear, nextMonth)))\n}\n\n/** Which weekday a date falls on, as `Intl` numbers them: 1 is Monday, 7 is\n * Sunday. `Date` numbers Sunday 0, which does not sort and does not match\n * what `getWeekInfo` returns. */\nexport function weekday(date: IsoDate): number {\n const { year, month, day } = parts(date)\n const js = new Date(year, month - 1, day, 12).getDay()\n return js === 0 ? 7 : js\n}\n\n/** Which day the week starts on here: 1 Monday, 7 Sunday.\n *\n * `getWeekInfo` is the current spelling and `weekInfo` the older one; some\n * engines have neither, and Monday is the majority answer worldwide. */\nexport function firstDayOfWeek(locale: string): number {\n try {\n const info = new Intl.Locale(locale) as Intl.Locale & {\n getWeekInfo?: () => { firstDay: number }\n weekInfo?: { firstDay: number }\n }\n return info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay ?? 1\n } catch {\n return 1\n }\n}\n\n/** The grid of a month: whole weeks, starting on the locale's first day, with\n * the days either side included so every row has seven.\n *\n * Returned as dates rather than as numbers, so a cell never has to be told\n * which month it belongs to - it knows, and a click on a trailing day works\n * without a special case. */\nexport function monthGrid(month: IsoDate, locale: string): IsoDate[][] {\n const { year, month: monthNumber } = parts(month)\n const first = format(year, monthNumber, 1)\n const start = firstDayOfWeek(locale)\n\n // How far back the grid starts: the distance from the first of the month\n // back to the most recent week start.\n const lead = (weekday(first) - start + 7) % 7\n const origin = addDays(first, -lead)\n\n const weeks: IsoDate[][] = []\n let cursor = origin\n // Six rows always, so the calendar does not change height between months -\n // a popup that resizes as you page through it is one that moves under the\n // pointer.\n for (let week = 0; week < 6; week += 1) {\n const row: IsoDate[] = []\n for (let day = 0; day < 7; day += 1) {\n row.push(cursor)\n cursor = addDays(cursor, 1)\n }\n weeks.push(row)\n }\n return weeks\n}\n\n/** The weekday initials, in the order this locale lays them out. */\nexport function weekdayNames(locale: string, start: number): string[] {\n const names = new Intl.DateTimeFormat(locale, { weekday: 'short' })\n // Any week works; this one begins on a Monday.\n const monday = Date.UTC(2024, 0, 1)\n return Array.from({ length: 7 }, (_, index) => {\n const offset = (start - 1 + index) % 7\n return names.format(new Date(monday + offset * 86_400_000))\n })\n}\n"
629
+ "content": "/*\n * The arithmetic a calendar runs on, with no React in it.\n *\n * Split out of the Calendar because the size gate asked the right question:\n * the file was two and a half times over its ceiling, and the reason was that\n * it held two things - the sums, and the grid that draws them. These are the\n * sums, and they are what DatePicker, DateRangePicker and any product doing\n * its own date work import.\n *\n * Everything here takes and returns `YYYY-MM-DD`, and never a `Date`. A\n * birthday has no timezone; a release date has no hour. Put one in a `Date`\n * and it becomes a moment - and moments cross midnight when they are\n * serialised, which is how a date reaches a server a day early. The string is\n * what a database column holds and what JSON carries.\n *\n * `Date` appears inside, in two places only: to ask `Intl` for a name, and to\n * add days. Both are wrapped here, so no caller ever holds one.\n *\n * No date library, deliberately. `react-day-picker` is good and would bring\n * `date-fns` and `@date-fns/tz` behind it - the first heavy dependency in a\n * set that is otherwise Base UI or nothing. `Intl` already knows the part a\n * library would be consulted for: which day the week starts on here, and what\n * the months are called. The rest is the arithmetic below, and it is only\n * hard when a date is stored as a moment.\n */\n\n/** A calendar date: `2026-09-02`. Not a moment - no time, no zone. */\nexport type IsoDate = string\n\n/** Whether a string is a calendar date this component can work with, and one\n * that actually exists. `2026-02-31` parses arithmetically and is not a day. */\nexport function isIsoDate(value: string): value is IsoDate {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return false\n const [year, month, day] = value.split('-').map(Number) as [number, number, number]\n if (month < 1 || month > 12 || day < 1) return false\n return day <= daysInMonth(year, month)\n}\n\n/** How many days that month has. The leap rule in full, because the\n * hundred-year exception is the part that gets left out. */\nexport function daysInMonth(year: number, month: number): number {\n if (month === 2) {\n const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0\n return leap ? 29 : 28\n }\n return [4, 6, 9, 11].includes(month) ? 30 : 31\n}\n\n/** Today, as a calendar date in the reader's own timezone.\n *\n * Deliberately not `new Date().toISOString().slice(0, 10)`, which is the\n * common spelling and is wrong: that converts to UTC first, so anyone east of\n * Greenwich late in the evening gets tomorrow. */\nexport function today(): IsoDate {\n const now = new Date()\n return format(now.getFullYear(), now.getMonth() + 1, now.getDate())\n}\n\nfunction format(year: number, month: number, day: number): IsoDate {\n return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`\n}\n\ninterface Parts {\n year: number\n month: number\n day: number\n}\n\nexport function parts(date: IsoDate): Parts {\n const [year, month, day] = date.split('-').map(Number) as [number, number, number]\n return { year, month, day }\n}\n\n/** The same date shifted by whole days. Goes through a `Date` at noon rather\n * than midnight: a shift over a daylight-saving boundary at midnight can land\n * on the same calendar day it started from. */\nexport function addDays(date: IsoDate, days: number): IsoDate {\n const { year, month, day } = parts(date)\n const moved = new Date(year, month - 1, day, 12)\n moved.setDate(moved.getDate() + days)\n return format(moved.getFullYear(), moved.getMonth() + 1, moved.getDate())\n}\n\n/** The same day-of-month in another month, clamped when it does not exist\n * there: a step back from 31 March lands on 28 February, not on 3 March. */\nexport function addMonths(date: IsoDate, months: number): IsoDate {\n const { year, month, day } = parts(date)\n const zero = year * 12 + (month - 1) + months\n const nextYear = Math.floor(zero / 12)\n const nextMonth = (zero % 12) + 1\n return format(nextYear, nextMonth, Math.min(day, daysInMonth(nextYear, nextMonth)))\n}\n\n/** Which weekday a date falls on, as `Intl` numbers them: 1 is Monday, 7 is\n * Sunday. `Date` numbers Sunday 0, which does not sort and does not match\n * what `getWeekInfo` returns. */\nexport function weekday(date: IsoDate): number {\n const { year, month, day } = parts(date)\n const js = new Date(year, month - 1, day, 12).getDay()\n return js === 0 ? 7 : js\n}\n\n/** Which day the week starts on here: 1 Monday, 7 Sunday.\n *\n * `getWeekInfo` is the current spelling and `weekInfo` the older one; some\n * engines have neither, and Monday is the majority answer worldwide. */\nexport function firstDayOfWeek(locale: string): number {\n try {\n const info = new Intl.Locale(locale) as Intl.Locale & {\n getWeekInfo?: () => { firstDay: number }\n weekInfo?: { firstDay: number }\n }\n return info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay ?? 1\n } catch {\n return 1\n }\n}\n\n/** The grid of a month: whole weeks, starting on the locale's first day, with\n * the days either side included so every row has seven.\n *\n * Returned as dates rather than as numbers, so a cell never has to be told\n * which month it belongs to - it knows, and a click on a trailing day works\n * without a special case. */\nexport function monthGrid(month: IsoDate, locale: string): IsoDate[][] {\n const { year, month: monthNumber } = parts(month)\n const first = format(year, monthNumber, 1)\n const start = firstDayOfWeek(locale)\n\n // How far back the grid starts: the distance from the first of the month\n // back to the most recent week start.\n const lead = (weekday(first) - start + 7) % 7\n const origin = addDays(first, -lead)\n\n const weeks: IsoDate[][] = []\n let cursor = origin\n // Six rows always, so the calendar does not change height between months -\n // a popup that resizes as you page through it is one that moves under the\n // pointer.\n for (let week = 0; week < 6; week += 1) {\n const row: IsoDate[] = []\n for (let day = 0; day < 7; day += 1) {\n row.push(cursor)\n cursor = addDays(cursor, 1)\n }\n weeks.push(row)\n }\n return weeks\n}\n\n/** The weeks a month actually touches: the rows of `monthGrid` holding at\n * least one of its days - four to six of them.\n *\n * The Calendar keeps six rows always, because a popup that changes height\n * moves under the pointer. A month filling a screen has the opposite problem:\n * a sixth row made entirely of the next month makes every month look six\n * weeks long, and takes a sixth of the height from the days that are in it. */\nexport function monthWeeks(month: IsoDate, locale: string): IsoDate[][] {\n const { year, month: monthNumber } = parts(month)\n return monthGrid(month, locale).filter((week) =>\n week.some((date) => {\n const day = parts(date)\n return day.year === year && day.month === monthNumber\n }),\n )\n}\n\n/** Where a calendar's cursor goes for a key, or `undefined` for a key that\n * does not move it. `weekStart` is the locale's first day, which is where\n * Home lands.\n *\n * One map for every grid of days in the set, so the Calendar and the month\n * view cannot come to disagree about what PageDown means. */\nexport function keyStep(key: string, from: IsoDate, weekStart: number): IsoDate | undefined {\n const intoWeek = (weekday(from) - weekStart + 7) % 7\n const steps: Record<string, () => IsoDate> = {\n ArrowRight: () => addDays(from, 1),\n ArrowLeft: () => addDays(from, -1),\n ArrowDown: () => addDays(from, 7),\n ArrowUp: () => addDays(from, -7),\n PageDown: () => addMonths(from, 1),\n PageUp: () => addMonths(from, -1),\n Home: () => addDays(from, -intoWeek),\n End: () => addDays(from, 6 - intoWeek),\n }\n return steps[key]?.()\n}\n\n/** The weekday initials, in the order this locale lays them out.\n *\n * The week below is midnight in UTC, so it is named in UTC. Named in the\n * reader's own zone, as it was until v0.34, every reader west of Greenwich saw\n * each midnight as the evening before: a Monday-first grid headed \"Sun Mon\n * Tue\", one column off from the days under it. The tests ran in UTC, where\n * the two agree, and passed. */\nexport function weekdayNames(locale: string, start: number): string[] {\n const names = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' })\n // Any week works; this one begins on a Monday.\n const monday = Date.UTC(2024, 0, 1)\n return Array.from({ length: 7 }, (_, index) => {\n const offset = (start - 1 + index) % 7\n return names.format(new Date(monday + offset * 86_400_000))\n })\n}\n"
589
630
  }
590
631
  ]
591
632
  },
@@ -595,7 +636,7 @@
595
636
  "title": "Calendar",
596
637
  "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.",
597
638
  "dependencies": [
598
- "dowel-ui@^0.33.0"
639
+ "dowel-ui@^0.34.0"
599
640
  ],
600
641
  "registryDependencies": [
601
642
  "https://lacodda.github.io/dowel/r/calendar-math.json"
@@ -605,7 +646,7 @@
605
646
  "path": "ui/calendar.tsx",
606
647
  "target": "@ui/calendar.tsx",
607
648
  "type": "registry:ui",
608
- "content": "import { useMemo, useState, type KeyboardEvent } from 'react'\nimport { cn, useLocale } from 'dowel-ui'\nimport {\n addDays,\n addMonths,\n firstDayOfWeek,\n monthGrid,\n parts,\n today,\n weekday,\n weekdayNames,\n type IsoDate,\n} from './calendar-math'\n\n/*\n * Calendar - a month of days.\n *\n * The sums live next door in `calendar-math`, which has no React in it; this\n * is the grid that draws them and the keyboard that moves around it.\n *\n * One tab stop for the whole grid, arrows within - the arrangement a radio\n * group has, and the reason a calendar is usable at all: forty-two tab stops\n * is not a control. Arrows move a cursor and only Enter chooses, so a product\n * listening for a change does not receive five dates on the way to the sixth.\n */\n\nexport interface CalendarProps {\n /** The selected day, or `undefined` for none. */\n value?: IsoDate\n onValueChange?: (value: IsoDate) => void\n /** Which month is shown. Uncontrolled unless given. */\n month?: IsoDate\n onMonthChange?: (month: IsoDate) => void\n /** Bounds, inclusive. A day outside them cannot be chosen. */\n min?: IsoDate\n max?: IsoDate\n /** For a range: the other end, so the days between can be shaded. */\n rangeEnd?: IsoDate\n /** Formats the names. The application's language by default - see `useLocale`. */\n locale?: string\n /** What the grid is called, for a screen reader. */\n 'aria-label'?: string\n /** Names the buttons that page the months. Required: they are icons, and an\n * icon with no name is a button that announces nothing. */\n previousMonthLabel: string\n nextMonthLabel: string\n className?: string\n}\n\nexport function Calendar({\n value,\n onValueChange,\n month,\n onMonthChange,\n min,\n max,\n rangeEnd,\n locale,\n previousMonthLabel,\n nextMonthLabel,\n className,\n 'aria-label': ariaLabel,\n}: CalendarProps) {\n const [ownMonth, setOwnMonth] = useState<IsoDate>(() => value ?? today())\n const shown = month ?? ownMonth\n\n /* Which day the keyboard is on. It is not the selection: arrowing around a\n * calendar moves a cursor, and only Enter chooses - otherwise every arrow\n * key would fire `onValueChange` and a product listening for it would save\n * five dates on the way to the sixth. */\n const [focused, setFocused] = useState<IsoDate>(() => value ?? today())\n\n const language = useLocale(locale)\n const weeks = useMemo(() => monthGrid(shown, language), [shown, language])\n const start = firstDayOfWeek(language)\n const names = useMemo(() => weekdayNames(language, start), [language, start])\n const heading = useMemo(\n () => new Intl.DateTimeFormat(language, { month: 'long', year: 'numeric' }).format(\n new Date(parts(shown).year, parts(shown).month - 1, 1),\n ),\n [shown, language],\n )\n const dayNumber = useMemo(() => new Intl.DateTimeFormat(language, { day: 'numeric' }), [language])\n const fullDate = useMemo(\n () => new Intl.DateTimeFormat(language, { dateStyle: 'long' }),\n [language],\n )\n\n const outOfBounds = (date: IsoDate) =>\n (min !== undefined && date < min) || (max !== undefined && date > max)\n\n const goToMonth = (next: IsoDate) => {\n if (month === undefined) setOwnMonth(next)\n onMonthChange?.(next)\n }\n\n const moveFocus = (next: IsoDate) => {\n setFocused(next)\n // Paging follows the cursor: arrowing off the end of a month shows the\n // next one rather than moving to a day nobody can see.\n if (parts(next).month !== parts(shown).month || parts(next).year !== parts(shown).year) {\n goToMonth(next)\n }\n }\n\n const onKeyDown = (event: KeyboardEvent) => {\n const jump: Record<string, () => IsoDate> = {\n ArrowRight: () => addDays(focused, 1),\n ArrowLeft: () => addDays(focused, -1),\n ArrowDown: () => addDays(focused, 7),\n ArrowUp: () => addDays(focused, -7),\n PageDown: () => addMonths(focused, 1),\n PageUp: () => addMonths(focused, -1),\n Home: () => addDays(focused, -((weekday(focused) - start + 7) % 7)),\n End: () => addDays(focused, 6 - ((weekday(focused) - start + 7) % 7)),\n }\n\n const move = jump[event.key]\n if (move) {\n event.preventDefault()\n moveFocus(move())\n return\n }\n\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (!outOfBounds(focused)) onValueChange?.(focused)\n }\n }\n\n const now = today()\n\n return (\n <div className={cn('w-64 select-none', className)}>\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <button\n type=\"button\"\n aria-label={previousMonthLabel}\n onClick={() => goToMonth(addMonths(shown, -1))}\n className={cn(\n 'flex size-7 items-center justify-center rounded-md 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 )}\n >\n <svg viewBox=\"0 0 16 16\" className=\"size-4\" aria-hidden>\n <path\n d=\"M10 3L5 8l5 5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n {/* The month is announced when it changes, so paging with the arrows\n * says where you have arrived rather than moving silently. */}\n <div aria-live=\"polite\" className=\"text-sm font-medium text-text\">\n {heading}\n </div>\n\n <button\n type=\"button\"\n aria-label={nextMonthLabel}\n onClick={() => goToMonth(addMonths(shown, 1))}\n className={cn(\n 'flex size-7 items-center justify-center rounded-md 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 )}\n >\n <svg viewBox=\"0 0 16 16\" className=\"size-4\" aria-hidden>\n <path\n d=\"M6 3l5 5-5 5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n </div>\n\n {/* One tab stop for the whole grid, and the arrows move within it - the\n * arrangement a radio group has, and the reason a calendar is usable at\n * all: forty-two tab stops is not a control. */}\n <div\n role=\"grid\"\n aria-label={ariaLabel}\n tabIndex={0}\n onKeyDown={onKeyDown}\n className={cn(\n 'rounded-md',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',\n )}\n >\n <div role=\"row\" className=\"mb-1 grid grid-cols-7\">\n {names.map((name) => (\n <div\n key={name}\n role=\"columnheader\"\n aria-label={name}\n className=\"caption py-1 text-center\"\n >\n {name}\n </div>\n ))}\n </div>\n\n {weeks.map((week) => (\n <div role=\"row\" key={week[0]} className=\"grid grid-cols-7\">\n {week.map((date) => {\n const outside = parts(date).month !== parts(shown).month\n const disabled = outOfBounds(date)\n const selected =\n value !== undefined &&\n (rangeEnd === undefined\n ? date === value\n : date === value || date === rangeEnd)\n const inRange =\n value !== undefined && rangeEnd !== undefined && date > value && date < rangeEnd\n\n return (\n <div role=\"gridcell\" key={date} aria-selected={selected || undefined}>\n <button\n type=\"button\"\n // Not a tab stop: the grid is the control. Announced with\n // its full date, because \"14\" on its own is not a date.\n tabIndex={-1}\n disabled={disabled}\n aria-label={fullDate.format(new Date(parts(date).year, parts(date).month - 1, parts(date).day))}\n aria-current={date === now ? 'date' : undefined}\n onClick={() => {\n setFocused(date)\n if (!disabled) onValueChange?.(date)\n }}\n className={cn(\n 'flex h-8 w-full items-center justify-center rounded-md text-sm tabular-nums',\n 'transition-colors',\n outside ? 'text-faint' : 'text-text',\n inRange && 'bg-accent-soft',\n selected && 'bg-accent font-medium text-on-accent',\n !selected && !disabled && 'hover:bg-soft',\n date === now && !selected && 'font-medium text-accent',\n date === focused && 'ring-1 ring-line-2',\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n {dayNumber.format(new Date(parts(date).year, parts(date).month - 1, parts(date).day))}\n </button>\n </div>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n}\n"
649
+ "content": "import { useMemo, useState, type KeyboardEvent } from 'react'\nimport { cn, useLocale } from 'dowel-ui'\nimport {\n addMonths,\n firstDayOfWeek,\n keyStep,\n monthGrid,\n parts,\n today,\n weekdayNames,\n type IsoDate,\n} from './calendar-math'\n\n/*\n * Calendar - a month of days.\n *\n * The sums live next door in `calendar-math`, which has no React in it; this\n * is the grid that draws them and the keyboard that moves around it.\n *\n * One tab stop for the whole grid, arrows within - the arrangement a radio\n * group has, and the reason a calendar is usable at all: forty-two tab stops\n * is not a control. Arrows move a cursor and only Enter chooses, so a product\n * listening for a change does not receive five dates on the way to the sixth.\n */\n\nexport interface CalendarProps {\n /** The selected day, or `undefined` for none. */\n value?: IsoDate\n onValueChange?: (value: IsoDate) => void\n /** Which month is shown. Uncontrolled unless given. */\n month?: IsoDate\n onMonthChange?: (month: IsoDate) => void\n /** Bounds, inclusive. A day outside them cannot be chosen. */\n min?: IsoDate\n max?: IsoDate\n /** For a range: the other end, so the days between can be shaded. */\n rangeEnd?: IsoDate\n /** Formats the names. The application's language by default - see `useLocale`. */\n locale?: string\n /** What the grid is called, for a screen reader. */\n 'aria-label'?: string\n /** Names the buttons that page the months. Required: they are icons, and an\n * icon with no name is a button that announces nothing. */\n previousMonthLabel: string\n nextMonthLabel: string\n className?: string\n}\n\nexport function Calendar({\n value,\n onValueChange,\n month,\n onMonthChange,\n min,\n max,\n rangeEnd,\n locale,\n previousMonthLabel,\n nextMonthLabel,\n className,\n 'aria-label': ariaLabel,\n}: CalendarProps) {\n const [ownMonth, setOwnMonth] = useState<IsoDate>(() => value ?? today())\n const shown = month ?? ownMonth\n\n /* Which day the keyboard is on. It is not the selection: arrowing around a\n * calendar moves a cursor, and only Enter chooses - otherwise every arrow\n * key would fire `onValueChange` and a product listening for it would save\n * five dates on the way to the sixth. */\n const [focused, setFocused] = useState<IsoDate>(() => value ?? today())\n\n const language = useLocale(locale)\n const weeks = useMemo(() => monthGrid(shown, language), [shown, language])\n const start = firstDayOfWeek(language)\n const names = useMemo(() => weekdayNames(language, start), [language, start])\n const heading = useMemo(\n () => new Intl.DateTimeFormat(language, { month: 'long', year: 'numeric' }).format(\n new Date(parts(shown).year, parts(shown).month - 1, 1),\n ),\n [shown, language],\n )\n const dayNumber = useMemo(() => new Intl.DateTimeFormat(language, { day: 'numeric' }), [language])\n const fullDate = useMemo(\n () => new Intl.DateTimeFormat(language, { dateStyle: 'long' }),\n [language],\n )\n\n const outOfBounds = (date: IsoDate) =>\n (min !== undefined && date < min) || (max !== undefined && date > max)\n\n const goToMonth = (next: IsoDate) => {\n if (month === undefined) setOwnMonth(next)\n onMonthChange?.(next)\n }\n\n const moveFocus = (next: IsoDate) => {\n setFocused(next)\n // Paging follows the cursor: arrowing off the end of a month shows the\n // next one rather than moving to a day nobody can see.\n if (parts(next).month !== parts(shown).month || parts(next).year !== parts(shown).year) {\n goToMonth(next)\n }\n }\n\n const onKeyDown = (event: KeyboardEvent) => {\n const next = keyStep(event.key, focused, start)\n if (next !== undefined) {\n event.preventDefault()\n moveFocus(next)\n return\n }\n\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n if (!outOfBounds(focused)) onValueChange?.(focused)\n }\n }\n\n const now = today()\n\n return (\n <div className={cn('w-64 select-none', className)}>\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <button\n type=\"button\"\n aria-label={previousMonthLabel}\n onClick={() => goToMonth(addMonths(shown, -1))}\n className={cn(\n 'flex size-7 items-center justify-center rounded-md 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 )}\n >\n <svg viewBox=\"0 0 16 16\" className=\"size-4\" aria-hidden>\n <path\n d=\"M10 3L5 8l5 5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n {/* The month is announced when it changes, so paging with the arrows\n * says where you have arrived rather than moving silently. */}\n <div aria-live=\"polite\" className=\"text-sm font-medium text-text\">\n {heading}\n </div>\n\n <button\n type=\"button\"\n aria-label={nextMonthLabel}\n onClick={() => goToMonth(addMonths(shown, 1))}\n className={cn(\n 'flex size-7 items-center justify-center rounded-md 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 )}\n >\n <svg viewBox=\"0 0 16 16\" className=\"size-4\" aria-hidden>\n <path\n d=\"M6 3l5 5-5 5\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n </div>\n\n {/* One tab stop for the whole grid, and the arrows move within it - the\n * arrangement a radio group has, and the reason a calendar is usable at\n * all: forty-two tab stops is not a control. */}\n <div\n role=\"grid\"\n aria-label={ariaLabel}\n tabIndex={0}\n onKeyDown={onKeyDown}\n className={cn(\n 'rounded-md',\n 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',\n )}\n >\n <div role=\"row\" className=\"mb-1 grid grid-cols-7\">\n {names.map((name) => (\n <div\n key={name}\n role=\"columnheader\"\n aria-label={name}\n className=\"caption py-1 text-center\"\n >\n {name}\n </div>\n ))}\n </div>\n\n {weeks.map((week) => (\n <div role=\"row\" key={week[0]} className=\"grid grid-cols-7\">\n {week.map((date) => {\n const outside = parts(date).month !== parts(shown).month\n const disabled = outOfBounds(date)\n const selected =\n value !== undefined &&\n (rangeEnd === undefined\n ? date === value\n : date === value || date === rangeEnd)\n const inRange =\n value !== undefined && rangeEnd !== undefined && date > value && date < rangeEnd\n\n return (\n <div role=\"gridcell\" key={date} aria-selected={selected || undefined}>\n <button\n type=\"button\"\n // Not a tab stop: the grid is the control. Announced with\n // its full date, because \"14\" on its own is not a date.\n tabIndex={-1}\n disabled={disabled}\n aria-label={fullDate.format(new Date(parts(date).year, parts(date).month - 1, parts(date).day))}\n aria-current={date === now ? 'date' : undefined}\n onClick={() => {\n setFocused(date)\n if (!disabled) onValueChange?.(date)\n }}\n className={cn(\n 'flex h-8 w-full items-center justify-center rounded-md text-sm tabular-nums',\n 'transition-colors',\n outside ? 'text-faint' : 'text-text',\n inRange && 'bg-accent-soft',\n selected && 'bg-accent font-medium text-on-accent',\n !selected && !disabled && 'hover:bg-soft',\n date === now && !selected && 'font-medium text-accent',\n date === focused && 'ring-1 ring-line-2',\n disabled && 'cursor-not-allowed opacity-40',\n )}\n >\n {dayNumber.format(new Date(parts(date).year, parts(date).month - 1, parts(date).day))}\n </button>\n </div>\n )\n })}\n </div>\n ))}\n </div>\n </div>\n )\n}\n"
609
650
  }
610
651
  ]
611
652
  },
@@ -616,7 +657,7 @@
616
657
  "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.",
617
658
  "dependencies": [
618
659
  "@base-ui/react",
619
- "dowel-ui@^0.33.0"
660
+ "dowel-ui@^0.34.0"
620
661
  ],
621
662
  "registryDependencies": [],
622
663
  "files": [
@@ -636,7 +677,7 @@
636
677
  "dependencies": [
637
678
  "@base-ui/react",
638
679
  "class-variance-authority",
639
- "dowel-ui@^0.33.0"
680
+ "dowel-ui@^0.34.0"
640
681
  ],
641
682
  "registryDependencies": [],
642
683
  "files": [
@@ -655,7 +696,7 @@
655
696
  "description": "The frame around a piece of code is the same everywhere and is written again in every product: the scroll that must not wrap, the gutter of line numbers that must not be selectable, the copy button, the caption saying which file this is, and the marking of the lines the reader was sent here to look at.",
656
697
  "dependencies": [
657
698
  "class-variance-authority",
658
- "dowel-ui@^0.33.0"
699
+ "dowel-ui@^0.34.0"
659
700
  ],
660
701
  "registryDependencies": [
661
702
  "https://lacodda.github.io/dowel/r/copy-button.json"
@@ -676,7 +717,7 @@
676
717
  "description": "One region of a screen that a reader shows or hides by choice - release notes under a version number, a filter panel, the raw payload under a log line. It differs from Accordion in the way a single light switch differs from a panel of them: there is one trigger and one panel, no group holding several and no rule about how many can be open, because there is only ever one to be open or not.",
677
718
  "dependencies": [
678
719
  "@base-ui/react",
679
- "dowel-ui@^0.33.0"
720
+ "dowel-ui@^0.34.0"
680
721
  ],
681
722
  "registryDependencies": [],
682
723
  "files": [
@@ -694,7 +735,7 @@
694
735
  "title": "ColorField",
695
736
  "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.",
696
737
  "dependencies": [
697
- "dowel-ui@^0.33.0"
738
+ "dowel-ui@^0.34.0"
698
739
  ],
699
740
  "registryDependencies": [
700
741
  "https://lacodda.github.io/dowel/r/input.json"
@@ -714,7 +755,7 @@
714
755
  "title": "ColumnResizeHandle",
715
756
  "description": "The handle, and the hook that keeps the widths it produces. Pointer events rather than HTML5 drag-and-drop: a desktop shell that takes file drops for itself never lets a `dragstart` reach the page, so the native API is a handle that does nothing there; pointer capture on the handle also keeps the drag alive when the pointer runs ahead of the cell, which at any speed above a crawl it does.",
716
757
  "dependencies": [
717
- "dowel-ui@^0.33.0"
758
+ "dowel-ui@^0.34.0"
718
759
  ],
719
760
  "registryDependencies": [],
720
761
  "files": [
@@ -734,7 +775,7 @@
734
775
  "dependencies": [
735
776
  "@base-ui/react",
736
777
  "class-variance-authority",
737
- "dowel-ui@^0.33.0"
778
+ "dowel-ui@^0.34.0"
738
779
  ],
739
780
  "registryDependencies": [
740
781
  "https://lacodda.github.io/dowel/r/input.json",
@@ -758,7 +799,7 @@
758
799
  "dependencies": [
759
800
  "@base-ui/react",
760
801
  "class-variance-authority",
761
- "dowel-ui@^0.33.0"
802
+ "dowel-ui@^0.34.0"
762
803
  ],
763
804
  "registryDependencies": [
764
805
  "https://lacodda.github.io/dowel/r/combobox.json",
@@ -782,7 +823,7 @@
782
823
  "dependencies": [
783
824
  "@base-ui/react",
784
825
  "class-variance-authority",
785
- "dowel-ui@^0.33.0"
826
+ "dowel-ui@^0.34.0"
786
827
  ],
787
828
  "registryDependencies": [
788
829
  "https://lacodda.github.io/dowel/r/dialog.json",
@@ -804,7 +845,7 @@
804
845
  "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>`.",
805
846
  "dependencies": [
806
847
  "@base-ui/react",
807
- "dowel-ui@^0.33.0"
848
+ "dowel-ui@^0.34.0"
808
849
  ],
809
850
  "registryDependencies": [
810
851
  "https://lacodda.github.io/dowel/r/layer.json",
@@ -825,7 +866,7 @@
825
866
  "title": "CopyButton",
826
867
  "description": "Whatever a product shows in a panel - code, a payload, a log, one side of a comparison - somebody eventually wants to take it away, and the button that lets them is written again every time with the same three things missed.",
827
868
  "dependencies": [
828
- "dowel-ui@^0.33.0"
869
+ "dowel-ui@^0.34.0"
829
870
  ],
830
871
  "registryDependencies": [],
831
872
  "files": [
@@ -843,7 +884,7 @@
843
884
  "title": "Copyable",
844
885
  "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.",
845
886
  "dependencies": [
846
- "dowel-ui@^0.33.0"
887
+ "dowel-ui@^0.34.0"
847
888
  ],
848
889
  "registryDependencies": [],
849
890
  "files": [
@@ -861,7 +902,7 @@
861
902
  "title": "DatePicker",
862
903
  "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.",
863
904
  "dependencies": [
864
- "dowel-ui@^0.33.0"
905
+ "dowel-ui@^0.34.0"
865
906
  ],
866
907
  "registryDependencies": [
867
908
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -884,7 +925,7 @@
884
925
  "title": "DateRangePicker",
885
926
  "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.",
886
927
  "dependencies": [
887
- "dowel-ui@^0.33.0"
928
+ "dowel-ui@^0.34.0"
888
929
  ],
889
930
  "registryDependencies": [
890
931
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -909,7 +950,7 @@
909
950
  "dependencies": [
910
951
  "@base-ui/react",
911
952
  "class-variance-authority",
912
- "dowel-ui@^0.33.0"
953
+ "dowel-ui@^0.34.0"
913
954
  ],
914
955
  "registryDependencies": [
915
956
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -946,7 +987,7 @@
946
987
  "description": "The question this answers is \"how did this read before, and how does it read now\" - a version against the one before it, a proposal against what is there, a file against what is on disk. Not a code review: there is no staging, no comment, nothing to accept. It is for looking.",
947
988
  "dependencies": [
948
989
  "class-variance-authority",
949
- "dowel-ui@^0.33.0"
990
+ "dowel-ui@^0.34.0"
950
991
  ],
951
992
  "registryDependencies": [
952
993
  "https://lacodda.github.io/dowel/r/copy-button.json",
@@ -968,7 +1009,7 @@
968
1009
  "description": "The line between one part of a screen and the next. The set already had four of them - in the menu, the select, the context menu and the action bar - and each was written inside the thing it divided, so a screen that wanted a rule between two sections had nothing and reached for a bare `<hr>` or a `div` with a background.",
969
1010
  "dependencies": [
970
1011
  "class-variance-authority",
971
- "dowel-ui@^0.33.0"
1012
+ "dowel-ui@^0.34.0"
972
1013
  ],
973
1014
  "registryDependencies": [],
974
1015
  "files": [
@@ -988,7 +1029,7 @@
988
1029
  "dependencies": [
989
1030
  "@base-ui/react",
990
1031
  "class-variance-authority",
991
- "dowel-ui@^0.33.0"
1032
+ "dowel-ui@^0.34.0"
992
1033
  ],
993
1034
  "registryDependencies": [
994
1035
  "https://lacodda.github.io/dowel/r/dialog.json",
@@ -1009,7 +1050,7 @@
1009
1050
  "title": "DurationField",
1010
1051
  "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.",
1011
1052
  "dependencies": [
1012
- "dowel-ui@^0.33.0"
1053
+ "dowel-ui@^0.34.0"
1013
1054
  ],
1014
1055
  "registryDependencies": [
1015
1056
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1030,7 +1071,7 @@
1030
1071
  "description": "Three kinds of nothing, and a product that draws the same panel for all three is telling the reader the wrong thing twice:\n * **empty** - there is nothing here yet, and that is normal. The panel says what would be here and offers the one action that makes it appear.",
1031
1072
  "dependencies": [
1032
1073
  "class-variance-authority",
1033
- "dowel-ui@^0.33.0"
1074
+ "dowel-ui@^0.34.0"
1034
1075
  ],
1035
1076
  "registryDependencies": [],
1036
1077
  "files": [
@@ -1084,7 +1125,7 @@
1084
1125
  "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.",
1085
1126
  "dependencies": [
1086
1127
  "@base-ui/react",
1087
- "dowel-ui@^0.33.0"
1128
+ "dowel-ui@^0.34.0"
1088
1129
  ],
1089
1130
  "registryDependencies": [],
1090
1131
  "files": [
@@ -1102,7 +1143,7 @@
1102
1143
  "title": "FileDrop",
1103
1144
  "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.",
1104
1145
  "dependencies": [
1105
- "dowel-ui@^0.33.0"
1146
+ "dowel-ui@^0.34.0"
1106
1147
  ],
1107
1148
  "registryDependencies": [],
1108
1149
  "files": [
@@ -1120,7 +1161,7 @@
1120
1161
  "title": "FilterPopover",
1121
1162
  "description": "A text box, a handful of checkboxes - with one way to clear it. The shell only: what goes in the panel is the caller's, since a stage is ticked and a title is typed and the popover has no opinion.",
1122
1163
  "dependencies": [
1123
- "dowel-ui@^0.33.0"
1164
+ "dowel-ui@^0.34.0"
1124
1165
  ],
1125
1166
  "registryDependencies": [
1126
1167
  "https://lacodda.github.io/dowel/r/button.json",
@@ -1142,7 +1183,7 @@
1142
1183
  "description": "The layout jump a loading picture leaves behind is not a styling defect, it is a missing number: the browser has nowhere to put the box until the file arrives and tells it the pixel size, so everything below the picture slides down and back up. `AspectRatio` is that number, given up front by the caller rather than discovered by the browser - a plain box on CSS `aspect-ratio` that reserves its space the instant it is in the tree, and lets any child (an `<img>`, a map, a video) fill it with `size-full`.",
1143
1184
  "dependencies": [
1144
1185
  "class-variance-authority",
1145
- "dowel-ui@^0.33.0"
1186
+ "dowel-ui@^0.34.0"
1146
1187
  ],
1147
1188
  "registryDependencies": [],
1148
1189
  "files": [
@@ -1160,7 +1201,7 @@
1160
1201
  "title": "InlineField",
1161
1202
  "description": "A value that is edited where it is shown: click it, type, Enter or leave to keep, Escape to take it back.",
1162
1203
  "dependencies": [
1163
- "dowel-ui@^0.33.0"
1204
+ "dowel-ui@^0.34.0"
1164
1205
  ],
1165
1206
  "registryDependencies": [
1166
1207
  "https://lacodda.github.io/dowel/r/field-draft.json"
@@ -1180,7 +1221,7 @@
1180
1221
  "title": "Input",
1181
1222
  "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.",
1182
1223
  "dependencies": [
1183
- "dowel-ui@^0.33.0"
1224
+ "dowel-ui@^0.34.0"
1184
1225
  ],
1185
1226
  "registryDependencies": [],
1186
1227
  "files": [
@@ -1214,7 +1255,7 @@
1214
1255
  "title": "JsonViewer",
1215
1256
  "description": "What a product reaches for when it has to show a response, a settings file, a webhook payload - data the reader needs to understand, not edit. The alternative it replaces is `JSON.stringify(value, null, 2)` inside a `<pre>`, which is fine for twenty lines and useless for two hundred: nothing folds, nothing is findable, and the shape of the document is somewhere inside the indentation.",
1216
1257
  "dependencies": [
1217
- "dowel-ui@^0.33.0"
1258
+ "dowel-ui@^0.34.0"
1218
1259
  ],
1219
1260
  "registryDependencies": [
1220
1261
  "https://lacodda.github.io/dowel/r/json-rows.json"
@@ -1234,7 +1275,7 @@
1234
1275
  "title": "Kbd",
1235
1276
  "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.",
1236
1277
  "dependencies": [
1237
- "dowel-ui@^0.33.0"
1278
+ "dowel-ui@^0.34.0"
1238
1279
  ],
1239
1280
  "registryDependencies": [],
1240
1281
  "files": [
@@ -1253,7 +1294,7 @@
1253
1294
  "description": "The shape every product builds out of two `<div>`s in a flex row, and the reason it is worth having once: it is a `<dl>`, and the pairing is what a screen reader announces. Two divs read as four unrelated pieces of text - \"Created\", \"2 hours ago\", \"Owner\", \"Ines\" - and nothing says which value belongs to which name. The right element says it for free.",
1254
1295
  "dependencies": [
1255
1296
  "class-variance-authority",
1256
- "dowel-ui@^0.33.0"
1297
+ "dowel-ui@^0.34.0"
1257
1298
  ],
1258
1299
  "registryDependencies": [],
1259
1300
  "files": [
@@ -1288,7 +1329,7 @@
1288
1329
  "description": "The distinction against its neighbours is the data's, not the drawing's. A column says each period is its own sum - hours worked in a week, and there is no Wednesday-afternoon figure between two weeks. A line says the value existed the whole time and was sampled: an account balance, a price, a temperature. Drawing a sum as a line claims readings nobody took; drawing a level as columns throws away the thing being watched.",
1289
1330
  "dependencies": [
1290
1331
  "class-variance-authority",
1291
- "dowel-ui@^0.33.0"
1332
+ "dowel-ui@^0.34.0"
1292
1333
  ],
1293
1334
  "registryDependencies": [
1294
1335
  "https://lacodda.github.io/dowel/r/line-scale.json"
@@ -1325,7 +1366,7 @@
1325
1366
  "description": "A row of a list, one thing said in a line: ListRow for a fact, RowButton for a choice.",
1326
1367
  "dependencies": [
1327
1368
  "@base-ui/react",
1328
- "dowel-ui@^0.33.0"
1369
+ "dowel-ui@^0.34.0"
1329
1370
  ],
1330
1371
  "registryDependencies": [],
1331
1372
  "files": [
@@ -1343,7 +1384,7 @@
1343
1384
  "title": "MarkedText",
1344
1385
  "description": "A textarea cannot colour a word. The way round it is older than React: draw the same text twice, once as marked-up HTML underneath and once as the textarea on top with its own text transparent, so the caret and the selection are the browser's and the colours are ours. The two have to agree on every metric - font, size, line height, padding, wrapping - or the marks slide off the words they mark. So both take ONE class list, given by the caller, and the textarea adds only what makes it invisible.",
1345
1386
  "dependencies": [
1346
- "dowel-ui@^0.33.0"
1387
+ "dowel-ui@^0.34.0"
1347
1388
  ],
1348
1389
  "registryDependencies": [],
1349
1390
  "files": [
@@ -1363,7 +1404,7 @@
1363
1404
  "dependencies": [
1364
1405
  "@base-ui/react",
1365
1406
  "class-variance-authority",
1366
- "dowel-ui@^0.33.0"
1407
+ "dowel-ui@^0.34.0"
1367
1408
  ],
1368
1409
  "registryDependencies": [
1369
1410
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -1385,7 +1426,7 @@
1385
1426
  "dependencies": [
1386
1427
  "@base-ui/react",
1387
1428
  "class-variance-authority",
1388
- "dowel-ui@^0.33.0"
1429
+ "dowel-ui@^0.34.0"
1389
1430
  ],
1390
1431
  "registryDependencies": [
1391
1432
  "https://lacodda.github.io/dowel/r/tooltip.json"
@@ -1405,7 +1446,7 @@
1405
1446
  "title": "NotificationBell",
1406
1447
  "description": "A bell that is always lit is a bell nobody reads, so the count is the product's decision and this draws it: nothing at zero, the number past that, `9+` past nine. Pressing it does not leave the screen - the last few entries open under it and the whole history is one more click, which is the shape every product converged on once the first one tried a page.",
1407
1448
  "dependencies": [
1408
- "dowel-ui@^0.33.0"
1449
+ "dowel-ui@^0.34.0"
1409
1450
  ],
1410
1451
  "registryDependencies": [
1411
1452
  "https://lacodda.github.io/dowel/r/button.json",
@@ -1427,7 +1468,7 @@
1427
1468
  "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.",
1428
1469
  "dependencies": [
1429
1470
  "@base-ui/react",
1430
- "dowel-ui@^0.33.0"
1471
+ "dowel-ui@^0.34.0"
1431
1472
  ],
1432
1473
  "registryDependencies": [
1433
1474
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1447,7 +1488,7 @@
1447
1488
  "title": "NumberFormat",
1448
1489
  "description": "Two things, and the second is the reason this is a component rather than a call to `toLocaleString` at each site.",
1449
1490
  "dependencies": [
1450
- "dowel-ui@^0.33.0"
1491
+ "dowel-ui@^0.34.0"
1451
1492
  ],
1452
1493
  "registryDependencies": [],
1453
1494
  "files": [
@@ -1466,7 +1507,7 @@
1466
1507
  "description": "What a screen says it is, and the buttons that act on the whole of it: a title, a line under it, and the actions at the far end of the same row.",
1467
1508
  "dependencies": [
1468
1509
  "class-variance-authority",
1469
- "dowel-ui@^0.33.0"
1510
+ "dowel-ui@^0.34.0"
1470
1511
  ],
1471
1512
  "registryDependencies": [],
1472
1513
  "files": [
@@ -1484,7 +1525,7 @@
1484
1525
  "title": "PageSize",
1485
1526
  "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.",
1486
1527
  "dependencies": [
1487
- "dowel-ui@^0.33.0"
1528
+ "dowel-ui@^0.34.0"
1488
1529
  ],
1489
1530
  "registryDependencies": [
1490
1531
  "https://lacodda.github.io/dowel/r/select.json"
@@ -1504,7 +1545,7 @@
1504
1545
  "title": "Pagination",
1505
1546
  "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.",
1506
1547
  "dependencies": [
1507
- "dowel-ui@^0.33.0"
1548
+ "dowel-ui@^0.34.0"
1508
1549
  ],
1509
1550
  "registryDependencies": [
1510
1551
  "https://lacodda.github.io/dowel/r/button.json"
@@ -1525,7 +1566,7 @@
1525
1566
  "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.",
1526
1567
  "dependencies": [
1527
1568
  "class-variance-authority",
1528
- "dowel-ui@^0.33.0"
1569
+ "dowel-ui@^0.34.0"
1529
1570
  ],
1530
1571
  "registryDependencies": [],
1531
1572
  "files": [
@@ -1543,7 +1584,7 @@
1543
1584
  "title": "PasswordField",
1544
1585
  "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.",
1545
1586
  "dependencies": [
1546
- "dowel-ui@^0.33.0"
1587
+ "dowel-ui@^0.34.0"
1547
1588
  ],
1548
1589
  "registryDependencies": [
1549
1590
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1565,7 +1606,7 @@
1565
1606
  "dependencies": [
1566
1607
  "@base-ui/react",
1567
1608
  "class-variance-authority",
1568
- "dowel-ui@^0.33.0"
1609
+ "dowel-ui@^0.34.0"
1569
1610
  ],
1570
1611
  "registryDependencies": [
1571
1612
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -1587,7 +1628,7 @@
1587
1628
  "dependencies": [
1588
1629
  "@base-ui/react",
1589
1630
  "class-variance-authority",
1590
- "dowel-ui@^0.33.0"
1631
+ "dowel-ui@^0.34.0"
1591
1632
  ],
1592
1633
  "registryDependencies": [
1593
1634
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -1607,7 +1648,7 @@
1607
1648
  "title": "ProductMark",
1608
1649
  "description": "The product-mark primitive.",
1609
1650
  "dependencies": [
1610
- "dowel-ui@^0.33.0"
1651
+ "dowel-ui@^0.34.0"
1611
1652
  ],
1612
1653
  "registryDependencies": [],
1613
1654
  "files": [
@@ -1625,7 +1666,7 @@
1625
1666
  "title": "ProductSwitcher",
1626
1667
  "description": "The line's products are meant to be used together: kasl-server reads what kasl records, rigger plans what furca commits, a note in scheda cites a track in lyrid. A person moving between them was typing addresses from memory. This is the one place that knows where the others are - the current product's small tile and name on the trigger, and the others in a menu, each with its own tile, so the colour a person already knows the product by is what they look for.",
1627
1668
  "dependencies": [
1628
- "dowel-ui@^0.33.0"
1669
+ "dowel-ui@^0.34.0"
1629
1670
  ],
1630
1671
  "registryDependencies": [
1631
1672
  "https://lacodda.github.io/dowel/r/menu.json",
@@ -1648,7 +1689,7 @@
1648
1689
  "dependencies": [
1649
1690
  "@base-ui/react",
1650
1691
  "class-variance-authority",
1651
- "dowel-ui@^0.33.0"
1692
+ "dowel-ui@^0.34.0"
1652
1693
  ],
1653
1694
  "registryDependencies": [],
1654
1695
  "files": [
@@ -1688,7 +1729,7 @@
1688
1729
  "dependencies": [
1689
1730
  "@base-ui/react",
1690
1731
  "class-variance-authority",
1691
- "dowel-ui@^0.33.0"
1732
+ "dowel-ui@^0.34.0"
1692
1733
  ],
1693
1734
  "registryDependencies": [],
1694
1735
  "files": [
@@ -1706,7 +1747,7 @@
1706
1747
  "title": "RatingScale",
1707
1748
  "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\".",
1708
1749
  "dependencies": [
1709
- "dowel-ui@^0.33.0"
1750
+ "dowel-ui@^0.34.0"
1710
1751
  ],
1711
1752
  "registryDependencies": [],
1712
1753
  "files": [
@@ -1724,7 +1765,7 @@
1724
1765
  "title": "RelativeTime",
1725
1766
  "description": "The relative-time primitive.",
1726
1767
  "dependencies": [
1727
- "dowel-ui@^0.33.0"
1768
+ "dowel-ui@^0.34.0"
1728
1769
  ],
1729
1770
  "registryDependencies": [],
1730
1771
  "files": [
@@ -1742,7 +1783,7 @@
1742
1783
  "title": "ReorderableList",
1743
1784
  "description": "The columns in a column picker, the stops of a dial, the roles of a profile. A hook and a grip rather than a list component: the rows are already something else's - a menu's items, a form's fields - and a component wrapping them would have to reproduce whatever that something else does.",
1744
1785
  "dependencies": [
1745
- "dowel-ui@^0.33.0"
1786
+ "dowel-ui@^0.34.0"
1746
1787
  ],
1747
1788
  "registryDependencies": [],
1748
1789
  "files": [
@@ -1760,7 +1801,7 @@
1760
1801
  "title": "SaveState",
1761
1802
  "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.",
1762
1803
  "dependencies": [
1763
- "dowel-ui@^0.33.0"
1804
+ "dowel-ui@^0.34.0"
1764
1805
  ],
1765
1806
  "registryDependencies": [
1766
1807
  "https://lacodda.github.io/dowel/r/spinner.json"
@@ -1782,7 +1823,7 @@
1782
1823
  "dependencies": [
1783
1824
  "@base-ui/react",
1784
1825
  "class-variance-authority",
1785
- "dowel-ui@^0.33.0"
1826
+ "dowel-ui@^0.34.0"
1786
1827
  ],
1787
1828
  "registryDependencies": [],
1788
1829
  "files": [
@@ -1790,7 +1831,7 @@
1790
1831
  "path": "ui/scroll-area.tsx",
1791
1832
  "target": "@ui/scroll-area.tsx",
1792
1833
  "type": "registry:ui",
1793
- "content": "import type { ReactNode, Ref } from 'react'\nimport { ScrollArea as Base } from '@base-ui/react/scroll-area'\nimport { cva } from 'class-variance-authority'\nimport { cn } from 'dowel-ui'\n\n/*\n * A box that scrolls, with the line's scrollbar drawn over its content.\n *\n * The line has one rule for scrollbars: a thin bar laid over the content,\n * never a gutter that takes width. A gutter is a column of the layout that\n * comes and goes with the length of the content, so a list that grows past\n * the fold pushes everything beside it sideways by the width of a bar - and\n * on a platform that draws a classic bar, the product wears someone else's\n * chrome in the middle of its own screen. The theme's global scrollbar rules\n * make native bars thin, but thin is still a gutter wherever the platform\n * draws one; only a bar that is not the browser's can promise to take no room.\n *\n * So the viewport hides its native bars and Base UI draws the thumb, sized and\n * placed from the scroll position. The bar is invisible at rest and appears\n * while the pointer is over the area or while it scrolls, because a bar that\n * is always there is a line across the content that nobody asked for. The\n * track is wider than the thumb and the thumb thickens under the pointer: a\n * four-pixel thumb reads as a hint and is hopeless as a grip, and the grip is\n * what somebody reaching for it wants. Both axes are handled, and Base UI\n * mounts a bar only for an axis that actually overflows.\n *\n * The keyboard is the part a hand-drawn scrollbar usually loses. A box that\n * scrolls must be reachable, or a reader without a pointer cannot see what is\n * below the fold (axe's `scrollable-region-focusable`), so Base UI makes the\n * viewport a tab stop exactly when it overflows. A tab stop has to be called\n * something, and the viewport is then a named group - which is why `label` is\n * required: whether the content will overflow is not known when the product\n * is written, and a name added only for the long case is a name forgotten for\n * it.\n *\n * `fade` softens the edges where there is more to see. It reads Base UI's\n * overflow distances, so an edge that is already at its end is left sharp -\n * a fade on both ends of a list scrolled to the top would say there is\n * something above when there is not.\n */\n\nexport const scrollAreaViewportVariants = cva(\n cn(\n 'size-full rounded-[inherit] outline-none',\n 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent',\n ),\n {\n variants: {\n /* A mask, not an overlay gradient: a gradient would have to be painted\n * in the colour of whatever is behind the area, which the component\n * cannot know. A mask fades the content itself, over any ground. Each\n * edge fades by at most `--fade`, and by less while the content is\n * nearer its end than that - so the fade arrives as scrolling starts\n * rather than switching on. */\n fade: {\n true: cn(\n '[--fade:calc(var(--spacing)*6)] [mask-composite:intersect]',\n '[mask-image:linear-gradient(to_bottom,transparent,var(--color-black)_min(var(--fade),var(--scroll-area-overflow-y-start,0px)),var(--color-black)_calc(100%_-_min(var(--fade),var(--scroll-area-overflow-y-end,0px))),transparent),linear-gradient(to_right,transparent,var(--color-black)_min(var(--fade),var(--scroll-area-overflow-x-start,0px)),var(--color-black)_calc(100%_-_min(var(--fade),var(--scroll-area-overflow-x-end,0px))),transparent)]',\n ),\n false: '',\n },\n },\n defaultVariants: { fade: false },\n },\n)\n\n/* The track is the grab zone and the thumb sits at its outer edge. Hidden at\n * rest; shown while the pointer is over the area or it scrolls, and held\n * while the thumb is being dragged, since the pointer may leave the area\n * mid-drag. */\nconst scrollbar = cn(\n 'group/bar flex p-0.5 opacity-0 [transition:opacity_var(--duration-slow)_var(--ease-out)]',\n 'data-[hovering]:opacity-100 data-[scrolling]:opacity-100 active:opacity-100',\n 'data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:justify-end',\n 'data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:justify-end',\n)\n\nconst thumb = cn(\n 'rounded-full bg-line-2 group-hover/bar:bg-dim',\n '[transition:width_var(--duration-quick)_var(--ease-out),height_var(--duration-quick)_var(--ease-out),background-color_var(--duration-quick)_var(--ease-out)]',\n 'data-[orientation=vertical]:w-1 group-hover/bar:data-[orientation=vertical]:w-full',\n 'data-[orientation=horizontal]:h-1 group-hover/bar:data-[orientation=horizontal]:h-full',\n)\n\nexport interface ScrollAreaProps {\n /** Names the viewport, which becomes a tab stop when its content overflows:\n * \"Recent activity\", \"Diff of main.rs\". No default - it is the product's\n * word. */\n label: string\n /** Fade the edges that have more content past them. */\n fade?: boolean\n /** The scrolling element itself, for a product that sets or reads its\n * position - a virtual list, a scroll-to-latest. */\n viewportRef?: Ref<HTMLDivElement>\n /** On the root: its size is the area's size, so the height goes here. */\n className?: string\n viewportClassName?: string\n children: ReactNode\n}\n\nexport function ScrollArea({\n label,\n fade = false,\n viewportRef,\n className,\n viewportClassName,\n children,\n}: ScrollAreaProps) {\n return (\n <Base.Root className={cn('relative min-h-0 min-w-0 overflow-hidden', className)}>\n <Base.Viewport\n ref={viewportRef}\n // Base UI gives the viewport `presentation`, which is right for a box\n // nobody can focus and wrong for one that is a tab stop: the role is\n // dropped the moment it is focusable, and the name goes with it.\n role=\"group\"\n aria-label={label}\n className={cn(scrollAreaViewportVariants({ fade }), viewportClassName)}\n >\n {/* The content box is what the horizontal overflow is measured on:\n * without it a wide child is squeezed to the viewport's width and\n * nothing ever overflows sideways. */}\n <Base.Content>{children}</Base.Content>\n </Base.Viewport>\n <Base.Scrollbar orientation=\"vertical\" className={scrollbar}>\n <Base.Thumb className={thumb} />\n </Base.Scrollbar>\n <Base.Scrollbar orientation=\"horizontal\" className={scrollbar}>\n <Base.Thumb className={thumb} />\n </Base.Scrollbar>\n </Base.Root>\n )\n}\n"
1834
+ "content": "import type { CSSProperties, ReactNode, Ref } from 'react'\nimport { ScrollArea as Base } from '@base-ui/react/scroll-area'\nimport { cva } from 'class-variance-authority'\nimport { cn } from 'dowel-ui'\n\n/*\n * A box that scrolls, with the line's scrollbar drawn over its content.\n *\n * The line has one rule for scrollbars: a thin bar laid over the content,\n * never a gutter that takes width. A gutter is a column of the layout that\n * comes and goes with the length of the content, so a list that grows past\n * the fold pushes everything beside it sideways by the width of a bar - and\n * on a platform that draws a classic bar, the product wears someone else's\n * chrome in the middle of its own screen. The theme's global scrollbar rules\n * make native bars thin, but thin is still a gutter wherever the platform\n * draws one; only a bar that is not the browser's can promise to take no room.\n *\n * So the viewport hides its native bars and Base UI draws the thumb, sized and\n * placed from the scroll position. The bar is invisible at rest and appears\n * while the pointer is over the area or while it scrolls, because a bar that\n * is always there is a line across the content that nobody asked for. The\n * track is wider than the thumb and the thumb thickens under the pointer: a\n * four-pixel thumb reads as a hint and is hopeless as a grip, and the grip is\n * what somebody reaching for it wants. Both axes are handled, and Base UI\n * mounts a bar only for an axis that actually overflows.\n *\n * The keyboard is the part a hand-drawn scrollbar usually loses. A box that\n * scrolls must be reachable, or a reader without a pointer cannot see what is\n * below the fold (axe's `scrollable-region-focusable`), so Base UI makes the\n * viewport a tab stop exactly when it overflows. A tab stop has to be called\n * something, and the viewport is then a named group - which is why `label` is\n * required: whether the content will overflow is not known when the product\n * is written, and a name added only for the long case is a name forgotten for\n * it.\n *\n * `fade` softens the edges where there is more to see. It reads Base UI's\n * overflow distances, so an edge that is already at its end is left sharp -\n * a fade on both ends of a list scrolled to the top would say there is\n * something above when there is not.\n *\n * `orientation` says which way the content may move. Base UI sizes its content\n * box to `fit-content`, which is right for a table that scrolls sideways and\n * wrong for a list: a row that should truncate to the column instead widens\n * the box, and the list scrolls sideways by the length of its longest title.\n * `vertical` holds the content to the viewport's width; `horizontal` holds it\n * to the viewport's height.\n */\n\ntype Orientation = NonNullable<ScrollAreaProps['orientation']>\n\n/* Inline, because the style Base UI writes is inline too - `min-width:\n * fit-content` on the content and `overflow: scroll` on the viewport - and a\n * class loses to it. */\nconst contentStyle: Record<Orientation, CSSProperties | undefined> = {\n both: undefined,\n vertical: { minWidth: 0 },\n horizontal: undefined,\n}\nconst viewportStyle: Record<Orientation, CSSProperties | undefined> = {\n both: undefined,\n vertical: { overflowX: 'hidden' },\n horizontal: { overflowY: 'hidden' },\n}\n\nexport const scrollAreaViewportVariants = cva(\n cn(\n 'size-full rounded-[inherit] outline-none',\n 'focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent',\n ),\n {\n variants: {\n /* A mask, not an overlay gradient: a gradient would have to be painted\n * in the colour of whatever is behind the area, which the component\n * cannot know. A mask fades the content itself, over any ground. Each\n * edge fades by at most `--fade`, and by less while the content is\n * nearer its end than that - so the fade arrives as scrolling starts\n * rather than switching on. */\n fade: {\n true: cn(\n '[--fade:calc(var(--spacing)*6)] [mask-composite:intersect]',\n '[mask-image:linear-gradient(to_bottom,transparent,var(--color-black)_min(var(--fade),var(--scroll-area-overflow-y-start,0px)),var(--color-black)_calc(100%_-_min(var(--fade),var(--scroll-area-overflow-y-end,0px))),transparent),linear-gradient(to_right,transparent,var(--color-black)_min(var(--fade),var(--scroll-area-overflow-x-start,0px)),var(--color-black)_calc(100%_-_min(var(--fade),var(--scroll-area-overflow-x-end,0px))),transparent)]',\n ),\n false: '',\n },\n },\n defaultVariants: { fade: false },\n },\n)\n\n/* The track is the grab zone and the thumb sits at its outer edge. Hidden at\n * rest; shown while the pointer is over the area or it scrolls, and held\n * while the thumb is being dragged, since the pointer may leave the area\n * mid-drag. */\nconst scrollbar = cn(\n 'group/bar flex p-0.5 opacity-0 [transition:opacity_var(--duration-slow)_var(--ease-out)]',\n 'data-[hovering]:opacity-100 data-[scrolling]:opacity-100 active:opacity-100',\n 'data-[orientation=vertical]:w-2.5 data-[orientation=vertical]:justify-end',\n 'data-[orientation=horizontal]:h-2.5 data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:justify-end',\n)\n\nconst thumb = cn(\n 'rounded-full bg-line-2 group-hover/bar:bg-dim',\n '[transition:width_var(--duration-quick)_var(--ease-out),height_var(--duration-quick)_var(--ease-out),background-color_var(--duration-quick)_var(--ease-out)]',\n 'data-[orientation=vertical]:w-1 group-hover/bar:data-[orientation=vertical]:w-full',\n 'data-[orientation=horizontal]:h-1 group-hover/bar:data-[orientation=horizontal]:h-full',\n)\n\nexport interface ScrollAreaProps {\n /** Names the viewport, which becomes a tab stop when its content overflows:\n * \"Recent activity\", \"Diff of main.rs\". No default - it is the product's\n * word. */\n label: string\n /** Fade the edges that have more content past them. */\n fade?: boolean\n /** Which way the content may move. `vertical` for a list whose rows\n * truncate to the column; `both` for a table or a picture. */\n orientation?: 'both' | 'vertical' | 'horizontal'\n /** The scrolling element itself, for a product that sets or reads its\n * position - a virtual list, a scroll-to-latest. */\n viewportRef?: Ref<HTMLDivElement>\n /** On the root: its size is the area's size, so the height goes here. */\n className?: string\n viewportClassName?: string\n children: ReactNode\n}\n\nexport function ScrollArea({\n label,\n fade = false,\n orientation = 'both',\n viewportRef,\n className,\n viewportClassName,\n children,\n}: ScrollAreaProps) {\n return (\n <Base.Root className={cn('relative min-h-0 min-w-0 overflow-hidden', className)}>\n <Base.Viewport\n ref={viewportRef}\n // Base UI gives the viewport `presentation`, which is right for a box\n // nobody can focus and wrong for one that is a tab stop: the role is\n // dropped the moment it is focusable, and the name goes with it.\n role=\"group\"\n aria-label={label}\n className={cn(scrollAreaViewportVariants({ fade }), viewportClassName)}\n style={viewportStyle[orientation]}\n >\n {/* The content box is what the horizontal overflow is measured on:\n * without it a wide child is squeezed to the viewport's width and\n * nothing ever overflows sideways. */}\n <Base.Content style={contentStyle[orientation]}>{children}</Base.Content>\n </Base.Viewport>\n {orientation === 'horizontal' ? null : (\n <Base.Scrollbar orientation=\"vertical\" className={scrollbar}>\n <Base.Thumb className={thumb} />\n </Base.Scrollbar>\n )}\n {orientation === 'vertical' ? null : (\n <Base.Scrollbar orientation=\"horizontal\" className={scrollbar}>\n <Base.Thumb className={thumb} />\n </Base.Scrollbar>\n )}\n </Base.Root>\n )\n}\n"
1794
1835
  }
1795
1836
  ]
1796
1837
  },
@@ -1800,7 +1841,7 @@
1800
1841
  "title": "SearchField",
1801
1842
  "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.",
1802
1843
  "dependencies": [
1803
- "dowel-ui@^0.33.0"
1844
+ "dowel-ui@^0.34.0"
1804
1845
  ],
1805
1846
  "registryDependencies": [
1806
1847
  "https://lacodda.github.io/dowel/r/input.json",
@@ -1823,7 +1864,7 @@
1823
1864
  "description": "A settings screen is the usual case: five or six sections, each its own address so it can be linked to and the back button walks between them, listed down the left with the current one tinted. Every product draws the same column, and every product draws the active row a little differently - which is exactly the drift a shared list exists to stop.",
1824
1865
  "dependencies": [
1825
1866
  "@base-ui/react",
1826
- "dowel-ui@^0.33.0"
1867
+ "dowel-ui@^0.34.0"
1827
1868
  ],
1828
1869
  "registryDependencies": [],
1829
1870
  "files": [
@@ -1842,7 +1883,7 @@
1842
1883
  "description": "Two to five options, one of them chosen, all of them in view - a radio group in a segment's clothes.",
1843
1884
  "dependencies": [
1844
1885
  "@base-ui/react",
1845
- "dowel-ui@^0.33.0"
1886
+ "dowel-ui@^0.34.0"
1846
1887
  ],
1847
1888
  "registryDependencies": [],
1848
1889
  "files": [
@@ -1862,7 +1903,7 @@
1862
1903
  "dependencies": [
1863
1904
  "@base-ui/react",
1864
1905
  "class-variance-authority",
1865
- "dowel-ui@^0.33.0"
1906
+ "dowel-ui@^0.34.0"
1866
1907
  ],
1867
1908
  "registryDependencies": [
1868
1909
  "https://lacodda.github.io/dowel/r/input.json",
@@ -1899,7 +1940,7 @@
1899
1940
  "title": "SkeletonOf",
1900
1941
  "description": "`Skeleton` and its shapes solved half the problem: they gave a product a list, a card and a grid to reach for instead of a spinner. The half left over is the one that actually causes the jump, and it is a human one - somebody has to look at the real thing, judge how many rows it has and how tall they are, and type that in. The judgement is made once, the screen changes a month later, and the placeholder goes on promising the old shape.",
1901
1942
  "dependencies": [
1902
- "dowel-ui@^0.33.0"
1943
+ "dowel-ui@^0.34.0"
1903
1944
  ],
1904
1945
  "registryDependencies": [],
1905
1946
  "files": [
@@ -1917,7 +1958,7 @@
1917
1958
  "title": "Skeleton",
1918
1959
  "description": "The rule the component is built on, and the reason it takes a shape rather than filling the space:\n * **A skeleton of the wrong shape is worse than no skeleton.**\n * It promises something the content does not keep, and the promise is paid for in a jump: the page settles, the scrollbar appears, and whatever the reader was about to click has moved. Measured rather than assumed - the line's own calendar showed a list of four short lines where a six-row month grid was about to land, and the skeleton was itself the jump it existed to prevent.",
1919
1960
  "dependencies": [
1920
- "dowel-ui@^0.33.0"
1961
+ "dowel-ui@^0.34.0"
1921
1962
  ],
1922
1963
  "registryDependencies": [],
1923
1964
  "files": [
@@ -1936,7 +1977,7 @@
1936
1977
  "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\".",
1937
1978
  "dependencies": [
1938
1979
  "@base-ui/react",
1939
- "dowel-ui@^0.33.0"
1980
+ "dowel-ui@^0.34.0"
1940
1981
  ],
1941
1982
  "registryDependencies": [],
1942
1983
  "files": [
@@ -1955,7 +1996,7 @@
1955
1996
  "description": "The shape of a history, not a chart of it: no axes, no gridlines, no ticks.",
1956
1997
  "dependencies": [
1957
1998
  "class-variance-authority",
1958
- "dowel-ui@^0.33.0"
1999
+ "dowel-ui@^0.34.0"
1959
2000
  ],
1960
2001
  "registryDependencies": [],
1961
2002
  "files": [
@@ -1974,7 +2015,7 @@
1974
2015
  "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.",
1975
2016
  "dependencies": [
1976
2017
  "class-variance-authority",
1977
- "dowel-ui@^0.33.0"
2018
+ "dowel-ui@^0.34.0"
1978
2019
  ],
1979
2020
  "registryDependencies": [],
1980
2021
  "files": [
@@ -1992,7 +2033,7 @@
1992
2033
  "title": "Splash",
1993
2034
  "description": "A desktop product has a second or two between the window appearing and the first screen being ready - a workspace to open, a database to migrate, a plugin to start - and a blank window for that long reads as a crash. So the window shows the product instead: the mark, the name, the promise, the version, and a bar that sweeps until there is something to draw.",
1994
2035
  "dependencies": [
1995
- "dowel-ui@^0.33.0"
2036
+ "dowel-ui@^0.34.0"
1996
2037
  ],
1997
2038
  "registryDependencies": [],
1998
2039
  "files": [
@@ -2011,7 +2052,7 @@
2011
2052
  "description": "The splitter primitive.",
2012
2053
  "dependencies": [
2013
2054
  "class-variance-authority",
2014
- "dowel-ui@^0.33.0"
2055
+ "dowel-ui@^0.34.0"
2015
2056
  ],
2016
2057
  "registryDependencies": [],
2017
2058
  "files": [
@@ -2030,7 +2071,7 @@
2030
2071
  "description": "The smallest thing on a dashboard and the one every product writes itself: a label above, a number below, sometimes a word about which way it moved.",
2031
2072
  "dependencies": [
2032
2073
  "class-variance-authority",
2033
- "dowel-ui@^0.33.0"
2074
+ "dowel-ui@^0.34.0"
2034
2075
  ],
2035
2076
  "registryDependencies": [],
2036
2077
  "files": [
@@ -2049,7 +2090,7 @@
2049
2090
  "description": "The smallest thing a screen can say about something's condition: a server is up, a job failed, a person is away. Every product of the line drew its own coloured circle, and every one of them drew it the same way - a `<span>` with a background - which means the condition existed for exactly the readers who could see it.",
2050
2091
  "dependencies": [
2051
2092
  "class-variance-authority",
2052
- "dowel-ui@^0.33.0"
2093
+ "dowel-ui@^0.34.0"
2053
2094
  ],
2054
2095
  "registryDependencies": [],
2055
2096
  "files": [
@@ -2068,7 +2109,7 @@
2068
2109
  "description": "The steps of a task too long for one screen, and where you stand in them: set up a workspace, import a library, configure a connection. It is a picture of progress on its own, and the top of a Wizard when the steps are a form - a product can show how far along something is without owning the form that moves it, which is why the two are separate components. Base UI has no stepper, so this one is written here rather than wrapped.",
2069
2110
  "dependencies": [
2070
2111
  "class-variance-authority",
2071
- "dowel-ui@^0.33.0"
2112
+ "dowel-ui@^0.34.0"
2072
2113
  ],
2073
2114
  "registryDependencies": [],
2074
2115
  "files": [
@@ -2087,7 +2128,7 @@
2087
2128
  "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.",
2088
2129
  "dependencies": [
2089
2130
  "@base-ui/react",
2090
- "dowel-ui@^0.33.0"
2131
+ "dowel-ui@^0.34.0"
2091
2132
  ],
2092
2133
  "registryDependencies": [],
2093
2134
  "files": [
@@ -2122,7 +2163,7 @@
2122
2163
  "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.",
2123
2164
  "dependencies": [
2124
2165
  "class-variance-authority",
2125
- "dowel-ui@^0.33.0"
2166
+ "dowel-ui@^0.34.0"
2126
2167
  ],
2127
2168
  "registryDependencies": [
2128
2169
  "https://lacodda.github.io/dowel/r/table-sort.json"
@@ -2144,7 +2185,7 @@
2144
2185
  "dependencies": [
2145
2186
  "@base-ui/react",
2146
2187
  "class-variance-authority",
2147
- "dowel-ui@^0.33.0"
2188
+ "dowel-ui@^0.34.0"
2148
2189
  ],
2149
2190
  "registryDependencies": [],
2150
2191
  "files": [
@@ -2163,7 +2204,7 @@
2163
2204
  "description": "Free text turned into a list: type a word, press Enter, it becomes a chip.",
2164
2205
  "dependencies": [
2165
2206
  "class-variance-authority",
2166
- "dowel-ui@^0.33.0"
2207
+ "dowel-ui@^0.34.0"
2167
2208
  ],
2168
2209
  "registryDependencies": [
2169
2210
  "https://lacodda.github.io/dowel/r/chip.json",
@@ -2184,7 +2225,7 @@
2184
2225
  "title": "Textarea",
2185
2226
  "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.",
2186
2227
  "dependencies": [
2187
- "dowel-ui@^0.33.0"
2228
+ "dowel-ui@^0.34.0"
2188
2229
  ],
2189
2230
  "registryDependencies": [
2190
2231
  "https://lacodda.github.io/dowel/r/input.json"
@@ -2205,7 +2246,7 @@
2205
2246
  "description": "The tier primitive.",
2206
2247
  "dependencies": [
2207
2248
  "class-variance-authority",
2208
- "dowel-ui@^0.33.0"
2249
+ "dowel-ui@^0.34.0"
2209
2250
  ],
2210
2251
  "registryDependencies": [],
2211
2252
  "files": [
@@ -2223,7 +2264,7 @@
2223
2264
  "title": "TimeField",
2224
2265
  "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`.",
2225
2266
  "dependencies": [
2226
- "dowel-ui@^0.33.0"
2267
+ "dowel-ui@^0.34.0"
2227
2268
  ],
2228
2269
  "registryDependencies": [
2229
2270
  "https://lacodda.github.io/dowel/r/input.json"
@@ -2244,7 +2285,7 @@
2244
2285
  "description": "What happened, in the order it happened: a release history, an audit trail, the steps a job went through. Four products of the line draw one, and all four drew it the same way - a list with a border on the left and a dot positioned over it by hand.",
2245
2286
  "dependencies": [
2246
2287
  "class-variance-authority",
2247
- "dowel-ui@^0.33.0"
2288
+ "dowel-ui@^0.34.0"
2248
2289
  ],
2249
2290
  "registryDependencies": [],
2250
2291
  "files": [
@@ -2264,7 +2305,7 @@
2264
2305
  "dependencies": [
2265
2306
  "@base-ui/react",
2266
2307
  "class-variance-authority",
2267
- "dowel-ui@^0.33.0"
2308
+ "dowel-ui@^0.34.0"
2268
2309
  ],
2269
2310
  "registryDependencies": [],
2270
2311
  "files": [
@@ -2284,7 +2325,7 @@
2284
2325
  "dependencies": [
2285
2326
  "@base-ui/react",
2286
2327
  "class-variance-authority",
2287
- "dowel-ui@^0.33.0"
2328
+ "dowel-ui@^0.34.0"
2288
2329
  ],
2289
2330
  "registryDependencies": [
2290
2331
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -2321,7 +2362,7 @@
2321
2362
  "description": "Two products had written this independently and arrived at the same construction - a rounded track, segments positioned absolutely by percent, a floor under the segment width so a short one does not vanish - differing only in what a segment meant. One drew the tiers of a rubric with the score standing among them; the other drew a working day as alternating work and breaks. Neither could be built from the other, and each knew something the other did not: the tiers had the marker and the three-state reading of a segment (passed, standing in, still ahead), the day had the minimum width and the difference between an empty track and an unknown one.",
2322
2363
  "dependencies": [
2323
2364
  "class-variance-authority",
2324
- "dowel-ui@^0.33.0"
2365
+ "dowel-ui@^0.34.0"
2325
2366
  ],
2326
2367
  "registryDependencies": [
2327
2368
  "https://lacodda.github.io/dowel/r/track-segments.json"
@@ -2357,7 +2398,7 @@
2357
2398
  "title": "TreeView",
2358
2399
  "description": "The shape products reach for and then get wrong in the same place every time. A tree is not a nest of lists with click handlers - it is one control with a cursor in it, and the difference is the whole component:\n * **One tab stop, not one per node.** A tree of four hundred files with a `tabIndex` on each is four hundred stops between the sidebar and the editor. The container is what the keyboard reaches, and the arrows move a cursor inside it - the arrangement a `RadioGroup` has, for the same reason.",
2359
2400
  "dependencies": [
2360
- "dowel-ui@^0.33.0"
2401
+ "dowel-ui@^0.34.0"
2361
2402
  ],
2362
2403
  "registryDependencies": [
2363
2404
  "https://lacodda.github.io/dowel/r/tree-rows.json"
@@ -2377,7 +2418,7 @@
2377
2418
  "title": "Truncate",
2378
2419
  "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.",
2379
2420
  "dependencies": [
2380
- "dowel-ui@^0.33.0"
2421
+ "dowel-ui@^0.34.0"
2381
2422
  ],
2382
2423
  "registryDependencies": [],
2383
2424
  "files": [
@@ -2395,7 +2436,7 @@
2395
2436
  "title": "VirtualList",
2396
2437
  "description": "The browser is fine with long lists until it is not: a hundred thousand `<div>`s is a layout the machine recomputes on every change, and the page stops responding while it does. What is drawn instead is the window the reader can actually see, held in place by a tall spacer, so the scrollbar still says how much there is.",
2397
2438
  "dependencies": [
2398
- "dowel-ui@^0.33.0"
2439
+ "dowel-ui@^0.34.0"
2399
2440
  ],
2400
2441
  "registryDependencies": [],
2401
2442
  "files": [
@@ -2414,7 +2455,7 @@
2414
2455
  "description": "With `decorations: false` the system draws nothing, so everything it used to do is the page's: dragging the window by its title bar, double-click to maximise, the three buttons, and the edges you grab to resize. Each is small; the reason to take them on at all is that a system title bar over an application title bar costs a strip of every laptop screen for nothing.",
2415
2456
  "dependencies": [
2416
2457
  "@tauri-apps/api",
2417
- "dowel-ui@^0.33.0"
2458
+ "dowel-ui@^0.34.0"
2418
2459
  ],
2419
2460
  "registryDependencies": [],
2420
2461
  "files": [
@@ -2432,7 +2473,7 @@
2432
2473
  "title": "Wizard",
2433
2474
  "description": "A Stepper with the current step's content under it and the Back / Next / Finish row that walks through the steps - checking each one before it lets the user past. The Stepper alone shows progress; this is the form that makes it, and a product that only needs the picture should not install the form.",
2434
2475
  "dependencies": [
2435
- "dowel-ui@^0.33.0"
2476
+ "dowel-ui@^0.34.0"
2436
2477
  ],
2437
2478
  "registryDependencies": [
2438
2479
  "https://lacodda.github.io/dowel/r/button.json",