@miozu/jera 0.6.4 → 0.7.1

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miozu/jera",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "description": "Zero-dependency, AI-first component library for Svelte 5",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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,20 @@
32
32
  expandable = false,
33
33
  expanded = $bindable(false),
34
34
  subroutes = [],
35
+ badge = null,
36
+ preload = true,
37
+ variant = 'default',
35
38
  onclick = null,
36
39
  isActiveRoute = () => false,
37
40
  class: className = '',
41
+ leading,
42
+ trailing,
38
43
  children
39
44
  } = $props();
40
45
 
46
+ // Preload attribute for SvelteKit
47
+ const preloadAttr = preload ? 'hover' : undefined;
48
+
41
49
  const leftbar = getContext('leftbar');
42
50
  const isCollapsed = $derived(leftbar?.collapsed ?? false);
43
51
 
@@ -67,13 +75,20 @@
67
75
  class="nav-item {className}"
68
76
  class:active
69
77
  class:collapsed={isCollapsed}
78
+ data-variant={variant}
70
79
  title={isCollapsed ? label : null}
80
+ data-sveltekit-preload-data={preloadAttr}
71
81
  >
82
+ {@render leading?.()}
72
83
  {#if Icon}
73
84
  <Icon size={18} class="nav-icon" />
74
85
  {/if}
75
86
  {#if !isCollapsed}
76
87
  <span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
88
+ {#if badge != null}
89
+ <span class="nav-badge" transition:fade={{ duration: 150 }}>{badge}</span>
90
+ {/if}
91
+ {@render trailing?.()}
77
92
  {/if}
78
93
  {@render children?.()}
79
94
  </a>
@@ -81,16 +96,22 @@
81
96
  <button
82
97
  class="nav-item expandable {className}"
83
98
  class:collapsed={isCollapsed}
99
+ data-variant={variant}
84
100
  onclick={handleClick}
85
101
  onmouseenter={handleMouseEnter}
86
102
  onmouseleave={handleMouseLeave}
87
103
  title={isCollapsed ? label : null}
88
104
  >
105
+ {@render leading?.()}
89
106
  {#if Icon}
90
107
  <Icon size={18} class="nav-icon" />
91
108
  {/if}
92
109
  {#if !isCollapsed}
93
110
  <span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
111
+ {#if badge != null}
112
+ <span class="nav-badge" transition:fade={{ duration: 150 }}>{badge}</span>
113
+ {/if}
114
+ {@render trailing?.()}
94
115
  <span transition:fade={{ duration: 150 }}>
95
116
  <svg
96
117
  xmlns="http://www.w3.org/2000/svg"
@@ -109,6 +130,7 @@
109
130
  </svg>
110
131
  </span>
111
132
  {/if}
133
+ {@render children?.()}
112
134
  </button>
113
135
  {#if expanded && !isCollapsed && subroutes.length > 0}
114
136
  <ul class="subnav-list" transition:slide={{ duration: 200, easing: cubicOut }}>
@@ -130,14 +152,20 @@
130
152
  class="nav-item {className}"
131
153
  class:active
132
154
  class:collapsed={isCollapsed}
155
+ data-variant={variant}
133
156
  onclick={handleClick}
134
157
  title={isCollapsed ? label : null}
135
158
  >
159
+ {@render leading?.()}
136
160
  {#if Icon}
137
161
  <Icon size={18} class="nav-icon" />
138
162
  {/if}
139
163
  {#if !isCollapsed}
140
164
  <span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
165
+ {#if badge != null}
166
+ <span class="nav-badge" transition:fade={{ duration: 150 }}>{badge}</span>
167
+ {/if}
168
+ {@render trailing?.()}
141
169
  {/if}
142
170
  {@render children?.()}
143
171
  </button>
@@ -217,6 +245,16 @@
217
245
  overflow: hidden;
218
246
  }
219
247
 
248
+ .nav-badge {
249
+ padding: 0.125rem 0.375rem;
250
+ font-size: 0.625rem;
251
+ font-weight: 600;
252
+ background-color: color-mix(in srgb, var(--color-base0D) 10%, transparent);
253
+ color: var(--color-base0D);
254
+ border-radius: 9999px;
255
+ flex-shrink: 0;
256
+ }
257
+
220
258
  .expand-icon {
221
259
  color: var(--color-base05);
222
260
  flex-shrink: 0;
@@ -267,4 +305,44 @@
267
305
  font-weight: 500;
268
306
  background-color: color-mix(in srgb, var(--color-base0D) 15%, transparent);
269
307
  }
308
+
309
+ /* Variants */
310
+ .nav-item[data-variant="warning"] {
311
+ color: var(--color-base0A);
312
+ }
313
+
314
+ .nav-item[data-variant="warning"]:hover {
315
+ color: var(--color-base0A);
316
+ background-color: color-mix(in srgb, var(--color-base0A) 10%, transparent);
317
+ }
318
+
319
+ .nav-item[data-variant="warning"] :global(svg) {
320
+ color: var(--color-base0A);
321
+ }
322
+
323
+ .nav-item[data-variant="danger"] {
324
+ color: var(--color-base08);
325
+ }
326
+
327
+ .nav-item[data-variant="danger"]:hover {
328
+ color: var(--color-base08);
329
+ background-color: color-mix(in srgb, var(--color-base08) 10%, transparent);
330
+ }
331
+
332
+ .nav-item[data-variant="danger"] :global(svg) {
333
+ color: var(--color-base08);
334
+ }
335
+
336
+ .nav-item[data-variant="success"] {
337
+ color: var(--color-base0B);
338
+ }
339
+
340
+ .nav-item[data-variant="success"]:hover {
341
+ color: var(--color-base0B);
342
+ background-color: color-mix(in srgb, var(--color-base0B) 10%, transparent);
343
+ }
344
+
345
+ .nav-item[data-variant="success"] :global(svg) {
346
+ color: var(--color-base0B);
347
+ }
270
348
  </style>
@@ -73,7 +73,7 @@
73
73
  background: transparent;
74
74
  border: none;
75
75
  cursor: pointer;
76
- font: inherit;
76
+ font-family: inherit;
77
77
  }
78
78
 
79
79
  .collapse-toggle:hover {
@@ -4,7 +4,15 @@
4
4
  * Reusable navigation components including sidebars, tabs, and accordions.
5
5
  */
6
6
 
7
- // Sidebar Components
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
+ }