@marianmeres/stuic 3.171.0 → 3.173.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 (93) hide show
  1. package/AGENTS.md +10 -8
  2. package/API.md +146 -2
  3. package/README.md +15 -3
  4. package/dist/actions/resizable-width.svelte.d.ts +14 -28
  5. package/dist/actions/resizable-width.svelte.js +16 -171
  6. package/dist/attachments/index.d.ts +1 -0
  7. package/dist/attachments/index.js +1 -0
  8. package/dist/attachments/resizable.d.ts +113 -0
  9. package/dist/attachments/resizable.fixture.svelte +53 -0
  10. package/dist/attachments/resizable.fixture.svelte.d.ts +10 -0
  11. package/dist/attachments/resizable.js +295 -0
  12. package/dist/components/AssetsPreview/AssetsPreview.fixture.svelte +1 -1
  13. package/dist/components/Calendar/Calendar.svelte +799 -0
  14. package/dist/components/Calendar/Calendar.svelte.d.ts +135 -0
  15. package/dist/components/Calendar/README.md +294 -0
  16. package/dist/components/Calendar/calendar-i18n-sk.d.ts +20 -0
  17. package/dist/components/Calendar/calendar-i18n-sk.js +46 -0
  18. package/dist/components/Calendar/calendar-i18n.d.ts +61 -0
  19. package/dist/components/Calendar/calendar-i18n.js +73 -0
  20. package/dist/components/Calendar/index.css +307 -0
  21. package/dist/components/Calendar/index.d.ts +5 -0
  22. package/dist/components/Calendar/index.js +5 -0
  23. package/dist/components/Calendar/iso-date.d.ts +107 -0
  24. package/dist/components/Calendar/iso-date.js +247 -0
  25. package/dist/components/CommandMenu/CommandMenu.fixture.svelte +1 -1
  26. package/dist/components/Input/FieldDate.svelte +349 -0
  27. package/dist/components/Input/FieldDate.svelte.d.ts +79 -0
  28. package/dist/components/Input/FieldDateRange.svelte +373 -0
  29. package/dist/components/Input/FieldDateRange.svelte.d.ts +90 -0
  30. package/dist/components/Input/README.md +149 -16
  31. package/dist/components/Input/_internal/FieldDateShell.svelte +327 -0
  32. package/dist/components/Input/_internal/FieldDateShell.svelte.d.ts +66 -0
  33. package/dist/components/Input/index.css +119 -0
  34. package/dist/components/Input/index.d.ts +2 -0
  35. package/dist/components/Input/index.js +2 -0
  36. package/dist/components/ModalDialog/ModalDialog.fixture.svelte +1 -1
  37. package/dist/components/RangeSlider/README.md +291 -0
  38. package/dist/components/RangeSlider/RangeSlider.svelte +763 -0
  39. package/dist/components/RangeSlider/RangeSlider.svelte.d.ts +130 -0
  40. package/dist/components/RangeSlider/i18n-sk.d.ts +17 -0
  41. package/dist/components/RangeSlider/i18n-sk.js +19 -0
  42. package/dist/components/RangeSlider/i18n.d.ts +33 -0
  43. package/dist/components/RangeSlider/i18n.js +41 -0
  44. package/dist/components/RangeSlider/index.css +430 -0
  45. package/dist/components/RangeSlider/index.d.ts +3 -0
  46. package/dist/components/RangeSlider/index.js +3 -0
  47. package/dist/components/Rating/README.md +206 -0
  48. package/dist/components/Rating/Rating.svelte +355 -0
  49. package/dist/components/Rating/Rating.svelte.d.ts +82 -0
  50. package/dist/components/Rating/i18n-sk.d.ts +17 -0
  51. package/dist/components/Rating/i18n-sk.js +21 -0
  52. package/dist/components/Rating/i18n.d.ts +34 -0
  53. package/dist/components/Rating/i18n.js +42 -0
  54. package/dist/components/Rating/index.css +170 -0
  55. package/dist/components/Rating/index.d.ts +3 -0
  56. package/dist/components/Rating/index.js +3 -0
  57. package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte +1 -1
  58. package/dist/components/SplitPane/README.md +169 -0
  59. package/dist/components/SplitPane/SplitPane.svelte +202 -0
  60. package/dist/components/SplitPane/SplitPane.svelte.d.ts +67 -0
  61. package/dist/components/SplitPane/i18n-sk.d.ts +17 -0
  62. package/dist/components/SplitPane/i18n-sk.js +18 -0
  63. package/dist/components/SplitPane/i18n.d.ts +31 -0
  64. package/dist/components/SplitPane/i18n.js +39 -0
  65. package/dist/components/SplitPane/index.css +153 -0
  66. package/dist/components/SplitPane/index.d.ts +3 -0
  67. package/dist/components/SplitPane/index.js +3 -0
  68. package/dist/components/WithSidePanel/README.md +19 -16
  69. package/dist/icons/index.d.ts +3 -0
  70. package/dist/icons/index.js +4 -0
  71. package/dist/index.css +4 -0
  72. package/dist/index.d.ts +4 -0
  73. package/dist/index.js +4 -0
  74. package/docs/_archive/README.md +10 -0
  75. package/docs/{component-testing → _archive/component-testing}/00-overview-and-roadmap.md +8 -6
  76. package/docs/{component-testing → _archive/component-testing}/01-framework-setup.md +4 -2
  77. package/docs/{component-testing → _archive/component-testing}/03-component-coverage-roadmap.md +3 -1
  78. package/docs/{component-testing → _archive/component-testing}/04-hard-cases-and-e2e.md +5 -3
  79. package/docs/{component-testing → _archive/component-testing}/05-ci.md +2 -0
  80. package/docs/{component-testing → _archive/component-testing}/PROGRESS.md +3 -1
  81. package/docs/_archive/component-testing/README.md +28 -0
  82. package/docs/{maybe-todo.md → _archive/maybe-todo.md} +24 -13
  83. package/docs/{upgrading.md → _archive/upgrading.md} +2 -0
  84. package/docs/architecture.md +19 -11
  85. package/docs/domains/actions.md +21 -20
  86. package/docs/domains/attachments.md +72 -9
  87. package/docs/domains/components.md +195 -49
  88. package/docs/domains/utils.md +2 -1
  89. package/docs/tasks.md +19 -9
  90. package/docs/{component-testing/02-test-conventions.md → testing-components.md} +30 -31
  91. package/docs/testing.md +3 -3
  92. package/package.json +6 -5
  93. package/docs/component-testing/README.md +0 -38
