@signal9/era-ui 4.15.0 → 4.15.2

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.
@@ -915,6 +915,7 @@ Inherits all props from `HTMLAttributes<HTMLDivElement>`.
915
915
  | `content?` | `'tier' \| 'xxs'` | `'tier'` | Tier of the controls inside — drives the bar's padding and gap so they
916
916
  stay concentric to them. `tier` (default) is one tier smaller; `xxs` is
917
917
  the compact pairing (xxs pills/fields in an sm or md bar). |
918
+ | `divider?` | `boolean` | `false` | The flush cap-a-panel chrome: outset divider below, bottom rd-md. |
918
919
  | `children?` | `Snippet` | — | — |
919
920
 
920
921
  <!-- end: bar -->
@@ -159,7 +159,7 @@
159
159
  "slug": "bar",
160
160
  "title": "Bar",
161
161
  "summary": "A flex container for toolbars and headers, in three heights.",
162
- "tokenEstimate": 181,
162
+ "tokenEstimate": 207,
163
163
  "sections": [
164
164
  "Import",
165
165
  "Props"
@@ -1,4 +1,4 @@
1
- export { PaneManager, getPaneManager, setPaneManagerContext, type Point, type Size, type Rect, type Insets, type Frame, type AppDefinition, type AppPane, type AppComponent, type AppProps, type PaneState, type JsonValue, type PaneSnapshot, type WorkspaceSnapshot, SNAPSHOT_VERSION, readSnapshot } from './pane-manager.svelte.js';
1
+ export { PaneManager, getPaneManager, tryGetPaneManager, setPaneManagerContext, type Point, type Size, type Rect, type Insets, type Frame, type AppDefinition, type AppPane, type AppComponent, type AppProps, type PaneState, type JsonValue, type PaneSnapshot, type WorkspaceSnapshot, SNAPSHOT_VERSION, readSnapshot } from './pane-manager.svelte.js';
2
2
  export { persistWorkspace, loadWorkspace, saveWorkspace, clearWorkspace, WORKSPACE_STORAGE_KEY } from './workspace-storage.svelte.js';
3
3
  export { type SnapZone, type Workspace } from './pane-manager.svelte.js';
4
4
  export { NotificationService, getNotifications, setNotificationContext, type AppNotification, type NotifyOptions, type NotificationAction, type NotificationTone } from './notifications.svelte.js';
package/dist/os/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Two stores (pane manager + notifications) shared via context, and the
3
3
  // components that render them: Desktop host, Pane, Taskbar (with its
4
4
  // built-in command bar), Toaster, NotificationCenter.
5
- export { PaneManager, getPaneManager, setPaneManagerContext, SNAPSHOT_VERSION, readSnapshot } from './pane-manager.svelte.js';
5
+ export { PaneManager, getPaneManager, tryGetPaneManager, setPaneManagerContext, SNAPSHOT_VERSION, readSnapshot } from './pane-manager.svelte.js';
6
6
  export { persistWorkspace, loadWorkspace, saveWorkspace, clearWorkspace, WORKSPACE_STORAGE_KEY } from './workspace-storage.svelte.js';
7
7
  export {} from './pane-manager.svelte.js';
8
8
  export { NotificationService, getNotifications, setNotificationContext } from './notifications.svelte.js';
@@ -296,3 +296,7 @@ export declare class PaneManager {
296
296
  }
297
297
  export declare function setPaneManagerContext(pm: PaneManager): PaneManager;
298
298
  export declare function getPaneManager(): PaneManager;
299
+ /** The optional form, for content that renders both inside and outside a
300
+ * Desktop (a docs route under ?bare, a component demoed standalone):
301
+ * undefined instead of a throw when no shell is up. */
302
+ export declare function tryGetPaneManager(): PaneManager | undefined;
@@ -448,3 +448,9 @@ export function getPaneManager() {
448
448
  throw new Error('getPaneManager() must be used inside a <Desktop>.');
449
449
  return pm;
450
450
  }
451
+ /** The optional form, for content that renders both inside and outside a
452
+ * Desktop (a docs route under ?bare, a component demoed standalone):
453
+ * undefined instead of a throw when no shell is up. */
454
+ export function tryGetPaneManager() {
455
+ return getContext(PM_KEY);
456
+ }
@@ -49,54 +49,8 @@
49
49
  --color-surface: var(--color-2);
50
50
  --color-hover: oklch(0.25 0 none);
51
51
  --color-fg: var(--color-11);
52
- /* EMPHASIS INK IS TINTED BY THE THEME, not pinned to the top of the ramp.
53
- *
54
- * Step 12 is the ramp's extreme, and at the extreme a neutral ramp has run out
55
- * of colour — so `bright` came out pure white on the dark themes and pure
56
- * black on signalnine light. That is the one place a themed UI should NOT go
57
- * achromatic: bright is the emphasis ink (headings, titles, the sentence you
58
- * are meant to read first), so it is the text carrying the most identity and
59
- * it was carrying none.
60
- *
61
- * It now takes 12% of --color-primary — the theme's own accent, which is also
62
- * what the accent picker writes, so choosing an accent tints the emphasis text
63
- * with it live. 12% is a cast, not a colour: it moves lightness by ~0.02 (the
64
- * default dark page keeps 11.6:1) and lands chroma near 0.013, which reads as
65
- * "warm white" or "blue-black" rather than as a coloured heading.
66
- *
67
- * A theme whose accent IS neutral (the default) is unchanged by construction —
68
- * there is no hue to lend — which is the correct behaviour rather than an
69
- * exception.
70
- *
71
- * The achromatic step 12s are written with hue `none` rather than hue 0, and
72
- * that is load-bearing: hue 0 is a REAL hue (red), so oklch interpolation
73
- * walks it round to the accent's the short way and a black-plus-blue mix came
74
- * out magenta (signalnine light's #000 landed on hue 349). `none` marks the
75
- * hue as powerless — which it is at chroma 0 — so the mix adopts the accent's
76
- * hue instead of travelling to it. The colours themselves are unchanged
77
- * everywhere else: at chroma 0 the hue paints nothing. */
78
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
79
- /* Muted is body copy, not a de-emphasis wash — the /text page sets whole
80
- * paragraphs in it — so it does NOT sit flat on step 10: it leans 40% of the
81
- * way to --color-bright. Step 10 alone read as "greyed out" at paragraph
82
- * length (7.80:1 on the default dark page); the lean lands it at 10.43:1, and
83
- * the ordering muted < fg < bright still holds by a clear margin (0.80 / 0.86
84
- * / 0.93 in dark), so the mixed-tone case — muted prose beside a bright
85
- * literal — keeps all three of its levels.
86
- *
87
- * It aims at BRIGHT rather than fg on purpose: fg is the ink of controls and
88
- * labels, bright is the ink of the sentence you are meant to read, and body
89
- * copy belongs with the second one. It also means muted inherits the theme's
90
- * tint through bright, so prose is never a neutral grey on a themed page.
91
- *
92
- * The lean is toward a TOKEN rather than a fixed lightness so it mirrors
93
- * correctly in light mode (muted DARKENS toward bright) and keeps each theme's
94
- * own hue family — the tinted scales interpolate between two steps of their
95
- * own ramp, never toward neutral.
96
- *
97
- * signalnine sets its muted by hand from measured source colours and is
98
- * deliberately exempt. */
99
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
52
+ /* --color-bright / --color-muted: declared once for every theme see the
53
+ * shared rule right after this block. */
100
54
  /* Inline link ink. Declared once here and inherited by every theme —
101
55
  * var(--color-bright) resolves in the element's own context, so a theme only
102
56
  * names this if it wants links to differ from emphasis text (signalnine
@@ -142,6 +96,54 @@
142
96
  --color-warning-fg: oklch(0.162 0 none);
143
97
  }
144
98
 
99
+ /* -------------------------------------------------- */
100
+ /* Emphasis + body ink — one law for every theme */
101
+ /* -------------------------------------------------- */
102
+ /* Declared ONCE and inherited, exactly like --color-link below in the base
103
+ * block: every theme selector targets the same elements (:root / a .dark or
104
+ * .light subtree), and the mixes reference tokens that resolve against THAT
105
+ * element's own ramp and accent — so each theme gets its own result from one
106
+ * formula, and a new theme cannot forget the law by not copying it.
107
+ *
108
+ * EMPHASIS INK IS TINTED BY THE THEME, not pinned to the top of the ramp.
109
+ * Step 12 is the ramp's extreme, and at the extreme a neutral ramp has run
110
+ * out of colour — bright used to come out pure white on the dark themes and
111
+ * literally #000 on signalnine light. That is the one place a themed UI
112
+ * should NOT go achromatic: bright is the emphasis ink (headings, titles,
113
+ * the sentence you are meant to read first), so it is the text carrying the
114
+ * most identity and it was carrying none. It takes 12% of --color-primary —
115
+ * the theme's own accent, and the property the accent picker writes, so
116
+ * choosing an accent tints the emphasis text live. 12% is a cast, not a
117
+ * colour: lightness moves ~0.02 (the default dark page keeps 11.6:1) and
118
+ * chroma lands near 0.013 — "warm white" / "blue-black", never a coloured
119
+ * heading. A theme whose accent IS neutral is unchanged by construction.
120
+ *
121
+ * The achromatic ramp steps are written with hue `none` rather than hue 0,
122
+ * and that is load-bearing here: hue 0 is a REAL hue (red), so oklch
123
+ * interpolation walks it toward the accent's hue the short way — #000 mixed
124
+ * with a CTA blue came out magenta. `none` marks the hue powerless (which it
125
+ * is at chroma 0), so the mix ADOPTS the accent's hue instead of travelling
126
+ * to it.
127
+ *
128
+ * MUTED IS BODY COPY, not a de-emphasis wash — the /text page sets whole
129
+ * paragraphs in it — so it does not sit flat on step 10: it leans 40% of the
130
+ * way to bright (10.43:1 on the default dark page; step 10 alone read as
131
+ * "greyed out" at paragraph length). It aims at BRIGHT rather than fg on
132
+ * purpose: fg is the ink of controls and labels, bright is the ink of the
133
+ * sentence you are meant to read, and body copy belongs with the second one
134
+ * — which also hands muted the theme's tint. The ordering muted < fg <
135
+ * bright holds by a clear margin (0.80 / 0.86 / 0.93 in dark).
136
+ *
137
+ * signalnine overrides muted only (measured source colours — Tomorrow's
138
+ * comment grey, yahoo's #5c7eb8): the single deliberate exception, and it
139
+ * wins on specificity, not on order. */
140
+ :root,
141
+ .dark,
142
+ .light {
143
+ --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
144
+ --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
145
+ }
146
+
145
147
  /* -------------------------------------------------- */
146
148
  /* Light theme (reversed scale) */
147
149
  /* -------------------------------------------------- */
@@ -166,8 +168,6 @@
166
168
  --color-surface: var(--color-2);
167
169
  --color-hover: oklch(0.8 0 none);
168
170
  --color-fg: var(--color-11);
169
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
170
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
171
171
 
172
172
  /* Accent tokens */
173
173
  --color-primary: var(--color-fg);
@@ -205,8 +205,6 @@
205
205
  --color-surface: var(--color-2);
206
206
  --color-hover: oklch(0.8 0 none);
207
207
  --color-fg: var(--color-11);
208
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
209
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
210
208
 
211
209
  /* Accent tokens */
212
210
  --color-primary: var(--color-fg);
@@ -249,8 +247,6 @@
249
247
  --color-surface: var(--color-2);
250
248
  --color-hover: oklch(0.252 0.024 56);
251
249
  --color-fg: var(--color-11);
252
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
253
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
254
250
 
255
251
  /* Warm sandstone primary + unexpected turquoise support. */
256
252
  --color-primary: oklch(0.74 0.108 68);
@@ -293,8 +289,6 @@
293
289
  --color-surface: var(--color-2);
294
290
  --color-hover: oklch(0.81 0.022 73);
295
291
  --color-fg: var(--color-11);
296
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
297
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
298
292
 
299
293
  --color-primary: oklch(0.515 0.16 38);
300
294
  --color-primary-fg: oklch(0.96 0.014 78);
@@ -328,8 +322,6 @@
328
322
  --color-surface: var(--color-2);
329
323
  --color-hover: oklch(0.81 0.022 73);
330
324
  --color-fg: var(--color-11);
331
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
332
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
333
325
 
334
326
  --color-primary: oklch(0.515 0.16 38);
335
327
  --color-primary-fg: oklch(0.96 0.014 78);
@@ -366,8 +358,6 @@
366
358
  --color-surface: var(--color-2);
367
359
  --color-hover: color-mix(in oklch, var(--color-3) 80%, var(--color-2));
368
360
  --color-fg: var(--color-11);
369
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
370
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
371
361
 
372
362
  --color-primary: oklch(0.8 0.034 54);
373
363
  --color-primary-fg: var(--color-1);
@@ -404,8 +394,6 @@
404
394
  --color-surface: var(--color-2);
405
395
  --color-hover: oklch(0.88 0.016 72);
406
396
  --color-fg: var(--color-11);
407
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
408
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
409
397
 
410
398
  --color-primary: oklch(0.55 0.12 58);
411
399
  --color-primary-fg: oklch(0.97 0.008 80);
@@ -439,8 +427,6 @@
439
427
  --color-surface: var(--color-2);
440
428
  --color-hover: oklch(0.88 0.016 72);
441
429
  --color-fg: var(--color-11);
442
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
443
- --color-muted: color-mix(in oklch, var(--color-10) 60%, var(--color-bright));
444
430
 
445
431
  --color-primary: oklch(0.55 0.12 58);
446
432
  --color-primary-fg: oklch(0.97 0.008 80);
@@ -513,7 +499,6 @@
513
499
  --color-surface: var(--color-2);
514
500
  --color-hover: oklch(0.255 0.006 84);
515
501
  --color-fg: var(--color-11);
516
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
517
502
  /* Tomorrow's comment grey, which is what it uses for de-emphasised text. */
518
503
  --color-muted: var(--color-8);
519
504
  /* The link follows the accent — one ink for "this is interactive". */
@@ -608,7 +593,6 @@
608
593
  --color-surface: var(--color-2);
609
594
  --color-hover: oklch(0.97 0.014 268.5); /* #f1f5ff */
610
595
  --color-fg: var(--color-11);
611
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
612
596
  --color-muted: var(--color-7);
613
597
  --color-link: var(--color-10);
614
598
  /* Cards read --era-surface-bg, which the surface contract defines as
@@ -651,7 +635,6 @@
651
635
  --color-surface: var(--color-2);
652
636
  --color-hover: oklch(0.97 0.014 268.5); /* #f1f5ff */
653
637
  --color-fg: var(--color-11);
654
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
655
638
  --color-muted: var(--color-7);
656
639
  --color-link: var(--color-10);
657
640
  /* Cards read --era-surface-bg, which the surface contract defines as
@@ -34,6 +34,28 @@
34
34
  content: {
35
35
  tier: 'gap-(--era-xs-inset-sm) px-(--era-xs-inset-sm)',
36
36
  xxs: ''
37
+ },
38
+ /**
39
+ * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
40
+ * an app's own section bars. One variant because the law kept getting
41
+ * restated by hand at every site:
42
+ *
43
+ * - shrink-0: the bar holds its tier height in a flex column.
44
+ * - The divider is drawn OUTSIDE the box (0 1px 0, never inset). An
45
+ * inset shadow paints over the bar's last pixel row, which belongs to
46
+ * the content's own bottom gap — a centred 18px control in a 24px bar
47
+ * has 3px above and 3 below, and the inset line ate one of the three.
48
+ * Outset costs no layout and puts the line on the boundary between
49
+ * the bar and what's beneath it.
50
+ * - Corners: the TOP defers to the panel's own radius (transparent bar,
51
+ * rounded surface behind), the BOTTOM rounds to rd-md so the divider
52
+ * curves up at the ends and the bar reads as a long pill. A square
53
+ * in-app bar (Notes) passes `rounded-none` and tailwind-merge drops
54
+ * both. Collapses to flat in square mode like every radius.
55
+ */
56
+ divider: {
57
+ true: 'shrink-0 rounded-t-none rounded-b-(--era-rd-md) [box-shadow:0_1px_0_var(--color-divider-faded)]',
58
+ false: ''
37
59
  }
38
60
  },
