@sveltia/ui 0.32.2 → 0.33.1

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.
@@ -0,0 +1,76 @@
1
+ <!--
2
+ @component
3
+ A resizable pane within a `<ResizablePaneGroup>`. Must be a direct child of
4
+ `<ResizablePaneGroup>`.
5
+ -->
6
+ <script>
7
+ import { getContext, untrack } from 'svelte';
8
+
9
+ /**
10
+ * @import { Snippet } from 'svelte';
11
+ * @import { PaneGroupContext } from '../../typedefs.js';
12
+ */
13
+
14
+ /**
15
+ * @typedef {object} Props
16
+ * @property {number} [defaultSize] Default size as a percentage (0–100). Panes without
17
+ * `defaultSize` share the remaining space equally.
18
+ * @property {number} [minSize] Minimum size as a percentage. Defaults to `0`.
19
+ * @property {number} [maxSize] Maximum size as a percentage. Defaults to `100`.
20
+ * @property {string} [class] The `class` attribute on the wrapper element.
21
+ * @property {Snippet} [children] Primary slot content.
22
+ * @property {(detail: { size: number }) => void} [onResize] Called whenever this pane’s size (in
23
+ * percent) changes.
24
+ */
25
+
26
+ /**
27
+ * @type {Props & Record<string, any>}
28
+ */
29
+ let {
30
+ /* eslint-disable prefer-const */
31
+ defaultSize = undefined,
32
+ minSize = 0,
33
+ maxSize = 100,
34
+ class: className,
35
+ children,
36
+ onResize,
37
+ ...restProps
38
+ /* eslint-enable prefer-const */
39
+ } = $props();
40
+
41
+ const id = $props.id();
42
+
43
+ /** @type {PaneGroupContext} */
44
+ const ctx = getContext('paneGroup');
45
+
46
+ if (!ctx) {
47
+ throw new Error('<ResizablePane> must be used inside a <ResizablePaneGroup>');
48
+ }
49
+
50
+ // svelte-ignore state_referenced_locally
51
+ const { index: paneIndex } = ctx.registerPane({ id, defaultSize, minSize, maxSize });
52
+
53
+ const direction = $derived(ctx.direction);
54
+ const size = $derived(ctx.sizes[paneIndex]);
55
+ const sizeStyle = $derived(size !== undefined ? size : (defaultSize ?? 0));
56
+
57
+ $effect(() => {
58
+ if (size !== undefined) {
59
+ untrack(() => onResize?.({ size: Number(size.toFixed(1)) }));
60
+ }
61
+ });
62
+ </script>
63
+
64
+ <div
65
+ {...restProps}
66
+ {id}
67
+ role="none"
68
+ class="sui resizable-pane {className ?? ''}"
69
+ style:flex-basis="{sizeStyle}%"
70
+ style:flex-grow="0"
71
+ style:flex-shrink="0"
72
+ style:overflow-x={direction === 'horizontal' ? 'auto' : undefined}
73
+ style:overflow-y={direction === 'vertical' ? 'auto' : undefined}
74
+ >
75
+ {@render children?.()}
76
+ </div>
@@ -0,0 +1,70 @@
1
+ export default ResizablePane;
2
+ type ResizablePane = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props & Record<string, any>>): void;
5
+ };
6
+ /**
7
+ * A resizable pane within a `<ResizablePaneGroup>`. Must be a direct child of
8
+ * `<ResizablePaneGroup>`.
9
+ */
10
+ declare const ResizablePane: import("svelte").Component<{
11
+ /**
12
+ * Default size as a percentage (0–100). Panes without
13
+ * `defaultSize` share the remaining space equally.
14
+ */
15
+ defaultSize?: number | undefined;
16
+ /**
17
+ * Minimum size as a percentage. Defaults to `0`.
18
+ */
19
+ minSize?: number | undefined;
20
+ /**
21
+ * Maximum size as a percentage. Defaults to `100`.
22
+ */
23
+ maxSize?: number | undefined;
24
+ /**
25
+ * The `class` attribute on the wrapper element.
26
+ */
27
+ class?: string | undefined;
28
+ /**
29
+ * Primary slot content.
30
+ */
31
+ children?: Snippet<[]> | undefined;
32
+ /**
33
+ * Called whenever this pane’s size (in
34
+ * percent) changes.
35
+ */
36
+ onResize?: ((detail: {
37
+ size: number;
38
+ }) => void) | undefined;
39
+ } & Record<string, any>, {}, "">;
40
+ type Props = {
41
+ /**
42
+ * Default size as a percentage (0–100). Panes without
43
+ * `defaultSize` share the remaining space equally.
44
+ */
45
+ defaultSize?: number | undefined;
46
+ /**
47
+ * Minimum size as a percentage. Defaults to `0`.
48
+ */
49
+ minSize?: number | undefined;
50
+ /**
51
+ * Maximum size as a percentage. Defaults to `100`.
52
+ */
53
+ maxSize?: number | undefined;
54
+ /**
55
+ * The `class` attribute on the wrapper element.
56
+ */
57
+ class?: string | undefined;
58
+ /**
59
+ * Primary slot content.
60
+ */
61
+ children?: Snippet<[]> | undefined;
62
+ /**
63
+ * Called whenever this pane’s size (in
64
+ * percent) changes.
65
+ */
66
+ onResize?: ((detail: {
67
+ size: number;
68
+ }) => void) | undefined;
69
+ };
70
+ import type { Snippet } from 'svelte';
@@ -1,5 +1,8 @@
1
1
  <script>
