@konomi-app/ui 5.2.1 → 5.3.2

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.
package/dist/index.cjs CHANGED
@@ -60,14 +60,168 @@ var createInitialState = () => ({
60
60
  queues: [],
61
61
  steps: [],
62
62
  timer: null,
63
- title: ""
63
+ title: "",
64
+ formFields: [],
65
+ formValues: {},
66
+ formErrors: {},
67
+ formTouched: {},
68
+ formLayout: {},
69
+ formValidateOnChange: true,
70
+ formValidateOnBlur: true,
71
+ stepFormSteps: [],
72
+ stepFormCurrentIndex: 0,
73
+ stepFormNextText: "\u6B21\u3078",
74
+ stepFormPrevText: "\u623B\u308B",
75
+ stepFormSubmitText: "OK"
64
76
  });
65
77
 
78
+ // src/zod-utils.ts
79
+ function resolveTypeName(field) {
80
+ const raw = field._def.typeName ?? field._def.type;
81
+ if (!raw) return "";
82
+ const v4Map = {
83
+ string: "ZodString",
84
+ number: "ZodNumber",
85
+ boolean: "ZodBoolean",
86
+ enum: "ZodEnum",
87
+ date: "ZodDate",
88
+ optional: "ZodOptional",
89
+ nullable: "ZodNullable",
90
+ default: "ZodDefault",
91
+ object: "ZodObject",
92
+ pipe: "ZodPipe"
93
+ };
94
+ return v4Map[raw] ?? raw;
95
+ }
96
+ function unwrapType(zodType) {
97
+ let inner = zodType;
98
+ let required = true;
99
+ let defaultValue = void 0;
100
+ let description = zodType.description ?? "";
101
+ while (true) {
102
+ const typeName = resolveTypeName(inner);
103
+ if (typeName === "ZodOptional" || typeName === "ZodNullable") {
104
+ required = false;
105
+ inner = inner._def.innerType;
106
+ } else if (typeName === "ZodDefault") {
107
+ const raw = inner._def.defaultValue;
108
+ defaultValue = typeof raw === "function" ? raw() : raw;
109
+ inner = inner._def.innerType;
110
+ } else if (typeName === "ZodEffects") {
111
+ inner = inner._def.schema;
112
+ } else {
113
+ break;
114
+ }
115
+ if (!description && inner.description) {
116
+ description = inner.description;
117
+ }
118
+ }
119
+ return { inner, required, defaultValue, description };
120
+ }
121
+ function extractFieldMeta(key, zodType) {
122
+ const { inner, required, defaultValue, description } = unwrapType(zodType);
123
+ const typeName = resolveTypeName(inner);
124
+ let inputType;
125
+ let options = [];
126
+ let min;
127
+ let max;
128
+ let minLength;
129
+ let maxLength;
130
+ switch (typeName) {
131
+ case "ZodString": {
132
+ inputType = "text";
133
+ if (inner.format === "email") inputType = "email";
134
+ else if (inner.format === "url") inputType = "url";
135
+ if (inner.minLength != null) minLength = inner.minLength;
136
+ if (inner.maxLength != null) maxLength = inner.maxLength;
137
+ const checks = inner._def.checks ?? [];
138
+ for (const check of checks) {
139
+ const kind = check.kind ?? check.def?.check;
140
+ const fmt = check.format ?? check.def?.format;
141
+ if (!fmt && (kind === "email" || fmt === "email")) inputType = "email";
142
+ else if (!fmt && (kind === "url" || fmt === "url")) inputType = "url";
143
+ else if (fmt === "email" && inputType === "text") inputType = "email";
144
+ else if (fmt === "url" && inputType === "text") inputType = "url";
145
+ if (kind === "min" && check.value != null && minLength == null) minLength = check.value;
146
+ if (kind === "max" && check.value != null && maxLength == null) maxLength = check.value;
147
+ }
148
+ break;
149
+ }
150
+ case "ZodNumber": {
151
+ inputType = "number";
152
+ if (inner.minValue != null) min = inner.minValue;
153
+ if (inner.maxValue != null) max = inner.maxValue;
154
+ if (min == null || max == null) {
155
+ const checks = inner._def.checks ?? [];
156
+ for (const check of checks) {
157
+ if (check.kind === "min" && check.value != null && min == null) min = check.value;
158
+ if (check.kind === "max" && check.value != null && max == null) max = check.value;
159
+ }
160
+ }
161
+ break;
162
+ }
163
+ case "ZodBoolean":
164
+ inputType = "checkbox";
165
+ break;
166
+ case "ZodEnum": {
167
+ inputType = "select";
168
+ if (inner.options?.length) {
169
+ options = [...inner.options];
170
+ } else if (inner._def.entries) {
171
+ options = Object.values(inner._def.entries);
172
+ } else if (inner._def.values?.length) {
173
+ options = [...inner._def.values];
174
+ }
175
+ break;
176
+ }
177
+ case "ZodNativeEnum": {
178
+ inputType = "select";
179
+ const enumValues = inner._def.values;
180
+ if (enumValues) {
181
+ options = Object.values(enumValues).filter((v) => typeof v === "string");
182
+ }
183
+ break;
184
+ }
185
+ case "ZodDate":
186
+ inputType = "date";
187
+ break;
188
+ default:
189
+ return null;
190
+ }
191
+ const label = description || key;
192
+ return {
193
+ key,
194
+ inputType,
195
+ label,
196
+ description: description && description !== label ? description : "",
197
+ required,
198
+ options,
199
+ placeholder: "",
200
+ min,
201
+ max,
202
+ minLength,
203
+ maxLength,
204
+ defaultValue
205
+ };
206
+ }
207
+ function extractFormFields(schema) {
208
+ const shapeDef = schema._def.shape;
209
+ if (!shapeDef) return [];
210
+ const shape = typeof shapeDef === "function" ? shapeDef() : shapeDef;
211
+ if (!shape) return [];
212
+ const fields = [];
213
+ for (const [key, zodType] of Object.entries(shape)) {
214
+ const meta = extractFieldMeta(key, zodType);
215
+ if (meta) fields.push(meta);
216
+ }
217
+ return fields;
218
+ }
219
+
66
220
  // src/controller.ts
