zabi-components 5.0.20 → 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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +355 -81
  3. package/dist/atoms/Button.svelte +1 -1
  4. package/dist/atoms/CardContent.svelte +1 -1
  5. package/dist/atoms/CardHeader.svelte +1 -1
  6. package/dist/atoms/Checkbox.svelte +1 -7
  7. package/dist/atoms/CodeBlock.svelte +6 -6
  8. package/dist/atoms/ColorPicker.svelte +80 -72
  9. package/dist/atoms/IconButton.svelte +88 -0
  10. package/dist/atoms/IconButton.svelte.d.ts +21 -0
  11. package/dist/atoms/Input.svelte +14 -10
  12. package/dist/atoms/Input.svelte.d.ts +2 -0
  13. package/dist/atoms/List.svelte +47 -0
  14. package/dist/atoms/List.svelte.d.ts +19 -0
  15. package/dist/atoms/ListItem.svelte +148 -0
  16. package/dist/atoms/ListItem.svelte.d.ts +35 -0
  17. package/dist/atoms/Progress.svelte +3 -5
  18. package/dist/atoms/Select.svelte +152 -19
  19. package/dist/atoms/Select.svelte.d.ts +23 -1
  20. package/dist/atoms/Textarea.svelte +11 -9
  21. package/dist/atoms/Textarea.svelte.d.ts +1 -0
  22. package/dist/atoms/Toggle.svelte +1 -7
  23. package/dist/atoms/Tooltip.svelte +8 -15
  24. package/dist/atoms/index.d.ts +3 -0
  25. package/dist/atoms/index.js +3 -0
  26. package/dist/components/atoms/index.d.ts +3 -0
  27. package/dist/components/atoms/index.d.ts.map +1 -1
  28. package/dist/components/index.d.ts +4 -0
  29. package/dist/components/index.d.ts.map +1 -1
  30. package/dist/components/molecules/index.d.ts +1 -1
  31. package/dist/components/molecules/navigation-menu-context.d.ts +7 -0
  32. package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -0
  33. package/dist/components/organisms/index.d.ts +2 -0
  34. package/dist/components/organisms/index.d.ts.map +1 -1
  35. package/dist/index.d.ts +4 -0
  36. package/dist/index.js +5 -8
  37. package/dist/molecules/Alert.svelte +8 -3
  38. package/dist/molecules/ContactForm.svelte +63 -31
  39. package/dist/molecules/ContactForm.svelte.d.ts +1 -4
  40. package/dist/molecules/Dropdown.svelte +2 -1
  41. package/dist/molecules/ImageUpload.svelte +26 -3
  42. package/dist/molecules/ImageUpload.svelte.d.ts +1 -0
  43. package/dist/molecules/Modal.svelte +44 -43
  44. package/dist/molecules/NavigationMenu.svelte +7 -10
  45. package/dist/molecules/NavigationMenuContent.svelte +8 -8
  46. package/dist/molecules/NavigationMenuItem.svelte +7 -6
  47. package/dist/molecules/NavigationMenuLink.svelte +7 -5
  48. package/dist/molecules/NavigationMenuList.svelte +0 -7
  49. package/dist/molecules/NavigationMenuTrigger.svelte +7 -7
  50. package/dist/molecules/Section.svelte +158 -0
  51. package/dist/molecules/Section.svelte.d.ts +27 -0
  52. package/dist/molecules/SlideUp.svelte +30 -0
  53. package/dist/molecules/Tabs.svelte +71 -4
  54. package/dist/molecules/index.d.ts +1 -1
  55. package/dist/molecules/index.js +1 -1
  56. package/dist/molecules/navigation-menu-context.d.ts +6 -0
  57. package/dist/molecules/navigation-menu-context.js +1 -0
  58. package/dist/organisms/Navigation.svelte +9 -6
  59. package/dist/organisms/Navigation.svelte.d.ts +1 -0
  60. package/dist/organisms/SidebarNavigation.svelte +420 -0
  61. package/dist/organisms/SidebarNavigation.svelte.d.ts +44 -0
  62. package/dist/organisms/SidebarProjectPanel.svelte +174 -0
  63. package/dist/organisms/SidebarProjectPanel.svelte.d.ts +32 -0
  64. package/dist/organisms/index.d.ts +2 -0
  65. package/dist/organisms/index.js +2 -0
  66. package/dist/zabi-components.css +412 -0
  67. package/package.json +7 -8
  68. package/dist/molecules/Sidebar.svelte +0 -324
  69. package/dist/molecules/Sidebar.svelte.d.ts +0 -30
