@cueplusplus/ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/chrome/_drag.js +59 -0
  3. package/dist/chrome/_status-bar-item.js +85 -0
  4. package/dist/chrome/_status-bar.d.ts +15 -0
  5. package/dist/chrome/_status-bar.js +171 -0
  6. package/dist/chrome/app-shell.d.ts +69 -1
  7. package/dist/chrome/app-shell.js +38 -11
  8. package/dist/chrome/index.d.ts +3 -2
  9. package/dist/chrome/status-bar.d.ts +87 -4
  10. package/dist/chrome/status-bar.js +80 -16
  11. package/dist/chrome/title-bar.d.ts +11 -1
  12. package/dist/chrome/title-bar.js +1 -3
  13. package/dist/date/_segments.js +15 -4
  14. package/dist/forms/_chassis.d.ts +104 -4
  15. package/dist/forms/_chassis.js +104 -4
  16. package/dist/forms/input-group.d.ts +8 -3
  17. package/dist/forms/input-group.js +8 -3
  18. package/dist/forms/number-field.d.ts +3 -2
  19. package/dist/forms/number-field.js +3 -2
  20. package/dist/index.d.ts +7 -3
  21. package/dist/index.js +4 -1
  22. package/dist/instruments/_data-row.js +118 -0
  23. package/dist/instruments/data-row.d.ts +94 -0
  24. package/dist/instruments/data-row.js +151 -0
  25. package/dist/instruments/data-tree.d.ts +126 -0
  26. package/dist/instruments/data-tree.js +303 -0
  27. package/dist/instruments/index.d.ts +5 -2
  28. package/dist/instruments/index.js +4 -1
  29. package/dist/instruments/ledger.d.ts +105 -0
  30. package/dist/instruments/ledger.js +114 -0
  31. package/dist/instruments/table.d.ts +70 -2
  32. package/dist/instruments/table.js +133 -39
  33. package/dist/theming/_presets.js +3 -3
  34. package/manifest/components/app-shell.json +45 -4
  35. package/manifest/components/autocomplete.json +2 -0
  36. package/manifest/components/color-field.json +2 -0
  37. package/manifest/components/color-picker.json +2 -0
  38. package/manifest/components/combobox.json +2 -0
  39. package/manifest/components/composer.json +2 -0
  40. package/manifest/components/data-row.json +203 -0
  41. package/manifest/components/data-tree.json +151 -0
  42. package/manifest/components/date-field.json +2 -0
  43. package/manifest/components/date-picker.json +2 -0
  44. package/manifest/components/date-range-picker.json +2 -0
  45. package/manifest/components/env-var-input.json +2 -0
  46. package/manifest/components/input-group.json +4 -2
  47. package/manifest/components/input.json +3 -1
  48. package/manifest/components/ledger.json +187 -0
  49. package/manifest/components/multi-select.json +2 -0
  50. package/manifest/components/musical-time-input.json +2 -0
  51. package/manifest/components/number-field.json +3 -1
  52. package/manifest/components/otp-field.json +2 -0
  53. package/manifest/components/password-input.json +2 -0
  54. package/manifest/components/scrub-input.json +2 -0
  55. package/manifest/components/search-input.json +2 -0
  56. package/manifest/components/select.json +1 -0
  57. package/manifest/components/status-bar.json +146 -12
  58. package/manifest/components/table-scroll-region.json +6 -0
  59. package/manifest/components/table.json +88 -4
  60. package/manifest/components/tags-input.json +2 -0
  61. package/manifest/components/textarea.json +3 -1
  62. package/manifest/components/time-field.json +2 -0
  63. package/manifest/components/title-bar.json +1 -1
  64. package/manifest/components/toggle-group.json +2 -0
  65. package/manifest/components/toggle.json +2 -0
  66. package/manifest/manifest.json +72 -33
  67. package/manifest/tokens.json +1 -1
  68. package/package.json +4 -4
