@cueplusplus/ui 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/dist/chrome/_drag.js +59 -0
- package/dist/chrome/_status-bar-item.js +85 -0
- package/dist/chrome/_status-bar.d.ts +15 -0
- package/dist/chrome/_status-bar.js +171 -0
- package/dist/chrome/app-shell.d.ts +69 -1
- package/dist/chrome/app-shell.js +38 -11
- package/dist/chrome/index.d.ts +3 -2
- package/dist/chrome/status-bar.d.ts +87 -4
- package/dist/chrome/status-bar.js +80 -16
- package/dist/chrome/title-bar.d.ts +11 -1
- package/dist/chrome/title-bar.js +1 -3
- package/dist/date/_segments.js +15 -4
- package/dist/forms/_chassis.d.ts +104 -4
- package/dist/forms/_chassis.js +104 -4
- package/dist/forms/input-group.d.ts +8 -3
- package/dist/forms/input-group.js +8 -3
- package/dist/forms/number-field.d.ts +3 -2
- package/dist/forms/number-field.js +3 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +5 -1
- package/dist/instruments/_data-row.d.ts +68 -0
- package/dist/instruments/_data-row.js +139 -0
- package/dist/instruments/data-row.d.ts +94 -0
- package/dist/instruments/data-row.js +151 -0
- package/dist/instruments/data-tree.d.ts +126 -0
- package/dist/instruments/data-tree.js +303 -0
- package/dist/instruments/index.d.ts +6 -2
- package/dist/instruments/index.js +5 -1
- package/dist/instruments/ledger.d.ts +105 -0
- package/dist/instruments/ledger.js +114 -0
- package/dist/instruments/table.d.ts +108 -2
- package/dist/instruments/table.js +165 -39
- package/dist/theming/_presets.js +3 -3
- package/manifest/components/app-shell.json +45 -4
- package/manifest/components/autocomplete.json +2 -0
- package/manifest/components/color-field.json +2 -0
- package/manifest/components/color-picker.json +2 -0
- package/manifest/components/combobox.json +2 -0
- package/manifest/components/composer.json +2 -0
- package/manifest/components/data-row.json +208 -0
- package/manifest/components/data-tree.json +151 -0
- package/manifest/components/date-field.json +2 -0
- package/manifest/components/date-picker.json +2 -0
- package/manifest/components/date-range-picker.json +2 -0
- package/manifest/components/env-var-input.json +2 -0
- package/manifest/components/input-group.json +4 -2
- package/manifest/components/input.json +3 -1
- package/manifest/components/ledger.json +187 -0
- package/manifest/components/multi-select.json +2 -0
- package/manifest/components/musical-time-input.json +2 -0
- package/manifest/components/number-field.json +3 -1
- package/manifest/components/otp-field.json +2 -0
- package/manifest/components/password-input.json +2 -0
- package/manifest/components/scrub-input.json +2 -0
- package/manifest/components/search-input.json +2 -0
- package/manifest/components/select.json +1 -0
- package/manifest/components/status-bar.json +146 -12
- package/manifest/components/table-scroll-region.json +6 -0
- package/manifest/components/table.json +113 -5
- package/manifest/components/tags-input.json +2 -0
- package/manifest/components/textarea.json +3 -1
- package/manifest/components/time-field.json +2 -0
- package/manifest/components/title-bar.json +1 -1
- package/manifest/components/toggle-group.json +2 -0
- package/manifest/components/toggle.json +2 -0
- package/manifest/manifest.json +72 -33
- package/manifest/tokens.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/instruments/ledger.tsx
|
|
5
|
+
/**
|
|
6
|
+
* The one type treatment every heading in the ledger wears: the mono uppercase
|
|
7
|
+
* label of `SectionHeader`, `Eyebrow` and `Table.Head`.
|
|
8
|
+
*
|
|
9
|
+
* Declared once here rather than per heading level so a tier and a group slug
|
|
10
|
+
* can never drift into two label styles; `font-normal` is the part that has to
|
|
11
|
+
* be said out loud, because `<h2>` and `<h3>` come bold by default and a bold
|
|
12
|
+
* micro-label in a dense list reads as a second kind of emphasis.
|
|
13
|
+
*/
|
|
14
|
+
const LEDGER_LABEL = "font-mono text-(length:--cue-text-label) font-normal tracking-[0.15em] text-fg-subtle uppercase";
|
|
15
|
+
/** `headingLevel` as a tag name, so the outline is real elements, not roles. */
|
|
16
|
+
const headingTag = (level) => level === 2 ? "h2" : level === 3 ? "h3" : "h4";
|
|
17
|
+
/**
|
|
18
|
+
* A grouped run of dense rows under sticky headings: the ledger idiom.
|
|
19
|
+
*
|
|
20
|
+
* The flattest of the three surfaces this package ships over one row model, and
|
|
21
|
+
* the one that does the least: headings, slugs, rows. **Nothing here collapses.**
|
|
22
|
+
* A ledger with a disclosure on it would be a tree with worse semantics, so the
|
|
23
|
+
* component emits no `aria-expanded` anywhere, and a test holds it to that —
|
|
24
|
+
* collapse belongs to `DataTree`, and grouping-with-columns belongs to a
|
|
25
|
+
* grouped `Table`.
|
|
26
|
+
*
|
|
27
|
+
* What it does own is the document outline. Tiers and groups are real `<h2>` /
|
|
28
|
+
* `<h3>` elements at caller-chosen levels, because in a list of two hundred
|
|
29
|
+
* rows the heading list *is* how a screen-reader user navigates.
|
|
30
|
+
*
|
|
31
|
+
* Static markup — no `"use client"`. The rows inside it may be anything;
|
|
32
|
+
* `DataRow` is the intended tenant.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* <Ledger.Root className="[--cue-data-row-cols:1fr_8rem_auto]">
|
|
36
|
+
* <Ledger.Tier heading="Ours" count={12}>
|
|
37
|
+
* <Ledger.Group label="released">
|
|
38
|
+
* <DataRow.Root interactive onActivate={open}>…</DataRow.Root>
|
|
39
|
+
* </Ledger.Group>
|
|
40
|
+
* <Ledger.Group label="lab">
|
|
41
|
+
* <Ledger.Empty>Nothing yet.</Ledger.Empty>
|
|
42
|
+
* </Ledger.Group>
|
|
43
|
+
* </Ledger.Tier>
|
|
44
|
+
* </Ledger.Root>
|
|
45
|
+
*/
|
|
46
|
+
const Ledger = {
|
|
47
|
+
Root: React.forwardRef(function LedgerRoot({ className, containment = true, ...elementProps }, ref) {
|
|
48
|
+
return /* @__PURE__ */ jsx("div", {
|
|
49
|
+
ref,
|
|
50
|
+
"data-slot": "ledger",
|
|
51
|
+
className: cn("flex flex-col font-mono text-(length:--cue-text-ui) text-fg", containment ? "[container-type:inline-size]" : null, className),
|
|
52
|
+
...elementProps
|
|
53
|
+
});
|
|
54
|
+
}),
|
|
55
|
+
Tier: React.forwardRef(function LedgerTier({ className, heading, count, sticky = true, headingLevel = 2, note, children, ...elementProps }, ref) {
|
|
56
|
+
const Heading = headingTag(headingLevel);
|
|
57
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
58
|
+
ref,
|
|
59
|
+
"data-slot": "ledger-tier",
|
|
60
|
+
className: cn("flex flex-col", className),
|
|
61
|
+
...elementProps,
|
|
62
|
+
children: [heading === void 0 ? null : /* @__PURE__ */ jsxs(Heading, {
|
|
63
|
+
"data-slot": "ledger-tier-heading",
|
|
64
|
+
className: cn(LEDGER_LABEL, "flex items-baseline gap-(--cue-space-2) py-(--cue-pad-row-y)", sticky ? "sticky top-0 z-2 bg-bg" : null),
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsx("span", {
|
|
67
|
+
"data-slot": "ledger-tier-label",
|
|
68
|
+
children: heading
|
|
69
|
+
}),
|
|
70
|
+
count === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
71
|
+
"data-slot": "ledger-tier-count",
|
|
72
|
+
className: "tabular-nums",
|
|
73
|
+
children: count
|
|
74
|
+
}),
|
|
75
|
+
note === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
76
|
+
"data-slot": "ledger-tier-note",
|
|
77
|
+
className: "tracking-normal normal-case",
|
|
78
|
+
children: note
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
}), children]
|
|
82
|
+
});
|
|
83
|
+
}),
|
|
84
|
+
Group: React.forwardRef(function LedgerGroup({ className, label, headingLevel = 3, sticky = true, children, ...elementProps }, ref) {
|
|
85
|
+
const Heading = headingTag(headingLevel);
|
|
86
|
+
const labelled = label !== void 0;
|
|
87
|
+
return /* @__PURE__ */ jsx("div", {
|
|
88
|
+
ref,
|
|
89
|
+
"data-slot": "ledger-group",
|
|
90
|
+
"data-labelled": labelled ? "" : void 0,
|
|
91
|
+
className: cn(labelled ? "grid [grid-template-columns:var(--cue-ledger-gutter,7rem)_minmax(0,1fr)]" : "flex flex-col", className),
|
|
92
|
+
...elementProps,
|
|
93
|
+
children: labelled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
94
|
+
"data-slot": "ledger-group-label",
|
|
95
|
+
className: cn(LEDGER_LABEL, "min-w-0 truncate py-(--cue-pad-row-y) pr-(--cue-space-3)", sticky ? "sticky self-start [top:var(--cue-ledger-slug-top,2.5rem)] bg-bg" : null),
|
|
96
|
+
children: label
|
|
97
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
98
|
+
"data-slot": "ledger-group-rows",
|
|
99
|
+
className: "flex min-w-0 flex-col",
|
|
100
|
+
children
|
|
101
|
+
})] }) : children
|
|
102
|
+
});
|
|
103
|
+
}),
|
|
104
|
+
Empty: React.forwardRef(function LedgerEmpty({ className, ...elementProps }, ref) {
|
|
105
|
+
return /* @__PURE__ */ jsx("p", {
|
|
106
|
+
ref,
|
|
107
|
+
"data-slot": "ledger-empty",
|
|
108
|
+
className: cn("px-(--cue-pad-row-x) py-(--cue-pad-row-y) font-mono text-(length:--cue-text-ui) text-fg-subtle", className),
|
|
109
|
+
...elementProps
|
|
110
|
+
});
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
//#endregion
|
|
114
|
+
export { Ledger };
|
|
@@ -14,10 +14,107 @@ interface TableScrollRegionProps extends Omit<React.ComponentPropsWithoutRef<"di
|
|
|
14
14
|
declare const TableScrollRegion: React.ForwardRefExoticComponent<TableScrollRegionProps & React.RefAttributes<HTMLDivElement>>;
|
|
15
15
|
interface TableRootProps extends React.ComponentPropsWithoutRef<"table"> {}
|
|
16
16
|
interface TableSectionProps extends React.ComponentPropsWithoutRef<"tbody"> {}
|
|
17
|
-
interface TableRowProps extends React.ComponentPropsWithoutRef<"tr"> {
|
|
17
|
+
interface TableRowProps extends React.ComponentPropsWithoutRef<"tr"> {
|
|
18
|
+
/**
|
|
19
|
+
* The row responds to a pointer and to focus: the row-hover wash, a pointer
|
|
20
|
+
* cursor across the whole row, and an inset focus ring. Defaults to `false`.
|
|
21
|
+
*
|
|
22
|
+
* Opt-in rather than automatic, because a table of figures nobody can click
|
|
23
|
+
* that lit up under the pointer would be promising an interaction it does not
|
|
24
|
+
* have. Wiring the row up to actually do something — a click handler, a
|
|
25
|
+
* keyboard path to the same thing — stays the caller's job; this is the paint.
|
|
26
|
+
*
|
|
27
|
+
* **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.
|
|
39
|
+
*/
|
|
40
|
+
interactive?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Draw the row as picked out of the table. Defaults to `false`.
|
|
43
|
+
*
|
|
44
|
+
* Sets `data-selected`, which is what the tint and the hover-revealed
|
|
45
|
+
* `DataRow.Actions` inside the row both key off — so a hand-written
|
|
46
|
+
* `<tr data-selected>` in a data-driven body gets exactly the same treatment.
|
|
47
|
+
*/
|
|
48
|
+
selected?: boolean;
|
|
49
|
+
}
|
|
18
50
|
interface TableHeadProps extends React.ComponentPropsWithoutRef<"th"> {
|
|
19
51
|
/** Right-align this column and set it in tabular figures. */
|
|
20
52
|
numeric?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Dock the header band to the top of the scroll container while the rows
|
|
55
|
+
* scroll under it. Defaults to `false`.
|
|
56
|
+
*
|
|
57
|
+
* Set it on the cells, not on the `<tr>`: sticky on a table row is honoured
|
|
58
|
+
* inconsistently across engines, and the cell is the reliable baseline
|
|
59
|
+
* everywhere.
|
|
60
|
+
*/
|
|
61
|
+
sticky?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** The heading elements a group divider's label may be asked to be. */
|
|
64
|
+
type TableGroupHeadingLevel = 2 | 3 | 4 | 5 | 6;
|
|
65
|
+
interface TableGroupRowProps extends Omit<React.ComponentPropsWithoutRef<"tr">, "children"> {
|
|
66
|
+
/**
|
|
67
|
+
* How many columns the divider spans. Required: it must equal the table's
|
|
68
|
+
* column count, or the group's cell stops covering the row and the columns
|
|
69
|
+
* below it shift by one.
|
|
70
|
+
*/
|
|
71
|
+
span: number;
|
|
72
|
+
/** What the group is called. */
|
|
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;
|
|
97
|
+
/** How many rows the group holds, printed beside its name. */
|
|
98
|
+
count?: number;
|
|
99
|
+
/**
|
|
100
|
+
* Dock the divider under the header band while the group's rows scroll past.
|
|
101
|
+
* Defaults to `false`.
|
|
102
|
+
*
|
|
103
|
+
* The offset is the custom property `--cue-table-group-top` (default `0`):
|
|
104
|
+
* how far down the divider docks is the rendered height of the sticky header
|
|
105
|
+
* above it, which depends on the density and type the caller configured. Set
|
|
106
|
+
* it on the table — `className="[--cue-table-group-top:2rem]"` — rather than
|
|
107
|
+
* measuring at runtime.
|
|
108
|
+
*/
|
|
109
|
+
sticky?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Classes for the divider's *cell*, not its row.
|
|
112
|
+
*
|
|
113
|
+
* The `<tr>` paints nothing — the ground, the type and the sticky offset all
|
|
114
|
+
* live on the `<th>` that spans it, and a background on the row would sit
|
|
115
|
+
* behind the cell's own. Every other prop, and the ref, go to the row.
|
|
116
|
+
*/
|
|
117
|
+
className?: string;
|
|
21
118
|
}
|
|
22
119
|
interface TableCellProps extends React.ComponentPropsWithoutRef<"td"> {
|
|
23
120
|
/** Right-align this cell and set it in tabular figures. */
|
|
@@ -35,6 +132,14 @@ interface TableCaptionProps extends React.ComponentPropsWithoutRef<"caption"> {}
|
|
|
35
132
|
* Static markup — no `"use client"`. Compose it with `Panel` for the rim.
|
|
36
133
|
* Use {@link TableScrollRegion} only when the table can exceed its container.
|
|
37
134
|
*
|
|
135
|
+
* For a grouped list, the table stays *one* table: `Table.Head sticky` docks the
|
|
136
|
+
* column names, and one `Table.Body` per group opens with a `Table.GroupRow`
|
|
137
|
+
* divider — so the columns are named once and scan straight through every group
|
|
138
|
+
* boundary. Rows may hold `DataRow` slots, and `DataRow.Actions` reveals inside
|
|
139
|
+
* a cell on hover and on selection exactly as it does in a `Ledger` row — on
|
|
140
|
+
* focus too, once the row holds something focusable outside the track, which a
|
|
141
|
+
* `<tr>` never is by itself. See {@link TableRowProps.interactive}.
|
|
142
|
+
*
|
|
38
143
|
* @example
|
|
39
144
|
* <Table.Root>
|
|
40
145
|
* <Table.Header>
|
|
@@ -56,9 +161,10 @@ declare const Table: {
|
|
|
56
161
|
Header: React.ForwardRefExoticComponent<TableSectionProps & React.RefAttributes<HTMLTableSectionElement>>;
|
|
57
162
|
Body: React.ForwardRefExoticComponent<TableSectionProps & React.RefAttributes<HTMLTableSectionElement>>;
|
|
58
163
|
Row: React.ForwardRefExoticComponent<TableRowProps & React.RefAttributes<HTMLTableRowElement>>;
|
|
164
|
+
GroupRow: React.ForwardRefExoticComponent<TableGroupRowProps & React.RefAttributes<HTMLTableRowElement>>;
|
|
59
165
|
Head: React.ForwardRefExoticComponent<TableHeadProps & React.RefAttributes<HTMLTableCellElement>>;
|
|
60
166
|
Cell: React.ForwardRefExoticComponent<TableCellProps & React.RefAttributes<HTMLTableCellElement>>;
|
|
61
167
|
Caption: React.ForwardRefExoticComponent<TableCaptionProps & React.RefAttributes<HTMLTableCaptionElement>>;
|
|
62
168
|
};
|
|
63
169
|
//#endregion
|
|
64
|
-
export { Table, TableCaptionProps, TableCellProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps };
|
|
170
|
+
export { Table, TableCaptionProps, TableCellProps, TableGroupRowProps, TableHeadProps, TableRootProps, TableRowProps, TableScrollRegion, TableScrollRegionProps, TableSectionProps };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/cn.js";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/instruments/table.tsx
|
|
5
5
|
/**
|
|
6
6
|
* A named horizontal viewport for a table wider than its container.
|
|
@@ -20,6 +20,138 @@ const TableScrollRegion = React.forwardRef(function TableScrollRegion({ classNam
|
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
/**
|
|
23
|
+
* The table frame. Owns the type, the collapse and the numeric alignment rule.
|
|
24
|
+
*
|
|
25
|
+
* `data-numeric` is the contract, not a prop: the alignment and
|
|
26
|
+
* `tabular-nums` live in one descendant selector here, so a hand-written
|
|
27
|
+
* `<td data-numeric>` inside a data-driven body lines up exactly like a
|
|
28
|
+
* `<Table.Cell numeric>`. Figures that do not share a column width are
|
|
29
|
+
* unreadable as a column, which is the whole point of a table.
|
|
30
|
+
*/
|
|
31
|
+
const TableRoot = React.forwardRef(function TableRoot({ className, ...elementProps }, ref) {
|
|
32
|
+
return /* @__PURE__ */ jsx("table", {
|
|
33
|
+
ref,
|
|
34
|
+
"data-slot": "table",
|
|
35
|
+
className: cn("w-full border-collapse font-mono text-(length:--cue-text-ui) text-fg", "[&_[data-numeric]]:text-right [&_[data-numeric]]:tabular-nums", className),
|
|
36
|
+
...elementProps
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
/** The header band. Sticky-able by the caller; sized by the row padding tokens. */
|
|
40
|
+
const TableHeader = React.forwardRef(function TableHeader({ className, ...elementProps }, ref) {
|
|
41
|
+
return /* @__PURE__ */ jsx("thead", {
|
|
42
|
+
ref,
|
|
43
|
+
"data-slot": "table-header",
|
|
44
|
+
className,
|
|
45
|
+
...elementProps
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
/** The data band. */
|
|
49
|
+
const TableBody = React.forwardRef(function TableBody({ className, ...elementProps }, ref) {
|
|
50
|
+
return /* @__PURE__ */ jsx("tbody", {
|
|
51
|
+
ref,
|
|
52
|
+
"data-slot": "table-body",
|
|
53
|
+
className,
|
|
54
|
+
...elementProps
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* One row, separated by a hairline underneath — dropped on the last row so the
|
|
59
|
+
* table never draws a rule against the panel rim it sits in. In a `<thead>` the
|
|
60
|
+
* "last row" is the header row itself, so the header's own bottom border (on
|
|
61
|
+
* the cells) is the only rule there and the two can never stack into 2px.
|
|
62
|
+
*
|
|
63
|
+
* Every row carries the `group/data-row` name unconditionally. It paints
|
|
64
|
+
* nothing by itself, and it is what lets a `DataRow.Actions` cluster inside a
|
|
65
|
+
* `Table.Cell` reveal on hover, on focus and on selection with no second
|
|
66
|
+
* implementation of that idiom for the table — the whole point of the three
|
|
67
|
+
* idioms sharing one row anatomy.
|
|
68
|
+
*
|
|
69
|
+
* **The focus half of that reveal is the caller's to supply here.** A `<tr>` is
|
|
70
|
+
* not focusable and this component does not make it one — a tab stop on every
|
|
71
|
+
* row of a long table is a tab stop a keyboard user has to walk past hundreds
|
|
72
|
+
* of times, which is the reason `Ledger` rows opt in individually. So the
|
|
73
|
+
* reveal-on-focus only happens if the row *contains* something focusable
|
|
74
|
+
* outside the hidden track: a link on the row's name, a checkbox in the leading
|
|
75
|
+
* cell, any real control. Without one, `:focus-within` can never fire — content
|
|
76
|
+
* inside a `visibility: hidden` subtree cannot be focused, so the track cannot
|
|
77
|
+
* bootstrap its own reveal — and a hidden action cluster is then reachable by
|
|
78
|
+
* pointer only, which is a WCAG 2.1.1 failure the screen gives no sign of. A
|
|
79
|
+
* table whose only per-row controls live in a hidden track wants either a
|
|
80
|
+
* focusable cell beside it or an always-visible cluster.
|
|
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
|
+
*
|
|
87
|
+
* The selected tint is the row's, not the cell's: a `background` on the `<tr>`
|
|
88
|
+
* shows through every cell in it, so the fill cannot end up ragged where one
|
|
89
|
+
* cell sets a ground of its own.
|
|
90
|
+
*/
|
|
91
|
+
const TableRow = React.forwardRef(function TableRow({ className, interactive = false, selected = false, ...elementProps }, ref) {
|
|
92
|
+
return /* @__PURE__ */ jsx("tr", {
|
|
93
|
+
ref,
|
|
94
|
+
"data-slot": "table-row",
|
|
95
|
+
"data-selected": selected ? "" : void 0,
|
|
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),
|
|
97
|
+
...elementProps
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* A column heading: the same mono uppercase label as `SectionHeader` and
|
|
102
|
+
* `Eyebrow`, on the raised surface so the band reads as chrome rather than as
|
|
103
|
+
* the first row of data.
|
|
104
|
+
*
|
|
105
|
+
* **Sticky swaps the bottom border for an inset shadow.** `Table.Root` collapses
|
|
106
|
+
* its borders, and a collapsed border belongs to the table's shared grid rather
|
|
107
|
+
* than to the cell — so when a cell detaches to stay at the top of the scroll
|
|
108
|
+
* container, its rule stays behind with the rows and the stuck band appears to
|
|
109
|
+
* lose its edge. An inset `box-shadow` is a paint effect on the cell itself, so
|
|
110
|
+
* it travels with it. The alternative — putting the whole table on
|
|
111
|
+
* `border-separate` — would change the geometry of every table in the library
|
|
112
|
+
* to fix one that scrolls.
|
|
113
|
+
*
|
|
114
|
+
* The opaque `bg-surface-1` is not decoration either: a transparent sticky cell
|
|
115
|
+
* is a sticky cell you read the rows through. A caller who wants the header on
|
|
116
|
+
* the page's own ground can pass `className="bg-bg"` — what it must not pass is
|
|
117
|
+
* nothing.
|
|
118
|
+
*/
|
|
119
|
+
const TableHead = React.forwardRef(function TableHead({ className, numeric = false, sticky = false, scope = "col", ...elementProps }, ref) {
|
|
120
|
+
return /* @__PURE__ */ jsx("th", {
|
|
121
|
+
ref,
|
|
122
|
+
scope,
|
|
123
|
+
"data-slot": "table-head",
|
|
124
|
+
"data-numeric": numeric ? "" : void 0,
|
|
125
|
+
className: cn("border-b border-border bg-surface-1 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 top-0 z-3 border-b-0 shadow-[inset_0_-1px_0_var(--cue-hair-strong)]" : null, className),
|
|
126
|
+
...elementProps
|
|
127
|
+
});
|
|
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";
|
|
154
|
+
/**
|
|
23
155
|
* The canonical console table: hairlines, mono figures, no zebra striping.
|
|
24
156
|
*
|
|
25
157
|
* Semantic `<table>` markup throughout — a grid of divs loses row/column
|
|
@@ -30,6 +162,14 @@ const TableScrollRegion = React.forwardRef(function TableScrollRegion({ classNam
|
|
|
30
162
|
* Static markup — no `"use client"`. Compose it with `Panel` for the rim.
|
|
31
163
|
* Use {@link TableScrollRegion} only when the table can exceed its container.
|
|
32
164
|
*
|
|
165
|
+
* For a grouped list, the table stays *one* table: `Table.Head sticky` docks the
|
|
166
|
+
* column names, and one `Table.Body` per group opens with a `Table.GroupRow`
|
|
167
|
+
* divider — so the columns are named once and scan straight through every group
|
|
168
|
+
* boundary. Rows may hold `DataRow` slots, and `DataRow.Actions` reveals inside
|
|
169
|
+
* a cell on hover and on selection exactly as it does in a `Ledger` row — on
|
|
170
|
+
* focus too, once the row holds something focusable outside the track, which a
|
|
171
|
+
* `<tr>` never is by itself. See {@link TableRowProps.interactive}.
|
|
172
|
+
*
|
|
33
173
|
* @example
|
|
34
174
|
* <Table.Root>
|
|
35
175
|
* <Table.Header>
|
|
@@ -47,48 +187,34 @@ const TableScrollRegion = React.forwardRef(function TableScrollRegion({ classNam
|
|
|
47
187
|
* </Table.Root>
|
|
48
188
|
*/
|
|
49
189
|
const Table = {
|
|
50
|
-
Root:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
}),
|
|
58
|
-
Header: React.forwardRef(function TableHeader({ className, ...elementProps }, ref) {
|
|
59
|
-
return /* @__PURE__ */ jsx("thead", {
|
|
60
|
-
ref,
|
|
61
|
-
"data-slot": "table-header",
|
|
62
|
-
className,
|
|
63
|
-
...elementProps
|
|
64
|
-
});
|
|
65
|
-
}),
|
|
66
|
-
Body: React.forwardRef(function TableBody({ className, ...elementProps }, ref) {
|
|
67
|
-
return /* @__PURE__ */ jsx("tbody", {
|
|
68
|
-
ref,
|
|
69
|
-
"data-slot": "table-body",
|
|
70
|
-
className,
|
|
71
|
-
...elementProps
|
|
72
|
-
});
|
|
73
|
-
}),
|
|
74
|
-
Row: React.forwardRef(function TableRow({ className, ...elementProps }, ref) {
|
|
190
|
+
Root: TableRoot,
|
|
191
|
+
Header: TableHeader,
|
|
192
|
+
Body: TableBody,
|
|
193
|
+
Row: TableRow,
|
|
194
|
+
GroupRow: React.forwardRef(function TableGroupRow({ className, span, label, count, headingLevel, sticky = false, ...elementProps }, ref) {
|
|
195
|
+
const Label = headingLevel === void 0 ? "span" : headingTag(headingLevel);
|
|
75
196
|
return /* @__PURE__ */ jsx("tr", {
|
|
76
197
|
ref,
|
|
77
|
-
"data-slot": "table-row",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
198
|
+
"data-slot": "table-group-row",
|
|
199
|
+
...elementProps,
|
|
200
|
+
children: /* @__PURE__ */ jsxs("th", {
|
|
201
|
+
colSpan: span,
|
|
202
|
+
scope: "rowgroup",
|
|
203
|
+
"data-slot": "table-group-cell",
|
|
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),
|
|
205
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
206
|
+
"data-slot": "table-group-label",
|
|
207
|
+
className: TABLE_GROUP_LABEL,
|
|
208
|
+
children: label
|
|
209
|
+
}), count === void 0 ? null : /* @__PURE__ */ jsx("span", {
|
|
210
|
+
"data-slot": "table-group-count",
|
|
211
|
+
className: "ml-(--cue-space-2) tabular-nums",
|
|
212
|
+
children: count
|
|
213
|
+
})]
|
|
214
|
+
})
|
|
90
215
|
});
|
|
91
216
|
}),
|
|
217
|
+
Head: TableHead,
|
|
92
218
|
Cell: React.forwardRef(function TableCell({ className, numeric = false, ...elementProps }, ref) {
|
|
93
219
|
return /* @__PURE__ */ jsx("td", {
|
|
94
220
|
ref,
|
package/dist/theming/_presets.js
CHANGED
|
@@ -263,7 +263,7 @@ const THEME_PRESETS = {
|
|
|
263
263
|
"info": "#1a75d2",
|
|
264
264
|
"stream": "hsl(212 78% 42%)",
|
|
265
265
|
"selection": "#cce0ff",
|
|
266
|
-
"focus": "#
|
|
266
|
+
"focus": "#c75600",
|
|
267
267
|
"data-ground": "#000000",
|
|
268
268
|
"scrim": "rgba(0,0,0,0.35)"
|
|
269
269
|
},
|
|
@@ -322,7 +322,7 @@ const THEME_PRESETS = {
|
|
|
322
322
|
"info": "#2f6fd0",
|
|
323
323
|
"stream": "hsl(221 78% 45%)",
|
|
324
324
|
"selection": "rgba(224,101,16,0.15)",
|
|
325
|
-
"focus": "#
|
|
325
|
+
"focus": "#c75600",
|
|
326
326
|
"data-ground": "#000000",
|
|
327
327
|
"scrim": "rgba(15,23,42,0.45)"
|
|
328
328
|
},
|
|
@@ -440,7 +440,7 @@ const THEME_PRESETS = {
|
|
|
440
440
|
"info": "#1a75d2",
|
|
441
441
|
"stream": "#0f6ac4",
|
|
442
442
|
"selection": "rgba(184,134,47,0.20)",
|
|
443
|
-
"focus": "#
|
|
443
|
+
"focus": "#9f6e08",
|
|
444
444
|
"data-ground": "#000000",
|
|
445
445
|
"scrim": "rgba(0,0,0,0.55)"
|
|
446
446
|
},
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
{
|
|
89
89
|
"name": "Bar",
|
|
90
90
|
"usage": "AppShell.Bar",
|
|
91
|
-
"description": "The bar across the top of the frame, divided by the navigation column: a\nheader cell exactly the column's width, and the rest of the bar beside it.\n\n`TitleBar` and `AppBar` span the whole frame and know nothing about the\ncolumn under them, which is right for a document and wrong for a console\nthat has an app mark over its navigation: two boxes that are meant to read\nas one column have to be measured from one length, or they drift the first\ntime the column changes width. This is the bar that is measured that way. It\nreads `--cue-nav-width` from {@link AppShellRoot}, so railing the console\nmoves the cell and the column together with nothing to keep in sync.\n\nBelow `md` the cell takes its content's width instead. That is the same\ndecision the column and the drawer trigger already make and not a third one:\nbelow the breakpoint the column is not in the layout, and a cell aligned to\na column that is not there is 14rem of a 390px bar walled off for nothing.\n\nA `<div>` rather than a `<header>`, like `TitleBar` beside it: a console\nframe that also carries an `AppBar` would otherwise have two `banner`\nlandmarks, and a page that wants one can say `role=\"banner\"` here.",
|
|
91
|
+
"description": "The bar across the top of the frame, divided by the navigation column: a\nheader cell exactly the column's width, and the rest of the bar beside it.\n\n`TitleBar` and `AppBar` span the whole frame and know nothing about the\ncolumn under them, which is right for a document and wrong for a console\nthat has an app mark over its navigation: two boxes that are meant to read\nas one column have to be measured from one length, or they drift the first\ntime the column changes width. This is the bar that is measured that way. It\nreads `--cue-nav-width` from {@link AppShellRoot}, so railing the console\nmoves the cell and the column together with nothing to keep in sync.\n\nBelow `md` the cell takes its content's width instead. That is the same\ndecision the column and the drawer trigger already make and not a third one:\nbelow the breakpoint the column is not in the layout, and a cell aligned to\na column that is not there is 14rem of a 390px bar walled off for nothing.\n\nA `<div>` rather than a `<header>`, like `TitleBar` beside it: a console\nframe that also carries an `AppBar` would otherwise have two `banner`\nlandmarks, and a page that wants one can say `role=\"banner\"` here.\n\n**It is also the desktop app's one chrome row.** `platformDrag` gives it the\ndrag contract `TitleBar` has always had, and `center` and `actions` give it\nthe two slots that made a second stacked bar necessary — so a Tauri or\nElectron window spends one `--cue-chrome-titlebar` on furniture instead of\ntwo. See {@link AppShellBarProps.platformDrag} for why the attribute lands on\nthree surfaces rather than one.\n\nThe bar is always three nested clusters — brand content, leading children,\ntrailing actions — whether or not it is draggable. One shape, not two: the\ndrag mode is then a class on a box that already exists rather than a\ndifferent tree, and a caller's own CSS cannot be right in a browser and wrong\nin a shell. In an ordinary web shell each cluster grows into the space its\ncell gives it, so an `ms-auto` on a brand control still reaches the cell's\ntrailing edge exactly as it did before the clusters existed; under\n`platformDrag` the clusters shrink to their content instead, so that the\nslack stays on the attributed cell and the window can be dragged by it.",
|
|
92
92
|
"props": [
|
|
93
93
|
{
|
|
94
94
|
"name": "brand",
|
|
@@ -102,7 +102,28 @@
|
|
|
102
102
|
"type": "string",
|
|
103
103
|
"required": false,
|
|
104
104
|
"defaultValue": null,
|
|
105
|
-
"description": "Extra classes for the header cell."
|
|
105
|
+
"description": "Extra classes for the header cell — the bordered box that spans the\nnavigation column, not the row of brand content inside it. Anything about\nthe cell's width, rim or gutter belongs here; anything about how the mark\nand its controls sit belongs on the nodes passed as `brand`."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "platformDrag",
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"required": false,
|
|
111
|
+
"defaultValue": "false",
|
|
112
|
+
"description": "Make the bar's dead space a window drag handle. Defaults to `false`.\n\nThis is the same contract {@link TitleBar } has always offered, on the bar\na console with a navigation column actually uses — a desktop app should\nnot need a `TitleBar` *and* an `AppShell.Bar` stacked on top of each other\nto get one draggable row, which is two chrome rungs of a small window\nspent on furniture.\n\nEvery structural surface is attributed and every interactive cluster is\ncarved back out, because the two shells this ships into fail in opposite\ndirections. Tauri matches the *exact* element under the cursor, so the\nbrand cell and the rest cell carry the attribute themselves — a click on\nthe cell's own dead space targets the cell, never the root beneath it, and\na bar that only attributed its root would be draggable nowhere. Electron\nbuilds a geometric mask from painted boxes instead and excludes nothing on\nits own, so the brand content, the leading children and `actions` each sit\nin a cluster marked `app-region: no-drag` — without it a button that merely\noverlaps the bar stops receiving clicks at all. See `./_drag.ts`.\n\n**It changes how the clusters are sized, and that is a trade.** Off, each\ncluster grows into its cell, so a caller's own `ms-auto` reaches the cell's\ntrailing rim. On, the clusters take their content's width and `actions` is\npushed over with `ms-auto` instead, because a cluster that fills its cell\nis a `no-drag` rectangle painted over every dead pixel the cell just\nattributed — the bar would be draggable nowhere but its paddings and its\none gap. The dead area belongs to the window in drag mode, and a caller who\nneeds content to reach the rim there should say so on a box of their own\ninside the cluster.\n\n`select-none` comes with it: dragging a window and selecting the text you\nstarted the drag on are the same gesture, and the shell wins.\n\nNot wired here: double-click-to-maximise. Tauri's injected script already\ndoes it for any drag region, macOS's cancel-by-dragging-away included.\nElectron does not, and an Electron app has to listen for `dblclick` and\ntoggle the window itself. Which of those an app is, is not something a\nlayout component can know."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "center",
|
|
116
|
+
"type": "ReactNode",
|
|
117
|
+
"required": false,
|
|
118
|
+
"defaultValue": null,
|
|
119
|
+
"description": "A title centred on the **bar**, not between the cells beside it.\n\nAbsolutely positioned against the bar's own box, so it stays put when a\nbutton appears in `actions` or the column rails — the whole reason a\ndocument name in desktop chrome is not just another flex child.\n`pointer-events-none` so the dead space around a short title still drags,\nwhich is also what makes it click through to the drag surface underneath\nin Tauri, whose hit-testing is the browser's own.\n\nInert by construction. Anything the reader has to click belongs in\n`actions` or in `children`, where it can be carved out of the drag region."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"name": "actions",
|
|
123
|
+
"type": "ReactNode",
|
|
124
|
+
"required": false,
|
|
125
|
+
"defaultValue": null,
|
|
126
|
+
"description": "The trailing cluster: connection state, a run indicator, an overflow menu.\n\nPinned to the bar's trailing edge and never squeezed, so it reads as window\nfurniture rather than the end of a sentence. `children` keeps flowing into\nthe same cell ahead of it, which is what lets an existing web-shell caller\npass free-form content and gain a cluster without rearranging anything."
|
|
106
127
|
}
|
|
107
128
|
],
|
|
108
129
|
"typeReferences": [
|
|
@@ -289,13 +310,16 @@
|
|
|
289
310
|
"--cue-chrome-titlebar",
|
|
290
311
|
"--cue-control-md",
|
|
291
312
|
"--cue-fg",
|
|
313
|
+
"--cue-fg-muted",
|
|
314
|
+
"--cue-font-mono",
|
|
292
315
|
"--cue-icon-md",
|
|
293
316
|
"--cue-nav-width",
|
|
294
317
|
"--cue-pad-row-x",
|
|
295
318
|
"--cue-space-3",
|
|
296
319
|
"--cue-space-4",
|
|
297
320
|
"--cue-space-5",
|
|
298
|
-
"--cue-surface-1"
|
|
321
|
+
"--cue-surface-1",
|
|
322
|
+
"--cue-text-label"
|
|
299
323
|
],
|
|
300
324
|
"summary": "The console frame: title bar, navigation, work area and status bar, one viewport tall with exactly one thing scrolling.",
|
|
301
325
|
"examples": [
|
|
@@ -329,6 +353,11 @@
|
|
|
329
353
|
"code": "<span className=\"group-data-[rail]/app-shell:hidden\">CUE++</span>",
|
|
330
354
|
"language": "tsx"
|
|
331
355
|
},
|
|
356
|
+
{
|
|
357
|
+
"title": "The desktop shape: one chrome row",
|
|
358
|
+
"code": "<AppShell.Bar\n platformDrag\n center=\"main-stage.cue\"\n brand={<><AppShell.NavTrigger /><Eyebrow>cue++</Eyebrow></>}\n actions={<><Chip tone=\"ok\">live</Chip><IconButton aria-label=\"Preview\" icon={Monitor} size=\"sm\" /></>}\n>\n <Toolbar.Button icon={Play}>Go</Toolbar.Button>\n</AppShell.Bar>",
|
|
359
|
+
"language": "tsx"
|
|
360
|
+
},
|
|
332
361
|
{
|
|
333
362
|
"title": "Where the footer goes",
|
|
334
363
|
"code": "<AppShell.Content>\n <AppShell.Scroller className=\"flex flex-col\">\n <div className=\"flex-1\">{children}</div>\n <Footer meta={…}>…</Footer>\n </AppShell.Scroller>\n</AppShell.Content>",
|
|
@@ -366,7 +395,9 @@
|
|
|
366
395
|
"Reaching for a viewport variant of your own because the shell has one. The `md` switch is licensed for navigation shape and nothing else; everything else in this system is intrinsically responsive.",
|
|
367
396
|
"Hard-coding the navigation column's width somewhere else in the frame — a header cell, a toolbar offset, a sticky rule. Read `--cue-nav-width` instead, or the two drift the first time the column rails.",
|
|
368
397
|
"Railing a column of items with no icons. At 3.5rem the label goes `sr-only` and the icon is all that is left; without one the reader gets an empty strip. `AppShell.Sidebar` takes `collapsible={false}` for exactly that column.",
|
|
369
|
-
"Ending the page with a `Footer` after `AppShell.Body`. A footer belongs to the content column — the last child of `AppShell.Scroller` — or it stretches under the navigation and pretends to be part of the frame."
|
|
398
|
+
"Ending the page with a `Footer` after `AppShell.Body`. A footer belongs to the content column — the last child of `AppShell.Scroller` — or it stretches under the navigation and pretends to be part of the frame.",
|
|
399
|
+
"Stacking a `TitleBar` above an `AppShell.Bar` in a desktop app to get a drag region. `AppShell.Bar platformDrag` is the same contract on the bar you already have, and two chrome rungs of a small window is a lot to spend on furniture.",
|
|
400
|
+
"Putting something clickable in `AppShell.Bar`'s `center`. It is `pointer-events-none` so the dead space around a short title still drags — an interactive centre belongs in `actions` or in `children`, where it is carved out of the drag region."
|
|
370
401
|
],
|
|
371
402
|
"specimens": [
|
|
372
403
|
{
|
|
@@ -378,6 +409,16 @@
|
|
|
378
409
|
"code": "<div className=\"w-full overflow-hidden rounded-(--radius-surface) border border-border\">\n {/* `h-[26rem]` overrides the shell's own `h-dvh`, and `main={false}`\n * keeps it from emitting a second `<main>` inside the page already\n * wrapping this corpus. Both are what embedding a whole-screen\n * component in a bench costs. */}\n <AppShell.Root className=\"h-[26rem]\" rail={rail} onRailChange={setRail}>\n {/* The toggle is rendered here rather than by the shell: where the\n * control lives is a decision about this bar, and persisting what\n * it sets is a decision about hydration. Both are the caller's. */}\n <AppShell.Bar\n brand={\n <>\n <AppShell.NavTrigger />\n <Eyebrow className=\"group-data-[rail]/app-shell:hidden\">CUE++</Eyebrow>\n <IconButton\n icon={PanelLeft}\n size=\"sm\"\n aria-label={rail ? \"Expand the sidebar\" : \"Collapse the sidebar\"}\n aria-pressed={rail}\n className=\"ms-auto\"\n onClick={() => setRail(!rail)}\n />\n </>\n }\n >\n <span className=\"font-mono text-(length:--cue-text-label) text-fg-muted\">\n main-stage.cue\n </span>\n <Chip tone=\"ok\" className=\"ms-auto\">\n live\n </Chip>\n </AppShell.Bar>\n\n <AppShell.Body>\n <AppShell.Sidebar aria-label=\"Console\" drawerTitle=\"Console\">\n <Sidebar.Section label=\"Show\">\n <Sidebar.Item icon={List} active>\n Cues\n </Sidebar.Item>\n <Sidebar.Item icon={Lightbulb} badge={<Chip>48</Chip>}>\n Fixtures\n </Sidebar.Item>\n </Sidebar.Section>\n </AppShell.Sidebar>\n\n <AppShell.Content main={false}>\n <Toolbar.Root aria-label=\"Cue list\">\n <Toolbar.Button icon={Plus}>Add cue</Toolbar.Button>\n <Toolbar.Separator />\n <Toolbar.Button icon={Monitor}>Live</Toolbar.Button>\n </Toolbar.Root>\n\n <AppShell.Scroller>\n <Panel>\n <PanelHeader title=\"Cue stack\" actions={<Chip tone=\"ok\">live</Chip>} />\n {CUES.map((cue) => (\n <Row key={cue.id}>\n <span className=\"w-[2rem] shrink-0 font-mono tabular-nums\">{cue.id}</span>\n <span className=\"flex-1 truncate\">{cue.label}</span>\n <span className=\"font-mono tabular-nums text-fg-muted\">{cue.time}</span>\n </Row>\n ))}\n </Panel>\n </AppShell.Scroller>\n </AppShell.Content>\n\n <AppShell.Aside aria-label=\"Inspector\" width=\"11rem\">\n <SectionHeader label=\"Selected\" />\n <p className=\"font-mono text-(length:--cue-text-ui) text-fg-muted\">\n Its own scroller, so reading it never moves the work area.\n </p>\n </AppShell.Aside>\n </AppShell.Body>\n\n <StatusBar>\n <StatusBar.Item tone=\"ok\">Art-Net</StatusBar.Item>\n <StatusBar.Item push>Cue 4</StatusBar.Item>\n </StatusBar>\n </AppShell.Root>\n</div>",
|
|
379
410
|
"note": "The console frame, and the arithmetic that holds it up: one viewport tall, an unbroken min-h-0 chain, and exactly one thing scrolling. AppShell.Bar divides the bar at the navigation column, so the mark over the column and the column itself are one box measured from one length. Boxed to 26rem here — a bench has no viewport to give it.",
|
|
380
411
|
"interaction": "the button beside the mark rails the column to 3.5rem of icons, and the header cell over it narrows with it — one --cue-nav-width, rewritten once. The 4px strip on the column's edge does the same. Below 48rem the column is not rendered at all and the trigger opens the same navigation as a drawer."
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"title": "AppShell.Bar",
|
|
415
|
+
"group": "chrome",
|
|
416
|
+
"components": [
|
|
417
|
+
"AppShell"
|
|
418
|
+
],
|
|
419
|
+
"code": "<div className=\"w-full overflow-hidden rounded-(--radius-surface) border border-border\">\n <AppShell.Root className=\"h-auto\">\n <AppShell.Bar\n platformDrag\n center=\"main-stage.cue\"\n brand={\n <>\n <AppShell.NavTrigger />\n <Eyebrow>cue++</Eyebrow>\n </>\n }\n actions={\n <>\n <Chip tone=\"ok\">live</Chip>\n <IconButton aria-label=\"Preview\" icon={Monitor} size=\"sm\" />\n </>\n }\n >\n <Toolbar.Root aria-label=\"Playback\" className=\"border-0 bg-transparent px-0\">\n <Toolbar.Button icon={Play}>Go</Toolbar.Button>\n <Toolbar.Button icon={Eye}>Blind</Toolbar.Button>\n </Toolbar.Root>\n </AppShell.Bar>\n </AppShell.Root>\n</div>",
|
|
420
|
+
"note": "The same bar a console uses, given the desktop app's three extra slots: platformDrag hands every dead pixel to the window manager, center pins the document name to the middle of the bar rather than the middle of the space left over, and actions holds the trailing cluster. One chrome row instead of a TitleBar with a Toolbar stacked under it.",
|
|
421
|
+
"interaction": "the drag region is inert in a browser — there is no window to move — and live in Tauri and Electron, where it is also carved back out around the mark, the leading controls and the actions, so every one of them still takes its click. Double-click-to-maximise is Tauri's for free and the app's own work in Electron."
|
|
381
422
|
}
|
|
382
423
|
]
|
|
383
424
|
}
|
|
@@ -144,6 +144,7 @@
|
|
|
144
144
|
"--cue-accent-soft",
|
|
145
145
|
"--cue-border",
|
|
146
146
|
"--cue-border-overlay",
|
|
147
|
+
"--cue-border-strong",
|
|
147
148
|
"--cue-chip-h",
|
|
148
149
|
"--cue-control-lg",
|
|
149
150
|
"--cue-control-md",
|
|
@@ -152,6 +153,7 @@
|
|
|
152
153
|
"--cue-fg",
|
|
153
154
|
"--cue-fg-muted",
|
|
154
155
|
"--cue-fg-subtle",
|
|
156
|
+
"--cue-focus",
|
|
155
157
|
"--cue-font-mono",
|
|
156
158
|
"--cue-icon-sm",
|
|
157
159
|
"--cue-radius-control",
|