@@ -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
 
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { SizeVariant } from "../../types/variants.js";
4
+ import Heading from "../atoms/Heading.svelte";
5
+
6
+ interface Props {
7
+ /** Section title */
8
+ title?: string;
9
+ /** Section description/subtitle */
10
+ description?: string;
11
+ /** Heading level for title (1-6) */
12
+ headingLevel?: number;
13
+ /** Section size variant */
14
+ size?: SizeVariant;
15
+ /** Maximum width of the section container */
16
+ maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "none";
17
+ /** Background variant */
18
+ background?: "default" | "muted" | "accent" | "transparent";
19
+ /** Padding variant */
20
+ padding?: "none" | "sm" | "md" | "lg" | "xl";
21
+ /** Whether to center the content */
22
+ centered?: boolean;
23
+ /** Additional CSS classes */
24
+ className?: string;
25
+ /** Section content */
26
+ children?: Snippet;
27
+ }
28
+
29
+ let {
30
+ title = "",
31
+ description = "",
32
+ headingLevel = 2,
33
+ size = "md",
34
+ maxWidth = "xl",
35
+ background = "default",
36
+ padding = "lg",
37
+ centered = false,
38
+ className = "",
39
+ children,
40
+ ...restProps
41
+ }: Props = $props();
42
+
43
+ const sizeClasses = $derived(() => {
44
+ switch (size) {
45
+ case "sm":
46
+ return "text-sm";
47
+ case "lg":
48
+ return "text-lg";
49
+ default:
50
+ return "text-base";
51
+ }
52
+ });
53
+
54
+ const maxWidthClasses = $derived(() => {
55
+ switch (maxWidth) {
56
+ case "sm":
57
+ return "max-w-screen-sm";
58
+ case "md":
59
+ return "max-w-screen-md";
60
+ case "lg":
61
+ return "max-w-screen-lg";
62
+ case "xl":
63
+ return "max-w-screen-xl";
64
+ case "2xl":
65
+ return "max-w-screen-2xl";
66
+ case "full":
67
+ return "max-w-full";
68
+ case "none":
69
+ return "";
70
+ default:
71
+ return "max-w-screen-xl";
72
+ }
73
+ });
74
+
75
+ const backgroundClasses = $derived(() => {
76
+ switch (background) {
77
+ case "muted":
78
+ return "bg-base-50";
79
+ case "accent":
80
+ return "bg-brand-50 dark:bg-brand-950";
81
+ case "transparent":
82
+ return "bg-transparent";
83
+ default:
84
+ return "bg-background";
85
+ }
86
+ });
87
+
88
+ const paddingClasses = $derived(() => {
89
+ switch (padding) {
90
+ case "none":
91
+ return "";
92
+ case "sm":
93
+ return "py-8 sm:py-12";
94
+ case "md":
95
+ return "py-12 sm:py-16";
96
+ case "lg":
97
+ return "py-16 sm:py-24";
98
+ case "xl":
99
+ return "py-24 sm:py-32";
100
+ default:
101
+ return "py-16 sm:py-24";
102
+ }
103
+ });
104
+
105
+ const containerClasses = $derived(() => {
106
+ const base = "w-full";
107
+ const maxWidthClass = maxWidthClasses();
108
+ const centeredClass = centered ? "mx-auto" : "";
109
+ return `${base} ${maxWidthClass} ${centeredClass}`.trim();
110
+ });
111
+
112
+ const contentClasses = $derived(() => {
113
+ const base = "px-4 sm:px-6 lg:px-8";
114
+ const centeredClass = centered ? "text-center" : "";
115
+ return `${base} ${centeredClass}`.trim();
116
+ });
117
+ </script>
118
+
119
+ <section
120
+ class="section {backgroundClasses()} {paddingClasses()} {className}"
121
+ {...restProps}
122
+ >
123
+ <div class={containerClasses()}>
124
+ <div class={contentClasses()}>
125
+ {#if title}
126
+ <div class="mb-6 {centered ? 'mx-auto' : ''}">
127
+ <Heading level={headingLevel} text={title} />
128
+ {#if description}
129
+ <p
130
+ class="mt-4 {sizeClasses()} text-description {centered
131
+ ? 'mx-auto max-w-2xl'
132
+ : ''}"
133
+ >
134
+ {description}
135
+ </p>
136
+ {/if}
137
+ </div>
138
+ {/if}
139
+
140
+ {#if children}
141
+ <div class="section-content {centered ? 'mx-auto' : ''}">
142
+ {@render children()}
143
+ </div>
144
+ {/if}
145
+ </div>
146
+ </div>
147
+ </section>
148
+
149
+ <style>
150
+ .section {
151
+ position: relative;
152
+ width: 100%;
153
+ }
154
+
155
+ .section-content {
156
+ width: 100%;
157
+ }
158
+ </style>
@@ -0,0 +1,27 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { SizeVariant } from "../../types/variants.js";
3
+ interface Props {
4
+ /** Section title */
5
+ title?: string;
6
+ /** Section description/subtitle */
7
+ description?: string;
8
+ /** Heading level for title (1-6) */
9
+ headingLevel?: number;
10
+ /** Section size variant */
11
+ size?: SizeVariant;
12
+ /** Maximum width of the section container */
13
+ maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "none";
14
+ /** Background variant */
15
+ background?: "default" | "muted" | "accent" | "transparent";
16
+ /** Padding variant */
17
+ padding?: "none" | "sm" | "md" | "lg" | "xl";
18
+ /** Whether to center the content */
19
+ centered?: boolean;
20
+ /** Additional CSS classes */
21
+ className?: string;
22
+ /** Section content */
23
+ children?: Snippet;
24
+ }
25
+ declare const Section: import("svelte").Component<Props, {}, "">;
26
+ type Section = ReturnType<typeof Section>;
27
+ export default Section;
@@ -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}
@@ -1,4 +1,11 @@
1
1
  <script lang="ts">
2
+ function createId(prefix: string): string {
3
+ if (typeof window !== "undefined") {
4
+ return `${prefix}-${Math.random().toString(36).slice(2, 11)}`;
5
+ }
6
+ return `${prefix}-ssr-${Date.now()}`;
7
+ }
8
+
2
9
  interface Props {
3
10
  tabs?: Array<{
4
11
  id: string;
@@ -19,17 +26,69 @@
19
26
  ...restProps
20
27
  }: Props & { children?: any } = $props();
21
28
 
29
+ const tabsBaseId = createId("tabs");
30
+
22
31
  function selectTab(tabId: string) {
23
32
  activeTab = tabId;
24
33
  }
25
34
 
35
+ function getEnabledTabs() {
36
+ return tabs.filter((tab) => !tab.disabled);
37
+ }
38
+
39
+ function getTabIndex(tabId: string) {
40
+ return tabs.findIndex((tab) => tab.id === tabId);
41
+ }
42
+
43
+ function getTabId(tabId: string) {
44
+ return `${tabsBaseId}-tab-${tabId}`;
45
+ }
46
+
47
+ function getPanelId(tabId: string) {
48
+ return `${tabsBaseId}-panel-${tabId}`;
49
+ }
50
+
51
+ $effect(() => {
52
+ if (tabs.length === 0) {
53
+ return;
54
+ }
55
+
56
+ const activeExists = tabs.some((tab) => tab.id === activeTab && !tab.disabled);
57
+ if (!activeExists) {
58
+ const firstEnabledTab = tabs.find((tab) => !tab.disabled);
59
+ if (firstEnabledTab) {
60
+ activeTab = firstEnabledTab.id;
61
+ }
62
+ }
63
+ });
64
+
26
65
  function handleKeydown(event: KeyboardEvent) {
66
+ const enabledTabs = getEnabledTabs();
67
+ if (enabledTabs.length === 0) {
68
+ return;
69
+ }
70
+
71
+ const currentIndex = enabledTabs.findIndex((tab) => tab.id === activeTab);
72
+ const fallbackIndex = currentIndex === -1 ? 0 : currentIndex;
73
+
27
74
  if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
28
- const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);
75
+ event.preventDefault();
29
76
  const direction = event.key === "ArrowLeft" ? -1 : 1;
30
77
  const nextIndex =
31
- (currentIndex + direction + tabs.length) % tabs.length;
32
- selectTab(tabs[nextIndex].id);
78
+ (fallbackIndex + direction + enabledTabs.length) %
79
+ enabledTabs.length;
80
+ selectTab(enabledTabs[nextIndex].id);
81
+ } else if (event.key === "Home") {
82
+ event.preventDefault();
83
+ selectTab(enabledTabs[0].id);
84
+ } else if (event.key === "End") {
85
+ event.preventDefault();
86
+ selectTab(enabledTabs[enabledTabs.length - 1].id);
87
+ } else if (event.key === "Enter" || event.key === " ") {
88
+ event.preventDefault();
89
+ if (currentIndex === -1) {
90
+ selectTab(enabledTabs[0].id);
91
+ }
33
92
  }
34
93
  }
