@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
|
@@ -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
|
-
'
|
|
56
|
-
compact
|
|
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-
|
|
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%' :
|
|
119
|
-
style:min-height={fillParent ? '100%' : fullHeight ? '
|
|
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
|
|
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
|
|
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,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;
|