@signal9/era-ui 4.14.1 → 4.15.1

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 -->
@@ -1028,6 +1029,7 @@ after the pane's own. Pass Compartments for reactive plugin sets. |
1028
1029
  | `onDragEnd?` | `(data: DragEventData) => void` | — | — |
1029
1030
  | `onClose?` | `() => void` | — | When set, a close button renders at the right of the handle bar. |
1030
1031
  | `header?` | `Snippet` | — | Extra handle-bar content, rendered after the title. |
1032
+ | `center?` | `Snippet` | — | Content centred on the bar itself — Pane.Handle's three-track grid. |
1031
1033
  | `children?` | `Snippet` | — | — |
1032
1034
 
1033
1035
  <!-- end: pane -->
@@ -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"
@@ -194,7 +194,7 @@
194
194
  "slug": "pane",
195
195
  "title": "Pane",
196
196
  "summary": "A draggable floating window with a handle bar.",
197
- "tokenEstimate": 416,
197
+ "tokenEstimate": 441,
198
198
  "sections": [
199
199
  "Import",
200
200
  "Props"
@@ -29,4 +29,5 @@ after the pane's own. Pass Compartments for reactive plugin sets. |
29
29
  | `onDragEnd?` | `(data: DragEventData) => void` | — | — |
30
30
  | `onClose?` | `() => void` | — | When set, a close button renders at the right of the handle bar. |
31
31
  | `header?` | `Snippet` | — | Extra handle-bar content, rendered after the title. |
32
+ | `center?` | `Snippet` | — | Content centred on the bar itself — Pane.Handle's three-track grid. |
32
33
  | `children?` | `Snippet` | — | — |
@@ -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';
@@ -150,6 +150,14 @@ export interface AppDefinition {
150
150
  * live AppPane too, so a host can swap it as the pane's content
151
151
  * changes. */
152
152
  actions?: Snippet;
153
+ /** Replaces the default icon+title cluster on the LEFT of the title bar.
154
+ * Assignable live, like `actions`. */
155
+ lead?: Snippet;
156
+ /** Content centred on the title bar itself (a document title, a breadcrumb) —
157
+ * rendered in Pane.Handle's three-track grid, so it stays pinned to the true
158
+ * centre of the bar however the sides grow. Assignable live. Interactive
159
+ * content in it must carry data-pane-control (the bar is the drag zone). */
160
+ center?: Snippet;
153
161
  }
154
162
  /** A live pane instance in the registry. */
155
163
  export interface AppPane {
@@ -173,6 +181,10 @@ export interface AppPane {
173
181
  minimizable: boolean;
174
182
  bodyClass?: string;
175
183
  actions?: Snippet;
184
+ /** See AppDefinition — both assignable on the live pane, which is how an
185
+ * app claims its own chrome: `pane.center = mySnippet` from the body. */
186
+ lead?: Snippet;
187
+ center?: Snippet;
176
188
  /** App-specific state, carried verbatim through a snapshot. */
177
189
  props?: JsonValue;
178
190
  }
@@ -284,3 +296,7 @@ export declare class PaneManager {
284
296
  }
285
297
  export declare function setPaneManagerContext(pm: PaneManager): PaneManager;
286
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;
@@ -183,6 +183,8 @@ export class PaneManager {
183
183
  minimizable: app.minimizable ?? true,
184
184
  bodyClass: app.bodyClass,
185
185
  actions: app.actions,
186
+ lead: app.lead,
187
+ center: app.center,
186
188
  props: app.props
187
189
  };
188
190
  this.panes.push(win);
@@ -364,6 +366,8 @@ export class PaneManager {
364
366
  minimizable: app.minimizable ?? true,
365
367
  bodyClass: app.bodyClass,
366
368
  actions: app.actions,
369
+ lead: app.lead,
370
+ center: app.center,
367
371
  props: s.props ?? app.props
368
372
  });
369
373
  }
@@ -444,3 +448,9 @@ export function getPaneManager() {
444
448
  throw new Error('getPaneManager() must be used inside a <Desktop>.');
445
449
  return pm;
446
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
+ }
@@ -92,14 +92,21 @@
92
92
  from the edge against 5px above and below it — read as crowded because it
93
93
  was. --era-inset-md is (h-md − text)/2, so the label now starts the same
94
94
  distance in from the edge as it sits from the top. -->