67
221
  function normalizeItemInput(input) {
68
222
  return typeof input === "string" ? { key: input, label: input } : input;
69
223
  }
70
- var _state, _listeners, _resolver, _timerId, _DialogController_instances, emit_fn, update_fn, createPromise_fn, resolve_fn, clearTimer_fn, updateItemStatus_fn;
224
+ var _state, _listeners, _resolver, _timerId, _formSchema, _formResult, _stepFormSchemas, _stepFormResults, _DialogController_instances, emit_fn, update_fn, validateFormField_fn, updateCurrentStep_fn, validateStepField_fn, createPromise_fn, resolve_fn, clearTimer_fn, updateItemStatus_fn;
71
225
  var DialogController = class {
72
226
  constructor() {
73
227
  __privateAdd(this, _DialogController_instances);
@@ -75,6 +229,10 @@ var DialogController = class {
75
229
  __privateAdd(this, _listeners, /* @__PURE__ */ new Set());
76
230
  __privateAdd(this, _resolver, null);
77
231
  __privateAdd(this, _timerId, null);
232
+ __privateAdd(this, _formSchema, null);
233
+ __privateAdd(this, _formResult, null);
234
+ __privateAdd(this, _stepFormSchemas, []);
235
+ __privateAdd(this, _stepFormResults, {});
78
236
  __privateSet(this, _state, createInitialState());
79
237
  }
80
238
  // ─── Observable ──────────────────────────────────────────
@@ -220,9 +378,202 @@ var DialogController = class {
220
378
  clearSteps() {
221
379
  __privateMethod(this, _DialogController_instances, update_fn).call(this, { steps: [] });
222
380
  }
381
+ // ─── Form ─────────────────────────────────────────────────
382
+ form(schema, options) {
383
+ __privateMethod(this, _DialogController_instances, clearTimer_fn).call(this);
384
+ __privateSet(this, _formSchema, schema);
385
+ __privateSet(this, _formResult, null);
386
+ const fields = extractFormFields(schema);
387
+ const defaultValues = {};
388
+ for (const field of fields) {
389
+ if (field.defaultValue !== void 0) {
390
+ defaultValues[field.key] = field.defaultValue;
391
+ }
392
+ }
393
+ if (options?.defaultValues) {
394
+ Object.assign(defaultValues, options.defaultValues);
395
+ }
396
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, {
397
+ open: true,
398
+ dialogType: "form",
399
+ title: options?.title ?? "",
400
+ label: "",
401
+ description: options?.description ?? "",
402
+ icon: null,
403
+ showConfirmButton: true,
404
+ showCancelButton: true,
405
+ confirmButtonText: options?.confirmButtonText ?? "OK",
406
+ cancelButtonText: options?.cancelButtonText ?? "\u30AD\u30E3\u30F3\u30BB\u30EB",
407
+ allowOutsideClick: options?.allowOutsideClick ?? false,
408
+ allowEscapeKey: options?.allowEscapeKey ?? true,
409
+ progress: null,
410
+ timer: null,
411
+ formFields: fields,
412
+ formValues: defaultValues,
413
+ formErrors: {},
414
+ formTouched: {},
415
+ formLayout: options?.layout ?? {},
416
+ formValidateOnChange: options?.validateOnChange ?? true,
417
+ formValidateOnBlur: options?.validateOnBlur ?? true
418
+ });
419
+ return __privateMethod(this, _DialogController_instances, createPromise_fn).call(this, null).then((r) => {
420
+ const data = __privateGet(this, _formResult);
421
+ __privateSet(this, _formSchema, null);
422
+ __privateSet(this, _formResult, null);
423
+ return r.isConfirmed ? data : null;
424
+ });
425
+ }
426
+ updateFormField(key, value) {
427
+ const formValues = { ...__privateGet(this, _state).formValues, [key]: value };
428
+ const formTouched = { ...__privateGet(this, _state).formTouched, [key]: true };
429
+ let formErrors = { ...__privateGet(this, _state).formErrors };
430
+ if (__privateGet(this, _formSchema) && __privateGet(this, _state).formValidateOnChange) {
431
+ formErrors = __privateMethod(this, _DialogController_instances, validateFormField_fn).call(this, key, formValues, formErrors);
432
+ }
433
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { formValues, formTouched, formErrors });
434
+ }
435
+ touchFormField(key) {
436
+ const formTouched = { ...__privateGet(this, _state).formTouched, [key]: true };
437
+ let formErrors = { ...__privateGet(this, _state).formErrors };
438
+ if (__privateGet(this, _formSchema) && __privateGet(this, _state).formValidateOnBlur) {
439
+ formErrors = __privateMethod(this, _DialogController_instances, validateFormField_fn).call(this, key, __privateGet(this, _state).formValues, formErrors);
440
+ }
441
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { formTouched, formErrors });
442
+ }
443
+ // ─── Step Form ───────────────────────────────────────────
444
+ showStepForm(steps, options) {
445
+ __privateMethod(this, _DialogController_instances, clearTimer_fn).call(this);
446
+ __privateSet(this, _stepFormSchemas, steps.map((s) => s.schema ?? null));
447
+ __privateSet(this, _stepFormResults, {});
448
+ const stepFormSteps = steps.map((s) => {
449
+ const fields = s.schema ? extractFormFields(s.schema) : [];
450
+ const values = {};
451
+ for (const f of fields) {
452
+ if (f.defaultValue !== void 0) values[f.key] = f.defaultValue;
453
+ }
454
+ if (s.defaultValues) Object.assign(values, s.defaultValues);
455
+ return {
456
+ key: s.key,
457
+ label: s.label,
458
+ description: s.description ?? "",
459
+ fields,
460
+ values,
461
+ errors: {},
462
+ touched: {},
463
+ layout: s.layout ?? {}
464
+ };
465
+ });
466
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, {
467
+ open: true,
468
+ dialogType: "step-form",
469
+ title: options?.title ?? "",
470
+ label: "",
471
+ description: "",
472
+ icon: null,
473
+ showConfirmButton: false,
474
+ showCancelButton: false,
475
+ allowOutsideClick: options?.allowOutsideClick ?? false,
476
+ allowEscapeKey: options?.allowEscapeKey ?? true,
477
+ progress: null,
478
+ timer: null,
479
+ stepFormSteps,
480
+ stepFormCurrentIndex: 0,
481
+ stepFormNextText: options?.nextButtonText ?? "\u6B21\u3078",
482
+ stepFormPrevText: options?.prevButtonText ?? "\u623B\u308B",
483
+ stepFormSubmitText: options?.submitButtonText ?? "OK",
484
+ cancelButtonText: options?.cancelButtonText ?? "\u30AD\u30E3\u30F3\u30BB\u30EB"
485
+ });
486
+ return __privateMethod(this, _DialogController_instances, createPromise_fn).call(this, null).then((r) => {
487
+ const data = __privateGet(this, _stepFormResults);
488
+ __privateSet(this, _stepFormSchemas, []);
489
+ __privateSet(this, _stepFormResults, {});
490
+ return r.isConfirmed ? data : null;
491
+ });
492
+ }
493
+ onStepNext() {
494
+ const s = __privateGet(this, _state);
495
+ const idx = s.stepFormCurrentIndex;
496
+ const step = s.stepFormSteps[idx];
497
+ if (!step) return;
498
+ const schema = __privateGet(this, _stepFormSchemas)[idx];
499
+ if (schema) {
500
+ const result = schema.safeParse(step.values);
501
+ if (!result.success) {
502
+ const errors = {};
503
+ for (const issue of result.error.issues) {
504
+ const key = issue.path[0]?.toString();
505
+ if (key && !errors[key]) errors[key] = issue.message;
506
+ }
507
+ const touched = {};
508
+ for (const f of step.fields) touched[f.key] = true;
509
+ __privateMethod(this, _DialogController_instances, updateCurrentStep_fn).call(this, { errors, touched });
510
+ return;
511
+ }
512
+ __privateGet(this, _stepFormResults)[step.key] = result.data;
513
+ }
514
+ const isLast = idx === s.stepFormSteps.length - 1;
515
+ if (isLast) {
516
+ const r = { isConfirmed: true, isCanceled: false, isDismissed: false };
517
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { ...createInitialState(), open: false });
518
+ __privateMethod(this, _DialogController_instances, resolve_fn).call(this, r);
519
+ return;
520
+ }
521
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { stepFormCurrentIndex: idx + 1 });
522
+ }
523
+ onStepPrev() {
524
+ const idx = __privateGet(this, _state).stepFormCurrentIndex;
525
+ if (idx <= 0) return;
526
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { stepFormCurrentIndex: idx - 1 });
527
+ }
528
+ updateStepFormField(fieldKey, value) {
529
+ const s = __privateGet(this, _state);
530
+ const idx = s.stepFormCurrentIndex;
531
+ const step = s.stepFormSteps[idx];
532
+ if (!step) return;
533
+ const values = { ...step.values, [fieldKey]: value };
534
+ const touched = { ...step.touched, [fieldKey]: true };
535
+ let errors = { ...step.errors };
536
+ const schema = __privateGet(this, _stepFormSchemas)[idx];
537
+ if (schema && s.formValidateOnChange) {
538
+ errors = __privateMethod(this, _DialogController_instances, validateStepField_fn).call(this, schema, fieldKey, values, errors);
539
+ }
540
+ __privateMethod(this, _DialogController_instances, updateCurrentStep_fn).call(this, { values, touched, errors });
541
+ }
542
+ touchStepFormField(fieldKey) {
543
+ const s = __privateGet(this, _state);
544
+ const idx = s.stepFormCurrentIndex;
545
+ const step = s.stepFormSteps[idx];
546
+ if (!step) return;
547
+ const touched = { ...step.touched, [fieldKey]: true };
548
+ let errors = { ...step.errors };
549
+ const schema = __privateGet(this, _stepFormSchemas)[idx];
550
+ if (schema && s.formValidateOnBlur) {
551
+ errors = __privateMethod(this, _DialogController_instances, validateStepField_fn).call(this, schema, fieldKey, step.values, errors);
552
+ }
553
+ __privateMethod(this, _DialogController_instances, updateCurrentStep_fn).call(this, { touched, errors });
554
+ }
223
555
  // ─── Button actions (called from the component) ──────────
