@placeholderco/placeholder-ui 1.0.3 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +26 -26
  2. package/README.md +179 -179
  3. package/dist/display/Alert.svelte +179 -179
  4. package/dist/display/Avatar.svelte +166 -166
  5. package/dist/display/LinkCollection.svelte +161 -161
  6. package/dist/display/Paper.svelte +118 -118
  7. package/dist/form/Autocomplete.svelte +223 -191
  8. package/dist/form/Autocomplete.svelte.d.ts +3 -1
  9. package/dist/form/AutocompleteMulti.svelte +356 -0
  10. package/dist/form/AutocompleteMulti.svelte.d.ts +28 -0
  11. package/dist/form/Checkbox.svelte +201 -201
  12. package/dist/form/Chips.svelte +128 -128
  13. package/dist/form/ComboBox.svelte +158 -158
  14. package/dist/form/ComboBox.svelte.d.ts +1 -1
  15. package/dist/form/ComboBoxItemBuilder.svelte +460 -460
  16. package/dist/form/ComboBoxMulti.svelte +197 -197
  17. package/dist/form/ComboBoxMulti.svelte.d.ts +1 -1
  18. package/dist/form/CronBuilder.svelte +693 -693
  19. package/dist/form/DatePicker.svelte +672 -672
  20. package/dist/form/DateTimePicker.svelte +712 -712
  21. package/dist/form/FileInput.svelte +235 -235
  22. package/dist/form/FormGroup.svelte +68 -68
  23. package/dist/form/Number.svelte +238 -238
  24. package/dist/form/PasswordInput.svelte +252 -252
  25. package/dist/form/RadioGroup.svelte +210 -210
  26. package/dist/form/Rating.svelte +235 -235
  27. package/dist/form/SegmentedControl.svelte +149 -149
  28. package/dist/form/Select.svelte +590 -590
  29. package/dist/form/Select.svelte.d.ts +1 -1
  30. package/dist/form/SelectMulti.svelte +613 -613
  31. package/dist/form/SelectMulti.svelte.d.ts +1 -1
  32. package/dist/form/Slider.svelte +358 -358
  33. package/dist/form/Switch.svelte +147 -147
  34. package/dist/form/TextArea.svelte +148 -148
  35. package/dist/form/Textbox.svelte +228 -228
  36. package/dist/form/TimePicker.svelte +267 -267
  37. package/dist/icon/Icon.svelte +52 -52
  38. package/dist/icon/alert-octagon.svg +5 -5
  39. package/dist/icon/alert-triangle.svg +5 -5
  40. package/dist/icon/archive.svg +1 -1
  41. package/dist/icon/arrow-down.svg +1 -1
  42. package/dist/icon/arrow-left.svg +1 -1
  43. package/dist/icon/arrow-right.svg +1 -1
  44. package/dist/icon/arrow-up.svg +1 -1
  45. package/dist/icon/at.svg +1 -1
  46. package/dist/icon/bell.svg +1 -1
  47. package/dist/icon/bookmark.svg +1 -1
  48. package/dist/icon/calendar.svg +1 -1
  49. package/dist/icon/camera.svg +1 -1
  50. package/dist/icon/chart-bar.svg +1 -1
  51. package/dist/icon/chart-line.svg +1 -1
  52. package/dist/icon/chart-pie.svg +1 -1
  53. package/dist/icon/checkbox.svg +1 -1
  54. package/dist/icon/checklist.svg +1 -1
  55. package/dist/icon/circle-check.svg +1 -1
  56. package/dist/icon/circle-x.svg +1 -1
  57. package/dist/icon/clock.svg +1 -1
  58. package/dist/icon/credit-card.svg +1 -1
  59. package/dist/icon/dots-vertical.svg +1 -1
  60. package/dist/icon/dots.svg +1 -1
  61. package/dist/icon/external-link.svg +1 -1
  62. package/dist/icon/eye-off.svg +1 -1
  63. package/dist/icon/eye.svg +1 -1
  64. package/dist/icon/filter.svg +1 -1
  65. package/dist/icon/fingerprint.svg +1 -1
  66. package/dist/icon/flag.svg +1 -1
  67. package/dist/icon/heart.svg +1 -1
  68. package/dist/icon/home.svg +1 -1
  69. package/dist/icon/key.svg +1 -1
  70. package/dist/icon/list-check.svg +1 -1
  71. package/dist/icon/login.svg +1 -1
  72. package/dist/icon/logout.svg +1 -1
  73. package/dist/icon/map-pin.svg +1 -1
  74. package/dist/icon/maximize.svg +1 -1
  75. package/dist/icon/microphone.svg +1 -1
  76. package/dist/icon/minimize.svg +1 -1
  77. package/dist/icon/note.svg +1 -1
  78. package/dist/icon/player-pause.svg +1 -1
  79. package/dist/icon/printer.svg +1 -1
  80. package/dist/icon/qrcode.svg +1 -1
  81. package/dist/icon/send.svg +1 -1
  82. package/dist/icon/settings.svg +1 -1
  83. package/dist/icon/share.svg +1 -1
  84. package/dist/icon/shopping-cart.svg +1 -1
  85. package/dist/icon/sort-ascending.svg +1 -1
  86. package/dist/icon/sort-descending.svg +1 -1
  87. package/dist/icon/star.svg +1 -1
  88. package/dist/icon/tag.svg +1 -1
  89. package/dist/icon/trending-down.svg +1 -1
  90. package/dist/icon/trending-up.svg +1 -1
  91. package/dist/icon/upload.svg +1 -1
  92. package/dist/icon/volume-off.svg +1 -1
  93. package/dist/icon/volume.svg +1 -1
  94. package/dist/icon/world.svg +1 -1
  95. package/dist/icon/zoom-in.svg +1 -1
  96. package/dist/icon/zoom-out.svg +1 -1
  97. package/dist/index.d.ts +1 -0
  98. package/dist/index.js +1 -0
  99. package/dist/layout/AppShell.svelte +169 -169
  100. package/dist/layout/CustomNavbar.svelte +61 -61
  101. package/dist/layout/Navbar.svelte +206 -206
  102. package/dist/layout/NavbarItemDisplay.svelte +29 -29
  103. package/dist/layout/Sidenav.svelte +712 -712
  104. package/dist/styles/components.css +199 -199
  105. package/dist/styles/dark.css +146 -146
  106. package/dist/styles/index.css +116 -116
  107. package/dist/styles/reset.css +110 -110
  108. package/dist/styles/semantic.css +86 -86
  109. package/dist/styles/tokens.css +203 -197
  110. package/dist/styles/utilities.css +523 -523
  111. package/dist/ui/Accordion.svelte +289 -289
  112. package/dist/ui/ActionIcon.svelte +76 -76
  113. package/dist/ui/Badge.svelte +329 -279
  114. package/dist/ui/Breadcrumbs.svelte +131 -131
  115. package/dist/ui/Button.svelte +432 -370
  116. package/dist/ui/ButtonVariant.d.ts +1 -1
  117. package/dist/ui/Dialog.svelte +307 -307
  118. package/dist/ui/Drawer.svelte +524 -524
  119. package/dist/ui/Dropdown.svelte +97 -97
  120. package/dist/ui/Dropzone.svelte +122 -122
  121. package/dist/ui/Link.svelte +32 -32
  122. package/dist/ui/Loader.svelte +70 -70
  123. package/dist/ui/LoadingOverlay.svelte +53 -53
  124. package/dist/ui/Pagination.svelte +135 -135
  125. package/dist/ui/Popover.svelte +225 -225
  126. package/dist/ui/Progress.svelte +191 -191
  127. package/dist/ui/RingProgress.svelte +141 -141
  128. package/dist/ui/Skeleton.svelte +85 -85
  129. package/dist/ui/Stepper.svelte +355 -355
  130. package/dist/ui/Table.svelte +345 -345
  131. package/dist/ui/Tabs.svelte +146 -146
  132. package/dist/ui/ThemeSwitcher.svelte +39 -39
  133. package/dist/ui/Timeline.svelte +225 -225
  134. package/dist/ui/Toaster.svelte +6 -6
  135. package/dist/ui/Tooltip.svelte +434 -434
  136. package/package.json +14 -14
