@cueplusplus/ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/chrome/_drag.js +59 -0
  3. package/dist/chrome/_status-bar-item.js +85 -0
  4. package/dist/chrome/_status-bar.d.ts +15 -0
  5. package/dist/chrome/_status-bar.js +171 -0
  6. package/dist/chrome/app-shell.d.ts +69 -1
  7. package/dist/chrome/app-shell.js +38 -11
  8. package/dist/chrome/index.d.ts +3 -2
  9. package/dist/chrome/status-bar.d.ts +87 -4
  10. package/dist/chrome/status-bar.js +80 -16
  11. package/dist/chrome/title-bar.d.ts +11 -1
  12. package/dist/chrome/title-bar.js +1 -3
  13. package/dist/date/_segments.js +15 -4
  14. package/dist/forms/_chassis.d.ts +104 -4
  15. package/dist/forms/_chassis.js +104 -4
  16. package/dist/forms/input-group.d.ts +8 -3
  17. package/dist/forms/input-group.js +8 -3
  18. package/dist/forms/number-field.d.ts +3 -2
  19. package/dist/forms/number-field.js +3 -2
  20. package/dist/index.d.ts +7 -3
  21. package/dist/index.js +4 -1
  22. package/dist/instruments/_data-row.js +118 -0
  23. package/dist/instruments/data-row.d.ts +94 -0
  24. package/dist/instruments/data-row.js +151 -0
  25. package/dist/instruments/data-tree.d.ts +126 -0
  26. package/dist/instruments/data-tree.js +303 -0
  27. package/dist/instruments/index.d.ts +5 -2
  28. package/dist/instruments/index.js +4 -1
  29. package/dist/instruments/ledger.d.ts +105 -0
  30. package/dist/instruments/ledger.js +114 -0
  31. package/dist/instruments/table.d.ts +70 -2
  32. package/dist/instruments/table.js +133 -39
  33. package/dist/theming/_presets.js +3 -3
  34. package/manifest/components/app-shell.json +45 -4
  35. package/manifest/components/autocomplete.json +2 -0
  36. package/manifest/components/color-field.json +2 -0
  37. package/manifest/components/color-picker.json +2 -0
  38. package/manifest/components/combobox.json +2 -0
  39. package/manifest/components/composer.json +2 -0
  40. package/manifest/components/data-row.json +203 -0
  41. package/manifest/components/data-tree.json +151 -0
  42. package/manifest/components/date-field.json +2 -0
  43. package/manifest/components/date-picker.json +2 -0
  44. package/manifest/components/date-range-picker.json +2 -0
  45. package/manifest/components/env-var-input.json +2 -0
  46. package/manifest/components/input-group.json +4 -2
  47. package/manifest/components/input.json +3 -1
  48. package/manifest/components/ledger.json +187 -0
  49. package/manifest/components/multi-select.json +2 -0
  50. package/manifest/components/musical-time-input.json +2 -0
  51. package/manifest/components/number-field.json +3 -1
  52. package/manifest/components/otp-field.json +2 -0
  53. package/manifest/components/password-input.json +2 -0
  54. package/manifest/components/scrub-input.json +2 -0
  55. package/manifest/components/search-input.json +2 -0
  56. package/manifest/components/select.json +1 -0
  57. package/manifest/components/status-bar.json +146 -12
  58. package/manifest/components/table-scroll-region.json +6 -0
  59. package/manifest/components/table.json +88 -4
  60. package/manifest/components/tags-input.json +2 -0
  61. package/manifest/components/textarea.json +3 -1
  62. package/manifest/components/time-field.json +2 -0
  63. package/manifest/components/title-bar.json +1 -1
  64. package/manifest/components/toggle-group.json +2 -0
  65. package/manifest/components/toggle.json +2 -0
  66. package/manifest/manifest.json +72 -33
  67. package/manifest/tokens.json +1 -1
  68. package/package.json +4 -4
