yaxa-svelte 1.5.2 → 1.7.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/README.md +5 -2
- package/dist/components/blocks/FAQ.svelte +139 -0
- package/dist/components/blocks/FAQ.svelte.d.ts +19 -0
- package/dist/components/blocks/FAQ.test.d.ts +1 -0
- package/dist/components/blocks/FAQ.test.js +9 -0
- package/dist/components/blocks/FeatureGrid.svelte +126 -0
- package/dist/components/blocks/FeatureGrid.svelte.d.ts +22 -0
- package/dist/components/blocks/FeatureGrid.test.d.ts +1 -0
- package/dist/components/blocks/FeatureGrid.test.js +9 -0
- package/dist/components/blocks/Testimonials.svelte +118 -0
- package/dist/components/blocks/Testimonials.svelte.d.ts +22 -0
- package/dist/components/blocks/Testimonials.test.d.ts +1 -0
- package/dist/components/blocks/Testimonials.test.js +9 -0
- package/dist/components/elements/EmptyState.svelte +99 -0
- package/dist/components/elements/EmptyState.svelte.d.ts +45 -0
- package/dist/components/elements/EmptyState.test.d.ts +1 -0
- package/dist/components/elements/EmptyState.test.js +16 -0
- package/dist/components/elements/Icon.svelte +1 -0
- package/dist/components/elements/Terminal.svelte +107 -0
- package/dist/components/elements/Terminal.svelte.d.ts +17 -0
- package/dist/components/elements/Terminal.test.d.ts +1 -0
- package/dist/components/elements/Terminal.test.js +10 -0
- package/dist/components/elements/Timeline.svelte +118 -0
- package/dist/components/elements/Timeline.svelte.d.ts +39 -0
- package/dist/components/elements/Timeline.test.d.ts +1 -0
- package/dist/components/elements/Timeline.test.js +15 -0
- package/dist/components/elements/Tree.svelte +161 -0
- package/dist/components/elements/Tree.svelte.d.ts +37 -0
- package/dist/components/elements/Tree.test.d.ts +1 -0
- package/dist/components/elements/Tree.test.js +13 -0
- package/dist/components/forms/Combobox.svelte +243 -0
- package/dist/components/forms/Combobox.svelte.d.ts +56 -0
- package/dist/components/forms/Combobox.test.d.ts +1 -0
- package/dist/components/forms/Combobox.test.js +16 -0
- package/dist/components/forms/MultiSelect.svelte +238 -0
- package/dist/components/forms/MultiSelect.svelte.d.ts +54 -0
- package/dist/components/forms/MultiSelect.test.d.ts +1 -0
- package/dist/components/forms/MultiSelect.test.js +16 -0
- package/dist/components/forms/NumberInput.svelte +141 -0
- package/dist/components/forms/NumberInput.svelte.d.ts +47 -0
- package/dist/components/forms/NumberInput.test.d.ts +1 -0
- package/dist/components/forms/NumberInput.test.js +16 -0
- package/dist/components/layout/Footer.svelte +11 -0
- package/dist/components/layout/Header.svelte +14 -0
- package/dist/components/navigation/Stepper.svelte +139 -0
- package/dist/components/navigation/Stepper.svelte.d.ts +36 -0
- package/dist/components/navigation/Stepper.test.d.ts +1 -0
- package/dist/components/navigation/Stepper.test.js +14 -0
- package/dist/components/overlays/Drawer.svelte +105 -0
- package/dist/components/overlays/Drawer.svelte.d.ts +32 -0
- package/dist/components/overlays/Drawer.test.d.ts +1 -0
- package/dist/components/overlays/Drawer.test.js +14 -0
- package/dist/components/overlays/NotificationCenter.svelte +163 -0
- package/dist/components/overlays/NotificationCenter.svelte.d.ts +22 -0
- package/dist/components/overlays/NotificationCenter.test.d.ts +1 -0
- package/dist/components/overlays/NotificationCenter.test.js +8 -0
- package/dist/components/saas/OrgSwitcher.svelte +146 -0
- package/dist/components/saas/OrgSwitcher.svelte.d.ts +20 -0
- package/dist/components/saas/OrgSwitcher.test.d.ts +1 -0
- package/dist/components/saas/OrgSwitcher.test.js +8 -0
- package/dist/composables/useIdle.svelte.d.ts +13 -0
- package/dist/composables/useIdle.svelte.js +51 -0
- package/dist/composables/useIdle.test.d.ts +1 -0
- package/dist/composables/useIdle.test.js +29 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +18 -0
- package/dist/site/config.d.ts +1 -0
- package/dist/site/schema.js +2 -0
- package/dist/testing/index.d.ts +24 -0
- package/dist/testing/index.js +41 -0
- package/dist/testing/index.test.d.ts +1 -0
- package/dist/testing/index.test.js +27 -0
- package/dist/vite/index.d.ts +18 -3
- package/dist/vite/index.js +38 -8
- package/dist/vite/index.test.d.ts +1 -0
- package/dist/vite/index.test.js +63 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
**The Intuitive Svelte UI & Solo SaaS Library**
|
|
6
6
|
_Nuxt UI v4 & Nuxt UI Pro Equivalent for SvelteKit 2.7+ & Svelte 5 with Built-in SEO & SaaS Parity._
|
|
7
7
|
|
|
8
|
+
[](https://www.npmjs.com/package/yaxa-svelte)
|
|
9
|
+
[](https://www.npmjs.com/package/yaxa-svelte)
|
|
8
10
|
[](https://svelte.dev)
|
|
9
11
|
[](https://kit.svelte.dev)
|
|
10
12
|
[](https://tailwindcss.com)
|
|
@@ -51,16 +53,17 @@ _(Optional full-stack SaaS features: `pnpm add better-auth drizzle-orm @polar-sh
|
|
|
51
53
|
|
|
52
54
|
### 2. Configure `vite.config.ts`
|
|
53
55
|
|
|
54
|
-
Add Tailwind CSS to your Vite plugins:
|
|
56
|
+
Add Tailwind CSS and the `yaxa()` plugin to your Vite plugins:
|
|
55
57
|
|
|
56
58
|
```ts
|
|
57
59
|
// vite.config.ts
|
|
58
60
|
import { sveltekit } from '@sveltejs/kit/vite';
|
|
59
61
|
import tailwindcss from '@tailwindcss/vite';
|
|
62
|
+
import { yaxa } from 'yaxa-svelte/vite';
|
|
60
63
|
import { defineConfig } from 'vite';
|
|
61
64
|
|
|
62
65
|
export default defineConfig({
|
|
63
|
-
plugins: [tailwindcss(), sveltekit()]
|
|
66
|
+
plugins: [tailwindcss(), sveltekit(), yaxa()]
|
|
64
67
|
});
|
|
65
68
|
```
|
|
66
69
|
|
|
@@ -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,99 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { tv, type VariantProps } from '../../utils/cn';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { IconSource } from './Icon.svelte';
|
|
5
|
+
|
|
6
|
+
export const emptyStateVariants = tv({
|
|
7
|
+
base: 'flex flex-col items-center justify-center text-center p-8 rounded-2xl bg-white dark:bg-neutral-900/50',
|
|
8
|
+
variants: {
|
|
9
|
+
size: {
|
|
10
|
+
sm: 'py-6 px-4 gap-2',
|
|
11
|
+
md: 'py-10 px-6 gap-3',
|
|
12
|
+
lg: 'py-16 px-8 gap-4'
|
|
13
|
+
},
|
|
14
|
+
bordered: {
|
|
15
|
+
true: 'border border-dashed border-neutral-300 dark:border-neutral-800',
|
|
16
|
+
false: 'border-0'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
size: 'md',
|
|
21
|
+
bordered: true
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type EmptyStateProps = VariantProps<typeof emptyStateVariants> & {
|
|
26
|
+
title?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
icon?: IconSource;
|
|
29
|
+
actions?: Snippet;
|
|
30
|
+
children?: Snippet;
|
|
31
|
+
class?: string;
|
|
32
|
+
};
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<script lang="ts">
|
|
36
|
+
import Icon from './Icon.svelte';
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
title,
|
|
40
|
+
description,
|
|
41
|
+
icon = 'sparkles',
|
|
42
|
+
size = 'md',
|
|
43
|
+
bordered = true,
|
|
44
|
+
actions,
|
|
45
|
+
children,
|
|
46
|
+
class: className = ''
|
|
47
|
+
}: EmptyStateProps = $props();
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class={emptyStateVariants({ size, bordered, class: className })}>
|
|
51
|
+
{#if icon}
|
|
52
|
+
<div
|
|
53
|
+
class="flex items-center justify-center rounded-2xl bg-neutral-100 p-4 text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400 {size ===
|
|
54
|
+
'sm'
|
|
55
|
+
? 'h-10 w-10 p-2'
|
|
56
|
+
: size === 'lg'
|
|
57
|
+
? 'h-16 w-16 p-5'
|
|
58
|
+
: 'h-12 w-12 p-3'}"
|
|
59
|
+
>
|
|
60
|
+
<Icon name={icon} size={size === 'sm' ? 'sm' : size === 'lg' ? 'lg' : 'md'} />
|
|
61
|
+
</div>
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
{#if title}
|
|
65
|
+
<h3
|
|
66
|
+
class="font-bold text-neutral-900 dark:text-white {size === 'sm'
|
|
67
|
+
? 'text-sm'
|
|
68
|
+
: size === 'lg'
|
|
69
|
+
? 'text-xl'
|
|
70
|
+
: 'text-base'}"
|
|
71
|
+
>
|
|
72
|
+
{title}
|
|
73
|
+
</h3>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
{#if description}
|
|
77
|
+
<p
|
|
78
|
+
class="max-w-md text-neutral-500 dark:text-neutral-400 {size === 'sm'
|
|
79
|
+
? 'text-xs'
|
|
80
|
+
: size === 'lg'
|
|
81
|
+
? 'text-base'
|
|
82
|
+
: 'text-sm'}"
|
|
83
|
+
>
|
|
84
|
+
{description}
|
|
85
|
+
</p>
|
|
86
|
+
{/if}
|
|
87
|
+
|
|
88
|
+
{#if children}
|
|
89
|
+
<div class="mt-1">
|
|
90
|
+
{@render children()}
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
|
|
94
|
+
{#if actions}
|
|
95
|
+
<div class="mt-4 flex flex-wrap items-center justify-center gap-3">
|
|
96
|
+
{@render actions()}
|
|
97
|
+
</div>
|
|
98
|
+
{/if}
|
|
99
|
+
</div>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type VariantProps } from '../../utils/cn';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { IconSource } from './Icon.svelte';
|
|
4
|
+
export declare const emptyStateVariants: import("tailwind-variants").TVReturnType<{
|
|
5
|
+
size: {
|
|
6
|
+
sm: "py-6 px-4 gap-2";
|
|
7
|
+
md: "py-10 px-6 gap-3";
|
|
8
|
+
lg: "py-16 px-8 gap-4";
|
|
9
|
+
};
|
|
10
|
+
bordered: {
|
|
11
|
+
true: "border border-dashed border-neutral-300 dark:border-neutral-800";
|
|
12
|
+
false: "border-0";
|
|
13
|
+
};
|
|
14
|
+
}, undefined, "flex flex-col items-center justify-center text-center p-8 rounded-2xl bg-white dark:bg-neutral-900/50", {
|
|
15
|
+
size: {
|
|
16
|
+
sm: "py-6 px-4 gap-2";
|
|
17
|
+
md: "py-10 px-6 gap-3";
|
|
18
|
+
lg: "py-16 px-8 gap-4";
|
|
19
|
+
};
|
|
20
|
+
bordered: {
|
|
21
|
+
true: "border border-dashed border-neutral-300 dark:border-neutral-800";
|
|
22
|
+
false: "border-0";
|
|
23
|
+
};
|
|
24
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
25
|
+
size: {
|
|
26
|
+
sm: "py-6 px-4 gap-2";
|
|
27
|
+
md: "py-10 px-6 gap-3";
|
|
28
|
+
lg: "py-16 px-8 gap-4";
|
|
29
|
+
};
|
|
30
|
+
bordered: {
|
|
31
|
+
true: "border border-dashed border-neutral-300 dark:border-neutral-800";
|
|
32
|
+
false: "border-0";
|
|
33
|
+
};
|
|
34
|
+
}, undefined>>;
|
|
35
|
+
export type EmptyStateProps = VariantProps<typeof emptyStateVariants> & {
|
|
36
|
+
title?: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
icon?: IconSource;
|
|
39
|
+
actions?: Snippet;
|
|
40
|
+
children?: Snippet;
|
|
41
|
+
class?: string;
|
|
42
|
+
};
|
|
43
|
+
declare const EmptyState: import("svelte").Component<EmptyStateProps, {}, "">;
|
|
44
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
45
|
+
export default EmptyState;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { emptyStateVariants } from './EmptyState.svelte';
|
|
3
|
+
describe('EmptyState Component', () => {
|
|
4
|
+
it('generates default variant classes', () => {
|
|
5
|
+
const classes = emptyStateVariants();
|
|
6
|
+
expect(classes).toContain('border-dashed');
|
|
7
|
+
expect(classes).toContain('py-10');
|
|
8
|
+
});
|
|
9
|
+
it('supports size and bordered variants', () => {
|
|
10
|
+
const smClasses = emptyStateVariants({ size: 'sm', bordered: false });
|
|
11
|
+
expect(smClasses).toContain('py-6');
|
|
12
|
+
expect(smClasses).toContain('border-0');
|
|
13
|
+
const lgClasses = emptyStateVariants({ size: 'lg' });
|
|
14
|
+
expect(lgClasses).toContain('py-16');
|
|
15
|
+
});
|
|
16
|
+
});
|