@happyvertical/smrt-svelte 0.43.3 → 0.43.5

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 (118) hide show
  1. package/AGENTS.md +45 -0
  2. package/README.md +73 -0
  3. package/dist/Provider.svelte +83 -26
  4. package/dist/Provider.svelte.d.ts +4 -12
  5. package/dist/Provider.svelte.d.ts.map +1 -1
  6. package/dist/__tests__/provider-webmcp-harness.svelte +14 -0
  7. package/dist/__tests__/provider-webmcp-harness.svelte.d.ts +8 -0
  8. package/dist/__tests__/provider-webmcp-harness.svelte.d.ts.map +1 -0
  9. package/dist/components/forms/AddressInput.svelte +155 -42
  10. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
  11. package/dist/components/forms/CheckboxInput.svelte +40 -13
  12. package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -1
  13. package/dist/components/forms/DateRangeInput.svelte +132 -16
  14. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
  15. package/dist/components/forms/DateTimeInput.svelte +48 -14
  16. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
  17. package/dist/components/forms/Form.svelte +667 -61
  18. package/dist/components/forms/Form.svelte.d.ts +6 -2
  19. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  20. package/dist/components/forms/MeasurementInput.svelte +137 -31
  21. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
  22. package/dist/components/forms/MoneyInput.svelte +42 -23
  23. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
  24. package/dist/components/forms/NumberInput.svelte +59 -27
  25. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
  26. package/dist/components/forms/PhoneInput.svelte +51 -22
  27. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
  28. package/dist/components/forms/SelectInput.svelte +38 -22
  29. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
  30. package/dist/components/forms/TextInput.svelte +32 -18
  31. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
  32. package/dist/components/forms/TextareaInput.svelte +25 -18
  33. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
  34. package/dist/components/forms/__tests__/AddressInput.behavior.test.js +94 -0
  35. package/dist/components/forms/__tests__/Form.behavior.test.js +1 -1
  36. package/dist/components/forms/__tests__/Form.extraction.test.js +68 -0
  37. package/dist/components/forms/__tests__/Form.test.js +28 -0
  38. package/dist/components/forms/__tests__/Form.webmcp.test.js +2120 -111
  39. package/dist/components/forms/__tests__/FormMicButton.test.js +0 -1
  40. package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte +30 -0
  41. package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts +7 -0
  42. package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts.map +1 -0
  43. package/dist/components/forms/__tests__/async-validation-form.fixture.svelte +19 -0
  44. package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts +9 -0
  45. package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts.map +1 -0
  46. package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte +21 -0
  47. package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts +8 -0
  48. package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts.map +1 -0
  49. package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte +27 -0
  50. package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts +9 -0
  51. package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts.map +1 -0
  52. package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte +48 -0
  53. package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts +10 -0
  54. package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts.map +1 -0
  55. package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte +26 -0
  56. package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts +13 -0
  57. package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts.map +1 -0
  58. package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte +52 -0
  59. package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts +16 -0
  60. package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts.map +1 -0
  61. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +108 -5
  62. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +34 -0
  63. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -1
  64. package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte +24 -0
  65. package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts +11 -0
  66. package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts.map +1 -0
  67. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte +69 -4
  68. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts +32 -0
  69. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts.map +1 -1
  70. package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte +44 -0
  71. package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts +9 -0
  72. package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts.map +1 -0
  73. package/dist/components/forms/__tests__/policy-field.fixture.svelte +35 -0
  74. package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts +8 -0
  75. package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts.map +1 -0
  76. package/dist/components/forms/prepare-field-value.d.ts +5 -0
  77. package/dist/components/forms/prepare-field-value.d.ts.map +1 -0
  78. package/dist/components/forms/prepare-field-value.js +15 -0
  79. package/dist/index.d.ts +3 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +2 -0
  82. package/dist/provider-webmcp.test.js +244 -0
  83. package/dist/state/form-context.d.ts +28 -6
  84. package/dist/state/form-context.d.ts.map +1 -1
  85. package/dist/state/form-context.js +75 -7
  86. package/dist/web/__tests__/provider-ui-registry.fixture.svelte +43 -0
  87. package/dist/web/__tests__/provider-ui-registry.fixture.svelte.d.ts +13 -0
  88. package/dist/web/__tests__/provider-ui-registry.fixture.svelte.d.ts.map +1 -0
  89. package/dist/web/__tests__/provider-ui-registry.test.js +235 -0
  90. package/dist/web/__tests__/webmcp-composed.child.fixture.svelte +62 -0
  91. package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts +8 -0
  92. package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts.map +1 -0
  93. package/dist/web/__tests__/webmcp-composed.fixture.svelte +52 -0
  94. package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts +12 -0
  95. package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts.map +1 -0
  96. package/dist/web/__tests__/webmcp-composed.integration.svelte.test.js +247 -0
  97. package/dist/web/__tests__/webmcp-ssr.fixture.svelte +13 -0
  98. package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts +19 -0
  99. package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts.map +1 -0
  100. package/dist/web/__tests__/webmcp-ui.test.js +586 -0
  101. package/dist/web/__tests__/webmcp.test.js +21 -1
  102. package/dist/web/index.d.ts +3 -0
  103. package/dist/web/index.d.ts.map +1 -1
  104. package/dist/web/index.js +2 -0
  105. package/dist/web/webmcp-provider.d.ts +23 -0
  106. package/dist/web/webmcp-provider.d.ts.map +1 -0
  107. package/dist/web/webmcp-provider.js +1 -0
  108. package/dist/web/webmcp-ui-context.d.ts +12 -0
  109. package/dist/web/webmcp-ui-context.d.ts.map +1 -0
  110. package/dist/web/webmcp-ui-context.js +16 -0
  111. package/dist/web/webmcp-ui.d.ts +21 -0
  112. package/dist/web/webmcp-ui.d.ts.map +1 -0
  113. package/dist/web/webmcp-ui.js +474 -0
  114. package/dist/web/webmcp.d.ts +1 -1
  115. package/dist/web/webmcp.svelte.d.ts +4 -1
  116. package/dist/web/webmcp.svelte.d.ts.map +1 -1
  117. package/dist/web/webmcp.svelte.js +6 -1
  118. package/package.json +8 -6