224
556
  onConfirm() {
225
557
  __privateMethod(this, _DialogController_instances, clearTimer_fn).call(this);
558
+ if (__privateGet(this, _state).dialogType === "form" && __privateGet(this, _formSchema)) {
559
+ const result = __privateGet(this, _formSchema).safeParse(__privateGet(this, _state).formValues);
560
+ if (!result.success) {
561
+ const formErrors = {};
562
+ for (const issue of result.error.issues) {
563
+ const key = issue.path[0]?.toString();
564
+ if (key && !formErrors[key]) {
565
+ formErrors[key] = issue.message;
566
+ }
567
+ }
568
+ const formTouched = {};
569
+ for (const field of __privateGet(this, _state).formFields) {
570
+ formTouched[field.key] = true;
571
+ }
572
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { formErrors, formTouched });
573
+ return;
574
+ }
575
+ __privateSet(this, _formResult, result.data);
576
+ }
226
577
  const r = { isConfirmed: true, isCanceled: false, isDismissed: false };
227
578
  __privateMethod(this, _DialogController_instances, update_fn).call(this, { ...createInitialState(), open: false });
228
579
  __privateMethod(this, _DialogController_instances, resolve_fn).call(this, r);
@@ -246,6 +597,10 @@ _state = new WeakMap();
246
597
  _listeners = new WeakMap();