95
- <Pane.Handle class="gap-(--era-gap) pl-(--era-inset-md)">
96
- {#if pane.icon}
97
- {@const Icon = pane.icon}
98
- <Icon class="size-(--era-h-xs) shrink-0 text-muted" />
95
+ <Pane.Handle class="gap-(--era-gap) pl-(--era-inset-md)" center={pane.center}>
96
+ {#if pane.lead}
97
+ <!-- The app replaced the whole icon+title cluster. Its content lives in
98
+ the drag zone like the default one; anything interactive in it
99
+ carries data-pane-control itself. -->
100
+ {@render pane.lead()}
101
+ {:else}
102
+ {#if pane.icon}
103
+ {@const Icon = pane.icon}
104
+ <Icon class="size-(--era-h-xs) shrink-0 text-muted" />
105
+ {/if}
106
+ <span class={cn('truncate text-body', focused ? 'text-bright' : 'text-muted')}
107
+ >{pane.title}</span
108
+ >
99
109
  {/if}
100
- <span class={cn('truncate text-body', focused ? 'text-bright' : 'text-muted')}
101
- >{pane.title}</span
102
- >
103
110
  <!-- Close / minimize / maximize. Deliberately NOT data-pane-control (same as
104
111
  the raw Pane.Close): a left press-and-HOLD drags the pane — neodrag only
105
112
  drags the primary button and suppresses the trailing click after a real
@@ -109,7 +116,7 @@
109
116
  rhythm as the rest of the handle. Like the close button they carry no
110
117
  data-pane-control, so a press-and-hold anywhere on the bar still drags
111
118
  the pane and only a clean click activates them. -->
112
- <div class="ml-auto flex shrink-0 items-center gap-(--era-gap)">
119
+ {#snippet trail()}
113
120
  {@render pane.actions?.()}
114
121
  <!-- The macOS traffic lights, one button: red = close at rest, and the
115
122
  held preview recolours to the action it will commit — yellow for
@@ -158,7 +165,7 @@
158
165
  <X />
159
166
  {/if}
160
167
  </Button>
161
- </div>
168
+ {/snippet}
162
169
  </Pane.Handle>
163
170
 
164
171
  <Pane.Content class={pane.bodyClass}>
@@ -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);
@@ -454,12 +440,16 @@
454
440
  }
455
441
 
456
442
  /* -------------------------------------------------- */
457
- /* Signalnine — dark (Tomorrow, measured) */
443
+ /* Signalnine — dark (Tomorrow bones, amber ink) */
458
444
  /* */
459
- /* The classic imageboard dark theme. The accents and */
460
- /* the upper scale are Tomorrow Night's own values, */
461
- /* converted to OKLCH — comment and foreground land */
462
- /* exactly at steps 8 and 11. */
445
+ /* The classic imageboard dark theme. The scale keeps */
446
+ /* Tomorrow Night's lightnesses — comment and */
447
+ /* foreground still land at steps 8 and 11 — but the */
448
+ /* BLUE IS GONE: the ramp's cast and the accent are */
449
+ /* both a cool amber (hue 84, the yellow side of */
450
+ /* amber), so chrome, links and emphasis share one */
451
+ /* temperature instead of a blue ramp under yellow */
452
+ /* accents. */
463
453
  /* */
464
454
  /* The DARK END is deliberately deeper than Tomorrow's. */
465
455
  /* Its #1d1f21 page read light next to era's other dark */
@@ -474,11 +464,11 @@
474
464
  /* the link blue had been placed IN the ramp, which */
475
465
  /* forced the steps under it to swerve to reach it. */
476
466
  /* Tomorrow does not work that way and neither does */
477
- /* this now — the ramp is one cool gray from end to */
478
- /* end, and the blue, red, green and yellow are ACCENT */
479
- /* tokens sitting on top of it. That is what an */
480
- /* imageboard actually looks like: gray chrome, colour */
481
- /* carried by the text. */
467
+ /* this now — the ramp is one gray from end to end */
468
+ /* (cast faintly amber, never placed IN the ramp), and */
469
+ /* the amber, red and green are ACCENT tokens sitting */
470
+ /* on top of it. That is what an imageboard actually */
471
+ /* looks like: gray chrome, colour carried by the text. */
482
472
  /* -------------------------------------------------- */
483
473
  :root[data-theme='signalnine'],
484
474
  [data-theme='signalnine'].dark {
@@ -488,18 +478,18 @@
488
478
  * own #1d1f21 — that read too light beside the other dark themes. Everything
489
479
  * from step 2 up re-spaces evenly from the new floor to the comment anchor,
490
480
  * so lowering the page did not leave a cliff between it and the panels. */
491
- --color-1: oklch(0.168 0.005 248); /* #0d0f11 */
492
- --color-2: oklch(0.225 0.007 258); /* #1a1c1f */
493
- --color-3: oklch(0.29 0.01 258); /* #282c30 */
494
- --color-4: oklch(0.355 0.011 258); /* #383c42 */
495
- --color-5: oklch(0.435 0.011 257);
496
- --color-6: oklch(0.515 0.01 256);
497
- --color-7: oklch(0.596 0.009 255);
498
- --color-8: oklch(0.677 0.006 200); /* #939899 Tomorrow comment */
499
- --color-9: oklch(0.73 0.005 190);
500
- --color-10: oklch(0.78 0.005 180);
501
- --color-11: oklch(0.83 0.004 157); /* #c5c8c6 Tomorrow foreground */
502
- --color-12: oklch(0.9 0.004 157);
481
+ --color-1: oklch(0.168 0.004 84); /* graphite floor, amber-cast */
482
+ --color-2: oklch(0.225 0.005 84);
483
+ --color-3: oklch(0.29 0.007 84);
484
+ --color-4: oklch(0.355 0.008 84);
485
+ --color-5: oklch(0.435 0.008 84);
486
+ --color-6: oklch(0.515 0.008 84);
487
+ --color-7: oklch(0.596 0.007 84);
488
+ --color-8: oklch(0.677 0.006 84); /* comment grey, warmed */
489
+ --color-9: oklch(0.73 0.005 84);
490
+ --color-10: oklch(0.78 0.005 84);
491
+ --color-11: oklch(0.83 0.005 84); /* foreground, warmed */
492
+ --color-12: oklch(0.9 0.005 84);
503
493
 
504
494
  /* Border sits a step LOWER than the usual 5. Tomorrow's own rules are
505
495
  * quiet — its border colour is the current-line grey — and at step 5 the
@@ -507,25 +497,25 @@
507
497
  * page, inside the 1.7–3.2 the other dark themes hold. */
508
498
  --color-border: var(--color-4);
509
499
  --color-surface: var(--color-2);
510
- --color-hover: oklch(0.255 0.009 258);
500
+ --color-hover: oklch(0.255 0.006 84);
511
501
  --color-fg: var(--color-11);
512
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
513
502
  /* Tomorrow's comment grey, which is what it uses for de-emphasised text. */
514
503
  --color-muted: var(--color-8);
515
- /* The link blue every imageboard reader knows. */
516
- --color-link: oklch(0.6975 0.0551 244.3); /* #81a2be */
517
-
518
- /* Tomorrow's accents, unmodified. They are pastels, so each takes DARK text
519
- * — the page colour — rather than white. */
520
- --color-primary: oklch(0.6975 0.0551 244.3); /* #81a2be blue */
521
- --color-primary-fg: oklch(0.168 0.005 248);
504
+ /* The link follows the accent — one ink for "this is interactive". */
505
+ --color-link: var(--color-primary);
506
+
507
+ /* A COOL amber: hue 84 — the yellow side of amber, not orange — with the
508
+ * pastel restraint of the Tomorrow accents it replaces, so it still takes
509
+ * DARK text (the page colour) rather than white. */
510
+ --color-primary: oklch(0.76 0.115 84); /* cool amber */
511
+ --color-primary-fg: oklch(0.168 0.004 84);
522
512
  --color-primary-hover: color-mix(in oklch, var(--color-primary) 85%, oklch(0 0 none));
523
513
  --color-destructive: oklch(0.6308 0.1298 21.4); /* #cc6666 red */
524
- --color-destructive-fg: oklch(0.168 0.005 248);
514
+ --color-destructive-fg: oklch(0.168 0.004 84);
525
515
  --color-success: oklch(0.7733 0.1095 113.4); /* #b5bd68 green */
526
- --color-success-fg: oklch(0.168 0.005 248);
516
+ --color-success-fg: oklch(0.168 0.004 84);
527
517
  --color-warning: oklch(0.8462 0.1115 82.9); /* #f0c674 yellow */
528
- --color-warning-fg: oklch(0.168 0.005 248);
518
+ --color-warning-fg: oklch(0.168 0.004 84);
529
519
  }
530
520
 
531
521
  /* Signalnine — light (yahoo.co.jp, measured) */
@@ -603,7 +593,6 @@
603
593
  --color-surface: var(--color-2);
604
594
  --color-hover: oklch(0.97 0.014 268.5); /* #f1f5ff */
605
595
  --color-fg: var(--color-11);
606
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
607
596
  --color-muted: var(--color-7);
608
597
  --color-link: var(--color-10);
609
598
  /* Cards read --era-surface-bg, which the surface contract defines as
@@ -646,7 +635,6 @@
646
635
  --color-surface: var(--color-2);
647
636
  --color-hover: oklch(0.97 0.014 268.5); /* #f1f5ff */
648
637
  --color-fg: var(--color-11);
649
- --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
650
638
  --color-muted: var(--color-7);
651
639
  --color-link: var(--color-10);
652
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">;
@@ -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. -->