@sveltia/ui 0.25.15 → 0.26.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.
Files changed (33) hide show
  1. package/dist/components/bottom-navigation/bottom-navigation.svelte +44 -0
  2. package/dist/components/bottom-navigation/bottom-navigation.svelte.d.ts +54 -0
  3. package/dist/components/button/button-group.svelte +1 -0
  4. package/dist/components/button/button.svelte +10 -3
  5. package/dist/components/button/floating-action-button-wrapper.svelte +41 -0
  6. package/dist/components/button/floating-action-button-wrapper.svelte.d.ts +18 -0
  7. package/dist/components/button/select-button-group.svelte +1 -0
  8. package/dist/components/button/split-button.svelte +1 -0
  9. package/dist/components/dialog/dialog.svelte +1 -0
  10. package/dist/components/drawer/drawer.svelte +1 -0
  11. package/dist/components/file/file-picker.svelte +48 -0
  12. package/dist/components/file/file-picker.svelte.d.ts +53 -0
  13. package/dist/components/listbox/option-group.svelte +7 -1
  14. package/dist/components/listbox/option.svelte +0 -3
  15. package/dist/components/menu/menu-button.svelte +28 -30
  16. package/dist/components/scroll/infinite-scroll.svelte +65 -0
  17. package/dist/components/scroll/infinite-scroll.svelte.d.ts +46 -0
  18. package/dist/components/select/combobox.svelte +4 -6
  19. package/dist/components/text-field/text-input.svelte +4 -2
  20. package/dist/components/toast/toast.svelte +28 -5
  21. package/dist/components/toolbar/toolbar.svelte +6 -7
  22. package/dist/components/typography/truncated-text.svelte +35 -0
  23. package/dist/components/typography/truncated-text.svelte.d.ts +26 -0
  24. package/dist/components/util/app-shell.svelte +5 -4
  25. package/dist/components/util/empty-state.svelte +33 -0
  26. package/dist/components/util/empty-state.svelte.d.ts +18 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +6 -0
  29. package/dist/services/events.svelte.js +2 -2
  30. package/dist/styles/variables.scss +5 -4
  31. package/dist/typedefs.d.ts +6 -2
  32. package/dist/typedefs.js +2 -1
  33. package/package.json +5 -5
@@ -0,0 +1,44 @@
1
+ <script>
2
+ /**
3
+ * @import { Snippet } from 'svelte';
4
+ */
5
+
6
+ /**
7
+ * @typedef {object} Props
8
+ * @property {string} [class] The `class` attribute on the wrapper element.
9
+ * @property {boolean} [hidden] Whether to hide the widget. An alias of the `aria-hidden`
10
+ * attribute.
11
+ * @property {boolean} [disabled] Whether to disable the widget. An alias of the `aria-disabled`
12
+ * attribute.
13
+ * @property {string} [title] Text label displayed above the group items.
14
+ * @property {Snippet} [children] Primary slot content.
15
+ */
16
+
17
+ /**
18
+ * @type {Props & Record<string, any>}
19
+ */
20
+ let {
21
+ /* eslint-disable prefer-const */
22
+ class: className,
23
+ children,
24
+ ...rest
25
+ /* eslint-enable prefer-const */
26
+ } = $props();
27
+ </script>
28
+
29
+ <div role="none" class="sui bottom-navigation {className}" {...rest}>
30
+ {@render children?.()}
31
+ </div>
32
+
33
+ <style>.bottom-navigation {
34
+ height: var(--sui-bottom-navigation-height, var(--sui-primary-toolbar-size));
35
+ border-top-width: var(--sui-bottom-navigation-border-color, 1px);
36
+ border-top-style: var(--sui-bottom-navigation-border-style, solid);
37
+ border-top-color: var(--sui-bottom-navigation-border-color, var(--sui-secondary-border-color));
38
+ }
39
+ .bottom-navigation :global(.buttons) {
40
+ flex: auto;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-evenly;
44
+ }</style>
@@ -0,0 +1,54 @@
1
+ export default BottomNavigation;
2
+ type BottomNavigation = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props & Record<string, any>>): void;
5
+ };
6
+ declare const BottomNavigation: import("svelte").Component<{
7
+ /**
8
+ * The `class` attribute on the wrapper element.
9
+ */
10
+ class?: string | undefined;
11
+ /**
12
+ * Whether to hide the widget. An alias of the `aria-hidden`
13
+ * attribute.
14
+ */
15
+ hidden?: boolean | undefined;
16
+ /**
17
+ * Whether to disable the widget. An alias of the `aria-disabled`
18
+ * attribute.
19
+ */
20
+ disabled?: boolean | undefined;
21
+ /**
22
+ * Text label displayed above the group items.
23
+ */
24
+ title?: string | undefined;
25
+ /**
26
+ * Primary slot content.
27
+ */
28
+ children?: Snippet<[]> | undefined;
29
+ } & Record<string, any>, {}, "">;
30
+ type Props = {
31
+ /**
32
+ * The `class` attribute on the wrapper element.
33
+ */
34
+ class?: string | undefined;
35
+ /**
36
+ * Whether to hide the widget. An alias of the `aria-hidden`
37
+ * attribute.
38
+ */
39
+ hidden?: boolean | undefined;
40
+ /**
41
+ * Whether to disable the widget. An alias of the `aria-disabled`
42
+ * attribute.
43
+ */
44
+ disabled?: boolean | undefined;
45
+ /**
46
+ * Text label displayed above the group items.
47
+ */
48
+ title?: string | undefined;
49
+ /**
50
+ * Primary slot content.
51
+ */
52
+ children?: Snippet<[]> | undefined;
53
+ };
54
+ import type { Snippet } from 'svelte';
@@ -26,6 +26,7 @@
26
26
  </div>