39
61
  compoundVariants: [
@@ -45,7 +67,7 @@
45
67
  // token because nothing pairs a page header with pill-tier controls.)
46
68
  { size: 'sm', content: 'xxs', class: 'gap-(--era-xxs-inset-sm) px-(--era-xxs-inset-sm)' }
47
69
  ],
48
- defaultVariants: { size: 'lg', content: 'tier' }
70
+ defaultVariants: { size: 'lg', content: 'tier', divider: false }
49
71
  });
50
72
  </script>
51
73
 
@@ -57,6 +79,7 @@
57
79
  ref = $bindable(null),
58
80
  size = 'lg',
59
81
  content = 'tier',
82
+ divider = false,
60
83
  children,
61
84
  class: className,
62
85
  ...restProps
@@ -70,10 +93,12 @@
70
93
  * the compact pairing (xxs pills/fields in an sm or md bar).
71
94
  */
72
95
  content?: 'tier' | 'xxs';
96
+ /** The flush cap-a-panel chrome: outset divider below, bottom rd-md. */
97
+ divider?: boolean;
73
98
  children?: Snippet;
74
99
  } = $props();
75
100
  </script>
76
101
 
77
- <div bind:this={ref} class={cn(barVariants({ size, content }), className)} {...restProps}>
102
+ <div bind:this={ref} class={cn(barVariants({ size, content, divider }), className)} {...restProps}>
78
103
  {@render children?.()}
