@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.
- package/package.json +2 -2
- package/src/components/navigation/DropdownContainer.svelte +575 -0
- package/src/components/navigation/LeftBar.svelte +179 -0
- package/src/components/navigation/LeftBarItem.svelte +267 -0
- package/src/components/navigation/LeftBarPopover.svelte +121 -0
- package/src/components/navigation/LeftBarSection.svelte +63 -0
- package/src/components/navigation/LeftBarToggle.svelte +87 -0
- package/src/components/primitives/Button.svelte +147 -94
- package/src/index.js +8 -0
|
@@ -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>
|