@miozu/jera 0.3.0 → 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.
- package/CLAUDE.md +350 -59
- package/README.md +30 -22
- package/llms.txt +37 -4
- package/package.json +12 -2
- package/src/components/docs/CodeBlock.svelte +203 -0
- package/src/components/docs/DocSection.svelte +120 -0
- package/src/components/docs/PropsTable.svelte +136 -0
- package/src/components/docs/SplitPane.svelte +98 -0
- package/src/components/docs/index.js +14 -0
- package/src/components/feedback/Alert.svelte +234 -0
- package/src/components/feedback/EmptyState.svelte +6 -6
- package/src/components/feedback/ProgressBar.svelte +8 -8
- package/src/components/feedback/Skeleton.svelte +4 -4
- package/src/components/feedback/Spinner.svelte +1 -1
- package/src/components/feedback/Toast.svelte +137 -173
- package/src/components/forms/Checkbox.svelte +10 -10
- package/src/components/forms/Dropzone.svelte +14 -14
- package/src/components/forms/FileUpload.svelte +16 -16
- package/src/components/forms/IconInput.svelte +4 -4
- package/src/components/forms/Input.svelte +14 -14
- package/src/components/forms/NumberInput.svelte +13 -13
- package/src/components/forms/PinInput.svelte +8 -8
- package/src/components/forms/Radio.svelte +8 -8
- package/src/components/forms/RangeSlider.svelte +12 -12
- package/src/components/forms/SearchInput.svelte +10 -10
- package/src/components/forms/Select.svelte +156 -158
- package/src/components/forms/Switch.svelte +4 -4
- package/src/components/forms/Textarea.svelte +9 -9
- package/src/components/navigation/Accordion.svelte +1 -1
- package/src/components/navigation/AccordionItem.svelte +6 -6
- package/src/components/navigation/NavigationContainer.svelte +344 -0
- package/src/components/navigation/Sidebar.svelte +334 -0
- package/src/components/navigation/SidebarAccountGroup.svelte +495 -0
- package/src/components/navigation/SidebarAccountItem.svelte +492 -0
- package/src/components/navigation/SidebarGroup.svelte +230 -0
- package/src/components/navigation/SidebarGroupSwitcher.svelte +262 -0
- package/src/components/navigation/SidebarItem.svelte +210 -0
- package/src/components/navigation/SidebarNavigationItem.svelte +470 -0
- package/src/components/navigation/SidebarPopover.svelte +145 -0
- package/src/components/navigation/SidebarSearch.svelte +236 -0
- package/src/components/navigation/SidebarSection.svelte +158 -0
- package/src/components/navigation/SidebarToggle.svelte +86 -0
- package/src/components/navigation/Tabs.svelte +18 -18
- package/src/components/navigation/WorkspaceMenu.svelte +416 -0
- package/src/components/navigation/blocks/NavigationAccountGroup.svelte +396 -0
- package/src/components/navigation/blocks/NavigationCustomBlock.svelte +74 -0
- package/src/components/navigation/blocks/NavigationGroupSwitcher.svelte +277 -0
- package/src/components/navigation/blocks/NavigationSearch.svelte +300 -0
- package/src/components/navigation/blocks/NavigationSection.svelte +230 -0
- package/src/components/navigation/index.js +22 -0
- package/src/components/overlays/ConfirmDialog.svelte +18 -18
- package/src/components/overlays/Dropdown.svelte +2 -2
- package/src/components/overlays/DropdownDivider.svelte +1 -1
- package/src/components/overlays/DropdownItem.svelte +5 -5
- package/src/components/overlays/Modal.svelte +13 -13
- package/src/components/overlays/Popover.svelte +3 -3
- package/src/components/primitives/Avatar.svelte +12 -12
- package/src/components/primitives/Badge.svelte +7 -7
- package/src/components/primitives/Button.svelte +126 -174
- package/src/components/primitives/Card.svelte +15 -15
- package/src/components/primitives/Divider.svelte +3 -3
- package/src/components/primitives/LazyImage.svelte +1 -1
- package/src/components/primitives/Link.svelte +2 -2
- package/src/components/primitives/Stat.svelte +197 -0
- package/src/components/primitives/StatusBadge.svelte +24 -24
- package/src/index.js +62 -7
- package/src/tokens/colors.css +96 -128
- package/src/utils/highlighter.js +124 -0
- package/src/utils/index.js +7 -2
- package/src/utils/navigation.svelte.js +423 -0
- package/src/utils/reactive.svelte.js +126 -37
- package/src/utils/sidebar.svelte.js +211 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NavigationContainer
|
|
3
|
+
|
|
4
|
+
Enterprise-grade navigation container with pluggable block architecture.
|
|
5
|
+
Supports multiple navigation patterns, themes, and enterprise features.
|
|
6
|
+
|
|
7
|
+
@example Basic usage
|
|
8
|
+
<NavigationContainer
|
|
9
|
+
navigationState={navState}
|
|
10
|
+
blocks={[
|
|
11
|
+
{ type: 'search', id: 'main-search' },
|
|
12
|
+
{ type: 'section', id: 'nav', title: 'Navigation', items: navItems },
|
|
13
|
+
{ type: 'account-group', id: 'accounts', accounts: accounts },
|
|
14
|
+
{ type: 'custom', id: 'footer', component: CustomFooter }
|
|
15
|
+
]}
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
@example Enterprise usage
|
|
19
|
+
<NavigationContainer
|
|
20
|
+
navigationState={navState}
|
|
21
|
+
theme="enterprise-dark"
|
|
22
|
+
blocks={complexBlocks}
|
|
23
|
+
plugins={[searchPlugin, analyticsPlugin]}
|
|
24
|
+
permissions={permissionSystem}
|
|
25
|
+
/>
|
|
26
|
+
-->
|
|
27
|
+
<script>
|
|
28
|
+
import { setContext, getContext } from 'svelte';
|
|
29
|
+
import { fade } from 'svelte/transition';
|
|
30
|
+
import { NAVIGATION_CONTEXT_KEY } from '../../utils/navigation.svelte.js';
|
|
31
|
+
import { SIDEBAR_CONTEXT_KEY } from '../../utils/sidebar.svelte.js';
|
|
32
|
+
|
|
33
|
+
// Block components
|
|
34
|
+
import NavigationSearch from './blocks/NavigationSearch.svelte';
|
|
35
|
+
import NavigationSection from './blocks/NavigationSection.svelte';
|
|
36
|
+
import NavigationAccountGroup from './blocks/NavigationAccountGroup.svelte';
|
|
37
|
+
import NavigationGroupSwitcher from './blocks/NavigationGroupSwitcher.svelte';
|
|
38
|
+
import NavigationCustomBlock from './blocks/NavigationCustomBlock.svelte';
|
|
39
|
+
|
|
40
|
+
let {
|
|
41
|
+
navigationState,
|
|
42
|
+
blocks = [],
|
|
43
|
+
theme = 'default',
|
|
44
|
+
plugins = [],
|
|
45
|
+
permissions = null,
|
|
46
|
+
renderCustomBlock = null,
|
|
47
|
+
onBlockEvent = null,
|
|
48
|
+
class: className = ''
|
|
49
|
+
} = $props();
|
|
50
|
+
|
|
51
|
+
// Set navigation context for child components
|
|
52
|
+
setContext(NAVIGATION_CONTEXT_KEY, navigationState);
|
|
53
|
+
|
|
54
|
+
// Get sidebar context if available
|
|
55
|
+
const sidebarContext = getContext(SIDEBAR_CONTEXT_KEY);
|
|
56
|
+
const isCollapsed = $derived(sidebarContext?.collapsed ?? false);
|
|
57
|
+
|
|
58
|
+
// Built-in block type registry
|
|
59
|
+
const blockComponents = {
|
|
60
|
+
'search': NavigationSearch,
|
|
61
|
+
'section': NavigationSection,
|
|
62
|
+
'account-group': NavigationAccountGroup,
|
|
63
|
+
'group-switcher': NavigationGroupSwitcher,
|
|
64
|
+
'custom': NavigationCustomBlock
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Computed theme classes
|
|
68
|
+
const themeClass = $derived(`nav-theme-${theme}`);
|
|
69
|
+
const containerClass = $derived([
|
|
70
|
+
'navigation-container',
|
|
71
|
+
themeClass,
|
|
72
|
+
isCollapsed && 'collapsed',
|
|
73
|
+
className
|
|
74
|
+
].filter(Boolean).join(' '));
|
|
75
|
+
|
|
76
|
+
// Initialize plugins
|
|
77
|
+
$effect(() => {
|
|
78
|
+
plugins.forEach(plugin => {
|
|
79
|
+
if (typeof plugin.init === 'function') {
|
|
80
|
+
plugin.init({ navigationState, blocks, theme, permissions });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
plugins.forEach(plugin => {
|
|
86
|
+
if (typeof plugin.destroy === 'function') {
|
|
87
|
+
plugin.destroy();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Handle block events
|
|
94
|
+
function handleBlockEvent(blockId, eventType, data) {
|
|
95
|
+
// Call plugin event handlers
|
|
96
|
+
plugins.forEach(plugin => {
|
|
97
|
+
if (typeof plugin.onBlockEvent === 'function') {
|
|
98
|
+
plugin.onBlockEvent(blockId, eventType, data);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Call user event handler
|
|
103
|
+
if (onBlockEvent) {
|
|
104
|
+
onBlockEvent(blockId, eventType, data);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Get component for block type
|
|
109
|
+
function getBlockComponent(block) {
|
|
110
|
+
if (block.component) {
|
|
111
|
+
return block.component;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (block.type && blockComponents[block.type]) {
|
|
115
|
+
return blockComponents[block.type];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fallback to custom block
|
|
119
|
+
return NavigationCustomBlock;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if block should be visible
|
|
123
|
+
function isBlockVisible(block) {
|
|
124
|
+
// Check permissions
|
|
125
|
+
if (permissions && block.permissions) {
|
|
126
|
+
const hasPermission = Array.isArray(block.permissions)
|
|
127
|
+
? block.permissions.some(perm => permissions.check(perm))
|
|
128
|
+
: permissions.check(block.permissions);
|
|
129
|
+
|
|
130
|
+
if (!hasPermission) return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check custom visibility function
|
|
134
|
+
if (typeof block.visible === 'function') {
|
|
135
|
+
return block.visible({ navigationState, isCollapsed });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check boolean visibility
|
|
139
|
+
if (typeof block.visible === 'boolean') {
|
|
140
|
+
return block.visible;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Default to visible
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Filter visible blocks
|
|
148
|
+
const visibleBlocks = $derived(blocks.filter(isBlockVisible));
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<nav class={containerClass} data-theme={theme}>
|
|
152
|
+
{#each visibleBlocks as block, index (block.id)}
|
|
153
|
+
<div
|
|
154
|
+
class="nav-block nav-block-{block.type}"
|
|
155
|
+
data-block-id={block.id}
|
|
156
|
+
data-block-type={block.type}
|
|
157
|
+
transition:fade={{ duration: 200, delay: index * 50 }}
|
|
158
|
+
>
|
|
159
|
+
{#if renderCustomBlock && block.type === 'custom'}
|
|
160
|
+
{@render renderCustomBlock(block, { navigationState, isCollapsed, onEvent: (type, data) => handleBlockEvent(block.id, type, data) })}
|
|
161
|
+
{:else}
|
|
162
|
+
{@const BlockComponent = getBlockComponent(block)}
|
|
163
|
+
<BlockComponent
|
|
164
|
+
{block}
|
|
165
|
+
{navigationState}
|
|
166
|
+
onEvent={(type, data) => handleBlockEvent(block.id, type, data)}
|
|
167
|
+
{...(block.props || {})}
|
|
168
|
+
/>
|
|
169
|
+
{/if}
|
|
170
|
+
</div>
|
|
171
|
+
{/each}
|
|
172
|
+
</nav>
|
|
173
|
+
|
|
174
|
+
<style>
|
|
175
|
+
.navigation-container {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: column;
|
|
178
|
+
width: 100%;
|
|
179
|
+
height: 100%;
|
|
180
|
+
overflow-y: auto;
|
|
181
|
+
overflow-x: hidden;
|
|
182
|
+
|
|
183
|
+
/* CSS Custom Properties for theming */
|
|
184
|
+
--nav-container-bg: var(--color-surface, var(--color-base01, #2C3040));
|
|
185
|
+
--nav-container-border: var(--color-border, color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent));
|
|
186
|
+
--nav-container-text: var(--color-text, var(--color-base06, #F3F4F7));
|
|
187
|
+
|
|
188
|
+
/* Spacing */
|
|
189
|
+
--nav-block-spacing: 0.5rem;
|
|
190
|
+
--nav-indent-size: 1rem;
|
|
191
|
+
|
|
192
|
+
/* Item styling */
|
|
193
|
+
--nav-item-padding: 0.375rem 0.75rem;
|
|
194
|
+
--nav-item-margin: 0 0.5rem 0.125rem 0.5rem;
|
|
195
|
+
--nav-item-gap: 0.5rem;
|
|
196
|
+
--nav-item-border-radius: 0.375rem;
|
|
197
|
+
--nav-item-font-size: 0.875rem;
|
|
198
|
+
|
|
199
|
+
/* Colors */
|
|
200
|
+
--nav-item-color: var(--color-text, var(--color-base06, #F3F4F7));
|
|
201
|
+
--nav-item-background: transparent;
|
|
202
|
+
--nav-item-border: none;
|
|
203
|
+
|
|
204
|
+
/* Hover states */
|
|
205
|
+
--nav-hover-opacity: 10%;
|
|
206
|
+
--nav-item-hover-color: var(--color-primary, var(--color-base0D, #83D2FC));
|
|
207
|
+
--nav-item-hover-background: color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) var(--nav-hover-opacity), transparent);
|
|
208
|
+
|
|
209
|
+
/* Active states */
|
|
210
|
+
--nav-active-opacity: 15%;
|
|
211
|
+
--nav-item-active-color: var(--color-primary, var(--color-base0D, #83D2FC));
|
|
212
|
+
--nav-item-active-background: color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) var(--nav-active-opacity), transparent);
|
|
213
|
+
--nav-item-active-weight: 500;
|
|
214
|
+
|
|
215
|
+
/* Icons */
|
|
216
|
+
--nav-icon-size: 18px;
|
|
217
|
+
--nav-icon-container-size: 1.5rem;
|
|
218
|
+
|
|
219
|
+
/* Badges */
|
|
220
|
+
--nav-badge-padding: 0.125rem 0.375rem;
|
|
221
|
+
--nav-badge-font-size: 0.625rem;
|
|
222
|
+
--nav-badge-weight: 600;
|
|
223
|
+
--nav-badge-opacity: 10%;
|
|
224
|
+
--nav-badge-color: var(--color-primary, var(--color-base0D, #83D2FC));
|
|
225
|
+
--nav-badge-background: color-mix(in srgb, var(--color-primary, var(--color-base0D, #83D2FC)) var(--nav-badge-opacity), transparent);
|
|
226
|
+
--nav-badge-radius: 9999px;
|
|
227
|
+
--nav-badge-min-width: 1rem;
|
|
228
|
+
--nav-badge-height: 1rem;
|
|
229
|
+
|
|
230
|
+
/* Expand buttons */
|
|
231
|
+
--nav-expand-icon-size: 14px;
|
|
232
|
+
--nav-expand-button-size: 1.25rem;
|
|
233
|
+
--nav-expand-button-color: var(--color-text-muted, var(--color-base05, #D0D2DB));
|
|
234
|
+
--nav-expand-button-hover-bg: color-mix(in srgb, var(--color-text-muted, var(--color-base05, #D0D2DB)) 10%, transparent);
|
|
235
|
+
--nav-expand-button-hover-color: var(--color-text, var(--color-base06, #F3F4F7));
|
|
236
|
+
--nav-expand-button-radius: 0.25rem;
|
|
237
|
+
|
|
238
|
+
/* Transitions */
|
|
239
|
+
--nav-transition-duration: 200ms;
|
|
240
|
+
--nav-transition-easing: ease;
|
|
241
|
+
|
|
242
|
+
/* Search */
|
|
243
|
+
--nav-search-highlight-bg: color-mix(in srgb, var(--color-warning, var(--color-base0A, #E8D176)) 10%, transparent);
|
|
244
|
+
--nav-search-highlight-border: 1px solid color-mix(in srgb, var(--color-warning, var(--color-base0A, #E8D176)) 30%, transparent);
|
|
245
|
+
--nav-search-indicator-size: 0.375rem;
|
|
246
|
+
--nav-search-indicator-color: var(--color-warning, var(--color-base0A, #E8D176));
|
|
247
|
+
--nav-search-indicator-offset: 0.25rem;
|
|
248
|
+
|
|
249
|
+
/* Children/nesting */
|
|
250
|
+
--nav-children-border: 1px solid color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent);
|
|
251
|
+
--nav-children-margin-left: calc(var(--nav-icon-container-size) / 2);
|
|
252
|
+
--nav-children-padding-left: 0.5rem;
|
|
253
|
+
|
|
254
|
+
/* Collapsed state */
|
|
255
|
+
--nav-item-collapsed-padding: 0.5rem;
|
|
256
|
+
--nav-item-collapsed-margin: 0 0.25rem 0.125rem 0.25rem;
|
|
257
|
+
--nav-item-collapsed-margin-offset: 0.5rem;
|
|
258
|
+
|
|
259
|
+
/* Depth-based styling */
|
|
260
|
+
--nav-depth-0-weight: 500;
|
|
261
|
+
--nav-depth-1-size: 0.8125rem;
|
|
262
|
+
--nav-depth-2-size: 0.75rem;
|
|
263
|
+
--nav-depth-2-opacity: 0.9;
|
|
264
|
+
|
|
265
|
+
/* Mobile responsive */
|
|
266
|
+
--nav-item-mobile-padding: 0.5rem 0.75rem;
|
|
267
|
+
--nav-children-mobile-margin: 0.75rem;
|
|
268
|
+
|
|
269
|
+
background: var(--nav-container-bg);
|
|
270
|
+
color: var(--nav-container-text);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.navigation-container.collapsed {
|
|
274
|
+
--nav-item-padding: var(--nav-item-collapsed-padding);
|
|
275
|
+
--nav-item-margin: var(--nav-item-collapsed-margin);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Theme variants */
|
|
279
|
+
.nav-theme-enterprise-dark {
|
|
280
|
+
--nav-container-bg: #1a1b23;
|
|
281
|
+
--nav-item-color: #e4e4e7;
|
|
282
|
+
--nav-item-hover-color: #3b82f6;
|
|
283
|
+
--nav-item-active-color: #3b82f6;
|
|
284
|
+
--nav-hover-opacity: 8%;
|
|
285
|
+
--nav-active-opacity: 12%;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.nav-theme-enterprise-light {
|
|
289
|
+
--nav-container-bg: #ffffff;
|
|
290
|
+
--nav-item-color: #374151;
|
|
291
|
+
--nav-item-hover-color: #3b82f6;
|
|
292
|
+
--nav-item-active-color: #3b82f6;
|
|
293
|
+
--nav-hover-opacity: 5%;
|
|
294
|
+
--nav-active-opacity: 10%;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.nav-theme-minimal {
|
|
298
|
+
--nav-item-border-radius: 0;
|
|
299
|
+
--nav-item-padding: 0.25rem 0.5rem;
|
|
300
|
+
--nav-hover-opacity: 5%;
|
|
301
|
+
--nav-active-opacity: 8%;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.nav-theme-colorful {
|
|
305
|
+
--nav-item-hover-color: #10b981;
|
|
306
|
+
--nav-item-active-color: #059669;
|
|
307
|
+
--nav-badge-color: #f59e0b;
|
|
308
|
+
--nav-hover-opacity: 12%;
|
|
309
|
+
--nav-active-opacity: 18%;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Block containers */
|
|
313
|
+
.nav-block {
|
|
314
|
+
margin-bottom: var(--nav-block-spacing);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.nav-block:last-child {
|
|
318
|
+
margin-bottom: 0;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Scrollbar styling */
|
|
322
|
+
.navigation-container::-webkit-scrollbar {
|
|
323
|
+
width: 4px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.navigation-container::-webkit-scrollbar-track {
|
|
327
|
+
background: transparent;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.navigation-container::-webkit-scrollbar-thumb {
|
|
331
|
+
background: var(--color-base03, #565E78);
|
|
332
|
+
border-radius: 2px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.navigation-container::-webkit-scrollbar-thumb:hover {
|
|
336
|
+
background: var(--color-base04, #737E99);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Firefox scrollbar */
|
|
340
|
+
.navigation-container {
|
|
341
|
+
scrollbar-width: thin;
|
|
342
|
+
scrollbar-color: var(--color-base03, #565E78) transparent;
|
|
343
|
+
}
|
|
344
|
+
</style>
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Sidebar
|
|
3
|
+
|
|
4
|
+
Flexible, collapsible sidebar container with smooth width transitions.
|
|
5
|
+
Provides context to child components for collapsed state awareness.
|
|
6
|
+
|
|
7
|
+
@example Basic usage (admin style)
|
|
8
|
+
<Sidebar bind:collapsed persistKey="admin-sidebar">
|
|
9
|
+
{#snippet header()}
|
|
10
|
+
<div class="p-4 border-b border-base02">
|
|
11
|
+
<span class="logo">Selify</span>
|
|
12
|
+
</div>
|
|
13
|
+
{/snippet}
|
|
14
|
+
|
|
15
|
+
<SidebarSection title="Navigation">
|
|
16
|
+
<SidebarItem href="/" icon={Home} label="Home" />
|
|
17
|
+
</SidebarSection>
|
|
18
|
+
|
|
19
|
+
{#snippet footer()}
|
|
20
|
+
<div class="p-4">
|
|
21
|
+
<SidebarToggle />
|
|
22
|
+
</div>
|
|
23
|
+
{/snippet}
|
|
24
|
+
</Sidebar>
|
|
25
|
+
|
|
26
|
+
@example Complex usage (dashboard style with custom header component)
|
|
27
|
+
<Sidebar bind:collapsed persistKey="dash-sidebar">
|
|
28
|
+
{#snippet header()}
|
|
29
|
+
<UserDropdown {collapsed} />
|
|
30
|
+
{/snippet}
|
|
31
|
+
|
|
32
|
+
<SidebarSection title="Main">
|
|
33
|
+
<SidebarItem href="/" icon={Home} label="Home" />
|
|
34
|
+
</SidebarSection>
|
|
35
|
+
|
|
36
|
+
<SidebarSection title="Accounts">
|
|
37
|
+
<SidebarAccountItem platform="instagram" label="@myaccount" />
|
|
38
|
+
</SidebarSection>
|
|
39
|
+
|
|
40
|
+
{#snippet footer()}
|
|
41
|
+
<SidebarToggle />
|
|
42
|
+
{/snippet}
|
|
43
|
+
</Sidebar>
|
|
44
|
+
-->
|
|
45
|
+
<script>
|
|
46
|
+
import { setContext, onMount } from 'svelte';
|
|
47
|
+
import { fly } from 'svelte/transition';
|
|
48
|
+
import { SIDEBAR_CONTEXT_KEY } from '../../utils/sidebar.svelte.js';
|
|
49
|
+
|
|
50
|
+
let {
|
|
51
|
+
collapsed = $bindable(false),
|
|
52
|
+
width = 240,
|
|
53
|
+
collapsedWidth = 64,
|
|
54
|
+
headerHeight = 60,
|
|
55
|
+
persistKey = null,
|
|
56
|
+
mobileOverlay = false,
|
|
57
|
+
overlayOpen = $bindable(false),
|
|
58
|
+
position = 'left',
|
|
59
|
+
class: className = '',
|
|
60
|
+
header,
|
|
61
|
+
footer,
|
|
62
|
+
children
|
|
63
|
+
} = $props();
|
|
64
|
+
|
|
65
|
+
// Internal collapsed state that syncs with prop
|
|
66
|
+
let isCollapsed = $state(collapsed);
|
|
67
|
+
|
|
68
|
+
// Sync internal state with prop
|
|
69
|
+
$effect(() => {
|
|
70
|
+
isCollapsed = collapsed;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Update prop when internal state changes
|
|
74
|
+
$effect(() => {
|
|
75
|
+
collapsed = isCollapsed;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Toggle function
|
|
79
|
+
function toggle() {
|
|
80
|
+
isCollapsed = !isCollapsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Hover popover state
|
|
84
|
+
let hoverPopover = $state({
|
|
85
|
+
item: null,
|
|
86
|
+
position: { top: 0, left: 0 }
|
|
87
|
+
});
|
|
88
|
+
let hoverTimeout = null;
|
|
89
|
+
|
|
90
|
+
function showPopover(itemId, position) {
|
|
91
|
+
if (!isCollapsed) return;
|
|
92
|
+
|
|
93
|
+
if (hoverTimeout) {
|
|
94
|
+
clearTimeout(hoverTimeout);
|
|
95
|
+
hoverTimeout = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
hoverPopover = { item: itemId, position };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hidePopover(delay = 150) {
|
|
102
|
+
if (hoverTimeout) clearTimeout(hoverTimeout);
|
|
103
|
+
|
|
104
|
+
hoverTimeout = setTimeout(() => {
|
|
105
|
+
hoverPopover = { item: null, position: { top: 0, left: 0 } };
|
|
106
|
+
}, delay);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function keepPopoverOpen() {
|
|
110
|
+
if (hoverTimeout) {
|
|
111
|
+
clearTimeout(hoverTimeout);
|
|
112
|
+
hoverTimeout = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Load from localStorage
|
|
117
|
+
onMount(() => {
|
|
118
|
+
if (persistKey && typeof localStorage !== 'undefined') {
|
|
119
|
+
try {
|
|
120
|
+
const saved = localStorage.getItem(persistKey);
|
|
121
|
+
if (saved !== null) {
|
|
122
|
+
isCollapsed = saved === 'true';
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Ignore localStorage errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Save to localStorage when state changes
|
|
131
|
+
$effect(() => {
|
|
132
|
+
if (persistKey && typeof localStorage !== 'undefined') {
|
|
133
|
+
try {
|
|
134
|
+
localStorage.setItem(persistKey, String(isCollapsed));
|
|
135
|
+
} catch (e) {
|
|
136
|
+
// Ignore localStorage errors
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Mobile functionality
|
|
142
|
+
function closeMobileOverlay() {
|
|
143
|
+
if (mobileOverlay) {
|
|
144
|
+
overlayOpen = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Provide context to children
|
|
149
|
+
setContext(SIDEBAR_CONTEXT_KEY, {
|
|
150
|
+
get collapsed() { return isCollapsed; },
|
|
151
|
+
get width() { return width; },
|
|
152
|
+
get collapsedWidth() { return collapsedWidth; },
|
|
153
|
+
get mobileOverlay() { return mobileOverlay; },
|
|
154
|
+
toggle,
|
|
155
|
+
expand() { isCollapsed = false; },
|
|
156
|
+
collapse() { isCollapsed = true; },
|
|
157
|
+
closeMobileOverlay,
|
|
158
|
+
showPopover,
|
|
159
|
+
hidePopover,
|
|
160
|
+
keepPopoverOpen,
|
|
161
|
+
get hoverPopover() { return hoverPopover; }
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Computed styles
|
|
165
|
+
const sidebarStyle = $derived(
|
|
166
|
+
`--sidebar-width: ${width}px; --sidebar-collapsed-width: ${collapsedWidth}px; --sidebar-header-height: ${headerHeight}px;`
|
|
167
|
+
);
|
|
168
|
+
</script>
|
|
169
|
+
|
|
170
|
+
{#if mobileOverlay}
|
|
171
|
+
<!-- Mobile overlay mode -->
|
|
172
|
+
{#if overlayOpen}
|
|
173
|
+
<div class="sidebar-overlay" onclick={closeMobileOverlay}></div>
|
|
174
|
+
<aside
|
|
175
|
+
class="sidebar mobile-overlay {className} {position}"
|
|
176
|
+
class:collapsed={isCollapsed}
|
|
177
|
+
style={sidebarStyle}
|
|
178
|
+
transition:fly={{x: position === 'left' ? -300 : 300, duration: 200}}
|
|
179
|
+
>
|
|
180
|
+
{#if header}
|
|
181
|
+
<div class="sidebar-header">
|
|
182
|
+
{@render header()}
|
|
183
|
+
<button class="mobile-close-btn" onclick={closeMobileOverlay}>
|
|
184
|
+
<svg
|
|
185
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
186
|
+
width="16"
|
|
187
|
+
height="16"
|
|
188
|
+
viewBox="0 0 24 24"
|
|
189
|
+
fill="none"
|
|
190
|
+
stroke="currentColor"
|
|
191
|
+
stroke-width="2"
|
|
192
|
+
stroke-linecap="round"
|
|
193
|
+
stroke-linejoin="round"
|
|
194
|
+
>
|
|
195
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
196
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
197
|
+
</svg>
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
<nav class="sidebar-content">
|
|
203
|
+
{@render children?.()}
|
|
204
|
+
</nav>
|
|
205
|
+
|
|
206
|
+
{#if footer}
|
|
207
|
+
<div class="sidebar-footer">
|
|
208
|
+
{@render footer()}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
</aside>
|
|
212
|
+
{/if}
|
|
213
|
+
{:else}
|
|
214
|
+
<!-- Default desktop mode -->
|
|
215
|
+
<aside
|
|
216
|
+
class="sidebar {className}"
|
|
217
|
+
class:collapsed={isCollapsed}
|
|
218
|
+
style={sidebarStyle}
|
|
219
|
+
>
|
|
220
|
+
{#if header}
|
|
221
|
+
<div class="sidebar-header">
|
|
222
|
+
{@render header()}
|
|
223
|
+
</div>
|
|
224
|
+
{/if}
|
|
225
|
+
|
|
226
|
+
<nav class="sidebar-content">
|
|
227
|
+
{@render children?.()}
|
|
228
|
+
</nav>
|
|
229
|
+
|
|
230
|
+
{#if footer}
|
|
231
|
+
<div class="sidebar-footer">
|
|
232
|
+
{@render footer()}
|
|
233
|
+
</div>
|
|
234
|
+
{/if}
|
|
235
|
+
</aside>
|
|
236
|
+
{/if}
|
|
237
|
+
|
|
238
|
+
<style>
|
|
239
|
+
.sidebar {
|
|
240
|
+
display: flex;
|
|
241
|
+
flex-direction: column;
|
|
242
|
+
background-color: var(--color-surface, var(--color-base01, #2C3040));
|
|
243
|
+
border-right: 1px solid var(--color-border, color-mix(in srgb, var(--color-base03, #565E78) 30%, transparent));
|
|
244
|
+
height: 100vh;
|
|
245
|
+
width: var(--sidebar-width);
|
|
246
|
+
transition: width 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
247
|
+
position: sticky;
|
|
248
|
+
top: 0;
|
|
249
|
+
will-change: width;
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.sidebar.collapsed {
|
|
254
|
+
width: var(--sidebar-collapsed-width);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.sidebar-header {
|
|
258
|
+
flex-shrink: 0;
|
|
259
|
+
min-height: var(--sidebar-header-height);
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.sidebar-content {
|
|
265
|
+
flex: 1;
|
|
266
|
+
overflow-y: auto;
|
|
267
|
+
overflow-x: hidden;
|
|
268
|
+
padding: 0.5rem 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.sidebar-footer {
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
margin-top: auto;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* Scrollbar styling */
|
|
277
|
+
.sidebar-content::-webkit-scrollbar {
|
|
278
|
+
width: 4px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.sidebar-content::-webkit-scrollbar-track {
|
|
282
|
+
background: transparent;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.sidebar-content::-webkit-scrollbar-thumb {
|
|
286
|
+
background: var(--color-base03, #565E78);
|
|
287
|
+
border-radius: 2px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.sidebar-content::-webkit-scrollbar-thumb:hover {
|
|
291
|
+
background: var(--color-base04, #737E99);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* Mobile overlay styles */
|
|
295
|
+
.sidebar-overlay {
|
|
296
|
+
position: fixed;
|
|
297
|
+
inset: 0;
|
|
298
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
299
|
+
z-index: 40;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.sidebar.mobile-overlay {
|
|
303
|
+
position: fixed;
|
|
304
|
+
z-index: 50;
|
|
305
|
+
height: 100vh;
|
|
306
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.sidebar.mobile-overlay.left {
|
|
310
|
+
left: 0;
|
|
311
|
+
top: 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.sidebar.mobile-overlay.right {
|
|
315
|
+
right: 0;
|
|
316
|
+
top: 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.mobile-close-btn {
|
|
320
|
+
padding: 0.5rem;
|
|
321
|
+
border-radius: 0.5rem;
|
|
322
|
+
color: var(--color-text-muted, var(--color-base05, #D0D2DB));
|
|
323
|
+
cursor: pointer;
|
|
324
|
+
border: none;
|
|
325
|
+
background: transparent;
|
|
326
|
+
transition: all 200ms ease;
|
|
327
|
+
margin-left: auto;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.mobile-close-btn:hover {
|
|
331
|
+
background-color: var(--color-surface-alt, var(--color-base02, #3E4359));
|
|
332
|
+
color: var(--color-text, var(--color-base06, #F3F4F7));
|
|
333
|
+
}
|
|
334
|
+
</style>
|