@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  # @cueplusplus/ui
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1217d75: `Table.GroupRow` takes `headingLevel` (`2`–`6`), which makes the divider's label
8
+ slot a real heading element instead of a `<span>` — so a grouped table joins the
9
+ page's heading outline without nesting an `<h2>` inside a `<span>`, which is what
10
+ passing a heading as `label` had to do. It defaults to no heading, so every
11
+ existing divider is unchanged.
12
+
13
+ The row-activation reading is now public: `activateRowFromClick` and
14
+ `activateRowFromKeyDown` are exported from `@cueplusplus/ui`, so a `Table.Row` —
15
+ which owns its own activation — or any other element a console makes into a row
16
+ runs the same decision `DataRow.Root` runs, instead of a second copy of it.
17
+
18
+ ### Patch Changes
19
+
20
+ - @cueplusplus/tokens@0.7.0
21
+
22
+ ## 0.6.0
23
+
24
+ ### Minor Changes
25
+
26
+ - bf116b5: `AppShell.Bar` gains the desktop drag contract `TitleBar` already had, plus a truly bar-centred `center` slot and a trailing `actions` cluster, so a Tauri or Electron console spends one chrome row on window furniture instead of two.
27
+
28
+ `StatusBar.Item` gains `interactive`, `prominence` and `render`, so a reading can become a real button or link with hover, press and inset keyboard-focus states, and the new `StatusBar.Group` fuses adjacent items into one compact control. The published manifest now lists `StatusBar.Item` and `StatusBar.Group` with their props, so a props table or lookup tool reads them the way it already reads `AppShell`'s parts.
29
+
30
+ ## Migrating
31
+
32
+ Nothing to change. Both additions are opt-in: without `platformDrag`, `center` or `actions` the bar emits no drag attributes at all, and a `StatusBar.Item` with none of the new props is the same inert `<div>` with the same classes it always was.
33
+
34
+ Two shapes moved underneath, in case your own CSS reaches into them. `AppShell.Bar` now wraps its brand, its leading children and its actions in `[data-slot="app-shell-brand-content"]`, `[data-slot="app-shell-bar-leading"]` and `[data-slot="app-shell-bar-actions"]` respectively — each a `<div>` laid out as a flex row with the bar's own gap, so the rhythm is unchanged and `brandClassName` still lands on the cell. Under `platformDrag` those clusters take their content's width rather than filling their cell, which is what leaves the bar's dead space to the window manager; an `ms-auto` that needs to reach the cell's rim belongs on a box inside the cluster there. And `StatusBar`'s root is now `overflow-x-clip`: the bar has always promised to be one row tall, and now it keeps that promise when it is overfilled.
35
+
36
+ - fb20c71: Dense data surfaces: `DataRow` is the shared row anatomy (marker, label, description, tags, figure and an action track hidden until hover, focus or selection, with a touch fallback and no reflow when it appears); `Ledger` groups rows under sticky tier headings and gutter slugs with no disclosure semantics; `DataTree` draws the same rows as a hierarchy under `role="tree"` with the APG keyboard map and CSS depth guides; and `Table` gains `Table.GroupRow` (a `<th scope="rowgroup">` divider so a grouped list stays one table with its columns named once), `sticky` on `Table.Head`, and `interactive`/`selected` on `Table.Row`. Existing tables are untouched: every addition is opt-in.
37
+
38
+ ### Patch Changes
39
+
40
+ - 778374d: The keyboard focus ring is now visible in the luma, venu and hivehub light
41
+ themes. Each set `--cue-focus` to its brand accent, and on paper that accent
42
+ read as low as 2.45:1 against the recessed fill a focused field paints it
43
+ over — under the 3:1 WCAG 1.4.11 asks of a focus indicator. Each light block
44
+ now carries its own focus colour: the same accent hue, stepped down in
45
+ lightness until it clears every surface a field can sit on. `--cue-accent` is
46
+ unchanged, so no button, chip or link moves.
47
+ - 013960d: Form controls now show one quiet focus signal instead of two. A focused field strengthens its rim by one rung (`--cue-border` → `--cue-border-strong`) and draws a single 2px ring in the theme's own `--cue-focus`, pulled flush against the rim rather than floating outside it on a gap. Grouped controls — `InputGroup`, `SearchInput`, `Combobox`, `MultiSelect`, `Autocomplete`, `TagsInput`, `NumberField`, `ScrubInput`, `Composer`, `MusicalTimeInput` and the segmented date and time fields — strengthen their rim for any focus inside but wait for a keyboard-visible descendant before drawing the ring, so a mouse click no longer lights the whole cluster up. An invalid field stays red while it is focused: because a flush ring is drawn over the rim it hugs, the ring itself turns `--cue-danger` rather than leaving a border nobody can see underneath it — so focus keeps its own indicator and invalid keeps its colour, on the plain controls and on the grouped ones alike.
48
+
49
+ ## Migrating
50
+
51
+ No API changed, and no consumer edit is required. Anyone who added local overrides to mute the old accent-rim-plus-outer-ring treatment can delete them: the defaults are now the quiet version, and a local override will fight the theme's focus colour rather than help it.
52
+
53
+ - Updated dependencies [778374d]
54
+ - @cueplusplus/tokens@0.6.0
55
+
3
56
  ## 0.5.0
