yaxa-svelte 1.9.0 → 1.11.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 (99) hide show
  1. package/README.md +18 -12
  2. package/dist/admin/components/AdminDashboard.svelte +6 -0
  3. package/dist/admin/components/DevSandbox.svelte +6 -2
  4. package/dist/admin/components/RecordDrawer.svelte +4 -0
  5. package/dist/admin/handler.js +13 -13
  6. package/dist/components/ai/AiChat.svelte +263 -0
  7. package/dist/components/ai/AiChat.svelte.d.ts +41 -0
  8. package/dist/components/ai/AiThought.svelte +108 -0
  9. package/dist/components/ai/AiThought.svelte.d.ts +12 -0
  10. package/dist/components/ai/AiToolCall.svelte +133 -0
  11. package/dist/components/ai/AiToolCall.svelte.d.ts +13 -0
  12. package/dist/components/ai/PromptBar.svelte +279 -0
  13. package/dist/components/ai/PromptBar.svelte.d.ts +22 -0
  14. package/dist/components/ai/ai.test.d.ts +1 -0
  15. package/dist/components/ai/ai.test.js +13 -0
  16. package/dist/components/ai/index.d.ts +8 -0
  17. package/dist/components/ai/index.js +4 -0
  18. package/dist/components/charts/BarChart.svelte +248 -0
  19. package/dist/components/charts/BarChart.svelte.d.ts +22 -0
  20. package/dist/components/charts/DonutChart.svelte +153 -0
  21. package/dist/components/charts/DonutChart.svelte.d.ts +18 -0
  22. package/dist/components/charts/LineChart.svelte +334 -0
  23. package/dist/components/charts/LineChart.svelte.d.ts +28 -0
  24. package/dist/components/charts/charts.test.d.ts +1 -0
  25. package/dist/components/charts/charts.test.js +11 -0
  26. package/dist/components/charts/index.d.ts +6 -0
  27. package/dist/components/charts/index.js +3 -0
  28. package/dist/components/elements/CodeBlock.svelte +86 -58
  29. package/dist/components/elements/CodeBlock.svelte.d.ts +2 -1
  30. package/dist/components/elements/DataTable.svelte +363 -144
  31. package/dist/components/elements/DataTable.svelte.d.ts +15 -2
  32. package/dist/components/elements/Icon.svelte +32 -1
  33. package/dist/components/forms/Checkbox.svelte +3 -0
  34. package/dist/components/forms/Checkbox.svelte.d.ts +1 -0
  35. package/dist/components/forms/ColorPicker.svelte +22 -2
  36. package/dist/components/forms/ColorPicker.svelte.d.ts +5 -0
  37. package/dist/components/forms/Combobox.svelte +3 -0
  38. package/dist/components/forms/CreditCardInput.svelte +167 -0
  39. package/dist/components/forms/CreditCardInput.svelte.d.ts +14 -0
  40. package/dist/components/forms/Dropzone.svelte +11 -0
  41. package/dist/components/forms/Dropzone.svelte.d.ts +4 -0
  42. package/dist/components/forms/FormField.svelte +6 -1
  43. package/dist/components/forms/FormField.svelte.d.ts +1 -0
  44. package/dist/components/forms/InputOTP.svelte +6 -0
  45. package/dist/components/forms/InputOTP.svelte.d.ts +2 -0
  46. package/dist/components/forms/MultiSelect.svelte +3 -0
  47. package/dist/components/forms/NumberInput.svelte +11 -1
  48. package/dist/components/forms/NumberInput.svelte.d.ts +4 -0
  49. package/dist/components/forms/PhoneInput.svelte +140 -0
  50. package/dist/components/forms/PhoneInput.svelte.d.ts +18 -0
  51. package/dist/components/forms/RadioGroup.svelte +2 -0
  52. package/dist/components/forms/RichTextEditor.svelte +265 -0
  53. package/dist/components/forms/RichTextEditor.svelte.d.ts +11 -0
  54. package/dist/components/forms/Select.svelte +14 -1
  55. package/dist/components/forms/Select.svelte.d.ts +5 -0
  56. package/dist/components/forms/Slider.svelte +14 -1
  57. package/dist/components/forms/Slider.svelte.d.ts +4 -0
  58. package/dist/components/forms/Switch.svelte +14 -1
  59. package/dist/components/forms/Switch.svelte.d.ts +3 -0
  60. package/dist/components/forms/advanced-forms.test.d.ts +1 -0
  61. package/dist/components/forms/advanced-forms.test.js +23 -0
  62. package/dist/components/layout/DashboardShell.svelte +6 -4
  63. package/dist/components/layout/DashboardShell.svelte.d.ts +1 -0
  64. package/dist/components/layout/Footer.svelte +10 -15
  65. package/dist/components/layout/Header.svelte +5 -4
  66. package/dist/components/layout/YaxaApp.svelte +9 -7
  67. package/dist/components/legal/LegalDocument.svelte +3 -2
  68. package/dist/components/navigation/CommandPalette.svelte +3 -0
  69. package/dist/components/navigation/DropdownMenu.svelte +14 -11
  70. package/dist/components/overlays/Popover.svelte +21 -4
  71. package/dist/components/overlays/Popover.svelte.d.ts +2 -0
  72. package/dist/components/overlays/Tooltip.svelte +28 -8
  73. package/dist/components/overlays/Tooltip.svelte.d.ts +3 -0
  74. package/dist/components/saas/Gate.svelte +3 -2
  75. package/dist/components/saas/UserMenu.svelte +3 -3
  76. package/dist/components/seo/Favicons.svelte +37 -12
  77. package/dist/components/seo/Favicons.svelte.d.ts +4 -0
  78. package/dist/components/seo/OgImage.svelte +3 -2
  79. package/dist/components/seo/Seo.svelte +6 -4
  80. package/dist/components/theme/ThemeStudio.svelte +411 -0
  81. package/dist/components/theme/ThemeStudio.svelte.d.ts +6 -0
  82. package/dist/composables/useGate.svelte.js +3 -2
  83. package/dist/composables/useLocale.svelte.js +8 -6
  84. package/dist/index.d.ts +24 -2
  85. package/dist/index.js +16 -3
  86. package/dist/site/config.d.ts +2 -0
  87. package/dist/site/context.d.ts +1 -30
  88. package/dist/site/context.js +1 -43
  89. package/dist/site/context.svelte.d.ts +37 -0
  90. package/dist/site/context.svelte.js +52 -0
  91. package/dist/site/context.test.d.ts +1 -0
  92. package/dist/site/context.test.js +29 -0
  93. package/dist/site/manifest.js +6 -5
  94. package/dist/styles/yaxa.css +102 -4
  95. package/dist/theme/theme.svelte.d.ts +6 -1
  96. package/dist/theme/theme.svelte.js +49 -0
  97. package/dist/vite/index.js +12 -4
  98. package/dist/vite/index.test.js +6 -6
  99. package/package.json +8 -12
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,18 @@ 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 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` |
240
246
 
241
247
  ---
242
248
 
@@ -284,8 +284,11 @@
284
284
  <div class="flex items-center gap-2">
285
285
  <form onsubmit={handleSearch} class="relative">
286
286
  <input
287
+ id="admin-search-input"
288
+ name="adminSearch"
287
289
  type="search"
288
290
  placeholder="Search records..."
291
+ aria-label="Search records"
289
292
  bind:value={searchQuery}
290
293
  class="w-48 rounded-lg border border-neutral-300 bg-white py-1.5 pr-3 pl-8 text-xs text-neutral-900 placeholder-neutral-400 transition-all focus:w-60 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
291
294
  />
@@ -424,6 +427,7 @@
424
427
  {#if col.dataType === 'boolean'}
425
428
  <input
426
429
  id={`create-${col.name}`}
430
+ name={col.name}
427
431
  type="checkbox"
428
432
  class="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
429
433
  checked={Boolean(newRecordData[col.name])}
@@ -432,6 +436,7 @@
432
436
  {:else if col.dataType === 'number' || col.dataType === 'integer'}
433
437
  <input
434
438
  id={`create-${col.name}`}
439
+ name={col.name}
435
440
  type="number"
436
441
  placeholder={col.primaryKey ? '(Auto-generated if empty)' : ''}
437
442
  class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs dark:border-neutral-700 dark:bg-neutral-900"
@@ -443,6 +448,7 @@
443
448
  {:else}
444
449
  <input
445
450
  id={`create-${col.name}`}
451
+ name={col.name}
446
452
  type="text"
447
453
  placeholder={col.primaryKey ? '(Auto-generated UUID if empty)' : ''}
448
454
  class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs dark:border-neutral-700 dark:bg-neutral-900"
@@ -5,7 +5,7 @@
5
5
  import Icon from '../../components/elements/Icon.svelte';
6
6
  import Progress from '../../components/elements/Progress.svelte';
7
7
  import { useUpload } from '../../composables/useUpload.svelte';
8
- import { getAuthUserContext } from '../../site/context';
8
+ import { useYaxa } from '../../site/context';
9
9
 
10
10
  interface Props {
11
11
  apiPrefix?: string;
@@ -15,7 +15,8 @@
15
15
  let { apiPrefix = '/admin/api', class: className = '' }: Props = $props();
16
16
 
17
17
  // Auth & Context info
18
- const user = $derived(getAuthUserContext());
18
+ const yaxa = useYaxa();
19
+ const user = $derived(yaxa.user);
19
20
 
20
21
  // Email Sandbox State
21
22
  let emailTo = $state('developer@example.com');
@@ -96,6 +97,7 @@
96
97
  >
97
98
  <input
98
99
  id="email-to"
100
+ name="emailTo"
99
101
  type="email"
100
102
  bind:value={emailTo}
101
103
  placeholder="you@example.com"
@@ -109,6 +111,7 @@
109
111
  >
110
112
  <select
111
113
  id="email-template"
114
+ name="emailTemplate"
112
115
  bind:value={emailTemplate}
113
116
  class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-xs text-neutral-900 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
114
117
  >
@@ -172,6 +175,7 @@
172
175
  >
173
176
  <input
174
177
  id="storage-file"
178
+ name="storageFile"
175
179
  type="file"
176
180
  onchange={(e) => {
177
181
  const files = e.currentTarget.files;
@@ -138,6 +138,7 @@
138
138
  <div class="flex items-center gap-2 pt-1">
139
139
  <input
140
140
  id={`field-${col.name}`}
141
+ name={col.name}
141
142
  type="checkbox"
142
143
  class="h-4 w-4 rounded border-neutral-300 text-primary-600 focus:ring-primary-500 dark:border-neutral-700 dark:bg-neutral-800"
143
144
  checked={Boolean(formData[col.name])}
@@ -150,6 +151,7 @@
150
151
  {:else if col.dataType === 'number' || col.dataType === 'integer'}
151
152
  <input
152
153
  id={`field-${col.name}`}
154
+ name={col.name}
153
155
  type="number"
154
156
  class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
155
157
  disabled={col.primaryKey}
@@ -162,6 +164,7 @@
162
164
  {:else if String(formData[col.name] ?? '').length > 60 || col.name.includes('description') || col.name.includes('body')}
163
165
  <textarea
164
166
  id={`field-${col.name}`}
167
+ name={col.name}
165
168
  rows={3}
166
169
  class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 font-mono text-sm text-xs text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
167
170
  disabled={col.primaryKey}
@@ -172,6 +175,7 @@
172
175
  {:else}
173
176
  <input
174
177
  id={`field-${col.name}`}
178
+ name={col.name}
175
179
  type="text"
176
180
  class="w-full rounded-md border border-neutral-300 bg-white px-3 py-1.5 text-sm text-neutral-900 placeholder-neutral-400 focus:border-primary-500 focus:ring-1 focus:ring-primary-500 focus:outline-none disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100"
177
181
  disabled={col.primaryKey}
@@ -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;