yaxa-svelte 1.10.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 (115) hide show
  1. package/README.md +19 -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/admin/handler.js +13 -13
  9. package/dist/components/ai/AiChat.svelte +263 -0
  10. package/dist/components/ai/AiChat.svelte.d.ts +41 -0
  11. package/dist/components/ai/AiThought.svelte +108 -0
  12. package/dist/components/ai/AiThought.svelte.d.ts +12 -0
  13. package/dist/components/ai/AiToolCall.svelte +133 -0
  14. package/dist/components/ai/AiToolCall.svelte.d.ts +13 -0
  15. package/dist/components/ai/PromptBar.svelte +279 -0
  16. package/dist/components/ai/PromptBar.svelte.d.ts +22 -0
  17. package/dist/components/ai/ai.test.d.ts +1 -0
  18. package/dist/components/ai/ai.test.js +13 -0
  19. package/dist/components/ai/index.d.ts +8 -0
  20. package/dist/components/ai/index.js +4 -0
  21. package/dist/components/blocks/FAQ.svelte +4 -2
  22. package/dist/components/blocks/FeatureGrid.svelte +50 -40
  23. package/dist/components/blocks/FeatureGrid.svelte.d.ts +1 -0
  24. package/dist/components/blocks/Testimonials.svelte +45 -31
  25. package/dist/components/blocks/Testimonials.svelte.d.ts +1 -0
  26. package/dist/components/charts/BarChart.svelte +248 -0
  27. package/dist/components/charts/BarChart.svelte.d.ts +22 -0
  28. package/dist/components/charts/DonutChart.svelte +153 -0
  29. package/dist/components/charts/DonutChart.svelte.d.ts +18 -0
  30. package/dist/components/charts/LineChart.svelte +334 -0
  31. package/dist/components/charts/LineChart.svelte.d.ts +28 -0
  32. package/dist/components/charts/charts.test.d.ts +1 -0
  33. package/dist/components/charts/charts.test.js +11 -0
  34. package/dist/components/charts/index.d.ts +6 -0
  35. package/dist/components/charts/index.js +3 -0
  36. package/dist/components/elements/CodeBlock.svelte +86 -58
  37. package/dist/components/elements/CodeBlock.svelte.d.ts +2 -1
  38. package/dist/components/elements/CodeDiff.svelte +454 -0
  39. package/dist/components/elements/CodeDiff.svelte.d.ts +38 -0
  40. package/dist/components/elements/CodeDiff.test.d.ts +1 -0
  41. package/dist/components/elements/CodeDiff.test.js +52 -0
  42. package/dist/components/elements/DataTable.dom.test.js +30 -2
  43. package/dist/components/elements/DataTable.svelte +418 -151
  44. package/dist/components/elements/DataTable.svelte.d.ts +32 -2
  45. package/dist/components/forms/Checkbox.svelte +30 -5
  46. package/dist/components/forms/Checkbox.svelte.d.ts +1 -0
  47. package/dist/components/forms/Combobox.svelte +31 -5
  48. package/dist/components/forms/Combobox.svelte.d.ts +6 -0
  49. package/dist/components/forms/CreditCardInput.svelte +191 -0
  50. package/dist/components/forms/CreditCardInput.svelte.d.ts +17 -0
  51. package/dist/components/forms/Form.dom.test.d.ts +1 -0
  52. package/dist/components/forms/Form.dom.test.js +59 -0
  53. package/dist/components/forms/Form.svelte +93 -3
  54. package/dist/components/forms/Form.svelte.d.ts +2 -0
  55. package/dist/components/forms/FormField.svelte +75 -11
  56. package/dist/components/forms/FormField.svelte.d.ts +2 -1
  57. package/dist/components/forms/Input.svelte +18 -1
  58. package/dist/components/forms/InputOTP.svelte +12 -3
  59. package/dist/components/forms/MultiSelect.svelte +33 -7
  60. package/dist/components/forms/MultiSelect.svelte.d.ts +6 -0
  61. package/dist/components/forms/NumberInput.svelte +23 -5
  62. package/dist/components/forms/NumberInput.svelte.d.ts +3 -0
  63. package/dist/components/forms/PhoneInput.svelte +174 -0
  64. package/dist/components/forms/PhoneInput.svelte.d.ts +23 -0
  65. package/dist/components/forms/RichTextEditor.svelte +265 -0
  66. package/dist/components/forms/RichTextEditor.svelte.d.ts +11 -0
  67. package/dist/components/forms/Select.svelte +36 -18
  68. package/dist/components/forms/Select.svelte.d.ts +3 -0
  69. package/dist/components/forms/Switch.svelte +22 -3
  70. package/dist/components/forms/Switch.svelte.d.ts +1 -0
  71. package/dist/components/forms/Textarea.svelte +23 -2
  72. package/dist/components/forms/advanced-forms.test.d.ts +1 -0
  73. package/dist/components/forms/advanced-forms.test.js +44 -0
  74. package/dist/components/forms/form-context.d.ts +38 -0
  75. package/dist/components/forms/form-context.js +15 -0
  76. package/dist/components/layout/DashboardShell.svelte +6 -4
  77. package/dist/components/layout/DashboardShell.svelte.d.ts +1 -0
  78. package/dist/components/layout/Footer.svelte +18 -30
  79. package/dist/components/overlays/Popover.svelte +16 -3
  80. package/dist/components/overlays/Popover.svelte.d.ts +2 -0
  81. package/dist/components/overlays/Tooltip.svelte +19 -3
  82. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -0
  83. package/dist/components/saas/AuthCard.svelte +299 -30
  84. package/dist/components/saas/AuthCard.svelte.d.ts +5 -1
  85. package/dist/components/saas/PasskeyUI.svelte +235 -0
  86. package/dist/components/saas/PasskeyUI.svelte.d.ts +13 -0
  87. package/dist/components/saas/TwoFactorModal.svelte +253 -0
  88. package/dist/components/saas/TwoFactorModal.svelte.d.ts +9 -0
  89. package/dist/components/theme/ThemeStudio.svelte +411 -0
  90. package/dist/components/theme/ThemeStudio.svelte.d.ts +6 -0
  91. package/dist/composables/useAuth.svelte.d.ts +732 -1
  92. package/dist/composables/useAuth.svelte.js +240 -2
  93. package/dist/composables/useDataTable.svelte.d.ts +34 -0
  94. package/dist/composables/useDataTable.svelte.js +130 -0
  95. package/dist/index.d.ts +41 -4
  96. package/dist/index.js +26 -4
  97. package/dist/motion/transitions.d.ts +22 -0
  98. package/dist/motion/transitions.js +55 -0
  99. package/dist/server/auth/index.d.ts +25 -1
  100. package/dist/server/auth/index.js +10 -3
  101. package/dist/server/db/pagination.d.ts +36 -0
  102. package/dist/server/db/pagination.js +40 -0
  103. package/dist/server/db/pagination.test.d.ts +1 -0
  104. package/dist/server/db/pagination.test.js +41 -0
  105. package/dist/server/db/schema-pg.d.ts +269 -0
  106. package/dist/server/db/schema-pg.js +28 -2
  107. package/dist/server/db/schema-sqlite.d.ts +291 -0
  108. package/dist/server/db/schema-sqlite.js +30 -4
  109. package/dist/server/index.d.ts +2 -0
  110. package/dist/server/index.js +2 -1
  111. package/dist/site/manifest.js +6 -5
  112. package/dist/styles/yaxa.css +81 -4
  113. package/dist/theme/theme.svelte.d.ts +6 -1
  114. package/dist/theme/theme.svelte.js +49 -0
  115. package/package.json +4 -2
