yaxa-svelte 1.11.0 → 1.12.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 (75) hide show
  1. package/README.md +13 -12
  2. package/dist/actions/index.d.ts +2 -0
  3. package/dist/actions/index.js +1 -0
  4. package/dist/actions/springTilt.d.ts +15 -0
  5. package/dist/actions/springTilt.js +57 -0
  6. package/dist/actions/springTilt.test.d.ts +1 -0
  7. package/dist/actions/springTilt.test.js +28 -0
  8. package/dist/components/blocks/FAQ.svelte +4 -2
  9. package/dist/components/blocks/FeatureGrid.svelte +50 -40
  10. package/dist/components/blocks/FeatureGrid.svelte.d.ts +1 -0
  11. package/dist/components/blocks/Testimonials.svelte +45 -31
  12. package/dist/components/blocks/Testimonials.svelte.d.ts +1 -0
  13. package/dist/components/elements/CodeDiff.svelte +454 -0
  14. package/dist/components/elements/CodeDiff.svelte.d.ts +38 -0
  15. package/dist/components/elements/CodeDiff.test.d.ts +1 -0
  16. package/dist/components/elements/CodeDiff.test.js +52 -0
  17. package/dist/components/elements/DataTable.dom.test.js +30 -2
  18. package/dist/components/elements/DataTable.svelte +128 -80
  19. package/dist/components/elements/DataTable.svelte.d.ts +19 -2
  20. package/dist/components/forms/Checkbox.svelte +30 -5
  21. package/dist/components/forms/Checkbox.svelte.d.ts +1 -0
  22. package/dist/components/forms/Combobox.svelte +31 -5
  23. package/dist/components/forms/Combobox.svelte.d.ts +6 -0
  24. package/dist/components/forms/CreditCardInput.svelte +32 -8
  25. package/dist/components/forms/CreditCardInput.svelte.d.ts +3 -0
  26. package/dist/components/forms/Form.dom.test.d.ts +1 -0
  27. package/dist/components/forms/Form.dom.test.js +59 -0
  28. package/dist/components/forms/Form.svelte +93 -3
  29. package/dist/components/forms/Form.svelte.d.ts +2 -0
  30. package/dist/components/forms/FormField.svelte +75 -11
  31. package/dist/components/forms/FormField.svelte.d.ts +2 -1
  32. package/dist/components/forms/Input.svelte +18 -1
  33. package/dist/components/forms/InputOTP.svelte +12 -3
  34. package/dist/components/forms/MultiSelect.svelte +33 -7
  35. package/dist/components/forms/MultiSelect.svelte.d.ts +6 -0
  36. package/dist/components/forms/NumberInput.svelte +23 -5
  37. package/dist/components/forms/NumberInput.svelte.d.ts +3 -0
  38. package/dist/components/forms/PhoneInput.svelte +36 -2
  39. package/dist/components/forms/PhoneInput.svelte.d.ts +5 -0
  40. package/dist/components/forms/Select.svelte +36 -18
  41. package/dist/components/forms/Select.svelte.d.ts +3 -0
  42. package/dist/components/forms/Switch.svelte +22 -3
  43. package/dist/components/forms/Switch.svelte.d.ts +1 -0
  44. package/dist/components/forms/Textarea.svelte +23 -2
  45. package/dist/components/forms/advanced-forms.test.js +21 -0
  46. package/dist/components/forms/form-context.d.ts +38 -0
  47. package/dist/components/forms/form-context.js +15 -0
  48. package/dist/components/layout/Footer.svelte +18 -30
  49. package/dist/components/saas/AuthCard.svelte +299 -30
  50. package/dist/components/saas/AuthCard.svelte.d.ts +5 -1
  51. package/dist/components/saas/PasskeyUI.svelte +235 -0
  52. package/dist/components/saas/PasskeyUI.svelte.d.ts +13 -0
  53. package/dist/components/saas/TwoFactorModal.svelte +253 -0
  54. package/dist/components/saas/TwoFactorModal.svelte.d.ts +9 -0
  55. package/dist/composables/useAuth.svelte.d.ts +732 -1
  56. package/dist/composables/useAuth.svelte.js +240 -2
  57. package/dist/composables/useDataTable.svelte.d.ts +34 -0
  58. package/dist/composables/useDataTable.svelte.js +130 -0
  59. package/dist/index.d.ts +18 -3
  60. package/dist/index.js +11 -2
  61. package/dist/motion/transitions.d.ts +22 -0
  62. package/dist/motion/transitions.js +55 -0
  63. package/dist/server/auth/index.d.ts +25 -1
  64. package/dist/server/auth/index.js +10 -3
  65. package/dist/server/db/pagination.d.ts +36 -0
  66. package/dist/server/db/pagination.js +40 -0
  67. package/dist/server/db/pagination.test.d.ts +1 -0
  68. package/dist/server/db/pagination.test.js +41 -0
  69. package/dist/server/db/schema-pg.d.ts +269 -0
  70. package/dist/server/db/schema-pg.js +28 -2
  71. package/dist/server/db/schema-sqlite.d.ts +291 -0
  72. package/dist/server/db/schema-sqlite.js +30 -4
  73. package/dist/server/index.d.ts +2 -0
  74. package/dist/server/index.js +2 -1
  75. package/package.json +1 -1
