@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.
Files changed (26) hide show
  1. package/dist/components/atoms/BrandMark/BrandMark.svelte +9 -3
  2. package/dist/components/atoms/BrandMark/BrandMark.svelte.d.ts +3 -1
  3. package/dist/components/molecules/AuthCard/AuthCard.svelte +4 -1
  4. package/dist/components/molecules/AuthCard/AuthCard.svelte.d.ts +2 -0
  5. package/dist/components/organisms/AppShell/AppShell.stories.d.ts +12 -0
  6. package/dist/components/organisms/AppShell/AppShell.stories.js +14 -2
  7. package/dist/components/organisms/AppShell/AppShell.svelte +199 -12
  8. package/dist/components/organisms/AppShell/AppShell.svelte.d.ts +16 -1
  9. package/dist/components/organisms/AppShell/AppShellStory.svelte +74 -7
  10. package/dist/components/organisms/AppShell/AppShellStory.svelte.d.ts +2 -0
  11. package/dist/components/organisms/AppShell/app-chrome.svelte.d.ts +18 -0
  12. package/dist/components/organisms/AppShell/app-chrome.svelte.js +8 -0
  13. package/dist/components/organisms/AuthShell/AuthShell.svelte +21 -11
  14. package/dist/components/organisms/AuthShell/AuthShell.svelte.d.ts +2 -0
  15. package/dist/components/organisms/NavRail/NavRail.stories.d.ts +12 -0
  16. package/dist/components/organisms/NavRail/NavRail.stories.js +11 -0
  17. package/dist/components/organisms/NavRail/NavRail.svelte +119 -0
  18. package/dist/components/organisms/NavRail/NavRail.svelte.d.ts +24 -0
  19. package/dist/components/organisms/NavRail/NavRailStory.svelte +27 -0
  20. package/dist/components/organisms/NavRail/NavRailStory.svelte.d.ts +3 -0
  21. package/dist/components/organisms/Navbar/Navbar.svelte +26 -31
  22. package/dist/components/organisms/Sidebar/Sidebar.svelte +103 -48
  23. package/dist/components/organisms/Sidebar/Sidebar.svelte.d.ts +11 -2
  24. package/dist/index.d.ts +5 -1
  25. package/dist/index.js +2 -0
  26. 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 bg-brand-500 font-bold tracking-tight text-white shadow-sm',
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 children}
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 @@ interface AuthCardProps {
4
4
  description?: string;
5
5
  brand?: string;
6
6
  mark?: string;
7
+ /** Official brand logo URL (SVG/PNG) for BrandMark */
8
+ logoSrc?: string;
7
9
  class?: string;
8
10
  footer?: Snippet;
9
11
  children?: Snippet;
@@ -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 bg-surface',
125
+ 'bg-surface relative flex w-full overflow-hidden',
49
126
  fullHeight && 'h-dvh',
50
- !fullHeight && framed && 'h-full min-h-0',
127
+ !fullHeight && framed && 'min-h-0 h-full',
51
128
  !fullHeight && !framed && 'h-[min(36rem,80vh)]',
52
- framed && 'rounded-2xl border border-border shadow-sm',
129
+ framed && 'rounded-2xl border-border shadow-sm border',
53
130
  className
54
131
  ]}
55
132
  >
56
- {#if showSidebar}
57
- <Sidebar
58
- {brand}
59
- groups={sidebarGroups}
60
- bind:value={sidebarValue}
61
- bind:collapsed
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
- <div class="flex min-h-0 min-w-0 flex-1 flex-col">
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 p-6', mainClass]}>
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: 'main',
25
+ id: 'ops',
19
26
  label: 'Workspace',
20
27
  items: [
21
- { id: 'home', label: 'Home' },
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 capitalize text-primary">{navValue}</h1>
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 border-border bg-surface-elevated p-4 text-sm text-secondary">
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,8 @@
1
+ import { createContext } from 'svelte';
2
+ export class AppChrome {
3
+ contextual = $state.raw(null);
4
+ setContextual(nav) {
5
+ this.contextual = nav;
6
+ }
7
+ }
8
+ export const [getAppChrome, setAppChrome] = createContext();