@@ -0,0 +1,303 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { flattenTree } from "../chrome/_tree.js";
4
+ import { ChevronRightGlyph } from "../chrome/_glyphs.js";
5
+ import { activateRowFromClick, dataRowVariants } from "./_data-row.js";
6
+ import * as React from "react";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ //#region src/instruments/data-tree.tsx
9
+ /**
10
+ * The hierarchy guides, drawn as a background rather than as characters or
11
+ * borders.
12
+ *
13
+ * One hairline per ancestor level, from a `repeating-linear-gradient` whose
14
+ * period is the indent step and whose painted width is `depth × step` — so the
15
+ * rules stop exactly where the row's own content begins. Three properties of
16
+ * this mechanism are the reason it is not box-drawing glyphs and not a stack of
17
+ * `border-left` spacers:
18
+ *
19
+ * - It is a `background-image`, so the row's hover and selection
20
+ * `background-color` cannot cover it, move it or switch it off. A guide drawn
21
+ * with borders on spacer elements has to be re-tinted for every row state.
22
+ * - It costs no DOM. A tree of two thousand visible rows adds no elements per
23
+ * level, which is what makes a virtualized variant of this row viable later.
24
+ * - It is a picture of structure, not a picture of a tree: `├` and `└` in a mono
25
+ * grid are characters a screen reader may read out and a proportional font
26
+ * destroys.
27
+ *
28
+ * The step is `--cue-data-tree-indent` (default `1.25rem`), a local custom
29
+ * property so a `@container` rule can tighten the tree at the same breakpoint it
30
+ * sheds a column — and switch the guides off entirely at the width where an
31
+ * indent is more expensive than it is informative.
32
+ */
33
+ const DATA_TREE_GUIDES = [
34
+ "bg-no-repeat",
35
+ "[background-image:repeating-linear-gradient(to_right,var(--cue-hair)_0,var(--cue-hair)_1px,transparent_1px,transparent_var(--cue-data-tree-indent,1.25rem))]",
36
+ "[background-size:calc(var(--depth)_*_var(--cue-data-tree-indent,1.25rem))_100%]",
37
+ "[background-position:var(--cue-pad-row-x)_0]"
38
+ ].join(" ");
39
+ /**
40
+ * `DataTreeItem` as the shape `flattenTree` walks, plus a way back.
41
+ *
42
+ * `flattenTree` (`src/chrome/_tree.ts`) is the outline algorithm this library
43
+ * already has, and re-implementing "which rows are visible, at what level, in
44
+ * what position of what set" for a second tree is how two trees start
45
+ * disagreeing about what a collapse hides. It reads `id`, `children` and
46
+ * `disabled`; its `TreeItem` also *declares* a `label`, which a data row does
47
+ * not have — its content is columns. So the walk maps to that shape with a null
48
+ * label and hands back an index from id to the caller's own item, which is what
49
+ * the row renderer reads.
50
+ *
51
+ * @param items The roots, in display order.
52
+ * @returns The outline `flattenTree` accepts, and the items keyed by id.
53
+ */
54
+ function outlineOf(items) {
55
+ const byId = /* @__PURE__ */ new Map();
56
+ const walk = (nodes) => nodes.map((node) => {
57
+ byId.set(node.id, node);
58
+ const outlineNode = {
59
+ id: node.id,
60
+ label: null,
61
+ disabled: node.disabled
62
+ };
63
+ if (node.children !== void 0) outlineNode.children = walk(node.children);
64
+ return outlineNode;
65
+ });
66
+ return {
67
+ outline: walk(items),
68
+ byId
69
+ };
70
+ }
71
+ /**
72
+ * A hierarchy of multi-column rows: the tree idiom over the `DataRow` anatomy.
73
+ *
74
+ * ## The ARIA pattern, chosen before the component was written
75
+ *
76
+ * `role="treegrid"` is **rejected** for this foundation. It is the semantically
77
+ * exact answer — tabular *and* hierarchical — and it is the one with the worst
78
+ * real-world assistive-technology record: Adrian Roselli's survey of expando
79
+ * tables calls its support poor and notes that the APG's own worked example
80
+ * carries unresolved issues; fancytree's maintainer describes treegrid support
81
+ * as unproven ground; MUI X shipped hierarchical rows with no `aria-level` /
82
+ * `aria-expanded` / `aria-setsize` at all and retrofitted them later. Treegrid
83
+ * earns its cost when cells are individually interactive or editable, because
84
+ * then a reader genuinely needs two-dimensional cell navigation. This surface is
85
+ * read-and-navigate: you move down rows, fold a branch, and open the thing a row
86
+ * names.
87
+ *
88
+ * So the pattern is `role="tree"` with `role="treeitem"` rows — VS Code's
89
+ * choice and Carbon's, and the best-supported composite pattern in ARIA:
90
+ *
91
+ * - A **flat DOM** with `aria-level` / `aria-posinset` / `aria-setsize` per row
92
+ * rather than nested `role="group"` wrappers.
93
+ * - **Roving tabindex**: the tree is one Tab stop and the arrows move inside it.
94
+ * - The **APG tree keyboard map**: Up/Down between visible rows, Home/End to the
95
+ * ends, Right to unfold then to descend, Left to fold then to ascend,
96
+ * Enter/Space to select.
97
+ * - Expansion lives on the **row**, as `aria-expanded` on the `treeitem` — that
98
+ * is what tree semantics mean by a parent, and it is why the chevron here is
99
+ * `role="presentation"` rather than a button. (The other half of this trade
100
+ * is `Table.GroupRow`, where the table's semantics own the grouping and a real
101
+ * `<button aria-expanded>` owns the disclosure. Two patterns, each internally
102
+ * consistent; what would be wrong is half of each.)
103
+ *
104
+ * **The documented trade**: columns are not independently navigable. A row's
105
+ * multi-column content lives *inside* the treeitem, so a screen-reader user
106
+ * hears the row as one string rather than moving cell to cell through it. For a
107
+ * dense browse-and-open surface that is the better bargain — `tree` works today
108
+ * on every screen reader, `treegrid` does not — but a surface whose cells are
109
+ * genuinely editable has outgrown this component and wants a real treegrid from
110
+ * a library that maintains one.
111
+ *
112
+ * ## Everything else
113
+ *
114
+ * Rows are the `DataRow` chassis: same padding, same hairline, same hover wash,
115
+ * same inset focus ring, same `group/data-row`, so `DataRow.Actions` reveals in
116
+ * a tree exactly as it does in a ledger. The row element itself is the
117
+ * treeitem — one element owns focus, expansion, selection and the click reading
118
+ * — and the caller's `DataRow.*` slots are its content.
119
+ *
120
+ * A tree whose items declare no children anywhere is a flat list: no chevrons,
121
+ * no spacer column where a chevron would be, no guides, and `aria-expanded` on
122
+ * nothing. A leaf row that carried `aria-expanded="false"` would tell a screen
123
+ * reader it has children it does not have.
124
+ *
125
+ * @example
126
+ * <DataTree
127
+ * aria-label="Patch"
128
+ * items={nodes}
129
+ * defaultExpanded={["inputs"]}
130
+ * selected={selected}
131
+ * onSelect={setSelected}
132
+ * className="[--cue-data-row-cols:1fr_8rem_auto]"
133
+ * />
134
+ */
135
+ const DataTree = React.forwardRef(function DataTree({ className, items, expanded: expandedProp, defaultExpanded, onExpandedChange, selected: selectedProp, defaultSelected = null, onSelect, guides = true, onKeyDown, ...elementProps }, ref) {
136
+ const [uncontrolledExpanded, setUncontrolledExpanded] = React.useState(defaultExpanded ?? []);
137
+ const expandedList = expandedProp ?? uncontrolledExpanded;
138
+ const expandedSet = React.useMemo(() => new Set(expandedList), [expandedList]);
139
+ const [uncontrolledSelected, setUncontrolledSelected] = React.useState(defaultSelected);
140
+ const selected = selectedProp === void 0 ? uncontrolledSelected : selectedProp;
141
+ const { outline, byId } = React.useMemo(() => outlineOf(items), [items]);
142
+ const rows = React.useMemo(() => flattenTree(outline, expandedSet), [outline, expandedSet]);
143
+ const hierarchical = React.useMemo(() => {
144
+ for (const item of byId.values()) if (item.children !== void 0) return true;
145
+ return false;
146
+ }, [byId]);
147
+ const [activeId, setActiveId] = React.useState(null);
148
+ const activeRowId = activeId !== null && rows.some((row) => row.item.id === activeId) ? activeId : rows[0]?.item.id ?? null;
149
+ const rowRefs = React.useRef(/* @__PURE__ */ new Map());
150
+ const registerRow = React.useCallback((id, element) => {
151
+ if (element === null) rowRefs.current.delete(id);
152
+ else rowRefs.current.set(id, element);
153
+ }, []);
154
+ const focusRow = React.useCallback((id) => {
155
+ setActiveId(id);
156
+ rowRefs.current.get(id)?.focus();
157
+ }, []);
158
+ const setExpanded = React.useCallback((id, next) => {
159
+ const current = expandedProp ?? uncontrolledExpanded;
160
+ const result = next ? current.includes(id) ? [...current] : [...current, id] : current.filter((entry) => entry !== id);
161
+ if (expandedProp === void 0) setUncontrolledExpanded(result);
162
+ onExpandedChange?.(result);
163
+ }, [
164
+ expandedProp,
165
+ onExpandedChange,
166
+ uncontrolledExpanded
167
+ ]);
168
+ const select = React.useCallback((row) => {
169
+ if (byId.get(row.item.id)?.disabled === true) return;
170
+ if (selectedProp === void 0) setUncontrolledSelected(row.item.id);
171
+ onSelect?.(row.item.id);
172
+ }, [
173
+ byId,
174
+ onSelect,
175
+ selectedProp
176
+ ]);
177
+ /**
178
+ * The APG tree keyboard map.
179
+ *
180
+ * A near-copy of `Tree`'s handler in `src/chrome/tree.tsx`, and deliberately
181
+ * so: the two trees paint different rows (a label with a glyph gutter there,
182
+ * columns with a CSS indent here) and share `flattenTree`, which is the half
183
+ * that is actually hard. Extracting the twenty lines below into a third module
184
+ * would have to thread the row registry, the expansion setter and the
185
+ * selection through it, and the seam would be wider than the code. If a third
186
+ * tree ever appears, that is the moment to hoist it.
187
+ */
188
+ const handleKeyDown = React.useCallback((event) => {
189
+ onKeyDown?.(event);
190
+ if (event.defaultPrevented) return;
191
+ if (event.target.getAttribute("data-slot") !== "data-tree-row") return;
192
+ const index = rows.findIndex((row) => row.item.id === activeRowId);
193
+ const row = rows[index];
194
+ if (row === void 0) return;
195
+ const move = (target) => {
196
+ if (target === void 0) return;
197
+ event.preventDefault();
198
+ focusRow(target.item.id);
199
+ };
200
+ switch (event.key) {
201
+ case "ArrowDown":
202
+ move(rows[index + 1]);
203
+ return;
204
+ case "ArrowUp":
205
+ move(rows[index - 1]);
206
+ return;
207
+ case "Home":
208
+ move(rows[0]);
209
+ return;
210
+ case "End":
211
+ move(rows[rows.length - 1]);
212
+ return;
213
+ case "ArrowRight": {
214
+ event.preventDefault();
215
+ if (row.hasChildren && !row.expanded) {
216
+ setExpanded(row.item.id, true);
217
+ return;
218
+ }
219
+ const child = rows[index + 1];
220
+ if (row.expanded && child?.parentId === row.item.id) focusRow(child.item.id);
221
+ return;
222
+ }
223
+ case "ArrowLeft":
224
+ event.preventDefault();
225
+ if (row.expanded) setExpanded(row.item.id, false);
226
+ else if (row.parentId !== null) focusRow(row.parentId);
227
+ return;
228
+ case "Enter":
229
+ case " ":
230
+ event.preventDefault();
231
+ select(row);
232
+ return;
233
+ }
234
+ }, [
235
+ activeRowId,
236
+ focusRow,
237
+ onKeyDown,
238
+ rows,
239
+ select,
240
+ setExpanded
241
+ ]);
242
+ return /* @__PURE__ */ jsx("div", {
243
+ ref,
244
+ ...elementProps,
245
+ role: "tree",
246
+ "data-slot": "data-tree",
247
+ className: cn("flex flex-col font-mono text-(length:--cue-text-ui) text-fg", className),
248
+ onKeyDown: handleKeyDown,
249
+ children: rows.map((row) => {
250
+ const item = byId.get(row.item.id);
251
+ if (item === void 0) return null;
252
+ const isSelected = selected === item.id;
253
+ const state = {
254
+ level: row.level,
255
+ hasChildren: row.hasChildren,
256
+ expanded: row.expanded,
257
+ selected: isSelected
258
+ };
259
+ return /* @__PURE__ */ jsxs("div", {
260
+ ref: (element) => registerRow(item.id, element),
261
+ role: "treeitem",
262
+ tabIndex: activeRowId === item.id ? 0 : -1,
263
+ "aria-level": row.level + 1,
264
+ "aria-posinset": row.position,
265
+ "aria-setsize": row.setSize,
266
+ "aria-expanded": row.hasChildren ? row.expanded : void 0,
267
+ "aria-selected": isSelected,
268
+ "aria-disabled": item.disabled === true ? true : void 0,
269
+ "data-slot": "data-tree-row",
270
+ "data-selected": isSelected ? "" : void 0,
271
+ "data-disabled": item.disabled === true ? "" : void 0,
272
+ style: { "--depth": row.level },
273
+ onFocus: () => setActiveId(item.id),
274
+ onClick: (event) => activateRowFromClick(event, () => select(row)),
275
+ className: cn(dataRowVariants({
276
+ layout: "flex",
277
+ indent: true,
278
+ interactive: true
279
+ }), guides && row.level > 0 ? DATA_TREE_GUIDES : null),
280
+ children: [hierarchical ? row.hasChildren ? /* @__PURE__ */ jsx("span", {
281
+ role: "presentation",
282
+ "aria-hidden": "true",
283
+ "data-slot": "data-tree-toggle",
284
+ onClick: (event) => {
285
+ event.stopPropagation();
286
+ setExpanded(item.id, !row.expanded);
287
+ },
288
+ className: "flex size-icon-sm shrink-0 cursor-pointer items-center justify-center text-fg-subtle",
289
+ children: /* @__PURE__ */ jsx(ChevronRightGlyph, { className: cn("size-icon-sm transition-transform duration-150 motion-reduce:transition-none", row.expanded ? "rotate-90" : null) })
290
+ }) : /* @__PURE__ */ jsx("span", {
291
+ "aria-hidden": "true",
292
+ className: "size-icon-sm shrink-0"
293
+ }) : null, /* @__PURE__ */ jsx("span", {
294
+ "data-slot": "data-tree-row-content",
295
+ className: "grid min-w-0 flex-1 items-center gap-(--cue-space-3) [grid-template-columns:var(--cue-data-row-cols,1fr_auto)]",
296
+ children: typeof item.row === "function" ? item.row(state) : item.row
297
+ })]
298
+ }, item.id);
299
+ })
300
+ });
301
+ });
302
+ //#endregion
303
+ export { DataTree };
@@ -1,13 +1,16 @@
1
1
  import { AppWindowFrame, AppWindowFrameProps } from "./app-window-frame.js";