package/README.md CHANGED
@@ -231,18 +231,19 @@ export const POST = createPolarWebhookHandler();
231
231
 
232
232
  ## 🧩 Component Library
233
233
 
234
- | Category | Components |
235
- | :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
236
- | **Theming & Studio** | `<ThemeStudio>`, `theme`, `ACCENT_PALETTES`, `NEUTRAL_PALETTES`, `RADIUS_PRESETS`, Harmonic Radius Multiplier Scale, Chart Palette Series Tokens |
237
- | **AI Primitives** | `<AiChat>`, `<PromptBar>`, `<AiThought>`, `<AiToolCall>` |
238
- | **Charts (Pure SVG)** | `<LineChart>`, `<BarChart>`, `<DonutChart>` (0 external charting dependencies, auto-responsive, dark/light theme aware) |
239
- | **SaaS Suite** | `<Gate>`, `<AuthCard>`, `<UserMenu>`, `<OrgSwitcher>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
240
- | **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
241
- | **Elements** | `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `DataTable` (with Column Visibility & Export), `Chip`, `Meter`, `MetricCard`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo`, `CodeBlock` (with Svelte 5 Runes Highlighting), `Tree`, `Timeline`, `Terminal`, `VirtualList`, `Carousel` |
242
- | **Forms** | `Form`, `FormField`, `Input`, `InputOTP`, `Textarea`, `Checkbox`, `Switch`, `Select`, `Combobox`, `MultiSelect`, `NumberInput`, `RadioGroup`, `Slider`, `ColorPicker`, `ToggleGroup`, `DatePicker`, `DateRangePicker`, `Dropzone`, `<RichTextEditor>`, `<PhoneInput>`, `<CreditCardInput>` |
243
- | **Layout & Blocks** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp`, `DashboardShell` (with independent sidebar tokens), `ResizablePanels`, Page Blocks (`Hero`, `SaaS Metrics`, `FAQ`, `Testimonials`, `FeatureGrid`) |
244
- | **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Drawer`, `Slideover`, `Popover`, `Tooltip` (with `arrow={false}` & `unstyled`), `Alert`, `Accordion`, `NotificationCenter`, `Stepper` |
245
- | **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useInfiniteScroll`, `useSortable` |
234
+ | Category | Components |
235
+ | :--------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
236
+ | **Theming & Studio** | `<ThemeStudio>`, `theme`, `ACCENT_PALETTES`, `NEUTRAL_PALETTES`, `RADIUS_PRESETS`, Harmonic Radius Multiplier Scale, Chart Palette Series Tokens |
237
+ | **AI Primitives** | `<AiChat>`, `<PromptBar>`, `<AiThought>`, `<AiToolCall>` |
238
+ | **Charts (Pure SVG)** | `<LineChart>`, `<BarChart>`, `<DonutChart>` (0 external charting dependencies, auto-responsive, dark/light theme aware) |
239
+ | **SaaS & Auth Suite** | `<PasskeyUI>` (WebAuthn), `<TwoFactorModal>` (TOTP), `<AuthCard>` (Polymorphic), `<Gate>`, `<UserMenu>`, `<OrgSwitcher>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
240
+ | **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
241
+ | **Elements & Code** | `<CodeDiff>` (Interactive Diff & Changelog), `<CodeBlock>`, `<DataTable>` (Client & Server Modes), `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `Chip`, `Meter`, `MetricCard`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo`, `Tree`, `Timeline`, `Terminal`, `VirtualList`, `Carousel` |
242
+ | **Forms (Auto-Wired)** | `<Form>`, `<FormField>`, `Input`, `InputOTP`, `Textarea`, `Checkbox`, `Switch`, `Select`, `Combobox`, `MultiSelect`, `NumberInput`, `PhoneInput`, `CreditCardInput`, `RadioGroup`, `Slider`, `ColorPicker`, `ToggleGroup`, `DatePicker`, `DateRangePicker`, `Dropzone`, `<RichTextEditor>` |
243
+ | **Layout & Blocks** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp`, `DashboardShell` (with independent sidebar tokens), `ResizablePanels`, Page Blocks (`Hero`, `SaaS Metrics`, `FAQ`, `Testimonials`, `FeatureGrid`) |
244
+ | **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Drawer`, `Slideover`, `Popover`, `Tooltip` (with `arrow={false}` & `unstyled`), `Alert`, `Accordion`, `NotificationCenter`, `Stepper` |
245
+ | **Motion & Actions** | `springTilt`, `smoothSlide`, `staggerFly`, `autosize`, `clickOutside`, `portal`, `sortableItem`, `infiniteScrollAction` |
246
+ | **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`, `useInfiniteScroll`, `useSortable`, `useDataTable`, `useLocale` |
246
247
 