4
57
 
5
58
  ### Minor Changes
@@ -0,0 +1,59 @@
1
+ //#region src/chrome/_drag.ts
2
+ /**
3
+ * The desktop drag contract, in one place because two bars now emit it.
4
+ *
5
+ * A window has exactly one way to be dragged by its chrome, and this library
6
+ * ships into two shells that disagree about what that way is:
7
+ *
8
+ * - **Tauri** listens for `mousedown` and walks the event's `composedPath()`,
9
+ * looking for `data-tauri-drag-region`. The bare form of the attribute means
10
+ * *this element and only this element* — the innermost element under the
11
+ * cursor has to be the attributed one — so a container carrying it does not
12
+ * hand the behaviour to its children. It also excludes anything that reads as
13
+ * a control (`a`, `button`, `input`, `select`, `textarea`, `label`,
14
+ * `summary`, `[contenteditable]`, `[tabindex]`, and the button/link/menuitem
15
+ * ARIA roles) for free, without being asked.
16
+ * - **Electron and Chromium** read the `app-region` CSS property and build a
17
+ * *geometric* hit-test mask from it, below the DOM's pointer-event model
18
+ * entirely: every painted pixel of a `drag` box drags, minus the pixels a
19
+ * `no-drag` box covers. It knows nothing about ancestry and excludes nothing
20
+ * on its own — every interactive descendant has to opt out by name.
21
+ *
22
+ * The two mechanisms fail in opposite directions, so the contract a shared
23
+ * component can honour is the union of both: **attribute every structural
24
+ * surface, carve every interactive cluster.** A bar that only attributes its
25
+ * root is undraggable in Tauri wherever a cell covers the root; a bar that only
26
+ * drags and never carves is a bar whose buttons move the window in Electron.
27
+ *
28
+ * Both spellings of the CSS property are emitted. `app-region` is the current
29
+ * canonical name in Electron's own documentation; `-webkit-app-region` is the
30
+ * legacy alias older Chromium builds still want, and neither shell minds
31
+ * reading a property it already understands twice.
32
+ *
33
+ * These are class strings rather than a `<div>` of their own on purpose: the
34
+ * surfaces that have to carry them — {@link TitleBar}'s root,
35
+ * `AppShell.Bar`'s root, its brand cell, its rest cell — already exist for
36
+ * layout reasons, and wrapping each one in a drag element would put a box in
37
+ * the flex arithmetic that is there for nothing but a CSS property.
38
+ */
39
+ /**
40
+ * Make a painted surface a window drag handle.
41
+ *
42
+ * Pair it with a bare `data-tauri-drag-region` attribute on the same element —
43
+ * the CSS half is Electron's and the attribute half is Tauri's, and an element
44
+ * with only one of them is draggable in only one shell.
45
+ */
46
+ const DRAG_CLASSES = "[app-region:drag] [-webkit-app-region:drag]";
47
+ /**
48
+ * Carve an interactive cluster back out of the surrounding drag region.
49
+ *
50
+ * Required for Electron, where a button that merely overlaps a drag box stops
51
+ * receiving clicks at all; harmless in Tauri, which had already excluded a
52
+ * real `<button>` on its own. Anything custom enough that Tauri's tag/role
53
+ * heuristic would miss it — an icon "button" built from a `<div>`, a bespoke
54
+ * menu trigger — additionally wants `data-tauri-drag-region="false"`, which is
55
+ * the caller's to add on the control itself.
56
+ */
57
+ const NO_DRAG_CLASSES = "[app-region:no-drag] [-webkit-app-region:no-drag]";
58
+ //#endregion
59
+ export { DRAG_CLASSES, NO_DRAG_CLASSES };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+ import { cn } from "../lib/cn.js";
3
+ import { statusBarItemClasses } from "./_status-bar.js";
4
+ import * as React from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { useRender } from "@base-ui/react/use-render";
7
+ //#region src/chrome/_status-bar-item.tsx
8
+ /**
9
+ * The elements for which `disabled` is a real HTML attribute.
10
+ *
11
+ * Everything else — an anchor above all — has to be told it is inert some other
12
+ * way. Setting `disabled` on an `<a>` produces a stray attribute the parser
13
+ * keeps and nothing reads: not a validity error, but not a disabled link
14
+ * either, because there is no such thing.
15
+ */
16
+ const DISABLEABLE_TAGS = /* @__PURE__ */ new Set([
17
+ "button",
18
+ "input",
19
+ "select",
20
+ "textarea",
21
+ "fieldset",
22
+ "optgroup"
23
+ ]);
24
+ /**
25
+ * Whether `disabled` can be handed to whatever this renders as.
26
+ *
27
+ * A `render` that is a component rather than an intrinsic tag decides for
28
+ * itself — Base UI's own triggers take a `disabled` prop, and refusing to pass
29
+ * it would break the composition this prop exists for — so only a string tag
30
+ * outside the set above is refused.
31
+ */
32
+ function takesDisabledAttribute(render) {
33
+ if (render === void 0) return true;
34
+ if (!React.isValidElement(render)) return true;
35
+ return typeof render.type !== "string" || DISABLEABLE_TAGS.has(render.type);
36
+ }
37
+ /**
38
+ * A status-bar item that is a real control: a `<button>`, or whatever `render`
39
+ * replaces it with.
40
+ *
41
+ * `type="button"` is supplied only when this renders its own `<button>`. A
42
+ * `render={<a href="…" />}` must not be handed a `type`, which on an anchor is
43
+ * a MIME hint and not a submit-behaviour switch — the same rule
44
+ * {@link IconButton} follows, and for the same reason.
45
+ *
46
+ * `disabled` is honoured on every interactive shape, by two routes. The
47
+ * attribute itself goes only where it means something ({@link DISABLEABLE_TAGS},
48
+ * plus any component `render`, which takes its own `disabled` prop); the state
49
+ * is stated everywhere with `aria-disabled` and `data-disabled`, which is what
50
+ * an anchor gets instead. `data-disabled` is not decoration — it is what greys
51
+ * the item out and lifts it out of the pointer's reach in `_status-bar.ts`,
52
+ * since `:disabled` never matches an `<a>`. The gap this closes was a
53
+ * `render={<button/>} disabled` that shipped enabled, clickable and undimmed.
54
+ */
55
+ const StatusBarInteractiveItem = React.forwardRef(function StatusBarInteractiveItem({ tone, prominence, className, push, icon: Icon, render, disabled, children, elementProps }, ref) {
56
+ const tagProps = {
57
+ ...render === void 0 ? { type: "button" } : {},
58
+ ...disabled === true && takesDisabledAttribute(render) ? { disabled: true } : {},
59
+ ...disabled === true ? {
60
+ "aria-disabled": true,
61
+ "data-disabled": ""
62
+ } : {}
63
+ };
64
+ return useRender({
65
+ render,
66
+ ref,
67
+ defaultTagName: "button",
68
+ props: {
69
+ "data-slot": "status-bar-item",
70
+ ...tagProps,
71
+ ...elementProps,
72
+ className: cn(statusBarItemClasses({
73
+ tone,
74
+ interactive: true,
75
+ prominence
76
+ }), push ? "ml-auto" : void 0, className),
77
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [Icon === void 0 ? null : /* @__PURE__ */ jsx(Icon, {
78
+ "aria-hidden": "true",
79
+ className: "size-icon-sm shrink-0"
80
+ }), children] })
81
+ }
82
+ });
83
+ });
84
+ //#endregion
85
+ export { StatusBarInteractiveItem };
@@ -0,0 +1,15 @@
1
+ import "./status-bar.js";
2
+ //#region src/chrome/_status-bar.d.ts
3
+ /**
4
+ * How loudly a status-bar item states itself.
5
+ *
6
+ * `quiet` is a reading — tone-coloured ink on the bar's own ground, which is
7
+ * what a status bar is for. `filled` is a chip: a full-height tone wash behind
8
+ * tone ink, for the handful of states VS Code reserves its coloured categories
9
+ * for (a warning, an error, the remote you are attached to). It is loud on
10
+ * purpose and it is meant to be rare; a bar of five filled items is a bar
11
+ * nobody reads.
12
+ */
13
+ type StatusBarProminence = "quiet" | "filled";
14
+ //#endregion
15
+ export { StatusBarProminence };
@@ -0,0 +1,171 @@
1
+ import { cn } from "../lib/cn.js";
2
+ //#region src/chrome/_status-bar.ts
3
+ /**
4
+ * The class half of `StatusBar` (see `./status-bar.tsx`).
5
+ *
6
+ * It lives on its own for the same reason `forms/_chassis.ts` does — the item's
7
+ * appearance is a pure function of three enumerated inputs and nothing else —
8
+ * and for one reason `_chassis.ts` does not have: a status bar is two kinds of
9
+ * thing wearing one skin. A static reading is markup, and markup is the whole
10
+ * point of `StatusBar` being renderable from a server component with no
11
+ * JavaScript at all. An *interactive* item is a control, and a control needs
12
+ * the render prop, which needs a hook, which needs `"use client"`. Putting the
13
+ * shared class strings here lets the static half stay static and the
14
+ * interactive half cross the boundary alone (`./_status-bar-item.tsx`), instead
15
+ * of the whole bar crossing it because one of its items might be a button.
16
+ *
17
+ * Everything below is token-backed and enumerated per tone. Nothing is derived
18
+ * at runtime from a tone name, because a class string assembled from a template
19
+ * literal is a class string Tailwind's scanner never sees.
20
+ */
21
+ /** Ink for each tone; `neutral` inherits the bar's own subtle grey. */
22
+ const ITEM_TONES = {
23
+ neutral: "",
24
+ accent: "text-accent",
25
+ ok: "text-ok",
26
+ busy: "text-busy",
27
+ warn: "text-warn",
28
+ danger: "text-danger",
29
+ info: "text-info"
30
+ };
31
+ /**
32
+ * Every item, static or not: one line, never wrapping, never growing the bar.
33
+ *
34
+ * This exact string is what a plain `<StatusBar.Item>` rendered before any of
35
+ * the interactive vocabulary existed, and it still is — the appearance function
36
+ * adds to it and never rewrites it, so a console that upgrades sees the same
37
+ * readouts it had.
38
+ */
39
+ const ITEM_BASE = "inline-flex min-w-0 shrink-0 items-center gap-(--cue-space-2) whitespace-nowrap";
40
+ /**
41
+ * What an item gains when it becomes a real control.
42
+ *
43
+ * The reset first: a `<button>` arrives with a border, a ground, a gutter and a
44
+ * font of the user agent's choosing, and none of them belong in a 24px strip.
45
+ * `text-(length:--cue-text-label)` and `font-mono` are re-declared rather than
46
+ * inherited for the same reason — form controls do not inherit type.
47
+ *
48
+ * `h-full` is the VS Code reading of a status item and the one that makes hover
49
+ * legible: the fill is the height of the bar, not the height of the words, so a
50
+ * pointer anywhere in the strip is over the target rather than beside it.
51
+ *
52
+ * The focus ring is inset (`-outline-offset-1`). An outset ring on a bar this
53
+ * low paints across the item's neighbours and the hairline above them, which
54
+ * reads as the whole bar being focused rather than one reading in it.
55
+ *
56
+ * The disabled treatment is declared twice, once for `:disabled` and once for
57
+ * `[data-disabled]`. An item that a `render` prop turned into an anchor can
58
+ * never match `:disabled` — the pseudo-class is defined on form controls — so
59
+ * without the data twin a disabled link would read as perfectly live.
60
+ */
61
+ const INTERACTIVE_CHASSIS = "h-full cursor-pointer border-0 bg-transparent py-0 font-mono text-(length:--cue-text-label) outline-none transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-1 focus-visible:outline-accent disabled:pointer-events-none disabled:opacity-40 data-[disabled]:pointer-events-none data-[disabled]:opacity-40 motion-reduce:transition-none";
62
+ /**
63
+ * A quiet control's gutter, and the negative margin that pays for it.
64
+ *
65
+ * The bar sets `gap-(--cue-space-4)` between items. Padding an item to give its
66
+ * hover fill somewhere to live would push its text `--cue-space-2` further from
67
+ * its neighbour, so the same length is taken straight back off the margin: the
68
+ * words stay exactly where a static reading's words would be, and the fill
69
+ * bleeds halfway into the gap either side. VS Code does the identical trick
70
+ * with a 5px label padding inside a 3px item margin.
71
+ */
72
+ const QUIET_GEOMETRY = "px-(--cue-space-2) -mx-(--cue-space-2)";
73
+ /**
74
+ * A filled item's gutter. Wider, and with no negative margin at all.
75
+ *
76
+ * A filled item is a chip rather than a reading, and a chip whose ground bleeds
77
+ * into the gap on both sides has no gap left to be a chip in. VS Code makes the
78
+ * same split — its coloured items take the wider 8px padding while a plain one
79
+ * keeps 5px — so this is the next space token up and the margin stays put.
80
+ */
81
+ const FILLED_GEOMETRY = "h-full px-(--cue-space-3)";
82
+ /**
83
+ * The ground a `quiet` control paints, hovered, pressed, and as a member of a
84
+ * hovered {@link STATUS_BAR_GROUP_CLASSES cluster}.
85
+ *
86
+ * Three rungs of one ladder: the bar sits on `surface-1`, a hovered item lifts
87
+ * to `surface-2`, a pressed one to `surface-3`. The cluster rule is the VS Code
88
+ * pairing exactly — hovering any segment grounds every segment at the ordinary
89
+ * hover rung, and the segment actually under the cursor goes one rung further,
90
+ * so a two-item group reads as one control without hiding which half is live.
91
+ * The compound `group-hover/status-group:hover:` outranks the plain `hover:` on
92
+ * specificity rather than on source order, so which of them wins is decided by
93
+ * the cascade and not by how Tailwind happened to sort that day.
94
+ */
95
+ const QUIET_GROUNDS = "hover:bg-surface-2 active:bg-surface-3 group-hover/status-group:bg-surface-2 group-hover/status-group:hover:bg-surface-3";
96
+ /**
97
+ * The fill a `filled` item paints, per tone.
98
+ *
99
+ * `bg-<tone>/10` is the tinted-`Chip` recipe, deliberately: "warn" has to be
100
+ * the same wash in the status bar that it is on the chip that raised it. The
101
+ * derived `--cue-<tone>-soft` properties are not used here because only
102
+ * `accent-soft` is bridged into Tailwind's colour namespace — `bg-warn-soft`
103
+ * compiles to nothing at all — while the alpha modifier reaches every tone and
104
+ * lands on the same 10–12% wash the token was derived at.
105
+ */
106
+ const FILLED_TONES = {
107
+ neutral: "bg-surface-2 text-fg",
108
+ accent: "bg-accent/10",
109
+ ok: "bg-ok/10",
110
+ busy: "bg-busy/10",
111
+ warn: "bg-warn/10",
112
+ danger: "bg-danger/10",
113
+ info: "bg-info/10"
114
+ };
115
+ /**
116
+ * Where a filled control's fill steps to under the pointer, per tone.
117
+ *
118
+ * The tint deepens rather than being swapped for a grey: a warning that turns
119
+ * neutral on hover has stopped warning at the exact moment somebody reached for
120
+ * it. `/20` is `Chip`'s own interactive step, and `/30` is one further for the
121
+ * press. Neutral has only the surface ladder to climb and its top rung is
122
+ * `surface-3`, so hover and press share it.
123
+ */
124
+ const FILLED_GROUNDS = {
125
+ neutral: "hover:bg-surface-3 active:bg-surface-3",
126
+ accent: "hover:bg-accent/20 active:bg-accent/30",
127
+ ok: "hover:bg-ok/20 active:bg-ok/30",
128
+ busy: "hover:bg-busy/20 active:bg-busy/30",
129
+ warn: "hover:bg-warn/20 active:bg-warn/30",
130
+ danger: "hover:bg-danger/20 active:bg-danger/30",
131
+ info: "hover:bg-info/20 active:bg-info/30"
132
+ };
133
+ /**
134
+ * The compact-adjacency cluster: children one pixel apart, whatever the bar's
135
+ * own gap is.
136
+ *
137
+ * `gap-px` is the seam. `*:mx-0` is what makes the seam survive an interactive
138
+ * member, whose `-mx-(--cue-space-2)` would otherwise pull its neighbours a
139
+ * whole space token *over* it — the negative margin exists to bleed a lone
140
+ * item's fill into the bar's gap, and inside a cluster there is no gap to bleed
141
+ * into. It lands on the group because that is where the decision is: these are
142
+ * segments of one control, not readings that happen to be adjacent.
143
+ *
144
+ * `group/status-group` is the handle the members' cluster-hover rules read, and
145
+ * it is left on the element whether or not any member is interactive so a
146
+ * consumer can hang `group-hover/status-group:` rules of their own on it.
147
+ */
148
+ const STATUS_BAR_GROUP_CLASSES = "group/status-group flex h-full min-w-0 shrink-0 items-center gap-px *:mx-0";
149
+ /**
150
+ * Every class a status-bar item wears, from the three things that vary.
151
+ *
152
+ * One function rather than a class per shape, because the shapes multiply:
153
+ * seven tones times two prominences times interactive-or-not is 28 items, and
154
+ * 28 hand-written strings drift. Ordering is load-bearing — the merge is
155
+ * last-one-wins, so the reset comes before the gutter that overrides it and the
156
+ * tone comes after the `text-inherit` that would otherwise flatten it.
157
+ *
158
+ * @param appearance - Tone, interactivity and prominence; every field optional.
159
+ * @returns The merged class string, with no duplicate or cancelled utilities.
160
+ * @example
161
+ * statusBarItemClasses({}) // exactly the static item's own chassis
162
+ * @example
163
+ * statusBarItemClasses({ tone: "danger", interactive: true, prominence: "filled" })
164
+ */
165
+ function statusBarItemClasses(appearance) {
166
+ const { tone = "neutral", interactive = false, prominence = "quiet" } = appearance;
167
+ const filled = prominence === "filled";
168
+ return cn(ITEM_BASE, interactive ? INTERACTIVE_CHASSIS : void 0, interactive && tone === "neutral" ? "text-inherit" : void 0, interactive && !filled ? QUIET_GEOMETRY : void 0, filled ? FILLED_GEOMETRY : void 0, ITEM_TONES[tone], filled ? FILLED_TONES[tone] : void 0, interactive && !filled ? QUIET_GROUNDS : void 0, interactive && filled ? FILLED_GROUNDS[tone] : void 0);
169
+ }
170
+ //#endregion
171
+ export { ITEM_BASE, STATUS_BAR_GROUP_CLASSES, statusBarItemClasses };
@@ -47,8 +47,76 @@ interface AppShellBarProps extends React.ComponentPropsWithoutRef<"div"> {
47
47
  * is worse than no cell.
48
48
  */
49
49
  brand?: React.ReactNode;
50
- /** Extra classes for the header cell. */
50
+ /**
51
+ * Extra classes for the header cell — the bordered box that spans the
52
+ * navigation column, not the row of brand content inside it. Anything about
53
+ * the cell's width, rim or gutter belongs here; anything about how the mark
54
+ * and its controls sit belongs on the nodes passed as `brand`.
55
+ */
51
56
  brandClassName?: string;
57
+ /**
58
+ * Make the bar's dead space a window drag handle. Defaults to `false`.
59
+ *
60
+ * This is the same contract {@link TitleBar} has always offered, on the bar
61
+ * a console with a navigation column actually uses — a desktop app should
62
+ * not need a `TitleBar` *and* an `AppShell.Bar` stacked on top of each other
63
+ * to get one draggable row, which is two chrome rungs of a small window
64
+ * spent on furniture.
65
+ *
66
+ * Every structural surface is attributed and every interactive cluster is
67
+ * carved back out, because the two shells this ships into fail in opposite
68
+ * directions. Tauri matches the *exact* element under the cursor, so the
69
+ * brand cell and the rest cell carry the attribute themselves — a click on
70
+ * the cell's own dead space targets the cell, never the root beneath it, and
71
+ * a bar that only attributed its root would be draggable nowhere. Electron
72
+ * builds a geometric mask from painted boxes instead and excludes nothing on
73
+ * its own, so the brand content, the leading children and `actions` each sit
74
+ * in a cluster marked `app-region: no-drag` — without it a button that merely
75
+ * overlaps the bar stops receiving clicks at all. See `./_drag.ts`.
76
+ *
77
+ * **It changes how the clusters are sized, and that is a trade.** Off, each
78
+ * cluster grows into its cell, so a caller's own `ms-auto` reaches the cell's
79
+ * trailing rim. On, the clusters take their content's width and `actions` is
80
+ * pushed over with `ms-auto` instead, because a cluster that fills its cell
81
+ * is a `no-drag` rectangle painted over every dead pixel the cell just
82
+ * attributed — the bar would be draggable nowhere but its paddings and its
83
+ * one gap. The dead area belongs to the window in drag mode, and a caller who
84
+ * needs content to reach the rim there should say so on a box of their own
85
+ * inside the cluster.
86
+ *
87
+ * `select-none` comes with it: dragging a window and selecting the text you
88
+ * started the drag on are the same gesture, and the shell wins.
89
+ *
90
+ * Not wired here: double-click-to-maximise. Tauri's injected script already
91
+ * does it for any drag region, macOS's cancel-by-dragging-away included.
92
+ * Electron does not, and an Electron app has to listen for `dblclick` and
93
+ * toggle the window itself. Which of those an app is, is not something a
94
+ * layout component can know.
95
+ */
96
+ platformDrag?: boolean;
97
+ /**
98
+ * A title centred on the **bar**, not between the cells beside it.
99
+ *
100
+ * Absolutely positioned against the bar's own box, so it stays put when a
101
+ * button appears in `actions` or the column rails — the whole reason a
102
+ * document name in desktop chrome is not just another flex child.
103
+ * `pointer-events-none` so the dead space around a short title still drags,
104
+ * which is also what makes it click through to the drag surface underneath
105
+ * in Tauri, whose hit-testing is the browser's own.
106
+ *
107
+ * Inert by construction. Anything the reader has to click belongs in
108
+ * `actions` or in `children`, where it can be carved out of the drag region.
109
+ */
110
+ center?: React.ReactNode;
111
+ /**
112
+ * The trailing cluster: connection state, a run indicator, an overflow menu.
113
+ *
114
+ * Pinned to the bar's trailing edge and never squeezed, so it reads as window
115
+ * furniture rather than the end of a sentence. `children` keeps flowing into
116
+ * the same cell ahead of it, which is what lets an existing web-shell caller
117
+ * pass free-form content and gain a cluster without rearranging anything.
118
+ */
119
+ actions?: React.ReactNode;
52
120
  }
