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.
Files changed (64) hide show
  1. package/README.md +1 -0
  2. package/dist/atoms/CardContent.svelte +1 -1
  3. package/dist/atoms/CardHeader.svelte +1 -1
  4. package/dist/atoms/Checkbox.svelte +1 -7
  5. package/dist/atoms/CodeBlock.svelte +6 -6
  6. package/dist/atoms/ColorPicker.svelte +80 -72
  7. package/dist/atoms/Input.svelte +14 -10
  8. package/dist/atoms/Input.svelte.d.ts +2 -0
  9. package/dist/atoms/List.svelte +47 -0
  10. package/dist/atoms/List.svelte.d.ts +19 -0
  11. package/dist/atoms/ListItem.svelte +148 -0
  12. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  13. package/dist/atoms/Progress.svelte +3 -5
  14. package/dist/atoms/Select.svelte +59 -16
  15. package/dist/atoms/Select.svelte.d.ts +13 -1
  16. package/dist/atoms/Textarea.svelte +11 -9
  17. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  18. package/dist/atoms/Toggle.svelte +1 -7
  19. package/dist/atoms/Tooltip.svelte +8 -15
  20. package/dist/atoms/index.d.ts +2 -0
  21. package/dist/atoms/index.js +2 -0
  22. package/dist/components/atoms/index.d.ts +2 -0
  23. package/dist/components/atoms/index.d.ts.map +1 -1
  24. package/dist/components/index.d.ts +4 -0
  25. package/dist/components/index.d.ts.map +1 -1
  26. package/dist/components/molecules/index.d.ts +0 -1
  27. package/dist/components/molecules/index.d.ts.map +1 -1
  28. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  29. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  30. package/dist/components/organisms/index.d.ts +2 -0
  31. package/dist/components/organisms/index.d.ts.map +1 -1
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.js +5 -8
  34. package/dist/molecules/Alert.svelte +8 -3
  35. package/dist/molecules/ContactForm.svelte +63 -31
  36. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  37. package/dist/molecules/Dropdown.svelte +2 -1
  38. package/dist/molecules/ImageUpload.svelte +26 -3
  39. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  40. package/dist/molecules/Modal.svelte +44 -43
  41. package/dist/molecules/NavigationMenu.svelte +7 -10
  42. package/dist/molecules/NavigationMenuContent.svelte +8 -8
  43. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  44. package/dist/molecules/NavigationMenuLink.svelte +7 -5
  45. package/dist/molecules/NavigationMenuList.svelte +0 -7
  46. package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
  47. package/dist/molecules/SlideUp.svelte +30 -0
  48. package/dist/molecules/Tabs.svelte +71 -4
  49. package/dist/molecules/index.d.ts +0 -1
  50. package/dist/molecules/index.js +0 -1
  51. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  52. package/dist/molecules/navigation-menu-context.js +1 -0
  53. package/dist/organisms/Navigation.svelte +7 -4
  54. package/dist/organisms/Navigation.svelte.d.ts +1 -0
  55. package/dist/organisms/SidebarNavigation.svelte +420 -0
  56. package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
  57. package/dist/organisms/SidebarProjectPanel.svelte +174 -0
  58. package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
  59. package/dist/organisms/index.d.ts +2 -0
  60. package/dist/organisms/index.js +2 -0
  61. package/dist/zabi-components.css +302 -0
  62. package/package.json +5 -4
  63. package/dist/molecules/Sidebar.svelte +0 -324
  64. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
@@ -14,9 +14,8 @@
14
14
 
15
15
  let {
16
16
  className = "",
17
- children,
18
- ...restProps
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 formData = new FormData(event.target as HTMLFormElement);
52
+ const submittedFormData = new FormData(event.target as HTMLFormElement);
30
53
  const data: ContactFormData = {
31
- name: (formData.get("name") as string) || "",
32
- email: (formData.get("email") as string) || "",
33
- message: (formData.get("message") as string) || "",
34
- subscribe: formData.get("subscribe") === "on" || false,
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
- console.log("Form submitted with data:", data);
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
- <Card size="md" fullWidth={true} title="Get in Touch">
42
- <Form onsubmit={handleFormSubmit} className="space-y-4">
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
- <Input
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
- </Form>
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
- type $$ComponentProps = Props & {
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
- let dropdownId = `dropdown-${Math.random().toString(36).substr(2, 9)}`;
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
- if (value && typeof URL !== "undefined" && URL.revokeObjectURL) {
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>
@@ -3,6 +3,7 @@ interface Props {
3
3
  disabled?: boolean;
4
4
  accept?: string;
5
5
  placeholder?: string;
6
+ errorMessage?: string;
6
7
  onchange?: (event: Event) => void;
7
8
  onclick?: (event: Event) => void;
8
9
  }
@@ -28,7 +28,7 @@
28
28
  ...restProps
29
29
  }: Props & { children?: any; footer?: any } = $props();
30
30
 
31
- let modalContainer: HTMLDivElement;
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
- if (isOpen && modalContainer) {
56
+ const container = modalContainer;
57
+ if (isOpen && container) {
57
58
  saveFocus();
58
- cleanupFocusTrap = trapFocus(modalContainer);
59
+ cleanupFocusTrap = trapFocus(container);
59
60
  // Small delay to ensure modal is rendered
60
61
  setTimeout(() => {
61
- focusFirstElement(modalContainer);
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
- <Card
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
- {#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}
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
- {#if children}
129
- <CardContent className="flex-1">
130
- {@render children?.()}
131
- </CardContent>
132
- {/if}
128
+ {#if children}
129
+ <CardContent className="flex-1">
130
+ {@render children?.()}
131
+ </CardContent>
132
+ {/if}
133
133
 
134
- {#if footer}
135
- <CardFooter className="flex justify-end gap-3 pt-4">
136
- {@render footer?.()}
137
- </CardFooter>
138
- {/if}
139
- </Card>
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 { getContext, setContext } from "svelte";
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
- interface NavigationMenuContext {
12
- activeItem: string | null;
13
- setActiveItem: (item: string | null) => void;
14
- isMobile: boolean;
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: NavigationMenuContext = {
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(NAVIGATION_MENU_CONTEXT, context);
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, onDestroy } from "svelte";
5
-
6
- const NAVIGATION_MENU_CONTEXT = Symbol("navigation-menu");
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
- activeItem: string | null;
26
- setActiveItem: (item: string | null) => void;
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
- const NAVIGATION_MENU_CONTEXT = Symbol("navigation-menu");
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
- activeItem: string | null;
22
- setActiveItem: (item: string | null) => void;
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
- const NAVIGATION_MENU_CONTEXT = Symbol("navigation-menu");
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
- setActiveItem: (item: string | null) => void;
24
- }>(NAVIGATION_MENU_CONTEXT);
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
- const NAVIGATION_MENU_CONTEXT = Symbol("navigation-menu");
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
- activeItem: string | null;
24
- setActiveItem: (item: string | null) => void;
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}