@miozu/jera 0.4.4 → 0.4.5

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.
@@ -0,0 +1,179 @@
1
+ <!--
2
+ @component LeftBar
3
+
4
+ A collapsible left sidebar with navigation sections, expandable groups, and hover popovers.
5
+ Exact 1:1 match with admin.selify.ai AdminSidebar styles.
6
+
7
+ @example Basic usage
8
+ <LeftBar
9
+ bind:collapsed
10
+ persistKey="my-sidebar"
11
+ >
12
+ {#snippet header()}
13
+ <DropdownContainer {themeState} {teamMember} {isCollapsed} />
14
+ {/snippet}
15
+
16
+ {#snippet navigation()}
17
+ <LeftBarSection>
18
+ <LeftBarItem href="/" icon={Home} label="Dashboard" active={isActive('/')} />
19
+ <LeftBarItem href="/settings" icon={Settings} label="Settings" />
20
+ </LeftBarSection>
21
+ {/snippet}
22
+
23
+ {#snippet footer()}
24
+ <LeftBarToggle />
25
+ {/snippet}
26
+ </LeftBar>
27
+ -->
28
+ <script>
29
+ import { setContext, onMount } from 'svelte';
30
+ import { slide, fade } from 'svelte/transition';
31
+ import { cubicOut } from 'svelte/easing';
32
+
33
+ let {
34
+ collapsed = $bindable(false),
35
+ persistKey = null,
36
+ class: className = '',
37
+ header,
38
+ navigation,
39
+ footer,
40
+ children
41
+ } = $props();
42
+
43
+ // Internal collapsed state
44
+ let isCollapsed = $state(collapsed);
45
+
46
+ // Sync states
47
+ $effect(() => {
48
+ isCollapsed = collapsed;
49
+ });
50
+
51
+ $effect(() => {
52
+ collapsed = isCollapsed;
53
+ });
54
+
55
+ // Toggle sidebar
56
+ function toggle() {
57
+ isCollapsed = !isCollapsed;
58
+ if (persistKey && typeof localStorage !== 'undefined') {
59
+ try {
60
+ localStorage.setItem(persistKey, String(isCollapsed));
61
+ } catch (e) {}
62
+ }
63
+ }
64
+
65
+ // Load saved state
66
+ onMount(() => {
67
+ if (persistKey && typeof localStorage !== 'undefined') {
68
+ try {
69
+ const saved = localStorage.getItem(persistKey);
70
+ if (saved !== null) {
71
+ isCollapsed = saved === 'true';
72
+ }
73
+ } catch (e) {}
74
+ }
75
+ });
76
+
77
+ // Hover popover state for collapsed sidebar
78
+ let hoverPopover = $state({
79
+ item: null,
80
+ position: { top: 0, left: 0 }
81
+ });
82
+ let hoverTimeout = null;
83
+
84
+ function showPopover(itemId, event) {
85
+ if (!isCollapsed) return;
86
+
87
+ if (hoverTimeout) clearTimeout(hoverTimeout);
88
+
89
+ const rect = event.currentTarget.getBoundingClientRect();
90
+ hoverPopover = {
91
+ item: itemId,
92
+ position: {
93
+ top: rect.top,
94
+ left: rect.right + 8
95
+ }
96
+ };
97
+ }
98
+
99
+ function hidePopover() {
100
+ hoverTimeout = setTimeout(() => {
101
+ hoverPopover.item = null;
102
+ }, 150);
103
+ }
104
+
105
+ function keepPopoverOpen() {
106
+ if (hoverTimeout) clearTimeout(hoverTimeout);
107
+ }
108
+
109
+ // Provide context to children
110
+ setContext('leftbar', {
111
+ get collapsed() { return isCollapsed; },
112
+ toggle,
113
+ expand() { isCollapsed = false; },
114
+ collapse() { isCollapsed = true; },
115
+ showPopover,
116
+ hidePopover,
117
+ keepPopoverOpen,
118
+ get hoverPopover() { return hoverPopover; }
119
+ });
120
+ </script>
121
+
122
+ <div class="workspace-sidebar {className}" class:collapsed={isCollapsed}>
123
+ <!-- Header slot (typically DropdownContainer) -->
124
+ {#if header}
125
+ {@render header()}
126
+ {/if}
127
+
128
+ <!-- Main Navigation -->
129
+ {#if navigation}
130
+ <nav class="main-nav">
131
+ {@render navigation()}
132
+ </nav>
133
+ {:else if children}
134
+ <nav class="main-nav">
135
+ {@render children()}
136
+ </nav>
137
+ {/if}
138
+
139
+ <!-- Footer with collapse toggle -->
140
+ {#if footer}
141
+ <div class="sidebar-footer">
142
+ {@render footer()}
143
+ </div>
144
+ {/if}
145
+ </div>
146
+
147
+ <style>
148
+ .workspace-sidebar {
149
+ display: flex;
150
+ flex-direction: column;
151
+ background-color: var(--color-base00);
152
+ height: 100vh;
153
+ position: fixed;
154
+ left: 0;
155
+ top: 0;
156
+ z-index: 40;
157
+ width: 240px;
158
+ transition: width 300ms cubic-bezier(0.4, 0, 0.2, 1);
159
+ will-change: width;
160
+ overflow: hidden;
161
+ }
162
+
163
+ .workspace-sidebar.collapsed {
164
+ width: 60px;
165
+ }
166
+
167
+ .main-nav {
168
+ padding-top: 0;
169
+ padding-bottom: 0;
170
+ }
171
+
172
+ .sidebar-footer {
173
+ margin-top: auto;
174
+ padding: 0.5rem;
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: 0.25rem;
178
+ }
179
+ </style>
@@ -0,0 +1,267 @@
1
+ <!--
2
+ @component LeftBarItem
3
+
4
+ A navigation item within LeftBar.
5
+ Exact 1:1 match with admin.selify.ai AdminSidebar nav-item styles.
6
+
7
+ @example Basic
8
+ <LeftBarItem href="/dashboard" icon={Home} label="Dashboard" active={isActive('/dashboard')} />
9
+
10
+ @example Expandable with subroutes
11
+ <LeftBarItem
12
+ label="Services"
13
+ icon={Server}
14
+ expandable
15
+ bind:expanded={servicesExpanded}
16
+ subroutes={[
17
+ { label: 'Overview', href: '/services' },
18
+ { label: 'Errors', href: '/errors' }
19
+ ]}
20
+ />
21
+ -->
22
+ <script>
23
+ import { getContext } from 'svelte';
24
+ import { slide, fade } from 'svelte/transition';
25
+ import { cubicOut } from 'svelte/easing';
26
+
27
+ let {
28
+ href = null,
29
+ label = '',
30
+ icon: Icon = null,
31
+ active = false,
32
+ expandable = false,
33
+ expanded = $bindable(false),
34
+ subroutes = [],
35
+ onclick = null,
36
+ isActiveRoute = () => false,
37
+ class: className = '',
38
+ children
39
+ } = $props();
40
+
41
+ const leftbar = getContext('leftbar');
42
+ const isCollapsed = $derived(leftbar?.collapsed ?? false);
43
+
44
+ function handleClick(e) {
45
+ if (expandable) {
46
+ e.preventDefault();
47
+ expanded = !expanded;
48
+ }
49
+ onclick?.(e);
50
+ }
51
+
52
+ function handleMouseEnter(e) {
53
+ if (expandable) {
54
+ leftbar?.showPopover?.(label, e);
55
+ }
56
+ }
57
+
58
+ function handleMouseLeave() {
59
+ leftbar?.hidePopover?.();
60
+ }
61
+ </script>
62
+
63
+ <li>
64
+ {#if href && !expandable}
65
+ <a
66
+ {href}
67
+ class="nav-item {className}"
68
+ class:active
69
+ class:collapsed={isCollapsed}
70
+ title={isCollapsed ? label : null}
71
+ >
72
+ {#if Icon}
73
+ <Icon size={18} class="nav-icon" />
74
+ {/if}
75
+ {#if !isCollapsed}
76
+ <span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
77
+ {/if}
78
+ {@render children?.()}
79
+ </a>
80
+ {:else if expandable}
81
+ <button
82
+ class="nav-item expandable {className}"
83
+ class:collapsed={isCollapsed}
84
+ onclick={handleClick}
85
+ onmouseenter={handleMouseEnter}
86
+ onmouseleave={handleMouseLeave}
87
+ title={isCollapsed ? label : null}
88
+ >
89
+ {#if Icon}
90
+ <Icon size={18} class="nav-icon" />
91
+ {/if}
92
+ {#if !isCollapsed}
93
+ <span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
94
+ <span transition:fade={{ duration: 150 }}>
95
+ <svg
96
+ xmlns="http://www.w3.org/2000/svg"
97
+ width="14"
98
+ height="14"
99
+ viewBox="0 0 24 24"
100
+ fill="none"
101
+ stroke="currentColor"
102
+ stroke-width="2"
103
+ stroke-linecap="round"
104
+ stroke-linejoin="round"
105
+ class="expand-icon"
106
+ class:rotate-180={expanded}
107
+ >
108
+ <polyline points="6 9 12 15 18 9"></polyline>
109
+ </svg>
110
+ </span>
111
+ {/if}
112
+ </button>
113
+ {#if expanded && !isCollapsed && subroutes.length > 0}
114
+ <ul class="subnav-list" transition:slide={{ duration: 200, easing: cubicOut }}>
115
+ {#each subroutes as route}
116
+ <li>
117
+ <a
118
+ href={route.href}
119
+ class="subnav-item"
120
+ class:active={isActiveRoute(route.href)}
121
+ >
122
+ {route.label}
123
+ </a>
124
+ </li>
125
+ {/each}
126
+ </ul>
127
+ {/if}
128
+ {:else}
129
+ <button
130
+ class="nav-item {className}"
131
+ class:active
132
+ class:collapsed={isCollapsed}
133
+ onclick={handleClick}
134
+ title={isCollapsed ? label : null}
135
+ >
136
+ {#if Icon}
137
+ <Icon size={18} class="nav-icon" />
138
+ {/if}
139
+ {#if !isCollapsed}
140
+ <span class="nav-label" transition:fade={{ duration: 150 }}>{label}</span>
141
+ {/if}
142
+ {@render children?.()}
143
+ </button>
144
+ {/if}
145
+ </li>
146
+
147
+ <style>
148
+ li {
149
+ list-style: none;
150
+ }
151
+
152
+ .nav-item {
153
+ width: calc(100% - 1rem);
154
+ padding: 0.375rem 0.75rem;
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 0.5rem;
158
+ font-size: 0.875rem;
159
+ color: var(--color-base06);
160
+ cursor: pointer;
161
+ border-radius: 0.375rem;
162
+ margin-left: 0.5rem;
163
+ margin-right: 0.5rem;
164
+ transition: all 150ms;
165
+ text-decoration: none;
166
+ overflow: hidden;
167
+ background: transparent;
168
+ border: none;
169
+ font: inherit;
170
+ text-align: left;
171
+ }
172
+
173
+ .nav-item:hover {
174
+ color: var(--color-base0D);
175
+ background-color: color-mix(in srgb, var(--color-base0D) 5%, transparent);
176
+ }
177
+
178
+ .nav-item.collapsed {
179
+ justify-content: center;
180
+ padding-left: 0.5rem;
181
+ padding-right: 0.5rem;
182
+ margin-left: 0.25rem;
183
+ margin-right: 0.25rem;
184
+ width: calc(100% - 0.5rem);
185
+ }
186
+
187
+ .nav-item.active {
188
+ background-color: color-mix(in srgb, var(--color-base0D) 15%, transparent);
189
+ color: var(--color-base0D);
190
+ font-weight: 500;
191
+ }
192
+
193
+ .nav-item.expandable {
194
+ position: relative;
195
+ }
196
+
197
+ .nav-item.expandable:hover {
198
+ color: var(--color-base0D);
199
+ background-color: color-mix(in srgb, var(--color-base0D) 5%, transparent);
200
+ }
201
+
202
+ .nav-item :global(svg.nav-icon) {
203
+ flex-shrink: 0;
204
+ transition: color 150ms;
205
+ }
206
+
207
+ .nav-item.expandable:hover :global(svg.nav-icon) {
208
+ color: var(--color-base0D);
209
+ }
210
+
211
+ .nav-label {
212
+ flex: 1;
213
+ text-align: left;
214
+ white-space: nowrap;
215
+ overflow: hidden;
216
+ }
217
+
218
+ .expand-icon {
219
+ color: var(--color-base04);
220
+ flex-shrink: 0;
221
+ margin-left: auto;
222
+ transition: all 200ms;
223
+ }
224
+
225
+ .nav-item.expandable:hover .expand-icon {
226
+ color: var(--color-base0D);
227
+ }
228
+
229
+ .rotate-180 {
230
+ transform: rotate(180deg);
231
+ }
232
+
233
+ /* Subnav */
234
+ .subnav-list {
235
+ margin-left: 1.75rem;
236
+ display: flex;
237
+ flex-direction: column;
238
+ list-style: none;
239
+ padding: 0;
240
+ }
241
+
242
+ .subnav-item {
243
+ display: block;
244
+ padding: 0.25rem 0.5rem;
245
+ font-size: 0.875rem;
246
+ color: var(--color-base04);
247
+ transition: color 150ms;
248
+ width: 100%;
249
+ text-align: left;
250
+ cursor: pointer;
251
+ background: transparent;
252
+ border: none;
253
+ border-radius: 0.375rem;
254
+ text-decoration: none;
255
+ }
256
+
257
+ .subnav-item:hover {
258
+ color: var(--color-base0D);
259
+ background-color: color-mix(in srgb, var(--color-base0D) 5%, transparent);
260
+ }
261
+
262
+ .subnav-item.active {
263
+ color: var(--color-base0D);
264
+ font-weight: 500;
265
+ background-color: color-mix(in srgb, var(--color-base0D) 15%, transparent);
266
+ }
267
+ </style>
@@ -0,0 +1,121 @@
1
+ <!--
2
+ @component LeftBarPopover
3
+
4
+ Hover popover for collapsed LeftBar showing subroutes.
5
+ Exact 1:1 match with admin.selify.ai AdminSidebar hover-popover styles.
6
+
7
+ @example
8
+ <LeftBarPopover
9
+ visible={hoverPopover.item === 'services'}
10
+ position={hoverPopover.position}
11
+ title="Services"
12
+ items={[
13
+ { label: 'Overview', href: '/services' },
14
+ { label: 'Errors', href: '/errors' }
15
+ ]}
16
+ />
17
+ -->
18
+ <script>
19
+ import { getContext } from 'svelte';
20
+
21
+ let {
22
+ visible = false,
23
+ position = { top: 0, left: 0 },
24
+ title = '',
25
+ items = [],
26
+ class: className = ''
27
+ } = $props();
28
+
29
+ const leftbar = getContext('leftbar');
30
+
31
+ function handleMouseEnter() {
32
+ leftbar?.keepPopoverOpen?.();
33
+ }
34
+
35
+ function handleMouseLeave() {
36
+ leftbar?.hidePopover?.();
37
+ }
38
+ </script>
39
+
40
+ {#if visible && items.length > 0}
41
+ <div
42
+ class="hover-popover {className}"
43
+ style="top: {position.top}px; left: {position.left}px;"
44
+ onmouseenter={handleMouseEnter}
45
+ onmouseleave={handleMouseLeave}
46
+ >
47
+ <div class="popover-header">
48
+ {title}
49
+ </div>
50
+ <ul class="popover-list">
51
+ {#each items as item}
52
+ <li>
53
+ <a href={item.href} class="popover-item">
54
+ {item.label}
55
+ </a>
56
+ </li>
57
+ {/each}
58
+ </ul>
59
+ </div>
60
+ {/if}
61
+
62
+ <style>
63
+ .hover-popover {
64
+ position: fixed;
65
+ z-index: 50;
66
+ background-color: var(--color-base00);
67
+ border: 1px solid color-mix(in srgb, var(--color-base03) 30%, transparent);
68
+ border-radius: 0.5rem;
69
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
70
+ min-width: 200px;
71
+ max-width: 280px;
72
+ overflow: hidden;
73
+ animation: popover-appear 0.15s ease-out;
74
+ }
75
+
76
+ @keyframes popover-appear {
77
+ from {
78
+ opacity: 0;
79
+ transform: translateX(-8px) scale(0.96);
80
+ }
81
+ to {
82
+ opacity: 1;
83
+ transform: translateX(0) scale(1);
84
+ }
85
+ }
86
+
87
+ .popover-header {
88
+ padding: 0.5rem 1rem;
89
+ font-size: 0.75rem;
90
+ font-weight: 600;
91
+ color: var(--color-base06);
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.05em;
94
+ background-color: color-mix(in srgb, var(--color-base01) 50%, transparent);
95
+ }
96
+
97
+ .popover-list {
98
+ padding: 0.25rem 0;
99
+ list-style: none;
100
+ margin: 0;
101
+ }
102
+
103
+ .popover-item {
104
+ display: block;
105
+ padding: 0.5rem 1rem;
106
+ font-size: 0.875rem;
107
+ color: var(--color-base06);
108
+ transition: all 150ms;
109
+ cursor: pointer;
110
+ text-decoration: none;
111
+ }
112
+
113
+ .popover-item:hover {
114
+ background-color: color-mix(in srgb, var(--color-base0D) 10%, transparent);
115
+ color: var(--color-base0D);
116
+ }
117
+
118
+ .popover-item:active {
119
+ background-color: color-mix(in srgb, var(--color-base0D) 15%, transparent);
120
+ }
121
+ </style>
@@ -0,0 +1,63 @@
1
+ <!--
2
+ @component LeftBarSection
3
+
4
+ A section within LeftBar with optional title.
5
+ Exact 1:1 match with admin.selify.ai AdminSidebar section styles.
6
+
7
+ @example
8
+ <LeftBarSection title="Administration">
9
+ <LeftBarItem href="/team" icon={Users} label="Team" />
10
+ <LeftBarItem href="/settings" icon={Settings} label="Settings" />
11
+ </LeftBarSection>
12
+ -->
13
+ <script>
14
+ import { getContext } from 'svelte';
15
+ import { fade } from 'svelte/transition';
16
+
17
+ let {
18
+ title = '',
19
+ class: className = '',
20
+ children
21
+ } = $props();
22
+
23
+ const leftbar = getContext('leftbar');
24
+ const isCollapsed = $derived(leftbar?.collapsed ?? false);
25
+ </script>
26
+
27
+ <div class="nav-section {className}">
28
+ {#if title && !isCollapsed}
29
+ <div class="section-header" transition:fade={{ duration: 150 }}>{title}</div>
30
+ {/if}
31
+ <ul class="nav-list">
32
+ {@render children?.()}
33
+ </ul>
34
+ </div>
35
+
36
+ <style>
37
+ .nav-section {
38
+ padding-top: 0;
39
+ padding-bottom: 0;
40
+ }
41
+
42
+ .section-header {
43
+ padding: 0.75rem 0.75rem 0.25rem 0.75rem;
44
+ margin-bottom: 0.25rem;
45
+ margin-top: 0.75rem;
46
+ font-size: 0.75rem;
47
+ font-weight: 600;
48
+ color: var(--color-base04);
49
+ text-align: left;
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.05em;
52
+ white-space: nowrap;
53
+ overflow: hidden;
54
+ }
55
+
56
+ .nav-list {
57
+ display: flex;
58
+ flex-direction: column;
59
+ list-style: none;
60
+ margin: 0;
61
+ padding: 0;
62
+ }
63
+ </style>
@@ -0,0 +1,87 @@
1
+ <!--
2
+ @component LeftBarToggle
3
+
4
+ Collapse/expand toggle button for LeftBar.
5
+ Exact 1:1 match with admin.selify.ai AdminSidebar collapse-toggle styles.
6
+
7
+ @example
8
+ <LeftBarToggle />
9
+ -->
10
+ <script>
11
+ import { getContext } from 'svelte';
12
+
13
+ let {
14
+ class: className = ''
15
+ } = $props();
16
+
17
+ const leftbar = getContext('leftbar');
18
+ const isCollapsed = $derived(leftbar?.collapsed ?? false);
19
+
20
+ function handleClick() {
21
+ leftbar?.toggle?.();
22
+ }
23
+ </script>
24
+
25
+ <button
26
+ class="collapse-toggle {className}"
27
+ onclick={handleClick}
28
+ title={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
29
+ >
30
+ {#if isCollapsed}
31
+ <!-- ChevronRight -->
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ width="16"
35
+ height="16"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ stroke-width="2"
40
+ stroke-linecap="round"
41
+ stroke-linejoin="round"
42
+ >
43
+ <polyline points="9 18 15 12 9 6"></polyline>
44
+ </svg>
45
+ {:else}
46
+ <!-- ChevronLeft -->
47
+ <svg
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ width="16"
50
+ height="16"
51
+ viewBox="0 0 24 24"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ stroke-width="2"
55
+ stroke-linecap="round"
56
+ stroke-linejoin="round"
57
+ >
58
+ <polyline points="15 18 9 12 15 6"></polyline>
59
+ </svg>
60
+ {/if}
61
+ </button>
62
+
63
+ <style>
64
+ .collapse-toggle {
65
+ width: 100%;
66
+ padding: 0.5rem;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ color: var(--color-base04);
71
+ border-radius: 0.25rem;
72
+ transition: all 150ms;
73
+ background: transparent;
74
+ border: none;
75
+ cursor: pointer;
76
+ font: inherit;
77
+ }
78
+
79
+ .collapse-toggle:hover {
80
+ color: var(--color-base06);
81
+ background-color: var(--color-base01);
82
+ }
83
+
84
+ .collapse-toggle :global(svg) {
85
+ flex-shrink: 0;
86
+ }
87
+ </style>