@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.
- package/README.md +77 -2
- package/dist/components/ChordPaletteOverlay.svelte +99 -0
- package/dist/components/ChordPaletteOverlay.svelte.d.ts +25 -0
- package/dist/components/PaneBase.svelte +159 -0
- package/dist/components/PaneBase.svelte.d.ts +27 -0
- package/dist/components/PanelHeaderSkeleton.svelte +85 -0
- package/dist/components/PanelHeaderSkeleton.svelte.d.ts +19 -0
- package/dist/components/PanelsZoom.svelte +213 -0
- package/dist/components/PanelsZoom.svelte.d.ts +26 -0
- package/dist/components/RailColumn.svelte +91 -0
- package/dist/components/RailColumn.svelte.d.ts +15 -0
- package/dist/components/ResizeGutter.svelte +68 -0
- package/dist/components/ResizeGutter.svelte.d.ts +13 -0
- package/dist/components/SidebarContainer.svelte +174 -0
- package/dist/components/SidebarContainer.svelte.d.ts +42 -0
- package/dist/components/SliderWidth.svelte +80 -0
- package/dist/components/SliderWidth.svelte.d.ts +27 -0
- package/dist/components/SliderZoom.svelte +77 -0
- package/dist/components/SliderZoom.svelte.d.ts +25 -0
- package/dist/i18n/LanguageMenu.svelte +165 -0
- package/dist/i18n/LanguageMenu.svelte.d.ts +25 -0
- package/dist/i18n/locale-prefs.svelte.d.ts +24 -0
- package/dist/i18n/locale-prefs.svelte.js +85 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +35 -0
- package/dist/interaction/.gitkeep +0 -0
- package/dist/interaction/chord-machine.d.ts +108 -0
- package/dist/interaction/chord-machine.js +121 -0
- package/dist/interaction/gutter-resize.d.ts +37 -0
- package/dist/interaction/gutter-resize.js +41 -0
- package/dist/layout/.gitkeep +0 -0
- package/dist/layout/floor-math.d.ts +48 -0
- package/dist/layout/floor-math.js +60 -0
- package/dist/layout/placement.d.ts +18 -0
- package/dist/layout/placement.js +27 -0
- package/dist/registry/.gitkeep +0 -0
- package/dist/registry/action-registry.d.ts +30 -0
- package/dist/registry/action-registry.js +44 -0
- package/dist/storage/create-prefs-store.d.ts +25 -0
- package/dist/storage/create-prefs-store.js +67 -0
- package/dist/storage/namespace.d.ts +27 -0
- package/dist/storage/namespace.js +32 -0
- package/dist/theme/.gitkeep +0 -0
- package/dist/theme/tokens.css +35 -0
- package/dist/theming/ThemeToggle.svelte +170 -0
- package/dist/theming/ThemeToggle.svelte.d.ts +27 -0
- package/dist/theming/applier.d.ts +15 -0
- package/dist/theming/applier.js +29 -0
- package/dist/theming/theme-prefs.svelte.d.ts +21 -0
- package/dist/theming/theme-prefs.svelte.js +91 -0
- package/dist/types/.gitkeep +0 -0
- package/package.json +34 -5
- package/src/components/ChordPaletteOverlay.svelte +99 -0
- package/src/components/PaneBase.svelte +159 -0
- package/src/components/PanelHeaderSkeleton.svelte +85 -0
- package/src/components/PanelsZoom.svelte +213 -0
- package/src/components/RailColumn.svelte +91 -0
- package/src/components/ResizeGutter.svelte +68 -0
- package/src/components/SidebarContainer.svelte +174 -0
- package/src/components/SliderWidth.svelte +80 -0
- package/src/components/SliderZoom.svelte +77 -0
- package/src/i18n/LanguageMenu.svelte +165 -0
- package/src/i18n/locale-prefs.svelte.ts +112 -0
- package/src/index.ts +67 -0
- package/src/interaction/.gitkeep +0 -0
- package/src/interaction/chord-machine.ts +208 -0
- package/src/interaction/gutter-resize.ts +76 -0
- package/src/layout/.gitkeep +0 -0
- package/src/layout/floor-math.ts +94 -0
- package/src/layout/placement.ts +30 -0
- package/src/registry/.gitkeep +0 -0
- package/src/registry/action-registry.ts +62 -0
- package/src/storage/create-prefs-store.ts +87 -0
- package/src/storage/namespace.ts +34 -0
- package/src/theme/.gitkeep +0 -0
- package/src/theme/tokens.css +35 -0
- package/src/theming/ThemeToggle.svelte +170 -0
- package/src/theming/applier.ts +30 -0
- package/src/theming/theme-prefs.svelte.ts +118 -0
- 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;
|