package/README.md CHANGED
@@ -23,13 +23,16 @@ _Nuxt UI v4 & Nuxt UI Pro Equivalent for SvelteKit 2.7+ & Svelte 5 with Built-in
23
23
 
24
24
  **Yaxa** brings the full developer experience, visual elegance, and full-stack toolkit of **Nuxt UI** and the **Nuxt SEO + SaaS ecosystem** to **SvelteKit 2.7+** and **Svelte 5**.
25
25
 
26
- - 🎨 **Tailwind CSS v4 Native Tokens**: Styled with `@theme` variables; customize any color or token directly.
26
+ - 🎨 **Tailwind CSS v4 Native Tokens**: Styled with `@theme` variables; harmonic radius scale (`--radius-xs` to `--radius-4xl`), chart palette tokens (`--color-chart-1` to `--color-chart-5`), and auto-adaptive scrollbars.
27
+ - 🎛️ **Visual Web Theme Studio**: Interactive runtime palette designer (`/theme` and `<ThemeStudio />`) with live sandbox preview and 1-click token export.
28
+ - 🤖 **AI-Native Primitives**: Drop-in `<AiChat>`, `<PromptBar>`, `<AiThought>`, and `<AiToolCall>` for conversational interfaces and agentic workflows.
29
+ - 📊 **Zero-Dependency SVG Charts**: Pure Svelte 5 `<LineChart>`, `<BarChart>`, and `<DonutChart>` with responsive tooltips, curved splines, and theme-palette synchronisation.
27
30
  - ⚡ **Pure Svelte 5 Runes**: Built using `$state`, `$derived`, `$props`, and `$bindable` — zero Virtual DOM overhead.
