@noahbaumgartner/svelte-ui 0.0.5 → 0.0.6

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -5
  3. package/dist/components/accordion/Accordion.svelte +16 -6
  4. package/dist/components/accordion/Accordion.svelte.d.ts +3 -1
  5. package/dist/components/accordion/AccordionItem.svelte +106 -67
  6. package/dist/components/accordion/AccordionItem.svelte.d.ts +5 -1
  7. package/dist/components/alert-dialog/AlertDialog.svelte +108 -76
  8. package/dist/components/alert-dialog/AlertDialog.svelte.d.ts +5 -2
  9. package/dist/components/avatar/Avatar.svelte +71 -49
  10. package/dist/components/avatar/Avatar.svelte.d.ts +5 -2
  11. package/dist/components/avatar-group/AvatarGroup.svelte +72 -45
  12. package/dist/components/avatar-group/AvatarGroup.svelte.d.ts +5 -1
  13. package/dist/components/badge/Badge.svelte +50 -28
  14. package/dist/components/badge/Badge.svelte.d.ts +6 -4
  15. package/dist/components/breadcrumb/Breadcrumb.svelte +21 -17
  16. package/dist/components/breadcrumb/Breadcrumb.svelte.d.ts +3 -1
  17. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte +17 -6
  18. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte.d.ts +5 -2
  19. package/dist/components/breadcrumb/BreadcrumbItem.svelte +75 -36
  20. package/dist/components/breadcrumb/BreadcrumbItem.svelte.d.ts +8 -5
  21. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte +32 -9
  22. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte.d.ts +10 -17
  23. package/dist/components/button/Button.svelte +206 -168
  24. package/dist/components/button/Button.svelte.d.ts +9 -5
  25. package/dist/components/button-group/ButtonGroup.svelte +52 -55
  26. package/dist/components/button-group/ButtonGroup.svelte.d.ts +3 -1
  27. package/dist/components/calendar/Calendar.svelte +232 -144
  28. package/dist/components/calendar/Calendar.svelte.d.ts +19 -2
  29. package/dist/components/calendar/dates.d.ts +4 -1
  30. package/dist/components/calendar/dates.js +8 -2
  31. package/dist/components/card/Card.svelte +48 -32
  32. package/dist/components/card/Card.svelte.d.ts +3 -1
  33. package/dist/components/card/CardAction.svelte +11 -7
  34. package/dist/components/card/CardAction.svelte.d.ts +3 -1
  35. package/dist/components/card/CardContent.svelte +8 -4
  36. package/dist/components/card/CardContent.svelte.d.ts +3 -1
  37. package/dist/components/card/CardDescription.svelte +11 -7
  38. package/dist/components/card/CardDescription.svelte.d.ts +3 -1
  39. package/dist/components/card/CardFooter.svelte +15 -11
  40. package/dist/components/card/CardFooter.svelte.d.ts +3 -1
  41. package/dist/components/card/CardHeader.svelte +15 -11
  42. package/dist/components/card/CardHeader.svelte.d.ts +3 -1
  43. package/dist/components/card/CardTitle.svelte +11 -7
  44. package/dist/components/card/CardTitle.svelte.d.ts +3 -1
  45. package/dist/components/checkbox/Checkbox.svelte +152 -61
  46. package/dist/components/checkbox/Checkbox.svelte.d.ts +5 -2
  47. package/dist/components/color-picker/ColorPicker.svelte +131 -78
  48. package/dist/components/color-picker/ColorPicker.svelte.d.ts +10 -1
  49. package/dist/components/context-menu/ContextMenu.svelte +78 -61
  50. package/dist/components/context-menu/ContextMenu.svelte.d.ts +5 -2
  51. package/dist/components/context-menu/ContextMenuInfo.svelte +91 -0
  52. package/dist/components/context-menu/ContextMenuInfo.svelte.d.ts +19 -0
  53. package/dist/components/context-menu/ContextMenuItem.svelte +102 -57
  54. package/dist/components/context-menu/ContextMenuItem.svelte.d.ts +10 -5
  55. package/dist/components/context-menu/ContextMenuLabel.svelte +28 -0
  56. package/dist/components/context-menu/ContextMenuLabel.svelte.d.ts +10 -0
  57. package/dist/components/context-menu/ContextMenuSeparator.svelte +29 -0
  58. package/dist/components/context-menu/ContextMenuSeparator.svelte.d.ts +8 -0
  59. package/dist/components/dialog/Dialog.svelte +149 -108
  60. package/dist/components/dialog/Dialog.svelte.d.ts +7 -2
  61. package/dist/components/drawer/Drawer.svelte +240 -189
  62. package/dist/components/drawer/Drawer.svelte.d.ts +7 -2
  63. package/dist/components/empty-state/EmptyState.svelte +67 -47
  64. package/dist/components/empty-state/EmptyState.svelte.d.ts +7 -4
  65. package/dist/components/entry/Entry.svelte +53 -42
  66. package/dist/components/entry/Entry.svelte.d.ts +2 -1
  67. package/dist/components/field/Field.svelte +66 -45
  68. package/dist/components/field/Field.svelte.d.ts +2 -1
  69. package/dist/components/field/FieldContent.svelte +11 -8
  70. package/dist/components/field/FieldContent.svelte.d.ts +2 -1
  71. package/dist/components/field/FieldDescription.svelte +18 -15
  72. package/dist/components/field/FieldDescription.svelte.d.ts +2 -1
  73. package/dist/components/field/FieldError.svelte +22 -13
  74. package/dist/components/field/FieldError.svelte.d.ts +2 -1
  75. package/dist/components/field/FieldGroup.svelte +15 -12
  76. package/dist/components/field/FieldGroup.svelte.d.ts +2 -1
  77. package/dist/components/field/FieldLegend.svelte +23 -14
  78. package/dist/components/field/FieldLegend.svelte.d.ts +2 -1
  79. package/dist/components/field/FieldSeparator.svelte +12 -9
  80. package/dist/components/field/FieldSeparator.svelte.d.ts +2 -1
  81. package/dist/components/field/FieldSet.svelte +18 -15
  82. package/dist/components/field/FieldSet.svelte.d.ts +2 -1
  83. package/dist/components/footer/Footer.svelte +63 -47
  84. package/dist/components/footer/Footer.svelte.d.ts +2 -1
  85. package/dist/components/footer/FooterColumn.svelte +17 -14
  86. package/dist/components/footer/FooterColumn.svelte.d.ts +2 -1
  87. package/dist/components/image/Image.svelte +61 -0
  88. package/dist/components/image/Image.svelte.d.ts +18 -0
  89. package/dist/components/input/Input.svelte +307 -208
  90. package/dist/components/input/Input.svelte.d.ts +12 -1
  91. package/dist/components/input-group/InputGroup.svelte +101 -61
  92. package/dist/components/input-group/InputGroup.svelte.d.ts +2 -1
  93. package/dist/components/input-group/InputGroupAddon.svelte +58 -49
  94. package/dist/components/kbd/Kbd.svelte +39 -30
  95. package/dist/components/kbd/Kbd.svelte.d.ts +2 -1
  96. package/dist/components/kbd/KbdGroup.svelte +15 -12
  97. package/dist/components/kbd/KbdGroup.svelte.d.ts +2 -1
  98. package/dist/components/label/Label.svelte +47 -28
  99. package/dist/components/label/Label.svelte.d.ts +2 -1
  100. package/dist/components/link/Link.svelte +71 -17
  101. package/dist/components/link/Link.svelte.d.ts +2 -1
  102. package/dist/components/mesh-gradient/MeshGradient.svelte +33 -22
  103. package/dist/components/mesh-gradient/MeshGradient.svelte.d.ts +2 -3
  104. package/dist/components/navbar/Navbar.svelte +137 -99
  105. package/dist/components/navbar/Navbar.svelte.d.ts +4 -2
  106. package/dist/components/navigation-menu/NavigationMenu.svelte +37 -20
  107. package/dist/components/navigation-menu/NavigationMenu.svelte.d.ts +4 -2
  108. package/dist/components/navigation-menu/NavigationMenuItem.svelte +131 -89
  109. package/dist/components/navigation-menu/NavigationMenuItem.svelte.d.ts +4 -2
  110. package/dist/components/navigation-menu/NavigationMenuLink.svelte +90 -51
  111. package/dist/components/navigation-menu/NavigationMenuLink.svelte.d.ts +7 -5
  112. package/dist/components/number-stepper/NumberStepper.svelte +39 -20
  113. package/dist/components/number-stepper/NumberStepper.svelte.d.ts +6 -1
  114. package/dist/components/popover/Popover.svelte +85 -65
  115. package/dist/components/popover/Popover.svelte.d.ts +5 -2
  116. package/dist/components/preview-card/PreviewCard.svelte +116 -98
  117. package/dist/components/preview-card/PreviewCard.svelte.d.ts +9 -6
  118. package/dist/components/progress/Progress.svelte +61 -38
  119. package/dist/components/progress/Progress.svelte.d.ts +2 -1
  120. package/dist/components/radio-group/RadioGroup.svelte +24 -19
  121. package/dist/components/radio-group/RadioGroup.svelte.d.ts +2 -1
  122. package/dist/components/radio-group/RadioGroupItem.svelte +101 -54
  123. package/dist/components/radio-group/RadioGroupItem.svelte.d.ts +2 -1
  124. package/dist/components/section/Section.svelte +114 -0
  125. package/dist/components/section/Section.svelte.d.ts +22 -0
  126. package/dist/components/select/Select.svelte +216 -133
  127. package/dist/components/select/Select.svelte.d.ts +11 -2
  128. package/dist/components/separator/Separator.svelte +21 -10
  129. package/dist/components/separator/Separator.svelte.d.ts +2 -1
  130. package/dist/components/sidebar/Sidebar.svelte +142 -108
  131. package/dist/components/sidebar/Sidebar.svelte.d.ts +6 -1
  132. package/dist/components/sidebar/SidebarContent.svelte +15 -12
  133. package/dist/components/sidebar/SidebarContent.svelte.d.ts +2 -1
  134. package/dist/components/sidebar/SidebarFooter.svelte +10 -7
  135. package/dist/components/sidebar/SidebarFooter.svelte.d.ts +2 -1
  136. package/dist/components/sidebar/SidebarGroup.svelte +35 -25
  137. package/dist/components/sidebar/SidebarGroup.svelte.d.ts +2 -1
  138. package/dist/components/sidebar/SidebarHeader.svelte +21 -16
  139. package/dist/components/sidebar/SidebarHeader.svelte.d.ts +6 -1
  140. package/dist/components/sidebar/SidebarInset.svelte +11 -8
  141. package/dist/components/sidebar/SidebarInset.svelte.d.ts +2 -1
  142. package/dist/components/sidebar/SidebarMenu.svelte +13 -10
  143. package/dist/components/sidebar/SidebarMenu.svelte.d.ts +2 -1
  144. package/dist/components/sidebar/SidebarMenuItem.svelte +195 -114
  145. package/dist/components/sidebar/SidebarMenuItem.svelte.d.ts +11 -5
  146. package/dist/components/sidebar/SidebarProvider.svelte +18 -9
  147. package/dist/components/sidebar/SidebarProvider.svelte.d.ts +2 -1
  148. package/dist/components/sidebar/SidebarSeparator.svelte +6 -1
  149. package/dist/components/sidebar/SidebarSeparator.svelte.d.ts +6 -17
  150. package/dist/components/sidebar/SidebarTrigger.svelte +17 -5
  151. package/dist/components/sidebar/SidebarTrigger.svelte.d.ts +4 -2
  152. package/dist/components/skeleton/Skeleton.svelte +26 -16
  153. package/dist/components/skeleton/Skeleton.svelte.d.ts +4 -2
  154. package/dist/components/spinner/Spinner.svelte +71 -46
  155. package/dist/components/spinner/Spinner.svelte.d.ts +2 -1
  156. package/dist/components/switch/Switch.svelte +126 -65
  157. package/dist/components/switch/Switch.svelte.d.ts +4 -2
  158. package/dist/components/table/Table.svelte +135 -76
  159. package/dist/components/table/Table.svelte.d.ts +12 -2
  160. package/dist/components/tabs/Tabs.svelte +173 -116
  161. package/dist/components/tabs/Tabs.svelte.d.ts +10 -8
  162. package/dist/components/textarea/Textarea.svelte +76 -42
  163. package/dist/components/textarea/Textarea.svelte.d.ts +4 -1
  164. package/dist/components/time-picker/TimePicker.svelte +214 -85
  165. package/dist/components/time-picker/TimePicker.svelte.d.ts +18 -1
  166. package/dist/components/toaster/Toaster.svelte +132 -70
  167. package/dist/components/toaster/Toaster.svelte.d.ts +18 -2
  168. package/dist/components/toaster/toast.svelte.d.ts +3 -3
  169. package/dist/components/toggle/Toggle.svelte +25 -4
  170. package/dist/components/toggle/Toggle.svelte.d.ts +1 -1
  171. package/dist/components/tooltip/Tooltip.svelte +87 -58
  172. package/dist/components/tooltip/Tooltip.svelte.d.ts +4 -2
  173. package/dist/icon.d.ts +12 -0
  174. package/dist/icon.js +1 -0
  175. package/dist/index.d.ts +6 -0
  176. package/dist/index.js +5 -0
  177. package/dist/internal/icons/ArrowDown.svelte +14 -0
  178. package/dist/internal/icons/ArrowDown.svelte.d.ts +4 -0
  179. package/dist/internal/icons/ArrowUp.svelte +14 -0
  180. package/dist/internal/icons/ArrowUp.svelte.d.ts +4 -0
  181. package/dist/internal/icons/Calendar.svelte +16 -0
  182. package/dist/internal/icons/Calendar.svelte.d.ts +4 -0
  183. package/dist/internal/icons/Check.svelte +8 -0
  184. package/dist/internal/icons/Check.svelte.d.ts +4 -0
  185. package/dist/internal/icons/ChevronDown.svelte +8 -0
  186. package/dist/internal/icons/ChevronDown.svelte.d.ts +4 -0
  187. package/dist/internal/icons/ChevronLeft.svelte +8 -0
  188. package/dist/internal/icons/ChevronLeft.svelte.d.ts +4 -0
  189. package/dist/internal/icons/ChevronRight.svelte +8 -0
  190. package/dist/internal/icons/ChevronRight.svelte.d.ts +4 -0
  191. package/dist/internal/icons/ChevronUp.svelte +8 -0
  192. package/dist/internal/icons/ChevronUp.svelte.d.ts +4 -0
  193. package/dist/internal/icons/ChevronsUpDown.svelte +14 -0
  194. package/dist/internal/icons/ChevronsUpDown.svelte.d.ts +4 -0
  195. package/dist/internal/icons/Clock.svelte +14 -0
  196. package/dist/internal/icons/Clock.svelte.d.ts +4 -0
  197. package/dist/internal/icons/Ellipsis.svelte +15 -0
  198. package/dist/internal/icons/Ellipsis.svelte.d.ts +4 -0
  199. package/dist/internal/icons/Icon.svelte +33 -0
  200. package/dist/internal/icons/Icon.svelte.d.ts +8 -0
  201. package/dist/internal/icons/Minus.svelte +8 -0
  202. package/dist/internal/icons/Minus.svelte.d.ts +4 -0
  203. package/dist/internal/icons/PanelLeft.svelte +14 -0
  204. package/dist/internal/icons/PanelLeft.svelte.d.ts +4 -0
  205. package/dist/internal/icons/Plus.svelte +14 -0
  206. package/dist/internal/icons/Plus.svelte.d.ts +4 -0
  207. package/dist/internal/icons/User.svelte +14 -0
  208. package/dist/internal/icons/User.svelte.d.ts +4 -0
  209. package/dist/internal/icons/X.svelte +14 -0
  210. package/dist/internal/icons/X.svelte.d.ts +4 -0
  211. package/dist/styles/tokens.css +39 -41
  212. package/package.json +5 -10
  213. package/dist/icons.d.ts +0 -2
  214. package/dist/icons.js +0 -4
