@sveltia/ui 0.32.1 → 0.33.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/dist/components/checkbox/checkbox-group.svelte.d.ts +2 -2
- package/dist/components/divider/divider.svelte.d.ts +2 -2
- package/dist/components/radio/radio-group.svelte.d.ts +2 -2
- package/dist/components/resizable-pane/resizable-handle.svelte +312 -0
- package/dist/components/resizable-pane/resizable-handle.svelte.d.ts +69 -0
- package/dist/components/resizable-pane/resizable-pane-group.svelte +179 -0
- package/dist/components/resizable-pane/resizable-pane-group.svelte.d.ts +53 -0
- package/dist/components/resizable-pane/resizable-pane.svelte +76 -0
- package/dist/components/resizable-pane/resizable-pane.svelte.d.ts +70 -0
- package/dist/components/select/select-tags.svelte +210 -24
- package/dist/components/select/select-tags.svelte.d.ts +34 -8
- package/dist/components/tabs/tab-box.svelte.d.ts +2 -2
- package/dist/components/tabs/tab-list.svelte.d.ts +2 -2
- package/dist/components/text-editor/core.js +40 -45
- package/dist/components/text-editor/markdown.test.js +1 -95
- package/dist/components/text-editor/transformers/table.js +24 -24
- package/dist/components/toolbar/toolbar.svelte.d.ts +2 -2
- package/dist/components/util/app-shell.svelte.d.ts +2 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/locales/en.d.ts +4 -0
- package/dist/locales/en.js +4 -0
- package/dist/locales/ja.d.ts +4 -0
- package/dist/locales/ja.js +4 -0
- package/dist/typedefs.d.ts +54 -0
- package/dist/typedefs.js +23 -0
- package/package.json +22 -22
|
@@ -22,7 +22,7 @@ declare const CheckboxGroup: import("svelte").Component<{
|
|
|
22
22
|
/**
|
|
23
23
|
* Orientation of the widget.
|
|
24
24
|
*/
|
|
25
|
-
orientation?: "
|
|
25
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
26
26
|
/**
|
|
27
27
|
* Primary slot content.
|
|
28
28
|
*/
|
|
@@ -46,7 +46,7 @@ type Props = {
|
|
|
46
46
|
/**
|
|
47
47
|
* Orientation of the widget.
|
|
48
48
|
*/
|
|
49
|
-
orientation?: "
|
|
49
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
50
50
|
/**
|
|
51
51
|
* Primary slot content.
|
|
52
52
|
*/
|
|
@@ -21,7 +21,7 @@ declare const Divider: import("svelte").Component<{
|
|
|
21
21
|
* Orientation of the widget. An alias of the
|
|
22
22
|
* `aria-orientation` attribute.
|
|
23
23
|
*/
|
|
24
|
-
orientation?: "
|
|
24
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
25
25
|
} & Record<string, any>, {}, "">;
|
|
26
26
|
type Props = {
|
|
27
27
|
/**
|
|
@@ -37,5 +37,5 @@ type Props = {
|
|
|
37
37
|
* Orientation of the widget. An alias of the
|
|
38
38
|
* `aria-orientation` attribute.
|
|
39
39
|
*/
|
|
40
|
-
orientation?: "
|
|
40
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
41
41
|
};
|
|
@@ -41,7 +41,7 @@ declare const RadioGroup: import("svelte").Component<{
|
|
|
41
41
|
* Orientation of the widget. An alias of the
|
|
42
42
|
* `aria-orientation` attribute.
|
|
43
43
|
*/
|
|
44
|
-
orientation?: "
|
|
44
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
45
45
|
/**
|
|
46
46
|
* Primary slot content.
|
|
47
47
|
*/
|
|
@@ -84,7 +84,7 @@ type Props = {
|
|
|
84
84
|
* Orientation of the widget. An alias of the
|
|
85
85
|
* `aria-orientation` attribute.
|
|
86
86
|
*/
|
|
87
|
-
orientation?: "
|
|
87
|
+
orientation?: "horizontal" | "vertical" | undefined;
|
|
88
88
|
/**
|
|
89
89
|
* Primary slot content.
|
|
90
90
|
*/
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
A draggable handle for resizing adjacent panes within a `<ResizablePaneGroup>`.
|
|
4
|
+
@see https://w3c.github.io/aria/#separator
|
|
5
|
+
@see https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
6
|
+
-->
|
|
7
|
+
<script>
|
|
8
|
+
import { getContext } from 'svelte';
|
|
9
|
+
import { isRTL } from '../../services/i18n.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @import { Snippet } from 'svelte';
|
|
13
|
+
* @import { PaneGroupContext } from '../../typedefs.js';
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} Props
|
|
18
|
+
* @property {boolean} [disabled] Whether to disable the handle.
|
|
19
|
+
* @property {boolean} [showHandleBar] Whether to show the handle bar.
|
|
20
|
+
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
21
|
+
* @property {Snippet} [children] Custom handle content. If omitted, a default visual indicator is
|
|
22
|
+
* rendered.
|
|
23
|
+
* @property {() => void} [onResizeStart] Called when a resize interaction begins (pointer down or
|
|
24
|
+
* first keyboard step).
|
|
25
|
+
* @property {() => void} [onResizeEnd] Called when a resize interaction ends (pointer up/cancel
|
|
26
|
+
* or handle blur).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @type {Props & Record<string, any>}
|
|
31
|
+
*/
|
|
32
|
+
let {
|
|
33
|
+
/* eslint-disable prefer-const */
|
|
34
|
+
disabled = false,
|
|
35
|
+
showHandleBar = false,
|
|
36
|
+
class: className,
|
|
37
|
+
children,
|
|
38
|
+
onResizeStart,
|
|
39
|
+
onResizeEnd,
|
|
40
|
+
...restProps
|
|
41
|
+
/* eslint-enable prefer-const */
|
|
42
|
+
} = $props();
|
|
43
|
+
|
|
44
|
+
/** @type {PaneGroupContext} */
|
|
45
|
+
const ctx = getContext('paneGroup');
|
|
46
|
+
|
|
47
|
+
if (!ctx) {
|
|
48
|
+
throw new Error('<ResizableHandle> must be used inside a <ResizablePaneGroup>');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleIndex = ctx.registerHandle();
|
|
52
|
+
const isHorizontal = $derived(ctx.direction === 'horizontal');
|
|
53
|
+
const sizes = $derived(ctx.sizes);
|
|
54
|
+
// The value reported via aria-valuenow is the size of the pane immediately before this handle
|
|
55
|
+
const currentPaneSize = $derived(sizes[handleIndex] ?? 0);
|
|
56
|
+
const currentPaneMin = $derived(
|
|
57
|
+
Math.max(
|
|
58
|
+
ctx.paneDefs[handleIndex]?.minSize ?? 0,
|
|
59
|
+
100 - ctx.paneDefs.reduce((sum, p, i) => sum + (i !== handleIndex ? p.maxSize : 0), 0),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
const currentPaneMax = $derived(
|
|
63
|
+
Math.min(
|
|
64
|
+
ctx.paneDefs[handleIndex]?.maxSize ?? 100,
|
|
65
|
+
100 - ctx.paneDefs.reduce((sum, p, i) => sum + (i !== handleIndex ? p.minSize : 0), 0),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A reference to the handle element.
|
|
71
|
+
* @type {HTMLElement | undefined}
|
|
72
|
+
*/
|
|
73
|
+
let element = $state();
|
|
74
|
+
let dragging = $state(false);
|
|
75
|
+
let startScreenPos = $state(0);
|
|
76
|
+
let targetPointerId = $state(0);
|
|
77
|
+
/**
|
|
78
|
+
* Whether the handle is being resized via keyboard (at least one step fired).
|
|
79
|
+
*/
|
|
80
|
+
let keyResizing = $state(false);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get the pane group container element's size in pixels for px→% conversion.
|
|
84
|
+
* @returns {number} Container size in pixels.
|
|
85
|
+
*/
|
|
86
|
+
const getContainerSize = () => {
|
|
87
|
+
const container = element?.closest('.resizable-pane-group');
|
|
88
|
+
|
|
89
|
+
if (!container) return 0;
|
|
90
|
+
|
|
91
|
+
return isHorizontal ? container.clientWidth : container.clientHeight;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle pointer move events (attached to `document` while dragging).
|
|
96
|
+
* @param {PointerEvent} event `pointermove` event.
|
|
97
|
+
*/
|
|
98
|
+
const onPointerMove = (event) => {
|
|
99
|
+
const { screenX, screenY, pointerId } = event;
|
|
100
|
+
|
|
101
|
+
if (disabled || !dragging || pointerId !== targetPointerId) return;
|
|
102
|
+
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
event.stopPropagation();
|
|
105
|
+
|
|
106
|
+
const screenPos = isHorizontal ? screenX : screenY;
|
|
107
|
+
const pixelDelta = screenPos - startScreenPos;
|
|
108
|
+
const containerSize = getContainerSize();
|
|
109
|
+
|
|
110
|
+
if (!containerSize) return;
|
|
111
|
+
|
|
112
|
+
let percentDelta = (pixelDelta / containerSize) * 100;
|
|
113
|
+
|
|
114
|
+
// In RTL with a horizontal layout, the visual direction is reversed
|
|
115
|
+
if (isHorizontal && $isRTL) {
|
|
116
|
+
percentDelta = -percentDelta;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
startScreenPos = screenPos;
|
|
120
|
+
ctx.resize(handleIndex, percentDelta);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle pointer up/cancel events (attached to `document` while dragging).
|
|
125
|
+
* @param {PointerEvent} event `pointerup` or `pointercancel` event.
|
|
126
|
+
*/
|
|
127
|
+
const onPointerUp = (event) => {
|
|
128
|
+
if (!dragging || event.pointerId !== targetPointerId) return;
|
|
129
|
+
|
|
130
|
+
dragging = false;
|
|
131
|
+
startScreenPos = 0;
|
|
132
|
+
targetPointerId = 0;
|
|
133
|
+
|
|
134
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
135
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
136
|
+
document.removeEventListener('pointercancel', onPointerUp);
|
|
137
|
+
onResizeEnd?.();
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle pointer down events on the handle.
|
|
142
|
+
* @param {PointerEvent} event `pointerdown` event.
|
|
143
|
+
*/
|
|
144
|
+
const onPointerDown = (event) => {
|
|
145
|
+
if (disabled) return;
|
|
146
|
+
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
event.stopPropagation();
|
|
149
|
+
|
|
150
|
+
dragging = true;
|
|
151
|
+
startScreenPos = isHorizontal ? event.screenX : event.screenY;
|
|
152
|
+
targetPointerId = event.pointerId;
|
|
153
|
+
onResizeStart?.();
|
|
154
|
+
|
|
155
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
156
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
157
|
+
document.addEventListener('pointercancel', onPointerUp);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Handle keyboard events for accessibility. Arrow keys move the handle by 1%; Shift+Arrow moves
|
|
162
|
+
* by 10%. Enter collapses/restores the primary pane. Home/End jump to min/max.
|
|
163
|
+
* @param {KeyboardEvent} event `keydown` event.
|
|
164
|
+
*/
|
|
165
|
+
const onKeyDown = (event) => {
|
|
166
|
+
if (disabled) return;
|
|
167
|
+
|
|
168
|
+
const { key, shiftKey } = event;
|
|
169
|
+
const step = shiftKey ? 10 : 1;
|
|
170
|
+
let delta = 0;
|
|
171
|
+
|
|
172
|
+
if (key === 'Enter') {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
ctx.toggleCollapse(handleIndex);
|
|
176
|
+
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (key === 'Home') {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
event.stopPropagation();
|
|
183
|
+
// Collapse to minimum — clamped internally
|
|
184
|
+
ctx.resize(handleIndex, -100);
|
|
185
|
+
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (key === 'End') {
|
|
190
|
+
event.preventDefault();
|
|
191
|
+
event.stopPropagation();
|
|
192
|
+
// Expand to maximum — clamped internally
|
|
193
|
+
ctx.resize(handleIndex, 100);
|
|
194
|
+
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (isHorizontal) {
|
|
199
|
+
// In RTL, Left/Right directions are visually swapped
|
|
200
|
+
if (key === 'ArrowLeft') {
|
|
201
|
+
delta = $isRTL ? step : -step;
|
|
202
|
+
} else if (key === 'ArrowRight') {
|
|
203
|
+
delta = $isRTL ? -step : step;
|
|
204
|
+
} else {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
} else if (key === 'ArrowUp') {
|
|
208
|
+
delta = -step;
|
|
209
|
+
} else if (key === 'ArrowDown') {
|
|
210
|
+
delta = step;
|
|
211
|
+
} else {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
event.stopPropagation();
|
|
217
|
+
|
|
218
|
+
if (!keyResizing) {
|
|
219
|
+
keyResizing = true;
|
|
220
|
+
onResizeStart?.();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ctx.resize(handleIndex, delta);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle blur to signal keyboard resize end.
|
|
228
|
+
*/
|
|
229
|
+
const onBlur = () => {
|
|
230
|
+
if (keyResizing) {
|
|
231
|
+
keyResizing = false;
|
|
232
|
+
onResizeEnd?.();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
238
|
+
<div
|
|
239
|
+
bind:this={element}
|
|
240
|
+
{...restProps}
|
|
241
|
+
role="separator"
|
|
242
|
+
tabindex={disabled ? -1 : 0}
|
|
243
|
+
aria-orientation={isHorizontal ? 'vertical' : 'horizontal'}
|
|
244
|
+
aria-valuenow={Math.round(currentPaneSize)}
|
|
245
|
+
aria-valuemin={currentPaneMin}
|
|
246
|
+
aria-valuemax={currentPaneMax}
|
|
247
|
+
aria-controls={ctx.paneDefs[handleIndex]?.id}
|
|
248
|
+
aria-disabled={disabled || undefined}
|
|
249
|
+
class="sui resizable-handle {className ?? ''}"
|
|
250
|
+
class:horizontal={isHorizontal}
|
|
251
|
+
class:vertical={!isHorizontal}
|
|
252
|
+
class:disabled
|
|
253
|
+
class:dragging
|
|
254
|
+
onpointerdown={onPointerDown}
|
|
255
|
+
onkeydown={onKeyDown}
|
|
256
|
+
onblur={onBlur}
|
|
257
|
+
>
|
|
258
|
+
{#if children}
|
|
259
|
+
{@render children()}
|
|
260
|
+
{:else if showHandleBar}
|
|
261
|
+
<div role="none" class="handle-bar"></div>
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<style>.resizable-handle {
|
|
266
|
+
position: relative;
|
|
267
|
+
flex: 0 0 auto;
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
justify-content: center;
|
|
271
|
+
touch-action: none;
|
|
272
|
+
outline-offset: 0;
|
|
273
|
+
background-color: transparent;
|
|
274
|
+
transition: background-color 200ms;
|
|
275
|
+
}
|
|
276
|
+
.resizable-handle:focus-visible, .resizable-handle:hover, .resizable-handle.dragging {
|
|
277
|
+
outline: none;
|
|
278
|
+
z-index: 1;
|
|
279
|
+
background-color: var(--sui-primary-accent-color-translucent);
|
|
280
|
+
}
|
|
281
|
+
.resizable-handle:focus-visible .handle-bar, .resizable-handle:hover .handle-bar, .resizable-handle.dragging .handle-bar {
|
|
282
|
+
background-color: var(--sui-primary-accent-color);
|
|
283
|
+
}
|
|
284
|
+
.resizable-handle.disabled {
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
opacity: 0.4;
|
|
287
|
+
}
|
|
288
|
+
.resizable-handle.horizontal {
|
|
289
|
+
width: var(--sui-resizable-handle-size, 4px);
|
|
290
|
+
height: 100%;
|
|
291
|
+
cursor: col-resize;
|
|
292
|
+
}
|
|
293
|
+
.resizable-handle.horizontal .handle-bar {
|
|
294
|
+
width: 2px;
|
|
295
|
+
height: 40%;
|
|
296
|
+
min-height: 20px;
|
|
297
|
+
}
|
|
298
|
+
.resizable-handle.vertical {
|
|
299
|
+
width: 100%;
|
|
300
|
+
height: var(--sui-resizable-handle-size, 4px);
|
|
301
|
+
cursor: row-resize;
|
|
302
|
+
}
|
|
303
|
+
.resizable-handle.vertical .handle-bar {
|
|
304
|
+
height: 2px;
|
|
305
|
+
width: 40%;
|
|
306
|
+
min-width: 20px;
|
|
307
|
+
}
|
|
308
|
+
.resizable-handle .handle-bar {
|
|
309
|
+
border-radius: 1px;
|
|
310
|
+
background-color: hsl(var(--sui-border-color-1-hsl));
|
|
311
|
+
transition: background-color 200ms;
|
|
312
|
+
}</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export default ResizableHandle;
|
|
2
|
+
type ResizableHandle = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<Props & Record<string, any>>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* A draggable handle for resizing adjacent panes within a `<ResizablePaneGroup>`.
|
|
8
|
+
* @see https://w3c.github.io/aria/#separator
|
|
9
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
10
|
+
*/
|
|
11
|
+
declare const ResizableHandle: import("svelte").Component<{
|
|
12
|
+
/**
|
|
13
|
+
* Whether to disable the handle.
|
|
14
|
+
*/
|
|
15
|
+
disabled?: boolean | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Whether to show the handle bar.
|
|
18
|
+
*/
|
|
19
|
+
showHandleBar?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The `class` attribute on the wrapper element.
|
|
22
|
+
*/
|
|
23
|
+
class?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Custom handle content. If omitted, a default visual indicator is
|
|
26
|
+
* rendered.
|
|
27
|
+
*/
|
|
28
|
+
children?: Snippet<[]> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Called when a resize interaction begins (pointer down or
|
|
31
|
+
* first keyboard step).
|
|
32
|
+
*/
|
|
33
|
+
onResizeStart?: (() => void) | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Called when a resize interaction ends (pointer up/cancel
|
|
36
|
+
* or handle blur).
|
|
37
|
+
*/
|
|
38
|
+
onResizeEnd?: (() => void) | undefined;
|
|
39
|
+
} & Record<string, any>, {}, "">;
|
|
40
|
+
type Props = {
|
|
41
|
+
/**
|
|
42
|
+
* Whether to disable the handle.
|
|
43
|
+
*/
|
|
44
|
+
disabled?: boolean | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Whether to show the handle bar.
|
|
47
|
+
*/
|
|
48
|
+
showHandleBar?: boolean | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The `class` attribute on the wrapper element.
|
|
51
|
+
*/
|
|
52
|
+
class?: string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Custom handle content. If omitted, a default visual indicator is
|
|
55
|
+
* rendered.
|
|
56
|
+
*/
|
|
57
|
+
children?: Snippet<[]> | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Called when a resize interaction begins (pointer down or
|
|
60
|
+
* first keyboard step).
|
|
61
|
+
*/
|
|
62
|
+
onResizeStart?: (() => void) | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Called when a resize interaction ends (pointer up/cancel
|
|
65
|
+
* or handle blur).
|
|
66
|
+
*/
|
|
67
|
+
onResizeEnd?: (() => void) | undefined;
|
|
68
|
+
};
|
|
69
|
+
import type { Snippet } from 'svelte';
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Container for resizable panes. Panes must be separated by `<ResizableHandle>` components.
|
|
4
|
+
@see https://w3c.github.io/aria/#separator
|
|
5
|
+
@see https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
6
|
+
-->
|
|
7
|
+
<script>
|
|
8
|
+
import { setContext } from 'svelte';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @import { Snippet } from 'svelte';
|
|
12
|
+
* @import { PaneGroupContext } from '../../typedefs.js';
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} Props
|
|
17
|
+
* @property {'horizontal' | 'vertical'} [direction] Layout direction of the panes.
|
|
18
|
+
* @property {string} [class] The `class` attribute on the wrapper element.
|
|
19
|
+
* @property {Snippet} [children] Primary slot content.
|
|
20
|
+
* @property {(detail: { sizes: number[] }) => void} [onResize] `resize` event handler, called
|
|
21
|
+
* whenever the pane sizes change.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @type {Props & Record<string, any>}
|
|
26
|
+
*/
|
|
27
|
+
let {
|
|
28
|
+
/* eslint-disable prefer-const */
|
|
29
|
+
direction = 'horizontal',
|
|
30
|
+
class: className,
|
|
31
|
+
children,
|
|
32
|
+
onResize,
|
|
33
|
+
...restProps
|
|
34
|
+
/* eslint-enable prefer-const */
|
|
35
|
+
} = $props();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `ResizablePane` definitions in registration order, populated synchronously by child
|
|
39
|
+
* `<ResizablePane>` components.
|
|
40
|
+
* @type {{ id: string, defaultSize: number | undefined, minSize: number, maxSize: number }[]}
|
|
41
|
+
*/
|
|
42
|
+
const _paneDefs = $state([]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Current pane sizes as percentages.
|
|
46
|
+
* @type {number[]}
|
|
47
|
+
*/
|
|
48
|
+
const sizes = $state([]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Per-handle saved sizes before collapse, for Enter key restore.
|
|
52
|
+
* @type {(number | undefined)[]}
|
|
53
|
+
*/
|
|
54
|
+
const _savedSizes = $state([]);
|
|
55
|
+
|
|
56
|
+
let _handleCount = 0;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initialize pane sizes from `defaultSize` props. Called from `onMount` once all panes have
|
|
60
|
+
* registered. Panes without `defaultSize` share the remaining space equally.
|
|
61
|
+
*/
|
|
62
|
+
const initSizes = () => {
|
|
63
|
+
if (!_paneDefs.length) return;
|
|
64
|
+
|
|
65
|
+
const totalSpecified = _paneDefs.reduce((sum, p) => sum + (p.defaultSize ?? 0), 0);
|
|
66
|
+
const unspecifiedCount = _paneDefs.filter((p) => p.defaultSize === undefined).length;
|
|
67
|
+
const remaining = Math.max(0, 100 - totalSpecified);
|
|
68
|
+
const defaultSize = unspecifiedCount > 0 ? remaining / unspecifiedCount : 0;
|
|
69
|
+
const newSizes = _paneDefs.map((p) => p.defaultSize ?? defaultSize);
|
|
70
|
+
|
|
71
|
+
sizes.splice(0, sizes.length, ...newSizes);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resize panes around a handle by the given delta (percentage points).
|
|
76
|
+
* @param {number} handleIndex Index of the resize handle.
|
|
77
|
+
* @param {number} deltaPercent Size delta in percentage points.
|
|
78
|
+
*/
|
|
79
|
+
const resize = (handleIndex, deltaPercent) => {
|
|
80
|
+
const beforeIdx = handleIndex;
|
|
81
|
+
const afterIdx = handleIndex + 1;
|
|
82
|
+
|
|
83
|
+
if (beforeIdx < 0 || afterIdx >= sizes.length) return;
|
|
84
|
+
|
|
85
|
+
const { minSize: minBefore = 0, maxSize: maxBefore = 100 } = _paneDefs[beforeIdx];
|
|
86
|
+
const { minSize: minAfter = 0, maxSize: maxAfter = 100 } = _paneDefs[afterIdx];
|
|
87
|
+
const prevBefore = sizes[beforeIdx];
|
|
88
|
+
const prevAfter = sizes[afterIdx];
|
|
89
|
+
// Clamp delta so neither pane exceeds its min/max constraints
|
|
90
|
+
const canGrow = Math.min(maxBefore - prevBefore, prevAfter - minAfter);
|
|
91
|
+
const canShrink = Math.min(prevBefore - minBefore, maxAfter - prevAfter);
|
|
92
|
+
|
|
93
|
+
const delta =
|
|
94
|
+
deltaPercent > 0 ? Math.min(deltaPercent, canGrow) : -Math.min(-deltaPercent, canShrink);
|
|
95
|
+
|
|
96
|
+
sizes[beforeIdx] = prevBefore + delta;
|
|
97
|
+
sizes[afterIdx] = prevAfter - delta;
|
|
98
|
+
|
|
99
|
+
onResize?.({ sizes: sizes.map((s) => Number(s.toFixed(1))) });
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Toggle collapse of the primary pane (before the given handle). If the pane is above its minimum
|
|
104
|
+
* size it is collapsed to `minSize`; if already at `minSize` the previous size is restored.
|
|
105
|
+
* @param {number} handleIndex Index of the resize handle.
|
|
106
|
+
*/
|
|
107
|
+
const toggleCollapse = (handleIndex) => {
|
|
108
|
+
const { minSize: minBefore = 0 } = _paneDefs[handleIndex];
|
|
109
|
+
|
|
110
|
+
if (_savedSizes[handleIndex] !== undefined) {
|
|
111
|
+
const delta = /** @type {number} */ (_savedSizes[handleIndex]) - sizes[handleIndex];
|
|
112
|
+
|
|
113
|
+
_savedSizes[handleIndex] = undefined;
|
|
114
|
+
resize(handleIndex, delta);
|
|
115
|
+
} else {
|
|
116
|
+
_savedSizes[handleIndex] = sizes[handleIndex];
|
|
117
|
+
resize(handleIndex, -(sizes[handleIndex] - minBefore));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
setContext(
|
|
122
|
+
'paneGroup',
|
|
123
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
124
|
+
/** @type {PaneGroupContext} */ ({
|
|
125
|
+
get direction() {
|
|
126
|
+
return direction;
|
|
127
|
+
},
|
|
128
|
+
sizes,
|
|
129
|
+
registerPane: ({ id, defaultSize, minSize, maxSize }) => {
|
|
130
|
+
const idx = _paneDefs.length;
|
|
131
|
+
|
|
132
|
+
_paneDefs.push({ id, defaultSize, minSize, maxSize });
|
|
133
|
+
|
|
134
|
+
return { index: idx };
|
|
135
|
+
},
|
|
136
|
+
registerHandle: () => {
|
|
137
|
+
const idx = _handleCount;
|
|
138
|
+
|
|
139
|
+
_handleCount += 1;
|
|
140
|
+
|
|
141
|
+
return idx;
|
|
142
|
+
},
|
|
143
|
+
resize,
|
|
144
|
+
toggleCollapse,
|
|
145
|
+
paneDefs: _paneDefs,
|
|
146
|
+
}),
|
|
147
|
+
/* eslint-enable jsdoc/require-jsdoc */
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
$effect(() => {
|
|
151
|
+
if (_paneDefs.length && !sizes.length) {
|
|
152
|
+
initSizes();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<div
|
|
158
|
+
{...restProps}
|
|
159
|
+
role="none"
|
|
160
|
+
class="sui resizable-pane-group {direction} {className ?? ''}"
|
|
161
|
+
data-direction={direction}
|
|
162
|
+
>
|
|
163
|
+
{@render children?.()}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<style>.resizable-pane-group {
|
|
167
|
+
display: flex;
|
|
168
|
+
overflow: hidden;
|
|
169
|
+
}
|
|
170
|
+
.resizable-pane-group.horizontal {
|
|
171
|
+
flex-direction: row;
|
|
172
|
+
width: 100%;
|
|
173
|
+
height: 100%;
|
|
174
|
+
}
|
|
175
|
+
.resizable-pane-group.vertical {
|
|
176
|
+
flex-direction: column;
|
|
177
|
+
width: 100%;
|
|
178
|
+
height: 100%;
|
|
179
|
+
}</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export default ResizablePaneGroup;
|
|
2
|
+
type ResizablePaneGroup = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<Props & Record<string, any>>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Container for resizable panes. Panes must be separated by `<ResizableHandle>` components.
|
|
8
|
+
* @see https://w3c.github.io/aria/#separator
|
|
9
|
+
* @see https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
|
|
10
|
+
*/
|
|
11
|
+
declare const ResizablePaneGroup: import("svelte").Component<{
|
|
12
|
+
/**
|
|
13
|
+
* Layout direction of the panes.
|
|
14
|
+
*/
|
|
15
|
+
direction?: "horizontal" | "vertical" | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The `class` attribute on the wrapper element.
|
|
18
|
+
*/
|
|
19
|
+
class?: string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Primary slot content.
|
|
22
|
+
*/
|
|
23
|
+
children?: Snippet<[]> | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* `resize` event handler, called
|
|
26
|
+
* whenever the pane sizes change.
|
|
27
|
+
*/
|
|
28
|
+
onResize?: ((detail: {
|
|
29
|
+
sizes: number[];
|
|
30
|
+
}) => void) | undefined;
|
|
31
|
+
} & Record<string, any>, {}, "">;
|
|
32
|
+
type Props = {
|
|
33
|
+
/**
|
|
34
|
+
* Layout direction of the panes.
|
|
35
|
+
*/
|
|
36
|
+
direction?: "horizontal" | "vertical" | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The `class` attribute on the wrapper element.
|
|
39
|
+
*/
|
|
40
|
+
class?: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Primary slot content.
|
|
43
|
+
*/
|
|
44
|
+
children?: Snippet<[]> | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* `resize` event handler, called
|
|
47
|
+
* whenever the pane sizes change.
|
|
48
|
+
*/
|
|
49
|
+
onResize?: ((detail: {
|
|
50
|
+
sizes: number[];
|
|
51
|
+
}) => void) | undefined;
|
|
52
|
+
};
|
|
53
|
+
import type { Snippet } from 'svelte';
|