zabi-components 7.0.1 → 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 +89 -57
- 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 +3 -437
- package/dist/zabi-components-theme-dark-only.css +223 -103
- package/dist/zabi-components-theme-dark.css +223 -103
- package/dist/zabi-components-theme-only.css +347 -107
- package/dist/zabi-components-theme.css +347 -107
- package/dist/zabi-components.css +953 -843
- package/docs/lucide-icons.md +3 -1
- package/package.json +20 -25
- 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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import SidebarBrandHeader from "../molecules/SidebarBrandHeader.svelte";
|
|
7
7
|
import SidebarFooter from "../molecules/SidebarFooter.svelte";
|
|
8
8
|
import SidebarNavSection from "../molecules/SidebarNavSection.svelte";
|
|
9
|
+
import Tooltip from "../atoms/Tooltip.svelte";
|
|
10
|
+
import SidebarShell from "./SidebarShell.svelte";
|
|
9
11
|
import { Command, Search } from "@lucide/svelte";
|
|
10
12
|
import type { Snippet } from "svelte";
|
|
11
13
|
import type { Component } from "svelte";
|
|
@@ -34,6 +36,8 @@
|
|
|
34
36
|
items?: SidebarNavigationItem[];
|
|
35
37
|
currentPath?: string;
|
|
36
38
|
ariaLabel?: string;
|
|
39
|
+
class?: string;
|
|
40
|
+
/** @deprecated use `class`. */
|
|
37
41
|
className?: string;
|
|
38
42
|
logoSrc?: string;
|
|
39
43
|
logoAlt?: string;
|
|
@@ -78,7 +82,8 @@
|
|
|
78
82
|
currentPath = "",
|
|
79
83
|
activePrimaryHref = "",
|
|
80
84
|
ariaLabel = "Sidebar navigation",
|
|
81
|
-
|
|
85
|
+
class: classAttr = "",
|
|
86
|
+
className: legacyClass = "",
|
|
82
87
|
logoSrc = "",
|
|
83
88
|
logoAlt = "",
|
|
84
89
|
brandName = "",
|
|
@@ -113,8 +118,10 @@
|
|
|
113
118
|
...restProps
|
|
114
119
|
}: Props = $props();
|
|
115
120
|
|
|
121
|
+
/** `class` is the public prop; `className` is a deprecated alias.
|
|
122
|
+
* Both are merged here so existing call sites keep working. */
|
|
123
|
+
|
|
116
124
|
const isCollapsed = $derived(mode === "collapsed");
|
|
117
|
-
const isCard = $derived(layout === "card");
|
|
118
125
|
const showBrandRow = $derived(
|
|
119
126
|
Boolean(logoSrc.trim() || brandName.trim()),
|
|
120
127
|
);
|
|
@@ -178,24 +185,6 @@
|
|
|
178
185
|
partitionBySection(filteredPrimaryItems),
|
|
179
186
|
);
|
|
180
187
|
|
|
181
|
-
const insetX = $derived(isCollapsed ? "px-2.5" : "px-4");
|
|
182
|
-
|
|
183
|
-
const containerClasses = $derived.by(() => {
|
|
184
|
-
const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
|
|
185
|
-
const railSurface = "border-r border-border bg-background text-headline";
|
|
186
|
-
const cardSurface =
|
|
187
|
-
"border-r border-border bg-background text-headline shadow-sm";
|
|
188
|
-
const surfaceClasses = isCard ? cardSurface : railSurface;
|
|
189
|
-
const verticalPad = isCard ? "py-4" : "py-5";
|
|
190
|
-
const baseClasses = `flex h-full min-h-0 max-h-full flex-col overflow-visible ${verticalPad}`;
|
|
191
|
-
|
|
192
|
-
return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const headerStackClasses = $derived(
|
|
196
|
-
`flex w-full shrink-0 flex-col gap-5 ${insetX}`,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
188
|
const iconContainerClasses = $derived(
|
|
200
189
|
"flex size-6 shrink-0 items-center justify-center leading-none text-current",
|
|
201
190
|
);
|
|
@@ -210,21 +199,43 @@
|
|
|
210
199
|
return isMuted ? "text-description" : "text-headline";
|
|
211
200
|
}
|
|
212
201
|
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
/**
|
|
203
|
+
* The selected row used to be carried by its fill alone, and the fill was
|
|
204
|
+
* 1.05:1 against the sidebar in light mode — not a fill, a rumour. Active
|
|
205
|
+
* and idle labels were #18181b and #27272a at the same weight, a 15-unit
|
|
206
|
+
* difference in one channel that nobody can see. So in light mode there
|
|
207
|
+
* was effectively no indication of where you were.
|
|
208
|
+
*
|
|
209
|
+
* Three cues now, only one of which is colour-discrimination dependent:
|
|
210
|
+
* the label takes --color-nav-menu-item-active (defined all along and
|
|
211
|
+
* never referenced — the row said `text-inherit`), a 3px bar marks the
|
|
212
|
+
* leading edge, and the tint stays as reinforcement rather than as the
|
|
213
|
+
* whole message.
|
|
214
|
+
*/
|
|
215
|
+
function isItemActive(item: SidebarNavigationItem): boolean {
|
|
216
|
+
return (
|
|
215
217
|
currentPath === item.href ||
|
|
216
|
-
(Boolean(activePrimaryHref) && item.href === activePrimaryHref)
|
|
218
|
+
(Boolean(activePrimaryHref) && item.href === activePrimaryHref)
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getNavItemClasses(item: SidebarNavigationItem): string {
|
|
223
|
+
const isActive = isItemActive(item);
|
|
217
224
|
const layoutClasses = isCollapsed
|
|
218
225
|
? "flex min-h-10 items-center justify-center px-0 py-2"
|
|
219
|
-
: "flex min-h-10 items-center gap-
|
|
226
|
+
: "flex min-h-10 items-center gap-3 px-2 py-2";
|
|
220
227
|
const structural =
|
|
221
|
-
"focus-ring focus-ring--nav w-full cursor-pointer rounded-
|
|
228
|
+
"focus-ring focus-ring--nav relative w-full cursor-pointer rounded-control no-underline transition-colors duration-150 outline-none";
|
|
222
229
|
|
|
223
230
|
if (isActive) {
|
|
224
|
-
return
|
|
231
|
+
return (
|
|
232
|
+
`${structural} ${layoutClasses} bg-nav-menu-active ` +
|
|
233
|
+
"text-nav-menu-item-active hover:bg-nav-menu-active " +
|
|
234
|
+
"active:bg-nav-menu-active-hover"
|
|
235
|
+
);
|
|
225
236
|
}
|
|
226
237
|
|
|
227
|
-
return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-
|
|
238
|
+
return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-surface-active`;
|
|
228
239
|
}
|
|
229
240
|
|
|
230
241
|
function handleNavigate(item: SidebarNavigationItem, event: MouseEvent) {
|
|
@@ -268,11 +279,15 @@
|
|
|
268
279
|
}
|
|
269
280
|
</script>
|
|
270
281
|
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
<SidebarShell
|
|
283
|
+
{mode}
|
|
284
|
+
{layout}
|
|
285
|
+
{ariaLabel}
|
|
286
|
+
class={classAttr}
|
|
287
|
+
className={legacyClass}
|
|
288
|
+
{...restProps}
|
|
289
|
+
>
|
|
290
|
+
{#snippet header()}
|
|
276
291
|
{#if showBrandRow}
|
|
277
292
|
<SidebarBrandHeader
|
|
278
293
|
collapsed={isCollapsed}
|
|
@@ -307,10 +322,10 @@
|
|
|
307
322
|
<Button
|
|
308
323
|
variant={searchTriggerVariant}
|
|
309
324
|
size={searchTriggerSize}
|
|
310
|
-
|
|
325
|
+
fullWidth
|
|
311
326
|
onclick={handleSearchClick}
|
|
312
327
|
>
|
|
313
|
-
<span class="flex w-full items-center justify-start gap-
|
|
328
|
+
<span class="flex w-full items-center justify-start gap-3">
|
|
314
329
|
<span class={iconContainerClasses} aria-hidden="true">
|
|
315
330
|
<TriggerIcon size={17} />
|
|
316
331
|
</span>
|
|
@@ -334,18 +349,13 @@
|
|
|
334
349
|
bind:value={searchValue}
|
|
335
350
|
placeholder={searchPlaceholder}
|
|
336
351
|
aria-label={searchPlaceholder}
|
|
337
|
-
class="focus-ring focus-ring--nav w-full min-w-0 min-h-10 rounded-
|
|
352
|
+
class="focus-ring focus-ring--nav 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"
|
|
338
353
|
/>
|
|
339
354
|
</div>
|
|
340
355
|
{/if}
|
|
341
356
|
{/if}
|
|
342
|
-
|
|
357
|
+
{/snippet}
|
|
343
358
|
|
|
344
|
-
<div
|
|
345
|
-
class={`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-1.5 ${insetX}`}
|
|
346
|
-
role="region"
|
|
347
|
-
aria-label="Navigation links"
|
|
348
|
-
>
|
|
349
359
|
{#if hasFilteredItems}
|
|
350
360
|
<div
|
|
351
361
|
class="flex w-full min-w-0 flex-col divide-y divide-border"
|
|
@@ -363,6 +373,19 @@
|
|
|
363
373
|
{#each group.items as item (item.id)}
|
|
364
374
|
{@const Icon = item.icon}
|
|
365
375
|
<li>
|
|
376
|
+
<!--
|
|
377
|
+
Collapsed rows are icon-only. aria-label
|
|
378
|
+
covers screen readers, but a sighted user
|
|
379
|
+
got an unlabelled glyph with no hover
|
|
380
|
+
affordance at all.
|
|
381
|
+
-->
|
|
382
|
+
<Tooltip
|
|
383
|
+
content={item.label}
|
|
384
|
+
placement="right"
|
|
385
|
+
disabled={!isCollapsed}
|
|
386
|
+
block
|
|
387
|
+
fixed
|
|
388
|
+
>
|
|
366
389
|
<a
|
|
367
390
|
href={item.href}
|
|
368
391
|
class={getNavItemClasses(item)}
|
|
@@ -370,6 +393,12 @@
|
|
|
370
393
|
aria-current={getAriaCurrent(item)}
|
|
371
394
|
aria-label={isCollapsed ? item.label : undefined}
|
|
372
395
|
>
|
|
396
|
+
{#if isItemActive(item)}
|
|
397
|
+
<span
|
|
398
|
+
class="absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-pill bg-nav-menu-item-active"
|
|
399
|
+
aria-hidden="true"
|
|
400
|
+
></span>
|
|
401
|
+
{/if}
|
|
373
402
|
{#if Icon}
|
|
374
403
|
<span
|
|
375
404
|
class={iconContainerClasses}
|
|
@@ -384,16 +413,31 @@
|
|
|
384
413
|
>{item.label}</span
|
|
385
414
|
>
|
|
386
415
|
{/if}
|
|
387
|
-
{#if
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
416
|
+
{#if getItemBadgeText(item) !== null}
|
|
417
|
+
{#if isCollapsed}
|
|
418
|
+
<!--
|
|
419
|
+
A notification count that
|
|
420
|
+
disappears when you collapse
|
|
421
|
+
the rail is a real loss; the
|
|
422
|
+
count becomes a dot rather
|
|
423
|
+
than nothing.
|
|
424
|
+
-->
|
|
425
|
+
<span
|
|
426
|
+
class="absolute right-2 top-2 size-2 rounded-pill bg-error"
|
|
427
|
+
aria-hidden="true"
|
|
428
|
+
></span>
|
|
429
|
+
{:else}
|
|
430
|
+
<span class="ml-auto">
|
|
431
|
+
<Badge
|
|
432
|
+
variant="default"
|
|
433
|
+
size="sm"
|
|
434
|
+
text={getItemBadgeText(item) ?? ""}
|
|
435
|
+
/>
|
|
436
|
+
</span>
|
|
437
|
+
{/if}
|
|
395
438
|
{/if}
|
|
396
439
|
</a>
|
|
440
|
+
</Tooltip>
|
|
397
441
|
</li>
|
|
398
442
|
{/each}
|
|
399
443
|
</SidebarNavSection>
|
|
@@ -446,7 +490,7 @@
|
|
|
446
490
|
{/if}
|
|
447
491
|
{:else}
|
|
448
492
|
<div
|
|
449
|
-
class="rounded-
|
|
493
|
+
class="rounded-container border border-border border-dashed bg-transparent px-4 py-4 ring-1 ring-border/60"
|
|
450
494
|
>
|
|
451
495
|
<h3 class="text-sm font-semibold {getTextToneClass()}">
|
|
452
496
|
{normalizedSearchTerm && searchMode === "input"
|
|
@@ -461,7 +505,7 @@
|
|
|
461
505
|
{#if !(normalizedSearchTerm && searchMode === "input")}
|
|
462
506
|
<button
|
|
463
507
|
type="button"
|
|
464
|
-
class="focus-ring focus-ring--nav mt-3 inline-flex min-h-10 cursor-pointer items-center rounded-
|
|
508
|
+
class="focus-ring focus-ring--nav 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 outline-none transition-colors hover:bg-action-primary-hover"
|
|
465
509
|
onclick={handleEmptyStateAction}
|
|
466
510
|
>
|
|
467
511
|
{emptyStateActionLabel}
|
|
@@ -469,11 +513,9 @@
|
|
|
469
513
|
{/if}
|
|
470
514
|
</div>
|
|
471
515
|
{/if}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
<SidebarFooter
|
|
476
|
-
collapsed={isCollapsed}
|
|
516
|
+
{#snippet footer({ insetX })}
|
|
517
|
+
<SidebarFooter
|
|
518
|
+
collapsed={isCollapsed}
|
|
477
519
|
{showProfile}
|
|
478
520
|
{profileName}
|
|
479
521
|
{profileEmail}
|
|
@@ -489,6 +531,7 @@
|
|
|
489
531
|
{profilePanelOpen}
|
|
490
532
|
{profilePanelControlsId}
|
|
491
533
|
{profilePanel}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
534
|
+
className={insetX}
|
|
535
|
+
/>
|
|
536
|
+
{/snippet}
|
|
537
|
+
</SidebarShell>
|
|
@@ -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;
|