2
2
  import { CopyButton, CopyButtonProps } from "./copy-button.js";
3
+ import { DataRow, DataRowDisclosure, DataRowRootProps, DataRowSlotProps } from "./data-row.js";
4
+ import { DataTree, DataTreeItem, DataTreeProps, DataTreeRowState } from "./data-tree.js";
3
5
  import { GroupBar, GroupBarItem, GroupBarProps, GroupBarTone } from "./group-bar.js";
6
+ import { Ledger, LedgerEmptyProps, LedgerGroupProps, LedgerRootProps, LedgerTierProps } from "./ledger.js";
4
7
  import { Meter, MeterProps, MeterProvenance, MeterTone } from "./meter.js";
5
8
  import { Progress, ProgressProps } from "./progress.js";
6
9
  import { SeamCell, SeamCellProps, SeamGrid, SeamGridProps, SeamGround } from "./seam-grid.js";
7
10
  import { Sparkline, SparklineProps } from "./sparkline.js";
8
11
  import { Stat, StatProps, StatTone } from "./stat.js";
9
- import { Table, TableCaptionProps, TableCellProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps } from "./table.js";
12
+ import { Table, TableCaptionProps, TableCellProps, TableGroupRowProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps } from "./table.js";
10
13
  import { TerminalFrame, TerminalFrameLine, TerminalFrameProps, TerminalLine, TerminalLineTone, TerminalScript, TerminalStep } from "./terminal-frame.js";
