@miozu/jera 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md
CHANGED
|
@@ -370,6 +370,107 @@ Props: `tabs` (array), `active`, `variant` (default/underline/pills), `size` (sm
|
|
|
370
370
|
Accordion props: `expanded` (array of ids), `multiple`
|
|
371
371
|
AccordionItem props: `id`, `title`, `disabled`
|
|
372
372
|
|
|
373
|
+
### LeftBar (Sidebar Navigation)
|
|
374
|
+
|
|
375
|
+
A lightweight, collapsible sidebar for admin-style navigation.
|
|
376
|
+
|
|
377
|
+
```svelte
|
|
378
|
+
<script>
|
|
379
|
+
import { LeftBar, LeftBarSection, LeftBarItem, LeftBarGroup, LeftBarToggle, createActiveChecker } from '@miozu/jera';
|
|
380
|
+
import { page } from '$app/stores';
|
|
381
|
+
import { Home, Settings, Users, Building2 } from 'lucide-svelte';
|
|
382
|
+
|
|
383
|
+
let collapsed = $state(false);
|
|
384
|
+
const isActive = createActiveChecker(() => $page.url.pathname);
|
|
385
|
+
</script>
|
|
386
|
+
|
|
387
|
+
<LeftBar bind:collapsed persistKey="my-sidebar">
|
|
388
|
+
{#snippet header()}
|
|
389
|
+
<div class="logo">MyApp</div>
|
|
390
|
+
{/snippet}
|
|
391
|
+
|
|
392
|
+
{#snippet navigation()}
|
|
393
|
+
<LeftBarSection>
|
|
394
|
+
<LeftBarItem href="/" icon={Home} label="Dashboard" active={isActive('/', 'exact')} />
|
|
395
|
+
<LeftBarItem href="/users" icon={Users} label="Users" badge={5} active={isActive('/users')} />
|
|
396
|
+
</LeftBarSection>
|
|
397
|
+
|
|
398
|
+
<LeftBarSection title="Management">
|
|
399
|
+
<LeftBarItem
|
|
400
|
+
label="Organization"
|
|
401
|
+
icon={Building2}
|
|
402
|
+
expandable
|
|
403
|
+
subroutes={[
|
|
404
|
+
{ label: 'Members', href: '/org/members' },
|
|
405
|
+
{ label: 'Settings', href: '/org/settings' }
|
|
406
|
+
]}
|
|
407
|
+
/>
|
|
408
|
+
</LeftBarSection>
|
|
409
|
+
|
|
410
|
+
<!-- Generic group for accounts, projects, workspaces, etc. -->
|
|
411
|
+
<LeftBarGroup
|
|
412
|
+
title="Connected Accounts"
|
|
413
|
+
items={accounts}
|
|
414
|
+
bind:expanded={accountsExpanded}
|
|
415
|
+
onItemClick={(account) => goto(`/account/${account.id}`)}
|
|
416
|
+
onAddClick={() => showConnectModal = true}
|
|
417
|
+
addLabel="Connect account"
|
|
418
|
+
/>
|
|
419
|
+
{/snippet}
|
|
420
|
+
|
|
421
|
+
{#snippet footer()}
|
|
422
|
+
<LeftBarToggle />
|
|
423
|
+
{/snippet}
|
|
424
|
+
</LeftBar>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
**LeftBar** props: `collapsed` (bindable), `persistKey`, `header` (snippet), `navigation` (snippet), `footer` (snippet)
|
|
428
|
+
|
|
429
|
+
**LeftBarSection** props: `title`, `class`
|
|
430
|
+
|
|
431
|
+
**LeftBarItem** props:
|
|
432
|
+
- `href` - Link destination (renders as `<a>`)
|
|
433
|
+
- `label` - Display text
|
|
434
|
+
- `icon` - Lucide icon component
|
|
435
|
+
- `active` - Boolean for active state
|
|
436
|
+
- `badge` - Number/string for badge count
|
|
437
|
+
- `expandable` - Enable expand/collapse
|
|
438
|
+
- `expanded` - Bindable expansion state
|
|
439
|
+
- `subroutes` - Array of `{ label, href }` for expandable items
|
|
440
|
+
- `preload` - Boolean for SvelteKit preload-data (default: true)
|
|
441
|
+
- `leading` - Snippet for content before icon
|
|
442
|
+
- `trailing` - Snippet for content after label (custom badges, etc.)
|
|
443
|
+
- `isActiveRoute` - Function to check subroute active state
|
|
444
|
+
|
|
445
|
+
**LeftBarGroup** props (generic collection for accounts, projects, etc.):
|
|
446
|
+
- `title` - Section header
|
|
447
|
+
- `items` - Array of items to display
|
|
448
|
+
- `expanded` - Bindable expansion state
|
|
449
|
+
- `expandedItems` - Object tracking which items are expanded
|
|
450
|
+
- `expandable` - Enable item-level expansion
|
|
451
|
+
- `showAdd` - Show add button
|
|
452
|
+
- `addLabel` - Add button text
|
|
453
|
+
- `showCount` - Show item count badge
|
|
454
|
+
- `onItemClick` - Item click handler
|
|
455
|
+
- `onAddClick` - Add button handler
|
|
456
|
+
- `getItemId`, `getItemName`, `getItemAvatar`, `getItemPlatform` - Item accessors
|
|
457
|
+
- `getSubroutes` - Function returning subroutes for an item
|
|
458
|
+
- `isItemActive`, `isSubrouteActive` - Active state checkers
|
|
459
|
+
- `item` - Custom item rendering snippet
|
|
460
|
+
|
|
461
|
+
**createActiveChecker** utility:
|
|
462
|
+
```javascript
|
|
463
|
+
import { createActiveChecker } from '@miozu/jera';
|
|
464
|
+
import { page } from '$app/stores';
|
|
465
|
+
|
|
466
|
+
// Create checker with current pathname
|
|
467
|
+
const isActive = createActiveChecker(() => $page.url.pathname);
|
|
468
|
+
|
|
469
|
+
// Use in components
|
|
470
|
+
<LeftBarItem active={isActive('/dashboard')} /> // prefix match
|
|
471
|
+
<LeftBarItem active={isActive('/dashboard', 'exact')} /> // exact match
|
|
472
|
+
```
|
|
473
|
+
|
|
373
474
|
### CodeBlock
|
|
374
475
|
```svelte
|
|
375
476
|
<script>
|
package/package.json
CHANGED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component LeftBarGroup
|
|
3
|
+
|
|
4
|
+
A collapsible group within LeftBar for displaying collections of entities.
|
|
5
|
+
Generic component that can represent accounts, projects, workspaces, channels, etc.
|
|
6
|
+
|
|
7
|
+
@example Basic usage with items
|
|
8
|
+
<LeftBarGroup
|
|
9
|
+
title="Connected Accounts"
|
|
10
|
+
items={accounts}
|
|
11
|
+
bind:expanded={accountsExpanded}
|
|
12
|
+
onItemClick={(item) => navigateTo(item)}
|
|
13
|
+
onAddClick={() => showConnectModal = true}
|
|
14
|
+
addLabel="Connect account"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
@example With custom item rendering
|
|
18
|
+
<LeftBarGroup title="Projects" bind:expanded>
|
|
19
|
+
{#snippet item(project)}
|
|
20
|
+
<div class="project-item">
|
|
21
|
+
<span class="project-color" style="background: {project.color}"></span>
|
|
22
|
+
<span>{project.name}</span>
|
|
23
|
+
</div>
|
|
24
|
+
{/snippet}
|
|
25
|
+
</LeftBarGroup>
|
|
26
|
+
|
|
27
|
+
@example With expandable items and subroutes
|
|
28
|
+
<LeftBarGroup
|
|
29
|
+
title="Accounts"
|
|
30
|
+
items={accounts}
|
|
31
|
+
expandable
|
|
32
|
+
bind:expandedItems={expandedAccounts}
|
|
33
|
+
getSubroutes={(account) => [
|
|
34
|
+
{ label: 'Overview', href: `/account/${account.id}` },
|
|
35
|
+
{ label: 'Settings', href: `/account/${account.id}/settings` }
|
|
36
|
+
]}
|
|
37
|
+
/>
|
|
38
|
+
-->
|
|
39
|
+
<script>
|
|
40
|
+
import { getContext } from 'svelte';
|
|
41
|
+
import { slide, fade } from 'svelte/transition';
|
|
42
|
+
import { cubicOut } from 'svelte/easing';
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
title = '',
|
|
46
|
+
items = [],
|
|
47
|
+
expanded = $bindable(false),
|
|
48
|
+
expandedItems = $bindable({}),
|
|
49
|
+
expandable = false,
|
|
50
|
+
searchable = false,
|
|
51
|
+
searchQuery = '',
|
|
52
|
+
addLabel = 'Add item',
|
|
53
|
+
showAdd = true,
|
|
54
|
+
showCount = true,
|
|
55
|
+
onItemClick = null,
|
|
56
|
+
onSubrouteClick = null,
|
|
57
|
+
onAddClick = null,
|
|
58
|
+
getItemId = (item) => item.id,
|
|
59
|
+
getItemName = (item) => item.name || item.label || '',
|
|
60
|
+
getItemAvatar = (item) => item.avatar_url || item.avatar || null,
|
|
61
|
+
getItemInitial = (item) => getItemName(item)?.charAt(0)?.toUpperCase() || '?',
|
|
62
|
+
getItemPlatform = (item) => item.platform || item.type || null,
|
|
63
|
+
getSubroutes = () => [],
|
|
64
|
+
isItemActive = () => false,
|
|
65
|
+
isSubrouteActive = () => false,
|
|
66
|
+
class: className = '',
|
|
67
|
+
item: itemSnippet,
|
|
68
|
+
children
|
|
69
|
+
} = $props();
|
|
70
|
+
|
|
71
|
+
const leftbar = getContext('leftbar');
|
|
72
|
+
const isCollapsed = $derived(leftbar?.collapsed ?? false);
|
|
73
|
+
|
|
74
|
+
// Filter items based on search
|
|
75
|
+
const filteredItems = $derived(
|
|
76
|
+
items.filter(item =>
|
|
77
|
+
!searchQuery ||
|
|
78
|
+
getItemName(item)?.toLowerCase().includes(searchQuery.toLowerCase())
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function toggleExpanded() {
|
|
83
|
+
expanded = !expanded;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleItemClick(item) {
|
|
87
|
+
if (expandable) {
|
|
88
|
+
const id = getItemId(item);
|
|
89
|
+
expandedItems = { ...expandedItems, [id]: !expandedItems[id] };
|
|
90
|
+
}
|
|
91
|
+
onItemClick?.(item);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleSubrouteClick(subroute, item) {
|
|
95
|
+
onSubrouteClick?.(subroute, item);
|
|
96
|
+
leftbar?.closeMobileOverlay?.();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleAddClick() {
|
|
100
|
+
onAddClick?.();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Platform colors
|
|
104
|
+
const platformColors = {
|
|
105
|
+
instagram: 'linear-gradient(to bottom right, rgb(168, 85, 247), rgb(236, 72, 153))',
|
|
106
|
+
shopify: 'linear-gradient(to bottom right, rgb(34, 197, 94), rgb(16, 185, 129))',
|
|
107
|
+
tiktok: 'linear-gradient(to bottom right, rgb(0, 0, 0), rgb(37, 244, 238))',
|
|
108
|
+
facebook: 'linear-gradient(to bottom right, rgb(59, 89, 152), rgb(24, 119, 242))',
|
|
109
|
+
twitter: 'linear-gradient(to bottom right, rgb(29, 161, 242), rgb(29, 161, 242))',
|
|
110
|
+
default: 'var(--color-base03)'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function getAvatarStyle(item) {
|
|
114
|
+
const platform = getItemPlatform(item);
|
|
115
|
+
return platformColors[platform] || platformColors.default;
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<div class="left-bar-group {className}">
|
|
120
|
+
<!-- Section Header -->
|
|
121
|
+
<button
|
|
122
|
+
class="group-header"
|
|
123
|
+
onclick={toggleExpanded}
|
|
124
|
+
aria-expanded={expanded}
|
|
125
|
+
>
|
|
126
|
+
{#if !isCollapsed}
|
|
127
|
+
<span class="group-title" transition:fade={{ duration: 150 }}>{title}</span>
|
|
128
|
+
{#if showCount && items.length > 0}
|
|
129
|
+
<span class="group-count" transition:fade={{ duration: 150 }}>{items.length}</span>
|
|
130
|
+
{/if}
|
|
131
|
+
{/if}
|
|
132
|
+
<svg
|
|
133
|
+
class="group-chevron"
|
|
134
|
+
class:rotate-180={expanded}
|
|
135
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
136
|
+
width="14"
|
|
137
|
+
height="14"
|
|
138
|
+
viewBox="0 0 24 24"
|
|
139
|
+
fill="none"
|
|
140
|
+
stroke="currentColor"
|
|
141
|
+
stroke-width="2"
|
|
142
|
+
stroke-linecap="round"
|
|
143
|
+
stroke-linejoin="round"
|
|
144
|
+
>
|
|
145
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
|
|
149
|
+
<!-- Items List -->
|
|
150
|
+
{#if expanded}
|
|
151
|
+
<div class="group-content" transition:slide={{ duration: 200, easing: cubicOut }}>
|
|
152
|
+
<!-- Custom children content -->
|
|
153
|
+
{#if children}
|
|
154
|
+
{@render children()}
|
|
155
|
+
{:else}
|
|
156
|
+
<!-- Default item rendering -->
|
|
157
|
+
{#each filteredItems as item}
|
|
158
|
+
{@const itemId = getItemId(item)}
|
|
159
|
+
{@const itemName = getItemName(item)}
|
|
160
|
+
{@const itemAvatar = getItemAvatar(item)}
|
|
161
|
+
{@const platform = getItemPlatform(item)}
|
|
162
|
+
{@const subroutes = getSubroutes(item)}
|
|
163
|
+
{@const isExpanded = expandedItems[itemId]}
|
|
164
|
+
|
|
165
|
+
<div class="group-item-wrapper">
|
|
166
|
+
<!-- Custom item snippet or default -->
|
|
167
|
+
{#if itemSnippet}
|
|
168
|
+
{@render itemSnippet(item, isExpanded)}
|
|
169
|
+
{:else}
|
|
170
|
+
<button
|
|
171
|
+
class="group-item"
|
|
172
|
+
class:active={isItemActive(item)}
|
|
173
|
+
class:expanded={isExpanded}
|
|
174
|
+
onclick={() => handleItemClick(item)}
|
|
175
|
+
title={isCollapsed ? itemName : null}
|
|
176
|
+
>
|
|
177
|
+
<!-- Avatar -->
|
|
178
|
+
<div class="item-avatar-container">
|
|
179
|
+
{#if itemAvatar}
|
|
180
|
+
<div class="item-avatar-wrapper">
|
|
181
|
+
<img src={itemAvatar} alt={itemName} class="item-avatar-img" />
|
|
182
|
+
</div>
|
|
183
|
+
{:else}
|
|
184
|
+
<div class="item-avatar" style="background: {getAvatarStyle(item)}">
|
|
185
|
+
{getItemInitial(item)}
|
|
186
|
+
</div>
|
|
187
|
+
{/if}
|
|
188
|
+
{#if platform}
|
|
189
|
+
<div class="platform-badge {platform}">
|
|
190
|
+
{#if platform === 'instagram'}
|
|
191
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="5"></rect><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line></svg>
|
|
192
|
+
{:else if platform === 'shopify'}
|
|
193
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{#if !isCollapsed}
|
|
200
|
+
<span class="item-name" transition:fade={{ duration: 150 }}>{itemName}</span>
|
|
201
|
+
{#if expandable && subroutes.length > 0}
|
|
202
|
+
<svg
|
|
203
|
+
class="item-chevron"
|
|
204
|
+
class:rotate-180={isExpanded}
|
|
205
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
206
|
+
width="14"
|
|
207
|
+
height="14"
|
|
208
|
+
viewBox="0 0 24 24"
|
|
209
|
+
fill="none"
|
|
210
|
+
stroke="currentColor"
|
|
211
|
+
stroke-width="2"
|
|
212
|
+
>
|
|
213
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
214
|
+
</svg>
|
|
215
|
+
{/if}
|
|
216
|
+
{/if}
|
|
217
|
+
</button>
|
|
218
|
+
|
|
219
|
+
<!-- Subroutes -->
|
|
220
|
+
{#if isExpanded && !isCollapsed && subroutes.length > 0}
|
|
221
|
+
<div class="item-subroutes" transition:slide={{ duration: 200, easing: cubicOut }}>
|
|
222
|
+
{#each subroutes as subroute}
|
|
223
|
+
<a
|
|
224
|
+
href={subroute.href}
|
|
225
|
+
class="subroute-item"
|
|
226
|
+
class:active={isSubrouteActive(subroute)}
|
|
227
|
+
onclick={() => handleSubrouteClick(subroute, item)}
|
|
228
|
+
>
|
|
229
|
+
{subroute.label}
|
|
230
|
+
</a>
|
|
231
|
+
{/each}
|
|
232
|
+
</div>
|
|
233
|
+
{/if}
|
|
234
|
+
{/if}
|
|
235
|
+
</div>
|
|
236
|
+
{/each}
|
|
237
|
+
{/if}
|
|
238
|
+
|
|
239
|
+
<!-- Add Button -->
|
|
240
|
+
{#if showAdd && onAddClick}
|
|
241
|
+
<button
|
|
242
|
+
class="add-item-btn"
|
|
243
|
+
class:collapsed={isCollapsed}
|
|
244
|
+
onclick={handleAddClick}
|
|
245
|
+
title={addLabel}
|
|
246
|
+
>
|
|
247
|
+
<svg
|
|
248
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
249
|
+
width="16"
|
|
250
|
+
height="16"
|
|
251
|
+
viewBox="0 0 24 24"
|
|
252
|
+
fill="none"
|
|
253
|
+
stroke="currentColor"
|
|
254
|
+
stroke-width="2"
|
|
255
|
+
>
|
|
256
|
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
257
|
+
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
258
|
+
</svg>
|
|
259
|
+
{#if !isCollapsed}
|
|
260
|
+
<span transition:fade={{ duration: 150 }}>{addLabel}</span>
|
|
261
|
+
{/if}
|
|
262
|
+
</button>
|
|
263
|
+
{/if}
|
|
264
|
+
</div>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<style>
|
|
269
|
+
.left-bar-group {
|
|
270
|
+
padding: 0.125rem 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Group Header */
|
|
274
|
+
.group-header {
|
|
275
|
+
width: 100%;
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 0.5rem;
|
|
279
|
+
padding: 0.5rem 0.75rem;
|
|
280
|
+
font-size: 0.75rem;
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
color: var(--color-base04);
|
|
283
|
+
text-transform: uppercase;
|
|
284
|
+
letter-spacing: 0.05em;
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
transition: all 200ms ease;
|
|
287
|
+
border: none;
|
|
288
|
+
background: transparent;
|
|
289
|
+
font-family: inherit;
|
|
290
|
+
justify-content: flex-start;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.group-header:hover {
|
|
294
|
+
color: var(--color-base05);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.group-title {
|
|
298
|
+
flex: 1;
|
|
299
|
+
text-align: left;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.group-count {
|
|
303
|
+
padding: 0.125rem 0.375rem;
|
|
304
|
+
font-size: 0.625rem;
|
|
305
|
+
background-color: var(--color-base02);
|
|
306
|
+
color: var(--color-base05);
|
|
307
|
+
border-radius: 9999px;
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.group-chevron {
|
|
312
|
+
color: var(--color-base04);
|
|
313
|
+
transition: all 200ms ease;
|
|
314
|
+
flex-shrink: 0;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.group-chevron.rotate-180 {
|
|
318
|
+
transform: rotate(180deg);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* Group Content */
|
|
322
|
+
.group-content {
|
|
323
|
+
display: flex;
|
|
324
|
+
flex-direction: column;
|
|
325
|
+
gap: 0.125rem;
|
|
326
|
+
padding: 0 0.5rem;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.group-item-wrapper {
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-direction: column;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Item Button */
|
|
335
|
+
.group-item {
|
|
336
|
+
width: 100%;
|
|
337
|
+
padding: 0.5rem 0.75rem;
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 0.75rem;
|
|
341
|
+
font-size: 0.875rem;
|
|
342
|
+
line-height: 1.25rem;
|
|
343
|
+
color: var(--color-base06);
|
|
344
|
+
cursor: pointer;
|
|
345
|
+
border-radius: 0.375rem;
|
|
346
|
+
transition: all 200ms ease;
|
|
347
|
+
overflow: hidden;
|
|
348
|
+
border: none;
|
|
349
|
+
background: transparent;
|
|
350
|
+
font-family: inherit;
|
|
351
|
+
text-align: left;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.group-item:hover {
|
|
355
|
+
color: var(--color-base0D);
|
|
356
|
+
background-color: color-mix(in srgb, var(--color-base0D) 5%, transparent);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.group-item.active,
|
|
360
|
+
.group-item.expanded {
|
|
361
|
+
color: var(--color-base0D);
|
|
362
|
+
background-color: color-mix(in srgb, var(--color-base0D) 10%, transparent);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* Avatar */
|
|
366
|
+
.item-avatar-container {
|
|
367
|
+
position: relative;
|
|
368
|
+
flex-shrink: 0;
|
|
369
|
+
width: 2rem;
|
|
370
|
+
height: 2rem;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.item-avatar-wrapper {
|
|
374
|
+
width: 2rem;
|
|
375
|
+
height: 2rem;
|
|
376
|
+
border-radius: 0.375rem;
|
|
377
|
+
overflow: hidden;
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
justify-content: center;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.item-avatar-img {
|
|
384
|
+
width: 100%;
|
|
385
|
+
height: 100%;
|
|
386
|
+
object-fit: cover;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.item-avatar {
|
|
390
|
+
width: 2rem;
|
|
391
|
+
height: 2rem;
|
|
392
|
+
border-radius: 0.375rem;
|
|
393
|
+
display: flex;
|
|
394
|
+
align-items: center;
|
|
395
|
+
justify-content: center;
|
|
396
|
+
font-size: 0.75rem;
|
|
397
|
+
font-weight: 600;
|
|
398
|
+
color: white;
|
|
399
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.platform-badge {
|
|
403
|
+
position: absolute;
|
|
404
|
+
bottom: -2px;
|
|
405
|
+
right: -2px;
|
|
406
|
+
width: 1rem;
|
|
407
|
+
height: 1rem;
|
|
408
|
+
border-radius: 9999px;
|
|
409
|
+
display: flex;
|
|
410
|
+
align-items: center;
|
|
411
|
+
justify-content: center;
|
|
412
|
+
color: white;
|
|
413
|
+
border: 1px solid var(--color-base00);
|
|
414
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
415
|
+
z-index: 10;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.platform-badge.instagram {
|
|
419
|
+
background: linear-gradient(135deg, #833ab4, #fd1d1d, #fcb045);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.platform-badge.shopify {
|
|
423
|
+
background-color: var(--color-base0B);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.item-name {
|
|
427
|
+
flex: 1;
|
|
428
|
+
white-space: nowrap;
|
|
429
|
+
overflow: hidden;
|
|
430
|
+
text-overflow: ellipsis;
|
|
431
|
+
font-weight: 500;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.item-chevron {
|
|
435
|
+
color: var(--color-base05);
|
|
436
|
+
transition: all 200ms ease;
|
|
437
|
+
flex-shrink: 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.item-chevron.rotate-180 {
|
|
441
|
+
transform: rotate(180deg);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Subroutes */
|
|
445
|
+
.item-subroutes {
|
|
446
|
+
margin-left: 2.5rem;
|
|
447
|
+
padding-left: 0.5rem;
|
|
448
|
+
border-left: 2px solid color-mix(in srgb, var(--color-base03) 30%, transparent);
|
|
449
|
+
display: flex;
|
|
450
|
+
flex-direction: column;
|
|
451
|
+
gap: 0.125rem;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.subroute-item {
|
|
455
|
+
padding: 0.375rem 0.75rem;
|
|
456
|
+
display: block;
|
|
457
|
+
font-size: 0.875rem;
|
|
458
|
+
line-height: 1.25rem;
|
|
459
|
+
color: var(--color-base05);
|
|
460
|
+
cursor: pointer;
|
|
461
|
+
border-radius: 0.375rem;
|
|
462
|
+
transition: all 200ms ease;
|
|
463
|
+
text-decoration: none;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.subroute-item:hover {
|
|
467
|
+
color: var(--color-base0D);
|
|
468
|
+
background-color: color-mix(in srgb, var(--color-base0D) 5%, transparent);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.subroute-item.active {
|
|
472
|
+
color: var(--color-base0D);
|
|
473
|
+
background-color: color-mix(in srgb, var(--color-base0D) 10%, transparent);
|
|
474
|
+
font-weight: 500;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* Add Button */
|
|
478
|
+
.add-item-btn {
|
|
479
|
+
width: 100%;
|
|
480
|
+
padding: 0.5rem 0.75rem;
|
|
481
|
+
margin-top: 0.25rem;
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
gap: 0.5rem;
|
|
485
|
+
justify-content: center;
|
|
486
|
+
font-size: 0.875rem;
|
|
487
|
+
line-height: 1.25rem;
|
|
488
|
+
color: var(--color-base0D);
|
|
489
|
+
cursor: pointer;
|
|
490
|
+
border-radius: 0.375rem;
|
|
491
|
+
transition: all 200ms ease;
|
|
492
|
+
border: 1px solid color-mix(in srgb, var(--color-base0D) 30%, transparent);
|
|
493
|
+
background: transparent;
|
|
494
|
+
font-family: inherit;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.add-item-btn:hover {
|
|
498
|
+
color: var(--color-base0D);
|
|
499
|
+
background-color: color-mix(in srgb, var(--color-base0D) 10%, transparent);
|
|
500
|
+
border-color: var(--color-base0D);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.add-item-btn.collapsed {
|
|
504
|
+
width: 2rem;
|
|
505
|
+
height: 2rem;
|
|
506
|
+
padding: 0;
|
|
507
|
+
margin: 0 auto;
|
|
508
|
+
justify-content: center;
|
|
509
|
+
}
|
|
510
|
+
</style>
|
|
@@ -32,12 +32,19 @@
|
|
|
32
32
|
expandable = false,
|
|
33
33
|
expanded = $bindable(false),
|
|
34
34
|
subroutes = [],
|
|
35
|
+
badge = null,
|
|
36
|
+
preload = true,
|
|
35
37
|
onclick = null,
|
|
36
38
|
isActiveRoute = () => false,
|
|
37
39
|
class: className = '',
|
|
40
|
+
leading,
|
|
41
|
+
trailing,
|
|
38
42
|
children
|
|
39
43
|
} = $props();
|
|
40
44
|
|
|
45
|
+
// Preload attribute for SvelteKit
|
|
46
|
+
const preloadAttr = preload ? 'hover' : undefined;
|
|
47
|
+
|
|
41
48
|
const leftbar = getContext('leftbar');
|
|
42
49
|
const isCollapsed = $derived(leftbar?.collapsed ?? false);
|
|
43
50
|
|
|
@@ -68,12 +75,18 @@
|
|
|
68
75
|
class:active
|
|
69
76
|
class:collapsed={isCollapsed}
|
|
70
77
|
title={isCollapsed ? label : null}
|
|
78
|
+
data-sveltekit-preload-data={preloadAttr}
|
|
71
79
|
>
|
|
80
|
+
{@render leading?.()}
|
|
72
81
|
{#if Icon}
|
|
73
82
|
<Icon size={18} class="nav-icon" />
|
|
74
83
|
{/if}
|
|
75
84
|
{#if !isCollapsed}
|
|
76
85
|
<span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
|
|
86
|
+
{#if badge != null}
|
|
87
|
+
<span class="nav-badge" transition:fade={{ duration: 150 }}>{badge}</span>
|
|
88
|
+
{/if}
|
|
89
|
+
{@render trailing?.()}
|
|
77
90
|
{/if}
|
|
78
91
|
{@render children?.()}
|
|
79
92
|
</a>
|
|
@@ -133,11 +146,16 @@
|
|
|
133
146
|
onclick={handleClick}
|
|
134
147
|
title={isCollapsed ? label : null}
|
|
135
148
|
>
|
|
149
|
+
{@render leading?.()}
|
|
136
150
|
{#if Icon}
|
|
137
151
|
<Icon size={18} class="nav-icon" />
|
|
138
152
|
{/if}
|
|
139
153
|
{#if !isCollapsed}
|
|
140
154
|
<span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
|
|
155
|
+
{#if badge != null}
|
|
156
|
+
<span class="nav-badge" transition:fade={{ duration: 150 }}>{badge}</span>
|
|
157
|
+
{/if}
|
|
158
|
+
{@render trailing?.()}
|
|
141
159
|
{/if}
|
|
142
160
|
{@render children?.()}
|
|
143
161
|
</button>
|
|
@@ -217,6 +235,16 @@
|
|
|
217
235
|
overflow: hidden;
|
|
218
236
|
}
|
|
219
237
|
|
|
238
|
+
.nav-badge {
|
|
239
|
+
padding: 0.125rem 0.375rem;
|
|
240
|
+
font-size: 0.625rem;
|
|
241
|
+
font-weight: 600;
|
|
242
|
+
background-color: color-mix(in srgb, var(--color-base0D) 10%, transparent);
|
|
243
|
+
color: var(--color-base0D);
|
|
244
|
+
border-radius: 9999px;
|
|
245
|
+
flex-shrink: 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
220
248
|
.expand-icon {
|
|
221
249
|
color: var(--color-base05);
|
|
222
250
|
flex-shrink: 0;
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
* Reusable navigation components including sidebars, tabs, and accordions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// LeftBar Components (simpler, lightweight)
|
|
8
|
+
export { default as LeftBar } from './LeftBar.svelte';
|
|
9
|
+
export { default as LeftBarItem } from './LeftBarItem.svelte';
|
|
10
|
+
export { default as LeftBarSection } from './LeftBarSection.svelte';
|
|
11
|
+
export { default as LeftBarGroup } from './LeftBarGroup.svelte';
|
|
12
|
+
export { default as LeftBarPopover } from './LeftBarPopover.svelte';
|
|
13
|
+
export { default as LeftBarToggle } from './LeftBarToggle.svelte';
|
|
14
|
+
|
|
15
|
+
// Sidebar Components (feature-rich)
|
|
8
16
|
export { default as Sidebar } from './Sidebar.svelte';
|
|
9
17
|
export { default as SidebarSection } from './SidebarSection.svelte';
|
|
10
18
|
export { default as SidebarItem } from './SidebarItem.svelte';
|
package/src/index.js
CHANGED
|
@@ -120,6 +120,7 @@ export { default as WorkspaceMenu } from './components/navigation/WorkspaceMenu.
|
|
|
120
120
|
export { default as LeftBar } from './components/navigation/LeftBar.svelte';
|
|
121
121
|
export { default as LeftBarSection } from './components/navigation/LeftBarSection.svelte';
|
|
122
122
|
export { default as LeftBarItem } from './components/navigation/LeftBarItem.svelte';
|
|
123
|
+
export { default as LeftBarGroup } from './components/navigation/LeftBarGroup.svelte';
|
|
123
124
|
export { default as LeftBarToggle } from './components/navigation/LeftBarToggle.svelte';
|
|
124
125
|
export { default as LeftBarPopover } from './components/navigation/LeftBarPopover.svelte';
|
|
125
126
|
export { default as DropdownContainer } from './components/navigation/DropdownContainer.svelte';
|
|
@@ -164,6 +165,7 @@ export {
|
|
|
164
165
|
export {
|
|
165
166
|
NavigationState,
|
|
166
167
|
createNavigationState,
|
|
168
|
+
createActiveChecker,
|
|
167
169
|
NAVIGATION_CONTEXT_KEY
|
|
168
170
|
} from './utils/navigation.svelte.js';
|
|
169
171
|
|
|
@@ -420,4 +420,36 @@ export function createNavigationState(options = {}) {
|
|
|
420
420
|
/**
|
|
421
421
|
* Navigation context key for Svelte context
|
|
422
422
|
*/
|
|
423
|
-
export const NAVIGATION_CONTEXT_KEY = 'navigation-state';
|
|
423
|
+
export const NAVIGATION_CONTEXT_KEY = 'navigation-state';
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create an active route checker function for use with LeftBar/Sidebar components.
|
|
427
|
+
* Works with SvelteKit's page store or any path string.
|
|
428
|
+
*
|
|
429
|
+
* @example With SvelteKit page store
|
|
430
|
+
* import { page } from '$app/stores';
|
|
431
|
+
* const isActive = createActiveChecker(() => $page.url.pathname);
|
|
432
|
+
*
|
|
433
|
+
* <LeftBarItem active={isActive('/dashboard')} />
|
|
434
|
+
* <LeftBarItem active={isActive('/settings', 'exact')} />
|
|
435
|
+
*
|
|
436
|
+
* @example With reactive path
|
|
437
|
+
* let currentPath = $state('/dashboard');
|
|
438
|
+
* const isActive = createActiveChecker(() => currentPath);
|
|
439
|
+
*
|
|
440
|
+
* @param {() => string} getPathname - Function that returns current pathname
|
|
441
|
+
* @returns {(path: string, matchType?: 'prefix' | 'exact') => boolean}
|
|
442
|
+
*/
|
|
443
|
+
export function createActiveChecker(getPathname) {
|
|
444
|
+
return function isActive(path, matchType = 'prefix') {
|
|
445
|
+
const currentPath = getPathname();
|
|
446
|
+
// Normalize paths by removing trailing slashes
|
|
447
|
+
const normalizedCurrent = currentPath.replace(/\/$/, '') || '/';
|
|
448
|
+
const normalizedPath = path.replace(/\/$/, '') || '/';
|
|
449
|
+
|
|
450
|
+
if (matchType === 'exact') {
|
|
451
|
+
return normalizedCurrent === normalizedPath;
|
|
452
|
+
}
|
|
453
|
+
return normalizedCurrent.startsWith(normalizedPath);
|
|
454
|
+
};
|
|
455
|
+
}
|