35
94
  </script>
@@ -45,6 +104,7 @@
45
104
  <button
46
105
  type="button"
47
106
  role="tab"
107
+ id={getTabId(tab.id)}
48
108
  class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab ===
49
109
  tab.id
50
110
  ? variant === 'pills'
@@ -54,13 +114,20 @@
54
114
  onclick={() => selectTab(tab.id)}
55
115
  disabled={tab.disabled}
56
116
  aria-selected={activeTab === tab.id}
117
+ aria-controls={getPanelId(tab.id)}
118
+ tabindex={activeTab === tab.id ? 0 : -1}
57
119
  >
58
120
  {tab.label}
59
121
  </button>
60
122
  {/each}
61
123
  </div>
62
124
 
63
- <div class="mt-4">
125
+ <div
126
+ class="mt-4"
127
+ role="tabpanel"
128
+ id={getPanelId(activeTab)}
129
+ aria-labelledby={getTabId(activeTab)}
130
+ >
64
131
  {@render children?.({ activeTab })}
65
132
  </div>
66
133
  </div>
@@ -11,6 +11,6 @@ export { default as NavigationMenuItem } from './NavigationMenuItem.svelte';
11
11
  export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.svelte';
12
12
  export { default as NavigationMenuContent } from './NavigationMenuContent.svelte';
