@r2digisolutions/components 0.10.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/atoms/BrandMark/BrandMark.svelte +9 -3
- package/dist/components/atoms/BrandMark/BrandMark.svelte.d.ts +3 -1
- package/dist/components/molecules/AuthCard/AuthCard.svelte +4 -1
- package/dist/components/molecules/AuthCard/AuthCard.svelte.d.ts +2 -0
- 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/AuthShell/AuthShell.svelte +21 -11
- package/dist/components/organisms/AuthShell/AuthShell.svelte.d.ts +2 -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,9 +4,11 @@
|
|
|
4
4
|
export type BrandMarkSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
5
5
|
|
|
6
6
|
interface BrandMarkProps {
|
|
7
|
-
/** 1–2 letter mark, or provide children for a custom glyph */
|
|
7
|
+
/** 1–2 letter mark, or provide children / logoSrc for a custom glyph */
|
|
8
8
|
mark?: string;
|
|
9
9
|
name?: string;
|
|
10
|
+
/** Official logo image URL (SVG/PNG). Replaces initials / children. */
|
|
11
|
+
logoSrc?: string;
|
|
10
12
|
size?: BrandMarkSize;
|
|
11
13
|
showName?: boolean;
|
|
12
14
|
class?: string;
|
|
@@ -16,6 +18,7 @@
|
|
|
16
18
|
const {
|
|
17
19
|
mark,
|
|
18
20
|
name = 'Brand',
|
|
21
|
+
logoSrc,
|
|
19
22
|
size = 'md',
|
|
20
23
|
showName = false,
|
|
21
24
|
class: className = '',
|
|
@@ -49,12 +52,15 @@
|
|
|
49
52
|
<span class={['inline-flex items-center gap-2.5', className]}>
|
|
50
53
|
<span
|
|
51
54
|
class={[
|
|
52
|
-
'inline-flex shrink-0 items-center justify-center
|
|
55
|
+
'inline-flex shrink-0 items-center justify-center shadow-sm',
|
|
56
|
+
logoSrc ? 'overflow-hidden bg-white' : 'bg-brand-500 font-bold tracking-tight text-white',
|
|
53
57
|
box[size]
|
|
54
58
|
]}
|
|
55
59
|
aria-hidden={showName || !!name}
|
|
56
60
|
>
|
|
57
|
-
{#if
|
|
61
|
+
{#if logoSrc}
|
|
62
|
+
<img src={logoSrc} alt="" class="h-[78%] w-[78%] object-contain" />
|
|
63
|
+
{:else if children}
|
|
58
64
|
{@render children()}
|
|
59
65
|
{:else}
|
|
60
66
|
{initials}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
export type BrandMarkSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
3
3
|
interface BrandMarkProps {
|
|
4
|
-
/** 1–2 letter mark, or provide children for a custom glyph */
|
|
4
|
+
/** 1–2 letter mark, or provide children / logoSrc for a custom glyph */
|
|
5
5
|
mark?: string;
|
|
6
6
|
name?: string;
|
|
7
|
+
/** Official logo image URL (SVG/PNG). Replaces initials / children. */
|
|
8
|
+
logoSrc?: string;
|
|
7
9
|
size?: BrandMarkSize;
|
|
8
10
|
showName?: boolean;
|
|
9
11
|
class?: string;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
description?: string;
|
|
9
9
|
brand?: string;
|
|
10
10
|
mark?: string;
|
|
11
|
+
/** Official brand logo URL (SVG/PNG) for BrandMark */
|
|
12
|
+
logoSrc?: string;
|
|
11
13
|
class?: string;
|
|
12
14
|
footer?: Snippet;
|
|
13
15
|
children?: Snippet;
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
description,
|
|
19
21
|
brand = 'R2 Digi',
|
|
20
22
|
mark,
|
|
23
|
+
logoSrc,
|
|
21
24
|
class: className = '',
|
|
22
25
|
footer,
|
|
23
26
|
children
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
class={`mx-auto w-full max-w-md border border-border shadow-sm ${className}`}
|
|
32
35
|
>
|
|
33
36
|
<div class="mb-6 flex flex-col items-center gap-3 text-center">
|
|
34
|
-
<BrandMark name={brand} {mark} size="lg" showName />
|
|
37
|
+
<BrandMark name={brand} {mark} {logoSrc} size="lg" showName />
|
|
35
38
|
<div>
|
|
36
39
|
<h1 class="text-xl font-semibold tracking-tight text-primary">{title}</h1>
|
|
37
40
|
{#if description}
|
|
@@ -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;
|