28
31
  - ♿ **Accessible by Default**: Headless primitives powered by **Bits UI**.
29
32
  - 🚀 **100% DRY SEO Parity**: Automate dynamic Open Graph cards (`/api/og`), environment-aware `robots.txt`, XML sitemaps with human-readable `sitemap.xsl` stylesheets, PWA manifests, and Schema.org JSON-LD from a single `src/site.config.ts`.
30
33
  - 🔐 **Batteries-Included SaaS Suite**: Pre-integrated **Better-Auth**, multi-dialect **Drizzle ORM** (Neon PostgreSQL & Turso LibSQL/SQLite), **Polar.sh** payments & webhook sync, and **Resend** transactional emails.
31
- - 🧩 **35+ Production Components**: Elements, forms, layout, overlays, and drop-in SaaS widgets (`<AuthCard>`, `<UserMenu>`, `<PricingTable>`, `<SubscriptionCard>`).
32
- - 🛠️ **Composable Runes**: `useAuth`, `useShortcuts`, `useClipboard`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`.
34
+ - 🧩 **45+ Production Components**: Elements, AI primitives, charts, advanced forms (`<RichTextEditor>`, `<PhoneInput>`, `<CreditCardInput>`), layout, overlays, and drop-in SaaS widgets (`<AuthCard>`, `<UserMenu>`, `<PricingTable>`, `<SubscriptionCard>`).
35
+ - 🛠️ **Composable Runes**: `useAuth`, `useShortcuts`, `useClipboard`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce`, `useIdle`.
33
36
 
34
37
  ---
35
38
 
@@ -228,15 +231,19 @@ export const POST = createPolarWebhookHandler();
228
231
 
229
232
  ## 🧩 Component Library
230
233
 
