@dorsk/tsumikit 0.23.0 → 0.25.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 (35) hide show
  1. package/README.md +174 -6
  2. package/dist/components/atoms/Button.svelte +1 -1
  3. package/dist/components/atoms/Card.svelte +65 -2
  4. package/dist/components/atoms/Card.svelte.d.ts +6 -0
  5. package/dist/components/atoms/Scrim.svelte +81 -0
  6. package/dist/components/atoms/Scrim.svelte.d.ts +14 -0
  7. package/dist/components/layouts/ResizablePanel.svelte +238 -120
  8. package/dist/components/layouts/ResizablePanel.svelte.d.ts +24 -8
  9. package/dist/components/layouts/resizable-panel-frame.d.ts +70 -0
  10. package/dist/components/layouts/resizable-panel-frame.js +161 -0
  11. package/dist/components/molecules/ConfirmModal.svelte +102 -0
  12. package/dist/components/molecules/ConfirmModal.svelte.d.ts +16 -0
  13. package/dist/components/molecules/KeyValue.svelte +117 -0
  14. package/dist/components/molecules/KeyValue.svelte.d.ts +20 -0
  15. package/dist/components/molecules/LoadMore.svelte +78 -0
  16. package/dist/components/molecules/LoadMore.svelte.d.ts +15 -0
  17. package/dist/components/molecules/Modal.svelte +66 -10
  18. package/dist/components/molecules/Modal.svelte.d.ts +12 -2
  19. package/dist/components/molecules/Pagination.svelte +209 -0
  20. package/dist/components/molecules/Pagination.svelte.d.ts +22 -0
  21. package/dist/components/molecules/SectionHeader.svelte +233 -0
  22. package/dist/components/molecules/SectionHeader.svelte.d.ts +29 -0
  23. package/dist/components/molecules/ThemePicker.svelte +11 -13
  24. package/dist/index.d.ts +7 -0
  25. package/dist/index.js +7 -0
  26. package/dist/stores/theme.svelte.d.ts +21 -5
  27. package/dist/stores/theme.svelte.js +48 -22
  28. package/dist/styles/app.css +7 -297
  29. package/dist/styles/reset.css +97 -0
  30. package/dist/styles/syntax.css +94 -0
  31. package/dist/styles/themes.css +729 -0
  32. package/dist/styles/tokens.css +207 -0
  33. package/dist/styles/utilities.css +111 -0
  34. package/dist/styles/variables.css +5 -926
  35. package/package.json +7 -2
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
- import { onDestroy, type Snippet } from 'svelte';
2
+ import type { Snippet } from 'svelte';
3
3
  import { browser } from '../../env';
4
4
  import Icon from '../atoms/Icon.svelte';
5
- import { createFrameBatcher } from './resizable-panel-frame.js';
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: Snippet;
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
- minWidth?: number;
34
- maxWidth?: number;
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 boundedMin = $derived(Math.max(1, Math.min(minWidth, maxWidth)));
58
- const boundedMax = $derived(Math.max(boundedMin, maxWidth));
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(collapsed ? `Expand ${label}` : `Collapse ${label}`);
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
- function persistWidth() {
94
- if (browser && widthKey) localStorage.setItem(widthKey, String(currentWidth));
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, persist = true) {
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
- onDestroy(() => pointerWidths.discard());
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
- <aside aria-label={label} class="panel">
233
- {#if !collapsed}
234
- <div class="panel-content">{@render panel()}</div>
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
- <button
257
- bind:this={handleEl}
258
- type="button"
259
- class="collapse-control"
260
- class:top={handlePlacement === 'top'}
261
- class:bottom={handlePlacement === 'bottom'}
262
- aria-label={toggleLabel}
263
- aria-expanded={!collapsed}
264
- title={toggleLabel}
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
- <Icon name={toggleIcon} size={14} />
269
- </button>
270
- </aside>
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
- <div class="main">{@render children()}</div>
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 { type Snippet } from 'svelte';
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: Snippet;
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
- minWidth?: number;
14
- maxWidth?: number;
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, {}, "open" | "collapsed">;
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
+ };