svelte-comp 1.3.3 → 1.3.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 (138) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -100
  3. package/dist/App.svelte +507 -507
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -235
  6. package/dist/app.d.ts +10 -0
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -170
  10. package/dist/lib/Calendar.svelte +384 -384
  11. package/dist/lib/Card.svelte +103 -103
  12. package/dist/lib/Carousel.svelte +293 -293
  13. package/dist/lib/Carousel.svelte.d.ts +1 -1
  14. package/dist/lib/CheckBox.svelte +210 -210
  15. package/dist/lib/CodeView.svelte +308 -307
  16. package/dist/lib/ColorPicker.svelte +159 -159
  17. package/dist/lib/ContextMenu.svelte +328 -322
  18. package/dist/lib/DatePicker.svelte +246 -246
  19. package/dist/lib/Dialog.svelte +233 -233
  20. package/dist/lib/Field.svelte +299 -299
  21. package/dist/lib/FilePicker.svelte +295 -240
  22. package/dist/lib/FilePicker.svelte.d.ts +6 -1
  23. package/dist/lib/Form.svelte +438 -438
  24. package/dist/lib/Hamburger.svelte +217 -217
  25. package/dist/lib/InstallPWA.svelte +94 -94
  26. package/dist/lib/Menu.svelte +623 -623
  27. package/dist/lib/NoticeBase.svelte +140 -140
  28. package/dist/lib/PaginatedCard.svelte +73 -73
  29. package/dist/lib/Pagination.svelte +119 -119
  30. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  31. package/dist/lib/ProgressBar.svelte +141 -141
  32. package/dist/lib/ProgressCircle.svelte +190 -190
  33. package/dist/lib/Radio.svelte +189 -189
  34. package/dist/lib/SearchInput.svelte +104 -104
  35. package/dist/lib/Select.svelte +524 -524
  36. package/dist/lib/Slider.svelte +253 -253
  37. package/dist/lib/Splitter.svelte +159 -150
  38. package/dist/lib/Switch.svelte +168 -167
  39. package/dist/lib/Table.svelte +299 -299
  40. package/dist/lib/Tabs.svelte +213 -213
  41. package/dist/lib/ThemeToggle.svelte +128 -127
  42. package/dist/lib/TimePicker.svelte +312 -312
  43. package/dist/lib/TimePickerNew.svelte +634 -0
  44. package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
  45. package/dist/lib/Toast.svelte +123 -123
  46. package/dist/lib/Tooltip.svelte +110 -110
  47. package/dist/lib/Topbar.svelte +107 -107
  48. package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
  49. package/dist/lib/__tests__/Accordion.test.js +171 -0
  50. package/dist/lib/__tests__/Badge.test.d.ts +1 -0
  51. package/dist/lib/__tests__/Badge.test.js +41 -0
  52. package/dist/lib/__tests__/Button.test.d.ts +1 -0
  53. package/dist/lib/__tests__/Button.test.js +269 -0
  54. package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
  55. package/dist/lib/__tests__/Calendar.test.js +171 -0
  56. package/dist/lib/__tests__/Card.test.d.ts +1 -0
  57. package/dist/lib/__tests__/Card.test.js +148 -0
  58. package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
  59. package/dist/lib/__tests__/Carousel.test.js +439 -0
  60. package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
  61. package/dist/lib/__tests__/CheckBox.test.js +152 -0
  62. package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
  63. package/dist/lib/__tests__/CodeView.test.js +157 -0
  64. package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
  65. package/dist/lib/__tests__/ColorPicker.test.js +93 -0
  66. package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
  67. package/dist/lib/__tests__/ContextMenu.test.js +67 -0
  68. package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
  69. package/dist/lib/__tests__/DatePicker.test.js +108 -0
  70. package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
  71. package/dist/lib/__tests__/Dialog.test.js +183 -0
  72. package/dist/lib/__tests__/Field.test.d.ts +1 -0
  73. package/dist/lib/__tests__/Field.test.js +190 -0
  74. package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
  75. package/dist/lib/__tests__/FilePicker.test.js +179 -0
  76. package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
  77. package/dist/lib/__tests__/Form.integration.test.js +158 -0
  78. package/dist/lib/__tests__/Form.test.d.ts +1 -0
  79. package/dist/lib/__tests__/Form.test.js +463 -0
  80. package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
  81. package/dist/lib/__tests__/Hamburger.test.js +161 -0
  82. package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
  83. package/dist/lib/__tests__/InstallPWA.test.js +15 -0
  84. package/dist/lib/__tests__/Menu.test.d.ts +1 -0
  85. package/dist/lib/__tests__/Menu.test.js +285 -0
  86. package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
  87. package/dist/lib/__tests__/NoticeBase.test.js +60 -0
  88. package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
  89. package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
  90. package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
  91. package/dist/lib/__tests__/Pagination.test.js +168 -0
  92. package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
  93. package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
  94. package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
  95. package/dist/lib/__tests__/ProgressBar.test.js +69 -0
  96. package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
  97. package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
  98. package/dist/lib/__tests__/Radio.test.d.ts +1 -0
  99. package/dist/lib/__tests__/Radio.test.js +127 -0
  100. package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
  101. package/dist/lib/__tests__/SearchInput.test.js +80 -0
  102. package/dist/lib/__tests__/Select.test.d.ts +1 -0
  103. package/dist/lib/__tests__/Select.test.js +408 -0
  104. package/dist/lib/__tests__/Slider.test.d.ts +1 -0
  105. package/dist/lib/__tests__/Slider.test.js +213 -0
  106. package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
  107. package/dist/lib/__tests__/Splitter.test.js +87 -0
  108. package/dist/lib/__tests__/Switch.test.d.ts +1 -0
  109. package/dist/lib/__tests__/Switch.test.js +97 -0
  110. package/dist/lib/__tests__/Table.test.d.ts +1 -0
  111. package/dist/lib/__tests__/Table.test.js +349 -0
  112. package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
  113. package/dist/lib/__tests__/Tabs.test.js +262 -0
  114. package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
  115. package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
  116. package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
  117. package/dist/lib/__tests__/TimePicker.test.js +146 -0
  118. package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
  119. package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
  120. package/dist/lib/__tests__/Toast.test.d.ts +1 -0
  121. package/dist/lib/__tests__/Toast.test.js +135 -0
  122. package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
  123. package/dist/lib/__tests__/Tooltip.test.js +171 -0
  124. package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
  125. package/dist/lib/__tests__/Topbar.test.js +25 -0
  126. package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
  127. package/dist/lib/__tests__/setupLangContext.js +65 -0
  128. package/dist/lib/__tests__/storage.test.d.ts +1 -0
  129. package/dist/lib/__tests__/storage.test.js +124 -0
  130. package/dist/lib/__tests__/utils.test.d.ts +1 -0
  131. package/dist/lib/__tests__/utils.test.js +11 -0
  132. package/dist/lib/index.d.ts +1 -0
  133. package/dist/lib/index.js +1 -0
  134. package/dist/lib/lang.d.ts +4 -0
  135. package/dist/lib/lang.js +4 -0
  136. package/dist/styles.css +234 -232
  137. package/dist/utils/index.js +15 -4
  138. package/package.json +52 -52