79
104
  </div>
@@ -22,6 +22,28 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
22
22
  tier: "gap-(--era-xs-inset-sm) px-(--era-xs-inset-sm)";
23
23
  xxs: "";
24
24
  };
25
+ /**
26
+ * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
27
+ * an app's own section bars. One variant because the law kept getting
28
+ * restated by hand at every site:
29
+ *
30
+ * - shrink-0: the bar holds its tier height in a flex column.
31
+ * - The divider is drawn OUTSIDE the box (0 1px 0, never inset). An
32
+ * inset shadow paints over the bar's last pixel row, which belongs to
33
+ * the content's own bottom gap — a centred 18px control in a 24px bar
34
+ * has 3px above and 3 below, and the inset line ate one of the three.
35
+ * Outset costs no layout and puts the line on the boundary between
36
+ * the bar and what's beneath it.
37
+ * - Corners: the TOP defers to the panel's own radius (transparent bar,
38
+ * rounded surface behind), the BOTTOM rounds to rd-md so the divider
39
+ * curves up at the ends and the bar reads as a long pill. A square
40
+ * in-app bar (Notes) passes `rounded-none` and tailwind-merge drops
41
+ * both. Collapses to flat in square mode like every radius.
42
+ */
43
+ divider: {
44
+ true: "shrink-0 rounded-t-none rounded-b-(--era-rd-md) [box-shadow:0_1px_0_var(--color-divider-faded)]";
45
+ false: "";
46
+ };
25
47
  }, undefined, "flex items-center era-text-trim", {
26
48
  size: {
27
49
  sm: "h-(--era-h-sm) rounded-(--era-rd-sm)";
@@ -46,6 +68,28 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
46
68
  tier: "gap-(--era-xs-inset-sm) px-(--era-xs-inset-sm)";
47
69
  xxs: "";
48
70
  };
71
+ /**
72
+ * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
73
+ * an app's own section bars. One variant because the law kept getting
74
+ * restated by hand at every site:
75
+ *
76
+ * - shrink-0: the bar holds its tier height in a flex column.
77
+ * - The divider is drawn OUTSIDE the box (0 1px 0, never inset). An
78
+ * inset shadow paints over the bar's last pixel row, which belongs to
79
+ * the content's own bottom gap — a centred 18px control in a 24px bar
80
+ * has 3px above and 3 below, and the inset line ate one of the three.
81
+ * Outset costs no layout and puts the line on the boundary between
82
+ * the bar and what's beneath it.
83
+ * - Corners: the TOP defers to the panel's own radius (transparent bar,
84
+ * rounded surface behind), the BOTTOM rounds to rd-md so the divider
85
+ * curves up at the ends and the bar reads as a long pill. A square
86
+ * in-app bar (Notes) passes `rounded-none` and tailwind-merge drops
87
+ * both. Collapses to flat in square mode like every radius.
88
+ */
89
+ divider: {
90
+ true: "shrink-0 rounded-t-none rounded-b-(--era-rd-md) [box-shadow:0_1px_0_var(--color-divider-faded)]";
91
+ false: "";
92
+ };
49
93
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
50
94
  size: {
51
95
  sm: "h-(--era-h-sm) rounded-(--era-rd-sm)";
@@ -70,6 +114,28 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
70
114
  tier: "gap-(--era-xs-inset-sm) px-(--era-xs-inset-sm)";
71
115
  xxs: "";
72
116
  };
117
+ /**
118
+ * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
119
+ * an app's own section bars. One variant because the law kept getting
120
+ * restated by hand at every site:
121
+ *
122
+ * - shrink-0: the bar holds its tier height in a flex column.
123
+ * - The divider is drawn OUTSIDE the box (0 1px 0, never inset). An
124
+ * inset shadow paints over the bar's last pixel row, which belongs to
125
+ * the content's own bottom gap — a centred 18px control in a 24px bar
126
+ * has 3px above and 3 below, and the inset line ate one of the three.
127
+ * Outset costs no layout and puts the line on the boundary between
128
+ * the bar and what's beneath it.
129
+ * - Corners: the TOP defers to the panel's own radius (transparent bar,
130
+ * rounded surface behind), the BOTTOM rounds to rd-md so the divider
131
+ * curves up at the ends and the bar reads as a long pill. A square
132
+ * in-app bar (Notes) passes `rounded-none` and tailwind-merge drops
133
+ * both. Collapses to flat in square mode like every radius.
134
+ */
135
+ divider: {
136
+ true: "shrink-0 rounded-t-none rounded-b-(--era-rd-md) [box-shadow:0_1px_0_var(--color-divider-faded)]";
137
+ false: "";
138
+ };
73
139
  }, undefined>>;
