@r2digisolutions/components 0.11.0 → 0.12.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/dist/components/organisms/AppShell/AppShell.stories.d.ts +12 -0
- package/dist/components/organisms/AppShell/AppShell.stories.js +14 -2
- package/dist/components/organisms/AppShell/AppShell.svelte +199 -12
- package/dist/components/organisms/AppShell/AppShell.svelte.d.ts +16 -1
- package/dist/components/organisms/AppShell/AppShellStory.svelte +74 -7
- package/dist/components/organisms/AppShell/AppShellStory.svelte.d.ts +2 -0
- package/dist/components/organisms/AppShell/app-chrome.svelte.d.ts +18 -0
- package/dist/components/organisms/AppShell/app-chrome.svelte.js +8 -0
- package/dist/components/organisms/NavRail/NavRail.stories.d.ts +12 -0
- package/dist/components/organisms/NavRail/NavRail.stories.js +11 -0
- package/dist/components/organisms/NavRail/NavRail.svelte +119 -0
- package/dist/components/organisms/NavRail/NavRail.svelte.d.ts +24 -0
- package/dist/components/organisms/NavRail/NavRailStory.svelte +27 -0
- package/dist/components/organisms/NavRail/NavRailStory.svelte.d.ts +3 -0
- package/dist/components/organisms/Navbar/Navbar.svelte +26 -31
- package/dist/components/organisms/Sidebar/Sidebar.svelte +103 -48
- package/dist/components/organisms/Sidebar/Sidebar.svelte.d.ts +11 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@ declare const meta: {
|
|
|
4
4
|
component: import("svelte").Component<{
|
|
5
5
|
fullHeight?: boolean;
|
|
6
6
|
framed?: boolean;
|
|
7
|
+
showRail?: boolean;
|
|
8
|
+
showContextual?: boolean;
|
|
7
9
|
}, {}, "">;
|
|
8
10
|
tags: string[];
|
|
9
11
|
parameters: {
|
|
@@ -16,13 +18,23 @@ declare const meta: {
|
|
|
16
18
|
framed: {
|
|
17
19
|
control: "boolean";
|
|
18
20
|
};
|
|
21
|
+
showRail: {
|
|
22
|
+
control: "boolean";
|
|
23
|
+
};
|
|
24
|
+
showContextual: {
|
|
25
|
+
control: "boolean";
|
|
26
|
+
};
|
|
19
27
|
};
|
|
20
28
|
args: {
|
|
21
29
|
fullHeight: true;
|
|
22
30
|
framed: false;
|
|
31
|
+
showRail: false;
|
|
32
|
+
showContextual: false;
|
|
23
33
|
};
|
|
24
34
|
};
|
|
25
35
|
export default meta;
|
|
26
36
|
type Story = StoryObj<typeof meta>;
|
|
27
37
|
export declare const Fullscreen: Story;
|
|
38
|
+
export declare const WithRail: Story;
|
|
39
|
+
export declare const Coolify: Story;
|
|
28
40
|
export declare const FramedPreview: Story;
|
|
@@ -8,15 +8,27 @@ const meta = {
|
|
|
8
8
|
},
|
|
9
9
|
argTypes: {
|
|
10
10
|
fullHeight: { control: 'boolean' },
|
|
11
|
-
framed: { control: 'boolean' }
|
|
11
|
+
framed: { control: 'boolean' },
|
|
12
|
+
showRail: { control: 'boolean' },
|
|
13
|
+
showContextual: { control: 'boolean' }
|
|
12
14
|
},
|
|
13
15
|
args: {
|
|
14
16
|
fullHeight: true,
|
|
15
|
-
framed: false
|
|
17
|
+
framed: false,
|
|
18
|
+
showRail: false,
|
|
19
|
+
showContextual: false
|
|
16
20
|
}
|
|
17
21
|
};
|
|
18
22
|
export default meta;
|
|
19
23
|
export const Fullscreen = {};
|
|
24
|
+
export const WithRail = {
|
|
25
|
+
name: 'With icon rail',
|
|
26
|
+
args: { showRail: true }
|
|
27
|
+
};
|
|
28
|
+
export const Coolify = {
|
|
29
|
+
name: 'Rail + contextual',
|
|
30
|
+
args: { showRail: true, showContextual: true }
|
|
31
|
+
};
|
|
20
32
|
export const FramedPreview = {
|
|
21
33
|
name: 'Framed preview',
|
|
22
34
|
args: { fullHeight: false, framed: true },
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import Sidebar, { type SidebarGroup } from '../Sidebar/Sidebar.svelte';
|
|
4
4
|
import Navbar, { type NavbarLink } from '../Navbar/Navbar.svelte';
|
|
5
|
+
import NavRail, { type NavRailItem } from '../NavRail/NavRail.svelte';
|
|
6
|
+
import IconButton from '../../atoms/IconButton/IconButton.svelte';
|
|
7
|
+
import Badge from '../../atoms/Badge/Badge.svelte';
|
|
8
|
+
import {
|
|
9
|
+
AppChrome,
|
|
10
|
+
getAppChrome,
|
|
11
|
+
setAppChrome,
|
|
12
|
+
type AppShellContextual
|
|
13
|
+
} from './app-chrome.svelte.js';
|
|
5
14
|
|
|
6
15
|
interface AppShellProps {
|
|
7
16
|
brand?: string;
|
|
@@ -12,6 +21,20 @@
|
|
|
12
21
|
collapsed?: boolean;
|
|
13
22
|
showSidebar?: boolean;
|
|
14
23
|
showNavbar?: boolean;
|
|
24
|
+
/** Icon rail. Shown when the list is non-empty. */
|
|
25
|
+
rail?: NavRailItem[];
|
|
26
|
+
railFooter?: NavRailItem[];
|
|
27
|
+
railValue?: string;
|
|
28
|
+
railBrand?: Snippet;
|
|
29
|
+
onrailchange?: (id: string) => void;
|
|
30
|
+
/** Storybook / explicit contextual nav. Nested layouts can also set this via `getAppChrome()`. */
|
|
31
|
+
contextualGroups?: SidebarGroup[];
|
|
32
|
+
contextualValue?: string;
|
|
33
|
+
contextualBrand?: string;
|
|
34
|
+
contextualHeader?: Snippet;
|
|
35
|
+
oncontextualchange?: (id: string) => void;
|
|
36
|
+
onsidebarchange?: (id: string) => void;
|
|
37
|
+
sidebarFooter?: Snippet;
|
|
15
38
|
/**
|
|
16
39
|
* Fill the viewport. Use `false` + `framed` for compact Storybook previews.
|
|
17
40
|
*/
|
|
@@ -34,6 +57,18 @@
|
|
|
34
57
|
collapsed = $bindable(false),
|
|
35
58
|
showSidebar = true,
|
|
36
59
|
showNavbar = true,
|
|
60
|
+
rail = [],
|
|
61
|
+
railFooter = [],
|
|
62
|
+
railValue = $bindable(''),
|
|
63
|
+
railBrand,
|
|
64
|
+
onrailchange,
|
|
65
|
+
contextualGroups = [],
|
|
66
|
+
contextualValue = $bindable(''),
|
|
67
|
+
contextualBrand = '',
|
|
68
|
+
contextualHeader,
|
|
69
|
+
oncontextualchange,
|
|
70
|
+
onsidebarchange,
|
|
71
|
+
sidebarFooter,
|
|
37
72
|
fullHeight = true,
|
|
38
73
|
framed = false,
|
|
39
74
|
mainClass = '',
|
|
@@ -41,44 +76,196 @@
|
|
|
41
76
|
actions,
|
|
42
77
|
children
|
|
43
78
|
}: AppShellProps = $props();
|
|
79
|
+
|
|
80
|
+
const chrome = (() => {
|
|
81
|
+
try {
|
|
82
|
+
return getAppChrome();
|
|
83
|
+
} catch {
|
|
84
|
+
const created = new AppChrome();
|
|
85
|
+
setAppChrome(created);
|
|
86
|
+
return created;
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
const propContextual = $derived.by((): AppShellContextual | null => {
|
|
91
|
+
if (contextualGroups.length === 0) return null;
|
|
92
|
+
return {
|
|
93
|
+
groups: contextualGroups,
|
|
94
|
+
value: contextualValue,
|
|
95
|
+
brand: contextualBrand,
|
|
96
|
+
header: contextualHeader,
|
|
97
|
+
onchange: oncontextualchange
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const resolvedContextual = $derived(chrome.contextual ?? propContextual);
|
|
102
|
+
const showRail = $derived(rail.length > 0 || railFooter.length > 0);
|
|
103
|
+
let mobileOpen = $state(false);
|
|
104
|
+
|
|
105
|
+
const mobileShowsContextual = $derived(Boolean(resolvedContextual));
|
|
106
|
+
|
|
107
|
+
function closeMobile() {
|
|
108
|
+
mobileOpen = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onPrimaryChange(id: string) {
|
|
112
|
+
onsidebarchange?.(id);
|
|
113
|
+
closeMobile();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onContextualNav(id: string) {
|
|
117
|
+
contextualValue = id;
|
|
118
|
+
resolvedContextual?.onchange?.(id);
|
|
119
|
+
closeMobile();
|
|
120
|
+
}
|
|
44
121
|
</script>
|
|
45
122
|
|
|
46
123
|
<div
|
|
47
124
|
class={[
|
|
48
|
-
'flex w-full overflow-hidden
|
|
125
|
+
'bg-surface relative flex w-full overflow-hidden',
|
|
49
126
|
fullHeight && 'h-dvh',
|
|
50
|
-
!fullHeight && framed && 'h-
|
|
127
|
+
!fullHeight && framed && 'min-h-0 h-full',
|
|
51
128
|
!fullHeight && !framed && 'h-[min(36rem,80vh)]',
|
|
52
|
-
framed && 'rounded-2xl border
|
|
129
|
+
framed && 'rounded-2xl border-border shadow-sm border',
|
|
53
130
|
className
|
|
54
131
|
]}
|
|
55
132
|
>
|
|
56
|
-
{#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
133
|
+
{#snippet contextualHeaderDefault()}
|
|
134
|
+
{#if resolvedContextual}
|
|
135
|
+
<div class="min-w-0 space-y-1">
|
|
136
|
+
{#if resolvedContextual.parentHref && resolvedContextual.parentLabel}
|
|
137
|
+
<a
|
|
138
|
+
href={resolvedContextual.parentHref}
|
|
139
|
+
class="text-muted hover:text-primary block truncate text-[11px]"
|
|
140
|
+
>
|
|
141
|
+
{resolvedContextual.parentLabel}
|
|
142
|
+
</a>
|
|
143
|
+
{/if}
|
|
144
|
+
<p class="text-sm font-semibold text-primary truncate">{resolvedContextual.brand}</p>
|
|
145
|
+
<div class="gap-1.5 flex flex-wrap items-center">
|
|
146
|
+
{#if resolvedContextual.status}
|
|
147
|
+
<Badge variant="secondary" size="sm">{resolvedContextual.status}</Badge>
|
|
148
|
+
{/if}
|
|
149
|
+
{#if resolvedContextual.description}
|
|
150
|
+
<span class="text-muted truncate text-[11px]">{resolvedContextual.description}</span>
|
|
151
|
+
{/if}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
{/snippet}
|
|
156
|
+
|
|
157
|
+
{#if showRail}
|
|
158
|
+
<NavRail
|
|
159
|
+
items={rail}
|
|
160
|
+
footerItems={railFooter}
|
|
161
|
+
bind:value={railValue}
|
|
162
|
+
brand={railBrand}
|
|
163
|
+
onchange={onrailchange}
|
|
62
164
|
/>
|
|
63
165
|
{/if}
|
|
64
|
-
|
|
166
|
+
|
|
167
|
+
{#if mobileOpen}
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
class="inset-0 bg-black/40 md:hidden fixed z-30"
|
|
171
|
+
aria-label="Cerrar menú"
|
|
172
|
+
onclick={closeMobile}
|
|
173
|
+
></button>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
{#if showSidebar}
|
|
177
|
+
<div
|
|
178
|
+
class={[
|
|
179
|
+
'z-40 h-full shrink-0',
|
|
180
|
+
mobileShowsContextual && 'max-md:hidden',
|
|
181
|
+
!mobileOpen && 'max-md:hidden',
|
|
182
|
+
mobileOpen &&
|
|
183
|
+
!mobileShowsContextual &&
|
|
184
|
+
'max-md:fixed max-md:inset-y-0 max-md:z-40 max-md:shadow-xl',
|
|
185
|
+
mobileOpen && !mobileShowsContextual && showRail && 'max-md:left-14',
|
|
186
|
+
mobileOpen && !mobileShowsContextual && !showRail && 'max-md:left-0'
|
|
187
|
+
]}
|
|
188
|
+
>
|
|
189
|
+
<Sidebar
|
|
190
|
+
{brand}
|
|
191
|
+
groups={sidebarGroups}
|
|
192
|
+
bind:value={sidebarValue}
|
|
193
|
+
bind:collapsed
|
|
194
|
+
footer={sidebarFooter}
|
|
195
|
+
onchange={onPrimaryChange}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
|
|
200
|
+
{#if resolvedContextual}
|
|
201
|
+
<div
|
|
202
|
+
class={[
|
|
203
|
+
'z-40 h-full shrink-0',
|
|
204
|
+
!mobileOpen && 'max-md:hidden',
|
|
205
|
+
mobileOpen &&
|
|
206
|
+
mobileShowsContextual &&
|
|
207
|
+
'max-md:fixed max-md:inset-y-0 max-md:z-40 max-md:shadow-xl',
|
|
208
|
+
mobileOpen && mobileShowsContextual && showRail && 'max-md:left-14',
|
|
209
|
+
mobileOpen && mobileShowsContextual && !showRail && 'max-md:left-0'
|
|
210
|
+
]}
|
|
211
|
+
>
|
|
212
|
+
<Sidebar
|
|
213
|
+
brand={resolvedContextual.brand ?? brand}
|
|
214
|
+
groups={resolvedContextual.groups}
|
|
215
|
+
value={resolvedContextual.value}
|
|
216
|
+
collapsible={false}
|
|
217
|
+
header={resolvedContextual.header ?? contextualHeaderDefault}
|
|
218
|
+
onchange={onContextualNav}
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
{/if}
|
|
222
|
+
|
|
223
|
+
<div class="min-h-0 min-w-0 flex flex-1 flex-col">
|
|
65
224
|
{#if showNavbar}
|
|
66
225
|
<Navbar
|
|
67
226
|
{brand}
|
|
68
227
|
links={navLinks}
|
|
69
228
|
bind:value={navValue}
|
|
70
|
-
showBrand={!showSidebar}
|
|
229
|
+
showBrand={!showSidebar && !showRail}
|
|
71
230
|
variant="pills"
|
|
72
231
|
size="sm"
|
|
73
232
|
bordered
|
|
74
233
|
maxWidth="full"
|
|
75
234
|
>
|
|
235
|
+
{#snippet leading()}
|
|
236
|
+
<div class="md:hidden">
|
|
237
|
+
<IconButton
|
|
238
|
+
label={mobileOpen ? 'Cerrar menú' : 'Abrir menú'}
|
|
239
|
+
size="sm"
|
|
240
|
+
onclick={() => (mobileOpen = !mobileOpen)}
|
|
241
|
+
>
|
|
242
|
+
<svg
|
|
243
|
+
class="h-4 w-4"
|
|
244
|
+
viewBox="0 0 24 24"
|
|
245
|
+
fill="none"
|
|
246
|
+
stroke="currentColor"
|
|
247
|
+
stroke-width="2"
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
>
|
|
250
|
+
{#if mobileOpen}
|
|
251
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
252
|
+
{:else}
|
|
253
|
+
<path
|
|
254
|
+
stroke-linecap="round"
|
|
255
|
+
stroke-linejoin="round"
|
|
256
|
+
d="M4 6h16M4 12h16M4 18h16"
|
|
257
|
+
/>
|
|
258
|
+
{/if}
|
|
259
|
+
</svg>
|
|
260
|
+
</IconButton>
|
|
261
|
+
</div>
|
|
262
|
+
{/snippet}
|
|
76
263
|
{#snippet actions()}
|
|
77
264
|
{#if actions}{@render actions()}{/if}
|
|
78
265
|
{/snippet}
|
|
79
266
|
</Navbar>
|
|
80
267
|
{/if}
|
|
81
|
-
<main class={['min-h-0 flex-1 overflow-auto
|
|
268
|
+
<main class={['min-h-0 p-6 flex-1 overflow-auto', mainClass]}>
|
|
82
269
|
{#if children}{@render children()}{/if}
|
|
83
270
|
</main>
|
|
84
271
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import { type SidebarGroup } from '../Sidebar/Sidebar.svelte';
|
|
3
3
|
import { type NavbarLink } from '../Navbar/Navbar.svelte';
|
|
4
|
+
import { type NavRailItem } from '../NavRail/NavRail.svelte';
|
|
4
5
|
interface AppShellProps {
|
|
5
6
|
brand?: string;
|
|
6
7
|
sidebarGroups?: SidebarGroup[];
|
|
@@ -10,6 +11,20 @@ interface AppShellProps {
|
|
|
10
11
|
collapsed?: boolean;
|
|
11
12
|
showSidebar?: boolean;
|
|
12
13
|
showNavbar?: boolean;
|
|
14
|
+
/** Icon rail. Shown when the list is non-empty. */
|
|
15
|
+
rail?: NavRailItem[];
|
|
16
|
+
railFooter?: NavRailItem[];
|
|
17
|
+
railValue?: string;
|
|
18
|
+
railBrand?: Snippet;
|
|
19
|
+
onrailchange?: (id: string) => void;
|
|
20
|
+
/** Storybook / explicit contextual nav. Nested layouts can also set this via `getAppChrome()`. */
|
|
21
|
+
contextualGroups?: SidebarGroup[];
|
|
22
|
+
contextualValue?: string;
|
|
23
|
+
contextualBrand?: string;
|
|
24
|
+
contextualHeader?: Snippet;
|
|
25
|
+
oncontextualchange?: (id: string) => void;
|
|
26
|
+
onsidebarchange?: (id: string) => void;
|
|
27
|
+
sidebarFooter?: Snippet;
|
|
13
28
|
/**
|
|
14
29
|
* Fill the viewport. Use `false` + `framed` for compact Storybook previews.
|
|
15
30
|
*/
|
|
@@ -22,6 +37,6 @@ interface AppShellProps {
|
|
|
22
37
|
actions?: Snippet;
|
|
23
38
|
children?: Snippet;
|
|
24
39
|
}
|
|
25
|
-
declare const AppShell: import("svelte").Component<AppShellProps, {}, "collapsed" | "sidebarValue" | "navValue">;
|
|
40
|
+
declare const AppShell: import("svelte").Component<AppShellProps, {}, "collapsed" | "sidebarValue" | "navValue" | "railValue" | "contextualValue">;
|
|
26
41
|
type AppShell = ReturnType<typeof AppShell>;
|
|
27
42
|
export default AppShell;
|
|
@@ -1,26 +1,68 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import AppShell from './AppShell.svelte';
|
|
3
3
|
import type { SidebarGroup } from '../Sidebar/Sidebar.svelte';
|
|
4
|
+
import type { NavRailItem } from '../NavRail/NavRail.svelte';
|
|
4
5
|
import type { NavbarLink } from '../Navbar/Navbar.svelte';
|
|
5
6
|
import Button from '../../atoms/Button/Button.svelte';
|
|
6
7
|
import Avatar from '../../atoms/Avatar/Avatar.svelte';
|
|
8
|
+
import Badge from '../../atoms/Badge/Badge.svelte';
|
|
9
|
+
import BrandMark from '../../atoms/BrandMark/BrandMark.svelte';
|
|
7
10
|
|
|
8
11
|
let {
|
|
9
12
|
fullHeight = true,
|
|
10
|
-
framed = false
|
|
13
|
+
framed = false,
|
|
14
|
+
showRail = true,
|
|
15
|
+
showContextual = false
|
|
11
16
|
}: {
|
|
12
17
|
fullHeight?: boolean;
|
|
13
18
|
framed?: boolean;
|
|
19
|
+
showRail?: boolean;
|
|
20
|
+
showContextual?: boolean;
|
|
14
21
|
} = $props();
|
|
15
22
|
|
|
16
23
|
const sidebarGroups: SidebarGroup[] = [
|
|
17
24
|
{
|
|
18
|
-
id: '
|
|
25
|
+
id: 'ops',
|
|
19
26
|
label: 'Workspace',
|
|
20
27
|
items: [
|
|
21
|
-
{ id: 'home', label: '
|
|
22
|
-
{ id: 'projects', label: 'Projects' },
|
|
23
|
-
{ id: 'billing', label: 'Billing' }
|
|
28
|
+
{ id: 'home', label: 'Dashboard', href: '#' },
|
|
29
|
+
{ id: 'projects', label: 'Projects', href: '#' },
|
|
30
|
+
{ id: 'billing', label: 'Billing', href: '#' }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'manage',
|
|
35
|
+
label: 'Manage',
|
|
36
|
+
items: [
|
|
37
|
+
{ id: 'team', label: 'Team', href: '#' },
|
|
38
|
+
{ id: 'settings', label: 'Settings', href: '#' }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const rail: NavRailItem[] = [
|
|
44
|
+
{ id: 'home', label: 'Home', href: '#' },
|
|
45
|
+
{ id: 'atom', label: 'Atom', href: '#' },
|
|
46
|
+
{ id: 'crm', label: 'CRM', href: '#' }
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const railFooter: NavRailItem[] = [{ id: 'settings', label: 'Settings', href: '#' }];
|
|
50
|
+
|
|
51
|
+
const contextualGroups: SidebarGroup[] = [
|
|
52
|
+
{
|
|
53
|
+
id: 'general',
|
|
54
|
+
label: 'General',
|
|
55
|
+
items: [
|
|
56
|
+
{ id: 'overview', label: 'Overview', href: '#' },
|
|
57
|
+
{ id: 'datos', label: 'Details', href: '#' }
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'biz',
|
|
62
|
+
label: 'Business',
|
|
63
|
+
items: [
|
|
64
|
+
{ id: 'contracts', label: 'Clients', href: '#' },
|
|
65
|
+
{ id: 'plans', label: 'Plans', href: '#' }
|
|
24
66
|
]
|
|
25
67
|
}
|
|
26
68
|
];
|
|
@@ -33,6 +75,8 @@
|
|
|
33
75
|
|
|
34
76
|
let sidebarValue = $state('projects');
|
|
35
77
|
let navValue = $state('overview');
|
|
78
|
+
let railValue = $state('atom');
|
|
79
|
+
let contextualValue = $state('overview');
|
|
36
80
|
</script>
|
|
37
81
|
|
|
38
82
|
<AppShell
|
|
@@ -40,24 +84,47 @@
|
|
|
40
84
|
{navLinks}
|
|
41
85
|
{fullHeight}
|
|
42
86
|
{framed}
|
|
87
|
+
rail={showRail ? rail : []}
|
|
88
|
+
railFooter={showRail ? railFooter : []}
|
|
89
|
+
contextualGroups={showContextual ? contextualGroups : []}
|
|
90
|
+
contextualBrand="Atom"
|
|
43
91
|
bind:sidebarValue
|
|
44
92
|
bind:navValue
|
|
93
|
+
bind:railValue
|
|
94
|
+
bind:contextualValue
|
|
45
95
|
>
|
|
96
|
+
{#snippet railBrand()}
|
|
97
|
+
<BrandMark mark="R2" size="sm" />
|
|
98
|
+
{/snippet}
|
|
99
|
+
{#snippet contextualHeader()}
|
|
100
|
+
<div class="min-w-0">
|
|
101
|
+
<p class="text-sm font-semibold text-primary truncate">Atom</p>
|
|
102
|
+
<div class="mt-1 gap-1.5 flex items-center">
|
|
103
|
+
<Badge variant="success" size="sm" dot>active</Badge>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
{/snippet}
|
|
46
107
|
{#snippet actions()}
|
|
47
108
|
<Button size="sm">New</Button>
|
|
48
109
|
<Avatar name="Rafa" size="sm" />
|
|
49
110
|
{/snippet}
|
|
50
111
|
<div class="space-y-2">
|
|
51
|
-
<h1 class="text-xl font-semibold
|
|
112
|
+
<h1 class="text-xl font-semibold text-primary capitalize">{navValue}</h1>
|
|
52
113
|
<p class="text-sm text-secondary">
|
|
53
114
|
Sidebar: <span class="font-medium text-primary">{sidebarValue}</span>
|
|
115
|
+
{#if showRail}
|
|
116
|
+
· Rail: <span class="font-medium text-primary">{railValue}</span>
|
|
117
|
+
{/if}
|
|
118
|
+
{#if showContextual}
|
|
119
|
+
· Contextual: <span class="font-medium text-primary">{contextualValue}</span>
|
|
120
|
+
{/if}
|
|
54
121
|
</p>
|
|
55
122
|
<p class="max-w-lg text-sm text-muted">
|
|
56
123
|
Scrollable content area. The shell fills the viewport so sidebar and main column reach the
|
|
57
124
|
bottom.
|
|
58
125
|
</p>
|
|
59
126
|
{#each Array.from({ length: 12 }, (_, i) => i + 1) as n (n)}
|
|
60
|
-
<div class="rounded-xl border
|
|
127
|
+
<div class="rounded-xl border-border bg-surface-elevated p-4 text-sm text-secondary border">
|
|
61
128
|
Content block {n}
|
|
62
129
|
</div>
|
|
63
130
|
{/each}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
type $$ComponentProps = {
|
|
2
2
|
fullHeight?: boolean;
|
|
3
3
|
framed?: boolean;
|
|
4
|
+
showRail?: boolean;
|
|
5
|
+
showContextual?: boolean;
|
|
4
6
|
};
|
|
5
7
|
declare const AppShellStory: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
8
|
type AppShellStory = ReturnType<typeof AppShellStory>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { SidebarGroup } from '../Sidebar/Sidebar.svelte';
|
|
3
|
+
export interface AppShellContextual {
|
|
4
|
+
groups: SidebarGroup[];
|
|
5
|
+
value: string;
|
|
6
|
+
brand?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
parentHref?: string;
|
|
10
|
+
parentLabel?: string;
|
|
11
|
+
header?: Snippet;
|
|
12
|
+
onchange?: (id: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare class AppChrome {
|
|
15
|
+
contextual: AppShellContextual | null;
|
|
16
|
+
setContextual(nav: AppShellContextual | null): void;
|
|
17
|
+
}
|
|
18
|
+
export declare const getAppChrome: () => AppChrome, setAppChrome: (context: AppChrome) => AppChrome;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/svelte';
|
|
2
|
+
declare const meta: {
|
|
3
|
+
title: string;
|
|
4
|
+
component: import("svelte").Component<Record<string, never>, {}, "">;
|
|
5
|
+
tags: string[];
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: string;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
export declare const Default: Story;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
import Tooltip from '../../atoms/Tooltip/Tooltip.svelte';
|
|
4
|
+
import RailMark from '../../atoms/RailMark/RailMark.svelte';
|
|
5
|
+
import Divider from '../../atoms/Divider/Divider.svelte';
|
|
6
|
+
|
|
7
|
+
export type NavRailIcon = Component<{
|
|
8
|
+
class?: string;
|
|
9
|
+
size?: number | string;
|
|
10
|
+
strokeWidth?: number | string;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
export interface NavRailItem {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
href?: string;
|
|
17
|
+
icon?: NavRailIcon;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface NavRailProps {
|
|
22
|
+
items?: NavRailItem[];
|
|
23
|
+
footerItems?: NavRailItem[];
|
|
24
|
+
value?: string;
|
|
25
|
+
class?: string;
|
|
26
|
+
brand?: Snippet;
|
|
27
|
+
onchange?: (id: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
items = [],
|
|
32
|
+
footerItems = [],
|
|
33
|
+
value = $bindable(''),
|
|
34
|
+
class: className = '',
|
|
35
|
+
brand,
|
|
36
|
+
onchange
|
|
37
|
+
}: NavRailProps = $props();
|
|
38
|
+
|
|
39
|
+
function select(id: string, disabled?: boolean) {
|
|
40
|
+
if (disabled) return;
|
|
41
|
+
value = id;
|
|
42
|
+
onchange?.(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const itemClass = (item: NavRailItem) => [
|
|
46
|
+
'relative flex h-10 w-10 items-center justify-center rounded-lg transition-colors',
|
|
47
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
|
|
48
|
+
value === item.id
|
|
49
|
+
? 'bg-brand-50 text-brand-700 dark:bg-brand-950/40 dark:text-brand-300'
|
|
50
|
+
: 'text-secondary hover:bg-surface-overlay hover:text-primary',
|
|
51
|
+
item.disabled && 'pointer-events-none cursor-not-allowed opacity-40'
|
|
52
|
+
];
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#snippet railButton(item: NavRailItem)}
|
|
56
|
+
<Tooltip content={item.label} side="right">
|
|
57
|
+
<span class="relative inline-flex">
|
|
58
|
+
<RailMark active={value === item.id} length="short" inset="sm" />
|
|
59
|
+
{#if item.href && !item.disabled}
|
|
60
|
+
<a
|
|
61
|
+
href={item.href}
|
|
62
|
+
aria-label={item.label}
|
|
63
|
+
aria-current={value === item.id ? 'page' : undefined}
|
|
64
|
+
class={itemClass(item)}
|
|
65
|
+
onclick={() => select(item.id, item.disabled)}
|
|
66
|
+
>
|
|
67
|
+
{#if item.icon}
|
|
68
|
+
<item.icon class="h-5 w-5" strokeWidth={1.75} />
|
|
69
|
+
{:else}
|
|
70
|
+
<span class="text-xs font-semibold">{item.label.slice(0, 1)}</span>
|
|
71
|
+
{/if}
|
|
72
|
+
</a>
|
|
73
|
+
{:else}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
aria-label={item.label}
|
|
77
|
+
aria-current={value === item.id ? 'page' : undefined}
|
|
78
|
+
disabled={item.disabled}
|
|
79
|
+
class={itemClass(item)}
|
|
80
|
+
onclick={() => select(item.id, item.disabled)}
|
|
81
|
+
>
|
|
82
|
+
{#if item.icon}
|
|
83
|
+
<item.icon class="h-5 w-5" strokeWidth={1.75} />
|
|
84
|
+
{:else}
|
|
85
|
+
<span class="text-xs font-semibold">{item.label.slice(0, 1)}</span>
|
|
86
|
+
{/if}
|
|
87
|
+
</button>
|
|
88
|
+
{/if}
|
|
89
|
+
</span>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
{/snippet}
|
|
92
|
+
|
|
93
|
+
<aside
|
|
94
|
+
class={[
|
|
95
|
+
'w-14 border-border bg-surface-elevated py-3 flex h-full shrink-0 flex-col items-center border-r',
|
|
96
|
+
className
|
|
97
|
+
]}
|
|
98
|
+
>
|
|
99
|
+
{#if brand}
|
|
100
|
+
<div class="mb-3 px-2 flex items-center justify-center">
|
|
101
|
+
{@render brand()}
|
|
102
|
+
</div>
|
|
103
|
+
{/if}
|
|
104
|
+
|
|
105
|
+
<nav class="gap-1 flex flex-1 flex-col items-center" aria-label="Rail">
|
|
106
|
+
{#each items as item (item.id)}
|
|
107
|
+
{@render railButton(item)}
|
|
108
|
+
{/each}
|
|
109
|
+
</nav>
|
|
110
|
+
|
|
111
|
+
{#if footerItems.length > 0}
|
|
112
|
+
<div class="w-8 py-2"><Divider /></div>
|
|
113
|
+
<nav class="gap-1 flex flex-col items-center" aria-label="Rail footer">
|
|
114
|
+
{#each footerItems as item (item.id)}
|
|
115
|
+
{@render railButton(item)}
|
|
116
|
+
{/each}
|
|
117
|
+
</nav>
|
|
118
|
+
{/if}
|
|
119
|
+
</aside>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
export type NavRailIcon = Component<{
|
|
3
|
+
class?: string;
|
|
4
|
+
size?: number | string;
|
|
5
|
+
strokeWidth?: number | string;
|
|
6
|
+
}>;
|
|
7
|
+
export interface NavRailItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
href?: string;
|
|
11
|
+
icon?: NavRailIcon;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface NavRailProps {
|
|
15
|
+
items?: NavRailItem[];
|
|
16
|
+
footerItems?: NavRailItem[];
|
|
17
|
+
value?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
brand?: Snippet;
|
|
20
|
+
onchange?: (id: string) => void;
|
|
21
|
+
}
|
|
22
|
+
declare const NavRail: Component<NavRailProps, {}, "value">;
|
|
23
|
+
type NavRail = ReturnType<typeof NavRail>;
|
|
24
|
+
export default NavRail;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import NavRail, { type NavRailItem } from './NavRail.svelte';
|
|
3
|
+
import BrandMark from '../../atoms/BrandMark/BrandMark.svelte';
|
|
4
|
+
|
|
5
|
+
const items: NavRailItem[] = [
|
|
6
|
+
{ id: 'home', label: 'Dashboard', href: '#' },
|
|
7
|
+
{ id: 'atom', label: 'Atom', href: '#' },
|
|
8
|
+
{ id: 'crm', label: 'CRM', href: '#' }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const footerItems: NavRailItem[] = [{ id: 'settings', label: 'Ajustes', href: '#' }];
|
|
12
|
+
|
|
13
|
+
let value = $state('atom');
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="rounded-xl border-border bg-surface h-[28rem] overflow-hidden border">
|
|
17
|
+
<div class="flex h-full">
|
|
18
|
+
<NavRail {items} {footerItems} bind:value>
|
|
19
|
+
{#snippet brand()}
|
|
20
|
+
<BrandMark mark="R2" size="sm" />
|
|
21
|
+
{/snippet}
|
|
22
|
+
</NavRail>
|
|
23
|
+
<div class="p-6 text-sm text-secondary flex flex-1 items-center justify-center">
|
|
24
|
+
Selected: <span class="ml-1 font-medium text-primary">{value}</span>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
@@ -68,42 +68,37 @@
|
|
|
68
68
|
|
|
69
69
|
<header
|
|
70
70
|
class={[
|
|
71
|
-
'relative z-40 w-full
|
|
72
|
-
bordered && 'border-
|
|
73
|
-
sticky && '
|
|
71
|
+
'bg-surface-elevated relative z-40 w-full',
|
|
72
|
+
bordered && 'border-border border-b',
|
|
73
|
+
sticky && 'top-0 sticky',
|
|
74
74
|
blur && 'bg-surface-elevated/85 backdrop-blur-md',
|
|
75
75
|
className
|
|
76
76
|
]}
|
|
77
77
|
>
|
|
78
78
|
<div
|
|
79
|
-
class={[
|
|
80
|
-
'mx-auto flex w-full items-center gap-3 px-4 sm:gap-4 sm:px-6',
|
|
81
|
-
height,
|
|
82
|
-
maxWidthClass
|
|
83
|
-
]}
|
|
79
|
+
class={['gap-3 px-4 sm:gap-4 sm:px-6 mx-auto flex w-full items-center', height, maxWidthClass]}
|
|
84
80
|
>
|
|
81
|
+
{#if leading}
|
|
82
|
+
<div class="flex shrink-0 items-center">{@render leading()}</div>
|
|
83
|
+
{/if}
|
|
85
84
|
{#if showBrand}
|
|
86
|
-
<div class="flex shrink-0 items-center
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
>
|
|
97
|
-
{brand.slice(0, 1).toUpperCase()}
|
|
98
|
-
</span>
|
|
99
|
-
{/if}
|
|
85
|
+
<div class="gap-2 flex shrink-0 items-center">
|
|
86
|
+
<span
|
|
87
|
+
class={[
|
|
88
|
+
'rounded-lg bg-brand-500 font-bold text-white flex items-center justify-center',
|
|
89
|
+
size === 'lg' ? 'h-8 w-8 text-xs' : 'h-7 w-7 text-[11px]'
|
|
90
|
+
]}
|
|
91
|
+
aria-hidden="true"
|
|
92
|
+
>
|
|
93
|
+
{brand.slice(0, 1).toUpperCase()}
|
|
94
|
+
</span>
|
|
100
95
|
<span class={['font-semibold tracking-tight text-primary', brandSize]}>{brand}</span>
|
|
101
96
|
</div>
|
|
102
97
|
{/if}
|
|
103
98
|
|
|
104
99
|
<nav
|
|
105
100
|
class={[
|
|
106
|
-
'
|
|
101
|
+
'min-w-0 gap-0.5 md:flex hidden h-full items-stretch',
|
|
107
102
|
centerLinks ? 'flex-1 justify-center' : 'flex-1',
|
|
108
103
|
!showBrand && !centerLinks && 'flex-1'
|
|
109
104
|
]}
|
|
@@ -116,14 +111,14 @@
|
|
|
116
111
|
disabled={link.disabled}
|
|
117
112
|
onclick={() => select(link.id, link.disabled)}
|
|
118
113
|
class={[
|
|
119
|
-
'
|
|
120
|
-
'focus-visible:
|
|
114
|
+
'gap-1.5 px-3 font-medium relative flex items-center transition-colors',
|
|
115
|
+
'focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
|
|
121
116
|
linkSize,
|
|
122
117
|
variant === 'underline' && 'border-b-2',
|
|
123
118
|
variant === 'underline' &&
|
|
124
119
|
(active
|
|
125
120
|
? 'border-brand-500 text-primary'
|
|
126
|
-
: '
|
|
121
|
+
: 'text-secondary hover:text-primary border-transparent'),
|
|
127
122
|
variant === 'pills' && 'my-2 rounded-lg',
|
|
128
123
|
variant === 'pills' &&
|
|
129
124
|
(active
|
|
@@ -137,7 +132,7 @@
|
|
|
137
132
|
{link.label}
|
|
138
133
|
{#if link.badge !== undefined && link.badge !== ''}
|
|
139
134
|
<span
|
|
140
|
-
class="
|
|
135
|
+
class="bg-brand-50 px-1.5 py-0.5 font-semibold text-brand-700 dark:bg-brand-950/50 dark:text-brand-300 rounded-full text-[10px]"
|
|
141
136
|
>
|
|
142
137
|
{link.badge}
|
|
143
138
|
</span>
|
|
@@ -147,7 +142,7 @@
|
|
|
147
142
|
</nav>
|
|
148
143
|
|
|
149
144
|
<nav
|
|
150
|
-
class="
|
|
145
|
+
class="min-w-0 gap-0.5 md:hidden flex flex-1 items-center overflow-x-auto"
|
|
151
146
|
aria-label="Primary"
|
|
152
147
|
>
|
|
153
148
|
{#each links as link (link.id)}
|
|
@@ -156,8 +151,8 @@
|
|
|
156
151
|
disabled={link.disabled}
|
|
157
152
|
onclick={() => select(link.id, link.disabled)}
|
|
158
153
|
class={[
|
|
159
|
-
'
|
|
160
|
-
'focus-visible:
|
|
154
|
+
'px-3 py-1 text-xs font-medium shrink-0 rounded-full transition-colors',
|
|
155
|
+
'focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none',
|
|
161
156
|
value === link.id
|
|
162
157
|
? 'bg-brand-50 text-brand-700 dark:bg-brand-950/50 dark:text-brand-300'
|
|
163
158
|
: 'text-secondary hover:bg-surface-overlay',
|
|
@@ -170,7 +165,7 @@
|
|
|
170
165
|
</nav>
|
|
171
166
|
|
|
172
167
|
{#if actions}
|
|
173
|
-
<div class="ml-auto flex shrink-0 items-center
|
|
168
|
+
<div class="gap-2 ml-auto flex shrink-0 items-center">
|
|
174
169
|
{@render actions()}
|
|
175
170
|
</div>
|
|
176
171
|
{/if}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
3
|
import Divider from '../../atoms/Divider/Divider.svelte';
|
|
4
4
|
|
|
5
|
+
export type SidebarIcon = Component<{
|
|
6
|
+
class?: string;
|
|
7
|
+
size?: number | string;
|
|
8
|
+
strokeWidth?: number | string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
5
11
|
export interface SidebarItem {
|
|
6
12
|
id: string;
|
|
7
13
|
label: string;
|
|
8
14
|
href?: string;
|
|
15
|
+
icon?: SidebarIcon;
|
|
9
16
|
disabled?: boolean;
|
|
10
17
|
}
|
|
11
18
|
|
|
@@ -20,7 +27,10 @@
|
|
|
20
27
|
groups?: SidebarGroup[];
|
|
21
28
|
value?: string;
|
|
22
29
|
collapsed?: boolean;
|
|
30
|
+
/** Show the collapse toggle. Default true. */
|
|
31
|
+
collapsible?: boolean;
|
|
23
32
|
class?: string;
|
|
33
|
+
header?: Snippet;
|
|
24
34
|
footer?: Snippet;
|
|
25
35
|
onchange?: (id: string) => void;
|
|
26
36
|
}
|
|
@@ -30,7 +40,9 @@
|
|
|
30
40
|
groups = [],
|
|
31
41
|
value = $bindable(''),
|
|
32
42
|
collapsed = $bindable(false),
|
|
43
|
+
collapsible = true,
|
|
33
44
|
class: className = '',
|
|
45
|
+
header,
|
|
34
46
|
footer,
|
|
35
47
|
onchange
|
|
36
48
|
}: SidebarProps = $props();
|
|
@@ -40,76 +52,119 @@
|
|
|
40
52
|
value = id;
|
|
41
53
|
onchange?.(id);
|
|
42
54
|
}
|
|
55
|
+
|
|
56
|
+
function itemClass(item: SidebarItem) {
|
|
57
|
+
return [
|
|
58
|
+
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-sm transition-colors',
|
|
59
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
|
|
60
|
+
value === item.id
|
|
61
|
+
? 'bg-brand-50 font-medium text-brand-700 dark:bg-brand-950/40 dark:text-brand-300'
|
|
62
|
+
: 'text-secondary hover:bg-surface-overlay hover:text-primary',
|
|
63
|
+
item.disabled && 'pointer-events-none cursor-not-allowed opacity-40',
|
|
64
|
+
collapsed && 'justify-center'
|
|
65
|
+
];
|
|
66
|
+
}
|
|
43
67
|
</script>
|
|
44
68
|
|
|
69
|
+
{#snippet itemGlyph(item: SidebarItem)}
|
|
70
|
+
{#if item.icon}
|
|
71
|
+
<item.icon class="h-4 w-4 shrink-0" strokeWidth={1.75} />
|
|
72
|
+
{:else}
|
|
73
|
+
<span
|
|
74
|
+
class="h-6 w-6 rounded-md bg-surface-overlay font-semibold flex shrink-0 items-center justify-center text-[11px]"
|
|
75
|
+
>
|
|
76
|
+
{item.label.slice(0, 1)}
|
|
77
|
+
</span>
|
|
78
|
+
{/if}
|
|
79
|
+
{/snippet}
|
|
80
|
+
|
|
81
|
+
{#snippet navItem(item: SidebarItem)}
|
|
82
|
+
{#if item.href && !item.disabled}
|
|
83
|
+
<a
|
|
84
|
+
href={item.href}
|
|
85
|
+
title={collapsed ? item.label : undefined}
|
|
86
|
+
aria-current={value === item.id ? 'page' : undefined}
|
|
87
|
+
class={itemClass(item)}
|
|
88
|
+
onclick={() => select(item.id, item.disabled)}
|
|
89
|
+
>
|
|
90
|
+
{@render itemGlyph(item)}
|
|
91
|
+
{#if !collapsed}
|
|
92
|
+
<span class="truncate">{item.label}</span>
|
|
93
|
+
{/if}
|
|
94
|
+
</a>
|
|
95
|
+
{:else}
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
disabled={item.disabled}
|
|
99
|
+
title={collapsed ? item.label : undefined}
|
|
100
|
+
aria-current={value === item.id ? 'page' : undefined}
|
|
101
|
+
class={itemClass(item)}
|
|
102
|
+
onclick={() => select(item.id, item.disabled)}
|
|
103
|
+
>
|
|
104
|
+
{@render itemGlyph(item)}
|
|
105
|
+
{#if !collapsed}
|
|
106
|
+
<span class="truncate">{item.label}</span>
|
|
107
|
+
{/if}
|
|
108
|
+
</button>
|
|
109
|
+
{/if}
|
|
110
|
+
{/snippet}
|
|
111
|
+
|
|
45
112
|
<aside
|
|
46
113
|
class={[
|
|
47
|
-
'flex h-full flex-col border-r
|
|
114
|
+
'border-border bg-surface-elevated flex h-full flex-col border-r transition-[width] duration-200',
|
|
48
115
|
collapsed ? 'w-[4.25rem]' : 'w-60',
|
|
49
116
|
className
|
|
50
117
|
]}
|
|
51
118
|
>
|
|
52
119
|
<div
|
|
53
120
|
class={[
|
|
54
|
-
'
|
|
55
|
-
collapsed ? 'justify-center' : 'justify-between'
|
|
121
|
+
'gap-2 px-3 py-4 flex items-center',
|
|
122
|
+
collapsed ? 'justify-center' : header || !collapsible ? 'justify-start' : 'justify-between'
|
|
56
123
|
]}
|
|
57
124
|
>
|
|
58
|
-
{#if !collapsed}
|
|
59
|
-
<
|
|
125
|
+
{#if header && !collapsed}
|
|
126
|
+
<div class="min-w-0 flex-1">{@render header()}</div>
|
|
127
|
+
{:else if !collapsed}
|
|
128
|
+
<span class="text-sm font-semibold text-primary truncate">{brand}</span>
|
|
129
|
+
{/if}
|
|
130
|
+
{#if collapsible}
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onclick={() => (collapsed = !collapsed)}
|
|
134
|
+
class="rounded-lg p-2 text-secondary hover:bg-surface-overlay hover:text-primary focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none"
|
|
135
|
+
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
class="h-4 w-4"
|
|
139
|
+
viewBox="0 0 24 24"
|
|
140
|
+
fill="none"
|
|
141
|
+
stroke="currentColor"
|
|
142
|
+
stroke-width="2"
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
>
|
|
145
|
+
{#if collapsed}
|
|
146
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h10M4 18h16" />
|
|
147
|
+
{:else}
|
|
148
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
149
|
+
{/if}
|
|
150
|
+
</svg>
|
|
151
|
+
</button>
|
|
60
152
|
{/if}
|
|
61
|
-
<button
|
|
62
|
-
type="button"
|
|
63
|
-
onclick={() => (collapsed = !collapsed)}
|
|
64
|
-
class="rounded-lg p-2 text-secondary hover:bg-surface-overlay hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30"
|
|
65
|
-
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
66
|
-
>
|
|
67
|
-
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
68
|
-
{#if collapsed}
|
|
69
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h10M4 18h16" />
|
|
70
|
-
{:else}
|
|
71
|
-
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
72
|
-
{/if}
|
|
73
|
-
</svg>
|
|
74
|
-
</button>
|
|
75
153
|
</div>
|
|
76
154
|
|
|
77
|
-
<nav class="flex-1 overflow-y-auto
|
|
155
|
+
<nav class="px-2 pb-3 flex-1 overflow-y-auto">
|
|
78
156
|
{#each groups as group, gi (group.id)}
|
|
79
157
|
{#if group.label && !collapsed}
|
|
80
|
-
<p class="mb-1 mt-3 px-2
|
|
158
|
+
<p class="mb-1 mt-3 px-2 font-semibold tracking-wide text-muted text-[11px] uppercase">
|
|
81
159
|
{group.label}
|
|
82
160
|
</p>
|
|
83
161
|
{:else if gi > 0}
|
|
84
162
|
<div class="my-2 px-2"><Divider /></div>
|
|
85
163
|
{/if}
|
|
86
|
-
<ul class="flex flex-col
|
|
164
|
+
<ul class="gap-0.5 flex flex-col">
|
|
87
165
|
{#each group.items as item (item.id)}
|
|
88
166
|
<li>
|
|
89
|
-
|
|
90
|
-
type="button"
|
|
91
|
-
disabled={item.disabled}
|
|
92
|
-
onclick={() => select(item.id, item.disabled)}
|
|
93
|
-
title={collapsed ? item.label : undefined}
|
|
94
|
-
class={[
|
|
95
|
-
'flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-sm transition-colors',
|
|
96
|
-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
|
|
97
|
-
value === item.id
|
|
98
|
-
? 'bg-brand-50 font-medium text-brand-700 dark:bg-brand-950/40 dark:text-brand-300'
|
|
99
|
-
: 'text-secondary hover:bg-surface-overlay hover:text-primary',
|
|
100
|
-
item.disabled && 'cursor-not-allowed opacity-40',
|
|
101
|
-
collapsed && 'justify-center'
|
|
102
|
-
]}
|
|
103
|
-
>
|
|
104
|
-
<span
|
|
105
|
-
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-surface-overlay text-[11px] font-semibold"
|
|
106
|
-
>
|
|
107
|
-
{item.label.slice(0, 1)}
|
|
108
|
-
</span>
|
|
109
|
-
{#if !collapsed}
|
|
110
|
-
<span class="truncate">{item.label}</span>
|
|
111
|
-
{/if}
|
|
112
|
-
</button>
|
|
167
|
+
{@render navItem(item)}
|
|
113
168
|
</li>
|
|
114
169
|
{/each}
|
|
115
170
|
</ul>
|
|
@@ -117,7 +172,7 @@
|
|
|
117
172
|
</nav>
|
|
118
173
|
|
|
119
174
|
{#if footer}
|
|
120
|
-
<div class={['border-
|
|
175
|
+
<div class={['border-border p-3 border-t', collapsed && 'flex justify-center']}>
|
|
121
176
|
{@render footer()}
|
|
122
177
|
</div>
|
|
123
178
|
{/if}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { Snippet } from 'svelte';
|
|
1
|
+
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
export type SidebarIcon = Component<{
|
|
3
|
+
class?: string;
|
|
4
|
+
size?: number | string;
|
|
5
|
+
strokeWidth?: number | string;
|
|
6
|
+
}>;
|
|
2
7
|
export interface SidebarItem {
|
|
3
8
|
id: string;
|
|
4
9
|
label: string;
|
|
5
10
|
href?: string;
|
|
11
|
+
icon?: SidebarIcon;
|
|
6
12
|
disabled?: boolean;
|
|
7
13
|
}
|
|
8
14
|
export interface SidebarGroup {
|
|
@@ -15,10 +21,13 @@ interface SidebarProps {
|
|
|
15
21
|
groups?: SidebarGroup[];
|
|
16
22
|
value?: string;
|
|
17
23
|
collapsed?: boolean;
|
|
24
|
+
/** Show the collapse toggle. Default true. */
|
|
25
|
+
collapsible?: boolean;
|
|
18
26
|
class?: string;
|
|
27
|
+
header?: Snippet;
|
|
19
28
|
footer?: Snippet;
|
|
20
29
|
onchange?: (id: string) => void;
|
|
21
30
|
}
|
|
22
|
-
declare const Sidebar:
|
|
31
|
+
declare const Sidebar: Component<SidebarProps, {}, "value" | "collapsed">;
|
|
23
32
|
type Sidebar = ReturnType<typeof Sidebar>;
|
|
24
33
|
export default Sidebar;
|
package/dist/index.d.ts
CHANGED
|
@@ -656,7 +656,9 @@ export { default as ToastContainer } from './components/organisms/Toast/ToastCon
|
|
|
656
656
|
export { toastStore, toast } from './components/organisms/Toast/toast.svelte.js';
|
|
657
657
|
export type { Toast, ToastVariant, ToastPosition, ToastAppearance, ToastAction } from './components/organisms/Toast/toast.svelte.js';
|
|
658
658
|
export { default as Sidebar } from './components/organisms/Sidebar/Sidebar.svelte';
|
|
659
|
-
export type { SidebarItem, SidebarGroup } from './components/organisms/Sidebar/Sidebar.svelte';
|
|
659
|
+
export type { SidebarItem, SidebarGroup, SidebarIcon } from './components/organisms/Sidebar/Sidebar.svelte';
|
|
660
|
+
export { default as NavRail } from './components/organisms/NavRail/NavRail.svelte';
|
|
661
|
+
export type { NavRailItem, NavRailIcon } from './components/organisms/NavRail/NavRail.svelte';
|
|
660
662
|
export { default as Navbar } from './components/organisms/Navbar/Navbar.svelte';
|
|
661
663
|
export type { NavbarLink } from './components/organisms/Navbar/Navbar.svelte';
|
|
662
664
|
export { default as BottomNav } from './components/organisms/BottomNav/BottomNav.svelte';
|
|
@@ -664,6 +666,8 @@ export type { BottomNavItem, BottomNavIcon } from './components/organisms/Bottom
|
|
|
664
666
|
export { default as CommandPalette } from './components/organisms/CommandPalette/CommandPalette.svelte';
|
|
665
667
|
export type { CommandItem } from './components/organisms/CommandPalette/CommandPalette.svelte';
|
|
666
668
|
export { default as AppShell } from './components/organisms/AppShell/AppShell.svelte';
|
|
669
|
+
export { getAppChrome, setAppChrome, AppChrome } from './components/organisms/AppShell/app-chrome.svelte.js';
|
|
670
|
+
export type { AppShellContextual } from './components/organisms/AppShell/app-chrome.svelte.js';
|
|
667
671
|
export { default as EditorShell } from './components/organisms/EditorShell/EditorShell.svelte';
|
|
668
672
|
export { default as BlueprintEditor } from './components/organisms/BlueprintEditor/BlueprintEditor.svelte';
|
|
669
673
|
export { default as CanvasEditor } from './components/organisms/CanvasEditor/CanvasEditor.svelte';
|
package/dist/index.js
CHANGED
|
@@ -454,10 +454,12 @@ export { default as GanttChart } from './components/organisms/GanttChart/GanttCh
|
|
|
454
454
|
export { default as ToastContainer } from './components/organisms/Toast/ToastContainer.svelte';
|
|
455
455
|
export { toastStore, toast } from './components/organisms/Toast/toast.svelte.js';
|
|
456
456
|
export { default as Sidebar } from './components/organisms/Sidebar/Sidebar.svelte';
|
|
457
|
+
export { default as NavRail } from './components/organisms/NavRail/NavRail.svelte';
|
|
457
458
|
export { default as Navbar } from './components/organisms/Navbar/Navbar.svelte';
|
|
458
459
|
export { default as BottomNav } from './components/organisms/BottomNav/BottomNav.svelte';
|
|
459
460
|
export { default as CommandPalette } from './components/organisms/CommandPalette/CommandPalette.svelte';
|
|
460
461
|
export { default as AppShell } from './components/organisms/AppShell/AppShell.svelte';
|
|
462
|
+
export { getAppChrome, setAppChrome, AppChrome } from './components/organisms/AppShell/app-chrome.svelte.js';
|
|
461
463
|
export { default as EditorShell } from './components/organisms/EditorShell/EditorShell.svelte';
|
|
462
464
|
export { default as BlueprintEditor } from './components/organisms/BlueprintEditor/BlueprintEditor.svelte';
|
|
463
465
|
export { default as CanvasEditor } from './components/organisms/CanvasEditor/CanvasEditor.svelte';
|