ui-svelte 0.2.17 → 0.2.19

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.
@@ -1,8 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { PauseFilledIcon, PlayFilledIcon } from '../icons/index.js';
3
+ import { Avatar, IconButton } from '../index.js';
3
4
  import { cn } from '../utils/class-names.js';
4
- import IconButton from './IconButton.svelte';
5
- import Avatar from '../display/Avatar.svelte';
6
5
 
7
6
  type Props = {
8
7
  class?: string;
@@ -207,6 +206,7 @@
207
206
  variant={variant === 'solid' ? 'soft' : 'solid'}
208
207
  {color}
209
208
  onclick={togglePlay}
209
+ ariaLabel={isPlaying ? 'Pause audio' : 'Play audio'}
210
210
  />
211
211
 
212
212
  <div class="media-content">
@@ -218,6 +218,7 @@
218
218
  aria-valuenow={Math.round((currentTime / duration) * 100) || 0}
219
219
  aria-valuemin="0"
220
220
  aria-valuemax="100"
221
+ aria-label="Audio progress"
221
222
  >
222
223
  <div class="media-bars" class:loading={isAnalyzing} class:loaded={!isAnalyzing}>
223
224
  {#if isAnalyzing}
@@ -21,6 +21,7 @@
21
21
  isWide?: boolean;
22
22
  isDisabled?: boolean;
23
23
  isSolid?: boolean;
24
+ ariaLabel?: string;
24
25
  };
25
26
 
26
27
  const {
@@ -37,7 +38,8 @@
37
38
  endIcon,
38
39
  isLoading,
39
40
  isWide,
40
- isDisabled
41
+ isDisabled,
42
+ ariaLabel
41
43
  }: Props = $props();
42
44
 
43
45
  const colors = {
@@ -82,7 +84,13 @@
82
84
  {/snippet}
83
85
 
84
86
  {#if href}
85
- <a class={baseClasses} {href} {target}>
87
+ <a
88
+ class={baseClasses}
89
+ {href}
90
+ {target}
91
+ aria-label={ariaLabel}
92
+ aria-disabled={isDisabled || undefined}
93
+ >
86
94
  {@render content()}
87
95
  </a>
88
96
  {:else}
@@ -92,6 +100,7 @@
92
100
  disabled={isDisabled || isLoading}
93
101
  class={baseClasses}
94
102
  aria-busy={isLoading}
103
+ aria-label={ariaLabel}
95
104
  >
96
105
  {#if isLoading}
97
106
  <span class="btn-loading">
@@ -16,6 +16,7 @@ type Props = {
16
16
  isWide?: boolean;
17
17
  isDisabled?: boolean;
18
18
  isSolid?: boolean;
19
+ ariaLabel?: string;
19
20
  };
20
21
  declare const Button: import("svelte").Component<Props, {}, "">;
21
22
  type Button = ReturnType<typeof Button>;
@@ -26,6 +26,7 @@
26
26
  offsetY?: string;
27
27
  onclick?: () => void;
28
28
  children?: Snippet;
29
+ ariaLabel?: string;
29
30
  };
30
31
 
31
32
  const {
@@ -40,7 +41,8 @@
40
41
  offsetX,
41
42
  offsetY,
42
43
  onclick,
43
- children
44
+ children,
45
+ ariaLabel
44
46
  }: Props = $props();
45
47
 
46
48
  let isOpen = $state(false);
@@ -97,7 +99,14 @@
97
99
  {#if children}
98
100
  {@render children()}
99
101
  {:else}
100
- <IconButton {icon} {color} {variant} {size} onclick={handleTriggerClick} />
102
+ <IconButton
103
+ {icon}
104
+ {color}
105
+ {variant}
106
+ {size}
107
+ onclick={handleTriggerClick}
108
+ ariaLabel={ariaLabel || (actions.length > 0 ? 'Open actions menu' : undefined)}
109
+ />
101
110
  {/if}
102
111
  </div>
103
112
  </div>
@@ -19,6 +19,7 @@ type Props = {
19
19
  offsetY?: string;
20
20
  onclick?: () => void;
21
21
  children?: Snippet;
22
+ ariaLabel?: string;
22
23
  };
23
24
  declare const Fab: import("svelte").Component<Props, {}, "">;
24
25
  type Fab = ReturnType<typeof Fab>;
@@ -16,6 +16,7 @@
16
16
  isLoading?: boolean;
17
17
  icon: IconData;
18
18
  isDisabled?: boolean;
19
+ ariaLabel?: string;
19
20
  };
20
21
 
21
22
  const {
@@ -29,7 +30,8 @@
29
30
  class: className,
30
31
  icon,
31
32
  isLoading,
32
- isDisabled
33
+ isDisabled,
34
+ ariaLabel
33
35
  }: Props = $props();
34
36
 
35
37
  const colors = {
@@ -68,7 +70,7 @@
68
70
  {/snippet}
69
71
 
70
72
  {#if href}
71
- <a class={baseClasses} {href} {target}>
73
+ <a class={baseClasses} {href} {target} aria-label={ariaLabel}>
72
74
  {@render content()}
73
75
  </a>
74
76
  {:else}
@@ -78,6 +80,7 @@
78
80
  disabled={isDisabled || isLoading}
79
81
  class={baseClasses}
80
82
  aria-busy={isLoading}
83
+ aria-label={ariaLabel}
81
84
  >
82
85
  {#if isLoading}
83
86
  <span class="btn-loading">
@@ -11,6 +11,7 @@ type Props = {
11
11
  isLoading?: boolean;
12
12
  icon: IconData;
13
13
  isDisabled?: boolean;
14
+ ariaLabel?: string;
14
15
  };
15
16
  declare const IconButton: import("svelte").Component<Props, {}, "">;
16
17
  type IconButton = ReturnType<typeof IconButton>;
@@ -109,6 +109,7 @@
109
109
  {color}
110
110
  variant="overlay"
111
111
  size="sm"
112
+ ariaLabel="Download image"
112
113
  />
113
114
  <IconButton
114
115
  onclick={handleToggleMaximize}
@@ -116,6 +117,7 @@
116
117
  {color}
117
118
  variant="overlay"
118
119
  size="sm"
120
+ ariaLabel="Toggle fullscreen"
119
121
  />
120
122
  </div>
121
123
  </div>
@@ -357,6 +357,7 @@
357
357
  {color}
358
358
  variant={variant === 'solid' ? 'soft' : 'solid'}
359
359
  size="md"
360
+ ariaLabel={isPlaying ? 'Pause playback' : 'Play recording'}
360
361
  />
361
362
 
362
363
  <div class="media-content">
@@ -382,6 +383,7 @@
382
383
  color="danger"
383
384
  variant="ghost"
384
385
  size="sm"
386
+ ariaLabel="Discard recording"
385
387
  />
386
388
  <IconButton
387
389
  onclick={confirmRecording}
@@ -390,6 +392,7 @@
390
392
  variant="ghost"
391
393
  size="sm"
392
394
  isLoading={isUploading}
395
+ ariaLabel="Confirm recording"
393
396
  />
394
397
  </div>
395
398
  {:else if !isRecording}
@@ -399,6 +402,7 @@
399
402
  {color}
400
403
  variant={variant === 'solid' ? 'soft' : 'solid'}
401
404
  size="md"
405
+ ariaLabel="Start recording"
402
406
  />
403
407
 
404
408
  <div class="media-content">
@@ -421,6 +425,7 @@
421
425
  {color}
422
426
  variant={variant === 'solid' ? 'soft' : 'solid'}
423
427
  size="md"
428
+ ariaLabel="Resume recording"
424
429
  />
425
430
  {:else}
426
431
  <IconButton
@@ -429,6 +434,7 @@
429
434
  {color}
430
435
  variant={variant === 'solid' ? 'soft' : 'solid'}
431
436
  size="md"
437
+ ariaLabel="Pause recording"
432
438
  />
433
439
  {/if}
434
440
 
@@ -456,6 +462,7 @@
456
462
  color="danger"
457
463
  variant="ghost"
458
464
  size="sm"
465
+ ariaLabel="Discard recording"
459
466
  />
460
467
  <IconButton
461
468
  onclick={stopRecording}
@@ -463,6 +470,7 @@
463
470
  {color}
464
471
  variant="ghost"
465
472
  size="sm"
473
+ ariaLabel="Stop recording"
466
474
  />
467
475
  </div>
468
476
  {/if}
@@ -20,6 +20,7 @@
20
20
  isWide?: boolean;
21
21
  isVertical?: boolean;
22
22
  isDisabled?: boolean;
23
+ ariaLabel?: string;
23
24
  };
24
25
 
25
26
  let {
@@ -33,7 +34,8 @@
33
34
  class: className,
34
35
  isWide,
35
36
  isVertical,
36
- isDisabled
37
+ isDisabled,
38
+ ariaLabel
37
39
  }: Props = $props();
38
40
 
39
41
  const colors = {
@@ -78,14 +80,15 @@
78
80
  }
79
81
  </script>
80
82
 
81
- <div class={groupClasses} role="group">
83
+ <div class={groupClasses} role="radiogroup" aria-label={ariaLabel}>
82
84
  {#each items as item}
83
85
  <button
84
86
  type="button"
85
87
  class={cn('toggle-group-item', sizes[size], value === item.id && 'is-active')}
86
88
  onclick={() => handleClick(item.id)}
87
89
  disabled={isDisabled}
88
- aria-pressed={value === item.id}
90
+ role="radio"
91
+ aria-checked={value === item.id}
89
92
  >
90
93
  {#if item.icon}
91
94
  <Icon icon={item.icon} />
@@ -15,6 +15,7 @@ type Props = {
15
15
  isWide?: boolean;
16
16
  isVertical?: boolean;
17
17
  isDisabled?: boolean;
18
+ ariaLabel?: string;
18
19
  };
19
20
  declare const ToggleGroup: import("svelte").Component<Props, {}, "value">;
20
21
  type ToggleGroup = ReturnType<typeof ToggleGroup>;
@@ -20,4 +20,5 @@
20
20
  {variant}
21
21
  class={className}
22
22
  onclick={theme.toggleTheme}
23
+ ariaLabel={theme.isDark ? 'Switch to light mode' : 'Switch to dark mode'}
23
24
  />
@@ -181,6 +181,7 @@
181
181
  {color}
182
182
  variant="overlay"
183
183
  size="sm"
184
+ ariaLabel={videoParams.paused ? 'Play video' : 'Pause video'}
184
185
  />
185
186
 
186
187
  <Chip variant="overlay" {color}>
@@ -217,6 +218,7 @@
217
218
  {color}
218
219
  variant="overlay"
219
220
  size="sm"
221
+ ariaLabel={videoParams.muted ? 'Unmute video' : 'Mute video'}
220
222
  />
221
223
  </div>
222
224
  <IconButton
@@ -225,6 +227,7 @@
225
227
  {color}
226
228
  variant="overlay"
227
229
  size="sm"
230
+ ariaLabel="Toggle picture-in-picture"
228
231
  />
229
232
  <IconButton
230
233
  onclick={handleToggleMaximize}
@@ -232,6 +235,7 @@
232
235
  {color}
233
236
  variant="overlay"
234
237
  size="sm"
238
+ ariaLabel="Toggle fullscreen"
235
239
  />
236
240
  </div>
237
241
  </div>
package/dist/css/base.css CHANGED
@@ -1,4 +1,7 @@
1
1
  @layer base {
2
+ html {
3
+ scroll-behavior: smooth;
4
+ }
2
5
  body {
3
6
  @apply h-dvh flex flex-col relative;
4
7
  @apply bg-background text-on-background;
@@ -15,6 +15,11 @@
15
15
  const { icon, class: className }: Props = $props();
16
16
  </script>
17
17
 
18
- <svg viewBox={icon.viewbox} class={cn('icon', className)} xmlns="http://www.w3.org/2000/svg">
18
+ <svg
19
+ viewBox={icon.viewbox}
20
+ class={cn('icon', className)}
21
+ xmlns="http://www.w3.org/2000/svg"
22
+ aria-hidden="true"
23
+ >
19
24
  {@html icon.body}
20
25
  </svg>
@@ -4,6 +4,11 @@
4
4
  import { cn } from '../utils/class-names.js';
5
5
  import { onMount } from 'svelte';
6
6
 
7
+ const instanceId = Math.random().toString(36).substring(2, 9);
8
+ const listboxId = `select-listbox-${instanceId}`;
9
+ const labelId = `select-label-${instanceId}`;
10
+ const helpTextId = `select-help-${instanceId}`;
11
+
7
12
  type Option = {
8
13
  id: string | number;
9
14
  label: string;
@@ -273,7 +278,7 @@
273
278
  <input type="text" {name} bind:value hidden />
274
279
 
275
280
  {#if !isFloatLabel && label}
276
- <div class="field-label">{label}</div>
281
+ <div class="field-label" id={labelId}>{label}</div>
277
282
  {/if}
278
283
 
279
284
  <button
@@ -291,6 +296,16 @@
291
296
  onclick={toggleDropdown}
292
297
  onmouseenter={() => (isActive = true)}
293
298
  onmouseleave={() => (isActive = false)}
299
+ role="combobox"
300
+ aria-haspopup="listbox"
301
+ aria-expanded={isOpen}
302
+ aria-controls={listboxId}
303
+ aria-activedescendant={isOpen && focusedIndex >= 0
304
+ ? `${listboxId}-option-${focusedIndex}`
305
+ : undefined}
306
+ aria-labelledby={label ? labelId : undefined}
307
+ aria-label={!label ? placeholder : undefined}
308
+ aria-describedby={errorText || helpText ? helpTextId : undefined}
294
309
  >
295
310
  {#if isFloatLabel && label}
296
311
  <span
@@ -325,13 +340,21 @@
325
340
  </button>
326
341
 
327
342
  {#if errorText || helpText}
328
- <div class={cn('field-help', errorText && 'is-danger')}>{errorText || helpText}</div>
343
+ <div id={helpTextId} class={cn('field-help', errorText && 'is-danger')}>
344
+ {errorText || helpText}
345
+ </div>
329
346
  {/if}
330
347
 
331
348
  <div class:is-active={isOpen} class="select-popover" bind:this={contentEl} {style}>
332
- <ul class="select-options" bind:this={optionsEl}>
349
+ <ul
350
+ class="select-options"
351
+ bind:this={optionsEl}
352
+ role="listbox"
353
+ id={listboxId}
354
+ aria-label={label}
355
+ >
333
356
  {#each options as item, index}
334
- <li>
357
+ <li id={`${listboxId}-option-${index}`} role="option" aria-selected={value === item.id}>
335
358
  <Item
336
359
  label={item.label}
337
360
  src={item.src}
@@ -165,6 +165,8 @@
165
165
  oninput={(e) => oninput?.((e.target as HTMLInputElement).value)}
166
166
  onfocusin={() => (isFocused = true)}
167
167
  onfocusout={() => (isFocused = false)}
168
+ aria-describedby={errorText || helpText ? `${uid}-help` : undefined}
169
+ aria-invalid={!!errorText}
168
170
  />
169
171
 
170
172
  {#if endContent}
@@ -181,7 +183,7 @@
181
183
  </label>
182
184
 
183
185
  {#if errorText || helpText}
184
- <div class={cn('field-help', errorText && 'is-danger')}>
186
+ <div id={`${uid}-help`} class={cn('field-help', errorText && 'is-danger')}>
185
187
  {errorText || helpText}
186
188
  </div>
187
189
  {/if}
@@ -49,6 +49,8 @@
49
49
  {defaultChecked}
50
50
  {disabled}
51
51
  onchange={() => onchange && onchange(checked!)}
52
+ role="switch"
53
+ aria-checked={checked}
52
54
  />
53
55
  {#if labelRight}
54
56
  <span class={cn('toggle-label-right', checked && 'is-active')}>{labelRight}</span>
@@ -49,7 +49,7 @@
49
49
  <svelte:head>
50
50
  <meta
51
51
  name="viewport"
52
- content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content"
52
+ content="width=device-width, initial-scale=1, maximum-scale=5, viewport-fit=cover, interactive-widget=resizes-content"
53
53
  />
54
54
  <script>
55
55
  let themeState = 'light';
@@ -48,6 +48,8 @@
48
48
  let openSubmenuIndex = $state<number | null>(null);
49
49
  let triggerElements = $state<Record<number, HTMLElement>>({});
50
50
  let popoverElement = $state<HTMLElement>();
51
+ let activeHash = $state<string | null>(null);
52
+ let sectionObserver: IntersectionObserver | null = null;
51
53
  let position = $state({
52
54
  top: 0,
53
55
  left: 0,
@@ -84,6 +86,11 @@
84
86
 
85
87
  function isItemActive(href?: string): boolean {
86
88
  if (!href) return false;
89
+
90
+ if (href.startsWith('#')) {
91
+ return activeHash === href;
92
+ }
93
+
87
94
  return page.url.pathname === href || page.url.pathname.startsWith(href + '/');
88
95
  }
89
96
 
@@ -191,7 +198,38 @@
191
198
  }
192
199
 
193
200
  onMount(() => {
194
- return () => stopEventListeners();
201
+ const hashLinks = items
202
+ .flatMap((item) => [item.href, ...(item.subitems?.map((s) => s.href) || [])])
203
+ .filter((href): href is string => !!href && href.startsWith('#'))
204
+ .map((href) => href.slice(1));
205
+
206
+ if (hashLinks.length > 0) {
207
+ sectionObserver = new IntersectionObserver(
208
+ (entries) => {
209
+ const visibleEntries = entries.filter((e) => e.isIntersecting);
210
+ if (visibleEntries.length > 0) {
211
+ const mostVisible = visibleEntries.reduce((prev, curr) =>
212
+ curr.intersectionRatio > prev.intersectionRatio ? curr : prev
213
+ );
214
+ activeHash = '#' + mostVisible.target.id;
215
+ }
216
+ },
217
+ {
218
+ threshold: [0.1, 0.5, 0.9],
219
+ rootMargin: '-10% 0px -10% 0px'
220
+ }
221
+ );
222
+
223
+ hashLinks.forEach((id) => {
224
+ const el = document.getElementById(id);
225
+ if (el) sectionObserver!.observe(el);
226
+ });
227
+ }
228
+
229
+ return () => {
230
+ sectionObserver?.disconnect();
231
+ stopEventListeners();
232
+ };
195
233
  });
196
234
  </script>
197
235
 
@@ -216,6 +254,9 @@
216
254
  )}
217
255
  bind:this={triggerElements[index]}
218
256
  onclick={() => handleItemClick(item, index)}
257
+ aria-haspopup={!!(item.subitems || item.megamenu)}
258
+ aria-expanded={openSubmenuIndex === index}
259
+ aria-controls={openSubmenuIndex === index ? `navmenu-popover-${index}` : undefined}
219
260
  >
220
261
  {#if item.icon}
221
262
  <Icon icon={item.icon} class="navmenu-icon" />
@@ -235,6 +276,7 @@
235
276
  {#if openSubmenuIndex !== null}
236
277
  {@const currentItem = items[openSubmenuIndex]}
237
278
  <div
279
+ id={`navmenu-popover-${openSubmenuIndex}`}
238
280
  class={cn(
239
281
  'navmenu-popover',
240
282
  sizeClasses[size],
@@ -243,6 +285,7 @@
243
285
  )}
244
286
  bind:this={popoverElement}
245
287
  {style}
288
+ role="menu"
246
289
  >
247
290
  {#if currentItem?.megamenu}
248
291
  {@render currentItem.megamenu()}
@@ -257,6 +300,7 @@
257
300
  openSubmenuIndex = null;
258
301
  stopEventListeners();
259
302
  }}
303
+ role="menuitem"
260
304
  >
261
305
  {#if subitem.icon}
262
306
  <Icon icon={subitem.icon} class="navmenu-submenu-icon" />
@@ -273,6 +317,7 @@
273
317
  type="button"
274
318
  class="navmenu-submenu-item"
275
319
  onclick={() => handleSubmenuItemClick(subitem)}
320
+ role="menuitem"
276
321
  >
277
322
  {#if subitem.icon}
278
323
  <Icon icon={subitem.icon} class="navmenu-submenu-icon" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ui-svelte",
3
- "version": "0.2.17",
3
+ "version": "0.2.19",
4
4
  "author": {
5
5
  "name": "SappsDev",
6
6
  "email": "info@sappsdev.com"