2
+ import { tick } from 'svelte';
2
3
  import { _ } from 'svelte-i18n';
4
+ import { flip } from 'svelte/animate';
5
+ import { isRTL } from '../../services/i18n.js';
3
6
  import Button from '../button/button.svelte';
4
7
  import Icon from '../icon/icon.svelte';
5
8
  import Option from '../listbox/option.svelte';
@@ -25,8 +28,12 @@
25
28
  * @property {boolean} [invalid] Whether to mark the widget invalid. An alias of the
26
29
  * `aria-invalid` attribute.
27
30
  * @property {Snippet} [children] Primary slot content.
28
- * @property {(event: CustomEvent) => void} [onAddValue] Custom `AddValue` event handler.
29
- * @property {(event: CustomEvent) => void} [onRemoveValue] Custom `RemoveValue` event handler.
31
+ * @property {(event: CustomEvent<{ value: string }>) => void} [onAddValue] Custom `AddValue`
32
+ * event handler.
33
+ * @property {(event: CustomEvent<{ value: string }>) => void} [onRemoveValue] Custom
34
+ * `RemoveValue` event handler.
35
+ * @property {(event: CustomEvent<{ values: string[] }>) => void} [onReorder] Custom `Reorder`
36
+ * event handler fired when the order of selected values changes.
30
37
  */
31
38
 
32
39
  /**
@@ -46,39 +53,184 @@
46
53
  children,
47
54
  onAddValue,
48
55
  onRemoveValue,
56
+ onReorder,
49
57
  ...restProps
50
58
  /* eslint-enable prefer-const */
51
59
  } = $props();
52
60
 
61
+ /** @type {Map<any, { label: string, value: any, searchValue?: string }>} */
62
+ const optionMap = $derived(new Map(options.map((o) => [o.value, o])));
63
+ const prevKey = $derived($isRTL ? 'ArrowRight' : 'ArrowLeft');
64
+ const nextKey = $derived($isRTL ? 'ArrowLeft' : 'ArrowRight');
65
+
66
+ /**
67
+ * Reference to the wrapper element.
68
+ * @type {HTMLElement | undefined}
69
+ */
70
+ let wrapperElement = $state();
71
+
53
72
  /**
54
73
  * @type {string | undefined}
55
74
  */
56
75
  let selectedValue = $state();
