zabi-components 6.0.0 → 7.0.0
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 +137 -1473
- package/dist/atoms/ActionPanel.svelte +116 -0
- package/dist/atoms/ActionPanel.svelte.d.ts +19 -0
- package/dist/atoms/Badge.svelte +1 -1
- package/dist/atoms/Badge.svelte.d.ts +1 -1
- package/dist/atoms/Button.svelte +35 -16
- package/dist/atoms/Button.svelte.d.ts +3 -1
- package/dist/atoms/Card.svelte +12 -3
- package/dist/atoms/Card.svelte.d.ts +2 -2
- package/dist/atoms/CardHeader.svelte +8 -4
- package/dist/atoms/CardHeader.svelte.d.ts +4 -2
- package/dist/atoms/Checkbox.svelte +50 -76
- package/dist/atoms/Checkbox.svelte.d.ts +7 -2
- package/dist/atoms/CodeBlock.svelte +20 -34
- package/dist/atoms/CodeBlock.svelte.d.ts +1 -0
- package/dist/atoms/ColorPicker.svelte +2 -6
- package/dist/atoms/Container.svelte +41 -0
- package/dist/atoms/Container.svelte.d.ts +12 -0
- package/dist/atoms/Divider.svelte +68 -0
- package/dist/atoms/Divider.svelte.d.ts +13 -0
- package/dist/atoms/FeatureCard.svelte +35 -50
- package/dist/atoms/FeatureCard.svelte.d.ts +9 -4
- package/dist/atoms/IconButton.svelte +33 -24
- package/dist/atoms/IconButton.svelte.d.ts +4 -8
- package/dist/atoms/Input.svelte +58 -26
- package/dist/atoms/Input.svelte.d.ts +6 -1
- package/dist/atoms/List.svelte +2 -7
- package/dist/atoms/List.svelte.d.ts +1 -6
- package/dist/atoms/ListItem.svelte +54 -65
- package/dist/atoms/ListItem.svelte.d.ts +7 -13
- package/dist/atoms/ListItemLeading.svelte +19 -0
- package/dist/atoms/ListItemLeading.svelte.d.ts +9 -0
- package/dist/atoms/Progress.svelte +1 -1
- package/dist/atoms/Radio.svelte +51 -0
- package/dist/atoms/Radio.svelte.d.ts +14 -0
- package/dist/atoms/Select.svelte +23 -30
- package/dist/atoms/Select.svelte.d.ts +0 -10
- package/dist/atoms/SelectionControl.svelte +98 -0
- package/dist/atoms/SelectionControl.svelte.d.ts +26 -0
- package/dist/atoms/Skeleton.svelte +39 -15
- package/dist/atoms/Skeleton.svelte.d.ts +7 -3
- package/dist/atoms/Table.svelte +27 -0
- package/dist/atoms/Table.svelte.d.ts +10 -0
- package/dist/atoms/Text.svelte +46 -0
- package/dist/atoms/Text.svelte.d.ts +13 -0
- package/dist/atoms/Textarea.svelte +74 -53
- package/dist/atoms/Textarea.svelte.d.ts +6 -2
- package/dist/atoms/ThemeToggle.svelte +35 -24
- package/dist/atoms/ThemeToggle.svelte.d.ts +1 -0
- package/dist/atoms/Toast.svelte +55 -31
- package/dist/atoms/Toast.svelte.d.ts +4 -1
- package/dist/atoms/Toggle.svelte +37 -22
- package/dist/atoms/Toggle.svelte.d.ts +2 -1
- package/dist/atoms/Tooltip.svelte +90 -11
- package/dist/atoms/index.d.ts +6 -0
- package/dist/atoms/index.js +6 -0
- package/dist/atoms/selection-control.styles.d.ts +25 -0
- package/dist/atoms/selection-control.styles.js +30 -0
- package/dist/components/atoms/index.d.ts +6 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +26 -0
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -0
- package/dist/components/index.d.ts +41 -5
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +10 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/navigation-menu-context.d.ts +11 -0
- package/dist/components/molecules/navigation-menu-context.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +39 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -0
- package/dist/components/organisms/index.d.ts +2 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/page.types.d.ts +69 -0
- package/dist/components/types/page.types.d.ts.map +1 -0
- package/dist/components/types/page.types.js +1 -0
- package/dist/components/types/variants.d.ts +64 -0
- package/dist/components/types/variants.d.ts.map +1 -0
- package/dist/components/types/variants.js +44 -0
- package/dist/components/util/fixed-sidebar-flyout.d.ts +23 -0
- package/dist/components/util/fixed-sidebar-flyout.d.ts.map +1 -0
- package/dist/components/util/focus-utils.d.ts +12 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +17 -0
- package/dist/components/util/ssr-safe.d.ts.map +1 -0
- package/dist/index.d.ts +41 -5
- package/dist/index.js +44 -8
- package/dist/lib/layout-width-tokens.d.ts +17 -0
- package/dist/lib/layout-width-tokens.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Button.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Button.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Input.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Input.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/List.d.ts +3 -0
- package/dist/lib/showcase/component-docs/List.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Modal.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Radio.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +3 -0
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +3 -0
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/_shared.d.ts +12 -0
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +1 -0
- package/dist/lib/showcase/component-docs/index.d.ts +4 -0
- package/dist/lib/showcase/component-docs/index.d.ts.map +1 -0
- package/dist/lib/showcase/components-catalog.d.ts.map +1 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +26 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -1
- package/dist/molecules/Alert.svelte +50 -23
- package/dist/molecules/Alert.svelte.d.ts +1 -1
- package/dist/molecules/ComponentDemo.svelte +2 -2
- package/dist/molecules/ContactForm.svelte +1 -1
- package/dist/molecules/Dropdown.svelte +99 -109
- package/dist/molecules/Dropdown.svelte.d.ts +12 -6
- package/dist/molecules/EmptyState.svelte +44 -0
- package/dist/molecules/EmptyState.svelte.d.ts +11 -0
- package/dist/molecules/FormField.svelte +89 -0
- package/dist/molecules/FormField.svelte.d.ts +23 -0
- package/dist/molecules/Header.svelte +41 -0
- package/dist/molecules/Header.svelte.d.ts +10 -0
- package/dist/molecules/Modal.svelte +37 -24
- package/dist/molecules/Modal.svelte.d.ts +4 -2
- package/dist/molecules/NavigationMenu.svelte +16 -0
- package/dist/molecules/NavigationMenu.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuContent.svelte +9 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuTrigger.svelte +9 -1
- package/dist/molecules/Page.svelte +16 -0
- package/dist/molecules/Page.svelte.d.ts +8 -0
- package/dist/molecules/PropsTable.svelte +61 -0
- package/dist/molecules/PropsTable.svelte.d.ts +8 -0
- package/dist/molecules/RadioGroup.svelte +197 -0
- package/dist/molecules/RadioGroup.svelte.d.ts +20 -0
- package/dist/molecules/Section.svelte +2 -11
- package/dist/molecules/Section.svelte.d.ts +2 -11
- package/dist/molecules/SidebarBrandHeader.svelte +68 -0
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +10 -0
- package/dist/molecules/SidebarFooter.svelte +119 -0
- package/dist/molecules/SidebarFooter.svelte.d.ts +23 -0
- package/dist/molecules/SidebarNavSection.svelte +49 -0
- package/dist/molecules/SidebarNavSection.svelte.d.ts +14 -0
- package/dist/molecules/SlideUp.svelte +19 -14
- package/dist/molecules/SlideUp.svelte.d.ts +1 -1
- package/dist/molecules/Tabs.svelte +4 -9
- package/dist/molecules/Toaster.svelte +21 -0
- package/dist/molecules/Toaster.svelte.d.ts +6 -0
- package/dist/molecules/ToasterToast.svelte +217 -0
- package/dist/molecules/ToasterToast.svelte.d.ts +7 -0
- package/dist/molecules/index.d.ts +10 -0
- package/dist/molecules/index.js +10 -0
- package/dist/molecules/navigation-menu-context.d.ts +11 -0
- package/dist/molecules/navigation-menu-context.js +18 -0
- package/dist/molecules/toast-store.d.ts +38 -0
- package/dist/molecules/toast-store.js +34 -0
- package/dist/organisms/SidebarAccountPanel.svelte +118 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +20 -0
- package/dist/organisms/SidebarNavigation.svelte +267 -237
- package/dist/organisms/SidebarNavigation.svelte.d.ts +21 -5
- package/dist/organisms/SidebarPanel.svelte +54 -22
- package/dist/organisms/SidebarPanel.svelte.d.ts +3 -0
- package/dist/organisms/{Navbar.svelte → TopNavbar.svelte} +22 -17
- package/dist/organisms/TopNavbar.svelte.d.ts +35 -0
- package/dist/organisms/index.d.ts +2 -1
- package/dist/organisms/index.js +2 -1
- package/dist/routes/lib/focus-utils.d.ts +7 -18
- package/dist/routes/lib/focus-utils.ts +32 -34
- package/dist/routes/lib/ssr-safe.ts +5 -4
- package/dist/routes/lib/variant-utils.ts +48 -39
- package/dist/types/components.d.ts +187 -0
- package/dist/types/components.d.ts.map +1 -0
- package/dist/types/events.d.ts +200 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/index.d.ts +171 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/page.types.d.ts +2 -31
- package/dist/types/page.types.d.ts.map +1 -1
- package/dist/types/page.types.js +1 -0
- package/dist/types/page.types.ts +79 -0
- package/dist/types/variants.d.ts +3 -0
- package/dist/types/variants.d.ts.map +1 -0
- package/dist/types/variants.js +1 -0
- package/dist/types/variants.ts +5 -6
- package/dist/util/fixed-sidebar-flyout.d.ts +22 -0
- package/dist/util/fixed-sidebar-flyout.js +148 -0
- package/dist/util/focus-utils.d.ts +11 -0
- package/dist/util/focus-utils.js +94 -0
- package/dist/util/ssr-safe.d.ts +16 -0
- package/dist/util/ssr-safe.js +48 -0
- package/dist/zabi-components-colors.css +14 -1
- package/dist/zabi-components-theme-dark-only.css +5 -0
- package/dist/zabi-components-theme-dark.css +5 -0
- package/dist/zabi-components-theme-only.css +9 -1
- package/dist/zabi-components-theme.css +9 -1
- package/dist/zabi-components.css +877 -192
- package/package.json +19 -7
- package/dist/organisms/Navbar.svelte.d.ts +0 -41
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
1
2
|
import type { Component } from "svelte";
|
|
3
|
+
import type { ButtonVariant, SizeVariant } from "../types/variants.js";
|
|
2
4
|
export interface SidebarNavigationItem {
|
|
3
5
|
id: string;
|
|
4
6
|
label: string;
|
|
@@ -10,13 +12,19 @@ export interface SidebarNavigationItem {
|
|
|
10
12
|
badgeText?: string | number;
|
|
11
13
|
badgeCount?: number;
|
|
12
14
|
group?: "primary" | "secondary";
|
|
15
|
+
/** Groups items under a section heading in the rail. */
|
|
16
|
+
section?: string;
|
|
13
17
|
}
|
|
14
18
|
interface Props {
|
|
15
19
|
mode?: "expanded" | "collapsed";
|
|
20
|
+
layout?: "rail" | "card";
|
|
16
21
|
items?: SidebarNavigationItem[];
|
|
17
22
|
currentPath?: string;
|
|
18
23
|
ariaLabel?: string;
|
|
19
24
|
className?: string;
|
|
25
|
+
logoSrc?: string;
|
|
26
|
+
logoAlt?: string;
|
|
27
|
+
brandName?: string;
|
|
20
28
|
showProfile?: boolean;
|
|
21
29
|
profileName?: string;
|
|
22
30
|
profileEmail?: string;
|
|
@@ -25,6 +33,13 @@ interface Props {
|
|
|
25
33
|
searchMode?: "input" | "button";
|
|
26
34
|
searchPlaceholder?: string;
|
|
27
35
|
searchValue?: string;
|
|
36
|
+
/** Trigger icon when search is a button or rail is collapsed; input mode keeps `Search` on the field. */
|
|
37
|
+
searchTriggerIcon?: Component<{
|
|
38
|
+
size?: number;
|
|
39
|
+
class?: string;
|
|
40
|
+
}>;
|
|
41
|
+
searchTriggerVariant?: ButtonVariant;
|
|
42
|
+
searchTriggerSize?: SizeVariant;
|
|
28
43
|
showLogout?: boolean;
|
|
29
44
|
logoutLabel?: string;
|
|
30
45
|
showThemeToggle?: boolean;
|
|
@@ -38,12 +53,13 @@ interface Props {
|
|
|
38
53
|
onLogout?: () => void;
|
|
39
54
|
onThemeToggle?: (nextIsLightMode: boolean) => void;
|
|
40
55
|
onEmptyStateAction?: () => void;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
onProfileClick?: (event?: MouseEvent) => void;
|
|
57
|
+
profilePanelOpen?: boolean;
|
|
58
|
+
profilePanelControlsId?: string;
|
|
59
|
+
profilePanel?: Snippet;
|
|
60
|
+
/** Parent nav href to highlight when `currentPath` is a deeper leaf (e.g. category row). */
|
|
45
61
|
activePrimaryHref?: string;
|
|
46
62
|
}
|
|
47
|
-
declare const SidebarNavigation: Component<Props, {}, "
|
|
63
|
+
declare const SidebarNavigation: Component<Props, {}, "isLightMode" | "searchValue">;
|
|
48
64
|
type SidebarNavigation = ReturnType<typeof SidebarNavigation>;
|
|
49
65
|
export default SidebarNavigation;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
interface Props {
|
|
16
16
|
className?: string;
|
|
17
17
|
widthClass?: string;
|
|
18
|
+
variant?: "plain" | "elevated";
|
|
18
19
|
ariaLabel?: string;
|
|
19
20
|
title?: string;
|
|
20
21
|
subtitle?: string;
|
|
@@ -25,15 +26,18 @@
|
|
|
25
26
|
selectedItemId?: string;
|
|
26
27
|
emptyStateTitle?: string;
|
|
27
28
|
emptyStateDescription?: string;
|
|
29
|
+
emptyStateActionLabel?: string;
|
|
28
30
|
selectLabel?: string;
|
|
29
31
|
closeLabel?: string;
|
|
30
32
|
onSelect?: (item: SidebarPanelItem) => void;
|
|
31
33
|
onClose?: () => void;
|
|
34
|
+
onEmptyStateAction?: () => void;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
let {
|
|
35
38
|
className = "",
|
|
36
39
|
widthClass = "w-80",
|
|
40
|
+
variant = "elevated",
|
|
37
41
|
ariaLabel = "Picker panel",
|
|
38
42
|
title = "Items",
|
|
39
43
|
subtitle = "Choose an item to continue",
|
|
@@ -44,10 +48,12 @@
|
|
|
44
48
|
selectedItemId = $bindable(""),
|
|
45
49
|
emptyStateTitle = "No matches",
|
|
46
50
|
emptyStateDescription = "Try a different keyword.",
|
|
51
|
+
emptyStateActionLabel = "",
|
|
47
52
|
selectLabel = "Select item",
|
|
48
53
|
closeLabel = "Close panel",
|
|
49
54
|
onSelect,
|
|
50
55
|
onClose,
|
|
56
|
+
onEmptyStateAction,
|
|
51
57
|
...restProps
|
|
52
58
|
}: Props = $props();
|
|
53
59
|
|
|
@@ -60,19 +66,24 @@
|
|
|
60
66
|
: items,
|
|
61
67
|
);
|
|
62
68
|
|
|
69
|
+
const isElevated = $derived(variant === "elevated");
|
|
70
|
+
|
|
63
71
|
const containerClasses = $derived.by(() => {
|
|
64
72
|
const resolvedWidthClass = widthClass.trim() || "w-80";
|
|
65
|
-
|
|
73
|
+
const shell = isElevated
|
|
74
|
+
? "rounded-3xl border border-border bg-card text-headline shadow-md ring-1 ring-border/50"
|
|
75
|
+
: "rounded-3xl border border-border bg-card text-headline shadow-sm";
|
|
76
|
+
return `${resolvedWidthClass} shrink-0 p-5 ${shell} ${className}`.trim();
|
|
66
77
|
});
|
|
67
78
|
|
|
68
79
|
function getItemClasses(itemId: string): string {
|
|
69
80
|
const isActive = selectedItemId === itemId;
|
|
70
81
|
const baseClasses =
|
|
71
|
-
"w-full cursor-pointer rounded-
|
|
82
|
+
"focus-ring focus-ring--nav w-full cursor-pointer rounded-lg px-3 py-2.5 text-left transition-colors duration-150";
|
|
72
83
|
if (isActive) {
|
|
73
|
-
return `${baseClasses} bg-
|
|
84
|
+
return `${baseClasses} bg-nav-menu-active text-inherit shadow-sm ring-1 ring-border/80`;
|
|
74
85
|
}
|
|
75
|
-
return `${baseClasses} hover:bg-
|
|
86
|
+
return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
function handleSelect(item: SidebarPanelItem): void {
|
|
@@ -87,44 +98,52 @@
|
|
|
87
98
|
onClose();
|
|
88
99
|
}
|
|
89
100
|
}
|
|
101
|
+
|
|
102
|
+
function handleEmptyStateAction(): void {
|
|
103
|
+
if (onEmptyStateAction) {
|
|
104
|
+
onEmptyStateAction();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
90
107
|
</script>
|
|
91
108
|
|
|
92
109
|
<aside class={containerClasses} aria-label={ariaLabel} {...restProps}>
|
|
93
|
-
<div class="mb-
|
|
94
|
-
<div class="min-w-0">
|
|
95
|
-
<h3 class="truncate text-base font-semibold text-headline">
|
|
96
|
-
|
|
110
|
+
<div class="mb-5 flex items-start justify-between gap-3">
|
|
111
|
+
<div class="min-w-0 pr-1">
|
|
112
|
+
<h3 class="truncate text-base font-semibold tracking-tight text-headline">
|
|
113
|
+
{title}
|
|
114
|
+
</h3>
|
|
115
|
+
<p class="mt-0.5 text-sm leading-relaxed text-description">{subtitle}</p>
|
|
97
116
|
</div>
|
|
98
117
|
<button
|
|
99
118
|
type="button"
|
|
100
|
-
class="inline-flex size-
|
|
119
|
+
class="-mr-0.5 -mt-0.5 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-lg text-nav-menu-item transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover focus-ring focus-ring--nav"
|
|
101
120
|
aria-label={closeLabel}
|
|
102
121
|
onclick={handleClose}
|
|
103
122
|
>
|
|
104
|
-
<X size={
|
|
123
|
+
<X size={18} />
|
|
105
124
|
</button>
|
|
106
125
|
</div>
|
|
107
126
|
|
|
108
127
|
{#if showSearch}
|
|
109
|
-
<div class="relative mb-
|
|
128
|
+
<div class="relative mb-5">
|
|
110
129
|
<span
|
|
111
|
-
class="pointer-events-none absolute left-
|
|
130
|
+
class="pointer-events-none absolute left-3 top-1/2 z-10 -translate-y-1/2 text-description"
|
|
112
131
|
aria-hidden="true"
|
|
113
132
|
>
|
|
114
|
-
<Search size={
|
|
133
|
+
<Search size={17} />
|
|
115
134
|
</span>
|
|
116
135
|
<Input
|
|
117
136
|
type="search"
|
|
118
137
|
bind:value={searchValue}
|
|
119
138
|
placeholder={searchPlaceholder}
|
|
120
139
|
aria-label={searchPlaceholder}
|
|
121
|
-
class="w-full min-w-0 rounded-xl pl-
|
|
140
|
+
class="w-full min-w-0 min-h-10 rounded-xl border-transparent !bg-transparent py-2 pl-10 text-sm ring-1 ring-border/60 hover:!bg-nav-menu-hover focus:!bg-transparent focus-ring focus-ring--nav"
|
|
122
141
|
/>
|
|
123
142
|
</div>
|
|
124
143
|
{/if}
|
|
125
144
|
|
|
126
145
|
{#if filteredItems.length > 0}
|
|
127
|
-
<ul class="space-y-
|
|
146
|
+
<ul class="space-y-1" role="listbox" aria-label={selectLabel}>
|
|
128
147
|
{#each filteredItems as item (item.id)}
|
|
129
148
|
<li role="presentation">
|
|
130
149
|
<button
|
|
@@ -135,18 +154,20 @@
|
|
|
135
154
|
aria-selected={selectedItemId === item.id}
|
|
136
155
|
>
|
|
137
156
|
<span class="flex items-center justify-between gap-3">
|
|
138
|
-
<span class="flex min-w-0 items-start gap-2">
|
|
157
|
+
<span class="flex min-w-0 items-start gap-2.5">
|
|
139
158
|
{#if item.icon}
|
|
140
159
|
{@const Icon = item.icon}
|
|
141
160
|
<span
|
|
142
|
-
class="mt-0.5 inline-flex size-
|
|
161
|
+
class="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center text-current"
|
|
143
162
|
aria-hidden="true"
|
|
144
163
|
>
|
|
145
|
-
<Icon size={
|
|
164
|
+
<Icon size={18} />
|
|
146
165
|
</span>
|
|
147
166
|
{/if}
|
|
148
167
|
<span class="min-w-0">
|
|
149
|
-
<span
|
|
168
|
+
<span
|
|
169
|
+
class="block truncate text-sm font-medium text-current"
|
|
170
|
+
>
|
|
150
171
|
{item.label}
|
|
151
172
|
</span>
|
|
152
173
|
{#if item.description}
|
|
@@ -167,9 +188,20 @@
|
|
|
167
188
|
{/each}
|
|
168
189
|
</ul>
|
|
169
190
|
{:else}
|
|
170
|
-
<div
|
|
191
|
+
<div
|
|
192
|
+
class="rounded-xl border border-dashed border-border bg-transparent px-3.5 py-4 ring-1 ring-border/40"
|
|
193
|
+
>
|
|
171
194
|
<h4 class="text-sm font-semibold text-headline">{emptyStateTitle}</h4>
|
|
172
|
-
<p class="mt-1 text-sm text-description">{emptyStateDescription}</p>
|
|
195
|
+
<p class="mt-1 text-sm leading-relaxed text-description">{emptyStateDescription}</p>
|
|
196
|
+
{#if emptyStateActionLabel.trim() && onEmptyStateAction}
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
class="mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm font-medium text-action-primary transition-colors hover:bg-action-primary-hover focus-ring focus-ring--nav"
|
|
200
|
+
onclick={handleEmptyStateAction}
|
|
201
|
+
>
|
|
202
|
+
{emptyStateActionLabel.trim()}
|
|
203
|
+
</button>
|
|
204
|
+
{/if}
|
|
173
205
|
</div>
|
|
174
206
|
{/if}
|
|
175
|
-
</aside>
|
|
207
|
+
</aside>
|
|
@@ -12,6 +12,7 @@ export interface SidebarPanelItem {
|
|
|
12
12
|
interface Props {
|
|
13
13
|
className?: string;
|
|
14
14
|
widthClass?: string;
|
|
15
|
+
variant?: "plain" | "elevated";
|
|
15
16
|
ariaLabel?: string;
|
|
16
17
|
title?: string;
|
|
17
18
|
subtitle?: string;
|
|
@@ -22,10 +23,12 @@ interface Props {
|
|
|
22
23
|
selectedItemId?: string;
|
|
23
24
|
emptyStateTitle?: string;
|
|
24
25
|
emptyStateDescription?: string;
|
|
26
|
+
emptyStateActionLabel?: string;
|
|
25
27
|
selectLabel?: string;
|
|
26
28
|
closeLabel?: string;
|
|
27
29
|
onSelect?: (item: SidebarPanelItem) => void;
|
|
28
30
|
onClose?: () => void;
|
|
31
|
+
onEmptyStateAction?: () => void;
|
|
29
32
|
}
|
|
30
33
|
declare const SidebarPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
|
|
31
34
|
type SidebarPanel = ReturnType<typeof SidebarPanel>;
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import ThemeToggle from "../atoms/ThemeToggle.svelte";
|
|
3
3
|
import type { Component, Snippet } from "svelte";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export interface NavbarNavItem {
|
|
5
|
+
export interface TopNavbarNavItem {
|
|
7
6
|
label: string;
|
|
8
7
|
href: string;
|
|
9
8
|
icon?: Component<{ size?: number; class?: string }>;
|
|
@@ -12,19 +11,14 @@
|
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
14
13
|
brand?: string;
|
|
15
|
-
/** When set, the brand is a link (use `""` to show text only). */
|
|
16
14
|
brandHref?: string;
|
|
17
|
-
/**
|
|
15
|
+
/** `aria-label` on `<nav>` when multiple nav landmarks exist. */
|
|
18
16
|
ariaLabel?: string;
|
|
19
17
|
showThemeToggle?: boolean;
|
|
20
18
|
className?: string;
|
|
21
|
-
/**
|
|
22
|
-
* When true, renders only the built-in link list (or `nav` snippet) inside a `nav` landmark.
|
|
23
|
-
* Use for headers that are not the full chrome bar.
|
|
24
|
-
*/
|
|
19
|
+
/** Slim mode: only the `nav` region (no full chrome bar). */
|
|
25
20
|
embedded?: boolean;
|
|
26
|
-
|
|
27
|
-
items?: NavbarNavItem[];
|
|
21
|
+
items?: TopNavbarNavItem[];
|
|
28
22
|
navVariant?: "header" | "sidebar";
|
|
29
23
|
currentPath?: string;
|
|
30
24
|
preventNavigation?: boolean;
|
|
@@ -63,7 +57,18 @@
|
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
|
|
60
|
+
/** App routes: prefix match except `/` and absolute URLs (exact match). */
|
|
61
|
+
function isNavItemActive(href: string): boolean {
|
|
62
|
+
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
63
|
+
return currentPath === href;
|
|
64
|
+
}
|
|
65
|
+
if (href === "/") {
|
|
66
|
+
return currentPath === "/";
|
|
67
|
+
}
|
|
68
|
+
return currentPath === href || currentPath.startsWith(`${href}/`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ulClasses = $derived.by(() => {
|
|
67
72
|
return navVariant === "sidebar"
|
|
68
73
|
? "flex flex-col gap-1"
|
|
69
74
|
: "flex flex-col gap-1 md:flex-row md:items-center md:justify-between";
|
|
@@ -74,7 +79,7 @@
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
function getIconContainerClasses(): string {
|
|
77
|
-
return "group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-[20px] shrink-0 text-nav-menu-item transition-colors duration-200 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover aria-[current=page]:bg-nav-menu-active aria-[current=page]:text-nav-menu-item-active
|
|
82
|
+
return "focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-[20px] shrink-0 text-nav-menu-item transition-colors duration-200 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover aria-[current=page]:bg-nav-menu-active aria-[current=page]:text-nav-menu-item-active";
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
function getStateLayerClasses(): string {
|
|
@@ -91,9 +96,9 @@
|
|
|
91
96
|
</script>
|
|
92
97
|
|
|
93
98
|
{#snippet defaultNavList()}
|
|
94
|
-
<ul class="{ulClasses
|
|
95
|
-
{#each items as item}
|
|
96
|
-
{@const isActive =
|
|
99
|
+
<ul class="{ulClasses} list-none m-0 p-0">
|
|
100
|
+
{#each items as item (item.href)}
|
|
101
|
+
{@const isActive = isNavItemActive(item.href)}
|
|
97
102
|
<li class={getNavItemClasses()}>
|
|
98
103
|
<a
|
|
99
104
|
href={item.href}
|
|
@@ -133,7 +138,7 @@
|
|
|
133
138
|
</nav>
|
|
134
139
|
{:else}
|
|
135
140
|
<nav
|
|
136
|
-
class="
|
|
141
|
+
class="border-b border-border bg-background sticky top-0 z-50 {className}"
|
|
137
142
|
aria-label={ariaLabel}
|
|
138
143
|
{...restProps}
|
|
139
144
|
>
|
|
@@ -143,7 +148,7 @@
|
|
|
143
148
|
{#if brandHref}
|
|
144
149
|
<a
|
|
145
150
|
href={brandHref}
|
|
146
|
-
class="text-xl font-bold text-headline hover:opacity-90
|
|
151
|
+
class="focus-ring text-xl font-bold text-headline hover:opacity-90"
|
|
147
152
|
>
|
|
148
153
|
{brand}
|
|
149
154
|
</a>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
export interface TopNavbarNavItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
icon?: Component<{
|
|
6
|
+
size?: number;
|
|
7
|
+
class?: string;
|
|
8
|
+
}>;
|
|
9
|
+
iconFilled?: Component<{
|
|
10
|
+
size?: number;
|
|
11
|
+
class?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
interface Props {
|
|
15
|
+
brand?: string;
|
|
16
|
+
brandHref?: string;
|
|
17
|
+
/** `aria-label` on `<nav>` when multiple nav landmarks exist. */
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
showThemeToggle?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Slim mode: only the `nav` region (no full chrome bar). */
|
|
22
|
+
embedded?: boolean;
|
|
23
|
+
items?: TopNavbarNavItem[];
|
|
24
|
+
navVariant?: "header" | "sidebar";
|
|
25
|
+
currentPath?: string;
|
|
26
|
+
preventNavigation?: boolean;
|
|
27
|
+
onclick?: (event: Event) => void;
|
|
28
|
+
}
|
|
29
|
+
type $$ComponentProps = Props & {
|
|
30
|
+
nav?: Snippet;
|
|
31
|
+
actions?: Snippet;
|
|
32
|
+
};
|
|
33
|
+
declare const TopNavbar: Component<$$ComponentProps, {}, "">;
|
|
34
|
+
type TopNavbar = ReturnType<typeof TopNavbar>;
|
|
35
|
+
export default TopNavbar;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { default as
|
|
1
|
+
export { default as TopNavbar } from './TopNavbar.svelte';
|
|
2
2
|
export { default as SidebarNavigation } from './SidebarNavigation.svelte';
|
|
3
|
+
export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
|
|
3
4
|
export { default as SidebarPanel } from './SidebarPanel.svelte';
|
package/dist/organisms/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { default as
|
|
1
|
+
export { default as TopNavbar } from './TopNavbar.svelte';
|
|
2
2
|
export { default as SidebarNavigation } from './SidebarNavigation.svelte';
|
|
3
|
+
export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
|
|
3
4
|
export { default as SidebarPanel } from './SidebarPanel.svelte';
|
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Get all focusable elements within a container
|
|
6
|
-
*/
|
|
1
|
+
/** Focus helpers for overlays (modals, sheets). Requires `app.css` focus ring classes. */
|
|
2
|
+
/** From `app.css` `@layer components` — import the library/app stylesheet. */
|
|
3
|
+
export declare const FOCUS_BRAND_CLASS = "focus-brand";
|
|
4
|
+
export declare const FOCUS_NAV_CLASS = "focus-nav";
|
|
7
5
|
export declare function getFocusableElements(container: HTMLElement): HTMLElement[];
|
|
8
|
-
/**
|
|
9
|
-
* Trap focus within a container element
|
|
10
|
-
*/
|
|
6
|
+
/** Tab wraps first↔last inside `container`. Caller should pair with `saveFocus` / `returnFocus`. */
|
|
11
7
|
export declare function trapFocus(container: HTMLElement): () => void;
|
|
12
|
-
/**
|
|
13
|
-
* Save the currently focused element
|
|
14
|
-
*/
|
|
15
8
|
export declare function saveFocus(): void;
|
|
16
|
-
/**
|
|
17
|
-
* Return focus to the previously focused element
|
|
18
|
-
*/
|
|
9
|
+
/** Restores focus from the last `saveFocus`. Skips disconnected or unfocusable nodes. */
|
|
19
10
|
export declare function returnFocus(): void;
|
|
20
|
-
/**
|
|
21
|
-
* Focus the first focusable element in a container
|
|
22
|
-
*/
|
|
23
11
|
export declare function focusFirstElement(container: HTMLElement): void;
|
|
12
|
+
//# sourceMappingURL=focus-utils.d.ts.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*/
|
|
1
|
+
/** Focus helpers for overlays (modals, sheets). Requires `app.css` focus ring classes. */
|
|
2
|
+
|
|
3
|
+
/** From `app.css` `@layer components` — import the library/app stylesheet. */
|
|
4
|
+
export const FOCUS_BRAND_CLASS = "focus-brand";
|
|
5
|
+
export const FOCUS_NAV_CLASS = "focus-nav";
|
|
4
6
|
|
|
5
|
-
/**
|
|
6
|
-
* Get all focusable elements within a container
|
|
7
|
-
*/
|
|
8
7
|
export function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|
9
8
|
const selector = [
|
|
10
9
|
'button:not([disabled])',
|
|
@@ -17,27 +16,23 @@ export function getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|
|
17
16
|
|
|
18
17
|
return Array.from(container.querySelectorAll<HTMLElement>(selector)).filter(
|
|
19
18
|
(el) => {
|
|
20
|
-
// Filter out elements that are not visible
|
|
21
19
|
const style = window.getComputedStyle(el);
|
|
22
20
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
23
21
|
}
|
|
24
22
|
);
|
|
25
23
|
}
|
|
26
24
|
|
|
27
|
-
/**
|
|
28
|
-
* Trap focus within a container element
|
|
29
|
-
*/
|
|
25
|
+
/** Tab wraps first↔last inside `container`. Caller should pair with `saveFocus` / `returnFocus`. */
|
|
30
26
|
export function trapFocus(container: HTMLElement): () => void {
|
|
31
27
|
const focusableElements = getFocusableElements(container);
|
|
32
28
|
|
|
33
29
|
if (focusableElements.length === 0) {
|
|
34
|
-
return () => {};
|
|
30
|
+
return () => {};
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
const firstElement = focusableElements[0];
|
|
38
34
|
const lastElement = focusableElements[focusableElements.length - 1];
|
|
39
35
|
|
|
40
|
-
// Focus first element
|
|
41
36
|
firstElement.focus();
|
|
42
37
|
|
|
43
38
|
function handleKeydown(event: KeyboardEvent) {
|
|
@@ -54,13 +49,11 @@ export function trapFocus(container: HTMLElement): () => void {
|
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
if (event.shiftKey) {
|
|
57
|
-
// Shift + Tab
|
|
58
52
|
if (currentFocus === firstElement) {
|
|
59
53
|
event.preventDefault();
|
|
60
54
|
lastElement.focus();
|
|
61
55
|
}
|
|
62
56
|
} else {
|
|
63
|
-
// Tab
|
|
64
57
|
if (currentFocus === lastElement) {
|
|
65
58
|
event.preventDefault();
|
|
66
59
|
firstElement.focus();
|
|
@@ -70,43 +63,48 @@ export function trapFocus(container: HTMLElement): () => void {
|
|
|
70
63
|
|
|
71
64
|
container.addEventListener('keydown', handleKeydown);
|
|
72
65
|
|
|
73
|
-
// Return cleanup function
|
|
74
66
|
return () => {
|
|
75
67
|
container.removeEventListener('keydown', handleKeydown);
|
|
76
68
|
};
|
|
77
69
|
}
|
|
78
70
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
*/
|
|
82
|
-
let previousFocus: HTMLElement | null = null;
|
|
71
|
+
/** One entry per overlay open; `returnFocus` pops LIFO (nested modals). */
|
|
72
|
+
const focusReturnStack: Array<HTMLElement | null> = [];
|
|
83
73
|
|
|
84
|
-
/**
|
|
85
|
-
* Save the currently focused element
|
|
86
|
-
*/
|
|
87
74
|
export function saveFocus(): void {
|
|
88
|
-
if (typeof document
|
|
89
|
-
|
|
75
|
+
if (typeof document === 'undefined') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const active = document.activeElement;
|
|
79
|
+
if (active instanceof HTMLElement && active !== document.body) {
|
|
80
|
+
focusReturnStack.push(active);
|
|
81
|
+
} else {
|
|
82
|
+
focusReturnStack.push(null);
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
85
|
|
|
93
|
-
/**
|
|
94
|
-
* Return focus to the previously focused element
|
|
95
|
-
*/
|
|
86
|
+
/** Restores focus from the last `saveFocus`. Skips disconnected or unfocusable nodes. */
|
|
96
87
|
export function returnFocus(): void {
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
if (typeof document === 'undefined') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const el = focusReturnStack.pop();
|
|
92
|
+
if (!el) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof el.focus !== 'function' || !el.isConnected) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
el.focus({ preventScroll: true });
|
|
100
|
+
} catch {
|
|
101
|
+
/* embeds / shadow roots may reject programmatic focus */
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
/**
|
|
104
|
-
* Focus the first focusable element in a container
|
|
105
|
-
*/
|
|
106
105
|
export function focusFirstElement(container: HTMLElement): void {
|
|
107
106
|
const focusableElements = getFocusableElements(container);
|
|
108
107
|
if (focusableElements.length > 0) {
|
|
109
108
|
focusableElements[0].focus();
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
|
-
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser-only guards and id generation for components that run under SSR.
|
|
3
|
+
* `generateId()` is non-deterministic on the client (random) vs counter on the server —
|
|
4
|
+
* use stable ids for ARIA pairs (e.g. `menuId` on NavigationMenu), not for persisted keys.
|
|
5
|
+
*/
|
|
3
6
|
|
|
4
|
-
// Check if we're in a browser environment
|
|
5
7
|
export function isBrowser(): boolean {
|
|
6
8
|
return typeof window !== 'undefined';
|
|
7
9
|
}
|
|
@@ -46,7 +48,6 @@ export function safeClearInterval(id: NodeJS.Timeout | undefined): void {
|
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
// Generate SSR-safe unique IDs
|
|
50
51
|
let idCounter = 0;
|
|
51
52
|
export function generateId(prefix: string = 'id'): string {
|
|
52
53
|
if (isBrowser()) {
|