27
27
 
28
28
  <style>.button-group {
29
+ flex: none;
29
30
  display: inline-flex;
30
31
  align-items: center;
31
32
  }</style>
@@ -6,6 +6,7 @@
6
6
  -->
7
7
  <script>
8
8
  import { activateKeyShortcuts } from '../../services/events.svelte';
9
+ import TruncatedText from '../typography/truncated-text.svelte';
9
10
  import Popup from '../util/popup.svelte';
10
11
 
11
12
  /**
@@ -29,6 +30,7 @@
29
30
  pressed = undefined,
30
31
  keyShortcuts = undefined,
31
32
  label = '',
33
+ lines = 1,
32
34
  variant = undefined,
33
35
  size = 'medium',
34
36
  iconic = false,
@@ -68,7 +70,9 @@
68
70
  {#if variant === 'link'}
69
71
  {#if label}
70
72
  <span role="none" class="label">
71
- {label}
73
+ <TruncatedText {lines}>
74
+ {label}
75
+ </TruncatedText>
72
76
  </span>
73
77
  {:else}
74
78
  <span role="none" class="label">
@@ -78,7 +82,9 @@
78
82
  {:else}
79
83
  {#if label}
80
84
  <span role="none" class="label">
81
- {label}
85
+ <TruncatedText {lines}>
86
+ {label}
87
+ </TruncatedText>
82
88
  </span>
83
89
  {/if}
84
90
  {@render children?.()}
@@ -98,6 +104,7 @@
98
104
  {/if}
99
105
 
100
106
  <style>button {
107
+ flex: none;
101
108
  display: inline-flex;
102
109
  align-items: center;
103
110
  gap: 4px;
@@ -114,7 +121,6 @@
114
121
  line-height: var(--sui-control-line-height);
115
122
  font-weight: var(--sui-font-weight-normal, normal);
116
123
  text-align: start;
117
- white-space: nowrap;
118
124
  cursor: pointer;
119
125
  transition: all 200ms;
120
126
  }
@@ -252,6 +258,7 @@ button:global(.pill) {
252
258
  padding: var(--sui-button-medium-pill-padding, 0 12px);
253
259
  }
254
260
  button:global(.flex) {
261
+ flex: auto;
255
262
  width: -moz-available;
256
263
  width: -webkit-fill-available;
257
264
  width: stretch;
@@ -0,0 +1,41 @@
1
+ <script>
2
+ /**
3
+ * @import { Snippet } from 'svelte';
4
+ */
5
+
6
+ /**
7
+ * @typedef {object} Props
8
+ * @property {Snippet} [children] Slot content.
9
+ */
10
+
11
+ /** @type {Props} */
12
+ let {
13
+ /* eslint-disable prefer-const */
14
+ children = undefined,
15
+ /* eslint-enable prefer-const */
16
+ } = $props();
17
+ </script>
18
+
19
+ <div role="none" class="sui floating-action-button-wrapper">
20
+ {@render children?.()}
21
+ </div>
22
+
23
+ <style>.floating-action-button-wrapper {
24
+ display: contents;
25
+ }
26
+ @media (width < 768px) {
27
+ .floating-action-button-wrapper {
28
+ display: block;
29
+ position: fixed;
30
+ inset: auto 16px 72px auto;
31
+ z-index: 100;
32
+ }
33
+ .floating-action-button-wrapper :global(button) {
34
+ border-radius: 50%;
35
+ height: 56px;
36
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
37
+ }
38
+ .floating-action-button-wrapper :global(button) :global(.icon) {
39
+ font-size: 32px;
40
+ }
41
+ }</style>
@@ -0,0 +1,18 @@
1
+ export default FloatingActionButtonWrapper;
2
+ type FloatingActionButtonWrapper = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props>): void;
5
+ };
6
+ declare const FloatingActionButtonWrapper: import("svelte").Component<{
7
+ /**
8
+ * Slot content.
9
+ */
10
+ children?: Snippet<[]> | undefined;
11
+ }, {}, "">;
12
+ type Props = {
13
+ /**
14
+ * Slot content.
15
+ */
16
+ children?: Snippet<[]> | undefined;
17
+ };
18
+ import type { Snippet } from 'svelte';
@@ -63,6 +63,7 @@
63
63
  </div>
64
64
 
65
65
  <style>.select-button-group {
66
+ flex: none;
66
67
  display: inline-flex;
67
68
  align-items: center;
68
69
  margin: var(--sui-focus-ring-width);
@@ -83,6 +83,7 @@
83
83
  </div>
84
84
 
85
85
  <style>.split-button {
86
+ flex: none;
86
87
  display: inline-flex;
87
88
  margin: var(--sui-focus-ring-width);
88
89
  }
@@ -229,6 +229,7 @@
229
229
  }
230
230
  .header .title {
231
231
  font-size: var(--sui-font-size-large);
232
+ font-weight: var(--sui-font-weight-bold);
232
233
  }
233
234
 
234
235
  .footer {
@@ -290,6 +290,7 @@
290
290
  }
291
291
  .header .title {
292
292
  font-size: var(--sui-font-size-large);
293
+ font-weight: var(--sui-font-weight-bold);
293
294
  }
294
295
 
