@aspect-ops/exon-ui 0.2.0 → 0.2.1

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 (38) hide show
  1. package/README.md +200 -0
  2. package/dist/components/Accordion/Accordion.svelte +2 -2
  3. package/dist/components/Accordion/AccordionItem.svelte +2 -2
  4. package/dist/components/Chatbot/ChatMessage.svelte +143 -0
  5. package/dist/components/Chatbot/ChatMessage.svelte.d.ts +8 -0
  6. package/dist/components/Chatbot/Chatbot.svelte +640 -0
  7. package/dist/components/Chatbot/Chatbot.svelte.d.ts +22 -0
  8. package/dist/components/Chatbot/index.d.ts +3 -0
  9. package/dist/components/Chatbot/index.js +2 -0
  10. package/dist/components/Chatbot/types.d.ts +48 -0
  11. package/dist/components/Chatbot/types.js +2 -0
  12. package/dist/components/ContactForm/ContactForm.svelte +564 -0
  13. package/dist/components/ContactForm/ContactForm.svelte.d.ts +44 -0
  14. package/dist/components/ContactForm/index.d.ts +1 -0
  15. package/dist/components/ContactForm/index.js +1 -0
  16. package/dist/components/DoughnutChart/DoughnutChart.svelte +372 -0
  17. package/dist/components/DoughnutChart/DoughnutChart.svelte.d.ts +25 -0
  18. package/dist/components/DoughnutChart/index.d.ts +1 -0
  19. package/dist/components/DoughnutChart/index.js +1 -0
  20. package/dist/components/FAB/FAB.svelte +5 -1
  21. package/dist/components/FAB/FABGroup.svelte +10 -2
  22. package/dist/components/FileUpload/FileUpload.svelte +12 -12
  23. package/dist/components/Mermaid/Mermaid.svelte +206 -0
  24. package/dist/components/Mermaid/Mermaid.svelte.d.ts +28 -0
  25. package/dist/components/Mermaid/index.d.ts +1 -0
  26. package/dist/components/Mermaid/index.js +1 -0
  27. package/dist/components/Mermaid/mermaid.d.ts +21 -0
  28. package/dist/components/Popover/PopoverTrigger.svelte +1 -3
  29. package/dist/components/ViewCounter/ViewCounter.svelte +157 -0
  30. package/dist/components/ViewCounter/ViewCounter.svelte.d.ts +17 -0
  31. package/dist/components/ViewCounter/index.d.ts +1 -0
  32. package/dist/components/ViewCounter/index.js +1 -0
  33. package/dist/index.d.ts +10 -1
  34. package/dist/index.js +9 -0
  35. package/dist/styles/tokens.css +2 -1
  36. package/dist/types/index.d.ts +1 -1
  37. package/dist/types/input.d.ts +20 -0
  38. package/package.json +2 -1