@@ -1,12 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { ripple } from '@happyvertical/smrt-ui';
3
3
  import { parseSpokenBoolean } from '@happyvertical/smrt-ui/utils/forms/formatters.js';
4
- import { onDestroy, onMount } from 'svelte';
5
4
  import { useAppState } from '../../hooks/useAppState.svelte.js';
6
5
  import {
7
6
  type FieldDefinition,
8
7
  tryGetFormContext,
9
8
  } from '../../state/form-context.js';
9
+ import { invalidStagedValue } from './prepare-field-value.js';
10
10
 
11
11
  export interface Props {
12
12
  /** Field name */
@@ -46,13 +46,18 @@ function updateValue(newValue: boolean) {
46
46
  }
47
47
 
48
48
  // Register with form context
49
- onMount(() => {
49
+ $effect(() => {
50
50
  if (formContext) {
51
+ const registeredName = name;
51
52
  const fieldDef: FieldDefinition = {
52
- name,
53
+ name: registeredName,
53
54
  type: 'checkbox',
54
- label,
55
- description: description || `Yes or no for ${label || name}`,
55
+ get label() {
56
+ return label;
57
+ },
58
+ get description() {
59
+ return description || `Yes or no for ${label || name}`;
60
+ },
56
61
  setValue: (v: unknown) => {
57
62
  if (typeof v === 'boolean') {
58
63
  updateValue(v);
@@ -64,16 +69,38 @@ onMount(() => {
64
69
  }
65
70
  },
66
71
  getValue: () => checked,
67
- constraints: { required },
72
+ prepareValue: (candidate) => {
73
+ if (typeof candidate === 'boolean') return candidate;
74
+ if (
75
+ candidate !== null &&
76
+ candidate !== undefined &&
77
+ typeof candidate !== 'string' &&
78
+ typeof candidate !== 'number'
79
+ ) {
80
+ return invalidStagedValue();
81
+ }
82
+ return (
83
+ parseSpokenBoolean(String(candidate ?? '')) ?? invalidStagedValue()
84
+ );
85
+ },
86
+ clear: () => {
87
+ updateValue(false);
88
+ return true;
89
+ },
90
+ getState: () => ({ disabled }),
91
+ get constraints() {
92
+ return { required };
93
+ },
94
+ validateValue: (candidate) => {
95
+ const next =
96
+ typeof candidate === 'boolean'
97
+ ? candidate
98
+ : parseSpokenBoolean(String(candidate ?? ''));
99
+ return next !== null && (!required || next);
100
+ },
68
101
  validate: () => !required || checked,
69
102
  };
70
- formContext.registerField(fieldDef);
71
- }
72
- });
73
-
74
- onDestroy(() => {
75
- if (formContext) {
76
- formContext.unregisterField(name);
103
+ return formContext.registerField(fieldDef);
77
104
  }
78
105
  });
79
106
 
@@ -1 +1 @@
1
- {"version":3,"file":"CheckboxInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CheckboxInput.svelte.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AA2FD,QAAA,MAAM,aAAa,kDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"CheckboxInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CheckboxInput.svelte.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;CACvC;AAsHD,QAAA,MAAM,aAAa,kDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
- import { onDestroy, onMount } from 'svelte';
4
3
  import { useAppState } from '../../hooks/useAppState.svelte.js';
5
4
  import { useSTT } from '../../hooks/useSTT.svelte.js';
6
5
  import { M } from '../../i18n/strings.forms.js';
@@ -9,6 +8,7 @@ import {
9
8
  type FieldDefinition,
10
9
  tryGetFormContext,
11
10
  } from '../../state/form-context.js';
11
+ import { invalidStagedValue } from './prepare-field-value.js';
12
12
  import type { DateRangeValue } from './types.js';
13
13
 
14
14
  const { t } = useI18n();
@@ -61,6 +61,8 @@ let {
61
61
  const app = useAppState();
62
62
  const stt = useSTT();
63
63
  const formContext = tryGetFormContext();
64
+ const fieldOwnerId = $props.id();
65
+ const fieldOwnerToken = `smrt-rich-field-${fieldOwnerId}`;
64
66
 
65
67
  const isSmrt = $derived(app.state.mode === 'smrt');
66
68
 
@@ -68,6 +70,7 @@ let isRecording = $state(false);
68
70
  let isParsing = $state(false);
69
71
  let parseError = $state<string | null>(null);
70
72
  let recordingStartTime = 0;
73
+ let primaryControl = $state<HTMLElement | null>(null);
71
74
 
72
75
  const MIN_HOLD_TIME = 500;
73
76
  const MIN_TRANSCRIPT_LENGTH = 2;
@@ -176,16 +179,37 @@ function formatToISO(date: Date): string {
176
179
  return `${year}-${month}-${day}`;
177
180
  }
178
181
 
182
+ function isCanonicalCalendarDate(value: string): boolean {
183
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
184
+ if (!match) return false;
185
+ const year = Number(match[1]);
186
+ const month = Number(match[2]);
187
+ const day = Number(match[3]);
188
+ const date = new Date(Date.UTC(year, month - 1, day));
189
+ return (
190
+ date.getUTCFullYear() === year &&
191
+ date.getUTCMonth() === month - 1 &&
192
+ date.getUTCDate() === day
193
+ );
194
+ }
195
+
179
196
  // Register with form context
180
- onMount(() => {
197
+ $effect(() => {
181
198
  if (formContext) {
199
+ const registeredName = name;
182
200
  const fieldDef: FieldDefinition = {
183
- name,
201
+ name: registeredName,
202
+ ownerToken: fieldOwnerToken,
184
203
  type: 'daterange',
185
- label,
186
- description:
187
- description ||
188
- 'A date range with start and end dates (say "from [start] to [end]")',
204
+ get label() {
205
+ return label;
206
+ },
207
+ get description() {
208
+ return (
209
+ description ||
210
+ 'A date range with start and end dates (say "from [start] to [end]")'
211
+ );
212
+ },
189
213
  setValue: (v: unknown) => {
190
214
  if (v === null || v === undefined) {
191
215
  updateValue('', '');
@@ -206,17 +230,102 @@ onMount(() => {
206
230
  .catch(() => {});
207
231
  },
208
232
  getValue: () => ({ startDate, endDate }),
209
- constraints: { required },
233
+ prepareValue: (candidate) => {
234
+ if (candidate === null || candidate === undefined) {
235
+ return { startDate: '', endDate: '' };
236
+ }
237
+ if (
238
+ typeof candidate !== 'object' ||
239
+ Array.isArray(candidate) ||
240
+ ![Object.prototype, null].includes(Object.getPrototypeOf(candidate))
241
+ ) {
242
+ return invalidStagedValue();
243
+ }
244
+ return candidate;
245
+ },
246
+ prepareExtractedValue: async (candidate) => {
247
+ if (typeof candidate !== 'string') {
248
+ return invalidStagedValue();
249
+ }
250
+ const parsed = await parseNaturalLanguageRange(candidate);
251
+ if (!parsed) {
252
+ return invalidStagedValue();
253
+ }
254
+ return { startDate: parsed.start, endDate: parsed.end };
255
+ },
256
+ clear: () => {
257
+ updateValue('', '');
258
+ return true;
259
+ },
260
+ getState: () => ({
261
+ disabled: disabled || primaryControl?.matches(':disabled') === true,
262
+ }),
263
+ get constraints() {
264
+ return { required };
265
+ },
266
+ focus: () => primaryControl?.focus(),
267
+ get webMcpSchema() {
268
+ return {
269
+ type: 'object',
270
+ additionalProperties: false,
271
+ properties: {
272
+ startDate: {
273
+ type: 'string',
274
+ format: 'date',
275
+ ...(minDate ? { formatMinimum: minDate } : {}),
276
+ ...(maxDate ? { formatMaximum: maxDate } : {}),
277
+ },
278
+ endDate: {
279
+ type: 'string',
280
+ format: 'date',
281
+ ...(minDate ? { formatMinimum: minDate } : {}),
282
+ ...(maxDate ? { formatMaximum: maxDate } : {}),
283
+ },
284
+ },
285
+ ...(required ? { required: ['startDate', 'endDate'] } : {}),
286
+ };
287
+ },
288
+ validateValue: (candidate) => {
289
+ if (candidate === null || candidate === undefined) return !required;
290
+ if (
291
+ typeof candidate !== 'object' ||
292
+ Array.isArray(candidate) ||
293
+ ![Object.prototype, null].includes(Object.getPrototypeOf(candidate))
294
+ ) {
295
+ return false;
296
+ }
297
+ const range = candidate as Record<string, unknown>;
298
+ if (
299
+ !Object.keys(range).every((key) =>
300
+ ['startDate', 'endDate'].includes(key),
301
+ ) ||
302
+ !Object.hasOwn(range, 'startDate') ||
303
+ !Object.hasOwn(range, 'endDate') ||
304
+ typeof range.startDate !== 'string' ||
305
+ typeof range.endDate !== 'string'
306
+ ) {
307
+ return false;
308
+ }
309
+ const start = range.startDate;
310
+ const end = range.endDate;
311
+ if (required && (!start || !end)) return false;
312
+ if (
313
+ (start && !isCanonicalCalendarDate(start)) ||
314
+ (end && !isCanonicalCalendarDate(end)) ||
315
+ (start && minDate && start < minDate) ||
316
+ (start && maxDate && start > maxDate) ||
317
+ (end && minDate && end < minDate) ||
318
+ (end && maxDate && end > maxDate) ||
319
+ (start && end && start > end)
320
+ ) {
321
+ return false;
322
+ }
323
+ return true;
324
+ },
210
325
  validate: () =>
211
326
  !required || (startDate.trim().length > 0 && endDate.trim().length > 0),
212
327
  };
213
- formContext.registerField(fieldDef);
214
- }
215
- });
216
-
217
- onDestroy(() => {
218
- if (formContext) {
219
- formContext.unregisterField(name);
328
+ return formContext.registerField(fieldDef);
220
329
  }
221
330
  });
222
331
 
@@ -336,7 +445,12 @@ function handleMicKeydown(e: KeyboardEvent) {
336
445
  const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
337
446
  </script>
338
447
 
339
- <div class="smrt-daterange" class:listening={isRecording} class:parsing={isParsing}>
448
+ <div
449
+ class="smrt-daterange"
450
+ class:listening={isRecording}
451
+ class:parsing={isParsing}
452
+ data-smrt-field-owner={fieldOwnerToken}
453
+ >
340
454
  {#if label}
341
455
  <label class="smrt-label" for={primaryControlId}>
342
456
  {label}
@@ -363,6 +477,7 @@ const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
363
477
  </div>
364
478
 
365
479
  <button
480
+ bind:this={primaryControl}
366
481
  id={primaryControlId}
367
482
  type="button"
368
483
  class="mic-btn"
@@ -395,6 +510,7 @@ const primaryControlId = $derived(isSmrt ? `${name}_voice` : `${name}_start`);
395
510
  <div class="date-field">
396
511
  <label for="{name}_start" class="field-label">Start</label>
397
512
  <input
513
+ bind:this={primaryControl}
398
514
  id="{name}_start"
399
515
  name="{name}[startDate]"
400
516
  type="date"
@@ -1 +1 @@
1
- {"version":3,"file":"DateRangeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateRangeInput.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC5C;AAqYD,QAAA,MAAM,cAAc,gEAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"DateRangeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateRangeInput.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6BAA6B;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2BAA2B;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC5C;AAkfD,QAAA,MAAM,cAAc,gEAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { useI18n } from '@happyvertical/smrt-ui/i18n';
3
3
  import { importOptional } from '@happyvertical/smrt-ui/utils/import-optional.js';
4
- import { onDestroy, onMount } from 'svelte';
5
4
  import { useAppState } from '../../hooks/useAppState.svelte.js';
6
5
  import { useSTT } from '../../hooks/useSTT.svelte.js';
7
6
  import { M } from '../../i18n/strings.forms.js';
@@ -10,6 +9,7 @@ import {
10
9
  type FieldDefinition,
11
10
  tryGetFormContext,
12
11
  } from '../../state/form-context.js';
12
+ import { prepareTextFieldValue } from './prepare-field-value.js';
13
13
 
14
14
  const { t } = useI18n();
15
15
 
@@ -105,33 +105,67 @@ function formatForDisplay(isoValue: string): string {
105
105
  }
106
106
  }
107
107
 
108
+ function isValidDateTimeValue(candidate: unknown): boolean {
109
+ if (typeof candidate !== 'string') return false;
110
+ if (candidate === '') return !required;
111
+ const match = includeTime
112
+ ? /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,3})?)?$/.exec(
113
+ candidate,
114
+ )
115
+ : /^(\d{4})-(\d{2})-(\d{2})$/.exec(candidate);
116
+ if (!match) return false;
117
+ const [, year, month, day, hour = '0', minute = '0', second = '0'] = match;
118
+ const date = new Date(
119
+ Date.UTC(
120
+ Number(year),
121
+ Number(month) - 1,
122
+ Number(day),
123
+ Number(hour),
124
+ Number(minute),
125
+ Number(second),
126
+ ),
127
+ );
128
+ return (
129
+ date.getUTCFullYear() === Number(year) &&
130
+ date.getUTCMonth() === Number(month) - 1 &&
131
+ date.getUTCDate() === Number(day) &&
132
+ date.getUTCHours() === Number(hour) &&
133
+ date.getUTCMinutes() === Number(minute) &&
134
+ date.getUTCSeconds() === Number(second)
135
+ );
136
+ }
137
+
108
138
  // Update display when value changes
109
139
  $effect(() => {
110
140
  displayValue = formatForDisplay(value);
111
141
  });
112
142
 
113
143
  // Register with form context
114
- onMount(() => {
144
+ $effect(() => {
115
145
  if (formContext) {
146
+ const registeredName = name;
116
147
  const fieldDef: FieldDefinition = {
117
- name,
148
+ name: registeredName,
118
149
  type: 'datetime',
119
- label,
120
- description: description ?? (includeTime ? 'Date and time' : 'Date'),
150
+ get label() {
151
+ return label;
152
+ },
153
+ get description() {
154
+ return description ?? (includeTime ? 'Date and time' : 'Date');
155
+ },
121
156
  setValue: (v: unknown) => {
122
157
  updateValue(String(v ?? ''));
123
158
  },
124
159
  getValue: () => value,
125
- constraints: { required },
126
- validate: () => !required || value.trim().length > 0,
160
+ prepareValue: prepareTextFieldValue,
161
+ getState: () => ({ disabled }),
162
+ get constraints() {
163
+ return { required };
164
+ },
165
+ validateValue: isValidDateTimeValue,
166
+ validate: () => isValidDateTimeValue(value),
127
167
  };
128
- formContext.registerField(fieldDef);
129
- }
130
- });
131
-
132
- onDestroy(() => {
133
- if (formContext) {
134
- formContext.unregisterField(name);
168
+ return formContext.registerField(fieldDef);
135
169
  }
136
170
  });
137
171
 
@@ -1 +1 @@
1
- {"version":3,"file":"DateTimeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateTimeInput.svelte.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AA+UD,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"DateTimeInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/DateTimeInput.svelte.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iCAAiC;IACjC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAiXD,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}