76
+
77
+ /**
78
+ * Index of the tag currently being dragged.
79
+ * @type {number | undefined}
80
+ */
81
+ let dragIndex = $state();
82
+
83
+ /**
84
+ * Insertion position during drag: the dragged item will be placed *before* this index (0 = before
85
+ * first, values.length = after last).
86
+ * @type {number | undefined}
87
+ */
88
+ let dropIndex = $state();
89
+
90
+ /**
91
+ * Move a selected value from one position to another and dispatch the `Reorder` event.
92
+ * @param {number} from Source index.
93
+ * @param {number} to Destination index.
94
+ */
95
+ const moveValue = (from, to) => {
96
+ if (from === to) return;
97
+
98
+ const newValues = [...values];
99
+ const [item] = newValues.splice(from, 1);
100
+
101
+ newValues.splice(to, 0, item);
102
+ values = newValues;
103
+ onReorder?.(new CustomEvent('Reorder', { detail: { values: newValues } }));
104
+ };
105
+
106
+ /**
107
+ * Move a value and focus the tag at the destination index.
108
+ * @param {number} from Source index.
109
+ * @param {number} to Destination index.
110
+ */
111
+ const moveAndFocus = async (from, to) => {
112
+ moveValue(from, to);
113
+ await tick();
114
+
115
+ /** @type {HTMLElement} */ (
116
+ wrapperElement?.querySelectorAll('.label[tabindex]')?.[to]
117
+ )?.focus();
118
+ };
57
119
  </script>
58
120
 
