@deepseek-insight/container-base 0.0.0-stage → 0.1.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 (80) hide show
  1. package/README.md +77 -2
  2. package/dist/components/ChordPaletteOverlay.svelte +99 -0
  3. package/dist/components/ChordPaletteOverlay.svelte.d.ts +25 -0
  4. package/dist/components/PaneBase.svelte +159 -0
  5. package/dist/components/PaneBase.svelte.d.ts +27 -0
  6. package/dist/components/PanelHeaderSkeleton.svelte +85 -0
  7. package/dist/components/PanelHeaderSkeleton.svelte.d.ts +19 -0
  8. package/dist/components/PanelsZoom.svelte +213 -0
  9. package/dist/components/PanelsZoom.svelte.d.ts +26 -0
  10. package/dist/components/RailColumn.svelte +91 -0
  11. package/dist/components/RailColumn.svelte.d.ts +15 -0
  12. package/dist/components/ResizeGutter.svelte +68 -0
  13. package/dist/components/ResizeGutter.svelte.d.ts +13 -0
  14. package/dist/components/SidebarContainer.svelte +174 -0
  15. package/dist/components/SidebarContainer.svelte.d.ts +42 -0
  16. package/dist/components/SliderWidth.svelte +80 -0
  17. package/dist/components/SliderWidth.svelte.d.ts +27 -0
  18. package/dist/components/SliderZoom.svelte +77 -0
  19. package/dist/components/SliderZoom.svelte.d.ts +25 -0
  20. package/dist/i18n/LanguageMenu.svelte +165 -0
  21. package/dist/i18n/LanguageMenu.svelte.d.ts +25 -0
  22. package/dist/i18n/locale-prefs.svelte.d.ts +24 -0
  23. package/dist/i18n/locale-prefs.svelte.js +85 -0
  24. package/dist/index.d.ts +34 -0
  25. package/dist/index.js +35 -0
  26. package/dist/interaction/.gitkeep +0 -0
  27. package/dist/interaction/chord-machine.d.ts +108 -0
  28. package/dist/interaction/chord-machine.js +121 -0
  29. package/dist/interaction/gutter-resize.d.ts +37 -0
  30. package/dist/interaction/gutter-resize.js +41 -0
  31. package/dist/layout/.gitkeep +0 -0
  32. package/dist/layout/floor-math.d.ts +48 -0
  33. package/dist/layout/floor-math.js +60 -0
  34. package/dist/layout/placement.d.ts +18 -0
  35. package/dist/layout/placement.js +27 -0
  36. package/dist/registry/.gitkeep +0 -0
  37. package/dist/registry/action-registry.d.ts +30 -0
  38. package/dist/registry/action-registry.js +44 -0
  39. package/dist/storage/create-prefs-store.d.ts +25 -0
  40. package/dist/storage/create-prefs-store.js +67 -0
  41. package/dist/storage/namespace.d.ts +27 -0
  42. package/dist/storage/namespace.js +32 -0
  43. package/dist/theme/.gitkeep +0 -0
  44. package/dist/theme/tokens.css +35 -0
  45. package/dist/theming/ThemeToggle.svelte +170 -0
  46. package/dist/theming/ThemeToggle.svelte.d.ts +27 -0
  47. package/dist/theming/applier.d.ts +15 -0
  48. package/dist/theming/applier.js +29 -0
  49. package/dist/theming/theme-prefs.svelte.d.ts +21 -0
  50. package/dist/theming/theme-prefs.svelte.js +91 -0
  51. package/dist/types/.gitkeep +0 -0
  52. package/package.json +34 -5
  53. package/src/components/ChordPaletteOverlay.svelte +99 -0
  54. package/src/components/PaneBase.svelte +159 -0
  55. package/src/components/PanelHeaderSkeleton.svelte +85 -0
  56. package/src/components/PanelsZoom.svelte +213 -0
  57. package/src/components/RailColumn.svelte +91 -0
  58. package/src/components/ResizeGutter.svelte +68 -0
  59. package/src/components/SidebarContainer.svelte +174 -0
  60. package/src/components/SliderWidth.svelte +80 -0
  61. package/src/components/SliderZoom.svelte +77 -0
  62. package/src/i18n/LanguageMenu.svelte +165 -0
  63. package/src/i18n/locale-prefs.svelte.ts +112 -0
  64. package/src/index.ts +67 -0
  65. package/src/interaction/.gitkeep +0 -0
  66. package/src/interaction/chord-machine.ts +208 -0
  67. package/src/interaction/gutter-resize.ts +76 -0
  68. package/src/layout/.gitkeep +0 -0
  69. package/src/layout/floor-math.ts +94 -0
  70. package/src/layout/placement.ts +30 -0
  71. package/src/registry/.gitkeep +0 -0
  72. package/src/registry/action-registry.ts +62 -0
  73. package/src/storage/create-prefs-store.ts +87 -0
  74. package/src/storage/namespace.ts +34 -0
  75. package/src/theme/.gitkeep +0 -0
  76. package/src/theme/tokens.css +35 -0
  77. package/src/theming/ThemeToggle.svelte +170 -0
  78. package/src/theming/applier.ts +30 -0
  79. package/src/theming/theme-prefs.svelte.ts +118 -0
  80. package/src/types/.gitkeep +0 -0