@@ -1,712 +1,712 @@
1
- <script lang="ts" module>
2
- export interface SidenavItem {
3
- label: string;
4
- href?: string;
5
- iconSvg?: string;
6
- active?: boolean;
7
- items?: SidenavItem[];
8
- onclick?: () => void;
9
- }
10
-
11
- export interface SidenavSection {
12
- title?: string;
13
- items: SidenavItem[];
14
- }
15
- </script>
16
-
17
- <script lang="ts">
18
- import type { Snippet } from "svelte";
19
- import Icon from "../icon/Icon.svelte";
20
- import Textbox from "../form/Textbox.svelte";
21
- import ActionIcon from "../ui/ActionIcon.svelte";
22
- import Drawer from "../ui/Drawer.svelte";
23
- import { iconChevronRight, iconSearch, iconX } from "../icon/index.js";
24
- import { slide } from "svelte/transition";
25
- import { viewportState } from "../theme.svelte.js";
26
-
27
- interface Props {
28
- sections?: SidenavSection[];
29
- bottomSections?: SidenavSection[];
30
- collapsed?: boolean;
31
- width?: string;
32
- collapsedWidth?: string;
33
- class?: string;
34
- header?: Snippet;
35
- footer?: Snippet;
36
- searchable?: boolean;
37
- searchPlaceholder?: string;
38
- onItemClick?: (item: SidenavItem) => void;
39
- // Responsive props
40
- responsive?: boolean;
41
- mobileBreakpoint?: number;
42
- mobileOpen?: boolean;
43
- mobileTitle?: string;
44
- mobileDrawerPosition?: "left" | "right";
45
- }
46
-
47
- let {
48
- sections = [],
49
- bottomSections = [],
50
- collapsed = false,
51
- width = "260px",
52
- collapsedWidth = "60px",
53
- class: className = "",
54
- header,
55
- footer,
56
- searchable = false,
57
- searchPlaceholder = "Search...",
58
- onItemClick,
59
- // Responsive props
60
- responsive = false,
61
- mobileBreakpoint = 768,
62
- mobileOpen = $bindable(false),
63
- mobileTitle = "",
64
- mobileDrawerPosition = "left",
65
- }: Props = $props();
66
-
67
- let searchQuery = $state("");
68
-
69
- // Track if we're in mobile mode using viewportState
70
- const isMobile = $derived(
71
- responsive && viewportState.isBelow(mobileBreakpoint),
72
- );
73
-
74
- // Auto-close drawer when resizing to desktop
75
- $effect(() => {
76
- if (
77
- responsive &&
78
- !viewportState.isBelow(mobileBreakpoint) &&
79
- mobileOpen
80
- ) {
81
- mobileOpen = false;
82
- }
83
- });
84
-
85
- // Simple fuzzy search - checks if characters appear in order
86
- function fuzzyMatch(query: string, text: string): boolean {
87
- if (!query) return true;
88
- const lowerQuery = query.toLowerCase();
89
- const lowerText = text.toLowerCase();
90
-
91
- let queryIndex = 0;
92
- for (
93
- let i = 0;
94
- i < lowerText.length && queryIndex < lowerQuery.length;
95
- i++
96
- ) {
97
- if (lowerText[i] === lowerQuery[queryIndex]) {
98
- queryIndex++;
99
- }
100
- }
101
- return queryIndex === lowerQuery.length;
102
- }
103
-
104
- // Filter sections based on search query
105
- function filterSections(sects: SidenavSection[]): SidenavSection[] {
106
- if (!searchQuery) return sects;
107
-
108
- return sects
109
- .map((section) => {
110
- const filteredItems = section.items
111
- .filter((item) => {
112
- // Check if item matches
113
- if (fuzzyMatch(searchQuery, item.label)) return true;
114
- // Check if any sub-item matches
115
- if (
116
- item.items?.some((sub) =>
117
- fuzzyMatch(searchQuery, sub.label),
118
- )
119
- )
120
- return true;
121
- return false;
122
- })
123
- .map((item) => {
124
- // If item has sub-items, filter those too
125
- if (item.items) {
126
- return {
127
- ...item,
128
- items: item.items.filter((sub) =>
129
- fuzzyMatch(searchQuery, sub.label),
130
- ),
131
- };
132
- }
133
- return item;
134
- });
135
-
136
- return { ...section, items: filteredItems };
137
- })
138
- .filter((section) => section.items.length > 0);
139
- }
140
-
141
- let filteredSections = $derived(filterSections(sections));
142
- let filteredBottomSections = $derived(filterSections(bottomSections));
143
-
144
- function clearSearch() {
145
- searchQuery = "";
146
- }
147
-
148
- let expandedSections: Set<string> = $state(new Set());
149
-
150
- function toggleSection(itemLabel: string) {
151
- if (expandedSections.has(itemLabel)) {
152
- expandedSections.delete(itemLabel);
153
- expandedSections = new Set(expandedSections);
154
- } else {
155
- expandedSections.add(itemLabel);
156
- expandedSections = new Set(expandedSections);
157
- }
158
- }
159
-
160
- function isExpanded(itemLabel: string): boolean {
161
- return expandedSections.has(itemLabel);
162
- }
163
-
164
- function handleItemClick(item: SidenavItem, e: MouseEvent) {
165
- if (item.items && item.items.length > 0) {
166
- e.preventDefault();
167
- toggleSection(item.label);
168
- } else {
169
- onItemClick?.(item);
170
- item.onclick?.();
171
- // Close drawer on mobile after navigation
172
- if (isMobile) {
173
- mobileOpen = false;
174
- }
175
- }
176
- }
177
-
178
- function handleLinkClick(item: SidenavItem) {
179
- onItemClick?.(item);
180
- item.onclick?.();
181
- // Close drawer on mobile after navigation
182
- if (isMobile) {
183
- mobileOpen = false;
184
- }
185
- }
186
-
187
- function handleDrawerItemClick(item: SidenavItem) {
188
- onItemClick?.(item);
189
- item.onclick?.();
190
- // Close drawer after clicking an item
191
- mobileOpen = false;
192
- }
193
- </script>
194
-
195
- {#if !isMobile}
196
- <!-- Desktop: Regular Sidenav -->
197
- <aside
198
- class="sidenav {className}"
199
- class:collapsed
200
- style="--sidenav-width: {width}; --sidenav-collapsed-width: {collapsedWidth};"
201
- >
202
- {#if header}
203
- <div class="sidenav-header">
204
- {@render header()}
205
- </div>
206
- {/if}
207
-
208
- {#if searchable && !collapsed}
209
- <div class="sidenav-search">
210
- <Textbox
211
- placeholder={searchPlaceholder}
212
- leftIconSvg={iconSearch}
213
- bind:value={searchQuery}
214
- containerClass="search-textbox"
215
- >
216
- {#snippet right()}
217
- {#if searchQuery}
218
- <ActionIcon
219
- svg={iconX}
220
- size="0.75rem"
221
- variant="secondary-subtle"
222
- onclick={clearSearch}
223
- class="search-clear-btn"
224
- />
225
- {/if}
226
- {/snippet}
227
- </Textbox>
228
- </div>
229
- {/if}
230
-
231
- <nav class="sidenav-content">
232
- {#each filteredSections as section}
233
- {#if section.title && !collapsed}
234
- <div class="section-title">{section.title}</div>
235
- {/if}
236
- <ul class="nav-list">
237
- {#each section.items as item}
238
- <li class="nav-item">
239
- {#if item.href && !(item.items && item.items.length > 0)}
240
- <a
241
- href={item.href}
242
- class="nav-link"
243
- class:active={item.active}
244
- title={collapsed ? item.label : undefined}
245
- onclick={() => handleLinkClick(item)}
246
- >
247
- {#if item.iconSvg}
248
- <Icon
249
- svg={item.iconSvg}
250
- size="1.25em"
251
- />
252
- {/if}
253
- {#if !collapsed}
254
- <span class="nav-label"
255
- >{item.label}</span
256
- >
257
- {/if}
258
- </a>
259
- {:else}
260
- <button
261
- type="button"
262
- class="nav-link"
263
- class:active={item.active}
264
- class:has-children={item.items &&
265
- item.items.length > 0}
266
- title={collapsed ? item.label : undefined}
267
- onclick={(e) => handleItemClick(item, e)}
268
- >
269
- {#if item.iconSvg}
270
- <Icon
271
- svg={item.iconSvg}
272
- size="1.25em"
273
- />
274
- {/if}
275
- {#if !collapsed}
276
- <span class="nav-label"
277
- >{item.label}</span
278
- >
279
- {#if item.items && item.items.length > 0}
280
- <Icon
281
- svg={iconChevronRight}
282
- size="0.75em"
283
- class="chevron {isExpanded(
284
- item.label,
285
- )
286
- ? 'expanded'
287
- : ''}"
288
- />
289
- {/if}
290
- {/if}
291
- </button>
292
- {#if item.items && item.items.length > 0 && isExpanded(item.label) && !collapsed}
293
- <ul
294
- class="sub-nav-list"
295
- transition:slide={{ duration: 200 }}
296
- >
297
- {#each item.items as subItem}
298
- <li class="sub-nav-item">
299
- {#if subItem.href}
300
- <a
301
- href={subItem.href}
302
- class="sub-nav-link"
303
- class:active={subItem.active}
304
- onclick={() =>
305
- handleLinkClick(
306
- subItem,
307
- )}
308
- >
309
- {#if subItem.iconSvg}
310
- <Icon
311
- svg={subItem.iconSvg}
312
- size="1em"
313
- />
314
- {/if}
315
- <span
316
- >{subItem.label}</span
317
- >
318
- </a>
319
- {:else}
320
- <button
321
- type="button"
322
- class="sub-nav-link"
323
- class:active={subItem.active}
324
- onclick={() =>
325
- handleLinkClick(
326
- subItem,
327
- )}
328
- >
329
- {#if subItem.iconSvg}
330
- <Icon
331
- svg={subItem.iconSvg}
332
- size="1em"
333
- />
334
- {/if}
335
- <span
336
- >{subItem.label}</span
337
- >
338
- </button>
339
- {/if}
340
- </li>
341
- {/each}
342
- </ul>
343
- {/if}
344
- {/if}
345
- </li>
346
- {/each}
347
- </ul>
348
- {/each}
349
- </nav>
350
-
351
- {#if filteredBottomSections.length > 0}
352
- <nav class="sidenav-bottom">
353
- {#each filteredBottomSections as section}
354
- {#if section.title && !collapsed}
355
- <div class="section-title">{section.title}</div>
356
- {/if}
357
- <ul class="nav-list">
358
- {#each section.items as item}
359
- <li class="nav-item">
360
- {#if item.href && !(item.items && item.items.length > 0)}
361
- <a
362
- href={item.href}
363
- class="nav-link"
364
- class:active={item.active}
365
- title={collapsed
366
- ? item.label
367
- : undefined}
368
- onclick={() => handleLinkClick(item)}
369
- >
370
- {#if item.iconSvg}
371
- <Icon
372
- svg={item.iconSvg}
373
- size="1.25em"
374
- />
375
- {/if}
376
- {#if !collapsed}
377
- <span class="nav-label"
378
- >{item.label}</span
379
- >
380
- {/if}
381
- </a>
382
- {:else}
383
- <button
384
- type="button"
385
- class="nav-link"
386
- class:active={item.active}
387
- class:has-children={item.items &&
388
- item.items.length > 0}
389
- title={collapsed
390
- ? item.label
391
- : undefined}
392
- onclick={(e) =>
393
- handleItemClick(item, e)}
394
- >
395
- {#if item.iconSvg}
396
- <Icon
397
- svg={item.iconSvg}
398
- size="1.25em"
399
- />
400
- {/if}
401
- {#if !collapsed}
402
- <span class="nav-label"
403
- >{item.label}</span
404
- >
405
- {#if item.items && item.items.length > 0}
406
- <Icon
407
- svg={iconChevronRight}
408
- size="0.75em"
409
- class="chevron {isExpanded(
410
- item.label,
411
- )
412
- ? 'expanded'
413
- : ''}"
414
- />
415
- {/if}
416
- {/if}
417
- </button>
418
- {#if item.items && item.items.length > 0 && isExpanded(item.label) && !collapsed}
419
- <ul
420
- class="sub-nav-list"
421
- transition:slide={{ duration: 200 }}
422
- >
423
- {#each item.items as subItem}
424
- <li class="sub-nav-item">
425
- {#if subItem.href}
426
- <a
427
- href={subItem.href}
428
- class="sub-nav-link"
429
- class:active={subItem.active}
430
- onclick={() =>
431
- handleLinkClick(
432
- subItem,
433
- )}
434
- >
435
- {#if subItem.iconSvg}
436
- <Icon
437
- svg={subItem.iconSvg}
438
- size="1em"
439
- />
440
- {/if}
441
- <span
442
- >{subItem.label}</span
443
- >
444
- </a>
445
- {:else}
446
- <button
447
- type="button"
448
- class="sub-nav-link"
449
- class:active={subItem.active}
450
- onclick={() =>
451
- handleLinkClick(
452
- subItem,
453
- )}
454
- >
455
- {#if subItem.iconSvg}
456
- <Icon
457
- svg={subItem.iconSvg}
458
- size="1em"
459
- />
460
- {/if}
461
- <span
462
- >{subItem.label}</span
463
- >
464
- </button>
465
- {/if}
466
- </li>
467
- {/each}
468
- </ul>
469
- {/if}
470
- {/if}
471
- </li>
472
- {/each}
473
- </ul>
474
- {/each}
475
- </nav>
476
- {/if}
477
-
478
- {#if footer}
479
- <div class="sidenav-footer">
480
- {@render footer()}
481
- </div>
482
- {/if}
483
- </aside>
484
- {/if}
485
-
486
- {#if isMobile}
487
- <!-- Mobile: Drawer -->
488
- <Drawer
489
- bind:open={mobileOpen}
490
- title={mobileTitle}
491
- position={mobileDrawerPosition}
492
- {sections}
493
- {bottomSections}
494
- {width}
495
- onItemClick={handleDrawerItemClick}
496
- >
497
- {#snippet header()}
498
- {#if header}
499
- {@render header()}
500
- {/if}
501
- {/snippet}
502
- {#snippet footer()}
503
- {#if footer}
504
- {@render footer()}
505
- {/if}
506
- {/snippet}
507
- </Drawer>
508
- {/if}
509
-
510
- <style>
511
- .sidenav {
512
- display: flex;
513
- flex-direction: column;
514
- width: var(--sidenav-width);
515
- min-width: var(--sidenav-width);
516
- height: 100%;
517
- background-color: var(--pui-paper-body-bg);
518
- transition:
519
- width var(--pui-transition-fast) var(--pui-ease-in-out),
520
- min-width var(--pui-transition-fast) var(--pui-ease-in-out);
521
- overflow: hidden;
522
- }
523
-
524
- .sidenav.collapsed {
525
- width: var(--sidenav-collapsed-width);
526
- min-width: var(--sidenav-collapsed-width);
527
- }
528
-
529
- .sidenav-header {
530
- padding: var(--pui-spacing-4);
531
- border-bottom: 1px solid var(--pui-border-default);
532
- flex-shrink: 0;
533
- }
534
-
535
- .sidenav-search {
536
- padding: var(--pui-spacing-2) var(--pui-spacing-3);
537
- border-bottom: 1px solid var(--pui-border-default);
538
- flex-shrink: 0;
539
- }
540
-
541
- .sidenav-search :global(.search-textbox) {
542
- margin: 0;
543
- }
544
-
545
- .sidenav-search :global(.search-clear-btn) {
546
- position: absolute;
547
- right: var(--pui-spacing-1);
548
- top: 50%;
549
- transform: translateY(-50%);
550
- }
551
-
552
- .sidenav-content {
553
- flex: 1;
554
- overflow-y: auto;
555
- overflow-x: hidden;
556
- }
557
-
558
- .sidenav-bottom {
559
- flex-shrink: 0;
560
- border-top: 1px solid var(--pui-border-default);
561
- padding: var(--pui-spacing-2) 0;
562
- }
563
-
564
- .sidenav-footer {
565
- padding: var(--pui-spacing-4);
566
- border-top: 1px solid var(--pui-border-default);
567
- flex-shrink: 0;
568
- }
569
-
570
- .section-title {
571
- padding: var(--pui-spacing-3) var(--pui-spacing-4) var(--pui-spacing-2);
572
- text-transform: uppercase;
573
- letter-spacing: 0.05em;
574
- color: var(--pui-text-muted);
575
- border-top: 1px solid var(--pui-border-default);
576
- border-bottom: 1px solid var(--pui-border-default);
577
- }
578
-
579
- .section-title:first-child {
580
- border-top: none;
581
- }
582
-
583
- .nav-list {
584
- list-style: none;
585
- margin: 0;
586
- padding: 0;
587
- }
588
-
589
- .nav-item {
590
- margin: 0;
591
- }
592
-
593
- .nav-link {
594
- display: flex;
595
- align-items: center;
596
- gap: var(--pui-spacing-3);
597
- width: 100%;
598
- padding: var(--pui-spacing-2_5) var(--pui-spacing-4);
599
- color: var(--pui-text-primary);
600
- background: none;
601
- border: none;
602
- text-decoration: none;
603
- cursor: pointer;
604
- transition:
605
- background-color var(--pui-transition-fast) var(--pui-ease-in-out),
606
- color var(--pui-transition-fast) var(--pui-ease-in-out);
607
- text-align: left;
608
- }
609
-
610
- .collapsed .nav-link {
611
- justify-content: center;
612
- padding: var(--pui-spacing-3);
613
- }
614
-
615
- .nav-link:hover {
616
- background-color: var(--pui-bg-hover);
617
- }
618
-
619
- :global(.dark) .nav-link:hover {
620
- background-color: var(--pui-bg-hover);
621
- }
622
-
623
- .nav-link.active {
624
- background-color: var(--pui-bg-active);
625
- border-left: 3px solid var(--pui-color-secondary);
626
- }
627
-
628
- :global(.dark) .nav-link.active {
629
- background-color: var(--pui-bg-active);
630
- border-left: 3px solid var(--pui-color-primary);
631
- }
632
-
633
- .nav-label {
634
- flex: 1;
635
- white-space: nowrap;
636
- overflow: hidden;
637
- text-overflow: ellipsis;
638
- }
639
-
640
- .nav-link :global(.chevron) {
641
- transition: transform var(--pui-transition-fast) var(--pui-ease-in-out);
642
- flex-shrink: 0;
643
- }
644
-
645
- .nav-link :global(.chevron.expanded) {
646
- transform: rotate(90deg);
647
- }
648
-
649
- .sub-nav-list {
650
- list-style: none;
651
- margin: 0;
652
- padding: 0;
653
- background-color: var(--pui-bg-subtle);
654
- }
655
-
656
- :global(.dark) .sub-nav-list {
657
- background-color: var(--pui-bg-subtle);
658
- }
659
-
660
- .sub-nav-item {
661
- margin: 0;
662
- }
663
-
664
- .sub-nav-link {
665
- display: flex;
666
- align-items: center;
667
- gap: var(--pui-spacing-2);
668
- width: 100%;
669
- padding: var(--pui-spacing-2) var(--pui-spacing-4) var(--pui-spacing-2)
670
- var(--pui-spacing-11);
671
- color: var(--pui-text-primary);
672
- background: none;
673
- border: none;
674
- text-decoration: none;
675
- cursor: pointer;
676
- transition:
677
- background-color var(--pui-transition-fast) var(--pui-ease-in-out),
678
- color var(--pui-transition-fast) var(--pui-ease-in-out);
679
- text-align: left;
680
- }
681
-
682
- .sub-nav-link:hover {
683
- background-color: var(--pui-bg-hover);
684
- }
685
-
686
- :global(.dark) .sub-nav-link:hover {
687
- background-color: var(--pui-bg-hover);
688
- }
689
-
690
- .sub-nav-link.active {
691
- border-left: 3px solid var(--pui-color-secondary);
692
- }
693
-
694
- :global(.dark) .sub-nav-link.active {
695
- border-left: 3px solid var(--pui-color-primary);
696
- }
697
-
698
- /* Scrollbar styling */
699
- .sidenav-content::-webkit-scrollbar {
700
- width: 6px;
701
- }
702
-
703
- .sidenav-content::-webkit-scrollbar-track {
704
- background: transparent;
705
- }
706
-
707
- .sidenav-content::-webkit-scrollbar-thumb {
708
- background-color: var(--pui-color-gray-600);
709
- border-radius: var(--pui-radius-sm);
710
- border: none;
711
- }
712
- </style>
1
+ <script lang="ts" module>
2
+ export interface SidenavItem {
3
+ label: string;
4
+ href?: string;
5
+ iconSvg?: string;
6
+ active?: boolean;
7
+ items?: SidenavItem[];
8
+ onclick?: () => void;
9
+ }
10
+
11
+ export interface SidenavSection {
12
+ title?: string;
13
+ items: SidenavItem[];
14
+ }
15
+ </script>
16
+
17
+ <script lang="ts">
18
+ import type { Snippet } from "svelte";
19
+ import Icon from "../icon/Icon.svelte";
20
+ import Textbox from "../form/Textbox.svelte";
21
+ import ActionIcon from "../ui/ActionIcon.svelte";
22
+ import Drawer from "../ui/Drawer.svelte";
23
+ import { iconChevronRight, iconSearch, iconX } from "../icon/index.js";
24
+ import { slide } from "svelte/transition";
25
+ import { viewportState } from "../theme.svelte.js";
26
+
27
+ interface Props {
28
+ sections?: SidenavSection[];
29
+ bottomSections?: SidenavSection[];
30
+ collapsed?: boolean;
31
+ width?: string;
32
+ collapsedWidth?: string;
33
+ class?: string;
34
+ header?: Snippet;
35
+ footer?: Snippet;
36
+ searchable?: boolean;
37
+ searchPlaceholder?: string;
38
+ onItemClick?: (item: SidenavItem) => void;
39
+ // Responsive props
40
+ responsive?: boolean;
41
+ mobileBreakpoint?: number;
42
+ mobileOpen?: boolean;
43
+ mobileTitle?: string;
44
+ mobileDrawerPosition?: "left" | "right";
45
+ }
46
+
47
+ let {
48
+ sections = [],
49
+ bottomSections = [],
50
+ collapsed = false,
51
+ width = "260px",
52
+ collapsedWidth = "60px",
53
+ class: className = "",
54
+ header,
55
+ footer,
56
+ searchable = false,
57
+ searchPlaceholder = "Search...",
58
+ onItemClick,
59
+ // Responsive props
60
+ responsive = false,
61
+ mobileBreakpoint = 768,
62
+ mobileOpen = $bindable(false),
63
+ mobileTitle = "",
64
+ mobileDrawerPosition = "left",
65
+ }: Props = $props();
66
+
67
+ let searchQuery = $state("");
68
+
69
+ // Track if we're in mobile mode using viewportState
70
+ const isMobile = $derived(
71
+ responsive && viewportState.isBelow(mobileBreakpoint),
72
+ );
73
+
74
+ // Auto-close drawer when resizing to desktop
75
+ $effect(() => {
76
+ if (
77
+ responsive &&
78
+ !viewportState.isBelow(mobileBreakpoint) &&
79
+ mobileOpen
80
+ ) {
81
+ mobileOpen = false;
82
+ }
83
+ });
84
+
85
+ // Simple fuzzy search - checks if characters appear in order
86
+ function fuzzyMatch(query: string, text: string): boolean {
87
+ if (!query) return true;
88
+ const lowerQuery = query.toLowerCase();
89
+ const lowerText = text.toLowerCase();
90
+
91
+ let queryIndex = 0;
92
+ for (
93
+ let i = 0;
94
+ i < lowerText.length && queryIndex < lowerQuery.length;
95
+ i++
96
+ ) {
97
+ if (lowerText[i] === lowerQuery[queryIndex]) {
98
+ queryIndex++;
99
+ }
100
+ }
101
+ return queryIndex === lowerQuery.length;
102
+ }
103
+
104
+ // Filter sections based on search query
105
+ function filterSections(sects: SidenavSection[]): SidenavSection[] {
106
+ if (!searchQuery) return sects;
107
+
108
+ return sects
109
+ .map((section) => {
110
+ const filteredItems = section.items
111
+ .filter((item) => {
112
+ // Check if item matches
113
+ if (fuzzyMatch(searchQuery, item.label)) return true;
114
+ // Check if any sub-item matches
115
+ if (
116
+ item.items?.some((sub) =>
117
+ fuzzyMatch(searchQuery, sub.label),
118
+ )
119
+ )
120
+ return true;
121
+ return false;
122
+ })
123
+ .map((item) => {
124
+ // If item has sub-items, filter those too
125
+ if (item.items) {
126
+ return {
127
+ ...item,
128
+ items: item.items.filter((sub) =>
129
+ fuzzyMatch(searchQuery, sub.label),
130
+ ),
131
+ };
132
+ }
133
+ return item;
134
+ });
135
+
136
+ return { ...section, items: filteredItems };
137
+ })
138
+ .filter((section) => section.items.length > 0);
139
+ }
140
+
141
+ let filteredSections = $derived(filterSections(sections));
142
+ let filteredBottomSections = $derived(filterSections(bottomSections));
143
+
144
+ function clearSearch() {
145
+ searchQuery = "";
146
+ }
147
+
148
+ let expandedSections: Set<string> = $state(new Set());
149
+
150
+ function toggleSection(itemLabel: string) {
151
+ if (expandedSections.has(itemLabel)) {
152
+ expandedSections.delete(itemLabel);
153
+ expandedSections = new Set(expandedSections);
154
+ } else {
155
+ expandedSections.add(itemLabel);
156
+ expandedSections = new Set(expandedSections);
157
+ }
158
+ }
159
+
160
+ function isExpanded(itemLabel: string): boolean {
161
+ return expandedSections.has(itemLabel);
162
+ }
163
+
164
+ function handleItemClick(item: SidenavItem, e: MouseEvent) {
165
+ if (item.items && item.items.length > 0) {
166
+ e.preventDefault();
167
+ toggleSection(item.label);
168
+ } else {
169
+ onItemClick?.(item);
170
+ item.onclick?.();
171
+ // Close drawer on mobile after navigation
172
+ if (isMobile) {
173
+ mobileOpen = false;
174
+ }
175
+ }
176
+ }
177
+
178
+ function handleLinkClick(item: SidenavItem) {
179
+ onItemClick?.(item);
180
+ item.onclick?.();
181
+ // Close drawer on mobile after navigation
182
+ if (isMobile) {
183
+ mobileOpen = false;
184
+ }
185
+ }
186
+
187
+ function handleDrawerItemClick(item: SidenavItem) {
188
+ onItemClick?.(item);
189
+ item.onclick?.();
190
+ // Close drawer after clicking an item
191
+ mobileOpen = false;
192
+ }
193
+ </script>
194
+
195
+ {#if !isMobile}
196
+ <!-- Desktop: Regular Sidenav -->
197
+ <aside
198
+ class="sidenav {className}"
199
+ class:collapsed
200
+ style="--sidenav-width: {width}; --sidenav-collapsed-width: {collapsedWidth};"
201
+ >
202
+ {#if header}
203
+ <div class="sidenav-header">
204
+ {@render header()}
205
+ </div>
206
+ {/if}
207
+
208
+ {#if searchable && !collapsed}
209
+ <div class="sidenav-search">
210
+ <Textbox
211
+ placeholder={searchPlaceholder}
212
+ leftIconSvg={iconSearch}
213
+ bind:value={searchQuery}
214
+ containerClass="search-textbox"
215
+ >
216
+ {#snippet right()}
217
+ {#if searchQuery}
218
+ <ActionIcon
219
+ svg={iconX}
220
+ size="0.75rem"
221
+ variant="secondary-subtle"
222
+ onclick={clearSearch}
223
+ class="search-clear-btn"
224
+ />
225
+ {/if}
226
+ {/snippet}
227
+ </Textbox>
228
+ </div>
229
+ {/if}
230
+
231
+ <nav class="sidenav-content">
232
+ {#each filteredSections as section}
233
+ {#if section.title && !collapsed}
234
+ <div class="section-title">{section.title}</div>
235
+ {/if}
236
+ <ul class="nav-list">
237
+ {#each section.items as item}
238
+ <li class="nav-item">
239
+ {#if item.href && !(item.items && item.items.length > 0)}
240
+ <a
241
+ href={item.href}
242
+ class="nav-link"
243
+ class:active={item.active}
244
+ title={collapsed ? item.label : undefined}
245
+ onclick={() => handleLinkClick(item)}
246
+ >
247
+ {#if item.iconSvg}
248
+ <Icon
249
+ svg={item.iconSvg}
250
+ size="1.25em"
251
+ />
252
+ {/if}
253
+ {#if !collapsed}
254
+ <span class="nav-label"
255
+ >{item.label}</span
256
+ >
257
+ {/if}
258
+ </a>
259
+ {:else}
260
+ <button
261
+ type="button"
262
+ class="nav-link"
263
+ class:active={item.active}
264
+ class:has-children={item.items &&
265
+ item.items.length > 0}
266
+ title={collapsed ? item.label : undefined}
267
+ onclick={(e) => handleItemClick(item, e)}
268
+ >
269
+ {#if item.iconSvg}
270
+ <Icon
271
+ svg={item.iconSvg}
272
+ size="1.25em"
273
+ />
274
+ {/if}
275
+ {#if !collapsed}
276
+ <span class="nav-label"
277
+ >{item.label}</span
278
+ >
279
+ {#if item.items && item.items.length > 0}
280
+ <Icon
281
+ svg={iconChevronRight}
282
+ size="0.75em"
283
+ class="chevron {isExpanded(
284
+ item.label,
285
+ )
286
+ ? 'expanded'
287
+ : ''}"
288
+ />
289
+ {/if}
290
+ {/if}
291
+ </button>
292
+ {#if item.items && item.items.length > 0 && isExpanded(item.label) && !collapsed}
293
+ <ul
294
+ class="sub-nav-list"
295
+ transition:slide={{ duration: 200 }}
296
+ >
297
+ {#each item.items as subItem}
298
+ <li class="sub-nav-item">
299
+ {#if subItem.href}
300
+ <a
301
+ href={subItem.href}
302
+ class="sub-nav-link"
303
+ class:active={subItem.active}
304
+ onclick={() =>
305
+ handleLinkClick(
306
+ subItem,
307
+ )}
308
+ >
309
+ {#if subItem.iconSvg}
310
+ <Icon
311
+ svg={subItem.iconSvg}
312
+ size="1em"
313
+ />
314
+ {/if}
315
+ <span
316
+ >{subItem.label}</span
317
+ >
318
+ </a>
319
+ {:else}
320
+ <button
321
+ type="button"
322
+ class="sub-nav-link"
323
+ class:active={subItem.active}
324
+ onclick={() =>
325
+ handleLinkClick(
326
+ subItem,
327
+ )}
328
+ >
329
+ {#if subItem.iconSvg}
330
+ <Icon
331
+ svg={subItem.iconSvg}
332
+ size="1em"
333
+ />
334
+ {/if}
335
+ <span
336
+ >{subItem.label}</span
337
+ >
338
+ </button>
339
+ {/if}
340
+ </li>
341
+ {/each}
342
+ </ul>
343
+ {/if}
344
+ {/if}
345
+ </li>
346
+ {/each}
347
+ </ul>
348
+ {/each}
349
+ </nav>
350
+
351
+ {#if filteredBottomSections.length > 0}
352
+ <nav class="sidenav-bottom">
353
+ {#each filteredBottomSections as section}
354
+ {#if section.title && !collapsed}
355
+ <div class="section-title">{section.title}</div>
356
+ {/if}
357
+ <ul class="nav-list">
358
+ {#each section.items as item}
359
+ <li class="nav-item">
360
+ {#if item.href && !(item.items && item.items.length > 0)}
361
+ <a
362
+ href={item.href}
363
+ class="nav-link"
364
+ class:active={item.active}
365
+ title={collapsed
366
+ ? item.label
367
+ : undefined}
368
+ onclick={() => handleLinkClick(item)}
369
+ >
370
+ {#if item.iconSvg}
371
+ <Icon
372
+ svg={item.iconSvg}
373
+ size="1.25em"
374
+ />
375
+ {/if}
376
+ {#if !collapsed}
377
+ <span class="nav-label"
378
+ >{item.label}</span
379
+ >
380
+ {/if}
381
+ </a>
382
+ {:else}
383
+ <button
384
+ type="button"
385
+ class="nav-link"
386
+ class:active={item.active}
387
+ class:has-children={item.items &&
388
+ item.items.length > 0}
389
+ title={collapsed
390
+ ? item.label
391
+ : undefined}
392
+ onclick={(e) =>
393
+ handleItemClick(item, e)}
394
+ >
395
+ {#if item.iconSvg}
396
+ <Icon
397
+ svg={item.iconSvg}
398
+ size="1.25em"
399
+ />
400
+ {/if}
401
+ {#if !collapsed}
402
+ <span class="nav-label"
403
+ >{item.label}</span
404
+ >
405
+ {#if item.items && item.items.length > 0}
406
+ <Icon
407
+ svg={iconChevronRight}
408
+ size="0.75em"
409
+ class="chevron {isExpanded(
410
+ item.label,
411
+ )
412
+ ? 'expanded'
413
+ : ''}"
414
+ />
415
+ {/if}
416
+ {/if}
417
+ </button>
418
+ {#if item.items && item.items.length > 0 && isExpanded(item.label) && !collapsed}
419
+ <ul
420
+ class="sub-nav-list"
421
+ transition:slide={{ duration: 200 }}
422
+ >
423
+ {#each item.items as subItem}
424
+ <li class="sub-nav-item">
425
+ {#if subItem.href}
426
+ <a
427
+ href={subItem.href}
428
+ class="sub-nav-link"
429
+ class:active={subItem.active}
430
+ onclick={() =>
431
+ handleLinkClick(
432
+ subItem,
433
+ )}
434
+ >
435
+ {#if subItem.iconSvg}
436
+ <Icon
437
+ svg={subItem.iconSvg}
438
+ size="1em"
439
+ />
440
+ {/if}
441
+ <span
442
+ >{subItem.label}</span
443
+ >
444
+ </a>
445
+ {:else}
446
+ <button
447
+ type="button"
448
+ class="sub-nav-link"
449
+ class:active={subItem.active}
450
+ onclick={() =>
451
+ handleLinkClick(
452
+ subItem,
453
+ )}
454
+ >
455
+ {#if subItem.iconSvg}
456
+ <Icon
457
+ svg={subItem.iconSvg}
458
+ size="1em"
459
+ />
460
+ {/if}
461
+ <span
462
+ >{subItem.label}</span
463
+ >
464
+ </button>
465
+ {/if}
466
+ </li>
467
+ {/each}
468
+ </ul>
469
+ {/if}
470
+ {/if}
471
+ </li>
472
+ {/each}
473
+ </ul>
474
+ {/each}
475
+ </nav>
476
+ {/if}
477
+
478
+ {#if footer}
479
+ <div class="sidenav-footer">
480
+ {@render footer()}
481
+ </div>
482
+ {/if}
483
+ </aside>
484
+ {/if}
485
+
486
+ {#if isMobile}
487
+ <!-- Mobile: Drawer -->
488
+ <Drawer
489
+ bind:open={mobileOpen}
490
+ title={mobileTitle}
491
+ position={mobileDrawerPosition}
492
+ {sections}
493
+ {bottomSections}
494
+ {width}
495
+ onItemClick={handleDrawerItemClick}
496
+ >
497
+ {#snippet header()}
498
+ {#if header}
499
+ {@render header()}
500
+ {/if}
501
+ {/snippet}
502
+ {#snippet footer()}
503
+ {#if footer}
504
+ {@render footer()}
505
+ {/if}
506
+ {/snippet}
507
+ </Drawer>
508
+ {/if}
509
+
510
+ <style>
511
+ .sidenav {
512
+ display: flex;
513
+ flex-direction: column;
514
+ width: var(--sidenav-width);
515
+ min-width: var(--sidenav-width);
516
+ height: 100%;
517
+ background-color: var(--pui-paper-body-bg);
518
+ transition:
519
+ width var(--pui-transition-fast) var(--pui-ease-in-out),
520
+ min-width var(--pui-transition-fast) var(--pui-ease-in-out);
521
+ overflow: hidden;
522
+ }
523
+
524
+ .sidenav.collapsed {
525
+ width: var(--sidenav-collapsed-width);
526
+ min-width: var(--sidenav-collapsed-width);
527
+ }
528
+
529
+ .sidenav-header {
530
+ padding: var(--pui-spacing-4);
531
+ border-bottom: 1px solid var(--pui-border-default);
532
+ flex-shrink: 0;
533
+ }
534
+
535
+ .sidenav-search {
536
+ padding: var(--pui-spacing-2) var(--pui-spacing-3);
537
+ border-bottom: 1px solid var(--pui-border-default);
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .sidenav-search :global(.search-textbox) {
542
+ margin: 0;
543
+ }
544
+
545
+ .sidenav-search :global(.search-clear-btn) {
546
+ position: absolute;
547
+ right: var(--pui-spacing-1);
548
+ top: 50%;
549
+ transform: translateY(-50%);
550
+ }
551
+
552
+ .sidenav-content {
553
+ flex: 1;
554
+ overflow-y: auto;
555
+ overflow-x: hidden;
556
+ }
557
+
558
+ .sidenav-bottom {
559
+ flex-shrink: 0;
560
+ border-top: 1px solid var(--pui-border-default);
561
+ padding: var(--pui-spacing-2) 0;
562
+ }
563
+
564
+ .sidenav-footer {
565
+ padding: var(--pui-spacing-4);
566
+ border-top: 1px solid var(--pui-border-default);
567
+ flex-shrink: 0;
568
+ }
569
+
570
+ .section-title {
571
+ padding: var(--pui-spacing-3) var(--pui-spacing-4) var(--pui-spacing-2);
572
+ text-transform: uppercase;
573
+ letter-spacing: 0.05em;
574
+ color: var(--pui-text-muted);
575
+ border-top: 1px solid var(--pui-border-default);
576
+ border-bottom: 1px solid var(--pui-border-default);
577
+ }
578
+
579
+ .section-title:first-child {
580
+ border-top: none;
581
+ }
582
+
583
+ .nav-list {
584
+ list-style: none;
585
+ margin: 0;
586
+ padding: 0;
587
+ }
588
+
589
+ .nav-item {
590
+ margin: 0;
591
+ }
592
+
593
+ .nav-link {
594
+ display: flex;
595
+ align-items: center;
596
+ gap: var(--pui-spacing-3);
597
+ width: 100%;
598
+ padding: var(--pui-spacing-2_5) var(--pui-spacing-4);
599
+ color: var(--pui-text-primary);
600
+ background: none;
601
+ border: none;
602
+ text-decoration: none;
603
+ cursor: pointer;
604
+ transition:
605
+ background-color var(--pui-transition-fast) var(--pui-ease-in-out),
606
+ color var(--pui-transition-fast) var(--pui-ease-in-out);
607
+ text-align: left;
608
+ }
609
+
610
+ .collapsed .nav-link {
611
+ justify-content: center;
612
+ padding: var(--pui-spacing-3);
613
+ }
614
+
615
+ .nav-link:hover {
616
+ background-color: var(--pui-bg-hover);
617
+ }
618
+
619
+ :global(.dark) .nav-link:hover {
620
+ background-color: var(--pui-bg-hover);
621
+ }
622
+
623
+ .nav-link.active {
624
+ background-color: var(--pui-bg-active);
625
+ border-left: 3px solid var(--pui-color-secondary);
626
+ }
627
+
628
+ :global(.dark) .nav-link.active {
629
+ background-color: var(--pui-bg-active);
630
+ border-left: 3px solid var(--pui-color-primary);
631
+ }
632
+
633
+ .nav-label {
634
+ flex: 1;
635
+ white-space: nowrap;
636
+ overflow: hidden;
637
+ text-overflow: ellipsis;
638
+ }
639
+
640
+ .nav-link :global(.chevron) {
641
+ transition: transform var(--pui-transition-fast) var(--pui-ease-in-out);
642
+ flex-shrink: 0;
643
+ }
644
+
645
+ .nav-link :global(.chevron.expanded) {
646
+ transform: rotate(90deg);
647
+ }
648
+
649
+ .sub-nav-list {
650
+ list-style: none;
651
+ margin: 0;
652
+ padding: 0;
653
+ background-color: var(--pui-bg-subtle);
654
+ }
655
+
656
+ :global(.dark) .sub-nav-list {
657
+ background-color: var(--pui-bg-subtle);
658
+ }
659
+
660
+ .sub-nav-item {
661
+ margin: 0;
662
+ }
663
+
664
+ .sub-nav-link {
665
+ display: flex;
666
+ align-items: center;
667
+ gap: var(--pui-spacing-2);
668
+ width: 100%;
669
+ padding: var(--pui-spacing-2) var(--pui-spacing-4) var(--pui-spacing-2)
670
+ var(--pui-spacing-11);
671
+ color: var(--pui-text-primary);
672
+ background: none;
673
+ border: none;
674
+ text-decoration: none;
675
+ cursor: pointer;
676
+ transition:
677
+ background-color var(--pui-transition-fast) var(--pui-ease-in-out),
678
+ color var(--pui-transition-fast) var(--pui-ease-in-out);
679
+ text-align: left;
680
+ }
681
+
682
+ .sub-nav-link:hover {
683
+ background-color: var(--pui-bg-hover);
684
+ }
685
+
686
+ :global(.dark) .sub-nav-link:hover {
687
+ background-color: var(--pui-bg-hover);
688
+ }
689
+
690
+ .sub-nav-link.active {
691
+ border-left: 3px solid var(--pui-color-secondary);
692
+ }
693
+
694
+ :global(.dark) .sub-nav-link.active {
695
+ border-left: 3px solid var(--pui-color-primary);
696
+ }
697
+
698
+ /* Scrollbar styling */
699
+ .sidenav-content::-webkit-scrollbar {
700
+ width: 6px;
701
+ }
702
+
703
+ .sidenav-content::-webkit-scrollbar-track {
704
+ background: transparent;
705
+ }
706
+
707
+ .sidenav-content::-webkit-scrollbar-thumb {
708
+ background-color: var(--pui-color-gray-600);
709
+ border-radius: var(--pui-radius-sm);
710
+ border: none;
711
+ }
712
+ </style>