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.
- package/dist/components/elements/Carousel.svelte +200 -0
- package/dist/components/elements/Carousel.svelte.d.ts +100 -0
- package/dist/components/elements/Carousel.test.d.ts +1 -0
- package/dist/components/elements/Carousel.test.js +11 -0
- package/dist/components/elements/VirtualList.svelte +110 -0
- package/dist/components/elements/VirtualList.svelte.d.ts +52 -0
- package/dist/components/elements/VirtualList.test.d.ts +1 -0
- package/dist/components/elements/VirtualList.test.js +13 -0
- package/dist/components/layout/DashboardShell.svelte +178 -0
- package/dist/components/layout/DashboardShell.svelte.d.ts +67 -0
- package/dist/components/layout/DashboardShell.test.d.ts +1 -0
- package/dist/components/layout/DashboardShell.test.js +15 -0
- package/dist/components/layout/ResizablePanels.svelte +192 -0
- package/dist/components/layout/ResizablePanels.svelte.d.ts +68 -0
- package/dist/components/layout/ResizablePanels.test.d.ts +1 -0
- package/dist/components/layout/ResizablePanels.test.js +14 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/server/auth/hook.d.ts +2 -2
- package/dist/server/auth/hook.js +9 -3
- package/dist/server/auth/index.d.ts +7 -1
- package/dist/server/auth/index.js +11 -1
- package/dist/server/auth/org.test.d.ts +1 -0
- package/dist/server/auth/org.test.js +16 -0
- package/dist/server/db/schema-pg.d.ts +395 -0
- package/dist/server/db/schema-pg.js +41 -2
- package/dist/server/db/schema-sqlite.d.ts +427 -0
- package/dist/server/db/schema-sqlite.js +41 -2
- package/dist/server/db/types.d.ts +28 -0
- package/dist/server/index.d.ts +1 -1
- 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>
|