@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
@@ -7,6 +7,8 @@
7
7
 
8
8
  interface AuthShellProps {
9
9
  brand?: string;
10
+ /** Official brand logo URL (SVG/PNG) for BrandMark */
11
+ logoSrc?: string;
10
12
  tagline?: string;
11
13
  footer?: string;
12
14
  headline?: string;
@@ -26,6 +28,7 @@
26
28
 
27
29
  const {
28
30
  brand = 'R2DigiSolutions',
31
+ logoSrc,
29
32
  tagline = 'Build faster with a cohesive design system.',
30
33
  footer = '© R2DigiSolutions. All rights reserved.',
31
34
  headline = 'Sign in to continue',
@@ -52,8 +55,10 @@
52
55
  {:else}
53
56
  <div
54
57
  class={[
55
- 'relative flex h-full min-h-0 flex-col justify-between overflow-hidden bg-gradient-to-br from-brand-600 via-brand-700 to-brand-950 text-white',
56
- compact ? 'gap-4 px-5 py-5' : 'gap-6 p-8 sm:p-10'
58
+ 'flex flex-col justify-between overflow-hidden bg-gradient-to-br from-brand-600 via-brand-700 to-brand-950 text-white',
59
+ compact
60
+ ? 'relative gap-4 px-5 py-5'
61
+ : 'absolute inset-0 gap-6 p-8 sm:p-10'
57
62
  ]}
58
63
  >
59
64
  <div
@@ -67,7 +72,7 @@
67
72
 
68
73
  <div class={['relative z-10', compact ? 'space-y-3' : 'space-y-6']}>
69
74
  <div class="flex items-center gap-2.5">
70
- <BrandMark name={brand} size={compact ? 'sm' : 'md'} />
75
+ <BrandMark name={brand} {logoSrc} size={compact ? 'sm' : 'md'} />
71
76
  <span class={['font-semibold tracking-tight text-white', compact ? 'text-sm' : 'text-base']}>
72
77
  {brand}
73
78
  </span>
@@ -90,7 +95,7 @@
90
95
  </div>
91
96
  {#if !compact && highlights.length}
92
97
  <ul class="hidden space-y-2.5 pt-1 text-sm text-white/75 sm:block">
93
- {#each highlights as item}
98
+ {#each highlights as item (item)}
94
99
  <li class="flex items-start gap-2">
95
100
  <span class="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-white/80"></span>
96
101
  {item}
@@ -110,28 +115,33 @@
110
115
  <div
111
116
  class={[
112
117
  'flex w-full flex-col overflow-hidden bg-surface',
118
+ // h-dvh (not height:100%): % collapses when html/body have no explicit height
113
119
  fillParent && 'h-full min-h-0',
114
- !fillParent && fullHeight && 'h-full min-h-dvh',
120
+ !fillParent && fullHeight && 'h-dvh min-h-dvh',
115
121
  !fillParent && !fullHeight && 'min-h-[32rem]',
116
122
  className
117
123
  ]}
118
- style:height={fillParent ? '100%' : fullHeight ? '100%' : undefined}
119
- style:min-height={fillParent ? '100%' : fullHeight ? '100dvh' : '32rem'}
124
+ style:height={fillParent ? '100%' : undefined}
125
+ style:min-height={fillParent ? '100%' : !fullHeight ? '32rem' : undefined}
120
126
  >
121
127
  <div
122
128
  class={[
123
129
  'flex h-full min-h-0 w-full flex-1 flex-col',
124
- showAside && 'lg:flex-row',
130
+ showAside && 'lg:flex-row lg:items-stretch',
125
131
  asideSide === 'right' && 'lg:flex-row-reverse'
126
132
  ]}
127
133
  >
128
134
  {#if showAside}
129
- <aside class="relative hidden h-full min-h-0 w-full overflow-hidden lg:block lg:w-1/2">
135
+ <aside
136
+ class="relative hidden min-h-0 w-full overflow-hidden lg:block lg:h-auto lg:w-1/2 lg:self-stretch"
137
+ >
130
138
  {@render marketingPanel(false)}
131
139
  </aside>
132
140
  {/if}
133
141
 
134
- <section class="relative flex h-full min-h-0 w-full flex-1 flex-col bg-surface">
142
+ <section
143
+ class="relative flex min-h-0 w-full flex-1 flex-col bg-surface lg:h-auto lg:self-stretch"
144
+ >
135
145
  <div
136
146
  class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-brand-500/5 via-transparent to-transparent"
137
147
  aria-hidden="true"
@@ -144,7 +154,7 @@
144
154
  </div>
145
155
  {:else}
146
156
  <div class="shrink-0 px-5 pt-6 text-center lg:hidden">
147
- <BrandMark name={brand} showName size="md" class="justify-center" />
157
+ <BrandMark name={brand} {logoSrc} showName size="md" class="justify-center" />
148
158
  <p class="mt-2 text-sm text-muted">{tagline}</p>
149
159
  </div>
150
160
  {/if}
@@ -2,6 +2,8 @@ import type { Snippet } from 'svelte';
2
2
  export type AuthAsideSide = 'left' | 'right' | 'none';
3
3
  interface AuthShellProps {
4
4
  brand?: string;
5
+ /** Official brand logo URL (SVG/PNG) for BrandMark */
6
+ logoSrc?: string;
5
7
  tagline?: string;
6
8
  footer?: string;
7
9
  headline?: string;
@@ -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,11 @@
1
+ import NavRailStory from './NavRailStory.svelte';
2
+ const meta = {
3
+ title: 'Organisms/NavRail',
4
+ component: NavRailStory,
5
+ tags: ['autodocs'],
6
+ parameters: {
7
+ layout: 'centered'
8
+ }
9
+ };
10
+ export default meta;
11
+ export const Default = {};
@@ -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>
@@ -0,0 +1,3 @@
1
+ declare const NavRailStory: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type NavRailStory = ReturnType<typeof NavRailStory>;
3
+ export default NavRailStory;
@@ -68,42 +68,37 @@
68
68
 
69
69
  <header
70
70
  class={[
71
- 'relative z-40 w-full bg-surface-elevated',
72
- bordered && 'border-b border-border',
73
- sticky && 'sticky top-0',
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 gap-2">
87
- {#if leading}
88
- {@render leading()}
89
- {:else}
90
- <span
91
- class={[
92
- 'flex items-center justify-center rounded-lg bg-brand-500 font-bold text-white',
93
- size === 'lg' ? 'h-8 w-8 text-xs' : 'h-7 w-7 text-[11px]'
94
- ]}
95
- aria-hidden="true"
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
- 'hidden h-full min-w-0 items-stretch gap-0.5 md:flex',
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
- 'relative flex items-center gap-1.5 px-3 font-medium transition-colors',
120
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-500/30',
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
- : 'border-transparent text-secondary hover:text-primary'),
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="rounded-full bg-brand-50 px-1.5 py-0.5 text-[10px] font-semibold text-brand-700 dark:bg-brand-950/50 dark:text-brand-300"
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="flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto md:hidden"
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
- 'shrink-0 rounded-full px-3 py-1 text-xs font-medium transition-colors',
160
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
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 gap-2">
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 border-border bg-surface-elevated transition-[width] duration-200',
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
- 'flex items-center gap-2 px-3 py-4',
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
- <span class="truncate text-sm font-semibold text-primary">{brand}</span>
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 px-2 pb-3">
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 text-[11px] font-semibold uppercase tracking-wide text-muted">
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 gap-0.5">
164
+ <ul class="gap-0.5 flex flex-col">
87
165
  {#each group.items as item (item.id)}
88
166
  <li>
89
- <button
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-t border-border p-3', collapsed && 'flex justify-center']}>
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: import("svelte").Component<SidebarProps, {}, "value" | "collapsed">;
31
+ declare const Sidebar: Component<SidebarProps, {}, "value" | "collapsed">;
23
32
  type Sidebar = ReturnType<typeof Sidebar>;
24
33
  export default Sidebar;