@@ -1,3 +1,16 @@
1
+ <script module lang="ts">
2
+ export type TimePickerLabels = {
3
+ /** Accessible name of the hours column. */
4
+ hours?: string;
5
+ /** Accessible name of the minutes column. */
6
+ minutes?: string;
7
+ /** Accessible name of the AM/PM column. */
8
+ period?: string;
9
+ am?: string;
10
+ pm?: string;
11
+ };
12
+ </script>
13
+
1
14
  <script lang="ts">
2
15
  import { tick } from 'svelte';
3
16
  import type { HTMLAttributes } from 'svelte/elements';
@@ -7,37 +20,96 @@
7
20
  value?: string;
8
21
  /** Minutes between the offered values. */
9
22
  minuteStep?: number;
23
+ /** BCP 47 tag used for numerals, AM/PM text and the 12/24-hour default. Without it the picker shows 24 hours. */
24
+ locale?: string;
25
+ /** Forces 12-hour (`true`) or 24-hour (`false`) columns, overriding the locale. */
26
+ hour12?: boolean;
27
+ /** Accessible names of the columns and the AM/PM text. */
28
+ labels?: TimePickerLabels;
10
29
  onselect?: (value: string) => void;
30
+ ref?: HTMLDivElement | null;
11
31
  };
12
32
 
