svelte-comp 1.3.5 → 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 (46) hide show
  1. package/LICENSE.md +21 -21
  2. package/README.md +101 -101
  3. package/dist/App.svelte +1046 -1046
  4. package/dist/Container.svelte +59 -59
  5. package/dist/app.css +234 -234
  6. package/dist/app.d.ts +10 -10
  7. package/dist/lib/Accordion.svelte +155 -155
  8. package/dist/lib/Badge.svelte +44 -44
  9. package/dist/lib/Button.svelte +185 -185
  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/CheckBox.svelte +210 -210
  14. package/dist/lib/CodeView.svelte +308 -308
  15. package/dist/lib/ColorPicker.svelte +159 -159
  16. package/dist/lib/ContextMenu.svelte +328 -328
  17. package/dist/lib/DatePicker.svelte +246 -246
  18. package/dist/lib/Dialog.svelte +233 -233
  19. package/dist/lib/Field.svelte +299 -299
  20. package/dist/lib/FilePicker.svelte +295 -295
  21. package/dist/lib/Form.svelte +438 -438
  22. package/dist/lib/Hamburger.svelte +217 -217
  23. package/dist/lib/InstallPWA.svelte +94 -94
  24. package/dist/lib/Menu.svelte +623 -623
  25. package/dist/lib/NoticeBase.svelte +140 -140
  26. package/dist/lib/PaginatedCard.svelte +73 -73
  27. package/dist/lib/Pagination.svelte +119 -119
  28. package/dist/lib/PrimaryColorSelect.svelte +111 -111
  29. package/dist/lib/ProgressBar.svelte +141 -141
  30. package/dist/lib/ProgressCircle.svelte +190 -190
  31. package/dist/lib/Radio.svelte +189 -189
  32. package/dist/lib/SearchInput.svelte +104 -104
  33. package/dist/lib/Select.svelte +524 -524
  34. package/dist/lib/Slider.svelte +253 -253
  35. package/dist/lib/Splitter.svelte +159 -159
  36. package/dist/lib/Switch.svelte +168 -168
  37. package/dist/lib/Table.svelte +299 -299
  38. package/dist/lib/Tabs.svelte +213 -213
  39. package/dist/lib/ThemeToggle.svelte +128 -128
  40. package/dist/lib/TimePicker.svelte +312 -312
  41. package/dist/lib/TimePickerNew.svelte +634 -634
  42. package/dist/lib/Toast.svelte +123 -123
  43. package/dist/lib/Tooltip.svelte +110 -110
  44. package/dist/lib/Topbar.svelte +112 -112
  45. package/dist/styles.css +234 -234
  46. 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-[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
+ <!-- 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}