yaxa-svelte 1.6.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 (67) hide show
  1. package/dist/components/blocks/FAQ.svelte +139 -0
  2. package/dist/components/blocks/FAQ.svelte.d.ts +19 -0
  3. package/dist/components/blocks/FAQ.test.d.ts +1 -0
  4. package/dist/components/blocks/FAQ.test.js +9 -0
  5. package/dist/components/blocks/FeatureGrid.svelte +126 -0
  6. package/dist/components/blocks/FeatureGrid.svelte.d.ts +22 -0
  7. package/dist/components/blocks/FeatureGrid.test.d.ts +1 -0
  8. package/dist/components/blocks/FeatureGrid.test.js +9 -0
  9. package/dist/components/blocks/Testimonials.svelte +118 -0
  10. package/dist/components/blocks/Testimonials.svelte.d.ts +22 -0
  11. package/dist/components/blocks/Testimonials.test.d.ts +1 -0
  12. package/dist/components/blocks/Testimonials.test.js +9 -0
  13. package/dist/components/elements/Carousel.svelte +200 -0
  14. package/dist/components/elements/Carousel.svelte.d.ts +100 -0
  15. package/dist/components/elements/Carousel.test.d.ts +1 -0
  16. package/dist/components/elements/Carousel.test.js +11 -0
  17. package/dist/components/elements/Terminal.svelte +107 -0
  18. package/dist/components/elements/Terminal.svelte.d.ts +17 -0
  19. package/dist/components/elements/Terminal.test.d.ts +1 -0
  20. package/dist/components/elements/Terminal.test.js +10 -0
  21. package/dist/components/elements/Timeline.svelte +118 -0
  22. package/dist/components/elements/Timeline.svelte.d.ts +39 -0
  23. package/dist/components/elements/Timeline.test.d.ts +1 -0
  24. package/dist/components/elements/Timeline.test.js +15 -0
  25. package/dist/components/elements/Tree.svelte +161 -0
  26. package/dist/components/elements/Tree.svelte.d.ts +37 -0
  27. package/dist/components/elements/Tree.test.d.ts +1 -0
  28. package/dist/components/elements/Tree.test.js +13 -0
  29. package/dist/components/elements/VirtualList.svelte +110 -0
  30. package/dist/components/elements/VirtualList.svelte.d.ts +52 -0
  31. package/dist/components/elements/VirtualList.test.d.ts +1 -0
  32. package/dist/components/elements/VirtualList.test.js +13 -0
  33. package/dist/components/layout/DashboardShell.svelte +178 -0
  34. package/dist/components/layout/DashboardShell.svelte.d.ts +67 -0
  35. package/dist/components/layout/DashboardShell.test.d.ts +1 -0
  36. package/dist/components/layout/DashboardShell.test.js +15 -0
  37. package/dist/components/layout/ResizablePanels.svelte +192 -0
  38. package/dist/components/layout/ResizablePanels.svelte.d.ts +68 -0
  39. package/dist/components/layout/ResizablePanels.test.d.ts +1 -0
  40. package/dist/components/layout/ResizablePanels.test.js +14 -0
  41. package/dist/components/overlays/NotificationCenter.svelte +163 -0
  42. package/dist/components/overlays/NotificationCenter.svelte.d.ts +22 -0
  43. package/dist/components/overlays/NotificationCenter.test.d.ts +1 -0
  44. package/dist/components/overlays/NotificationCenter.test.js +8 -0
  45. package/dist/components/saas/OrgSwitcher.svelte +146 -0
  46. package/dist/components/saas/OrgSwitcher.svelte.d.ts +20 -0
  47. package/dist/components/saas/OrgSwitcher.test.d.ts +1 -0
  48. package/dist/components/saas/OrgSwitcher.test.js +8 -0
  49. package/dist/composables/useIdle.svelte.d.ts +13 -0
  50. package/dist/composables/useIdle.svelte.js +51 -0
  51. package/dist/composables/useIdle.test.d.ts +1 -0
  52. package/dist/composables/useIdle.test.js +29 -0
  53. package/dist/index.d.ts +26 -0
  54. package/dist/index.js +14 -0
  55. package/dist/server/auth/hook.d.ts +2 -2
  56. package/dist/server/auth/hook.js +9 -3
  57. package/dist/server/auth/index.d.ts +7 -1
  58. package/dist/server/auth/index.js +11 -1
  59. package/dist/server/auth/org.test.d.ts +1 -0
  60. package/dist/server/auth/org.test.js +16 -0
  61. package/dist/server/db/schema-pg.d.ts +395 -0
  62. package/dist/server/db/schema-pg.js +41 -2
  63. package/dist/server/db/schema-sqlite.d.ts +427 -0
  64. package/dist/server/db/schema-sqlite.js +41 -2
  65. package/dist/server/db/types.d.ts +28 -0
  66. package/dist/server/index.d.ts +1 -1
  67. package/package.json +1 -1
