@prolibu-suite/cobalt-form 0.2.2 → 2.0.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 (29) hide show
  1. package/dist/cjs/co-form-wizard.cjs.entry.js +40 -11
  2. package/dist/cjs/co-form.cjs.entry.js +1 -1
  3. package/dist/cjs/cobalt-form.cjs.js +1 -1
  4. package/dist/cjs/{index-CLL7Ervz.js → index-Brpri7nS.js} +120 -19
  5. package/dist/cjs/index.cjs.js +146 -5
  6. package/dist/cjs/loader.cjs.js +1 -1
  7. package/dist/cobalt-form/cobalt-form.esm.js +1 -1
  8. package/dist/cobalt-form/index.esm.js +1 -1
  9. package/dist/cobalt-form/p-296094ec.entry.js +1 -0
  10. package/dist/cobalt-form/{p-2168be6b.entry.js → p-95e1cedb.entry.js} +1 -1
  11. package/dist/cobalt-form/{p-CPE0t-C2.js → p-CzvtCEKX.js} +3 -3
  12. package/dist/collection/components/co-form/co-form.css +326 -4
  13. package/dist/collection/components/co-form/co-form.js +167 -4
  14. package/dist/collection/components/co-form-wizard/co-form-wizard.css +5 -0
  15. package/dist/collection/components/co-form-wizard/co-form-wizard.js +92 -10
  16. package/dist/components/co-form-wizard.js +1 -1
  17. package/dist/components/index.js +7 -7
  18. package/dist/esm/co-form-wizard.entry.js +40 -11
  19. package/dist/esm/co-form.entry.js +1 -1
  20. package/dist/esm/cobalt-form.js +1 -1
  21. package/dist/esm/{index-CPE0t-C2.js → index-CzvtCEKX.js} +120 -19
  22. package/dist/esm/index.js +146 -5
  23. package/dist/esm/loader.js +1 -1
  24. package/dist/types/components/co-form/co-form.d.ts +7 -0
  25. package/dist/types/components/co-form-wizard/co-form-wizard.d.ts +15 -1
  26. package/dist/types/components.d.ts +33 -0
  27. package/dist/types/jsx-cobalt.d.ts +3 -0
  28. package/package.json +3 -3
  29. package/dist/cobalt-form/p-0a2a284f.entry.js +0 -1