231
- | Category | Components |
232
- | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
233
- | **SaaS Suite** | `<Gate>`, `<AuthCard>`, `<UserMenu>`, `<PricingCard>`, `<PricingTable>`, `<SubscriptionCard>`, `useAuth`, `useGate`, `useUpload` |
234
- | **Admin Suite** | `<AdminDashboard>`, `<RecordDrawer>`, `<DevSandbox>`, `<ImpersonationBanner>`, `createYaxaAdminHook`, `createDrizzleAdmin` |
235
- | **Elements** | `Button`, `ButtonGroup`, `Badge`, `Avatar`, `AvatarGroup`, `DataTable`, `Chip`, `Meter`, `MetricCard`, `Kbd`, `Icon`, `Spinner`, `Progress`, `Skeleton`, `Link`, `Logo` |
236
- | **Forms** | `Form`, `FormField`, `Input`, `InputOTP`, `Textarea`, `Checkbox`, `Switch`, `Select`, `RadioGroup`, `Slider`, `ColorPicker`, `ToggleGroup`, `DatePicker`, `DateRangePicker` |
237
- | **Layout & Blocks** | `Container`, `Header`, `Footer`, `Section`, `Card`, `Divider`, `YaxaApp`, Page Blocks (Hero, SaaS Metrics, 2FA Security) |
238
- | **Overlays & Nav** | `Tabs`, `Breadcrumb`, `Pagination`, `CommandPalette` (`⌘K`), `DropdownMenu`, `ContextMenu`, `Modal`, `Slideover`, `Popover`, `Tooltip`, `Alert`, `Accordion` |
239
- | **Composables** | `useAuth`, `useGate`, `useUpload`, `useClipboard`, `useShortcuts`, `useColorMode`, `useToast`, `useMediaQuery`, `useDebounce` |
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` |
240
247
 
241
248
  ---
242
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
+ });
@@ -16,17 +16,17 @@ export function introspectTables(schema) {
16
16
  const columns = [];
17
17
  let primaryKey = 'id';
18
18
  for (const [colKey, col] of Object.entries(rawColumns)) {
19
- const colAny = col;
20
- const isPk = Boolean(colAny.primary || colAny.isPrimaryKey);
19
+ const colTyped = col;
20
+ const isPk = Boolean(colTyped.primary || colTyped.isPrimaryKey);
21
21
  if (isPk) {
22
- primaryKey = colAny.name || colKey;
22
+ primaryKey = colTyped.name || colKey;
23
23
  }
24
24
  columns.push({
25
- name: colAny.name || colKey,
26
- dataType: colAny.dataType || colAny.columnType || typeof colAny,
25
+ name: colTyped.name || colKey,
26
+ dataType: colTyped.dataType || colTyped.columnType || typeof colTyped,
27
27
  primaryKey: isPk,
28
- notNull: Boolean(colAny.notNull),
29
- hasDefault: Boolean(colAny.hasDefault || colAny.default !== undefined)
28
+ notNull: Boolean(colTyped.notNull),
29
+ hasDefault: Boolean(colTyped.hasDefault || colTyped.default !== undefined)
30
30
  });
31
31
  }
32
32
  const info = {
@@ -98,13 +98,13 @@ export class DrizzleAdminService {
98
98
  const searchTerm = `%${options.search.trim()}%`;
99
99
  const textColFilters = [];
100
100
  for (const [, col] of Object.entries(rawColumns)) {
101
- const colAny = col;
102
- if (colAny.dataType === 'string' ||
103
- colAny.dataType === 'text' ||
104
- colAny.columnType?.includes('Text') ||
105
- colAny.columnType?.includes('Varchar')) {
101
+ const colTyped = col;
102
+ if (colTyped.dataType === 'string' ||
103
+ colTyped.dataType === 'text' ||
104
+ colTyped.columnType?.includes('Text') ||
105
+ colTyped.columnType?.includes('Varchar')) {
106
106
  try {
107
- textColFilters.push(like(colAny, searchTerm));
107
+ textColFilters.push(like(col, searchTerm));
108
108
  }
109
109
  catch {
110
110
  // fallback
@@ -0,0 +1,263 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export interface AiMessageThought {
5
+ duration?: number;
6
+ running?: boolean;
7
+ text?: string;
8
+ steps?: string[];
9
+ }
10
+
11
+ export interface AiMessageTool {
12
+ name: string;
13
+ status?: 'idle' | 'running' | 'success' | 'error';
14
+ args?: Record<string, any> | string;
15
+ result?: Record<string, any> | string;
16
+ }
17
+
18
+ export interface AiMessage {
19
+ id: string;
20
+ role: 'user' | 'assistant' | 'system';
21
+ content: string;
22
+ createdAt?: Date | string;
23
+ thought?: AiMessageThought;
24
+ tools?: AiMessageTool[];
25
+ isStreaming?: boolean;
26
+ }
27
+
28
+ export interface AiChatProps {
29
+ messages?: AiMessage[];
30
+ loading?: boolean;
31
+ userAvatar?: string;
32
+ assistantAvatar?: string;
33
+ assistantName?: string;
34
+ placeholder?: string;
35
+ autoScroll?: boolean;
36
+ class?: string;
37
+ onsend?: (text: string, files?: File[]) => void;
38
+ onstop?: () => void;
39
+ onretry?: (message: AiMessage) => void;
40
+ header?: Snippet;
41
+ messageSlot?: Snippet<[AiMessage]>;
42
+ footer?: Snippet;
43
+ }
44
+ </script>
45
+
46
+ <script lang="ts">
47
+ import Avatar from '../elements/Avatar.svelte';
48
+ import Icon from '../elements/Icon.svelte';
49
+ import PromptBar from './PromptBar.svelte';
50
+ import AiThought from './AiThought.svelte';
51
+ import AiToolCall from './AiToolCall.svelte';
52
+ import { useToast } from '../../composables/useToast';
53
+
54
+ let {
55
+ messages = [],
56
+ loading = false,
57
+ userAvatar = '',
58
+ assistantAvatar = '',
59
+ assistantName = 'AI Assistant',
60
+ placeholder = 'Ask a question or type / for commands...',
61
+ autoScroll = true,
62
+ class: className = '',
63
+ onsend,
64
+ onstop,
65
+ onretry,
66
+ header,
67
+ messageSlot,
68
+ footer
69
+ }: AiChatProps = $props();
70
+
71
+ let scrollContainer: HTMLDivElement | null = $state(null);
72
+ let promptValue = $state('');
73
+ const toast = useToast();
74
+
75
+ function scrollToBottom(smooth = true) {
76
+ if (scrollContainer && autoScroll) {
77
+ scrollContainer.scrollTo({
78
+ top: scrollContainer.scrollHeight,
79
+ behavior: smooth ? 'smooth' : 'auto'
80
+ });
81
+ }
82
+ }
83
+
84
+ $effect(() => {
85
+ if (messages.length > 0) {
86
+ setTimeout(() => scrollToBottom(true), 50);
87
+ }
88
+ });
89
+
90
+ function copyMessage(text: string) {
91
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
92
+ navigator.clipboard.writeText(text);
93
+ toast.success('Copied to clipboard');
94
+ }
95
+ }
96
+ </script>
97
+
98
+ <div
99
+ class="flex h-full min-h-[500px] w-full flex-col overflow-hidden rounded-2xl border border-neutral-200 bg-white shadow-md dark:border-neutral-800 dark:bg-neutral-950 {className}"
100
+ >
101
+ <!-- Chat Header Bar -->
102
+ <div
103
+ class="flex items-center justify-between border-b border-neutral-200/80 bg-neutral-50/50 px-4 py-3 dark:border-neutral-800/80 dark:bg-neutral-900/50"
104
+ >
105
+ {#if header}
106
+ {@render header()}
107
+ {:else}
108
+ <div class="flex items-center gap-3">
109
+ <div
110
+ class="flex h-8 w-8 items-center justify-center rounded-xl bg-primary-100 text-primary-600 dark:bg-primary-950 dark:text-primary-400"
111
+ >
112
+ <Icon name="bot" size="sm" />
113
+ </div>
114
+ <div>
115
+ <div class="text-sm font-semibold text-neutral-900 dark:text-white">
116
+ {assistantName}
117
+ </div>
118
+ <div class="flex items-center gap-1.5 text-[11px] text-neutral-500">
119
+ <span
120
+ class="h-1.5 w-1.5 rounded-full {loading
121
+ ? 'animate-ping bg-amber-500'
122
+ : 'bg-emerald-500'}"
123
+ ></span>
124
+ <span>{loading ? 'Generating response...' : 'Online & Ready'}</span>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ {/if}
129
+ </div>
130
+
131
+ <!-- Scrollable Messages Container -->
132
+ <div
133
+ bind:this={scrollContainer}
134
+ class="yaxa-scrollbar flex-1 space-y-6 overflow-y-auto p-4 sm:p-6"
135
+ >
136
+ {#if messages.length === 0}
137
+ <div class="flex h-full min-h-[300px] flex-col items-center justify-center text-center">
138
+ <div
139
+ class="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-primary-50 text-primary-600 dark:bg-primary-950/60 dark:text-primary-400"
140
+ >
141
+ <Icon name="sparkles" size="lg" />
142
+ </div>
143
+ <h3 class="text-base font-semibold text-neutral-900 dark:text-white">
144
+ How can I help you today?
145
+ </h3>
146
+ <p class="mt-1 max-w-sm text-xs text-neutral-500">
147
+ Ask questions, analyze documents, generate code, or explore ideas with interactive AI.
148
+ </p>
149
+ </div>
150
+ {:else}
151
+ {#each messages as msg (msg.id)}
152
+ {#if messageSlot}
153
+ {@render messageSlot(msg)}
154
+ {:else}
155
+ <div class="flex gap-3.5 {msg.role === 'user' ? 'justify-end' : 'justify-start'}">
156
+ {#if msg.role === 'assistant'}
157
+ <Avatar
158
+ src={assistantAvatar}
159
+ alt={assistantName}
160
+ size="sm"
161
+ class="mt-1 flex-shrink-0"
162
+ />
163
+ {/if}
164
+
165
+ <div
166
+ class="flex max-w-[85%] flex-col gap-1 sm:max-w-[75%] {msg.role === 'user'
167
+ ? 'items-end'
168
+ : 'items-start'}"
169
+ >
170
+ <!-- Agent Reasoning / Thoughts -->
171
+ {#if msg.thought}
172
+ <AiThought
173
+ duration={msg.thought.duration}
174
+ running={msg.thought.running}
175
+ steps={msg.thought.steps}
176
+ class="w-full"
177
+ >
178
+ {#if msg.thought.text}
179
+ <p>{msg.thought.text}</p>
180
+ {/if}
181
+ </AiThought>
182
+ {/if}
183
+
184
+ <!-- Agent Tool Invocations -->
185
+ {#if msg.tools && msg.tools.length > 0}
186
+ <div class="w-full space-y-2">
187
+ {#each msg.tools as tool}
188
+ <AiToolCall
189
+ name={tool.name}
190
+ status={tool.status}
191
+ args={tool.args}
192
+ result={tool.result}
193
+ />
194
+ {/each}
195
+ </div>
196
+ {/if}
197
+
198
+ <!-- Message Bubble -->
199
+ <div
200
+ class="relative rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-xs {msg.role ===
201
+ 'user'
202
+ ? 'bg-primary-500 text-white dark:bg-primary-600'
203
+ : 'border border-neutral-200/80 bg-neutral-50 text-neutral-900 dark:border-neutral-800/80 dark:bg-neutral-900 dark:text-neutral-100'}"
204
+ >
205
+ <div class="whitespace-pre-wrap">{msg.content}</div>
206
+
207
+ {#if msg.isStreaming}
208
+ <span class="inline-block h-4 w-1.5 animate-pulse bg-current"></span>
209
+ {/if}
210
+ </div>
211
+
212
+ <!-- Action Buttons -->
213
+ <div class="mt-1 flex items-center gap-1.5 px-1 text-[11px] text-neutral-400">
214
+ <button
215
+ type="button"
216
+ onclick={() => copyMessage(msg.content)}
217
+ class="hover:text-neutral-700 dark:hover:text-neutral-200"
218
+ title="Copy message"
219
+ >
220
+ <Icon name="copy" size="xs" />
221
+ </button>
222
+
223
+ {#if msg.role === 'assistant' && onretry}
224
+ <button
225
+ type="button"
226
+ onclick={() => onretry(msg)}
227
+ class="hover:text-neutral-700 dark:hover:text-neutral-200"
228
+ title="Regenerate response"
229
+ >
230
+ <Icon name="rotate-cw" size="xs" />
231
+ </button>
232
+ {/if}
233
+ </div>
234
+ </div>
235
+
236
+ {#if msg.role === 'user'}
237
+ <Avatar src={userAvatar} alt="User" size="sm" class="mt-1 flex-shrink-0" />
238
+ {/if}
239
+ </div>
240
+ {/if}
241
+ {/each}
242
+ {/if}
243
+ </div>
244
+
245
+ <!-- Bottom Prompt Input Bar -->
246
+ <div
247
+ class="border-t border-neutral-200/80 bg-neutral-50/50 p-3 sm:p-4 dark:border-neutral-800/80 dark:bg-neutral-900/50"
248
+ >
249
+ {#if footer}
250
+ {@render footer()}
251
+ {:else}
252
+ <PromptBar
253
+ bind:value={promptValue}
254
+ {placeholder}
255
+ {loading}
256
+ onsubmit={(text, files) => {
257
+ if (onsend) onsend(text, files);
258
+ }}
259
+ {onstop}
260
+ />
261
+ {/if}
262
+ </div>
263
+ </div>
@@ -0,0 +1,41 @@
1
+ import type { Snippet } from 'svelte';
2
+ export interface AiMessageThought {
3
+ duration?: number;
4
+ running?: boolean;
5
+ text?: string;
6
+ steps?: string[];
7
+ }
8
+ export interface AiMessageTool {
9
+ name: string;
10
+ status?: 'idle' | 'running' | 'success' | 'error';
11
+ args?: Record<string, any> | string;
12
+ result?: Record<string, any> | string;
13
+ }
14
+ export interface AiMessage {
15
+ id: string;
16
+ role: 'user' | 'assistant' | 'system';
17
+ content: string;
18
+ createdAt?: Date | string;
19
+ thought?: AiMessageThought;
20
+ tools?: AiMessageTool[];
21
+ isStreaming?: boolean;
22
+ }
23
+ export interface AiChatProps {
24
+ messages?: AiMessage[];
25
+ loading?: boolean;
26
+ userAvatar?: string;
27
+ assistantAvatar?: string;
28
+ assistantName?: string;
29
+ placeholder?: string;
30
+ autoScroll?: boolean;
31
+ class?: string;
32
+ onsend?: (text: string, files?: File[]) => void;
33
+ onstop?: () => void;
34
+ onretry?: (message: AiMessage) => void;
35
+ header?: Snippet;
36
+ messageSlot?: Snippet<[AiMessage]>;
37
+ footer?: Snippet;
38
+ }
39
+ declare const AiChat: import("svelte").Component<AiChatProps, {}, "">;
40
+ type AiChat = ReturnType<typeof AiChat>;
41
+ export default AiChat;
@@ -0,0 +1,108 @@
1
+ <script module lang="ts">
2
+ export interface AiThoughtProps {
3
+ duration?: number;
4
+ running?: boolean;
5
+ title?: string;
6
+ steps?: string[];
7
+ open?: boolean;
8
+ class?: string;
9
+ children?: import('svelte').Snippet;
10
+ }
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ import Icon from '../elements/Icon.svelte';
15
+
16
+ let {
17
+ duration,
18
+ running = false,
19
+ title = 'Thinking Process',
20
+ steps = [],
21
+ open = $bindable(false),
22
+ class: className = '',
23
+ children
24
+ }: AiThoughtProps = $props();
25
+
26
+ function toggle() {
27
+ open = !open;
28
+ }
29
+ </script>
30
+
31
+ <div
32
+ class="my-2 overflow-hidden rounded-xl border border-neutral-200/80 bg-neutral-50/60 transition-all dark:border-neutral-800/80 dark:bg-neutral-900/40 {className}"
33
+ >
34
+ <!-- Header Bar -->
35
+ <button
36
+ type="button"
37
+ onclick={toggle}
38
+ class="flex w-full items-center justify-between px-3.5 py-2.5 text-left text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-100/60 dark:text-neutral-300 dark:hover:bg-neutral-800/40"
39
+ aria-expanded={open}
40
+ >
41
+ <div class="flex items-center gap-2">
42
+ {#if running}
43
+ <span class="relative flex h-2 w-2">
44
+ <span
45
+ class="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-400 opacity-75"
46
+ ></span>
47
+ <span class="relative inline-flex h-2 w-2 rounded-full bg-primary-500"></span>
48
+ </span>
49
+ {:else}
50
+ <Icon name="sparkles" size="xs" class="text-primary-500" />
51
+ {/if}
52
+
53
+ <span class="font-semibold text-neutral-800 dark:text-neutral-200">
54
+ {title}
55
+ </span>
56
+
57
+ {#if running}
58
+ <span class="animate-pulse text-[11px] text-primary-600 dark:text-primary-400">
59
+ Thinking...
60
+ </span>
61
+ {:else if duration !== undefined}
62
+ <span
63
+ class="py-0.2 rounded bg-neutral-200/60 px-1.5 text-[10px] text-neutral-500 dark:bg-neutral-800 dark:text-neutral-400"
64
+ >
65
+ {duration.toFixed(1)}s
66
+ </span>
67
+ {/if}
68
+ </div>
69
+
70
+ <div class="flex items-center gap-2 text-neutral-400">
71
+ {#if steps.length > 0}
72
+ <span class="text-[11px] text-neutral-500">
73
+ {steps.length}
74
+ {steps.length === 1 ? 'step' : 'steps'}
75
+ </span>
76
+ {/if}
77
+ <Icon
78
+ name={open ? 'chevron-up' : 'chevron-down'}
79
+ size="xs"
80
+ class="transition-transform duration-200"
81
+ />
82
+ </div>
83
+ </button>
84
+
85
+ <!-- Collapsible Content -->
86
+ {#if open}
87
+ <div
88
+ class="border-t border-neutral-200/60 px-3.5 py-3 text-xs leading-relaxed text-neutral-600 dark:border-neutral-800/60 dark:text-neutral-300"
89
+ >
90
+ {#if steps.length > 0}
91
+ <ul
92
+ class="mb-2.5 space-y-1.5 border-b border-neutral-200/40 pb-2.5 dark:border-neutral-800/40"
93
+ >
94
+ {#each steps as step, idx (idx)}
95
+ <li class="flex items-center gap-2 text-[11px] text-neutral-500 dark:text-neutral-400">
96
+ <Icon name="check" size="xs" class="text-emerald-500" />
97
+ <span>{step}</span>
98
+ </li>
99
+ {/each}
100
+ </ul>
101
+ {/if}
102
+
103
+ {#if children}
104
+ {@render children()}
105
+ {/if}
106
+ </div>
107
+ {/if}
108
+ </div>
@@ -0,0 +1,12 @@
1
+ export interface AiThoughtProps {
2
+ duration?: number;
3
+ running?: boolean;
4
+ title?: string;
5
+ steps?: string[];
6
+ open?: boolean;
7
+ class?: string;
8
+ children?: import('svelte').Snippet;
9
+ }
10
+ declare const AiThought: import("svelte").Component<AiThoughtProps, {}, "open">;
11
+ type AiThought = ReturnType<typeof AiThought>;
12
+ export default AiThought;