@cueplusplus/ui 0.6.0 → 0.8.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 +29 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/instruments/_data-row.d.ts +68 -0
- package/dist/instruments/_data-row.js +21 -0
- package/dist/instruments/_ledger-disclosure.js +103 -0
- package/dist/instruments/_ledger.d.ts +21 -0
- package/dist/instruments/_ledger.js +5 -0
- package/dist/instruments/index.d.ts +2 -1
- package/dist/instruments/index.js +2 -1
- package/dist/instruments/ledger.d.ts +112 -13
- package/dist/instruments/ledger.js +128 -38
- package/dist/instruments/table.d.ts +40 -2
- package/dist/instruments/table.js +35 -3
- package/manifest/components/data-row.json +5 -0
- package/manifest/components/data-tree.json +1 -0
- package/manifest/components/ledger.json +82 -7
- package/manifest/components/table.json +28 -4
- package/manifest/manifest.json +7 -7
- package/manifest/tokens.json +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
|
+
import { headingTag } from "./_ledger.js";
|
|
3
|
+
import { LedgerDisclosure } from "./_ledger-disclosure.js";
|
|
2
4
|
import * as React from "react";
|
|
3
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
6
|
//#region src/instruments/ledger.tsx
|
|
@@ -12,24 +14,68 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
12
14
|
* micro-label in a dense list reads as a second kind of emphasis.
|
|
13
15
|
*/
|
|
14
16
|
const LEDGER_LABEL = "font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] text-fg-subtle uppercase";
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The disclosure button a collapsible heading holds.
|
|
19
|
+
*
|
|
20
|
+
* Worn *with* {@link LEDGER_LABEL}, which the heading around it is already
|
|
21
|
+
* wearing. The repetition is deliberate: a `<button>` carries the UA's own font
|
|
22
|
+
* rather than its parent's, and while Tailwind's preflight resets that to
|
|
23
|
+
* `font: inherit`, a component whose micro-label silently becomes 13px Arial if
|
|
24
|
+
* a consumer drops preflight is a component with a very confusing bug report.
|
|
25
|
+
* One constant applied twice cannot drift; two recipes can.
|
|
26
|
+
*
|
|
27
|
+
* `w-full` so the whole width of the heading is the hit area rather than the
|
|
28
|
+
* word itself, and the focus ring is inset (`-outline-offset-2`) for the same
|
|
29
|
+
* reason `_data-row.ts` gives: these headings dock at the top of clipped panes,
|
|
30
|
+
* where an outset ring on the first one is cut off by the pane's own rim.
|
|
31
|
+
*/
|
|
32
|
+
const LEDGER_TOGGLE = "flex w-full min-w-0 cursor-pointer items-baseline gap-(--cue-space-2) text-left outline-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-accent";
|
|
17
33
|
/**
|
|
18
34
|
* A grouped run of dense rows under sticky headings: the ledger idiom.
|
|
19
35
|
*
|
|
20
|
-
* The flattest of the three surfaces this package ships over one row model
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
36
|
+
* The flattest of the three surfaces this package ships over one row model:
|
|
37
|
+
* headings, slugs, rows. What it owns above all is the document outline. Tiers
|
|
38
|
+
* and groups are real `<h2>` / `<h3>` elements at caller-chosen levels, because
|
|
39
|
+
* in a list of two hundred rows the heading list *is* how a screen-reader user
|
|
40
|
+
* navigates.
|
|
41
|
+
*
|
|
42
|
+
* ## Folding: the accordion pattern, opt-in, and why it lives here
|
|
43
|
+
*
|
|
44
|
+
* This component used to refuse disclosure outright, on the grounds that *a
|
|
45
|
+
* ledger which folds is a tree with worse semantics*. That ruling was right
|
|
46
|
+
* about rows and wrong about places, and the distinction is the whole of
|
|
47
|
+
* {@link LedgerTierProps.collapsible}:
|
|
26
48
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
49
|
+
* - **`DataTree` folds a row.** Its `role="treeitem"` rows carry their own
|
|
50
|
+
* `aria-expanded`, its chevron is `role="presentation"` because in tree
|
|
51
|
+
* semantics the row owns its expansion, and a tier and a subgroup drawn
|
|
52
|
+
* through it stop being places and become rows. A consumer who measured this
|
|
53
|
+
* exact shape through `DataTree` counted **zero** `h2`/`h3` elements and zero
|
|
54
|
+
* `button[aria-expanded]`, which is `DataTree` working correctly and still
|
|
55
|
+
* not being what a grouped document needs.
|
|
56
|
+
* - **`Ledger` folds a place.** A tier is already an `<h2>`, which is already
|
|
57
|
+
* the right element for it, so the disclosure goes *inside* the heading as a
|
|
58
|
+
* real `<button aria-expanded aria-controls>` — the accordion pattern, and
|
|
59
|
+
* the same one `Table.GroupRow`'s note has always pointed at for a table that
|
|
60
|
+
* genuinely needs to fold. The outline is untouched: the heading list of a
|
|
61
|
+
* folding ledger and a plain one are identical, which is what lets a page
|
|
62
|
+
* offer both idioms over one dataset without the outline moving under a
|
|
63
|
+
* reader who switches.
|
|
30
64
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
65
|
+
* So it is one component with two forms rather than a `DisclosureLedger` beside
|
|
66
|
+
* it: everything a folding ledger needs — the gutter track, the sticky offsets,
|
|
67
|
+
* the label recipe, the type contract — is this component, and a sibling would
|
|
68
|
+
* have been a copy of all of it wrapped around one `<button>`.
|
|
69
|
+
*
|
|
70
|
+
* Expansion is controllable and stores nothing. *Which* places a reader has
|
|
71
|
+
* folded is a fact about the reader; consumers keep that set themselves and
|
|
72
|
+
* pass it back in. A folded run of rows is hidden rather than unmounted, so a
|
|
73
|
+
* group that keeps its own fold inside a tier still keeps it after the tier has
|
|
74
|
+
* been shut and opened over it.
|
|
75
|
+
*
|
|
76
|
+
* Static markup — no `"use client"` — **until a heading is asked to fold**, at
|
|
77
|
+
* which point that heading and its rows become a small client island and the
|
|
78
|
+
* rest of the ledger keeps rendering on the server.
|
|
33
79
|
*
|
|
34
80
|
* @example
|
|
35
81
|
* <Ledger.Root className="[--cue-data-row-cols:1fr_8rem_auto]">
|
|
@@ -42,6 +88,18 @@ const headingTag = (level) => level === 2 ? "h2" : level === 3 ? "h3" : "h4";
|
|
|
42
88
|
* </Ledger.Group>
|
|
43
89
|
* </Ledger.Tier>
|
|
44
90
|
* </Ledger.Root>
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Folded places are the reader's, so the reader's storage holds them.
|
|
94
|
+
* <Ledger.Tier
|
|
95
|
+
* heading="Ours"
|
|
96
|
+
* count={13}
|
|
97
|
+
* collapsible
|
|
98
|
+
* expanded={!folded.includes("ours")}
|
|
99
|
+
* onExpandedChange={(open) => remember("ours", open)}
|
|
100
|
+
* >
|
|
101
|
+
* <Ledger.Group label="released">…</Ledger.Group>
|
|
102
|
+
* </Ledger.Tier>
|
|
45
103
|
*/
|
|
46
104
|
const Ledger = {
|
|
47
105
|
Root: React.forwardRef(function LedgerRoot({ className, containment = true, ...elementProps }, ref) {
|
|
@@ -52,53 +110,85 @@ const Ledger = {
|
|
|
52
110
|
...elementProps
|
|
53
111
|
});
|
|
54
112
|
}),
|
|
55
|
-
Tier: React.forwardRef(function LedgerTier({ className, heading, count, sticky = true, headingLevel = 2, note, children, ...elementProps }, ref) {
|
|
113
|
+
Tier: React.forwardRef(function LedgerTier({ className, heading, count, sticky = true, headingLevel = 2, note, collapsible = false, expanded, defaultExpanded = true, onExpandedChange, children, ...elementProps }, ref) {
|
|
56
114
|
const Heading = headingTag(headingLevel);
|
|
57
|
-
|
|
115
|
+
const words = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
116
|
+
/* @__PURE__ */ jsx("span", {
|
|
117
|
+
"data-slot": "ledger-tier-label",
|
|
118
|
+
children: heading
|
|
119
|
+
}),
|
|
120
|
+
count === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
121
|
+
"data-slot": "ledger-tier-count",
|
|
122
|
+
className: "tabular-nums",
|
|
123
|
+
children: count
|
|
124
|
+
}),
|
|
125
|
+
note === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
126
|
+
"data-slot": "ledger-tier-note",
|
|
127
|
+
className: "tracking-normal normal-case",
|
|
128
|
+
children: note
|
|
129
|
+
})
|
|
130
|
+
] });
|
|
131
|
+
const ground = sticky ? "sticky top-0 z-2 bg-bg" : null;
|
|
132
|
+
return /* @__PURE__ */ jsx("section", {
|
|
58
133
|
ref,
|
|
59
134
|
"data-slot": "ledger-tier",
|
|
60
135
|
className: cn("flex flex-col", className),
|
|
61
136
|
...elementProps,
|
|
62
|
-
children:
|
|
137
|
+
children: heading === void 0 ? children : collapsible ? /* @__PURE__ */ jsx(LedgerDisclosure, {
|
|
138
|
+
headingLevel,
|
|
139
|
+
headingSlot: "ledger-tier-heading",
|
|
140
|
+
headingClassName: cn(LEDGER_LABEL, "py-(--cue-pad-row-y)", ground),
|
|
141
|
+
toggleSlot: "ledger-tier-toggle",
|
|
142
|
+
toggleClassName: cn(LEDGER_LABEL, LEDGER_TOGGLE),
|
|
143
|
+
panelSlot: "ledger-tier-rows",
|
|
144
|
+
panelClassName: "flex flex-col",
|
|
145
|
+
label: words,
|
|
146
|
+
expanded,
|
|
147
|
+
defaultExpanded,
|
|
148
|
+
onExpandedChange,
|
|
149
|
+
children
|
|
150
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
63
151
|
"data-slot": "ledger-tier-heading",
|
|
64
|
-
className: cn(LEDGER_LABEL, "flex items-baseline gap-(--cue-space-2) py-(--cue-pad-row-y)",
|
|
65
|
-
children:
|
|
66
|
-
|
|
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]
|
|
152
|
+
className: cn(LEDGER_LABEL, "flex items-baseline gap-(--cue-space-2) py-(--cue-pad-row-y)", ground),
|
|
153
|
+
children: words
|
|
154
|
+
}), children] })
|
|
82
155
|
});
|
|
83
156
|
}),
|
|
84
|
-
Group: React.forwardRef(function LedgerGroup({ className, label, headingLevel = 3, sticky = true, children, ...elementProps }, ref) {
|
|
157
|
+
Group: React.forwardRef(function LedgerGroup({ className, label, headingLevel = 3, sticky = true, collapsible = false, expanded, defaultExpanded = true, onExpandedChange, children, ...elementProps }, ref) {
|
|
85
158
|
const Heading = headingTag(headingLevel);
|
|
86
159
|
const labelled = label !== void 0;
|
|
160
|
+
const ground = sticky ? "sticky self-start [top:var(--cue-ledger-slug-top,2.5rem)] bg-bg" : null;
|
|
87
161
|
return /* @__PURE__ */ jsx("div", {
|
|
88
162
|
ref,
|
|
89
163
|
"data-slot": "ledger-group",
|
|
90
164
|
"data-labelled": labelled ? "" : void 0,
|
|
91
165
|
className: cn(labelled ? "grid [grid-template-columns:var(--cue-ledger-gutter,7rem)_minmax(0,1fr)]" : "flex flex-col", className),
|
|
92
166
|
...elementProps,
|
|
93
|
-
children: labelled ?
|
|
167
|
+
children: !labelled ? children : collapsible ? /* @__PURE__ */ jsx(LedgerDisclosure, {
|
|
168
|
+
headingLevel,
|
|
169
|
+
headingSlot: "ledger-group-label",
|
|
170
|
+
headingClassName: cn(LEDGER_LABEL, "min-w-0 py-(--cue-pad-row-y) pr-(--cue-space-3)", ground),
|
|
171
|
+
toggleSlot: "ledger-group-toggle",
|
|
172
|
+
toggleClassName: cn(LEDGER_LABEL, LEDGER_TOGGLE),
|
|
173
|
+
panelSlot: "ledger-group-rows",
|
|
174
|
+
panelClassName: "flex min-w-0 flex-col",
|
|
175
|
+
label: /* @__PURE__ */ jsx("span", {
|
|
176
|
+
className: "min-w-0 truncate",
|
|
177
|
+
children: label
|
|
178
|
+
}),
|
|
179
|
+
expanded,
|
|
180
|
+
defaultExpanded,
|
|
181
|
+
onExpandedChange,
|
|
182
|
+
children
|
|
183
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
94
184
|
"data-slot": "ledger-group-label",
|
|
95
|
-
className: cn(LEDGER_LABEL, "min-w-0 truncate py-(--cue-pad-row-y) pr-(--cue-space-3)",
|
|
185
|
+
className: cn(LEDGER_LABEL, "min-w-0 truncate py-(--cue-pad-row-y) pr-(--cue-space-3)", ground),
|
|
96
186
|
children: label
|
|
97
187
|
}), /* @__PURE__ */ jsx("div", {
|
|
98
188
|
"data-slot": "ledger-group-rows",
|
|
99
189
|
className: "flex min-w-0 flex-col",
|
|
100
190
|
children
|
|
101
|
-
})] })
|
|
191
|
+
})] })
|
|
102
192
|
});
|
|
103
193
|
}),
|
|
104
194
|
Empty: React.forwardRef(function LedgerEmpty({ className, ...elementProps }, ref) {
|
|
@@ -16,13 +16,26 @@ interface TableRootProps extends React.ComponentPropsWithoutRef<"table"> {}
|
|
|
16
16
|
interface TableSectionProps extends React.ComponentPropsWithoutRef<"tbody"> {}
|
|
17
17
|
interface TableRowProps extends React.ComponentPropsWithoutRef<"tr"> {
|
|
18
18
|
/**
|
|
19
|
-
* The row responds to a pointer: the row-hover wash
|
|
20
|
-
* across the whole row. Defaults to `false`.
|
|
19
|
+
* The row responds to a pointer and to focus: the row-hover wash, a pointer
|
|
20
|
+
* cursor across the whole row, and an inset focus ring. Defaults to `false`.
|
|
21
21
|
*
|
|
22
22
|
* Opt-in rather than automatic, because a table of figures nobody can click
|
|
23
23
|
* that lit up under the pointer would be promising an interaction it does not
|
|
24
24
|
* have. Wiring the row up to actually do something — a click handler, a
|
|
25
25
|
* keyboard path to the same thing — stays the caller's job; this is the paint.
|
|
26
|
+
*
|
|
27
|
+
* **The focus ring ships with it, because the caller's half is what makes the
|
|
28
|
+
* row focusable.** A `<tr>` cannot take focus on its own, so the moment a
|
|
29
|
+
* caller gives one a `tabIndex` to run
|
|
30
|
+
* {@link activateRowFromKeyDown} against, a keyboard user can reach and
|
|
31
|
+
* activate a row that shows nothing — a WCAG 2.4.7 failure the pointer user
|
|
32
|
+
* never sees. The ring is *inset* (`-outline-offset-2`), the same ruling as
|
|
33
|
+
* `Row` and `DataRow.Root`: these tables live in clipped panes and scroll
|
|
34
|
+
* regions, where an outset ring on the first or last row is cut off by the
|
|
35
|
+
* pane's own rim. `outline-solid` rides along with every `outline-<n>` here
|
|
36
|
+
* because Tailwind v4 leaves `--tw-outline-style` unset otherwise and the
|
|
37
|
+
* ring never paints — `test/focus-ring.test.ts` is what keeps the pair
|
|
38
|
+
* together.
|
|
26
39
|
*/
|
|
27
40
|
interactive?: boolean;
|
|
28
41
|
/**
|
|
@@ -47,6 +60,8 @@ interface TableHeadProps extends React.ComponentPropsWithoutRef<"th"> {
|
|
|
47
60
|
*/
|
|
48
61
|
sticky?: boolean;
|
|
49
62
|
}
|
|
63
|
+
/** The heading elements a group divider's label may be asked to be. */
|
|
64
|
+
type TableGroupHeadingLevel = 2 | 3 | 4 | 5 | 6;
|
|
50
65
|
interface TableGroupRowProps extends Omit<React.ComponentPropsWithoutRef<"tr">, "children"> {
|
|
51
66
|
/**
|
|
52
67
|
* How many columns the divider spans. Required: it must equal the table's
|
|
@@ -56,6 +71,29 @@ interface TableGroupRowProps extends Omit<React.ComponentPropsWithoutRef<"tr">,
|
|
|
56
71
|
span: number;
|
|
57
72
|
/** What the group is called. */
|
|
58
73
|
label: React.ReactNode;
|
|
74
|
+
/**
|
|
75
|
+
* Make the label slot a real heading element. Defaults to `undefined` — no
|
|
76
|
+
* heading, and the label stays the `<span>` it has always been.
|
|
77
|
+
*
|
|
78
|
+
* **Set this rather than passing a heading as `label`.** The label slot wraps
|
|
79
|
+
* whatever it is given, so an `<h2>` handed in as `label` lands *inside* a
|
|
80
|
+
* `<span>` — which renders, and exposes the heading, and does not validate: a
|
|
81
|
+
* `<span>` is phrasing content and may not contain a heading. With this prop
|
|
82
|
+
* the wrapper *is* the heading (`<h2 data-slot="table-group-label">`), so
|
|
83
|
+
* there is nothing to nest.
|
|
84
|
+
*
|
|
85
|
+
* A heading inside a `<th>` is valid — `<th>` takes flow content — and it is
|
|
86
|
+
* useful: a screen-reader user navigates a long grouped table by heading, and
|
|
87
|
+
* without one the group names are reachable only by walking the rows. The
|
|
88
|
+
* `scope="rowgroup"` association is untouched either way; this adds the
|
|
89
|
+
* divider to the document outline, it does not change what the cell heads.
|
|
90
|
+
*
|
|
91
|
+
* Levels run `2`–`6` because the outline has to describe the real nesting of
|
|
92
|
+
* the page the table sits in, not the table's own idea of itself — the same
|
|
93
|
+
* ruling as {@link LedgerTierProps.headingLevel}. There is no `1`: a group
|
|
94
|
+
* divider is never the title of the document.
|
|
95
|
+
*/
|
|
96
|
+
headingLevel?: TableGroupHeadingLevel;
|
|
59
97
|
/** How many rows the group holds, printed beside its name. */
|
|
60
98
|
count?: number;
|
|
61
99
|
/**
|
|
@@ -79,6 +79,11 @@ const TableBody = React.forwardRef(function TableBody({ className, ...elementPro
|
|
|
79
79
|
* table whose only per-row controls live in a hidden track wants either a
|
|
80
80
|
* focusable cell beside it or an always-visible cluster.
|
|
81
81
|
*
|
|
82
|
+
* A caller who instead makes the *row* focusable — a `tabIndex` plus
|
|
83
|
+
* {@link activateRowFromClick} and {@link activateRowFromKeyDown}, which is the
|
|
84
|
+
* shape a row that opens a drawer takes — gets the focus ring from
|
|
85
|
+
* {@link TableRowProps.interactive} and needs to draw nothing of its own.
|
|
86
|
+
*
|
|
82
87
|
* The selected tint is the row's, not the cell's: a `background` on the `<tr>`
|
|
83
88
|
* shows through every cell in it, so the fill cannot end up ragged where one
|
|
84
89
|
* cell sets a ground of its own.
|
|
@@ -88,7 +93,7 @@ const TableRow = React.forwardRef(function TableRow({ className, interactive = f
|
|
|
88
93
|
ref,
|
|
89
94
|
"data-slot": "table-row",
|
|
90
95
|
"data-selected": selected ? "" : void 0,
|
|
91
|
-
className: cn("group/data-row border-b border-border last:border-b-0 data-[selected]:bg-accent-soft", interactive ? "cursor-pointer hover:bg-(--cue-row-hover)" : null, className),
|
|
96
|
+
className: cn("group/data-row border-b border-border last:border-b-0 data-[selected]:bg-accent-soft", interactive ? "cursor-pointer outline-none hover:bg-(--cue-row-hover) focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-accent" : null, className),
|
|
92
97
|
...elementProps
|
|
93
98
|
});
|
|
94
99
|
});
|
|
@@ -121,6 +126,31 @@ const TableHead = React.forwardRef(function TableHead({ className, numeric = fal
|
|
|
121
126
|
...elementProps
|
|
122
127
|
});
|
|
123
128
|
});
|
|
129
|
+
/** `headingLevel` as a tag name, so the outline is real elements, not roles. */
|
|
130
|
+
const headingTag = (level) => `h${level}`;
|
|
131
|
+
/**
|
|
132
|
+
* The label slot's own treatment, worn by the `<span>` and by every heading
|
|
133
|
+
* level alike — which is the point: the two forms have to be indistinguishable.
|
|
134
|
+
*
|
|
135
|
+
* Everything here is a no-op on a `<span>` and a correction on a heading, and
|
|
136
|
+
* each of the four exists because a UA stylesheet sets it on `<h2>`–`<h6>`:
|
|
137
|
+
*
|
|
138
|
+
* - `inline` — headings are `display: block`, and a block label would push the
|
|
139
|
+
* count onto a second line. This is the one that breaks the layout outright.
|
|
140
|
+
* - `text-[length:inherit]` — `<h2>` is `1.5em` and `<h6>` is `0.67em`, so
|
|
141
|
+
* without this the divider's type would change with the level it was given,
|
|
142
|
+
* which is a heading level leaking into the visual design.
|
|
143
|
+
* - `font-normal` — headings come bold, and a bold micro-label in a dense list
|
|
144
|
+
* reads as a second kind of emphasis (the same ruling as `Ledger`'s heading).
|
|
145
|
+
* - `m-0` — headings carry a block margin that would thicken the divider row.
|
|
146
|
+
*
|
|
147
|
+
* Preflight already does the middle two for consumers who import it, but the
|
|
148
|
+
* library does not assume the consumer's reset: the divider has to look the
|
|
149
|
+
* same either way. Tracking, case and colour are *not* restated — no UA rule
|
|
150
|
+
* touches them, so they inherit from the `<th>` that owns the recipe, and a
|
|
151
|
+
* second copy of a treatment is exactly how two surfaces drift apart.
|
|
152
|
+
*/
|
|
153
|
+
const TABLE_GROUP_LABEL = "m-0 inline text-[length:inherit] font-normal";
|
|
124
154
|
/**
|
|
125
155
|
* The canonical console table: hairlines, mono figures, no zebra striping.
|
|
126
156
|
*
|
|
@@ -161,7 +191,8 @@ const Table = {
|
|
|
161
191
|
Header: TableHeader,
|
|
162
192
|
Body: TableBody,
|
|
163
193
|
Row: TableRow,
|
|
164
|
-
GroupRow: React.forwardRef(function TableGroupRow({ className, span, label, count, sticky = false, ...elementProps }, ref) {
|
|
194
|
+
GroupRow: React.forwardRef(function TableGroupRow({ className, span, label, count, headingLevel, sticky = false, ...elementProps }, ref) {
|
|
195
|
+
const Label = headingLevel === void 0 ? "span" : headingTag(headingLevel);
|
|
165
196
|
return /* @__PURE__ */ jsx("tr", {
|
|
166
197
|
ref,
|
|
167
198
|
"data-slot": "table-group-row",
|
|
@@ -171,8 +202,9 @@ const Table = {
|
|
|
171
202
|
scope: "rowgroup",
|
|
172
203
|
"data-slot": "table-group-cell",
|
|
173
204
|
className: cn("bg-bg px-(--cue-pad-row-x) py-(--cue-pad-row-y) text-left font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] whitespace-nowrap text-fg-subtle uppercase", sticky ? "sticky z-2 [top:var(--cue-table-group-top,0px)]" : null, className),
|
|
174
|
-
children: [/* @__PURE__ */ jsx(
|
|
205
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
175
206
|
"data-slot": "table-group-label",
|
|
207
|
+
className: TABLE_GROUP_LABEL,
|
|
176
208
|
children: label
|
|
177
209
|
}), count === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
178
210
|
"data-slot": "table-group-count",
|
|
@@ -157,6 +157,11 @@
|
|
|
157
157
|
"code": "@container patch (max-width: 46rem) {\n .patch [data-slot=\"data-row\"] {\n --cue-data-row-cols: 1.5rem minmax(8rem, 1fr) auto;\n }\n .patch [data-slot=\"data-row-description\"] {\n display: none;\n }\n}",
|
|
158
158
|
"language": "css"
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
"title": "The same decision, on a table row",
|
|
162
|
+
"code": "import { activateRowFromClick, activateRowFromKeyDown } from \"@cueplusplus/ui\";\n\n<Table.Row\n interactive\n tabIndex={0}\n onClick={(event) => activateRowFromClick(event, () => open(device))}\n onKeyDown={(event) => activateRowFromKeyDown(event, () => open(device))}\n>",
|
|
163
|
+
"language": "tsx"
|
|
164
|
+
},
|
|
160
165
|
{
|
|
161
166
|
"title": "A density island for the list, not for the app",
|
|
162
167
|
"code": "<div className=\"[--cue-chip-h:1rem] [--cue-control-sm:1.25rem]\">\n <Ledger.Root>…</Ledger.Root>\n</div>",
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"whenNotToUse": [
|
|
129
129
|
"A single-column outline of labels with a glyph gutter. Use `Tree`, which is that component.",
|
|
130
130
|
"A grouped list nobody folds. Use `Ledger` or a grouped `Table`; disclosure semantics you never use are a promise you never keep.",
|
|
131
|
+
"A grouped document whose *headings* fold, where the `h2`/`h3` outline has to survive. Use `Ledger` with `collapsible`: a tree makes every tier and subgroup a `treeitem` row and leaves no headings behind.",
|
|
131
132
|
"A grid whose cells are individually editable. That needs a real `treegrid` from a library that maintains one — this component trades cell navigation away on purpose."
|
|
132
133
|
],
|
|
133
134
|
"commonMistakes": [
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"importPath": "@cueplusplus/ui",
|
|
6
6
|
"peerDependencies": [],
|
|
7
7
|
"clientOnly": false,
|
|
8
|
-
"description": "A grouped run of dense rows under sticky headings: the ledger idiom.\n\nThe flattest of the three surfaces this package ships over one row model,
|
|
8
|
+
"description": "A grouped run of dense rows under sticky headings: the ledger idiom.\n\nThe flattest of the three surfaces this package ships over one row model:\nheadings, slugs, rows. What it owns above all is the document outline. Tiers\nand groups are real `<h2>` / `<h3>` elements at caller-chosen levels, because\nin a list of two hundred rows the heading list *is* how a screen-reader user\nnavigates.\n\n## Folding: the accordion pattern, opt-in, and why it lives here\n\nThis component used to refuse disclosure outright, on the grounds that *a\nledger which folds is a tree with worse semantics*. That ruling was right\nabout rows and wrong about places, and the distinction is the whole of\n{@link LedgerTierProps.collapsible}:\n\n- **`DataTree` folds a row.** Its `role=\"treeitem\"` rows carry their own\n `aria-expanded`, its chevron is `role=\"presentation\"` because in tree\n semantics the row owns its expansion, and a tier and a subgroup drawn\n through it stop being places and become rows. A consumer who measured this\n exact shape through `DataTree` counted **zero** `h2`/`h3` elements and zero\n `button[aria-expanded]`, which is `DataTree` working correctly and still\n not being what a grouped document needs.\n- **`Ledger` folds a place.** A tier is already an `<h2>`, which is already\n the right element for it, so the disclosure goes *inside* the heading as a\n real `<button aria-expanded aria-controls>` — the accordion pattern, and\n the same one `Table.GroupRow`'s note has always pointed at for a table that\n genuinely needs to fold. The outline is untouched: the heading list of a\n folding ledger and a plain one are identical, which is what lets a page\n offer both idioms over one dataset without the outline moving under a\n reader who switches.\n\nSo it is one component with two forms rather than a `DisclosureLedger` beside\nit: everything a folding ledger needs — the gutter track, the sticky offsets,\nthe label recipe, the type contract — is this component, and a sibling would\nhave been a copy of all of it wrapped around one `<button>`.\n\nExpansion is controllable and stores nothing. *Which* places a reader has\nfolded is a fact about the reader; consumers keep that set themselves and\npass it back in. A folded run of rows is hidden rather than unmounted, so a\ngroup that keeps its own fold inside a tier still keeps it after the tier has\nbeen shut and opened over it.\n\nStatic markup — no `\"use client\"` — **until a heading is asked to fold**, at\nwhich point that heading and its rows become a small client island and the\nrest of the ledger keeps rendering on the server.",
|
|
9
9
|
"props": [],
|
|
10
10
|
"typeReferences": [],
|
|
11
11
|
"subcomponents": [
|
|
@@ -71,6 +71,34 @@
|
|
|
71
71
|
"required": false,
|
|
72
72
|
"defaultValue": null,
|
|
73
73
|
"description": "A qualifier printed after the name — a date, a source, a state."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "collapsible",
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"required": false,
|
|
79
|
+
"defaultValue": "false",
|
|
80
|
+
"description": "Put a disclosure in the tier's heading that folds its rows away. Defaults\nto `false`, and a tier with no `heading` has nothing to fold from, so it\nignores this and renders as it always has.\n\n**This is the accordion pattern, not the tree pattern.** What folds is the\nheading — a *place* — and the `<h2>` stays exactly where it was with a real\n`<button aria-expanded aria-controls>` inside it. The rows below are hidden\nwhile it is shut, which takes them out of the accessibility tree and out of\nthe layout but keeps whatever state they hold. If what you are folding is a\n*row*, and the thing under it is that row's children rather than that\nplace's contents, you want `DataTree` and its `treeitem` semantics instead."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "expanded",
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"required": false,
|
|
86
|
+
"defaultValue": null,
|
|
87
|
+
"description": "Whether the tier is open, when the caller owns the state.\n\n**Persistence is the caller's.** Which places a reader has folded is a fact\nabout the reader, not about the list, and it belongs wherever that consumer\nkeeps the rest of them — `localStorage`, a URL, a profile. This library\nstores nothing. Pass this with {@link LedgerTierProps.onExpandedChange};\nomit both and the tier keeps its own state from\n{@link LedgerTierProps.defaultExpanded}. Ignored unless `collapsible`."
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "defaultExpanded",
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"required": false,
|
|
93
|
+
"defaultValue": "true",
|
|
94
|
+
"description": "Whether an uncontrolled tier starts open. Defaults to `true`.\n\nOpen, because a ledger whose content is folded away on first paint has\nhidden the thing the reader came for; the reader folds what they are done\nwith. Ignored unless `collapsible`, and ignored entirely once\n{@link LedgerTierProps.expanded} is passed."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "onExpandedChange",
|
|
98
|
+
"type": "((expanded: boolean) => void)",
|
|
99
|
+
"required": false,
|
|
100
|
+
"defaultValue": null,
|
|
101
|
+
"description": "Called with the tier's next state each time the reader presses the\ndisclosure — `true` when it is being opened, `false` when it is being shut.\n\nFires in both the controlled and the uncontrolled form, so a consumer can\nrecord what was folded without also having to own the state."
|
|
74
102
|
}
|
|
75
103
|
],
|
|
76
104
|
"typeReferences": [
|
|
@@ -102,6 +130,34 @@
|
|
|
102
130
|
"required": false,
|
|
103
131
|
"defaultValue": "true",
|
|
104
132
|
"description": "Dock the slug under the tier heading while the group's rows scroll past.\nDefaults to `true`.\n\nThe offset is a custom property, `--cue-ledger-slug-top` (default `2.5rem`):\nhow far down the slug docks depends on how tall the tier heading above it\nrenders, which is a function of the density and the type the *caller*\nconfigured. Set it on the ledger — `className=\"[--cue-ledger-slug-top:2rem]\"` —\nrather than measuring at runtime."
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "collapsible",
|
|
136
|
+
"type": "boolean",
|
|
137
|
+
"required": false,
|
|
138
|
+
"defaultValue": "false",
|
|
139
|
+
"description": "Put a disclosure in the group's slug that folds its rows away. Defaults to\n`false`, and an unlabelled group has no slug to fold from, so it ignores\nthis. The same accordion pattern the tier takes — see\n{@link LedgerTierProps.collapsible} for why it is not the tree pattern."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "expanded",
|
|
143
|
+
"type": "boolean",
|
|
144
|
+
"required": false,
|
|
145
|
+
"defaultValue": null,
|
|
146
|
+
"description": "Whether the group is open, when the caller owns the state. Persistence is\nthe caller's; see {@link LedgerTierProps.expanded}."
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"name": "defaultExpanded",
|
|
150
|
+
"type": "boolean",
|
|
151
|
+
"required": false,
|
|
152
|
+
"defaultValue": "true",
|
|
153
|
+
"description": "Whether an uncontrolled group starts open. Defaults to `true`."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "onExpandedChange",
|
|
157
|
+
"type": "((expanded: boolean) => void)",
|
|
158
|
+
"required": false,
|
|
159
|
+
"defaultValue": null,
|
|
160
|
+
"description": "Called with the group's next state each time the reader presses the slug."
|
|
105
161
|
}
|
|
106
162
|
],
|
|
107
163
|
"typeReferences": [
|
|
@@ -121,11 +177,13 @@
|
|
|
121
177
|
"variants": {},
|
|
122
178
|
"defaultVariants": {},
|
|
123
179
|
"tokensUsed": [
|
|
180
|
+
"--cue-accent",
|
|
124
181
|
"--cue-bg",
|
|
125
182
|
"--cue-data-row-cols",
|
|
126
183
|
"--cue-fg",
|
|
127
184
|
"--cue-fg-subtle",
|
|
128
185
|
"--cue-font-mono",
|
|
186
|
+
"--cue-icon-sm",
|
|
129
187
|
"--cue-ledger-gutter",
|
|
130
188
|
"--cue-ledger-slug-top",
|
|
131
189
|
"--cue-pad-row-x",
|
|
@@ -135,7 +193,7 @@
|
|
|
135
193
|
"--cue-text-label",
|
|
136
194
|
"--cue-text-ui"
|
|
137
195
|
],
|
|
138
|
-
"summary": "Grouped dense rows under sticky tier headings and gutter slugs, with
|
|
196
|
+
"summary": "Grouped dense rows under sticky tier headings and gutter slugs, with an opt-in disclosure that folds a heading without leaving the outline.",
|
|
139
197
|
"examples": [
|
|
140
198
|
{
|
|
141
199
|
"title": "Tiers, groups, rows",
|
|
@@ -147,9 +205,14 @@
|
|
|
147
205
|
"code": "<Ledger.Tier>\n <Ledger.Group>\n <DataRow.Root>…</DataRow.Root>\n </Ledger.Group>\n</Ledger.Tier>",
|
|
148
206
|
"language": "tsx"
|
|
149
207
|
},
|
|
208
|
+
{
|
|
209
|
+
"title": "Folded places, persisted by the caller",
|
|
210
|
+
"code": "<Ledger.Tier\n heading=\"Ours\"\n count={13}\n collapsible\n expanded={!folded.includes(\"ours\")}\n onExpandedChange={(open) => remember(\"ours\", open)}\n>\n <Ledger.Group label=\"released\" collapsible defaultExpanded={false}>\n <DataRow.Root>…</DataRow.Root>\n </Ledger.Group>\n</Ledger.Tier>",
|
|
211
|
+
"language": "tsx"
|
|
212
|
+
},
|
|
150
213
|
{
|
|
151
214
|
"title": "Usage",
|
|
152
|
-
"code": "<Ledger.Root className=\"[--cue-data-row-cols:1fr_8rem_auto]\">\n <Ledger.Tier heading=\"Ours\" count={12}>\n <Ledger.Group label=\"released\">\n <DataRow.Root interactive onActivate={open}>…</DataRow.Root>\n </Ledger.Group>\n <Ledger.Group label=\"lab\">\n <Ledger.Empty>Nothing yet.</Ledger.Empty>\n </Ledger.Group>\n </Ledger.Tier>\n</Ledger.Root>",
|
|
215
|
+
"code": "<Ledger.Root className=\"[--cue-data-row-cols:1fr_8rem_auto]\">\n <Ledger.Tier heading=\"Ours\" count={12}>\n <Ledger.Group label=\"released\">\n <DataRow.Root interactive onActivate={open}>…</DataRow.Root>\n </Ledger.Group>\n <Ledger.Group label=\"lab\">\n <Ledger.Empty>Nothing yet.</Ledger.Empty>\n </Ledger.Group>\n </Ledger.Tier>\n</Ledger.Root>\n// Folded places are the reader's, so the reader's storage holds them.\n<Ledger.Tier\n heading=\"Ours\"\n count={13}\n collapsible\n expanded={!folded.includes(\"ours\")}\n onExpandedChange={(open) => remember(\"ours\", open)}\n>\n <Ledger.Group label=\"released\">…</Ledger.Group>\n</Ledger.Tier>",
|
|
153
216
|
"language": "tsx"
|
|
154
217
|
}
|
|
155
218
|
],
|
|
@@ -158,16 +221,18 @@
|
|
|
158
221
|
"mdUrl": "/docs/components/ledger.md",
|
|
159
222
|
"jsonUrl": "/r/components/ledger.json",
|
|
160
223
|
"whenToUse": [
|
|
161
|
-
"A long
|
|
162
|
-
"A surface whose groups should be reachable by heading, since the tiers and slugs are real `h2`/`h3` elements."
|
|
224
|
+
"A long grouped list: a corpus by source, a patch by direction, a run log by day.",
|
|
225
|
+
"A surface whose groups should be reachable by heading, since the tiers and slugs are real `h2`/`h3` elements.",
|
|
226
|
+
"A grouped list the reader folds *by place* — `collapsible` puts a real `button[aria-expanded]` inside the heading and leaves the `h2`/`h3` outline exactly as it was."
|
|
163
227
|
],
|
|
164
228
|
"whenNotToUse": [
|
|
165
|
-
"A hierarchy
|
|
229
|
+
"A hierarchy whose *rows* fold into their own children. Use `DataTree`, where the row is the treeitem and owns its expansion.",
|
|
166
230
|
"Rows meant to be compared down a named column. Use a grouped `Table`, where the columns are named once in a `thead`.",
|
|
167
231
|
"A handful of rows in a panel. Use `Row` inside `Panel`; a tier heading over three rows is furniture with nothing to organise."
|
|
168
232
|
],
|
|
169
233
|
"commonMistakes": [
|
|
170
|
-
"
|
|
234
|
+
"Reaching for `DataTree` to fold a grouped list. A tree turns every tier and subgroup into a `treeitem` row and leaves zero headings behind — if the thing folding is a *place*, `collapsible` is the prop.",
|
|
235
|
+
"Storing the folded set inside the component. Which places a reader folded is a fact about the reader: pass `expanded` and `onExpandedChange` and keep it wherever the rest of that reader's preferences live.",
|
|
171
236
|
"Framing each group in a `Panel`. A card per group turns one list into a stack of little tables and breaks the top-to-bottom scan.",
|
|
172
237
|
"Leaving `--cue-ledger-slug-top` at its default after changing the density or the heading type, which docks the slug over the tier heading instead of under it.",
|
|
173
238
|
"Passing an `empty` string as a prop. `Ledger.Empty` takes the words because \"this place is empty\" and \"your filter emptied it\" are different sentences."
|
|
@@ -182,6 +247,16 @@
|
|
|
182
247
|
"code": "<Panel className=\"w-full\">\n <div className=\"max-h-[18rem] overflow-y-auto\">\n <Ledger.Root className={PATCH_TRACKS}>\n <Ledger.Tier heading=\"Patch\" count={PATCH_DEVICES.length} note=\"polled 18:04\">\n {PATCH_GROUPS.map((group) => (\n <Ledger.Group key={group.id} label={group.label}>\n {group.rows.map((device) => (\n <DataRow.Root key={device.id} interactive>\n <PatchRowSlots device={device} />\n </DataRow.Root>\n ))}\n </Ledger.Group>\n ))}\n <Ledger.Group label=\"spare\">\n <Ledger.Empty>Nothing yet.</Ledger.Empty>\n </Ledger.Group>\n </Ledger.Tier>\n </Ledger.Root>\n </div>\n</Panel>",
|
|
183
248
|
"note": "The same rows as tiers and slugs: sticky headings, a gutter for the group name, and no disclosure anywhere — the ledger does not collapse.",
|
|
184
249
|
"interaction": "the tier heading and the group slug dock as the rows scroll under them; scroll the pane to see it."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"title": "Ledger — collapsible",
|
|
253
|
+
"group": "instruments",
|
|
254
|
+
"components": [
|
|
255
|
+
"Ledger"
|
|
256
|
+
],
|
|
257
|
+
"code": "<Panel className=\"w-full\">\n <div className=\"max-h-[18rem] overflow-y-auto\">\n <Ledger.Root className={PATCH_TRACKS}>\n <Ledger.Tier\n heading=\"Patch\"\n count={PATCH_DEVICES.length}\n collapsible\n expanded={patchOpen}\n onExpandedChange={setPatchOpen}\n >\n {PATCH_GROUPS.map((group, index) => (\n <Ledger.Group\n key={group.id}\n label={group.label}\n collapsible\n defaultExpanded={index === 0}\n >\n {group.rows.map((device) => (\n <DataRow.Root key={device.id} interactive>\n <PatchRowSlots device={device} />\n </DataRow.Root>\n ))}\n </Ledger.Group>\n ))}\n </Ledger.Tier>\n </Ledger.Root>\n </div>\n</Panel>",
|
|
258
|
+
"note": "The same ledger with `collapsible` on the tier and on one group. The disclosure is a real `button[aria-expanded aria-controls]` *inside* the `h2`/`h3`, so the heading outline is identical to the bench above it. A folded run of rows is hidden rather than unmounted — out of the layout and out of the accessibility tree, but still holding its state, which is what lets a group keep its own fold while the tier above it is shut and reopened. Which places are folded is the caller's to keep; this bench keeps it in React state and nothing is stored by the library.",
|
|
259
|
+
"interaction": "press a tier heading or a group slug to fold it; the chevron turns, the sibling group stays open, and Tab reaches each disclosure as a real button with Enter and Space. Fold the first group, then fold and reopen the tier over it: the group is still folded."
|
|
185
260
|
}
|
|
186
261
|
]
|
|
187
262
|
}
|