247
598
  _resolver = new WeakMap();
248
599
  _timerId = new WeakMap();
600
+ _formSchema = new WeakMap();
601
+ _formResult = new WeakMap();
602
+ _stepFormSchemas = new WeakMap();
603
+ _stepFormResults = new WeakMap();
249
604
  _DialogController_instances = new WeakSet();
250
605
  emit_fn = function() {
251
606
  const snapshot = { ...__privateGet(this, _state) };
@@ -255,6 +610,47 @@ update_fn = function(patch) {
255
610
  Object.assign(__privateGet(this, _state), patch);
256
611
  __privateMethod(this, _DialogController_instances, emit_fn).call(this);
257
612
  };
613
+ validateFormField_fn = function(key, values, errors) {
614
+ const result = __privateGet(this, _formSchema).safeParse(values);
615
+ const updated = { ...errors };
616
+ if (result.success) {
617
+ delete updated[key];
618
+ } else {
619
+ const fieldIssue = result.error.issues.find(
620
+ (issue) => issue.path[0]?.toString() === key
621
+ );
622
+ if (fieldIssue) {
623
+ updated[key] = fieldIssue.message;
624
+ } else {
625
+ delete updated[key];
626
+ }
627
+ }
628
+ return updated;
629
+ };
630
+ updateCurrentStep_fn = function(patch) {
631
+ const idx = __privateGet(this, _state).stepFormCurrentIndex;
632
+ const stepFormSteps = __privateGet(this, _state).stepFormSteps.map(
633
+ (st, i) => i === idx ? { ...st, ...patch } : st
634
+ );
635
+ __privateMethod(this, _DialogController_instances, update_fn).call(this, { stepFormSteps });
636
+ };
637
+ validateStepField_fn = function(schema, fieldKey, values, errors) {
638
+ const result = schema.safeParse(values);
639
+ const updated = { ...errors };
640
+ if (result.success) {
641
+ delete updated[fieldKey];
642
+ } else {
643
+ const issue = result.error.issues.find(
644
+ (iss) => iss.path[0]?.toString() === fieldKey
645
+ );
646
+ if (issue) {
647
+ updated[fieldKey] = issue.message;
648
+ } else {
649
+ delete updated[fieldKey];
650
+ }
651
+ }
652
+ return updated;
653
+ };
258
654
  // ─── Internal ────────────────────────────────────────────
259
655
  createPromise_fn = function(timer) {
260
656
  return new Promise((resolve) => {
@@ -343,6 +739,24 @@ var overlayStyles = import_lit.css`
343
739
  --dialog-spinner-track: rgb(59 130 246 / 0.2);
344
740
  --dialog-spinner-arc: var(--dialog-primary);
345
741
 
742
+ /* Form */
743
+ --dialog-form-width: 500px;
744
+ --dialog-form-max-height: 60vh;
745
+ --dialog-form-gap: 16px;
746
+ --dialog-form-columns: 1;
747
+ --dialog-form-label-color: #374151;
748
+ --dialog-form-label-size: 13px;
749
+ --dialog-form-label-weight: 500;
750
+ --dialog-form-input-bg: #fff;
751
+ --dialog-form-input-border: #d1d5db;
752
+ --dialog-form-input-border-focus: var(--dialog-primary);
753
+ --dialog-form-input-radius: 6px;
754
+ --dialog-form-input-padding: 8px 12px;
755
+ --dialog-form-input-font-size: 14px;
756
+ --dialog-form-error-color: var(--dialog-error);
757
+ --dialog-form-hint-color: #9ca3af;
758
+ --dialog-form-required-color: var(--dialog-error);
759
+
346
760
  display: contents;
347
761
  font-family: var(--dialog-font-family);
348
762
  color: var(--dialog-text-color);
@@ -494,8 +908,6 @@ var overlayStyles = import_lit.css`
494
908
  }
495
909
  }
496
910
 
497
- /* spin と spinner-enter は同じ transform を書き換えるため衝突する。
498
- ラッパーでスケール/フェードを担い、.spinner は回転専用にする。 */
499
911
  .spinner-wrap {
500
912
  font-size: var(--dialog-spinner-size);
501
913
  width: 1em;
@@ -978,6 +1390,204 @@ var overlayStyles = import_lit.css`
978
1390
  background-color: #e5e7eb;
979
1391
  transition: background-color 400ms ease;
980
1392
  }
1393
+
1394
+ /* ─── Form ─── */
1395
+
1396
+ @media (min-width: 640px) {
1397
+ .card[data-type='form'] {
1398
+ width: var(--dialog-form-width);
1399
+ }
1400
+ }
1401
+
1402
+ .form-scroll-container {
1403
+ max-height: var(--dialog-form-max-height);
1404
+ overflow-y: auto;
1405
+ width: 100%;
1406
+ padding: 4px 0;
1407
+ }
1408
+
1409
+ .form-grid {
1410
+ display: grid;
1411
+ grid-template-columns: repeat(var(--dialog-form-columns, 1), 1fr);
1412
+ gap: var(--dialog-form-gap);
1413
+ width: 100%;
1414
+ }
1415
+
1416
+ @media (max-width: 639px) {
1417
+ .form-grid {
1418
+ grid-template-columns: 1fr !important;
1419
+ }
1420
+ }
1421
+
1422
+ .form-field {
1423
+ display: flex;
1424
+ flex-direction: column;
1425
+ gap: 4px;
1426
+ text-align: left;
1427
+ }
1428
+
1429
+ .form-field[data-type='checkbox'] {
1430
+ grid-column: 1 / -1;
1431
+ }
1432
+
1433
+ .form-label {
1434
+ font-size: var(--dialog-form-label-size);
1435
+ font-weight: var(--dialog-form-label-weight);
1436
+ color: var(--dialog-form-label-color);
1437
+ }
1438
+
1439
+ .form-required {
1440
+ color: var(--dialog-form-required-color);
1441
+ margin-left: 2px;
1442
+ }
1443
+
1444
+ .form-input,
1445
+ .form-select {
1446
+ padding: var(--dialog-form-input-padding);
1447
+ font-size: var(--dialog-form-input-font-size);
1448
+ font-family: inherit;
1449
+ background: var(--dialog-form-input-bg);
1450
+ border: 1px solid var(--dialog-form-input-border);
1451
+ border-radius: var(--dialog-form-input-radius);
1452
+ color: var(--dialog-text-color);
1453
+ outline: none;
1454
+ transition:
1455
+ border-color 150ms ease,
1456
+ box-shadow 150ms ease;
1457
+ width: 100%;
1458
+ box-sizing: border-box;
1459
+ }
1460
+
1461
+ .form-input:focus,
1462
+ .form-select:focus {
1463
+ border-color: var(--dialog-form-input-border-focus);
1464
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
1465
+ }
1466
+
1467
+ .form-field[data-error] .form-input,
1468
+ .form-field[data-error] .form-select {
1469
+ border-color: var(--dialog-form-error-color);
1470
+ }
1471
+
1472
+ .form-field[data-error] .form-input:focus,
1473
+ .form-field[data-error] .form-select:focus {
1474
+ box-shadow: 0 0 0 3px rgb(239 68 68 / 0.1);
1475
+ }
1476
+
1477
+ .form-error {
1478
+ font-size: 12px;
1479
+ color: var(--dialog-form-error-color);
1480
+ min-height: 1em;
1481
+ }
1482
+
1483
+ .form-hint {
1484
+ font-size: 12px;
1485
+ color: var(--dialog-form-hint-color);
1486
+ }
1487
+
1488
+ .form-checkbox {
1489
+ width: 18px;
1490
+ height: 18px;
1491
+ accent-color: var(--dialog-primary);
1492
+ cursor: pointer;
1493
+ flex-shrink: 0;
1494
+ }
1495
+
1496
+ .form-checkbox-label {
1497
+ display: flex;
1498
+ align-items: center;
1499
+ gap: 8px;
1500
+ cursor: pointer;
1501
+ font-size: var(--dialog-form-input-font-size);
1502
+ color: var(--dialog-form-label-color);
1503
+ }
1504
+
1505
+ .form-checkbox-text {
1506
+ font-size: var(--dialog-form-label-size);
1507
+ font-weight: var(--dialog-form-label-weight);
1508
+ color: var(--dialog-form-label-color);
1509
+ }
1510
+
1511
+ .form-group {
1512
+ border: 1px solid var(--dialog-card-border);
1513
+ border-radius: var(--dialog-form-input-radius);
1514
+ padding: 16px;
1515
+ margin: 0 0 8px;
1516
+ width: 100%;
1517
+ box-sizing: border-box;
1518
+ }
1519
+
1520
+ .form-group-label {
1521
+ font-size: var(--dialog-form-label-size);
1522
+ font-weight: 600;
1523
+ color: var(--dialog-form-label-color);
1524
+ padding: 0 4px;
1525
+ }
1526
+
1527
+ /* ─── Step Form ─── */
1528
+
1529
+ @media (min-width: 640px) {
1530
+ .card[data-type='step-form'] {
1531
+ width: var(--dialog-form-width);
1532
+ }
1533
+ }
1534
+
1535
+ .step-form-counter {
1536
+ font-size: 12px;
1537
+ color: var(--dialog-form-hint-color);
1538
+ text-align: center;
1539
+ margin: 0 0 4px;
1540
+ }
1541
+
1542
+ .actions-step-form {
1543
+ display: flex;
1544
+ flex-direction: row;
1545
+ justify-content: space-between;
1546
+ align-items: center;
1547
+ gap: 8px;
1548
+ margin-top: 20px;
1549
+ width: 100%;
1550
+ }
1551
+
1552
+ .step-form-nav {
1553
+ display: flex;
1554
+ flex-direction: row;
1555
+ gap: 8px;
1556
+ align-items: center;
1557
+ }
1558
+
1559
+ .btn-prev {
1560
+ padding: var(--dialog-btn-padding);
1561
+ font-size: var(--dialog-btn-font-size);
1562
+ font-family: inherit;
1563
+ font-weight: 500;
1564
+ border-radius: var(--dialog-btn-radius);
1565
+ cursor: pointer;
1566
+ border: 1px solid var(--dialog-form-input-border);
1567
+ background: transparent;
1568
+ color: var(--dialog-text-color);
1569
+ transition: background-color 120ms ease, border-color 120ms ease;
1570
+ }
1571
+
1572
+ .btn-prev:hover {
1573
+ background: #f9fafb;
1574
+ border-color: #9ca3af;
1575
+ }
1576
+
1577
+ .btn-prev:active {
1578
+ transform: scale(0.98);
1579
+ }
1580
+
1581
+ @media (max-width: 639px) {
1582
+ .actions-step-form {
1583
+ flex-direction: column-reverse;
1584
+ }
1585
+
1586
+ .step-form-nav {
1587
+ width: 100%;
1588
+ justify-content: flex-end;
1589
+ }
1590
+ }
981
1591
  `;
982
1592
 
983
1593
  // src/overlay-dialog.ts
@@ -1001,6 +1611,7 @@ var OverlayDialog = class extends import_lit2.LitElement {
1001
1611
  this._unsubscribe = this.controller.subscribe((s) => {
1002
1612
  const wasOpen = this._state.open;
1003
1613
  const prevDialogType = this._state.dialogType;
1614
+ const prevStepIndex = this._state.stepFormCurrentIndex;
1004
1615
  if (s.open && !wasOpen) {
1005
1616
  this._isClosing = false;
1006
1617
  clearTimeout(this._closeTimer);
@@ -1015,6 +1626,8 @@ var OverlayDialog = class extends import_lit2.LitElement {
1015
1626
  }
1016
1627
  if (s.open && s.dialogType !== prevDialogType) {
1017
1628
  this._bodyKey++;
1629
+ } else if (s.open && s.dialogType === "step-form" && s.stepFormCurrentIndex !== prevStepIndex) {
1630
+ this._bodyKey++;
1018
1631
  }
1019
1632
  this._state = s;
1020
1633
  this._syncBodyScroll(s.open);
@@ -1244,6 +1857,187 @@ var OverlayDialog = class extends import_lit2.LitElement {
1244
1857
  </ul>
1245
1858
  `;
1246
1859
  }
1860
+ // ─── Form Helpers ────────────────────────────────────────
1861
+ _createFormContext() {
1862
+ const s = this._state;
1863
+ return {
1864
+ getValue: (k) => s.formValues[k],
1865
+ getError: (k) => s.formErrors[k] ?? "",
1866
+ getTouched: (k) => !!s.formTouched[k],
1867
+ onUpdate: (k, v) => this.controller.updateFormField(k, v),
1868
+ onBlur: (k) => this.controller.touchFormField(k)
1869
+ };
1870
+ }
1871
+ _createStepFormContext() {
1872
+ const s = this._state;
1873
+ const step = s.stepFormSteps[s.stepFormCurrentIndex];
1874
+ return {
1875
+ getValue: (k) => step?.values[k],
1876
+ getError: (k) => step?.errors[k] ?? "",
1877
+ getTouched: (k) => !!step?.touched[k],
1878
+ onUpdate: (k, v) => this.controller.updateStepFormField(k, v),
1879
+ onBlur: (k) => this.controller.touchStepFormField(k)
1880
+ };
1881
+ }
1882
+ _getOrderedFields(fields, layout) {
1883
+ const order = layout.fieldOrder;
1884
+ if (!order?.length) return fields;
1885
+ const fieldMap = new Map(fields.map((f) => [f.key, f]));
1886
+ const ordered = [];
1887
+ for (const key of order) {
1888
+ const f = fieldMap.get(key);
1889
+ if (f) {
1890
+ ordered.push(f);
1891
+ fieldMap.delete(key);
1892
+ }
1893
+ }
1894
+ for (const f of fieldMap.values()) {
1895
+ ordered.push(f);
1896
+ }
1897
+ return ordered;
1898
+ }
1899
+ _renderFormGrid(fields, columns, gap, ctx) {
1900
+ return import_lit2.html`
1901
+ <div class="form-grid" style="--dialog-form-columns:${columns}; gap:${gap}">
1902
+ ${fields.map((f) => this._renderFormField(f, ctx))}
1903
+ </div>
1904
+ `;
1905
+ }
1906
+ _renderGroupedForm(allFields, groups, layout, ctx) {
1907
+ const fieldMap = new Map(allFields.map((f) => [f.key, f]));
1908
+ const usedKeys = /* @__PURE__ */ new Set();
1909
+ const gap = layout.gap ?? "16px";
1910
+ const groupFragments = groups.map((group) => {
1911
+ const groupFields = [];
1912
+ for (const key of group.fields) {
1913
+ const f = fieldMap.get(key);
1914
+ if (f) {
1915
+ groupFields.push(f);
1916
+ usedKeys.add(key);
1917
+ }
1918
+ }
1919
+ if (!groupFields.length) return import_lit2.nothing;
1920
+ const cols = group.columns ?? layout.columns ?? 1;
1921
+ return import_lit2.html`
1922
+ <fieldset class="form-group">
1923
+ ${group.label ? import_lit2.html`<legend class="form-group-label">${group.label}</legend>` : import_lit2.nothing}
1924
+ ${this._renderFormGrid(groupFields, cols, gap, ctx)}
1925
+ </fieldset>
1926
+ `;
1927
+ });
1928
+ const remaining = allFields.filter((f) => !usedKeys.has(f.key));
1929
+ return import_lit2.html`
1930
+ ${groupFragments}
1931
+ ${remaining.length ? this._renderFormGrid(remaining, layout.columns ?? 1, gap, ctx) : import_lit2.nothing}
1932
+ `;
1933
+ }
1934
+ _renderForm(fields, layout, ctx) {
1935
+ const ordered = this._getOrderedFields(fields, layout);
1936
+ const gap = layout.gap ?? "16px";
1937
+ if (layout.groups?.length) {
1938
+ return this._renderGroupedForm(ordered, layout.groups, layout, ctx);
1939
+ }
1940
+ return this._renderFormGrid(ordered, layout.columns ?? 1, gap, ctx);
1941
+ }
1942
+ _renderFormField(field, ctx) {
1943
+ const value = ctx.getValue(field.key);
1944
+ const error = ctx.getError(field.key);
1945
+ const touched = ctx.getTouched(field.key);
1946
+ const showError = touched && !!error;
1947
+ if (field.inputType === "checkbox") {
1948
+ return import_lit2.html`
1949
+ <div class="form-field" data-type="checkbox" ?data-error=${showError}>
1950
+ <label class="form-checkbox-label">
1951
+ <input
1952
+ type="checkbox"
1953
+ class="form-checkbox"
1954
+ .checked=${!!value}
1955
+ @change=${(e) => ctx.onUpdate(field.key, e.target.checked)}
1956
+ @blur=${() => ctx.onBlur(field.key)}
1957
+ />
1958
+ <span class="form-checkbox-text">
1959
+ ${field.label}
1960
+ ${field.required ? import_lit2.html`<span class="form-required">*</span>` : import_lit2.nothing}
1961
+ </span>
1962
+ </label>
1963
+ ${showError ? import_lit2.html`<span class="form-error">${error}</span>` : import_lit2.nothing}
1964
+ </div>
1965
+ `;
1966
+ }
1967
+ return import_lit2.html`
1968
+ <div class="form-field" data-type=${field.inputType} ?data-error=${showError}>
1969
+ <label class="form-label" for="form-${field.key}">
1970
+ ${field.label} ${field.required ? import_lit2.html`<span class="form-required">*</span>` : import_lit2.nothing}
1971
+ </label>
1972
+ ${field.description ? import_lit2.html`<span class="form-hint">${field.description}</span>` : import_lit2.nothing}
1973
+ ${this._renderFormInput(field, value, ctx)}
1974
+ ${showError ? import_lit2.html`<span class="form-error">${error}</span>` : import_lit2.nothing}
1975
+ </div>
1976
+ `;
1977
+ }
1978
+ _renderFormInput(field, value, ctx) {
1979
+ switch (field.inputType) {
1980
+ case "select":
1981
+ return import_lit2.html`
1982
+ <select
1983
+ class="form-select"
1984
+ id="form-${field.key}"
1985
+ @change=${(e) => ctx.onUpdate(field.key, e.target.value)}
1986
+ @blur=${() => ctx.onBlur(field.key)}
1987
+ >
1988
+ <option value="" ?selected=${!value}>選択してください</option>
1989
+ ${field.options.map(
1990
+ (opt) => import_lit2.html`<option value=${opt} ?selected=${value === opt}>${opt}</option>`
1991
+ )}
1992
+ </select>
1993
+ `;
1994
+ case "number":
1995
+ return import_lit2.html`
1996
+ <input
1997
+ type="number"
1998
+ class="form-input"
1999
+ id="form-${field.key}"
2000
+ .value=${value != null ? String(value) : ""}
2001
+ min=${field.min ?? import_lit2.nothing}
2002
+ max=${field.max ?? import_lit2.nothing}
2003
+ placeholder=${field.placeholder || import_lit2.nothing}
2004
+ @input=${(e) => {
2005
+ const v = e.target.valueAsNumber;
2006
+ ctx.onUpdate(field.key, Number.isNaN(v) ? void 0 : v);
2007
+ }}
2008
+ @blur=${() => ctx.onBlur(field.key)}
2009
+ />
2010
+ `;
2011
+ case "date":
2012
+ return import_lit2.html`
2013
+ <input
2014
+ type="date"
2015
+ class="form-input"
2016
+ id="form-${field.key}"
2017
+ .value=${value != null ? String(value) : ""}
2018
+ @input=${(e) => {
2019
+ const str = e.target.value;
2020
+ ctx.onUpdate(field.key, str || void 0);
2021
+ }}
2022
+ @blur=${() => ctx.onBlur(field.key)}
2023
+ />
2024
+ `;
2025
+ default:
2026
+ return import_lit2.html`
2027
+ <input
2028
+ type=${field.inputType}
2029
+ class="form-input"
2030
+ id="form-${field.key}"
2031
+ .value=${value ?? ""}
2032
+ minlength=${field.minLength ?? import_lit2.nothing}
2033
+ maxlength=${field.maxLength ?? import_lit2.nothing}
2034
+ placeholder=${field.placeholder || import_lit2.nothing}
2035
+ @input=${(e) => ctx.onUpdate(field.key, e.target.value)}
2036
+ @blur=${() => ctx.onBlur(field.key)}
2037
+ />
2038
+ `;
2039
+ }
2040
+ }
1247
2041
  _renderButtons() {
1248
2042
  const s = this._state;
1249
2043
  if (!s.showConfirmButton && !s.showCancelButton) return import_lit2.nothing;
@@ -1258,6 +2052,39 @@ var OverlayDialog = class extends import_lit2.LitElement {
1258
2052
  </div>
1259
2053
  `;
1260
2054
  }
2055
+ // ─── Step Form Helpers ───────────────────────────────────
2056
+ _renderStepFormButtons() {
2057
+ const s = this._state;
2058
+ const isFirst = s.stepFormCurrentIndex === 0;
2059
+ const isLast = s.stepFormCurrentIndex === s.stepFormSteps.length - 1;
2060
+ const submitText = isLast ? s.stepFormSubmitText : s.stepFormNextText;
2061
+ return import_lit2.html`
2062
+ <div class="actions actions-step-form">
2063
+ <button class="btn btn-cancel" @click=${() => this.controller.onCancel()}>
2064
+ ${s.cancelButtonText}
2065
+ </button>
2066
+ <div class="step-form-nav">
2067
+ ${isFirst ? import_lit2.nothing : import_lit2.html`
2068
+ <button class="btn btn-prev" @click=${() => this.controller.onStepPrev()}>
2069
+ ${s.stepFormPrevText}
2070
+ </button>
2071
+ `}
2072
+ <button class="btn btn-confirm" @click=${() => this.controller.onStepNext()}>
2073
+ ${submitText}
2074
+ </button>
2075
+ </div>
2076
+ </div>
2077
+ `;
2078
+ }
2079
+ _deriveStepItems() {
2080
+ const s = this._state;
2081
+ return s.stepFormSteps.map((st, i) => {
2082
+ let status = "pending";
2083
+ if (i < s.stepFormCurrentIndex) status = "done";
2084
+ else if (i === s.stepFormCurrentIndex) status = "active";
2085
+ return { key: st.key, label: st.label, status };
2086
+ });
2087
+ }
1261
2088
  _renderBody() {
1262
2089
  const s = this._state;
1263
2090
  switch (s.dialogType) {
@@ -1284,16 +2111,47 @@ var OverlayDialog = class extends import_lit2.LitElement {
1284
2111
  ${s.label ? import_lit2.html`<p class="label">${s.label}</p>` : import_lit2.nothing}
1285
2112
  ${this._renderStepsList(s.steps)}
1286
2113
  `;
2114
+ case "form": {
2115
+ const ctx = this._createFormContext();
2116
+ return import_lit2.html`
2117
+ ${s.title ? import_lit2.html`<p class="label">${s.title}</p>` : import_lit2.nothing}
2118
+ ${s.description ? import_lit2.html`<p class="description">${s.description}</p>` : import_lit2.nothing}
2119
+ <div class="form-scroll-container">
2120
+ ${this._renderForm(s.formFields, s.formLayout, ctx)}
2121
+ </div>
2122
+ ${this._renderButtons()}
2123
+ `;
2124
+ }
2125
+ case "step-form": {
2126
+ const stepItems = this._deriveStepItems();
2127
+ const currentStep = s.stepFormSteps[s.stepFormCurrentIndex];
2128
+ if (!currentStep) return import_lit2.html`${import_lit2.nothing}`;
2129
+ const ctx = this._createStepFormContext();
2130
+ const stepCount = s.stepFormSteps.length;
2131
+ const counterText = `${s.stepFormCurrentIndex + 1} / ${stepCount}`;
2132
+ return import_lit2.html`
2133
+ ${this._renderStepsHeader(stepItems)}
2134
+ <p class="step-form-counter">${counterText}</p>
2135
+ <p class="label">${currentStep.label}</p>
2136
+ ${currentStep.description ? import_lit2.html`<p class="description">${currentStep.description}</p>` : import_lit2.nothing}
2137
+ ${currentStep.fields.length ? import_lit2.html`
2138
+ <div class="form-scroll-container">
2139
+ ${this._renderForm(currentStep.fields, currentStep.layout, ctx)}
2140
+ </div>
2141
+ ` : import_lit2.nothing}
2142
+ ${this._renderStepFormButtons()}
2143
+ `;
2144
+ }
1287
2145
  default:
1288
2146
  return import_lit2.html`${import_lit2.nothing}`;
1289
2147
  }
1290
2148
  }
1291
2149
  render() {
1292
2150
  const s = this._state;
1293
- const showHeaderTitle = s.title && s.dialogType !== "alert" && s.dialogType !== "confirm";
2151
+ const showHeaderTitle = s.title && s.dialogType !== "alert" && s.dialogType !== "confirm" && s.dialogType !== "form" && s.dialogType !== "step-form";
1294
2152
  return import_lit2.html`
1295
2153
  <div class="backdrop" ?data-open=${s.open} @click=${this._onBackdropClick}>
1296
- <div class="card" ?data-closing=${this._isClosing}>
2154
+ <div class="card" data-type=${s.dialogType} ?data-closing=${this._isClosing}>
1297
2155
  ${showHeaderTitle ? import_lit2.html`<p class="dialog-title">${s.title}</p>` : import_lit2.nothing}
1298
2156
  <div class="card-body">
1299
2157
  ${(0, import_keyed.keyed)(this._bodyKey, import_lit2.html`<div class="body-inner">${this._renderBody()}</div>`)}
@@ -1349,6 +2207,28 @@ var DialogSingleton = class {
1349
2207
  __privateMethod(this, _DialogSingleton_instances, ensureElement_fn).call(this);
1350
2208
  return __privateGet(this, _controller).confirm(optionsOrLabel);
1351
2209
  }
2210
+ // ─── Form ──────────────────────────────────────────────────
2211
+ form(schema, options) {
2212
+ __privateMethod(this, _DialogSingleton_instances, ensureElement_fn).call(this);
2213
+ return __privateGet(this, _controller).form(schema, options);
2214
+ }
2215
+ // ─── Step Form ─────────────────────────────────────────────
2216
+ showStepForm(steps, options) {
2217
+ __privateMethod(this, _DialogSingleton_instances, ensureElement_fn).call(this);
2218
+ return __privateGet(this, _controller).showStepForm(steps, options);
2219
+ }
2220
+ onStepNext() {
2221
+ __privateGet(this, _controller).onStepNext();
2222
+ }
2223
+ onStepPrev() {
2224
+ __privateGet(this, _controller).onStepPrev();
2225
+ }
2226
+ updateStepFormField(fieldKey, value) {
2227
+ __privateGet(this, _controller).updateStepFormField(fieldKey, value);
2228
+ }
2229
+ touchStepFormField(fieldKey) {
2230
+ __privateGet(this, _controller).touchStepFormField(fieldKey);
2231
+ }
1352
2232
  // ─── Loading helpers ─────────────────────────────────────
1353
2233
  showLoading(label) {
1354
2234
  __privateMethod(this, _DialogSingleton_instances, ensureElement_fn).call(this);