zabi-components 7.0.2 → 8.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 +46 -3
- package/THEMING.md +255 -15
- package/dist/atoms/ActionPanel.svelte +12 -7
- package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
- package/dist/atoms/Badge.svelte +93 -56
- package/dist/atoms/Badge.svelte.d.ts +15 -3
- package/dist/atoms/Button.svelte +49 -49
- package/dist/atoms/Button.svelte.d.ts +3 -0
- package/dist/atoms/Card.svelte +31 -28
- package/dist/atoms/Card.svelte.d.ts +5 -2
- package/dist/atoms/CardContent.svelte +10 -2
- package/dist/atoms/CardContent.svelte.d.ts +2 -0
- package/dist/atoms/CardFooter.svelte +10 -2
- package/dist/atoms/CardFooter.svelte.d.ts +2 -0
- package/dist/atoms/CardHeader.svelte +10 -2
- package/dist/atoms/CardHeader.svelte.d.ts +2 -0
- package/dist/atoms/Checkbox.svelte +4 -0
- package/dist/atoms/Checkbox.svelte.d.ts +2 -0
- package/dist/atoms/CodeBlock.svelte +15 -7
- package/dist/atoms/CodeBlock.svelte.d.ts +9 -3
- package/dist/atoms/ColorPicker.svelte +19 -9
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -1
- package/dist/atoms/Container.svelte +2 -3
- package/dist/atoms/Divider.svelte +4 -4
- package/dist/atoms/FeatureCard.svelte +4 -3
- package/dist/atoms/Heading.svelte +56 -27
- package/dist/atoms/Heading.svelte.d.ts +17 -4
- package/dist/atoms/IconButton.svelte +30 -37
- package/dist/atoms/Input.svelte +26 -34
- package/dist/atoms/List.svelte +10 -2
- package/dist/atoms/List.svelte.d.ts +2 -0
- package/dist/atoms/ListItem.svelte +2 -2
- package/dist/atoms/ListItemLeading.svelte +4 -2
- package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
- package/dist/atoms/OptimizedImage.svelte +11 -2
- package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
- package/dist/atoms/Progress.svelte +4 -1
- package/dist/atoms/Progress.svelte.d.ts +2 -0
- package/dist/atoms/Radio.svelte +4 -0
- package/dist/atoms/Radio.svelte.d.ts +2 -0
- package/dist/atoms/Select.svelte +65 -67
- package/dist/atoms/Select.svelte.d.ts +4 -0
- package/dist/atoms/SelectionControl.svelte +5 -1
- package/dist/atoms/SelectionControl.svelte.d.ts +2 -0
- package/dist/atoms/Skeleton.svelte +5 -3
- package/dist/atoms/Table.svelte +2 -1
- package/dist/atoms/Text.svelte +34 -5
- package/dist/atoms/Text.svelte.d.ts +7 -1
- package/dist/atoms/Textarea.svelte +7 -6
- package/dist/atoms/ThemeToggle.svelte +17 -14
- package/dist/atoms/ThemeToggle.svelte.d.ts +2 -0
- package/dist/atoms/Toast.svelte +10 -8
- package/dist/atoms/Toggle.svelte +21 -13
- package/dist/atoms/Toggle.svelte.d.ts +7 -0
- package/dist/atoms/Tooltip.svelte +80 -22
- package/dist/atoms/Tooltip.svelte.d.ts +15 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +1 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/util/cn.d.ts +8 -0
- package/dist/components/util/cn.d.ts.map +1 -0
- package/dist/components/util/ssr-safe.d.ts +4 -4
- package/dist/components/util/ssr-safe.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -6
- package/dist/molecules/Alert.svelte +43 -64
- package/dist/molecules/Alert.svelte.d.ts +7 -1
- package/dist/molecules/ComponentDemo.svelte +10 -2
- package/dist/molecules/ComponentDemo.svelte.d.ts +2 -0
- package/dist/molecules/ContactForm.svelte +12 -2
- package/dist/molecules/ContactForm.svelte.d.ts +2 -0
- package/dist/molecules/Dropdown.svelte +40 -11
- package/dist/molecules/Dropdown.svelte.d.ts +4 -0
- package/dist/molecules/EmptyState.svelte +2 -1
- package/dist/molecules/Form.svelte +25 -14
- package/dist/molecules/Form.svelte.d.ts +13 -5
- package/dist/molecules/FormField.svelte +2 -1
- package/dist/molecules/Header.svelte +13 -3
- package/dist/molecules/Header.svelte.d.ts +2 -0
- package/dist/molecules/ImageUpload.svelte +54 -15
- package/dist/molecules/ImageUpload.svelte.d.ts +16 -5
- package/dist/molecules/Modal.svelte +110 -31
- package/dist/molecules/Modal.svelte.d.ts +9 -6
- package/dist/molecules/NavigationMenu.svelte +15 -3
- package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
- package/dist/molecules/NavigationMenuContent.svelte +36 -38
- package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuItem.svelte +10 -2
- package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuLink.svelte +10 -2
- package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuList.svelte +10 -2
- package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
- package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
- package/dist/molecules/Page.svelte +8 -2
- package/dist/molecules/Page.svelte.d.ts +2 -0
- package/dist/molecules/PropsTable.svelte +6 -3
- package/dist/molecules/PropsTable.svelte.d.ts +2 -0
- package/dist/molecules/RadioGroup.svelte +5 -1
- package/dist/molecules/RadioGroup.svelte.d.ts +2 -0
- package/dist/molecules/Section.svelte +12 -4
- package/dist/molecules/Section.svelte.d.ts +3 -1
- package/dist/molecules/SidebarBrandHeader.svelte +13 -4
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +2 -0
- package/dist/molecules/SidebarFooter.svelte +12 -4
- package/dist/molecules/SidebarFooter.svelte.d.ts +2 -0
- package/dist/molecules/SidebarNavSection.svelte +12 -4
- package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
- package/dist/molecules/SlideUp.svelte +85 -21
- package/dist/molecules/SlideUp.svelte.d.ts +5 -4
- package/dist/molecules/Tabs.svelte +25 -19
- package/dist/molecules/Tabs.svelte.d.ts +8 -4
- package/dist/molecules/Toaster.svelte +3 -2
- package/dist/molecules/ToasterToast.svelte +41 -10
- package/dist/molecules/ToasterToast.svelte.d.ts +2 -0
- package/dist/organisms/SidebarAccountPanel.svelte +4 -0
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarNavigation.svelte +102 -59
- package/dist/organisms/SidebarNavigation.svelte.d.ts +2 -0
- package/dist/organisms/SidebarPanel.svelte +19 -11
- package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarShell.svelte +112 -0
- package/dist/organisms/SidebarShell.svelte.d.ts +52 -0
- package/dist/organisms/TopNavbar.svelte +88 -17
- package/dist/organisms/TopNavbar.svelte.d.ts +2 -0
- package/dist/organisms/index.d.ts +1 -0
- package/dist/organisms/index.js +1 -0
- package/dist/routes/lib/variant-utils.d.ts +26 -0
- package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
- package/dist/types/events.js +1 -0
- package/dist/types/index.js +3 -0
- package/dist/util/cn.d.ts +7 -0
- package/dist/util/cn.js +48 -0
- package/dist/util/ssr-safe.d.ts +4 -4
- package/dist/zabi-components-colors.css +2 -2
- package/dist/zabi-components-theme-dark-only.css +139 -92
- package/dist/zabi-components-theme-dark.css +139 -92
- package/dist/zabi-components-theme-only.css +268 -122
- package/dist/zabi-components-theme.css +268 -122
- package/dist/zabi-components.css +831 -883
- package/docs/lucide-icons.md +3 -1
- package/package.json +19 -24
- package/dist/lib/layout-width-tokens.d.ts +0 -17
- package/dist/lib/layout-width-tokens.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/List.d.ts +0 -3
- package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/index.d.ts +0 -4
- package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
- package/dist/lib/showcase/components-catalog.d.ts +0 -3
- package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
- package/dist/routes/lib/focus-utils.d.ts +0 -12
- package/dist/routes/lib/focus-utils.ts +0 -110
- package/dist/routes/lib/ssr-safe.ts +0 -58
- package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import Badge from "../atoms/Badge.svelte";
|
|
4
4
|
import { Search, X } from "@lucide/svelte";
|
|
5
5
|
import type { Component } from "svelte";
|
|
6
|
+
import { cn } from "../util/cn.js";
|
|
6
7
|
|
|
7
8
|
export interface SidebarPanelItem {
|
|
8
9
|
id: string;
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
17
|
+
class?: string;
|
|
18
|
+
/** @deprecated use `class`. */
|
|
16
19
|
className?: string;
|
|
17
20
|
widthClass?: string;
|
|
18
21
|
variant?: "plain" | "elevated";
|
|
@@ -35,7 +38,8 @@
|
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
let {
|
|
38
|
-
|
|
41
|
+
class: classAttr = "",
|
|
42
|
+
className: legacyClass = "",
|
|
39
43
|
widthClass = "w-80",
|
|
40
44
|
variant = "elevated",
|
|
41
45
|
ariaLabel = "Picker panel",
|
|
@@ -57,6 +61,10 @@
|
|
|
57
61
|
...restProps
|
|
58
62
|
}: Props = $props();
|
|
59
63
|
|
|
64
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
65
|
+
* Both are merged here so existing call sites keep working. */
|
|
66
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
67
|
+
|
|
60
68
|
const normalizedSearchTerm = $derived(searchValue.trim().toLowerCase());
|
|
61
69
|
const filteredItems = $derived(
|
|
62
70
|
normalizedSearchTerm
|
|
@@ -71,19 +79,19 @@
|
|
|
71
79
|
const containerClasses = $derived.by(() => {
|
|
72
80
|
const resolvedWidthClass = widthClass.trim() || "w-80";
|
|
73
81
|
const shell = isElevated
|
|
74
|
-
? "rounded-
|
|
75
|
-
: "rounded-
|
|
76
|
-
return `${resolvedWidthClass} shrink-0 p-5 ${shell} ${className}
|
|
82
|
+
? "rounded-container border border-border bg-card text-headline shadow-sm ring-1 ring-border/50"
|
|
83
|
+
: "rounded-container border border-border bg-card text-headline shadow-sm";
|
|
84
|
+
return cn(`${resolvedWidthClass} shrink-0 p-5 ${shell} ${className}`);
|
|
77
85
|
});
|
|
78
86
|
|
|
79
87
|
function getItemClasses(itemId: string): string {
|
|
80
88
|
const isActive = selectedItemId === itemId;
|
|
81
89
|
const baseClasses =
|
|
82
|
-
"focus-ring focus-ring--nav w-full cursor-pointer rounded-
|
|
90
|
+
"focus-ring focus-ring--nav w-full cursor-pointer rounded-control px-3 py-2 text-left transition-colors duration-150";
|
|
83
91
|
if (isActive) {
|
|
84
92
|
return `${baseClasses} bg-nav-menu-active text-inherit shadow-sm ring-1 ring-border/80`;
|
|
85
93
|
}
|
|
86
|
-
return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-
|
|
94
|
+
return `${baseClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
function handleSelect(item: SidebarPanelItem): void {
|
|
@@ -116,7 +124,7 @@
|
|
|
116
124
|
</div>
|
|
117
125
|
<button
|
|
118
126
|
type="button"
|
|
119
|
-
class="-mr-0.5 -mt-0.5 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-
|
|
127
|
+
class="-mr-0.5 -mt-0.5 inline-flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-control text-nav-menu-item transition-colors hover:bg-nav-menu-hover hover:text-nav-menu-item-hover focus-ring focus-ring--nav"
|
|
120
128
|
aria-label={closeLabel}
|
|
121
129
|
onclick={handleClose}
|
|
122
130
|
>
|
|
@@ -137,7 +145,7 @@
|
|
|
137
145
|
bind:value={searchValue}
|
|
138
146
|
placeholder={searchPlaceholder}
|
|
139
147
|
aria-label={searchPlaceholder}
|
|
140
|
-
class="w-full min-w-0 min-h-10 rounded-
|
|
148
|
+
class="w-full min-w-0 min-h-10 rounded-container 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"
|
|
141
149
|
/>
|
|
142
150
|
</div>
|
|
143
151
|
{/if}
|
|
@@ -154,7 +162,7 @@
|
|
|
154
162
|
aria-selected={selectedItemId === item.id}
|
|
155
163
|
>
|
|
156
164
|
<span class="flex items-center justify-between gap-3">
|
|
157
|
-
<span class="flex min-w-0 items-start gap-
|
|
165
|
+
<span class="flex min-w-0 items-start gap-3">
|
|
158
166
|
{#if item.icon}
|
|
159
167
|
{@const Icon = item.icon}
|
|
160
168
|
<span
|
|
@@ -189,14 +197,14 @@
|
|
|
189
197
|
</ul>
|
|
190
198
|
{:else}
|
|
191
199
|
<div
|
|
192
|
-
class="rounded-
|
|
200
|
+
class="rounded-container border border-dashed border-border bg-transparent px-4 py-4 ring-1 ring-border/40"
|
|
193
201
|
>
|
|
194
202
|
<h4 class="text-sm font-semibold text-headline">{emptyStateTitle}</h4>
|
|
195
203
|
<p class="mt-1 text-sm leading-relaxed text-description">{emptyStateDescription}</p>
|
|
196
204
|
{#if emptyStateActionLabel.trim() && onEmptyStateAction}
|
|
197
205
|
<button
|
|
198
206
|
type="button"
|
|
199
|
-
class="mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-
|
|
207
|
+
class="mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-control 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
208
|
onclick={handleEmptyStateAction}
|
|
201
209
|
>
|
|
202
210
|
{emptyStateActionLabel.trim()}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The chrome of a sidebar and nothing else: width, surface, collapse
|
|
8
|
+
* behaviour, the inset its regions share, and a scroll container for the
|
|
9
|
+
* middle.
|
|
10
|
+
*
|
|
11
|
+
* `SidebarNavigation` is a fully-specified sidebar driven by ~44 props —
|
|
12
|
+
* excellent when your sidebar looks like that one, useless when it does
|
|
13
|
+
* not, because the parts are not reachable. This is the same shell with
|
|
14
|
+
* the regions left open, so you can drop in `SidebarBrandHeader`,
|
|
15
|
+
* `SidebarNavSection` and `SidebarFooter` (or your own markup) and skip
|
|
16
|
+
* the props you would never set.
|
|
17
|
+
*
|
|
18
|
+
* `SidebarNavigation` is built on it, so the two cannot drift apart.
|
|
19
|
+
*
|
|
20
|
+
* ```svelte
|
|
21
|
+
* <SidebarShell mode="collapsed">
|
|
22
|
+
* {#snippet header({ collapsed })}
|
|
23
|
+
* <SidebarBrandHeader {collapsed} brandName="Zabi" />
|
|
24
|
+
* {/snippet}
|
|
25
|
+
* <SidebarNavSection title="Main" sectionKey="main">…</SidebarNavSection>
|
|
26
|
+
* {#snippet footer({ collapsed, insetX })}
|
|
27
|
+
* <SidebarFooter {collapsed} className={insetX} />
|
|
28
|
+
* {/snippet}
|
|
29
|
+
* </SidebarShell>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** Passed to every region so it can match the shell's collapse and inset. */
|
|
34
|
+
export interface SidebarRegionContext {
|
|
35
|
+
collapsed: boolean;
|
|
36
|
+
/** Horizontal padding utility the shell's own regions use. */
|
|
37
|
+
insetX: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
|
|
41
|
+
mode?: "expanded" | "collapsed";
|
|
42
|
+
/** `rail` attaches to the viewport edge; `card` is a floating panel. */
|
|
43
|
+
layout?: "rail" | "card";
|
|
44
|
+
ariaLabel?: string;
|
|
45
|
+
class?: string;
|
|
46
|
+
/** @deprecated use `class`. */
|
|
47
|
+
className?: string;
|
|
48
|
+
/** Brand row, search — anything above the scrolling nav area. */
|
|
49
|
+
header?: Snippet<[SidebarRegionContext]>;
|
|
50
|
+
/** The scrolling middle. */
|
|
51
|
+
children?: Snippet<[SidebarRegionContext]>;
|
|
52
|
+
/** Account row, logout, theme toggle. Outside the scroll container. */
|
|
53
|
+
footer?: Snippet<[SidebarRegionContext]>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
mode = "expanded",
|
|
58
|
+
layout = "rail",
|
|
59
|
+
ariaLabel = "Sidebar navigation",
|
|
60
|
+
class: classAttr = "",
|
|
61
|
+
className: legacyClass = "",
|
|
62
|
+
header,
|
|
63
|
+
children,
|
|
64
|
+
footer,
|
|
65
|
+
...restProps
|
|
66
|
+
}: Props = $props();
|
|
67
|
+
|
|
68
|
+
const className = $derived(cn(classAttr, legacyClass));
|
|
69
|
+
const isCollapsed = $derived(mode === "collapsed");
|
|
70
|
+
const isCard = $derived(layout === "card");
|
|
71
|
+
|
|
72
|
+
const insetX = $derived(isCollapsed ? "px-2" : "px-4");
|
|
73
|
+
|
|
74
|
+
const containerClasses = $derived.by(() => {
|
|
75
|
+
const widthClass = isCollapsed ? "w-[72px]" : "w-[266px]";
|
|
76
|
+
const railSurface = "border-r border-border bg-background text-headline";
|
|
77
|
+
const cardSurface =
|
|
78
|
+
"border-r border-border bg-background text-headline shadow-sm";
|
|
79
|
+
const surfaceClasses = isCard ? cardSurface : railSurface;
|
|
80
|
+
const verticalPad = isCard ? "py-4" : "py-5";
|
|
81
|
+
const baseClasses = `flex h-full min-h-0 max-h-full flex-col overflow-visible ${verticalPad}`;
|
|
82
|
+
|
|
83
|
+
return cn(`${baseClasses} ${widthClass} ${surfaceClasses} ${className}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const region = $derived({ collapsed: isCollapsed, insetX });
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
|
|
90
|
+
<div class="flex min-h-0 w-full min-w-0 flex-1 flex-col">
|
|
91
|
+
{#if header}
|
|
92
|
+
<div class={cn("flex w-full shrink-0 flex-col gap-5 pb-3", insetX)}>
|
|
93
|
+
{@render header(region)}
|
|
94
|
+
</div>
|
|
95
|
+
{/if}
|
|
96
|
+
|
|
97
|
+
<div
|
|
98
|
+
class={cn(
|
|
99
|
+
"flex min-h-0 min-w-0 flex-1 flex-col gap-3 overflow-y-auto overflow-x-hidden overscroll-y-contain pt-4 pb-2",
|
|
100
|
+
insetX,
|
|
101
|
+
)}
|
|
102
|
+
role="region"
|
|
103
|
+
aria-label="Navigation links"
|
|
104
|
+
>
|
|
105
|
+
{@render children?.(region)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{#if footer}
|
|
110
|
+
{@render footer(region)}
|
|
111
|
+
{/if}
|
|
112
|
+
</nav>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
/**
|
|
4
|
+
* The chrome of a sidebar and nothing else: width, surface, collapse
|
|
5
|
+
* behaviour, the inset its regions share, and a scroll container for the
|
|
6
|
+
* middle.
|
|
7
|
+
*
|
|
8
|
+
* `SidebarNavigation` is a fully-specified sidebar driven by ~44 props —
|
|
9
|
+
* excellent when your sidebar looks like that one, useless when it does
|
|
10
|
+
* not, because the parts are not reachable. This is the same shell with
|
|
11
|
+
* the regions left open, so you can drop in `SidebarBrandHeader`,
|
|
12
|
+
* `SidebarNavSection` and `SidebarFooter` (or your own markup) and skip
|
|
13
|
+
* the props you would never set.
|
|
14
|
+
*
|
|
15
|
+
* `SidebarNavigation` is built on it, so the two cannot drift apart.
|
|
16
|
+
*
|
|
17
|
+
* ```svelte
|
|
18
|
+
* <SidebarShell mode="collapsed">
|
|
19
|
+
* {#snippet header({ collapsed })}
|
|
20
|
+
* <SidebarBrandHeader {collapsed} brandName="Zabi" />
|
|
21
|
+
* {/snippet}
|
|
22
|
+
* <SidebarNavSection title="Main" sectionKey="main">…</SidebarNavSection>
|
|
23
|
+
* {#snippet footer({ collapsed, insetX })}
|
|
24
|
+
* <SidebarFooter {collapsed} className={insetX} />
|
|
25
|
+
* {/snippet}
|
|
26
|
+
* </SidebarShell>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
/** Passed to every region so it can match the shell's collapse and inset. */
|
|
30
|
+
export interface SidebarRegionContext {
|
|
31
|
+
collapsed: boolean;
|
|
32
|
+
/** Horizontal padding utility the shell's own regions use. */
|
|
33
|
+
insetX: string;
|
|
34
|
+
}
|
|
35
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
|
|
36
|
+
mode?: "expanded" | "collapsed";
|
|
37
|
+
/** `rail` attaches to the viewport edge; `card` is a floating panel. */
|
|
38
|
+
layout?: "rail" | "card";
|
|
39
|
+
ariaLabel?: string;
|
|
40
|
+
class?: string;
|
|
41
|
+
/** @deprecated use `class`. */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** Brand row, search — anything above the scrolling nav area. */
|
|
44
|
+
header?: Snippet<[SidebarRegionContext]>;
|
|
45
|
+
/** The scrolling middle. */
|
|
46
|
+
children?: Snippet<[SidebarRegionContext]>;
|
|
47
|
+
/** Account row, logout, theme toggle. Outside the scroll container. */
|
|
48
|
+
footer?: Snippet<[SidebarRegionContext]>;
|
|
49
|
+
};
|
|
50
|
+
declare const SidebarShell: import("svelte").Component<Props, {}, "">;
|
|
51
|
+
type SidebarShell = ReturnType<typeof SidebarShell>;
|
|
52
|
+
export default SidebarShell;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import ThemeToggle from "../atoms/ThemeToggle.svelte";
|
|
3
|
+
import IconButton from "../atoms/IconButton.svelte";
|
|
4
|
+
import { ExternalLink, Menu, X } from "@lucide/svelte";
|
|
3
5
|
import type { Component, Snippet } from "svelte";
|
|
6
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
7
|
+
import { cn } from "../util/cn.js";
|
|
4
8
|
|
|
5
9
|
export interface TopNavbarNavItem {
|
|
6
10
|
label: string;
|
|
@@ -15,6 +19,8 @@
|
|
|
15
19
|
/** `aria-label` on `<nav>` when multiple nav landmarks exist. */
|
|
16
20
|
ariaLabel?: string;
|
|
17
21
|
showThemeToggle?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
/** @deprecated use `class`. */
|
|
18
24
|
className?: string;
|
|
19
25
|
/** Slim mode: only the `nav` region (no full chrome bar). */
|
|
20
26
|
embedded?: boolean;
|
|
@@ -30,7 +36,8 @@
|
|
|
30
36
|
brandHref,
|
|
31
37
|
ariaLabel,
|
|
32
38
|
showThemeToggle = true,
|
|
33
|
-
|
|
39
|
+
class: classAttr = "",
|
|
40
|
+
className: legacyClass = "",
|
|
34
41
|
embedded = false,
|
|
35
42
|
items = [],
|
|
36
43
|
navVariant = "header",
|
|
@@ -42,12 +49,24 @@
|
|
|
42
49
|
...restProps
|
|
43
50
|
}: Props & { nav?: Snippet; actions?: Snippet } = $props();
|
|
44
51
|
|
|
52
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
53
|
+
* Both are merged here so existing call sites keep working. */
|
|
54
|
+
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
55
|
+
|
|
56
|
+
const mobileMenuId = generateId("topnavbar-menu");
|
|
57
|
+
|
|
45
58
|
let isMenuOpen = $state(false);
|
|
46
59
|
|
|
47
60
|
function toggleMenu() {
|
|
48
61
|
isMenuOpen = !isMenuOpen;
|
|
49
62
|
}
|
|
50
63
|
|
|
64
|
+
function handleWindowKeydown(event: KeyboardEvent) {
|
|
65
|
+
if (event.key === "Escape" && isMenuOpen) {
|
|
66
|
+
isMenuOpen = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
function handleNavLinkClick(event: MouseEvent) {
|
|
52
71
|
if (preventNavigation) {
|
|
53
72
|
event.preventDefault();
|
|
@@ -57,6 +76,11 @@
|
|
|
57
76
|
}
|
|
58
77
|
}
|
|
59
78
|
|
|
79
|
+
/** Absolute URLs leave the site, so they are marked and open in a new tab. */
|
|
80
|
+
function isExternal(href: string): boolean {
|
|
81
|
+
return href.startsWith("http://") || href.startsWith("https://");
|
|
82
|
+
}
|
|
83
|
+
|
|
60
84
|
/** App routes: prefix match except `/` and absolute URLs (exact match). */
|
|
61
85
|
function isNavItemActive(href: string): boolean {
|
|
62
86
|
if (href.startsWith("http://") || href.startsWith("https://")) {
|
|
@@ -78,8 +102,30 @@
|
|
|
78
102
|
return "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";
|
|
79
103
|
}
|
|
80
104
|
|
|
81
|
-
|
|
82
|
-
|
|
105
|
+
/**
|
|
106
|
+
* The active state is resolved here rather than with
|
|
107
|
+
* `aria-[current=page]:text-nav-menu-item-active`.
|
|
108
|
+
*
|
|
109
|
+
* That variant was dead: the semantic utilities are hand-written in
|
|
110
|
+
* app.css and sit outside Tailwind's cascade layer, so an unlayered
|
|
111
|
+
* `.text-nav-menu-item` beats anything Tailwind generates for a variant of
|
|
112
|
+
* the same property. The fill happened to survive only because no plain
|
|
113
|
+
* `bg-*` class competed with it — so the selected tab painted its pill but
|
|
114
|
+
* kept the idle label colour (#27272a where #3c52ba was intended), which
|
|
115
|
+
* is exactly the "active state does almost nothing" failure the sidebar
|
|
116
|
+
* had.
|
|
117
|
+
*
|
|
118
|
+
* Plain classes chosen in JS cannot lose that way.
|
|
119
|
+
*/
|
|
120
|
+
function getIconContainerClasses(isActive: boolean): string {
|
|
121
|
+
const base =
|
|
122
|
+
"focus-ring focus-ring--nav group/nav-item flex flex-col items-center justify-center overflow-clip relative rounded-pill shrink-0 transition-colors duration-200 outline-none";
|
|
123
|
+
return isActive
|
|
124
|
+
? cn(base, "bg-nav-menu-active text-nav-menu-item-active")
|
|
125
|
+
: cn(
|
|
126
|
+
base,
|
|
127
|
+
"text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover",
|
|
128
|
+
);
|
|
83
129
|
}
|
|
84
130
|
|
|
85
131
|
function getStateLayerClasses(): string {
|
|
@@ -87,7 +133,9 @@
|
|
|
87
133
|
}
|
|
88
134
|
|
|
89
135
|
function getLabelClasses(): string {
|
|
90
|
-
|
|
136
|
+
// `whitespace-pre` used to sit beside `text-nowrap` — same property,
|
|
137
|
+
// and it preserved literal whitespace as a side effect.
|
|
138
|
+
return "font-medium leading-4 relative shrink-0 text-center text-nowrap tracking-wide text-xs text-inherit";
|
|
91
139
|
}
|
|
92
140
|
|
|
93
141
|
function getIconWrapperClasses(): string {
|
|
@@ -99,12 +147,15 @@
|
|
|
99
147
|
<ul class="{ulClasses} list-none m-0 p-0">
|
|
100
148
|
{#each items as item (item.href)}
|
|
101
149
|
{@const isActive = isNavItemActive(item.href)}
|
|
150
|
+
{@const external = isExternal(item.href)}
|
|
102
151
|
<li class={getNavItemClasses()}>
|
|
103
152
|
<a
|
|
104
153
|
href={item.href}
|
|
105
|
-
class={getIconContainerClasses()}
|
|
154
|
+
class={getIconContainerClasses(isActive)}
|
|
106
155
|
onclick={handleNavLinkClick}
|
|
107
156
|
aria-current={isActive ? "page" : undefined}
|
|
157
|
+
target={external ? "_blank" : undefined}
|
|
158
|
+
rel={external ? "noopener noreferrer" : undefined}
|
|
108
159
|
>
|
|
109
160
|
<div class={getStateLayerClasses()}>
|
|
110
161
|
{#if item.iconFilled && isActive}
|
|
@@ -118,9 +169,17 @@
|
|
|
118
169
|
<Icon size={16} class="w-4 h-4" />
|
|
119
170
|
</div>
|
|
120
171
|
{/if}
|
|
121
|
-
<
|
|
172
|
+
<span class={getLabelClasses()}>
|
|
122
173
|
{item.label}
|
|
123
|
-
</
|
|
174
|
+
</span>
|
|
175
|
+
{#if external}
|
|
176
|
+
<ExternalLink
|
|
177
|
+
size={12}
|
|
178
|
+
class="shrink-0 text-current opacity-70"
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
/>
|
|
181
|
+
<span class="sr-only">(opens in a new tab)</span>
|
|
182
|
+
{/if}
|
|
124
183
|
</div>
|
|
125
184
|
</a>
|
|
126
185
|
</li>
|
|
@@ -128,6 +187,8 @@
|
|
|
128
187
|
</ul>
|
|
129
188
|
{/snippet}
|
|
130
189
|
|
|
190
|
+
<svelte:window onkeydown={handleWindowKeydown} />
|
|
191
|
+
|
|
131
192
|
{#if embedded}
|
|
132
193
|
<nav class={className} aria-label={ariaLabel} {...restProps}>
|
|
133
194
|
{#if nav}
|
|
@@ -138,7 +199,7 @@
|
|
|
138
199
|
</nav>
|
|
139
200
|
{:else}
|
|
140
201
|
<nav
|
|
141
|
-
class="border-b border-border bg-background sticky top-0 z-
|
|
202
|
+
class={cn("border-b border-border bg-background sticky top-0 z-sticky", className)}
|
|
142
203
|
aria-label={ariaLabel}
|
|
143
204
|
{...restProps}
|
|
144
205
|
>
|
|
@@ -148,7 +209,7 @@
|
|
|
148
209
|
{#if brandHref}
|
|
149
210
|
<a
|
|
150
211
|
href={brandHref}
|
|
151
|
-
class="focus-ring text-xl font-bold text-headline hover:
|
|
212
|
+
class="focus-ring text-xl font-bold text-headline transition-colors hover:text-link"
|
|
152
213
|
>
|
|
153
214
|
{brand}
|
|
154
215
|
</a>
|
|
@@ -158,7 +219,7 @@
|
|
|
158
219
|
</div>
|
|
159
220
|
|
|
160
221
|
<div class="hidden md:block">
|
|
161
|
-
<div class="ml-10 flex items-
|
|
222
|
+
<div class="ml-10 flex items-center space-x-4">
|
|
162
223
|
{#if nav}
|
|
163
224
|
{@render nav()}
|
|
164
225
|
{:else if items.length > 0}
|
|
@@ -177,19 +238,29 @@
|
|
|
177
238
|
</div>
|
|
178
239
|
|
|
179
240
|
<div class="md:hidden">
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
241
|
+
<!--
|
|
242
|
+
Was a bare ☰ glyph in a text-2xl span: font-dependent,
|
|
243
|
+
off the icon scale, and with no disclosure semantics, so
|
|
244
|
+
a screen reader could not tell the menu was open.
|
|
245
|
+
-->
|
|
246
|
+
<IconButton
|
|
247
|
+
variant="ghost"
|
|
248
|
+
label={isMenuOpen ? "Close menu" : "Open menu"}
|
|
183
249
|
onclick={toggleMenu}
|
|
184
|
-
aria-
|
|
250
|
+
aria-expanded={isMenuOpen}
|
|
251
|
+
aria-controls={mobileMenuId}
|
|
185
252
|
>
|
|
186
|
-
|
|
187
|
-
|
|
253
|
+
{#if isMenuOpen}
|
|
254
|
+
<X size={20} />
|
|
255
|
+
{:else}
|
|
256
|
+
<Menu size={20} />
|
|
257
|
+
{/if}
|
|
258
|
+
</IconButton>
|
|
188
259
|
</div>
|
|
189
260
|
</div>
|
|
190
261
|
|
|
191
262
|
{#if isMenuOpen}
|
|
192
|
-
<div class="md:hidden">
|
|
263
|
+
<div class="md:hidden" id={mobileMenuId}>
|
|
193
264
|
<div
|
|
194
265
|
class="px-2 pt-2 pb-3 space-y-1 sm:px-3 border-t border-border"
|
|
195
266
|
>
|
|
@@ -17,6 +17,8 @@ interface Props {
|
|
|
17
17
|
/** `aria-label` on `<nav>` when multiple nav landmarks exist. */
|
|
18
18
|
ariaLabel?: string;
|
|
19
19
|
showThemeToggle?: boolean;
|
|
20
|
+
class?: string;
|
|
21
|
+
/** @deprecated use `class`. */
|
|
20
22
|
className?: string;
|
|
21
23
|
/** Slim mode: only the `nav` region (no full chrome bar). */
|
|
22
24
|
embedded?: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as TopNavbar } from './TopNavbar.svelte';
|
|
2
2
|
export { default as SidebarNavigation } from './SidebarNavigation.svelte';
|
|
3
|
+
export { default as SidebarShell } from './SidebarShell.svelte';
|
|
3
4
|
export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
|
|
4
5
|
export { default as SidebarPanel } from './SidebarPanel.svelte';
|
package/dist/organisms/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as TopNavbar } from './TopNavbar.svelte';
|
|
2
2
|
export { default as SidebarNavigation } from './SidebarNavigation.svelte';
|
|
3
|
+
export { default as SidebarShell } from './SidebarShell.svelte';
|
|
3
4
|
export { default as SidebarAccountPanel } from './SidebarAccountPanel.svelte';
|
|
4
5
|
export { default as SidebarPanel } from './SidebarPanel.svelte';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant class helpers — semantic design tokens only (see src/app.css).
|
|
3
|
+
*/
|
|
4
|
+
import type { SemanticVariant } from '../../types/variants.js';
|
|
5
|
+
export type Variant = SemanticVariant;
|
|
6
|
+
/**
|
|
7
|
+
* Border + focus ring classes for text inputs (Input, Textarea).
|
|
8
|
+
*/
|
|
9
|
+
export declare function getInputVariantClasses(variant: Variant): string;
|
|
10
|
+
/**
|
|
11
|
+
* Card surfaces — border + background using semantic tokens.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getCardVariantClasses(variant: Variant): string;
|
|
14
|
+
/**
|
|
15
|
+
* Generic variant classes for borders, text, or backgrounds.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getVariantClasses(variant: Variant, type: 'border' | 'text' | 'bg'): string;
|
|
18
|
+
export declare function getAllVariantClasses(variant: Variant): {
|
|
19
|
+
border: string;
|
|
20
|
+
text: string;
|
|
21
|
+
bg: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Create a variant class map for use in reactive statements
|
|
25
|
+
*/
|
|
26
|
+
export declare function createVariantClassMap<T extends string>(variants: T[], prefix: string): Record<T, string>;
|
|
@@ -1,55 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Variant class helpers — semantic design tokens only (see src/app.css).
|
|
3
3
|
*/
|
|
4
|
-
|
|
5
|
-
import type { SemanticVariant } from '../../types/variants.js';
|
|
6
|
-
|
|
7
|
-
export type Variant = SemanticVariant;
|
|
8
|
-
|
|
9
4
|
/**
|
|
10
5
|
* Border + focus ring classes for text inputs (Input, Textarea).
|
|
11
6
|
*/
|
|
12
|
-
export function getInputVariantClasses(variant
|
|
13
|
-
const variantMap
|
|
14
|
-
default:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'focus-ring border-success focus-visible:border-success',
|
|
18
|
-
warning:
|
|
19
|
-
'focus-ring border-warning focus-visible:border-warning',
|
|
7
|
+
export function getInputVariantClasses(variant) {
|
|
8
|
+
const variantMap = {
|
|
9
|
+
default: 'focus-ring border-input-border focus-visible:border-input-border',
|
|
10
|
+
success: 'focus-ring border-success focus-visible:border-success',
|
|
11
|
+
warning: 'focus-ring border-warning focus-visible:border-warning',
|
|
20
12
|
error: 'focus-ring border-error focus-visible:border-error',
|
|
21
13
|
info: 'focus-ring border-info focus-visible:border-info',
|
|
22
14
|
};
|
|
23
|
-
|
|
24
15
|
return variantMap[variant] || variantMap.default;
|
|
25
16
|
}
|
|
26
|
-
|
|
27
17
|
/**
|
|
28
18
|
* Card surfaces — border + background using semantic tokens.
|
|
29
19
|
*/
|
|
30
|
-
export function getCardVariantClasses(variant
|
|
31
|
-
const variantMap
|
|
20
|
+
export function getCardVariantClasses(variant) {
|
|
21
|
+
const variantMap = {
|
|
32
22
|
default: 'border-border bg-card',
|
|
33
23
|
success: 'border-success bg-card',
|
|
34
24
|
warning: 'border-warning bg-card',
|
|
35
25
|
error: 'border-error bg-card',
|
|
36
26
|
info: 'border-info bg-card',
|
|
37
27
|
};
|
|
38
|
-
|
|
39
28
|
return variantMap[variant] || variantMap.default;
|
|
40
29
|
}
|
|
41
|
-
|
|
42
30
|
/**
|
|
43
31
|
* Generic variant classes for borders, text, or backgrounds.
|
|
44
32
|
*/
|
|
45
|
-
export function getVariantClasses(
|
|
46
|
-
|
|
47
|
-
type: 'border' | 'text' | 'bg',
|
|
48
|
-
): string {
|
|
49
|
-
const variantMap: Record<
|
|
50
|
-
Variant,
|
|
51
|
-
Record<'border' | 'text' | 'bg', string>
|
|
52
|
-
> = {
|
|
33
|
+
export function getVariantClasses(variant, type) {
|
|
34
|
+
const variantMap = {
|
|
53
35
|
default: {
|
|
54
36
|
border: 'border-border',
|
|
55
37
|
text: 'text-body',
|
|
@@ -76,34 +58,21 @@ export function getVariantClasses(
|
|
|
76
58
|
bg: 'bg-card',
|
|
77
59
|
},
|
|
78
60
|
};
|
|
79
|
-
|
|
80
61
|
return variantMap[variant]?.[type] || variantMap.default[type];
|
|
81
62
|
}
|
|
82
|
-
|
|
83
|
-
export function getAllVariantClasses(variant: Variant): {
|
|
84
|
-
border: string;
|
|
85
|
-
text: string;
|
|
86
|
-
bg: string;
|
|
87
|
-
} {
|
|
63
|
+
export function getAllVariantClasses(variant) {
|
|
88
64
|
return {
|
|
89
65
|
border: getVariantClasses(variant, 'border'),
|
|
90
66
|
text: getVariantClasses(variant, 'text'),
|
|
91
67
|
bg: getVariantClasses(variant, 'bg'),
|
|
92
68
|
};
|
|
93
69
|
}
|
|
94
|
-
|
|
95
70
|
/**
|
|
96
71
|
* Create a variant class map for use in reactive statements
|
|
97
72
|
*/
|
|
98
|
-
export function createVariantClassMap
|
|
99
|
-
variants
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
(acc, variant) => {
|
|
104
|
-
acc[variant] = `${prefix}-${variant}`;
|
|
105
|
-
return acc;
|
|
106
|
-
},
|
|
107
|
-
{} as Record<T, string>,
|
|
108
|
-
);
|
|
73
|
+
export function createVariantClassMap(variants, prefix) {
|
|
74
|
+
return variants.reduce((acc, variant) => {
|
|
75
|
+
acc[variant] = `${prefix}-${variant}`;
|
|
76
|
+
return acc;
|
|
77
|
+
}, {});
|
|
109
78
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|