zabi-components 5.0.22 → 6.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 +41 -44
- package/THEME.md +473 -0
- package/THEMING.md +248 -0
- package/dist/atoms/Card.svelte +40 -67
- package/dist/atoms/Card.svelte.d.ts +3 -3
- package/dist/atoms/CardHeader.svelte +10 -1
- package/dist/atoms/CardHeader.svelte.d.ts +3 -0
- package/dist/atoms/CodeBlock.svelte +1 -1
- package/dist/atoms/ColorPicker.svelte +1 -1
- package/dist/atoms/Input.svelte +2 -2
- package/dist/atoms/Progress.svelte +1 -1
- package/dist/atoms/Select.svelte +7 -5
- package/dist/atoms/Textarea.svelte +2 -2
- package/dist/atoms/Toast.svelte +1 -1
- package/dist/components/index.d.ts +1 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +1 -2
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/lib/showcase/components-catalog.d.ts +3 -0
- package/dist/lib/showcase/components-catalog.d.ts.map +1 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts +63 -0
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +1 -0
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +15 -0
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +1 -0
- package/dist/molecules/Alert.svelte +1 -1
- package/dist/molecules/ComponentDemo.svelte +53 -50
- package/dist/molecules/ContactForm.svelte +69 -64
- package/dist/molecules/Dropdown.svelte +2 -1
- package/dist/molecules/ImageUpload.svelte +1 -1
- package/dist/molecules/Modal.svelte +2 -2
- package/dist/molecules/NavigationMenuContent.svelte +1 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuTrigger.svelte +1 -4
- package/dist/molecules/Section.svelte +12 -12
- package/dist/molecules/SlideUp.svelte +2 -2
- package/dist/molecules/Tabs.svelte +3 -3
- package/dist/organisms/Navbar.svelte +170 -53
- package/dist/organisms/Navbar.svelte.d.ts +31 -4
- package/dist/organisms/SidebarNavigation.svelte +209 -165
- package/dist/organisms/SidebarNavigation.svelte.d.ts +5 -0
- package/dist/organisms/{SidebarProjectPanel.svelte → SidebarPanel.svelte} +18 -17
- package/dist/organisms/{SidebarProjectPanel.svelte.d.ts → SidebarPanel.svelte.d.ts} +6 -6
- package/dist/organisms/index.d.ts +1 -2
- package/dist/organisms/index.js +1 -2
- package/dist/types/page.types.d.ts +32 -0
- package/dist/types/page.types.d.ts.map +1 -0
- package/dist/types/variants.ts +76 -0
- package/dist/zabi-components-colors.css +347 -324
- package/dist/zabi-components-theme-dark-only.css +159 -151
- package/dist/zabi-components-theme-dark.css +159 -151
- package/dist/zabi-components-theme-only.css +245 -230
- package/dist/zabi-components-theme.css +245 -230
- package/dist/zabi-components.css +437 -101
- package/docs/theme-imports.md +112 -0
- package/package.json +16 -6
- package/dist/organisms/Navigation.svelte +0 -119
- package/dist/organisms/Navigation.svelte.d.ts +0 -21
|
@@ -41,12 +41,18 @@
|
|
|
41
41
|
onLogout?: () => void;
|
|
42
42
|
onThemeToggle?: (nextIsLightMode: boolean) => void;
|
|
43
43
|
onEmptyStateAction?: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Highlights a primary (e.g. category) row when the current route is a child
|
|
46
|
+
* of that section, while `currentPath` points at the leaf (e.g. a component).
|
|
47
|
+
*/
|
|
48
|
+
activePrimaryHref?: string;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
let {
|
|
47
52
|
mode = "expanded",
|
|
48
53
|
items = [],
|
|
49
54
|
currentPath = "",
|
|
55
|
+
activePrimaryHref = "",
|
|
50
56
|
ariaLabel = "Sidebar navigation",
|
|
51
57
|
className = "",
|
|
52
58
|
showProfile = true,
|
|
@@ -81,17 +87,31 @@
|
|
|
81
87
|
const secondaryItems = $derived(
|
|
82
88
|
items.filter((item) => item.group === "secondary"),
|
|
83
89
|
);
|
|
90
|
+
function itemMatchesSearch(item: SidebarNavigationItem): boolean {
|
|
91
|
+
return item.label.toLowerCase().includes(normalizedSearchTerm);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Keep the current (and section) row visible even when it does not match the filter. */
|
|
95
|
+
function itemPinnedBySelection(item: SidebarNavigationItem): boolean {
|
|
96
|
+
if (currentPath === item.href) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return Boolean(activePrimaryHref) && item.href === activePrimaryHref;
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
const filteredPrimaryItems = $derived(
|
|
85
103
|
searchMode === "input" && normalizedSearchTerm
|
|
86
|
-
? primaryItems.filter(
|
|
87
|
-
item
|
|
104
|
+
? primaryItems.filter(
|
|
105
|
+
(item) =>
|
|
106
|
+
itemMatchesSearch(item) || itemPinnedBySelection(item),
|
|
88
107
|
)
|
|
89
108
|
: primaryItems,
|
|
90
109
|
);
|
|
91
110
|
const filteredSecondaryItems = $derived(
|
|
92
111
|
searchMode === "input" && normalizedSearchTerm
|
|
93
|
-
? secondaryItems.filter(
|
|
94
|
-
item
|
|
112
|
+
? secondaryItems.filter(
|
|
113
|
+
(item) =>
|
|
114
|
+
itemMatchesSearch(item) || itemPinnedBySelection(item),
|
|
95
115
|
)
|
|
96
116
|
: secondaryItems,
|
|
97
117
|
);
|
|
@@ -103,7 +123,7 @@
|
|
|
103
123
|
const widthClass = isCollapsed ? "w-[104px]" : "w-[266px]";
|
|
104
124
|
const surfaceClasses = "bg-background border-border text-headline";
|
|
105
125
|
const baseClasses =
|
|
106
|
-
"h-full min-h-
|
|
126
|
+
"flex h-full min-h-0 max-h-full flex-col overflow-hidden border-r py-6";
|
|
107
127
|
|
|
108
128
|
return `${baseClasses} ${widthClass} ${surfaceClasses} ${className}`.trim();
|
|
109
129
|
});
|
|
@@ -112,16 +132,20 @@
|
|
|
112
132
|
"size-14 rounded-2xl bg-action-primary text-action-primary flex items-center justify-center font-semibold text-base shrink-0",
|
|
113
133
|
);
|
|
114
134
|
|
|
135
|
+
/** Search trigger (collapsed icon strip or button mode): matches nav item interaction tokens. */
|
|
136
|
+
const searchControlStates =
|
|
137
|
+
"text-nav-menu-item outline-none transition-colors duration-150 hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
|
|
138
|
+
|
|
115
139
|
const searchButtonClasses = $derived(
|
|
116
|
-
|
|
140
|
+
`flex min-h-11 w-full cursor-pointer items-center justify-center rounded-2xl border border-input-border bg-input px-0 py-2.5 ${searchControlStates}`,
|
|
117
141
|
);
|
|
118
142
|
const searchTriggerClasses = $derived.by(() => {
|
|
119
143
|
return isCollapsed
|
|
120
144
|
? searchButtonClasses
|
|
121
|
-
:
|
|
145
|
+
: `flex min-h-11 w-full cursor-pointer items-center gap-3 rounded-2xl border border-input-border bg-input px-4 py-2.5 text-left ${searchControlStates}`;
|
|
122
146
|
});
|
|
123
147
|
const iconContainerClasses = $derived(
|
|
124
|
-
"flex size-6 shrink-0 items-center justify-center leading-none",
|
|
148
|
+
"flex size-6 shrink-0 items-center justify-center leading-none text-current",
|
|
125
149
|
);
|
|
126
150
|
const shouldRenderSearchButton = $derived(
|
|
127
151
|
isCollapsed || searchMode === "button" || Boolean(onSearchClick),
|
|
@@ -132,23 +156,26 @@
|
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
function getNavItemClasses(item: SidebarNavigationItem): string {
|
|
135
|
-
const isActive =
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
const isActive =
|
|
160
|
+
currentPath === item.href ||
|
|
161
|
+
(Boolean(activePrimaryHref) && item.href === activePrimaryHref);
|
|
138
162
|
const layoutClasses = isCollapsed
|
|
139
163
|
? "flex min-h-11 items-center justify-center px-0 py-2.5"
|
|
140
164
|
: "flex min-h-11 items-center gap-3 px-4 py-2.5";
|
|
165
|
+
const structural =
|
|
166
|
+
"w-full cursor-pointer rounded-lg no-underline transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
|
|
141
167
|
|
|
142
168
|
if (isActive) {
|
|
143
|
-
|
|
169
|
+
/* Current page: keep active surface on hover (do not apply inactive hover bg). */
|
|
170
|
+
return `${structural} ${layoutClasses} bg-nav-menu-active text-inherit hover:bg-nav-menu-active hover:text-inherit active:opacity-90`;
|
|
144
171
|
}
|
|
145
172
|
|
|
146
|
-
return `${
|
|
173
|
+
return `${structural} ${layoutClasses} text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200`;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
function getControlButtonClasses(): string {
|
|
150
177
|
const baseClasses =
|
|
151
|
-
"w-full rounded-lg text-
|
|
178
|
+
"w-full cursor-pointer rounded-lg text-nav-menu-item transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-base-200 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus";
|
|
152
179
|
const layoutClasses = isCollapsed
|
|
153
180
|
? "flex min-h-11 items-center justify-center px-0 py-2.5"
|
|
154
181
|
: "flex min-h-11 items-center gap-3 px-4 py-2.5";
|
|
@@ -202,10 +229,22 @@
|
|
|
202
229
|
}
|
|
203
230
|
return null;
|
|
204
231
|
}
|
|
232
|
+
|
|
233
|
+
function getAriaCurrent(
|
|
234
|
+
item: SidebarNavigationItem,
|
|
235
|
+
): "page" | "location" | undefined {
|
|
236
|
+
if (currentPath === item.href) {
|
|
237
|
+
return "page";
|
|
238
|
+
}
|
|
239
|
+
if (activePrimaryHref && item.href === activePrimaryHref) {
|
|
240
|
+
return "location";
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
205
244
|
</script>
|
|
206
245
|
|
|
207
246
|
<nav class={containerClasses} aria-label={ariaLabel} {...restProps}>
|
|
208
|
-
<div class="flex w-full flex-col gap-
|
|
247
|
+
<div class="flex w-full shrink-0 flex-col gap-6 px-2">
|
|
209
248
|
{#if showProfile}
|
|
210
249
|
<div class="flex w-full items-center gap-3">
|
|
211
250
|
<div class={avatarClasses} aria-hidden="true">
|
|
@@ -226,53 +265,101 @@
|
|
|
226
265
|
</div>
|
|
227
266
|
{/if}
|
|
228
267
|
|
|
229
|
-
|
|
230
|
-
{#if
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
268
|
+
{#if showSearch}
|
|
269
|
+
{#if shouldRenderSearchButton}
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
class={searchTriggerClasses}
|
|
273
|
+
onclick={handleSearchClick}
|
|
274
|
+
aria-label={searchPlaceholder}
|
|
275
|
+
>
|
|
276
|
+
<span
|
|
277
|
+
class="truncate text-base text-inherit transition-colors"
|
|
237
278
|
>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
</div>
|
|
258
|
-
{/if}
|
|
279
|
+
{searchValue || searchPlaceholder}
|
|
280
|
+
</span>
|
|
281
|
+
</button>
|
|
282
|
+
{:else}
|
|
283
|
+
<div class="relative w-full">
|
|
284
|
+
<span
|
|
285
|
+
class="pointer-events-none absolute left-4 top-1/2 z-10 -translate-y-1/2"
|
|
286
|
+
aria-hidden="true"
|
|
287
|
+
>
|
|
288
|
+
<Search size={20} class={getTextToneClass(true)} />
|
|
289
|
+
</span>
|
|
290
|
+
<Input
|
|
291
|
+
type="search"
|
|
292
|
+
bind:value={searchValue}
|
|
293
|
+
placeholder={searchPlaceholder}
|
|
294
|
+
aria-label={searchPlaceholder}
|
|
295
|
+
class="w-full min-w-0 rounded-2xl pl-12"
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
259
298
|
{/if}
|
|
299
|
+
{/if}
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<div
|
|
303
|
+
class="flex min-h-0 flex-1 w-full flex-col gap-6 overflow-y-auto overflow-x-hidden overscroll-y-contain py-1"
|
|
304
|
+
role="region"
|
|
305
|
+
aria-label="Navigation links"
|
|
306
|
+
>
|
|
307
|
+
{#if hasFilteredItems}
|
|
308
|
+
<ul
|
|
309
|
+
class="flex w-full flex-col gap-2 px-2"
|
|
310
|
+
aria-label="Primary navigation"
|
|
311
|
+
>
|
|
312
|
+
{#each filteredPrimaryItems as item (item.id)}
|
|
313
|
+
{@const Icon = item.icon}
|
|
314
|
+
<li>
|
|
315
|
+
<a
|
|
316
|
+
href={item.href}
|
|
317
|
+
class={getNavItemClasses(item)}
|
|
318
|
+
onclick={(event) => handleNavigate(item, event)}
|
|
319
|
+
aria-current={getAriaCurrent(item)}
|
|
320
|
+
aria-label={isCollapsed ? item.label : undefined}
|
|
321
|
+
>
|
|
322
|
+
{#if Icon}
|
|
323
|
+
<span
|
|
324
|
+
class={iconContainerClasses}
|
|
325
|
+
aria-hidden="true"
|
|
326
|
+
>
|
|
327
|
+
<Icon size={20} />
|
|
328
|
+
</span>
|
|
329
|
+
{/if}
|
|
330
|
+
{#if !isCollapsed}
|
|
331
|
+
<span
|
|
332
|
+
class="text-base leading-snug text-inherit"
|
|
333
|
+
>{item.label}</span
|
|
334
|
+
>
|
|
335
|
+
{/if}
|
|
336
|
+
{#if !isCollapsed && getItemBadgeText(item) !== null}
|
|
337
|
+
<span class="ml-auto">
|
|
338
|
+
<Badge
|
|
339
|
+
variant="default"
|
|
340
|
+
size="sm"
|
|
341
|
+
text={getItemBadgeText(item) ?? ""}
|
|
342
|
+
/>
|
|
343
|
+
</span>
|
|
344
|
+
{/if}
|
|
345
|
+
</a>
|
|
346
|
+
</li>
|
|
347
|
+
{/each}
|
|
348
|
+
</ul>
|
|
260
349
|
|
|
261
|
-
{#if
|
|
350
|
+
{#if filteredSecondaryItems.length > 0}
|
|
262
351
|
<ul
|
|
263
|
-
class="flex w-full flex-col gap-2"
|
|
264
|
-
aria-label="
|
|
352
|
+
class="flex w-full flex-col gap-3 px-2"
|
|
353
|
+
aria-label="Secondary navigation"
|
|
265
354
|
>
|
|
266
|
-
{#each
|
|
355
|
+
{#each filteredSecondaryItems as item (item.id)}
|
|
267
356
|
{@const Icon = item.icon}
|
|
268
357
|
<li>
|
|
269
358
|
<a
|
|
270
359
|
href={item.href}
|
|
271
360
|
class={getNavItemClasses(item)}
|
|
272
361
|
onclick={(event) => handleNavigate(item, event)}
|
|
273
|
-
aria-current={
|
|
274
|
-
? "page"
|
|
275
|
-
: undefined}
|
|
362
|
+
aria-current={getAriaCurrent(item)}
|
|
276
363
|
aria-label={isCollapsed
|
|
277
364
|
? item.label
|
|
278
365
|
: undefined}
|
|
@@ -286,135 +373,92 @@
|
|
|
286
373
|
</span>
|
|
287
374
|
{/if}
|
|
288
375
|
{#if !isCollapsed}
|
|
289
|
-
<span
|
|
376
|
+
<span
|
|
377
|
+
class="text-base leading-snug text-inherit"
|
|
290
378
|
>{item.label}</span
|
|
291
379
|
>
|
|
292
380
|
{/if}
|
|
293
|
-
{#if !isCollapsed && getItemBadgeText(item) !== null}
|
|
294
|
-
<span class="ml-auto">
|
|
295
|
-
<Badge
|
|
296
|
-
variant="default"
|
|
297
|
-
size="sm"
|
|
298
|
-
text={getItemBadgeText(item) ?? ""}
|
|
299
|
-
/>
|
|
300
|
-
</span>
|
|
301
|
-
{/if}
|
|
302
381
|
</a>
|
|
303
382
|
</li>
|
|
304
383
|
{/each}
|
|
305
384
|
</ul>
|
|
306
|
-
|
|
307
|
-
{#if filteredSecondaryItems.length > 0}
|
|
308
|
-
<ul
|
|
309
|
-
class="flex w-full flex-col gap-3"
|
|
310
|
-
aria-label="Secondary navigation"
|
|
311
|
-
>
|
|
312
|
-
{#each filteredSecondaryItems as item (item.id)}
|
|
313
|
-
{@const Icon = item.icon}
|
|
314
|
-
<li>
|
|
315
|
-
<a
|
|
316
|
-
href={item.href}
|
|
317
|
-
class={getNavItemClasses(item)}
|
|
318
|
-
onclick={(event) =>
|
|
319
|
-
handleNavigate(item, event)}
|
|
320
|
-
aria-current={currentPath === item.href
|
|
321
|
-
? "page"
|
|
322
|
-
: undefined}
|
|
323
|
-
aria-label={isCollapsed
|
|
324
|
-
? item.label
|
|
325
|
-
: undefined}
|
|
326
|
-
>
|
|
327
|
-
{#if Icon}
|
|
328
|
-
<span
|
|
329
|
-
class={iconContainerClasses}
|
|
330
|
-
aria-hidden="true"
|
|
331
|
-
>
|
|
332
|
-
<Icon size={20} />
|
|
333
|
-
</span>
|
|
334
|
-
{/if}
|
|
335
|
-
{#if !isCollapsed}
|
|
336
|
-
<span class="text-base leading-snug"
|
|
337
|
-
>{item.label}</span
|
|
338
|
-
>
|
|
339
|
-
{/if}
|
|
340
|
-
</a>
|
|
341
|
-
</li>
|
|
342
|
-
{/each}
|
|
343
|
-
</ul>
|
|
344
|
-
{/if}
|
|
345
|
-
{:else}
|
|
346
|
-
<div
|
|
347
|
-
class="rounded-2xl border border-border border-dashed bg-base-50 px-4 py-5"
|
|
348
|
-
>
|
|
349
|
-
<h3 class="text-base font-semibold {getTextToneClass()}">
|
|
350
|
-
{normalizedSearchTerm
|
|
351
|
-
&& searchMode === "input"
|
|
352
|
-
? "No matching navigation items"
|
|
353
|
-
: emptyStateTitle}
|
|
354
|
-
</h3>
|
|
355
|
-
<p class="mt-1 text-sm {getTextToneClass(true)}">
|
|
356
|
-
{normalizedSearchTerm
|
|
357
|
-
&& searchMode === "input"
|
|
358
|
-
? `No results found for "${searchValue}". Try another keyword.`
|
|
359
|
-
: emptyStateDescription}
|
|
360
|
-
</p>
|
|
361
|
-
{#if !(normalizedSearchTerm && searchMode === "input")}
|
|
362
|
-
<button
|
|
363
|
-
type="button"
|
|
364
|
-
class="mt-4 inline-flex min-h-11 items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary hover:bg-action-primary-hover focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0"
|
|
365
|
-
onclick={handleEmptyStateAction}
|
|
366
|
-
>
|
|
367
|
-
{emptyStateActionLabel}
|
|
368
|
-
</button>
|
|
369
|
-
{/if}
|
|
370
|
-
</div>
|
|
371
385
|
{/if}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<div class="flex w-full flex-col gap-2">
|
|
376
|
-
{#if showLogout}
|
|
377
|
-
<button
|
|
378
|
-
type="button"
|
|
379
|
-
class={getControlButtonClasses()}
|
|
380
|
-
onclick={handleLogout}
|
|
386
|
+
{:else}
|
|
387
|
+
<div
|
|
388
|
+
class="rounded-2xl border border-border border-dashed bg-base-50 px-4 py-5"
|
|
381
389
|
>
|
|
382
|
-
<
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
390
|
+
<h3 class="text-base font-semibold {getTextToneClass()}">
|
|
391
|
+
{normalizedSearchTerm && searchMode === "input"
|
|
392
|
+
? "No matching navigation items"
|
|
393
|
+
: emptyStateTitle}
|
|
394
|
+
</h3>
|
|
395
|
+
<p class="mt-1 text-sm {getTextToneClass(true)}">
|
|
396
|
+
{normalizedSearchTerm && searchMode === "input"
|
|
397
|
+
? `No results found for "${searchValue}". Try another keyword.`
|
|
398
|
+
: emptyStateDescription}
|
|
399
|
+
</p>
|
|
400
|
+
{#if !(normalizedSearchTerm && searchMode === "input")}
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
class="mt-4 inline-flex min-h-11 cursor-pointer items-center rounded-lg bg-action-primary px-3 py-2 text-sm text-action-primary outline-none transition-colors hover:bg-action-primary-hover focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus"
|
|
404
|
+
onclick={handleEmptyStateAction}
|
|
405
|
+
>
|
|
406
|
+
{emptyStateActionLabel}
|
|
407
|
+
</button>
|
|
387
408
|
{/if}
|
|
388
|
-
</
|
|
409
|
+
</div>
|
|
389
410
|
{/if}
|
|
411
|
+
</div>
|
|
390
412
|
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
{#if showLogout || showThemeToggle}
|
|
414
|
+
<div
|
|
415
|
+
class="flex w-full shrink-0 flex-col gap-2 border-t border-border pt-4 px-2"
|
|
416
|
+
>
|
|
417
|
+
{#if showLogout}
|
|
393
418
|
<button
|
|
394
419
|
type="button"
|
|
395
|
-
class=
|
|
396
|
-
|
|
397
|
-
: 'bg-base-200'}"
|
|
398
|
-
onclick={handleThemeToggle}
|
|
399
|
-
role="switch"
|
|
400
|
-
aria-checked={isLightMode}
|
|
401
|
-
aria-label={isLightMode
|
|
402
|
-
? "Switch to dark mode"
|
|
403
|
-
: "Switch to light mode"}
|
|
420
|
+
class={getControlButtonClasses()}
|
|
421
|
+
onclick={handleLogout}
|
|
404
422
|
>
|
|
405
|
-
<span
|
|
406
|
-
|
|
407
|
-
? 'translate-x-6 bg-base-900 text-action-primary'
|
|
408
|
-
: 'translate-x-0 bg-base-900 text-base-50'}"
|
|
409
|
-
>
|
|
410
|
-
{#if isLightMode}
|
|
411
|
-
<Sun size={14} />
|
|
412
|
-
{:else}
|
|
413
|
-
<Moon size={14} />
|
|
414
|
-
{/if}
|
|
423
|
+
<span class={iconContainerClasses} aria-hidden="true">
|
|
424
|
+
<LogOut size={20} />
|
|
415
425
|
</span>
|
|
426
|
+
{#if !isCollapsed}
|
|
427
|
+
<span class="text-base text-inherit">{logoutLabel}</span
|
|
428
|
+
>
|
|
429
|
+
{/if}
|
|
416
430
|
</button>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
431
|
+
{/if}
|
|
432
|
+
|
|
433
|
+
{#if showThemeToggle}
|
|
434
|
+
<div class={themeToggleContainerClasses}>
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
class="inline-flex h-8 w-14 cursor-pointer items-center rounded-2xl p-1 outline-none transition-colors duration-150 hover:opacity-95 active:opacity-90 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-nav-menu-focus {isLightMode
|
|
438
|
+
? 'bg-action-primary'
|
|
439
|
+
: 'bg-base-200'}"
|
|
440
|
+
onclick={handleThemeToggle}
|
|
441
|
+
role="switch"
|
|
442
|
+
aria-checked={isLightMode}
|
|
443
|
+
title={lightModeLabel}
|
|
444
|
+
aria-label={isLightMode
|
|
445
|
+
? "Switch to dark mode"
|
|
446
|
+
: "Switch to light mode"}
|
|
447
|
+
>
|
|
448
|
+
<span
|
|
449
|
+
class="flex h-6 w-6 items-center justify-center rounded-xl leading-none shadow-sm transition-transform duration-150 active:scale-95 {isLightMode
|
|
450
|
+
? 'translate-x-6 bg-base-900 text-action-primary'
|
|
451
|
+
: 'translate-x-0 bg-base-900 text-base-50'}"
|
|
452
|
+
>
|
|
453
|
+
{#if isLightMode}
|
|
454
|
+
<Sun size={14} />
|
|
455
|
+
{:else}
|
|
456
|
+
<Moon size={14} />
|
|
457
|
+
{/if}
|
|
458
|
+
</span>
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
{/if}
|
|
462
|
+
</div>
|
|
463
|
+
{/if}
|
|
420
464
|
</nav>
|
|
@@ -38,6 +38,11 @@ interface Props {
|
|
|
38
38
|
onLogout?: () => void;
|
|
39
39
|
onThemeToggle?: (nextIsLightMode: boolean) => void;
|
|
40
40
|
onEmptyStateAction?: () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Highlights a primary (e.g. category) row when the current route is a child
|
|
43
|
+
* of that section, while `currentPath` points at the leaf (e.g. a component).
|
|
44
|
+
*/
|
|
45
|
+
activePrimaryHref?: string;
|
|
41
46
|
}
|
|
42
47
|
declare const SidebarNavigation: Component<Props, {}, "searchValue" | "isLightMode">;
|
|
43
48
|
type SidebarNavigation = ReturnType<typeof SidebarNavigation>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { Search, X } from "@lucide/svelte";
|
|
5
5
|
import type { Component } from "svelte";
|
|
6
6
|
|
|
7
|
-
export interface
|
|
7
|
+
export interface SidebarPanelItem {
|
|
8
8
|
id: string;
|
|
9
9
|
label: string;
|
|
10
10
|
icon?: Component<{ size?: number; class?: string }>;
|
|
@@ -21,30 +21,30 @@
|
|
|
21
21
|
showSearch?: boolean;
|
|
22
22
|
searchPlaceholder?: string;
|
|
23
23
|
searchValue?: string;
|
|
24
|
-
items?:
|
|
24
|
+
items?: SidebarPanelItem[];
|
|
25
25
|
selectedItemId?: string;
|
|
26
26
|
emptyStateTitle?: string;
|
|
27
27
|
emptyStateDescription?: string;
|
|
28
28
|
selectLabel?: string;
|
|
29
29
|
closeLabel?: string;
|
|
30
|
-
onSelect?: (item:
|
|
30
|
+
onSelect?: (item: SidebarPanelItem) => void;
|
|
31
31
|
onClose?: () => void;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
let {
|
|
35
35
|
className = "",
|
|
36
36
|
widthClass = "w-80",
|
|
37
|
-
ariaLabel = "
|
|
38
|
-
title = "
|
|
39
|
-
subtitle = "Choose
|
|
37
|
+
ariaLabel = "Picker panel",
|
|
38
|
+
title = "Items",
|
|
39
|
+
subtitle = "Choose an item to continue",
|
|
40
40
|
showSearch = true,
|
|
41
|
-
searchPlaceholder = "Search
|
|
41
|
+
searchPlaceholder = "Search...",
|
|
42
42
|
searchValue = $bindable(""),
|
|
43
43
|
items = [],
|
|
44
44
|
selectedItemId = $bindable(""),
|
|
45
|
-
emptyStateTitle = "No
|
|
46
|
-
emptyStateDescription = "Try a different keyword
|
|
47
|
-
selectLabel = "Select
|
|
45
|
+
emptyStateTitle = "No matches",
|
|
46
|
+
emptyStateDescription = "Try a different keyword.",
|
|
47
|
+
selectLabel = "Select item",
|
|
48
48
|
closeLabel = "Close panel",
|
|
49
49
|
onSelect,
|
|
50
50
|
onClose,
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
function getItemClasses(itemId: string): string {
|
|
69
69
|
const isActive = selectedItemId === itemId;
|
|
70
70
|
const baseClasses =
|
|
71
|
-
"w-full rounded-xl px-3 py-3 text-left transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0";
|
|
71
|
+
"w-full cursor-pointer rounded-xl px-3 py-3 text-left transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0";
|
|
72
72
|
if (isActive) {
|
|
73
73
|
return `${baseClasses} bg-action-primary-subtle text-headline`;
|
|
74
74
|
}
|
|
75
75
|
return `${baseClasses} hover:bg-base-50 text-description`;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function handleSelect(item:
|
|
78
|
+
function handleSelect(item: SidebarPanelItem): void {
|
|
79
79
|
selectedItemId = item.id;
|
|
80
80
|
if (onSelect) {
|
|
81
81
|
onSelect(item);
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
</div>
|
|
98
98
|
<button
|
|
99
99
|
type="button"
|
|
100
|
-
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md text-description hover:bg-base-50 hover:text-headline focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0"
|
|
100
|
+
class="inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-description hover:bg-base-50 hover:text-headline focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-0"
|
|
101
101
|
aria-label={closeLabel}
|
|
102
102
|
onclick={handleClose}
|
|
103
103
|
>
|
|
@@ -124,14 +124,15 @@
|
|
|
124
124
|
{/if}
|
|
125
125
|
|
|
126
126
|
{#if filteredItems.length > 0}
|
|
127
|
-
<ul class="space-y-2" aria-label={selectLabel}>
|
|
127
|
+
<ul class="space-y-2" role="listbox" aria-label={selectLabel}>
|
|
128
128
|
{#each filteredItems as item (item.id)}
|
|
129
|
-
<li>
|
|
129
|
+
<li role="presentation">
|
|
130
130
|
<button
|
|
131
131
|
type="button"
|
|
132
|
+
role="option"
|
|
132
133
|
class={getItemClasses(item.id)}
|
|
133
134
|
onclick={() => handleSelect(item)}
|
|
134
|
-
aria-
|
|
135
|
+
aria-selected={selectedItemId === item.id}
|
|
135
136
|
>
|
|
136
137
|
<span class="flex items-center justify-between gap-3">
|
|
137
138
|
<span class="flex min-w-0 items-start gap-2">
|
|
@@ -171,4 +172,4 @@
|
|
|
171
172
|
<p class="mt-1 text-sm text-description">{emptyStateDescription}</p>
|
|
172
173
|
</div>
|
|
173
174
|
{/if}
|
|
174
|
-
</aside>
|
|
175
|
+
</aside>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Component } from "svelte";
|
|
2
|
-
export interface
|
|
2
|
+
export interface SidebarPanelItem {
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
5
|
icon?: Component<{
|
|
@@ -18,15 +18,15 @@ interface Props {
|
|
|
18
18
|
showSearch?: boolean;
|
|
19
19
|
searchPlaceholder?: string;
|
|
20
20
|
searchValue?: string;
|
|
21
|
-
items?:
|
|
21
|
+
items?: SidebarPanelItem[];
|
|
22
22
|
selectedItemId?: string;
|
|
23
23
|
emptyStateTitle?: string;
|
|
24
24
|
emptyStateDescription?: string;
|
|
25
25
|
selectLabel?: string;
|
|
26
26
|
closeLabel?: string;
|
|
27
|
-
onSelect?: (item:
|
|
27
|
+
onSelect?: (item: SidebarPanelItem) => void;
|
|
28
28
|
onClose?: () => void;
|
|
29
29
|
}
|
|
30
|
-
declare const
|
|
31
|
-
type
|
|
32
|
-
export default
|
|
30
|
+
declare const SidebarPanel: Component<Props, {}, "searchValue" | "selectedItemId">;
|
|
31
|
+
type SidebarPanel = ReturnType<typeof SidebarPanel>;
|
|
32
|
+
export default SidebarPanel;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { default as Navbar } from './Navbar.svelte';
|
|
2
|
-
export { default as Navigation } from './Navigation.svelte';
|
|
3
2
|
export { default as SidebarNavigation } from './SidebarNavigation.svelte';
|
|
4
|
-
export { default as
|
|
3
|
+
export { default as SidebarPanel } from './SidebarPanel.svelte';
|
package/dist/organisms/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { default as Navbar } from './Navbar.svelte';
|
|
2
|
-
export { default as Navigation } from './Navigation.svelte';
|
|
3
2
|
export { default as SidebarNavigation } from './SidebarNavigation.svelte';
|
|
4
|
-
export { default as
|
|
3
|
+
export { default as SidebarPanel } from './SidebarPanel.svelte';
|