@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +53 -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 +8 -3
  21. package/dist/index.js +5 -1
  22. package/dist/instruments/_data-row.d.ts +68 -0
  23. package/dist/instruments/_data-row.js +139 -0
  24. package/dist/instruments/data-row.d.ts +94 -0
  25. package/dist/instruments/data-row.js +151 -0
  26. package/dist/instruments/data-tree.d.ts +126 -0
  27. package/dist/instruments/data-tree.js +303 -0
  28. package/dist/instruments/index.d.ts +6 -2
  29. package/dist/instruments/index.js +5 -1
  30. package/dist/instruments/ledger.d.ts +105 -0
  31. package/dist/instruments/ledger.js +114 -0
  32. package/dist/instruments/table.d.ts +108 -2
  33. package/dist/instruments/table.js +165 -39
  34. package/dist/theming/_presets.js +3 -3
  35. package/manifest/components/app-shell.json +45 -4
  36. package/manifest/components/autocomplete.json +2 -0
  37. package/manifest/components/color-field.json +2 -0
  38. package/manifest/components/color-picker.json +2 -0
  39. package/manifest/components/combobox.json +2 -0
  40. package/manifest/components/composer.json +2 -0
  41. package/manifest/components/data-row.json +208 -0
  42. package/manifest/components/data-tree.json +151 -0
  43. package/manifest/components/date-field.json +2 -0
  44. package/manifest/components/date-picker.json +2 -0
  45. package/manifest/components/date-range-picker.json +2 -0
  46. package/manifest/components/env-var-input.json +2 -0
  47. package/manifest/components/input-group.json +4 -2
  48. package/manifest/components/input.json +3 -1
  49. package/manifest/components/ledger.json +187 -0
  50. package/manifest/components/multi-select.json +2 -0
  51. package/manifest/components/musical-time-input.json +2 -0
  52. package/manifest/components/number-field.json +3 -1
  53. package/manifest/components/otp-field.json +2 -0
  54. package/manifest/components/password-input.json +2 -0
  55. package/manifest/components/scrub-input.json +2 -0
  56. package/manifest/components/search-input.json +2 -0
  57. package/manifest/components/select.json +1 -0
  58. package/manifest/components/status-bar.json +146 -12
  59. package/manifest/components/table-scroll-region.json +6 -0
  60. package/manifest/components/table.json +113 -5
  61. package/manifest/components/tags-input.json +2 -0
  62. package/manifest/components/textarea.json +3 -1
  63. package/manifest/components/time-field.json +2 -0
  64. package/manifest/components/title-bar.json +1 -1
  65. package/manifest/components/toggle-group.json +2 -0
  66. package/manifest/components/toggle.json +2 -0
  67. package/manifest/manifest.json +72 -33
  68. package/manifest/tokens.json +1 -1
  69. 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: React.forwardRef(function TableRoot({ className, ...elementProps }, ref) {
51
- return /* @__PURE__ */ jsx("table", {
52
- ref,
53
- "data-slot": "table",
54
- className: cn("w-full border-collapse font-mono text-(length:--cue-text-ui) text-fg", "[&_[data-numeric]]:text-right [&_[data-numeric]]:tabular-nums", className),
55
- ...elementProps
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
- className: cn("border-b border-border last:border-b-0", className),
79
- ...elementProps
80
- });
81
- }),
82
- Head: React.forwardRef(function TableHead({ className, numeric = false, scope = "col", ...elementProps }, ref) {
83
- return /* @__PURE__ */ jsx("th", {
84
- ref,
85
- scope,
86
- "data-slot": "table-head",
87
- "data-numeric": numeric ? "" : void 0,
88
- 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", className),
89
- ...elementProps
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,
@@ -263,7 +263,7 @@ const THEME_PRESETS = {
263
263
  "info": "#1a75d2",
264
264
  "stream": "hsl(212 78% 42%)",
265
265
  "selection": "#cce0ff",
266
- "focus": "#e06510",
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": "#e06510",
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": "#b8862f",
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",