247
248
  ---
248
249
 
@@ -8,3 +8,5 @@ export { infiniteScroll } from './infiniteScroll';
8
8
  export type { InfiniteScrollActionOptions } from './infiniteScroll';
9
9
  export { sortableItem } from './sortable';
10
10
  export type { SortableItemOptions } from './sortable';
11
+ export { springTilt } from './springTilt';
12
+ export type { SpringTiltOptions } from './springTilt';
@@ -3,3 +3,4 @@ export { clickOutside } from './clickOutside';
3
3
  export { portal } from './portal';
4
4
  export { infiniteScroll } from './infiniteScroll';
5
5
  export { sortableItem } from './sortable';
6
+ export { springTilt } from './springTilt';
@@ -0,0 +1,15 @@
1
+ export interface SpringTiltOptions {
2
+ max?: number;
3
+ perspective?: number;
4
+ scale?: number;
5
+ stiffness?: number;
6
+ damping?: number;
7
+ disabled?: boolean;
8
+ }
9
+ /**
10
+ * Svelte Action that applies physics-driven 3D spring tilt on hover.
11
+ */
12
+ export declare function springTilt(node: HTMLElement, options?: SpringTiltOptions): {
13
+ update(newOptions?: SpringTiltOptions): void;
14
+ destroy(): void;
15
+ };
@@ -0,0 +1,57 @@
1
+ import { spring } from 'svelte/motion';
2
+ /**
3
+ * Svelte Action that applies physics-driven 3D spring tilt on hover.
4
+ */
5
+ export function springTilt(node, options = {}) {
6
+ let max = options.max ?? 10;
7
+ let perspective = options.perspective ?? 1000;
8
+ let scale = options.scale ?? 1.02;
9
+ const stiffness = options.stiffness ?? 0.12;
10
+ const damping = options.damping ?? 0.28;
11
+ let disabled = options.disabled ?? false;
12
+ const coords = spring({ x: 0, y: 0, scale: 1 }, { stiffness, damping });
13
+ const unsub = coords.subscribe((val) => {
14
+ if (disabled) {
15
+ node.style.transform = '';
16
+ return;
17
+ }
18
+ node.style.transform = `perspective(${perspective}px) rotateX(${val.x.toFixed(2)}deg) rotateY(${val.y.toFixed(2)}deg) scale(${val.scale.toFixed(3)})`;
19
+ node.style.transformStyle = 'preserve-3d';
20
+ });
21
+ function onMouseMove(e) {
22
+ if (disabled)
23
+ return;
24
+ const rect = node.getBoundingClientRect();
25
+ const x = e.clientX - rect.left;
26
+ const y = e.clientY - rect.top;
27
+ const centerX = rect.width / 2;
28
+ const centerY = rect.height / 2;
29
+ const rotateX = -((y - centerY) / (centerY || 1)) * max;
30
+ const rotateY = ((x - centerX) / (centerX || 1)) * max;
31
+ coords.set({ x: rotateX, y: rotateY, scale });
32
+ }
33
+ function onMouseLeave() {
34
+ if (disabled)
35
+ return;
36
+ coords.set({ x: 0, y: 0, scale: 1 });
37
+ }
38
+ node.addEventListener('mousemove', onMouseMove);
39
+ node.addEventListener('mouseleave', onMouseLeave);
40
+ return {
41
+ update(newOptions = {}) {
42
+ max = newOptions.max ?? 10;
43
+ perspective = newOptions.perspective ?? 1000;
44
+ scale = newOptions.scale ?? 1.02;
45
+ disabled = Boolean(newOptions.disabled);
46
+ if (disabled) {
47
+ node.style.transform = '';
48
+ }
49
+ },
50
+ destroy() {
51
+ unsub();
52
+ node.removeEventListener('mousemove', onMouseMove);
53
+ node.removeEventListener('mouseleave', onMouseLeave);
54
+ node.style.transform = '';
55
+ }
56
+ };
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { springTilt } from './springTilt';
3
+ describe('springTilt Action', () => {
4
+ it('attaches event listeners and updates transform on mousemove', () => {
5
+ const div = document.createElement('div');
6
+ Object.defineProperty(div, 'getBoundingClientRect', {
7
+ value: () => ({
8
+ left: 0,
9
+ top: 0,
10
+ width: 200,
11
+ height: 200,
12
+ right: 200,
13
+ bottom: 200
14
+ })
15
+ });
16
+ const action = springTilt(div, { max: 15, perspective: 800 });
17
+ expect(div.style.transformStyle).toBe('preserve-3d');
18
+ // Dispatch mousemove
19
+ const moveEvent = new MouseEvent('mousemove', {
20
+ clientX: 150,
21
+ clientY: 150
22
+ });
23
+ div.dispatchEvent(moveEvent);
24
+ // Cleanup
25
+ action.destroy();
26
+ expect(div.style.transform).toBe('');
27
+ });
28
+ });
@@ -25,6 +25,7 @@
25
25
  <script lang="ts">
26
26
  import Icon from '../elements/Icon.svelte';
27
27
  import Badge from '../elements/Badge.svelte';
28
+ import { smoothSlide } from '../../motion/transitions';
28
29
 
29
30
  let {
30
31
  title = 'Frequently Asked Questions',
@@ -120,14 +121,15 @@
120
121
  <Icon
121
122
  name="chevron-down"
122
123
  size="sm"
123
- class="ml-4 shrink-0 text-neutral-400 transition-transform duration-200 {isOpen
124
- ? 'rotate-180'
124
+ class="ml-4 shrink-0 text-neutral-400 transition-transform duration-250 ease-out {isOpen
125
+ ? 'rotate-180 text-primary-600 dark:text-primary-400'
125
126
  : ''}"
126
127
  />
127
128
  </button>
128
129
 
129
130
  {#if isOpen}
130
131
  <div
132
+ transition:smoothSlide={{ duration: 220 }}
131
133
  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
134
  >
133
135
  {item.answer}
@@ -21,6 +21,7 @@
21
21
  badge?: string;
22
22
  features?: FeatureItem[];
23
23
  columns?: 2 | 3 | 4;
24
+ interactive?: boolean;
24
25
  class?: string;
25
26
  };
26
27
  </script>
@@ -29,6 +30,8 @@
29
30
  import Icon from '../elements/Icon.svelte';
30
31
  import Badge from '../elements/Badge.svelte';
31
32
  import Card from '../layout/Card.svelte';
33
+ import { springTilt } from '../../actions/springTilt';
34
+ import { staggerFly } from '../../motion/transitions';
32
35
 
33
36
  let {
34
37
  title = 'Everything you need to ship faster',
@@ -36,6 +39,7 @@
36
39
  badge = 'Features',
37
40
  features = [],
38
41
  columns = 3,
42
+ interactive = true,
39
43
  class: className = ''
40
44
  }: FeatureGridProps = $props();
41
45
 
@@ -76,51 +80,57 @@
76
80
  {/if}
77
81
 
78
82
  <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"
83
+ {#each features as feature, idx (feature.id)}
84
+ <div
85
+ in:staggerFly={{ index: idx, y: 24, duration: 320 }}
86
+ use:springTilt={{ disabled: !interactive, max: 8, scale: 1.015 }}
87
+ class="h-full"
82
88
  >
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}
89
+ <Card
90
+ class="group relative flex h-full flex-col justify-between p-6 transition-all duration-200 hover:shadow-xl dark:hover:border-neutral-700"
91
+ >
92
+ <div>
93
+ <!-- Icon & Tag Row -->
94
+ <div class="mb-4 flex items-center justify-between">
95
+ {#if feature.icon}
96
+ <div
97
+ 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"
98
+ >
99
+ <Icon name={feature.icon} size="sm" />
100
+ </div>
101
+ {/if}
93
102
 
94
- {#if feature.tag}
95
- <Badge size="xs" variant="subtle" color="primary">
96
- {feature.tag}
97
- </Badge>
98
- {/if}
99
- </div>
103
+ {#if feature.tag}
104
+ <Badge size="xs" variant="subtle" color="primary">
105
+ {feature.tag}
106
+ </Badge>
107
+ {/if}
108
+ </div>
100
109
 
101
- <!-- Title -->
102
- <h3 class="text-base font-bold text-neutral-900 dark:text-white">
103
- {feature.title}
104
- </h3>
110
+ <!-- Title -->
111
+ <h3 class="text-base font-bold text-neutral-900 dark:text-white">
112
+ {feature.title}
113
+ </h3>
105
114
 
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>
115
+ <!-- Description -->
116
+ <p class="mt-2 text-xs leading-relaxed text-neutral-600 dark:text-neutral-400">
117
+ {feature.description}
118
+ </p>
121
119
  </div>
122
- {/if}
123
- </Card>
120
+
121
+ {#if feature.href}
122
+ <div class="mt-4 border-t border-neutral-100 pt-3 dark:border-neutral-800/80">
123
+ <a
124
+ href={feature.href}
125
+ 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"
126
+ >
127
+ Learn more
128
+ <Icon name="arrow-right" size="xs" />
129
+ </a>
130
+ </div>
131
+ {/if}
132
+ </Card>
133
+ </div>
124
134
  {/each}
125
135
  </div>
126
136
  </section>
@@ -15,6 +15,7 @@ export type FeatureGridProps = VariantProps<typeof featureGridVariants> & {
15
15
  badge?: string;
16
16
  features?: FeatureItem[];
17
17
  columns?: 2 | 3 | 4;
18
+ interactive?: boolean;
18
19
  class?: string;
19
20
  };
20
21
  declare const FeatureGrid: import("svelte").Component<FeatureGridProps, {}, "">;
@@ -21,6 +21,7 @@
21
21
  badge?: string;
22
22
  items?: TestimonialItem[];
23
23
  columns?: 1 | 2 | 3;
24
+ interactive?: boolean;
24
25
  class?: string;
25
26
  };
26
27
  </script>
@@ -30,6 +31,8 @@
30
31
  import Card from '../layout/Card.svelte';
31
32
  import Badge from '../elements/Badge.svelte';
32
33
  import Avatar from '../elements/Avatar.svelte';
34
+ import { springTilt } from '../../actions/springTilt';
35
+ import { staggerFly } from '../../motion/transitions';
33
36
 
34
37
  let {
35
38
  title = 'Loved by solo founders & engineering teams',
@@ -37,6 +40,7 @@
37
40
  badge = 'Social Proof',
38
41
  items = [],
39
42
  columns = 3,
43
+ interactive = true,
40
44
  class: className = ''
41
45
  }: TestimonialsProps = $props();
42
46
 
@@ -77,42 +81,52 @@
77
81
  {/if}
78
82
 
79
83
  <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}
84
+ {#each items as item, idx (item.id)}
85
+ <div
86
+ in:staggerFly={{ index: idx, y: 20, duration: 300 }}
87
+ use:springTilt={{ disabled: !interactive, max: 6, scale: 1.01 }}
88
+ class="h-full"
89
+ >
90
+ <Card
91
+ class="flex h-full flex-col justify-between p-6 transition-all duration-200 hover:shadow-lg dark:hover:border-neutral-700"
92
+ >
93
+ <div>
94
+ <!-- Star Rating -->
95
+ {#if item.rating}
96
+ <div class="mb-4 flex items-center gap-1 text-amber-400">
97
+ {#each Array.from({ length: item.rating }, (_, i) => i) as starIndex (starIndex)}
98
+ <Icon name="star" size="xs" />
99
+ {/each}
100
+ </div>
101
+ {/if}
91
102
 
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>
103
+ <!-- Quote Text -->
104
+ <blockquote
105
+ class="text-sm leading-relaxed text-neutral-700 italic dark:text-neutral-300"
106
+ >
107
+ "{item.quote}"
108
+ </blockquote>
109
+ </div>
97
110
 
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" />
111
+ <!-- Author Info -->
112
+ <div
113
+ class="mt-6 flex items-center gap-3 border-t border-neutral-100 pt-4 dark:border-neutral-800"
114
+ >
115
+ <Avatar src={item.avatar} alt={item.author} size="sm" />
103
116
 
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}
117
+ <div class="min-w-0 flex-1">
118
+ <div class="truncate text-xs font-semibold text-neutral-900 dark:text-white">
119
+ {item.author}
111
120
  </div>
112
- {/if}
121
+ {#if item.role || item.company}
122
+ <div class="truncate text-[11px] text-neutral-500 dark:text-neutral-400">
123
+ {item.role}{item.role && item.company ? ' • ' : ''}{item.company}
124
+ </div>
125
+ {/if}
126
+ </div>
113
127
  </div>
114
- </div>
115
- </Card>
128
+ </Card>
129
+ </div>
116
130
  {/each}
117
131
  </div>
118
132
  </section>
@@ -15,6 +15,7 @@ export type TestimonialsProps = VariantProps<typeof testimonialsVariants> & {
15
15
  badge?: string;
16
16
  items?: TestimonialItem[];
17
17
  columns?: 1 | 2 | 3;
18
+ interactive?: boolean;
18
19
  class?: string;
19
20
  };
20
21
  declare const Testimonials: import("svelte").Component<TestimonialsProps, {}, "">;