dowel-ui 0.34.0 → 0.34.1

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.34.0"
310
+ "dowel-ui@^0.34.1"
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.34.0"
331
+ "dowel-ui@^0.34.1"
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.34.0"
353
+ "dowel-ui@^0.34.1"
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.34.0"
372
+ "dowel-ui@^0.34.1"
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.34.0"
392
+ "dowel-ui@^0.34.1"
393
393
  ],
394
394
  "registryDependencies": [
395
395
  "https://lacodda.github.io/dowel/r/activity-heatmap.json",
@@ -416,7 +416,7 @@
416
416
  "path": "ui/activity-weeks.tsx",
417
417
  "target": "@ui/activity-weeks.tsx",
418
418
  "type": "registry:ui",
419
- "content": "/*\n * Laying a run of dates out in weeks, with no React in it.\n *\n * Split out for the reason `table-sort` and `track-segments` are - a product\n * that draws this somewhere other than the DOM needs the arithmetic, not a\n * component. One of the line's consumers draws it in a terminal.\n *\n * The rule the whole grid rests on, and the one worth stating loudest:\n * **a cell has four meanings and only one of them is a number.** Nothing\n * recorded, a value, a day still in progress, and a date outside the range\n * asked for are four different facts. A grid that paints \"nothing recorded\" as\n * the palest shade of \"a value\" tells the reader somebody did nothing on a day\n * nobody reported - which is a claim invented by the drawing.\n */\n\n/** What one square of the grid is. */\nexport type CellKind =\n /** No entry for this date. Not zero - no data at all. */\n | 'none'\n /** An entry with a figure behind it. */\n | 'value'\n /** Under way, with no total yet: today's row, a day still open. */\n | 'partial'\n /** A date the grid draws to keep its shape, outside the range asked for. */\n | 'outside'\n\n/** What the caller knows about one date. */\nexport interface ActivityEntry {\n /** `YYYY-MM-DD`. A label, never a moment: no zone shifts a date here. */\n date: string\n /** The figure. `null` for a day under way, which has no total yet. */\n value: number | null\n}\n\nexport interface Cell {\n date: string\n kind: CellKind\n value: number | null\n /** Which of the five steps this lands on, 1-5; `null` unless `kind` is\n * `value`. Discrete rather than continuous, because five shades can be told\n * apart and matched against a legend where a smooth gradient can only say\n * \"more\" and \"less\". */\n step: number | null\n /** Saturday or Sunday, read from the date itself.\n *\n * Only the weekend, never \"a day off\": which days someone actually rests is\n * a calendar this does not have, and guessing would mark an ordinary\n * Saturday shift as unusual. */\n weekend: boolean\n}\n\n/** Five steps, and the reason there are five: a legend a reader can count. */\nexport const STEPS = 5\n\n/** Sunday-first, because that is what `Date#getUTCDay` counts from. */\nexport type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6\n\nconst DAY = 86_400_000\n\nfunction parse(date: string): number {\n return Date.parse(`${date}T00:00:00Z`)\n}\n\nfunction format(time: number): string {\n return new Date(time).toISOString().slice(0, 10)\n}\n\n/** Every date from `from` to `to` inclusive, as `YYYY-MM-DD`.\n *\n * Stepped through UTC so no local zone can shift a day: a date here is a\n * label, not a moment. */\nexport function datesBetween(from: string, to: string): string[] {\n const start = parse(from)\n const end = parse(to)\n if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return []\n\n const dates: string[] = []\n for (let time = start; time <= end; time += DAY) dates.push(format(time))\n return dates\n}\n\nexport function isWeekend(date: string): boolean {\n const day = new Date(parse(date)).getUTCDay()\n return day === 0 || day === 6\n}\n\nexport interface WeeksOptions {\n /** First date of the range the caller asked for. */\n from: string\n /** Last date. */\n to: string\n /** The day a week starts on. Monday in most of the world, Sunday in the US -\n * and it changes which column a date lands in, so it is stated rather than\n * guessed from a locale the grid cannot see. */\n weekStartsOn?: Weekday\n /** The ceiling the steps are measured against. Defaults to the largest value\n * present.\n *\n * Shared rather than per-column, so two grids can be compared: scaled to\n * itself, a quiet month and a heavy one each get their own darkest square,\n * which is the one thing a heatmap is for. */\n busiest?: number\n}\n\n/**\n * The range as columns of weeks, each column seven cells from the top.\n *\n * Columns rather than rows because that is the shape the grid is read in: a\n * week is a column, a weekday is a row, and time runs left to right.\n *\n * The first and last columns are padded to seven with `outside` cells, so\n * every column is the same height and a weekday stays on its own row. They are\n * not \"no data\" - the caller never asked about them - and drawing them as\n * empty squares would add days to the range that the reader did not request.\n */\nexport function weeks(entries: ActivityEntry[], options: WeeksOptions): Cell[][] {\n const { from, to, weekStartsOn = 1 } = options\n const dates = datesBetween(from, to)\n if (dates.length === 0) return []\n\n const byDate = new Map(entries.map((entry) => [entry.date, entry]))\n\n const values = entries\n .map((entry) => entry.value)\n .filter((value): value is number => value !== null && Number.isFinite(value))\n const busiest = options.busiest ?? (values.length > 0 ? Math.max(...values) : 0)\n\n const cellFor = (date: string): Cell => {\n const entry = byDate.get(date)\n const weekend = isWeekend(date)\n\n if (entry === undefined) return { date, kind: 'none', value: null, step: null, weekend }\n if (entry.value === null) return { date, kind: 'partial', value: null, step: null, weekend }\n\n return { date, kind: 'value', value: entry.value, step: stepFor(entry.value, busiest), weekend }\n }\n\n const outside = (date: string): Cell => ({\n date,\n kind: 'outside',\n value: null,\n step: null,\n weekend: isWeekend(date),\n })\n\n /* Where the first date sits in its own week, so the run starts on the right\n * row rather than at the top of the first column. */\n const columnOf = (date: string) => (new Date(parse(date)).getUTCDay() - weekStartsOn + 7) % 7\n\n const columns: Cell[][] = []\n let column: Cell[] = []\n\n // Pad the head, back-dating the squares so each one is the date it stands\n // for - a reader hovering the first column should not find a blank.\n const lead = columnOf(dates[0]!)\n for (let i = lead; i > 0; i--) column.push(outside(format(parse(dates[0]!) - i * DAY)))\n\n for (const date of dates) {\n column.push(cellFor(date))\n if (column.length === 7) {\n columns.push(column)\n column = []\n }\n }\n\n if (column.length > 0) {\n const last = parse(dates.at(-1)!)\n for (let i = 1; column.length < 7; i++) column.push(outside(format(last + i * DAY)))\n columns.push(column)\n }\n\n return columns\n}\n\n/**\n * Which step a value lands on, 1 to `STEPS`.\n *\n * The floor is 1 rather than 0: any value at all is a value, and a square\n * indistinguishable from an empty one would file a twenty-minute day under\n * \"nothing recorded\" - the confusion this module exists to prevent. A value of\n * zero is the exception, and it is the caller's own statement: zero measured is\n * not the same as nothing measured, and it still earns the faintest step.\n */\nexport function stepFor(value: number, busiest: number): number {\n /* No ceiling to measure against. That happens two ways and they mean\n * opposite things: a caller who stated no `busiest` and whose only values\n * are zero (every day measured, every day empty - the faintest step, because\n * that is what they are), or a caller who stated a ceiling of zero while\n * sending a real figure (the ceiling is wrong, and the figure is all the\n * grid has - the darkest, so it is not hidden). */\n if (!(busiest > 0)) return value > 0 ? STEPS : 1\n const share = Math.min(1, Math.max(0, value / busiest))\n return Math.max(1, Math.ceil(share * STEPS))\n}\n\n/** The weekday rows, in the order the grid draws them, as offsets from Sunday.\n *\n * Returned rather than assumed so a caller labelling the rows and a grid\n * drawing them cannot disagree about which row is Monday. */\nexport function weekdayRows(weekStartsOn: Weekday = 1): Weekday[] {\n return [0, 1, 2, 3, 4, 5, 6].map((offset) => ((weekStartsOn + offset) % 7) as Weekday)\n}\n"
419
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\n * Laying a run of dates out in weeks, with no React in it.\n *\n * Split out for the reason `table-sort` and `track-segments` are - a product\n * that draws this somewhere other than the DOM needs the arithmetic, not a\n * component. One of the line's consumers draws it in a terminal.\n *\n * The rule the whole grid rests on, and the one worth stating loudest:\n * **a cell has four meanings and only one of them is a number.** Nothing\n * recorded, a value, a day still in progress, and a date outside the range\n * asked for are four different facts. A grid that paints \"nothing recorded\" as\n * the palest shade of \"a value\" tells the reader somebody did nothing on a day\n * nobody reported - which is a claim invented by the drawing.\n */\n\n/** What one square of the grid is. */\nexport type CellKind =\n /** No entry for this date. Not zero - no data at all. */\n | 'none'\n /** An entry with a figure behind it. */\n | 'value'\n /** Under way, with no total yet: today's row, a day still open. */\n | 'partial'\n /** A date the grid draws to keep its shape, outside the range asked for. */\n | 'outside'\n\n/** What the caller knows about one date. */\nexport interface ActivityEntry {\n /** `YYYY-MM-DD`. A label, never a moment: no zone shifts a date here. */\n date: string\n /** The figure. `null` for a day under way, which has no total yet. */\n value: number | null\n}\n\nexport interface Cell {\n date: string\n kind: CellKind\n value: number | null\n /** Which of the five steps this lands on, 1-5; `null` unless `kind` is\n * `value`. Discrete rather than continuous, because five shades can be told\n * apart and matched against a legend where a smooth gradient can only say\n * \"more\" and \"less\". */\n step: number | null\n /** Saturday or Sunday, read from the date itself.\n *\n * Only the weekend, never \"a day off\": which days someone actually rests is\n * a calendar this does not have, and guessing would mark an ordinary\n * Saturday shift as unusual. */\n weekend: boolean\n}\n\n/** Five steps, and the reason there are five: a legend a reader can count. */\nexport const STEPS = 5\n\n/** Sunday-first, because that is what `Date#getUTCDay` counts from. */\nexport type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6\n\nconst DAY = 86_400_000\n\nfunction parse(date: string): number {\n return Date.parse(`${date}T00:00:00Z`)\n}\n\nfunction format(time: number): string {\n return new Date(time).toISOString().slice(0, 10)\n}\n\n/** Every date from `from` to `to` inclusive, as `YYYY-MM-DD`.\n *\n * Stepped through UTC so no local zone can shift a day: a date here is a\n * label, not a moment. */\nexport function datesBetween(from: string, to: string): string[] {\n const start = parse(from)\n const end = parse(to)\n if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return []\n\n const dates: string[] = []\n for (let time = start; time <= end; time += DAY) dates.push(format(time))\n return dates\n}\n\nexport function isWeekend(date: string): boolean {\n const day = new Date(parse(date)).getUTCDay()\n return day === 0 || day === 6\n}\n\nexport interface WeeksOptions {\n /** First date of the range the caller asked for. */\n from: string\n /** Last date. */\n to: string\n /** The day a week starts on. Monday in most of the world, Sunday in the US -\n * and it changes which column a date lands in, so it is stated rather than\n * guessed from a locale the grid cannot see. */\n weekStartsOn?: Weekday\n /** The ceiling the steps are measured against. Defaults to the largest value\n * present.\n *\n * Shared rather than per-column, so two grids can be compared: scaled to\n * itself, a quiet month and a heavy one each get their own darkest square,\n * which is the one thing a heatmap is for. */\n busiest?: number\n}\n\n/**\n * The range as columns of weeks, each column seven cells from the top.\n *\n * Columns rather than rows because that is the shape the grid is read in: a\n * week is a column, a weekday is a row, and time runs left to right.\n *\n * The first and last columns are padded to seven with `outside` cells, so\n * every column is the same height and a weekday stays on its own row. They are\n * not \"no data\" - the caller never asked about them - and drawing them as\n * empty squares would add days to the range that the reader did not request.\n */\nexport function weeks(entries: ActivityEntry[], options: WeeksOptions): Cell[][] {\n const { from, to, weekStartsOn = 1 } = options\n const dates = datesBetween(from, to)\n if (dates.length === 0) return []\n\n const byDate = new Map(entries.map((entry) => [entry.date, entry]))\n\n const values = entries\n .map((entry) => entry.value)\n .filter((value): value is number => value !== null && Number.isFinite(value))\n const busiest = options.busiest ?? (values.length > 0 ? Math.max(...values) : 0)\n\n const cellFor = (date: string): Cell => {\n const entry = byDate.get(date)\n const weekend = isWeekend(date)\n\n if (entry === undefined) return { date, kind: 'none', value: null, step: null, weekend }\n if (entry.value === null) return { date, kind: 'partial', value: null, step: null, weekend }\n\n return { date, kind: 'value', value: entry.value, step: stepFor(entry.value, busiest), weekend }\n }\n\n const outside = (date: string): Cell => ({\n date,\n kind: 'outside',\n value: null,\n step: null,\n weekend: isWeekend(date),\n })\n\n /* Where the first date sits in its own week, so the run starts on the right\n * row rather than at the top of the first column. */\n const columnOf = (date: string) => (new Date(parse(date)).getUTCDay() - weekStartsOn + 7) % 7\n\n const columns: Cell[][] = []\n let column: Cell[] = []\n\n // Pad the head, back-dating the squares so each one is the date it stands\n // for - a reader hovering the first column should not find a blank.\n const lead = columnOf(dates[0]!)\n for (let i = lead; i > 0; i--) column.push(outside(format(parse(dates[0]!) - i * DAY)))\n\n for (const date of dates) {\n column.push(cellFor(date))\n if (column.length === 7) {\n columns.push(column)\n column = []\n }\n }\n\n if (column.length > 0) {\n const last = parse(dates.at(-1)!)\n for (let i = 1; column.length < 7; i++) column.push(outside(format(last + i * DAY)))\n columns.push(column)\n }\n\n return columns\n}\n\n/**\n * Which step a value lands on, 1 to `STEPS`.\n *\n * The floor is 1 rather than 0: any value at all is a value, and a square\n * indistinguishable from an empty one would file a twenty-minute day under\n * \"nothing recorded\" - the confusion this module exists to prevent. A value of\n * zero is the exception, and it is the caller's own statement: zero measured is\n * not the same as nothing measured, and it still earns the faintest step.\n */\nexport function stepFor(value: number, busiest: number): number {\n /* No ceiling to measure against. That happens two ways and they mean\n * opposite things: a caller who stated no `busiest` and whose only values\n * are zero (every day measured, every day empty - the faintest step, because\n * that is what they are), or a caller who stated a ceiling of zero while\n * sending a real figure (the ceiling is wrong, and the figure is all the\n * grid has - the darkest, so it is not hidden). */\n if (!(busiest > 0)) return value > 0 ? STEPS : 1\n const share = Math.min(1, Math.max(0, value / busiest))\n return Math.max(1, Math.ceil(share * STEPS))\n}\n\n/** The weekday rows, in the order the grid draws them, as offsets from Sunday.\n *\n * Returned rather than assumed so a caller labelling the rows and a grid\n * drawing them cannot disagree about which row is Monday. */\nexport function weekdayRows(weekStartsOn: Weekday = 1): Weekday[] {\n return [0, 1, 2, 3, 4, 5, 6].map((offset) => ((weekStartsOn + offset) % 7) as Weekday)\n}\n"
420
420
  }
421
421
  ]
