@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,524 +1,524 @@
1
- <script lang="ts">
2
- import type { Snippet } from 'svelte';
3
- import type { SidenavItem, SidenavSection } from '../layout/Sidenav.svelte';
4
- import Icon from '../icon/Icon.svelte';
5
- import Link from './Link.svelte';
6
- import ActionIcon from './ActionIcon.svelte';
7
- import { iconChevronRight, iconX } from '../icon/index.js';
8
- import { fade, fly } from 'svelte/transition';
9
- import { slide } from 'svelte/transition';
10
-
11
- interface Props {
12
- open?: boolean;
13
- position?: 'left' | 'right';
14
- width?: string;
15
- closeOnClickOutside?: boolean;
16
- closeOnEscape?: boolean;
17
- closeForLargeScreens?: boolean;
18
- largeScreenBreakpoint?: number;
19
- sections?: SidenavSection[];
20
- bottomSections?: SidenavSection[];
21
- title?: string;
22
- class?: string;
23
- header?: Snippet;
24
- footer?: Snippet;
25
- children?: Snippet;
26
- onclose?: () => void;
27
- onItemClick?: (item: SidenavItem) => void;
28
- }
29
-
30
- let {
31
- open = $bindable(false),
32
- position = 'left',
33
- width = '320px',
34
- closeOnClickOutside = true,
35
- closeOnEscape = true,
36
- closeForLargeScreens = false,
37
- largeScreenBreakpoint = 1024,
38
- sections = [],
39
- bottomSections = [],
40
- title = '',
41
- class: className = '',
42
- header,
43
- footer,
44
- children,
45
- onclose,
46
- onItemClick
47
- }: Props = $props();
48
-
49
- let expandedSections: Set<string> = $state(new Set());
50
-
51
- function close() {
52
- open = false;
53
- onclose?.();
54
- }
55
-
56
- function handleBackdropClick() {
57
- if (closeOnClickOutside) {
58
- close();
59
- }
60
- }
61
-
62
- function handleKeydown(e: KeyboardEvent) {
63
- if (open && closeOnEscape && e.key === 'Escape') {
64
- close();
65
- }
66
- }
67
-
68
- function handleWindowResize() {
69
- if (closeForLargeScreens && typeof window !== 'undefined' && window.innerWidth > largeScreenBreakpoint) {
70
- close();
71
- }
72
- }
73
-
74
- function toggleSection(itemLabel: string) {
75
- if (expandedSections.has(itemLabel)) {
76
- expandedSections.delete(itemLabel);
77
- expandedSections = new Set(expandedSections);
78
- } else {
79
- expandedSections.add(itemLabel);
80
- expandedSections = new Set(expandedSections);
81
- }
82
- }
83
-
84
- function isExpanded(itemLabel: string): boolean {
85
- return expandedSections.has(itemLabel);
86
- }
87
-
88
- function handleItemClick(item: SidenavItem, e: MouseEvent) {
89
- if (item.items && item.items.length > 0) {
90
- e.preventDefault();
91
- toggleSection(item.label);
92
- } else {
93
- onItemClick?.(item);
94
- item.onclick?.();
95
- }
96
- }
97
-
98
- function handleLinkClick(item: SidenavItem) {
99
- onItemClick?.(item);
100
- item.onclick?.();
101
- }
102
-
103
- // Body scroll lock
104
- $effect(() => {
105
- if (typeof document !== 'undefined') {
106
- const body = document.body;
107
- if (open) {
108
- body.style.overflow = 'hidden';
109
- } else {
110
- body.style.overflow = '';
111
- }
112
- return () => {
113
- body.style.overflow = '';
114
- };
115
- }
116
- });
117
-
118
- const hasNavContent = $derived(sections.length > 0 || bottomSections.length > 0);
119
- const flyDirection = $derived(position === 'left' ? -320 : 320);
120
- </script>
121
-
122
- <svelte:window onresize={handleWindowResize} onkeydown={handleKeydown} />
123
-
124
- {#if open}
125
- <!-- Backdrop -->
126
- <div
127
- class="drawer-backdrop"
128
- transition:fade={{ duration: 200 }}
129
- onclick={handleBackdropClick}
130
- onkeydown={(e) => e.key === 'Enter' && handleBackdropClick()}
131
- role="button"
132
- tabindex="-1"
133
- aria-label="Close drawer"
134
- ></div>
135
-
136
- <!-- Drawer -->
137
- <div
138
- class="drawer {className}"
139
- class:drawer-left={position === 'left'}
140
- class:drawer-right={position === 'right'}
141
- style="--drawer-width: {width};"
142
- transition:fly={{ x: flyDirection, duration: 250, opacity: 1 }}
143
- onclick={(e) => e.stopPropagation()}
144
- onkeydown={(e) => e.stopPropagation()}
145
- role="dialog"
146
- aria-modal="true"
147
- tabindex="-1"
148
- >
149
- <!-- Header (always shown for close button) -->
150
- <div class="drawer-header">
151
- {#if header}
152
- {@render header()}
153
- {:else if title}
154
- <h2 class="drawer-title">{title}</h2>
155
- {/if}
156
- <ActionIcon
157
- svg={iconX}
158
- size="1.25rem"
159
- variant="secondary-subtle"
160
- onclick={close}
161
- class="drawer-close-btn"
162
- />
163
- </div>
164
-
165
- <!-- Main content area -->
166
- <div class="drawer-content">
167
- {#if hasNavContent}
168
- <!-- Navigation sections -->
169
- <nav class="drawer-nav">
170
- {#each sections as section}
171
- {#if section.title}
172
- <div class="section-title">{section.title}</div>
173
- {/if}
174
- <ul class="nav-list">
175
- {#each section.items as item}
176
- <li class="nav-item">
177
- {#if item.href && !(item.items && item.items.length > 0)}
178
- <Link
179
- href={item.href}
180
- class="nav-link {item.active ? 'active' : ''}"
181
- onclick={() => handleLinkClick(item)}
182
- >
183
- {#if item.iconSvg}
184
- <Icon svg={item.iconSvg} size="1.25em" />
185
- {/if}
186
- <span class="nav-label">{item.label}</span>
187
- </Link>
188
- {:else}
189
- <button
190
- type="button"
191
- class="nav-link"
192
- class:active={item.active}
193
- class:has-children={item.items && item.items.length > 0}
194
- onclick={(e) => handleItemClick(item, e)}
195
- >
196
- {#if item.iconSvg}
197
- <Icon svg={item.iconSvg} size="1.25em" />
198
- {/if}
199
- <span class="nav-label">{item.label}</span>
200
- {#if item.items && item.items.length > 0}
201
- <Icon
202
- svg={iconChevronRight}
203
- size="0.75em"
204
- class="chevron {isExpanded(item.label) ? 'expanded' : ''}"
205
- />
206
- {/if}
207
- </button>
208
- {#if item.items && item.items.length > 0 && isExpanded(item.label)}
209
- <ul class="sub-nav-list" transition:slide={{ duration: 200 }}>
210
- {#each item.items as subItem}
211
- <li class="sub-nav-item">
212
- {#if subItem.href}
213
- <Link
214
- href={subItem.href}
215
- class="sub-nav-link {subItem.active ? 'active' : ''}"
216
- onclick={() => handleLinkClick(subItem)}
217
- >
218
- {#if subItem.iconSvg}
219
- <Icon svg={subItem.iconSvg} size="1em" />
220
- {/if}
221
- <span>{subItem.label}</span>
222
- </Link>
223
- {:else}
224
- <button
225
- type="button"
226
- class="sub-nav-link"
227
- class:active={subItem.active}
228
- onclick={() => handleLinkClick(subItem)}
229
- >
230
- {#if subItem.iconSvg}
231
- <Icon svg={subItem.iconSvg} size="1em" />
232
- {/if}
233
- <span>{subItem.label}</span>
234
- </button>
235
- {/if}
236
- </li>
237
- {/each}
238
- </ul>
239
- {/if}
240
- {/if}
241
- </li>
242
- {/each}
243
- </ul>
244
- {/each}
245
- </nav>
246
- {:else if children}
247
- {@render children()}
248
- {/if}
249
- </div>
250
-
251
- <!-- Bottom sections -->
252
- {#if bottomSections.length > 0}
253
- <nav class="drawer-bottom">
254
- {#each bottomSections as section}
255
- {#if section.title}
256
- <div class="section-title">{section.title}</div>
257
- {/if}
258
- <ul class="nav-list">
259
- {#each section.items as item}
260
- <li class="nav-item">
261
- {#if item.href}
262
- <Link
263
- href={item.href}
264
- class="nav-link {item.active ? 'active' : ''}"
265
- onclick={() => handleLinkClick(item)}
266
- >
267
- {#if item.iconSvg}
268
- <Icon svg={item.iconSvg} size="1.25em" />
269
- {/if}
270
- <span class="nav-label">{item.label}</span>
271
- </Link>
272
- {:else}
273
- <button
274
- type="button"
275
- class="nav-link"
276
- class:active={item.active}
277
- onclick={() => handleLinkClick(item)}
278
- >
279
- {#if item.iconSvg}
280
- <Icon svg={item.iconSvg} size="1.25em" />
281
- {/if}
282
- <span class="nav-label">{item.label}</span>
283
- </button>
284
- {/if}
285
- </li>
286
- {/each}
287
- </ul>
288
- {/each}
289
- </nav>
290
- {/if}
291
-
292
- <!-- Footer -->
293
- {#if footer}
294
- <div class="drawer-footer">
295
- {@render footer()}
296
- </div>
297
- {/if}
298
- </div>
299
- {/if}
300
-
301
- <style>
302
- .drawer-backdrop {
303
- position: fixed;
304
- inset: 0;
305
- background-color: var(--pui-bg-overlay);
306
- z-index: var(--pui-z-modal-backdrop);
307
- }
308
-
309
- .drawer {
310
- position: fixed;
311
- top: 0;
312
- bottom: 0;
313
- display: flex;
314
- flex-direction: column;
315
- width: var(--drawer-width);
316
- max-width: 100vw;
317
- background-color: var(--pui-paper-body-bg);
318
- border-right: 1px solid var(--pui-border-default);
319
- z-index: var(--pui-z-modal);
320
- overflow: hidden;
321
- }
322
-
323
- .drawer-left {
324
- left: 0;
325
- border-right: 1px solid var(--pui-border-default);
326
- border-left: none;
327
- }
328
-
329
- .drawer-right {
330
- right: 0;
331
- border-left: 1px solid var(--pui-border-default);
332
- border-right: none;
333
- }
334
-
335
- .drawer-header {
336
- display: flex;
337
- align-items: center;
338
- justify-content: space-between;
339
- gap: var(--pui-spacing-4);
340
- padding: var(--pui-spacing-3) var(--pui-spacing-4);
341
- border-bottom: 1px solid var(--pui-border-default);
342
- flex-shrink: 0;
343
- min-height: 46px;
344
- }
345
-
346
- .drawer-title {
347
- margin: 0;
348
- font-size: var(--pui-font-size-lg);
349
- font-weight: var(--pui-font-weight-semibold);
350
- color: var(--pui-text-primary);
351
- flex: 1;
352
- }
353
-
354
- .drawer-header :global(.drawer-close-btn) {
355
- flex-shrink: 0;
356
- margin-left: auto;
357
- }
358
-
359
- .drawer-content {
360
- flex: 1;
361
- overflow-y: auto;
362
- overflow-x: hidden;
363
- }
364
-
365
- .drawer-nav {
366
- padding: var(--pui-spacing-2) 0;
367
- }
368
-
369
- .drawer-bottom {
370
- flex-shrink: 0;
371
- border-top: 1px solid var(--pui-border-default);
372
- padding: var(--pui-spacing-2) 0;
373
- }
374
-
375
- .drawer-footer {
376
- padding: var(--pui-spacing-4);
377
- border-top: 1px solid var(--pui-border-default);
378
- flex-shrink: 0;
379
- }
380
-
381
- .section-title {
382
- padding: var(--pui-spacing-3) var(--pui-spacing-4) var(--pui-spacing-2);
383
- font-size: var(--pui-font-size-xs);
384
- font-weight: var(--pui-font-weight-semibold);
385
- text-transform: uppercase;
386
- letter-spacing: var(--pui-letter-spacing-wide);
387
- color: var(--pui-text-muted);
388
- }
389
-
390
- .nav-list {
391
- list-style: none;
392
- margin: 0;
393
- padding: 0;
394
- }
395
-
396
- .nav-item {
397
- margin: 0;
398
- }
399
-
400
- .nav-item :global(.nav-link),
401
- .nav-item > button.nav-link {
402
- display: flex;
403
- align-items: center;
404
- gap: var(--pui-spacing-3);
405
- width: 100%;
406
- padding: var(--pui-spacing-2_5) var(--pui-spacing-4);
407
- font-size: var(--pui-font-size-base);
408
- font-weight: var(--pui-font-weight-medium);
409
- color: var(--pui-text-primary);
410
- background: none;
411
- border: none;
412
- text-decoration: none;
413
- cursor: pointer;
414
- transition: background-color var(--pui-transition-fast) var(--pui-ease-out), color var(--pui-transition-fast) var(--pui-ease-out);
415
- text-align: left;
416
- }
417
-
418
- .nav-item :global(.nav-link:hover),
419
- .nav-item > button.nav-link:hover {
420
- background-color: var(--pui-bg-hover);
421
- }
422
-
423
- :global(.dark) .nav-item :global(.nav-link:hover),
424
- :global(.dark) .nav-item > button.nav-link:hover {
425
- background-color: var(--pui-bg-hover);
426
- }
427
-
428
- .nav-item :global(.nav-link.active),
429
- .nav-item > button.nav-link.active {
430
- background-color: rgba(var(--pui-color-secondary-rgb), 0.2);
431
- border-left: 3px solid var(--pui-color-secondary);
432
- }
433
-
434
- :global(.dark) .nav-item :global(.nav-link.active),
435
- :global(.dark) .nav-item > button.nav-link.active {
436
- background-color: rgba(var(--pui-color-primary-rgb), 0.3);
437
- border-left: 3px solid var(--pui-color-primary);
438
- }
439
-
440
- .nav-label {
441
- flex: 1;
442
- white-space: nowrap;
443
- overflow: hidden;
444
- text-overflow: ellipsis;
445
- }
446
-
447
- .nav-link :global(.chevron) {
448
- transition: transform var(--pui-transition-fast) var(--pui-ease-out);
449
- flex-shrink: 0;
450
- }
451
-
452
- .nav-link :global(.chevron.expanded) {
453
- transform: rotate(90deg);
454
- }
455
-
456
- .sub-nav-list {
457
- list-style: none;
458
- margin: 0;
459
- padding: 0;
460
- background-color: var(--pui-bg-hover);
461
- }
462
-
463
- :global(.dark) .sub-nav-list {
464
- background-color: rgba(0, 0, 0, 0.2);
465
- }
466
-
467
- .sub-nav-item {
468
- margin: 0;
469
- }
470
-
471
- .sub-nav-item :global(.sub-nav-link),
472
- .sub-nav-item > button.sub-nav-link {
473
- display: flex;
474
- align-items: center;
475
- gap: var(--pui-spacing-2);
476
- width: 100%;
477
- padding: var(--pui-spacing-2) var(--pui-spacing-4) var(--pui-spacing-2) var(--pui-spacing-11);
478
- font-size: var(--pui-font-size-sm);
479
- font-weight: var(--pui-font-weight-normal);
480
- color: var(--pui-text-primary);
481
- background: none;
482
- border: none;
483
- text-decoration: none;
484
- cursor: pointer;
485
- transition: background-color var(--pui-transition-fast) var(--pui-ease-out), color var(--pui-transition-fast) var(--pui-ease-out);
486
- text-align: left;
487
- }
488
-
489
- .sub-nav-item :global(.sub-nav-link:hover),
490
- .sub-nav-item > button.sub-nav-link:hover {
491
- background-color: var(--pui-bg-active);
492
- }
493
-
494
- :global(.dark) .sub-nav-item :global(.sub-nav-link:hover),
495
- :global(.dark) .sub-nav-item > button.sub-nav-link:hover {
496
- background-color: var(--pui-bg-active);
497
- }
498
-
499
- .sub-nav-item :global(.sub-nav-link.active),
500
- .sub-nav-item > button.sub-nav-link.active {
501
- font-weight: var(--pui-font-weight-medium);
502
- border-left: 3px solid var(--pui-color-secondary);
503
- }
504
-
505
- :global(.dark) .sub-nav-item :global(.sub-nav-link.active),
506
- :global(.dark) .sub-nav-item > button.sub-nav-link.active {
507
- border-left: 3px solid var(--pui-color-primary);
508
- }
509
-
510
- /* Scrollbar styling */
511
- .drawer-content::-webkit-scrollbar {
512
- width: 6px;
513
- }
514
-
515
- .drawer-content::-webkit-scrollbar-track {
516
- background: transparent;
517
- }
518
-
519
- .drawer-content::-webkit-scrollbar-thumb {
520
- background-color: var(--pui-color-gray-700);
521
- border-radius: var(--pui-radius-sm);
522
- border: none;
523
- }
524
- </style>
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { SidenavItem, SidenavSection } from '../layout/Sidenav.svelte';
4
+ import Icon from '../icon/Icon.svelte';
5
+ import Link from './Link.svelte';
6
+ import ActionIcon from './ActionIcon.svelte';
7
+ import { iconChevronRight, iconX } from '../icon/index.js';
8
+ import { fade, fly } from 'svelte/transition';
9
+ import { slide } from 'svelte/transition';
10
+
11
+ interface Props {
12
+ open?: boolean;
13
+ position?: 'left' | 'right';
14
+ width?: string;
15
+ closeOnClickOutside?: boolean;
16
+ closeOnEscape?: boolean;
17
+ closeForLargeScreens?: boolean;
18
+ largeScreenBreakpoint?: number;
19
+ sections?: SidenavSection[];
20
+ bottomSections?: SidenavSection[];
21
+ title?: string;
22
+ class?: string;
23
+ header?: Snippet;
24
+ footer?: Snippet;
25
+ children?: Snippet;
26
+ onclose?: () => void;
27
+ onItemClick?: (item: SidenavItem) => void;
28
+ }
29
+
30
+ let {
31
+ open = $bindable(false),
32
+ position = 'left',
33
+ width = '320px',
34
+ closeOnClickOutside = true,
35
+ closeOnEscape = true,
36
+ closeForLargeScreens = false,
37
+ largeScreenBreakpoint = 1024,
38
+ sections = [],
39
+ bottomSections = [],
40
+ title = '',
41
+ class: className = '',
42
+ header,
43
+ footer,
44
+ children,
45
+ onclose,
46
+ onItemClick
47
+ }: Props = $props();
48
+
49
+ let expandedSections: Set<string> = $state(new Set());
50
+
51
+ function close() {
52
+ open = false;
53
+ onclose?.();
54
+ }
55
+
56
+ function handleBackdropClick() {
57
+ if (closeOnClickOutside) {
58
+ close();
59
+ }
60
+ }
61
+
62
+ function handleKeydown(e: KeyboardEvent) {
63
+ if (open && closeOnEscape && e.key === 'Escape') {
64
+ close();
65
+ }
66
+ }
67
+
68
+ function handleWindowResize() {
69
+ if (closeForLargeScreens && typeof window !== 'undefined' && window.innerWidth > largeScreenBreakpoint) {
70
+ close();
71
+ }
72
+ }
73
+
74
+ function toggleSection(itemLabel: string) {
75
+ if (expandedSections.has(itemLabel)) {
76
+ expandedSections.delete(itemLabel);
77
+ expandedSections = new Set(expandedSections);
78
+ } else {
79
+ expandedSections.add(itemLabel);
80
+ expandedSections = new Set(expandedSections);
81
+ }
82
+ }
83
+
84
+ function isExpanded(itemLabel: string): boolean {
85
+ return expandedSections.has(itemLabel);
86
+ }
87
+
88
+ function handleItemClick(item: SidenavItem, e: MouseEvent) {
89
+ if (item.items && item.items.length > 0) {
90
+ e.preventDefault();
91
+ toggleSection(item.label);
92
+ } else {
93
+ onItemClick?.(item);
94
+ item.onclick?.();
95
+ }
96
+ }
97
+
98
+ function handleLinkClick(item: SidenavItem) {
99
+ onItemClick?.(item);
100
+ item.onclick?.();
101
+ }
102
+
103
+ // Body scroll lock
104
+ $effect(() => {
105
+ if (typeof document !== 'undefined') {
106
+ const body = document.body;
107
+ if (open) {
108
+ body.style.overflow = 'hidden';
109
+ } else {
110
+ body.style.overflow = '';
111
+ }
112
+ return () => {
113
+ body.style.overflow = '';
114
+ };
115
+ }
116
+ });
117
+
118
+ const hasNavContent = $derived(sections.length > 0 || bottomSections.length > 0);
119
+ const flyDirection = $derived(position === 'left' ? -320 : 320);
120
+ </script>
121
+
122
+ <svelte:window onresize={handleWindowResize} onkeydown={handleKeydown} />
123
+
124
+ {#if open}
125
+ <!-- Backdrop -->
126
+ <div
127
+ class="drawer-backdrop"
128
+ transition:fade={{ duration: 200 }}
129
+ onclick={handleBackdropClick}
130
+ onkeydown={(e) => e.key === 'Enter' && handleBackdropClick()}
131
+ role="button"
132
+ tabindex="-1"
133
+ aria-label="Close drawer"
134
+ ></div>
135
+
136
+ <!-- Drawer -->
137
+ <div
138
+ class="drawer {className}"
139
+ class:drawer-left={position === 'left'}
140
+ class:drawer-right={position === 'right'}
141
+ style="--drawer-width: {width};"
142
+ transition:fly={{ x: flyDirection, duration: 250, opacity: 1 }}
143
+ onclick={(e) => e.stopPropagation()}
144
+ onkeydown={(e) => e.stopPropagation()}
145
+ role="dialog"
146
+ aria-modal="true"
147
+ tabindex="-1"
148
+ >
149
+ <!-- Header (always shown for close button) -->
150
+ <div class="drawer-header">
151
+ {#if header}
152
+ {@render header()}
153
+ {:else if title}
154
+ <h2 class="drawer-title">{title}</h2>
155
+ {/if}
156
+ <ActionIcon
157
+ svg={iconX}
158
+ size="1.25rem"
159
+ variant="secondary-subtle"
160
+ onclick={close}
161
+ class="drawer-close-btn"
162
+ />
163
+ </div>
164
+
165
+ <!-- Main content area -->
166
+ <div class="drawer-content">
167
+ {#if hasNavContent}
168
+ <!-- Navigation sections -->
169
+ <nav class="drawer-nav">
170
+ {#each sections as section}
171
+ {#if section.title}
172
+ <div class="section-title">{section.title}</div>
173
+ {/if}
174
+ <ul class="nav-list">
175
+ {#each section.items as item}
176
+ <li class="nav-item">
177
+ {#if item.href && !(item.items && item.items.length > 0)}
178
+ <Link
179
+ href={item.href}
180
+ class="nav-link {item.active ? 'active' : ''}"
181
+ onclick={() => handleLinkClick(item)}
182
+ >
183
+ {#if item.iconSvg}
184
+ <Icon svg={item.iconSvg} size="1.25em" />
185
+ {/if}
186
+ <span class="nav-label">{item.label}</span>
187
+ </Link>
188
+ {:else}
189
+ <button
190
+ type="button"
191
+ class="nav-link"
192
+ class:active={item.active}
193
+ class:has-children={item.items && item.items.length > 0}
194
+ onclick={(e) => handleItemClick(item, e)}
195
+ >
196
+ {#if item.iconSvg}
197
+ <Icon svg={item.iconSvg} size="1.25em" />
198
+ {/if}
199
+ <span class="nav-label">{item.label}</span>
200
+ {#if item.items && item.items.length > 0}
201
+ <Icon
202
+ svg={iconChevronRight}
203
+ size="0.75em"
204
+ class="chevron {isExpanded(item.label) ? 'expanded' : ''}"
205
+ />
206
+ {/if}
207
+ </button>
208
+ {#if item.items && item.items.length > 0 && isExpanded(item.label)}
209
+ <ul class="sub-nav-list" transition:slide={{ duration: 200 }}>
210
+ {#each item.items as subItem}
211
+ <li class="sub-nav-item">
212
+ {#if subItem.href}
213
+ <Link
214
+ href={subItem.href}
215
+ class="sub-nav-link {subItem.active ? 'active' : ''}"
216
+ onclick={() => handleLinkClick(subItem)}
217
+ >
218
+ {#if subItem.iconSvg}
219
+ <Icon svg={subItem.iconSvg} size="1em" />
220
+ {/if}
221
+ <span>{subItem.label}</span>
222
+ </Link>
223
+ {:else}
224
+ <button
225
+ type="button"
226
+ class="sub-nav-link"
227
+ class:active={subItem.active}
228
+ onclick={() => handleLinkClick(subItem)}
229
+ >
230
+ {#if subItem.iconSvg}
231
+ <Icon svg={subItem.iconSvg} size="1em" />
232
+ {/if}
233
+ <span>{subItem.label}</span>
234
+ </button>
235
+ {/if}
236
+ </li>
237
+ {/each}
238
+ </ul>
239
+ {/if}
240
+ {/if}
241
+ </li>
242
+ {/each}
243
+ </ul>
244
+ {/each}
245
+ </nav>
246
+ {:else if children}
247
+ {@render children()}
248
+ {/if}
249
+ </div>
250
+
251
+ <!-- Bottom sections -->
252
+ {#if bottomSections.length > 0}
253
+ <nav class="drawer-bottom">
254
+ {#each bottomSections as section}
255
+ {#if section.title}
256
+ <div class="section-title">{section.title}</div>
257
+ {/if}
258
+ <ul class="nav-list">
259
+ {#each section.items as item}
260
+ <li class="nav-item">
261
+ {#if item.href}
262
+ <Link
263
+ href={item.href}
264
+ class="nav-link {item.active ? 'active' : ''}"
265
+ onclick={() => handleLinkClick(item)}
266
+ >
267
+ {#if item.iconSvg}
268
+ <Icon svg={item.iconSvg} size="1.25em" />
269
+ {/if}
270
+ <span class="nav-label">{item.label}</span>
271
+ </Link>
272
+ {:else}
273
+ <button
274
+ type="button"
275
+ class="nav-link"
276
+ class:active={item.active}
277
+ onclick={() => handleLinkClick(item)}
278
+ >
279
+ {#if item.iconSvg}
280
+ <Icon svg={item.iconSvg} size="1.25em" />
281
+ {/if}
282
+ <span class="nav-label">{item.label}</span>
283
+ </button>
284
+ {/if}
285
+ </li>
286
+ {/each}
287
+ </ul>
288
+ {/each}
289
+ </nav>
290
+ {/if}
291
+
292
+ <!-- Footer -->
293
+ {#if footer}
294
+ <div class="drawer-footer">
295
+ {@render footer()}
296
+ </div>
297
+ {/if}
298
+ </div>
299
+ {/if}
300
+
301
+ <style>
302
+ .drawer-backdrop {
303
+ position: fixed;
304
+ inset: 0;
305
+ background-color: var(--pui-bg-overlay);
306
+ z-index: var(--pui-z-modal-backdrop);
307
+ }
308
+
309
+ .drawer {
310
+ position: fixed;
311
+ top: 0;
312
+ bottom: 0;
313
+ display: flex;
314
+ flex-direction: column;
315
+ width: var(--drawer-width);
316
+ max-width: 100vw;
317
+ background-color: var(--pui-paper-body-bg);
318
+ border-right: 1px solid var(--pui-border-default);
319
+ z-index: var(--pui-z-modal);
320
+ overflow: hidden;
321
+ }
322
+
323
+ .drawer-left {
324
+ left: 0;
325
+ border-right: 1px solid var(--pui-border-default);
326
+ border-left: none;
327
+ }
328
+
329
+ .drawer-right {
330
+ right: 0;
331
+ border-left: 1px solid var(--pui-border-default);
332
+ border-right: none;
333
+ }
334
+
335
+ .drawer-header {
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: space-between;
339
+ gap: var(--pui-spacing-4);
340
+ padding: var(--pui-spacing-3) var(--pui-spacing-4);
341
+ border-bottom: 1px solid var(--pui-border-default);
342
+ flex-shrink: 0;
343
+ min-height: 46px;
344
+ }
345
+
346
+ .drawer-title {
347
+ margin: 0;
348
+ font-size: var(--pui-font-size-lg);
349
+ font-weight: var(--pui-font-weight-semibold);
350
+ color: var(--pui-text-primary);
351
+ flex: 1;
352
+ }
353
+
354
+ .drawer-header :global(.drawer-close-btn) {
355
+ flex-shrink: 0;
356
+ margin-left: auto;
357
+ }
358
+
359
+ .drawer-content {
360
+ flex: 1;
361
+ overflow-y: auto;
362
+ overflow-x: hidden;
363
+ }
364
+
365
+ .drawer-nav {
366
+ padding: var(--pui-spacing-2) 0;
367
+ }
368
+
369
+ .drawer-bottom {
370
+ flex-shrink: 0;
371
+ border-top: 1px solid var(--pui-border-default);
372
+ padding: var(--pui-spacing-2) 0;
373
+ }
374
+
375
+ .drawer-footer {
376
+ padding: var(--pui-spacing-4);
377
+ border-top: 1px solid var(--pui-border-default);
378
+ flex-shrink: 0;
379
+ }
380
+
381
+ .section-title {
382
+ padding: var(--pui-spacing-3) var(--pui-spacing-4) var(--pui-spacing-2);
383
+ font-size: var(--pui-font-size-xs);
384
+ font-weight: var(--pui-font-weight-semibold);
385
+ text-transform: uppercase;
386
+ letter-spacing: var(--pui-letter-spacing-wide);
387
+ color: var(--pui-text-muted);
388
+ }
389
+
390
+ .nav-list {
391
+ list-style: none;
392
+ margin: 0;
393
+ padding: 0;
394
+ }
395
+
396
+ .nav-item {
397
+ margin: 0;
398
+ }
399
+
400
+ .nav-item :global(.nav-link),
401
+ .nav-item > button.nav-link {
402
+ display: flex;
403
+ align-items: center;
404
+ gap: var(--pui-spacing-3);
405
+ width: 100%;
406
+ padding: var(--pui-spacing-2_5) var(--pui-spacing-4);
407
+ font-size: var(--pui-font-size-base);
408
+ font-weight: var(--pui-font-weight-medium);
409
+ color: var(--pui-text-primary);
410
+ background: none;
411
+ border: none;
412
+ text-decoration: none;
413
+ cursor: pointer;
414
+ transition: background-color var(--pui-transition-fast) var(--pui-ease-out), color var(--pui-transition-fast) var(--pui-ease-out);
415
+ text-align: left;
416
+ }
417
+
418
+ .nav-item :global(.nav-link:hover),
419
+ .nav-item > button.nav-link:hover {
420
+ background-color: var(--pui-bg-hover);
421
+ }
422
+
423
+ :global(.dark) .nav-item :global(.nav-link:hover),
424
+ :global(.dark) .nav-item > button.nav-link:hover {
425
+ background-color: var(--pui-bg-hover);
426
+ }
427
+
428
+ .nav-item :global(.nav-link.active),
429
+ .nav-item > button.nav-link.active {
430
+ background-color: rgba(var(--pui-color-secondary-rgb), 0.2);
431
+ border-left: 3px solid var(--pui-color-secondary);
432
+ }
433
+
434
+ :global(.dark) .nav-item :global(.nav-link.active),
435
+ :global(.dark) .nav-item > button.nav-link.active {
436
+ background-color: rgba(var(--pui-color-primary-rgb), 0.3);
437
+ border-left: 3px solid var(--pui-color-primary);
438
+ }
439
+
440
+ .nav-label {
441
+ flex: 1;
442
+ white-space: nowrap;
443
+ overflow: hidden;
444
+ text-overflow: ellipsis;
445
+ }
446
+
447
+ .nav-link :global(.chevron) {
448
+ transition: transform var(--pui-transition-fast) var(--pui-ease-out);
449
+ flex-shrink: 0;
450
+ }
451
+
452
+ .nav-link :global(.chevron.expanded) {
453
+ transform: rotate(90deg);
454
+ }
455
+
456
+ .sub-nav-list {
457
+ list-style: none;
458
+ margin: 0;
459
+ padding: 0;
460
+ background-color: var(--pui-bg-hover);
461
+ }
462
+
463
+ :global(.dark) .sub-nav-list {
464
+ background-color: rgba(0, 0, 0, 0.2);
465
+ }
466
+
467
+ .sub-nav-item {
468
+ margin: 0;
469
+ }
470
+
471
+ .sub-nav-item :global(.sub-nav-link),
472
+ .sub-nav-item > button.sub-nav-link {
473
+ display: flex;
474
+ align-items: center;
475
+ gap: var(--pui-spacing-2);
476
+ width: 100%;
477
+ padding: var(--pui-spacing-2) var(--pui-spacing-4) var(--pui-spacing-2) var(--pui-spacing-11);
478
+ font-size: var(--pui-font-size-sm);
479
+ font-weight: var(--pui-font-weight-normal);
480
+ color: var(--pui-text-primary);
481
+ background: none;
482
+ border: none;
483
+ text-decoration: none;
484
+ cursor: pointer;
485
+ transition: background-color var(--pui-transition-fast) var(--pui-ease-out), color var(--pui-transition-fast) var(--pui-ease-out);
486
+ text-align: left;
487
+ }
488
+
489
+ .sub-nav-item :global(.sub-nav-link:hover),
490
+ .sub-nav-item > button.sub-nav-link:hover {
491
+ background-color: var(--pui-bg-active);
492
+ }
493
+
494
+ :global(.dark) .sub-nav-item :global(.sub-nav-link:hover),
495
+ :global(.dark) .sub-nav-item > button.sub-nav-link:hover {
496
+ background-color: var(--pui-bg-active);
497
+ }
498
+
499
+ .sub-nav-item :global(.sub-nav-link.active),
500
+ .sub-nav-item > button.sub-nav-link.active {
501
+ font-weight: var(--pui-font-weight-medium);
502
+ border-left: 3px solid var(--pui-color-secondary);
503
+ }
504
+
505
+ :global(.dark) .sub-nav-item :global(.sub-nav-link.active),
506
+ :global(.dark) .sub-nav-item > button.sub-nav-link.active {
507
+ border-left: 3px solid var(--pui-color-primary);
508
+ }
509
+
510
+ /* Scrollbar styling */
511
+ .drawer-content::-webkit-scrollbar {
512
+ width: 6px;
513
+ }
514
+
515
+ .drawer-content::-webkit-scrollbar-track {
516
+ background: transparent;
517
+ }
518
+
519
+ .drawer-content::-webkit-scrollbar-thumb {
520
+ background-color: var(--pui-color-gray-700);
521
+ border-radius: var(--pui-radius-sm);
522
+ border: none;
523
+ }
524
+ </style>