yaxa-svelte 1.7.0 → 1.9.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/Badge.test.d.ts +1 -0
- package/dist/components/elements/Badge.test.js +27 -0
- package/dist/components/elements/Button.svelte +52 -12
- package/dist/components/elements/Button.svelte.d.ts +5 -6
- package/dist/components/elements/Button.test.d.ts +1 -0
- package/dist/components/elements/Button.test.js +39 -0
- 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/DataTable.dom.test.d.ts +1 -0
- package/dist/components/elements/DataTable.dom.test.js +74 -0
- package/dist/components/elements/DataTable.svelte +11 -12
- package/dist/components/elements/Skeleton.svelte +1 -1
- package/dist/components/elements/Skeleton.svelte.d.ts +3 -3
- package/dist/components/elements/Skeleton.test.d.ts +1 -0
- package/dist/components/elements/Skeleton.test.js +49 -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/forms/Combobox.dom.test.d.ts +1 -0
- package/dist/components/forms/Combobox.dom.test.js +32 -0
- package/dist/components/forms/InputOTP.test.d.ts +1 -0
- package/dist/components/forms/InputOTP.test.js +61 -0
- package/dist/components/forms/Switch.test.d.ts +1 -0
- package/dist/components/forms/Switch.test.js +27 -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 +211 -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/components/overlays/Modal.dom.test.d.ts +1 -0
- package/dist/components/overlays/Modal.dom.test.js +31 -0
- package/dist/components/overlays/Modal.test.d.ts +1 -0
- package/dist/components/overlays/Modal.test.js +23 -0
- package/dist/components/overlays/Tooltip.dom.test.d.ts +1 -0
- package/dist/components/overlays/Tooltip.dom.test.js +14 -0
- package/dist/components/overlays/Tooltip.svelte +25 -23
- package/dist/composables/useClipboard.test.d.ts +1 -0
- package/dist/composables/useClipboard.test.js +33 -0
- package/dist/composables/useToast.test.d.ts +1 -0
- package/dist/composables/useToast.test.js +46 -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/email/email.test.d.ts +1 -0
- package/dist/server/email/email.test.js +25 -0
- package/dist/server/endpoints.test.d.ts +1 -0
- package/dist/server/endpoints.test.js +88 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/storage/storage.test.d.ts +1 -0
- package/dist/server/storage/storage.test.js +62 -0
- package/dist/site/og.js +1 -1
- package/dist/styles/yaxa.css +9 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +206 -0
- package/dist/testing/index.test.js +87 -1
- package/dist/testing/setup.d.ts +0 -0
- package/dist/testing/setup.js +38 -0
- package/dist/theme/theme.svelte.js +4 -1
- package/package.json +22 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { badgeVariants } from './Badge.svelte';
|
|
3
|
+
describe('Badge Component', () => {
|
|
4
|
+
it('generates solid primary badge classes by default', () => {
|
|
5
|
+
const classes = badgeVariants({ variant: 'solid', color: 'primary' });
|
|
6
|
+
expect(classes).toContain('bg-primary-600');
|
|
7
|
+
expect(classes).toContain('inline-flex');
|
|
8
|
+
expect(classes).toContain('font-medium');
|
|
9
|
+
});
|
|
10
|
+
it('supports multiple variants: outline, soft, subtle', () => {
|
|
11
|
+
const outline = badgeVariants({ variant: 'outline', color: 'success' });
|
|
12
|
+
expect(outline).toContain('border');
|
|
13
|
+
expect(outline).toContain('bg-transparent');
|
|
14
|
+
const soft = badgeVariants({ variant: 'soft', color: 'warning' });
|
|
15
|
+
expect(soft).toContain('inline-flex');
|
|
16
|
+
const subtle = badgeVariants({ variant: 'subtle', color: 'error' });
|
|
17
|
+
expect(subtle).toContain('border');
|
|
18
|
+
});
|
|
19
|
+
it('supports size scales (xs, sm, md, lg)', () => {
|
|
20
|
+
const xs = badgeVariants({ size: 'xs' });
|
|
21
|
+
expect(xs).toContain('text-[10px]');
|
|
22
|
+
expect(xs).toContain('px-1.5');
|
|
23
|
+
const lg = badgeVariants({ size: 'lg' });
|
|
24
|
+
expect(lg).toContain('text-sm');
|
|
25
|
+
expect(lg).toContain('px-3');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -193,48 +193,48 @@
|
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
export type ButtonProps = VariantProps<typeof buttonVariants> & {
|
|
196
|
+
type?: 'button' | 'submit' | 'reset';
|
|
196
197
|
href?: string;
|
|
197
|
-
|
|
198
|
+
as?: 'button' | 'a' | 'span' | 'div';
|
|
198
199
|
disabled?: boolean;
|
|
200
|
+
loading?: boolean;
|
|
199
201
|
icon?: IconSource;
|
|
200
202
|
trailingIcon?: IconSource;
|
|
201
|
-
type?: 'button' | 'submit' | 'reset';
|
|
202
|
-
class?: string;
|
|
203
203
|
leading?: Snippet;
|
|
204
204
|
trailing?: Snippet;
|
|
205
205
|
children?: Snippet;
|
|
206
|
-
onclick?: (
|
|
207
|
-
|
|
208
|
-
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean;
|
|
206
|
+
onclick?: (event: MouseEvent) => void;
|
|
207
|
+
class?: string;
|
|
209
208
|
[key: string]: unknown;
|
|
210
209
|
};
|
|
211
210
|
</script>
|
|
212
211
|
|
|
213
212
|
<script lang="ts">
|
|
214
|
-
import Icon from './Icon.svelte';
|
|
215
213
|
import Spinner from './Spinner.svelte';
|
|
214
|
+
import Icon from './Icon.svelte';
|
|
216
215
|
|
|
217
216
|
let {
|
|
217
|
+
type = 'button',
|
|
218
218
|
href,
|
|
219
|
+
as = 'button',
|
|
219
220
|
variant = 'solid',
|
|
220
221
|
color = 'primary',
|
|
221
222
|
size = 'md',
|
|
222
223
|
block = false,
|
|
223
224
|
square = false,
|
|
224
|
-
loading = false,
|
|
225
225
|
disabled = false,
|
|
226
|
+
loading = false,
|
|
226
227
|
icon,
|
|
227
228
|
trailingIcon,
|
|
228
|
-
type = 'button',
|
|
229
|
-
class: className = '',
|
|
230
229
|
leading,
|
|
231
230
|
trailing,
|
|
232
231
|
children,
|
|
233
232
|
onclick,
|
|
233
|
+
class: className = '',
|
|
234
234
|
...restProps
|
|
235
235
|
}: ButtonProps = $props();
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
const classes = $derived(
|
|
238
238
|
buttonVariants({
|
|
239
239
|
variant,
|
|
240
240
|
color,
|
|
@@ -246,7 +246,47 @@
|
|
|
246
246
|
);
|
|
247
247
|
</script>
|
|
248
248
|
|
|
249
|
-
{#if
|
|
249
|
+
{#if as === 'span'}
|
|
250
|
+
<span class={classes} aria-disabled={disabled || loading} {onclick} {...restProps}>
|
|
251
|
+
{#if loading}
|
|
252
|
+
<Spinner size={size === 'xs' || size === 'sm' ? 'sm' : 'md'} />
|
|
253
|
+
{:else if leading}
|
|
254
|
+
{@render leading()}
|
|
255
|
+
{:else if icon}
|
|
256
|
+
<Icon name={icon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
257
|
+
{/if}
|
|
258
|
+
|
|
259
|
+
{#if children}
|
|
260
|
+
{@render children()}
|
|
261
|
+
{/if}
|
|
262
|
+
|
|
263
|
+
{#if trailing}
|
|
264
|
+
{@render trailing()}
|
|
265
|
+
{:else if trailingIcon}
|
|
266
|
+
<Icon name={trailingIcon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
267
|
+
{/if}
|
|
268
|
+
</span>
|
|
269
|
+
{:else if as === 'div'}
|
|
270
|
+
<div class={classes} aria-disabled={disabled || loading} {onclick} {...restProps}>
|
|
271
|
+
{#if loading}
|
|
272
|
+
<Spinner size={size === 'xs' || size === 'sm' ? 'sm' : 'md'} />
|
|
273
|
+
{:else if leading}
|
|
274
|
+
{@render leading()}
|
|
275
|
+
{:else if icon}
|
|
276
|
+
<Icon name={icon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
277
|
+
{/if}
|
|
278
|
+
|
|
279
|
+
{#if children}
|
|
280
|
+
{@render children()}
|
|
281
|
+
{/if}
|
|
282
|
+
|
|
283
|
+
{#if trailing}
|
|
284
|
+
{@render trailing()}
|
|
285
|
+
{:else if trailingIcon}
|
|
286
|
+
<Icon name={trailingIcon} size={size === 'xs' ? 'xs' : size === 'sm' ? 'sm' : 'md'} />
|
|
287
|
+
{/if}
|
|
288
|
+
</div>
|
|
289
|
+
{:else if href || as === 'a'}
|
|
250
290
|
<a
|
|
251
291
|
{href}
|
|
252
292
|
class={classes}
|
|
@@ -90,19 +90,18 @@ export declare const buttonVariants: import("tailwind-variants").TVReturnType<{
|
|
|
90
90
|
};
|
|
91
91
|
}, undefined>>;
|
|
92
92
|
export type ButtonProps = VariantProps<typeof buttonVariants> & {
|
|
93
|
+
type?: 'button' | 'submit' | 'reset';
|
|
93
94
|
href?: string;
|
|
94
|
-
|
|
95
|
+
as?: 'button' | 'a' | 'span' | 'div';
|
|
95
96
|
disabled?: boolean;
|
|
97
|
+
loading?: boolean;
|
|
96
98
|
icon?: IconSource;
|
|
97
99
|
trailingIcon?: IconSource;
|
|
98
|
-
type?: 'button' | 'submit' | 'reset';
|
|
99
|
-
class?: string;
|
|
100
100
|
leading?: Snippet;
|
|
101
101
|
trailing?: Snippet;
|
|
102
102
|
children?: Snippet;
|
|
103
|
-
onclick?: (
|
|
104
|
-
|
|
105
|
-
'aria-current'?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | 'false' | boolean;
|
|
103
|
+
onclick?: (event: MouseEvent) => void;
|
|
104
|
+
class?: string;
|
|
106
105
|
[key: string]: unknown;
|
|
107
106
|
};
|
|
108
107
|
declare const Button: import("svelte").Component<ButtonProps, {}, "">;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buttonVariants } from './Button.svelte';
|
|
3
|
+
describe('Button Component', () => {
|
|
4
|
+
it('generates solid primary button classes by default', () => {
|
|
5
|
+
const classes = buttonVariants({ variant: 'solid', color: 'primary' });
|
|
6
|
+
expect(classes).toContain('bg-primary-600');
|
|
7
|
+
expect(classes).toContain('inline-flex');
|
|
8
|
+
expect(classes).toContain('select-none');
|
|
9
|
+
});
|
|
10
|
+
it('supports multiple variants: outline, ghost, soft, subtle, link', () => {
|
|
11
|
+
const outline = buttonVariants({ variant: 'outline', color: 'neutral' });
|
|
12
|
+
expect(outline).toContain('border');
|
|
13
|
+
expect(outline).toContain('bg-transparent');
|
|
14
|
+
const ghost = buttonVariants({ variant: 'ghost', color: 'error' });
|
|
15
|
+
expect(ghost).toContain('bg-transparent');
|
|
16
|
+
const link = buttonVariants({ variant: 'link' });
|
|
17
|
+
expect(link).toContain('underline-offset-4');
|
|
18
|
+
});
|
|
19
|
+
it('supports size scales and square/block modifiers', () => {
|
|
20
|
+
const xs = buttonVariants({ size: 'xs' });
|
|
21
|
+
expect(xs).toContain('text-xs');
|
|
22
|
+
expect(xs).toContain('px-2');
|
|
23
|
+
const xl = buttonVariants({ size: 'xl' });
|
|
24
|
+
expect(xl).toContain('text-base');
|
|
25
|
+
expect(xl).toContain('px-5');
|
|
26
|
+
const block = buttonVariants({ block: true });
|
|
27
|
+
expect(block).toContain('w-full');
|
|
28
|
+
const square = buttonVariants({ square: true });
|
|
29
|
+
expect(square).toContain('aspect-square');
|
|
30
|
+
});
|
|
31
|
+
it('applies compound color classes for success, warning, and error', () => {
|
|
32
|
+
const success = buttonVariants({ variant: 'solid', color: 'success' });
|
|
33
|
+
expect(success).toContain('bg-emerald-600');
|
|
34
|
+
const warning = buttonVariants({ variant: 'solid', color: 'warning' });
|
|
35
|
+
expect(warning).toContain('bg-amber-600');
|
|
36
|
+
const error = buttonVariants({ variant: 'solid', color: 'error' });
|
|
37
|
+
expect(error).toContain('bg-rose-600');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { flushSync } from 'svelte';
|
|
3
|
+
import { renderComponent } from '../../testing';
|
|
4
|
+
import DataTable, {} from './DataTable.svelte';
|
|
5
|
+
const testData = [
|
|
6
|
+
{ id: '1', name: 'Alice Smith', role: 'Engineer', score: 95 },
|
|
7
|
+
{ id: '2', name: 'Bob Jones', role: 'Designer', score: 82 },
|
|
8
|
+
{ id: '3', name: 'Charlie Brown', role: 'Manager', score: 88 }
|
|
9
|
+
];
|
|
10
|
+
const testColumns = [
|
|
11
|
+
{ key: 'name', label: 'Full Name', sortable: true },
|
|
12
|
+
{ key: 'role', label: 'Job Role', sortable: true },
|
|
13
|
+
{ key: 'score', label: 'Performance Score', sortable: true }
|
|
14
|
+
];
|
|
15
|
+
describe('DataTable Component (DOM & Interactions)', () => {
|
|
16
|
+
it('renders table headers and rows accurately', () => {
|
|
17
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
18
|
+
data: testData,
|
|
19
|
+
columns: testColumns
|
|
20
|
+
});
|
|
21
|
+
const thElements = target.querySelectorAll('thead th');
|
|
22
|
+
expect(thElements.length).toBe(3);
|
|
23
|
+
expect(thElements[0].textContent).toContain('Full Name');
|
|
24
|
+
expect(thElements[1].textContent).toContain('Job Role');
|
|
25
|
+
const trElements = target.querySelectorAll('tbody tr');
|
|
26
|
+
expect(trElements.length).toBe(3);
|
|
27
|
+
expect(trElements[0].textContent).toContain('Alice Smith');
|
|
28
|
+
expect(trElements[1].textContent).toContain('Bob Jones');
|
|
29
|
+
cleanup();
|
|
30
|
+
});
|
|
31
|
+
it('filters rows according to searchQuery', () => {
|
|
32
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
33
|
+
data: testData,
|
|
34
|
+
columns: testColumns,
|
|
35
|
+
searchQuery: 'Designer'
|
|
36
|
+
});
|
|
37
|
+
const trElements = target.querySelectorAll('tbody tr');
|
|
38
|
+
expect(trElements.length).toBe(1);
|
|
39
|
+
expect(trElements[0].textContent).toContain('Bob Jones');
|
|
40
|
+
expect(trElements[0].textContent).toContain('Designer');
|
|
41
|
+
cleanup();
|
|
42
|
+
});
|
|
43
|
+
it('displays custom emptyText when no matching rows are found', () => {
|
|
44
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
45
|
+
data: testData,
|
|
46
|
+
columns: testColumns,
|
|
47
|
+
searchQuery: 'NonExistentQueryXYZ',
|
|
48
|
+
emptyText: 'No members found'
|
|
49
|
+
});
|
|
50
|
+
const trElements = target.querySelectorAll('tbody tr');
|
|
51
|
+
expect(trElements.length).toBe(1);
|
|
52
|
+
expect(trElements[0].textContent).toContain('No members found');
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
it('sorts rows when clicking on sortable column headers', () => {
|
|
56
|
+
const { target, cleanup } = renderComponent(DataTable, {
|
|
57
|
+
data: testData,
|
|
58
|
+
columns: testColumns
|
|
59
|
+
});
|
|
60
|
+
const sortButton = target.querySelector('thead th button');
|
|
61
|
+
expect(sortButton).toBeDefined();
|
|
62
|
+
// Click to sort by name (ascending)
|
|
63
|
+
sortButton.click();
|
|
64
|
+
flushSync();
|
|
65
|
+
let firstRowName = target.querySelector('tbody tr td')?.textContent;
|
|
66
|
+
expect(firstRowName).toContain('Alice Smith');
|
|
67
|
+
// Click again to sort by name (descending)
|
|
68
|
+
sortButton.click();
|
|
69
|
+
flushSync();
|
|
70
|
+
firstRowName = target.querySelector('tbody tr td')?.textContent;
|
|
71
|
+
expect(firstRowName).toContain('Charlie Brown');
|
|
72
|
+
cleanup();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -48,15 +48,14 @@
|
|
|
48
48
|
|
|
49
49
|
let sortKey = $state<string | null>(null);
|
|
50
50
|
let sortOrder = $state<'asc' | 'desc'>('asc');
|
|
51
|
-
let
|
|
51
|
+
let customWidths = $state<Record<string, number>>({});
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const initial: Record<string, number> = {};
|
|
53
|
+
let effectiveWidths = $derived.by(() => {
|
|
54
|
+
const widths: Record<string, number> = {};
|
|
56
55
|
columns.forEach((c) => {
|
|
57
|
-
if (c.width)
|
|
56
|
+
if (c.width) widths[String(c.key)] = c.width;
|
|
58
57
|
});
|
|
59
|
-
|
|
58
|
+
return { ...widths, ...customWidths };
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
function handleSort(key: string) {
|
|
@@ -73,7 +72,7 @@
|
|
|
73
72
|
|
|
74
73
|
const startX = 'touches' in e ? e.touches[0].clientX : e.clientX;
|
|
75
74
|
const currentCol = columns.find((c) => String(c.key) === columnKey);
|
|
76
|
-
const currentWidth =
|
|
75
|
+
const currentWidth = effectiveWidths[columnKey] || 150;
|
|
77
76
|
const minWidth = currentCol?.minWidth || 60;
|
|
78
77
|
const maxWidth = currentCol?.maxWidth || 600;
|
|
79
78
|
|
|
@@ -81,7 +80,7 @@
|
|
|
81
80
|
const clientX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
|
|
82
81
|
const delta = clientX - startX;
|
|
83
82
|
const newWidth = Math.max(minWidth, Math.min(maxWidth, currentWidth + delta));
|
|
84
|
-
|
|
83
|
+
customWidths[columnKey] = newWidth;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
function onEnd() {
|
|
@@ -180,8 +179,8 @@
|
|
|
180
179
|
{/if}
|
|
181
180
|
{#each columns as column}
|
|
182
181
|
{@const colKey = String(column.key)}
|
|
183
|
-
{@const widthStyle =
|
|
184
|
-
? `width: ${
|
|
182
|
+
{@const widthStyle = effectiveWidths[colKey]
|
|
183
|
+
? `width: ${effectiveWidths[colKey]}px; min-width: ${effectiveWidths[colKey]}px;`
|
|
185
184
|
: ''}
|
|
186
185
|
<th
|
|
187
186
|
class="relative px-4 py-3 {getPinClass(column.pinned)} {column.class || ''}"
|
|
@@ -291,8 +290,8 @@
|
|
|
291
290
|
{/if}
|
|
292
291
|
{#each columns as column}
|
|
293
292
|
{@const colKey = String(column.key)}
|
|
294
|
-
{@const widthStyle =
|
|
295
|
-
? `width: ${
|
|
293
|
+
{@const widthStyle = effectiveWidths[colKey]
|
|
294
|
+
? `width: ${effectiveWidths[colKey]}px; min-width: ${effectiveWidths[colKey]}px;`
|
|
296
295
|
: ''}
|
|
297
296
|
<td
|
|
298
297
|
class="px-4 py-3.5 {getPinClass(column.pinned)} {column.class || ''}"
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
variant: {
|
|
8
8
|
pulse: 'animate-pulse',
|
|
9
9
|
shimmer:
|
|
10
|
-
'before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-
|
|
10
|
+
'before:absolute before:inset-0 before:-translate-x-full before:animate-[yaxa-shimmer-slide_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/60 before:to-transparent dark:before:via-white/20',
|
|
11
11
|
none: ''
|
|
12
12
|
},
|
|
13
13
|
shape: {
|