74
140
  import type { HTMLAttributes } from 'svelte/elements';
75
141
  import type { Snippet } from 'svelte';
@@ -83,6 +149,8 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
83
149
  * the compact pairing (xxs pills/fields in an sm or md bar).
84
150
  */
85
151
  content?: 'tier' | 'xxs';
152
+ /** The flush cap-a-panel chrome: outset divider below, bottom rd-md. */
153
+ divider?: boolean;
86
154
  children?: Snippet;
87
155
  };
88
156
  declare const Bar: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -15,16 +15,13 @@
15
15
  <ContextMenu.Content
16
16
  bind:ref
17
17
  align="start"
18
- // p-gutter, not p-(--era-panel-inset): the inset is SURFACE-owned and resolves
19
- // to 0 on flat, so a menu there sat its rows flush against its own edges —
20
- // the highlight ran edge to edge and the panel read as a list with no frame.
21
- // The gutter is --era-gap, the same distance that separates the rows from
22
- // each other, so a row sits an even gap from every edge (the pattern the
23
- // conversations rail already documents). It is >= the panel inset at every
24
- // density, so glass and bevel keep the frame protection the inset exists
25
- // for.
18
+ // p-(--era-panel-inset), SURFACE-owned on purpose: 0 on flat rows run the
19
+ // panel's full width, flush, and the first row sits directly under the
20
+ // anchor bar (the reference terminal look) while glass and bevel keep the
21
+ // 2px frame that stops a full-width highlight from breaking their chrome.
22
+ // A fixed gutter was tried here and rejected: it padded flat menus too.
26
23
  class={cn(
27
- 'z-50 w-max overflow-hidden rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) p-gutter shadow-(--era-shadow-lg) glass-blur',
24
+ 'z-50 w-max overflow-hidden rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) p-(--era-panel-inset) shadow-(--era-shadow-lg) glass-blur',
28
25
  className
29
26
  )}