@@ -0,0 +1,564 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ // Field configuration
5
+ interface FieldConfig {
6
+ name: string;
7
+ label: string;
8
+ type: 'text' | 'email' | 'tel' | 'textarea' | 'select';
9
+ placeholder?: string;
10
+ required?: boolean;
11
+ options?: { value: string; label: string }[];
12
+ rows?: number;
13
+ validation?: (value: string) => string | null;
14
+ }
15
+
16
+ interface FormData {
17
+ [key: string]: string;
18
+ }
19
+
20
+ interface FeedbackState {
21
+ type: 'success' | 'error' | 'warning' | 'info' | null;
22
+ message: string;
23
+ }
24
+
25
+ interface Props {
26
+ /** Form fields configuration */
27
+ fields?: FieldConfig[];
28
+ /** Initial form values */
29
+ initialValues?: FormData;
30
+ /** Submit button text */
31
+ submitText?: string;
32
+ /** Whether to show success message after submit */
33
+ showSuccessMessage?: boolean;
34
+ /** Success message text */
35
+ successMessage?: string;
36
+ /** Privacy notice text */
37
+ privacyNotice?: string;
38
+ /** Whether to extract UTM params from URL */
39
+ extractUtmParams?: boolean;
40
+ /** Custom class */
41
+ class?: string;
42
+ /** Called on form submit. Should return { success: boolean, error?: string } */
43
+ onSubmit?: (data: FormData) => Promise<{ success: boolean; error?: string }>;
44
+ /** Called when validation state changes */
45
+ onValidate?: (isValid: boolean, errors: Record<string, string>) => void;
46
+ }
47
+
48
+ // Default fields
49
+ const defaultFields: FieldConfig[] = [
50
+ {
51
+ name: 'firstName',
52
+ label: 'First Name',
53
+ type: 'text',
54
+ placeholder: 'John',
55
+ required: true
56
+ },
57
+ {
58
+ name: 'lastName',
59
+ label: 'Last Name',
60
+ type: 'text',
61
+ placeholder: 'Doe',
62
+ required: true
63
+ },
64
+ {
65
+ name: 'email',
66
+ label: 'Email',
67
+ type: 'email',
68
+ placeholder: 'john@company.com',
69
+ required: true,
70
+ validation: (value) => {
71
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
72
+ return emailRegex.test(value) ? null : 'Please enter a valid email address';
73
+ }
74
+ },
75
+ {
76
+ name: 'phone',
77
+ label: 'Phone',
78
+ type: 'tel',
79
+ placeholder: '+1 (555) 123-4567'
80
+ },
81
+ {
82
+ name: 'company',
83
+ label: 'Company',
84
+ type: 'text',
85
+ placeholder: 'Acme Inc.'
86
+ },
87
+ {
88
+ name: 'message',
89
+ label: 'Message',
90
+ type: 'textarea',
91
+ placeholder: 'How can we help you?',
92
+ rows: 4,
93
+ required: true
94
+ }
95
+ ];
96
+
97
+ let {
98
+ fields = defaultFields,
99
+ initialValues = {},
100
+ submitText = 'Submit',
101
+ showSuccessMessage = true,
102
+ successMessage = "Thank you! We'll be in touch soon.",
103
+ privacyNotice = 'By submitting this form, you agree to our privacy policy.',
104
+ extractUtmParams = true,
105
+ class: className = '',
106
+ onSubmit,
107
+ onValidate
108
+ }: Props = $props();
109
+
110
+ // State - intentionally capture initial values
111
+ // eslint-disable-next-line svelte/valid-compile
112
+ let formData = $state<FormData>({ ...initialValues });
113
+ let errors = $state<Record<string, string>>({});
114
+ let touched = $state<Record<string, boolean>>({});
115
+ let isSubmitting = $state(false);
116
+ let feedback = $state<FeedbackState>({ type: null, message: '' });
117
+ let utmParams = $state<Record<string, string>>({});
118
+
119
+ // Initialize form data with field defaults
120
+ $effect(() => {
121
+ fields.forEach((field) => {
122
+ if (formData[field.name] === undefined) {
123
+ formData[field.name] = initialValues[field.name] || '';
124
+ }
125
+ });
126
+ });
127
+
128
+ // Extract UTM params on mount
129
+ onMount(() => {
130
+ if (extractUtmParams && typeof window !== 'undefined') {
131
+ const params = new URLSearchParams(window.location.search);
132
+ const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
133
+ utmKeys.forEach((key) => {
134
+ const value = params.get(key);
135
+ if (value) {
136
+ utmParams[key] = value;
137
+ }
138
+ });
139
+ }
140
+ });
141
+
142
+ // Validate single field
143
+ function validateField(field: FieldConfig, value: string): string | null {
144
+ if (field.required && !value.trim()) {
145
+ return `${field.label} is required`;
146
+ }
147
+ if (field.validation) {
148
+ return field.validation(value);
149
+ }
150
+ return null;
151
+ }
152
+
153
+ // Validate all fields
154
+ function validateAll(): boolean {
155
+ const newErrors: Record<string, string> = {};
156
+ let isValid = true;
157
+
158
+ fields.forEach((field) => {
159
+ const error = validateField(field, formData[field.name] || '');
160
+ if (error) {
161
+ newErrors[field.name] = error;
162
+ isValid = false;
163
+ }
164
+ });
165
+
166
+ errors = newErrors;
167
+ onValidate?.(isValid, newErrors);
168
+ return isValid;
169
+ }
170
+
171
+ // Handle field blur
172
+ function handleBlur(field: FieldConfig) {
173
+ touched[field.name] = true;
174
+ const error = validateField(field, formData[field.name] || '');
175
+ if (error) {
176
+ errors[field.name] = error;
177
+ } else {
178
+ delete errors[field.name];
179
+ errors = { ...errors };
180
+ }
181
+ }
182
+
183
+ // Handle input change
184
+ function handleInput(field: FieldConfig, value: string) {
185
+ formData[field.name] = value;
186
+
187
+ // Clear error on input if field was touched
188
+ if (touched[field.name]) {
189
+ const error = validateField(field, value);
190
+ if (error) {
191
+ errors[field.name] = error;
192
+ } else {
193
+ delete errors[field.name];
194
+ errors = { ...errors };
195
+ }
196
+ }
197
+ }
198
+
199
+ // Handle form submit
200
+ async function handleSubmit(event: SubmitEvent) {
201
+ event.preventDefault();
202
+
203
+ // Mark all fields as touched
204
+ fields.forEach((field) => {
205
+ touched[field.name] = true;
206
+ });
207
+
208
+ if (!validateAll()) {
209
+ feedback = {
210
+ type: 'error',
211
+ message: 'Please fix the errors above.'
212
+ };
213
+ return;
214
+ }
215
+
216
+ if (!onSubmit) {
217
+ feedback = {
218
+ type: 'warning',
219
+ message: 'No submit handler configured.'
220
+ };
221
+ return;
222
+ }
223
+
224
+ isSubmitting = true;
225
+ feedback = { type: null, message: '' };
226
+
227
+ try {
228
+ // Combine form data with UTM params
229
+ const submitData = {
230
+ ...formData,
231
+ ...utmParams
232
+ };
233
+
234
+ const result = await onSubmit(submitData);
235
+
236
+ if (result.success) {
237
+ if (showSuccessMessage) {
238
+ feedback = {
239
+ type: 'success',
240
+ message: successMessage
241
+ };
242
+ }
243
+ // Reset form
244
+ formData = { ...initialValues };
245
+ touched = {};
246
+ errors = {};
247
+ } else {
248
+ feedback = {
249
+ type: 'error',
250
+ message: result.error || 'Something went wrong. Please try again.'
251
+ };
252
+ }
253
+ } catch (error) {
254
+ feedback = {
255
+ type: 'error',
256
+ message: 'An unexpected error occurred. Please try again.'
257
+ };
258
+ } finally {
259
+ isSubmitting = false;
260
+ }
261
+ }
262
+
263
+ // Check if form has errors
264
+ const hasErrors = $derived(Object.keys(errors).length > 0);
265
+ </script>
266
+
267
+ <form class="contact-form {className}" onsubmit={handleSubmit} novalidate>
268
+ {#if feedback.type}
269
+ <div class="contact-form__feedback contact-form__feedback--{feedback.type}" role="alert">
270
+ {#if feedback.type === 'success'}
271
+ <svg
272
+ xmlns="http://www.w3.org/2000/svg"
273
+ width="20"
274
+ height="20"
275
+ viewBox="0 0 24 24"
276
+ fill="none"
277
+ stroke="currentColor"
278
+ stroke-width="2"
279
+ >
280
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
281
+ <polyline points="22 4 12 14.01 9 11.01" />
282
+ </svg>
283
+ {:else if feedback.type === 'error'}
284
+ <svg
285
+ xmlns="http://www.w3.org/2000/svg"
286
+ width="20"
287
+ height="20"
288
+ viewBox="0 0 24 24"
289
+ fill="none"
290
+ stroke="currentColor"
291
+ stroke-width="2"
292
+ >
293
+ <circle cx="12" cy="12" r="10" />
294
+ <line x1="15" y1="9" x2="9" y2="15" />
295
+ <line x1="9" y1="9" x2="15" y2="15" />
296
+ </svg>
297
+ {/if}
298
+ <span>{feedback.message}</span>
299
+ </div>
300
+ {/if}
301
+
302
+ <div class="contact-form__fields">
303
+ {#each fields as field}
304
+ <div
305
+ class="contact-form__field"
306
+ class:contact-form__field--error={touched[field.name] && errors[field.name]}
307
+ class:contact-form__field--full={field.type === 'textarea'}
308
+ >
309
+ <label class="contact-form__label" for={field.name}>
310
+ {field.label}
311
+ {#if field.required}
312
+ <span class="contact-form__required">*</span>
313
+ {/if}
314
+ </label>
315
+
316
+ {#if field.type === 'textarea'}
317
+ <textarea
318
+ id={field.name}
319
+ name={field.name}
320
+ class="contact-form__input contact-form__textarea"
321
+ placeholder={field.placeholder}
322
+ rows={field.rows || 4}
323
+ value={formData[field.name] || ''}
324
+ oninput={(e) => handleInput(field, e.currentTarget.value)}
325
+ onblur={() => handleBlur(field)}
326
+ aria-invalid={touched[field.name] && !!errors[field.name]}
327
+ aria-describedby={errors[field.name] ? `${field.name}-error` : undefined}
328
+ ></textarea>
329
+ {:else if field.type === 'select' && field.options}
330
+ <select
331
+ id={field.name}
332
+ name={field.name}
333
+ class="contact-form__input contact-form__select"
334
+ value={formData[field.name] || ''}
335
+ onchange={(e) => handleInput(field, e.currentTarget.value)}
336
+ onblur={() => handleBlur(field)}
337
+ aria-invalid={touched[field.name] && !!errors[field.name]}
338
+ >
339
+ <option value="">{field.placeholder || 'Select an option'}</option>
340
+ {#each field.options as option}
341
+ <option value={option.value}>{option.label}</option>
342
+ {/each}
343
+ </select>
344
+ {:else}
345
+ <input
346
+ id={field.name}
347
+ name={field.name}
348
+ type={field.type}
349
+ class="contact-form__input"
350
+ placeholder={field.placeholder}
351
+ value={formData[field.name] || ''}
352
+ oninput={(e) => handleInput(field, e.currentTarget.value)}
353
+ onblur={() => handleBlur(field)}
354
+ aria-invalid={touched[field.name] && !!errors[field.name]}
355
+ aria-describedby={errors[field.name] ? `${field.name}-error` : undefined}
356
+ />
357
+ {/if}
358
+
359
+ {#if touched[field.name] && errors[field.name]}
360
+ <span id="{field.name}-error" class="contact-form__error" role="alert">
361
+ {errors[field.name]}
362
+ </span>
363
+ {/if}
364
+ </div>
365
+ {/each}
366
+ </div>
367
+
368
+ {#if privacyNotice}
369
+ <p class="contact-form__privacy">{privacyNotice}</p>
370
+ {/if}
371
+
372
+ <button type="submit" class="contact-form__submit" disabled={isSubmitting}>
373
+ {#if isSubmitting}
374
+ <span class="contact-form__spinner"></span>
375
+ Submitting...
376
+ {:else}
377
+ {submitText}
378
+ {/if}
379
+ </button>
380
+ </form>
381
+
382
+ <style>
383
+ .contact-form {
384
+ display: flex;
385
+ flex-direction: column;
386
+ gap: var(--space-lg, 1.5rem);
387
+ font-family: inherit;
388
+ }
389
+
390
+ /* Feedback */
391
+ .contact-form__feedback {
392
+ display: flex;
393
+ align-items: center;
394
+ gap: var(--space-sm, 0.5rem);
395
+ padding: var(--space-md, 1rem);
396
+ border-radius: var(--radius-md, 0.375rem);
397
+ font-size: var(--text-sm, 0.875rem);
398
+ }
399
+
400
+ .contact-form__feedback--success {
401
+ background: var(--color-success-bg, #dcfce7);
402
+ color: var(--color-success, #16a34a);
403
+ }
404
+
405
+ .contact-form__feedback--error {
406
+ background: var(--color-error-bg, #fee2e2);
407
+ color: var(--color-error, #dc2626);
408
+ }
409
+
410
+ .contact-form__feedback--warning {
411
+ background: var(--color-warning-bg, #fef3c7);
412
+ color: var(--color-warning, #d97706);
413
+ }
414
+
415
+ .contact-form__feedback--info {
416
+ background: var(--color-primary-bg, #dbeafe);
417
+ color: var(--color-primary, #2563eb);
418
+ }
419
+
420
+ /* Fields Grid */
421
+ .contact-form__fields {
422
+ display: grid;
423
+ grid-template-columns: repeat(2, 1fr);
424
+ gap: var(--space-md, 1rem);
425
+ }
426
+
427
+ @media (max-width: 640px) {
428
+ .contact-form__fields {
429
+ grid-template-columns: 1fr;
430
+ }
431
+ }
432
+
433
+ /* Field */
434
+ .contact-form__field {
435
+ display: flex;
436
+ flex-direction: column;
437
+ gap: var(--space-xs, 0.25rem);
438
+ }
439
+
440
+ .contact-form__field--full {
441
+ grid-column: 1 / -1;
442
+ }
443
+
444
+ /* Label */
445
+ .contact-form__label {
446
+ font-size: var(--text-sm, 0.875rem);
447
+ font-weight: var(--font-medium, 500);
448
+ color: var(--color-text, #1f2937);
449
+ }
450
+
451
+ .contact-form__required {
452
+ color: var(--color-error, #dc2626);
453
+ }
454
+
455
+ /* Input */
456
+ .contact-form__input {
457
+ min-height: var(--touch-target-min, 44px);
458
+ padding: var(--space-sm, 0.5rem) var(--space-md, 1rem);
459
+ border: 1px solid var(--color-border, #d1d5db);
460
+ border-radius: var(--radius-md, 0.375rem);
461
+ background: var(--color-bg, #ffffff);
462
+ color: var(--color-text, #1f2937);
463
+ font-family: inherit;
464
+ font-size: var(--text-sm, 0.875rem);
465
+ transition:
466
+ border-color var(--transition-fast, 150ms ease),
467
+ box-shadow var(--transition-fast, 150ms ease);
468
+ }
469
+
470
+ .contact-form__input::placeholder {
471
+ color: var(--color-text-muted, #9ca3af);
472
+ }
473
+
474
+ .contact-form__input:focus {
475
+ outline: none;
476
+ border-color: var(--color-primary, #3b82f6);
477
+ box-shadow: 0 0 0 3px var(--color-primary-alpha, rgba(59, 130, 246, 0.1));
478
+ }
479
+
480
+ .contact-form__field--error .contact-form__input {
481
+ border-color: var(--color-error, #dc2626);
482
+ }
483
+
484
+ .contact-form__field--error .contact-form__input:focus {
485
+ box-shadow: 0 0 0 3px var(--color-error-alpha, rgba(220, 38, 38, 0.1));
486
+ }
487
+
488
+ /* Textarea */
489
+ .contact-form__textarea {
490
+ resize: vertical;
491
+ min-height: 100px;
492
+ }
493
+
494
+ /* Select */
495
+ .contact-form__select {
496
+ appearance: none;
497
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
498
+ background-repeat: no-repeat;
499
+ background-position: right 0.75rem center;
500
+ padding-right: 2.5rem;
501
+ }
502
+
503
+ /* Error message */
504
+ .contact-form__error {
505
+ font-size: var(--text-xs, 0.75rem);
506
+ color: var(--color-error, #dc2626);
507
+ }
508
+
509
+ /* Privacy */
510
+ .contact-form__privacy {
511
+ font-size: var(--text-xs, 0.75rem);
512
+ color: var(--color-text-muted, #6b7280);
513
+ margin: 0;
514
+ }
515
+
516
+ /* Submit */
517
+ .contact-form__submit {
518
+ display: flex;
519
+ align-items: center;
520
+ justify-content: center;
521
+ gap: var(--space-sm, 0.5rem);
522
+ min-height: var(--touch-target-min, 44px);
523
+ padding: var(--space-sm, 0.5rem) var(--space-lg, 1.5rem);
524
+ border: none;
525
+ border-radius: var(--radius-md, 0.375rem);
526
+ background: var(--color-primary, #3b82f6);
527
+ color: var(--color-text-inverse, #ffffff);
528
+ font-family: inherit;
529
+ font-size: var(--text-sm, 0.875rem);
530
+ font-weight: var(--font-medium, 500);
531
+ cursor: pointer;
532
+ transition: background var(--transition-fast, 150ms ease);
533
+ }
534
+
535
+ .contact-form__submit:hover:not(:disabled) {
536
+ background: var(--color-primary-hover, #2563eb);
537
+ }
538
+
539
+ .contact-form__submit:focus-visible {
540
+ outline: 2px solid var(--color-primary, #3b82f6);
541
+ outline-offset: 2px;
542
+ }
543
+
544
+ .contact-form__submit:disabled {
545
+ opacity: 0.7;
546
+ cursor: not-allowed;
547
+ }
548
+
549
+ /* Spinner */
550
+ .contact-form__spinner {
551
+ width: 1rem;
552
+ height: 1rem;
553
+ border: 2px solid transparent;
554
+ border-top-color: currentColor;
555
+ border-radius: var(--radius-full, 9999px);
556
+ animation: spin 0.75s linear infinite;
557
+ }
558
+
559
+ @keyframes spin {
560
+ to {
561
+ transform: rotate(360deg);
562
+ }
563
+ }
564
+ </style>
@@ -0,0 +1,44 @@
1
+ interface FieldConfig {
2
+ name: string;
3
+ label: string;
4
+ type: 'text' | 'email' | 'tel' | 'textarea' | 'select';
5
+ placeholder?: string;
6
+ required?: boolean;
7
+ options?: {
8
+ value: string;
9
+ label: string;
10
+ }[];
11
+ rows?: number;
12
+ validation?: (value: string) => string | null;
13
+ }
14
+ interface FormData {
15
+ [key: string]: string;
16
+ }
17
+ interface Props {
18
+ /** Form fields configuration */
19
+ fields?: FieldConfig[];
20
+ /** Initial form values */
21
+ initialValues?: FormData;
22
+ /** Submit button text */
23
+ submitText?: string;
24
+ /** Whether to show success message after submit */
25
+ showSuccessMessage?: boolean;
26
+ /** Success message text */
27
+ successMessage?: string;
28
+ /** Privacy notice text */
29
+ privacyNotice?: string;
30
+ /** Whether to extract UTM params from URL */
31
+ extractUtmParams?: boolean;
32
+ /** Custom class */
33
+ class?: string;
34
+ /** Called on form submit. Should return { success: boolean, error?: string } */
35
+ onSubmit?: (data: FormData) => Promise<{
36
+ success: boolean;
37
+ error?: string;
38
+ }>;
39
+ /** Called when validation state changes */
40
+ onValidate?: (isValid: boolean, errors: Record<string, string>) => void;
41
+ }
42
+ declare const ContactForm: import("svelte").Component<Props, {}, "">;
43
+ type ContactForm = ReturnType<typeof ContactForm>;
44
+ export default ContactForm;
@@ -0,0 +1 @@
1
+ export { default as ContactForm } from './ContactForm.svelte';
@@ -0,0 +1 @@
1
+ export { default as ContactForm } from './ContactForm.svelte';