@@ -68,13 +68,13 @@ form[data-has-bg] {
68
68
  padding: var(--co-spacing-2xl, 24px);
69
69
  background: var(--co-semantic-surface-page, #fff);
70
70
  border-radius: var(--co-border-radius-sm, 12px);
71
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
71
+ box-shadow: var(--co-shadow-lg, 0 4px 24px var(--co-color-opacity-negro-8));
72
72
  }
73
73
 
74
74
  form[data-theme="dark"][data-has-bg] {
75
- background: #232936;
76
- color: #f9fafb;
77
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
75
+ background: var(--co-semantic-surface-page);
76
+ color: var(--co-semantic-text-default);
77
+ box-shadow: 0 4px 24px var(--co-color-opacity-negro-50);
78
78
  }
79
79
 
80
80
  /* Dark mode — re-bind ALL semantic tokens so cards, inputs, and buttons
@@ -189,3 +189,325 @@ form[data-hide-header] .co-form-layout > slot[name="header"] {
189
189
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
190
190
  gap: var(--co-spacing-sm, 8px);
191
191
  }
192
+
193
+ /* Textarea (long answer) */
194
+ .co-form-textarea-wrap {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: var(--co-spacing-xxs, 2px);
198
+ }
199
+
200
+ .co-form-textarea-wrap__label {
201
+ font-size: var(--co-font-size-14, 14px);
202
+ font-weight: 500;
203
+ color: var(--co-semantic-text-default, #111);
204
+ }
205
+
206
+ .co-form-textarea {
207
+ width: 100%;
208
+ min-height: 100px;
209
+ padding: var(--co-spacing-sm, 8px) var(--co-spacing-md, 12px);
210
+ font-family: var(--co-font-family-primary, sans-serif);
211
+ font-size: var(--co-font-size-14, 14px);
212
+ color: var(--co-semantic-text-default, #111);
213
+ background: var(--co-semantic-surface-secondary, #f7f6f0);
214
+ border: 1px solid var(--co-semantic-border-default, #e5e7eb);
215
+ border-radius: var(--co-border-radius-xxs, 4px);
216
+ resize: vertical;
217
+ outline: none;
218
+ transition: border-color 120ms;
219
+ box-sizing: border-box;
220
+ }
221
+
222
+ .co-form-textarea:focus {
223
+ border-color: var(--co-color-primary-azul, #2563eb);
224
+ }
225
+
226
+ .co-form-textarea--error {
227
+ border-color: var(--co-color-status-error, #AA2E1B);
228
+ }
229
+
230
+ .co-form-textarea:disabled {
231
+ opacity: 0.5;
232
+ cursor: not-allowed;
233
+ }
234
+
235
+ .co-form-textarea-wrap__helper {
236
+ margin: 0;
237
+ font-size: var(--co-font-size-12, 12px);
238
+ color: var(--co-semantic-text-secondary, #6b7280);
239
+ }
240
+
241
+ .co-form-textarea-wrap__helper[data-status="error"] {
242
+ color: var(--co-color-status-error, #AA2E1B);
243
+ }
244
+
245
+ /* Number field — constrain width */
246
+ co-number-field {
247
+ max-width: 240px;
248
+ }
249
+
250
+ /* Ordering */
251
+ .co-form-ordering {
252
+ grid-column: 1 / -1;
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: var(--co-spacing-xs, 4px);
256
+ }
257
+
258
+ .co-form-ordering__label {
259
+ font-size: var(--co-font-size-14, 14px);
260
+ font-weight: 500;
261
+ color: var(--co-semantic-text-default, #111);
262
+ margin-bottom: var(--co-spacing-xs, 4px);
263
+ }
264
+
265
+ .co-form-ordering__helper {
266
+ margin: 0 0 var(--co-spacing-xs, 4px);
267
+ font-size: var(--co-font-size-12, 12px);
268
+ color: var(--co-semantic-text-secondary, #6b7280);
269
+ }
270
+
271
+ .co-form-ordering__list {
272
+ display: flex;
273
+ flex-direction: column;
274
+ gap: var(--co-spacing-xs, 4px);
275
+ }
276
+
277
+ .co-form-ordering__item {
278
+ display: flex;
279
+ align-items: center;
280
+ gap: var(--co-spacing-sm, 8px);
281
+ padding: var(--co-spacing-sm, 8px) var(--co-spacing-md, 12px);
282
+ border: 1px solid var(--co-semantic-border-default, #e5e7eb);
283
+ border-radius: var(--co-border-radius-xxs, 4px);
284
+ background: var(--co-semantic-surface-secondary, #f7f6f0);
285
+ transition: border-color 120ms, box-shadow 120ms, opacity 120ms;
286
+ cursor: grab;
287
+ user-select: none;
288
+ }
289
+
290
+ .co-form-ordering__item:hover {
291
+ border-color: var(--co-color-primary-azul, #2563eb);
292
+ }
293
+
294
+ .co-form-ordering__item.is-dragging {
295
+ opacity: 0.4;
296
+ cursor: grabbing;
297
+ }
298
+
299
+ .co-form-ordering__item.is-drop-target {
300
+ border-color: var(--co-color-primary-azul, #2563eb);
301
+ box-shadow: 0 -2px 0 0 var(--co-color-primary-azul, #2563eb);
302
+ }
303
+
304
+ .co-form-ordering__grip {
305
+ display: flex;
306
+ align-items: center;
307
+ color: var(--co-semantic-text-muted, #9ca3af);
308
+ flex-shrink: 0;
309
+ cursor: grab;
310
+ }
311
+
312
+ .co-form-ordering__num {
313
+ display: inline-flex;
314
+ align-items: center;
315
+ justify-content: center;
316
+ width: 24px;
317
+ height: 24px;
318
+ border-radius: var(--co-border-radius-full, 9999px);
319
+ background: color-mix(in srgb, var(--co-color-primary-azul, #2563eb) 10%, transparent);
320
+ color: var(--co-color-primary-azul, #2563eb);
321
+ font-size: 12px;
322
+ font-weight: 700;
323
+ flex-shrink: 0;
324
+ }
325
+
326
+ .co-form-ordering__text {
327
+ flex: 1;
328
+ font-size: var(--co-font-size-14, 14px);
329
+ color: var(--co-semantic-text-default, #111);
330
+ }
331
+
332
+ .co-form-ordering__actions {
333
+ display: flex;
334
+ flex-direction: column;
335
+ gap: 1px;
336
+ flex-shrink: 0;
337
+ }
338
+
339
+ .co-form-ordering__move-btn {
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: center;
343
+ width: 22px;
344
+ height: 16px;
345
+ padding: 0;
346
+ border: none;
347
+ background: transparent;
348
+ color: var(--co-semantic-text-muted, #9ca3af);
349
+ cursor: pointer;
350
+ border-radius: var(--co-border-radius-xxs, 4px);
351
+ }
352
+
353
+ .co-form-ordering__move-btn:hover:not(:disabled) {
354
+ background: var(--co-semantic-surface-hover, #f3f4f6);
355
+ color: var(--co-semantic-text-default, #111);
356
+ }
357
+
358
+ .co-form-ordering__move-btn:disabled {
359
+ opacity: 0.3;
360
+ cursor: not-allowed;
361
+ }
362
+
363
+ /* Matching */
364
+ .co-form-matching {
365
+ grid-column: 1 / -1;
366
+ display: flex;
367
+ flex-direction: column;
368
+ gap: var(--co-spacing-xs, 4px);
369
+ }
370
+
371
+ .co-form-matching__label {
372
+ font-size: var(--co-font-size-14, 14px);
373
+ font-weight: 500;
374
+ color: var(--co-semantic-text-default, #111);
375
+ margin-bottom: var(--co-spacing-xs, 4px);
376
+ }
377
+
378
+ .co-form-matching__helper {
379
+ margin: 0 0 var(--co-spacing-xs, 4px);
380
+ font-size: var(--co-font-size-12, 12px);
381
+ color: var(--co-semantic-text-secondary, #6b7280);
382
+ }
383
+
384
+ .co-form-matching__list {
385
+ display: flex;
386
+ flex-direction: column;
387
+ gap: var(--co-spacing-sm, 8px);
388
+ }
389
+
390
+ .co-form-matching__row {
391
+ display: grid;
392
+ grid-template-columns: 1fr 20px 1fr;
393
+ gap: var(--co-spacing-sm, 8px);
394
+ align-items: center;
395
+ }
396
+
397
+ .co-form-matching__term {
398
+ font-size: var(--co-font-size-14, 14px);
399
+ font-weight: 500;
400
+ color: var(--co-semantic-text-default, #111);
401
+ padding: var(--co-spacing-sm, 8px) var(--co-spacing-md, 12px);
402
+ border: 1px solid var(--co-semantic-border-default, #e5e7eb);
403
+ border-radius: var(--co-border-radius-xxs, 4px);
404
+ background: var(--co-semantic-surface-secondary, #f7f6f0);
405
+ }
406
+
407
+ .co-form-matching__row co-icon {
408
+ color: var(--co-semantic-text-muted, #9ca3af);
409
+ justify-self: center;
410
+ }
411
+
412
+ .co-form-matching__select {
413
+ min-width: 0;
414
+ }
415
+
416
+ /* Image Choice */
417
+ .co-form-image-choice {
418
+ grid-column: 1 / -1;
419
+ display: flex;
420
+ flex-direction: column;
421
+ gap: var(--co-spacing-xs, 4px);
422
+ }
423
+
424
+ .co-form-image-choice__label {
425
+ font-size: var(--co-font-size-14, 14px);
426
+ font-weight: 500;
427
+ color: var(--co-semantic-text-default, #111);
428
+ margin-bottom: var(--co-spacing-xs, 4px);
429
+ }
430
+
431
+ .co-form-image-choice__helper {
432
+ margin: 0 0 var(--co-spacing-xs, 4px);
433
+ font-size: var(--co-font-size-12, 12px);
434
+ color: var(--co-semantic-text-secondary, #6b7280);
435
+ }
436
+
437
+ .co-form-image-choice__helper[data-status="error"] {
438
+ color: var(--co-color-status-error, #AA2E1B);
439
+ }
440
+
441
+ .co-form-image-choice__grid {
442
+ display: grid;
443
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
444
+ gap: var(--co-spacing-sm, 8px);
445
+ }
446
+
447
+ .co-form-image-choice__card {
448
+ display: flex;
449
+ flex-direction: column;
450
+ border: 2px solid var(--co-semantic-border-default, #e5e7eb);
451
+ border-radius: var(--co-border-radius-xs, 8px);
452
+ overflow: hidden;
453
+ cursor: pointer;
454
+ transition: border-color 120ms, box-shadow 120ms;
455
+ background: var(--co-semantic-surface-secondary, #f7f6f0);
456
+ }
457
+
458
+ .co-form-image-choice__card:hover:not(.is-disabled) {
459
+ border-color: var(--co-color-primary-azul, #2563eb);
460
+ }
461
+
462
+ .co-form-image-choice__card.is-selected {
463
+ border-color: var(--co-color-primary-azul, #2563eb);
464
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--co-color-primary-azul, #2563eb) 20%, transparent);
465
+ }
466
+
467
+ .co-form-image-choice__card.is-disabled {
468
+ opacity: 0.5;
469
+ cursor: not-allowed;
470
+ }
471
+
472
+ .co-form-image-choice__img-wrap {
473
+ width: 100%;
474
+ height: 100px;
475
+ display: flex;
476
+ align-items: center;
477
+ justify-content: center;
478
+ overflow: hidden;
479
+ color: var(--co-semantic-text-muted, #9ca3af);
480
+ }
481
+
482
+ .co-form-image-choice__img {
483
+ width: 100%;
484
+ height: 100%;
485
+ object-fit: cover;
486
+ }
487
+
488
+ .co-form-image-choice__footer {
489
+ display: flex;
490
+ align-items: center;
491
+ gap: var(--co-spacing-xs, 4px);
492
+ padding: var(--co-spacing-xs, 4px) var(--co-spacing-sm, 8px);
493
+ border-top: 1px solid var(--co-semantic-border-default, #e5e7eb);
494
+ min-height: 32px;
495
+ }
496
+
497
+ .co-form-image-choice__check {
498
+ flex-shrink: 0;
499
+ display: flex;
500
+ color: var(--co-semantic-text-muted, #9ca3af);
501
+ }
502
+
503
+ .co-form-image-choice__card.is-selected .co-form-image-choice__check {
504
+ color: var(--co-color-primary-azul, #2563eb);
505
+ }
506
+
507
+ .co-form-image-choice__text {
508
+ font-size: var(--co-font-size-13, 13px);
509
+ color: var(--co-semantic-text-default, #111);
510
+ overflow: hidden;
511
+ text-overflow: ellipsis;
512
+ white-space: nowrap;
513
+ }
@@ -53,6 +53,8 @@ export class CoForm {
53
53
  this.footerAlign = 'end';
54
54
  /** Hide the entire footer (use when embedded in a wizard or custom shell). */
55
55
  this.hideFooter = false;
56
+ /** When to run validation: on every change, on blur, or only on submit. */
57
+ this.validateOn = 'change';
56
58
  this.snapshot = {
57
59
  values: {},
58
60
  errors: {},
@@ -128,6 +130,9 @@ export class CoForm {
128
130
  }
129
131
  this.handleFieldChange(f.name, value);
130
132
  };
133
+ // ── Ordering (kind: 'ordering') ───────────────────────────────────────────
134
+ this.orderingDragIdx = null;
135
+ this.orderingDropIdx = null;
131
136
  }
132
137
  componentWillLoad() {
133
138
  this.rebuildController();
@@ -195,6 +200,7 @@ export class CoForm {
195
200
  this.controller = createForm({
196
201
  schema: { instance },
197
202
  initialValues,
203
+ validateOn: this.validateOn,
198
204
  });
199
205
  this.fields = describeFields(instance.formSchema.properties);
200
206
  this.snapshot = this.controller.state;
@@ -455,8 +461,14 @@ export class CoForm {
455
461
  switch (f.kind) {
456
462
  case 'text':
457
463
  return (h("co-input", Object.assign({}, common, { value: value !== null && value !== void 0 ? value : '', onCoInput: (e) => this.handleFieldChange(f.name, e.detail) })));
458
- case 'textarea':
459
- return (h("co-input", Object.assign({}, common, { multiline: true, value: value !== null && value !== void 0 ? value : '', onCoInput: (e) => this.handleFieldChange(f.name, e.detail) })));
464
+ case 'textarea': {
465
+ const textareaId = `co-form-${f.name}`;
466
+ const helperId = helperText ? `co-form-${f.name}-helper` : undefined;
467
+ return (h("div", { class: "co-form-textarea-wrap", "data-full": f.originalAttrs.full ? 'true' : undefined }, f.label && h("label", { class: "co-form-textarea-wrap__label", htmlFor: textareaId }, f.label), h("textarea", { id: textareaId, class: {
468
+ 'co-form-textarea': true,
469
+ 'co-form-textarea--error': status === 'error',
470
+ }, name: f.name, placeholder: f.placeholder, disabled: disabled, required: required, rows: 4, "aria-invalid": status === 'error' ? 'true' : undefined, "aria-describedby": helperId, value: value !== null && value !== void 0 ? value : '', onInput: (e) => this.handleFieldChange(f.name, e.target.value) }), helperText && h("p", { id: helperId, class: "co-form-textarea-wrap__helper", "data-status": status }, helperText)));
471
+ }
460
472
  case 'html':
461
473
  return (h("co-input", Object.assign({}, common, { multiline: true, value: value !== null && value !== void 0 ? value : '', onCoInput: (e) => this.handleFieldChange(f.name, e.detail) })));
462
474
  case 'date':
@@ -467,16 +479,28 @@ export class CoForm {
467
479
  return (h("co-switch", Object.assign({}, common, { checked: !!value, onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
468
480
  case 'select':
469
481
  return (h("co-select", Object.assign({}, common, { options: JSON.stringify(this.optionsFor(f)), value: String(value !== null && value !== void 0 ? value : ''), onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
482
+ case 'radio':
483
+ return (h("co-radio-group", Object.assign({}, common, { options: JSON.stringify(this.optionsFor(f)), value: String(value !== null && value !== void 0 ? value : ''), onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
484
+ case 'checkbox':
485
+ return (h("co-checkbox-group", Object.assign({}, common, { options: JSON.stringify(this.optionsFor(f)), value: JSON.stringify(Array.isArray(value) ? value : []), onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
470
486
  case 'rating':
471
487
  return (h("co-rating", Object.assign({}, common, { value: typeof value === 'number' ? value : 0, max: Number((_c = f.originalAttrs.max) !== null && _c !== void 0 ? _c : 5), onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
472
488
  case 'slider':
473
489
  return (h("co-slider", Object.assign({}, common, { value: typeof value === 'number' ? value : Number((_d = f.originalAttrs.min) !== null && _d !== void 0 ? _d : 0), min: Number((_e = f.originalAttrs.min) !== null && _e !== void 0 ? _e : 0), max: Number((_f = f.originalAttrs.max) !== null && _f !== void 0 ? _f : 100), step: Number((_g = f.originalAttrs.step) !== null && _g !== void 0 ? _g : 1), onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
474
490
  case 'color':
475
- return (h("co-input", Object.assign({}, common, { type: "color", value: typeof value === 'string' ? value : '#000000', onCoInput: (e) => this.handleFieldChange(f.name, e.detail) })));
491
+ return (h("co-color-picker", Object.assign({}, common, { value: typeof value === 'string' ? value : '', presets: f.originalAttrs.presets ? JSON.stringify(f.originalAttrs.presets) : undefined, onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
476
492
  case 'section':
477
493
  return (h("div", { class: "co-form-section", key: f.name }, h("h3", { class: "co-form-section__title" }, f.label), f.helperText && h("p", { class: "co-form-section__desc" }, f.helperText)));
494
+ case 'phone':
495
+ return (h("co-phone-field", Object.assign({}, common, { value: typeof value === 'string' ? value : '', defaultCountry: f.originalAttrs.defaultCountry || '', onCoChange: (e) => this.handleFieldChange(f.name, e.detail) })));
478
496
  case 'file':
479
497
  return (h("co-input", Object.assign({}, common, { type: "file", onCoInput: (e) => this.handleFieldChange(f.name, e.detail) })));
498
+ case 'ordering':
499
+ return this.renderOrdering(f, value);
500
+ case 'matching':
501
+ return this.renderMatching(f, value);
502
+ case 'image-choice':
503
+ return this.renderImageChoice(f, value);
480
504
  case 'array':
481
505
  return this.renderRepeater(f, value);
482
506
  case 'ref': {
@@ -529,6 +553,123 @@ export class CoForm {
529
553
  return (h("co-input", { key: n, label: ((_a = itemSchema[n]) === null || _a === void 0 ? void 0 : _a.label) || n, value: (_b = row[n]) !== null && _b !== void 0 ? _b : '', disabled: this.disabled || f.disabled, onCoInput: (e) => patchRow(idx, n, e.detail) }));
530
554
  })), h("co-icon-button", { icon: "trash", variant: "ghost", onClick: () => removeRow(idx), disabled: this.disabled || f.disabled })))), h("co-button", { label: "Agregar", variant: "outlined", "icon-left": "plus", onClick: addRow, disabled: this.disabled || f.disabled })));
531
555
  }
556
+ // ── Image Choice (kind: 'image-choice') ────────────────────────────────
557
+ renderImageChoice(f, value) {
558
+ const opts = Array.isArray(f.originalAttrs.enum) ? f.originalAttrs.enum : [];
559
+ const images = Array.isArray(f.originalAttrs.optionImages) ? f.originalAttrs.optionImages : [];
560
+ const isMulti = !!f.originalAttrs.multiSelect;
561
+ const isDisabled = this.disabled || f.disabled;
562
+ const isTouched = !!this.snapshot.touched[f.name];
563
+ const errors = isTouched ? this.snapshot.errors[f.name] || [] : [];
564
+ const helperText = errors[0] || f.helperText;
565
+ // For single select: value is a string (option text or index).
566
+ // For multi select: value is an array.
567
+ const selected = isMulti
568
+ ? (Array.isArray(value) ? value : [])
569
+ : (value != null && value !== '' ? [String(value)] : []);
570
+ const toggle = (opt) => {
571
+ if (isDisabled)
572
+ return;
573
+ if (isMulti) {
574
+ const next = selected.includes(opt)
575
+ ? selected.filter((s) => s !== opt)
576
+ : [...selected, opt];
577
+ this.handleFieldChange(f.name, next);
578
+ }
579
+ else {
580
+ this.handleFieldChange(f.name, opt);
581
+ }
582
+ };
583
+ return (h("div", { class: "co-form-image-choice", key: f.name, "data-full": f.originalAttrs.full ? 'true' : undefined }, f.label && h("label", { class: "co-form-image-choice__label" }, f.label), helperText && h("p", { class: "co-form-image-choice__helper", "data-status": errors.length ? 'error' : undefined }, helperText), h("div", { class: "co-form-image-choice__grid", role: isMulti ? 'group' : 'radiogroup', "aria-label": f.label || f.name }, opts.map((opt, idx) => {
584
+ const isSelected = selected.includes(opt);
585
+ const imgSrc = images[idx] || '';
586
+ return (h("div", { class: `co-form-image-choice__card ${isSelected ? 'is-selected' : ''} ${isDisabled ? 'is-disabled' : ''}`, key: `${f.name}-${idx}`, onClick: () => toggle(opt), onKeyDown: (e) => {
587
+ if (e.key === 'Enter' || e.key === ' ') {
588
+ e.preventDefault();
589
+ toggle(opt);
590
+ }
591
+ }, role: isMulti ? 'checkbox' : 'radio', "aria-checked": isSelected ? 'true' : 'false', "aria-disabled": isDisabled ? 'true' : undefined, tabIndex: isDisabled ? -1 : 0 }, h("div", { class: "co-form-image-choice__img-wrap" }, imgSrc
592
+ ? h("img", { src: imgSrc, alt: opt, class: "co-form-image-choice__img" })
593
+ : h("co-icon", { name: "image-square", size: "32" })), h("div", { class: "co-form-image-choice__footer" }, h("span", { class: "co-form-image-choice__check" }, isSelected && h("co-icon", { name: "check-circle", size: "18", weight: "fill" }), !isSelected && h("co-icon", { name: "circle", size: "18" })), opt && h("span", { class: "co-form-image-choice__text" }, opt))));
594
+ }))));
595
+ }
596
+ renderOrdering(f, value) {
597
+ const items = Array.isArray(f.originalAttrs.enum) ? f.originalAttrs.enum : [];
598
+ // Value is the user's ordering: an array of item strings in the order they chose.
599
+ const ordered = Array.isArray(value) ? value : [...items];
600
+ const isDisabled = this.disabled || f.disabled;
601
+ const isTouched = !!this.snapshot.touched[f.name];
602
+ const errors = isTouched ? this.snapshot.errors[f.name] || [] : [];
603
+ const helperText = errors[0] || f.helperText;
604
+ const onDragStart = (ev, idx) => {
605
+ if (isDisabled)
606
+ return;
607
+ this.orderingDragIdx = idx;
608
+ if (ev.dataTransfer) {
609
+ ev.dataTransfer.effectAllowed = 'move';
610
+ ev.dataTransfer.setData('text/plain', String(idx));
611
+ }
612
+ };
613
+ const onDragOver = (ev, idx) => {
614
+ if (this.orderingDragIdx === null)
615
+ return;
616
+ ev.preventDefault();
617
+ if (ev.dataTransfer)
618
+ ev.dataTransfer.dropEffect = 'move';
619
+ this.orderingDropIdx = idx;
620
+ };
621
+ const onDrop = (ev, idx) => {
622
+ ev.preventDefault();
623
+ const fromIdx = this.orderingDragIdx;
624
+ this.orderingDragIdx = null;
625
+ this.orderingDropIdx = null;
626
+ if (fromIdx === null || fromIdx === idx)
627
+ return;
628
+ const next = [...ordered];
629
+ const [moved] = next.splice(fromIdx, 1);
630
+ // Adjust target for downward moves: removing fromIdx shifts later indices down by 1
631
+ const insertAt = fromIdx < idx ? idx - 1 : idx;
632
+ next.splice(insertAt, 0, moved);
633
+ this.handleFieldChange(f.name, next);
634
+ };
635
+ const onDragEnd = () => {
636
+ this.orderingDragIdx = null;
637
+ this.orderingDropIdx = null;
638
+ };
639
+ const moveItem = (fromIdx, toIdx) => {
640
+ if (isDisabled || toIdx < 0 || toIdx >= ordered.length)
641
+ return;
642
+ const next = [...ordered];
643
+ const [moved] = next.splice(fromIdx, 1);
644
+ next.splice(toIdx, 0, moved);
645
+ this.handleFieldChange(f.name, next);
646
+ };
647
+ return (h("div", { class: "co-form-ordering", key: f.name, "data-full": f.originalAttrs.full ? 'true' : undefined }, f.label && h("label", { class: "co-form-ordering__label" }, f.label), helperText && h("p", { class: "co-form-ordering__helper" }, helperText), h("div", { class: "co-form-ordering__list", role: "list", "aria-label": f.label }, ordered.map((item, idx) => (h("div", { class: {
648
+ 'co-form-ordering__item': true,
649
+ 'is-dragging': this.orderingDragIdx === idx,
650
+ 'is-drop-target': this.orderingDropIdx === idx && this.orderingDragIdx !== idx,
651
+ }, key: `${f.name}-${idx}`, role: "listitem", draggable: !isDisabled, onDragStart: (ev) => onDragStart(ev, idx), onDragOver: (ev) => onDragOver(ev, idx), onDrop: (ev) => onDrop(ev, idx), onDragEnd: onDragEnd }, h("span", { class: "co-form-ordering__grip", "aria-hidden": "true" }, h("co-icon", { name: "dots-six-vertical", size: "16" })), h("span", { class: "co-form-ordering__num" }, idx + 1), h("span", { class: "co-form-ordering__text" }, item), h("span", { class: "co-form-ordering__actions" }, h("button", { type: "button", class: "co-form-ordering__move-btn", disabled: isDisabled || idx === 0, onClick: () => moveItem(idx, idx - 1), "aria-label": `Move ${item} up` }, h("co-icon", { name: "caret-up", size: "14" })), h("button", { type: "button", class: "co-form-ordering__move-btn", disabled: isDisabled || idx === ordered.length - 1, onClick: () => moveItem(idx, idx + 1), "aria-label": `Move ${item} down` }, h("co-icon", { name: "caret-down", size: "14" })))))))));
652
+ }
653
+ // ── Matching (kind: 'matching') ───────────────────────────────────────────
654
+ renderMatching(f, value) {
655
+ const leftItems = Array.isArray(f.originalAttrs.matchLeft) ? f.originalAttrs.matchLeft : [];
656
+ const rightItems = Array.isArray(f.originalAttrs.matchRight) ? f.originalAttrs.matchRight : [];
657
+ // Value is an object mapping left index → selected right value string.
658
+ const pairs = value && typeof value === 'object' ? value : {};
659
+ const isDisabled = this.disabled || f.disabled;
660
+ const isTouched = !!this.snapshot.touched[f.name];
661
+ const errors = isTouched ? this.snapshot.errors[f.name] || [] : [];
662
+ const helperText = errors[0] || f.helperText;
663
+ const rightOptions = rightItems.map((r) => ({ label: r, value: r }));
664
+ const handleSelect = (leftIdx, selectedValue) => {
665
+ const next = Object.assign(Object.assign({}, pairs), { [String(leftIdx)]: selectedValue });
666
+ this.handleFieldChange(f.name, next);
667
+ };
668
+ return (h("div", { class: "co-form-matching", key: f.name, "data-full": f.originalAttrs.full ? 'true' : undefined }, f.label && h("label", { class: "co-form-matching__label" }, f.label), helperText && h("p", { class: "co-form-matching__helper" }, helperText), h("div", { class: "co-form-matching__list" }, leftItems.map((term, idx) => {
669
+ var _a;
670
+ return (h("div", { class: "co-form-matching__row", key: `${f.name}-${idx}` }, h("span", { class: "co-form-matching__term" }, term), h("co-icon", { name: "arrow-right", size: "16" }), h("co-select", { class: "co-form-matching__select", options: JSON.stringify(rightOptions), value: (_a = pairs[String(idx)]) !== null && _a !== void 0 ? _a : '', placeholder: "Select match\u2026", disabled: isDisabled, onCoChange: (e) => handleSelect(idx, e.detail) })));
671
+ }))));
672
+ }
532
673
  static get is() { return "co-form"; }
533
674
  static get originalStyleUrls() {
534
675
  return {
@@ -814,6 +955,26 @@ export class CoForm {
814
955
  "attribute": "hide-footer",
815
956
  "defaultValue": "false"
816
957
  },
958
+ "validateOn": {
959
+ "type": "string",
960
+ "mutable": false,
961
+ "complexType": {
962
+ "original": "'change' | 'blur' | 'submit'",
963
+ "resolved": "\"blur\" | \"change\" | \"submit\"",
964
+ "references": {}
965
+ },
966
+ "required": false,
967
+ "optional": false,
968
+ "docs": {
969
+ "tags": [],
970
+ "text": "When to run validation: on every change, on blur, or only on submit."
971
+ },
972
+ "getter": false,
973
+ "setter": false,
974
+ "reflect": false,
975
+ "attribute": "validate-on",
976
+ "defaultValue": "'change'"
977
+ },
817
978
  "theme": {
818
979
  "type": "string",
819
980
  "mutable": false,
@@ -847,7 +1008,9 @@ export class CoForm {
847
1008
  "snapshot": {},
848
1009
  "refStates": {},
849
1010
  "themeStyles": {},
850
- "hasBg": {}
1011
+ "hasBg": {},
1012
+ "orderingDragIdx": {},
1013
+ "orderingDropIdx": {}
851
1014
  };
852
1015
  }
853
1016
  static get events() {
@@ -190,6 +190,11 @@
190
190
  background: var(--co-semantic-surface-hover, #f9fafb);
191
191
  }
192
192
 
193
+ /* When only the submit/next button is present, push it right */
194
+ .co-form-wizard__nav co-button:only-child {
195
+ margin-left: auto;
196
+ }
197
+
193
198
  .co-form-wizard__empty {
194
199
  padding: var(--co-spacing-lg, 16px);
195
200
  text-align: center;