package/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # @deepseek-insight/container-base
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Generic container furniture extracted from DeepSeek Insight (ADR-0062, R1):
4
+ zoom viewport, pane shells, rail + gutter, layout math, action registry,
5
+ armed-chord keyboard machine, prefs storage primitive, and a default theme
6
+ token set.
7
+
8
+ ## Install (npm)
9
+
10
+ ```sh
11
+ npm i @deepseek-insight/container-base
12
+ ```
13
+
14
+ svelte 5 is the only peer dependency (required — the package ships zero
15
+ runtime dependencies). The package is published with built `dist/` output
16
+ plus the raw `.svelte` sources packed alongside (ADR-0064 D2), so both
17
+ Svelte-aware toolchains and plain bundlers resolve a usable entry.
18
+
19
+ ### Consumption modes
20
+
21
+ | Mode | Who | What they get |
22
+ | --- | --- | --- |
23
+ | npm (`npm i`) | any Svelte 5 app | `dist/index.js` + `dist/index.d.ts` via the hardened exports map; component `.svelte` sources ride in the tarball for the toolchain to compile |
24
+ | workspace source (`workspace:*`) | DSI and the playground in this repo | same exports map, resolved through the pnpm workspace link — dist **plus** the packed raw sources (ADR-0064 D2 scope amendment) |
25
+
26
+ DSI vocabulary (paraglide, session types, domain unions) never enters this
27
+ package (boundary audit, task 5.3-T). The single public surface remains
28
+ `index.ts` — `dist/index.js` is built from it, never a second API.
29
+
30
+ ### Embedding the pre-paint theme applier
31
+
32
+ The applier ships as a script **source** the consumer generates into its
33
+ HTML template, so the theme lands before first paint (no flash):
34
+
35
+ ```html
36
+ <!-- src/app.html — inside <head>, before %sveltekit.head% -->
37
+ <script>${themeApplierScript('my-theme')}</script>
38
+ ```
39
+
40
+ ```ts
41
+ // a shared module / build step of the consumer
42
+ import { themeApplierScript } from '@deepseek-insight/container-base';
43
+ // generate the string once and inject it into app.html
44
+ ```
45
+
46
+ The survival-organs list below works identically in both modes — the
47
+ published tarball contains every organ listed.
48
+
49
+ ---
50
+
51
+ ## R2 — the survival organs (ADR-0063)
52
+
53
+ Everything a second consumer would otherwise re-roll badly now ships here:
54
+
55
+ - **`createThemePrefs({ baseKey })`** — the Three-State Lamp mechanism: choice `system/light/dark`, OS resolution, `html[data-theme]` hinge, profile-suffixed key.
56
+ - **`themeApplierScript(baseKey)`** — the pre-paint inline script SOURCE the consumer generates into its HTML template (see each app's `sync` script / `src/app.html`). It re-derives the identical key grammar.
57
+ - **`ThemeToggle`** — generic lamp mount: `prefs`, `triggerLabel`, `optionLabels`, `icons`, `dropUp`. Zero words, zero colors.
58
+ - **`createLocalePrefs({ baseKey, cookieName, locales, defaultLocale })`** — locale survival: dual-write (storage + mirror cookie the consumer's SSR chain reads), cookie→storage→default resolution, junk fallback.
59
+ - **`LanguageMenu`** — generic language mount: `prefs`, `locales`, `labels` (native names), `triggerLabel`, `icon`, `dropUp`. The base never translates.
60
+ - **`SliderWidth` / `SliderZoom`** — honest-slider controls: `WIDTH_BOUNDS`/`ZOOM_BOUNDS` defaults (prop-overridable), per-input live commit via `oncommit`, captions via snippet, `testId` stems.
61
+ - **`SidebarContainer`** — the sidebar shell: collapsed stub / embedded bare rail / standalone width-wrapper + gutter. Words and skin via snippets; state page-owned.
62
+ - **`createPrefsStore`** — the persistence primitive (SSR guard, junk fallback, clamp, best-effort write). Blob shapes are consumer-owned.
63
+
64
+ ### The two-app contract
65
+
66
+ | Fact | DSI | Playground (packages/container-base/playground) |
67
+ | --- | --- | --- |
68
+ | theme key | `dsi-theme` | `pg-theme` |
69
+ | locale key + cookie | `dsi.locale` (paraglide SSR reads it) | `pg.locale` |
70
+ | sliders blob | route-owned via panel prefs | `pg.panel` `{width, zoom}` |
71
+
72
+ Keys and cookies are per-app choices — the apps never share identity, only
73
+ mechanism. `?profile=<name>` suffixes every storage key (per-desk memory),
74
+ and the pre-paint applier re-derives that suffix before first paint.
75
+
76
+ The survival matrix (`tests/e2e/survival-matrix.spec.ts`) proves it live:
77
+ theme x locale x {DSI, playground} x reload at FIRST PAINT, plus
78
+ independence and junk-blob rows.
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ChordPaletteOverlay — the armed chord's face (ADR-0006a amendment
4
+ * D16/D18 port; container-base W4, ADR-0062 D8): a centered overlay
5
+ * with the live option rows and a decay ticker. The consumer passes
6
+ * its OWN labels (rows carry label text; title/ariaLabel arrive as
7
+ * props — the package never learns a translation system) and its
8
+ * action-window length (windowMs). The overlay holds NO armed state:
9
+ * open/since come in, onexpire fires out.
10
+ *
11
+ * While open it fills a footer progress bar 0→100% across
12
+ * idleCloseMs — inside windowMs the bar reads in-action, past it
13
+ * idle — and at 100% fires onexpire once. The clock is THIS
14
+ * component's visibility lifecycle (D18: view-lifecycle, not
15
+ * reducer state).
16
+ */
17
+
18
+ let {
19
+ open,
20
+ rows,
21
+ since,
22
+ windowMs,
23
+ idleCloseMs = 10_000,
24
+ title,
25
+ ariaLabel,
26
+ onexpire
27
+ }: {
28
+ /** True between the arming chord and the fire / disarm / decay. */
29
+ open: boolean;
30
+ /** The live options, already filtered by the consumer's reducer. */
31
+ rows: { key: string; label: string }[];
32
+ /** Date.now() of the arming — the decay clock's zero. */
33
+ since: number;
34
+ /** The consumer's action-window length (ms) — inside it the bar
35
+ * reads in-action; the consumer owns the semantics (ADR-0052). */
36
+ windowMs: number;
37
+ /** The idle window before the overlay closes itself (default 10s). */
38
+ idleCloseMs?: number;
39
+ /** The strip title (consumer-localized, names the armed prefix). */
40
+ title: string;
41
+ /** Accessible name for the overlay (consumer-localized). */
42
+ ariaLabel: string;
43
+ /** Fired once at 100% — the host resets the chord. */
44
+ onexpire?: () => void;
45
+ } = $props();
46
+
47
+ // The decay clock: the ticker runs ONLY while open — the host's timer
48
+ // discipline counts every persistent client interval, and an overlay
49
+ // that lives for seconds must not add a permanent one.
50
+ let now = $state(Date.now());
51
+ const elapsed = $derived(open ? Math.max(0, Math.min(now - (since || Date.now()), idleCloseMs)) : 0);
52
+ const expired = $derived(open && elapsed >= idleCloseMs);
53
+
54
+ $effect(() => {
55
+ if (!open) return;
56
+ const ticker = setInterval(() => (now = Date.now()), 100);
57
+ return () => clearInterval(ticker);
58
+ });
59
+ $effect(() => {
60
+ if (expired) onexpire?.();
61
+ });
62
+ const pct = $derived(Math.round((elapsed / idleCloseMs) * 100));
63
+ const inActionWindow = $derived(elapsed < windowMs);
64
+ // Ground rule: the ticker colors cross through tokens — no palette classes (W6).
65
+ const barColor = $derived(inActionWindow ? 'bg-[var(--color-chord-armed)]' : 'bg-[var(--color-chord-idle)]');
66
+ </script>
67
+
68
+ {#if open}
69
+ <div
70
+ class="fixed top-1/2 left-1/2 z-50 w-72 -translate-x-1/2 -translate-y-1/2 rounded-lg border border-[var(--color-surface-border)] bg-[var(--color-surface-elevated)] px-3 py-2 shadow-lg backdrop-blur"
71
+ data-testid="chord-palette"
72
+ data-chord-elapsed={elapsed}
73
+ aria-live="polite"
74
+ aria-label={ariaLabel}
75
+ >
76
+ <div class="mb-1 text-[10px] font-semibold tracking-wide text-[var(--color-text-secondary)] uppercase">
77
+ {title}
78
+ </div>
79
+ <ul class="m-0 list-none space-y-0.5 p-0">
80
+ {#each rows as row (row.key)}
81
+ <li class="flex items-center gap-2 text-xs" data-testid="chord-palette-row" data-chord-key={row.key}>
82
+ <kbd class="min-w-5 rounded border border-[var(--color-surface-border)] bg-[var(--color-surface-secondary)] px-1 text-center font-mono text-[11px]">
83
+ {row.key}
84
+ </kbd>
85
+ <span class="text-[var(--color-text-primary)]">{row.label}</span>
86
+ </li>
87
+ {/each}
88
+ </ul>
89
+ <!-- The idle clock: in-action while the consumer's window is open, idle after -->
90
+ <div class="mt-2 h-1 w-full overflow-hidden rounded bg-[var(--color-surface-secondary)]" data-testid="chord-palette-decay">
91
+ <div
92
+ class="h-full {barColor} transition-[width] duration-100 ease-linear"
93
+ style="width: {pct}%"
94
+ data-testid="chord-palette-progress"
95
+ data-chord-phase={inActionWindow ? 'action' : 'idle'}
96
+ ></div>
97
+ </div>
98
+ </div>
99
+ {/if}
@@ -0,0 +1,25 @@
1
+ type $$ComponentProps = {
2
+ /** True between the arming chord and the fire / disarm / decay. */
3
+ open: boolean;
4
+ /** The live options, already filtered by the consumer's reducer. */
5
+ rows: {
6
+ key: string;
7
+ label: string;
8
+ }[];
9
+ /** Date.now() of the arming — the decay clock's zero. */
10
+ since: number;
11
+ /** The consumer's action-window length (ms) — inside it the bar
12
+ * reads in-action; the consumer owns the semantics (ADR-0052). */
13
+ windowMs: number;
14
+ /** The idle window before the overlay closes itself (default 10s). */
15
+ idleCloseMs?: number;
16
+ /** The strip title (consumer-localized, names the armed prefix). */
17
+ title: string;
18
+ /** Accessible name for the overlay (consumer-localized). */
19
+ ariaLabel: string;
20
+ /** Fired once at 100% — the host resets the chord. */
21
+ onexpire?: () => void;
22
+ };
23
+ declare const ChordPaletteOverlay: import("svelte").Component<$$ComponentProps, {}, "">;
24
+ type ChordPaletteOverlay = ReturnType<typeof ChordPaletteOverlay>;
25
+ export default ChordPaletteOverlay;
@@ -0,0 +1,159 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ /**
4
+ * PaneBase — one pane's fixed-width column shell (split from DSI's
5
+ * PanelColumn, ADR-0062 W4, D8): the GENERIC chrome — the inline
6
+ * triple width lock, the selection ring, the narrow-container query,
7
+ * the activate/loupe gestures — lives here; the consumer supplies its
8
+ * own vocabulary as SNIPPETS (header / toolbar / content / footer).
9
+ * The package never reads a consumer store, type, or translation.
10
+ *
11
+ * Gestures (owner contract, carried verbatim from the split):
12
+ * - pointerdown/focusin anywhere INSIDE the column fire onactivate
13
+ * (interacting with a pane focuses its floor slot).
14
+ * - click with Alt held, target outside a control, fires onloupe —
15
+ * evaluated on the CLICK phase (never pointerdown: a dialog under
16
+ * an in-flight press eats its own compatibility mousedown), with
17
+ * ONE closest() exclusion instead of per-control stopPropagation.
18
+ */
19
+ let {
20
+ width,
21
+ paneId,
22
+ sessionTag = null,
23
+ selected = false,
24
+ onactivate,
25
+ onloupe,
26
+ header,
27
+ toolbar,
28
+ children,
29
+ footer
30
+ }: {
31
+ /** Column width in px — triple-locked inline (width/min/max). */
32
+ width: number;
33
+ /** The pane's id — data-panel-id (the zoom's selection-follow hook). */
34
+ paneId: string;
35
+ /** Opaque session-ish tag — data-session-id (null when the pane
36
+ * carries no session; the value is never interpreted here). */
37
+ sessionTag?: string | null;
38
+ /** Selection cue — the ring on the selected column. */
39
+ selected?: boolean;
40
+ /** Activate (select) this pane — pointerdown/focusin inside. */
41
+ onactivate?: () => void;
42
+ /** Loupe trigger — Alt+Click release outside a control. */
43
+ onloupe?: () => void;
44
+ /** Header strip slot (the consumer's floor chrome). */
45
+ header: Snippet;
46
+ /** Toolbar slot under the header (badges, controls). */
47
+ toolbar?: Snippet;
48
+ /** Content slot — the pane body. */
49
+ children: Snippet;
50
+ /** Footer slot (after the content). */
51
+ footer?: Snippet;
52
+ } = $props();
53
+
54
+ /** The loupe trigger: Alt held, target not inside a control —
55
+ * evaluated on click release (see the props doc). */
56
+ function handleLoupeTrigger(event: MouseEvent): void {
57
+ if (!event.altKey) return;
58
+ const target = event.target;
59
+ if (
60
+ target instanceof Element &&
61
+ target.closest('button, a, input, textarea, [role="button"]') !== null
62
+ ) {
63
+ return;
64
+ }
65
+ onloupe?.();
66
+ }
67
+ </script>
68
+
69
+ <!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events — the column is a
70
+ LAYOUT container, not a control: pointerdown is a passive
71
+ selection-follow, focusin is the keyboard parity for the same
72
+ selection, every real control inside stays itself, and the
73
+ click-phase Alt+Click is the loupe trigger. -->
74
+ <div
75
+ class="column"
76
+ class:selected
77
+ data-testid="panel-column"
78
+ data-panel-id={paneId}
79
+ data-session-id={sessionTag}
80
+ style="width: {width}px; min-width: {width}px; max-width: {width}px;"
81
+ onpointerdown={() => onactivate?.()}
82
+ onfocusin={() => onactivate?.()}
83
+ onclick={handleLoupeTrigger}
84
+ >
85
+ {@render header()}
86
+ {#if toolbar}
87
+ {@render toolbar()}
88
+ {/if}
89
+ <div class="body">
90
+ {@render children()}
91
+ </div>
92
+ {#if footer}
93
+ {@render footer()}
94
+ {/if}
95
+ </div>
96
+
97
+ <style>
98
+ .column {
99
+ position: relative; /* the width badge's absolute anchor */
100
+ /* Size container: the column's inline size is the narrow-header
101
+ trigger — header children query it instead of receiving width
102
+ as props. */
103
+ container-type: inline-size;
104
+ display: flex;
105
+ flex-direction: column;
106
+ height: 100%;
107
+ background: var(--color-surface-primary, var(--color-surface-elevated));
108
+ border-right: 1px solid var(--color-surface-border);
109
+ flex-shrink: 0;
110
+ }
111
+
112
+ /* Selection ring — a rounded gradient drawn OUTSIDE the column (same
113
+ geometry the flat outline had: the band sits in the resize seam, so
114
+ it never paints over any child component; the floor row carries
115
+ matching breathing room so the ring's segments are not clipped by
116
+ the viewport's overflow; both live inside the zoom transform, so
117
+ they scale in lockstep at any zoom). The ring is a masked
118
+ pseudo-element, not an outline: outlines cannot take a gradient or
119
+ a radius. The content-box + exclude mask carves the band; the
120
+ corners round without touching the column's own square seams. */
121
+ .column.selected::after {
122
+ content: '';
123
+ position: absolute;
124
+ inset: -3px;
125
+ z-index: 5;
126
+ pointer-events: none; /* the gutter and header own their pointers */
127
+ border-radius: 0.75rem;
128
+ padding: 5px;
129
+ /* Theme vars only (D7): the consumer's theme re-points them. */
130
+ background: linear-gradient(
131
+ 135deg,
132
+ var(--color-accent-blue) 0%,
133
+ var(--color-accent-purple) 45%,
134
+ var(--color-accent-rose) 100%
135
+ );
136
+ -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
137
+ -webkit-mask-composite: xor;
138
+ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
139
+ mask-composite: exclude;
140
+ }
141
+
142
+ /* Narrow columns (<530px): header text hides, icons carry identity.
143
+ :global because the text spans live in child components — Svelte
144
+ scoping stops at this component's own elements, and the container
145
+ query must reach the descendants. */
146
+ @container (max-width: 529px) {
147
+ .column :global(.narrow-hide) {
148
+ display: none;
149
+ }
150
+ }
151
+
152
+ .body {
153
+ flex: 1;
154
+ min-height: 0;
155
+ display: flex;
156
+ flex-direction: column;
157
+ overflow: hidden;
158
+ }
159
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Column width in px — triple-locked inline (width/min/max). */
4
+ width: number;
5
+ /** The pane's id — data-panel-id (the zoom's selection-follow hook). */
6
+ paneId: string;
7
+ /** Opaque session-ish tag — data-session-id (null when the pane
8
+ * carries no session; the value is never interpreted here). */
9
+ sessionTag?: string | null;
10
+ /** Selection cue — the ring on the selected column. */
11
+ selected?: boolean;
12
+ /** Activate (select) this pane — pointerdown/focusin inside. */
13
+ onactivate?: () => void;
14
+ /** Loupe trigger — Alt+Click release outside a control. */
15
+ onloupe?: () => void;
16
+ /** Header strip slot (the consumer's floor chrome). */
17
+ header: Snippet;
18
+ /** Toolbar slot under the header (badges, controls). */
19
+ toolbar?: Snippet;
20
+ /** Content slot — the pane body. */
21
+ children: Snippet;
22
+ /** Footer slot (after the content). */
23
+ footer?: Snippet;
24
+ };
25
+ declare const PaneBase: import("svelte").Component<$$ComponentProps, {}, "">;
26
+ type PaneBase = ReturnType<typeof PaneBase>;
27
+ export default PaneBase;
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ /**
4
+ * PanelHeaderSkeleton — a pane column's header-strip LAYOUT skeleton
5
+ * (split from DSI's PanelHeader, ADR-0062 W4, D8): the strip carries
6
+ * the geometry — flex row, label side + actions side, the bottom
7
+ * border, the selected wash — and the consumer renders its own
8
+ * controls INTO the slots (labels, copy buttons, chevrons, close).
9
+ * No consumer vocabulary: no stores, no icons, no translation.
10
+ *
11
+ * The element ref is BINDABLE: a consumer whose verbs need the strip's
12
+ * geometry (e.g. a canvas-capture button aiming at the strip's
13
+ * PARENT — the column) binds it back.
14
+ */
15
+ let {
16
+ title,
17
+ selected = false,
18
+ labelTestId,
19
+ headerEl = $bindable(undefined),
20
+ leading,
21
+ actions
22
+ }: {
23
+ /** Tooltip for the whole strip (the consumer's label text). */
24
+ title?: string;
25
+ /** Selection cue — the selected column's header carries the wash. */
26
+ selected?: boolean;
27
+ /** The label span's testid — the consumer's DOM hook (its own
28
+ * variants override; the default hook is the consumer's choice). */
29
+ labelTestId?: string;
30
+ /** The strip element — bind back for geometry-aimed verbs. */
31
+ headerEl?: HTMLDivElement | undefined;
32
+ /** Leading slot: the identity (label, copy control). */
33
+ leading: Snippet;
34
+ /** Actions slot: the verbs (chevrons, capture, close). */
35
+ actions: Snippet;
36
+ } = $props();
37
+ </script>
38
+
39
+ <div class="panel-header" class:selected data-testid="panel-header" {title} bind:this={headerEl}>
40
+ <span class="label" data-testid={labelTestId ?? 'panel-header-label'}>
41
+ {@render leading()}
42
+ </span>
43
+ <span class="actions">
44
+ {@render actions()}
45
+ </span>
46
+ </div>
47
+
48
+ <style>
49
+ .panel-header {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ gap: 0.5rem;
54
+ padding: 0.25rem 0.5rem;
55
+ min-height: 1.75rem;
56
+ font-size: 0.6875rem;
57
+ color: var(--color-text-secondary);
58
+ border-bottom: 1px solid var(--color-surface-border);
59
+ background: var(--color-surface-primary, var(--color-surface-elevated));
60
+ flex-shrink: 0;
61
+ }
62
+
63
+ /* Focus tint: the selected column's floor strip carries the same tint
64
+ as the conversation header/footer below it — the column you would
65
+ type into reads as one tinted frame. */
66
+ .panel-header.selected {
67
+ background: var(--color-header-wash);
68
+ }
69
+
70
+ .label {
71
+ min-width: 0;
72
+ display: inline-flex;
73
+ align-items: center;
74
+ gap: 0.25rem;
75
+ overflow: hidden;
76
+ white-space: nowrap;
77
+ }
78
+
79
+ .actions {
80
+ flex-shrink: 0;
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 0.125rem;
84
+ }
85
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Tooltip for the whole strip (the consumer's label text). */
4
+ title?: string;
5
+ /** Selection cue — the selected column's header carries the wash. */
6
+ selected?: boolean;
7
+ /** The label span's testid — the consumer's DOM hook (its own
8
+ * variants override; the default hook is the consumer's choice). */
9
+ labelTestId?: string;
10
+ /** The strip element — bind back for geometry-aimed verbs. */
11
+ headerEl?: HTMLDivElement | undefined;
12
+ /** Leading slot: the identity (label, copy control). */
13
+ leading: Snippet;
14
+ /** Actions slot: the verbs (chevrons, capture, close). */
15
+ actions: Snippet;
16
+ };
17
+ declare const PanelHeaderSkeleton: import("svelte").Component<$$ComponentProps, {}, "headerEl">;
18
+ type PanelHeaderSkeleton = ReturnType<typeof PanelHeaderSkeleton>;
19
+ export default PanelHeaderSkeleton;