422
422
  },
@@ -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.34.0"
430
+ "dowel-ui@^0.34.1"
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.34.0"
449
+ "dowel-ui@^0.34.1"
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.34.0"
468
+ "dowel-ui@^0.34.1"
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.34.0"
487
+ "dowel-ui@^0.34.1"
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.34.0"
506
+ "dowel-ui@^0.34.1"
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.34.0"
525
+ "dowel-ui@^0.34.1"
526
526
  ],
527
527
  "registryDependencies": [],
528
528
  "files": [
@@ -541,7 +541,7 @@
541
541
  "description": "One thing on a day of a month view, in one line: a release, a lesson, a payment that falls due.",
542
542
  "dependencies": [
543
543
  "@base-ui/react",
544
- "dowel-ui@^0.34.0"
544
+ "dowel-ui@^0.34.1"
545
545
  ],
546
546
  "registryDependencies": [],
547
547
  "files": [
@@ -559,7 +559,7 @@
559
559
  "title": "BigCalendar",
560
560
  "description": "A month at a time, with what is on each day of it, filling the box it is given.",
561
561
  "dependencies": [
562
- "dowel-ui@^0.34.0"
562
+ "dowel-ui@^0.34.1"
563
563
  ],
564
564
  "registryDependencies": [
565
565
  "https://lacodda.github.io/dowel/r/big-calendar-item.json",
@@ -582,7 +582,7 @@
582
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.",
583
583
  "dependencies": [
584
584
  "@base-ui/react",
585
- "dowel-ui@^0.34.0"
585
+ "dowel-ui@^0.34.1"
586
586
  ],
587
587
  "registryDependencies": [],
588
588
  "files": [
@@ -602,7 +602,7 @@
602
602
  "dependencies": [
603
603
  "@base-ui/react",
604
604
  "class-variance-authority",
605
- "dowel-ui@^0.34.0"
605
+ "dowel-ui@^0.34.1"
606
606
  ],
607
607
  "registryDependencies": [],
608
608
  "files": [
@@ -626,7 +626,7 @@
626
626
  "path": "ui/calendar-math.tsx",
627
627
  "target": "@ui/calendar-math.tsx",
628
628
  "type": "registry:ui",
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"
629
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\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"
630
630
  }
631
631
  ]
632
632
  },
@@ -636,7 +636,7 @@
636
636
  "title": "Calendar",
637
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.",
638
638
  "dependencies": [
639
- "dowel-ui@^0.34.0"
639
+ "dowel-ui@^0.34.1"
640
640
  ],
641
641
  "registryDependencies": [
642
642
  "https://lacodda.github.io/dowel/r/calendar-math.json"
@@ -657,7 +657,7 @@
657
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.",
658
658
  "dependencies": [
659
659
  "@base-ui/react",
660
- "dowel-ui@^0.34.0"
660
+ "dowel-ui@^0.34.1"
661
661
  ],
662
662
  "registryDependencies": [],
663
663
  "files": [
@@ -677,7 +677,7 @@
677
677
  "dependencies": [
678
678
  "@base-ui/react",
679
679
  "class-variance-authority",
680
- "dowel-ui@^0.34.0"
680
+ "dowel-ui@^0.34.1"
681
681
  ],
682
682
  "registryDependencies": [],
683
683
  "files": [
@@ -696,7 +696,7 @@
696
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.",
697
697
  "dependencies": [
698
698
  "class-variance-authority",
699
- "dowel-ui@^0.34.0"
699
+ "dowel-ui@^0.34.1"
700
700
  ],
701
701
  "registryDependencies": [
702
702
  "https://lacodda.github.io/dowel/r/copy-button.json"
@@ -717,7 +717,7 @@
717
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.",
718
718
  "dependencies": [
719
719
  "@base-ui/react",
720
- "dowel-ui@^0.34.0"
720
+ "dowel-ui@^0.34.1"
721
721
  ],
722
722
  "registryDependencies": [],
723
723
  "files": [
@@ -735,7 +735,7 @@
735
735
  "title": "ColorField",
736
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.",
737
737
  "dependencies": [
738
- "dowel-ui@^0.34.0"
738
+ "dowel-ui@^0.34.1"
739
739
  ],
740
740
  "registryDependencies": [
741
741
  "https://lacodda.github.io/dowel/r/input.json"
@@ -755,7 +755,7 @@
755
755
  "title": "ColumnResizeHandle",
756
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.",
757
757
  "dependencies": [
758
- "dowel-ui@^0.34.0"
758
+ "dowel-ui@^0.34.1"
759
759
  ],
760
760
  "registryDependencies": [],
761
761
  "files": [
@@ -775,7 +775,7 @@
775
775
  "dependencies": [
776
776
  "@base-ui/react",
777
777
  "class-variance-authority",
778
- "dowel-ui@^0.34.0"
778
+ "dowel-ui@^0.34.1"
779
779
  ],
780
780
  "registryDependencies": [
781
781
  "https://lacodda.github.io/dowel/r/input.json",
@@ -799,7 +799,7 @@
799
799
  "dependencies": [
800
800
  "@base-ui/react",
801
801
  "class-variance-authority",
802
- "dowel-ui@^0.34.0"
802
+ "dowel-ui@^0.34.1"
803
803
  ],
804
804
  "registryDependencies": [
805
805
  "https://lacodda.github.io/dowel/r/combobox.json",
@@ -823,7 +823,7 @@
823
823
  "dependencies": [
824
824
  "@base-ui/react",
825
825
  "class-variance-authority",
826
- "dowel-ui@^0.34.0"
826
+ "dowel-ui@^0.34.1"
827
827
  ],
828
828
  "registryDependencies": [
829
829
  "https://lacodda.github.io/dowel/r/dialog.json",
@@ -845,7 +845,7 @@
845
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>`.",
846
846
  "dependencies": [
847
847
  "@base-ui/react",
848
- "dowel-ui@^0.34.0"
848
+ "dowel-ui@^0.34.1"
849
849
  ],
850
850
  "registryDependencies": [
851
851
  "https://lacodda.github.io/dowel/r/layer.json",
@@ -866,7 +866,7 @@
866
866
  "title": "CopyButton",
867
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.",
868
868
  "dependencies": [
869
- "dowel-ui@^0.34.0"
869
+ "dowel-ui@^0.34.1"
870
870
  ],
871
871
  "registryDependencies": [],
872
872
  "files": [
@@ -884,7 +884,7 @@
884
884
  "title": "Copyable",
885
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.",
886
886
  "dependencies": [
887
- "dowel-ui@^0.34.0"
887
+ "dowel-ui@^0.34.1"
888
888
  ],
889
889
  "registryDependencies": [],
890
890
  "files": [
@@ -902,7 +902,7 @@
902
902
  "title": "DatePicker",
903
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.",
904
904
  "dependencies": [
905
- "dowel-ui@^0.34.0"
905
+ "dowel-ui@^0.34.1"
906
906
  ],
907
907
  "registryDependencies": [
908
908
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -925,7 +925,7 @@
925
925
  "title": "DateRangePicker",
926
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.",
927
927
  "dependencies": [
928
- "dowel-ui@^0.34.0"
928
+ "dowel-ui@^0.34.1"
929
929
  ],
930
930
  "registryDependencies": [
931
931
  "https://lacodda.github.io/dowel/r/calendar.json",
@@ -950,7 +950,7 @@
950
950
  "dependencies": [
951
951
  "@base-ui/react",
952
952
  "class-variance-authority",
953
- "dowel-ui@^0.34.0"
953
+ "dowel-ui@^0.34.1"
954
954
  ],
955
955
  "registryDependencies": [
956
956
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -976,7 +976,7 @@
976
976
  "path": "ui/diff-lines.tsx",
977
977
  "target": "@ui/diff-lines.tsx",
978
978
  "type": "registry:ui",
979
- "content": "/*\n * The comparison behind DiffView, with no React in it.\n *\n * Split out like `line-scale` and `table-sort`: a product that wants to know\n * how much moved between two drafts - to put a number in a list, to decide\n * whether to offer the comparison at all - should not have to render a\n * component to find out.\n *\n * Lines rather than words, and that is a choice about the subject. The text\n * being compared here is prose someone wrote and revised - a draft, a note, a\n * configuration file - and prose is revised BY THE LINE. A word-level diff of\n * a rewritten paragraph is confetti: technically accurate, and it answers a\n * question nobody asked. Where the subject really is word-level - a title, a\n * single sentence - a product compares the two strings itself.\n *\n * Plain longest-common-subsequence. The bodies are a page or two, so the exact\n * O(n·m) answer costs nothing and there is no reason to reach for a heuristic.\n *\n * Taken from kilna, which had it first, with the arithmetic unchanged and the\n * pairing added: `rows` is the part the donor did not have, and its absence is\n * what made the donor's two columns drift out of step.\n */\n\n/** One step through the comparison. */\nexport type Change =\n | { kind: 'same'; text: string }\n | { kind: 'added'; text: string }\n | { kind: 'removed'; text: string }\n\n/**\n * One row of a side-by-side comparison: what stands on each side of it.\n *\n * `null` is a side with nothing there - the gap opposite an inserted line -\n * and it is deliberately not an empty string. An empty string is a line\n * somebody wrote that happens to have no characters on it, and a comparison\n * that cannot tell those apart draws a blank line as a deletion.\n */\nexport interface DiffRow {\n before: string | null\n after: string | null\n kind: Change['kind']\n /** Line numbers in each text, 1-based, for a gutter. `null` on the side that\n * has nothing. */\n beforeLine: number | null\n afterLine: number | null\n}\n\n/** How many lines each side may have before the comparison gives up. A table\n * of 2000×2000 is four million cells; past that the honest answer is \"too long\n * to compare\", not a frozen window. */\nexport const LIMIT = 2000\n\nexport function diffLines(before: string, after: string): Change[] {\n const a = before.split('\\n')\n const b = after.split('\\n')\n\n if (a.length > LIMIT || b.length > LIMIT) {\n return [\n { kind: 'removed', text: before },\n { kind: 'added', text: after },\n ]\n }\n\n // lcs[i][j] - the length of the longest common subsequence of a[i..] and\n // b[j..]. Filled backwards so the walk forwards can be greedy.\n const lcs: number[][] = Array.from({ length: a.length + 1 }, () =>\n Array.from({ length: b.length + 1 }, () => 0),\n )\n\n for (let i = a.length - 1; i >= 0; i -= 1) {\n for (let j = b.length - 1; j >= 0; j -= 1) {\n lcs[i]![j] = a[i] === b[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!)\n }\n }\n\n const changes: Change[] = []\n let i = 0\n let j = 0\n\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n changes.push({ kind: 'same', text: a[i]! })\n i += 1\n j += 1\n } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) {\n changes.push({ kind: 'removed', text: a[i]! })\n i += 1\n } else {\n changes.push({ kind: 'added', text: b[j]! })\n j += 1\n }\n }\n\n while (i < a.length) {\n changes.push({ kind: 'removed', text: a[i]! })\n i += 1\n }\n while (j < b.length) {\n changes.push({ kind: 'added', text: b[j]! })\n j += 1\n }\n\n return changes\n}\n\n/**\n * The changes as rows of two columns - the thing that makes side-by-side mean\n * anything.\n *\n * The obvious way to draw two columns is to filter the change list twice: keep\n * everything that is not `added` on the left, everything that is not `removed`\n * on the right. Both columns come out individually correct and they stop\n * lining up at the first insertion, because from there on they hold different\n * numbers of rows. The reader then compares line 4 against line 3 for the rest\n * of the screen, and nothing about the drawing looks wrong - which is why the\n * defect survived in the product this was taken from.\n *\n * Pairing instead makes the alignment structural: a row is one object with two\n * sides, so the columns are the same height by construction rather than by two\n * filters happening to agree.\n *\n * A removal immediately followed by an insertion is paired into ONE row rather\n * than two. That is the common shape of an edit - a line was rewritten - and\n * showing the old and the new opposite each other is the whole point of the\n * comparison. Left as separate rows, a rewritten line reads as a deletion\n * followed by an unrelated addition, with a gap opposite each.\n */\nexport function rows(changes: readonly Change[]): DiffRow[] {\n const out: DiffRow[] = []\n let beforeLine = 1\n let afterLine = 1\n\n for (let at = 0; at < changes.length; at += 1) {\n const change = changes[at]!\n\n if (change.kind === 'same') {\n out.push({\n before: change.text,\n after: change.text,\n kind: 'same',\n beforeLine: beforeLine++,\n afterLine: afterLine++,\n })\n continue\n }\n\n if (change.kind === 'removed') {\n // A removal with an insertion right behind it is a rewrite: pair them,\n // so the reader sees what the line became rather than two separate\n // events with a gap opposite each.\n const next = changes[at + 1]\n if (next?.kind === 'added') {\n out.push({\n before: change.text,\n after: next.text,\n kind: 'removed',\n beforeLine: beforeLine++,\n afterLine: afterLine++,\n })\n at += 1\n continue\n }\n out.push({\n before: change.text,\n after: null,\n kind: 'removed',\n beforeLine: beforeLine++,\n afterLine: null,\n })\n continue\n }\n\n out.push({\n before: null,\n after: change.text,\n kind: 'added',\n beforeLine: null,\n afterLine: afterLine++,\n })\n }\n\n return out\n}\n\n/** How much moved, for the one line above a comparison - or for a list that\n * wants to say \"12 lines changed\" without drawing anything. */\nexport function countChanges(changes: readonly Change[]): { added: number; removed: number } {\n let added = 0\n let removed = 0\n for (const change of changes) {\n if (change.kind === 'added') added += 1\n else if (change.kind === 'removed') removed += 1\n }\n return { added, removed }\n}\n"
979
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\n * The comparison behind DiffView, with no React in it.\n *\n * Split out like `line-scale` and `table-sort`: a product that wants to know\n * how much moved between two drafts - to put a number in a list, to decide\n * whether to offer the comparison at all - should not have to render a\n * component to find out.\n *\n * Lines rather than words, and that is a choice about the subject. The text\n * being compared here is prose someone wrote and revised - a draft, a note, a\n * configuration file - and prose is revised BY THE LINE. A word-level diff of\n * a rewritten paragraph is confetti: technically accurate, and it answers a\n * question nobody asked. Where the subject really is word-level - a title, a\n * single sentence - a product compares the two strings itself.\n *\n * Plain longest-common-subsequence. The bodies are a page or two, so the exact\n * O(n·m) answer costs nothing and there is no reason to reach for a heuristic.\n *\n * Taken from kilna, which had it first, with the arithmetic unchanged and the\n * pairing added: `rows` is the part the donor did not have, and its absence is\n * what made the donor's two columns drift out of step.\n */\n\n/** One step through the comparison. */\nexport type Change =\n | { kind: 'same'; text: string }\n | { kind: 'added'; text: string }\n | { kind: 'removed'; text: string }\n\n/**\n * One row of a side-by-side comparison: what stands on each side of it.\n *\n * `null` is a side with nothing there - the gap opposite an inserted line -\n * and it is deliberately not an empty string. An empty string is a line\n * somebody wrote that happens to have no characters on it, and a comparison\n * that cannot tell those apart draws a blank line as a deletion.\n */\nexport interface DiffRow {\n before: string | null\n after: string | null\n kind: Change['kind']\n /** Line numbers in each text, 1-based, for a gutter. `null` on the side that\n * has nothing. */\n beforeLine: number | null\n afterLine: number | null\n}\n\n/** How many lines each side may have before the comparison gives up. A table\n * of 2000×2000 is four million cells; past that the honest answer is \"too long\n * to compare\", not a frozen window. */\nexport const LIMIT = 2000\n\nexport function diffLines(before: string, after: string): Change[] {\n const a = before.split('\\n')\n const b = after.split('\\n')\n\n if (a.length > LIMIT || b.length > LIMIT) {\n return [\n { kind: 'removed', text: before },\n { kind: 'added', text: after },\n ]\n }\n\n // lcs[i][j] - the length of the longest common subsequence of a[i..] and\n // b[j..]. Filled backwards so the walk forwards can be greedy.\n const lcs: number[][] = Array.from({ length: a.length + 1 }, () =>\n Array.from({ length: b.length + 1 }, () => 0),\n )\n\n for (let i = a.length - 1; i >= 0; i -= 1) {\n for (let j = b.length - 1; j >= 0; j -= 1) {\n lcs[i]![j] = a[i] === b[j] ? lcs[i + 1]![j + 1]! + 1 : Math.max(lcs[i + 1]![j]!, lcs[i]![j + 1]!)\n }\n }\n\n const changes: Change[] = []\n let i = 0\n let j = 0\n\n while (i < a.length && j < b.length) {\n if (a[i] === b[j]) {\n changes.push({ kind: 'same', text: a[i]! })\n i += 1\n j += 1\n } else if (lcs[i + 1]![j]! >= lcs[i]![j + 1]!) {\n changes.push({ kind: 'removed', text: a[i]! })\n i += 1\n } else {\n changes.push({ kind: 'added', text: b[j]! })\n j += 1\n }\n }\n\n while (i < a.length) {\n changes.push({ kind: 'removed', text: a[i]! })\n i += 1\n }\n while (j < b.length) {\n changes.push({ kind: 'added', text: b[j]! })\n j += 1\n }\n\n return changes\n}\n\n/**\n * The changes as rows of two columns - the thing that makes side-by-side mean\n * anything.\n *\n * The obvious way to draw two columns is to filter the change list twice: keep\n * everything that is not `added` on the left, everything that is not `removed`\n * on the right. Both columns come out individually correct and they stop\n * lining up at the first insertion, because from there on they hold different\n * numbers of rows. The reader then compares line 4 against line 3 for the rest\n * of the screen, and nothing about the drawing looks wrong - which is why the\n * defect survived in the product this was taken from.\n *\n * Pairing instead makes the alignment structural: a row is one object with two\n * sides, so the columns are the same height by construction rather than by two\n * filters happening to agree.\n *\n * A removal immediately followed by an insertion is paired into ONE row rather\n * than two. That is the common shape of an edit - a line was rewritten - and\n * showing the old and the new opposite each other is the whole point of the\n * comparison. Left as separate rows, a rewritten line reads as a deletion\n * followed by an unrelated addition, with a gap opposite each.\n */\nexport function rows(changes: readonly Change[]): DiffRow[] {\n const out: DiffRow[] = []\n let beforeLine = 1\n let afterLine = 1\n\n for (let at = 0; at < changes.length; at += 1) {\n const change = changes[at]!\n\n if (change.kind === 'same') {\n out.push({\n before: change.text,\n after: change.text,\n kind: 'same',\n beforeLine: beforeLine++,\n afterLine: afterLine++,\n })\n continue\n }\n\n if (change.kind === 'removed') {\n // A removal with an insertion right behind it is a rewrite: pair them,\n // so the reader sees what the line became rather than two separate\n // events with a gap opposite each.\n const next = changes[at + 1]\n if (next?.kind === 'added') {\n out.push({\n before: change.text,\n after: next.text,\n kind: 'removed',\n beforeLine: beforeLine++,\n afterLine: afterLine++,\n })\n at += 1\n continue\n }\n out.push({\n before: change.text,\n after: null,\n kind: 'removed',\n beforeLine: beforeLine++,\n afterLine: null,\n })\n continue\n }\n\n out.push({\n before: null,\n after: change.text,\n kind: 'added',\n beforeLine: null,\n afterLine: afterLine++,\n })\n }\n\n return out\n}\n\n/** How much moved, for the one line above a comparison - or for a list that\n * wants to say \"12 lines changed\" without drawing anything. */\nexport function countChanges(changes: readonly Change[]): { added: number; removed: number } {\n let added = 0\n let removed = 0\n for (const change of changes) {\n if (change.kind === 'added') added += 1\n else if (change.kind === 'removed') removed += 1\n }\n return { added, removed }\n}\n"
980
980
  }
981
981
  ]
982
982
  },
@@ -987,7 +987,7 @@
987
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.",
988
988
  "dependencies": [
989
989
  "class-variance-authority",
990
- "dowel-ui@^0.34.0"
990
+ "dowel-ui@^0.34.1"
991
991
  ],
992
992
  "registryDependencies": [
993
993
  "https://lacodda.github.io/dowel/r/copy-button.json",
@@ -1009,7 +1009,7 @@
1009
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.",
1010
1010
  "dependencies": [
1011
1011
  "class-variance-authority",
1012
- "dowel-ui@^0.34.0"
1012
+ "dowel-ui@^0.34.1"
1013
1013
  ],
1014
1014
  "registryDependencies": [],
1015
1015
  "files": [
@@ -1029,7 +1029,7 @@
1029
1029
  "dependencies": [
1030
1030
  "@base-ui/react",
1031
1031
  "class-variance-authority",
1032
- "dowel-ui@^0.34.0"
1032
+ "dowel-ui@^0.34.1"
1033
1033
  ],
1034
1034
  "registryDependencies": [
1035
1035
  "https://lacodda.github.io/dowel/r/dialog.json",
@@ -1050,7 +1050,7 @@
1050
1050
  "title": "DurationField",
1051
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.",
1052
1052
  "dependencies": [
1053
- "dowel-ui@^0.34.0"
1053
+ "dowel-ui@^0.34.1"
1054
1054
  ],
1055
1055
  "registryDependencies": [
1056
1056
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1071,7 +1071,7 @@
1071
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.",
1072
1072
  "dependencies": [
1073
1073
  "class-variance-authority",
1074
- "dowel-ui@^0.34.0"
1074
+ "dowel-ui@^0.34.1"
1075
1075
  ],
1076
1076
  "registryDependencies": [],
1077
1077
  "files": [
@@ -1125,7 +1125,7 @@
1125
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.",
1126
1126
  "dependencies": [
1127
1127
  "@base-ui/react",
1128
- "dowel-ui@^0.34.0"
1128
+ "dowel-ui@^0.34.1"
1129
1129
  ],
1130
1130
  "registryDependencies": [],
1131
1131
  "files": [
@@ -1143,7 +1143,7 @@
1143
1143
  "title": "FileDrop",
1144
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.",
1145
1145
  "dependencies": [
1146
- "dowel-ui@^0.34.0"
1146
+ "dowel-ui@^0.34.1"
1147
1147
  ],
1148
1148
  "registryDependencies": [],
1149
1149
  "files": [
@@ -1161,7 +1161,7 @@
1161
1161
  "title": "FilterPopover",
1162
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.",
1163
1163
  "dependencies": [
1164
- "dowel-ui@^0.34.0"
1164
+ "dowel-ui@^0.34.1"
1165
1165
  ],
1166
1166
  "registryDependencies": [
1167
1167
  "https://lacodda.github.io/dowel/r/button.json",
@@ -1183,7 +1183,7 @@
1183
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`.",
1184
1184
  "dependencies": [
1185
1185
  "class-variance-authority",
1186
- "dowel-ui@^0.34.0"
1186
+ "dowel-ui@^0.34.1"
1187
1187
  ],
1188
1188
  "registryDependencies": [],
1189
1189
  "files": [
@@ -1201,7 +1201,7 @@
1201
1201
  "title": "InlineField",
1202
1202
  "description": "A value that is edited where it is shown: click it, type, Enter or leave to keep, Escape to take it back.",
1203
1203
  "dependencies": [
1204
- "dowel-ui@^0.34.0"
1204
+ "dowel-ui@^0.34.1"
1205
1205
  ],
1206
1206
  "registryDependencies": [
1207
1207
  "https://lacodda.github.io/dowel/r/field-draft.json"
@@ -1221,7 +1221,7 @@
1221
1221
  "title": "Input",
1222
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.",
1223
1223
  "dependencies": [
1224
- "dowel-ui@^0.34.0"
1224
+ "dowel-ui@^0.34.1"
1225
1225
  ],
1226
1226
  "registryDependencies": [],
1227
1227
  "files": [
@@ -1245,7 +1245,7 @@
1245
1245
  "path": "ui/json-rows.tsx",
1246
1246
  "target": "@ui/json-rows.tsx",
1247
1247
  "type": "registry:ui",
1248
- "content": "/*\n * What a JSON value looks like as a list of rows, with no React in it.\n *\n * The sibling of `tree-rows`, built the same way and for the same reason: a\n * product that wants to count the rows before drawing any of them - to put a\n * viewer inside a VirtualList, to say \"1,204 entries\" - imports this and never\n * the component.\n *\n * Flattening is also what makes the keyboard simple. Down is the next row of\n * this list and Up the previous, whatever the nesting; a recursive walk at\n * every keystroke asks the same question and answers it differently at each\n * depth.\n */\n\n/** What a value is, for drawing and for deciding whether it opens.\n *\n * `null` is its own kind rather than an absence: in JSON it is a value\n * somebody wrote, and a viewer that shows it as an empty cell says the key is\n * missing when it is present and null - a distinction that decides bugs. */\nexport type JsonKind = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'\n\n/** Any value `JSON.parse` can return. */\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\nexport interface JsonRow {\n /**\n * Where this sits, in JSONPath: `$.items[3].name`.\n *\n * The identity of a row, and deliberately not its index: a row's index\n * changes when a branch above it opens, and anything remembered by index -\n * which rows are open, which is selected - would jump to a different value\n * the moment something above it moved. It is also what a reader copies when\n * they want to point at this value from somewhere else.\n */\n path: string\n /** The key, or the index as written in the path. `null` only for the root. */\n key: string | null\n /** True when the key is an array index rather than an object's name - drawn\n * differently, because `0` as a name and `0` as a position are not the same\n * thing. */\n index: boolean\n kind: JsonKind\n /** The value itself, for a leaf. A branch has none: what it holds is in the\n * rows below it. */\n value?: string | number | boolean | null\n depth: number\n /** How many entries a branch holds, so a closed one can say so without being\n * opened. */\n size?: number\n /** The path of the branch this row sits in, if any. What Left uses to get\n * out of a deep branch in one press. */\n parent?: string\n}\n\nexport function kindOf(value: JsonValue): JsonKind {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value as JsonKind\n}\n\nexport function isBranch(kind: JsonKind): boolean {\n return kind === 'object' || kind === 'array'\n}\n\n/*\n * A key as it appears in a path.\n *\n * Dot notation where the key is an ordinary identifier, brackets otherwise -\n * which is not decoration. `$.user name` is not a path anything can resolve,\n * and a key containing a dot (`$.a.b` for the single key `\"a.b\"`) is a path\n * that resolves to the WRONG value silently. Both are common in real data:\n * configuration files and anything exported from a spreadsheet.\n */\nfunction step(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)\n ? `.${key}`\n : `[${JSON.stringify(key)}]`\n}\n\n/**\n * The rows a value shows, in the order the eye and the keyboard travel.\n *\n * Only what is visible: a closed branch contributes its own row and nothing\n * below it. That is what keeps a viewer of a large document cheap - a\n * thousand-entry array that nobody opened costs one row.\n */\nexport function visibleRows(\n value: JsonValue,\n open: ReadonlySet<string>,\n { root = '$' }: { root?: string } = {},\n): JsonRow[] {\n const rows: JsonRow[] = []\n\n const walk = (\n node: JsonValue,\n path: string,\n key: string | null,\n index: boolean,\n depth: number,\n parent?: string,\n ): void => {\n const kind = kindOf(node)\n\n if (!isBranch(kind)) {\n rows.push({ path, key, index, kind, value: node as string | number | boolean | null, depth, parent })\n return\n }\n\n const entries: [string, JsonValue][] = Array.isArray(node)\n ? node.map((item, at) => [String(at), item])\n : Object.entries(node as { [key: string]: JsonValue })\n\n rows.push({ path, key, index, kind, depth, size: entries.length, parent })\n\n if (!open.has(path)) return\n\n for (const [childKey, child] of entries) {\n walk(\n child,\n Array.isArray(node) ? `${path}[${childKey}]` : `${path}${step(childKey)}`,\n childKey,\n Array.isArray(node),\n depth + 1,\n path,\n )\n }\n }\n\n walk(value, root, null, false, 0)\n return rows\n}\n\n/**\n * The paths to open so a document arrives readable.\n *\n * Bounded twice, by depth AND by size, and the second bound is the one that\n * does the work. A depth bound alone reads as sufficient and is not: the cost\n * of opening is measured in ROWS, while depth counts LEVELS, and those track\n * each other only while the branches are small - which is the case nobody\n * needed protecting from.\n *\n * Measured, not reasoned: a payload holding `assets: [1204 entries]` at its\n * top level rendered 1216 rows and 24,000 pixels of scroll on arrival under a\n * depth-2 bound, because the array sits AT depth 2. That is exactly the freeze\n * the bound exists to prevent, produced by the bound itself. The shape that\n * hurts is one huge branch near the surface, and a level count is blindest to\n * precisely that shape.\n *\n * So a branch opens when it is shallow enough AND holds fewer than `size`\n * entries. A branch left shut is not descended through either - what is inside\n * something the reader cannot see does not need deciding about.\n */\nexport function branchPaths(\n value: JsonValue,\n {\n root = '$',\n depth = 2,\n /** How many entries a branch may hold and still open by itself. Twenty is\n * about a screen: enough that a settings file arrives open, few enough\n * that a list of records arrives as a list of records. */\n size = 20,\n }: { root?: string; depth?: number; size?: number } = {},\n): Set<string> {\n const paths = new Set<string>()\n\n const walk = (node: JsonValue, path: string, level: number): void => {\n const kind = kindOf(node)\n if (!isBranch(kind) || level > depth) return\n\n const entries: [string, JsonValue][] = Array.isArray(node)\n ? node.map((item, at) => [String(at), item])\n : Object.entries(node as { [key: string]: JsonValue })\n\n // Too big to open, so it stays shut - and nothing below it is considered:\n // those rows are not going to be drawn either way.\n if (entries.length > size) return\n\n paths.add(path)\n\n for (const [key, child] of entries) {\n walk(child, Array.isArray(node) ? `${path}[${key}]` : `${path}${step(key)}`, level + 1)\n }\n }\n\n walk(value, root, 1)\n return paths\n}\n\n/**\n * What a closed branch says about itself: `{ 4 }` or `[ 1204 ]`.\n *\n * The count rather than a preview of the contents. A preview of the first\n * entries reads as though those are all of them, which is the one thing a\n * closed branch must not imply.\n */\nexport function summarise(row: JsonRow): string {\n if (row.kind === 'array') return `[ ${row.size ?? 0} ]`\n return `{ ${row.size ?? 0} }`\n}\n"
1248
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\n * What a JSON value looks like as a list of rows, with no React in it.\n *\n * The sibling of `tree-rows`, built the same way and for the same reason: a\n * product that wants to count the rows before drawing any of them - to put a\n * viewer inside a VirtualList, to say \"1,204 entries\" - imports this and never\n * the component.\n *\n * Flattening is also what makes the keyboard simple. Down is the next row of\n * this list and Up the previous, whatever the nesting; a recursive walk at\n * every keystroke asks the same question and answers it differently at each\n * depth.\n */\n\n/** What a value is, for drawing and for deciding whether it opens.\n *\n * `null` is its own kind rather than an absence: in JSON it is a value\n * somebody wrote, and a viewer that shows it as an empty cell says the key is\n * missing when it is present and null - a distinction that decides bugs. */\nexport type JsonKind = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null'\n\n/** Any value `JSON.parse` can return. */\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }\n\nexport interface JsonRow {\n /**\n * Where this sits, in JSONPath: `$.items[3].name`.\n *\n * The identity of a row, and deliberately not its index: a row's index\n * changes when a branch above it opens, and anything remembered by index -\n * which rows are open, which is selected - would jump to a different value\n * the moment something above it moved. It is also what a reader copies when\n * they want to point at this value from somewhere else.\n */\n path: string\n /** The key, or the index as written in the path. `null` only for the root. */\n key: string | null\n /** True when the key is an array index rather than an object's name - drawn\n * differently, because `0` as a name and `0` as a position are not the same\n * thing. */\n index: boolean\n kind: JsonKind\n /** The value itself, for a leaf. A branch has none: what it holds is in the\n * rows below it. */\n value?: string | number | boolean | null\n depth: number\n /** How many entries a branch holds, so a closed one can say so without being\n * opened. */\n size?: number\n /** The path of the branch this row sits in, if any. What Left uses to get\n * out of a deep branch in one press. */\n parent?: string\n}\n\nexport function kindOf(value: JsonValue): JsonKind {\n if (value === null) return 'null'\n if (Array.isArray(value)) return 'array'\n return typeof value as JsonKind\n}\n\nexport function isBranch(kind: JsonKind): boolean {\n return kind === 'object' || kind === 'array'\n}\n\n/*\n * A key as it appears in a path.\n *\n * Dot notation where the key is an ordinary identifier, brackets otherwise -\n * which is not decoration. `$.user name` is not a path anything can resolve,\n * and a key containing a dot (`$.a.b` for the single key `\"a.b\"`) is a path\n * that resolves to the WRONG value silently. Both are common in real data:\n * configuration files and anything exported from a spreadsheet.\n */\nfunction step(key: string): string {\n return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)\n ? `.${key}`\n : `[${JSON.stringify(key)}]`\n}\n\n/**\n * The rows a value shows, in the order the eye and the keyboard travel.\n *\n * Only what is visible: a closed branch contributes its own row and nothing\n * below it. That is what keeps a viewer of a large document cheap - a\n * thousand-entry array that nobody opened costs one row.\n */\nexport function visibleRows(\n value: JsonValue,\n open: ReadonlySet<string>,\n { root = '$' }: { root?: string } = {},\n): JsonRow[] {\n const rows: JsonRow[] = []\n\n const walk = (\n node: JsonValue,\n path: string,\n key: string | null,\n index: boolean,\n depth: number,\n parent?: string,\n ): void => {\n const kind = kindOf(node)\n\n if (!isBranch(kind)) {\n rows.push({ path, key, index, kind, value: node as string | number | boolean | null, depth, parent })\n return\n }\n\n const entries: [string, JsonValue][] = Array.isArray(node)\n ? node.map((item, at) => [String(at), item])\n : Object.entries(node as { [key: string]: JsonValue })\n\n rows.push({ path, key, index, kind, depth, size: entries.length, parent })\n\n if (!open.has(path)) return\n\n for (const [childKey, child] of entries) {\n walk(\n child,\n Array.isArray(node) ? `${path}[${childKey}]` : `${path}${step(childKey)}`,\n childKey,\n Array.isArray(node),\n depth + 1,\n path,\n )\n }\n }\n\n walk(value, root, null, false, 0)\n return rows\n}\n\n/**\n * The paths to open so a document arrives readable.\n *\n * Bounded twice, by depth AND by size, and the second bound is the one that\n * does the work. A depth bound alone reads as sufficient and is not: the cost\n * of opening is measured in ROWS, while depth counts LEVELS, and those track\n * each other only while the branches are small - which is the case nobody\n * needed protecting from.\n *\n * Measured, not reasoned: a payload holding `assets: [1204 entries]` at its\n * top level rendered 1216 rows and 24,000 pixels of scroll on arrival under a\n * depth-2 bound, because the array sits AT depth 2. That is exactly the freeze\n * the bound exists to prevent, produced by the bound itself. The shape that\n * hurts is one huge branch near the surface, and a level count is blindest to\n * precisely that shape.\n *\n * So a branch opens when it is shallow enough AND holds fewer than `size`\n * entries. A branch left shut is not descended through either - what is inside\n * something the reader cannot see does not need deciding about.\n */\nexport function branchPaths(\n value: JsonValue,\n {\n root = '$',\n depth = 2,\n /** How many entries a branch may hold and still open by itself. Twenty is\n * about a screen: enough that a settings file arrives open, few enough\n * that a list of records arrives as a list of records. */\n size = 20,\n }: { root?: string; depth?: number; size?: number } = {},\n): Set<string> {\n const paths = new Set<string>()\n\n const walk = (node: JsonValue, path: string, level: number): void => {\n const kind = kindOf(node)\n if (!isBranch(kind) || level > depth) return\n\n const entries: [string, JsonValue][] = Array.isArray(node)\n ? node.map((item, at) => [String(at), item])\n : Object.entries(node as { [key: string]: JsonValue })\n\n // Too big to open, so it stays shut - and nothing below it is considered:\n // those rows are not going to be drawn either way.\n if (entries.length > size) return\n\n paths.add(path)\n\n for (const [key, child] of entries) {\n walk(child, Array.isArray(node) ? `${path}[${key}]` : `${path}${step(key)}`, level + 1)\n }\n }\n\n walk(value, root, 1)\n return paths\n}\n\n/**\n * What a closed branch says about itself: `{ 4 }` or `[ 1204 ]`.\n *\n * The count rather than a preview of the contents. A preview of the first\n * entries reads as though those are all of them, which is the one thing a\n * closed branch must not imply.\n */\nexport function summarise(row: JsonRow): string {\n if (row.kind === 'array') return `[ ${row.size ?? 0} ]`\n return `{ ${row.size ?? 0} }`\n}\n"
1249
1249
  }
1250
1250
  ]
1251
1251
  },
@@ -1255,7 +1255,7 @@
1255
1255
  "title": "JsonViewer",
1256
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.",
1257
1257
  "dependencies": [
1258
- "dowel-ui@^0.34.0"
1258
+ "dowel-ui@^0.34.1"
1259
1259
  ],
1260
1260
  "registryDependencies": [
1261
1261
  "https://lacodda.github.io/dowel/r/json-rows.json"
@@ -1275,7 +1275,7 @@
1275
1275
  "title": "Kbd",
1276
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.",
1277
1277
  "dependencies": [
1278
- "dowel-ui@^0.34.0"
1278
+ "dowel-ui@^0.34.1"
1279
1279
  ],
1280
1280
  "registryDependencies": [],
1281
1281
  "files": [
@@ -1294,7 +1294,7 @@
1294
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.",
1295
1295
  "dependencies": [
1296
1296
  "class-variance-authority",
1297
- "dowel-ui@^0.34.0"
1297
+ "dowel-ui@^0.34.1"
1298
1298
  ],
1299
1299
  "registryDependencies": [],
1300
1300
  "files": [
@@ -1329,7 +1329,7 @@
1329
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.",
1330
1330
  "dependencies": [
1331
1331
  "class-variance-authority",
1332
- "dowel-ui@^0.34.0"
1332
+ "dowel-ui@^0.34.1"
1333
1333
  ],
1334
1334
  "registryDependencies": [
1335
1335
  "https://lacodda.github.io/dowel/r/line-scale.json"
@@ -1355,7 +1355,7 @@
1355
1355
  "path": "ui/line-scale.tsx",
1356
1356
  "target": "@ui/line-scale.tsx",
1357
1357
  "type": "registry:ui",
1358
- "content": "/*\n * The arithmetic behind LineChart, with no React in it.\n *\n * Split out like `track-segments` and `activity-weeks`: a product labelling\n * its own points, or checking its own domain sums, should not import a\n * component to get at the numbers.\n *\n * Two things live here and neither is obvious. Turning a series with holes in\n * it into drawable runs, and choosing the ticks on an axis - which is a\n * question about what reads as a round number, not about dividing a range into\n * equal parts.\n */\n\n/** One reading. `value: null` is a measurement that was not taken - which is\n * not a reading of zero, and not a reason to move the ones after it. */\nexport interface Point {\n /** Position along the axis. Same units throughout: a timestamp, an index. */\n at: number\n value: number | null\n}\n\nexport interface Bounds {\n /** The value the left edge stands for. */\n from: number\n /** The right edge. */\n to: number\n /** The bottom of the plot. */\n min: number\n /** The top. */\n max: number\n}\n\n/** A point placed in the box, in percentages: x from the left, y from the top\n * (SVG's own direction, so a larger value has a smaller y). */\nexport interface Placed {\n x: number\n y: number\n at: number\n value: number\n}\n\n/**\n * The bounds a series is drawn against.\n *\n * The y range does NOT start at zero by default, and that is a deliberate\n * departure from the bar's rule. A bar's length *is* the quantity, so its\n * baseline has to be zero or the length lies. A line's job is the shape of a\n * change, and a balance moving between 4,900 and 5,100 flattens into a\n * horizontal rule on a zero-based axis - the very thing the reader opened the\n * chart to see. What keeps it honest is that the axis is labelled: the ticks\n * say where the bottom is, so nobody reads the floor as nothing.\n *\n * A caller who wants zero states it, and for a quantity that is a count rather\n * than a level - requests, errors - they should.\n */\nexport function boundsOf(points: Point[], stated: Partial<Bounds> = {}): Bounds | null {\n const drawn = points.filter((point): point is Point & { value: number } => point.value !== null)\n if (drawn.length === 0) return null\n\n const values = drawn.map((point) => point.value)\n const positions = points.map((point) => point.at)\n\n const from = stated.from ?? Math.min(...positions)\n const to = stated.to ?? Math.max(...positions)\n\n let min = stated.min ?? Math.min(...values)\n let max = stated.max ?? Math.max(...values)\n\n /* A flat series has no range to scale against, and dividing by it would put\n * every point at the same y - or at NaN. Given a band around the value, the\n * line sits in the middle of the plot and reads as what it is: unchanging. */\n if (!(max > min)) {\n const pad = Math.abs(max) > 0 ? Math.abs(max) * 0.1 : 1\n min = max - pad\n max = max + pad\n }\n\n return { from, to, min, max }\n}\n\n/**\n * The series as runs of consecutive readings.\n *\n * A hole breaks the line rather than being drawn through or closed up, and the\n * three options are worth naming because two of them lie:\n *\n * **Interpolating across** invents a reading nobody took - the one thing a\n * chart of measurements must never do.\n *\n * **Dropping the point** keeps the line whole and moves every later reading\n * to the left, so the axis stops matching the data: a chart that says the\n * value was 40 in March when it was 40 in April.\n *\n * **Breaking the line** leaves the gap visible, keeps every other point\n * where it belongs, and invents nothing. So that is what happens here.\n *\n * A run of one point is kept: it has no line, but it has a dot, and dropping\n * it would hide a reading that exists.\n */\nexport function runs(points: Point[], bounds: Bounds): Placed[][] {\n const span = bounds.to - bounds.from\n const height = bounds.max - bounds.min\n if (!(span > 0) || !(height > 0)) return []\n\n const out: Placed[][] = []\n let run: Placed[] = []\n\n for (const point of points) {\n if (point.value === null) {\n if (run.length > 0) out.push(run)\n run = []\n continue\n }\n\n const clamped = Math.min(Math.max(point.value, bounds.min), bounds.max)\n run.push({\n x: ((point.at - bounds.from) / span) * 100,\n // SVG's y grows downward, so the largest value sits at the top.\n y: ((bounds.max - clamped) / height) * 100,\n at: point.at,\n value: point.value,\n })\n }\n\n if (run.length > 0) out.push(run)\n return out\n}\n\n/** A run as an SVG path, in the same percentages.\n *\n * Straight segments rather than a curve: a spline through measured points\n * overshoots between them, inventing highs and lows that were never recorded -\n * the same lie as interpolating across a gap, drawn more prettily. */\nexport function pathOf(run: Placed[]): string {\n return run\n .map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)} ${point.y.toFixed(2)}`)\n .join(' ')\n}\n\n/**\n * Ticks for the value axis, on round numbers.\n *\n * Not the range cut into equal parts: 4,900 to 5,100 in four gives 4,950 and\n * 5,050, which nobody reads as a landmark. A tick's job is to be recognised\n * instantly, so the step is the nearest 1, 2, 5 or 10 above what the range\n * needs, and the ticks are the multiples of it inside the range.\n *\n * Returns nothing when a step would not fit - a range too narrow for a round\n * number is better with no ticks than with invented ones.\n */\nexport function ticksFor(bounds: Bounds, wanted = 4): number[] {\n const height = bounds.max - bounds.min\n if (!(height > 0) || wanted < 1) return []\n\n const rough = height / wanted\n const magnitude = 10 ** Math.floor(Math.log10(rough))\n const step = [1, 2, 5, 10].map((n) => n * magnitude).find((candidate) => candidate >= rough)\n if (step === undefined) return []\n\n const ticks: number[] = []\n for (let tick = Math.ceil(bounds.min / step) * step; tick <= bounds.max; tick += step) {\n // Floating point leaves 0.30000000000000004 where 0.3 was meant; the step\n // is what decides how many decimals are real.\n const decimals = Math.max(0, -Math.floor(Math.log10(step)))\n ticks.push(Number(tick.toFixed(decimals)))\n }\n\n return ticks\n}\n\n/** Where a value sits in the plot, as a percentage from the top - for a tick's\n * rule, or a threshold drawn across the line. */\nexport function yOf(value: number, bounds: Bounds): number | null {\n const height = bounds.max - bounds.min\n if (!(height > 0)) return null\n if (value < bounds.min || value > bounds.max) return null\n return ((bounds.max - value) / height) * 100\n}\n"
1358
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\n * The arithmetic behind LineChart, with no React in it.\n *\n * Split out like `track-segments` and `activity-weeks`: a product labelling\n * its own points, or checking its own domain sums, should not import a\n * component to get at the numbers.\n *\n * Two things live here and neither is obvious. Turning a series with holes in\n * it into drawable runs, and choosing the ticks on an axis - which is a\n * question about what reads as a round number, not about dividing a range into\n * equal parts.\n */\n\n/** One reading. `value: null` is a measurement that was not taken - which is\n * not a reading of zero, and not a reason to move the ones after it. */\nexport interface Point {\n /** Position along the axis. Same units throughout: a timestamp, an index. */\n at: number\n value: number | null\n}\n\nexport interface Bounds {\n /** The value the left edge stands for. */\n from: number\n /** The right edge. */\n to: number\n /** The bottom of the plot. */\n min: number\n /** The top. */\n max: number\n}\n\n/** A point placed in the box, in percentages: x from the left, y from the top\n * (SVG's own direction, so a larger value has a smaller y). */\nexport interface Placed {\n x: number\n y: number\n at: number\n value: number\n}\n\n/**\n * The bounds a series is drawn against.\n *\n * The y range does NOT start at zero by default, and that is a deliberate\n * departure from the bar's rule. A bar's length *is* the quantity, so its\n * baseline has to be zero or the length lies. A line's job is the shape of a\n * change, and a balance moving between 4,900 and 5,100 flattens into a\n * horizontal rule on a zero-based axis - the very thing the reader opened the\n * chart to see. What keeps it honest is that the axis is labelled: the ticks\n * say where the bottom is, so nobody reads the floor as nothing.\n *\n * A caller who wants zero states it, and for a quantity that is a count rather\n * than a level - requests, errors - they should.\n */\nexport function boundsOf(points: Point[], stated: Partial<Bounds> = {}): Bounds | null {\n const drawn = points.filter((point): point is Point & { value: number } => point.value !== null)\n if (drawn.length === 0) return null\n\n const values = drawn.map((point) => point.value)\n const positions = points.map((point) => point.at)\n\n const from = stated.from ?? Math.min(...positions)\n const to = stated.to ?? Math.max(...positions)\n\n let min = stated.min ?? Math.min(...values)\n let max = stated.max ?? Math.max(...values)\n\n /* A flat series has no range to scale against, and dividing by it would put\n * every point at the same y - or at NaN. Given a band around the value, the\n * line sits in the middle of the plot and reads as what it is: unchanging. */\n if (!(max > min)) {\n const pad = Math.abs(max) > 0 ? Math.abs(max) * 0.1 : 1\n min = max - pad\n max = max + pad\n }\n\n return { from, to, min, max }\n}\n\n/**\n * The series as runs of consecutive readings.\n *\n * A hole breaks the line rather than being drawn through or closed up, and the\n * three options are worth naming because two of them lie:\n *\n * **Interpolating across** invents a reading nobody took - the one thing a\n * chart of measurements must never do.\n *\n * **Dropping the point** keeps the line whole and moves every later reading\n * to the left, so the axis stops matching the data: a chart that says the\n * value was 40 in March when it was 40 in April.\n *\n * **Breaking the line** leaves the gap visible, keeps every other point\n * where it belongs, and invents nothing. So that is what happens here.\n *\n * A run of one point is kept: it has no line, but it has a dot, and dropping\n * it would hide a reading that exists.\n */\nexport function runs(points: Point[], bounds: Bounds): Placed[][] {\n const span = bounds.to - bounds.from\n const height = bounds.max - bounds.min\n if (!(span > 0) || !(height > 0)) return []\n\n const out: Placed[][] = []\n let run: Placed[] = []\n\n for (const point of points) {\n if (point.value === null) {\n if (run.length > 0) out.push(run)\n run = []\n continue\n }\n\n const clamped = Math.min(Math.max(point.value, bounds.min), bounds.max)\n run.push({\n x: ((point.at - bounds.from) / span) * 100,\n // SVG's y grows downward, so the largest value sits at the top.\n y: ((bounds.max - clamped) / height) * 100,\n at: point.at,\n value: point.value,\n })\n }\n\n if (run.length > 0) out.push(run)\n return out\n}\n\n/** A run as an SVG path, in the same percentages.\n *\n * Straight segments rather than a curve: a spline through measured points\n * overshoots between them, inventing highs and lows that were never recorded -\n * the same lie as interpolating across a gap, drawn more prettily. */\nexport function pathOf(run: Placed[]): string {\n return run\n .map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(2)} ${point.y.toFixed(2)}`)\n .join(' ')\n}\n\n/**\n * Ticks for the value axis, on round numbers.\n *\n * Not the range cut into equal parts: 4,900 to 5,100 in four gives 4,950 and\n * 5,050, which nobody reads as a landmark. A tick's job is to be recognised\n * instantly, so the step is the nearest 1, 2, 5 or 10 above what the range\n * needs, and the ticks are the multiples of it inside the range.\n *\n * Returns nothing when a step would not fit - a range too narrow for a round\n * number is better with no ticks than with invented ones.\n */\nexport function ticksFor(bounds: Bounds, wanted = 4): number[] {\n const height = bounds.max - bounds.min\n if (!(height > 0) || wanted < 1) return []\n\n const rough = height / wanted\n const magnitude = 10 ** Math.floor(Math.log10(rough))\n const step = [1, 2, 5, 10].map((n) => n * magnitude).find((candidate) => candidate >= rough)\n if (step === undefined) return []\n\n const ticks: number[] = []\n for (let tick = Math.ceil(bounds.min / step) * step; tick <= bounds.max; tick += step) {\n // Floating point leaves 0.30000000000000004 where 0.3 was meant; the step\n // is what decides how many decimals are real.\n const decimals = Math.max(0, -Math.floor(Math.log10(step)))\n ticks.push(Number(tick.toFixed(decimals)))\n }\n\n return ticks\n}\n\n/** Where a value sits in the plot, as a percentage from the top - for a tick's\n * rule, or a threshold drawn across the line. */\nexport function yOf(value: number, bounds: Bounds): number | null {\n const height = bounds.max - bounds.min\n if (!(height > 0)) return null\n if (value < bounds.min || value > bounds.max) return null\n return ((bounds.max - value) / height) * 100\n}\n"
1359
1359
  }
1360
1360
  ]
1361
1361
  },
@@ -1366,7 +1366,7 @@
1366
1366
  "description": "A row of a list, one thing said in a line: ListRow for a fact, RowButton for a choice.",
1367
1367
  "dependencies": [
1368
1368
  "@base-ui/react",
1369
- "dowel-ui@^0.34.0"
1369
+ "dowel-ui@^0.34.1"
1370
1370
  ],
1371
1371
  "registryDependencies": [],
1372
1372
  "files": [
@@ -1384,7 +1384,7 @@
1384
1384
  "title": "MarkedText",
1385
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.",
1386
1386
  "dependencies": [
1387
- "dowel-ui@^0.34.0"
1387
+ "dowel-ui@^0.34.1"
1388
1388
  ],
1389
1389
  "registryDependencies": [],
1390
1390
  "files": [
@@ -1404,7 +1404,7 @@
1404
1404
  "dependencies": [
1405
1405
  "@base-ui/react",
1406
1406
  "class-variance-authority",
1407
- "dowel-ui@^0.34.0"
1407
+ "dowel-ui@^0.34.1"
1408
1408
  ],
1409
1409
  "registryDependencies": [
1410
1410
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -1426,7 +1426,7 @@
1426
1426
  "dependencies": [
1427
1427
  "@base-ui/react",
1428
1428
  "class-variance-authority",
1429
- "dowel-ui@^0.34.0"
1429
+ "dowel-ui@^0.34.1"
1430
1430
  ],
1431
1431
  "registryDependencies": [
1432
1432
  "https://lacodda.github.io/dowel/r/tooltip.json"
@@ -1446,7 +1446,7 @@
1446
1446
  "title": "NotificationBell",
1447
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.",
1448
1448
  "dependencies": [
1449
- "dowel-ui@^0.34.0"
1449
+ "dowel-ui@^0.34.1"
1450
1450
  ],
1451
1451
  "registryDependencies": [
1452
1452
  "https://lacodda.github.io/dowel/r/button.json",
@@ -1468,7 +1468,7 @@
1468
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.",
1469
1469
  "dependencies": [
1470
1470
  "@base-ui/react",
1471
- "dowel-ui@^0.34.0"
1471
+ "dowel-ui@^0.34.1"
1472
1472
  ],
1473
1473
  "registryDependencies": [
1474
1474
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1488,7 +1488,7 @@
1488
1488
  "title": "NumberFormat",
1489
1489
  "description": "Two things, and the second is the reason this is a component rather than a call to `toLocaleString` at each site.",
1490
1490
  "dependencies": [
1491
- "dowel-ui@^0.34.0"
1491
+ "dowel-ui@^0.34.1"
1492
1492
  ],
1493
1493
  "registryDependencies": [],
1494
1494
  "files": [
@@ -1507,7 +1507,7 @@
1507
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.",
1508
1508
  "dependencies": [
1509
1509
  "class-variance-authority",
1510
- "dowel-ui@^0.34.0"
1510
+ "dowel-ui@^0.34.1"
1511
1511
  ],
1512
1512
  "registryDependencies": [],
1513
1513
  "files": [
@@ -1525,7 +1525,7 @@
1525
1525
  "title": "PageSize",
1526
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.",
1527
1527
  "dependencies": [
1528
- "dowel-ui@^0.34.0"
1528
+ "dowel-ui@^0.34.1"
1529
1529
  ],
1530
1530
  "registryDependencies": [
1531
1531
  "https://lacodda.github.io/dowel/r/select.json"
@@ -1545,7 +1545,7 @@
1545
1545
  "title": "Pagination",
1546
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.",
1547
1547
  "dependencies": [
1548
- "dowel-ui@^0.34.0"
1548
+ "dowel-ui@^0.34.1"
1549
1549
  ],
1550
1550
  "registryDependencies": [
1551
1551
  "https://lacodda.github.io/dowel/r/button.json"
@@ -1566,7 +1566,7 @@
1566
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.",
1567
1567
  "dependencies": [
1568
1568
  "class-variance-authority",
1569
- "dowel-ui@^0.34.0"
1569
+ "dowel-ui@^0.34.1"
1570
1570
  ],
1571
1571
  "registryDependencies": [],
1572
1572
  "files": [
@@ -1584,7 +1584,7 @@
1584
1584
  "title": "PasswordField",
1585
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.",
1586
1586
  "dependencies": [
1587
- "dowel-ui@^0.34.0"
1587
+ "dowel-ui@^0.34.1"
1588
1588
  ],
1589
1589
  "registryDependencies": [
1590
1590
  "https://lacodda.github.io/dowel/r/input.json"
@@ -1606,7 +1606,7 @@
1606
1606
  "dependencies": [
1607
1607
  "@base-ui/react",
1608
1608
  "class-variance-authority",
1609
- "dowel-ui@^0.34.0"
1609
+ "dowel-ui@^0.34.1"
1610
1610
  ],
1611
1611
  "registryDependencies": [
1612
1612
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -1628,7 +1628,7 @@
1628
1628
  "dependencies": [
1629
1629
  "@base-ui/react",
1630
1630
  "class-variance-authority",
1631
- "dowel-ui@^0.34.0"
1631
+ "dowel-ui@^0.34.1"
1632
1632
  ],
1633
1633
  "registryDependencies": [
1634
1634
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -1648,7 +1648,7 @@
1648
1648
  "title": "ProductMark",
1649
1649
  "description": "The product-mark primitive.",
1650
1650
  "dependencies": [
1651
- "dowel-ui@^0.34.0"
1651
+ "dowel-ui@^0.34.1"
1652
1652
  ],
1653
1653
  "registryDependencies": [],
1654
1654
  "files": [
@@ -1666,7 +1666,7 @@
1666
1666
  "title": "ProductSwitcher",
1667
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.",
1668
1668
  "dependencies": [
1669
- "dowel-ui@^0.34.0"
1669
+ "dowel-ui@^0.34.1"
1670
1670
  ],
1671
1671
  "registryDependencies": [
1672
1672
  "https://lacodda.github.io/dowel/r/menu.json",
@@ -1689,7 +1689,7 @@
1689
1689
  "dependencies": [
1690
1690
  "@base-ui/react",
1691
1691
  "class-variance-authority",
1692
- "dowel-ui@^0.34.0"
1692
+ "dowel-ui@^0.34.1"
1693
1693
  ],
1694
1694
  "registryDependencies": [],
1695
1695
  "files": [
@@ -1729,7 +1729,7 @@
1729
1729
  "dependencies": [
1730
1730
  "@base-ui/react",
1731
1731
  "class-variance-authority",
1732
- "dowel-ui@^0.34.0"
1732
+ "dowel-ui@^0.34.1"
1733
1733
  ],
1734
1734
  "registryDependencies": [],
1735
1735
  "files": [
@@ -1747,7 +1747,7 @@
1747
1747
  "title": "RatingScale",
1748
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\".",
1749
1749
  "dependencies": [
1750
- "dowel-ui@^0.34.0"
1750
+ "dowel-ui@^0.34.1"
1751
1751
  ],
1752
1752
  "registryDependencies": [],
1753
1753
  "files": [
@@ -1765,7 +1765,7 @@
1765
1765
  "title": "RelativeTime",
1766
1766
  "description": "The relative-time primitive.",
1767
1767
  "dependencies": [
1768
- "dowel-ui@^0.34.0"
1768
+ "dowel-ui@^0.34.1"
1769
1769
  ],
1770
1770
  "registryDependencies": [],
1771
1771
  "files": [
@@ -1783,7 +1783,7 @@
1783
1783
  "title": "ReorderableList",
1784
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.",
1785
1785
  "dependencies": [
1786
- "dowel-ui@^0.34.0"
1786
+ "dowel-ui@^0.34.1"
1787
1787
  ],
1788
1788
  "registryDependencies": [],
1789
1789
  "files": [
@@ -1801,7 +1801,7 @@
1801
1801
  "title": "SaveState",
1802
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.",
1803
1803
  "dependencies": [
1804
- "dowel-ui@^0.34.0"
1804
+ "dowel-ui@^0.34.1"
1805
1805
  ],
1806
1806
  "registryDependencies": [
1807
1807
  "https://lacodda.github.io/dowel/r/spinner.json"
@@ -1823,7 +1823,7 @@
1823
1823
  "dependencies": [
1824
1824
  "@base-ui/react",
1825
1825
  "class-variance-authority",
1826
- "dowel-ui@^0.34.0"
1826
+ "dowel-ui@^0.34.1"
1827
1827
  ],
1828
1828
  "registryDependencies": [],
1829
1829
  "files": [
@@ -1841,7 +1841,7 @@
1841
1841
  "title": "SearchField",
1842
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.",
1843
1843
  "dependencies": [
1844
- "dowel-ui@^0.34.0"
1844
+ "dowel-ui@^0.34.1"
1845
1845
  ],
1846
1846
  "registryDependencies": [
1847
1847
  "https://lacodda.github.io/dowel/r/input.json",
@@ -1864,7 +1864,7 @@
1864
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.",
1865
1865
  "dependencies": [
1866
1866
  "@base-ui/react",
1867
- "dowel-ui@^0.34.0"
1867
+ "dowel-ui@^0.34.1"
1868
1868
  ],
1869
1869
  "registryDependencies": [],
1870
1870
  "files": [
@@ -1883,7 +1883,7 @@
1883
1883
  "description": "Two to five options, one of them chosen, all of them in view - a radio group in a segment's clothes.",
1884
1884
  "dependencies": [
1885
1885
  "@base-ui/react",
1886
- "dowel-ui@^0.34.0"
1886
+ "dowel-ui@^0.34.1"
1887
1887
  ],
1888
1888
  "registryDependencies": [],
1889
1889
  "files": [
@@ -1903,7 +1903,7 @@
1903
1903
  "dependencies": [
1904
1904
  "@base-ui/react",
1905
1905
  "class-variance-authority",
1906
- "dowel-ui@^0.34.0"
1906
+ "dowel-ui@^0.34.1"
1907
1907
  ],
1908
1908
  "registryDependencies": [
1909
1909
  "https://lacodda.github.io/dowel/r/input.json",
@@ -1940,7 +1940,7 @@
1940
1940
  "title": "SkeletonOf",
1941
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.",
1942
1942
  "dependencies": [
1943
- "dowel-ui@^0.34.0"
1943
+ "dowel-ui@^0.34.1"
1944
1944
  ],
1945
1945
  "registryDependencies": [],
1946
1946
  "files": [
@@ -1958,7 +1958,7 @@
1958
1958
  "title": "Skeleton",
1959
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.",
1960
1960
  "dependencies": [
1961
- "dowel-ui@^0.34.0"
1961
+ "dowel-ui@^0.34.1"
1962
1962
  ],
1963
1963
  "registryDependencies": [],
1964
1964
  "files": [
@@ -1977,7 +1977,7 @@
1977
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\".",
1978
1978
  "dependencies": [
1979
1979
  "@base-ui/react",
1980
- "dowel-ui@^0.34.0"
1980
+ "dowel-ui@^0.34.1"
1981
1981
  ],
1982
1982
  "registryDependencies": [],
1983
1983
  "files": [
@@ -1996,7 +1996,7 @@
1996
1996
  "description": "The shape of a history, not a chart of it: no axes, no gridlines, no ticks.",
1997
1997
  "dependencies": [
1998
1998
  "class-variance-authority",
1999
- "dowel-ui@^0.34.0"
1999
+ "dowel-ui@^0.34.1"
2000
2000
  ],
2001
2001
  "registryDependencies": [],
2002
2002
  "files": [
@@ -2015,7 +2015,7 @@
2015
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.",
2016
2016
  "dependencies": [
2017
2017
  "class-variance-authority",
2018
- "dowel-ui@^0.34.0"
2018
+ "dowel-ui@^0.34.1"
2019
2019
  ],
2020
2020
  "registryDependencies": [],
2021
2021
  "files": [
@@ -2033,7 +2033,7 @@
2033
2033
  "title": "Splash",
2034
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.",
2035
2035
  "dependencies": [
2036
- "dowel-ui@^0.34.0"
2036
+ "dowel-ui@^0.34.1"
2037
2037
  ],
2038
2038
  "registryDependencies": [],
2039
2039
  "files": [
@@ -2052,7 +2052,7 @@
2052
2052
  "description": "The splitter primitive.",
2053
2053
  "dependencies": [
2054
2054
  "class-variance-authority",
2055
- "dowel-ui@^0.34.0"
2055
+ "dowel-ui@^0.34.1"
2056
2056
  ],
2057
2057
  "registryDependencies": [],
2058
2058
  "files": [
@@ -2071,7 +2071,7 @@
2071
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.",
2072
2072
  "dependencies": [
2073
2073
  "class-variance-authority",
2074
- "dowel-ui@^0.34.0"
2074
+ "dowel-ui@^0.34.1"
2075
2075
  ],
2076
2076
  "registryDependencies": [],
2077
2077
  "files": [
@@ -2090,7 +2090,7 @@
2090
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.",
2091
2091
  "dependencies": [
2092
2092
  "class-variance-authority",
2093
- "dowel-ui@^0.34.0"
2093
+ "dowel-ui@^0.34.1"
2094
2094
  ],
2095
2095
  "registryDependencies": [],
2096
2096
  "files": [
@@ -2109,7 +2109,7 @@
2109
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.",
2110
2110
  "dependencies": [
2111
2111
  "class-variance-authority",
2112
- "dowel-ui@^0.34.0"
2112
+ "dowel-ui@^0.34.1"
2113
2113
  ],
2114
2114
  "registryDependencies": [],
2115
2115
  "files": [
@@ -2128,7 +2128,7 @@
2128
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.",
2129
2129
  "dependencies": [
2130
2130
  "@base-ui/react",
2131
- "dowel-ui@^0.34.0"
2131
+ "dowel-ui@^0.34.1"
2132
2132
  ],
2133
2133
  "registryDependencies": [],
2134
2134
  "files": [
@@ -2152,7 +2152,7 @@
2152
2152
  "path": "ui/table-sort.tsx",
2153
2153
  "target": "@ui/table-sort.tsx",
2154
2154
  "type": "registry:ui",
2155
- "content": "/*\n * How a column is ordered, with no React in it.\n *\n * Split out of the Table for the reason `calendar-math` was split out of the\n * Calendar: these are the sums, and the component is the thing that draws\n * them. A product sorting its own rows - on the server, in a worker, before\n * the data ever reaches a component - imports this and nothing else.\n *\n * No table library, deliberately. The obvious choice here is TanStack Table,\n * and it would be the first dependency a product has to install beyond Base\n * UI. What it offers is a model of columns, pages and sorting state; what the\n * line's one real table needed was the rule below, which the model does not\n * have. So the model is the part that is written here, and it is small.\n *\n * The rule, and the reason this file exists at all:\n *\n * **Absence sorts last, whichever way the column points.**\n *\n * A row with no value in this column is not the smallest - it is unknown, and\n * the two are different facts. Rank absence with the rest and flip the sign,\n * and a descending sort floats every empty row to the top: the reader asks for\n * \"highest first\" and is handed the rows that have no value at all. This is\n * measured rather than assumed - it is what the first version of the line's\n * catalogue did, and the fix is the shape below, where presence is settled\n * before the direction is applied.\n *\n * It is the same fact `RatingScale` is built on: not judged yet is a state,\n * not a zero. A table that sorts them together loses it on the first click.\n */\n\n/** Which way a column points. */\nexport type SortDirection = 'asc' | 'desc'\n\n/** A column, and which way it points. `column` is the caller's own key - the\n * id it gave the column, not an index, so reordering columns cannot silently\n * change what is sorted. */\nexport interface Sort<Column extends string = string> {\n column: Column\n direction: SortDirection\n}\n\n/** What can be compared. `null` and `undefined` both mean absent - a product\n * gets whichever its data uses, and having to normalise them before sorting\n * is the kind of step that gets forgotten in one branch of a switch. */\nexport type SortValue = string | number | boolean | null | undefined\n\n/** Reads the value of a column out of a row.\n *\n * Given rather than inferred: a column id is not always a field name, and a\n * sortable column is often a computed one - a duration that is stored as two\n * timestamps, a name that sorts by surname. */\nexport type SortAccessor<Row, Column extends string = string> = (\n row: Row,\n column: Column,\n) => SortValue\n\nexport interface SortOptions<Row> {\n /** The tiebreaker: what decides the order of rows that compare equal.\n *\n * Without one, `Array.prototype.sort` is stable and therefore leaves ties in\n * input order - which sounds fine until the input is re-fetched and arrives\n * in a different order, and a column of ties reshuffles under the reader\n * with no click. Pass the row's id. */\n tiebreak?: (row: Row) => SortValue\n /** The language text is compared in - the application's, from\n * `useLocale()`. Passed to `Intl.Collator`, so `ä` sorts where the reader\n * expects rather than after `z`. Required: left out, the collator used the\n * browser's language, which is not the one the table is written in. */\n locale: string\n}\n\n/** Order the rows. Returns a new array; the input is not touched, because a\n * component that sorts its own prop in place mutates the caller's state. */\nexport function sortRows<Row, Column extends string = string>(\n rows: readonly Row[],\n sort: Sort<Column>,\n accessor: SortAccessor<Row, Column>,\n options: SortOptions<Row>,\n): Row[] {\n const sign = sort.direction === 'asc' ? 1 : -1\n // One collator for the whole sort rather than one `localeCompare` per\n // comparison: building it is the expensive half, and a sort of n rows asks\n // for it n log n times.\n const collator = new Intl.Collator(options.locale)\n\n return [...rows].sort((a, b) => {\n const left = accessor(a, sort.column)\n const right = accessor(b, sort.column)\n\n // Presence first, and outside the sign. This is the whole point of the\n // file: absence is last in both directions.\n const absent = presence(left, right)\n if (absent !== 0) return absent\n\n const ranked = compareValues(left, right, collator) * sign\n if (ranked !== 0) return ranked\n\n if (options.tiebreak) {\n const tied = compareValues(options.tiebreak(a), options.tiebreak(b), collator)\n // Deliberately not multiplied by the sign. The tiebreaker is there to\n // make the order stable, and an order that reverses with the column is\n // not stable - the rows that tie would swap places on every click.\n if (tied !== 0) return tied\n }\n return 0\n })\n}\n\n/** Whether a value counts as absent.\n *\n * Both spellings of nothing, and neither `0` nor `''` nor `false`, which are\n * values a row genuinely has.\n *\n * `NaN` counts too, and that is not tidiness. Subtraction with it returns\n * `NaN`, which `sort` reads as \"these two are equal\" - so a row whose number\n * is not a number takes whatever place the input happened to give it, and the\n * order changes when the data is re-fetched. A number that is not a number is\n * not a small number; it is unknown, which is what this file already has a\n * place for. */\nexport function isAbsent(value: SortValue): boolean {\n if (value === null || value === undefined) return true\n return typeof value === 'number' && Number.isNaN(value)\n}\n\n/** Which of the two lacks a value; absent sorts last, always. */\nfunction presence(left: SortValue, right: SortValue): number {\n const leftHas = !isAbsent(left)\n const rightHas = !isAbsent(right)\n if (leftHas === rightHas) return 0\n return leftHas ? -1 : 1\n}\n\n/** Compare two present values of the same column.\n *\n * Numbers by subtraction, text by collator, and booleans as false-then-true.\n * A column whose values are of mixed type compares as text: that is a data\n * problem the table cannot fix, and ordering by `String` at least gives the\n * same answer twice. */\nfunction compareValues(left: SortValue, right: SortValue, collator: Intl.Collator): number {\n if (isAbsent(left) || isAbsent(right)) return presence(left, right)\n\n // NaN never reaches here: `isAbsent` counts it as absent, so `presence`\n // settles it above, outside the sign.\n if (typeof left === 'number' && typeof right === 'number') return left - right\n if (typeof left === 'boolean' && typeof right === 'boolean') {\n return Number(left) - Number(right)\n }\n return collator.compare(String(left), String(right))\n}\n\n/** What clicking a column heading does.\n *\n * Three states rather than two, and the third is the reason this is a function\n * and not `direction === 'asc' ? 'desc' : 'asc'`: clicking a *different*\n * column starts it ascending rather than inheriting the direction of the one\n * before. Inheriting is what a two-state toggle does, and it means the first\n * click on a new column can hand back an order nobody asked for. */\nexport function toggleSort<Column extends string>(\n sort: Sort<Column>,\n column: Column,\n): Sort<Column> {\n if (sort.column !== column) return { column, direction: 'asc' }\n return { column, direction: sort.direction === 'asc' ? 'desc' : 'asc' }\n}\n\n/** What a column heading announces: `ascending`, `descending`, or nothing.\n *\n * The value belongs in `aria-sort` on the `<th>`, and only on the one that is\n * sorted - `aria-sort=\"none\"` on every other heading is allowed by the spec\n * and read out by some screen readers on every cell, which turns a table into\n * a recital. `undefined` removes the attribute. */\nexport function ariaSort(\n sort: Sort<string> | undefined,\n column: string,\n): 'ascending' | 'descending' | undefined {\n if (!sort || sort.column !== column) return undefined\n return sort.direction === 'asc' ? 'ascending' : 'descending'\n}\n"
2155
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\n * How a column is ordered, with no React in it.\n *\n * Split out of the Table for the reason `calendar-math` was split out of the\n * Calendar: these are the sums, and the component is the thing that draws\n * them. A product sorting its own rows - on the server, in a worker, before\n * the data ever reaches a component - imports this and nothing else.\n *\n * No table library, deliberately. The obvious choice here is TanStack Table,\n * and it would be the first dependency a product has to install beyond Base\n * UI. What it offers is a model of columns, pages and sorting state; what the\n * line's one real table needed was the rule below, which the model does not\n * have. So the model is the part that is written here, and it is small.\n *\n * The rule, and the reason this file exists at all:\n *\n * **Absence sorts last, whichever way the column points.**\n *\n * A row with no value in this column is not the smallest - it is unknown, and\n * the two are different facts. Rank absence with the rest and flip the sign,\n * and a descending sort floats every empty row to the top: the reader asks for\n * \"highest first\" and is handed the rows that have no value at all. This is\n * measured rather than assumed - it is what the first version of the line's\n * catalogue did, and the fix is the shape below, where presence is settled\n * before the direction is applied.\n *\n * It is the same fact `RatingScale` is built on: not judged yet is a state,\n * not a zero. A table that sorts them together loses it on the first click.\n */\n\n/** Which way a column points. */\nexport type SortDirection = 'asc' | 'desc'\n\n/** A column, and which way it points. `column` is the caller's own key - the\n * id it gave the column, not an index, so reordering columns cannot silently\n * change what is sorted. */\nexport interface Sort<Column extends string = string> {\n column: Column\n direction: SortDirection\n}\n\n/** What can be compared. `null` and `undefined` both mean absent - a product\n * gets whichever its data uses, and having to normalise them before sorting\n * is the kind of step that gets forgotten in one branch of a switch. */\nexport type SortValue = string | number | boolean | null | undefined\n\n/** Reads the value of a column out of a row.\n *\n * Given rather than inferred: a column id is not always a field name, and a\n * sortable column is often a computed one - a duration that is stored as two\n * timestamps, a name that sorts by surname. */\nexport type SortAccessor<Row, Column extends string = string> = (\n row: Row,\n column: Column,\n) => SortValue\n\nexport interface SortOptions<Row> {\n /** The tiebreaker: what decides the order of rows that compare equal.\n *\n * Without one, `Array.prototype.sort` is stable and therefore leaves ties in\n * input order - which sounds fine until the input is re-fetched and arrives\n * in a different order, and a column of ties reshuffles under the reader\n * with no click. Pass the row's id. */\n tiebreak?: (row: Row) => SortValue\n /** The language text is compared in - the application's, from\n * `useLocale()`. Passed to `Intl.Collator`, so `ä` sorts where the reader\n * expects rather than after `z`. Required: left out, the collator used the\n * browser's language, which is not the one the table is written in. */\n locale: string\n}\n\n/** Order the rows. Returns a new array; the input is not touched, because a\n * component that sorts its own prop in place mutates the caller's state. */\nexport function sortRows<Row, Column extends string = string>(\n rows: readonly Row[],\n sort: Sort<Column>,\n accessor: SortAccessor<Row, Column>,\n options: SortOptions<Row>,\n): Row[] {\n const sign = sort.direction === 'asc' ? 1 : -1\n // One collator for the whole sort rather than one `localeCompare` per\n // comparison: building it is the expensive half, and a sort of n rows asks\n // for it n log n times.\n const collator = new Intl.Collator(options.locale)\n\n return [...rows].sort((a, b) => {\n const left = accessor(a, sort.column)\n const right = accessor(b, sort.column)\n\n // Presence first, and outside the sign. This is the whole point of the\n // file: absence is last in both directions.\n const absent = presence(left, right)\n if (absent !== 0) return absent\n\n const ranked = compareValues(left, right, collator) * sign\n if (ranked !== 0) return ranked\n\n if (options.tiebreak) {\n const tied = compareValues(options.tiebreak(a), options.tiebreak(b), collator)\n // Deliberately not multiplied by the sign. The tiebreaker is there to\n // make the order stable, and an order that reverses with the column is\n // not stable - the rows that tie would swap places on every click.\n if (tied !== 0) return tied\n }\n return 0\n })\n}\n\n/** Whether a value counts as absent.\n *\n * Both spellings of nothing, and neither `0` nor `''` nor `false`, which are\n * values a row genuinely has.\n *\n * `NaN` counts too, and that is not tidiness. Subtraction with it returns\n * `NaN`, which `sort` reads as \"these two are equal\" - so a row whose number\n * is not a number takes whatever place the input happened to give it, and the\n * order changes when the data is re-fetched. A number that is not a number is\n * not a small number; it is unknown, which is what this file already has a\n * place for. */\nexport function isAbsent(value: SortValue): boolean {\n if (value === null || value === undefined) return true\n return typeof value === 'number' && Number.isNaN(value)\n}\n\n/** Which of the two lacks a value; absent sorts last, always. */\nfunction presence(left: SortValue, right: SortValue): number {\n const leftHas = !isAbsent(left)\n const rightHas = !isAbsent(right)\n if (leftHas === rightHas) return 0\n return leftHas ? -1 : 1\n}\n\n/** Compare two present values of the same column.\n *\n * Numbers by subtraction, text by collator, and booleans as false-then-true.\n * A column whose values are of mixed type compares as text: that is a data\n * problem the table cannot fix, and ordering by `String` at least gives the\n * same answer twice. */\nfunction compareValues(left: SortValue, right: SortValue, collator: Intl.Collator): number {\n if (isAbsent(left) || isAbsent(right)) return presence(left, right)\n\n // NaN never reaches here: `isAbsent` counts it as absent, so `presence`\n // settles it above, outside the sign.\n if (typeof left === 'number' && typeof right === 'number') return left - right\n if (typeof left === 'boolean' && typeof right === 'boolean') {\n return Number(left) - Number(right)\n }\n return collator.compare(String(left), String(right))\n}\n\n/** What clicking a column heading does.\n *\n * Three states rather than two, and the third is the reason this is a function\n * and not `direction === 'asc' ? 'desc' : 'asc'`: clicking a *different*\n * column starts it ascending rather than inheriting the direction of the one\n * before. Inheriting is what a two-state toggle does, and it means the first\n * click on a new column can hand back an order nobody asked for. */\nexport function toggleSort<Column extends string>(\n sort: Sort<Column>,\n column: Column,\n): Sort<Column> {\n if (sort.column !== column) return { column, direction: 'asc' }\n return { column, direction: sort.direction === 'asc' ? 'desc' : 'asc' }\n}\n\n/** What a column heading announces: `ascending`, `descending`, or nothing.\n *\n * The value belongs in `aria-sort` on the `<th>`, and only on the one that is\n * sorted - `aria-sort=\"none\"` on every other heading is allowed by the spec\n * and read out by some screen readers on every cell, which turns a table into\n * a recital. `undefined` removes the attribute. */\nexport function ariaSort(\n sort: Sort<string> | undefined,\n column: string,\n): 'ascending' | 'descending' | undefined {\n if (!sort || sort.column !== column) return undefined\n return sort.direction === 'asc' ? 'ascending' : 'descending'\n}\n"
2156
2156
  }
2157
2157
  ]
2158
2158
  },
@@ -2163,7 +2163,7 @@
2163
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.",
2164
2164
  "dependencies": [
2165
2165
  "class-variance-authority",
2166
- "dowel-ui@^0.34.0"
2166
+ "dowel-ui@^0.34.1"
2167
2167
  ],
2168
2168
  "registryDependencies": [
2169
2169
  "https://lacodda.github.io/dowel/r/table-sort.json"
@@ -2185,7 +2185,7 @@
2185
2185
  "dependencies": [
2186
2186
  "@base-ui/react",
2187
2187
  "class-variance-authority",
2188
- "dowel-ui@^0.34.0"
2188
+ "dowel-ui@^0.34.1"
2189
2189
  ],
2190
2190
  "registryDependencies": [],
2191
2191
  "files": [
@@ -2204,7 +2204,7 @@
2204
2204
  "description": "Free text turned into a list: type a word, press Enter, it becomes a chip.",
2205
2205
  "dependencies": [
2206
2206
  "class-variance-authority",
2207
- "dowel-ui@^0.34.0"
2207
+ "dowel-ui@^0.34.1"
2208
2208
  ],
2209
2209
  "registryDependencies": [
2210
2210
  "https://lacodda.github.io/dowel/r/chip.json",
@@ -2225,7 +2225,7 @@
2225
2225
  "title": "Textarea",
2226
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.",
2227
2227
  "dependencies": [
2228
- "dowel-ui@^0.34.0"
2228
+ "dowel-ui@^0.34.1"
2229
2229
  ],
2230
2230
  "registryDependencies": [
2231
2231
  "https://lacodda.github.io/dowel/r/input.json"
@@ -2246,7 +2246,7 @@
2246
2246
  "description": "The tier primitive.",
2247
2247
  "dependencies": [
2248
2248
  "class-variance-authority",
2249
- "dowel-ui@^0.34.0"
2249
+ "dowel-ui@^0.34.1"
2250
2250
  ],
2251
2251
  "registryDependencies": [],
2252
2252
  "files": [
@@ -2264,7 +2264,7 @@
2264
2264
  "title": "TimeField",
2265
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`.",
2266
2266
  "dependencies": [
2267
- "dowel-ui@^0.34.0"
2267
+ "dowel-ui@^0.34.1"
2268
2268
  ],
2269
2269
  "registryDependencies": [
2270
2270
  "https://lacodda.github.io/dowel/r/input.json"
@@ -2285,7 +2285,7 @@
2285
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.",
2286
2286
  "dependencies": [
2287
2287
  "class-variance-authority",
2288
- "dowel-ui@^0.34.0"
2288
+ "dowel-ui@^0.34.1"
2289
2289
  ],
2290
2290
  "registryDependencies": [],
2291
2291
  "files": [
@@ -2305,7 +2305,7 @@
2305
2305
  "dependencies": [
2306
2306
  "@base-ui/react",
2307
2307
  "class-variance-authority",
2308
- "dowel-ui@^0.34.0"
2308
+ "dowel-ui@^0.34.1"
2309
2309
  ],
2310
2310
  "registryDependencies": [],
2311
2311
  "files": [
@@ -2325,7 +2325,7 @@
2325
2325
  "dependencies": [
2326
2326
  "@base-ui/react",
2327
2327
  "class-variance-authority",
2328
- "dowel-ui@^0.34.0"
2328
+ "dowel-ui@^0.34.1"
2329
2329
  ],
2330
2330
  "registryDependencies": [
2331
2331
  "https://lacodda.github.io/dowel/r/layer.json"
@@ -2351,7 +2351,7 @@
2351
2351
  "path": "ui/track-segments.tsx",
2352
2352
  "target": "@ui/track-segments.tsx",
2353
2353
  "type": "registry:ui",
2354
- "content": "/*\n * The arithmetic behind Track, with no React in it.\n *\n * Split out for the reason `table-sort` and `tree-rows` are: a product that\n * needs the numbers - to label a segment, to test its own domain code, to draw\n * the same shape somewhere that is not the DOM - should not import a component\n * to get them.\n *\n * What lives here is only the geometry. Turning a day of work into segments,\n * or a set of tiers into them, is the product's own arithmetic and stays with\n * the product: this knows about spans and percentages and nothing else.\n */\n\n/** A segment as the caller states it, in whatever units the caller counts in. */\nexport interface SegmentInput {\n /** Where it starts. Same units as `end` and as the track's span. */\n start: number\n /** Where it ends. A segment ending before it starts is empty, not backwards. */\n end: number\n}\n\n/** A segment as it is drawn: percentages of the track. */\nexport interface Placed {\n /** Distance from the left edge, 0-100. */\n left: number\n /** Width, 0-100. */\n width: number\n /** Whether the width is the real one, or the floor standing in for it.\n *\n * Worth knowing rather than hiding: a caller labelling segments may want to\n * say \"under a minute\" instead of a duration the bar is no longer drawing to\n * scale. */\n widened: boolean\n}\n\n/** The smallest a segment may be drawn, in percent.\n *\n * A ten-second pause in an eight-hour day is 0.03% of the track, which rounds\n * to no pixels at all: the segment is real, was measured, and would simply not\n * be there. The floor is the width at which a sliver is still visible on a\n * track a few hundred pixels wide. */\nexport const MIN_SEGMENT_WIDTH = 0.6\n\nexport interface PlaceOptions {\n /** The value the left edge stands for. Defaults to the first segment's start. */\n from?: number\n /** The value the right edge stands for. Defaults to the last segment's end. */\n to?: number\n /** Override the floor - `0` to draw every segment exactly to scale. */\n minWidth?: number\n}\n\n/**\n * Lay segments out along a track, as percentages.\n *\n * The scale is stated by `from` and `to` rather than inferred, because the two\n * readings differ and both are wanted: a day of work is read against itself\n * (an eight-hour day drawn across a third of the width wastes the space where\n * the breaks are), while a set of tiers is read against the whole 0-100 scale,\n * where the distance to the next tier is the distance you have to close.\n *\n * Widening a sliver to the floor is what makes this worth having once. It also\n * introduces the only subtlety here: a widened segment can run into the one\n * after it, and two segments drawn overlapping is a worse lie than a segment\n * drawn slightly too wide. So the pass is done in order, and each segment is\n * held back to where the next one starts - a floor is a request, not a\n * guarantee, and the last one may be trimmed by the end of the track.\n */\nexport function place(segments: SegmentInput[], options: PlaceOptions = {}): Placed[] {\n if (segments.length === 0) return []\n\n const from = options.from ?? Math.min(...segments.map((s) => s.start))\n const to = options.to ?? Math.max(...segments.map((s) => s.end))\n const span = to - from\n\n // A track with no span has nothing to scale against: every segment would be\n // at the same place with the same width, which is not a drawing of anything.\n if (!(span > 0)) return []\n\n const floor = options.minWidth ?? MIN_SEGMENT_WIDTH\n\n const raw = segments.map((segment) => {\n const start = Math.min(Math.max(segment.start, from), to)\n const end = Math.min(Math.max(segment.end, start), to)\n return { left: ((start - from) / span) * 100, width: ((end - start) / span) * 100 }\n })\n\n if (floor <= 0) return raw.map((segment) => ({ ...segment, widened: false }))\n\n /*\n * Widening is a layout pass, not a per-segment decision, and the first\n * version of this got it wrong in a way worth recording: it capped each\n * sliver at \"where the next segment starts\", which protects against overlap\n * and also means a segment that touches its neighbour can never grow at all.\n * Both shapes this component exists for are contiguous - work then break\n * then work, one tier after another - so the floor did nothing for either of\n * them, and the tests missed it because their crowded case had a gap.\n *\n * So a widened segment pushes what follows instead. That borrows room the\n * track does not have, and the debt is paid back at the end by the segments\n * wide enough to afford it - never by another sliver, which would undo the\n * widening we just did.\n */\n const widened: Placed[] = []\n let shift = 0\n\n for (const segment of raw) {\n const left = segment.left + shift\n if (segment.width >= floor) {\n widened.push({ left, width: segment.width, widened: false })\n continue\n }\n shift += floor - segment.width\n widened.push({ left, width: floor, widened: true })\n }\n\n if (shift === 0) return widened\n\n /* The debt is only owed if the track has actually overflowed. A sliver on an\n * otherwise empty track - one segment, or several with gaps between them -\n * grows into room nobody was using, and nothing needs to give way. */\n const end = widened.at(-1)!\n const overflow = end.left + end.width - 100\n if (overflow <= 0) return widened\n\n /* Who can pay: everything above the floor, by however much it has to spare.\n * Taken in proportion, so one long stretch is not singled out to absorb the\n * whole debt while its neighbour keeps its exact width. */\n const spare = widened.reduce((sum, s) => sum + (s.widened ? 0 : Math.max(0, s.width - floor)), 0)\n\n /* Nobody can pay: every segment is at or under the floor, and the track is\n * genuinely too crowded to draw honestly at this width. The floor loses -\n * a bar running off its own end is worse than slivers too thin to see. */\n if (spare <= 0) return raw.map((segment) => ({ ...segment, widened: false }))\n\n let paid = 0\n return widened.map((segment) => {\n const left = segment.left - paid\n if (segment.widened) return { ...segment, left }\n\n const contribution = (Math.max(0, segment.width - floor) / spare) * overflow\n paid += contribution\n return { left, width: segment.width - contribution, widened: false }\n })\n}\n\n/** Where a marker sits on the same scale, as a percentage, or `null` when it\n * falls outside the track.\n *\n * Outside rather than clamped: a marker pinned to the edge says \"here, at the\n * very end\", which is a different statement from \"not on this track at all\". */\nexport function markerAt(value: number, from: number, to: number): number | null {\n const span = to - from\n if (!(span > 0)) return null\n if (value < from || value > to) return null\n return ((value - from) / span) * 100\n}\n"
2354
+ "content": "export {}\n// Code first, so the description below survives `shadcn add`: the CLI writes\n// a file from its first token on and drops every comment above it.\n\n/*\n * The arithmetic behind Track, with no React in it.\n *\n * Split out for the reason `table-sort` and `tree-rows` are: a product that\n * needs the numbers - to label a segment, to test its own domain code, to draw\n * the same shape somewhere that is not the DOM - should not import a component\n * to get them.\n *\n * What lives here is only the geometry. Turning a day of work into segments,\n * or a set of tiers into them, is the product's own arithmetic and stays with\n * the product: this knows about spans and percentages and nothing else.\n */\n\n/** A segment as the caller states it, in whatever units the caller counts in. */\nexport interface SegmentInput {\n /** Where it starts. Same units as `end` and as the track's span. */\n start: number\n /** Where it ends. A segment ending before it starts is empty, not backwards. */\n end: number\n}\n\n/** A segment as it is drawn: percentages of the track. */\nexport interface Placed {\n /** Distance from the left edge, 0-100. */\n left: number\n /** Width, 0-100. */\n width: number\n /** Whether the width is the real one, or the floor standing in for it.\n *\n * Worth knowing rather than hiding: a caller labelling segments may want to\n * say \"under a minute\" instead of a duration the bar is no longer drawing to\n * scale. */\n widened: boolean\n}\n\n/** The smallest a segment may be drawn, in percent.\n *\n * A ten-second pause in an eight-hour day is 0.03% of the track, which rounds\n * to no pixels at all: the segment is real, was measured, and would simply not\n * be there. The floor is the width at which a sliver is still visible on a\n * track a few hundred pixels wide. */\nexport const MIN_SEGMENT_WIDTH = 0.6\n\nexport interface PlaceOptions {\n /** The value the left edge stands for. Defaults to the first segment's start. */\n from?: number\n /** The value the right edge stands for. Defaults to the last segment's end. */\n to?: number\n /** Override the floor - `0` to draw every segment exactly to scale. */\n minWidth?: number\n}\n\n/**\n * Lay segments out along a track, as percentages.\n *\n * The scale is stated by `from` and `to` rather than inferred, because the two\n * readings differ and both are wanted: a day of work is read against itself\n * (an eight-hour day drawn across a third of the width wastes the space where\n * the breaks are), while a set of tiers is read against the whole 0-100 scale,\n * where the distance to the next tier is the distance you have to close.\n *\n * Widening a sliver to the floor is what makes this worth having once. It also\n * introduces the only subtlety here: a widened segment can run into the one\n * after it, and two segments drawn overlapping is a worse lie than a segment\n * drawn slightly too wide. So the pass is done in order, and each segment is\n * held back to where the next one starts - a floor is a request, not a\n * guarantee, and the last one may be trimmed by the end of the track.\n */\nexport function place(segments: SegmentInput[], options: PlaceOptions = {}): Placed[] {\n if (segments.length === 0) return []\n\n const from = options.from ?? Math.min(...segments.map((s) => s.start))\n const to = options.to ?? Math.max(...segments.map((s) => s.end))\n const span = to - from\n\n // A track with no span has nothing to scale against: every segment would be\n // at the same place with the same width, which is not a drawing of anything.\n if (!(span > 0)) return []\n\n const floor = options.minWidth ?? MIN_SEGMENT_WIDTH\n\n const raw = segments.map((segment) => {\n const start = Math.min(Math.max(segment.start, from), to)\n const end = Math.min(Math.max(segment.end, start), to)\n return { left: ((start - from) / span) * 100, width: ((end - start) / span) * 100 }\n })\n\n if (floor <= 0) return raw.map((segment) => ({ ...segment, widened: false }))\n\n /*\n * Widening is a layout pass, not a per-segment decision, and the first\n * version of this got it wrong in a way worth recording: it capped each\n * sliver at \"where the next segment starts\", which protects against overlap\n * and also means a segment that touches its neighbour can never grow at all.\n * Both shapes this component exists for are contiguous - work then break\n * then work, one tier after another - so the floor did nothing for either of\n * them, and the tests missed it because their crowded case had a gap.\n *\n * So a widened segment pushes what follows instead. That borrows room the\n * track does not have, and the debt is paid back at the end by the segments\n * wide enough to afford it - never by another sliver, which would undo the\n * widening we just did.\n */\n const widened: Placed[] = []\n let shift = 0\n\n for (const segment of raw) {\n const left = segment.left + shift\n if (segment.width >= floor) {\n widened.push({ left, width: segment.width, widened: false })\n continue\n }\n shift += floor - segment.width\n widened.push({ left, width: floor, widened: true })\n }\n\n if (shift === 0) return widened\n\n /* The debt is only owed if the track has actually overflowed. A sliver on an\n * otherwise empty track - one segment, or several with gaps between them -\n * grows into room nobody was using, and nothing needs to give way. */\n const end = widened.at(-1)!\n const overflow = end.left + end.width - 100\n if (overflow <= 0) return widened\n\n /* Who can pay: everything above the floor, by however much it has to spare.\n * Taken in proportion, so one long stretch is not singled out to absorb the\n * whole debt while its neighbour keeps its exact width. */\n const spare = widened.reduce((sum, s) => sum + (s.widened ? 0 : Math.max(0, s.width - floor)), 0)\n\n /* Nobody can pay: every segment is at or under the floor, and the track is\n * genuinely too crowded to draw honestly at this width. The floor loses -\n * a bar running off its own end is worse than slivers too thin to see. */\n if (spare <= 0) return raw.map((segment) => ({ ...segment, widened: false }))\n\n let paid = 0\n return widened.map((segment) => {\n const left = segment.left - paid\n if (segment.widened) return { ...segment, left }\n\n const contribution = (Math.max(0, segment.width - floor) / spare) * overflow\n paid += contribution\n return { left, width: segment.width - contribution, widened: false }\n })\n}\n\n/** Where a marker sits on the same scale, as a percentage, or `null` when it\n * falls outside the track.\n *\n * Outside rather than clamped: a marker pinned to the edge says \"here, at the\n * very end\", which is a different statement from \"not on this track at all\". */\nexport function markerAt(value: number, from: number, to: number): number | null {\n const span = to - from\n if (!(span > 0)) return null\n if (value < from || value > to) return null\n return ((value - from) / span) * 100\n}\n"
2355
2355
  }
2356
2356
  ]
2357
2357
  },
@@ -2362,7 +2362,7 @@
2362
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.",
2363
2363
  "dependencies": [
2364
2364
  "class-variance-authority",
2365
- "dowel-ui@^0.34.0"
2365
+ "dowel-ui@^0.34.1"
2366
2366
  ],
2367
2367
  "registryDependencies": [
2368
2368
  "https://lacodda.github.io/dowel/r/track-segments.json"
@@ -2398,7 +2398,7 @@
2398
2398
  "title": "TreeView",
2399
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.",
2400
2400
  "dependencies": [
2401
- "dowel-ui@^0.34.0"
2401
+ "dowel-ui@^0.34.1"
2402
2402
  ],
2403
2403
  "registryDependencies": [
2404
2404
  "https://lacodda.github.io/dowel/r/tree-rows.json"
@@ -2418,7 +2418,7 @@
2418
2418
  "title": "Truncate",
2419
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.",
2420
2420
  "dependencies": [
2421
- "dowel-ui@^0.34.0"
2421
+ "dowel-ui@^0.34.1"
2422
2422
  ],
2423
2423
  "registryDependencies": [],
2424
2424
  "files": [
@@ -2436,7 +2436,7 @@
2436
2436
  "title": "VirtualList",
2437
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.",
2438
2438
  "dependencies": [
2439
- "dowel-ui@^0.34.0"
2439
+ "dowel-ui@^0.34.1"
2440
2440
  ],
2441
2441
  "registryDependencies": [],
2442
2442
  "files": [
@@ -2455,7 +2455,7 @@
2455
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.",
2456
2456
  "dependencies": [
2457
2457
  "@tauri-apps/api",
2458
- "dowel-ui@^0.34.0"
2458
+ "dowel-ui@^0.34.1"
2459
2459
  ],
2460
2460
  "registryDependencies": [],
2461
2461
  "files": [
@@ -2473,7 +2473,7 @@
2473
2473
  "title": "Wizard",
2474
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.",
2475
2475
  "dependencies": [
2476
- "dowel-ui@^0.34.0"
2476
+ "dowel-ui@^0.34.1"
2477
2477
  ],
2478
2478
  "registryDependencies": [
2479
2479
  "https://lacodda.github.io/dowel/r/button.json",