@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,213 @@
1
+ <script lang="ts">
2
+ /**
3
+ * PanelsZoom — the floor's zoom frame (Panel Floor W3 task 3.2,
4
+ * ADR-0006 R9; OCI PanelsZoom port; container-base W4, ADR-0062).
5
+ *
6
+ * transform: scale(zoom) does NOT change layout width — scrollWidth
7
+ * ignores transforms — so a scaled-down row that visually fits still
8
+ * reports its unscaled width and the browser draws a phantom
9
+ * horizontal scrollbar. The fix is a two-layer frame:
10
+ *
11
+ * 1. THIS component's viewport div carries the scrollable overflow
12
+ * and the grey workspace paint.
13
+ * 2. The zoom-frame div inside it carries the LAYOUT width, set in
14
+ * JS to `row.scrollWidth × zoom` — the visual truth at every
15
+ * zoom. Layout width is what scrolling measures, so the
16
+ * scrollbar appears iff the scaled row truly overflows.
17
+ * 3. The row inside the frame carries scale(zoom), origin
18
+ * top-left, width max-content (so it measures its natural
19
+ * width), and a counter-scaled height (100/zoom % — a
20
+ * scaled-down row still fills the frame vertically).
21
+ *
22
+ * The measurement effect re-runs on a GEOMETRY FINGERPRINT — the
23
+ * string of all panel widths + count — because scrollWidth itself is
24
+ * not reactive; the fingerprint is.
25
+ *
26
+ * Presentational + mechanical only: zoom, panels, and the selected
27
+ * panel's id arrive as props (the id + the floor's id sequence drive
28
+ * the selection-follow — selection change or reshuffle — the one
29
+ * scroll this component writes itself); the row's children arrive as
30
+ * a snippet.
31
+ */
32
+ import ResizeGutter from './ResizeGutter.svelte';
33
+
34
+ let {
35
+ zoom = 1,
36
+ panels,
37
+ railWidth = null,
38
+ selectedPanelId = null,
39
+ children
40
+ }: {
41
+ /** Floor zoom multiplier (clamped by the consumer's prefs — one clamp site). */
42
+ zoom?: number;
43
+ /** Open panels — the geometry fingerprint source (widths + count)
44
+ * and the reorder fingerprint source (the id sequence). */
45
+ panels: Array<{ id: string; width: number }>;
46
+ /** Leading sticky-column width (px) — the embedded rail column;
47
+ * null/undefined = no rail column.
48
+ * RIDES THE FINGERPRINT (OCI `leading ? railWidth : ''`): mounting,
49
+ * unmounting, or resizing the rail changes row.scrollWidth, and
50
+ * scrollWidth itself is not reactive — the fingerprint is. */
51
+ railWidth?: number | null;
52
+ /** The focused panel's id — the selection-follow target: when it
53
+ * changes, a column the viewport cannot see is scrolled into view
54
+ * (the floor can be several viewports wide; the selection ring
55
+ * must never sit off-screen behind the scroll edge). */
56
+ selectedPanelId?: string | null;
57
+ /** Snippet: one rendered child per panel. */
58
+ children: import('svelte').Snippet;
59
+ } = $props();
60
+
61
+ // ResizeGutter is re-exported for hosts that compose the row the way
62
+ // the OCI floor does; the gutter itself stays a separate public face.
63
+ void ResizeGutter;
64
+
65
+ /** Scroll viewport (overflow-x lives here, grey workspace paint). */
66
+ let viewport = $state<HTMLDivElement | undefined>(undefined);
67
+ /** Zoom-frame div — JS-measured layout width lives here. */
68
+ let frame = $state<HTMLDivElement | undefined>(undefined);
69
+ /** Row div — transform-scaled, natural-width content. */
70
+ let row = $state<HTMLDivElement | undefined>(undefined);
71
+
72
+ /**
73
+ * Geometry fingerprint: every panel width + the count + the leading
74
+ * rail column's width (empty when absent — OCI `leading ? railWidth :
75
+ * ''`). Any width change, add, remove, or rail mount/unmount/resize
76
+ * changes the string and re-runs the measure effect below.
77
+ * (scrollWidth is not reactive; the fingerprint is.)
78
+ */
79
+ const geometryFingerprint = $derived(
80
+ panels.map((p) => p.width).join(',') + `#${panels.length}` + `|${railWidth ?? ''}`
81
+ );
82
+
83
+ /**
84
+ * Reorder fingerprint: the panel-ID SEQUENCE. A move reshuffles the
85
+ * columns — it changes this string while leaving the geometry
86
+ * fingerprint untouched, and the reshuffle can push the focused
87
+ * column past a viewport edge. Width-only edits never change it, so
88
+ * a gutter drag or zoom still never steals the scroll.
89
+ */
90
+ const orderFingerprint = $derived(panels.map((p) => p.id).join(','));
91
+
92
+ $effect(() => {
93
+ // The row's transform reads --zoom (a var, so the counter-scaled
94
+ // height calc can share it); keep it in lockstep with the prop.
95
+ if (row) row.style.setProperty('--zoom', String(zoom));
96
+ });
97
+
98
+ $effect(() => {
99
+ // Depend on the fingerprint (and zoom) — the re-measure triggers.
100
+ void geometryFingerprint;
101
+ void zoom;
102
+ if (!frame || !row) return;
103
+ // Measure in a rAF so the browser has laid the (possibly new)
104
+ // geometry out before scrollWidth is read. The frame carries BOTH
105
+ // layout dimensions (width AND height scaled) — a 1px height stub
106
+ // let overflow-y:hidden clip the row's real content under the
107
+ // viewport, breaking pointer events on panel children.
108
+ requestAnimationFrame(() => {
109
+ if (!frame || !row) return;
110
+ // Layout WIDTH only: scrollWidth is transform-blind, so the frame
111
+ // carries the scaled truth for horizontal scrolling. HEIGHT is NOT
112
+ // JS-set — the frame fills the viewport (height:100%) and the row
113
+ // counter-scales (100%/zoom) inside it; a JS height here would be
114
+ // circular (scrollHeight of a percentage-height child).
115
+ frame.style.width = `${Math.ceil(row.scrollWidth * zoom)}px`;
116
+ });
117
+ });
118
+
119
+ $effect(() => {
120
+ // Selection-follow: a NEWLY selected panel — or the focused panel
121
+ // after a RESHUFFLE — is scrolled into view: nearest, horizontal
122
+ // only (the viewport never scrolls vertically). Fires on the
123
+ // id-sequence change or the selection change ONLY: geometry edits
124
+ // never steal the scroll. Placed after the measure effect so, in
125
+ // the add+select flush, the frame's rAF runs before this one.
126
+ const target = selectedPanelId;
127
+ void orderFingerprint;
128
+ if (target === null) return;
129
+ requestAnimationFrame(() => {
130
+ if (!viewport) return;
131
+ const col = viewport.querySelector<HTMLElement>(
132
+ `[data-panel-id="${CSS.escape(target)}"]`
133
+ );
134
+ if (!col) return;
135
+ // getBoundingClientRect is transform-true (zoom included), and
136
+ // the frame carries the scaled layout width — visual coordinates
137
+ // ARE scrollLeft's coordinate space. The selection ring extends
138
+ // 4px × zoom outward, so it joins the reveal.
139
+ const vpRect = viewport.getBoundingClientRect();
140
+ const colRect = col.getBoundingClientRect();
141
+ const ring = 4 * zoom;
142
+ const left = colRect.left - vpRect.left + viewport.scrollLeft - ring;
143
+ const right = left + colRect.width + ring * 2;
144
+ let reveal: number | null = null;
145
+ if (left < viewport.scrollLeft) {
146
+ reveal = Math.max(0, left);
147
+ } else if (right > viewport.scrollLeft + viewport.clientWidth) {
148
+ reveal = right - viewport.clientWidth;
149
+ }
150
+ if (reveal === null) return; // fully visible (ring included) — hands off
151
+ // Inertial: the reveal GLIDES; prefers-reduced-motion collapses
152
+ // it to the instant jump.
153
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
154
+ viewport.scrollTo({ left: reveal, behavior: reduceMotion ? 'auto' : 'smooth' });
155
+ });
156
+ });
157
+ </script>
158
+
159
+ <div class="panels-viewport" bind:this={viewport} data-testid="panels-viewport">
160
+ <div class="zoom-frame" bind:this={frame} data-testid="panels-zoom-frame">
161
+ <div class="panels-row" bind:this={row} data-testid="panels-row">
162
+ {@render children()}
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <style>
168
+ .panels-viewport {
169
+ flex: 1;
170
+ min-width: 0;
171
+ overflow-x: auto;
172
+ overflow-y: hidden;
173
+ /* Grey workspace paint — the floor the panels sit on. */
174
+ background: var(--color-surface-secondary);
175
+ }
176
+
177
+ .zoom-frame {
178
+ /* Fills the viewport vertically — the row counter-scales inside. */
179
+ height: 100%;
180
+ }
181
+
182
+
183
+ .panels-row {
184
+ display: flex;
185
+ align-items: stretch;
186
+ /* SSC#2 G8 (2026-11-14/15): border-box so the 4px ring-breathing
187
+ * padding sits INSIDE the counter-scaled height — content-box let
188
+ * children resolve height:100% to the full calc'd value and overflow
189
+ * the viewport bottom by the scaled padding whenever the row fits
190
+ * horizontally (no scrollbar shrinking the viewport, i.e. any
191
+ * zoomed-out desk). Matches DSI's resolved geometry.
192
+ * The -8px in the calc reserves the selected ring's protrusion (3px
193
+ * out + 5px band, PaneBase ::after) OUTSIDE the scaled fill: with the
194
+ * row filling the viewport EXACTLY, the ring's bottom band landed
195
+ * flush on the viewport edge at every zoom and read as cut off (the
196
+ * operator's 2026-11-15 report). The reservation scales back up with
197
+ * the row, so the slack stays proportional, not a fixed gap. */
198
+ box-sizing: border-box;
199
+ /* Natural width so the frame can measure the true row width. */
200
+ width: max-content;
201
+ /* The zoom transform — visual only, layout-blind by CSS design. */
202
+ transform: scale(var(--zoom, 1));
203
+ transform-origin: top left;
204
+ height: calc((100% - 8px) / var(--zoom, 1));
205
+ /* Counter-scaled height: a scaled-down row still fills the frame
206
+ vertically, with the ring reservation above leaving the selected
207
+ ring's bottom band visible at every zoom. */
208
+ /* Breathing room for the selected column's OUTWARD ring (4px
209
+ extent beyond the border box). The padding lives INSIDE this
210
+ transform, so it scales with zoom in lockstep with the ring. */
211
+ padding: 4px;
212
+ }
213
+ </style>
@@ -0,0 +1,26 @@
1
+ type $$ComponentProps = {
2
+ /** Floor zoom multiplier (clamped by the consumer's prefs — one clamp site). */
3
+ zoom?: number;
4
+ /** Open panels — the geometry fingerprint source (widths + count)
5
+ * and the reorder fingerprint source (the id sequence). */
6
+ panels: Array<{
7
+ id: string;
8
+ width: number;
9
+ }>;
10
+ /** Leading sticky-column width (px) — the embedded rail column;
11
+ * null/undefined = no rail column.
12
+ * RIDES THE FINGERPRINT (OCI `leading ? railWidth : ''`): mounting,
13
+ * unmounting, or resizing the rail changes row.scrollWidth, and
14
+ * scrollWidth itself is not reactive — the fingerprint is. */
15
+ railWidth?: number | null;
16
+ /** The focused panel's id — the selection-follow target: when it
17
+ * changes, a column the viewport cannot see is scrolled into view
18
+ * (the floor can be several viewports wide; the selection ring
19
+ * must never sit off-screen behind the scroll edge). */
20
+ selectedPanelId?: string | null;
21
+ /** Snippet: one rendered child per panel. */
22
+ children: import('svelte').Snippet;
23
+ };
24
+ declare const PanelsZoom: import("svelte").Component<$$ComponentProps, {}, "">;
25
+ type PanelsZoom = ReturnType<typeof PanelsZoom>;
26
+ export default PanelsZoom;
@@ -0,0 +1,91 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ /**
4
+ * RailColumn — a fixed-width LEADING column inside the zoom row
5
+ * (generalized from DSI's StickyColumnContainer, OCI ControlRail port;
6
+ * ADR-0062 W4, D6: the i18n gutter label arrives as a PROP — the full
7
+ * sidebar container is an R2 exception, the rail ships in R1).
8
+ *
9
+ * "Rail" = the width is TRIPLE-LOCKED (width/min/max all the same
10
+ * px), unlike pane columns whose widths live in [MIN, MAX]: the rail
11
+ * never flexes when the row measures itself at max-content. The
12
+ * column's background paints the full column height — the rail inside
13
+ * fills it (align-items: stretch).
14
+ *
15
+ * The trailing resize gutter is included BY DEFAULT (OCI parity) but
16
+ * only when pane columns actually follow (showGutter) — a rail alone
17
+ * on the floor has nothing to resize against. The gutter is the
18
+ * column's flex SIBLING (the rail's overflow must never clip its hit
19
+ * area) and reports mousedown through onResizeStart — the host owns
20
+ * the drag math and the single clamp.
21
+ */
22
+ let {
23
+ width,
24
+ label,
25
+ showGutter = true,
26
+ onResizeStart,
27
+ children
28
+ }: {
29
+ /** Sticky (fixed) column width in px — triple-locked, never flexes. */
30
+ width: number;
31
+ /** Accessible label for the trailing gutter (consumer-localized). */
32
+ label: string;
33
+ /** Trailing resize gutter — only when pane columns follow (OCI). */
34
+ showGutter?: boolean;
35
+ /** Gutter mousedown — the host runs the drag and owns the clamp. */
36
+ onResizeStart?: (e: MouseEvent) => void;
37
+ children: Snippet;
38
+ } = $props();
39
+ </script>
40
+
41
+ <div
42
+ class="rail-column"
43
+ data-testid="sidebar-column"
44
+ style="width: {width}px; min-width: {width}px; max-width: {width}px;"
45
+ >
46
+ {@render children()}
47
+ </div>
48
+
49
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions — mouse-drag
50
+ resize handle; keyboard-driven resize is a known gap, not a silent
51
+ omission (filed per ADR-0062 §7) -->
52
+ {#if showGutter}
53
+ <div
54
+ class="rail-gutter"
55
+ data-testid="sidebar-gutter"
56
+ role="separator"
57
+ aria-orientation="vertical"
58
+ aria-label={label}
59
+ title={label}
60
+ onmousedown={(e) => onResizeStart?.(e)}
61
+ ></div>
62
+ {/if}
63
+
64
+ <style>
65
+ /* Column-geometry essentials (OCI StickyColumnContainer .rail-column):
66
+ the triple-locked width arrives INLINE (width/min/max); this rule
67
+ carries the flex-column shell + the surface paint. */
68
+ .rail-column {
69
+ flex-shrink: 0;
70
+ display: flex;
71
+ flex-direction: column;
72
+ background: var(--color-surface-elevated, var(--color-surface-elevated));
73
+ position: relative;
74
+ overflow: visible;
75
+ }
76
+
77
+ /* Resize gutter — 6px column right of the locked column (same seam
78
+ as the app sidebar's own gutter in the out-of-zoom placement: one
79
+ visual contract, two host sites). */
80
+ .rail-gutter {
81
+ width: 6px;
82
+ flex-shrink: 0;
83
+ cursor: col-resize;
84
+ transition: background-color 0.15s ease;
85
+ }
86
+
87
+ .rail-gutter:hover {
88
+ background: var(--color-accent-blue);
89
+ opacity: 0.25;
90
+ }
91
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Sticky (fixed) column width in px — triple-locked, never flexes. */
4
+ width: number;
5
+ /** Accessible label for the trailing gutter (consumer-localized). */
6
+ label: string;
7
+ /** Trailing resize gutter — only when pane columns follow (OCI). */
8
+ showGutter?: boolean;
9
+ /** Gutter mousedown — the host runs the drag and owns the clamp. */
10
+ onResizeStart?: (e: MouseEvent) => void;
11
+ children: Snippet;
12
+ };
13
+ declare const RailColumn: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type RailColumn = ReturnType<typeof RailColumn>;
15
+ export default RailColumn;
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ResizeGutter — the 6px draggable seam after every pane (ADR-0006 R8
4
+ * port, generalized for the container base, ADR-0062 W4 / D6: the i18n
5
+ * labels arrive as PROPS — the package never learns a consumer's
6
+ * translation system).
7
+ *
8
+ * The gutter does not resize anything itself — mousedown reports the
9
+ * event upward through the injected onGutterDown hook, and the HOST
10
+ * (the drag owner) snapshots widths and runs the resize math. No drag
11
+ * state lives here.
12
+ */
13
+ let {
14
+ index,
15
+ ariaLabel,
16
+ title,
17
+ onGutterDown
18
+ }: {
19
+ /** The pane index this seam trails — the host's drag payload. */
20
+ index: number;
21
+ /** Accessible name for the seam (consumer-localized). */
22
+ ariaLabel: string;
23
+ /** Hover tooltip (consumer-localized). */
24
+ title: string;
25
+ /** Mousedown — the HOST owns the drag and the clamp. */
26
+ onGutterDown?: (event: MouseEvent) => void;
27
+ } = $props();
28
+ </script>
29
+
30
+ <!-- A real button (a11y): the gutter is an interactive separator; drag
31
+ reports upward — the host owns the drag math. Button styling resets
32
+ to the 6px seam. Keyboard-driven resize is a known gap (filed, not
33
+ hidden — ADR-0062 §7). -->
34
+ <button
35
+ type="button"
36
+ class="gutter"
37
+ data-testid="panel-gutter-{index}"
38
+ aria-label={ariaLabel}
39
+ {title}
40
+ onmousedown={(e) => {
41
+ e.preventDefault();
42
+ onGutterDown?.(e);
43
+ }}
44
+ ></button>
45
+
46
+ <style>
47
+ .gutter {
48
+ display: block;
49
+ padding: 0;
50
+ border: none;
51
+ flex: 0 0 6px;
52
+ width: 6px;
53
+ align-self: stretch;
54
+ cursor: col-resize;
55
+ background: transparent;
56
+ transition: background-color 0.15s ease;
57
+ }
58
+
59
+ .gutter:hover,
60
+ .gutter:active {
61
+ background: var(--color-accent-blue);
62
+ }
63
+
64
+ .gutter:focus-visible {
65
+ outline: 2px solid var(--color-accent-blue);
66
+ outline-offset: -2px;
67
+ }
68
+ </style>
@@ -0,0 +1,13 @@
1
+ type $$ComponentProps = {
2
+ /** The pane index this seam trails — the host's drag payload. */
3
+ index: number;
4
+ /** Accessible name for the seam (consumer-localized). */
5
+ ariaLabel: string;
6
+ /** Hover tooltip (consumer-localized). */
7
+ title: string;
8
+ /** Mousedown — the HOST owns the drag and the clamp. */
9
+ onGutterDown?: (event: MouseEvent) => void;
10
+ };
11
+ declare const ResizeGutter: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type ResizeGutter = ReturnType<typeof ResizeGutter>;
13
+ export default ResizeGutter;
@@ -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,42 @@
1
+ /** The collapsed-stub geometry (OCI ControlRail parity — the width the
2
+ * DSI shell carries as a constant; kept here so base and consumers
3
+ * agree on the number). */
4
+ export declare const SIDEBAR_COLLAPSED_WIDTH = 34;
5
+ /**
6
+ * SidebarContainer — the generic sidebar shell (ADR-0063 D5). Contract
7
+ * derived from DSI's AppSidebar (read-first mandate, Tasks 5.1): three
8
+ * branches, one component —
9
+ *
10
+ * collapsed → 34px stub; ALL content (skin, words, controls)
11
+ * arrives via the `collapsed` snippet — the base
12
+ * owns only the geometry and the testid.
13
+ * embedded → the BARE rail (no width wrapper, no gutter):
14
+ * the hosting column owns both (OCI ControlRail).
15
+ * standalone → width-carrying wrapper + badge slot + the rail
16
+ * + the drag gutter as the rail's flex SIBLING
17
+ * (the rail's overflow:hidden must never clip the
18
+ * gutter's hit area — the 2026-09-11 bug).
19
+ *
20
+ * State is OWNED OUTSIDE (props + callbacks): collapse/width/drag live
21
+ * with the page (OCI ownership split); the container renders and
22
+ * reports. Words ride snippets/props; colors ride tokens — sections
23
+ * render via the default snippet so the consumer composes its own.
24
+ */
25
+ import type { Component, Snippet } from 'svelte';
26
+ type $$ComponentProps = {
27
+ collapsed?: boolean;
28
+ width?: number;
29
+ embedded?: boolean;
30
+ header?: Snippet;
31
+ children?: Snippet;
32
+ collapsedContent?: Snippet;
33
+ collapsedSkin?: Snippet;
34
+ badge?: Snippet;
35
+ onResizeStart?: (e: MouseEvent) => void;
36
+ gutterLabel?: string;
37
+ testId?: string;
38
+ testIdCollapsed?: string;
39
+ };
40
+ declare const SidebarContainer: Component<$$ComponentProps, {}, "">;
41
+ type SidebarContainer = ReturnType<typeof SidebarContainer>;
42
+ export default SidebarContainer;