@miozu/jera 0.0.2 → 0.4.2

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 (82) hide show
  1. package/CLAUDE.md +734 -0
  2. package/README.md +219 -1
  3. package/llms.txt +97 -0
  4. package/package.json +54 -14
  5. package/src/actions/index.js +375 -0
  6. package/src/components/docs/CodeBlock.svelte +203 -0
  7. package/src/components/docs/DocSection.svelte +120 -0
  8. package/src/components/docs/PropsTable.svelte +136 -0
  9. package/src/components/docs/SplitPane.svelte +98 -0
  10. package/src/components/docs/index.js +14 -0
  11. package/src/components/feedback/Alert.svelte +234 -0
  12. package/src/components/feedback/EmptyState.svelte +179 -0
  13. package/src/components/feedback/ProgressBar.svelte +116 -0
  14. package/src/components/feedback/Skeleton.svelte +107 -0
  15. package/src/components/feedback/Spinner.svelte +77 -0
  16. package/src/components/feedback/Toast.svelte +261 -0
  17. package/src/components/forms/Checkbox.svelte +147 -0
  18. package/src/components/forms/Dropzone.svelte +248 -0
  19. package/src/components/forms/FileUpload.svelte +266 -0
  20. package/src/components/forms/IconInput.svelte +184 -0
  21. package/src/components/forms/Input.svelte +121 -0
  22. package/src/components/forms/NumberInput.svelte +225 -0
  23. package/src/components/forms/PinInput.svelte +169 -0
  24. package/src/components/forms/Radio.svelte +143 -0
  25. package/src/components/forms/RadioGroup.svelte +62 -0
  26. package/src/components/forms/RangeSlider.svelte +212 -0
  27. package/src/components/forms/SearchInput.svelte +175 -0
  28. package/src/components/forms/Select.svelte +324 -0
  29. package/src/components/forms/Switch.svelte +159 -0
  30. package/src/components/forms/Textarea.svelte +122 -0
  31. package/src/components/navigation/Accordion.svelte +65 -0
  32. package/src/components/navigation/AccordionItem.svelte +146 -0
  33. package/src/components/navigation/NavigationContainer.svelte +344 -0
  34. package/src/components/navigation/Sidebar.svelte +334 -0
  35. package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
  36. package/src/components/navigation/SidebarAccountItem.svelte +492 -0
  37. package/src/components/navigation/SidebarGroup.svelte +230 -0
  38. package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
  39. package/src/components/navigation/SidebarItem.svelte +210 -0
  40. package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
  41. package/src/components/navigation/SidebarPopover.svelte +145 -0
  42. package/src/components/navigation/SidebarSearch.svelte +236 -0
  43. package/src/components/navigation/SidebarSection.svelte +158 -0
  44. package/src/components/navigation/SidebarToggle.svelte +86 -0
  45. package/src/components/navigation/Tabs.svelte +239 -0
  46. package/src/components/navigation/WorkspaceMenu.svelte +416 -0
  47. package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
  48. package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
  49. package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
  50. package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
  51. package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
  52. package/src/components/navigation/index.js +22 -0
  53. package/src/components/overlays/ConfirmDialog.svelte +272 -0
  54. package/src/components/overlays/Dropdown.svelte +153 -0
  55. package/src/components/overlays/DropdownDivider.svelte +23 -0
  56. package/src/components/overlays/DropdownItem.svelte +97 -0
  57. package/src/components/overlays/Modal.svelte +232 -0
  58. package/src/components/overlays/Popover.svelte +206 -0
  59. package/src/components/primitives/Avatar.svelte +132 -0
  60. package/src/components/primitives/Badge.svelte +118 -0
  61. package/src/components/primitives/Button.svelte +214 -0
  62. package/src/components/primitives/Card.svelte +104 -0
  63. package/src/components/primitives/Divider.svelte +105 -0
  64. package/src/components/primitives/LazyImage.svelte +104 -0
  65. package/src/components/primitives/Link.svelte +122 -0
  66. package/src/components/primitives/Stat.svelte +197 -0
  67. package/src/components/primitives/StatusBadge.svelte +122 -0
  68. package/src/index.js +183 -0
  69. package/src/tokens/colors.css +157 -0
  70. package/src/tokens/effects.css +128 -0
  71. package/src/tokens/index.css +81 -0
  72. package/src/tokens/spacing.css +49 -0
  73. package/src/tokens/typography.css +79 -0
  74. package/src/utils/cn.svelte.js +175 -0
  75. package/src/utils/highlighter.js +124 -0
  76. package/src/utils/index.js +22 -0
  77. package/src/utils/navigation.svelte.js +423 -0
  78. package/src/utils/reactive.svelte.js +328 -0
  79. package/src/utils/sidebar.svelte.js +211 -0
  80. package/jera.js +0 -135
  81. package/www/components/jera/Input/Input.svelte +0 -63
  82. package/www/components/jera/Input/index.js +0 -1