53
121
  type AppShellBodyProps = React.ComponentPropsWithoutRef<"div">;
54
122
  interface AppShellSidebarProps extends SidebarRootProps {
@@ -5,6 +5,7 @@ import { Sidebar } from "../layout/sidebar.js";
5
5
  import { CrossGlyph } from "../overlays/_glyphs.js";
6
6
  import { Drawer } from "../overlays/drawer.js";
7
7
  import { IconButton } from "../primitives/icon-button.js";
8
+ import { DRAG_CLASSES, NO_DRAG_CLASSES } from "./_drag.js";
8
9
  import * as React from "react";
9
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
11
  //#region src/chrome/app-shell.tsx
@@ -121,21 +122,47 @@ const AppShell = {
121
122
  })
122
123
  });
123
124
  }),
124
- Bar: React.forwardRef(function AppShellBar({ className, brand, brandClassName, children, ...elementProps }, ref) {
125
+ Bar: React.forwardRef(function AppShellBar({ className, brand, brandClassName, platformDrag = false, center, actions, children, ...elementProps }, ref) {
126
+ const dragProps = platformDrag ? { "data-tauri-drag-region": "" } : void 0;
127
+ const dragClasses = platformDrag ? DRAG_CLASSES : void 0;
128
+ const clusterClasses = cn("flex min-w-0 items-center gap-(--cue-space-3)", platformDrag ? NO_DRAG_CLASSES : "grow");
125
129
  return /* @__PURE__ */ jsxs("div", {
126
130
  ref,
127
131
  "data-slot": "app-shell-bar",
128
- className: cn("flex h-(--cue-chrome-titlebar) w-full shrink-0 items-stretch border-b border-border bg-surface-1", className),
132
+ ...dragProps,
133
+ className: cn("relative flex h-(--cue-chrome-titlebar) w-full shrink-0 items-stretch border-b border-border bg-surface-1", dragClasses, platformDrag ? "select-none" : void 0, className),
129
134
  ...elementProps,
130
- children: [brand === void 0 ? null : /* @__PURE__ */ jsx("div", {
131
- "data-slot": "app-shell-brand",
132
- className: cn("flex w-auto min-w-0 shrink-0 items-center gap-(--cue-space-3) border-r border-border px-(--cue-pad-row-x) transition-[width] duration-150 ease-out md:w-(--cue-nav-width) motion-reduce:transition-none", brandClassName),
133
- children: brand
134
- }), /* @__PURE__ */ jsx("div", {
135
- "data-slot": "app-shell-bar-rest",
136
- className: "flex min-w-0 flex-1 items-center gap-(--cue-space-3) px-(--cue-pad-row-x)",
137
- children
138
- })]
135
+ children: [
136
+ brand === void 0 ? null : /* @__PURE__ */ jsx("div", {
137
+ "data-slot": "app-shell-brand",
138
+ ...dragProps,
139
+ className: cn("flex w-auto min-w-0 shrink-0 items-center gap-(--cue-space-3) border-r border-border px-(--cue-pad-row-x) transition-[width] duration-150 ease-out md:w-(--cue-nav-width) motion-reduce:transition-none", dragClasses, brandClassName),
140
+ children: /* @__PURE__ */ jsx("div", {
141
+ "data-slot": "app-shell-brand-content",
142
+ className: clusterClasses,
143
+ children: brand
144
+ })
145
+ }),
146
+ center === void 0 ? null : /* @__PURE__ */ jsx("div", {
147
+ "data-slot": "app-shell-bar-center",
148
+ className: "pointer-events-none absolute top-1/2 left-1/2 max-w-[50%] -translate-x-1/2 -translate-y-1/2 truncate font-mono text-(length:--cue-text-label) text-fg-muted",
149
+ children: center
150
+ }),
151
+ /* @__PURE__ */ jsxs("div", {
152
+ "data-slot": "app-shell-bar-rest",
153
+ ...dragProps,
154
+ className: cn("flex min-w-0 flex-1 items-center gap-(--cue-space-3) px-(--cue-pad-row-x)", dragClasses),
155
+ children: [/* @__PURE__ */ jsx("div", {
156
+ "data-slot": "app-shell-bar-leading",
157
+ className: clusterClasses,
158
+ children
159
+ }), actions === void 0 ? null : /* @__PURE__ */ jsx("div", {
160
+ "data-slot": "app-shell-bar-actions",
161
+ className: cn(clusterClasses, "grow-0 shrink-0", platformDrag ? "ms-auto" : void 0),
162
+ children: actions
163
+ })]
164
+ })
165
+ ]
139
166
  });
140
167
  }),
141
168
  Body: React.forwardRef(function AppShellBody({ className, ...elementProps }, ref) {
@@ -13,9 +13,10 @@ import { Row, RowProps, SeamList, SeamListProps, rowVariants } from "./row.js";
13
13
  import { ScrollArea, ScrollAreaOrientation, ScrollAreaProps } from "./scroll-area.js";
14
14
  import { SectionHeader, SectionHeaderProps } from "./section-header.js";
15
15
  import { SegmentedControl, SegmentedControlItem, SegmentedControlProps, SegmentedControlSize } from "./segmented-control.js";
16
- import { StatusBar, StatusBarItemProps, StatusBarProps, StatusBarTone } from "./status-bar.js";
16
+ import { StatusBarProminence } from "./_status-bar.js";
17
+ import { StatusBar, StatusBarGroupProps, StatusBarItemProps, StatusBarProps, StatusBarTone } from "./status-bar.js";
17
18
  import { ScrollableTabsList, ScrollableTabsListProps, Tabs, TabsListProps, TabsPanelProps, TabsRootProps, TabsTabProps } from "./tabs.js";
18
19
  import { TitleBar, TitleBarProps } from "./title-bar.js";
19
20
  import { Tree, TreeProps, TreeVisibility, TreeVisibilityToggle, TreeVisibilityToggleProps } from "./tree.js";
20
21
  import { Toolbar, ToolbarButtonProps, ToolbarGroupProps, ToolbarRootProps, ToolbarSeparatorProps } from "./toolbar.js";
21
- export { AppBar, type AppBarActionsProps, type AppBarBrandProps, type AppBarLinkProps, type AppBarNavProps, type AppBarRegister, type AppBarRootProps, type AppBarSeparatorProps, AppShell, type AppShellAsideProps, type AppShellBarProps, type AppShellBodyProps, type AppShellContentProps, type AppShellNavTriggerProps, type AppShellRootProps, type AppShellScrollerProps, type AppShellSidebarProps, type BarOverflow, EmptyState, type EmptyStateProps, Eyebrow, type EyebrowProps, Footer, type FooterProps, NavigationMenu, type NavigationMenuContentProps, type NavigationMenuItemProps, type NavigationMenuLinkProps, type NavigationMenuListProps, type NavigationMenuRootProps, type NavigationMenuTriggerProps, type NavigationMenuViewportProps, PageShell, type PageShellProps, type PageWidth, Panel, PanelHeader, type PanelHeaderProps, type PanelProps, Row, type RowProps, ScrollArea, type ScrollAreaOrientation, type ScrollAreaProps, type ScrollEdges, ScrollableTabsList, type ScrollableTabsListProps, SeamList, type SeamListProps, SectionHeader, type SectionHeaderProps, SegmentedControl, type SegmentedControlItem, type SegmentedControlProps, type SegmentedControlSize, StatusBar, type StatusBarItemProps, type StatusBarProps, type StatusBarTone, TAB_FADE_PX, TREE_GUIDE_GLYPHS, Tabs, type TabsListProps, type TabsPanelProps, type TabsRootProps, type TabsTabProps, TitleBar, type TitleBarProps, Toolbar, type ToolbarButtonProps, type ToolbarGroupProps, type ToolbarRootProps, type ToolbarSeparatorProps, Tree, type TreeGuide, type TreeItem, type TreeProps, type TreeRow, type TreeVisibility, TreeVisibilityToggle, type TreeVisibilityToggleProps, computeScrollEdges, edgeFadeMask, flattenTree, rowVariants };
22
+ export { AppBar, type AppBarActionsProps, type AppBarBrandProps, type AppBarLinkProps, type AppBarNavProps, type AppBarRegister, type AppBarRootProps, type AppBarSeparatorProps, AppShell, type AppShellAsideProps, type AppShellBarProps, type AppShellBodyProps, type AppShellContentProps, type AppShellNavTriggerProps, type AppShellRootProps, type AppShellScrollerProps, type AppShellSidebarProps, type BarOverflow, EmptyState, type EmptyStateProps, Eyebrow, type EyebrowProps, Footer, type FooterProps, NavigationMenu, type NavigationMenuContentProps, type NavigationMenuItemProps, type NavigationMenuLinkProps, type NavigationMenuListProps, type NavigationMenuRootProps, type NavigationMenuTriggerProps, type NavigationMenuViewportProps, PageShell, type PageShellProps, type PageWidth, Panel, PanelHeader, type PanelHeaderProps, type PanelProps, Row, type RowProps, ScrollArea, type ScrollAreaOrientation, type ScrollAreaProps, type ScrollEdges, ScrollableTabsList, type ScrollableTabsListProps, SeamList, type SeamListProps, SectionHeader, type SectionHeaderProps, SegmentedControl, type SegmentedControlItem, type SegmentedControlProps, type SegmentedControlSize, StatusBar, type StatusBarGroupProps, type StatusBarItemProps, type StatusBarProminence, type StatusBarProps, type StatusBarTone, TAB_FADE_PX, TREE_GUIDE_GLYPHS, Tabs, type TabsListProps, type TabsPanelProps, type TabsRootProps, type TabsTabProps, TitleBar, type TitleBarProps, Toolbar, type ToolbarButtonProps, type ToolbarGroupProps, type ToolbarRootProps, type ToolbarSeparatorProps, Tree, type TreeGuide, type TreeItem, type TreeProps, type TreeRow, type TreeVisibility, TreeVisibilityToggle, type TreeVisibilityToggleProps, computeScrollEdges, edgeFadeMask, flattenTree, rowVariants };