@dorsk/tsumikit 0.22.0 → 0.24.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 +151 -8
- package/dist/components/atoms/Button.svelte +121 -12
- package/dist/components/atoms/Button.svelte.d.ts +4 -0
- package/dist/components/atoms/Scrim.svelte +81 -0
- package/dist/components/atoms/Scrim.svelte.d.ts +14 -0
- package/dist/components/layouts/Cluster.svelte +33 -0
- package/dist/components/layouts/Cluster.svelte.d.ts +1 -0
- package/dist/components/layouts/ResizablePanel.svelte +238 -120
- package/dist/components/layouts/ResizablePanel.svelte.d.ts +24 -8
- package/dist/components/layouts/resizable-panel-frame.d.ts +70 -0
- package/dist/components/layouts/resizable-panel-frame.js +161 -0
- package/dist/components/molecules/CopyButton.svelte +11 -2
- package/dist/components/molecules/CopyButton.svelte.d.ts +4 -0
- package/dist/components/molecules/FileButton.svelte +32 -3
- package/dist/components/molecules/FileButton.svelte.d.ts +4 -0
- package/dist/components/molecules/IconButton.svelte +34 -6
- package/dist/components/molecules/IconButton.svelte.d.ts +3 -0
- package/dist/components/molecules/Popover.svelte +32 -2
- package/dist/components/molecules/Popover.svelte.d.ts +5 -0
- package/dist/components/molecules/SelectButton.svelte +6 -1
- package/dist/components/molecules/SelectButton.svelte.d.ts +2 -0
- package/dist/components/molecules/ThemePicker.svelte +11 -13
- package/dist/components/organisms/DataTable.svelte +210 -10
- package/dist/components/organisms/DataTable.svelte.d.ts +27 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/stores/theme.svelte.d.ts +21 -5
- package/dist/stores/theme.svelte.js +48 -22
- package/dist/styles/app.css +7 -297
- package/dist/styles/reset.css +97 -0
- package/dist/styles/syntax.css +94 -0
- package/dist/styles/themes.css +729 -0
- package/dist/styles/tokens.css +207 -0
- package/dist/styles/utilities.css +111 -0
- package/dist/styles/variables.css +5 -915
- package/package.json +7 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
3
|
import { browser } from '../../env';
|
|
4
4
|
import Icon from '../atoms/Icon.svelte';
|
|
5
|
-
import
|
|
5
|
+
import Scrim from '../atoms/Scrim.svelte';
|
|
6
|
+
import { resizeHandle, resolveLength } from './resizable-panel-frame.js';
|
|
6
7
|
import { parseStoredCollapsed, parseStoredWidth } from './resizable-panel-persistence';
|
|
7
8
|
|
|
8
9
|
let {
|
|
@@ -18,23 +19,31 @@
|
|
|
18
19
|
persistCollapsed = true,
|
|
19
20
|
resizeStep = 16,
|
|
20
21
|
handlePlacement = 'bottom',
|
|
21
|
-
stickyHandle = true
|
|
22
|
+
stickyHandle = true,
|
|
23
|
+
mode = 'inline',
|
|
24
|
+
open = $bindable(false),
|
|
25
|
+
onclose,
|
|
26
|
+
scrim,
|
|
27
|
+
fullWidthBelow,
|
|
28
|
+
clampToViewport = true
|
|
22
29
|
}: {
|
|
23
30
|
/** Content shown while the panel is expanded. */
|
|
24
31
|
panel: Snippet;
|
|
25
|
-
/** Main content beside the panel. */
|
|
26
|
-
children
|
|
32
|
+
/** Main content beside the panel (optional in overlay mode). */
|
|
33
|
+
children?: Snippet;
|
|
27
34
|
/** Physical edge occupied by the panel. */
|
|
28
35
|
side?: 'left' | 'right';
|
|
29
|
-
/** Accessible name for the panel landmark. */
|
|
36
|
+
/** Accessible name for the panel landmark / dialog. */
|
|
30
37
|
label?: string;
|
|
31
38
|
/** Initial expanded width in pixels. */
|
|
32
39
|
width?: number;
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
/** Pixel number or CSS length (`'12rem'`, `'30vw'`). */
|
|
41
|
+
minWidth?: number | string;
|
|
42
|
+
/** Pixel number or CSS length (`'40rem'`, `'90vw'`). */
|
|
43
|
+
maxWidth?: number | string;
|
|
35
44
|
/** localStorage key used to restore the expanded width. */
|
|
36
45
|
widthKey?: string;
|
|
37
|
-
/** Bindable collapsed state. */
|
|
46
|
+
/** Bindable collapsed state (inline mode). */
|
|
38
47
|
collapsed?: boolean;
|
|
39
48
|
/** Persist collapsed state as `${widthKey}:collapsed`. */
|
|
40
49
|
persistCollapsed?: boolean;
|
|
@@ -45,23 +54,69 @@
|
|
|
45
54
|
/** Keep the collapse handle in view when the panel scrolls past the
|
|
46
55
|
* viewport, repositioning on scroll/resize via requestAnimationFrame. */
|
|
47
56
|
stickyHandle?: boolean;
|
|
57
|
+
/** `inline` shares the row with `children`; `overlay` fixes the panel to
|
|
58
|
+
* its viewport edge as a non-modal drawer above the page. */
|
|
59
|
+
mode?: 'inline' | 'overlay';
|
|
60
|
+
/** Bindable drawer visibility (overlay mode). */
|
|
61
|
+
open?: boolean;
|
|
62
|
+
/** Overlay mode: Escape, scrim click or the edge control closed the drawer. */
|
|
63
|
+
onclose?: () => void;
|
|
64
|
+
/** Overlay mode: dim the page behind the drawer; clicking it closes (default true). */
|
|
65
|
+
scrim?: boolean;
|
|
66
|
+
/** Overlay mode: viewport width (CSS length) under which the drawer spans
|
|
67
|
+
* the full viewport and hides its resize handle and scrim. */
|
|
68
|
+
fullWidthBelow?: string;
|
|
69
|
+
/** Overlay mode: cap the width at the viewport and re-clamp on window resize. */
|
|
70
|
+
clampToViewport?: boolean;
|
|
48
71
|
} = $props();
|
|
49
72
|
|
|
50
73
|
let root: HTMLDivElement;
|
|
74
|
+
let panelEl = $state<HTMLElement | null>(null);
|
|
51
75
|
let handleEl = $state<HTMLButtonElement | null>(null);
|
|
52
76
|
let panelWidth = $state<number>();
|
|
53
77
|
let resizing = $state(false);
|
|
54
78
|
let restored = false;
|
|
55
79
|
let stickyShift = $state(0);
|
|
80
|
+
let viewportWidth = $state<number>();
|
|
81
|
+
let lengthTick = $state(0);
|
|
82
|
+
let fullBleed = $state(false);
|
|
56
83
|
|
|
57
|
-
const
|
|
58
|
-
const
|
|
84
|
+
const overlay = $derived(mode === 'overlay');
|
|
85
|
+
const shown = $derived(overlay ? open : !collapsed);
|
|
86
|
+
const showScrim = $derived(overlay && open && (scrim ?? true));
|
|
87
|
+
|
|
88
|
+
function measureLength(css: string) {
|
|
89
|
+
if (!browser || !root) return undefined;
|
|
90
|
+
console.count('measure ' + css);
|
|
91
|
+
const probe = document.createElement('div');
|
|
92
|
+
probe.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;width:${css}`;
|
|
93
|
+
root.appendChild(probe);
|
|
94
|
+
const measured = probe.getBoundingClientRect().width;
|
|
95
|
+
probe.remove();
|
|
96
|
+
return measured;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const minPx = $derived.by(() => {
|
|
100
|
+
void lengthTick;
|
|
101
|
+
return resolveLength(minWidth, measureLength) ?? 180;
|
|
102
|
+
});
|
|
103
|
+
const maxPx = $derived.by(() => {
|
|
104
|
+
void lengthTick;
|
|
105
|
+
return resolveLength(maxWidth, measureLength) ?? 480;
|
|
106
|
+
});
|
|
107
|
+
const viewportCap = $derived(
|
|
108
|
+
overlay && clampToViewport && viewportWidth ? viewportWidth : Number.POSITIVE_INFINITY
|
|
109
|
+
);
|
|
110
|
+
const boundedMin = $derived(Math.max(1, Math.min(minPx, maxPx, viewportCap)));
|
|
111
|
+
const boundedMax = $derived(Math.max(boundedMin, Math.min(maxPx, viewportCap)));
|
|
59
112
|
const currentWidth = $derived(
|
|
60
113
|
Math.round(Math.max(boundedMin, Math.min(panelWidth ?? width, boundedMax)))
|
|
61
114
|
);
|
|
62
|
-
const toggleLabel = $derived(
|
|
115
|
+
const toggleLabel = $derived(
|
|
116
|
+
overlay ? `Close ${label}` : collapsed ? `Expand ${label}` : `Collapse ${label}`
|
|
117
|
+
);
|
|
63
118
|
const toggleIcon = $derived(
|
|
64
|
-
collapsed
|
|
119
|
+
!overlay && collapsed
|
|
65
120
|
? side === 'left'
|
|
66
121
|
? 'chevron-right'
|
|
67
122
|
: 'chevron-left'
|
|
@@ -73,6 +128,7 @@
|
|
|
73
128
|
$effect(() => {
|
|
74
129
|
if (!browser || restored) return;
|
|
75
130
|
restored = true;
|
|
131
|
+
lengthTick += 1;
|
|
76
132
|
if (!widthKey) return;
|
|
77
133
|
|
|
78
134
|
const savedWidth = parseStoredWidth(
|
|
@@ -90,28 +146,70 @@
|
|
|
90
146
|
}
|
|
91
147
|
});
|
|
92
148
|
|
|
93
|
-
|
|
94
|
-
if (browser
|
|
149
|
+
$effect(() => {
|
|
150
|
+
if (!browser) return;
|
|
151
|
+
const sync = () => {
|
|
152
|
+
console.count('sync');
|
|
153
|
+
viewportWidth = window.innerWidth;
|
|
154
|
+
lengthTick += 1;
|
|
155
|
+
};
|
|
156
|
+
sync();
|
|
157
|
+
addEventListener('resize', sync);
|
|
158
|
+
return () => removeEventListener('resize', sync);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
$effect(() => {
|
|
162
|
+
if (!browser || !overlay || !fullWidthBelow) {
|
|
163
|
+
fullBleed = false;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.count('bleed');
|
|
167
|
+
const query = matchMedia(`(max-width: ${fullWidthBelow})`);
|
|
168
|
+
const sync = () => {
|
|
169
|
+
fullBleed = query.matches;
|
|
170
|
+
};
|
|
171
|
+
sync();
|
|
172
|
+
query.addEventListener('change', sync);
|
|
173
|
+
return () => query.removeEventListener('change', sync);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
$effect(() => {
|
|
177
|
+
if (!browser || !overlay || !open) return;
|
|
178
|
+
console.count('escape');
|
|
179
|
+
const onKeydown = (event: KeyboardEvent) => {
|
|
180
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
close();
|
|
183
|
+
};
|
|
184
|
+
document.addEventListener('keydown', onKeydown);
|
|
185
|
+
return () => document.removeEventListener('keydown', onKeydown);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
$effect(() => {
|
|
189
|
+
if (!browser || !overlay || !open || !panelEl) return;
|
|
190
|
+
console.count('focus');
|
|
191
|
+
const previous = document.activeElement as HTMLElement | null;
|
|
192
|
+
if (!panelEl.contains(previous)) panelEl.focus({ preventScroll: true });
|
|
193
|
+
return () => {
|
|
194
|
+
if (previous?.isConnected && document.activeElement === document.body) previous.focus();
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
function persistWidth(nextWidth: number) {
|
|
199
|
+
if (browser && widthKey) localStorage.setItem(widthKey, String(nextWidth));
|
|
95
200
|
}
|
|
96
201
|
|
|
97
|
-
function setWidth(nextWidth: number
|
|
202
|
+
function setWidth(nextWidth: number) {
|
|
98
203
|
panelWidth = Math.round(Math.max(boundedMin, Math.min(nextWidth, boundedMax)));
|
|
99
|
-
if (persist) persistWidth();
|
|
100
204
|
}
|
|
101
205
|
|
|
102
|
-
const pointerWidths = createFrameBatcher<number>(
|
|
103
|
-
(callback) => requestAnimationFrame(callback),
|
|
104
|
-
(handle) => cancelAnimationFrame(handle),
|
|
105
|
-
(nextWidth) => setWidth(nextWidth, false)
|
|
106
|
-
);
|
|
107
|
-
|
|
108
206
|
// Keep the collapse handle within the viewport (and its panel) while a long
|
|
109
207
|
// panel scrolls past the fold. Scroll fires often, so coalesce recomputes
|
|
110
208
|
// into one per animation frame.
|
|
111
209
|
let stickyFrame: number | undefined;
|
|
112
210
|
|
|
113
211
|
function computeSticky() {
|
|
114
|
-
if (!stickyHandle || !root || !handleEl) {
|
|
212
|
+
if (!stickyHandle || overlay || !root || !handleEl) {
|
|
115
213
|
stickyShift = 0;
|
|
116
214
|
return;
|
|
117
215
|
}
|
|
@@ -139,7 +237,7 @@
|
|
|
139
237
|
}
|
|
140
238
|
|
|
141
239
|
$effect(() => {
|
|
142
|
-
if (!browser || !stickyHandle) {
|
|
240
|
+
if (!browser || !stickyHandle || overlay) {
|
|
143
241
|
stickyShift = 0;
|
|
144
242
|
return;
|
|
145
243
|
}
|
|
@@ -154,69 +252,22 @@
|
|
|
154
252
|
};
|
|
155
253
|
});
|
|
156
254
|
|
|
157
|
-
|
|
255
|
+
function close() {
|
|
256
|
+
if (!open) return;
|
|
257
|
+
open = false;
|
|
258
|
+
onclose?.();
|
|
259
|
+
}
|
|
158
260
|
|
|
159
261
|
function toggle() {
|
|
262
|
+
if (overlay) {
|
|
263
|
+
close();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
160
266
|
collapsed = !collapsed;
|
|
161
267
|
if (browser && widthKey && persistCollapsed) {
|
|
162
268
|
localStorage.setItem(`${widthKey}:collapsed`, String(collapsed));
|
|
163
269
|
}
|
|
164
270
|
}
|
|
165
|
-
|
|
166
|
-
function widthFromPointer(clientX: number) {
|
|
167
|
-
const bounds = root.getBoundingClientRect();
|
|
168
|
-
return side === 'left' ? clientX - bounds.left : bounds.right - clientX;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function startResize(event: PointerEvent) {
|
|
172
|
-
resizing = true;
|
|
173
|
-
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
174
|
-
event.preventDefault();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function resize(event: PointerEvent) {
|
|
178
|
-
if (!resizing) return;
|
|
179
|
-
pointerWidths.schedule(widthFromPointer(event.clientX));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function finishResize(event: PointerEvent) {
|
|
183
|
-
if (!resizing) return;
|
|
184
|
-
const finalWidth = Math.round(
|
|
185
|
-
Math.max(boundedMin, Math.min(widthFromPointer(event.clientX), boundedMax))
|
|
186
|
-
);
|
|
187
|
-
pointerWidths.flush(finalWidth);
|
|
188
|
-
resizing = false;
|
|
189
|
-
try {
|
|
190
|
-
(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId);
|
|
191
|
-
} catch {
|
|
192
|
-
// Pointer capture may already have been released by the browser.
|
|
193
|
-
}
|
|
194
|
-
if (browser && widthKey) localStorage.setItem(widthKey, String(finalWidth));
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function resizeWithKeyboard(event: KeyboardEvent) {
|
|
198
|
-
let nextWidth: number | undefined;
|
|
199
|
-
const direction = side === 'left' ? 1 : -1;
|
|
200
|
-
|
|
201
|
-
switch (event.key) {
|
|
202
|
-
case 'ArrowLeft':
|
|
203
|
-
nextWidth = currentWidth - resizeStep * direction;
|
|
204
|
-
break;
|
|
205
|
-
case 'ArrowRight':
|
|
206
|
-
nextWidth = currentWidth + resizeStep * direction;
|
|
207
|
-
break;
|
|
208
|
-
case 'Home':
|
|
209
|
-
nextWidth = boundedMin;
|
|
210
|
-
break;
|
|
211
|
-
case 'End':
|
|
212
|
-
nextWidth = boundedMax;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (nextWidth === undefined) return;
|
|
217
|
-
event.preventDefault();
|
|
218
|
-
setWidth(nextWidth);
|
|
219
|
-
}
|
|
220
271
|
</script>
|
|
221
272
|
|
|
222
273
|
<div
|
|
@@ -224,52 +275,74 @@
|
|
|
224
275
|
class="panel-layout"
|
|
225
276
|
class:left={side === 'left'}
|
|
226
277
|
class:right={side === 'right'}
|
|
227
|
-
class:collapsed
|
|
278
|
+
class:collapsed={!overlay && collapsed}
|
|
279
|
+
class:overlay
|
|
280
|
+
class:full-bleed={fullBleed}
|
|
228
281
|
class:resizing
|
|
229
282
|
style="--panel-width: {currentWidth}px"
|
|
230
283
|
data-tsu="ResizablePanel"
|
|
231
284
|
>
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
<!-- The separator role is interactive when focusable and wired to the
|
|
236
|
-
required arrow/Home/End keyboard behavior. -->
|
|
237
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
238
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
239
|
-
<div
|
|
240
|
-
class="resize-handle"
|
|
241
|
-
role="separator"
|
|
242
|
-
tabindex="0"
|
|
243
|
-
aria-label={`Resize ${label}`}
|
|
244
|
-
aria-orientation="vertical"
|
|
245
|
-
aria-valuemin={boundedMin}
|
|
246
|
-
aria-valuemax={boundedMax}
|
|
247
|
-
aria-valuenow={currentWidth}
|
|
248
|
-
onkeydown={resizeWithKeyboard}
|
|
249
|
-
onpointerdown={startResize}
|
|
250
|
-
onpointermove={resize}
|
|
251
|
-
onpointerup={finishResize}
|
|
252
|
-
onpointercancel={finishResize}
|
|
253
|
-
></div>
|
|
254
|
-
{/if}
|
|
285
|
+
{#if showScrim}
|
|
286
|
+
<Scrim onclose={close} hideBelow={fullWidthBelow} label={`Close ${label}`} />
|
|
287
|
+
{/if}
|
|
255
288
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
class
|
|
262
|
-
|
|
263
|
-
aria-
|
|
264
|
-
|
|
265
|
-
style="transform: translateY({stickyShift}px)"
|
|
266
|
-
onclick={toggle}
|
|
289
|
+
{#if !overlay || open}
|
|
290
|
+
<svelte:element
|
|
291
|
+
this={overlay ? 'div' : 'aside'}
|
|
292
|
+
bind:this={panelEl}
|
|
293
|
+
aria-label={label}
|
|
294
|
+
class="panel"
|
|
295
|
+
role={overlay ? 'dialog' : undefined}
|
|
296
|
+
aria-modal={overlay ? 'false' : undefined}
|
|
297
|
+
tabindex={overlay ? -1 : undefined}
|
|
267
298
|
>
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
299
|
+
{#if shown}
|
|
300
|
+
<div class="panel-content">{@render panel()}</div>
|
|
301
|
+
<!-- The separator role is interactive when focusable and wired to the
|
|
302
|
+
required arrow/Home/End keyboard behavior by the resizeHandle action. -->
|
|
303
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
304
|
+
<div
|
|
305
|
+
class="resize-handle"
|
|
306
|
+
role="separator"
|
|
307
|
+
tabindex="0"
|
|
308
|
+
aria-label={`Resize ${label}`}
|
|
309
|
+
aria-orientation="vertical"
|
|
310
|
+
aria-valuemin={boundedMin}
|
|
311
|
+
aria-valuemax={boundedMax}
|
|
312
|
+
aria-valuenow={currentWidth}
|
|
313
|
+
use:resizeHandle={{
|
|
314
|
+
side,
|
|
315
|
+
min: boundedMin,
|
|
316
|
+
max: boundedMax,
|
|
317
|
+
step: resizeStep,
|
|
318
|
+
measure: () => currentWidth,
|
|
319
|
+
onwidth: setWidth,
|
|
320
|
+
oncommit: persistWidth,
|
|
321
|
+
onactive: (active) => (resizing = active)
|
|
322
|
+
}}
|
|
323
|
+
></div>
|
|
324
|
+
{/if}
|
|
271
325
|
|
|
272
|
-
|
|
326
|
+
<button
|
|
327
|
+
bind:this={handleEl}
|
|
328
|
+
type="button"
|
|
329
|
+
class="collapse-control"
|
|
330
|
+
class:top={handlePlacement === 'top'}
|
|
331
|
+
class:bottom={handlePlacement === 'bottom'}
|
|
332
|
+
aria-label={toggleLabel}
|
|
333
|
+
aria-expanded={overlay ? undefined : !collapsed}
|
|
334
|
+
title={toggleLabel}
|
|
335
|
+
style="transform: translateY({stickyShift}px)"
|
|
336
|
+
onclick={toggle}
|
|
337
|
+
>
|
|
338
|
+
<Icon name={toggleIcon} size={14} />
|
|
339
|
+
</button>
|
|
340
|
+
</svelte:element>
|
|
341
|
+
{/if}
|
|
342
|
+
|
|
343
|
+
{#if children}
|
|
344
|
+
<div class="main">{@render children()}</div>
|
|
345
|
+
{/if}
|
|
273
346
|
</div>
|
|
274
347
|
|
|
275
348
|
<style>
|
|
@@ -443,9 +516,54 @@
|
|
|
443
516
|
}
|
|
444
517
|
}
|
|
445
518
|
|
|
519
|
+
/* Overlay mode: a fixed non-modal drawer on the viewport edge. The page
|
|
520
|
+
(`.main`) flows as normal underneath; the handle overhang is a viewport
|
|
521
|
+
edge, so the container clamp does not apply. */
|
|
522
|
+
.panel-layout.overlay {
|
|
523
|
+
display: block;
|
|
524
|
+
container: none;
|
|
525
|
+
}
|
|
526
|
+
.overlay .panel {
|
|
527
|
+
position: fixed;
|
|
528
|
+
inset-block: 0;
|
|
529
|
+
left: 0;
|
|
530
|
+
z-index: var(--z-drawer);
|
|
531
|
+
width: min(var(--panel-current-width), 100vw);
|
|
532
|
+
box-shadow: var(--shadow-md);
|
|
533
|
+
outline: none;
|
|
534
|
+
animation: panel-slide-left 0.18s var(--ease);
|
|
535
|
+
}
|
|
536
|
+
.overlay.right .panel {
|
|
537
|
+
right: 0;
|
|
538
|
+
left: auto;
|
|
539
|
+
animation-name: panel-slide-right;
|
|
540
|
+
}
|
|
541
|
+
.overlay .resize-handle {
|
|
542
|
+
--handle-shift: -6px;
|
|
543
|
+
}
|
|
544
|
+
.overlay.full-bleed .panel {
|
|
545
|
+
width: 100vw;
|
|
546
|
+
border: 0;
|
|
547
|
+
box-shadow: none;
|
|
548
|
+
}
|
|
549
|
+
.overlay.full-bleed .resize-handle {
|
|
550
|
+
display: none;
|
|
551
|
+
}
|
|
552
|
+
@keyframes panel-slide-left {
|
|
553
|
+
from {
|
|
554
|
+
transform: translateX(-100%);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
@keyframes panel-slide-right {
|
|
558
|
+
from {
|
|
559
|
+
transform: translateX(100%);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
446
563
|
@media (prefers-reduced-motion: reduce) {
|
|
447
564
|
.panel {
|
|
448
565
|
transition: none;
|
|
566
|
+
animation: none;
|
|
449
567
|
}
|
|
450
568
|
}
|
|
451
569
|
</style>
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
/** Content shown while the panel is expanded. */
|
|
4
4
|
panel: Snippet;
|
|
5
|
-
/** Main content beside the panel. */
|
|
6
|
-
children
|
|
5
|
+
/** Main content beside the panel (optional in overlay mode). */
|
|
6
|
+
children?: Snippet;
|
|
7
7
|
/** Physical edge occupied by the panel. */
|
|
8
8
|
side?: 'left' | 'right';
|
|
9
|
-
/** Accessible name for the panel landmark. */
|
|
9
|
+
/** Accessible name for the panel landmark / dialog. */
|
|
10
10
|
label?: string;
|
|
11
11
|
/** Initial expanded width in pixels. */
|
|
12
12
|
width?: number;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/** Pixel number or CSS length (`'12rem'`, `'30vw'`). */
|
|
14
|
+
minWidth?: number | string;
|
|
15
|
+
/** Pixel number or CSS length (`'40rem'`, `'90vw'`). */
|
|
16
|
+
maxWidth?: number | string;
|
|
15
17
|
/** localStorage key used to restore the expanded width. */
|
|
16
18
|
widthKey?: string;
|
|
17
|
-
/** Bindable collapsed state. */
|
|
19
|
+
/** Bindable collapsed state (inline mode). */
|
|
18
20
|
collapsed?: boolean;
|
|
19
21
|
/** Persist collapsed state as `${widthKey}:collapsed`. */
|
|
20
22
|
persistCollapsed?: boolean;
|
|
@@ -25,7 +27,21 @@ type $$ComponentProps = {
|
|
|
25
27
|
/** Keep the collapse handle in view when the panel scrolls past the
|
|
26
28
|
* viewport, repositioning on scroll/resize via requestAnimationFrame. */
|
|
27
29
|
stickyHandle?: boolean;
|
|
30
|
+
/** `inline` shares the row with `children`; `overlay` fixes the panel to
|
|
31
|
+
* its viewport edge as a non-modal drawer above the page. */
|
|
32
|
+
mode?: 'inline' | 'overlay';
|
|
33
|
+
/** Bindable drawer visibility (overlay mode). */
|
|
34
|
+
open?: boolean;
|
|
35
|
+
/** Overlay mode: Escape, scrim click or the edge control closed the drawer. */
|
|
36
|
+
onclose?: () => void;
|
|
37
|
+
/** Overlay mode: dim the page behind the drawer; clicking it closes (default true). */
|
|
38
|
+
scrim?: boolean;
|
|
39
|
+
/** Overlay mode: viewport width (CSS length) under which the drawer spans
|
|
40
|
+
* the full viewport and hides its resize handle and scrim. */
|
|
41
|
+
fullWidthBelow?: string;
|
|
42
|
+
/** Overlay mode: cap the width at the viewport and re-clamp on window resize. */
|
|
43
|
+
clampToViewport?: boolean;
|
|
28
44
|
};
|
|
29
|
-
declare const ResizablePanel: import("svelte").Component<$$ComponentProps, {}, "collapsed">;
|
|
45
|
+
declare const ResizablePanel: import("svelte").Component<$$ComponentProps, {}, "collapsed" | "open">;
|
|
30
46
|
type ResizablePanel = ReturnType<typeof ResizablePanel>;
|
|
31
47
|
export default ResizablePanel;
|
|
@@ -14,3 +14,73 @@ export function createFrameBatcher<T>(requestFrame: (callback: FrameRequestCallb
|
|
|
14
14
|
flush(value: T): void;
|
|
15
15
|
discard(): void;
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a width prop that is either a pixel number or a CSS length.
|
|
19
|
+
* Plain `px` strings parse directly; anything else is handed to `measure`,
|
|
20
|
+
* which lays the length out and returns its pixel size (or `undefined` when
|
|
21
|
+
* there is no DOM to measure in).
|
|
22
|
+
*
|
|
23
|
+
* @param {number | string} value
|
|
24
|
+
* @param {(css: string) => number | undefined} measure
|
|
25
|
+
* @returns {number | undefined}
|
|
26
|
+
*/
|
|
27
|
+
export function resolveLength(value: number | string, measure: (css: string) => number | undefined): number | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} ResizeHandleParams
|
|
30
|
+
* @property {'left' | 'right'} side Edge the resized box sits on; dragging away from it grows the box.
|
|
31
|
+
* @property {(width: number) => void} onwidth Called once per animation frame while dragging and on every keyboard step.
|
|
32
|
+
* @property {(width: number) => void} [oncommit] Called with the settled width on pointer release and after each keyboard step.
|
|
33
|
+
* @property {() => void} [onreset] Double-click on the handle.
|
|
34
|
+
* @property {(active: boolean) => void} [onactive] Drag start/end, for a `resizing` class.
|
|
35
|
+
* @property {() => number} [measure] Current width in px; defaults to the handle's parent box.
|
|
36
|
+
* @property {number} [min]
|
|
37
|
+
* @property {number} [max]
|
|
38
|
+
* @property {number} [step] Pixels per arrow key press (default 16).
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Svelte action turning any element into a pointer + keyboard width grip.
|
|
42
|
+
* One pointer-capture / rAF-coalesced implementation shared by
|
|
43
|
+
* ResizablePanel and consumer-built grips.
|
|
44
|
+
*
|
|
45
|
+
* Usage: <div role="separator" tabindex="0" use:resizeHandle={{ side, min, max, onwidth }}></div>
|
|
46
|
+
*
|
|
47
|
+
* @param {HTMLElement} node
|
|
48
|
+
* @param {ResizeHandleParams} params
|
|
49
|
+
*/
|
|
50
|
+
export function resizeHandle(node: HTMLElement, params: ResizeHandleParams): {
|
|
51
|
+
/** @param {ResizeHandleParams} next */
|
|
52
|
+
update(next: ResizeHandleParams): void;
|
|
53
|
+
destroy(): void;
|
|
54
|
+
};
|
|
55
|
+
export type ResizeHandleParams = {
|
|
56
|
+
/**
|
|
57
|
+
* Edge the resized box sits on; dragging away from it grows the box.
|
|
58
|
+
*/
|
|
59
|
+
side: "left" | "right";
|
|
60
|
+
/**
|
|
61
|
+
* Called once per animation frame while dragging and on every keyboard step.
|
|
62
|
+
*/
|
|
63
|
+
onwidth: (width: number) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Called with the settled width on pointer release and after each keyboard step.
|
|
66
|
+
*/
|
|
67
|
+
oncommit?: ((width: number) => void) | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Double-click on the handle.
|
|
70
|
+
*/
|
|
71
|
+
onreset?: (() => void) | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Drag start/end, for a `resizing` class.
|
|
74
|
+
*/
|
|
75
|
+
onactive?: ((active: boolean) => void) | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Current width in px; defaults to the handle's parent box.
|
|
78
|
+
*/
|
|
79
|
+
measure?: (() => number) | undefined;
|
|
80
|
+
min?: number | undefined;
|
|
81
|
+
max?: number | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Pixels per arrow key press (default 16).
|
|
84
|
+
*/
|
|
85
|
+
step?: number | undefined;
|
|
86
|
+
};
|