@@ -0,0 +1,151 @@
1
+ {
2
+ "name": "DataTree",
3
+ "slug": "data-tree",
4
+ "group": "instruments",
5
+ "importPath": "@cueplusplus/ui",
6
+ "peerDependencies": [],
7
+ "clientOnly": true,
8
+ "description": "A hierarchy of multi-column rows: the tree idiom over the `DataRow` anatomy.\n\n## The ARIA pattern, chosen before the component was written\n\n`role=\"treegrid\"` is **rejected** for this foundation. It is the semantically\nexact answer — tabular *and* hierarchical — and it is the one with the worst\nreal-world assistive-technology record: Adrian Roselli's survey of expando\ntables calls its support poor and notes that the APG's own worked example\ncarries unresolved issues; fancytree's maintainer describes treegrid support\nas unproven ground; MUI X shipped hierarchical rows with no `aria-level` /\n`aria-expanded` / `aria-setsize` at all and retrofitted them later. Treegrid\nearns its cost when cells are individually interactive or editable, because\nthen a reader genuinely needs two-dimensional cell navigation. This surface is\nread-and-navigate: you move down rows, fold a branch, and open the thing a row\nnames.\n\nSo the pattern is `role=\"tree\"` with `role=\"treeitem\"` rows — VS Code's\nchoice and Carbon's, and the best-supported composite pattern in ARIA:\n\n- A **flat DOM** with `aria-level` / `aria-posinset` / `aria-setsize` per row\n rather than nested `role=\"group\"` wrappers.\n- **Roving tabindex**: the tree is one Tab stop and the arrows move inside it.\n- The **APG tree keyboard map**: Up/Down between visible rows, Home/End to the\n ends, Right to unfold then to descend, Left to fold then to ascend,\n Enter/Space to select.\n- Expansion lives on the **row**, as `aria-expanded` on the `treeitem` — that\n is what tree semantics mean by a parent, and it is why the chevron here is\n `role=\"presentation\"` rather than a button. (The other half of this trade\n is `Table.GroupRow`, where the table's semantics own the grouping and a real\n `<button aria-expanded>` owns the disclosure. Two patterns, each internally\n consistent; what would be wrong is half of each.)\n\n**The documented trade**: columns are not independently navigable. A row's\nmulti-column content lives *inside* the treeitem, so a screen-reader user\nhears the row as one string rather than moving cell to cell through it. For a\ndense browse-and-open surface that is the better bargain — `tree` works today\non every screen reader, `treegrid` does not — but a surface whose cells are\ngenuinely editable has outgrown this component and wants a real treegrid from\na library that maintains one.\n\n## Everything else\n\nRows are the `DataRow` chassis: same padding, same hairline, same hover wash,\nsame inset focus ring, same `group/data-row`, so `DataRow.Actions` reveals in\na tree exactly as it does in a ledger. The row element itself is the\ntreeitem — one element owns focus, expansion, selection and the click reading\n— and the caller's `DataRow.*` slots are its content.\n\nA tree whose items declare no children anywhere is a flat list: no chevrons,\nno spacer column where a chevron would be, no guides, and `aria-expanded` on\nnothing. A leaf row that carried `aria-expanded=\"false\"` would tell a screen\nreader it has children it does not have.",
9
+ "props": [
10
+ {
11
+ "name": "items",
12
+ "type": "readonly DataTreeItem[]",
13
+ "required": true,
14
+ "defaultValue": null,
15
+ "description": "The roots, in display order."
16
+ },
17
+ {
18
+ "name": "expanded",
19
+ "type": "readonly string[]",
20
+ "required": false,
21
+ "defaultValue": null,
22
+ "description": "Expanded ids when controlled. Pass with `onExpandedChange`."
23
+ },
24
+ {
25
+ "name": "defaultExpanded",
26
+ "type": "readonly string[]",
27
+ "required": false,
28
+ "defaultValue": null,
29
+ "description": "Initially expanded ids when uncontrolled."
30
+ },
31
+ {
32
+ "name": "onExpandedChange",
33
+ "type": "((expanded: string[]) => void)",
34
+ "required": false,
35
+ "defaultValue": null,
36
+ "description": "Called with the whole next expanded set whenever a row is folded or unfolded."
37
+ },
38
+ {
39
+ "name": "selected",
40
+ "type": "string | null",
41
+ "required": false,
42
+ "defaultValue": null,
43
+ "description": "Selected id when controlled. Pass with `onSelect`."
44
+ },
45
+ {
46
+ "name": "defaultSelected",
47
+ "type": "string | null",
48
+ "required": false,
49
+ "defaultValue": "null",
50
+ "description": "Initially selected id when uncontrolled."
51
+ },
52
+ {
53
+ "name": "onSelect",
54
+ "type": "((id: string) => void)",
55
+ "required": false,
56
+ "defaultValue": null,
57
+ "description": "Called with the id of the row the user selected."
58
+ },
59
+ {
60
+ "name": "guides",
61
+ "type": "boolean",
62
+ "required": false,
63
+ "defaultValue": "true",
64
+ "description": "Draw the depth guides. Defaults to `true`.\n\nRoots never draw one — there is no ancestor to draw — so a flat tree draws\nnone at all whatever this is set to."
65
+ },
66
+ {
67
+ "name": "aria-label",
68
+ "type": "string",
69
+ "required": false,
70
+ "defaultValue": null,
71
+ "description": "Accessible name for the tree. Required in practice — name what it lists."
72
+ }
73
+ ],
74
+ "typeReferences": [
75
+ "Omit<React.ComponentPropsWithoutRef<\"div\">, \"onSelect\">"
76
+ ],
77
+ "subcomponents": [],
78
+ "parts": [],
79
+ "variants": {},
80
+ "defaultVariants": {},
81
+ "tokensUsed": [
82
+ "--cue-accent",
83
+ "--cue-accent-soft",
84
+ "--cue-data-row-cols",
85
+ "--cue-data-tree-indent",
86
+ "--cue-fg",
87
+ "--cue-fg-subtle",
88
+ "--cue-font-mono",
89
+ "--cue-hair",
90
+ "--cue-icon-sm",
91
+ "--cue-pad-row-x",
92
+ "--cue-pad-row-y",
93
+ "--cue-row-hover",
94
+ "--cue-space-3",
95
+ "--cue-text-ui"
96
+ ],
97
+ "summary": "A hierarchy of multi-column rows under real `role=\"tree\"` semantics, with CSS depth guides.",
98
+ "examples": [
99
+ {
100
+ "title": "Items carry rows, not labels",
101
+ "code": "const items: DataTreeItem[] = groups.map((group) => ({\n id: group.id,\n row: <DataRow.Label>{group.label}</DataRow.Label>,\n children: group.rows.map((device) => ({\n id: device.id,\n row: (\n <>\n <DataRow.Label>{device.name}</DataRow.Label>\n <DataRow.Meta>{device.latency}</DataRow.Meta>\n </>\n ),\n })),\n}));\n\n<DataTree\n aria-label=\"Patch\"\n className=\"[--cue-data-row-cols:1fr_6rem]\"\n items={items}\n defaultExpanded={[\"inputs\"]}\n selected={selected}\n onSelect={setSelected}\n/>;",
102
+ "language": "tsx"
103
+ },
104
+ {
105
+ "title": "The container, named",
106
+ "code": "<div className=\"patch [container-type:inline-size] [container-name:patch]\">\n <DataTree … />\n</div>",
107
+ "language": "tsx"
108
+ },
109
+ {
110
+ "title": "Narrow the step before you drop the guides",
111
+ "code": "@container patch (max-width: 32rem) {\n .patch [data-slot=\"data-tree\"] {\n --cue-data-tree-indent: 0.75rem;\n }\n}",
112
+ "language": "css"
113
+ },
114
+ {
115
+ "title": "Usage",
116
+ "code": "<DataTree\n aria-label=\"Patch\"\n items={nodes}\n defaultExpanded={[\"inputs\"]}\n selected={selected}\n onSelect={setSelected}\n className=\"[--cue-data-row-cols:1fr_8rem_auto]\"\n/>",
117
+ "language": "tsx"
118
+ }
119
+ ],
120
+ "status": "stable",
121
+ "url": "/docs/components/data-tree",
122
+ "mdUrl": "/docs/components/data-tree.md",
123
+ "jsonUrl": "/r/components/data-tree.json",
124
+ "whenToUse": [
125
+ "A grouped list the reader folds and unfolds, where each row still carries columns — a patch by bus, a corpus by stack, a plan by phase.",
126
+ "A hierarchy that has to keep working for a screen-reader user today: this is the `tree` pattern, which is the best-supported composite pattern in ARIA."
127
+ ],
128
+ "whenNotToUse": [
129
+ "A single-column outline of labels with a glyph gutter. Use `Tree`, which is that component.",
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 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
+ "commonMistakes": [
134
+ "Expecting cell-by-cell arrow navigation. Columns live inside the treeitem, so a row is announced as one string; that is the documented trade for the support `tree` has and `treegrid` does not.",
135
+ "Giving a leaf `children: []` to make it look like the others. An empty array *is* a parent — an empty folder — and it will carry `aria-expanded`.",
136
+ "Passing `expanded` without `onExpandedChange`, which freezes the tree: controlled means the caller owns the next state.",
137
+ "Fighting the guides with a row background. They are a `background-image` and survive hover and selection fills by design; switch them off with `guides={false}` instead."
138
+ ],
139
+ "specimens": [
140
+ {
141
+ "title": "DataTree",
142
+ "group": "instruments",
143
+ "components": [
144
+ "DataTree"
145
+ ],
146
+ "code": "<Panel className={`w-full ${PATCH_TRACKS}`}>\n <DataTree\n aria-label=\"Patch\"\n items={PATCH_TREE}\n defaultExpanded={[\"inputs\"]}\n defaultSelected=\"scarlett\"\n />\n</Panel>",
147
+ "note": "The same rows as a hierarchy: `role='tree'` with `treeitem` rows, chosen over `treegrid` for the support it actually has. Guides are a background gradient keyed off the row's depth, so a hover or selection fill cannot cover them.",
148
+ "interaction": "one tab stop: arrows move between rows, Right unfolds then descends, Left folds then climbs, Enter selects."
149
+ }
150
+ ]
151
+ }
@@ -35,12 +35,14 @@
35
35
  "--cue-accent",
36
36
  "--cue-accent-fg",
37
37
  "--cue-border",
38
+ "--cue-border-strong",
38
39
  "--cue-control-lg",
39
40
  "--cue-control-md",
40
41
  "--cue-control-sm",
41
42
  "--cue-danger",
42
43
  "--cue-fg",
43
44
  "--cue-fg-subtle",
45
+ "--cue-focus",
44
46
  "--cue-font-mono",
45
47
  "--cue-radius-control",
46
48
  "--cue-space-1",
@@ -145,12 +145,14 @@
145
145
  "tokensUsed": [
146
146
  "--cue-accent",
147
147
  "--cue-border",
148
+ "--cue-border-strong",
148
149
  "--cue-control-lg",
149
150
  "--cue-control-md",
150
151
  "--cue-control-sm",
151
152
  "--cue-danger",
152
153
  "--cue-fg",
153
154
  "--cue-fg-subtle",
155
+ "--cue-focus",
154
156
  "--cue-font-mono",
155
157
  "--cue-icon-sm",
156
158
  "--cue-radius-control",
@@ -145,12 +145,14 @@
145
145
  "tokensUsed": [
146
146
  "--cue-accent",
147
147
  "--cue-border",
148
+ "--cue-border-strong",
148
149
  "--cue-control-lg",
149
150
  "--cue-control-md",
150
151
  "--cue-control-sm",
151
152
  "--cue-danger",
152
153
  "--cue-fg",
153
154
  "--cue-fg-subtle",
155
+ "--cue-focus",
154
156
  "--cue-font-mono",
155
157
  "--cue-icon-sm",
156
158
  "--cue-radius-control",
@@ -102,12 +102,14 @@
102
102
  "tokensUsed": [
103
103
  "--cue-accent",
104
104
  "--cue-border",
105
+ "--cue-border-strong",
105
106
  "--cue-control-lg",
106
107
  "--cue-control-md",
107
108
  "--cue-control-sm",
108
109
  "--cue-danger",
109
110
  "--cue-fg",
110
111
  "--cue-fg-subtle",
112
+ "--cue-focus",
111
113
  "--cue-font-mono",
112
114
  "--cue-icon-md",
113
115
  "--cue-icon-sm",
@@ -5,7 +5,7 @@
5
5
  "importPath": "@cueplusplus/ui",
6
6
  "peerDependencies": [],
7
7
  "clientOnly": true,
8
- "description": "A control and its adornments, drawn as a single field.\n\nThe group — not the input — owns the chassis, so there is exactly one rim and\none fill however many affordances are bolted on. It publishes that fact\nthrough context: an {@link Input } rendered inside strips its own border,\nbackground, height and padding and inherits the group's font size. Focus\nfeedback moves to `focus-within`, since a `<div>` never matches\n`:focus-visible`.",
8
+ "description": "A control and its adornments, drawn as a single field.\n\nThe group — not the input — owns the chassis, so there is exactly one rim and\none fill however many affordances are bolted on. It publishes that fact\nthrough context: an {@link Input } rendered inside strips its own border,\nbackground, height and padding and inherits the group's font size.\n\nFocus feedback splits in two, because a `<div>` matches neither `:focus` nor\n`:focus-visible` itself. The rim strengthens on `:focus-within` — anything\nfocused inside, however it got there — while the ring waits for\n`:has(:focus-visible)`, so a mouse click into the field does not light the\nwhole cluster up. Both halves live on the wrapper; the inner control paints\nno ring of its own.",
9
9
  "props": [
10
10
  {
11
11
  "name": "leading",
@@ -46,12 +46,14 @@
46
46
  "tokensUsed": [
47
47
  "--cue-accent",
48
48
  "--cue-border",
49
+ "--cue-border-strong",
49
50
  "--cue-control-lg",
50
51
  "--cue-control-md",
51
52
  "--cue-control-sm",
52
53
  "--cue-danger",
53
54
  "--cue-fg",
54
55
  "--cue-fg-subtle",
56
+ "--cue-focus",
55
57
  "--cue-font-mono",
56
58
  "--cue-radius-control",
57
59
  "--cue-space-2",
@@ -84,7 +86,7 @@
84
86
  ],
85
87
  "code": "<Specimens>\n <Specimen label=\"leading\">\n <InputGroup leading={<Plug aria-hidden=\"true\" className=\"size-icon-md\" />}>\n <Input aria-label=\"Node address\" defaultValue=\"10.0.0.42\" />\n </InputGroup>\n </Specimen>\n <Specimen label=\"trailing\">\n <InputGroup trailing={<span className=\"font-mono text-fg-subtle\">%</span>}>\n <Input aria-label=\"Level\" defaultValue=\"80\" />\n </InputGroup>\n </Specimen>\n <Specimen label=\"both, sm\">\n <InputGroup\n size=\"sm\"\n leading={<Ruler aria-hidden=\"true\" className=\"size-icon-sm\" />}\n trailing={<span className=\"font-mono text-fg-subtle\">m</span>}\n >\n <Input aria-label=\"Throw distance\" defaultValue=\"7.5\" />\n </InputGroup>\n </Specimen>\n</Specimens>",
86
88
  "note": "One rim around a control and its adornments, so the group focuses as a unit.",
87
- "interaction": "focus inside the group lights the whole rim, not just the input."
89
+ "interaction": "any focus inside strengthens the whole rim; the ring waits for a keyboard-visible descendant, so a click on an adornment does not light the cluster up. The inner control never rings — the group owns the rim."
88
90
  }
89
91
  ],
90
92
  "todo": true
@@ -32,12 +32,14 @@
32
32
  "tokensUsed": [
33
33
  "--cue-accent",
34
34
  "--cue-border",
35
+ "--cue-border-strong",
35
36
  "--cue-control-lg",
36
37
  "--cue-control-md",
37
38
  "--cue-control-sm",
38
39
  "--cue-danger",
39
40
  "--cue-fg",
40
41
  "--cue-fg-subtle",
42
+ "--cue-focus",
41
43
  "--cue-font-mono",
42
44
  "--cue-radius-control",
43
45
  "--cue-space-3",
@@ -83,7 +85,7 @@
83
85
  ],
84
86
  "code": "<Specimens>\n {SIZES.map((size) => (\n <Specimen key={size} label={size}>\n <Input aria-label={`Cue label, ${size}`} size={size} defaultValue=\"1/12 @ 80%\" />\n </Specimen>\n ))}\n <Specimen label=\"placeholder\">\n <Input aria-label=\"Empty cue label\" placeholder=\"e.g. Blackout\" />\n </Specimen>\n <Specimen label=\"disabled\">\n <Input aria-label=\"Disabled cue label\" defaultValue=\"Read-only\" disabled />\n </Specimen>\n <Specimen label=\"readonly\">\n <Input aria-label=\"Read-only cue label\" defaultValue=\"1/12 @ 80%\" readOnly />\n </Specimen>\n</Specimens>",
85
87
  "note": "The recessed chassis: sunken fill, hairline rim, height from the density ladder.",
86
- "interaction": "the rim goes accent on focus and the ring sits outside it; `invalid` turns the rim danger and outranks focus."
88
+ "interaction": "the rim strengthens one rung on focus and a single flush ring in the theme's focus colour sits just inside it — clicking in shows the same ring as tabbing in, because a typing surface is always focus-visible; `invalid` keeps the danger rim and the ring stays visible beside it."
87
89
  }
88
90
  ]
89
91
  }
@@ -0,0 +1,187 @@
1
+ {
2
+ "name": "Ledger",
3
+ "slug": "ledger",
4
+ "group": "instruments",
5
+ "importPath": "@cueplusplus/ui",
6
+ "peerDependencies": [],
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, and\nthe one that does the least: headings, slugs, rows. **Nothing here collapses.**\nA ledger with a disclosure on it would be a tree with worse semantics, so the\ncomponent emits no `aria-expanded` anywhere, and a test holds it to that —\ncollapse belongs to `DataTree`, and grouping-with-columns belongs to a\ngrouped `Table`.\n\nWhat it does own is the document outline. Tiers and groups are real `<h2>` /\n`<h3>` elements at caller-chosen levels, because in a list of two hundred\nrows the heading list *is* how a screen-reader user navigates.\n\nStatic markup — no `\"use client\"`. The rows inside it may be anything;\n`DataRow` is the intended tenant.",
9
+ "props": [],
10
+ "typeReferences": [],
11
+ "subcomponents": [
12
+ "Root",
13
+ "Tier",
14
+ "Group",
15
+ "Empty"
16
+ ],
17
+ "parts": [
18
+ {
19
+ "name": "Root",
20
+ "usage": "Ledger.Root",
21
+ "description": "The ledger surface: the type contract, and the container the columns shed\nagainst.\n\nDeliberately unframed. A ledger is a continuous document of rows, so the\nhairlines belong to the rows and there is no border, radius or card around\nany part of it — a per-group card would turn one list into a stack of little\ntables and break the top-to-bottom scan the whole idiom exists for.",
22
+ "props": [
23
+ {
24
+ "name": "containment",
25
+ "type": "boolean",
26
+ "required": false,
27
+ "defaultValue": "true",
28
+ "description": "Make the ledger a size container, so a caller's `@container` rules shed\ncolumns against the *pane* rather than the viewport. Defaults to `true`.\n\nThis is the difference between a list that sheds a column when the window\nnarrows and one that also sheds it when a detail drawer opens beside it and\ntakes half the pane — which is the same visual problem and, without a\ncontainer, an entirely separate piece of code. Switch it off only when an\nancestor is already the container the rules name; nesting containers moves\nwhich one a bare `@container` query resolves against."
29
+ }
30
+ ],
31
+ "typeReferences": [
32
+ "React.ComponentPropsWithoutRef<\"div\">"
33
+ ]
34
+ },
35
+ {
36
+ "name": "Tier",
37
+ "usage": "Ledger.Tier",
38
+ "description": "One named run of the ledger, with its own sticky heading.\n\nThe heading paints its own ground (`bg-bg`) because a sticky element with a\ntransparent background is a sticky element you can read rows straight through.",
39
+ "props": [
40
+ {
41
+ "name": "heading",
42
+ "type": "ReactNode",
43
+ "required": false,
44
+ "defaultValue": null,
45
+ "description": "The tier's name. Omitted, the tier draws no heading and reserves no space\nfor one — which is how a single unnamed run of rows uses the same component\nas a grouped one."
46
+ },
47
+ {
48
+ "name": "count",
49
+ "type": "number",
50
+ "required": false,
51
+ "defaultValue": null,
52
+ "description": "How many rows the tier holds, printed beside its name."
53
+ },
54
+ {
55
+ "name": "sticky",
56
+ "type": "boolean",
57
+ "required": false,
58
+ "defaultValue": "true",
59
+ "description": "Dock the heading to the top of the scroll container while the tier's rows\nscroll past. Defaults to `true`."
60
+ },
61
+ {
62
+ "name": "headingLevel",
63
+ "type": "4 | 2 | 3",
64
+ "required": false,
65
+ "defaultValue": "2",
66
+ "description": "Which heading element the tier's name is. Defaults to `2`.\n\nThe outline is the navigation: a screen-reader user moves through a long\nledger by heading, so the levels have to describe the real nesting of the\npage the ledger sits in, not the ledger's own idea of itself."
67
+ },
68
+ {
69
+ "name": "note",
70
+ "type": "ReactNode",
71
+ "required": false,
72
+ "defaultValue": null,
73
+ "description": "A qualifier printed after the name — a date, a source, a state."
74
+ }
75
+ ],
76
+ "typeReferences": [
77
+ "React.ComponentPropsWithoutRef<\"section\">"
78
+ ]
79
+ },
80
+ {
81
+ "name": "Group",
82
+ "usage": "Ledger.Group",
83
+ "description": "A run of rows under a slug in the left gutter.\n\nThe gutter is a grid track whose width is the custom property\n`--cue-ledger-gutter` (default `7rem`), so a `@container` rule can narrow it\nat the same breakpoint it sheds a column, without this component knowing\nanything about the caller's breakpoints.\n\nThe slug is `self-start`, which is the load-bearing half of making it stick:\na stretched grid item is exactly as tall as the group, and an element as tall\nas its own scroll range has nowhere to slide to.",
84
+ "props": [
85
+ {
86
+ "name": "label",
87
+ "type": "ReactNode",
88
+ "required": false,
89
+ "defaultValue": null,
90
+ "description": "The group's slug, printed in the left gutter.\n\nOmitted, the group is a plain block: no gutter, no heading, no indent. A\ngrouped list that has one unnamed run in it should not pay a 7rem empty\ncolumn for it, and an empty gutter reads as a missing label rather than as\nan absent one."
91
+ },
92
+ {
93
+ "name": "headingLevel",
94
+ "type": "4 | 3",
95
+ "required": false,
96
+ "defaultValue": "3",
97
+ "description": "Which heading element the slug is. Defaults to `3`."
98
+ },
99
+ {
100
+ "name": "sticky",
101
+ "type": "boolean",
102
+ "required": false,
103
+ "defaultValue": "true",
104
+ "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."
105
+ }
106
+ ],
107
+ "typeReferences": [
108
+ "React.ComponentPropsWithoutRef<\"div\">"
109
+ ]
110
+ },
111
+ {
112
+ "name": "Empty",
113
+ "usage": "Ledger.Empty",
114
+ "description": "The line that stands where a group's rows would be.\n\nA slot rather than an `empty` prop on the group, because there are two\ndifferent emptinesses and only the caller knows which one this is: a place\nsomebody made that has nothing in it yet (\"Nothing yet.\") and a filter that\nemptied a place that does have things in it (\"Nothing matches …\"). A prop\nwould have to guess, and guessing conflates them — which is precisely the bug\nthis slot exists to make impossible. For a whole surface with nothing in it\nand no heading left to keep, use `EmptyState` instead.",
115
+ "props": [],
116
+ "typeReferences": [
117
+ "React.ComponentPropsWithoutRef<\"p\">"
118
+ ]
119
+ }
120
+ ],
121
+ "variants": {},
122
+ "defaultVariants": {},
123
+ "tokensUsed": [
124
+ "--cue-bg",
125
+ "--cue-data-row-cols",
126
+ "--cue-fg",
127
+ "--cue-fg-subtle",
128
+ "--cue-font-mono",
129
+ "--cue-ledger-gutter",
130
+ "--cue-ledger-slug-top",
131
+ "--cue-pad-row-x",
132
+ "--cue-pad-row-y",
133
+ "--cue-space-2",
134
+ "--cue-space-3",
135
+ "--cue-text-label",
136
+ "--cue-text-ui"
137
+ ],
138
+ "summary": "Grouped dense rows under sticky tier headings and gutter slugs, with no disclosure anywhere.",
139
+ "examples": [
140
+ {
141
+ "title": "Tiers, groups, rows",
142
+ "code": "<Ledger.Root className=\"[--cue-data-row-cols:1fr_8rem_auto] [--cue-ledger-slug-top:1.75rem]\">\n <Ledger.Tier heading=\"Patch\" count={5} note=\"polled 18:04\">\n <Ledger.Group label=\"inputs\">\n {inputs.map((device) => (\n <DataRow.Root key={device.id} interactive onActivate={() => open(device)}>\n …\n </DataRow.Root>\n ))}\n </Ledger.Group>\n <Ledger.Group label=\"spare\">\n <Ledger.Empty>Nothing yet.</Ledger.Empty>\n </Ledger.Group>\n </Ledger.Tier>\n</Ledger.Root>",
143
+ "language": "tsx"
144
+ },
145
+ {
146
+ "title": "One unnamed group",
147
+ "code": "<Ledger.Tier>\n <Ledger.Group>\n <DataRow.Root>…</DataRow.Root>\n </Ledger.Group>\n</Ledger.Tier>",
148
+ "language": "tsx"
149
+ },
150
+ {
151
+ "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>",
153
+ "language": "tsx"
154
+ }
155
+ ],
156
+ "status": "stable",
157
+ "url": "/docs/components/ledger",
158
+ "mdUrl": "/docs/components/ledger.md",
159
+ "jsonUrl": "/r/components/ledger.json",
160
+ "whenToUse": [
161
+ "A long list that is grouped but never folded: a corpus by source, a patch by direction, a run log by day.",
162
+ "A surface whose groups should be reachable by heading, since the tiers and slugs are real `h2`/`h3` elements."
163
+ ],
164
+ "whenNotToUse": [
165
+ "A hierarchy the reader folds. Use `DataTree`, which owns collapse and the tree semantics that go with it.",
166
+ "Rows meant to be compared down a named column. Use a grouped `Table`, where the columns are named once in a `thead`.",
167
+ "A handful of rows in a panel. Use `Row` inside `Panel`; a tier heading over three rows is furniture with nothing to organise."
168
+ ],
169
+ "commonMistakes": [
170
+ "Adding a toggle to a tier. A ledger that folds is a tree with worse semantics — the component emits no `aria-expanded`, and a test holds it to that.",
171
+ "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
+ "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
+ "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."
174
+ ],
175
+ "specimens": [
176
+ {
177
+ "title": "Ledger",
178
+ "group": "instruments",
179
+ "components": [
180
+ "Ledger"
181
+ ],
182
+ "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
+ "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
+ "interaction": "the tier heading and the group slug dock as the rows scroll under them; scroll the pane to see it."
185
+ }
186
+ ]
187
+ }
@@ -137,6 +137,7 @@
137
137
  "--cue-accent-soft",
138
138
  "--cue-border",
139
139
  "--cue-border-overlay",
140
+ "--cue-border-strong",
140
141
  "--cue-chip-h",
141
142
  "--cue-control-lg",
142
143
  "--cue-control-md",
@@ -145,6 +146,7 @@
145
146
  "--cue-fg",
146
147
  "--cue-fg-muted",
147
148
  "--cue-fg-subtle",
149
+ "--cue-focus",
148
150
  "--cue-font-mono",
149
151
  "--cue-icon-sm",
150
152
  "--cue-radius-control",
@@ -67,12 +67,14 @@
67
67
  "tokensUsed": [
68
68
  "--cue-accent",
69
69
  "--cue-border",
70
+ "--cue-border-strong",
70
71
  "--cue-control-lg",
71
72
  "--cue-control-md",
72
73
  "--cue-control-sm",
73
74
  "--cue-danger",
74
75
  "--cue-fg",
75
76
  "--cue-fg-subtle",
77
+ "--cue-focus",
76
78
  "--cue-font-mono",
77
79
  "--cue-radius-control",
78
80
  "--cue-space-2",
@@ -5,7 +5,7 @@
5
5
  "importPath": "@cueplusplus/ui",
6
6
  "peerDependencies": [],
7
7
  "clientOnly": true,
8
- "description": "A numeric control with explicit − / + affordances and no native spinners.\n\nNative `<input type=\"number\">` spinners are unusable at console density — a\npair of 4px arrows that only appear on hover — so this is a text input with\n`inputMode=\"numeric\"` flanked by real buttons, which is also what makes it\ntouch-usable. Arrow keys step, Shift steps by `largeStep`, Alt by `smallStep`,\nand values typed out of range are clamped to `min`/`max` on blur.\n\nThe group owns the rim; the input and both buttons render bare inside it, so\nthe whole cluster reads as one field and focus feedback moves to\n`focus-within`.",
8
+ "description": "A numeric control with explicit − / + affordances and no native spinners.\n\nNative `<input type=\"number\">` spinners are unusable at console density — a\npair of 4px arrows that only appear on hover — so this is a text input with\n`inputMode=\"numeric\"` flanked by real buttons, which is also what makes it\ntouch-usable. Arrow keys step, Shift steps by `largeStep`, Alt by `smallStep`,\nand values typed out of range are clamped to `min`/`max` on blur.\n\nThe group owns the rim; the input and both buttons render bare inside it, so\nthe whole cluster reads as one field. Focus feedback follows the group's\nsplit: the rim strengthens on `:focus-within`, and the ring waits for\n`:has(:focus-visible)` so a click on a stepper does not paint one.",
9
9
  "props": [
10
10
  {
11
11
  "name": "className",
@@ -60,6 +60,7 @@
60
60
  "tokensUsed": [
61
61
  "--cue-accent",
62
62
  "--cue-border",
63
+ "--cue-border-strong",
63
64
  "--cue-control-lg",
64
65
  "--cue-control-md",
65
66
  "--cue-control-sm",
@@ -67,6 +68,7 @@
67
68
  "--cue-fg",
68
69
  "--cue-fg-muted",
69
70
  "--cue-fg-subtle",
71
+ "--cue-focus",
70
72
  "--cue-font-mono",
71
73
  "--cue-radius-control",
72
74
  "--cue-space-2",
@@ -81,12 +81,14 @@
81
81
  "tokensUsed": [
82
82
  "--cue-accent",
83
83
  "--cue-border",
84
+ "--cue-border-strong",
84
85
  "--cue-control-lg",
85
86
  "--cue-control-md",
86
87
  "--cue-control-sm",
87
88
  "--cue-danger",
88
89
  "--cue-fg",
89
90
  "--cue-fg-subtle",
91
+ "--cue-focus",
90
92
  "--cue-font-mono",
91
93
  "--cue-radius-control",
92
94
  "--cue-space-1",
@@ -74,12 +74,14 @@
74
74
  "tokensUsed": [
75
75
  "--cue-accent",
76
76
  "--cue-border",
77
+ "--cue-border-strong",
77
78
  "--cue-control-lg",
78
79
  "--cue-control-md",
79
80
  "--cue-control-sm",
80
81
  "--cue-danger",
81
82
  "--cue-fg",
82
83
  "--cue-fg-subtle",
84
+ "--cue-focus",
83
85
  "--cue-font-mono",
84
86
  "--cue-icon-md",
85
87
  "--cue-radius-control",
@@ -144,12 +144,14 @@
144
144
  "tokensUsed": [
145
145
  "--cue-accent",
146
146
  "--cue-border",
147
+ "--cue-border-strong",
147
148
  "--cue-control-lg",
148
149
  "--cue-control-md",
149
150
  "--cue-control-sm",
150
151
  "--cue-danger",
151
152
  "--cue-fg",
152
153
  "--cue-fg-subtle",
154
+ "--cue-focus",
153
155
  "--cue-font-mono",
154
156
  "--cue-radius-control",
155
157
  "--cue-space-2",
@@ -88,12 +88,14 @@
88
88
  "tokensUsed": [
89
89
  "--cue-accent",
90
90
  "--cue-border",
91
+ "--cue-border-strong",
91
92
  "--cue-control-lg",
92
93
  "--cue-control-md",
93
94
  "--cue-control-sm",
94
95
  "--cue-danger",
95
96
  "--cue-fg",
96
97
  "--cue-fg-subtle",
98
+ "--cue-focus",
97
99
  "--cue-font-mono",
98
100
  "--cue-icon-md",
99
101
  "--cue-icon-sm",
@@ -138,6 +138,7 @@
138
138
  "--cue-fg",
139
139
  "--cue-fg-muted",
140
140
  "--cue-fg-subtle",
141
+ "--cue-focus",
141
142
  "--cue-font-mono",
142
143
  "--cue-icon-md",
143
144
  "--cue-icon-sm",