zabi-components 5.0.21 → 5.0.22
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/README.md +1 -0
- package/dist/atoms/CardContent.svelte +1 -1
- package/dist/atoms/CardHeader.svelte +1 -1
- package/dist/atoms/Checkbox.svelte +1 -7
- package/dist/atoms/CodeBlock.svelte +6 -6
- package/dist/atoms/ColorPicker.svelte +80 -72
- package/dist/atoms/Input.svelte +14 -10
- package/dist/atoms/Input.svelte.d.ts +2 -0
- package/dist/atoms/List.svelte +47 -0
- package/dist/atoms/List.svelte.d.ts +19 -0
- package/dist/atoms/ListItem.svelte +148 -0
- package/dist/atoms/ListItem.svelte.d.ts +35 -0
- package/dist/atoms/Progress.svelte +3 -5
- package/dist/atoms/Select.svelte +59 -16
- package/dist/atoms/Select.svelte.d.ts +13 -1
- package/dist/atoms/Textarea.svelte +11 -9
- package/dist/atoms/Textarea.svelte.d.ts +1 -0
- package/dist/atoms/Toggle.svelte +1 -7
- package/dist/atoms/Tooltip.svelte +8 -15
- package/dist/atoms/index.d.ts +2 -0
- package/dist/atoms/index.js +2 -0
- package/dist/components/atoms/index.d.ts +2 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +0 -1
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
- package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
- package/dist/components/organisms/index.d.ts +2 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -8
- package/dist/molecules/Alert.svelte +8 -3
- package/dist/molecules/ContactForm.svelte +63 -31
- package/dist/molecules/ContactForm.svelte.d.ts +1 -4
- package/dist/molecules/Dropdown.svelte +2 -1
- package/dist/molecules/ImageUpload.svelte +26 -3
- package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
- package/dist/molecules/Modal.svelte +44 -43
- package/dist/molecules/NavigationMenu.svelte +7 -10
- package/dist/molecules/NavigationMenuContent.svelte +8 -8
- package/dist/molecules/NavigationMenuItem.svelte +7 -6
- package/dist/molecules/NavigationMenuLink.svelte +7 -5
- package/dist/molecules/NavigationMenuList.svelte +0 -7
- package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
- package/dist/molecules/SlideUp.svelte +30 -0
- package/dist/molecules/Tabs.svelte +71 -4
- package/dist/molecules/index.d.ts +0 -1
- package/dist/molecules/index.js +0 -1
- package/dist/molecules/navigation-menu-context.d.ts +6 -0
- package/dist/molecules/navigation-menu-context.js +1 -0
- package/dist/organisms/Navigation.svelte +7 -4
- package/dist/organisms/Navigation.svelte.d.ts +1 -0
- package/dist/organisms/SidebarNavigation.svelte +420 -0
- package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
- package/dist/organisms/SidebarProjectPanel.svelte +174 -0
- package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
- package/dist/organisms/index.d.ts +2 -0
- package/dist/organisms/index.js +2 -0
- package/dist/zabi-components.css +302 -0
- package/package.json +5 -4
- package/dist/molecules/Sidebar.svelte +0 -324
- package/dist/molecules/Sidebar.svelte.d.ts +0 -30
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
let {
|
|
16
16
|
className = "",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} = $props<Props & { children?: any }>();
|
|
17
|
+
onsubmit,
|
|
18
|
+
}: Props = $props();
|
|
20
19
|
|
|
21
20
|
let formData = $state<ContactFormData>({
|
|
22
21
|
name: "",
|
|
@@ -25,21 +24,65 @@
|
|
|
25
24
|
subscribe: false,
|
|
26
25
|
});
|
|
27
26
|
|
|
27
|
+
let fieldErrors = $state<Partial<Record<keyof ContactFormData, string>>>({});
|
|
28
|
+
let formErrorMessage = $state("");
|
|
29
|
+
|
|
30
|
+
function validate(data: ContactFormData) {
|
|
31
|
+
const nextErrors: Partial<Record<keyof ContactFormData, string>> = {};
|
|
32
|
+
|
|
33
|
+
if (!data.name.trim()) {
|
|
34
|
+
nextErrors.name = "Please enter your name.";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!data.email.trim()) {
|
|
38
|
+
nextErrors.email = "Please enter your email address.";
|
|
39
|
+
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
|
|
40
|
+
nextErrors.email = "Please enter a valid email address.";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!(data.message || "").trim()) {
|
|
44
|
+
nextErrors.message = "Please enter a message.";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fieldErrors = nextErrors;
|
|
48
|
+
return Object.keys(nextErrors).length === 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
function handleFormSubmit(event: SubmitEvent) {
|
|
29
|
-
const
|
|
52
|
+
const submittedFormData = new FormData(event.target as HTMLFormElement);
|
|
30
53
|
const data: ContactFormData = {
|
|
31
|
-
name: (
|
|
32
|
-
email: (
|
|
33
|
-
message: (
|
|
34
|
-
subscribe:
|
|
54
|
+
name: (submittedFormData.get("name") as string) || "",
|
|
55
|
+
email: (submittedFormData.get("email") as string) || "",
|
|
56
|
+
message: (submittedFormData.get("message") as string) || "",
|
|
57
|
+
subscribe: submittedFormData.get("subscribe") === "on" || false,
|
|
35
58
|
};
|
|
36
59
|
|
|
37
|
-
|
|
60
|
+
if (!validate(data)) {
|
|
61
|
+
formErrorMessage =
|
|
62
|
+
"We could not submit your request. Fix the highlighted fields and try again.";
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
formErrorMessage = "";
|
|
67
|
+
if (onsubmit) {
|
|
68
|
+
onsubmit(event);
|
|
69
|
+
}
|
|
38
70
|
}
|
|
39
71
|
</script>
|
|
40
72
|
|
|
41
|
-
<
|
|
42
|
-
<
|
|
73
|
+
<div class={className}>
|
|
74
|
+
<Card size="md" fullWidth={true} title="Get in Touch">
|
|
75
|
+
<Form onsubmit={handleFormSubmit} className="space-y-4">
|
|
76
|
+
{#if formErrorMessage}
|
|
77
|
+
<div
|
|
78
|
+
class="rounded-lg border border-error px-4 py-3 text-sm text-error"
|
|
79
|
+
role="alert"
|
|
80
|
+
>
|
|
81
|
+
<p class="font-medium">Something went wrong</p>
|
|
82
|
+
<p>{formErrorMessage}</p>
|
|
83
|
+
<p class="mt-1">Recovery action: review your inputs and resubmit.</p>
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
43
86
|
<div class="space-y-4">
|
|
44
87
|
<Input
|
|
45
88
|
type="text"
|
|
@@ -49,6 +92,8 @@
|
|
|
49
92
|
value={formData.name}
|
|
50
93
|
oninput={(e) =>
|
|
51
94
|
(formData.name = (e.target as HTMLInputElement).value)}
|
|
95
|
+
variant={fieldErrors.name ? "error" : "default"}
|
|
96
|
+
message={fieldErrors.name || ""}
|
|
52
97
|
/>
|
|
53
98
|
<Input
|
|
54
99
|
type="email"
|
|
@@ -58,24 +103,8 @@
|
|
|
58
103
|
value={formData.email}
|
|
59
104
|
oninput={(e) =>
|
|
60
105
|
(formData.email = (e.target as HTMLInputElement).value)}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
type="email"
|
|
64
|
-
name="email"
|
|
65
|
-
label="Email"
|
|
66
|
-
placeholder="Enter your email"
|
|
67
|
-
value={formData.email}
|
|
68
|
-
oninput={(e) =>
|
|
69
|
-
(formData.email = (e.target as HTMLInputElement).value)}
|
|
70
|
-
/>
|
|
71
|
-
<Input
|
|
72
|
-
type="email"
|
|
73
|
-
name="email"
|
|
74
|
-
label="Email"
|
|
75
|
-
placeholder="Enter your email"
|
|
76
|
-
value={formData.email}
|
|
77
|
-
oninput={(e) =>
|
|
78
|
-
(formData.email = (e.target as HTMLInputElement).value)}
|
|
106
|
+
variant={fieldErrors.email ? "error" : "default"}
|
|
107
|
+
message={fieldErrors.email || ""}
|
|
79
108
|
/>
|
|
80
109
|
<Textarea
|
|
81
110
|
name="message"
|
|
@@ -87,6 +116,8 @@
|
|
|
87
116
|
(formData.message = (
|
|
88
117
|
e.target as HTMLTextAreaElement
|
|
89
118
|
).value)}
|
|
119
|
+
variant={fieldErrors.message ? "error" : "default"}
|
|
120
|
+
message={fieldErrors.message || ""}
|
|
90
121
|
/>
|
|
91
122
|
<Checkbox
|
|
92
123
|
name="subscribe"
|
|
@@ -103,5 +134,6 @@
|
|
|
103
134
|
Send Message
|
|
104
135
|
</Button>
|
|
105
136
|
</div>
|
|
106
|
-
|
|
107
|
-
</Card>
|
|
137
|
+
</Form>
|
|
138
|
+
</Card>
|
|
139
|
+
</div>
|
|
@@ -2,9 +2,6 @@ interface Props {
|
|
|
2
2
|
className?: string;
|
|
3
3
|
onsubmit?: (event: SubmitEvent) => void;
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
children?: any;
|
|
7
|
-
};
|
|
8
|
-
declare const ContactForm: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
5
|
+
declare const ContactForm: import("svelte").Component<Props, {}, "">;
|
|
9
6
|
type ContactForm = ReturnType<typeof ContactForm>;
|
|
10
7
|
export default ContactForm;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Button from "../atoms/Button.svelte";
|
|
3
|
+
import { generateId } from "../../routes/lib/ssr-safe.js";
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
isOpen?: boolean;
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
...restProps
|
|
27
28
|
}: Props & { children?: any; trigger?: any } = $props();
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
const dropdownId = generateId("dropdown");
|
|
30
31
|
let triggerElement = $state<HTMLElement | null>(null);
|
|
31
32
|
let menuElement = $state<HTMLElement | null>(null);
|
|
32
33
|
let openedViaKeyboard = $state(false);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onDestroy } from "svelte";
|
|
2
3
|
import Button from "../atoms/Button.svelte";
|
|
3
4
|
import { Image } from "@lucide/svelte";
|
|
4
5
|
|
|
@@ -7,6 +8,7 @@
|
|
|
7
8
|
disabled?: boolean;
|
|
8
9
|
accept?: string;
|
|
9
10
|
placeholder?: string;
|
|
11
|
+
errorMessage?: string;
|
|
10
12
|
onchange?: (event: Event) => void;
|
|
11
13
|
onclick?: (event: Event) => void;
|
|
12
14
|
}
|
|
@@ -16,11 +18,20 @@
|
|
|
16
18
|
disabled = false,
|
|
17
19
|
accept = "image/*",
|
|
18
20
|
placeholder = "No image selected",
|
|
21
|
+
errorMessage = "",
|
|
19
22
|
children,
|
|
20
23
|
...restProps
|
|
21
24
|
} = $props<Props & { children?: any }>();
|
|
22
25
|
|
|
23
26
|
let fileInput = $state<HTMLInputElement>();
|
|
27
|
+
let currentObjectUrl = $state<string | null>(null);
|
|
28
|
+
|
|
29
|
+
function revokeCurrentObjectUrl() {
|
|
30
|
+
if (currentObjectUrl && typeof URL !== "undefined" && URL.revokeObjectURL) {
|
|
31
|
+
URL.revokeObjectURL(currentObjectUrl);
|
|
32
|
+
currentObjectUrl = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
function handleFileSelect(event: Event) {
|
|
26
37
|
if (disabled) return;
|
|
@@ -31,17 +42,17 @@
|
|
|
31
42
|
const file = input.files[0];
|
|
32
43
|
|
|
33
44
|
if (typeof URL !== "undefined" && URL.createObjectURL) {
|
|
45
|
+
revokeCurrentObjectUrl();
|
|
34
46
|
const url = URL.createObjectURL(file);
|
|
35
47
|
value = url;
|
|
48
|
+
currentObjectUrl = url;
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
function removeImage() {
|
|
40
53
|
if (disabled) return;
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
URL.revokeObjectURL(value);
|
|
44
|
-
}
|
|
55
|
+
revokeCurrentObjectUrl();
|
|
45
56
|
value = null;
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -49,6 +60,10 @@
|
|
|
49
60
|
if (disabled) return;
|
|
50
61
|
fileInput?.click();
|
|
51
62
|
}
|
|
63
|
+
|
|
64
|
+
onDestroy(() => {
|
|
65
|
+
revokeCurrentObjectUrl();
|
|
66
|
+
});
|
|
52
67
|
</script>
|
|
53
68
|
|
|
54
69
|
<div class="space-y-3">
|
|
@@ -118,4 +133,12 @@
|
|
|
118
133
|
{disabled}
|
|
119
134
|
class="hidden"
|
|
120
135
|
/>
|
|
136
|
+
|
|
137
|
+
{#if errorMessage}
|
|
138
|
+
<div class="rounded-lg border border-error px-3 py-2 text-sm text-error" role="alert">
|
|
139
|
+
<p class="font-medium">Image upload failed</p>
|
|
140
|
+
<p>{errorMessage}</p>
|
|
141
|
+
<p class="mt-1">Recovery action: try another file or retry upload.</p>
|
|
142
|
+
</div>
|
|
143
|
+
{/if}
|
|
121
144
|
</div>
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
...restProps
|
|
29
29
|
}: Props & { children?: any; footer?: any } = $props();
|
|
30
30
|
|
|
31
|
-
let modalContainer
|
|
31
|
+
let modalContainer = $state<HTMLDivElement>();
|
|
32
32
|
let cleanupFocusTrap: (() => void) | null = null;
|
|
33
33
|
|
|
34
34
|
const sizeClasses = $derived(
|
|
@@ -53,12 +53,13 @@
|
|
|
53
53
|
|
|
54
54
|
// Handle focus trap when modal opens
|
|
55
55
|
$effect(() => {
|
|
56
|
-
|
|
56
|
+
const container = modalContainer;
|
|
57
|
+
if (isOpen && container) {
|
|
57
58
|
saveFocus();
|
|
58
|
-
cleanupFocusTrap = trapFocus(
|
|
59
|
+
cleanupFocusTrap = trapFocus(container);
|
|
59
60
|
// Small delay to ensure modal is rendered
|
|
60
61
|
setTimeout(() => {
|
|
61
|
-
focusFirstElement(
|
|
62
|
+
focusFirstElement(container);
|
|
62
63
|
}, 0);
|
|
63
64
|
} else if (!isOpen && cleanupFocusTrap) {
|
|
64
65
|
cleanupFocusTrap();
|
|
@@ -93,49 +94,49 @@
|
|
|
93
94
|
aria-labelledby={title ? "modal-title" : undefined}
|
|
94
95
|
tabindex="-1"
|
|
95
96
|
>
|
|
96
|
-
<
|
|
97
|
+
<div
|
|
97
98
|
bind:this={modalContainer}
|
|
98
|
-
variant="default"
|
|
99
|
-
fullWidth={false}
|
|
100
99
|
class="bg-card rounded-t-3xl md:rounded-3xl shadow-xl min-w-[320px] {sizeClasses} max-h-[90vh] overflow-y-auto animate-[slideUp_0.3s_ease-out] md:animate-none flex flex-col p-0"
|
|
101
100
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
<Card variant="default" fullWidth={false}>
|
|
102
|
+
{#if title || description}
|
|
103
|
+
<CardHeader
|
|
104
|
+
{description}
|
|
105
|
+
className="px-6 pt-6 pb-4"
|
|
106
|
+
>
|
|
107
|
+
{#if title}
|
|
108
|
+
<div class="flex items-center justify-between">
|
|
109
|
+
<h2
|
|
110
|
+
id="modal-title"
|
|
111
|
+
class="text-2xl font-normal leading-8 text-headline tracking-normal"
|
|
112
|
+
>
|
|
113
|
+
{title}
|
|
114
|
+
</h2>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onclick={closeModal}
|
|
118
|
+
class="text-description hover:text-headline text-2xl cursor-pointer transition-colors w-8 h-8 flex items-center justify-center rounded-full hover:bg-base-100"
|
|
119
|
+
aria-label="Close"
|
|
120
|
+
>
|
|
121
|
+
×
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
{/if}
|
|
125
|
+
</CardHeader>
|
|
126
|
+
{/if}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
{#if children}
|
|
129
|
+
<CardContent className="flex-1">
|
|
130
|
+
{@render children?.()}
|
|
131
|
+
</CardContent>
|
|
132
|
+
{/if}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
{#if footer}
|
|
135
|
+
<CardFooter className="flex justify-end gap-3 pt-4">
|
|
136
|
+
{@render footer?.()}
|
|
137
|
+
</CardFooter>
|
|
138
|
+
{/if}
|
|
139
|
+
</Card>
|
|
140
|
+
</div>
|
|
140
141
|
</div>
|
|
141
142
|
{/if}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import {
|
|
3
|
+
import { setContext } from "svelte";
|
|
4
4
|
import { onMount } from "svelte";
|
|
5
5
|
import NavigationMenuList from "./NavigationMenuList.svelte";
|
|
6
6
|
import NavigationMenuItem from "./NavigationMenuItem.svelte";
|
|
@@ -8,13 +8,10 @@
|
|
|
8
8
|
import NavigationMenuContent from "./NavigationMenuContent.svelte";
|
|
9
9
|
import NavigationMenuLink from "./NavigationMenuLink.svelte";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const NAVIGATION_MENU_CONTEXT = Symbol("navigation-menu");
|
|
11
|
+
import {
|
|
12
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
13
|
+
type NavigationMenuContextValue,
|
|
14
|
+
} from "./navigation-menu-context.js";
|
|
18
15
|
|
|
19
16
|
export interface NavigationMenuItemData {
|
|
20
17
|
value: string;
|
|
@@ -76,7 +73,7 @@
|
|
|
76
73
|
}
|
|
77
74
|
});
|
|
78
75
|
|
|
79
|
-
const context:
|
|
76
|
+
const context: NavigationMenuContextValue = {
|
|
80
77
|
get activeItem() {
|
|
81
78
|
return activeItem;
|
|
82
79
|
},
|
|
@@ -86,7 +83,7 @@
|
|
|
86
83
|
},
|
|
87
84
|
};
|
|
88
85
|
|
|
89
|
-
setContext(
|
|
86
|
+
setContext(NAVIGATION_MENU_CONTEXT_KEY, context);
|
|
90
87
|
</script>
|
|
91
88
|
|
|
92
89
|
<nav
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
-
import { onMount
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { onMount } from "svelte";
|
|
5
|
+
import {
|
|
6
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
7
|
+
type NavigationMenuContextValue,
|
|
8
|
+
} from "./navigation-menu-context.js";
|
|
7
9
|
|
|
8
10
|
interface Props {
|
|
9
11
|
value?: string;
|
|
@@ -21,11 +23,9 @@
|
|
|
21
23
|
let contentElement = $state<HTMLElement | null>(null);
|
|
22
24
|
let parentItem = $state<HTMLElement | null>(null);
|
|
23
25
|
|
|
24
|
-
const context = getContext<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
isMobile: boolean;
|
|
28
|
-
}>(NAVIGATION_MENU_CONTEXT);
|
|
26
|
+
const context = getContext<NavigationMenuContextValue>(
|
|
27
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
28
|
+
);
|
|
29
29
|
|
|
30
30
|
const isActive = $derived(context?.activeItem === value);
|
|
31
31
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
|
+
type NavigationMenuContextValue,
|
|
7
|
+
} from "./navigation-menu-context.js";
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
value?: string;
|
|
@@ -17,10 +19,9 @@
|
|
|
17
19
|
...restProps
|
|
18
20
|
}: Props = $props();
|
|
19
21
|
|
|
20
|
-
const context = getContext<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}>(NAVIGATION_MENU_CONTEXT);
|
|
22
|
+
const context = getContext<NavigationMenuContextValue>(
|
|
23
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
24
|
+
);
|
|
24
25
|
|
|
25
26
|
const isActive = $derived(context?.activeItem === value);
|
|
26
27
|
</script>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
|
+
type NavigationMenuContextValue,
|
|
7
|
+
} from "./navigation-menu-context.js";
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
href?: string;
|
|
@@ -19,9 +21,9 @@
|
|
|
19
21
|
...restProps
|
|
20
22
|
}: Props = $props();
|
|
21
23
|
|
|
22
|
-
const context = getContext<
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const context = getContext<NavigationMenuContextValue>(
|
|
25
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
26
|
+
);
|
|
25
27
|
|
|
26
28
|
function handleClick() {
|
|
27
29
|
context?.setActiveItem(null);
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { getContext } from "svelte";
|
|
4
|
-
|
|
5
|
-
const NAVIGATION_MENU_CONTEXT = Symbol("navigation-menu");
|
|
6
3
|
|
|
7
4
|
interface Props {
|
|
8
5
|
className?: string;
|
|
@@ -14,10 +11,6 @@
|
|
|
14
11
|
children,
|
|
15
12
|
...restProps
|
|
16
13
|
}: Props = $props();
|
|
17
|
-
|
|
18
|
-
const context = getContext<{
|
|
19
|
-
isMobile: boolean;
|
|
20
|
-
}>(NAVIGATION_MENU_CONTEXT);
|
|
21
14
|
</script>
|
|
22
15
|
|
|
23
16
|
<ul
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import { getContext } from "svelte";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
6
|
+
type NavigationMenuContextValue,
|
|
7
|
+
} from "./navigation-menu-context.js";
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
value?: string;
|
|
@@ -19,11 +21,9 @@
|
|
|
19
21
|
|
|
20
22
|
let triggerElement = $state<HTMLElement | null>(null);
|
|
21
23
|
|
|
22
|
-
const context = getContext<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
isMobile: boolean;
|
|
26
|
-
}>(NAVIGATION_MENU_CONTEXT);
|
|
24
|
+
const context = getContext<NavigationMenuContextValue>(
|
|
25
|
+
NAVIGATION_MENU_CONTEXT_KEY,
|
|
26
|
+
);
|
|
27
27
|
|
|
28
28
|
const isActive = $derived(context?.activeItem === value);
|
|
29
29
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
focusFirstElement,
|
|
4
|
+
returnFocus,
|
|
5
|
+
saveFocus,
|
|
6
|
+
trapFocus,
|
|
7
|
+
} from "../../routes/lib/focus-utils.js";
|
|
8
|
+
|
|
2
9
|
interface Props {
|
|
3
10
|
isOpen?: boolean;
|
|
4
11
|
title?: string;
|
|
@@ -13,13 +20,35 @@
|
|
|
13
20
|
...restProps
|
|
14
21
|
} = $props<Props & { children?: any }>();
|
|
15
22
|
|
|
23
|
+
let slideUpContainer = $state<HTMLDivElement>();
|
|
24
|
+
let cleanupFocusTrap: (() => void) | null = null;
|
|
25
|
+
|
|
16
26
|
function closeSlideUp(event?: Event) {
|
|
17
27
|
isOpen = false;
|
|
28
|
+
if (cleanupFocusTrap) {
|
|
29
|
+
cleanupFocusTrap();
|
|
30
|
+
cleanupFocusTrap = null;
|
|
31
|
+
}
|
|
32
|
+
returnFocus();
|
|
18
33
|
if (onclick && event) {
|
|
19
34
|
(onclick as (event: Event) => void)(event);
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
$effect(() => {
|
|
39
|
+
const container = slideUpContainer;
|
|
40
|
+
if (isOpen && container) {
|
|
41
|
+
saveFocus();
|
|
42
|
+
cleanupFocusTrap = trapFocus(container);
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
focusFirstElement(container);
|
|
45
|
+
}, 0);
|
|
46
|
+
} else if (!isOpen && cleanupFocusTrap) {
|
|
47
|
+
cleanupFocusTrap();
|
|
48
|
+
cleanupFocusTrap = null;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
23
52
|
function handleBackdropClick(event: Event) {
|
|
24
53
|
if (event.target === event.currentTarget) {
|
|
25
54
|
closeSlideUp(event);
|
|
@@ -48,6 +77,7 @@
|
|
|
48
77
|
tabindex="-1"
|
|
49
78
|
>
|
|
50
79
|
<div
|
|
80
|
+
bind:this={slideUpContainer}
|
|
51
81
|
class="fixed bottom-0 left-0 right-0 bg-card rounded-t-3xl shadow-xl z-modal max-h-[90vh] overflow-y-auto animate-[slideUp_0.3s_ease-out] flex flex-col"
|
|
52
82
|
>
|
|
53
83
|
{#if title}
|