@@ -1,217 +1,217 @@
1
- <!-- src/lib/Hamburger.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component Hamburger
5
- * @description Off-canvas navigation drawer controlled by a floating hamburger button.
6
- *
7
- * @prop menuItems {Item[]} - Menu entries rendered in the drawer
8
- * @default []
9
- *
10
- * @prop activeItem {string} - ID of the currently active item
11
- * @default ""
12
- *
13
- * @prop header {Snippet} - Custom content rendered above the menu
14
- *
15
- * @prop footer {Snippet} - Custom content rendered below the menu
16
- *
17
- * @prop closeOnSelect {boolean} - Automatically closes after selecting an item
18
- * @default true
19
- *
20
- * @prop onSelect {(id: string) => void} - Fired when a menu item is chosen
21
- *
22
- * @prop onOpenChange {(v: boolean) => void} - Fired when open state changes in controlled mode
23
- *
24
- * @prop pressed {boolean} - Controlled open state
25
- *
26
- * @prop class {string} - Extra classes applied to the trigger button
27
- * @default ""
28
- *
29
- * @prop width {number | string} - Drawer width (px or CSS value)
30
- * @default 300
31
- *
32
- * @note Clicking outside the panel or pressing `Escape` closes the drawer.
33
- * @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
34
- * @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
35
- * @note When `menuItems` is empty, a "No items" placeholder is shown.
36
- * @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
37
- */
38
- import type { Snippet } from "svelte";
39
- import type { Item } from "./types";
40
- import { cx, throttle, focusFirstInteractive, trapFocus } from "../utils";
41
-
42
- type Props = {
43
- menuItems?: Item[];
44
- activeItem?: string;
45
- header?: Snippet;
46
- footer?: Snippet;
47
- closeOnSelect?: boolean;
48
- onSelect?: (id: string) => void;
49
- onOpenChange?: (v: boolean) => void;
50
- pressed?: boolean;
51
- class?: string;
52
- width?: number | string;
53
- };
54
-
55
- let {
56
- menuItems = [],
57
- activeItem = "",
58
- header,
59
- footer,
60
- closeOnSelect = true,
61
- onSelect,
62
- onOpenChange,
63
- pressed,
64
- class: externalClass = "",
65
- width = 300,
66
- }: Props = $props();
67
-
68
- let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
69
- let panelEl = $state<HTMLDivElement | undefined>(undefined);
70
- let releaseFocus: (() => void) | null = null;
71
-
72
- let _open = $state(false);
73
- const open = $derived(pressed ?? _open);
74
-
75
- function setOpen(v: boolean) {
76
- if (pressed === undefined) {
77
- _open = v;
78
- } else {
79
- onOpenChange?.(v);
80
- }
81
- }
82
-
83
- function toggle() {
84
- setOpen(!open);
85
- }
86
-
87
- function closeMenu() {
88
- setOpen(false);
89
- queueMicrotask(() => triggerEl?.focus());
90
- }
91
-
92
- const throttledClose = throttle(() => closeMenu(), 150);
93
-
94
- function handleKeydown(e: KeyboardEvent) {
95
- if (e.key === "Escape") throttledClose();
96
- }
97
-
98
- $effect(() => {
99
- if (open && panelEl) {
100
- queueMicrotask(() => {
101
- focusFirstInteractive(panelEl!);
102
- });
103
- releaseFocus?.();
104
- releaseFocus = trapFocus(panelEl);
105
- document.addEventListener("keydown", handleKeydown);
106
- } else {
107
- releaseFocus?.();
108
- releaseFocus = null;
109
- document.removeEventListener("keydown", handleKeydown);
110
- }
111
-
112
- return () => {
113
- document.removeEventListener("keydown", handleKeydown);
114
- releaseFocus?.();
115
- releaseFocus = null;
116
- };
117
- });
118
-
119
- const triggerBase =
120
- "fixed top-4 left-4 inline-flex items-center justify-center h-8 w-8 rounded-md border border-[var(--border-color-default)] bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] transition-colors z-[var(--z-modal)]";
121
-
122
- const triggerClass = $derived(cx(triggerBase, externalClass));
123
- </script>
124
-
125
- <button
126
- type="button"
127
- aria-label="Toggle navigation"
128
- aria-expanded={open}
129
- class={triggerClass}
130
- onclick={toggle}
131
- bind:this={triggerEl}
132
- >
133
- <span class="relative block w-5 h-3.5">
134
- <span
135
- class={cx(
136
- "absolute left-0 top-1/2 h-[2px] w-full bg-current transition-transform duration-200",
137
- open ? "translate-y-[-50%] rotate-45" : "translate-y-[calc(-50%_-_6px)]"
138
- )}
139
- ></span>
140
- <span
141
- class={cx(
142
- "absolute left-0 top-1/2 h-[2px] w-full bg-current transition-opacity duration-200 translate-y-[-50%]",
143
- open ? "opacity-0" : "opacity-100"
144
- )}
145
- ></span>
146
- <span
147
- class={cx(
148
- "absolute left-0 top-1/2 h-[2px] w-full bg-current transition-transform duration-200",
149
- open
150
- ? "translate-y-[-50%] -rotate-45"
151
- : "translate-y-[calc(-50%_+_6px)]"
152
- )}
153
- ></span>
154
- </span>
155
- </button>
156
-
157
- {#if open}
158
- <div class="fixed inset-0 z-[var(--z-overlay)] flex">
159
- <div
160
- role="dialog"
161
- aria-modal="true"
162
- tabindex="-1"
163
- bind:this={panelEl}
164
- class="flex flex-col h-full bg-[var(--color-bg-surface)] shadow-xl"
165
- style={`width:${typeof width === "number" ? `${width}px` : width}`}
166
- >
167
- {#if header}
168
- <div class="p-4 border-b border-[var(--border-color-default)]">
169
- {@render header?.()}
170
- </div>
171
- {/if}
172
-
173
- <div class="flex-1 overflow-y-auto pt-10" tabindex="-1">
174
- {#if menuItems.length === 0}
175
- <div class="text-xs opacity-70 px-3 py-2 text-center">No items</div>
176
- {:else}
177
- <ul class="grid gap-2 p-4">
178
- {#each menuItems as it (it.id)}
179
- {#if it.type === "section"}
180
- <li class="px-3 pt-2 mt-3 text-[var(--color-text-muted)] text-[var(--text-xs)] lowercase tracking-wide opacity-70">
181
- {it.label}
182
- </li>
183
- {:else}
184
- <li>
185
- <button
186
- type="button"
187
- class="w-full text-left px-3 py-2 rounded-md hover:bg-[var(--color-bg-hover)] focus:outline-[var(--border-color-focus)] focus:outline-2 transition-colors"
188
- aria-current={activeItem === it.id ? "page" : undefined}
189
- onclick={() => {
190
- onSelect?.(it.id);
191
- if (closeOnSelect) closeMenu();
192
- }}
193
- >
194
- {it.label}
195
- </button>
196
- </li>
197
- {/if}
198
- {/each}
199
- </ul>
200
- {/if}
201
- </div>
202
-
203
- {#if footer}
204
- <div class="p-4 border-t border-[var(--border-color-default)]">
205
- {@render footer?.()}
206
- </div>
207
- {/if}
208
- </div>
209
-
210
- <button
211
- type="button"
212
- class="flex-1 bg-black/40"
213
- aria-hidden="true"
214
- onclick={closeMenu}
215
- ></button>
216
- </div>
217
- {/if}
1
+ <!-- src/lib/Hamburger.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component Hamburger
5
+ * @description Off-canvas navigation drawer controlled by a floating hamburger button.
6
+ *
7
+ * @prop menuItems {Item[]} - Menu entries rendered in the drawer
8
+ * @default []
9
+ *
10
+ * @prop activeItem {string} - ID of the currently active item
11
+ * @default ""
12
+ *
13
+ * @prop header {Snippet} - Custom content rendered above the menu
14
+ *
15
+ * @prop footer {Snippet} - Custom content rendered below the menu
16
+ *
17
+ * @prop closeOnSelect {boolean} - Automatically closes after selecting an item
18
+ * @default true
19
+ *
20
+ * @prop onSelect {(id: string) => void} - Fired when a menu item is chosen
21
+ *
22
+ * @prop onOpenChange {(v: boolean) => void} - Fired when open state changes in controlled mode
23
+ *
24
+ * @prop pressed {boolean} - Controlled open state
25
+ *
26
+ * @prop class {string} - Extra classes applied to the trigger button
27
+ * @default ""
28
+ *
29
+ * @prop width {number | string} - Drawer width (px or CSS value)
30
+ * @default 300
31
+ *
32
+ * @note Clicking outside the panel or pressing `Escape` closes the drawer.
33
+ * @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
34
+ * @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
35
+ * @note When `menuItems` is empty, a "No items" placeholder is shown.
36
+ * @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
37
+ */
38
+ import type { Snippet } from "svelte";
39
+ import type { Item } from "./types";
40
+ import { cx, throttle, focusFirstInteractive, trapFocus } from "../utils";
41
+
42
+ type Props = {
43
+ menuItems?: Item[];
44
+ activeItem?: string;
45
+ header?: Snippet;
46
+ footer?: Snippet;
47
+ closeOnSelect?: boolean;
48
+ onSelect?: (id: string) => void;
49
+ onOpenChange?: (v: boolean) => void;
50
+ pressed?: boolean;
51
+ class?: string;
52
+ width?: number | string;
53
+ };
54
+
55
+ let {
56
+ menuItems = [],
57
+ activeItem = "",
58
+ header,
59
+ footer,
60
+ closeOnSelect = true,
61
+ onSelect,
62
+ onOpenChange,
63
+ pressed,
64
+ class: externalClass = "",
65
+ width = 300,
66
+ }: Props = $props();
67
+
68
+ let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
69
+ let panelEl = $state<HTMLDivElement | undefined>(undefined);
70
+ let releaseFocus: (() => void) | null = null;
71
+
72
+ let _open = $state(false);
73
+ const open = $derived(pressed ?? _open);
74
+
75
+ function setOpen(v: boolean) {
76
+ if (pressed === undefined) {
77
+ _open = v;
78
+ } else {
79
+ onOpenChange?.(v);
80
+ }
81
+ }
82
+
83
+ function toggle() {
84
+ setOpen(!open);
85
+ }
86
+
87
+ function closeMenu() {
88
+ setOpen(false);
89
+ queueMicrotask(() => triggerEl?.focus());
90
+ }
91
+
92
+ const throttledClose = throttle(() => closeMenu(), 150);
93
+
94
+ function handleKeydown(e: KeyboardEvent) {
95
+ if (e.key === "Escape") throttledClose();
96
+ }
97
+
98
+ $effect(() => {
99
+ if (open && panelEl) {
100
+ queueMicrotask(() => {
101
+ focusFirstInteractive(panelEl!);
102
+ });
103
+ releaseFocus?.();
104
+ releaseFocus = trapFocus(panelEl);
105
+ document.addEventListener("keydown", handleKeydown);
106
+ } else {
107
+ releaseFocus?.();
108
+ releaseFocus = null;
109
+ document.removeEventListener("keydown", handleKeydown);
110
+ }
111
+
112
+ return () => {
113
+ document.removeEventListener("keydown", handleKeydown);
114
+ releaseFocus?.();
115
+ releaseFocus = null;
116
+ };
117
+ });
118
+
119
+ const triggerBase =
120
+ "fixed top-4 left-4 inline-flex items-center justify-center h-8 w-8 rounded-[var(--radius-md)] [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11 border border-[var(--border-color-default)] bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] transition-colors z-[var(--z-modal)]";
121
+
122
+ const triggerClass = $derived(cx(triggerBase, externalClass));
123
+ </script>
124
+
125
+ <button
126
+ type="button"
127
+ aria-label="Toggle navigation"
128
+ aria-expanded={open}
129
+ class={triggerClass}
130
+ onclick={toggle}
131
+ bind:this={triggerEl}
132
+ >
133
+ <span class="relative block w-5 h-3.5">
134
+ <span
135
+ class={cx(
136
+ "absolute left-0 top-1/2 h-[2px] w-full bg-current transition-transform duration-[var(--transition-fast)]",
137
+ open ? "translate-y-[-50%] rotate-45" : "translate-y-[calc(-50%_-_6px)]"
138
+ )}
139
+ ></span>
140
+ <span
141
+ class={cx(
142
+ "absolute left-0 top-1/2 h-[2px] w-full bg-current transition-opacity duration-[var(--transition-fast)] translate-y-[-50%]",
143
+ open ? "opacity-0" : "opacity-100"
144
+ )}
145
+ ></span>
146
+ <span
147
+ class={cx(
148
+ "absolute left-0 top-1/2 h-[2px] w-full bg-current transition-transform duration-[var(--transition-fast)]",
149
+ open
150
+ ? "translate-y-[-50%] -rotate-45"
151
+ : "translate-y-[calc(-50%_+_6px)]"
152
+ )}
153
+ ></span>
154
+ </span>
155
+ </button>
156
+
157
+ {#if open}
158
+ <div class="fixed inset-0 z-[var(--z-overlay)] flex">
159
+ <div
160
+ role="dialog"
161
+ aria-modal="true"
162
+ tabindex="-1"
163
+ bind:this={panelEl}
164
+ class="flex flex-col h-full bg-[var(--color-bg-surface)] shadow-xl"
165
+ style={`width:${typeof width === "number" ? `${width}px` : width}`}
166
+ >
167
+ {#if header}
168
+ <div class="p-[var(--spacing-md)] border-b border-[var(--border-color-default)]">
169
+ {@render header?.()}
170
+ </div>
171
+ {/if}
172
+
173
+ <div class="flex-1 overflow-y-auto" tabindex="-1">
174
+ {#if menuItems.length === 0}
175
+ <div class="[font-size:var(--text-xs)] opacity-70 px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[var(--spacing-sm)] text-center">No items</div>
176
+ {:else}
177
+ <ul class="grid gap-[var(--spacing-sm)] p-[var(--spacing-md)]">
178
+ {#each menuItems as it (it.id)}
179
+ {#if it.type === "section"}
180
+ <li class="px-[calc(var(--spacing-sm)+var(--spacing-xs))] pt-[var(--spacing-sm)] mt-[calc(var(--spacing-sm)+var(--spacing-xs))] text-[var(--color-text-muted)] [font-size:var(--text-xs)] lowercase tracking-wide opacity-70">
181
+ {it.label}
182
+ </li>
183
+ {:else}
184
+ <li>
185
+ <button
186
+ type="button"
187
+ class="w-full text-left px-[calc(var(--spacing-sm)+var(--spacing-xs))] py-[var(--spacing-sm)] rounded-[var(--radius-md)] hover:bg-[var(--color-bg-hover)] focus:outline-[var(--border-color-focus)] focus:outline-2 transition-colors"
188
+ aria-current={activeItem === it.id ? "page" : undefined}
189
+ onclick={() => {
190
+ onSelect?.(it.id);
191
+ if (closeOnSelect) closeMenu();
192
+ }}
193
+ >
194
+ {it.label}
195
+ </button>
196
+ </li>
197
+ {/if}
198
+ {/each}
199
+ </ul>
200
+ {/if}
201
+ </div>
202
+
203
+ {#if footer}
204
+ <div class="p-[var(--spacing-md)] border-t border-[var(--border-color-default)]">
205
+ {@render footer?.()}
206
+ </div>
207
+ {/if}
208
+ </div>
209
+
210
+ <button
211
+ type="button"
212
+ class="flex-1 bg-[oklch(0_0_0/0.4)]"
213
+ aria-hidden="true"
214
+ onclick={closeMenu}
215
+ ></button>
216
+ </div>
217
+ {/if}
@@ -1,94 +1,94 @@
1
- <!-- src/lib/InstallPWA.svelte -->
2
- <script lang="ts">
3
- /**
4
- * @component InstallPWA
5
- * @description Installs the app using the browser PWA prompt.
6
- *
7
- * @prop alwaysShow {boolean} - Forces the install button to be visible
8
- * @default false
9
- *
10
- * @prop inline {boolean} - Renders the button inline instead of fixed
11
- * @default false
12
- *
13
- * @prop class {string} - Additional button classes
14
- * @default ""
15
- *
16
- * @note Uses the `beforeinstallprompt` event and defers the prompt until user action.
17
- */
18
- import Button from "./Button.svelte";
19
-
20
- interface BeforeInstallPromptEvent extends Event {
21
- prompt: () => Promise<void>;
22
- userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
23
- }
24
-
25
- type Props = {
26
- alwaysShow?: boolean;
27
- inline?: boolean;
28
- class?: string;
29
- };
30
-
31
- let {
32
- alwaysShow = false,
33
- inline = false,
34
- class: externalClass = "",
35
- }: Props = $props();
36
-
37
- let deferredPrompt = $state<BeforeInstallPromptEvent | null>(null);
38
- let showButton = $state(false);
39
-
40
- $effect(() => {
41
- const handler = (e: Event) => {
42
- const bipEvent = e as BeforeInstallPromptEvent;
43
- bipEvent.preventDefault();
44
- deferredPrompt = bipEvent;
45
- showButton = true;
46
- };
47
-
48
- window.addEventListener("beforeinstallprompt", handler);
49
- return () => window.removeEventListener("beforeinstallprompt", handler);
50
- });
51
-
52
- function installPWA() {
53
- if (deferredPrompt) {
54
- deferredPrompt.prompt();
55
- deferredPrompt.userChoice.then(() => {
56
- showButton = false;
57
- });
58
- }
59
- }
60
- </script>
61
-
62
- {#if showButton || alwaysShow}
63
- <Button
64
- onClick={installPWA}
65
- sz="sm"
66
- class={inline
67
- ? `z-[1000] flex items-center gap-2.5 ${externalClass}`
68
- : `fixed bottom-5 right-5 z-[10] flex items-center gap-2.5 ${externalClass}`}
69
- variant="pill"
70
- >
71
- <span class="flex items-center gap-2">
72
- <svg
73
- xmlns="http://www.w3.org/2000/svg"
74
- width="24"
75
- height="24"
76
- viewBox="0 0 24 24"
77
- fill="none"
78
- stroke="currentColor"
79
- stroke-width="2"
80
- stroke-linecap="round"
81
- stroke-linejoin="round"
82
- class="w-4 h-4"
83
- aria-hidden="true"
84
- >
85
- <path d="M12 15V3" />
86
- <path
87
- d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
88
- />
89
- <path d="m7 10 5 5 5-5" />
90
- </svg>
91
- <span>Install App</span>
92
- </span>
93
- </Button>
94
- {/if}
1
+ <!-- src/lib/InstallPWA.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * @component InstallPWA
5
+ * @description Installs the app using the browser PWA prompt.
6
+ *
7
+ * @prop alwaysShow {boolean} - Forces the install button to be visible
8
+ * @default false
9
+ *
10
+ * @prop inline {boolean} - Renders the button inline instead of fixed
11
+ * @default false
12
+ *
13
+ * @prop class {string} - Additional button classes
14
+ * @default ""
15
+ *
16
+ * @note Uses the `beforeinstallprompt` event and defers the prompt until user action.
17
+ */
18
+ import Button from "./Button.svelte";
19
+
20
+ interface BeforeInstallPromptEvent extends Event {
21
+ prompt: () => Promise<void>;
22
+ userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>;
23
+ }
24
+
25
+ type Props = {
26
+ alwaysShow?: boolean;
27
+ inline?: boolean;
28
+ class?: string;
29
+ };
30
+
31
+ let {
32
+ alwaysShow = false,
33
+ inline = false,
34
+ class: externalClass = "",
35
+ }: Props = $props();
36
+
37
+ let deferredPrompt = $state<BeforeInstallPromptEvent | null>(null);
38
+ let showButton = $state(false);
39
+
40
+ $effect(() => {
41
+ const handler = (e: Event) => {
42
+ const bipEvent = e as BeforeInstallPromptEvent;
43
+ bipEvent.preventDefault();
44
+ deferredPrompt = bipEvent;
45
+ showButton = true;
46
+ };
47
+
48
+ window.addEventListener("beforeinstallprompt", handler);
49
+ return () => window.removeEventListener("beforeinstallprompt", handler);
50
+ });
51
+
52
+ function installPWA() {
53
+ if (deferredPrompt) {
54
+ deferredPrompt.prompt();
55
+ deferredPrompt.userChoice.then(() => {
56
+ showButton = false;
57
+ });
58
+ }
59
+ }
60
+ </script>
61
+
62
+ {#if showButton || alwaysShow}
63
+ <Button
64
+ onClick={installPWA}
65
+ sz="sm"
66
+ class={inline
67
+ ? `z-[1000] flex items-center gap-2.5 ${externalClass}`
68
+ : `fixed bottom-5 right-5 z-[10] flex items-center gap-2.5 ${externalClass}`}
69
+ variant="pill"
70
+ >
71
+ <span class="flex items-center gap-2">
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ width="24"
75
+ height="24"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ stroke-width="2"
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ class="w-4 h-4"
83
+ aria-hidden="true"
84
+ >
85
+ <path d="M12 15V3" />
86
+ <path
87
+ d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
88
+ />
89
+ <path d="m7 10 5 5 5-5" />
90
+ </svg>
91
+ <span>Install App</span>
92
+ </span>
93
+ </Button>
94
+ {/if}