295
296
  .footer {
@@ -0,0 +1,48 @@
1
+ <script>
2
+ /**
3
+ * @typedef {object} Props
4
+ * @property {string} [accept] The `accept` attribute for the `<input type="file">`.
5
+ * @property {boolean} [multiple] Whether to accept multiple files.
6
+ * @property {(detail: { files: File[], file: File }) => void} [onSelect] Custom `select` event
7
+ * handler. Since `multiple` could be false, we pass both `file` and `files` with the arguments.
8
+ * @property {() => void} [onCancel] `cancel` event handler.
9
+ */
10
+
11
+ /** @type {Props} */
12
+ let {
13
+ /* eslint-disable prefer-const */
14
+ accept = undefined,
15
+ multiple = false,
16
+ onSelect = undefined,
17
+ onCancel = undefined,
18
+ /* eslint-enable prefer-const */
19
+ } = $props();
20
+
21
+ /** @type {HTMLInputElement | undefined} */
22
+ let filePicker = $state();
23
+
24
+ /**
25
+ * Show the browser’s file picker dialog.
26
+ */
27
+ export const open = () => {
28
+ filePicker?.click();
29
+ };
30
+ </script>
31
+
32
+ <input
33
+ class="sui file-picker"
34
+ type="file"
35
+ hidden
36
+ {accept}
37
+ {multiple}
38
+ bind:this={filePicker}
39
+ onchange={({ target }) => {
40
+ const files = [.../** @type {FileList} */ (/** @type {HTMLInputElement} */ (target).files)];
41
+
42
+ onSelect?.({ files, file: files[0] });
43
+ }}
44
+ oncancel={(event) => {
45
+ event.stopPropagation();
46
+ onCancel?.();
47
+ }}
48
+ />
@@ -0,0 +1,53 @@
1
+ export default FilePicker;
2
+ type FilePicker = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props>): void;
5
+ } & {
6
+ open: () => void;
7
+ };
8
+ declare const FilePicker: import("svelte").Component<{
9
+ /**
10
+ * The `accept` attribute for the `<input type="file">`.
11
+ */
12
+ accept?: string | undefined;
13
+ /**
14
+ * Whether to accept multiple files.
15
+ */
16
+ multiple?: boolean | undefined;
17
+ /**
18
+ * Custom `select` event
19
+ * handler. Since `multiple` could be false, we pass both `file` and `files` with the arguments.
20
+ */
21
+ onSelect?: ((detail: {
22
+ files: File[];
23
+ file: File;
24
+ }) => void) | undefined;
25
+ /**
26
+ * `cancel` event handler.
27
+ */
28
+ onCancel?: (() => void) | undefined;
29
+ }, {
30
+ open: () => void;
31
+ }, "">;
32
+ type Props = {
33
+ /**
34
+ * The `accept` attribute for the `<input type="file">`.
35
+ */
36
+ accept?: string | undefined;
37
+ /**
38
+ * Whether to accept multiple files.
39
+ */
40
+ multiple?: boolean | undefined;
41
+ /**
42
+ * Custom `select` event
43
+ * handler. Since `multiple` could be false, we pass both `file` and `files` with the arguments.
44
+ */
45
+ onSelect?: ((detail: {
46
+ files: File[];
47
+ file: File;
48
+ }) => void) | undefined;
49
+ /**
50
+ * `cancel` event handler.
51
+ */
52
+ onCancel?: (() => void) | undefined;
53
+ };
@@ -7,6 +7,8 @@
7
7
  @see https://www.w3.org/WAI/ARIA/apg/patterns/listbox/examples/listbox-grouped/
8
8
  -->
9
9
  <script>
10
+ import TruncatedText from '../typography/truncated-text.svelte';
11
+
10
12
  /**
11
13
  * @import { Snippet } from 'svelte';
12
14
  */
@@ -50,7 +52,11 @@
50
52
  aria-labelledby="{id}-label"
51
53
  aria-roledescription="option group"
52
54
  >
53
- <div role="none" id="{id}-label" class="label">{label}</div>
55
+ <div role="none" id="{id}-label" class="label">
56
+ <TruncatedText>
57
+ {label}
58
+ </TruncatedText>
59
+ </div>
54
60
  <div role="none" class="inner" inert={disabled}>
55
61
  {@render children?.()}
56
62
  </div>
@@ -100,9 +100,6 @@
100
100
  width: 100%;
101
101
  height: auto;
102
102
  min-height: var(--sui-option-height);
103
- overflow: hidden;
104
- text-overflow: ellipsis;
105
- white-space: nowrap;
106
103
  }
107
104
  .option :global(button) :global(*) {
108
105
  flex: none;
@@ -55,36 +55,34 @@
55
55
  };
56
56
  </script>
57
57
 
