yaxa-svelte 1.7.0 → 1.8.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 (31) hide show
  1. package/dist/components/elements/Carousel.svelte +200 -0
  2. package/dist/components/elements/Carousel.svelte.d.ts +100 -0
  3. package/dist/components/elements/Carousel.test.d.ts +1 -0
  4. package/dist/components/elements/Carousel.test.js +11 -0
  5. package/dist/components/elements/VirtualList.svelte +110 -0
  6. package/dist/components/elements/VirtualList.svelte.d.ts +52 -0
  7. package/dist/components/elements/VirtualList.test.d.ts +1 -0
  8. package/dist/components/elements/VirtualList.test.js +13 -0
  9. package/dist/components/layout/DashboardShell.svelte +178 -0
  10. package/dist/components/layout/DashboardShell.svelte.d.ts +67 -0
  11. package/dist/components/layout/DashboardShell.test.d.ts +1 -0
  12. package/dist/components/layout/DashboardShell.test.js +15 -0
  13. package/dist/components/layout/ResizablePanels.svelte +192 -0
  14. package/dist/components/layout/ResizablePanels.svelte.d.ts +68 -0
  15. package/dist/components/layout/ResizablePanels.test.d.ts +1 -0
  16. package/dist/components/layout/ResizablePanels.test.js +14 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.js +4 -0
  19. package/dist/server/auth/hook.d.ts +2 -2
  20. package/dist/server/auth/hook.js +9 -3
  21. package/dist/server/auth/index.d.ts +7 -1
  22. package/dist/server/auth/index.js +11 -1
  23. package/dist/server/auth/org.test.d.ts +1 -0
  24. package/dist/server/auth/org.test.js +16 -0
  25. package/dist/server/db/schema-pg.d.ts +395 -0
  26. package/dist/server/db/schema-pg.js +41 -2
  27. package/dist/server/db/schema-sqlite.d.ts +427 -0
  28. package/dist/server/db/schema-sqlite.js +41 -2
  29. package/dist/server/db/types.d.ts +28 -0
  30. package/dist/server/index.d.ts +1 -1
  31. package/package.json +1 -1