11
14
  import { TwoStepButton, TwoStepButtonProps, TwoStepTone } from "./two-step-button.js";
12
15
  import { UsageChart, UsageChartPoint, UsageChartProps, UsageChartTone } from "./usage-chart.js";
13
- export { AppWindowFrame, type AppWindowFrameProps, CopyButton, type CopyButtonProps, GroupBar, type GroupBarItem, type GroupBarProps, type GroupBarTone, Meter, type MeterProps, type MeterProvenance, type MeterTone, Progress, type ProgressProps, SeamCell, type SeamCellProps, SeamGrid, type SeamGridProps, type SeamGround, Sparkline, type SparklineProps, Stat, type StatProps, type StatTone, Table, type TableCaptionProps, type TableCellProps, type TableHeadProps, type TableRootProps, type TableRowProps, TableScrollRegion, type TableScrollRegionProps, type TableSectionProps, TerminalFrame, type TerminalFrameLine, type TerminalFrameProps, type TerminalLine, type TerminalLineTone, type TerminalScript, type TerminalStep, TwoStepButton, type TwoStepButtonProps, type TwoStepTone, UsageChart, type UsageChartPoint, type UsageChartProps, type UsageChartTone };
16
+ export { AppWindowFrame, type AppWindowFrameProps, CopyButton, type CopyButtonProps, DataRow, type DataRowDisclosure, type DataRowRootProps, type DataRowSlotProps, DataTree, type DataTreeItem, type DataTreeProps, type DataTreeRowState, GroupBar, type GroupBarItem, type GroupBarProps, type GroupBarTone, Ledger, type LedgerEmptyProps, type LedgerGroupProps, type LedgerRootProps, type LedgerTierProps, Meter, type MeterProps, type MeterProvenance, type MeterTone, Progress, type ProgressProps, SeamCell, type SeamCellProps, SeamGrid, type SeamGridProps, type SeamGround, Sparkline, type SparklineProps, Stat, type StatProps, type StatTone, Table, type TableCaptionProps, type TableCellProps, type TableGroupRowProps, type TableHeadProps, type TableRootProps, type TableRowProps, TableScrollRegion, type TableScrollRegionProps, type TableSectionProps, TerminalFrame, type TerminalFrameLine, type TerminalFrameProps, type TerminalLine, type TerminalLineTone, type TerminalScript, type TerminalStep, TwoStepButton, type TwoStepButtonProps, type TwoStepTone, UsageChart, type UsageChartPoint, type UsageChartProps, type UsageChartTone };
@@ -1,6 +1,9 @@
1
1
  import { AppWindowFrame } from "./app-window-frame.js";