58
- <div role="none" class="wrapper">
59
- <Button
60
- {...restProps}
61
- bind:element={buttonElement}
62
- class="sui menu-button {className}"
63
- {hidden}
64
- {disabled}
65
- {label}
66
- {variant}
67
- {size}
68
- {iconic}
69
- aria-haspopup="menu"
70
- >
71
- {#snippet startIcon()}
72
- {@render _startIcon?.()}
73
- {/snippet}
74
- {#snippet children()}
75
- {@render _children?.()}
76
- {/snippet}
77
- {#snippet endIcon()}
78
- {#if _endIcon}
79
- {@render _endIcon()}
80
- {:else if iconic}
81
- <Icon name="more_vert" />
82
- {:else}
83
- <Icon name="arrow_drop_down" class="small-arrow" />
84
- {/if}
85
- {/snippet}
86
- </Button>
87
- </div>
58
+ <Button
59
+ {...restProps}
60
+ bind:element={buttonElement}
61
+ class="sui menu-button {className}"
62
+ {hidden}
63
+ {disabled}
64
+ {label}
65
+ {variant}
66
+ {size}
67
+ {iconic}
68
+ aria-haspopup="menu"
69
+ >
70
+ {#snippet startIcon()}
71
+ {@render _startIcon?.()}
72
+ {/snippet}
73
+ {#snippet children()}
74
+ {@render _children?.()}
75
+ {/snippet}
76
+ {#snippet endIcon()}
77
+ {#if _endIcon}
78
+ {@render _endIcon()}
79
+ {:else if iconic}
80
+ <Icon name="more_vert" />
81
+ {:else}
82
+ <Icon name="arrow_drop_down" class="small-arrow" />
83
+ {/if}
84
+ {/snippet}
85
+ </Button>
88
86
 
89
87
  <Popup
90
88
  anchor={buttonElement}
@@ -0,0 +1,65 @@
1
+ <!--
2
+ @component Enable infinite scroll for list items for better rendering performance.
3
+ @see https://svelte.dev/docs/svelte/v5-migration-guide#Snippets-instead-of-slots-Passing-data-back-up
4
+ -->
5
+ <script>
6
+ /**
7
+ * @import { Snippet } from 'svelte';
8
+ */
9
+
10
+ /**
11
+ * @typedef {object} Props
12
+ * @property {any[]} items Item list.
13
+ * @property {string} itemKey Item key used for the `each` loop.
14
+ * @property {number} [itemChunkSize] Number of items to be loaded at once.
15
+ * @property {Snippet<[any]>} renderItem Snippet to render each item.
16
+ */
17
+
18
+ /** @type {Props} */
19
+ let {
20
+ /* eslint-disable prefer-const */
21
+ items,
22
+ itemKey,
23
+ itemChunkSize = 25,
24
+ renderItem,
25
+ /* eslint-enable prefer-const */
26
+ } = $props();
27
+
28
+ /** @type {number} */
29
+ let loadedItemSize = $state(itemChunkSize);
30
+
31
+ /** @type {HTMLElement | undefined} */
32
+ let spinner = $state(undefined);
33
+
34
+ const loading = $derived(items.length > loadedItemSize);
35
+
36
+ const observer = new IntersectionObserver(([{ isIntersecting }]) => {
37
+ if (isIntersecting) {
38
+ if (loading) {
39
+ loadedItemSize += itemChunkSize;
40
+ } else {
41
+ observer.disconnect();
42
+ }
43
+ }
44
+ });
45
+
46
+ $effect(() => {
47
+ if (spinner) {
48
+ observer.observe(spinner);
49
+ }
50
+ });
51
+ </script>
52
+
53
+ {#each items.slice(0, loadedItemSize) as item (item[itemKey])}
54
+ {@render renderItem(item)}
55
+ {/each}
56
+
57
+ {#if loading}
58
+ <div role="none" class="spinner" bind:this={spinner}></div>
59
+ {/if}
60
+
61
+ <style>
62
+ .spinner {
63
+ height: 1px;
64
+ }
65
+ </style>
@@ -0,0 +1,46 @@
1
+ export default InfiniteScroll;
2
+ type InfiniteScroll = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props>): void;
5
+ };
6
+ /**
7
+ * Enable infinite scroll for list items for better rendering performance.
8
+ * @see https://svelte.dev/docs/svelte/v5-migration-guide#Snippets-instead-of-slots-Passing-data-back-up
9
+ */
10
+ declare const InfiniteScroll: import("svelte").Component<{
11
+ /**
12
+ * Item list.
13
+ */
14
+ items: any[];
15
+ /**
16
+ * Item key used for the `each` loop.
17
+ */
18
+ itemKey: string;
19
+ /**
20
+ * Number of items to be loaded at once.
21
+ */
22
+ itemChunkSize?: number | undefined;
23
+ /**
24
+ * Snippet to render each item.
25
+ */
26
+ renderItem: Snippet<[any]>;
27
+ }, {}, "">;
28
+ type Props = {
29
+ /**
30
+ * Item list.
31
+ */
32
+ items: any[];
33
+ /**
34
+ * Item key used for the `each` loop.
35
+ */
36
+ itemKey: string;
37
+ /**
38
+ * Number of items to be loaded at once.
39
+ */
40
+ itemChunkSize?: number | undefined;
41
+ /**
42
+ * Snippet to render each item.
43
+ */
44
+ renderItem: Snippet<[any]>;
45
+ };
46
+ import type { Snippet } from 'svelte';
@@ -12,6 +12,7 @@
12
12
  import Listbox from '../listbox/listbox.svelte';
13
13
  import SearchBar from '../text-field/search-bar.svelte';
14
14
  import TextInput from '../text-field/text-input.svelte';
15
+ import TruncatedText from '../typography/truncated-text.svelte';
15
16
  import Popup from '../util/popup.svelte';
16
17
 
17
18
  /**
@@ -137,7 +138,9 @@
137
138
  aria-haspopup="listbox"
138
139
  >
139
140
  <div role="none" class="label">
140
- {value !== undefined ? label : $_('_sui.combobox.select_an_option')}
141
+ <TruncatedText>
142
+ {value !== undefined ? label : $_('_sui.combobox.select_an_option')}
143
+ </TruncatedText>
141
144
  </div>
142
145
  </div>
143
146
  {:else}
@@ -297,11 +300,6 @@
297
300
  border-color: var(--sui-error-border-color);
298
301
  }
299
302
  .combobox div[role=combobox] .label {
300
- display: -webkit-box;
301
- -webkit-box-orient: vertical;
302
- -webkit-line-clamp: 1;
303
- line-clamp: 1;
304
- overflow: hidden;
305
303
  width: 100%;
306
304
  }
307
305
  .combobox :global(.text-input) {
@@ -6,6 +6,7 @@
6
6
  -->
7
7
  <script>
8
8
  import { activateKeyShortcuts } from '../../services/events.svelte';
9
+ import TruncatedText from '../typography/truncated-text.svelte';
9
10
 
10
11
  /**
11
12
  * @import { Snippet } from 'svelte';
@@ -75,7 +76,9 @@
75
76
  />
76
77
  {#if ariaLabel && showInlineLabel}
77
78
  <span id="{id}-label" class="label" class:hidden={!!value} aria-hidden="true">
78
- {ariaLabel}
79
+ <TruncatedText>
80
+ {ariaLabel}
81
+ </TruncatedText>
79
82
  </span>
80
83
  {/if}
81
84
  </div>
@@ -158,7 +161,6 @@ input ~ :global(button) :global(.icon) {
158
161
  display: flex;
159
162
  align-items: center;
160
163
  justify-content: var(--sui-textbox-placeholder-text-align, var(--sui-textbox-text-align, start));
161
- white-space: nowrap;
162
164
  pointer-events: none;
163
165
  }
164
166
  .label.hidden {
@@ -31,7 +31,7 @@
31
31
  show = $bindable(false),
32
32
  id = undefined,
33
33
  duration = 5000,
34
- position = 'bottom-right',
34
+ position = 'auto',
35
35
  children,
36
36
  ...restProps
37
37
  /* eslint-enable prefer-const */
@@ -80,6 +80,22 @@
80
80
  };
81
81
  });
82
82
 
83
+ onMount(() => {
84
+ if (position === 'auto') {
85
+ const mql = globalThis.matchMedia('(width < 1024px)');
86
+
87
+ // eslint-disable-next-line jsdoc/require-jsdoc
88
+ const setMode = () => {
89
+ position = mql.matches
90
+ ? 'bottom-center'
91
+ : `bottom-${document.dir === 'rtl' ? 'left' : 'right'}`;
92
+ };
93
+
94
+ setMode();
95
+ mql.addEventListener('change', setMode);
96
+ }
97
+ });
98
+
83
99
  $effect(() => {
84
100
  if (popover && toast) {
85
101
  popover.appendChild(toast);
@@ -111,7 +127,7 @@
111
127
 
112
128
  <style>.toast-base {
113
129
  position: fixed;
114
- inset: 0;
130
+ inset: 16px;
115
131
  z-index: 99999;
116
132
  display: flex;
117
133
  flex-direction: column;
@@ -120,20 +136,27 @@
120
136
  gap: 8px;
121
137
  margin: 0;
122
138
  border: 0;
123
- padding: 16px;
124
- width: 100dvw;
125
- height: 100dvh;
139
+ padding: 0;
140
+ width: auto;
141
+ height: auto;
126
142
  background-color: transparent;
127
143
  font-family: var(--sui-font-family-default);
128
144
  font-size: var(--sui-font-size-default);
129
145
  font-weight: var(--sui-font-weight-normal, normal);
146
+ text-align: center;
130
147
  pointer-events: none;
131
148
  -webkit-user-select: none;
132
149
  user-select: none;
133
150
  }
134
151
 
152
+ :global(body:has(.sui.bottom-navigation:not([inert]:not([hidden])))) :global(.toast-base) {
153
+ bottom: calc(var(--sui-bottom-navigation-height) + 16px);
154
+ }
155
+
135
156
  .toast {
136
157
  position: absolute;
158
+ width: max-content;
159
+ max-width: 80dvw;
137
160
  box-shadow: 0 8px 16px var(--sui-popup-shadow-color);
138
161
  opacity: 1;
139
162
  transition-duration: 250ms;
@@ -56,12 +56,17 @@
56
56
  flex: none !important;
57
57
  display: flex;
58
58
  align-items: center;
59
- padding: 0 8px;
59
+ padding-inline: 8px;
60
60
  background-color: var(--toolbar-background-color, transparent);
61
61
  }
62
62
  [role=toolbar].primary {
63
63
  --toolbar-size: var(--sui-primary-toolbar-size);
64
64
  }
65
+ @media (width < 768px) {
66
+ [role=toolbar].secondary {
67
+ padding-inline: 0;
68
+ }
69
+ }
65
70
  [role=toolbar][aria-orientation=horizontal] {
66
71
  height: var(--toolbar-size);
67
72
  }
@@ -86,12 +91,6 @@
86
91
  [role=toolbar] :global(h2):first-child {
87
92
  padding-inline-start: 12px;
88
93
  }
89
- [role=toolbar] :global(h2) :global(strong) {
90
- display: block;
91
- overflow: hidden;
92
- text-overflow: ellipsis;
93
- white-space: nowrap;
94
- }
95
94
  [role=toolbar] :global(h2) :global(span) {
96
95
  font-size: var(--sui-font-size-small);
97
96
  font-weight: var(--sui-font-weight-normal, normal);
@@ -0,0 +1,35 @@
1
+ <script>
2
+ /**
3
+ * @import { Snippet } from 'svelte';
4
+ */
5
+
6
+ /**
7
+ * @typedef {object} Props
8
+ * @property {number} [lines] Number of lines.
9
+ * @property {Snippet} [children] Primary slot content.
10
+ */
11
+
12
+ /** @type {Props} */
13
+ let {
14
+ /* eslint-disable prefer-const */
15
+ lines = 1,
16
+ children = undefined,
17
+ /* eslint-enable prefer-const */
18
+ } = $props();
19
+ </script>
20
+
21
+ <span
22
+ role="none"
23
+ class="sui truncated-text"
24
+ style="-webkit-line-clamp: {lines}; line-clamp: {lines};"
25
+ >
26
+ {@render children?.()}
27
+ </span>
28
+
29
+ <style>.truncated-text {
30
+ display: -webkit-box;
31
+ -webkit-box-orient: vertical;
32
+ overflow: hidden;
33
+ white-space: normal;
34
+ overflow-wrap: anywhere;
35
+ }</style>
@@ -0,0 +1,26 @@
1
+ export default TruncatedText;
2
+ type TruncatedText = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props>): void;
5
+ };
6
+ declare const TruncatedText: import("svelte").Component<{
7
+ /**
8
+ * Number of lines.
9
+ */
10
+ lines?: number | undefined;
11
+ /**
12
+ * Primary slot content.
13
+ */
14
+ children?: Snippet<[]> | undefined;
15
+ }, {}, "">;
16
+ type Props = {
17
+ /**
18
+ * Number of lines.
19
+ */
20
+ lines?: number | undefined;
21
+ /**
22
+ * Primary slot content.
23
+ */
24
+ children?: Snippet<[]> | undefined;
25
+ };
26
+ import type { Snippet } from 'svelte';
@@ -202,7 +202,7 @@
202
202
  --sui-control-large-border-width: 1px;
203
203
  --sui-control-large-border-radius: calc(var(--sui-control-large-height) / 8);
204
204
  --sui-control-large-padding: 0 calc((var(--sui-control-large-height) / 3));
205
- --sui-control-large-height: 48px;
205
+ --sui-control-large-height: 40px;
206
206
  --sui-control-border-color: hsl(var(--sui-border-color-2-hsl));
207
207
  --sui-control-foreground-color: var(--sui-primary-foreground-color);
208
208
  --sui-control-background-color: hsl(var(--sui-background-color-4-hsl));
@@ -251,15 +251,16 @@
251
251
  --sui-tab-large-height: var(--sui-control-large-height);
252
252
  --sui-primary-toolbar-size: 56px;
253
253
  --sui-secondary-toolbar-size: 48px;
254
+ --sui-bottom-navigation-height: var(--sui-primary-toolbar-size);
254
255
  --sui-primary-row-height: 56px;
255
256
  --sui-secondary-row-height: 40px;
256
257
  }
257
258
  @media (pointer: coarse) {
258
259
  :global(:root),
259
260
  :global(:host) {
260
- --sui-control-small-height: 30px;
261
- --sui-control-medium-height: 40px;
262
- --sui-control-large-height: 60px;
261
+ --sui-control-small-height: 40px;
262
+ --sui-control-medium-height: 48px;
263
+ --sui-control-large-height: 56px;
263
264
  --sui-checkbox-height: 24px;
264
265
  --sui-secondary-row-height: 48px;
265
266
  }
@@ -0,0 +1,33 @@
1
+ <script>
2
+ /**
3
+ * @import { Snippet } from 'svelte';
4
+ */
5
+
6
+ /**
7
+ * @typedef {object} Props
8
+ * @property {Snippet} [children] Slot content.
9
+ */
10
+
11
+ /** @type {Props} */
12
+ let {
13
+ /* eslint-disable prefer-const */
14
+ children = undefined,
15
+ /* eslint-enable prefer-const */
16
+ } = $props();
17
+ </script>
18
+
19
+ <div role="none" class="sui empty-state">
20
+ {@render children?.()}
21
+ </div>
22
+
23
+ <style>.empty-state {
24
+ display: flex;
25
+ flex-direction: column;
26
+ align-items: center;
27
+ justify-content: center;
28
+ gap: 16px;
29
+ padding: 16px;
30
+ width: 100%;
31
+ height: 100%;
32
+ text-align: center;
33
+ }</style>
@@ -0,0 +1,18 @@
1
+ export default EmptyState;
2
+ type EmptyState = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<Props>): void;
5
+ };
6
+ declare const EmptyState: import("svelte").Component<{
7
+ /**
8
+ * Slot content.
9
+ */
10
+ children?: Snippet<[]> | undefined;
11
+ }, {}, "">;
12
+ type Props = {
13
+ /**
14
+ * Slot content.
15
+ */
16
+ children?: Snippet<[]> | undefined;
17
+ };
18
+ import type { Snippet } from 'svelte';
package/dist/index.d.ts CHANGED
@@ -4,8 +4,10 @@ export function initLocales({ fallbackLocale, initialLocale }?: {
4
4
  } | undefined): void;
5
5
  export { default as Alert } from "./components/alert/alert.svelte";
6
6
  export { default as Infobar } from "./components/alert/infobar.svelte";
7
+ export { default as BottomNavigation } from "./components/bottom-navigation/bottom-navigation.svelte";
7
8
  export { default as ButtonGroup } from "./components/button/button-group.svelte";
8
9
  export { default as Button } from "./components/button/button.svelte";
10
+ export { default as FloatingActionButtonWrapper } from "./components/button/floating-action-button-wrapper.svelte";
9
11
  export { default as SelectButtonGroup } from "./components/button/select-button-group.svelte";
10
12
  export { default as SelectButton } from "./components/button/select-button.svelte";
11
13
  export { default as SplitButton } from "./components/button/split-button.svelte";
@@ -20,6 +22,7 @@ export { default as Disclosure } from "./components/disclosure/disclosure.svelte
20
22
  export { default as Divider } from "./components/divider/divider.svelte";
21
23
  export { default as Spacer } from "./components/divider/spacer.svelte";
22
24
  export { default as Drawer } from "./components/drawer/drawer.svelte";
25
+ export { default as FilePicker } from "./components/file/file-picker.svelte";
23
26
  export { default as GridBody } from "./components/grid/grid-body.svelte";
24
27
  export { default as GridCell } from "./components/grid/grid-cell.svelte";
25
28
  export { default as GridColHeader } from "./components/grid/grid-col-header.svelte";
@@ -41,6 +44,7 @@ export { default as Menu } from "./components/menu/menu.svelte";
41
44
  export { default as Progressbar } from "./components/progressbar/progressbar.svelte";
42
45
  export { default as RadioGroup } from "./components/radio/radio-group.svelte";
43
46
  export { default as Radio } from "./components/radio/radio.svelte";
47
+ export { default as InfiniteScroll } from "./components/scroll/infinite-scroll.svelte";
44
48
  export { default as Combobox } from "./components/select/combobox.svelte";
45
49
  export { default as SelectTags } from "./components/select/select-tags.svelte";
46
50
  export { default as Select } from "./components/select/select.svelte";
@@ -68,7 +72,9 @@ export { default as TextArea } from "./components/text-field/text-area.svelte";
68
72
  export { default as TextInput } from "./components/text-field/text-input.svelte";
69
73
  export { default as Toast } from "./components/toast/toast.svelte";
70
74
  export { default as Toolbar } from "./components/toolbar/toolbar.svelte";
75
+ export { default as TruncatedText } from "./components/typography/truncated-text.svelte";
71
76
  export { default as AppShell } from "./components/util/app-shell.svelte";
77
+ export { default as EmptyState } from "./components/util/empty-state.svelte";
72
78
  export { default as Group } from "./components/util/group.svelte";
73
79
  export { default as Modal } from "./components/util/modal.svelte";
74
80
  export * from "./typedefs";
package/dist/index.js CHANGED
@@ -29,8 +29,10 @@ initLocales();
29
29
 
30
30
  export { default as Alert } from './components/alert/alert.svelte';
31
31
  export { default as Infobar } from './components/alert/infobar.svelte';
32
+ export { default as BottomNavigation } from './components/bottom-navigation/bottom-navigation.svelte';
32
33
  export { default as ButtonGroup } from './components/button/button-group.svelte';
33
34
  export { default as Button } from './components/button/button.svelte';
35
+ export { default as FloatingActionButtonWrapper } from './components/button/floating-action-button-wrapper.svelte';
34
36
  export { default as SelectButtonGroup } from './components/button/select-button-group.svelte';
35
37
  export { default as SelectButton } from './components/button/select-button.svelte';
36
38
  export { default as SplitButton } from './components/button/split-button.svelte';
@@ -45,6 +47,7 @@ export { default as Disclosure } from './components/disclosure/disclosure.svelte
45
47
  export { default as Divider } from './components/divider/divider.svelte';
46
48
  export { default as Spacer } from './components/divider/spacer.svelte';
47
49
  export { default as Drawer } from './components/drawer/drawer.svelte';
50
+ export { default as FilePicker } from './components/file/file-picker.svelte';
48
51
  export { default as GridBody } from './components/grid/grid-body.svelte';
49
52
  export { default as GridCell } from './components/grid/grid-cell.svelte';
50
53
  export { default as GridColHeader } from './components/grid/grid-col-header.svelte';
@@ -66,6 +69,7 @@ export { default as Menu } from './components/menu/menu.svelte';
66
69
  export { default as Progressbar } from './components/progressbar/progressbar.svelte';
67
70
  export { default as RadioGroup } from './components/radio/radio-group.svelte';
68
71
  export { default as Radio } from './components/radio/radio.svelte';
72
+ export { default as InfiniteScroll } from './components/scroll/infinite-scroll.svelte';
69
73
  export { default as Combobox } from './components/select/combobox.svelte';
70
74
  export { default as SelectTags } from './components/select/select-tags.svelte';
71
75
  export { default as Select } from './components/select/select.svelte';
@@ -93,7 +97,9 @@ export { default as TextArea } from './components/text-field/text-area.svelte';
93
97
  export { default as TextInput } from './components/text-field/text-input.svelte';
94
98
  export { default as Toast } from './components/toast/toast.svelte';
95
99
  export { default as Toolbar } from './components/toolbar/toolbar.svelte';
100
+ export { default as TruncatedText } from './components/typography/truncated-text.svelte';
96
101
  export { default as AppShell } from './components/util/app-shell.svelte';
102
+ export { default as EmptyState } from './components/util/empty-state.svelte';
97
103
  export { default as Group } from './components/util/group.svelte';
98
104
  export { default as Modal } from './components/util/modal.svelte';
99
105
 
@@ -100,7 +100,7 @@ export const activateKeyShortcuts = (element, shortcuts = '') => {
100
100
  * Remove the event listener.
101
101
  */
102
102
  const removeListener = () => {
103
- globalThis.removeEventListener('keydown', handler);
103
+ globalThis.removeEventListener('keydown', handler, { capture: true });
104
104
  element.removeAttribute('aria-keyshortcuts');
105
105
  };
106
106
 
@@ -113,7 +113,7 @@ export const activateKeyShortcuts = (element, shortcuts = '') => {
113
113
  : undefined;
114
114
 
115
115
  if (platformKeyShortcuts) {
116
- globalThis.addEventListener('keydown', handler);
116
+ globalThis.addEventListener('keydown', handler, { capture: true });
117
117
  element.setAttribute('aria-keyshortcuts', platformKeyShortcuts);
118
118
  }
119
119
  };
@@ -180,7 +180,7 @@
180
180
  --sui-control-large-border-width: 1px;
181
181
  --sui-control-large-border-radius: calc(var(--sui-control-large-height) / 8);
182
182
  --sui-control-large-padding: 0 calc((var(--sui-control-large-height) / 3));
183
- --sui-control-large-height: 48px;
183
+ --sui-control-large-height: 40px;
184
184
  --sui-control-border-color: hsl(var(--sui-border-color-2-hsl));
185
185
  --sui-control-foreground-color: var(--sui-primary-foreground-color);
186
186
  --sui-control-background-color: hsl(var(--sui-background-color-4-hsl));
@@ -236,15 +236,16 @@
236
236
  // Toolbar
237
237
  --sui-primary-toolbar-size: 56px;
238
238
  --sui-secondary-toolbar-size: 48px;
239
+ --sui-bottom-navigation-height: var(--sui-primary-toolbar-size);
239
240
  // table
240
241
  --sui-primary-row-height: 56px;
241
242
  --sui-secondary-row-height: 40px;
242
243
 
243
244
  // Make controls larger on touch devices, e.g. mobile & tablet
244
245
  @media (pointer: coarse) {
245
- --sui-control-small-height: 30px;
246
- --sui-control-medium-height: 40px;
247
- --sui-control-large-height: 60px;
246
+ --sui-control-small-height: 40px;
247
+ --sui-control-medium-height: 48px;
248
+ --sui-control-large-height: 56px;
248
249
  --sui-checkbox-height: 24px;
249
250
  --sui-secondary-row-height: 48px;
250
251
  }
@@ -53,6 +53,10 @@ export type ButtonProps = {
53
53
  * Text label displayed on the button.
54
54
  */
55
55
  label?: string | undefined;
56
+ /**
57
+ * Number of lines to show the label. Overflowing text will be truncated.
58
+ */
59
+ lines?: number | undefined;
56
60
  /**
57
61
  * The style variant
58
62
  * of the button.
@@ -120,7 +124,7 @@ export type ModalProps = {
120
124
  * The `role` attribute on the `<dialog>`
121
125
  * element.
122
126
  */
123
- role?: "dialog" | "alertdialog" | "none" | undefined;
127
+ role?: "none" | "dialog" | "alertdialog" | undefined;
124
128
  /**
125
129
  * Whether to show the modal.
126
130
  */
@@ -550,7 +554,7 @@ export type InputEventHandlers = {
550
554
  onchange?: ((event: Event) => void) | undefined;
551
555
  };
552
556
  export type PopupPosition = ("top-left" | "top-right" | "right-top" | "right-bottom" | "bottom-left" | "bottom-right" | "left-top" | "left-bottom");
553
- export type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
557
+ export type ToastPosition = "auto" | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
554
558
  export type TextEditorBlockType = "paragraph" | "heading-1" | "heading-2" | "heading-3" | "heading-4" | "heading-5" | "heading-6" | "bulleted-list" | "numbered-list" | "blockquote" | "code-block";
555
559
  export type TextEditorFormatType = "bold" | "italic" | "code";
556
560
  export type TextEditorInlineType = TextEditorFormatType | "link";
package/dist/typedefs.js CHANGED
@@ -24,6 +24,7 @@
24
24
  * attribute. Accepts the special `Accel` key, which will be replaced with `Control` or `Meta`
25
25
  * depending on the user’s operating system.
26
26
  * @property {string} [label] Text label displayed on the button.
27
+ * @property {number} [lines] Number of lines to show the label. Overflowing text will be truncated.
27
28
  * @property {'primary' | 'secondary' | 'tertiary' | 'ghost' | 'link'} [variant] The style variant
28
29
  * of the button.
29
30
  * @property {'small' | 'medium' | 'large'} [size] The size of the button.
@@ -212,7 +213,7 @@
212
213
  */
213
214
 
214
215
  /**
215
- * @typedef {'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' |
216
+ * @typedef {'auto' | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' |
216
217
  * 'bottom-right'} ToastPosition
217
218
  */
218
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltia/ui",
3
- "version": "0.25.15",
3
+ "version": "0.26.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -48,13 +48,13 @@
48
48
  "devDependencies": {
49
49
  "@playwright/test": "^1.51.1",
50
50
  "@sveltejs/adapter-auto": "^6.0.0",
51
- "@sveltejs/kit": "^2.20.4",
51
+ "@sveltejs/kit": "^2.20.5",
52
52
  "@sveltejs/package": "^2.3.10",
53
53
  "@sveltejs/vite-plugin-svelte": "5.0.3",
54
54
  "cspell": "^8.18.1",
55
55
  "eslint": "^8.57.1",
56
56
  "eslint-config-airbnb-base": "^15.0.0",
57
- "eslint-config-prettier": "^10.1.1",
57
+ "eslint-config-prettier": "^10.1.2",
58
58
  "eslint-plugin-import": "^2.31.0",
59
59
  "eslint-plugin-jsdoc": "^50.6.9",
60
60
  "eslint-plugin-svelte": "^2.46.1",
@@ -66,12 +66,12 @@
66
66
  "stylelint": "^16.18.0",
67
67
  "stylelint-config-recommended-scss": "^14.1.0",
68
68
  "stylelint-scss": "^6.11.1",
69
- "svelte": "5.25.8",
69
+ "svelte": "5.26.2",
70
70
  "svelte-check": "^4.1.5",
71
71
  "svelte-i18n": "^4.0.1",
72
72
  "svelte-preprocess": "^6.0.3",
73
73
  "tslib": "^2.8.1",
74
- "vite": "^6.2.5",
74
+ "vite": "^6.2.6",
75
75
  "vitest": "^3.1.1"
76
76
  },
77
77
  "exports": {