@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
@@ -0,0 +1,174 @@
1
+ <script module lang="ts">
2
+ /** The collapsed-stub geometry (OCI ControlRail parity — the width the
3
+ * DSI shell carries as a constant; kept here so base and consumers
4
+ * agree on the number). */
5
+ export const SIDEBAR_COLLAPSED_WIDTH = 34;
6
+ </script>
7
+
8
+ <script lang="ts">
9
+ /**
10
+ * SidebarContainer — the generic sidebar shell (ADR-0063 D5). Contract
11
+ * derived from DSI's AppSidebar (read-first mandate, Tasks 5.1): three
12
+ * branches, one component —
13
+ *
14
+ * collapsed → 34px stub; ALL content (skin, words, controls)
15
+ * arrives via the `collapsed` snippet — the base
16
+ * owns only the geometry and the testid.
17
+ * embedded → the BARE rail (no width wrapper, no gutter):
18
+ * the hosting column owns both (OCI ControlRail).
19
+ * standalone → width-carrying wrapper + badge slot + the rail
20
+ * + the drag gutter as the rail's flex SIBLING
21
+ * (the rail's overflow:hidden must never clip the
22
+ * gutter's hit area — the 2026-09-11 bug).
23
+ *
24
+ * State is OWNED OUTSIDE (props + callbacks): collapse/width/drag live
25
+ * with the page (OCI ownership split); the container renders and
26
+ * reports. Words ride snippets/props; colors ride tokens — sections
27
+ * render via the default snippet so the consumer composes its own.
28
+ */
29
+ import type { Component, Snippet } from 'svelte';
30
+
31
+ let {
32
+ collapsed = false,
33
+ width = 400,
34
+ embedded = false,
35
+ /** The expanded rail's header row (brand, actions, collapse trigger). */
36
+ header,
37
+ /** The rail body — the consumer's sections (default snippet). */
38
+ children,
39
+ /** The collapsed-stub SKIN — paint only (gradient, texture). The base
40
+ * renders it as a pointer-transparent absolute underlay, so skin and
41
+ * content can never fight over paint order (the SSC#1 lesson). */
42
+ collapsedSkin,
43
+ /** The collapsed-stub CONTENT — interactive elements only (buttons,
44
+ * mounts). The base wraps it in a positioned layer above the skin. */
45
+ collapsedContent,
46
+ /** Width-badge slot (standalone wrapper only; consumer renders it). */
47
+ badge,
48
+ /** Gutter reports mousedown; the page owns drag + clamp. */
49
+ onResizeStart,
50
+ /** Accessible gutter name (consumer words). */
51
+ gutterLabel = 'Resize',
52
+ testId = 'sidebar',
53
+ testIdCollapsed = 'sidebar-collapsed'
54
+ }: {
55
+ collapsed?: boolean;
56
+ width?: number;
57
+ embedded?: boolean;
58
+ header?: Snippet;
59
+ children?: Snippet;
60
+ collapsedContent?: Snippet;
61
+ collapsedSkin?: Snippet;
62
+ badge?: Snippet;
63
+ onResizeStart?: (e: MouseEvent) => void;
64
+ gutterLabel?: string;
65
+ testId?: string;
66
+ testIdCollapsed?: string;
67
+ } = $props();
68
+ </script>
69
+
70
+ {#if collapsed}
71
+ <div class="cb-sidebar-stub" data-testid={testIdCollapsed}>
72
+ {#if collapsedSkin}
73
+ <div class="cb-stub-skin">{@render collapsedSkin()}</div>
74
+ {/if}
75
+ {#if collapsedContent}
76
+ <div class="cb-stub-content">{@render collapsedContent()}</div>
77
+ {/if}
78
+ </div>
79
+ {:else if embedded}
80
+ <aside class="cb-sidebar cb-sidebar-embedded" data-testid={testId}>
81
+ {#if header}{@render header()}{/if}
82
+ {@render children?.()}
83
+ </aside>
84
+ {:else}
85
+ <div class="cb-sidebar-wrap" style="width: {width}px" data-testid={testId + '-wrap'}>
86
+ {#if badge}{@render badge()}{/if}
87
+ <aside class="cb-sidebar" data-testid={testId}>
88
+ {#if header}{@render header()}{/if}
89
+ {@render children?.()}
90
+ </aside>
91
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions —
92
+ mouse-drag resize handle (ResizeGutter pattern); keyboard-driven
93
+ resize is a known gap, not a silent omission -->
94
+ <div
95
+ class="cb-sidebar-gutter"
96
+ data-testid={testId + '-gutter'}
97
+ role="separator"
98
+ aria-orientation="vertical"
99
+ aria-label={gutterLabel}
100
+ onmousedown={(e) => onResizeStart?.(e)}
101
+ ></div>
102
+ </div>
103
+ {/if}
104
+
105
+ <style>
106
+ .cb-sidebar-stub {
107
+ position: relative;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ gap: 0.25rem;
112
+ flex-shrink: 0;
113
+ width: 34px;
114
+ min-width: 34px;
115
+ height: 100%;
116
+ padding: 0.5rem 0;
117
+ border: none;
118
+ border-right: 1px solid var(--color-surface-border);
119
+ color: var(--color-text-secondary);
120
+ background: var(--color-surface-secondary);
121
+ isolation: isolate;
122
+ }
123
+ /* SSC#1 structural contract (2026-11-14): skin and content are SEPARATE
124
+ * base-owned layers, so paint order is the DOM, not the consumer's
125
+ * positioning choices. The skin layer is an absolute, pointer-transparent
126
+ * underlay; the content layer is positioned and paints above by DOM
127
+ * order. A consumer cannot re-earn the buried-buttons bug (DSI's
128
+ * gradient once covered its static Restart/About buttons). */
129
+ .cb-stub-skin {
130
+ position: absolute;
131
+ inset: 0;
132
+ pointer-events: none;
133
+ }
134
+ .cb-stub-content {
135
+ position: relative;
136
+ display: flex;
137
+ flex-direction: column;
138
+ align-items: center;
139
+ gap: 0.25rem;
140
+ flex: 1;
141
+ min-height: 0;
142
+ width: 100%;
143
+ }
144
+ .cb-sidebar-wrap {
145
+ position: relative;
146
+ display: flex;
147
+ flex-shrink: 0;
148
+ height: 100%;
149
+ min-height: 0;
150
+ }
151
+ .cb-sidebar {
152
+ display: flex;
153
+ flex-direction: column;
154
+ flex: 1;
155
+ min-width: 0;
156
+ height: 100%;
157
+ min-height: 0;
158
+ overflow: hidden;
159
+ background: var(--color-surface-elevated);
160
+ }
161
+ .cb-sidebar-embedded {
162
+ width: 100%;
163
+ }
164
+ .cb-sidebar-gutter {
165
+ width: 6px;
166
+ flex-shrink: 0;
167
+ cursor: col-resize;
168
+ transition: background-color 0.15s ease;
169
+ }
170
+ .cb-sidebar-gutter:hover {
171
+ background: var(--color-accent-blue);
172
+ opacity: 0.25;
173
+ }
174
+ </style>
@@ -0,0 +1,80 @@
1
+ <script module lang="ts">
2
+ /**
3
+ * SliderWidth — the honest uniform-width control (ADR-0063 D4, moved
4
+ * from DSI's control bar). Default bounds are the panel-prefs defaults
5
+ * the floor clamps against (480..860, step 1); a consumer overrides
6
+ * them via props — the clamp site stays the CONSUMER's (the commit
7
+ * callback routes wherever the route owner decides), so the
8
+ * single-bound-site rule holds. Every thumb move commits (per-input
9
+ * cadence) — panels resize LIVE under the dragging thumb.
10
+ */
11
+ export const WIDTH_BOUNDS = { min: 480, max: 860, step: 1 } as const;
12
+ </script>
13
+
14
+ <script lang="ts">
15
+ let {
16
+ /** Bound width (px); updates flow back up via $bindable per input. */
17
+ value = $bindable(730),
18
+ /** Bounds — default WIDTH_BOUNDS, prop-overridable. */
19
+ min = WIDTH_BOUNDS.min,
20
+ max = WIDTH_BOUNDS.max,
21
+ step = WIDTH_BOUNDS.step,
22
+ /** LIVE commit per input event (the honest-slider contract). */
23
+ oncommit,
24
+ /** Caption content — consumer words (the base ships none). */
25
+ caption,
26
+ /** Accessible name for the input (consumer words). */
27
+ ariaLabel,
28
+ /** Testid stem: <stem>, <stem>-input, <stem>-value. */
29
+ testId = 'cb-slider-width'
30
+ }: {
31
+ value?: number;
32
+ min?: number;
33
+ max?: number;
34
+ step?: number;
35
+ oncommit?: (width: number) => void;
36
+ caption?: import('svelte').Snippet<[number]>;
37
+ ariaLabel: string;
38
+ testId?: string;
39
+ } = $props();
40
+ </script>
41
+
42
+ <label class="cb-slider-width" data-testid={testId}>
43
+ {#if caption}
44
+ <span class="cb-slider-caption">
45
+ {@render caption(value)}
46
+ </span>
47
+ {/if}
48
+ <input
49
+ type="range"
50
+ {min}
51
+ {max}
52
+ {step}
53
+ bind:value
54
+ oninput={() => oncommit?.(value)}
55
+ data-testid={testId + '-input'}
56
+ aria-label={ariaLabel}
57
+ />
58
+ </label>
59
+
60
+ <style>
61
+ .cb-slider-width {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 0.25rem;
65
+ font-size: 0.6875rem;
66
+ color: var(--color-text-secondary);
67
+ }
68
+ .cb-slider-caption {
69
+ display: flex;
70
+ justify-content: space-between;
71
+ }
72
+ .cb-slider-width input {
73
+ width: 100%;
74
+ accent-color: var(--color-accent-blue);
75
+ }
76
+ .cb-slider-width input:focus-visible {
77
+ outline: 2px solid var(--color-accent-blue);
78
+ outline-offset: 2px;
79
+ }
80
+ </style>
@@ -0,0 +1,77 @@
1
+ <script module lang="ts">
2
+ /**
3
+ * SliderZoom — the honest floor-zoom control (ADR-0063 D4, moved from
4
+ * DSI's control bar). Default bounds 0.25..1.25 (the floor's zoom
5
+ * scale); prop-overridable. The bound value carries EVERY thumb move
6
+ * live to the owner (PanelsZoom re-measures per move — spec-13's
7
+ * no-phantom-scrollbar contract rides on this cadence).
8
+ */
9
+ export const ZOOM_BOUNDS = { min: 0.25, max: 1.25, step: 0.001 } as const;
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ let {
14
+ /** Floor zoom multiplier; updates flow back up via $bindable per input. */
15
+ value = $bindable(1),
16
+ min = ZOOM_BOUNDS.min,
17
+ max = ZOOM_BOUNDS.max,
18
+ step = ZOOM_BOUNDS.step,
19
+ /** Optional LIVE commit per input event (bind already carries value). */
20
+ oncommit,
21
+ /** Caption content — consumer words (the base ships none). */
22
+ caption,
23
+ /** Accessible name for the input (consumer words). */
24
+ ariaLabel,
25
+ /** Testid stem: <stem>, <stem>-input, <stem>-value. */
26
+ testId = 'cb-slider-zoom'
27
+ }: {
28
+ value?: number;
29
+ min?: number;
30
+ max?: number;
31
+ step?: number;
32
+ oncommit?: (zoom: number) => void;
33
+ caption?: import('svelte').Snippet<[number]>;
34
+ ariaLabel: string;
35
+ testId?: string;
36
+ } = $props();
37
+ </script>
38
+
39
+ <label class="cb-slider-zoom" data-testid={testId}>
40
+ {#if caption}
41
+ <span class="cb-slider-caption">
42
+ {@render caption(value)}
43
+ </span>
44
+ {/if}
45
+ <input
46
+ type="range"
47
+ {min}
48
+ {max}
49
+ {step}
50
+ bind:value
51
+ oninput={() => oncommit?.(value)}
52
+ data-testid={testId + '-input'}
53
+ aria-label={ariaLabel}
54
+ />
55
+ </label>
56
+
57
+ <style>
58
+ .cb-slider-zoom {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 0.25rem;
62
+ font-size: 0.6875rem;
63
+ color: var(--color-text-secondary);
64
+ }
65
+ .cb-slider-caption {
66
+ display: flex;
67
+ justify-content: space-between;
68
+ }
69
+ .cb-slider-zoom input {
70
+ width: 100%;
71
+ accent-color: var(--color-accent-blue);
72
+ }
73
+ .cb-slider-zoom input:focus-visible {
74
+ outline: 2px solid var(--color-accent-blue);
75
+ outline-offset: 2px;
76
+ }
77
+ </style>
@@ -0,0 +1,165 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LanguageMenu — the generic mount over a LocalePrefs store (ADR-0063 D3).
4
+ * The base carries NO words and NO skin: the language list and its labels
5
+ * are consumer props, the trigger icon is a consumer component, colors
6
+ * cross only through tokens. Native names are NOT translated copy —
7
+ * every locale shows every name in its own language (that is the point
8
+ * of a language menu), which is why they arrive as data, not snippets.
9
+ */
10
+ import type { Component } from 'svelte';
11
+
12
+ let {
13
+ /** The consumer's createLocalePrefs instance. */
14
+ prefs,
15
+ /** Locale ids in display order (validated by the store). */
16
+ locales,
17
+ /** Native names per locale id. */
18
+ labels,
19
+ /** aria-label / title for the trigger (consumer's words). */
20
+ triggerLabel,
21
+ /** The trigger icon (consumer's icon set — e.g. a languages glyph). */
22
+ icon,
23
+ /** Open the list upward (rail-bottom mounts). */
24
+ dropUp = false
25
+ }: {
26
+ prefs: {
27
+ readonly locale: string;
28
+ set: (next: string) => void;
29
+ };
30
+ locales: readonly string[];
31
+ labels: Record<string, string>;
32
+ triggerLabel: string;
33
+ icon: Component<{ size?: number }>;
34
+ dropUp?: boolean;
35
+ } = $props();
36
+
37
+ let open = $state(false);
38
+ let rootEl = $state<HTMLElement | null>(null);
39
+
40
+ // outside click closes — one window listener while open (removed on close)
41
+ $effect(() => {
42
+ if (!open) return;
43
+ const onDocClick = (ev: MouseEvent) => {
44
+ if (rootEl && !rootEl.contains(ev.target as Node)) open = false;
45
+ };
46
+ document.addEventListener('click', onDocClick);
47
+ return () => document.removeEventListener('click', onDocClick);
48
+ });
49
+
50
+ function choose(loc: string): void {
51
+ open = false;
52
+ prefs.set(loc);
53
+ }
54
+
55
+ const TriggerIcon = $derived(icon);
56
+ </script>
57
+
58
+ <div
59
+ class="cb-language-menu"
60
+ data-testid="language-menu"
61
+ bind:this={rootEl}
62
+ >
63
+ <button
64
+ type="button"
65
+ class="cb-language-trigger"
66
+ title={triggerLabel}
67
+ aria-label={triggerLabel}
68
+ aria-expanded={open}
69
+ data-testid="language-menu-trigger"
70
+ onclick={() => (open = !open)}
71
+ >
72
+ <TriggerIcon size={16} />
73
+ <span class="cb-language-code">{prefs.locale.toUpperCase()}</span>
74
+ </button>
75
+ {#if open}
76
+ <div
77
+ class="cb-language-list"
78
+ class:cb-drop-up={dropUp}
79
+ data-testid="language-menu-list"
80
+ role="menu"
81
+ >
82
+ {#each locales as loc (loc)}
83
+ <button
84
+ type="button"
85
+ class="cb-language-option"
86
+ class:cb-option-active={prefs.locale === loc}
87
+ data-testid={'locale-option-' + loc}
88
+ data-active={prefs.locale === loc ? 'true' : 'false'}
89
+ role="menuitem"
90
+ onclick={() => choose(loc)}
91
+ >
92
+ {labels[loc] ?? loc}
93
+ {#if prefs.locale === loc}<span aria-hidden="true">✓</span>{/if}
94
+ </button>
95
+ {/each}
96
+ </div>
97
+ {/if}
98
+ </div>
99
+
100
+ <style>
101
+ /* Structure only — colors cross through tokens (ground rule). */
102
+ .cb-language-menu {
103
+ position: relative;
104
+ display: inline-block;
105
+ }
106
+ .cb-language-trigger {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 0.25rem;
110
+ border: none;
111
+ background: transparent;
112
+ color: var(--color-text-secondary);
113
+ border-radius: 0.25rem;
114
+ padding: 0.25rem;
115
+ font-size: 0.75rem;
116
+ font-weight: 500;
117
+ cursor: pointer;
118
+ }
119
+ .cb-language-trigger:hover {
120
+ color: var(--color-text-primary);
121
+ }
122
+ .cb-language-code {
123
+ text-transform: uppercase;
124
+ }
125
+ .cb-language-list {
126
+ position: absolute;
127
+ right: 0;
128
+ z-index: 50;
129
+ width: 9rem;
130
+ overflow: hidden;
131
+ margin-top: 0.25rem;
132
+ padding-top: 0.25rem;
133
+ padding-bottom: 0.25rem;
134
+ border-radius: 0.375rem;
135
+ border: 1px solid var(--color-surface-border);
136
+ background: var(--color-surface-elevated);
137
+ color: var(--color-text-primary);
138
+ box-shadow: 0 4px 6px -1px var(--color-surface-border);
139
+ }
140
+ .cb-language-list.cb-drop-up {
141
+ bottom: 100%;
142
+ margin-top: 0;
143
+ margin-bottom: 0.25rem;
144
+ }
145
+ .cb-language-option {
146
+ display: flex;
147
+ width: 100%;
148
+ align-items: center;
149
+ justify-content: space-between;
150
+ padding: 0.375rem 0.75rem;
151
+ text-align: left;
152
+ font-size: 0.75rem;
153
+ border: none;
154
+ background: transparent;
155
+ color: inherit;
156
+ cursor: pointer;
157
+ }
158
+ .cb-language-option:hover {
159
+ background: var(--color-surface-secondary);
160
+ }
161
+ .cb-language-option.cb-option-active {
162
+ color: var(--color-accent-purple);
163
+ font-weight: 600;
164
+ }
165
+ </style>
@@ -0,0 +1,112 @@
1
+ // The base locale survival store (ADR-0063 D3): a client-side choice with
2
+ // DUAL-WRITE persistence — localStorage[<baseKey>] (profile-suffixed via
3
+ // storage/namespace) AND a mirror cookie the consumer names (DSI passes
4
+ // 'dsi.locale' so its SSR chain stays the reader; the base only mirrors,
5
+ // never races the SSR chain). Reads resolve cookie first, then storage,
6
+ // then the consumer default. Junk is junk: anything outside the
7
+ // consumer's locale list falls back to the default, never throws.
8
+ //
9
+ // SSR (no document / no localStorage): reads return the default, writes
10
+ // are silent no-ops — the consumer's SSR chain owns truth.
11
+
12
+ import { storageKey, sanitizeWorkspaceProfile } from '../storage/namespace';
13
+
14
+ export interface LocalePrefsOptions {
15
+ /** The un-suffixed localStorage key (e.g. 'dsi.locale', 'pg.locale'). */
16
+ baseKey: string;
17
+ /** The mirror cookie name. The consumer's SSR chain reads THIS name. */
18
+ cookieName: string;
19
+ /** The valid locale ids — anything else is junk. */
20
+ locales: readonly string[];
21
+ /** Fallback when nothing (or junk) is stored. */
22
+ defaultLocale: string;
23
+ /** Cookie lifetime seconds (default ~400 days, matching common SSR locale cookies). */
24
+ cookieMaxAge?: number;
25
+ }
26
+
27
+ export interface LocalePrefs {
28
+ readonly locale: string;
29
+ /** The exact storage key this store uses for a profile. */
30
+ key(profile?: string | null): string;
31
+ /** Boot-time adoption: resolve cookie → storage → default into state. */
32
+ adopt(): void;
33
+ /** Validate + dual-write (storage + cookie) + update state. */
34
+ set(next: string): void;
35
+ /** Raw resolution without touching state (SSR-safe). */
36
+ read(): string;
37
+ }
38
+
39
+ function readCookie(name: string): string | null {
40
+ if (typeof document === 'undefined') return null;
41
+ try {
42
+ const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name.replace(/[.$?*|{}()\[\]\\/+^]/g, '\\$&') + '=([^;]*)'));
43
+ return m ? decodeURIComponent(m[1]) : null;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export function createLocalePrefs(options: LocalePrefsOptions): LocalePrefs {
50
+ const { baseKey, cookieName, locales, defaultLocale } = options;
51
+ const maxAge = options.cookieMaxAge ?? 34560000;
52
+
53
+ let locale = $state<string>(defaultLocale);
54
+
55
+ const currentProfile = (): string | null =>
56
+ typeof location === 'undefined'
57
+ ? null
58
+ : sanitizeWorkspaceProfile(new URLSearchParams(location.search).get('profile') ?? '');
59
+
60
+ const keyFor = (profile?: string | null): string => storageKey(baseKey, profile ?? currentProfile());
61
+
62
+ const resolve = (): string => {
63
+ const cookie = readCookie(cookieName);
64
+ if (cookie !== null && locales.includes(cookie)) return cookie;
65
+ if (typeof localStorage !== 'undefined') {
66
+ try {
67
+ const raw = localStorage.getItem(keyFor());
68
+ if (raw !== null && locales.includes(raw)) return raw;
69
+ } catch {
70
+ // blocked storage — the cookie already had its say
71
+ }
72
+ }
73
+ return defaultLocale;
74
+ };
75
+
76
+ const persist = (next: string): void => {
77
+ if (typeof localStorage !== 'undefined') {
78
+ try {
79
+ localStorage.setItem(keyFor(), next);
80
+ } catch {
81
+ // store full or blocked — the cookie still holds the choice
82
+ }
83
+ }
84
+ if (typeof document !== 'undefined') {
85
+ try {
86
+ document.cookie = cookieName + '=' + encodeURIComponent(next) + '; path=/; max-age=' + maxAge + '; samesite=lax';
87
+ } catch {
88
+ // cookie write blocked — the storage mirror holds
89
+ }
90
+ }
91
+ };
92
+
93
+ return {
94
+ get locale(): string {
95
+ return locale;
96
+ },
97
+ key(profile?: string | null): string {
98
+ return keyFor(profile);
99
+ },
100
+ read(): string {
101
+ return resolve();
102
+ },
103
+ adopt() {
104
+ locale = resolve();
105
+ },
106
+ set(next: string) {
107
+ if (!locales.includes(next)) return; // junk → silent no-op
108
+ locale = next;
109
+ persist(next);
110
+ }
111
+ };
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @deepseek-insight/container-base — the ONLY public entry.
3
+ * Internals are reached exclusively through here (no deep imports).
4
+ */
5
+
6
+ /** Probe export for workspace resolution (Wave 1); real surface lands W2+. */
7
+ export const CONTAINER_BASE_VERSION = '0.1.0';
8
+
9
+ // ── components (W4, D6/D8) ──
10
+ export { default as PanelsZoom } from './components/PanelsZoom.svelte';
11
+ export { default as ResizeGutter } from './components/ResizeGutter.svelte';
12
+ export { default as RailColumn } from './components/RailColumn.svelte';
13
+ export { default as PaneBase } from './components/PaneBase.svelte';
14
+ export { default as PanelHeaderSkeleton } from './components/PanelHeaderSkeleton.svelte';
15
+ export { default as ChordPaletteOverlay } from './components/ChordPaletteOverlay.svelte';
16
+
17
+ // ── layout (W3, D8) ──
18
+ export type { PaneCore, WidthClamp, PaneMoveDir } from './layout/floor-math';
19
+ export { resizeOne, scaleProportionally, isUniformRow, movePanelWithin } from './layout/floor-math';
20
+ export { slotAfter, insertAtAnchor, focusedSlot } from './layout/placement';
21
+
22
+ // ── interaction (W5, D5) ──
23
+ export {
24
+ CHORD_WINDOW_MS,
25
+ IDLE_CHORD,
26
+ chordKey,
27
+ decideChordKeydown,
28
+ hasAnyPreset,
29
+ isEditableTarget
30
+ } from './interaction/chord-machine';
31
+ export type {
32
+ ChordPrefix,
33
+ ChordState,
34
+ ChordKey,
35
+ ChordAction,
36
+ ChordTables,
37
+ ChordOptionRow
38
+ } from './interaction/chord-machine';
39
+
40
+ export { createGutterResize } from './interaction/gutter-resize';
41
+ export type { GutterResizeMachine } from './interaction/gutter-resize';
42
+ // ── registry (W3, D8) ──
43
+ export { createActionTable } from './registry/action-registry';
44
+ export type { ActionTable } from './registry/action-registry';
45
+
46
+ // ── storage (W2, D4) ──
47
+ export { sanitizeWorkspaceProfile, storageKey } from './storage/namespace';
48
+ export { createPrefsStore } from './storage/create-prefs-store';
49
+ export type { PrefsStore, PrefsStoreOptions } from './storage/create-prefs-store';
50
+
51
+ // ── theming (R2 W2, ADR-0063 D2) ──
52
+ export { createThemePrefs } from './theming/theme-prefs.svelte';
53
+ export type { ThemeChoice, ResolvedTheme, ThemePrefs, ThemePrefsOptions } from './theming/theme-prefs.svelte';
54
+ export { themeApplierScript } from './theming/applier';
55
+ export { default as ThemeToggle } from './theming/ThemeToggle.svelte';
56
+
57
+ // ── i18n (R2 W3, ADR-0063 D3) ──
58
+ export { createLocalePrefs } from './i18n/locale-prefs.svelte';
59
+ export type { LocalePrefs, LocalePrefsOptions } from './i18n/locale-prefs.svelte';
60
+ export { default as LanguageMenu } from './i18n/LanguageMenu.svelte';
61
+
62
+ // ── sliders (R2 W4, ADR-0063 D4) ──
63
+ export { default as SliderWidth, WIDTH_BOUNDS } from './components/SliderWidth.svelte';
64
+ export { default as SliderZoom, ZOOM_BOUNDS } from './components/SliderZoom.svelte';
65
+
66
+ // ── sidebar (R2 W5, ADR-0063 D5) ──
67
+ export { default as SidebarContainer, SIDEBAR_COLLAPSED_WIDTH } from './components/SidebarContainer.svelte';
File without changes