@cueplusplus/ui 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/dist/chrome/_drag.js +59 -0
- package/dist/chrome/_status-bar-item.js +85 -0
- package/dist/chrome/_status-bar.d.ts +15 -0
- package/dist/chrome/_status-bar.js +171 -0
- package/dist/chrome/app-shell.d.ts +69 -1
- package/dist/chrome/app-shell.js +38 -11
- package/dist/chrome/index.d.ts +3 -2
- package/dist/chrome/status-bar.d.ts +87 -4
- package/dist/chrome/status-bar.js +80 -16
- package/dist/chrome/title-bar.d.ts +11 -1
- package/dist/chrome/title-bar.js +1 -3
- package/dist/date/_segments.js +15 -4
- package/dist/forms/_chassis.d.ts +104 -4
- package/dist/forms/_chassis.js +104 -4
- package/dist/forms/input-group.d.ts +8 -3
- package/dist/forms/input-group.js +8 -3
- package/dist/forms/number-field.d.ts +3 -2
- package/dist/forms/number-field.js +3 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -1
- package/dist/instruments/_data-row.d.ts +68 -0
- package/dist/instruments/_data-row.js +139 -0
- package/dist/instruments/data-row.d.ts +94 -0
- package/dist/instruments/data-row.js +151 -0
- package/dist/instruments/data-tree.d.ts +126 -0
- package/dist/instruments/data-tree.js +303 -0
- package/dist/instruments/index.d.ts +6 -2
- package/dist/instruments/index.js +5 -1
- package/dist/instruments/ledger.d.ts +105 -0
- package/dist/instruments/ledger.js +114 -0
- package/dist/instruments/table.d.ts +108 -2
- package/dist/instruments/table.js +165 -39
- package/dist/theming/_presets.js +3 -3
- package/manifest/components/app-shell.json +45 -4
- package/manifest/components/autocomplete.json +2 -0
- package/manifest/components/color-field.json +2 -0
- package/manifest/components/color-picker.json +2 -0
- package/manifest/components/combobox.json +2 -0
- package/manifest/components/composer.json +2 -0
- package/manifest/components/data-row.json +208 -0
- package/manifest/components/data-tree.json +151 -0
- package/manifest/components/date-field.json +2 -0
- package/manifest/components/date-picker.json +2 -0
- package/manifest/components/date-range-picker.json +2 -0
- package/manifest/components/env-var-input.json +2 -0
- package/manifest/components/input-group.json +4 -2
- package/manifest/components/input.json +3 -1
- package/manifest/components/ledger.json +187 -0
- package/manifest/components/multi-select.json +2 -0
- package/manifest/components/musical-time-input.json +2 -0
- package/manifest/components/number-field.json +3 -1
- package/manifest/components/otp-field.json +2 -0
- package/manifest/components/password-input.json +2 -0
- package/manifest/components/scrub-input.json +2 -0
- package/manifest/components/search-input.json +2 -0
- package/manifest/components/select.json +1 -0
- package/manifest/components/status-bar.json +146 -12
- package/manifest/components/table-scroll-region.json +6 -0
- package/manifest/components/table.json +113 -5
- package/manifest/components/tags-input.json +2 -0
- package/manifest/components/textarea.json +3 -1
- package/manifest/components/time-field.json +2 -0
- package/manifest/components/title-bar.json +1 -1
- package/manifest/components/toggle-group.json +2 -0
- package/manifest/components/toggle.json +2 -0
- package/manifest/manifest.json +72 -33
- package/manifest/tokens.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
//#region src/instruments/data-tree.d.ts
|
|
3
|
+
/** What a row knows about itself when its content is a function. */
|
|
4
|
+
interface DataTreeRowState {
|
|
5
|
+
/** Depth from the roots; `0` for a root. */
|
|
6
|
+
level: number;
|
|
7
|
+
/** Whether the node declares children at all. */
|
|
8
|
+
hasChildren: boolean;
|
|
9
|
+
/** Whether those children are currently shown. */
|
|
10
|
+
expanded: boolean;
|
|
11
|
+
/** Whether this row is the selected one. */
|
|
12
|
+
selected: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** One node of the tree: an identity, a row's worth of content, and children. */
|
|
15
|
+
interface DataTreeItem {
|
|
16
|
+
/** Stable identity: what selection, expansion and focus are keyed on. */
|
|
17
|
+
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* The row's content — normally `DataRow` slots, in the same order and with
|
|
20
|
+
* the same tracks as the other idioms drawing this dataset.
|
|
21
|
+
*
|
|
22
|
+
* A function receives the row's own state, for the cases where the content
|
|
23
|
+
* differs by it: a count that only shows while a node is folded, a different
|
|
24
|
+
* marker for an expanded parent.
|
|
25
|
+
*/
|
|
26
|
+
row: React.ReactNode | ((state: DataTreeRowState) => React.ReactNode);
|
|
27
|
+
/**
|
|
28
|
+
* Nested nodes. An empty array still makes the row a parent — an empty folder
|
|
29
|
+
* is a folder, and `aria-expanded` on it is the truth.
|
|
30
|
+
*/
|
|
31
|
+
children?: readonly DataTreeItem[];
|
|
32
|
+
/** Grey the row out; it stays reachable by keyboard but cannot be selected. */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
}
|
|
35
|
+
interface DataTreeProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onSelect"> {
|
|
36
|
+
/** The roots, in display order. */
|
|
37
|
+
items: readonly DataTreeItem[];
|
|
38
|
+
/** Expanded ids when controlled. Pass with `onExpandedChange`. */
|
|
39
|
+
expanded?: readonly string[];
|
|
40
|
+
/** Initially expanded ids when uncontrolled. */
|
|
41
|
+
defaultExpanded?: readonly string[];
|
|
42
|
+
/** Called with the whole next expanded set whenever a row is folded or unfolded. */
|
|
43
|
+
onExpandedChange?: (expanded: string[]) => void;
|
|
44
|
+
/** Selected id when controlled. Pass with `onSelect`. */
|
|
45
|
+
selected?: string | null;
|
|
46
|
+
/** Initially selected id when uncontrolled. */
|
|
47
|
+
defaultSelected?: string | null;
|
|
48
|
+
/** Called with the id of the row the user selected. */
|
|
49
|
+
onSelect?: (id: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Draw the depth guides. Defaults to `true`.
|
|
52
|
+
*
|
|
53
|
+
* Roots never draw one — there is no ancestor to draw — so a flat tree draws
|
|
54
|
+
* none at all whatever this is set to.
|
|
55
|
+
*/
|
|
56
|
+
guides?: boolean;
|
|
57
|
+
/** Accessible name for the tree. Required in practice — name what it lists. */
|
|
58
|
+
"aria-label"?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A hierarchy of multi-column rows: the tree idiom over the `DataRow` anatomy.
|
|
62
|
+
*
|
|
63
|
+
* ## The ARIA pattern, chosen before the component was written
|
|
64
|
+
*
|
|
65
|
+
* `role="treegrid"` is **rejected** for this foundation. It is the semantically
|
|
66
|
+
* exact answer — tabular *and* hierarchical — and it is the one with the worst
|
|
67
|
+
* real-world assistive-technology record: Adrian Roselli's survey of expando
|
|
68
|
+
* tables calls its support poor and notes that the APG's own worked example
|
|
69
|
+
* carries unresolved issues; fancytree's maintainer describes treegrid support
|
|
70
|
+
* as unproven ground; MUI X shipped hierarchical rows with no `aria-level` /
|
|
71
|
+
* `aria-expanded` / `aria-setsize` at all and retrofitted them later. Treegrid
|
|
72
|
+
* earns its cost when cells are individually interactive or editable, because
|
|
73
|
+
* then a reader genuinely needs two-dimensional cell navigation. This surface is
|
|
74
|
+
* read-and-navigate: you move down rows, fold a branch, and open the thing a row
|
|
75
|
+
* names.
|
|
76
|
+
*
|
|
77
|
+
* So the pattern is `role="tree"` with `role="treeitem"` rows — VS Code's
|
|
78
|
+
* choice and Carbon's, and the best-supported composite pattern in ARIA:
|
|
79
|
+
*
|
|
80
|
+
* - A **flat DOM** with `aria-level` / `aria-posinset` / `aria-setsize` per row
|
|
81
|
+
* rather than nested `role="group"` wrappers.
|
|
82
|
+
* - **Roving tabindex**: the tree is one Tab stop and the arrows move inside it.
|
|
83
|
+
* - The **APG tree keyboard map**: Up/Down between visible rows, Home/End to the
|
|
84
|
+
* ends, Right to unfold then to descend, Left to fold then to ascend,
|
|
85
|
+
* Enter/Space to select.
|
|
86
|
+
* - Expansion lives on the **row**, as `aria-expanded` on the `treeitem` — that
|
|
87
|
+
* is what tree semantics mean by a parent, and it is why the chevron here is
|
|
88
|
+
* `role="presentation"` rather than a button. (The other half of this trade
|
|
89
|
+
* is `Table.GroupRow`, where the table's semantics own the grouping and a real
|
|
90
|
+
* `<button aria-expanded>` owns the disclosure. Two patterns, each internally
|
|
91
|
+
* consistent; what would be wrong is half of each.)
|
|
92
|
+
*
|
|
93
|
+
* **The documented trade**: columns are not independently navigable. A row's
|
|
94
|
+
* multi-column content lives *inside* the treeitem, so a screen-reader user
|
|
95
|
+
* hears the row as one string rather than moving cell to cell through it. For a
|
|
96
|
+
* dense browse-and-open surface that is the better bargain — `tree` works today
|
|
97
|
+
* on every screen reader, `treegrid` does not — but a surface whose cells are
|
|
98
|
+
* genuinely editable has outgrown this component and wants a real treegrid from
|
|
99
|
+
* a library that maintains one.
|
|
100
|
+
*
|
|
101
|
+
* ## Everything else
|
|
102
|
+
*
|
|
103
|
+
* Rows are the `DataRow` chassis: same padding, same hairline, same hover wash,
|
|
104
|
+
* same inset focus ring, same `group/data-row`, so `DataRow.Actions` reveals in
|
|
105
|
+
* a tree exactly as it does in a ledger. The row element itself is the
|
|
106
|
+
* treeitem — one element owns focus, expansion, selection and the click reading
|
|
107
|
+
* — and the caller's `DataRow.*` slots are its content.
|
|
108
|
+
*
|
|
109
|
+
* A tree whose items declare no children anywhere is a flat list: no chevrons,
|
|
110
|
+
* no spacer column where a chevron would be, no guides, and `aria-expanded` on
|
|
111
|
+
* nothing. A leaf row that carried `aria-expanded="false"` would tell a screen
|
|
112
|
+
* reader it has children it does not have.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* <DataTree
|
|
116
|
+
* aria-label="Patch"
|
|
117
|
+
* items={nodes}
|
|
118
|
+
* defaultExpanded={["inputs"]}
|
|
119
|
+
* selected={selected}
|
|
120
|
+
* onSelect={setSelected}
|
|
121
|
+
* className="[--cue-data-row-cols:1fr_8rem_auto]"
|
|
122
|
+
* />
|
|
123
|
+
*/
|
|
124
|
+
declare const DataTree: React.ForwardRefExoticComponent<DataTreeProps & React.RefAttributes<HTMLDivElement>>;
|
|
125
|
+
//#endregion
|
|
126
|
+
export { DataTree, DataTreeItem, DataTreeProps, DataTreeRowState };
|
|
@@ -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,17 @@
|
|
|
1
|
+
import { activateRowFromClick, activateRowFromKeyDown } from "./_data-row.js";
|
|
1
2
|
import { AppWindowFrame, AppWindowFrameProps } from "./app-window-frame.js";
|
|
2
3
|
import { CopyButton, CopyButtonProps } from "./copy-button.js";
|
|
4
|
+
import { DataRow, DataRowDisclosure, DataRowRootProps, DataRowSlotProps } from "./data-row.js";
|
|
5
|
+
import { DataTree, DataTreeItem, DataTreeProps, DataTreeRowState } from "./data-tree.js";
|
|
3
6
|
import { GroupBar, GroupBarItem, GroupBarProps, GroupBarTone } from "./group-bar.js";
|
|
7
|
+
import { Ledger, LedgerEmptyProps, LedgerGroupProps, LedgerRootProps, LedgerTierProps } from "./ledger.js";
|
|
4
8
|
import { Meter, MeterProps, MeterProvenance, MeterTone } from "./meter.js";
|
|
5
9
|
import { Progress, ProgressProps } from "./progress.js";
|
|
6
10
|
import { SeamCell, SeamCellProps, SeamGrid, SeamGridProps, SeamGround } from "./seam-grid.js";
|
|
7
11
|
import { Sparkline, SparklineProps } from "./sparkline.js";
|
|
8
12
|
import { Stat, StatProps, StatTone } from "./stat.js";
|
|
9
|
-
import { Table, TableCaptionProps, TableCellProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps } from "./table.js";
|
|
13
|
+
import { Table, TableCaptionProps, TableCellProps, TableGroupRowProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps } from "./table.js";
|
|
10
14
|
import { TerminalFrame, TerminalFrameLine, TerminalFrameProps, TerminalLine, TerminalLineTone, TerminalScript, TerminalStep } from "./terminal-frame.js";
|
|
11
15
|
import { TwoStepButton, TwoStepButtonProps, TwoStepTone } from "./two-step-button.js";
|
|
12
16
|
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 };
|
|
17
|
+
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, activateRowFromClick, activateRowFromKeyDown };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { activateRowFromClick, activateRowFromKeyDown } from "./_data-row.js";
|
|
1
2
|
import { AppWindowFrame } from "./app-window-frame.js";
|
|
2
3
|
import { CopyButton } from "./copy-button.js";
|
|
4
|
+
import { DataRow } from "./data-row.js";
|
|
5
|
+
import { DataTree } from "./data-tree.js";
|
|
3
6
|
import { GroupBar } from "./group-bar.js";
|
|
7
|
+
import { Ledger } from "./ledger.js";
|
|
4
8
|
import { Meter } from "./meter.js";
|
|
5
9
|
import { Progress } from "./progress.js";
|
|
6
10
|
import { SeamCell, SeamGrid } from "./seam-grid.js";
|
|
@@ -10,4 +14,4 @@ import { Table, TableScrollRegion } from "./table.js";
|
|
|
10
14
|
import { TerminalFrame } from "./terminal-frame.js";
|
|
11
15
|
import { TwoStepButton } from "./two-step-button.js";
|
|
12
16
|
import { UsageChart } from "./usage-chart.js";
|
|
13
|
-
export { AppWindowFrame, CopyButton, GroupBar, Meter, Progress, SeamCell, SeamGrid, Sparkline, Stat, Table, TableScrollRegion, TerminalFrame, TwoStepButton, UsageChart };
|
|
17
|
+
export { AppWindowFrame, CopyButton, DataRow, DataTree, GroupBar, Ledger, Meter, Progress, SeamCell, SeamGrid, Sparkline, Stat, Table, TableScrollRegion, TerminalFrame, TwoStepButton, UsageChart, activateRowFromClick, activateRowFromKeyDown };
|
|
@@ -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 };
|