13
33
  let {
14
34
  value = $bindable(''),
15
35
  minuteStep = 1,
36
+ locale,
37
+ hour12,
38
+ labels,
16
39
  onselect,
40
+ ref = $bindable(null),
17
41
  class: className,
18
42
  ...rest
19
43
  }: Props = $props();
20
44
 
21
- const pad = (n: number) => String(n).padStart(2, '0');
22
- const hours = Array.from({ length: 24 }, (_, i) => i);
23
- let minutes = $derived(
24
- Array.from(
25
- { length: Math.ceil(60 / Math.max(1, minuteStep)) },
26
- (_, i) => i * Math.max(1, minuteStep)
27
- )
45
+ type Column = 'hour' | 'minute' | 'period';
46
+ type Entry = { id: number; text: string };
47
+
48
+ let twelve = $derived(
49
+ hour12 ??
50
+ (locale !== undefined &&
51
+ ['h11', 'h12'].includes(
52
+ new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hourCycle ?? ''
53
+ ))
28
54
  );
29
55
 
56
+ let format = $derived(
57
+ new Intl.NumberFormat(locale, { minimumIntegerDigits: 2, useGrouping: false }).format
58
+ );
59
+ let formatPlain = $derived(new Intl.NumberFormat(locale, { useGrouping: false }).format);
60
+
61
+ function dayPeriod(hour: number, fallback: string) {
62
+ if (locale === undefined) return fallback;
63
+ return (
64
+ new Intl.DateTimeFormat(locale, { hour: 'numeric', hour12: true })
65
+ .formatToParts(new Date(2000, 0, 1, hour))
66
+ .find((part) => part.type === 'dayPeriod')?.value ?? fallback
67
+ );
68
+ }
69
+
70
+ let hoursLabel = $derived(labels?.hours ?? 'Hours');
71
+ let minutesLabel = $derived(labels?.minutes ?? 'Minutes');
72
+ let periodLabel = $derived(labels?.period ?? 'AM/PM');
73
+ let amLabel = $derived(labels?.am ?? dayPeriod(9, 'AM'));
74
+ let pmLabel = $derived(labels?.pm ?? dayPeriod(15, 'PM'));
75
+
76
+ let hourEntries: Entry[] = $derived(
77
+ twelve
78
+ ? Array.from({ length: 12 }, (_, i) => {
79
+ const id = i === 0 ? 12 : i;
80
+ return { id, text: formatPlain(id) };
81
+ })
82
+ : Array.from({ length: 24 }, (_, i) => ({ id: i, text: format(i) }))
83
+ );
84
+ let minuteEntries: Entry[] = $derived(
85
+ Array.from({ length: Math.ceil(60 / Math.max(1, minuteStep)) }, (_, i) => {
86
+ const id = i * Math.max(1, minuteStep);
87
+ return { id, text: format(id) };
88
+ })
89
+ );
90
+ let periodEntries: Entry[] = $derived([
91
+ { id: 0, text: amLabel },
92
+ { id: 1, text: pmLabel }
93
+ ]);
94
+
30
95
  let hour = $derived(value ? Number(value.slice(0, 2)) : undefined);
31
96
  let minute = $derived(value ? Number(value.slice(3, 5)) : undefined);
97
+ let period = $derived(hour === undefined ? undefined : hour >= 12 ? 1 : 0);
98
+ let shownHour = $derived(hour === undefined ? undefined : twelve ? hour % 12 || 12 : hour);
32
99
 
33
- let hoursEl: HTMLElement | undefined = $state();
34
- let minutesEl: HTMLElement | undefined = $state();
100
+ let columnEls: Partial<Record<Column, HTMLElement>> = $state({});
35
101
 
36
102
  function set(nextHour: number, nextMinute: number) {
37
- value = `${pad(nextHour)}:${pad(nextMinute)}`;
103
+ value = `${String(nextHour).padStart(2, '0')}:${String(nextMinute).padStart(2, '0')}`;
38
104
  onselect?.(value);
39
105
  }
40
106
 
107
+ function choose(column: Column, id: number) {
108
+ if (column === 'minute') return set(hour ?? 0, id);
109
+ if (column === 'period') return set(((hour ?? 0) % 12) + id * 12, minute ?? 0);
110
+ set(twelve ? (id % 12) + (period ?? 0) * 12 : id, minute ?? 0);
111
+ }
112
+
41
113
  function center(column: HTMLElement | undefined) {
42
114
  const item = column?.querySelector<HTMLElement>('[tabindex="0"]');
43
115
  if (column && item)
@@ -47,14 +119,16 @@
47
119
  $effect(() => {
48
120
  void value;
49
121
  tick().then(() => {
50
- center(hoursEl);
51
- center(minutesEl);
122
+ center(columnEls.hour);
123
+ center(columnEls.minute);
52
124
  });
53
125
  });
54
126
 
55
- async function handleKeydown(event: KeyboardEvent, column: 'hour' | 'minute') {
56
- const list = column === 'hour' ? hours : minutes;
57
- const current = list.indexOf((column === 'hour' ? hour : minute) ?? list[0]);
127
+ async function handleKeydown(event: KeyboardEvent, column: Column) {
128
+ const list = { hour: hourEntries, minute: minuteEntries, period: periodEntries }[column];
129
+ const selected = { hour: shownHour, minute, period }[column];
130
+ const found = list.findIndex((entry) => entry.id === (selected ?? list[0].id));
131
+ const current = found === -1 ? 0 : found;
58
132
  const next = {
59
133
  ArrowUp: current - 1,
60
134
  ArrowDown: current + 1,
@@ -64,99 +138,154 @@
64
138
  if (next === undefined) return;
65
139
 
66
140
  event.preventDefault();
67
- const clamped = list[Math.min(list.length - 1, Math.max(0, next))];
68
- if (column === 'hour') set(clamped, minute ?? 0);
69
- else set(hour ?? 0, clamped);
141
+ choose(column, list[Math.min(list.length - 1, Math.max(0, next))].id);
70
142
  await tick();
71
- (column === 'hour' ? hoursEl : minutesEl)
72
- ?.querySelector<HTMLElement>('[tabindex="0"]')
73
- ?.focus();
143
+ columnEls[column]?.querySelector<HTMLElement>('[tabindex="0"]')?.focus();
74
144
  }
75
145
  </script>
76
146
 
77
- {#snippet columnItems(list: number[], current: number | undefined, column: 'hour' | 'minute')}
78
- {#each list as item, i (item)}
147
+ {#snippet columnItems(list: Entry[], current: number | undefined, column: Column)}
148
+ {#each list as item, i (item.id)}
79
149
  <button
80
150
  type="button"
81
- class={['time-picker-item', { 'time-picker-item--selected': item === current }]}
82
- tabindex={item === current || (current === undefined && i === 0) ? 0 : -1}
83
- aria-pressed={item === current}
84
- onclick={() => (column === 'hour' ? set(item, minute ?? 0) : set(hour ?? 0, item))}
151
+ class={['time-picker-item', { 'time-picker-item--selected': item.id === current }]}
152
+ tabindex={item.id === current || (current === undefined && i === 0) ? 0 : -1}
153
+ aria-pressed={item.id === current}
154
+ onclick={() => choose(column, item.id)}
85
155
  onkeydown={(event) => handleKeydown(event, column)}
86
156
  >
87
- {pad(item)}
157
+ {item.text}
88
158
  </button>
89
159
  {/each}
90
160
  {/snippet}
91
161
 
92
- <div {...rest} class={['time-picker', className]}>
93
- <div class="time-picker-column" role="group" aria-label="Hours" bind:this={hoursEl}>
94
- {@render columnItems(hours, hour, 'hour')}
162
+ <div {...rest} bind:this={ref} class={['time-picker', className]}>
163
+ <div class="time-picker-column" role="group" aria-label={hoursLabel} bind:this={columnEls.hour}>
164
+ {@render columnItems(hourEntries, shownHour, 'hour')}
95
165
  </div>
96
- <div class="time-picker-column" role="group" aria-label="Minutes" bind:this={minutesEl}>
97
- {@render columnItems(minutes, minute, 'minute')}
166
+ <div
167
+ class="time-picker-column"
168
+ role="group"
169
+ aria-label={minutesLabel}
170
+ bind:this={columnEls.minute}
171
+ >
172
+ {@render columnItems(minuteEntries, minute, 'minute')}
98
173
  </div>
174
+ {#if twelve}
175
+ <div
176
+ class="time-picker-column"
177
+ role="group"
178
+ aria-label={periodLabel}
179
+ bind:this={columnEls.period}
180
+ >
181
+ {@render columnItems(periodEntries, period, 'period')}
182
+ </div>
183
+ {/if}
99
184
  </div>
100
185
 
101
186
  <style>
102
- .time-picker {
103
- display: flex;
104
- gap: 4px;
105
- height: 232px;
106
- font-size: 13px;
107
- color: var(--color-text);
108
- user-select: none;
109
- }
187
+ @layer svelte-ui {
188
+ .time-picker {
189
+ display: flex;
190
+ gap: 4px;
191
+ height: 232px;
192
+ font-size: 13px;
193
+ color: var(--color-text);
194
+ user-select: none;
195
+ }
110
196
 
111
- .time-picker-column {
112
- display: flex;
113
- flex-direction: column;
114
- gap: 2px;
115
- width: 48px;
116
- padding: 0 2px;
117
- overflow-y: auto;
118
- scrollbar-width: none;
119
- scroll-snap-type: y proximity;
120
- }
197
+ .time-picker-column {
198
+ display: flex;
199
+ flex-direction: column;
200
+ gap: 2px;
201
+ width: 48px;
202
+ padding: 0 2px;
203
+ overflow-y: auto;
204
+ scrollbar-width: none;
205
+ scroll-snap-type: y proximity;
206
+ }
121
207
 
122
- .time-picker-column + .time-picker-column {
123
- padding-left: 6px;
124
- border-left: 1px solid var(--color-border);
125
- }
208
+ .time-picker-column + .time-picker-column {
209
+ padding-left: 6px;
210
+ border-left: 1px solid var(--color-border);
211
+ }
126
212
 
127
- .time-picker-item {
128
- flex-shrink: 0;
129
- height: 28px;
130
- padding: 0;
131
- font-family: inherit;
132
- font-size: 13px;
133
- font-variant-numeric: tabular-nums;
134
- color: var(--color-text);
135
- background-color: transparent;
136
- border: none;
137
- border-radius: 8px;
138
- outline: none;
139
- cursor: pointer;
140
- scroll-snap-align: center;
141
- transition: background-color 200ms ease;
142
- }
213
+ .time-picker-item {
214
+ flex-shrink: 0;
215
+ height: 28px;
216
+ padding: 0;
217
+ font-family: inherit;
218
+ font-size: 13px;
219
+ font-variant-numeric: tabular-nums;
220
+ color: var(--color-text);
221
+ background-color: transparent;
222
+ border: none;
223
+ border-radius: 8px;
224
+ outline: none;
225
+ cursor: pointer;
226
+ scroll-snap-align: center;
227
+ transition: background-color 200ms ease;
228
+ }
143
229
 
144
- .time-picker-item:hover {
145
- background-color: var(--color-surface);
146
- }
230
+ .time-picker-item:hover {
231
+ background-color: var(--color-surface);
232
+ }
147
233
 
148
- .time-picker-item:focus-visible {
149
- outline: 2px solid var(--color-accent);
150
- outline-offset: -2px;
151
- }
234
+ .time-picker-item:focus-visible {
235
+ outline: 2px solid var(--color-accent);
236
+ outline-offset: -2px;
237
+ }
152
238
 
153
- .time-picker-item--selected,
154
- .time-picker-item--selected:hover {
155
- color: var(--color-accent-foreground);
156
- background-color: var(--color-accent);
157
- }
239
+ .time-picker-item--selected,
240
+ .time-picker-item--selected:hover {
241
+ color: var(--color-accent-foreground);
242
+ background-color: var(--color-accent);
243
+ }
244
+
245
+ .time-picker-item--selected:focus-visible {
246
+ outline-color: var(--color-accent-foreground);
247
+ }
248
+
249
+ @media (pointer: coarse) {
250
+ .time-picker-column {
251
+ width: 56px;
252
+ }
253
+
254
+ .time-picker-item {
255
+ min-height: 44px;
256
+ }
257
+ }
258
+
259
+ @media (forced-colors: active) {
260
+ .time-picker-item {
261
+ color: ButtonText;
262
+ }
263
+
264
+ .time-picker-item--selected,
265
+ .time-picker-item--selected:hover {
266
+ color: HighlightText;
267
+ background-color: Highlight;
268
+ }
269
+
270
+ .time-picker-item:hover:not(.time-picker-item--selected) {
271
+ outline: 1px solid Highlight;
272
+ outline-offset: -1px;
273
+ }
274
+
275
+ .time-picker-item:focus-visible,
276
+ .time-picker-item--selected:focus-visible {
277
+ outline: 2px solid CanvasText;
278
+ }
279
+ }
280
+
281
+ @media (prefers-reduced-motion: reduce) {
282
+ .time-picker-item {
283
+ transition: none;
284
+ }
158
285
 
159
- .time-picker-item--selected:focus-visible {
160
- outline-color: var(--color-accent-foreground);
286
+ .time-picker-column {
287
+ scroll-behavior: auto;
288
+ }
289
+ }
161
290
  }
162
291
  </style>
@@ -1,11 +1,28 @@
1
+ export type TimePickerLabels = {
2
+ /** Accessible name of the hours column. */
3
+ hours?: string;
4
+ /** Accessible name of the minutes column. */
5
+ minutes?: string;
6
+ /** Accessible name of the AM/PM column. */
7
+ period?: string;
8
+ am?: string;
9
+ pm?: string;
10
+ };
1
11
  import type { HTMLAttributes } from 'svelte/elements';
2
12
  type Props = Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> & {
3
13
  /** Bindable, `HH:MM` in 24-hour time like `<input type="time">`. */
4
14
  value?: string;
5
15
  /** Minutes between the offered values. */
6
16
  minuteStep?: number;
17
+ /** BCP 47 tag used for numerals, AM/PM text and the 12/24-hour default. Without it the picker shows 24 hours. */
18
+ locale?: string;
19
+ /** Forces 12-hour (`true`) or 24-hour (`false`) columns, overriding the locale. */
20
+ hour12?: boolean;
21
+ /** Accessible names of the columns and the AM/PM text. */
22
+ labels?: TimePickerLabels;
7
23
  onselect?: (value: string) => void;
24
+ ref?: HTMLDivElement | null;
8
25
  };
9
- declare const TimePicker: import("svelte").Component<Props, {}, "value">;
26
+ declare const TimePicker: import("svelte").Component<Props, {}, "ref" | "value">;
10
27
  type TimePicker = ReturnType<typeof TimePicker>;
11
28
  export default TimePicker;
@@ -1,16 +1,49 @@
1
+ <script module lang="ts">
2
+ export type ToasterLabels = {
3
+ /** Accessible name of the notification region. */
4
+ region?: string;
5
+ /** Hint shown on a toast that closes when clicked. */
6
+ dismiss?: string;
7
+ };
8
+ </script>
9
+
1
10
  <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import type { HTMLAttributes } from 'svelte/elements';
2
13
  import type { Attachment } from 'svelte/attachments';
3
14
  import { fly } from 'svelte/transition';
4
15
  import Button from '../button/Button.svelte';
5
16
  import Spinner from '../spinner/Spinner.svelte';
6
17
  import { toast, toasts, type ToastData } from './toast.svelte.js';
7
18
 
8
- type Props = {
9
- /** Accessible name of the notification region. */
19
+ type Props = Omit<HTMLAttributes<HTMLOListElement>, 'children'> & {
20
+ /** Accessible name of the notification region. Shorthand for `labels.region`. */
10
21
  label?: string;
22
+ /** Texts of the region. */
23
+ labels?: ToasterLabels;
24
+ /** BCP 47 tag of the toast messages; sets `lang` on the region. */
25
+ locale?: string;
26
+ /** Replaces the message of each toast. */
27
+ message?: Snippet<[toast: ToastData]>;
28
+ ref?: HTMLOListElement | null;
11
29
  };
12
30
 
13
- let { label = 'Notifications' }: Props = $props();
31
+ let {
32
+ label,
33
+ labels,
34
+ locale,
35
+ message,
36
+ ref = $bindable(null),
37
+ class: className,
38
+ ...rest
39
+ }: Props = $props();
40
+
41
+ const regionLabel = $derived(label ?? labels?.region ?? 'Notifications');
42
+ const dismissLabel = $derived(labels?.dismiss ?? 'Dismiss');
43
+
44
+ function reducedMotion() {
45
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
46
+ }
14
47
 
15
48
  // Hovering or focusing the toast pauses its timer; they restart on leave.
16
49
  let paused = $state(false);
@@ -23,11 +56,13 @@
23
56
  };
24
57
 
25
58
  const exitDuration = 150;
59
+ const enterDuration = 200;
26
60
 
27
61
  // Waits for the outgoing toast, still in the DOM, to slide out first.
28
62
  function enter(node: HTMLElement) {
29
63
  const replacing = node.parentElement!.childElementCount > 1;
30
- return fly(node, { y: 24, duration: 200, delay: replacing ? exitDuration : 0 });
64
+ if (reducedMotion()) return fly(node, { y: 0, duration: 0 });
65
+ return fly(node, { y: 24, duration: enterDuration, delay: replacing ? exitDuration : 0 });
31
66
  }
32
67
 
33
68
  function timer(data: ToastData): Attachment {
@@ -41,8 +76,11 @@
41
76
  </script>
42
77
 
43
78
  <ol
44
- class="toaster"
45
- aria-label={label}
79
+ {...rest}
80
+ bind:this={ref}
81
+ class={['toaster', className]}
82
+ lang={locale}
83
+ aria-label={regionLabel}
46
84
  aria-live="polite"
47
85
  popover="manual"
48
86
  onpointerenter={() => (paused = true)}
@@ -56,18 +94,21 @@
56
94
  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
57
95
  <li
58
96
  class="toast"
97
+ title={dismissLabel}
59
98
  onclick={() => toast.dismiss(data.id)}
60
99
  aria-busy={data.loading || undefined}
61
100
  {@attach timer(data)}
62
101
  in:enter
63
- out:fly={{ y: 24, duration: exitDuration }}
102
+ out:fly={{ y: 24, duration: reducedMotion() ? 0 : exitDuration }}
64
103
  >
65
104
  {#if data.loading}
66
105
  <Spinner class="toast-spinner" aria-hidden="true" />
67
106
  {:else if data.icon}
68
107
  <data.icon class="toast-icon" aria-hidden="true" />
69
108
  {/if}
70
- <span class="toast-message">{data.message}</span>
109
+ <span class="toast-message">
110
+ {#if message}{@render message(data)}{:else}{data.message}{/if}
111
+ </span>
71
112
  {#if data.action}
72
113
  {@const action = data.action}
73
114
  <Button class="toast-action" variant="secondary" size="sm" onclick={action.onclick}>
@@ -79,73 +120,94 @@
79
120
  </ol>
80
121
 
81
122
  <style>
82
- .toaster {
83
- position: fixed;
84
- inset: auto 0 16px;
85
- /* One cell: the outgoing toast overlaps the incoming one instead of stacking */
86
- display: grid;
87
- /* minmax(0, …) keeps a long message from widening the column past the screen */
88
- grid-template-columns: minmax(0, 1fr);
89
- justify-items: center;
90
- align-items: end;
91
- width: auto;
92
- height: auto;
93
- margin: 0;
94
- padding: 0 16px;
95
- overflow: visible;
96
- list-style: none;
97
- background: none;
98
- border: none;
99
- pointer-events: none;
100
- }
123
+ @layer svelte-ui {
124
+ .toaster {
125
+ position: fixed;
126
+ inset: auto 0 16px;
127
+ /* One cell: the outgoing toast overlaps the incoming one instead of stacking */
128
+ display: grid;
129
+ /* minmax(0, …) keeps a long message from widening the column past the screen */
130
+ grid-template-columns: minmax(0, 1fr);
131
+ justify-items: center;
132
+ align-items: end;
133
+ width: auto;
134
+ height: auto;
135
+ margin: 0;
136
+ padding: 0 16px;
137
+ overflow: visible;
138
+ list-style: none;
139
+ background: none;
140
+ border: none;
141
+ pointer-events: none;
142
+ }
101
143
 
102
- .toast {
103
- grid-area: 1 / 1;
104
- display: flex;
105
- align-items: center;
106
- gap: 10px;
107
- box-sizing: border-box;
108
- max-width: min(420px, 100%);
109
- height: 44px;
110
- padding: 8px 16px 8px 14px;
111
- font-size: 13px;
112
- line-height: 1.4;
113
- background-color: var(--color-bg);
114
- color: var(--color-text);
115
- border: 1px solid var(--color-border);
116
- border-radius: 16px;
117
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
118
- pointer-events: auto;
119
- cursor: pointer;
120
- }
144
+ .toast {
145
+ grid-area: 1 / 1;
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 10px;
149
+ box-sizing: border-box;
150
+ max-width: min(420px, 100%);
151
+ height: 44px;
152
+ padding: 8px 16px 8px 14px;
153
+ font-size: 13px;
154
+ line-height: 1.4;
155
+ background-color: var(--color-bg);
156
+ color: var(--color-text);
157
+ border: 1px solid var(--color-border);
158
+ border-radius: 16px;
159
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
160
+ pointer-events: auto;
161
+ cursor: pointer;
162
+ }
121
163
 
122
- /* Same gap to the right edge as above and below the 28px button: (44 - 2 - 28) / 2 */
123
- .toast:has(:global(.toast-action)) {
124
- padding-right: 7px;
125
- }
164
+ /* Same gap to the right edge as above and below the 28px button: (44 - 2 - 28) / 2 */
165
+ .toast:has(:global(.toast-action)) {
166
+ padding-right: 7px;
167
+ }
126
168
 
127
- /* Same sizes as in a Button */
128
- .toast :global(.toast-icon) {
129
- width: 16px;
130
- height: 16px;
131
- flex-shrink: 0;
132
- }
169
+ /* Same sizes as in a Button */
170
+ .toast :global(.toast-icon) {
171
+ width: 16px;
172
+ height: 16px;
173
+ flex-shrink: 0;
174
+ }
133
175
 
134
- .toast :global(.toast-spinner) {
135
- --spinner-size: 14px;
136
- margin: 0 1px;
137
- }
176
+ .toast :global(.toast-spinner) {
177
+ --spinner-size: 14px;
178
+ margin: 0 1px;
179
+ }
138
180
 
139
- .toast-message {
140
- flex: 1;
141
- min-width: 0;
142
- overflow: hidden;
143
- white-space: nowrap;
144
- text-overflow: ellipsis;
145
- }
181
+ .toast-message {
182
+ flex: 1;
183
+ min-width: 0;
184
+ overflow: hidden;
185
+ white-space: nowrap;
186
+ text-overflow: ellipsis;
187
+ }
188
+
189
+ .toast :global(.toast-action) {
190
+ flex-shrink: 0;
191
+ margin-left: 6px;
192
+ }
193
+
194
+ @media (pointer: coarse) {
195
+ .toast {
196
+ height: auto;
197
+ min-height: 56px;
198
+ }
199
+
200
+ .toast :global(.toast-action) {
201
+ min-width: 44px;
202
+ min-height: 44px;
203
+ }
204
+ }
146
205
 
147
- .toast :global(.toast-action) {
148
- flex-shrink: 0;
149
- margin-left: 6px;
206
+ @media (forced-colors: active) {
207
+ .toast {
208
+ border-color: CanvasText;
209
+ box-shadow: none;
210
+ }
211
+ }
150
212
  }
151
213
  </style>
@@ -1,7 +1,23 @@
1
- type Props = {
1
+ export type ToasterLabels = {
2
2
  /** Accessible name of the notification region. */
3
+ region?: string;
4
+ /** Hint shown on a toast that closes when clicked. */
5
+ dismiss?: string;
6
+ };
7
+ import type { Snippet } from 'svelte';
8
+ import type { HTMLAttributes } from 'svelte/elements';
9
+ import { type ToastData } from './toast.svelte.js';
10
+ type Props = Omit<HTMLAttributes<HTMLOListElement>, 'children'> & {
11
+ /** Accessible name of the notification region. Shorthand for `labels.region`. */
3
12
  label?: string;
13
+ /** Texts of the region. */
14
+ labels?: ToasterLabels;
15
+ /** BCP 47 tag of the toast messages; sets `lang` on the region. */
16
+ locale?: string;
17
+ /** Replaces the message of each toast. */
18
+ message?: Snippet<[toast: ToastData]>;
19
+ ref?: HTMLOListElement | null;
4
20
  };
5
- declare const Toaster: import("svelte").Component<Props, {}, "">;
21
+ declare const Toaster: import("svelte").Component<Props, {}, "ref">;
6
22
  type Toaster = ReturnType<typeof Toaster>;
7
23
  export default Toaster;