@@ -0,0 +1,139 @@
1
+ <script module lang="ts">
2
+ import { tv, type VariantProps } from '../../utils/cn';
3
+
4
+ export interface FAQItem {
5
+ id: string | number;
6
+ question: string;
7
+ answer: string;
8
+ category?: string;
9
+ }
10
+
11
+ export const faqVariants = tv({
12
+ base: 'w-full py-12 max-w-3xl mx-auto'
13
+ });
14
+
15
+ export type FAQProps = VariantProps<typeof faqVariants> & {
16
+ title?: string;
17
+ description?: string;
18
+ badge?: string;
19
+ items?: FAQItem[];
20
+ categories?: string[];
21
+ class?: string;
22
+ };
23
+ </script>
24
+
25
+ <script lang="ts">
26
+ import Icon from '../elements/Icon.svelte';
27
+ import Badge from '../elements/Badge.svelte';
28
+
29
+ let {
30
+ title = 'Frequently Asked Questions',
31
+ description = 'Everything you need to know about licensing, deployment, and tech stack.',
32
+ badge = 'FAQ',
33
+ items = [],
34
+ categories = [],
35
+ class: className = ''
36
+ }: FAQProps = $props();
37
+
38
+ let selectedCategory = $state('all');
39
+ let openIds = $state<Array<string | number>>([]);
40
+
41
+ let availableCategories = $derived(
42
+ categories.length > 0
43
+ ? ['all', ...categories]
44
+ : ['all', ...new Set(items.map((i) => i.category).filter((c): c is string => Boolean(c)))]
45
+ );
46
+
47
+ let filteredItems = $derived(
48
+ selectedCategory === 'all' ? items : items.filter((item) => item.category === selectedCategory)
49
+ );
50
+
51
+ function toggleItem(id: string | number) {
52
+ if (openIds.includes(id)) {
53
+ openIds = openIds.filter((i) => i !== id);
54
+ } else {
55
+ openIds = [...openIds, id];
56
+ }
57
+ }
58
+ </script>
59
+
60
+ <section class={faqVariants({ class: className })}>
61
+ {#if title || description || badge}
62
+ <div class="mb-10 space-y-3 text-center">
63
+ {#if badge}
64
+ <div class="inline-flex">
65
+ <Badge variant="subtle" color="primary" size="sm">
66
+ {badge}
67
+ </Badge>
68
+ </div>
69
+ {/if}
70
+
71
+ {#if title}
72
+ <h2
73
+ class="text-2xl font-extrabold tracking-tight text-neutral-900 sm:text-3xl dark:text-white"
74
+ >
75
+ {title}
76
+ </h2>
77
+ {/if}
78
+
79
+ {#if description}
80
+ <p class="mx-auto max-w-xl text-sm text-neutral-600 dark:text-neutral-400">
81
+ {description}
82
+ </p>
83
+ {/if}
84
+ </div>
85
+ {/if}
86
+
87
+ <!-- Category Filter Tabs -->
88
+ {#if availableCategories.length > 2}
89
+ <div class="mb-8 flex flex-wrap items-center justify-center gap-2">
90
+ {#each availableCategories as cat}
91
+ <button
92
+ type="button"
93
+ onclick={() => (selectedCategory = cat)}
94
+ class="rounded-full px-3 py-1 text-xs font-medium capitalize transition-colors {selectedCategory ===
95
+ cat
96
+ ? 'bg-primary-600 text-white shadow-xs'
97
+ : 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700'}"
98
+ >
99
+ {cat}
100
+ </button>
101
+ {/each}
102
+ </div>
103
+ {/if}
104
+
105
+ <!-- FAQ Accordion List -->
106
+ <div
107
+ class="divide-y divide-neutral-200 rounded-2xl border border-neutral-200 bg-white dark:divide-neutral-800 dark:border-neutral-800 dark:bg-neutral-900/40"
108
+ >
109
+ {#each filteredItems as item (item.id)}
110
+ {@const isOpen = openIds.includes(item.id)}
111
+
112
+ <div class="overflow-hidden">
113
+ <button
114
+ type="button"
115
+ onclick={() => toggleItem(item.id)}
116
+ aria-expanded={isOpen}
117
+ class="flex w-full items-center justify-between p-5 text-left text-sm font-semibold text-neutral-900 transition-colors hover:bg-neutral-50 dark:text-white dark:hover:bg-neutral-800/40"
118
+ >
119
+ <span>{item.question}</span>
120
+ <Icon
121
+ name="chevron-down"
122
+ size="sm"
123
+ class="ml-4 shrink-0 text-neutral-400 transition-transform duration-200 {isOpen
124
+ ? 'rotate-180'
125
+ : ''}"
126
+ />
127
+ </button>
128
+
129
+ {#if isOpen}
130
+ <div
131
+ class="border-t border-neutral-100/60 px-5 pt-3 pb-5 text-xs leading-relaxed text-neutral-600 dark:border-neutral-800/60 dark:text-neutral-400"
132
+ >
133
+ {item.answer}
134
+ </div>
135
+ {/if}
136
+ </div>
137
+ {/each}
138
+ </div>
139
+ </section>
@@ -0,0 +1,19 @@
1
+ import { type VariantProps } from '../../utils/cn';
2
+ export interface FAQItem {
3
+ id: string | number;
4
+ question: string;
5
+ answer: string;
6
+ category?: string;
7
+ }
8
+ export declare const faqVariants: import("tailwind-variants").TVReturnType<{} | {} | {}, undefined, "w-full py-12 max-w-3xl mx-auto", {} | {}, undefined, import("tailwind-variants").TVReturnTypeLike<unknown, undefined>>;
9
+ export type FAQProps = VariantProps<typeof faqVariants> & {
10
+ title?: string;
11
+ description?: string;
12
+ badge?: string;
13
+ items?: FAQItem[];
14
+ categories?: string[];
15
+ class?: string;
16
+ };
17
+ declare const FAQ: import("svelte").Component<FAQProps, {}, "">;
18
+ type FAQ = ReturnType<typeof FAQ>;
19
+ export default FAQ;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { faqVariants } from './FAQ.svelte';
3
+ describe('FAQ Block', () => {
4
+ it('generates default variant classes', () => {
5
+ const classes = faqVariants();
6
+ expect(classes).toContain('w-full');
7
+ expect(classes).toContain('max-w-3xl');
8
+ });
9
+ });
@@ -0,0 +1,126 @@
1
+ <script module lang="ts">
2
+ import { tv, type VariantProps } from '../../utils/cn';
3
+ import type { IconSource } from '../elements/Icon.svelte';
4
+
5
+ export interface FeatureItem {
6
+ id: string | number;
7
+ title: string;
8
+ description: string;
9
+ icon?: IconSource;
10
+ tag?: string;
11
+ href?: string;
12
+ }
13
+
14
+ export const featureGridVariants = tv({
15
+ base: 'w-full py-12'
16
+ });
17
+
18
+ export type FeatureGridProps = VariantProps<typeof featureGridVariants> & {
19
+ title?: string;
20
+ description?: string;
21
+ badge?: string;
22
+ features?: FeatureItem[];
23
+ columns?: 2 | 3 | 4;
24
+ class?: string;
25
+ };
26
+ </script>
27
+
28
+ <script lang="ts">
29
+ import Icon from '../elements/Icon.svelte';
30
+ import Badge from '../elements/Badge.svelte';
31
+ import Card from '../layout/Card.svelte';
32
+
33
+ let {
34
+ title = 'Everything you need to ship faster',
35
+ description = 'Production-ready primitives and architecture designed for modern full-stack apps.',
36
+ badge = 'Features',
37
+ features = [],
38
+ columns = 3,
39
+ class: className = ''
40
+ }: FeatureGridProps = $props();
41
+
42
+ let gridCols = $derived(
43
+ columns === 2
44
+ ? 'grid-cols-1 md:grid-cols-2 max-w-4xl mx-auto'
45
+ : columns === 4
46
+ ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4'
47
+ : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
48
+ );
49
+ </script>
50
+
51
+ <section class={featureGridVariants({ class: className })}>
52
+ {#if title || description || badge}
53
+ <div class="mb-12 space-y-3 text-center">
54
+ {#if badge}
55
+ <div class="inline-flex">
56
+ <Badge variant="subtle" color="primary" size="sm">
57
+ {badge}
58
+ </Badge>
59
+ </div>
60
+ {/if}
61
+
62
+ {#if title}
63
+ <h2
64
+ class="text-2xl font-extrabold tracking-tight text-neutral-900 sm:text-3xl dark:text-white"
65
+ >
66
+ {title}
67
+ </h2>
68
+ {/if}
69
+
70
+ {#if description}
71
+ <p class="mx-auto max-w-2xl text-sm text-neutral-600 dark:text-neutral-400">
72
+ {description}
73
+ </p>
74
+ {/if}
75
+ </div>
76
+ {/if}
77
+
78
+ <div class="grid gap-6 {gridCols}">
79
+ {#each features as feature (feature.id)}
80
+ <Card
81
+ class="group relative flex flex-col justify-between p-6 transition-all duration-200 hover:-translate-y-1 hover:shadow-xl dark:hover:border-neutral-700"
82
+ >
83
+ <div>
84
+ <!-- Icon & Tag Row -->
85
+ <div class="mb-4 flex items-center justify-between">
86
+ {#if feature.icon}
87
+ <div
88
+ class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary-50 text-primary-600 transition-colors group-hover:bg-primary-600 group-hover:text-white dark:bg-primary-950/60 dark:text-primary-400"
89
+ >
90
+ <Icon name={feature.icon} size="sm" />
91
+ </div>
92
+ {/if}
93
+
94
+ {#if feature.tag}
95
+ <Badge size="xs" variant="subtle" color="primary">
96
+ {feature.tag}
97
+ </Badge>
98
+ {/if}
99
+ </div>
100
+
101
+ <!-- Title -->
102
+ <h3 class="text-base font-bold text-neutral-900 dark:text-white">
103
+ {feature.title}
104
+ </h3>
105
+
106
+ <!-- Description -->
107
+ <p class="mt-2 text-xs leading-relaxed text-neutral-600 dark:text-neutral-400">
108
+ {feature.description}
109
+ </p>
110
+ </div>
111
+
112
+ {#if feature.href}
113
+ <div class="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800/80">
114
+ <a
115
+ href={feature.href}
116
+ class="inline-flex items-center gap-1 text-xs font-semibold text-primary-600 transition-colors hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300"
117
+ >
118
+ Learn more
119
+ <Icon name="arrow-right" size="xs" />
120
+ </a>
121
+ </div>
122
+ {/if}
123
+ </Card>
124
+ {/each}
125
+ </div>
126
+ </section>
@@ -0,0 +1,22 @@
1
+ import { type VariantProps } from '../../utils/cn';
2
+ import type { IconSource } from '../elements/Icon.svelte';
3
+ export interface FeatureItem {
4
+ id: string | number;
5
+ title: string;
6
+ description: string;
7
+ icon?: IconSource;
8
+ tag?: string;
9
+ href?: string;
10
+ }
11
+ export declare const featureGridVariants: import("tailwind-variants").TVReturnType<{} | {} | {}, undefined, "w-full py-12", {} | {}, undefined, import("tailwind-variants").TVReturnTypeLike<unknown, undefined>>;
12
+ export type FeatureGridProps = VariantProps<typeof featureGridVariants> & {
13
+ title?: string;
14
+ description?: string;
15
+ badge?: string;
16
+ features?: FeatureItem[];
17
+ columns?: 2 | 3 | 4;
18
+ class?: string;
19
+ };
20
+ declare const FeatureGrid: import("svelte").Component<FeatureGridProps, {}, "">;
21
+ type FeatureGrid = ReturnType<typeof FeatureGrid>;
22
+ export default FeatureGrid;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { featureGridVariants } from './FeatureGrid.svelte';
3
+ describe('FeatureGrid Block', () => {
4
+ it('generates default variant classes', () => {
5
+ const classes = featureGridVariants();
6
+ expect(classes).toContain('w-full');
7
+ expect(classes).toContain('py-12');
8
+ });
9
+ });
@@ -0,0 +1,118 @@
1
+ <script module lang="ts">
2
+ import { tv, type VariantProps } from '../../utils/cn';
3
+
4
+ export interface TestimonialItem {
5
+ id: string | number;
6
+ quote: string;
7
+ author: string;
8
+ role?: string;
9
+ company?: string;
10
+ avatar?: string;
11
+ rating?: number;
12
+ }
13
+
14
+ export const testimonialsVariants = tv({
15
+ base: 'w-full py-12'
16
+ });
17
+
18
+ export type TestimonialsProps = VariantProps<typeof testimonialsVariants> & {
19
+ title?: string;
20
+ description?: string;
21
+ badge?: string;
22
+ items?: TestimonialItem[];
23
+ columns?: 1 | 2 | 3;
24
+ class?: string;
25
+ };
26
+ </script>
27
+
28
+ <script lang="ts">
29
+ import Icon from '../elements/Icon.svelte';
30
+ import Card from '../layout/Card.svelte';
31
+ import Badge from '../elements/Badge.svelte';
32
+ import Avatar from '../elements/Avatar.svelte';
33
+
34
+ let {
35
+ title = 'Loved by solo founders & engineering teams',
36
+ description = 'See what developers and makers are building with Yaxa.',
37
+ badge = 'Social Proof',
38
+ items = [],
39
+ columns = 3,
40
+ class: className = ''
41
+ }: TestimonialsProps = $props();
42
+
43
+ let gridCols = $derived(
44
+ columns === 1
45
+ ? 'grid-cols-1 max-w-2xl mx-auto'
46
+ : columns === 2
47
+ ? 'grid-cols-1 md:grid-cols-2 max-w-4xl mx-auto'
48
+ : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
49
+ );
50
+ </script>
51
+
52
+ <section class={testimonialsVariants({ class: className })}>
53
+ {#if title || description || badge}
54
+ <div class="mb-10 space-y-3 text-center">
55
+ {#if badge}
56
+ <div class="inline-flex">
57
+ <Badge variant="subtle" color="primary" size="sm">
58
+ {badge}
59
+ </Badge>
60
+ </div>
61
+ {/if}
62
+
63
+ {#if title}
64
+ <h2
65
+ class="text-2xl font-extrabold tracking-tight text-neutral-900 sm:text-3xl dark:text-white"
66
+ >
67
+ {title}
68
+ </h2>
69
+ {/if}
70
+
71
+ {#if description}
72
+ <p class="mx-auto max-w-2xl text-sm text-neutral-600 dark:text-neutral-400">
73
+ {description}
74
+ </p>
75
+ {/if}
76
+ </div>
77
+ {/if}
78
+
79
+ <div class="grid gap-6 {gridCols}">
80
+ {#each items as item (item.id)}
81
+ <Card class="flex flex-col justify-between p-6 transition-all duration-200 hover:shadow-lg">
82
+ <div>
83
+ <!-- Star Rating -->
84
+ {#if item.rating}
85
+ <div class="mb-4 flex items-center gap-1 text-amber-400">
86
+ {#each Array.from({ length: item.rating }, (_, i) => i) as starIndex (starIndex)}
87
+ <Icon name="star" size="xs" />
88
+ {/each}
89
+ </div>
90
+ {/if}
91
+
92
+ <!-- Quote Text -->
93
+ <blockquote class="text-sm leading-relaxed text-neutral-700 italic dark:text-neutral-300">
94
+ "{item.quote}"
95
+ </blockquote>
96
+ </div>
97
+
98
+ <!-- Author Info -->
99
+ <div
100
+ class="mt-6 flex items-center gap-3 border-t border-neutral-100 pt-4 dark:border-neutral-800"
101
+ >
102
+ <Avatar src={item.avatar} alt={item.author} size="sm" />
103
+
104
+ <div class="min-w-0 flex-1">
105
+ <div class="truncate text-xs font-semibold text-neutral-900 dark:text-white">
106
+ {item.author}
107
+ </div>
108
+ {#if item.role || item.company}
109
+ <div class="truncate text-[11px] text-neutral-500 dark:text-neutral-400">
110
+ {item.role}{item.role && item.company ? ' • ' : ''}{item.company}
111
+ </div>
112
+ {/if}
113
+ </div>
114
+ </div>
115
+ </Card>
116
+ {/each}
117
+ </div>
118
+ </section>
@@ -0,0 +1,22 @@
1
+ import { type VariantProps } from '../../utils/cn';
2
+ export interface TestimonialItem {
3
+ id: string | number;
4
+ quote: string;
5
+ author: string;
6
+ role?: string;
7
+ company?: string;
8
+ avatar?: string;
9
+ rating?: number;
10
+ }
11
+ export declare const testimonialsVariants: import("tailwind-variants").TVReturnType<{} | {} | {}, undefined, "w-full py-12", {} | {}, undefined, import("tailwind-variants").TVReturnTypeLike<unknown, undefined>>;
12
+ export type TestimonialsProps = VariantProps<typeof testimonialsVariants> & {
13
+ title?: string;
14
+ description?: string;
15
+ badge?: string;
16
+ items?: TestimonialItem[];
17
+ columns?: 1 | 2 | 3;
18
+ class?: string;
19
+ };
20
+ declare const Testimonials: import("svelte").Component<TestimonialsProps, {}, "">;
21
+ type Testimonials = ReturnType<typeof Testimonials>;
22
+ export default Testimonials;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { testimonialsVariants } from './Testimonials.svelte';
3
+ describe('Testimonials Block', () => {
4
+ it('generates default variant classes', () => {
5
+ const classes = testimonialsVariants();
6
+ expect(classes).toContain('w-full');
7
+ expect(classes).toContain('py-12');
8
+ });
9
+ });
@@ -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>