@@ -0,0 +1,200 @@
1
+ <script module lang="ts">
2
+ import { tv, type VariantProps } from '../../utils/cn';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ export const carouselVariants = tv({
6
+ slots: {
7
+ root: 'relative w-full overflow-hidden rounded-2xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900',
8
+ viewport: 'relative flex w-full overflow-hidden',
9
+ slide: 'min-w-full transition-transform duration-500 ease-out',
10
+ controls: 'absolute inset-0 flex items-center justify-between pointer-events-none p-4',
11
+ arrowBtn:
12
+ 'pointer-events-auto inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/80 shadow-md backdrop-blur-xs transition hover:bg-white hover:scale-105 active:scale-95 dark:bg-neutral-900/80 dark:hover:bg-neutral-800 text-neutral-800 dark:text-neutral-100',
13
+ dots: 'absolute bottom-4 left-0 right-0 flex items-center justify-center gap-2 pointer-events-none',
14
+ dotBtn: 'pointer-events-auto h-2 rounded-full transition-all duration-300'
15
+ }
16
+ });
17
+
18
+ export type CarouselProps<T> = VariantProps<typeof carouselVariants> & {
19
+ items: T[];
20
+ currentIndex?: number;
21
+ autoplay?: boolean;
22
+ interval?: number;
23
+ loop?: boolean;
24
+ showDots?: boolean;
25
+ showArrows?: boolean;
26
+ pauseOnHover?: boolean;
27
+ item?: Snippet<[T, number]>;
28
+ empty?: Snippet;
29
+ class?: string;
30
+ };
31
+ </script>
32
+
33
+ <script lang="ts" generics="T">
34
+ import Icon from './Icon.svelte';
35
+
36
+ let {
37
+ items = [],
38
+ currentIndex = $bindable(0),
39
+ autoplay = false,
40
+ interval = 4000,
41
+ loop = true,
42
+ showDots = true,
43
+ showArrows = true,
44
+ pauseOnHover = true,
45
+ item,
46
+ empty,
47
+ class: className = ''
48
+ }: CarouselProps<T> = $props();
49
+
50
+ let isHovered = $state(false);
51
+ let touchStartX = $state(0);
52
+
53
+ const slots = $derived(carouselVariants());
54
+
55
+ function next() {
56
+ if (items.length === 0) return;
57
+ if (currentIndex < items.length - 1) {
58
+ currentIndex += 1;
59
+ } else if (loop) {
60
+ currentIndex = 0;
61
+ }
62
+ }
63
+
64
+ function prev() {
65
+ if (items.length === 0) return;
66
+ if (currentIndex > 0) {
67
+ currentIndex -= 1;
68
+ } else if (loop) {
69
+ currentIndex = items.length - 1;
70
+ }
71
+ }
72
+
73
+ function goTo(index: number) {
74
+ if (index >= 0 && index < items.length) {
75
+ currentIndex = index;
76
+ }
77
+ }
78
+
79
+ // Autoplay timer
80
+ $effect(() => {
81
+ if (!autoplay || items.length <= 1) return;
82
+ if (pauseOnHover && isHovered) return;
83
+
84
+ const timer = setInterval(() => {
85
+ next();
86
+ }, interval);
87
+
88
+ return () => clearInterval(timer);
89
+ });
90
+
91
+ function handleTouchStart(e: TouchEvent) {
92
+ touchStartX = e.touches[0].clientX;
93
+ }
94
+
95
+ function handleTouchEnd(e: TouchEvent) {
96
+ const touchEndX = e.changedTouches[0].clientX;
97
+ const diff = touchStartX - touchEndX;
98
+ if (Math.abs(diff) > 40) {
99
+ if (diff > 0) {
100
+ next();
101
+ } else {
102
+ prev();
103
+ }
104
+ }
105
+ }
106
+ </script>
107
+
108
+ <div
109
+ class="{slots.root()} {className}"
110
+ role="region"
111
+ aria-roledescription="carousel"
112
+ aria-label="Image and Card Slider"
113
+ onmouseenter={() => (isHovered = true)}
114
+ onmouseleave={() => (isHovered = false)}
115
+ ontouchstart={handleTouchStart}
116
+ ontouchend={handleTouchEnd}
117
+ >
118
+ {#if items.length === 0}
119
+ <div
120
+ class="flex min-h-64 items-center justify-center p-8 text-neutral-500 dark:text-neutral-400"
121
+ >
122
+ {#if empty}
123
+ {@render empty()}
124
+ {:else}
125
+ <p class="text-sm">No carousel slides available.</p>
126
+ {/if}
127
+ </div>
128
+ {:else}
129
+ <!-- Viewport -->
130
+ <div class={slots.viewport()}>
131
+ <div
132
+ class="flex w-full transition-transform duration-500 ease-out"
133
+ style="transform: translateX(-{currentIndex * 100}%);"
134
+ >
135
+ {#each items as slide, index}
136
+ <div
137
+ class="min-w-full flex-shrink-0"
138
+ role="group"
139
+ aria-roledescription="slide"
140
+ aria-label="Slide {index + 1} of {items.length}"
141
+ >
142
+ {#if item}
143
+ {@render item(slide, index)}
144
+ {:else}
145
+ <div class="flex min-h-64 items-center justify-center p-8">
146
+ {String(slide)}
147
+ </div>
148
+ {/if}
149
+ </div>
150
+ {/each}
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Navigation Arrows -->
155
+ {#if showArrows && items.length > 1}
156
+ <div class={slots.controls()}>
157
+ <button
158
+ type="button"
159
+ onclick={prev}
160
+ class={slots.arrowBtn()}
161
+ aria-label="Previous slide"
162
+ disabled={!loop && currentIndex === 0}
163
+ >
164
+ <Icon name="chevron-left" size="sm" />
165
+ </button>
166
+
167
+ <button
168
+ type="button"
169
+ onclick={next}
170
+ class={slots.arrowBtn()}
171
+ aria-label="Next slide"
172
+ disabled={!loop && currentIndex === items.length - 1}
173
+ >
174
+ <Icon name="chevron-right" size="sm" />
175
+ </button>
176
+ </div>
177
+ {/if}
178
+
179
+ <!-- Indicator Dots -->
180
+ {#if showDots && items.length > 1}
181
+ <div class={slots.dots()}>
182
+ {#each items as slide, index (index)}
183
+ <button
184
+ type="button"
185
+ onclick={() => goTo(index)}
186
+ class="{slots.dotBtn()} {currentIndex === index
187
+ ? 'w-6 bg-primary-600 dark:bg-primary-400'
188
+ : 'w-2 bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600'}"
189
+ aria-label="Go to slide {index + 1}: {typeof slide === 'object' &&
190
+ slide &&
191
+ 'title' in slide
192
+ ? String(slide.title)
193
+ : index + 1}"
194
+ aria-current={currentIndex === index ? 'true' : 'false'}
195
+ ></button>
196
+ {/each}
197
+ </div>
198
+ {/if}
199
+ {/if}
200
+ </div>
@@ -0,0 +1,100 @@
1
+ import { type VariantProps } from '../../utils/cn';
2
+ import type { Snippet } from 'svelte';
3
+ export declare const carouselVariants: import("tailwind-variants").TVReturnType<{
4
+ [key: string]: {
5
+ [key: string]: import("tailwind-variants").ClassValue | {
6
+ root?: import("tailwind-variants").ClassValue;
7
+ viewport?: import("tailwind-variants").ClassValue;
8
+ slide?: import("tailwind-variants").ClassValue;
9
+ controls?: import("tailwind-variants").ClassValue;
10
+ arrowBtn?: import("tailwind-variants").ClassValue;
11
+ dots?: import("tailwind-variants").ClassValue;
12
+ dotBtn?: import("tailwind-variants").ClassValue;
13
+ };
14
+ };
15
+ } | {
16
+ [x: string]: {
17
+ [x: string]: import("tailwind-variants").ClassValue | {
18
+ root?: import("tailwind-variants").ClassValue;
19
+ viewport?: import("tailwind-variants").ClassValue;
20
+ slide?: import("tailwind-variants").ClassValue;
21
+ controls?: import("tailwind-variants").ClassValue;
22
+ arrowBtn?: import("tailwind-variants").ClassValue;
23
+ dots?: import("tailwind-variants").ClassValue;
24
+ dotBtn?: import("tailwind-variants").ClassValue;
25
+ };
26
+ };
27
+ } | {}, {
28
+ root: string;
29
+ viewport: string;
30
+ slide: string;
31
+ controls: string;
32
+ arrowBtn: string;
33
+ dots: string;
34
+ dotBtn: string;
35
+ }, undefined, {
36
+ [key: string]: {
37
+ [key: string]: import("tailwind-variants").ClassValue | {
38
+ root?: import("tailwind-variants").ClassValue;
39
+ viewport?: import("tailwind-variants").ClassValue;
40
+ slide?: import("tailwind-variants").ClassValue;
41
+ controls?: import("tailwind-variants").ClassValue;
42
+ arrowBtn?: import("tailwind-variants").ClassValue;
43
+ dots?: import("tailwind-variants").ClassValue;
44
+ dotBtn?: import("tailwind-variants").ClassValue;
45
+ };
46
+ };
47
+ } | {}, {
48
+ root: string;
49
+ viewport: string;
50
+ slide: string;
51
+ controls: string;
52
+ arrowBtn: string;
53
+ dots: string;
54
+ dotBtn: string;
55
+ }, import("tailwind-variants").TVReturnTypeLike<unknown, {
56
+ root: string;
57
+ viewport: string;
58
+ slide: string;
59
+ controls: string;
60
+ arrowBtn: string;
61
+ dots: string;
62
+ dotBtn: string;
63
+ }>>;
64
+ export type CarouselProps<T> = VariantProps<typeof carouselVariants> & {
65
+ items: T[];
66
+ currentIndex?: number;
67
+ autoplay?: boolean;
68
+ interval?: number;
69
+ loop?: boolean;
70
+ showDots?: boolean;
71
+ showArrows?: boolean;
72
+ pauseOnHover?: boolean;
73
+ item?: Snippet<[T, number]>;
74
+ empty?: Snippet;
75
+ class?: string;
76
+ };
77
+ declare function $$render<T>(): {
78
+ props: CarouselProps<T>;
79
+ exports: {};
80
+ bindings: "currentIndex";
81
+ slots: {};
82
+ events: {};
83
+ };
84
+ declare class __sveltets_Render<T> {
85
+ props(): ReturnType<typeof $$render<T>>['props'];
86
+ events(): ReturnType<typeof $$render<T>>['events'];
87
+ slots(): ReturnType<typeof $$render<T>>['slots'];
88
+ bindings(): "currentIndex";
89
+ exports(): {};
90
+ }
91
+ interface $$IsomorphicComponent {
92
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
93
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
94
+ } & ReturnType<__sveltets_Render<T>['exports']>;
95
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
96
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
97
+ }
98
+ declare const Carousel: $$IsomorphicComponent;
99
+ type Carousel<T> = InstanceType<typeof Carousel<T>>;
100
+ export default Carousel;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { carouselVariants } from './Carousel.svelte';
3
+ describe('Carousel', () => {
4
+ it('generates correct carousel slot classes', () => {
5
+ const slots = carouselVariants();
6
+ expect(slots.root()).toContain('relative w-full overflow-hidden');
7
+ expect(slots.viewport()).toContain('relative flex w-full overflow-hidden');
8
+ expect(slots.controls()).toContain('pointer-events-none');
9
+ expect(slots.arrowBtn()).toContain('rounded-full');
10
+ });
11
+ });
@@ -0,0 +1,110 @@
1
+ <script module lang="ts">
2
+ import { tv, type VariantProps } from '../../utils/cn';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ export const virtualListVariants = tv({
6
+ base: 'relative w-full overflow-y-auto rounded-xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900',
7
+ variants: {
8
+ bordered: {
9
+ true: 'border border-neutral-200 dark:border-neutral-800',
10
+ false: 'border-0'
11
+ }
12
+ },
13
+ defaultVariants: {
14
+ bordered: true
15
+ }
16
+ });
17
+
18
+ export type VirtualListProps<T> = VariantProps<typeof virtualListVariants> & {
19
+ items: T[];
20
+ itemHeight?: number;
21
+ height?: number | string;
22
+ overscan?: number;
23
+ emptyText?: string;
24
+ item?: Snippet<[T, number]>;
25
+ empty?: Snippet;
26
+ class?: string;
27
+ };
28
+ </script>
29
+
30
+ <script lang="ts" generics="T">
31
+ let {
32
+ items = [],
33
+ itemHeight = 48,
34
+ height = 400,
35
+ overscan = 4,
36
+ emptyText = 'No items found',
37
+ item,
38
+ empty,
39
+ bordered = true,
40
+ class: className = ''
41
+ }: VirtualListProps<T> = $props();
42
+
43
+ let scrollTop = $state(0);
44
+ let measuredHeight = $state<number | null>(null);
45
+
46
+ const viewportHeight = $derived(measuredHeight ?? (typeof height === 'number' ? height : 400));
47
+
48
+ const heightStyle = $derived(typeof height === 'number' ? `${height}px` : height);
49
+
50
+ const totalHeight = $derived(items.length * itemHeight);
51
+
52
+ const startIndex = $derived(Math.max(0, Math.floor(scrollTop / itemHeight) - overscan));
53
+
54
+ const visibleCount = $derived(Math.ceil(viewportHeight / itemHeight) + 2 * overscan);
55
+
56
+ const endIndex = $derived(Math.min(items.length, startIndex + visibleCount));
57
+
58
+ const visibleItems = $derived(
59
+ items.slice(startIndex, endIndex).map((data, i) => ({
60
+ data,
61
+ index: startIndex + i,
62
+ top: (startIndex + i) * itemHeight
63
+ }))
64
+ );
65
+
66
+ function onScroll(e: Event) {
67
+ const target = e.currentTarget as HTMLElement;
68
+ scrollTop = target.scrollTop;
69
+ if (target.clientHeight > 0) {
70
+ measuredHeight = target.clientHeight;
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <div
76
+ class={virtualListVariants({ bordered, class: className })}
77
+ style="height: {heightStyle};"
78
+ onscroll={onScroll}
79
+ role="feed"
80
+ aria-busy={false}
81
+ >
82
+ {#if items.length === 0}
83
+ <div
84
+ class="flex h-full min-h-32 items-center justify-center p-6 text-sm text-neutral-500 dark:text-neutral-400"
85
+ >
86
+ {#if empty}
87
+ {@render empty()}
88
+ {:else}
89
+ <p>{emptyText}</p>
90
+ {/if}
91
+ </div>
92
+ {:else}
93
+ <!-- Total virtual space height holder -->
94
+ <div class="relative w-full" style="height: {totalHeight}px;">
95
+ {#each visibleItems as { data, index, top } (index)}
96
+ <div class="absolute right-0 left-0 w-full" style="top: {top}px; height: {itemHeight}px;">
97
+ {#if item}
98
+ {@render item(data, index)}
99
+ {:else}
100
+ <div
101
+ class="flex h-full items-center border-b border-neutral-100 px-4 text-sm text-neutral-700 dark:border-neutral-800 dark:text-neutral-300"
102
+ >
103
+ {String(data)}
104
+ </div>
105
+ {/if}
106
+ </div>
107
+ {/each}
108
+ </div>
109
+ {/if}
110
+ </div>
@@ -0,0 +1,52 @@
1
+ import { type VariantProps } from '../../utils/cn';
2
+ import type { Snippet } from 'svelte';
3
+ export declare const virtualListVariants: import("tailwind-variants").TVReturnType<{
4
+ bordered: {
5
+ true: "border border-neutral-200 dark:border-neutral-800";
6
+ false: "border-0";
7
+ };
8
+ }, undefined, "relative w-full overflow-y-auto rounded-xl border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900", {
9
+ bordered: {
10
+ true: "border border-neutral-200 dark:border-neutral-800";
11
+ false: "border-0";
12
+ };
13
+ }, undefined, import("tailwind-variants").TVReturnTypeLike<{
14
+ bordered: {
15
+ true: "border border-neutral-200 dark:border-neutral-800";
16
+ false: "border-0";
17
+ };
18
+ }, undefined>>;
19
+ export type VirtualListProps<T> = VariantProps<typeof virtualListVariants> & {
20
+ items: T[];
21
+ itemHeight?: number;
22
+ height?: number | string;
23
+ overscan?: number;
24
+ emptyText?: string;
25
+ item?: Snippet<[T, number]>;
26
+ empty?: Snippet;
27
+ class?: string;
28
+ };
29
+ declare function $$render<T>(): {
30
+ props: VirtualListProps<T>;
31
+ exports: {};
32
+ bindings: "";
33
+ slots: {};
34
+ events: {};
35
+ };
36
+ declare class __sveltets_Render<T> {
37
+ props(): ReturnType<typeof $$render<T>>['props'];
38
+ events(): ReturnType<typeof $$render<T>>['events'];
39
+ slots(): ReturnType<typeof $$render<T>>['slots'];
40
+ bindings(): "";
41
+ exports(): {};
42
+ }
43
+ interface $$IsomorphicComponent {
44
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
45
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
46
+ } & ReturnType<__sveltets_Render<T>['exports']>;
47
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
48
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
49
+ }
50
+ declare const VirtualList: $$IsomorphicComponent;
51
+ type VirtualList<T> = InstanceType<typeof VirtualList<T>>;
52
+ export default VirtualList;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { virtualListVariants } from './VirtualList.svelte';
3
+ describe('VirtualList', () => {
4
+ it('generates correct variant classes with default bordered state', () => {
5
+ const classes = virtualListVariants({ bordered: true });
6
+ expect(classes).toContain('relative w-full overflow-y-auto');
7
+ expect(classes).toContain('border');
8
+ });
9
+ it('supports unbordered variant', () => {
10
+ const classes = virtualListVariants({ bordered: false });
11
+ expect(classes).toContain('border-0');
12
+ });
13
+ });
@@ -0,0 +1,178 @@
1
+ <script module lang="ts">
2
+ import { tv, type VariantProps } from '../../utils/cn';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ export const dashboardShellVariants = tv({
6
+ slots: {
7
+ root: 'flex min-h-screen w-full bg-neutral-50 text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100',
8
+ sidebar:
9
+ 'relative hidden flex-col border-r border-neutral-200 bg-white transition-all duration-300 ease-in-out md:flex dark:border-neutral-800 dark:bg-neutral-900',
10
+ topbar:
11
+ 'flex h-16 w-full items-center justify-between border-b border-neutral-200 bg-white/80 px-4 backdrop-blur-md dark:border-neutral-800 dark:bg-neutral-900/80 md:px-6',
12
+ main: 'flex flex-1 flex-col overflow-hidden',
13
+ content: 'flex-1 overflow-y-auto p-4 md:p-6 lg:p-8'
14
+ },
15
+ variants: {
16
+ collapsed: {
17
+ true: {
18
+ sidebar: 'w-18'
19
+ },
20
+ false: {
21
+ sidebar: 'w-64'
22
+ }
23
+ }
24
+ },
25
+ defaultVariants: {
26
+ collapsed: false
27
+ }
28
+ });
29
+
30
+ export type DashboardShellProps = VariantProps<typeof dashboardShellVariants> & {
31
+ collapsed?: boolean;
32
+ mobileOpen?: boolean;
33
+ stickyHeader?: boolean;
34
+ brand?: Snippet;
35
+ navigation?: Snippet<[{ collapsed: boolean }]>;
36
+ sidebarFooter?: Snippet<[{ collapsed: boolean }]>;
37
+ topbar?: Snippet;
38
+ actions?: Snippet;
39
+ children?: Snippet;
40
+ class?: string;
41
+ };
42
+ </script>
43
+
44
+ <script lang="ts">
45
+ import Icon from '../elements/Icon.svelte';
46
+ import Drawer from '../overlays/Drawer.svelte';
47
+
48
+ let {
49
+ collapsed = $bindable(false),
50
+ mobileOpen = $bindable(false),
51
+ stickyHeader = true,
52
+ brand,
53
+ navigation,
54
+ sidebarFooter,
55
+ topbar,
56
+ actions,
57
+ children,
58
+ class: className = ''
59
+ }: DashboardShellProps = $props();
60
+
61
+ const slots = $derived(dashboardShellVariants({ collapsed }));
62
+
63
+ function toggleCollapse() {
64
+ collapsed = !collapsed;
65
+ }
66
+
67
+ function handleKeydown(e: KeyboardEvent) {
68
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'b') {
69
+ e.preventDefault();
70
+ toggleCollapse();
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <svelte:window onkeydown={handleKeydown} />
76
+
77
+ <div class="{slots.root()} {className}">
78
+ <!-- Desktop Sidebar -->
79
+ <aside class={slots.sidebar()} aria-label="Sidebar Navigation">
80
+ <!-- Sidebar Brand Header -->
81
+ <div
82
+ class="flex h-16 items-center justify-between border-b border-neutral-200 px-4 dark:border-neutral-800"
83
+ >
84
+ <div class="flex items-center gap-3 overflow-hidden">
85
+ {#if brand}
86
+ {@render brand()}
87
+ {/if}
88
+ </div>
89
+
90
+ <!-- Collapse Toggle Button -->
91
+ <button
92
+ type="button"
93
+ onclick={toggleCollapse}
94
+ class="hidden rounded-lg p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-900 md:inline-flex dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white"
95
+ aria-label={collapsed ? 'Expand sidebar (Ctrl+B)' : 'Collapse sidebar (Ctrl+B)'}
96
+ title={collapsed ? 'Expand sidebar (Ctrl+B)' : 'Collapse sidebar (Ctrl+B)'}
97
+ >
98
+ <Icon name={collapsed ? 'panel-left-open' : 'panel-left-close'} size="sm" />
99
+ </button>
100
+ </div>
101
+
102
+ <!-- Sidebar Navigation Area -->
103
+ <div class="flex-1 overflow-y-auto px-3 py-4">
104
+ {#if navigation}
105
+ {@render navigation({ collapsed })}
106
+ {/if}
107
+ </div>
108
+
109
+ <!-- Sidebar Footer Area -->
110
+ {#if sidebarFooter}
111
+ <div class="border-t border-neutral-200 p-3 dark:border-neutral-800">
112
+ {@render sidebarFooter({ collapsed })}
113
+ </div>
114
+ {/if}
115
+ </aside>
116
+
117
+ <!-- Mobile Drawer -->
118
+ <Drawer bind:open={mobileOpen} responsive={false} showHandle={true} class="p-0">
119
+ <div class="flex h-full flex-col bg-white dark:bg-neutral-900">
120
+ <div
121
+ class="flex h-16 items-center justify-between border-b border-neutral-200 px-4 dark:border-neutral-800"
122
+ >
123
+ <div class="flex items-center gap-3">
124
+ {#if brand}
125
+ {@render brand()}
126
+ {/if}
127
+ </div>
128
+ </div>
129
+
130
+ <div class="flex-1 overflow-y-auto px-4 py-4">
131
+ {#if navigation}
132
+ {@render navigation({ collapsed: false })}
133
+ {/if}
134
+ </div>
135
+
136
+ {#if sidebarFooter}
137
+ <div class="border-t border-neutral-200 p-4 dark:border-neutral-800">
138
+ {@render sidebarFooter({ collapsed: false })}
139
+ </div>
140
+ {/if}
141
+ </div>
142
+ </Drawer>
143
+
144
+ <!-- Main App Shell -->
145
+ <div class={slots.main()}>
146
+ <!-- Topbar Header -->
147
+ <header class="{slots.topbar()} {stickyHeader ? 'sticky top-0 z-30' : ''}">
148
+ <div class="flex items-center gap-3">
149
+ <!-- Mobile Menu Trigger -->
150
+ <button
151
+ type="button"
152
+ onclick={() => (mobileOpen = true)}
153
+ class="inline-flex rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-900 md:hidden dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-white"
154
+ aria-label="Open mobile menu"
155
+ >
156
+ <Icon name="menu" size="md" />
157
+ </button>
158
+
159
+ {#if topbar}
160
+ {@render topbar()}
161
+ {/if}
162
+ </div>
163
+
164
+ {#if actions}
165
+ <div class="flex items-center gap-2">
166
+ {@render actions()}
167
+ </div>
168
+ {/if}
169
+ </header>
170
+
171
+ <!-- Main Content Viewport -->
172
+ <main class={slots.content()}>
173
+ {#if children}
174
+ {@render children()}
175
+ {/if}
176
+ </main>
177
+ </div>
178
+ </div>