13
13
  export { default as NavigationMenuLink } from './NavigationMenuLink.svelte';
14
- export { default as Sidebar } from './Sidebar.svelte';
14
+ export { default as Section } from './Section.svelte';
15
15
  export { default as SlideUp } from './SlideUp.svelte';
16
16
  export { default as Tabs } from './Tabs.svelte';
@@ -11,6 +11,6 @@ export { default as NavigationMenuItem } from './NavigationMenuItem.svelte';
11
11
  export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.svelte';
12
12
  export { default as NavigationMenuContent } from './NavigationMenuContent.svelte';
13
13
  export { default as NavigationMenuLink } from './NavigationMenuLink.svelte';
14
- export { default as Sidebar } from './Sidebar.svelte';
14
+ export { default as Section } from './Section.svelte';
15
15
  export { default as SlideUp } from './SlideUp.svelte';
16
16
  export { default as Tabs } from './Tabs.svelte';
@@ -0,0 +1,6 @@
1
+ export interface NavigationMenuContextValue {
2
+ activeItem: string | null;
3
+ setActiveItem: (item: string | null) => void;
4
+ isMobile: boolean;
5
+ }
6
+ export declare const NAVIGATION_MENU_CONTEXT_KEY: unique symbol;
@@ -0,0 +1 @@
1
+ export const NAVIGATION_MENU_CONTEXT_KEY = Symbol("navigation-menu");
@@ -12,6 +12,7 @@
12
12
  variant?: "header" | "sidebar";
13
13
  items?: NavigationItem[];
14
14
  currentPath?: string;
15
+ preventNavigation?: boolean;
15
16
  onclick?: (event: Event) => void;
16
17
  className?: string;
17
18
  }
@@ -20,13 +21,16 @@
20
21
  variant = "header",
21
22
  items = [],
22
23
  currentPath = "",
24
+ preventNavigation = false,
23
25
  onclick,
24
26
  className = "",
25
27
  ...restProps
26
28
  }: Props & { children?: any } = $props();
27
29
 
28
- function handleClick(item: NavigationItem, event: MouseEvent) {
29
- event.preventDefault();
30
+ function handleClick(event: MouseEvent) {
31
+ if (preventNavigation) {
32
+ event.preventDefault();
33
+ }
30
34
  if (onclick) {
31
35
  onclick(event as any);
32
36
  }
@@ -35,11 +39,11 @@
35
39
  const ulClasses = $derived(() => {
36
40
  return variant === "sidebar"
37
41
  ? "flex flex-col gap-1"
38
- : "flex items-center justify-between gap-1";
42
+ : "flex flex-col gap-1 md:flex-row md:items-center md:justify-between";
39
43
  });
40
44
 
41
45
  function getNavItemClasses(itemHref: string): string {
42
- const baseClasses = "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer";
46
+ const baseClasses = "flex flex-col gap-1 grow h-full items-center justify-center min-h-0 min-w-0 relative shrink-0 cursor-pointer w-full md:w-auto";
43
47
 
44
48
  return baseClasses;
45
49
  }
@@ -88,8 +92,7 @@
88
92
  <a
89
93
  href={item.href}
90
94
  class={iconContainerClasses}
91
- onclick={(e) => handleClick(item, e)}
92
- role="button"
95
+ onclick={handleClick}
93
96
  aria-current={isActive ? "page" : undefined}
94
97
  >
95
98
  <div class={getStateLayerClasses()}>
@@ -9,6 +9,7 @@ interface Props {
9
9
  variant?: "header" | "sidebar";
10
10
  items?: NavigationItem[];
11
11
  currentPath?: string;
12
+ preventNavigation?: boolean;
12
13
  onclick?: (event: Event) => void;
13
14
  className?: string;
14
15
  }