2
2
  import { CopyButton } from "./copy-button.js";
3
+ import { DataRow } from "./data-row.js";
4
+ import { DataTree } from "./data-tree.js";
3
5
  import { GroupBar } from "./group-bar.js";
6
+ import { Ledger } from "./ledger.js";
4
7
  import { Meter } from "./meter.js";
5
8
  import { Progress } from "./progress.js";
6
9
  import { SeamCell, SeamGrid } from "./seam-grid.js";
@@ -10,4 +13,4 @@ import { Table, TableScrollRegion } from "./table.js";
10
13
  import { TerminalFrame } from "./terminal-frame.js";
11
14
  import { TwoStepButton } from "./two-step-button.js";
12
15
  import { UsageChart } from "./usage-chart.js";
13
- export { AppWindowFrame, CopyButton, GroupBar, Meter, Progress, SeamCell, SeamGrid, Sparkline, Stat, Table, TableScrollRegion, TerminalFrame, TwoStepButton, UsageChart };
16
+ export { AppWindowFrame, CopyButton, DataRow, DataTree, GroupBar, Ledger, Meter, Progress, SeamCell, SeamGrid, Sparkline, Stat, Table, TableScrollRegion, TerminalFrame, TwoStepButton, UsageChart };
@@ -0,0 +1,105 @@
1
+ import * as React from "react";
2
+ //#region src/instruments/ledger.d.ts
3
+ /** The heading elements a tier or a group may be given. */
4
+ type LedgerHeadingLevel = 2 | 3 | 4;
5
+ interface LedgerRootProps extends React.ComponentPropsWithoutRef<"div"> {
6
+ /**
7
+ * Make the ledger a size container, so a caller's `@container` rules shed
8
+ * columns against the *pane* rather than the viewport. Defaults to `true`.
9
+ *
10
+ * This is the difference between a list that sheds a column when the window
11
+ * narrows and one that also sheds it when a detail drawer opens beside it and
12
+ * takes half the pane — which is the same visual problem and, without a
13
+ * container, an entirely separate piece of code. Switch it off only when an
14
+ * ancestor is already the container the rules name; nesting containers moves
15
+ * which one a bare `@container` query resolves against.
16
+ */
17
+ containment?: boolean;
18
+ }
19
+ interface LedgerTierProps extends React.ComponentPropsWithoutRef<"section"> {
20
+ /**
21
+ * The tier's name. Omitted, the tier draws no heading and reserves no space
22
+ * for one — which is how a single unnamed run of rows uses the same component
23
+ * as a grouped one.
24
+ */
25
+ heading?: React.ReactNode;
26
+ /** How many rows the tier holds, printed beside its name. */
27
+ count?: number;
28
+ /**
29
+ * Dock the heading to the top of the scroll container while the tier's rows
30
+ * scroll past. Defaults to `true`.
31
+ */
32
+ sticky?: boolean;
33
+ /**
34
+ * Which heading element the tier's name is. Defaults to `2`.
35
+ *
36
+ * The outline is the navigation: a screen-reader user moves through a long
37
+ * ledger by heading, so the levels have to describe the real nesting of the
38
+ * page the ledger sits in, not the ledger's own idea of itself.
39
+ */
40
+ headingLevel?: LedgerHeadingLevel;
41
+ /** A qualifier printed after the name — a date, a source, a state. */
42
+ note?: React.ReactNode;
43
+ }
44
+ interface LedgerGroupProps extends React.ComponentPropsWithoutRef<"div"> {
45
+ /**
46
+ * The group's slug, printed in the left gutter.
47
+ *
48
+ * Omitted, the group is a plain block: no gutter, no heading, no indent. A
49
+ * grouped list that has one unnamed run in it should not pay a 7rem empty
50
+ * column for it, and an empty gutter reads as a missing label rather than as
51
+ * an absent one.
52
+ */
53
+ label?: React.ReactNode;
54
+ /** Which heading element the slug is. Defaults to `3`. */
55
+ headingLevel?: 3 | 4;
56
+ /**
57
+ * Dock the slug under the tier heading while the group's rows scroll past.
58
+ * Defaults to `true`.
59
+ *
60
+ * The offset is a custom property, `--cue-ledger-slug-top` (default `2.5rem`):
61
+ * how far down the slug docks depends on how tall the tier heading above it
62
+ * renders, which is a function of the density and the type the *caller*
63
+ * configured. Set it on the ledger — `className="[--cue-ledger-slug-top:2rem]"` —
64
+ * rather than measuring at runtime.
65
+ */
66
+ sticky?: boolean;
67
+ }
68
+ interface LedgerEmptyProps extends React.ComponentPropsWithoutRef<"p"> {}
69
+ /**
70
+ * A grouped run of dense rows under sticky headings: the ledger idiom.
71
+ *
72
+ * The flattest of the three surfaces this package ships over one row model, and
73
+ * the one that does the least: headings, slugs, rows. **Nothing here collapses.**
74
+ * A ledger with a disclosure on it would be a tree with worse semantics, so the
75
+ * component emits no `aria-expanded` anywhere, and a test holds it to that —
76
+ * collapse belongs to `DataTree`, and grouping-with-columns belongs to a
77
+ * grouped `Table`.
78
+ *
79
+ * What it does own is the document outline. Tiers and groups are real `<h2>` /
80
+ * `<h3>` elements at caller-chosen levels, because in a list of two hundred
81
+ * rows the heading list *is* how a screen-reader user navigates.
82
+ *
83
+ * Static markup — no `"use client"`. The rows inside it may be anything;
84
+ * `DataRow` is the intended tenant.
85
+ *
86
+ * @example
87
+ * <Ledger.Root className="[--cue-data-row-cols:1fr_8rem_auto]">
88
+ * <Ledger.Tier heading="Ours" count={12}>
89
+ * <Ledger.Group label="released">
90
+ * <DataRow.Root interactive onActivate={open}>…</DataRow.Root>
91
+ * </Ledger.Group>
92
+ * <Ledger.Group label="lab">
93
+ * <Ledger.Empty>Nothing yet.</Ledger.Empty>
94
+ * </Ledger.Group>
95
+ * </Ledger.Tier>
96
+ * </Ledger.Root>
97
+ */
98
+ declare const Ledger: {
99
+ Root: React.ForwardRefExoticComponent<LedgerRootProps & React.RefAttributes<HTMLDivElement>>;
100
+ Tier: React.ForwardRefExoticComponent<LedgerTierProps & React.RefAttributes<HTMLElement>>;
101
+ Group: React.ForwardRefExoticComponent<LedgerGroupProps & React.RefAttributes<HTMLDivElement>>;
102
+ Empty: React.ForwardRefExoticComponent<LedgerEmptyProps & React.RefAttributes<HTMLParagraphElement>>;
103
+ };
104
+ //#endregion
105
+ export { Ledger, LedgerEmptyProps, LedgerGroupProps, LedgerRootProps, LedgerTierProps };
@@ -0,0 +1,114 @@
1
+ import { cn } from "../lib/cn.js";
2
+ import * as React from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/instruments/ledger.tsx
5
+ /**
6
+ * The one type treatment every heading in the ledger wears: the mono uppercase
7
+ * label of `SectionHeader`, `Eyebrow` and `Table.Head`.
8
+ *
9
+ * Declared once here rather than per heading level so a tier and a group slug
10
+ * can never drift into two label styles; `font-normal` is the part that has to
11
+ * be said out loud, because `<h2>` and `<h3>` come bold by default and a bold
12
+ * micro-label in a dense list reads as a second kind of emphasis.
13
+ */
14
+ const LEDGER_LABEL = "font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] text-fg-subtle uppercase";
15
+ /** `headingLevel` as a tag name, so the outline is real elements, not roles. */
16
+ const headingTag = (level) => level === 2 ? "h2" : level === 3 ? "h3" : "h4";
17
+ /**
18
+ * A grouped run of dense rows under sticky headings: the ledger idiom.
19
+ *
20
+ * The flattest of the three surfaces this package ships over one row model, and
21
+ * the one that does the least: headings, slugs, rows. **Nothing here collapses.**
22
+ * A ledger with a disclosure on it would be a tree with worse semantics, so the
23
+ * component emits no `aria-expanded` anywhere, and a test holds it to that —
24
+ * collapse belongs to `DataTree`, and grouping-with-columns belongs to a
25
+ * grouped `Table`.
26
+ *
27
+ * What it does own is the document outline. Tiers and groups are real `<h2>` /
28
+ * `<h3>` elements at caller-chosen levels, because in a list of two hundred
29
+ * rows the heading list *is* how a screen-reader user navigates.
30
+ *
31
+ * Static markup — no `"use client"`. The rows inside it may be anything;
32
+ * `DataRow` is the intended tenant.
33
+ *
34
+ * @example
35
+ * <Ledger.Root className="[--cue-data-row-cols:1fr_8rem_auto]">
36
+ * <Ledger.Tier heading="Ours" count={12}>
37
+ * <Ledger.Group label="released">
38
+ * <DataRow.Root interactive onActivate={open}>…</DataRow.Root>
39
+ * </Ledger.Group>
40
+ * <Ledger.Group label="lab">
41
+ * <Ledger.Empty>Nothing yet.</Ledger.Empty>
42
+ * </Ledger.Group>
43
+ * </Ledger.Tier>
44
+ * </Ledger.Root>
45
+ */
46
+ const Ledger = {
47
+ Root: React.forwardRef(function LedgerRoot({ className, containment = true, ...elementProps }, ref) {
48
+ return /* @__PURE__ */ jsx("div", {
49
+ ref,
50
+ "data-slot": "ledger",
51
+ className: cn("flex flex-col font-mono text-(length:--cue-text-ui) text-fg", containment ? "[container-type:inline-size]" : null, className),
52
+ ...elementProps
53
+ });
54
+ }),
55
+ Tier: React.forwardRef(function LedgerTier({ className, heading, count, sticky = true, headingLevel = 2, note, children, ...elementProps }, ref) {
56
+ const Heading = headingTag(headingLevel);
57
+ return /* @__PURE__ */ jsxs("section", {
58
+ ref,
59
+ "data-slot": "ledger-tier",
60
+ className: cn("flex flex-col", className),
61
+ ...elementProps,
62
+ children: [heading === void 0 ? null : /* @__PURE__ */ jsxs(Heading, {
63
+ "data-slot": "ledger-tier-heading",
64
+ className: cn(LEDGER_LABEL, "flex items-baseline gap-(--cue-space-2) py-(--cue-pad-row-y)", sticky ? "sticky top-0 z-2 bg-bg" : null),
65
+ children: [
66
+ /* @__PURE__ */ jsx("span", {
67
+ "data-slot": "ledger-tier-label",
68
+ children: heading
69
+ }),
70
+ count === void 0 ? null : /* @__PURE__ */ jsx("span", {
71
+ "data-slot": "ledger-tier-count",
72
+ className: "tabular-nums",
73
+ children: count
74
+ }),
75
+ note === void 0 ? null : /* @__PURE__ */ jsx("span", {
76
+ "data-slot": "ledger-tier-note",
77
+ className: "tracking-normal normal-case",
78
+ children: note
79
+ })
80
+ ]
81
+ }), children]
82
+ });
83
+ }),
84
+ Group: React.forwardRef(function LedgerGroup({ className, label, headingLevel = 3, sticky = true, children, ...elementProps }, ref) {
85
+ const Heading = headingTag(headingLevel);
86
+ const labelled = label !== void 0;
87
+ return /* @__PURE__ */ jsx("div", {
88
+ ref,
89
+ "data-slot": "ledger-group",
90
+ "data-labelled": labelled ? "" : void 0,
91
+ className: cn(labelled ? "grid [grid-template-columns:var(--cue-ledger-gutter,7rem)_minmax(0,1fr)]" : "flex flex-col", className),
92
+ ...elementProps,
93
+ children: labelled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
94
+ "data-slot": "ledger-group-label",
95
+ className: cn(LEDGER_LABEL, "min-w-0 truncate py-(--cue-pad-row-y) pr-(--cue-space-3)", sticky ? "sticky self-start [top:var(--cue-ledger-slug-top,2.5rem)] bg-bg" : null),
96
+ children: label
97
+ }), /* @__PURE__ */ jsx("div", {
98
+ "data-slot": "ledger-group-rows",
99
+ className: "flex min-w-0 flex-col",
100
+ children
101
+ })] }) : children
102
+ });
103
+ }),
104
+ Empty: React.forwardRef(function LedgerEmpty({ className, ...elementProps }, ref) {
105
+ return /* @__PURE__ */ jsx("p", {
106
+ ref,
107
+ "data-slot": "ledger-empty",
108
+ className: cn("px-(--cue-pad-row-x) py-(--cue-pad-row-y) font-mono text-(length:--cue-text-ui) text-fg-subtle", className),
109
+ ...elementProps
110
+ });
111
+ })
112
+ };
113
+ //#endregion
114
+ export { Ledger };
@@ -14,10 +14,69 @@ interface TableScrollRegionProps extends Omit<React.ComponentPropsWithoutRef<"di
14
14
  declare const TableScrollRegion: React.ForwardRefExoticComponent<TableScrollRegionProps & React.RefAttributes<HTMLDivElement>>;