30
27
  {sideOffset}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { Dialog } from 'bits-ui';
4
+ import Overlay from './dialog-overlay.svelte';
4
5
  import { cn, type PartProps } from '../../utils/index.js';
5
6
 
6
7
  let {
@@ -22,7 +23,7 @@
22
23
  </script>
23
24
 
24
25
  <Dialog.Portal>
25
- <Dialog.Overlay class="fixed inset-0 z-50 bg-(--era-overlay-bg)" />
26
+ <Overlay />
26
27
  <!-- border-divider: the pane's hairline-edge law. A floating panel must be
27
28
  separable from what's behind it, and on the flat surface shadow-lg
28
29
  collapses to none — without the hairline the whole edge was a tone step. -->
@@ -1,10 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { Dialog } from 'bits-ui';
4
3
  import X from '@lucide/svelte/icons/x';
5
4
  import { Bar } from '../bar';
6
- import { Button } from '../button';
7
5
  import { cn } from '../../utils/index.js';
6
+ import Close from './dialog-close.svelte';
8
7
 
9
8
  let {
10
9
  dismissable = true,
@@ -18,39 +17,25 @@
18
17
  } = $props();
19
18
  </script>
20
19
 
21
- <!-- The pane handle's chrome, verbatim, for an overlay's top bar — one grammar
22
- for every floating panel's header so a dialog and a pane read as the same
23
- kind of surface:
20
+ <!-- The pane handle's chrome — by reference, not by copy: Bar's `divider`
21
+ variant IS the cap law (outset divider, rd-md corners), shared with
22
+ Pane.Handle. What this part adds:
24
23
 
25
- · an md bar holding xxs controls, walls and gap --era-xxs-inset-md
26
- · the divider drawn OUTSIDE the box (an inset one eats a pixel of every
27
- child's bottom gap — see Pane.Handle)
28
24
  · the leading side at --era-inset-md: a title has no corner to nest, so
29
25
  it takes the text inset, the same asymmetry the pane and the
30
26
  Conversations row document
31
- · the × is the dismiss family's: an xxs Button whose tier default IS the
32
- 12px ink, destructive like Pane.Close
27
+ · the × is the dismiss family's: an xxs `Dialog.Close` whose tier
28
+ default IS the 12px ink, destructive like Pane.Close
33
29
 
34
30
  Rendered flush by Dialog.Content / Sheet.Content when passed as their
35
31
  `header` snippet, so the bar spans the panel edge to edge and the panel's
36
32
  radius follows this corner region (rd-md — the same law that sizes the
37
33
  pane's own corners). -->
38
- <Bar
39
- size="md"
40
- content="xxs"
41
- class={cn(
42
- 'shrink-0 rounded-t-none rounded-b-(--era-rd-md) pl-(--era-inset-md) [box-shadow:0_1px_0_var(--color-divider-faded)] select-none',
43
- className
44
- )}
45
- >
34
+ <Bar size="md" content="xxs" divider class={cn('pl-(--era-inset-md) select-none', className)}>
46
35
  {@render children?.()}
47
36
  {#if dismissable}
48
- <Dialog.Close>
49
- {#snippet child({ props })}
50
- <Button icon size="xxs" tone="destructive" aria-label="Close" class="ml-auto" {...props}>
51
- <X />
52
- </Button>
53
- {/snippet}
54
- </Dialog.Close>
37
+ <Close icon size="xxs" tone="destructive" aria-label="Close" class="ml-auto">
38
+ <X />
39
+ </Close>
55
40
  {/if}
56
41
  </Bar>
@@ -15,16 +15,13 @@
15
15
  <DropdownMenu.Content
16
16
  bind:ref
17
17
  align="start"
18
- // p-gutter, not p-(--era-panel-inset): the inset is SURFACE-owned and resolves
19
- // to 0 on flat, so a menu there sat its rows flush against its own edges —
20
- // the highlight ran edge to edge and the panel read as a list with no frame.
21
- // The gutter is --era-gap, the same distance that separates the rows from
22
- // each other, so a row sits an even gap from every edge (the pattern the
23
- // conversations rail already documents). It is >= the panel inset at every
24
- // density, so glass and bevel keep the frame protection the inset exists
25
- // for.
18
+ // p-(--era-panel-inset), SURFACE-owned on purpose: 0 on flat rows run the
19
+ // panel's full width, flush, and the first row sits directly under the
20
+ // anchor bar (the reference terminal look) while glass and bevel keep the
21
+ // 2px frame that stops a full-width highlight from breaking their chrome.
22
+ // A fixed gutter was tried here and rejected: it padded flat menus too.
26
23
  class={cn(
27
- 'z-50 w-max min-w-(--bits-dropdown-menu-anchor-width) overflow-hidden rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) p-gutter shadow-(--era-shadow-lg) glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
24
+ 'z-50 w-max min-w-(--bits-dropdown-menu-anchor-width) overflow-hidden rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) p-(--era-panel-inset) shadow-(--era-shadow-lg) glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
28
25
  className
29
26
  )}
30
27
  {sideOffset}
@@ -15,16 +15,13 @@
15
15
  <Menubar.Content
16
16
  bind:ref
17
17
  align="start"
18
- // p-gutter, not p-(--era-panel-inset): the inset is SURFACE-owned and resolves
19
- // to 0 on flat, so a menu there sat its rows flush against its own edges —
20
- // the highlight ran edge to edge and the panel read as a list with no frame.
21
- // The gutter is --era-gap, the same distance that separates the rows from
22
- // each other, so a row sits an even gap from every edge (the pattern the
23
- // conversations rail already documents). It is >= the panel inset at every
24
- // density, so glass and bevel keep the frame protection the inset exists
25
- // for.
18
+ // p-(--era-panel-inset), SURFACE-owned on purpose: 0 on flat rows run the
19
+ // panel's full width, flush, and the first row sits directly under the
20
+ // anchor bar (the reference terminal look) while glass and bevel keep the
21
+ // 2px frame that stops a full-width highlight from breaking their chrome.
22
+ // A fixed gutter was tried here and rejected: it padded flat menus too.
26
23
  class={cn(
27
- 'z-50 w-max min-w-(--bits-menubar-menu-anchor-width) overflow-hidden rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) p-gutter shadow-(--era-shadow-lg) glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
24
+ 'z-50 w-max min-w-(--bits-menubar-menu-anchor-width) overflow-hidden rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) p-(--era-panel-inset) shadow-(--era-shadow-lg) glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
28
25
  className
29
26
  )}
30
27
  {sideOffset}
@@ -42,22 +42,12 @@
42
42
  data-pane-handle
43
43
  size="md"
44
44
  content="xxs"
45
+ divider
45
46
  class={cn(
46
- // The divider is drawn OUTSIDE the box (0 1px 0), not inset.
47
- //
48
- // An inset shadow paints over the last pixel row INSIDE the bar, which is a
49
- // row belonging to the content's own gap: a centred 18px control in a 24px
50
- // bar has 3px above and 3px below, and the inset line ate one of the three
51
- // below it. Every child of the bar therefore read 3px on top and right but
52
- // 2px on the bottom — even by the box, visibly tight to the eye, which
53
- // measures to the LINE.
54
- //
55
- // Outset costs no layout (so the bar keeps its full 24px content box and the
56
- // concentric 3px walls) and puts the line where it belongs: the boundary
57
- // between the bar and the content beneath it, rather than a pixel of the
58
- // bar itself. It still follows the bottom radius, so the divider curves up
59
- // at the ends exactly as before.
60
- 'shrink-0 rounded-t-none rounded-b-(--era-rd-md) [box-shadow:0_1px_0_var(--color-divider-faded)] select-none',
47
+ // The cap chrome (outset divider, rd-md bottom corners) is Bar's `divider`
48
+ // variant — the law and its full rationale live there, shared with
49
+ // Dialog.Header and any app's own section bars.
50
+ 'select-none',
61
51
  // THE CENTRE LAW — [minmax(0,1fr) minmax(0,auto) minmax(0,1fr)].
62
52
  //
63
53
  // A centred title must be centred on the BAR, not on whatever space the
@@ -77,16 +67,19 @@
77
67
  {...restProps}
78
68
  >
79
69
  {#if center}
80
- <div class="flex min-w-0 items-center gap-(--era-gap)">{@render children?.()}</div>
70
+ <!-- gap-[inherit] on every cluster: whatever gap the Bar resolved — its
71
+ content pairing, or a caller's override (the os pane sets --era-gap) —
72
+ is also the rhythm INSIDE each cluster, so the two can never drift. -->
73
+ <div class="flex min-w-0 items-center gap-[inherit]">{@render children?.()}</div>
81
74
  <!-- Anything interactive dropped in here (an editable title, a picker)
82
75
  carries data-pane-control itself: the bar is the drag zone, and only
83
76
  the control can know that a press on it means caret, not drag. -->
84
- <div class="flex min-w-0 items-center justify-center gap-(--era-gap)">{@render center()}</div>
85
- <div class="flex min-w-0 items-center justify-end gap-(--era-gap)">{@render trail?.()}</div>
77
+ <div class="flex min-w-0 items-center justify-center gap-[inherit]">{@render center()}</div>
78
+ <div class="flex min-w-0 items-center justify-end gap-[inherit]">{@render trail?.()}</div>
86
79
  {:else}
87
80
  {@render children?.()}
88
81
  {#if trail}
89
- <div class="ml-auto flex shrink-0 items-center gap-(--era-gap)">{@render trail()}</div>
82
+ <div class="ml-auto flex shrink-0 items-center gap-[inherit]">{@render trail()}</div>
90
83
  {/if}
91
84
  {/if}
92
85
  </Bar>