@@ -0,0 +1,470 @@
1
+ <!--
2
+ @component SidebarNavigationItem
3
+
4
+ Enterprise-grade recursive navigation item with unlimited depth nesting.
5
+ Supports sophisticated navigation hierarchies with intelligent expansion,
6
+ search highlighting, and permission-based visibility.
7
+
8
+ @example Basic item
9
+ <SidebarNavigationItem
10
+ item={{ id: 'home', label: 'Home', href: '/', icon: HomeIcon }}
11
+ navigationState={navState}
12
+ />
13
+
14
+ @example Recursive with children
15
+ <SidebarNavigationItem
16
+ item={{
17
+ id: 'settings',
18
+ label: 'Settings',
19
+ icon: SettingsIcon,
20
+ children: [
21
+ { id: 'profile', label: 'Profile', href: '/settings/profile' },
22
+ { id: 'security', label: 'Security', href: '/settings/security' }
23
+ ]
24
+ }}
25
+ navigationState={navState}
26
+ />
27
+
28
+ @example With permissions
29
+ <SidebarNavigationItem
30
+ item={{
31
+ id: 'admin',
32
+ label: 'Admin',
33
+ permissions: ['admin.view'],
34
+ children: [...adminItems]
35
+ }}
36
+ navigationState={navState}
37
+ />
38
+ -->
39
+ <script>
40
+ import { getContext } from 'svelte';
41
+ import { fly, slide } from 'svelte/transition';
42
+ import { cubicOut } from 'svelte/easing';
43
+ import { SIDEBAR_CONTEXT_KEY } from '../../utils/sidebar.svelte.js';
44
+ import { NAVIGATION_CONTEXT_KEY } from '../../utils/navigation.svelte.js';
45
+
46
+ let {
47
+ item,
48
+ navigationState = null,
49
+ depth = 0,
50
+ maxDepth = 10,
51
+ onNavigate = null,
52
+ onToggle = null,
53
+ renderCustomIcon = null,
54
+ renderCustomBadge = null,
55
+ class: className = ''
56
+ } = $props();
57
+
58
+ // Get sidebar context for collapse state
59
+ const sidebar = getContext(SIDEBAR_CONTEXT_KEY);
60
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
61
+
62
+ // Get navigation state from context if not provided
63
+ const contextNavState = getContext(NAVIGATION_CONTEXT_KEY);
64
+ const navState = navigationState || contextNavState;
65
+
66
+ // Computed properties
67
+ const hasChildren = $derived((item.children?.length || 0) > 0);
68
+ const isExpanded = $derived(navState?.isItemExpanded(item.id) ?? false);
69
+ const isActive = $derived(navState?.isActive(item) ?? false);
70
+ const isInActivePath = $derived(navState?.isInActivePath(item) ?? false);
71
+ const isVisible = $derived(navState?.isVisible(item) ?? true);
72
+ const canExpand = $derived(hasChildren && depth < maxDepth);
73
+
74
+ // Check if item matches search
75
+ const matchesSearch = $derived(item._matchesSearch ?? false);
76
+
77
+ // Dynamic styling based on depth and state
78
+ const itemStyles = $derived.by(() => {
79
+ const styles = {
80
+ '--nav-item-depth': depth,
81
+ '--nav-item-indent': `${depth * 1}rem`,
82
+ '--nav-item-opacity': isVisible ? 1 : 0.5
83
+ };
84
+
85
+ return Object.entries(styles)
86
+ .map(([key, value]) => `${key}: ${value}`)
87
+ .join('; ');
88
+ });
89
+
90
+ // Dynamic CSS classes
91
+ const itemClass = $derived([
92
+ 'nav-item',
93
+ 'nav-item-recursive',
94
+ `depth-${depth}`,
95
+ isActive && 'active',
96
+ isInActivePath && 'in-active-path',
97
+ hasChildren && 'has-children',
98
+ isExpanded && 'expanded',
99
+ matchesSearch && 'matches-search',
100
+ !isVisible && 'hidden',
101
+ isCollapsed && 'collapsed',
102
+ className
103
+ ].filter(Boolean).join(' '));
104
+
105
+ // Handle click events
106
+ function handleClick(event) {
107
+ if (!isVisible) return;
108
+
109
+ // If has children and clicked on expand area, toggle
110
+ if (hasChildren && canExpand) {
111
+ event.preventDefault();
112
+ toggleExpansion();
113
+ return;
114
+ }
115
+
116
+ // Otherwise navigate
117
+ if (item.href) {
118
+ if (onNavigate) {
119
+ const shouldProceed = onNavigate(item, event);
120
+ if (shouldProceed === false) return;
121
+ }
122
+
123
+ // Set as active in navigation state
124
+ if (navState) {
125
+ navState.setActive(item);
126
+ }
127
+
128
+ // Close mobile sidebar if applicable
129
+ if (sidebar?.closeMobileOverlay) {
130
+ sidebar.closeMobileOverlay();
131
+ }
132
+ } else if (hasChildren) {
133
+ // If no href but has children, toggle expansion
134
+ toggleExpansion();
135
+ }
136
+ }
137
+
138
+ function toggleExpansion() {
139
+ if (!hasChildren || !canExpand) return;
140
+
141
+ if (navState) {
142
+ navState.toggleItem(item.id);
143
+ }
144
+
145
+ if (onToggle) {
146
+ onToggle(item, !isExpanded);
147
+ }
148
+ }
149
+
150
+ // Handle keyboard navigation
151
+ function handleKeydown(event) {
152
+ if (!isVisible) return;
153
+
154
+ switch (event.key) {
155
+ case 'Enter':
156
+ case ' ':
157
+ event.preventDefault();
158
+ handleClick(event);
159
+ break;
160
+ case 'ArrowRight':
161
+ if (hasChildren && !isExpanded && canExpand) {
162
+ event.preventDefault();
163
+ toggleExpansion();
164
+ }
165
+ break;
166
+ case 'ArrowLeft':
167
+ if (hasChildren && isExpanded) {
168
+ event.preventDefault();
169
+ toggleExpansion();
170
+ }
171
+ break;
172
+ }
173
+ }
174
+
175
+ // Icon component resolution
176
+ const IconComponent = $derived(item.icon);
177
+
178
+ // Badge rendering
179
+ const badgeValue = $derived(typeof item.badge === 'function' ? item.badge() : item.badge);
180
+ </script>
181
+
182
+ {#if isVisible}
183
+ <li class="nav-item-container" style={itemStyles}>
184
+ <!-- Main navigation item -->
185
+ <div
186
+ class={itemClass}
187
+ role="button"
188
+ tabindex="0"
189
+ onclick={handleClick}
190
+ onkeydown={handleKeydown}
191
+ aria-expanded={hasChildren ? isExpanded : undefined}
192
+ aria-current={isActive ? 'page' : undefined}
193
+ title={isCollapsed ? item.label : item.description || null}
194
+ >
195
+ <!-- Expand/collapse indicator -->
196
+ {#if hasChildren && canExpand && !isCollapsed}
197
+ <button
198
+ class="expand-button"
199
+ onclick={(e) => { e.stopPropagation(); toggleExpansion(); }}
200
+ aria-label={isExpanded ? 'Collapse' : 'Expand'}
201
+ transition:fly={{ x: -10, duration: 200, delay: depth * 25 }}
202
+ >
203
+ <svg
204
+ class="expand-icon {isExpanded ? 'expanded' : 'collapsed'}"
205
+ xmlns="http://www.w3.org/2000/svg"
206
+ width="var(--nav-expand-icon-size, 14)"
207
+ height="var(--nav-expand-icon-size, 14)"
208
+ viewBox="0 0 24 24"
209
+ fill="none"
210
+ stroke="currentColor"
211
+ stroke-width="2"
212
+ stroke-linecap="round"
213
+ stroke-linejoin="round"
214
+ >
215
+ <polyline points="6 9 12 15 18 9"></polyline>
216
+ </svg>
217
+ </button>
218
+ {/if}
219
+
220
+ <!-- Icon -->
221
+ {#if IconComponent}
222
+ <div class="nav-icon-container">
223
+ {#if renderCustomIcon}
224
+ {@render renderCustomIcon(item, { isActive, isExpanded, depth })}
225
+ {:else}
226
+ <IconComponent size="var(--nav-icon-size, 18)" class="nav-icon" />
227
+ {/if}
228
+ </div>
229
+ {/if}
230
+
231
+ <!-- Label -->
232
+ {#if !isCollapsed}
233
+ <span
234
+ class="nav-label"
235
+ transition:fly={{ x: -20, duration: 200, delay: 50 + (depth * 25) }}
236
+ >
237
+ {item.label}
238
+ </span>
239
+ {/if}
240
+
241
+ <!-- Badge -->
242
+ {#if badgeValue && !isCollapsed}
243
+ <div class="nav-badge-container">
244
+ {#if renderCustomBadge}
245
+ {@render renderCustomBadge(item, badgeValue, { isActive, isExpanded, depth })}
246
+ {:else}
247
+ <span
248
+ class="nav-badge"
249
+ transition:fly={{ x: -15, duration: 200, delay: 75 + (depth * 25) }}
250
+ >
251
+ {badgeValue}
252
+ </span>
253
+ {/if}
254
+ </div>
255
+ {/if}
256
+
257
+ <!-- Search match highlight -->
258
+ {#if matchesSearch}
259
+ <div class="search-match-indicator" title="Matches search"></div>
260
+ {/if}
261
+ </div>
262
+
263
+ <!-- Recursive children -->
264
+ {#if hasChildren && isExpanded && canExpand}
265
+ <ul
266
+ class="nav-children"
267
+ transition:slide={{ duration: 200, easing: cubicOut }}
268
+ >
269
+ {#each item.children as child}
270
+ <svelte:self
271
+ item={child}
272
+ {navigationState}
273
+ depth={depth + 1}
274
+ {maxDepth}
275
+ {onNavigate}
276
+ {onToggle}
277
+ {renderCustomIcon}
278
+ {renderCustomBadge}
279
+ />
280
+ {/each}
281
+ </ul>
282
+ {/if}
283
+ </li>
284
+ {/if}
285
+
286
+ <style>
287
+ .nav-item-container {
288
+ list-style: none;
289
+ margin: 0;
290
+ padding: 0;
291
+ }
292
+
293
+ .nav-item-recursive {
294
+ width: 100%;
295
+ display: flex;
296
+ align-items: center;
297
+ gap: var(--nav-item-gap, 0.5rem);
298
+ padding: var(--nav-item-padding, 0.375rem 0.75rem);
299
+ margin: var(--nav-item-margin, 0 0.5rem 0.125rem 0.5rem);
300
+ padding-left: calc(var(--nav-item-padding-left, 0.75rem) + var(--nav-item-indent, 0px));
301
+ font-size: var(--nav-item-font-size, 0.875rem);
302
+ color: var(--nav-item-color, var(--color-text, var(--color-base06, #F3F4F7)));
303
+ background: var(--nav-item-background, transparent);
304
+ border: var(--nav-item-border, none);
305
+ border-radius: var(--nav-item-border-radius, 0.375rem);
306
+ cursor: pointer;
307
+ transition: all var(--nav-transition-duration, 200ms) var(--nav-transition-easing, ease);
308
+ position: relative;
309
+ text-decoration: none;
310
+ font-family: inherit;
311
+ text-align: left;
312
+ opacity: var(--nav-item-opacity, 1);
313
+ }
314
+
315
+ .nav-item-recursive:hover {
316
+ color: var(--nav-item-hover-color, var(--color-primary, var(--color-base0D, #83D2FC)));
317
+ background: var(--nav-item-hover-background, color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) var(--nav-hover-opacity, 10%), transparent));
318
+ }
319
+
320
+ .nav-item-recursive.active {
321
+ color: var(--nav-item-active-color, var(--color-primary, var(--color-base0D, #83D2FC)));
322
+ background: var(--nav-item-active-background, color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) var(--nav-active-opacity, 15%), transparent));
323
+ font-weight: var(--nav-item-active-weight, 500);
324
+ }
325
+
326
+ .nav-item-recursive.in-active-path {
327
+ color: var(--nav-item-path-color, var(--color-primary, var(--color-base0D, #83D2FC)));
328
+ opacity: var(--nav-item-path-opacity, 0.8);
329
+ }
330
+
331
+ .nav-item-recursive.matches-search {
332
+ background: var(--nav-search-highlight-bg, color-mix(in srgb, var(--color-warning, var(--color-base0A, #E8D176)) 10%, transparent));
333
+ border: var(--nav-search-highlight-border, 1px solid color-mix(in srgb, var(--color-warning, var(--color-base0A, #E8D176)) 30%, transparent));
334
+ }
335
+
336
+ .nav-item-recursive.collapsed {
337
+ justify-content: center;
338
+ padding: var(--nav-item-collapsed-padding, 0.5rem);
339
+ margin: var(--nav-item-collapsed-margin, 0 0.25rem 0.125rem 0.25rem);
340
+ width: calc(100% - var(--nav-item-collapsed-margin-offset, 0.5rem));
341
+ }
342
+
343
+ .nav-item-recursive.hidden {
344
+ display: none;
345
+ }
346
+
347
+ /* Depth-based styling */
348
+ .nav-item-recursive.depth-0 {
349
+ font-weight: var(--nav-depth-0-weight, 500);
350
+ }
351
+
352
+ .nav-item-recursive.depth-1 {
353
+ font-size: var(--nav-depth-1-size, 0.8125rem);
354
+ }
355
+
356
+ .nav-item-recursive.depth-2 {
357
+ font-size: var(--nav-depth-2-size, 0.75rem);
358
+ opacity: var(--nav-depth-2-opacity, 0.9);
359
+ }
360
+
361
+ /* Expand button */
362
+ .expand-button {
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ width: var(--nav-expand-button-size, 1.25rem);
367
+ height: var(--nav-expand-button-size, 1.25rem);
368
+ padding: 0;
369
+ border: none;
370
+ background: transparent;
371
+ color: var(--nav-expand-button-color, var(--color-text-muted, var(--color-base05, #D0D2DB)));
372
+ cursor: pointer;
373
+ border-radius: var(--nav-expand-button-radius, 0.25rem);
374
+ transition: all var(--nav-transition-duration, 200ms) var(--nav-transition-easing, ease);
375
+ flex-shrink: 0;
376
+ }
377
+
378
+ .expand-button:hover {
379
+ background: var(--nav-expand-button-hover-bg, color-mix(in srgb, var(--color-text-muted, var(--color-base05, #D0D2DB)) 10%, transparent));
380
+ color: var(--nav-expand-button-hover-color, var(--color-text, var(--color-base06, #F3F4F7)));
381
+ }
382
+
383
+ .expand-icon {
384
+ transition: transform var(--nav-transition-duration, 200ms) var(--nav-transition-easing, ease);
385
+ }
386
+
387
+ .expand-icon.expanded {
388
+ transform: rotate(180deg);
389
+ }
390
+
391
+ /* Icon container */
392
+ .nav-icon-container {
393
+ display: flex;
394
+ align-items: center;
395
+ justify-content: center;
396
+ width: var(--nav-icon-container-size, 1.5rem);
397
+ height: var(--nav-icon-container-size, 1.5rem);
398
+ flex-shrink: 0;
399
+ }
400
+
401
+ .nav-icon {
402
+ transition: color var(--nav-transition-duration, 200ms) var(--nav-transition-easing, ease);
403
+ }
404
+
405
+ /* Label */
406
+ .nav-label {
407
+ flex: 1;
408
+ white-space: nowrap;
409
+ overflow: hidden;
410
+ text-overflow: ellipsis;
411
+ font-weight: var(--nav-label-weight, 400);
412
+ min-width: 0;
413
+ }
414
+
415
+ /* Badge container */
416
+ .nav-badge-container {
417
+ display: flex;
418
+ align-items: center;
419
+ flex-shrink: 0;
420
+ margin-left: auto;
421
+ }
422
+
423
+ .nav-badge {
424
+ display: inline-flex;
425
+ align-items: center;
426
+ justify-content: center;
427
+ padding: var(--nav-badge-padding, 0.125rem 0.375rem);
428
+ font-size: var(--nav-badge-font-size, 0.625rem);
429
+ font-weight: var(--nav-badge-weight, 600);
430
+ color: var(--nav-badge-color, var(--color-primary, var(--color-base0D, #83D2FC)));
431
+ background: var(--nav-badge-background, color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) var(--nav-badge-opacity, 10%), transparent));
432
+ border-radius: var(--nav-badge-radius, 9999px);
433
+ min-width: var(--nav-badge-min-width, 1rem);
434
+ height: var(--nav-badge-height, 1rem);
435
+ }
436
+
437
+ /* Search match indicator */
438
+ .search-match-indicator {
439
+ position: absolute;
440
+ right: var(--nav-search-indicator-offset, 0.25rem);
441
+ top: 50%;
442
+ transform: translateY(-50%);
443
+ width: var(--nav-search-indicator-size, 0.375rem);
444
+ height: var(--nav-search-indicator-size, 0.375rem);
445
+ background: var(--nav-search-indicator-color, var(--color-warning, var(--color-base0A, #E8D176)));
446
+ border-radius: 50%;
447
+ flex-shrink: 0;
448
+ }
449
+
450
+ /* Children container */
451
+ .nav-children {
452
+ list-style: none;
453
+ margin: 0;
454
+ padding: 0;
455
+ border-left: var(--nav-children-border, 1px solid color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent));
456
+ margin-left: var(--nav-children-margin-left, calc(var(--nav-icon-container-size, 1.5rem) / 2));
457
+ padding-left: var(--nav-children-padding-left, 0.5rem);
458
+ }
459
+
460
+ /* Responsive adjustments */
461
+ @media (max-width: 768px) {
462
+ .nav-item-recursive {
463
+ padding: var(--nav-item-mobile-padding, 0.5rem 0.75rem);
464
+ }
465
+
466
+ .nav-children {
467
+ margin-left: var(--nav-children-mobile-margin, 0.75rem);
468
+ }
469
+ }
470
+ </style>
@@ -0,0 +1,145 @@
1
+ <!--
2
+ @component SidebarPopover
3
+
4
+ Hover popover for collapsed sidebar state.
5
+ Shows navigation items when hovering over collapsed groups.
6
+
7
+ @example
8
+ <SidebarPopover
9
+ title="Settings"
10
+ items={[
11
+ { href: '/settings/general', label: 'General' },
12
+ { href: '/settings/security', label: 'Security' }
13
+ ]}
14
+ position={{ top: 100, left: 68 }}
15
+ visible={true}
16
+ onmouseenter={keepOpen}
17
+ onmouseleave={hidePopover}
18
+ />
19
+ -->
20
+ <script>
21
+ let {
22
+ title = '',
23
+ items = [],
24
+ position = { top: 0, left: 0 },
25
+ visible = false,
26
+ onmouseenter = null,
27
+ onmouseleave = null,
28
+ class: className = ''
29
+ } = $props();
30
+ </script>
31
+
32
+ {#if visible}
33
+ <div
34
+ class="sidebar-popover {className}"
35
+ style="top: {position.top}px; left: {position.left}px;"
36
+ onmouseenter={onmouseenter}
37
+ onmouseleave={onmouseleave}
38
+ role="menu"
39
+ aria-label={title}
40
+ >
41
+ {#if title}
42
+ <div class="popover-header">
43
+ {title}
44
+ </div>
45
+ {/if}
46
+ <ul class="popover-list">
47
+ {#each items as item}
48
+ <li role="menuitem">
49
+ {#if item.href}
50
+ <a href={item.href} class="popover-item">
51
+ {item.label}
52
+ </a>
53
+ {:else if item.onclick}
54
+ <button type="button" class="popover-item" onclick={item.onclick}>
55
+ {item.label}
56
+ </button>
57
+ {:else}
58
+ <span class="popover-item disabled">
59
+ {item.label}
60
+ </span>
61
+ {/if}
62
+ </li>
63
+ {/each}
64
+ </ul>
65
+ </div>
66
+ {/if}
67
+
68
+ <style>
69
+ .sidebar-popover {
70
+ position: fixed;
71
+ z-index: 50;
72
+ background-color: var(--color-bg, var(--color-base00, #232733));
73
+ border: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent);
74
+ border-radius: 0.5rem;
75
+ box-shadow:
76
+ 0 25px 50px -12px rgba(0, 0, 0, 0.25),
77
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1);
78
+ min-width: 200px;
79
+ max-width: 280px;
80
+ overflow: hidden;
81
+ animation: popover-appear 0.15s ease-out;
82
+ }
83
+
84
+ @keyframes popover-appear {
85
+ from {
86
+ opacity: 0;
87
+ transform: translateX(-8px) scale(0.96);
88
+ }
89
+ to {
90
+ opacity: 1;
91
+ transform: translateX(0) scale(1);
92
+ }
93
+ }
94
+
95
+ .popover-header {
96
+ padding: 0.5rem 1rem;
97
+ font-size: 0.75rem;
98
+ font-weight: 600;
99
+ color: var(--color-text, var(--color-base06, #F3F4F7));
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.05em;
102
+ border-bottom: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 20%, transparent);
103
+ background-color: color-mix(in srgb, var(--color-surface, var(--color-base01, #2C3040)) 50%, transparent);
104
+ }
105
+
106
+ .popover-list {
107
+ padding: 0.25rem 0;
108
+ margin: 0;
109
+ list-style: none;
110
+ }
111
+
112
+ .popover-list li {
113
+ list-style: none;
114
+ }
115
+
116
+ .popover-item {
117
+ display: block;
118
+ width: 100%;
119
+ padding: 0.5rem 1rem;
120
+ font-size: 0.875rem;
121
+ color: var(--color-text, var(--color-base06, #F3F4F7));
122
+ text-decoration: none;
123
+ text-align: left;
124
+ background: transparent;
125
+ border: none;
126
+ cursor: pointer;
127
+ transition: all 150ms ease;
128
+ font-family: inherit;
129
+ }
130
+
131
+ .popover-item:hover {
132
+ background-color: color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) 10%, transparent);
133
+ color: var(--color-primary, var(--color-base0D, #83D2FC));
134
+ }
135
+
136
+ .popover-item:active {
137
+ background-color: color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) 15%, transparent);
138
+ }
139
+
140
+ .popover-item.disabled {
141
+ opacity: 0.5;
142
+ cursor: not-allowed;
143
+ pointer-events: none;
144
+ }
145
+ </style>