15
15
  interface TableRootProps extends React.ComponentPropsWithoutRef<"table"> {}
16
16
  interface TableSectionProps extends React.ComponentPropsWithoutRef<"tbody"> {}
17
- interface TableRowProps extends React.ComponentPropsWithoutRef<"tr"> {}
17
+ interface TableRowProps extends React.ComponentPropsWithoutRef<"tr"> {
18
+ /**
19
+ * The row responds to a pointer: the row-hover wash and a pointer cursor
20
+ * across the whole row. Defaults to `false`.
21
+ *
22
+ * Opt-in rather than automatic, because a table of figures nobody can click
23
+ * that lit up under the pointer would be promising an interaction it does not
24
+ * have. Wiring the row up to actually do something — a click handler, a
25
+ * keyboard path to the same thing — stays the caller's job; this is the paint.
26
+ */
27
+ interactive?: boolean;
28
+ /**
29
+ * Draw the row as picked out of the table. Defaults to `false`.
30
+ *
31
+ * Sets `data-selected`, which is what the tint and the hover-revealed
32
+ * `DataRow.Actions` inside the row both key off — so a hand-written
33
+ * `<tr data-selected>` in a data-driven body gets exactly the same treatment.
34
+ */
35
+ selected?: boolean;
36
+ }
18
37
  interface TableHeadProps extends React.ComponentPropsWithoutRef<"th"> {
19
38
  /** Right-align this column and set it in tabular figures. */
20
39
  numeric?: boolean;
40
+ /**
41
+ * Dock the header band to the top of the scroll container while the rows
42
+ * scroll under it. Defaults to `false`.
43
+ *
44
+ * Set it on the cells, not on the `<tr>`: sticky on a table row is honoured
45
+ * inconsistently across engines, and the cell is the reliable baseline
46
+ * everywhere.
47
+ */
48
+ sticky?: boolean;
49
+ }
50
+ interface TableGroupRowProps extends Omit<React.ComponentPropsWithoutRef<"tr">, "children"> {
51
+ /**
52
+ * How many columns the divider spans. Required: it must equal the table's
53
+ * column count, or the group's cell stops covering the row and the columns
54
+ * below it shift by one.
55
+ */
56
+ span: number;
57
+ /** What the group is called. */
58
+ label: React.ReactNode;
59
+ /** How many rows the group holds, printed beside its name. */
60
+ count?: number;
61
+ /**
62
+ * Dock the divider under the header band while the group's rows scroll past.
63
+ * Defaults to `false`.
64
+ *
65
+ * The offset is the custom property `--cue-table-group-top` (default `0`):
66
+ * how far down the divider docks is the rendered height of the sticky header
67
+ * above it, which depends on the density and type the caller configured. Set
68
+ * it on the table — `className="[--cue-table-group-top:2rem]"` — rather than
69
+ * measuring at runtime.
70
+ */
71
+ sticky?: boolean;
72
+ /**
73
+ * Classes for the divider's *cell*, not its row.
74
+ *
75
+ * The `<tr>` paints nothing — the ground, the type and the sticky offset all
76
+ * live on the `<th>` that spans it, and a background on the row would sit
77
+ * behind the cell's own. Every other prop, and the ref, go to the row.
78
+ */
79
+ className?: string;
21
80
  }
