@cueplusplus/ui 0.7.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 +10 -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/ledger.d.ts +112 -13
- package/dist/instruments/ledger.js +128 -38
- package/manifest/components/data-tree.json +1 -0
- package/manifest/components/ledger.json +82 -7
- package/manifest/manifest.json +5 -5
- package/manifest/tokens.json +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @cueplusplus/ui
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4c7b963: `Ledger.Tier` and `Ledger.Group` take `collapsible`: the heading stays the `h2`/`h3` element it always was and gains a real `button[aria-expanded aria-controls]` inside it, which folds the run of rows below away. The heading outline is untouched, so a page that offers the same dataset folded and unfolded does not move a screen-reader user's heading list. Expansion is controlled or uncontrolled — `expanded`, `defaultExpanded`, `onExpandedChange` — and the library stores nothing: which places a reader folded is the caller's to keep. A folded run of rows is hidden rather than unmounted, so a group holding its own fold inside a tier still holds it after the tier above has been shut and opened again. A ledger nobody asks to fold renders exactly as before, with no client boundary.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- @cueplusplus/tokens@0.8.0
|
|
12
|
+
|
|
3
13
|
## 0.7.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
import { ChevronRightGlyph } from "../chrome/_glyphs.js";
|
|
4
|
+
import { headingTag } from "./_ledger.js";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/instruments/_ledger-disclosure.tsx
|
|
8
|
+
/**
|
|
9
|
+
* A heading that folds the rows beneath it: the accordion pattern, once, for
|
|
10
|
+
* both of the ledger's headings.
|
|
11
|
+
*
|
|
12
|
+
* ## Why the button is inside the heading rather than instead of it
|
|
13
|
+
*
|
|
14
|
+
* This is the disclosure shape the APG calls an accordion, and the one
|
|
15
|
+
* `Table.GroupRow`'s own note already points at — *"a real
|
|
16
|
+
* `<button aria-expanded aria-controls>` inside this cell, never the row
|
|
17
|
+
* itself"*. The heading element survives, so the page's `h2`/`h3` outline is
|
|
18
|
+
* unchanged whether a tier folds or not, and a screen-reader user who navigates
|
|
19
|
+
* a two-hundred-row ledger by heading — which is what the outline is *for* —
|
|
20
|
+
* finds the same places in the same order either way. Making the heading the
|
|
21
|
+
* button, or replacing it with one, would trade that outline for a control, and
|
|
22
|
+
* the outline is the more valuable of the two.
|
|
23
|
+
*
|
|
24
|
+
* The glyph is `aria-hidden`, so the button's accessible name is the heading's
|
|
25
|
+
* own words. A reader hears "Ours, 13, collapsed button", not "button".
|
|
26
|
+
*
|
|
27
|
+
* ## Why the folded rows are hidden rather than unmounted
|
|
28
|
+
*
|
|
29
|
+
* A folded run of rows stays mounted inside a panel that carries `hidden`. The
|
|
30
|
+
* obvious alternative — dropping the children while the run is shut, so that
|
|
31
|
+
* folding something away costs nothing to have folded — is wrong here, and the
|
|
32
|
+
* reason is that **the rows are not always inert**. A `Ledger.Group` inside a
|
|
33
|
+
* tier is itself a disclosure holding its own uncontrolled state: unmount it
|
|
34
|
+
* and that state goes with it, so a reader who folds three groups, folds the
|
|
35
|
+
* tier over them and opens it again finds all three sprung back open. A control
|
|
36
|
+
* that says nothing about touching them had quietly undone their work. React
|
|
37
|
+
* state below a fold is the caller's too, and this component is not entitled to
|
|
38
|
+
* throw it away.
|
|
39
|
+
*
|
|
40
|
+
* Nothing is given up in exchange. A `hidden` subtree is out of the
|
|
41
|
+
* accessibility tree exactly as an absent one is — no heading in it reaches the
|
|
42
|
+
* outline, no row in it answers `getByRole`, and a screen reader walks past all
|
|
43
|
+
* of it — so the two forms are indistinguishable to the reader this component
|
|
44
|
+
* is built for. What differs is only that the caller's state survives.
|
|
45
|
+
*
|
|
46
|
+
* The panel element itself would have to stay in any case: `aria-controls` must
|
|
47
|
+
* name an element that exists, and pointed at a node that leaves with its
|
|
48
|
+
* contents it becomes a dangling reference and an `aria-valid-attr-value`
|
|
49
|
+
* failure — the very trap `DataRowDisclosure.controls` documents.
|
|
50
|
+
*
|
|
51
|
+
* ## Why the folded panel wears no classes at all
|
|
52
|
+
*
|
|
53
|
+
* `hidden` is a UA rule, `[hidden] { display: none }`, and **any author
|
|
54
|
+
* `display` beats it**: a panel that keeps `flex` while it is `hidden` is a
|
|
55
|
+
* panel that is not hidden. Rather than fight that with a `display` utility
|
|
56
|
+
* whose win depends on Tailwind's emission order, the folded panel is given no
|
|
57
|
+
* class list at all, and the layout classes come back when it opens.
|
|
58
|
+
*
|
|
59
|
+
* Keeping the rows mounted is what makes this load-bearing rather than tidy.
|
|
60
|
+
* When the panel was emptied on folding there was nothing inside to leak; now
|
|
61
|
+
* the panel's own `display: none` is the only thing standing between a folded
|
|
62
|
+
* run of rows and a visible one. It is enough — a `display: none` element
|
|
63
|
+
* generates no boxes for any of its descendants, whatever `display` each of
|
|
64
|
+
* them asks for — but it has to actually apply, and jsdom cannot see whether it
|
|
65
|
+
* does, which is why `tests/browser/data-surfaces.spec.ts` reads the computed
|
|
66
|
+
* `display` on both real engines.
|
|
67
|
+
*/
|
|
68
|
+
function LedgerDisclosure({ headingLevel, headingSlot, headingClassName, toggleSlot, toggleClassName, panelSlot, panelClassName, label, expanded: expandedProp, defaultExpanded = true, onExpandedChange, children }) {
|
|
69
|
+
const [selfExpanded, setSelfExpanded] = React.useState(defaultExpanded);
|
|
70
|
+
const expanded = expandedProp ?? selfExpanded;
|
|
71
|
+
const panelId = React.useId();
|
|
72
|
+
const Heading = headingTag(headingLevel);
|
|
73
|
+
const toggle = () => {
|
|
74
|
+
const next = !expanded;
|
|
75
|
+
if (expandedProp === void 0) setSelfExpanded(next);
|
|
76
|
+
onExpandedChange?.(next);
|
|
77
|
+
};
|
|
78
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
79
|
+
"data-slot": headingSlot,
|
|
80
|
+
className: headingClassName,
|
|
81
|
+
children: /* @__PURE__ */ jsxs("button", {
|
|
82
|
+
type: "button",
|
|
83
|
+
"data-slot": toggleSlot,
|
|
84
|
+
"aria-expanded": expanded,
|
|
85
|
+
"aria-controls": panelId,
|
|
86
|
+
onClick: toggle,
|
|
87
|
+
className: toggleClassName,
|
|
88
|
+
children: [/* @__PURE__ */ jsx(ChevronRightGlyph, {
|
|
89
|
+
"aria-hidden": "true",
|
|
90
|
+
"data-slot": `${toggleSlot}-glyph`,
|
|
91
|
+
className: cn("size-icon-sm shrink-0 self-center transition-transform duration-150 motion-reduce:transition-none", expanded ? "rotate-90" : null)
|
|
92
|
+
}), label]
|
|
93
|
+
})
|
|
94
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
95
|
+
id: panelId,
|
|
96
|
+
"data-slot": panelSlot,
|
|
97
|
+
hidden: !expanded,
|
|
98
|
+
className: expanded ? panelClassName : void 0,
|
|
99
|
+
children
|
|
100
|
+
})] });
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { LedgerDisclosure };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/instruments/_ledger.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The outline half of the ledger, in a module carrying no runtime directive.
|
|
4
|
+
*
|
|
5
|
+
* `ledger.tsx` renders on the server and has to keep doing so — a document of
|
|
6
|
+
* two hundred rows that ships its own JavaScript in order to be *read* is
|
|
7
|
+
* exactly what the static form exists to avoid — while the disclosure in
|
|
8
|
+
* `_ledger-disclosure.tsx` holds React state and is therefore `"use client"`.
|
|
9
|
+
*
|
|
10
|
+
* A server module may **render** a client one, but it may not **call into** it:
|
|
11
|
+
* every value imported from a `"use client"` file is a client reference, and
|
|
12
|
+
* invoking one during a server render throws rather than returning a tag name.
|
|
13
|
+
* So the two symbols both halves need — what a heading level is, and which
|
|
14
|
+
* element it names — live here, in a plain module either graph can compile,
|
|
15
|
+
* instead of being copied into both and drifting the day one of them learns
|
|
16
|
+
* about `h5`.
|
|
17
|
+
*/
|
|
18
|
+
/** The heading elements a tier or a group may be given. */
|
|
19
|
+
type LedgerHeadingLevel = 2 | 3 | 4;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { LedgerHeadingLevel };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { LedgerHeadingLevel } from "./_ledger.js";
|
|
1
2
|
import * as React from "react";
|
|
2
3
|
//#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
4
|
interface LedgerRootProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
6
5
|
/**
|
|
7
6
|
* Make the ledger a size container, so a caller's `@container` rules shed
|
|
@@ -40,6 +39,48 @@ interface LedgerTierProps extends React.ComponentPropsWithoutRef<"section"> {
|
|
|
40
39
|
headingLevel?: LedgerHeadingLevel;
|
|
41
40
|
/** A qualifier printed after the name — a date, a source, a state. */
|
|
42
41
|
note?: React.ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Put a disclosure in the tier's heading that folds its rows away. Defaults
|
|
44
|
+
* to `false`, and a tier with no `heading` has nothing to fold from, so it
|
|
45
|
+
* ignores this and renders as it always has.
|
|
46
|
+
*
|
|
47
|
+
* **This is the accordion pattern, not the tree pattern.** What folds is the
|
|
48
|
+
* heading — a *place* — and the `<h2>` stays exactly where it was with a real
|
|
49
|
+
* `<button aria-expanded aria-controls>` inside it. The rows below are hidden
|
|
50
|
+
* while it is shut, which takes them out of the accessibility tree and out of
|
|
51
|
+
* the layout but keeps whatever state they hold. If what you are folding is a
|
|
52
|
+
* *row*, and the thing under it is that row's children rather than that
|
|
53
|
+
* place's contents, you want `DataTree` and its `treeitem` semantics instead.
|
|
54
|
+
*/
|
|
55
|
+
collapsible?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the tier is open, when the caller owns the state.
|
|
58
|
+
*
|
|
59
|
+
* **Persistence is the caller's.** Which places a reader has folded is a fact
|
|
60
|
+
* about the reader, not about the list, and it belongs wherever that consumer
|
|
61
|
+
* keeps the rest of them — `localStorage`, a URL, a profile. This library
|
|
62
|
+
* stores nothing. Pass this with {@link LedgerTierProps.onExpandedChange};
|
|
63
|
+
* omit both and the tier keeps its own state from
|
|
64
|
+
* {@link LedgerTierProps.defaultExpanded}. Ignored unless `collapsible`.
|
|
65
|
+
*/
|
|
66
|
+
expanded?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether an uncontrolled tier starts open. Defaults to `true`.
|
|
69
|
+
*
|
|
70
|
+
* Open, because a ledger whose content is folded away on first paint has
|
|
71
|
+
* hidden the thing the reader came for; the reader folds what they are done
|
|
72
|
+
* with. Ignored unless `collapsible`, and ignored entirely once
|
|
73
|
+
* {@link LedgerTierProps.expanded} is passed.
|
|
74
|
+
*/
|
|
75
|
+
defaultExpanded?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Called with the tier's next state each time the reader presses the
|
|
78
|
+
* disclosure — `true` when it is being opened, `false` when it is being shut.
|
|
79
|
+
*
|
|
80
|
+
* Fires in both the controlled and the uncontrolled form, so a consumer can
|
|
81
|
+
* record what was folded without also having to own the state.
|
|
82
|
+
*/
|
|
83
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
43
84
|
}
|
|
44
85
|
interface LedgerGroupProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
45
86
|
/**
|
|
@@ -64,24 +105,70 @@ interface LedgerGroupProps extends React.ComponentPropsWithoutRef<"div"> {
|
|
|
64
105
|
* rather than measuring at runtime.
|
|
65
106
|
*/
|
|
66
107
|
sticky?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Put a disclosure in the group's slug that folds its rows away. Defaults to
|
|
110
|
+
* `false`, and an unlabelled group has no slug to fold from, so it ignores
|
|
111
|
+
* this. The same accordion pattern the tier takes — see
|
|
112
|
+
* {@link LedgerTierProps.collapsible} for why it is not the tree pattern.
|
|
113
|
+
*/
|
|
114
|
+
collapsible?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Whether the group is open, when the caller owns the state. Persistence is
|
|
117
|
+
* the caller's; see {@link LedgerTierProps.expanded}.
|
|
118
|
+
*/
|
|
119
|
+
expanded?: boolean;
|
|
120
|
+
/** Whether an uncontrolled group starts open. Defaults to `true`. */
|
|
121
|
+
defaultExpanded?: boolean;
|
|
122
|
+
/** Called with the group's next state each time the reader presses the slug. */
|
|
123
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
67
124
|
}
|
|
68
125
|
interface LedgerEmptyProps extends React.ComponentPropsWithoutRef<"p"> {}
|
|
69
126
|
/**
|
|
70
127
|
* A grouped run of dense rows under sticky headings: the ledger idiom.
|
|
71
128
|
*
|
|
72
|
-
* The flattest of the three surfaces this package ships over one row model
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
129
|
+
* The flattest of the three surfaces this package ships over one row model:
|
|
130
|
+
* headings, slugs, rows. What it owns above all is the document outline. Tiers
|
|
131
|
+
* and groups are real `<h2>` / `<h3>` elements at caller-chosen levels, because
|
|
132
|
+
* in a list of two hundred rows the heading list *is* how a screen-reader user
|
|
133
|
+
* navigates.
|
|
134
|
+
*
|
|
135
|
+
* ## Folding: the accordion pattern, opt-in, and why it lives here
|
|
78
136
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* rows
|
|
137
|
+
* This component used to refuse disclosure outright, on the grounds that *a
|
|
138
|
+
* ledger which folds is a tree with worse semantics*. That ruling was right
|
|
139
|
+
* about rows and wrong about places, and the distinction is the whole of
|
|
140
|
+
* {@link LedgerTierProps.collapsible}:
|
|
82
141
|
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
142
|
+
* - **`DataTree` folds a row.** Its `role="treeitem"` rows carry their own
|
|
143
|
+
* `aria-expanded`, its chevron is `role="presentation"` because in tree
|
|
144
|
+
* semantics the row owns its expansion, and a tier and a subgroup drawn
|
|
145
|
+
* through it stop being places and become rows. A consumer who measured this
|
|
146
|
+
* exact shape through `DataTree` counted **zero** `h2`/`h3` elements and zero
|
|
147
|
+
* `button[aria-expanded]`, which is `DataTree` working correctly and still
|
|
148
|
+
* not being what a grouped document needs.
|
|
149
|
+
* - **`Ledger` folds a place.** A tier is already an `<h2>`, which is already
|
|
150
|
+
* the right element for it, so the disclosure goes *inside* the heading as a
|
|
151
|
+
* real `<button aria-expanded aria-controls>` — the accordion pattern, and
|
|
152
|
+
* the same one `Table.GroupRow`'s note has always pointed at for a table that
|
|
153
|
+
* genuinely needs to fold. The outline is untouched: the heading list of a
|
|
154
|
+
* folding ledger and a plain one are identical, which is what lets a page
|
|
155
|
+
* offer both idioms over one dataset without the outline moving under a
|
|
156
|
+
* reader who switches.
|
|
157
|
+
*
|
|
158
|
+
* So it is one component with two forms rather than a `DisclosureLedger` beside
|
|
159
|
+
* it: everything a folding ledger needs — the gutter track, the sticky offsets,
|
|
160
|
+
* the label recipe, the type contract — is this component, and a sibling would
|
|
161
|
+
* have been a copy of all of it wrapped around one `<button>`.
|
|
162
|
+
*
|
|
163
|
+
* Expansion is controllable and stores nothing. *Which* places a reader has
|
|
164
|
+
* folded is a fact about the reader; consumers keep that set themselves and
|
|
165
|
+
* pass it back in. A folded run of rows is hidden rather than unmounted, so a
|
|
166
|
+
* group that keeps its own fold inside a tier still keeps it after the tier has
|
|
167
|
+
* been shut and opened over it.
|
|
168
|
+
*
|
|
169
|
+
* Static markup — no `"use client"` — **until a heading is asked to fold**, at
|
|
170
|
+
* which point that heading and its rows become a small client island and the
|
|
171
|
+
* rest of the ledger keeps rendering on the server.
|
|
85
172
|
*
|
|
86
173
|
* @example
|
|
87
174
|
* <Ledger.Root className="[--cue-data-row-cols:1fr_8rem_auto]">
|
|
@@ -94,6 +181,18 @@ interface LedgerEmptyProps extends React.ComponentPropsWithoutRef<"p"> {}
|
|
|
94
181
|
* </Ledger.Group>
|
|
95
182
|
* </Ledger.Tier>
|
|
96
183
|
* </Ledger.Root>
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* // Folded places are the reader's, so the reader's storage holds them.
|
|
187
|
+
* <Ledger.Tier
|
|
188
|
+
* heading="Ours"
|
|
189
|
+
* count={13}
|
|
190
|
+
* collapsible
|
|
191
|
+
* expanded={!folded.includes("ours")}
|
|
192
|
+
* onExpandedChange={(open) => remember("ours", open)}
|
|
193
|
+
* >
|
|
194
|
+
* <Ledger.Group label="released">…</Ledger.Group>
|
|
195
|
+
* </Ledger.Tier>
|
|
97
196
|
*/
|
|
98
197
|
declare const Ledger: {
|
|
99
198
|
Root: React.ForwardRefExoticComponent<LedgerRootProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -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) {
|
|
@@ -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
|
}
|
package/manifest/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"library": "@cueplusplus/ui",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
6
6
|
"themes": [
|
|
7
7
|
"cue",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"tokens": {
|
|
31
31
|
"path": "./tokens.json",
|
|
32
|
-
"sha256": "
|
|
32
|
+
"sha256": "6d7c8411b968edd7c1d6dca18d845ec52370da8f6364cc28454b48122fb3ead0"
|
|
33
33
|
},
|
|
34
34
|
"systemApis": [
|
|
35
35
|
{
|
|
@@ -3501,7 +3501,7 @@
|
|
|
3501
3501
|
"mdUrl": "/docs/components/data-tree.md",
|
|
3502
3502
|
"jsonUrl": "/r/components/data-tree.json",
|
|
3503
3503
|
"path": "./components/data-tree.json",
|
|
3504
|
-
"sha256": "
|
|
3504
|
+
"sha256": "3e68587c9fde809009a4a9661636a7b64dbe72b519cde4d6093bd36996106bdf"
|
|
3505
3505
|
},
|
|
3506
3506
|
{
|
|
3507
3507
|
"name": "GroupBar",
|
|
@@ -3521,13 +3521,13 @@
|
|
|
3521
3521
|
"slug": "ledger",
|
|
3522
3522
|
"group": "instruments",
|
|
3523
3523
|
"importPath": "@cueplusplus/ui",
|
|
3524
|
-
"summary": "Grouped dense rows under sticky tier headings and gutter slugs, with
|
|
3524
|
+
"summary": "Grouped dense rows under sticky tier headings and gutter slugs, with an opt-in disclosure that folds a heading without leaving the outline.",
|
|
3525
3525
|
"status": "stable",
|
|
3526
3526
|
"url": "/docs/components/ledger",
|
|
3527
3527
|
"mdUrl": "/docs/components/ledger.md",
|
|
3528
3528
|
"jsonUrl": "/r/components/ledger.json",
|
|
3529
3529
|
"path": "./components/ledger.json",
|
|
3530
|
-
"sha256": "
|
|
3530
|
+
"sha256": "52cc7f45dbf809e5be6eb04fc2420038143ee199d4f8bef29033d78197c20da6"
|
|
3531
3531
|
},
|
|
3532
3532
|
{
|
|
3533
3533
|
"name": "LogViewer",
|
package/manifest/tokens.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueplusplus/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "CUE++ design system components: Tailwind v4 styled wrappers over Base UI, driven by @cueplusplus/tokens.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
"clsx": "^2.1.1",
|
|
141
141
|
"culori": "^4.0.2",
|
|
142
142
|
"tailwind-merge": "^3.6.0",
|
|
143
|
-
"@cueplusplus/tokens": "0.
|
|
143
|
+
"@cueplusplus/tokens": "0.8.0"
|
|
144
144
|
},
|
|
145
145
|
"peerDependencies": {
|
|
146
146
|
"@assistant-ui/react": "^0.15.16",
|
|
@@ -253,8 +253,8 @@
|
|
|
253
253
|
"tsdown": "0.22.14",
|
|
254
254
|
"typescript": "^5.7.0",
|
|
255
255
|
"vitest": "^4.1.10",
|
|
256
|
-
"@repo/
|
|
257
|
-
"@repo/
|
|
256
|
+
"@repo/docgen": "0.0.0",
|
|
257
|
+
"@repo/typescript-config": "0.0.0"
|
|
258
258
|
},
|
|
259
259
|
"scripts": {
|
|
260
260
|
"build": "pnpm run check:manifest && tsdown",
|