59
- <div role="none" class="sui select-tags {className}" class:disabled={disabled || readonly} {hidden}>
60
- {#each values as value}
61
- {@const option = options.find((o) => o.value === value)}
62
- {#if option}
63
- <span role="none">
64
- {option.label}
65
- <Button
66
- iconic
67
- size="small"
68
- disabled={disabled || readonly}
69
- aria-label={$_('remove_x', { values: { name: option.label } })}
70
- onclick={() => {
71
- values = values.filter((v) => v !== value);
72
- onRemoveValue?.(new CustomEvent('RemoveValue', { detail: { value } }));
121
+ <div
122
+ role="none"
123
+ class="sui select-tags {className}"
124
+ class:disabled={disabled || readonly}
125
+ {hidden}
126
+ bind:this={wrapperElement}
127
+ >
128
+ <span
129
+ role="listbox"
130
+ aria-multiselectable="true"
131
+ aria-label={$_('_sui.select_tags.selected_options')}
132
+ >
133
+ {#each values as value, index (value)}
134
+ {@const option = optionMap.get(value)}
135
+ {@const label = option?.label || option?.value || value}
136
+ <span
137
+ role="none"
138
+ draggable={!disabled && !readonly}
139
+ class:drag-source={dragIndex === index}
140
+ class:drop-before={dropIndex === index && dragIndex !== index && dragIndex !== index - 1}
141
+ class:drop-after={dropIndex === values.length &&
142
+ index === values.length - 1 &&
143
+ dragIndex !== values.length - 1}
144
+ ondragstart={(event) => {
145
+ dragIndex = index;
146
+
147
+ if (event.dataTransfer) {
148
+ event.dataTransfer.setData('text/plain', label);
149
+ event.dataTransfer.effectAllowed = 'move';
150
+ }
151
+ }}
152
+ ondragover={(event) => {
153
+ event.preventDefault();
154
+
155
+ if (event.dataTransfer) {
156
+ event.dataTransfer.dropEffect = 'move';
157
+ }
158
+
159
+ const rect = event.currentTarget.getBoundingClientRect();
160
+ const inFirstHalf = event.clientX < rect.left + rect.width / 2;
161
+
162
+ dropIndex = inFirstHalf !== $isRTL ? index : index + 1;
163
+ }}
164
+ ondrop={async (event) => {
165
+ event.preventDefault();
166
+
167
+ const fromIndex = dragIndex;
168
+ const toIndex = dropIndex;
169
+
170
+ dragIndex = undefined;
171
+ dropIndex = undefined;
172
+
173
+ if (
174
+ fromIndex !== undefined &&
175
+ toIndex !== undefined &&
176
+ toIndex !== fromIndex &&
177
+ toIndex !== fromIndex + 1
178
+ ) {
179
+ await moveAndFocus(fromIndex, toIndex > fromIndex ? toIndex - 1 : toIndex);
180
+ }
181
+ }}
182
+ ondragend={() => {
183
+ dragIndex = undefined;
184
+ dropIndex = undefined;
185
+ }}
186
+ animate:flip={{ duration: 200 }}
187
+ >
188
+ <span
189
+ class="label"
190
+ role="option"
191
+ aria-selected="true"
192
+ tabindex={disabled || readonly ? undefined : 0}
193
+ onkeydown={async (event) => {
194
+ const { key } = event;
195
+
196
+ const targetIndex =
197
+ key === prevKey && index > 0
198
+ ? index - 1
199
+ : key === nextKey && index < values.length - 1
200
+ ? index + 1
201
+ : key === 'Home' && index > 0
202
+ ? 0
203
+ : key === 'End' && index < values.length - 1
204
+ ? values.length - 1
205
+ : -1;
206
+
207
+ if (targetIndex === -1) return;
208
+
209
+ event.preventDefault();
210
+ await moveAndFocus(index, targetIndex);
73
211
  }}
74
212
  >
75
- {#snippet startIcon()}
76
- <Icon name="close" />
77
- {/snippet}
78
- </Button>
213
+ {label}
214
+ </span>
215
+ {#if option}
216
+ <Button
217
+ iconic
218
+ size="small"
219
+ disabled={disabled || readonly}
220
+ aria-label={$_('_sui.select_tags.remove_x', { values: { name: label } })}
221
+ onclick={() => {
222
+ values = values.filter((v) => v !== value);
223
+ onRemoveValue?.(new CustomEvent('RemoveValue', { detail: { value } }));
224
+ }}
225
+ >
226
+ {#snippet startIcon()}
227
+ <Icon name="close" />
228
+ {/snippet}
229
+ </Button>
230
+ {/if}
79
231
  </span>
80
- {/if}
81
- {/each}
232
+ {/each}
233
+ </span>
82
234
  {#if (typeof max !== 'number' || values.length < max) && values.length < options.length}
83
235
  <Select
84
236
  {...restProps}
@@ -116,15 +268,49 @@
116
268
  .select-tags.disabled > * {
117
269
  opacity: 0.5;
118
270
  }
119
- .select-tags span {
271
+ .select-tags span[role=listbox] {
272
+ display: contents;
273
+ }
274
+ .select-tags span[draggable] {
120
275
  display: inline-flex;
121
276
  align-items: center;
277
+ position: relative;
122
278
  margin: var(--sui-focus-ring-width);
123
279
  padding: 0;
124
280
  padding-inline-start: 8px;
125
281
  border-radius: var(--sui-control-medium-border-radius);
126
282
  background-color: var(--sui-secondary-background-color);
283
+ cursor: grab;
284
+ outline: none;
285
+ }
286
+ .select-tags span[draggable]:focus-within {
287
+ outline: var(--sui-focus-ring-width) solid var(--sui-primary-accent-color-translucent);
288
+ outline-offset: 1px;
289
+ }
290
+ .select-tags span[draggable].drag-source {
291
+ opacity: 0.4;
292
+ cursor: grabbing;
293
+ }
294
+ .select-tags span[draggable].drop-before::before, .select-tags span[draggable].drop-after::after {
295
+ content: "";
296
+ position: absolute;
297
+ top: 0;
298
+ bottom: 0;
299
+ margin-left: -1px;
300
+ border-radius: 1px;
301
+ width: 4px;
302
+ background-color: var(--sui-primary-accent-color);
303
+ pointer-events: none;
304
+ }
305
+ .select-tags span[draggable].drop-before::before {
306
+ inset-inline-start: calc(-1 * var(--sui-focus-ring-width) - 1px);
307
+ }
308
+ .select-tags span[draggable].drop-after::after {
309
+ inset-inline-end: calc(-1 * var(--sui-focus-ring-width) - 1px);
310
+ }
311
+ .select-tags span[draggable] .label {
312
+ outline: none;
127
313
  }
128
- .select-tags span :global(.icon) {
314
+ .select-tags span[draggable] :global(.icon) {
129
315
  font-size: var(--sui-font-size-large);
130
316
  }</style>
@@ -53,13 +53,26 @@ declare const SelectTags: import("svelte").Component<{
53
53
  */
54
54
  children?: Snippet<[]> | undefined;
55
55
  /**
56
- * Custom `AddValue` event handler.
56
+ * Custom `AddValue`
57
+ * event handler.
57
58
  */
58
- onAddValue?: ((event: CustomEvent) => void) | undefined;
59
+ onAddValue?: ((event: CustomEvent<{
60
+ value: string;
61
+ }>) => void) | undefined;
59
62
  /**
60
- * Custom `RemoveValue` event handler.
63
+ * Custom
64
+ * `RemoveValue` event handler.
61
65
  */
62
- onRemoveValue?: ((event: CustomEvent) => void) | undefined;
66
+ onRemoveValue?: ((event: CustomEvent<{
67
+ value: string;
68
+ }>) => void) | undefined;
69
+ /**
70
+ * Custom `Reorder`
71
+ * event handler fired when the order of selected values changes.
72
+ */
73
+ onReorder?: ((event: CustomEvent<{
74
+ values: string[];
75
+ }>) => void) | undefined;
63
76
  } & Record<string, any>, {}, "values">;
64
77
  type Props = {
65
78
  /**
@@ -111,12 +124,25 @@ type Props = {
111
124
  */
112
125
  children?: Snippet<[]> | undefined;
113
126
  /**
114
- * Custom `AddValue` event handler.
127
+ * Custom `AddValue`
128
+ * event handler.
129
+ */
130
+ onAddValue?: ((event: CustomEvent<{
131
+ value: string;
132
+ }>) => void) | undefined;
133
+ /**
134
+ * Custom
135
+ * `RemoveValue` event handler.
115
136
  */
116
- onAddValue?: ((event: CustomEvent) => void) | undefined;
137
+ onRemoveValue?: ((event: CustomEvent<{
138
+ value: string;
139
+ }>) => void) | undefined;
117
140
  /**
118
- * Custom `RemoveValue` event handler.
141
+ * Custom `Reorder`
142
+ * event handler fired when the order of selected values changes.
119
143
  */
120
- onRemoveValue?: ((event: CustomEvent) => void) | undefined;
144
+ onReorder?: ((event: CustomEvent<{
145
+ values: string[];
146
+ }>) => void) | undefined;
121
147
  };
122
148
  import type { Snippet } from 'svelte';
@@ -16,7 +16,7 @@ declare const TabBox: import("svelte").Component<{
16
16
  * Orientation of the widget. This is
17
17
  * typically contrary to `<TabList>`’s `orientation`.
18
18
  */
19
- orientation?: "vertical" | "horizontal" | undefined;
19
+ orientation?: "horizontal" | "vertical" | undefined;
20
20
  /**
21
21
  * Primary slot content.
22
22
  */
@@ -31,7 +31,7 @@ type Props = {
31
31
  * Orientation of the widget. This is
32
32
  * typically contrary to `<TabList>`’s `orientation`.
33
33
  */
34
- orientation?: "vertical" | "horizontal" | undefined;
34
+ orientation?: "horizontal" | "vertical" | undefined;
35
35
  /**
36
36
  * Primary slot content.
37
37
  */
@@ -26,7 +26,7 @@ declare const TabList: import("svelte").Component<{
26
26
  * Orientation of the widget. An alias of the
27
27
  * `aria-orientation` attribute.
28
28
  */
29
- orientation?: "vertical" | "horizontal" | undefined;
29
+ orientation?: "horizontal" | "vertical" | undefined;
30
30
  /**
31
31
  * The `data-name` attribute on the wrapper element.
32
32
  */
@@ -58,7 +58,7 @@ type Props = {
58
58
  * Orientation of the widget. An alias of the
59
59
  * `aria-orientation` attribute.
60
60
  */
61
- orientation?: "vertical" | "horizontal" | undefined;
61
+ orientation?: "horizontal" | "vertical" | undefined;
62
62
  /**
63
63
  * The `data-name` attribute on the wrapper element.
64
64
  */
@@ -26,7 +26,7 @@ declare const Toolbar: import("svelte").Component<{
26
26
  * Orientation of the widget. An alias of the
27
27
  * `aria-orientation` attribute.
28
28
  */
29
- orientation?: "vertical" | "horizontal" | undefined;
29
+ orientation?: "horizontal" | "vertical" | undefined;
30
30
  /**
31
31
  * The style variant of the toolbar.
32
32
  */
@@ -54,7 +54,7 @@ type Props = {
54
54
  * Orientation of the widget. An alias of the
55
55
  * `aria-orientation` attribute.
56
56
  */
57
- orientation?: "vertical" | "horizontal" | undefined;
57
+ orientation?: "horizontal" | "vertical" | undefined;
58
58
  /**
59
59
  * The style variant of the toolbar.
60
60
  */
@@ -12,7 +12,7 @@ declare const AppShell: import("svelte").Component<{
12
12
  /**
13
13
  * Orientation of the app shell’s children.
14
14
  */
15
- orientation?: "vertical" | "horizontal" | undefined;
15
+ orientation?: "horizontal" | "vertical" | undefined;
16
16
  /**
17
17
  * Primary slot content.
18
18
  */
@@ -22,7 +22,7 @@ type Props = {
22
22
  /**
23
23
  * Orientation of the app shell’s children.
24
24
  */
25
- orientation?: "vertical" | "horizontal" | undefined;
25
+ orientation?: "horizontal" | "vertical" | undefined;
26
26
  /**
27
27
  * Primary slot content.
28
28
  */
package/dist/index.d.ts CHANGED
@@ -40,6 +40,9 @@ export { default as Menu } from "./components/menu/menu.svelte";
40
40
  export { default as Progressbar } from "./components/progressbar/progressbar.svelte";
41
41
  export { default as RadioGroup } from "./components/radio/radio-group.svelte";
42
42
  export { default as Radio } from "./components/radio/radio.svelte";
43
+ export { default as ResizableHandle } from "./components/resizable-pane/resizable-handle.svelte";
44
+ export { default as ResizablePaneGroup } from "./components/resizable-pane/resizable-pane-group.svelte";
45
+ export { default as ResizablePane } from "./components/resizable-pane/resizable-pane.svelte";
43
46
  export { default as InfiniteScroll } from "./components/scroll/infinite-scroll.svelte";
44
47
  export { default as Combobox } from "./components/select/combobox.svelte";
45
48
  export { default as SelectTags } from "./components/select/select-tags.svelte";
package/dist/index.js CHANGED
@@ -44,6 +44,9 @@ export { default as Menu } from './components/menu/menu.svelte';
44
44
  export { default as Progressbar } from './components/progressbar/progressbar.svelte';
45
45
  export { default as RadioGroup } from './components/radio/radio-group.svelte';
46
46
  export { default as Radio } from './components/radio/radio.svelte';
47
+ export { default as ResizableHandle } from './components/resizable-pane/resizable-handle.svelte';
48
+ export { default as ResizablePaneGroup } from './components/resizable-pane/resizable-pane-group.svelte';
49
+ export { default as ResizablePane } from './components/resizable-pane/resizable-pane.svelte';
47
50
  export { default as InfiniteScroll } from './components/scroll/infinite-scroll.svelte';
48
51
  export { default as Combobox } from './components/select/combobox.svelte';
49
52
  export { default as SelectTags } from './components/select/select-tags.svelte';
@@ -35,6 +35,10 @@ export namespace strings {
35
35
  let show_password: string;
36
36
  let hide_password: string;
37
37
  }
38
+ namespace select_tags {
39
+ let selected_options: string;
40
+ let remove_x: string;
41
+ }
38
42
  namespace text_editor {
39
43
  let text_editor_1: string;
40
44
  export { text_editor_1 as text_editor };
@@ -35,6 +35,10 @@ export const strings = {
35
35
  show_password: 'Show Password',
36
36
  hide_password: 'Hide Password',
37
37
  },
38
+ select_tags: {
39
+ selected_options: 'Selected options',
40
+ remove_x: 'Remove {name}',
41
+ },
38
42
  text_editor: {
39
43
  text_editor: 'Text Editor',
40
44
  code_editor: 'Code Editor',
@@ -35,6 +35,10 @@ export namespace strings {
35
35
  let show_password: string;
36
36
  let hide_password: string;
37
37
  }
38
+ namespace select_tags {
39
+ let selected_options: string;
40
+ let remove_x: string;
41
+ }
38
42
  namespace text_editor {
39
43
  let text_editor_1: string;
40
44
  export { text_editor_1 as text_editor };
@@ -35,6 +35,10 @@ export const strings = {
35
35
  show_password: 'パスワードを表示',
36
36
  hide_password: 'パスワードを隠す',
37
37
  },
38
+ select_tags: {
39
+ selected_options: '選択済みのオプション',
40
+ remove_x: '{name} を削除',
41
+ },
38
42
  text_editor: {
39
43
  text_editor: 'テキストエディター',
40
44
  code_editor: 'コードエディター',