@signal9/era-ui 4.14.1 → 4.15.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.
@@ -1028,6 +1028,7 @@ after the pane's own. Pass Compartments for reactive plugin sets. |
1028
1028
  | `onDragEnd?` | `(data: DragEventData) => void` | — | — |
1029
1029
  | `onClose?` | `() => void` | — | When set, a close button renders at the right of the handle bar. |
1030
1030
  | `header?` | `Snippet` | — | Extra handle-bar content, rendered after the title. |
1031
+ | `center?` | `Snippet` | — | Content centred on the bar itself — Pane.Handle's three-track grid. |
1031
1032
  | `children?` | `Snippet` | — | — |
1032
1033
 
1033
1034
  <!-- end: pane -->
@@ -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` | — | — |
@@ -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
  }
@@ -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
  }
@@ -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}>
@@ -454,12 +454,16 @@
454
454
  }
455
455
 
456
456
  /* -------------------------------------------------- */
457
- /* Signalnine — dark (Tomorrow, measured) */
457
+ /* Signalnine — dark (Tomorrow bones, amber ink) */
458
458
  /* */
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. */
459
+ /* The classic imageboard dark theme. The scale keeps */
460
+ /* Tomorrow Night's lightnesses — comment and */
461
+ /* foreground still land at steps 8 and 11 — but the */
462
+ /* BLUE IS GONE: the ramp's cast and the accent are */
463
+ /* both a cool amber (hue 84, the yellow side of */
464
+ /* amber), so chrome, links and emphasis share one */
465
+ /* temperature instead of a blue ramp under yellow */
466
+ /* accents. */
463
467
  /* */
464
468
  /* The DARK END is deliberately deeper than Tomorrow's. */
465
469
  /* Its #1d1f21 page read light next to era's other dark */
@@ -474,11 +478,11 @@
474
478
  /* the link blue had been placed IN the ramp, which */
475
479
  /* forced the steps under it to swerve to reach it. */
476
480
  /* 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. */
481
+ /* this now — the ramp is one gray from end to end */
482
+ /* (cast faintly amber, never placed IN the ramp), and */
483
+ /* the amber, red and green are ACCENT tokens sitting */
484
+ /* on top of it. That is what an imageboard actually */
485
+ /* looks like: gray chrome, colour carried by the text. */
482
486
  /* -------------------------------------------------- */
483
487
  :root[data-theme='signalnine'],
484
488
  [data-theme='signalnine'].dark {
@@ -488,18 +492,18 @@
488
492
  * own #1d1f21 — that read too light beside the other dark themes. Everything
489
493
  * from step 2 up re-spaces evenly from the new floor to the comment anchor,
490
494
  * 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);
495
+ --color-1: oklch(0.168 0.004 84); /* graphite floor, amber-cast */
496
+ --color-2: oklch(0.225 0.005 84);
497
+ --color-3: oklch(0.29 0.007 84);
498
+ --color-4: oklch(0.355 0.008 84);
499
+ --color-5: oklch(0.435 0.008 84);
500
+ --color-6: oklch(0.515 0.008 84);
501
+ --color-7: oklch(0.596 0.007 84);
502
+ --color-8: oklch(0.677 0.006 84); /* comment grey, warmed */
503
+ --color-9: oklch(0.73 0.005 84);
504
+ --color-10: oklch(0.78 0.005 84);
505
+ --color-11: oklch(0.83 0.005 84); /* foreground, warmed */
506
+ --color-12: oklch(0.9 0.005 84);
503
507
 
504
508
  /* Border sits a step LOWER than the usual 5. Tomorrow's own rules are
505
509
  * quiet — its border colour is the current-line grey — and at step 5 the
@@ -507,25 +511,26 @@
507
511
  * page, inside the 1.7–3.2 the other dark themes hold. */
508
512
  --color-border: var(--color-4);
509
513
  --color-surface: var(--color-2);
510
- --color-hover: oklch(0.255 0.009 258);
514
+ --color-hover: oklch(0.255 0.006 84);
511
515
  --color-fg: var(--color-11);
512
516
  --color-bright: color-mix(in oklch, var(--color-12) 88%, var(--color-primary));
513
517
  /* Tomorrow's comment grey, which is what it uses for de-emphasised text. */
514
518
  --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);
519
+ /* The link follows the accent — one ink for "this is interactive". */
520
+ --color-link: var(--color-primary);
521
+
522
+ /* A COOL amber: hue 84 — the yellow side of amber, not orange — with the
523
+ * pastel restraint of the Tomorrow accents it replaces, so it still takes
524
+ * DARK text (the page colour) rather than white. */
525
+ --color-primary: oklch(0.76 0.115 84); /* cool amber */
526
+ --color-primary-fg: oklch(0.168 0.004 84);
522
527
  --color-primary-hover: color-mix(in oklch, var(--color-primary) 85%, oklch(0 0 none));
523
528
  --color-destructive: oklch(0.6308 0.1298 21.4); /* #cc6666 red */
524
- --color-destructive-fg: oklch(0.168 0.005 248);
529
+ --color-destructive-fg: oklch(0.168 0.004 84);
525
530
  --color-success: oklch(0.7733 0.1095 113.4); /* #b5bd68 green */
526
- --color-success-fg: oklch(0.168 0.005 248);
531
+ --color-success-fg: oklch(0.168 0.004 84);
527
532
  --color-warning: oklch(0.8462 0.1115 82.9); /* #f0c674 yellow */
528
- --color-warning-fg: oklch(0.168 0.005 248);
533
+ --color-warning-fg: oklch(0.168 0.004 84);
529
534
  }
530
535
 
531
536
  /* Signalnine — light (yahoo.co.jp, measured) */
@@ -7,11 +7,20 @@
7
7
  let {
8
8
  ref = $bindable(null),
9
9
  children,
10
+ center,
11
+ trail,
10
12
  class: className,
11
13
  ...restProps
12
14
  }: HTMLAttributes<HTMLDivElement> & {
13
15
  ref?: HTMLDivElement | null;
14
16
  children?: Snippet;
17
+ /** Content centred on the BAR itself (a document title, a breadcrumb).
18
+ * Presence switches the handle to its three-track grid — see below. */
19
+ center?: Snippet;
20
+ /** Trailing cluster. With `center` it is the grid's third track; without,
21
+ * it renders as the familiar ml-auto cluster. The close/window controls
22
+ * belong here, always LAST — extra trailing content goes before them. */
23
+ trail?: Snippet;
15
24
  } = $props();
16
25
  </script>
17
26
 
@@ -49,9 +58,35 @@
49
58
  // bar itself. It still follows the bottom radius, so the divider curves up
50
59
  // at the ends exactly as before.
51
60
  'shrink-0 rounded-t-none rounded-b-(--era-rd-md) [box-shadow:0_1px_0_var(--color-divider-faded)] select-none',
61
+ // THE CENTRE LAW — [minmax(0,1fr) minmax(0,auto) minmax(0,1fr)].
62
+ //
63
+ // A centred title must be centred on the BAR, not on whatever space the
64
+ // sides happened to leave (a flex-1 spacer centres on the leftovers, so the
65
+ // title drifts every time the lead or trail grows — the reference terminal
66
+ // app has exactly that drift). Equal fr tracks pin the middle to the true
67
+ // centre no matter how unequal the sides are; when a side outgrows its
68
+ // share it TRUNCATES (minmax 0) instead of shoving the title, and only when
69
+ // both sides are exhausted does the centre itself start to yield. Nothing
70
+ // ever overlaps and nothing ever jumps.
71
+ //
72
+ // Grid only when centred content exists: the flex flow is the right model
73
+ // for every other handle, and children/trail render in it untouched.
74
+ center && 'grid grid-cols-[minmax(0,1fr)_minmax(0,auto)_minmax(0,1fr)]',
52
75
  className
53
76
  )}
54
77
  {...restProps}
55
78
  >
56
- {@render children?.()}
79
+ {#if center}
80
+ <div class="flex min-w-0 items-center gap-(--era-gap)">{@render children?.()}</div>
81
+ <!-- Anything interactive dropped in here (an editable title, a picker)
82
+ carries data-pane-control itself: the bar is the drag zone, and only
83
+ 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>
86
+ {:else}
87
+ {@render children?.()}
88
+ {#if trail}
89
+ <div class="ml-auto flex shrink-0 items-center gap-(--era-gap)">{@render trail()}</div>
90
+ {/if}
91
+ {/if}
57
92
  </Bar>
@@ -3,6 +3,13 @@ import type { HTMLAttributes } from 'svelte/elements';
3
3
  type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
4
4
  ref?: HTMLDivElement | null;
5
5
  children?: Snippet;
6
+ /** Content centred on the BAR itself (a document title, a breadcrumb).
7
+ * Presence switches the handle to its three-track grid — see below. */
8
+ center?: Snippet;
9
+ /** Trailing cluster. With `center` it is the grid's third track; without,
10
+ * it renders as the familiar ml-auto cluster. The close/window controls
11
+ * belong here, always LAST — extra trailing content goes before them. */
12
+ trail?: Snippet;
6
13
  };
7
14
  declare const PaneHandle: import("svelte").Component<$$ComponentProps, {}, "ref">;
8
15
  type PaneHandle = ReturnType<typeof PaneHandle>;
@@ -23,6 +23,7 @@
23
23
  onDragEnd,
24
24
  onClose,
25
25
  header,
26
+ center,
26
27
  children,
27
28
  class: className,
28
29
  ...restProps
@@ -51,6 +52,8 @@
51
52
  onClose?: () => void;
52
53
  /** Extra handle-bar content, rendered after the title. */
53
54
  header?: Snippet;
55
+ /** Content centred on the bar itself — Pane.Handle's three-track grid. */
56
+ center?: Snippet;
54
57
  children?: Snippet;
55
58
  } = $props();
56
59
  </script>
@@ -58,6 +61,11 @@
58
61
  <!-- The ergonomic single-component pane: a titled handle + body, composed from
59
62
  the same Pane.Root / Handle / Content / Close parts you can assemble by hand
60
63
  for richer layouts (e.g. tabs in the handle — see Pane.Tabs). -->
64
+ <!-- Declared OUTSIDE <Root> — a snippet that is a direct child of a component
65
+ becomes one of its props, and Root has no `closeTrail`. -->
66
+ {#snippet closeTrail()}
67
+ <Close onclick={onClose} />
68
+ {/snippet}
61
69
  <Root
62
70
  bind:ref
63
71
  bind:position
@@ -71,11 +79,13 @@
71
79
  class={className}
72
80
  {...restProps}
73
81
  >
74
- <Handle>
82
+ <!-- The close cluster rides the `trail` slot, so it lands in the grid's third
83
+ track when `center` is present and in the ml-auto cluster when it isn't —
84
+ one close button, correct in both geometries. -->
85
+ <Handle {center} trail={onClose ? closeTrail : undefined}>
75
86
  {#if icon}{@const Icon = icon}<Icon class="size-(--era-h-xs) shrink-0 text-muted" />{/if}
76
87
  {#if title}<span class="text-body text-bright">{title}</span>{/if}
77
88
  {@render header?.()}
78
- {#if onClose}<Close onclick={onClose} class="ml-auto" />{/if}
79
89
  </Handle>
80
90
  {#if children}
81
91
  <Content>
@@ -33,6 +33,8 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
33
33
  onClose?: () => void;
34
34
  /** Extra handle-bar content, rendered after the title. */
35
35
  header?: Snippet;
36
+ /** Content centred on the bar itself — Pane.Handle's three-track grid. */
37
+ center?: Snippet;
36
38
  children?: Snippet;
37
39
  };
38
40
  declare const Pane: Component<$$ComponentProps, {}, "position" | "size" | "ref">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "4.14.1",
3
+ "version": "4.15.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",