@@ -0,0 +1,113 @@
1
+ import type { Attachment } from "svelte/attachments";
2
+ /** Which box dimension is resized: `x` = width, `y` = height. */
3
+ export type ResizableAxis = "x" | "y";
4
+ /** `px`, or `%` of the parent element's size along the axis. */
5
+ export type ResizableUnits = "px" | "%";
6
+ /** Payload of {@link ResizableOptions.onResize}. */
7
+ export interface ResizableInfo {
8
+ /** The applied (clamped) size, in `units` */
9
+ size: number;
10
+ units: ResizableUnits;
11
+ axis: ResizableAxis;
12
+ /** The parent element's size along the axis in px, as measured for this resize */
13
+ container: number;
14
+ }
15
+ /** Imperative handle, delivered through {@link ResizableOptions.onInit}. */
16
+ export interface ResizableApi {
17
+ /** The last applied size, in `units` */
18
+ readonly current: number;
19
+ /** Applies a size (in `units`): clamped to `min` / `max`, persisted, announced, reported via `onResize` */
20
+ set(size: number): void;
21
+ /** Restores `resetTo` (defaults to `initial`) — what double-click / Enter on the handle do */
22
+ reset(): void;
23
+ }
24
+ /**
25
+ * Options for the {@link resizable} attachment.
26
+ */
27
+ export interface ResizableOptions {
28
+ /** Master switch. When `false` nothing is set up: no handle, no size applied. Default `true`. */
29
+ enabled?: boolean;
30
+ /**
31
+ * `x` resizes the width (the handle sits on a vertical edge), `y` resizes the height
32
+ * (horizontal edge). Default `x`.
33
+ */
34
+ axis?: ResizableAxis;
35
+ /**
36
+ * Size applied on setup when nothing is stored under `key`. `0` (default) leaves the
37
+ * element's own CSS size alone until the first resize.
38
+ */
39
+ initial?: number;
40
+ /** Lower bound in `units`. `0` = none. */
41
+ min?: number;
42
+ /** Upper bound in `units`. `0` = none (`%` is still capped at 100). */
43
+ max?: number;
44
+ /** `px` (default) or `%` of the parent element. Not meant to change after setup. */
45
+ units?: ResizableUnits;
46
+ /**
47
+ * Puts the handle on the start edge (left / top) and inverts the drag direction — for
48
+ * an element sitting at the end (right / bottom) of its container.
49
+ */
50
+ reverse?: boolean;
51
+ /** Persist the size under this key (as `resizable-width-<key>` / `resizable-height-<key>`). */
52
+ key?: string | number | null | undefined;
53
+ /** Where `key` persists to. Default `session`. */
54
+ storage?: "local" | "session";
55
+ /** Keyboard step in `units`; Shift multiplies it by 10. Default `10` for px, `1` for %. */
56
+ step?: number;
57
+ /** Accessible name of the handle (it is a `separator`). Default `"Resize"`. */
58
+ label?: string;
59
+ /** The size restored by double-click / Enter / `api.reset()`. Defaults to `initial`. */
60
+ resetTo?: number;
61
+ /**
62
+ * Use this element as the handle instead of creating one inside the resized element.
63
+ * It receives the separator semantics, the keyboard and the pointer handling (and
64
+ * `touch-action: none`), but no visual styling — that stays with the consumer.
65
+ */
66
+ handle?: HTMLElement;
67
+ /** Extra classes of the created handle (the full-edge strip). Ignored with `handle`. */
68
+ handleClass?: string;
69
+ /** Extra classes of the created handle's grip. Ignored with `handle`. */
70
+ handleDragClass?: string;
71
+ /** Fires whenever a size is applied: on setup, while dragging, per key press, on reset and `api.set()` */
72
+ onResize?: (info: ResizableInfo) => void;
73
+ /** Receives the imperative api, once per setup */
74
+ onInit?: (api: ResizableApi) => void;
75
+ debug?: (...args: unknown[]) => void;
76
+ }
77
+ /**
78
+ * Makes an element's width (`axis: "x"`) or height (`axis: "y"`) resizable by dragging
79
+ * its edge. The handle is a keyboard-operable ARIA window splitter (`role="separator"`,
80
+ * focusable, `aria-valuenow` in `units`): arrow keys along the axis move it by `step`
81
+ * (×10 with Shift), Home / End go to `min` / `max`, Enter (like double-click) resets.
82
+ * Pointer Events with capture cover mouse, touch and pen in one code path.
83
+ *
84
+ * By default a thin handle strip with a grip is appended inside the element on the
85
+ * resized edge (the element becomes `position: relative`); pass `handle` to drive an
86
+ * element of your own (a sibling separator, say) instead. The size can be persisted
87
+ * under `key`. `min` / `max` clamp every path (drag, keys, `api.set()`).
88
+ *
89
+ * A factory: call it with the options (or a function returning them — reactive reads
90
+ * inside re-run the attachment) and attach the result.
91
+ *
92
+ * @example
93
+ * ```svelte
94
+ * <script>
95
+ * import { resizable } from "@marianmeres/stuic";
96
+ * </script>
97
+ *
98
+ * <!-- side by side: the first flex child gets a draggable width -->
99
+ * <div class="flex">
100
+ * <aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
101
+ * <main class="flex-1">…</main>
102
+ * </div>
103
+ *
104
+ * <!-- stacked: a draggable height, in % of the (definite-height) parent -->
105
+ * <div class="flex flex-col h-96">
106
+ * <div {@attach resizable({ axis: "y", units: "%", initial: 40, max: 80 })}>…</div>
107
+ * <div class="flex-1">…</div>
108
+ * </div>
109
+ * ```
110
+ *
111
+ * @param options - {@link ResizableOptions}, or a function returning them
112
+ */
113
+ export declare function resizable(options?: ResizableOptions | (() => ResizableOptions)): Attachment<HTMLElement>;
@@ -0,0 +1,53 @@
1
+ <!--
2
+ TEST-ONLY fixture for the `resizable` attachment (and the `resizableWidth` action
3
+ wrapper). A 400x300 flex container, the resized element first, a flexing sibling
4
+ after it; `external` adds a sibling to serve as the provided handle; `unmount`
5
+ tears the whole thing down so cleanup can be observed on the detached nodes.
6
+ -->
7
+ <script lang="ts">
8
+ import { resizable, type ResizableOptions } from "./resizable.js";
9
+ import {
10
+ resizableWidth,
11
+ type ResizableWidthOptions,
12
+ } from "../actions/resizable-width.svelte.js";
13
+
14
+ let {
15
+ options = {} as ResizableOptions,
16
+ action = undefined as ResizableWidthOptions | undefined,
17
+ external = false,
18
+ vertical = false,
19
+ } = $props();
20
+
21
+ let handleEl = $state<HTMLDivElement>();
22
+ let mounted = $state(true);
23
+ </script>
24
+
25
+ <div
26
+ data-testid="container"
27
+ style="width:400px;height:300px;display:flex;flex-direction:{vertical
28
+ ? 'column'
29
+ : 'row'}"
30
+ >
31
+ {#if mounted}
32
+ {#if action}
33
+ <div data-testid="el" style="width:100px" use:resizableWidth={() => action}>el</div>
34
+ {:else}
35
+ <div
36
+ data-testid="el"
37
+ style={vertical ? "height:100px" : "width:100px"}
38
+ {@attach external
39
+ ? handleEl && resizable({ ...options, handle: handleEl })
40
+ : resizable(options)}
41
+ >
42
+ el
43
+ </div>
44
+ {/if}
45
+ {#if external}
46
+ <div data-testid="handle" data-foo="bar" bind:this={handleEl}></div>
47
+ {/if}
48
+ {/if}
49
+ <div style="flex:1">rest</div>
50
+ </div>
51
+ <button type="button" data-testid="unmount" onclick={() => (mounted = false)}
52
+ >unmount</button
53
+ >
@@ -0,0 +1,10 @@
1
+ import { type ResizableOptions } from "./resizable.js";
2
+ import { type ResizableWidthOptions } from "../actions/resizable-width.svelte.js";
3
+ declare const Resizable: import("svelte").Component<{
4
+ options?: ResizableOptions;
5
+ action?: ResizableWidthOptions | undefined;
6
+ external?: boolean;
7
+ vertical?: boolean;
8
+ }, {}, "">;
9
+ type Resizable = ReturnType<typeof Resizable>;
10
+ export default Resizable;
@@ -0,0 +1,295 @@
1
+ import { getId } from "../utils/get-id.js";
2
+ import { localStorageState, sessionStorageState, } from "../utils/persistent-state.svelte.js";
3
+ import { twMerge } from "../utils/tw-merge.js";
4
+ const DEFAULT_LABEL = "Resize";
5
+ /**
6
+ * Makes an element's width (`axis: "x"`) or height (`axis: "y"`) resizable by dragging
7
+ * its edge. The handle is a keyboard-operable ARIA window splitter (`role="separator"`,
8
+ * focusable, `aria-valuenow` in `units`): arrow keys along the axis move it by `step`
9
+ * (×10 with Shift), Home / End go to `min` / `max`, Enter (like double-click) resets.
10
+ * Pointer Events with capture cover mouse, touch and pen in one code path.
11
+ *
12
+ * By default a thin handle strip with a grip is appended inside the element on the
13
+ * resized edge (the element becomes `position: relative`); pass `handle` to drive an
14
+ * element of your own (a sibling separator, say) instead. The size can be persisted
15
+ * under `key`. `min` / `max` clamp every path (drag, keys, `api.set()`).
16
+ *
17
+ * A factory: call it with the options (or a function returning them — reactive reads
18
+ * inside re-run the attachment) and attach the result.
19
+ *
20
+ * @example
21
+ * ```svelte
22
+ * <script>
23
+ * import { resizable } from "@marianmeres/stuic";
24
+ * </script>
25
+ *
26
+ * <!-- side by side: the first flex child gets a draggable width -->
27
+ * <div class="flex">
28
+ * <aside {@attach resizable({ initial: 300, min: 200, max: 600, key: "sidebar" })}>…</aside>
29
+ * <main class="flex-1">…</main>
30
+ * </div>
31
+ *
32
+ * <!-- stacked: a draggable height, in % of the (definite-height) parent -->
33
+ * <div class="flex flex-col h-96">
34
+ * <div {@attach resizable({ axis: "y", units: "%", initial: 40, max: 80 })}>…</div>
35
+ * <div class="flex-1">…</div>
36
+ * </div>
37
+ * ```
38
+ *
39
+ * @param options - {@link ResizableOptions}, or a function returning them
40
+ */
41
+ export function resizable(options = {}) {
42
+ return (el) => setup(el, typeof options === "function" ? options() : options);
43
+ }
44
+ // internals //////////////////////////////////////////////////////////////////////////
45
+ const HANDLE_CLS_COMMON = [
46
+ "group",
47
+ "absolute",
48
+ "bg-black/20 hover:bg-black/30",
49
+ "dark:bg-white/10 dark:hover:bg-white/20",
50
+ "transition-colors duration-200",
51
+ "touch-none",
52
+ "focus-visible:outline-2 focus-visible:outline-(--stuic-color-ring)",
53
+ ];
54
+ const HANDLE_CLS_X = [...HANDLE_CLS_COMMON, "top-0 bottom-0 w-[1px] cursor-col-resize"];
55
+ const HANDLE_CLS_Y = [...HANDLE_CLS_COMMON, "left-0 right-0 h-[1px] cursor-row-resize"];
56
+ const GRIP_CLS_COMMON = [
57
+ "absolute",
58
+ "rounded border border-black/20 dark:border-white/20",
59
+ "bg-gray-300 group-hover:bg-gray-400",
60
+ "dark:bg-gray-600 dark:group-hover:bg-gray-500",
61
+ "transition-colors duration-200",
62
+ "touch-none",
63
+ ];
64
+ const GRIP_CLS_X = [
65
+ ...GRIP_CLS_COMMON,
66
+ "h-[20px] w-[9px] -translate-x-[4px] top-1/2 -translate-y-1/2 cursor-col-resize",
67
+ ];
68
+ const GRIP_CLS_Y = [
69
+ ...GRIP_CLS_COMMON,
70
+ "w-[20px] h-[9px] -translate-y-[4px] left-1/2 -translate-x-1/2 cursor-row-resize",
71
+ ];
72
+ function create_handle(el, isX, reverse, handleClass, handleDragClass) {
73
+ const handle = document.createElement("div");
74
+ handle.setAttribute("data-handle", "true");
75
+ const grip = document.createElement("div");
76
+ grip.classList.add(...twMerge((isX ? GRIP_CLS_X : GRIP_CLS_Y).join(" "), handleDragClass).split(" "));
77
+ handle.appendChild(grip);
78
+ const edge = isX ? (reverse ? "left-0" : "right-0") : reverse ? "top-0" : "bottom-0";
79
+ handle.classList.add(...twMerge((isX ? HANDLE_CLS_X : HANDLE_CLS_Y).join(" "), edge, handleClass).split(" "));
80
+ el.appendChild(handle);
81
+ return handle;
82
+ }
83
+ function setup(el, options) {
84
+ const { enabled = true, axis = "x", initial = 0, min = 0, max = 0, units = "px", reverse = false, key, storage = "session", step = units === "%" ? 1 : 10, label = DEFAULT_LABEL, resetTo = initial, handle: providedHandle, handleClass = "", handleDragClass = "", onResize, onInit, debug, } = options;
85
+ const _debug = (...args) => debug?.("[resizable]", ...args);
86
+ _debug("setup", options);
87
+ if (!enabled)
88
+ return;
89
+ const container = el.parentElement;
90
+ if (!container)
91
+ return;
92
+ const isX = axis === "x";
93
+ const dim = isX ? "width" : "height";
94
+ // storage: a stored size wins over `initial`
95
+ const stored = key
96
+ ? (storage === "local" ? localStorageState : sessionStorageState)(`resizable-${dim}-${key}`, initial)
97
+ : null;
98
+ // handle /////////////////////////////////////////////////////////////////////////
99
+ const handle = providedHandle ?? create_handle(el, isX, reverse, handleClass, handleDragClass);
100
+ // what a provided handle looked like before, so the cleanup can restore it
101
+ const prevAttrs = new Map();
102
+ const set_attr = (name, value) => {
103
+ if (!prevAttrs.has(name))
104
+ prevAttrs.set(name, handle.getAttribute(name));
105
+ handle.setAttribute(name, value);
106
+ };
107
+ const prevPosition = el.style.position;
108
+ const prevTouchAction = handle.style.touchAction;
109
+ if (!providedHandle)
110
+ el.style.position = "relative"; // the created handle is absolute
111
+ handle.style.touchAction = "none"; // no scrolling / zooming mid-drag
112
+ const assignedId = el.id ? null : (el.id = getId("stuic-resizable-"));
113
+ set_attr("role", "separator");
114
+ set_attr("tabindex", "0");
115
+ set_attr("aria-orientation", isX ? "vertical" : "horizontal");
116
+ set_attr("aria-label", label);
117
+ set_attr("aria-controls", el.id);
118
+ // sizing /////////////////////////////////////////////////////////////////////////
119
+ const measure = () => (isX ? container.offsetWidth : container.offsetHeight);
120
+ const current_px = () => parseFloat(getComputedStyle(el)[dim]) || 0;
121
+ // px -> units; with no measurable container (hidden) a % size is left as it is
122
+ const to_units = (px, cs) => units === "%" ? (cs > 0 ? (px / cs) * 100 : current) : px;
123
+ const clamp = (value) => {
124
+ const before = value;
125
+ if (min)
126
+ value = Math.max(min, value);
127
+ if (max)
128
+ value = Math.min(max, value);
129
+ if (before !== value)
130
+ _debug("clamped", before, "->", value, units);
131
+ return value;
132
+ };
133
+ let current = 0; // last applied size, in units
134
+ function update_aria(cs) {
135
+ set_attr("aria-valuemin", `${min || 0}`);
136
+ set_attr("aria-valuemax", `${max || (units === "%" ? 100 : Math.round(cs))}`);
137
+ set_attr("aria-valuenow", `${Math.round(current)}`);
138
+ set_attr("aria-valuetext", `${Math.round(current)}${units}`);
139
+ }
140
+ function commit(size, cs) {
141
+ size = clamp(units === "%" ? Math.min(100, size) : size);
142
+ el.style[dim] = `${size}${units}`;
143
+ current = size;
144
+ _debug("applied", size, units);
145
+ update_aria(cs);
146
+ const info = { size, units, axis, container: cs };
147
+ onResize?.(info);
148
+ if (stored)
149
+ stored.current = size;
150
+ return info;
151
+ }
152
+ /** Applies a size given in `units` — needs no container measurement, so it also works while hidden. */
153
+ function apply(size) {
154
+ commit(size, measure());
155
+ }
156
+ /** Applies a size given in px (a drag), against the container as measured when it started. */
157
+ function apply_px(px, cs) {
158
+ commit(to_units(px, cs), cs);
159
+ }
160
+ function reset() {
161
+ if (resetTo)
162
+ apply(resetTo);
163
+ }
164
+ // pointer ////////////////////////////////////////////////////////////////////////
165
+ let dragging = false;
166
+ let pointerId = -1;
167
+ let startCoord = 0;
168
+ let startPx = 0;
169
+ let containerSize = 0;
170
+ function on_pointerdown(e) {
171
+ if (e.button !== 0 && e.pointerType === "mouse")
172
+ return;
173
+ dragging = true;
174
+ pointerId = e.pointerId;
175
+ startCoord = isX ? e.clientX : e.clientY;
176
+ startPx = current_px();
177
+ containerSize = measure();
178
+ try {
179
+ handle.setPointerCapture(e.pointerId);
180
+ }
181
+ catch {
182
+ /* capture is best-effort (synthetic pointers have no id to capture) */
183
+ }
184
+ document.body.style.cursor = isX ? "col-resize" : "row-resize";
185
+ document.body.style.userSelect = "none";
186
+ handle.setAttribute("data-resizing", "");
187
+ }
188
+ function on_pointermove(e) {
189
+ if (!dragging || e.pointerId !== pointerId)
190
+ return;
191
+ e.preventDefault();
192
+ const delta = (isX ? e.clientX : e.clientY) - startCoord;
193
+ apply_px(reverse ? startPx - delta : startPx + delta, containerSize);
194
+ }
195
+ function on_pointerup(e) {
196
+ if (!dragging || e.pointerId !== pointerId)
197
+ return;
198
+ stop();
199
+ }
200
+ function stop() {
201
+ if (!dragging)
202
+ return;
203
+ dragging = false;
204
+ try {
205
+ handle.releasePointerCapture(pointerId);
206
+ }
207
+ catch {
208
+ /* noop */
209
+ }
210
+ document.body.style.cursor = "";
211
+ document.body.style.userSelect = "";
212
+ handle.removeAttribute("data-resizing");
213
+ }
214
+ // keyboard ///////////////////////////////////////////////////////////////////////
215
+ function on_keydown(e) {
216
+ const amount = e.shiftKey ? step * 10 : step;
217
+ // delta along the screen direction (+ = right / down), like a pointer drag
218
+ let delta;
219
+ if (e.key === (isX ? "ArrowRight" : "ArrowDown"))
220
+ delta = amount;
221
+ else if (e.key === (isX ? "ArrowLeft" : "ArrowUp"))
222
+ delta = -amount;
223
+ else if (e.key === "Home")
224
+ delta = NaN;
225
+ else if (e.key === "End")
226
+ delta = NaN;
227
+ else if (e.key === "Enter")
228
+ delta = NaN;
229
+ else
230
+ return;
231
+ e.preventDefault();
232
+ if (e.key === "Home")
233
+ return apply(min || 0);
234
+ if (e.key === "End")
235
+ return apply(max || (units === "%" ? 100 : measure()));
236
+ if (e.key === "Enter")
237
+ return reset();
238
+ apply(current + (reverse ? -delta : delta));
239
+ }
240
+ function on_dblclick() {
241
+ reset();
242
+ }
243
+ function on_selectstart(e) {
244
+ e.preventDefault();
245
+ }
246
+ // go /////////////////////////////////////////////////////////////////////////////
247
+ const startSize = stored ? (stored.current ?? initial) : initial;
248
+ if (startSize) {
249
+ apply(startSize);
250
+ }
251
+ else {
252
+ const cs = measure();
253
+ current = to_units(current_px(), cs);
254
+ update_aria(cs);
255
+ }
256
+ handle.addEventListener("pointerdown", on_pointerdown);
257
+ handle.addEventListener("pointermove", on_pointermove);
258
+ handle.addEventListener("pointerup", on_pointerup);
259
+ handle.addEventListener("pointercancel", on_pointerup);
260
+ handle.addEventListener("keydown", on_keydown);
261
+ handle.addEventListener("dblclick", on_dblclick);
262
+ handle.addEventListener("selectstart", on_selectstart);
263
+ onInit?.({
264
+ get current() {
265
+ return current;
266
+ },
267
+ set: apply,
268
+ reset,
269
+ });
270
+ return () => {
271
+ stop();
272
+ handle.removeEventListener("pointerdown", on_pointerdown);
273
+ handle.removeEventListener("pointermove", on_pointermove);
274
+ handle.removeEventListener("pointerup", on_pointerup);
275
+ handle.removeEventListener("pointercancel", on_pointerup);
276
+ handle.removeEventListener("keydown", on_keydown);
277
+ handle.removeEventListener("dblclick", on_dblclick);
278
+ handle.removeEventListener("selectstart", on_selectstart);
279
+ if (providedHandle) {
280
+ for (const [name, value] of prevAttrs) {
281
+ if (value === null)
282
+ handle.removeAttribute(name);
283
+ else
284
+ handle.setAttribute(name, value);
285
+ }
286
+ handle.style.touchAction = prevTouchAction;
287
+ }
288
+ else {
289
+ handle.remove();
290
+ el.style.position = prevPosition;
291
+ }
292
+ if (assignedId && el.id === assignedId)
293
+ el.removeAttribute("id");
294
+ };
295
+ }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- // Conventions escape hatch (docs/component-testing/02-test-conventions.md):
2
+ // Conventions escape hatch (docs/testing-components.md):
3
3
  // AssetsPreview is imperative-only — it opens a ModalDialog via a ref method
4
4
  // open(index?). A *.svelte.test.ts file can't hold a bind:this + a trigger, so
5
5
  // this fixture exposes an opener button that calls open(openIndex); all other