22
81
  interface TableCellProps extends React.ComponentPropsWithoutRef<"td"> {
23
82
  /** Right-align this cell and set it in tabular figures. */
@@ -35,6 +94,14 @@ interface TableCaptionProps extends React.ComponentPropsWithoutRef<"caption"> {}
35
94
  * Static markup — no `"use client"`. Compose it with `Panel` for the rim.
36
95
  * Use {@link TableScrollRegion} only when the table can exceed its container.
37
96
  *
97
+ * For a grouped list, the table stays *one* table: `Table.Head sticky` docks the
98
+ * column names, and one `Table.Body` per group opens with a `Table.GroupRow`
99
+ * divider — so the columns are named once and scan straight through every group
100
+ * boundary. Rows may hold `DataRow` slots, and `DataRow.Actions` reveals inside
101
+ * a cell on hover and on selection exactly as it does in a `Ledger` row — on
102
+ * focus too, once the row holds something focusable outside the track, which a
103
+ * `<tr>` never is by itself. See {@link TableRowProps.interactive}.
104
+ *
38
105
  * @example
39
106
  * <Table.Root>
40
107
  * <Table.Header>
@@ -56,9 +123,10 @@ declare const Table: {
56
123
  Header: React.ForwardRefExoticComponent<TableSectionProps & React.RefAttributes<HTMLTableSectionElement>>;
57
124
  Body: React.ForwardRefExoticComponent<TableSectionProps & React.RefAttributes<HTMLTableSectionElement>>;
58
125
  Row: React.ForwardRefExoticComponent<TableRowProps & React.RefAttributes<HTMLTableRowElement>>;
126
+ GroupRow: React.ForwardRefExoticComponent<TableGroupRowProps & React.RefAttributes<HTMLTableRowElement>>;
59
127
  Head: React.ForwardRefExoticComponent<TableHeadProps & React.RefAttributes<HTMLTableCellElement>>;
60
128
  Cell: React.ForwardRefExoticComponent<TableCellProps & React.RefAttributes<HTMLTableCellElement>>;
61
129
  Caption: React.ForwardRefExoticComponent<TableCaptionProps & React.RefAttributes<HTMLTableCaptionElement>>;
62
130
  };
63
131
  //#endregion
64
- export { Table, TableCaptionProps, TableCellProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps };
132
+ export { Table, TableCaptionProps, TableCellProps, TableGroupRowProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps };