@happyvertical/smrt-svelte 0.42.6 → 0.43.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 (54) hide show
  1. package/README.md +20 -0
  2. package/dist/Provider.svelte +49 -0
  3. package/dist/Provider.svelte.d.ts +15 -0
  4. package/dist/Provider.svelte.d.ts.map +1 -1
  5. package/dist/__tests__/data-surface-bridge.test.js +351 -0
  6. package/dist/components/forms/AddressInput.svelte +11 -0
  7. package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
  8. package/dist/components/forms/CheckboxInput.svelte +3 -1
  9. package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -1
  10. package/dist/components/forms/DateRangeInput.svelte +3 -0
  11. package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
  12. package/dist/components/forms/DateTimeInput.svelte +2 -0
  13. package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
  14. package/dist/components/forms/Form.svelte +172 -6
  15. package/dist/components/forms/Form.svelte.d.ts +8 -1
  16. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  17. package/dist/components/forms/MeasurementInput.svelte +3 -0
  18. package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
  19. package/dist/components/forms/MoneyInput.svelte +3 -0
  20. package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
  21. package/dist/components/forms/NumberInput.svelte +3 -0
  22. package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
  23. package/dist/components/forms/PhoneInput.svelte +2 -0
  24. package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
  25. package/dist/components/forms/SelectInput.svelte +7 -1
  26. package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
  27. package/dist/components/forms/TextInput.svelte +5 -1
  28. package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
  29. package/dist/components/forms/TextareaInput.svelte +3 -1
  30. package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
  31. package/dist/components/forms/__tests__/Form.webmcp.test.js +216 -0
  32. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +25 -3
  33. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +5 -0
  34. package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -1
  35. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte +40 -0
  36. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts +10 -0
  37. package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts.map +1 -0
  38. package/dist/data-surface.d.ts +104 -0
  39. package/dist/data-surface.d.ts.map +1 -0
  40. package/dist/data-surface.js +348 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -0
  44. package/dist/state/form-context.d.ts +2 -0
  45. package/dist/state/form-context.d.ts.map +1 -1
  46. package/dist/web/__tests__/webmcp-harness.svelte +14 -0
  47. package/dist/web/__tests__/webmcp-harness.svelte.d.ts +7 -0
  48. package/dist/web/__tests__/webmcp-harness.svelte.d.ts.map +1 -0
  49. package/dist/web/__tests__/webmcp.test.js +39 -0
  50. package/dist/web/webmcp.d.ts +21 -0
  51. package/dist/web/webmcp.svelte.d.ts +28 -0
  52. package/dist/web/webmcp.svelte.d.ts.map +1 -0
  53. package/dist/web/webmcp.svelte.js +27 -0
  54. package/package.json +17 -5
@@ -18,6 +18,7 @@ import {
18
18
  type SMRTFormContext,
19
19
  setFormContext,
20
20
  } from '../../state/form-context.js';
21
+ import { useWebMcpTool } from '../../web/webmcp.svelte.js';
21
22
  import type { LLMModelId, STTAdapterType } from './types.js';
22
23
 
23
24
  const { t } = useI18n();
@@ -36,7 +37,11 @@ export interface Props {
36
37
  /** LLM model for extraction (or 'none' for regex-only) */
37
38
  llmModel?: LLMModelId;
38
39
  /** Called when form is submitted (if provided, prevents native submission) */
39
- onsubmit?: (data: Record<string, unknown>) => void;
40
+ onsubmit?: (data: Record<string, unknown>) => void | Promise<void>;
41
+ /** Expose this form's submit intent to WebMCP. */
42
+ webmcp?: boolean | { name?: string; description?: string };
43
+ /** Collection/model identity used when naming the generated intent. */
44
+ collection?: string;
40
45
  /** HTTP method for native form submission (default: GET) */
41
46
  method?: 'GET' | 'POST';
42
47
  /** Form action URL for native form submission */
@@ -58,6 +63,8 @@ const {
58
63
  sttAdapter = 'whisper-wasm',
59
64
  llmModel = 'none',
60
65
  onsubmit,
66
+ webmcp,
67
+ collection,
61
68
  method,
62
69
  action,
63
70
  formId,
@@ -187,9 +194,170 @@ const formContext: SMRTFormContext = {
187
194
  },
188
195
  };
189
196
 
197
+ function webMcpFieldSchema(field: FieldDefinition): Record<string, unknown> {
198
+ let schema: Record<string, unknown>;
199
+ if (field.webMcpSchema) {
200
+ schema = { ...field.webMcpSchema };
201
+ } else {
202
+ switch (field.type) {
203
+ case 'measurement':
204
+ schema = {
205
+ type: 'object',
206
+ properties: {
207
+ value: {
208
+ type: 'number',
209
+ ...(field.constraints?.min !== undefined
210
+ ? { minimum: Number(field.constraints.min) }
211
+ : {}),
212
+ ...(field.constraints?.max !== undefined
213
+ ? { maximum: Number(field.constraints.max) }
214
+ : {}),
215
+ },
216
+ unit: {
217
+ type: 'string',
218
+ enum: ['ft', 'in', 'm', 'cm', 'mm', 'yd'],
219
+ },
220
+ },
221
+ ...(field.constraints?.required
222
+ ? { required: ['value', 'unit'] }
223
+ : {}),
224
+ };
225
+ break;
226
+ case 'daterange':
227
+ schema = {
228
+ type: 'object',
229
+ properties: {
230
+ startDate: { type: 'string' },
231
+ endDate: { type: 'string' },
232
+ },
233
+ ...(field.constraints?.required
234
+ ? { required: ['startDate', 'endDate'] }
235
+ : {}),
236
+ };
237
+ break;
238
+ case 'address':
239
+ schema = {
240
+ type: 'object',
241
+ properties: {
242
+ street: { type: 'string' },
243
+ city: { type: 'string' },
244
+ province: { type: 'string' },
245
+ postalCode: { type: 'string' },
246
+ country: { type: 'string' },
247
+ },
248
+ ...(field.constraints?.required
249
+ ? {
250
+ required: [
251
+ 'street',
252
+ 'city',
253
+ 'province',
254
+ 'postalCode',
255
+ 'country',
256
+ ],
257
+ }
258
+ : {}),
259
+ };
260
+ break;
261
+ case 'number':
262
+ case 'money':
263
+ schema = { type: 'number' };
264
+ break;
265
+ case 'checkbox':
266
+ schema = { type: 'boolean' };
267
+ break;
268
+ default:
269
+ schema = { type: 'string' };
270
+ }
271
+ }
272
+
273
+ if (field.label) schema.title = field.label;
274
+ if (field.description) schema.description = field.description;
275
+ if (field.options) {
276
+ schema.enum = field.options.map((option) => option.value);
277
+ }
278
+ if (schema.type === 'number' && field.constraints?.min !== undefined) {
279
+ schema.minimum = Number(field.constraints.min);
280
+ }
281
+ if (schema.type === 'number' && field.constraints?.max !== undefined) {
282
+ schema.maximum = Number(field.constraints.max);
283
+ }
284
+ if (schema.type === 'string' && field.constraints?.minLength !== undefined) {
285
+ schema.minLength = field.constraints.minLength;
286
+ }
287
+ if (schema.type === 'string' && field.constraints?.maxLength !== undefined) {
288
+ schema.maxLength = field.constraints.maxLength;
289
+ }
290
+ if (schema.type === 'string' && field.constraints?.pattern) {
291
+ schema.pattern = field.constraints.pattern;
292
+ }
293
+ return schema;
294
+ }
295
+
296
+ function formInputSchema(): Record<string, unknown> {
297
+ const properties: Record<string, unknown> = {};
298
+ const required: string[] = [];
299
+
300
+ for (const field of fields.values()) {
301
+ properties[field.name] = webMcpFieldSchema(field);
302
+ if (field.constraints?.required) required.push(field.name);
303
+ }
304
+
305
+ return {
306
+ type: 'object',
307
+ properties,
308
+ ...(required.length > 0 ? { required } : {}),
309
+ };
310
+ }
311
+
190
312
  // Provide context to children
191
313
  setFormContext(formContext);
192
314
 
315
+ async function submitForWebMcp(args: Record<string, unknown>): Promise<string> {
316
+ // Stage tool arguments through the same field setters used by native input
317
+ // controls, then run the shared validation and submit path.
318
+ for (const [name, value] of Object.entries(args)) {
319
+ fields.get(name)?.setValue(value);
320
+ }
321
+
322
+ const data: Record<string, unknown> = {};
323
+ let valid = true;
324
+ for (const [name, field] of fields) {
325
+ data[name] = field.getValue();
326
+ if (
327
+ field.constraints?.required &&
328
+ (data[name] === '' || data[name] == null)
329
+ ) {
330
+ valid = false;
331
+ }
332
+ if (field.validate) {
333
+ try {
334
+ valid = field.validate() && valid;
335
+ } catch {
336
+ valid = false;
337
+ }
338
+ }
339
+ }
340
+ if (!valid) return 'Validation failed';
341
+ if (!onsubmit) return 'No submit handler configured';
342
+
343
+ await onsubmit(data);
344
+ return 'Submitted successfully';
345
+ }
346
+
347
+ useWebMcpTool(() => {
348
+ if (!webmcp) return null;
349
+ const options = typeof webmcp === 'object' ? webmcp : {};
350
+ return {
351
+ name: options.name ?? `${collection ?? resolvedFormId}_submit`,
352
+ description:
353
+ options.description ??
354
+ `Submit the ${collection ?? name ?? resolvedFormId} form after validating its fields`,
355
+ inputSchema: formInputSchema(),
356
+ annotations: { readOnlyHint: false },
357
+ execute: submitForWebMcp,
358
+ };
359
+ });
360
+
193
361
  // Clean up extracted values based on field type
194
362
  function cleanValue(value: unknown, fieldType: string): unknown {
195
363
  if (typeof value !== 'string') return value;
@@ -608,11 +776,9 @@ function handleSubmit(e: Event) {
608
776
  // This allows native form submission for SvelteKit form actions
609
777
  if (onsubmit) {
610
778
  e.preventDefault();
611
- const data: Record<string, unknown> = {};
612
- for (const [name, field] of fields) {
613
- data[name] = field.getValue();
614
- }
615
- onsubmit(data);
779
+ void submitForWebMcp({}).catch((error) => {
780
+ logger.error('Form submit failed', { error });
781
+ });
616
782
  }
617
783
  // Otherwise, let native form submission happen (e.g., for SvelteKit use:enhance)
618
784
  }
@@ -15,7 +15,14 @@ export interface Props {
15
15
  /** LLM model for extraction (or 'none' for regex-only) */
16
16
  llmModel?: LLMModelId;
17
17
  /** Called when form is submitted (if provided, prevents native submission) */
18
- onsubmit?: (data: Record<string, unknown>) => void;
18
+ onsubmit?: (data: Record<string, unknown>) => void | Promise<void>;
19
+ /** Expose this form's submit intent to WebMCP. */
20
+ webmcp?: boolean | {
21
+ name?: string;
22
+ description?: string;
23
+ };
24
+ /** Collection/model identity used when naming the generated intent. */
25
+ collection?: string;
19
26
  /** HTTP method for native form submission (default: GET) */
20
27
  method?: 'GET' | 'POST';
21
28
  /** Form action URL for native form submission */
@@ -1 +1 @@
1
- {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAIhC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAWtC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAupBD,QAAA,MAAM,IAAI;uBAhFe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kCAQZ,0BAA0B;MAwEZ,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAIhC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAYtC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyzBD,QAAA,MAAM,IAAI;uBAhFe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kCAQZ,0BAA0B;MAwEZ,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -302,6 +302,9 @@ onMount(() => {
302
302
  }
303
303
  },
304
304
  getValue: () => (value !== null ? { value, unit } : null),
305
+ constraints: { required, min, max },
306
+ validate: () =>
307
+ value === null ? !required : Number.isFinite(value) && isInRange,
305
308
  };
306
309
  formContext.registerField(fieldDef);
307
310
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MeasurementInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/MeasurementInput.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGpE,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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,qCAAqC;IACrC,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,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,WAAW,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3D;AAmVD,QAAA,MAAM,gBAAgB,yDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"MeasurementInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/MeasurementInput.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGpE,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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,qCAAqC;IACrC,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;IAC1B,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,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,WAAW,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3D;AAsVD,QAAA,MAAM,gBAAgB,yDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -252,6 +252,9 @@ onMount(() => {
252
252
  }
253
253
  },
254
254
  getValue: () => value,
255
+ constraints: { required, min, max },
256
+ validate: () =>
257
+ value === null ? !required : Number.isFinite(value) && isInRange,
255
258
  };
256
259
  formContext.registerField(fieldDef);
257
260
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MoneyInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/MoneyInput.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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,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,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3C;AAiSD,QAAA,MAAM,UAAU,gDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"MoneyInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/MoneyInput.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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACzB,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,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,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3C;AAoSD,QAAA,MAAM,UAAU,gDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -175,6 +175,9 @@ onMount(() => {
175
175
  }
176
176
  },
177
177
  getValue: () => value,
178
+ constraints: { required, min, max, step },
179
+ validate: () =>
180
+ value === null ? !required : Number.isFinite(value) && isInRange,
178
181
  };
179
182
  formContext.registerField(fieldDef);
180
183
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NumberInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/NumberInput.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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,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,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3C;AAoMD,QAAA,MAAM,WAAW,gDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"NumberInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/NumberInput.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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,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,GAAG,IAAI,KAAK,IAAI,CAAC;CAC3C;AAuMD,QAAA,MAAM,WAAW,gDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -149,6 +149,8 @@ onMount(() => {
149
149
  updateValue(formatted);
150
150
  },
151
151
  getValue: () => value,
152
+ constraints: { required },
153
+ validate: () => !required || value.trim().length > 0,
152
154
  };
153
155
  formContext.registerField(fieldDef);
154
156
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PhoneInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/PhoneInput.svelte.ts"],"names":[],"mappings":"AAeA,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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,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;AA4SD,QAAA,MAAM,UAAU,gDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"PhoneInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/PhoneInput.svelte.ts"],"names":[],"mappings":"AAeA,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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,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;AA8SD,QAAA,MAAM,UAAU,gDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -77,6 +77,12 @@ onMount(() => {
77
77
  }
78
78
  },
79
79
  getValue: () => value,
80
+ constraints: { required },
81
+ options: options.map((option) => ({
82
+ value: option.value,
83
+ label: option.label,
84
+ })),
85
+ validate: () => !required || value.trim().length > 0,
80
86
  };
81
87
  formContext.registerField(fieldDef);
82
88
  }
@@ -251,4 +257,4 @@ function handleChange(e: Event) {
251
257
  .smrt-mode {
252
258
  --field-active: var(--smrt-color-tertiary);
253
259
  }
254
- </style>
260
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"SelectInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/SelectInput.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,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,wBAAwB;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,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;AA2GD,QAAA,MAAM,WAAW,gDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"SelectInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/SelectInput.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,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,wBAAwB;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,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;AAkHD,QAAA,MAAM,WAAW,gDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -102,6 +102,10 @@ onMount(() => {
102
102
  updateValue(String(v ?? ''));
103
103
  },
104
104
  getValue: () => value,
105
+ constraints: { required },
106
+ validate: () =>
107
+ (!required || value.trim().length > 0) &&
108
+ (type !== 'email' || value.length === 0 || isValidEmail),
105
109
  };
106
110
  formContext.registerField(fieldDef);
107
111
  }
@@ -440,4 +444,4 @@ function handleInput(e: Event) {
440
444
  opacity: 0.38;
441
445
  pointer-events: none;
442
446
  }
443
- </style>
447
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"TextInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextInput.svelte.ts"],"names":[],"mappings":"AAoBA,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,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAwPD,QAAA,MAAM,SAAS,gDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"TextInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextInput.svelte.ts"],"names":[],"mappings":"AAoBA,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,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AA6PD,QAAA,MAAM,SAAS,gDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -90,6 +90,8 @@ onMount(() => {
90
90
  }
91
91
  },
92
92
  getValue: () => value,
93
+ constraints: { required },
94
+ validate: () => !required || value.trim().length > 0,
93
95
  };
94
96
  formContext.registerField(fieldDef);
95
97
  }
@@ -408,4 +410,4 @@ function handleInput(e: Event) {
408
410
  .smrt-mode {
409
411
  --field-active: var(--smrt-color-tertiary);
410
412
  }
411
- </style>
413
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"TextareaInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextareaInput.svelte.ts"],"names":[],"mappings":"AAiBA,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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AA8ND,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"TextareaInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextareaInput.svelte.ts"],"names":[],"mappings":"AAiBA,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,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAiOD,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,216 @@
1
+ import { render } from '@testing-library/svelte';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { tick } from 'svelte';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ vi.mock('../../../hooks/useAppState.svelte.js', () => ({
6
+ useAppState: () => ({ state: { mode: 'default' }, setMode: vi.fn() }),
7
+ }));
8
+ vi.mock('../../../hooks/useSTT.svelte.js', () => ({
9
+ useSTT: () => ({
10
+ isListening: false,
11
+ lastResult: '',
12
+ isReady: false,
13
+ adapterType: null,
14
+ isInitializing: false,
15
+ downloadProgress: 0,
16
+ initialize: vi.fn(),
17
+ start: vi.fn(),
18
+ stop: vi.fn(),
19
+ }),
20
+ }));
21
+ import FormWithFields from './form-with-fields.fixture.svelte';
22
+ import FormWithStructuredFields from './form-with-structured-fields.fixture.svelte';
23
+ afterEach(() => {
24
+ delete document.modelContext;
25
+ });
26
+ describe('Form WebMCP submit intent', () => {
27
+ it('registers a field-derived tool and dispatches validation + submit', async () => {
28
+ const registered = [];
29
+ document.modelContext = {
30
+ registerTool(tool) {
31
+ registered.push(tool);
32
+ },
33
+ };
34
+ const onsubmit = vi.fn();
35
+ render(FormWithFields, { props: { webmcp: true, onsubmit } });
36
+ await tick();
37
+ await tick();
38
+ expect(registered).toHaveLength(1);
39
+ expect(registered[0].inputSchema).toMatchObject({
40
+ type: 'object',
41
+ properties: { fullname: { type: 'string' }, age: { type: 'number' } },
42
+ });
43
+ expect(registered[0].name).toContain('submit');
44
+ const result = await registered[0].execute({
45
+ fullname: 'Ada Lovelace',
46
+ age: 36,
47
+ });
48
+ expect(result).toBe('Submitted successfully');
49
+ expect(onsubmit).toHaveBeenCalledWith({
50
+ fullname: 'Ada Lovelace',
51
+ age: 36,
52
+ });
53
+ });
54
+ it('publishes required and numeric constraints and validates them before submit', async () => {
55
+ const registered = [];
56
+ document.modelContext = {
57
+ registerTool(tool) {
58
+ registered.push(tool);
59
+ },
60
+ };
61
+ const onsubmit = vi.fn();
62
+ render(FormWithFields, {
63
+ props: {
64
+ webmcp: true,
65
+ textRequired: true,
66
+ ageRequired: true,
67
+ ageMin: 18,
68
+ ageMax: 65,
69
+ onsubmit,
70
+ },
71
+ });
72
+ await tick();
73
+ await tick();
74
+ expect(registered).toHaveLength(1);
75
+ const schema = registered[0].inputSchema;
76
+ expect(schema.required).toEqual(['fullname', 'age']);
77
+ expect(schema.properties.age).toMatchObject({
78
+ type: 'number',
79
+ minimum: 18,
80
+ maximum: 65,
81
+ });
82
+ expect(await registered[0].execute({ age: 17 })).toBe('Validation failed');
83
+ expect(await registered[0].execute({ fullname: 'Ada', age: 66 })).toBe('Validation failed');
84
+ expect(onsubmit).not.toHaveBeenCalled();
85
+ expect(await registered[0].execute({ fullname: 'Ada', age: 36 })).toBe('Submitted successfully');
86
+ expect(onsubmit).toHaveBeenCalledWith({ fullname: 'Ada', age: 36 });
87
+ });
88
+ it('keeps the browser path a no-op without WebMCP', async () => {
89
+ const onsubmit = vi.fn();
90
+ const view = render(FormWithFields, { props: { onsubmit } });
91
+ await userEvent.click(view.getByRole('button', { name: 'Submit' }));
92
+ expect(onsubmit).toHaveBeenCalledTimes(1);
93
+ });
94
+ it('publishes and accepts structured measurement, date range, and address fields', async () => {
95
+ const registered = [];
96
+ document.modelContext = {
97
+ registerTool(tool) {
98
+ registered.push(tool);
99
+ },
100
+ };
101
+ const onsubmit = vi.fn();
102
+ render(FormWithStructuredFields, { props: { webmcp: true, onsubmit } });
103
+ await tick();
104
+ await tick();
105
+ expect(registered).toHaveLength(1);
106
+ const schema = registered[0].inputSchema;
107
+ expect(schema.required).toEqual(['measurement', 'dates', 'address']);
108
+ expect(schema.properties.measurement).toMatchObject({
109
+ type: 'object',
110
+ properties: {
111
+ value: { type: 'number' },
112
+ unit: { type: 'string' },
113
+ },
114
+ required: ['value', 'unit'],
115
+ });
116
+ expect(schema.properties.dates).toMatchObject({
117
+ type: 'object',
118
+ properties: {
119
+ startDate: { type: 'string' },
120
+ endDate: { type: 'string' },
121
+ },
122
+ required: ['startDate', 'endDate'],
123
+ });
124
+ expect(schema.properties.address).toMatchObject({
125
+ type: 'object',
126
+ properties: {
127
+ street: { type: 'string' },
128
+ city: { type: 'string' },
129
+ province: { type: 'string' },
130
+ postalCode: { type: 'string' },
131
+ country: { type: 'string' },
132
+ },
133
+ required: ['street', 'city', 'province', 'postalCode', 'country'],
134
+ });
135
+ const values = {
136
+ measurement: { value: 1.5, unit: 'm' },
137
+ dates: { startDate: '2026-01-01', endDate: '2026-01-02' },
138
+ address: {
139
+ street: '1 Main St',
140
+ city: 'Edmonton',
141
+ province: 'AB',
142
+ postalCode: 'T1A 1A1',
143
+ country: 'CA',
144
+ },
145
+ };
146
+ expect(await registered[0].execute({
147
+ ...values,
148
+ address: {},
149
+ })).toBe('Validation failed');
150
+ expect(onsubmit).not.toHaveBeenCalled();
151
+ expect(await registered[0].execute(values)).toBe('Submitted successfully');
152
+ expect(onsubmit).toHaveBeenCalledWith(values);
153
+ });
154
+ it('limits the address schema and payload to configured fields', async () => {
155
+ const registered = [];
156
+ document.modelContext = {
157
+ registerTool(tool) {
158
+ registered.push(tool);
159
+ },
160
+ };
161
+ const onsubmit = vi.fn();
162
+ render(FormWithStructuredFields, {
163
+ props: {
164
+ webmcp: true,
165
+ addressFields: ['city', 'country'],
166
+ onsubmit,
167
+ },
168
+ });
169
+ await tick();
170
+ await tick();
171
+ expect(registered).toHaveLength(1);
172
+ const schema = registered[0].inputSchema;
173
+ expect(schema.properties.address).toMatchObject({
174
+ type: 'object',
175
+ properties: {
176
+ city: { type: 'string' },
177
+ country: { type: 'string' },
178
+ },
179
+ required: ['city', 'country'],
180
+ });
181
+ expect(schema.properties.address.properties).not.toHaveProperty('street');
182
+ const values = {
183
+ measurement: { value: 1.5, unit: 'm' },
184
+ dates: { startDate: '2026-01-01', endDate: '2026-01-02' },
185
+ address: { city: 'Edmonton', country: 'CA' },
186
+ };
187
+ expect(await registered[0].execute(values)).toBe('Submitted successfully');
188
+ expect(onsubmit).toHaveBeenCalledWith(values);
189
+ });
190
+ it('allows partial payloads for optional structured fields', async () => {
191
+ const registered = [];
192
+ document.modelContext = {
193
+ registerTool(tool) {
194
+ registered.push(tool);
195
+ },
196
+ };
197
+ const onsubmit = vi.fn();
198
+ render(FormWithStructuredFields, {
199
+ props: { webmcp: true, structuredRequired: false, onsubmit },
200
+ });
201
+ await tick();
202
+ await tick();
203
+ expect(registered).toHaveLength(1);
204
+ const schema = registered[0].inputSchema;
205
+ expect(schema.properties.measurement).not.toHaveProperty('required');
206
+ expect(schema.properties.dates).not.toHaveProperty('required');
207
+ expect(schema.properties.address).not.toHaveProperty('required');
208
+ const partialValues = {
209
+ measurement: { value: 1.5 },
210
+ dates: { startDate: '2026-01-01' },
211
+ address: { city: 'Edmonton' },
212
+ };
213
+ expect(await registered[0].execute(partialValues)).toBe('Submitted successfully');
214
+ expect(onsubmit).toHaveBeenCalledTimes(1);
215
+ });
216
+ });
@@ -14,6 +14,11 @@ let {
14
14
  textValue = '',
15
15
  numberValue = null,
16
16
  showAge = true,
17
+ webmcp = false,
18
+ textRequired = false,
19
+ ageRequired = false,
20
+ ageMin = undefined,
21
+ ageMax = undefined,
17
22
  }: {
18
23
  onsubmit?: (data: Record<string, unknown>) => void;
19
24
  method?: 'GET' | 'POST';
@@ -21,13 +26,30 @@ let {
21
26
  textValue?: string;
22
27
  numberValue?: number | null;
23
28
  showAge?: boolean;
29
+ webmcp?: boolean;
30
+ textRequired?: boolean;
31
+ ageRequired?: boolean;
32
+ ageMin?: number;
33
+ ageMax?: number;
24
34
  } = $props();
25
35
  </script>
26
36
 
27
- <Form {onsubmit} {method} {action}>
28
- <TextInput name="fullname" label="Full name" bind:value={textValue} />
37
+ <Form {onsubmit} {method} {action} {webmcp}>
38
+ <TextInput
39
+ name="fullname"
40
+ label="Full name"
41
+ required={textRequired}
42
+ bind:value={textValue}
43
+ />
29
44
  {#if showAge}
30
- <NumberInput name="age" label="Age" bind:value={numberValue} />
45
+ <NumberInput
46
+ name="age"
47
+ label="Age"
48
+ required={ageRequired}
49
+ min={ageMin}
50
+ max={ageMax}
51
+ bind:value={numberValue}
52
+ />
31
53
  {/if}
32
54
  <button type="submit">Submit</button>
33
55
  </Form>
@@ -5,6 +5,11 @@ type $$ComponentProps = {
5
5
  textValue?: string;
6
6
  numberValue?: number | null;
7
7
  showAge?: boolean;
8
+ webmcp?: boolean;
9
+ textRequired?: boolean;
10
+ ageRequired?: boolean;
11
+ ageMin?: number;
12
+ ageMax?: number;
8
13
  };
9
14
  declare const FormWithFields: import("svelte").Component<$$ComponentProps, {}, "">;
10
15
  type FormWithFields = ReturnType<typeof FormWithFields>;
@@ -1 +1 @@
1
- {"version":3,"file":"form-with-fields.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/form-with-fields.fixture.svelte.ts"],"names":[],"mappings":"AAWC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA2BF,QAAA,MAAM,cAAc,sDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"form-with-fields.fixture.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/__tests__/form-with-fields.fixture.svelte.ts"],"names":[],"mappings":"AAWC,KAAK,gBAAgB,GAAI;IACxB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACnD,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAgCF,QAAA,MAAM,cAAc,sDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}