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.
- package/LICENSE.md +21 -21
- package/README.md +101 -100
- package/dist/App.svelte +507 -507
- package/dist/Container.svelte +59 -59
- package/dist/app.css +234 -235
- package/dist/app.d.ts +10 -0
- package/dist/lib/Accordion.svelte +155 -155
- package/dist/lib/Badge.svelte +44 -44
- package/dist/lib/Button.svelte +185 -170
- package/dist/lib/Calendar.svelte +384 -384
- package/dist/lib/Card.svelte +103 -103
- package/dist/lib/Carousel.svelte +293 -293
- package/dist/lib/Carousel.svelte.d.ts +1 -1
- package/dist/lib/CheckBox.svelte +210 -210
- package/dist/lib/CodeView.svelte +308 -307
- package/dist/lib/ColorPicker.svelte +159 -159
- package/dist/lib/ContextMenu.svelte +328 -322
- package/dist/lib/DatePicker.svelte +246 -246
- package/dist/lib/Dialog.svelte +233 -233
- package/dist/lib/Field.svelte +299 -299
- package/dist/lib/FilePicker.svelte +295 -240
- package/dist/lib/FilePicker.svelte.d.ts +6 -1
- package/dist/lib/Form.svelte +438 -438
- package/dist/lib/Hamburger.svelte +217 -217
- package/dist/lib/InstallPWA.svelte +94 -94
- package/dist/lib/Menu.svelte +623 -623
- package/dist/lib/NoticeBase.svelte +140 -140
- package/dist/lib/PaginatedCard.svelte +73 -73
- package/dist/lib/Pagination.svelte +119 -119
- package/dist/lib/PrimaryColorSelect.svelte +111 -111
- package/dist/lib/ProgressBar.svelte +141 -141
- package/dist/lib/ProgressCircle.svelte +190 -190
- package/dist/lib/Radio.svelte +189 -189
- package/dist/lib/SearchInput.svelte +104 -104
- package/dist/lib/Select.svelte +524 -524
- package/dist/lib/Slider.svelte +253 -253
- package/dist/lib/Splitter.svelte +159 -150
- package/dist/lib/Switch.svelte +168 -167
- package/dist/lib/Table.svelte +299 -299
- package/dist/lib/Tabs.svelte +213 -213
- package/dist/lib/ThemeToggle.svelte +128 -127
- package/dist/lib/TimePicker.svelte +312 -312
- package/dist/lib/TimePickerNew.svelte +634 -0
- package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
- package/dist/lib/Toast.svelte +123 -123
- package/dist/lib/Tooltip.svelte +110 -110
- package/dist/lib/Topbar.svelte +107 -107
- package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
- package/dist/lib/__tests__/Accordion.test.js +171 -0
- package/dist/lib/__tests__/Badge.test.d.ts +1 -0
- package/dist/lib/__tests__/Badge.test.js +41 -0
- package/dist/lib/__tests__/Button.test.d.ts +1 -0
- package/dist/lib/__tests__/Button.test.js +269 -0
- package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
- package/dist/lib/__tests__/Calendar.test.js +171 -0
- package/dist/lib/__tests__/Card.test.d.ts +1 -0
- package/dist/lib/__tests__/Card.test.js +148 -0
- package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
- package/dist/lib/__tests__/Carousel.test.js +439 -0
- package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
- package/dist/lib/__tests__/CheckBox.test.js +152 -0
- package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
- package/dist/lib/__tests__/CodeView.test.js +157 -0
- package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
- package/dist/lib/__tests__/ColorPicker.test.js +93 -0
- package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
- package/dist/lib/__tests__/ContextMenu.test.js +67 -0
- package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/DatePicker.test.js +108 -0
- package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
- package/dist/lib/__tests__/Dialog.test.js +183 -0
- package/dist/lib/__tests__/Field.test.d.ts +1 -0
- package/dist/lib/__tests__/Field.test.js +190 -0
- package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/FilePicker.test.js +179 -0
- package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.integration.test.js +158 -0
- package/dist/lib/__tests__/Form.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.test.js +463 -0
- package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
- package/dist/lib/__tests__/Hamburger.test.js +161 -0
- package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
- package/dist/lib/__tests__/InstallPWA.test.js +15 -0
- package/dist/lib/__tests__/Menu.test.d.ts +1 -0
- package/dist/lib/__tests__/Menu.test.js +285 -0
- package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
- package/dist/lib/__tests__/NoticeBase.test.js +60 -0
- package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
- package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
- package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
- package/dist/lib/__tests__/Pagination.test.js +168 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
- package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressBar.test.js +69 -0
- package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
- package/dist/lib/__tests__/Radio.test.d.ts +1 -0
- package/dist/lib/__tests__/Radio.test.js +127 -0
- package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
- package/dist/lib/__tests__/SearchInput.test.js +80 -0
- package/dist/lib/__tests__/Select.test.d.ts +1 -0
- package/dist/lib/__tests__/Select.test.js +408 -0
- package/dist/lib/__tests__/Slider.test.d.ts +1 -0
- package/dist/lib/__tests__/Slider.test.js +213 -0
- package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
- package/dist/lib/__tests__/Splitter.test.js +87 -0
- package/dist/lib/__tests__/Switch.test.d.ts +1 -0
- package/dist/lib/__tests__/Switch.test.js +97 -0
- package/dist/lib/__tests__/Table.test.d.ts +1 -0
- package/dist/lib/__tests__/Table.test.js +349 -0
- package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
- package/dist/lib/__tests__/Tabs.test.js +262 -0
- package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
- package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
- package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePicker.test.js +146 -0
- package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
- package/dist/lib/__tests__/Toast.test.d.ts +1 -0
- package/dist/lib/__tests__/Toast.test.js +135 -0
- package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
- package/dist/lib/__tests__/Tooltip.test.js +171 -0
- package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
- package/dist/lib/__tests__/Topbar.test.js +25 -0
- package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
- package/dist/lib/__tests__/setupLangContext.js +65 -0
- package/dist/lib/__tests__/storage.test.d.ts +1 -0
- package/dist/lib/__tests__/storage.test.js +124 -0
- package/dist/lib/__tests__/utils.test.d.ts +1 -0
- package/dist/lib/__tests__/utils.test.js +11 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/lang.d.ts +4 -0
- package/dist/lib/lang.js +4 -0
- package/dist/styles.css +234 -232
- package/dist/utils/index.js +15 -4
- 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
|
-
|
|
137
|
-
open ? "translate-y-[-50%] rotate-45" : "translate-y-[calc(-50%_-_6px)]"
|
|
138
|
-
)}
|
|
139
|
-
></span>
|
|
140
|
-
<span
|
|
141
|
-
class={cx(
|
|
142
|
-
|
|
143
|
-
open ? "opacity-0" : "opacity-100"
|
|
144
|
-
)}
|
|
145
|
-
></span>
|
|
146
|
-
<span
|
|
147
|
-
class={cx(
|
|
148
|
-
|
|
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
|
-
|
|
169
|
-
{@render header?.()}
|
|
170
|
-
</div>
|
|
171
|
-
{/if}
|
|
172
|
-
|
|
173
|
-
<div class="flex-1 overflow-y-auto
|
|
174
|
-
{#if menuItems.length === 0}
|
|
175
|
-
|
|
176
|
-
{:else}
|
|
177
|
-
|
|
178
|
-
{#each menuItems as it (it.id)}
|
|
179
|
-
{#if it.type === "section"}
|
|
180
|
-
|
|
181
|
-
{it.label}
|
|
182
|
-
</li>
|
|
183
|
-
{:else}
|
|
184
|
-
<li>
|
|
185
|
-
<button
|
|
186
|
-
type="button"
|
|
187
|
-
|
|
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
|
-
|
|
205
|
-
{@render footer?.()}
|
|
206
|
-
</div>
|
|
207
|
-
{/if}
|
|
208
|
-
</div>
|
|
209
|
-
|
|
210
|
-
<button
|
|
211
|
-
type="button"
|
|
212
|
-
|
|
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}
|