imean-service-engine-htmx-plugin 2.1.0 → 2.2.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.
package/dist/index.mjs CHANGED
@@ -293,144 +293,350 @@ function getFieldValue(field, initialData) {
293
293
  if (value === null || value === void 0) {
294
294
  return "";
295
295
  }
296
+ if (typeof value === "object") {
297
+ if (value instanceof Date) {
298
+ return value.toISOString();
299
+ }
300
+ try {
301
+ return JSON.stringify(value);
302
+ } catch (e) {
303
+ return "";
304
+ }
305
+ }
296
306
  return String(value);
297
307
  }
298
308
  return "";
299
309
  }
310
+ function isJsonString(value) {
311
+ if (!value || typeof value !== "string") {
312
+ return false;
313
+ }
314
+ const trimmed = value.trim();
315
+ return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
316
+ }
317
+ function formatJsonString(value) {
318
+ if (!value || typeof value !== "string") {
319
+ return value;
320
+ }
321
+ try {
322
+ const parsed = JSON.parse(value);
323
+ return JSON.stringify(parsed, null, 2);
324
+ } catch (e) {
325
+ return value;
326
+ }
327
+ }
328
+ function renderFormField(field, initialData, formFieldRenderers) {
329
+ const value = getFieldValue(field, initialData);
330
+ const customRenderer = formFieldRenderers?.[field.name];
331
+ if (customRenderer) {
332
+ let parsedValue = null;
333
+ if (value && isJsonString(value)) {
334
+ try {
335
+ parsedValue = JSON.parse(value);
336
+ } catch (e) {
337
+ parsedValue = null;
338
+ }
339
+ } else if (value) {
340
+ parsedValue = value;
341
+ }
342
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
343
+ /* @__PURE__ */ jsxs(
344
+ "label",
345
+ {
346
+ htmlFor: field.name,
347
+ className: "block text-sm font-semibold text-gray-700",
348
+ "data-testid": `label-${field.name}`,
349
+ children: [
350
+ field.label,
351
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
352
+ ]
353
+ }
354
+ ),
355
+ /* @__PURE__ */ jsx("div", { children: customRenderer({
356
+ field,
357
+ value: parsedValue,
358
+ initialData,
359
+ fieldName: field.name
360
+ }) }),
361
+ field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
362
+ ] }, field.name);
363
+ }
364
+ const shouldUseTextarea = field.type === "textarea" || value && isJsonString(value) && field.type !== "select";
365
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
366
+ /* @__PURE__ */ jsxs(
367
+ "label",
368
+ {
369
+ htmlFor: field.name,
370
+ className: "block text-sm font-semibold text-gray-700",
371
+ "data-testid": `label-${field.name}`,
372
+ children: [
373
+ field.label,
374
+ field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
375
+ ]
376
+ }
377
+ ),
378
+ shouldUseTextarea ? /* @__PURE__ */ jsx(
379
+ "textarea",
380
+ {
381
+ id: field.name,
382
+ name: field.name,
383
+ required: field.required,
384
+ placeholder: field.placeholder || (isJsonString(value) ? "JSON \u683C\u5F0F\u6570\u636E" : ""),
385
+ rows: isJsonString(value) ? 10 : 4,
386
+ className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y font-mono text-sm",
387
+ "data-testid": `input-${field.name}`,
388
+ children: isJsonString(value) ? formatJsonString(value) : value
389
+ },
390
+ `${field.name}-${value}`
391
+ ) : field.type === "select" ? /* @__PURE__ */ jsxs(
392
+ "select",
393
+ {
394
+ id: field.name,
395
+ name: field.name,
396
+ required: field.required,
397
+ className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
398
+ "data-testid": `select-${field.name}`,
399
+ children: [
400
+ !field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
401
+ field.options && field.options.length > 0 ? field.options.map((option) => {
402
+ const optionValue = String(option.value);
403
+ const isSelected = value === optionValue;
404
+ return /* @__PURE__ */ jsx(
405
+ "option",
406
+ {
407
+ value: optionValue,
408
+ selected: isSelected,
409
+ children: option.label
410
+ },
411
+ optionValue
412
+ );
413
+ }) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
414
+ ]
415
+ },
416
+ `${field.name}-${value || ""}`
417
+ ) : /* @__PURE__ */ jsx(
418
+ "input",
419
+ {
420
+ type: field.type || "text",
421
+ id: field.name,
422
+ name: field.name,
423
+ required: field.required,
424
+ placeholder: field.placeholder,
425
+ step: field.type === "number" ? field.step ?? void 0 : void 0,
426
+ className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200",
427
+ value,
428
+ "data-testid": `input-${field.name}`
429
+ },
430
+ `${field.name}-${value}`
431
+ ),
432
+ field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
433
+ ] }, field.name);
434
+ }
300
435
  function FormPage(props) {
301
- const { fields, submitUrl, method = "post", initialData, formId } = props;
436
+ const { fields, groups, submitUrl, method = "post", initialData, formId, isDialog = false, formFieldRenderers } = props;
302
437
  const finalFormId = formId || `form-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
303
438
  if (process.env.NODE_ENV === "development" && initialData) {
439
+ const fieldNames = fields ? fields.map((f) => f.name).join(", ") : groups ? groups.map((g) => g.fields.map((f) => f.name).join(", ")).join(" | ") : "";
304
440
  console.log(
305
- `[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fields.map((f) => f.name).join(", ")}`
441
+ `[FormPage] initialData: ${JSON.stringify(initialData)}, fields: ${fieldNames}`
306
442
  );
307
443
  }
308
- return /* @__PURE__ */ jsx("div", { className: "w-full", "data-testid": "form-container", children: /* @__PURE__ */ jsxs(
309
- "form",
310
- {
311
- id: finalFormId,
312
- method: method === "put" ? "post" : method,
313
- action: submitUrl,
314
- "hx-boost": "true",
315
- "hx-method": method === "put" ? "put" : void 0,
316
- "hx-indicator": "#form-loading-indicator",
317
- className: "space-y-6",
318
- "data-testid": "form",
319
- children: [
320
- /* @__PURE__ */ jsxs(
321
- "div",
322
- {
323
- id: "form-loading-indicator",
324
- className: "htmx-indicator fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50",
325
- children: [
326
- /* @__PURE__ */ jsxs(
327
- "svg",
328
- {
329
- className: "animate-spin h-4 w-4",
330
- xmlns: "http://www.w3.org/2000/svg",
331
- fill: "none",
332
- viewBox: "0 0 24 24",
333
- children: [
334
- /* @__PURE__ */ jsx(
335
- "circle",
336
- {
337
- className: "opacity-25",
338
- cx: "12",
339
- cy: "12",
340
- r: "10",
341
- stroke: "currentColor",
342
- strokeWidth: "4"
343
- }
344
- ),
345
- /* @__PURE__ */ jsx(
346
- "path",
347
- {
348
- className: "opacity-75",
349
- fill: "currentColor",
350
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
351
- }
352
- )
353
- ]
354
- }
355
- ),
356
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
357
- ]
444
+ return /* @__PURE__ */ jsxs("div", { className: "w-full", "data-testid": "form-container", "x-data": `{ activeTab: 0 }`, children: [
445
+ /* @__PURE__ */ jsxs(
446
+ "div",
447
+ {
448
+ id: "form-loading-indicator",
449
+ className: "htmx-indicator fixed top-4 right-4 bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 z-50",
450
+ children: [
451
+ /* @__PURE__ */ jsxs(
452
+ "svg",
453
+ {
454
+ className: "animate-spin h-4 w-4",
455
+ xmlns: "http://www.w3.org/2000/svg",
456
+ fill: "none",
457
+ viewBox: "0 0 24 24",
458
+ children: [
459
+ /* @__PURE__ */ jsx(
460
+ "circle",
461
+ {
462
+ className: "opacity-25",
463
+ cx: "12",
464
+ cy: "12",
465
+ r: "10",
466
+ stroke: "currentColor",
467
+ strokeWidth: "4"
468
+ }
469
+ ),
470
+ /* @__PURE__ */ jsx(
471
+ "path",
472
+ {
473
+ className: "opacity-75",
474
+ fill: "currentColor",
475
+ d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
476
+ }
477
+ )
478
+ ]
479
+ }
480
+ ),
481
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: "\u63D0\u4EA4\u4E2D..." })
482
+ ]
483
+ }
484
+ ),
485
+ groups && groups.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
486
+ /* @__PURE__ */ jsx("div", { className: `sticky top-0 z-10 bg-white border-b border-gray-200 shadow-sm ${isDialog ? "px-6" : "-mx-6 px-6 -mt-6"}`, children: /* @__PURE__ */ jsx("nav", { className: "flex -mb-px w-full", "aria-label": "Tabs", "data-testid": "form-tabs", children: groups.map((group, index) => /* @__PURE__ */ jsx(
487
+ "button",
488
+ {
489
+ type: "button",
490
+ "x-on:click": `activeTab = ${index}`,
491
+ "x-bind:class": `activeTab === ${index} ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'`,
492
+ className: "px-6 py-4 text-sm font-medium border-b-2 transition-colors duration-150 whitespace-nowrap flex-1 text-center",
493
+ "data-testid": `form-tab-${index}`,
494
+ children: group.label
495
+ },
496
+ index
497
+ )) }) }),
498
+ /* @__PURE__ */ jsx(
499
+ "form",
500
+ {
501
+ id: finalFormId,
502
+ method: method === "put" ? "post" : method,
503
+ action: submitUrl,
504
+ "hx-boost": "true",
505
+ ...method === "put" ? { "hx-put": submitUrl } : {},
506
+ "hx-indicator": "#form-loading-indicator",
507
+ "data-testid": "form",
508
+ className: isDialog ? "p-6" : "mt-6",
509
+ children: /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6", children: groups.map((group, index) => /* @__PURE__ */ jsx(
510
+ "div",
511
+ {
512
+ "x-show": `activeTab === ${index}`,
513
+ className: "space-y-6",
514
+ "data-testid": `form-tab-content-${index}`,
515
+ children: group.fields.map((field) => renderFormField(field, initialData, formFieldRenderers))
516
+ },
517
+ index
518
+ )) }) })
519
+ }
520
+ )
521
+ ] }) : (
522
+ /* 平铺模式(向后兼容) */
523
+ /* @__PURE__ */ jsx(
524
+ "form",
525
+ {
526
+ id: finalFormId,
527
+ method: method === "put" ? "post" : method,
528
+ action: submitUrl,
529
+ "hx-boost": "true",
530
+ ...method === "put" ? { "hx-put": submitUrl } : {},
531
+ "hx-indicator": "#form-loading-indicator",
532
+ className: "space-y-6",
533
+ "data-testid": "form",
534
+ children: fields && fields.length > 0 && /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => renderFormField(field, initialData, formFieldRenderers)) }) })
535
+ }
536
+ )
537
+ )
538
+ ] });
539
+ }
540
+
541
+ // src/utils/form-data-processor.ts
542
+ function preprocessFormData(data, zodSchema) {
543
+ if (!zodSchema) {
544
+ return data;
545
+ }
546
+ const processed = { ...data };
547
+ const def = zodSchema._def;
548
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
549
+ if (!shape || typeof shape !== "object") {
550
+ return processed;
551
+ }
552
+ for (const [fieldName, fieldSchema] of Object.entries(shape)) {
553
+ const value = processed[fieldName];
554
+ if (value === void 0) {
555
+ continue;
556
+ }
557
+ if (value === null) {
558
+ processed[fieldName] = void 0;
559
+ continue;
560
+ }
561
+ if (value === "") {
562
+ processed[fieldName] = void 0;
563
+ continue;
564
+ }
565
+ const fieldDef = fieldSchema._def;
566
+ let typeName = fieldDef?.type || fieldDef?.typeName;
567
+ if (typeName === "optional" || typeName === "ZodOptional") {
568
+ const innerType = fieldDef.innerType;
569
+ if (innerType) {
570
+ const innerDef = innerType._def;
571
+ typeName = innerDef?.type || innerDef?.typeName;
572
+ }
573
+ }
574
+ if (typeName === "number" || typeName === "ZodNumber") {
575
+ if (typeof value === "string") {
576
+ const trimmed = value.trim();
577
+ if (trimmed !== "") {
578
+ const numValue = Number(trimmed);
579
+ if (!isNaN(numValue)) {
580
+ processed[fieldName] = numValue;
358
581
  }
359
- ),
360
- /* @__PURE__ */ jsx("div", { className: "bg-white rounded-lg border border-gray-200 shadow-sm", children: /* @__PURE__ */ jsx("div", { className: "p-6 space-y-6", children: fields.map((field) => {
361
- const value = getFieldValue(field, initialData);
362
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2", "data-testid": `field-${field.name}`, children: [
363
- /* @__PURE__ */ jsxs(
364
- "label",
365
- {
366
- htmlFor: field.name,
367
- className: "block text-sm font-semibold text-gray-700",
368
- "data-testid": `label-${field.name}`,
369
- children: [
370
- field.label,
371
- field.required && /* @__PURE__ */ jsx("span", { className: "text-red-500 ml-1", children: "*" })
372
- ]
373
- }
374
- ),
375
- field.type === "textarea" ? /* @__PURE__ */ jsx(
376
- "textarea",
377
- {
378
- id: field.name,
379
- name: field.name,
380
- required: field.required,
381
- placeholder: field.placeholder,
382
- rows: 4,
383
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-y",
384
- value,
385
- "data-testid": `input-${field.name}`
386
- },
387
- `${field.name}-${value}`
388
- ) : field.type === "select" ? /* @__PURE__ */ jsxs(
389
- "select",
390
- {
391
- id: field.name,
392
- name: field.name,
393
- required: field.required,
394
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 bg-white",
395
- "data-testid": `select-${field.name}`,
396
- children: [
397
- !field.required && /* @__PURE__ */ jsx("option", { value: "", selected: !value || value === "", children: "\u8BF7\u9009\u62E9" }),
398
- field.options && field.options.length > 0 ? field.options.map((option) => {
399
- const optionValue = String(option.value);
400
- const isSelected = value === optionValue;
401
- return /* @__PURE__ */ jsx(
402
- "option",
403
- {
404
- value: optionValue,
405
- selected: isSelected,
406
- children: option.label
407
- },
408
- optionValue
409
- );
410
- }) : /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u6682\u65E0\u9009\u9879" })
411
- ]
412
- },
413
- `${field.name}-${value || ""}`
414
- ) : /* @__PURE__ */ jsx(
415
- "input",
416
- {
417
- type: field.type || "text",
418
- id: field.name,
419
- name: field.name,
420
- required: field.required,
421
- placeholder: field.placeholder,
422
- className: "w-full px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200",
423
- value,
424
- "data-testid": `input-${field.name}`
425
- },
426
- `${field.name}-${value}`
427
- ),
428
- field.description && /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 mt-1", children: field.description })
429
- ] }, field.name);
430
- }) }) })
431
- ]
582
+ }
583
+ } else if (typeof value === "number") {
584
+ processed[fieldName] = value;
585
+ }
432
586
  }
433
- ) });
587
+ if (typeName === "boolean" || typeName === "ZodBoolean") {
588
+ if (typeof value === "string") {
589
+ const trimmed = value.trim().toLowerCase();
590
+ if (trimmed === "true" || trimmed === "1" || trimmed === "on") {
591
+ processed[fieldName] = true;
592
+ } else if (trimmed === "false" || trimmed === "0" || trimmed === "off" || trimmed === "") {
593
+ processed[fieldName] = false;
594
+ }
595
+ } else if (typeof value === "boolean") {
596
+ processed[fieldName] = value;
597
+ }
598
+ }
599
+ if (typeName === "array" || typeName === "ZodArray") {
600
+ if (typeof value === "string") {
601
+ const trimmed = value.trim();
602
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
603
+ try {
604
+ const parsed = JSON.parse(trimmed);
605
+ processed[fieldName] = parsed;
606
+ } catch (e) {
607
+ }
608
+ } else {
609
+ const parts = trimmed.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
610
+ processed[fieldName] = parts;
611
+ }
612
+ }
613
+ }
614
+ if (typeName === "object" || typeName === "ZodObject") {
615
+ if (typeof value === "string" && value.trim() !== "") {
616
+ try {
617
+ const trimmed = value.trim();
618
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
619
+ const parsed = JSON.parse(trimmed);
620
+ processed[fieldName] = parsed;
621
+ }
622
+ } catch (e) {
623
+ }
624
+ }
625
+ }
626
+ if (typeName === "any" || typeName === "ZodAny") {
627
+ if (typeof value === "string" && value.trim() !== "") {
628
+ try {
629
+ const trimmed = value.trim();
630
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
631
+ const parsed = JSON.parse(trimmed);
632
+ processed[fieldName] = parsed;
633
+ }
634
+ } catch (e) {
635
+ }
636
+ }
637
+ }
638
+ }
639
+ return processed;
434
640
  }
435
641
 
436
642
  // src/utils/schema-utils.ts
@@ -457,13 +663,14 @@ function parseFieldSchema(fieldName, fieldSchema) {
457
663
  return null;
458
664
  }
459
665
  const label = getFieldDescription(fieldSchema) || fieldName;
460
- const { type, required, options, innerSchema } = analyzeFieldType(fieldSchema);
666
+ const { type, required, options, innerSchema, step } = analyzeFieldType(fieldSchema);
461
667
  return {
462
668
  name: fieldName,
463
669
  label,
464
670
  type,
465
671
  required,
466
672
  options,
673
+ step,
467
674
  schema: innerSchema || fieldSchema
468
675
  };
469
676
  }
@@ -520,10 +727,20 @@ function analyzeFieldType(schema) {
520
727
  let fieldType = "text";
521
728
  if (def?.checks) {
522
729
  const hasEmailCheck = def.checks.some(
523
- (check) => check.kind === "email"
730
+ (check) => check.format === "email" || check.constructor?.name === "ZodEmail" || check._zod?.def?.format === "email"
524
731
  );
525
732
  if (hasEmailCheck) {
526
733
  fieldType = "email";
734
+ } else {
735
+ const maxLengthCheck = def.checks.find(
736
+ (check) => check.constructor?.name === "$ZodCheckMaxLength" || check._zod?.def?.check === "max_length" || check._zod?.def?.maximum !== void 0
737
+ );
738
+ if (maxLengthCheck) {
739
+ const maxLength = maxLengthCheck._zod?.def?.maximum ?? maxLengthCheck.value ?? maxLengthCheck.maximum;
740
+ if (maxLength !== void 0 && maxLength > 50) {
741
+ fieldType = "textarea";
742
+ }
743
+ }
527
744
  }
528
745
  }
529
746
  return {
@@ -533,10 +750,22 @@ function analyzeFieldType(schema) {
533
750
  };
534
751
  }
535
752
  if (typeName === "number" || typeName === "ZodNumber") {
753
+ let step = void 0;
754
+ if (def?.checks) {
755
+ const hasIntCheck = def.checks.some(
756
+ (check) => check.isInt === true || check.format === "safeint" || check.def?.format === "safeint" || check.kind === "int" || check.constructor?.name === "ZodInt" || check._zod?.def?.check === "int" || check._zod?.def?.kind === "int"
757
+ );
758
+ if (!hasIntCheck) {
759
+ step = "any";
760
+ }
761
+ } else {
762
+ step = "any";
763
+ }
536
764
  return {
537
765
  type: "number",
538
766
  required: true,
539
- innerSchema: schema
767
+ innerSchema: schema,
768
+ step
540
769
  };
541
770
  }
542
771
  if (typeName === "date" || typeName === "ZodDate") {
@@ -601,7 +830,8 @@ function modelFieldsToFormFields(fields) {
601
830
  type: field.type,
602
831
  label: field.label,
603
832
  required: field.required,
604
- options: field.options
833
+ options: field.options,
834
+ step: field.step
605
835
  }));
606
836
  }
607
837
  function getFieldNamesFromFields(fields) {
@@ -619,6 +849,8 @@ var BaseFormFeature = class extends BaseFeature {
619
849
  descriptionGetter;
620
850
  /** 当前请求的表单 ID(用于在 render 和 getActions 之间共享) */
621
851
  currentFormId;
852
+ /** 自定义表单字段渲染器 */
853
+ formFieldRenderers;
622
854
  /**
623
855
  * 获取或生成表单 ID(确保在同一个请求中保持一致)
624
856
  */
@@ -685,59 +917,7 @@ var BaseFormFeature = class extends BaseFeature {
685
917
  if (!this.schema) {
686
918
  return data;
687
919
  }
688
- const processed = { ...data };
689
- for (const key of Object.keys(processed)) {
690
- if (processed[key] === "") {
691
- processed[key] = void 0;
692
- }
693
- }
694
- const def = this.schema._def;
695
- const shape = typeof def.shape === "function" ? def.shape() : def.shape;
696
- if (!shape || typeof shape !== "object") {
697
- return data;
698
- }
699
- for (const [fieldName, fieldSchema] of Object.entries(shape)) {
700
- const value = processed[fieldName];
701
- if (value === void 0 || value === null) {
702
- continue;
703
- }
704
- const fieldDef = fieldSchema._def;
705
- let typeName = fieldDef?.typeName;
706
- let isOptional = false;
707
- if (typeName === "ZodOptional") {
708
- isOptional = true;
709
- const innerType = fieldDef.innerType?._def;
710
- typeName = innerType?.typeName;
711
- }
712
- if (isOptional && typeof value === "string" && value.trim() === "") {
713
- if (fieldName === "authorId") {
714
- logger.info(`[BaseFormFeature] authorId empty optional -> undefined`);
715
- }
716
- processed[fieldName] = void 0;
717
- continue;
718
- }
719
- if (typeName === "ZodNumber") {
720
- if (typeof value === "string") {
721
- const numValue = Number(value);
722
- if (fieldName === "authorId") {
723
- logger.info(
724
- `[BaseFormFeature] authorId raw value="${value}", numValue=${numValue}, isOptional=${isOptional}`
725
- );
726
- }
727
- if (!isNaN(numValue)) {
728
- processed[fieldName] = numValue;
729
- }
730
- }
731
- }
732
- if (typeName === "ZodArray") {
733
- if (typeof value === "string") {
734
- const parts = value.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
735
- processed[fieldName] = parts;
736
- }
737
- }
738
- }
739
- logger.info(`[BaseFormFeature] processed form data: ${JSON.stringify(processed)}`);
740
- return processed;
920
+ return preprocessFormData(data, this.schema);
741
921
  }
742
922
  /**
743
923
  * 处理请求
@@ -747,12 +927,23 @@ var BaseFormFeature = class extends BaseFeature {
747
927
  this.currentFormId = void 0;
748
928
  if (ctx.req.method === "GET") {
749
929
  return this.render(context);
750
- } else if (ctx.req.method === "POST" || ctx.req.method === "PUT") {
930
+ } else if (ctx.req.method === "POST" || ctx.req.method === "PUT" || ctx.req.method === "PATCH") {
931
+ const methodOverride = ctx.req.header("X-HTTP-Method-Override");
932
+ const actualMethod = methodOverride || ctx.req.method;
933
+ const expectedMethod = this.getFormAction() === "edit" ? "PUT" : "POST";
934
+ if (actualMethod.toUpperCase() !== expectedMethod) {
935
+ logger.warn(
936
+ `[BaseFormFeature] Method mismatch: expected ${expectedMethod}, got ${actualMethod} (request method: ${ctx.req.method}, X-HTTP-Method-Override: ${methodOverride || "none"})`
937
+ );
938
+ }
751
939
  const originalData = { ...context.body };
752
940
  logger.info(
753
941
  `[BaseFormFeature] Original body data: ${JSON.stringify(originalData)}`
754
942
  );
755
943
  let data = this.preprocessFormData(context.body);
944
+ logger.info(
945
+ `[BaseFormFeature] Preprocessed data: ${JSON.stringify(data)}`
946
+ );
756
947
  if (!this.schema) {
757
948
  throw new Error("Schema is required for form validation");
758
949
  }
@@ -769,7 +960,10 @@ var BaseFormFeature = class extends BaseFeature {
769
960
  );
770
961
  return this.render(context, originalData);
771
962
  }
772
- const item = await this.handleSubmit(context, parseResult.data);
963
+ const item = await this.handleSubmit(
964
+ context,
965
+ parseResult.data
966
+ );
773
967
  if (!item) {
774
968
  context.sendError(
775
969
  this.getFormAction() === "create" ? "\u521B\u5EFA\u5931\u8D25" : "\u66F4\u65B0\u5931\u8D25",
@@ -787,6 +981,9 @@ var BaseFormFeature = class extends BaseFeature {
787
981
  `[BaseFormFeature] Dialog mode: setting refresh to close dialog and refresh list`
788
982
  );
789
983
  context.setRefresh(true);
984
+ if (context.redirectUrl) {
985
+ context.redirectUrl = void 0;
986
+ }
790
987
  return null;
791
988
  } else {
792
989
  const redirectUrl = this.getSuccessRedirectUrl(context, item);
@@ -799,12 +996,11 @@ var BaseFormFeature = class extends BaseFeature {
799
996
  }
800
997
  }
801
998
  formFieldNames;
999
+ groups;
802
1000
  /**
803
1001
  * 渲染表单页面
804
1002
  */
805
1003
  async render(context, initialData) {
806
- const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
807
- const fields = modelFieldsToFormFields(filteredFields);
808
1004
  let formData;
809
1005
  if (this.getFormAction() === "edit") {
810
1006
  if (initialData) {
@@ -826,6 +1022,50 @@ var BaseFormFeature = class extends BaseFeature {
826
1022
  }
827
1023
  const method = this.getFormAction() === "create" ? "post" : "put";
828
1024
  const formId = this.getFormId(context);
1025
+ if (this.groups && this.groups.length > 0) {
1026
+ if (!this.schema) {
1027
+ throw new Error("Schema is required when using groups");
1028
+ }
1029
+ const schema = this.schema;
1030
+ const groupSchemas = this.groups.map((group) => {
1031
+ const pickObject = group.fields.reduce(
1032
+ (acc, fieldName) => {
1033
+ acc[fieldName] = true;
1034
+ return acc;
1035
+ },
1036
+ {}
1037
+ );
1038
+ return {
1039
+ label: group.label,
1040
+ schema: schema.pick(pickObject),
1041
+ fields: group.fields
1042
+ };
1043
+ });
1044
+ const groupFields = groupSchemas.map(
1045
+ ({ label, schema: schema2, fields: fieldNames }) => {
1046
+ const groupFields2 = parseSchemaToFields(schema2);
1047
+ const formFields = modelFieldsToFormFields(groupFields2);
1048
+ return {
1049
+ label,
1050
+ fields: formFields
1051
+ };
1052
+ }
1053
+ );
1054
+ return /* @__PURE__ */ jsx(
1055
+ FormPage,
1056
+ {
1057
+ groups: groupFields,
1058
+ submitUrl,
1059
+ method,
1060
+ initialData: formData,
1061
+ formId,
1062
+ isDialog: context.isDialog,
1063
+ formFieldRenderers: this.formFieldRenderers
1064
+ }
1065
+ );
1066
+ }
1067
+ const filteredFields = this.formFieldNames ? filterFieldsByNames(this.fields || [], this.formFieldNames) : this.fields || [];
1068
+ const fields = modelFieldsToFormFields(filteredFields);
829
1069
  return /* @__PURE__ */ jsx(
830
1070
  FormPage,
831
1071
  {
@@ -833,7 +1073,9 @@ var BaseFormFeature = class extends BaseFeature {
833
1073
  submitUrl,
834
1074
  method,
835
1075
  initialData: formData,
836
- formId
1076
+ formId,
1077
+ isDialog: context.isDialog,
1078
+ formFieldRenderers: this.formFieldRenderers
837
1079
  }
838
1080
  );
839
1081
  }
@@ -942,6 +1184,8 @@ var DefaultCreateFeature = class extends BaseFormFeature {
942
1184
  this.fields = parseSchemaToFields(options.schema);
943
1185
  this.createItem = options.createItem;
944
1186
  this.formFieldNames = options.formFieldNames;
1187
+ this.groups = options.groups;
1188
+ this.formFieldRenderers = options.formFieldRenderers;
945
1189
  }
946
1190
  getFormAction() {
947
1191
  return "create";
@@ -1001,12 +1245,119 @@ var DefaultDeleteFeature = class extends BaseFeature {
1001
1245
  }
1002
1246
  }
1003
1247
  };
1248
+ function Card(props) {
1249
+ const {
1250
+ children,
1251
+ title,
1252
+ className = "",
1253
+ shadow = true,
1254
+ bordered = false,
1255
+ noPadding = false
1256
+ } = props;
1257
+ const baseClasses = "bg-white rounded-lg";
1258
+ const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
1259
+ const borderClass = bordered ? "border border-gray-200" : "";
1260
+ const paddingClass = noPadding ? "" : "p-6";
1261
+ return /* @__PURE__ */ jsxs(
1262
+ "div",
1263
+ {
1264
+ className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
1265
+ children: [
1266
+ title && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
1267
+ /* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
1268
+ ]
1269
+ }
1270
+ );
1271
+ }
1272
+ function renderDefaultValue(value) {
1273
+ if (value === null || value === void 0) {
1274
+ return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
1275
+ }
1276
+ if (Array.isArray(value)) {
1277
+ if (value.length === 0) {
1278
+ return /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "\u6682\u65E0\u6570\u636E" });
1279
+ }
1280
+ if (value.length > 0 && typeof value[0] === "object" && value[0] !== null) {
1281
+ return /* @__PURE__ */ jsxs("span", { className: "text-gray-600", children: [
1282
+ "\u5305\u542B ",
1283
+ value.length,
1284
+ " \u9879\uFF08\u5BF9\u8C61\u6570\u7EC4\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u81EA\u5B9A\u4E49\u6E32\u67D3\uFF09"
1285
+ ] });
1286
+ }
1287
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: value.map((item, index) => /* @__PURE__ */ jsx(
1288
+ "span",
1289
+ {
1290
+ className: "px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm",
1291
+ children: String(item)
1292
+ },
1293
+ index
1294
+ )) });
1295
+ }
1296
+ if (typeof value === "object") {
1297
+ if (value instanceof Date) {
1298
+ return /* @__PURE__ */ jsx("span", { children: value.toLocaleString() });
1299
+ }
1300
+ return /* @__PURE__ */ jsx("pre", { className: "bg-gray-50 border border-gray-200 rounded p-3 text-xs overflow-x-auto", children: JSON.stringify(value, null, 2) });
1301
+ }
1302
+ if (typeof value === "boolean") {
1303
+ return /* @__PURE__ */ jsx("span", { className: `px-2 py-1 rounded text-sm font-medium ${value ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-600"}`, children: value ? "\u662F" : "\u5426" });
1304
+ }
1305
+ if (typeof value === "number") {
1306
+ return /* @__PURE__ */ jsx("span", { children: value.toLocaleString() });
1307
+ }
1308
+ return /* @__PURE__ */ jsx("span", { children: String(value) });
1309
+ }
1310
+ function renderField(field, value, item) {
1311
+ let content;
1312
+ if (field.render) {
1313
+ const rendered = field.render(value, item);
1314
+ if (rendered === null || rendered === void 0) {
1315
+ content = /* @__PURE__ */ jsx("span", { className: "text-gray-400", children: "-" });
1316
+ } else if (typeof rendered === "string" || typeof rendered === "number" || typeof rendered === "boolean") {
1317
+ content = /* @__PURE__ */ jsx("span", { children: String(rendered) });
1318
+ } else {
1319
+ content = rendered;
1320
+ }
1321
+ } else {
1322
+ content = renderDefaultValue(value);
1323
+ }
1324
+ return /* @__PURE__ */ jsxs(
1325
+ "div",
1326
+ {
1327
+ className: "\r\n px-4 py-4 sm:px-6 sm:py-5\r\n flex flex-col\r\n hover:bg-gray-50/50 transition-colors duration-150\r\n group\r\n gap-1.5\r\n ",
1328
+ children: [
1329
+ /* @__PURE__ */ jsx("dt", { className: "\r\n text-xs sm:text-sm font-semibold text-gray-600 sm:text-gray-700\r\n flex items-start\r\n leading-tight sm:leading-5\r\n tracking-wide\r\n ", children: /* @__PURE__ */ jsx("span", { className: "min-w-0 uppercase sm:normal-case", children: field.label }) }),
1330
+ /* @__PURE__ */ jsx("dd", { className: "\r\n text-sm sm:text-base text-gray-900\r\n break-words\r\n leading-relaxed\r\n min-w-0\r\n ", children: /* @__PURE__ */ jsx("div", { className: "min-w-0", children: content }) })
1331
+ ]
1332
+ },
1333
+ field.key
1334
+ );
1335
+ }
1004
1336
  function DetailPage(props) {
1005
- const { item, fields } = props;
1006
- return /* @__PURE__ */ jsx("div", { className: "bg-white border border-gray-300 rounded p-4", children: /* @__PURE__ */ jsx("dl", { className: "grid grid-cols-1 gap-4", children: fields.map((field) => /* @__PURE__ */ jsxs("div", { children: [
1007
- /* @__PURE__ */ jsx("dt", { className: "font-semibold text-gray-700", children: field.label }),
1008
- /* @__PURE__ */ jsx("dd", { className: "mt-1 text-gray-900", children: String(item[field.key] ?? "-") })
1009
- ] }, field.key)) }) });
1337
+ const { item, fields, groups } = props;
1338
+ if (groups && groups.length > 0) {
1339
+ return /* @__PURE__ */ jsx("div", { className: "space-y-6", children: groups.map((group, groupIndex) => /* @__PURE__ */ jsx(
1340
+ Card,
1341
+ {
1342
+ title: group.label,
1343
+ shadow: true,
1344
+ bordered: true,
1345
+ noPadding: true,
1346
+ children: /* @__PURE__ */ jsx("dl", { className: "divide-y divide-gray-100", children: group.fields.map((field) => {
1347
+ const value = group.values[field.key];
1348
+ return renderField(field, value, item);
1349
+ }) })
1350
+ },
1351
+ groupIndex
1352
+ )) });
1353
+ }
1354
+ if (fields && fields.length > 0) {
1355
+ return /* @__PURE__ */ jsx("div", { className: "bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden", children: /* @__PURE__ */ jsx("dl", { className: "divide-y divide-gray-100", children: fields.map((field) => {
1356
+ const value = item[field.key];
1357
+ return renderField(field, value, item);
1358
+ }) }) });
1359
+ }
1360
+ return null;
1010
1361
  }
1011
1362
  var DefaultDetailFeature = class extends BaseFeature {
1012
1363
  getItem;
@@ -1014,6 +1365,8 @@ var DefaultDetailFeature = class extends BaseFeature {
1014
1365
  titleGetter;
1015
1366
  descriptionGetter;
1016
1367
  detailFieldNames;
1368
+ fieldRenderers;
1369
+ groups;
1017
1370
  constructor(options) {
1018
1371
  super({
1019
1372
  name: "detail",
@@ -1029,6 +1382,8 @@ var DefaultDetailFeature = class extends BaseFeature {
1029
1382
  this.titleGetter = options.getTitle;
1030
1383
  this.descriptionGetter = options.getDescription;
1031
1384
  this.detailFieldNames = options.detailFieldNames;
1385
+ this.fieldRenderers = options.fieldRenderers;
1386
+ this.groups = options.groups;
1032
1387
  }
1033
1388
  async getTitle(context) {
1034
1389
  if (this.titleGetter) {
@@ -1059,6 +1414,40 @@ var DefaultDetailFeature = class extends BaseFeature {
1059
1414
  if (!item) {
1060
1415
  return context.ctx.json({ error: "Not found" }, 404);
1061
1416
  }
1417
+ if (this.groups && this.groups.length > 0) {
1418
+ if (!this.schema) {
1419
+ throw new Error("Schema is required when using groups");
1420
+ }
1421
+ const schema = this.schema;
1422
+ const groupSchemas = this.groups.map((group) => {
1423
+ const pickObject = group.fields.reduce((acc, fieldName) => {
1424
+ acc[fieldName] = true;
1425
+ return acc;
1426
+ }, {});
1427
+ return {
1428
+ label: group.label,
1429
+ schema: schema.pick(pickObject),
1430
+ fields: group.fields
1431
+ };
1432
+ });
1433
+ const groupFields = groupSchemas.map(({ label, schema: schema2, fields: fieldNames }) => {
1434
+ const groupFields2 = parseSchemaToFields(schema2);
1435
+ const detailFields2 = groupFields2.map((field) => ({
1436
+ key: field.name,
1437
+ label: field.label,
1438
+ render: this.fieldRenderers?.[field.name]
1439
+ }));
1440
+ return {
1441
+ label,
1442
+ fields: detailFields2,
1443
+ values: fieldNames.reduce((acc, fieldName) => {
1444
+ acc[fieldName] = item[fieldName];
1445
+ return acc;
1446
+ }, {})
1447
+ };
1448
+ });
1449
+ return /* @__PURE__ */ jsx(DetailPage, { item, groups: groupFields });
1450
+ }
1062
1451
  const detailFields = this.detailFieldNames ? filterFieldsByNames(this.fields || [], this.detailFieldNames) : this.fields || [];
1063
1452
  if (this.detailFieldNames) {
1064
1453
  const systemFields = ["id", "createdAt", "updatedAt"];
@@ -1077,7 +1466,9 @@ var DefaultDetailFeature = class extends BaseFeature {
1077
1466
  const detailFieldNames = getFieldNamesFromFields(detailFields);
1078
1467
  const fields = detailFieldNames.map((fieldName) => ({
1079
1468
  key: fieldName,
1080
- label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName
1469
+ label: getFieldLabelFromFields(this.fields || [], fieldName) || fieldName,
1470
+ render: this.fieldRenderers?.[fieldName]
1471
+ // 如果有自定义渲染函数则使用
1081
1472
  }));
1082
1473
  return /* @__PURE__ */ jsx(DetailPage, { item, fields });
1083
1474
  }
@@ -1155,6 +1546,8 @@ var DefaultEditFeature = class extends BaseFormFeature {
1155
1546
  this.getItem = options.getItem;
1156
1547
  this.updateItem = options.updateItem;
1157
1548
  this.formFieldNames = options.formFieldNames;
1549
+ this.groups = options.groups;
1550
+ this.formFieldRenderers = options.formFieldRenderers;
1158
1551
  }
1159
1552
  getFormAction() {
1160
1553
  return "edit";
@@ -1390,30 +1783,6 @@ function Button(props) {
1390
1783
  }
1391
1784
  );
1392
1785
  }
1393
- function Card(props) {
1394
- const {
1395
- children,
1396
- title,
1397
- className = "",
1398
- shadow = true,
1399
- bordered = false,
1400
- noPadding = false
1401
- } = props;
1402
- const baseClasses = "bg-white rounded-lg";
1403
- const shadowClass = shadow ? "shadow-sm hover:shadow-md transition-shadow" : "";
1404
- const borderClass = bordered ? "border border-gray-200" : "";
1405
- const paddingClass = noPadding ? "" : "p-6";
1406
- return /* @__PURE__ */ jsxs(
1407
- "div",
1408
- {
1409
- className: `${baseClasses} ${shadowClass} ${borderClass} ${className}`,
1410
- children: [
1411
- title && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-b border-gray-200", children: /* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900", children: title }) }),
1412
- /* @__PURE__ */ jsx("div", { className: noPadding ? "" : paddingClass, children })
1413
- ]
1414
- }
1415
- );
1416
- }
1417
1786
  function EmptyState(props) {
1418
1787
  const { message = "\u6682\u65E0\u6570\u636E", children } = props;
1419
1788
  return /* @__PURE__ */ jsx("div", { className: "text-center py-12", children: children || /* @__PURE__ */ jsx("p", { className: "text-gray-500 text-sm", children: message }) });
@@ -1833,6 +2202,7 @@ var DefaultListFeature = class extends BaseFeature {
1833
2202
  deleteItem;
1834
2203
  listFieldNames;
1835
2204
  filterSchema;
2205
+ columnRenderers;
1836
2206
  constructor(options) {
1837
2207
  super({
1838
2208
  name: "list",
@@ -1847,6 +2217,7 @@ var DefaultListFeature = class extends BaseFeature {
1847
2217
  this.deleteItem = options.deleteItem;
1848
2218
  this.listFieldNames = options.listFieldNames;
1849
2219
  this.filterSchema = options.filterSchema;
2220
+ this.columnRenderers = options.columnRenderers;
1850
2221
  }
1851
2222
  getRoutes() {
1852
2223
  return [{ method: "get", path: "/list" }];
@@ -1859,7 +2230,9 @@ var DefaultListFeature = class extends BaseFeature {
1859
2230
  const filterFields = this.filterSchema ? modelFieldsToFormFields(parseSchemaToFields(this.filterSchema)) : [];
1860
2231
  const columns = listFieldNames.map((fieldName) => ({
1861
2232
  key: fieldName,
1862
- label: getFieldLabelFromFields(this.fields || [], fieldName)
2233
+ label: getFieldLabelFromFields(this.fields || [], fieldName),
2234
+ render: this.columnRenderers?.[fieldName]
2235
+ // 如果有自定义渲染函数则使用
1863
2236
  }));
1864
2237
  const model = context.model;
1865
2238
  const prefix = context.prefix || "";
@@ -2301,7 +2674,8 @@ function Dialog(props) {
2301
2674
  className = "",
2302
2675
  size = "lg",
2303
2676
  closeOnBackdropClick = true,
2304
- actions = []
2677
+ actions = [],
2678
+ fixedContentHeight = false
2305
2679
  } = props;
2306
2680
  const sizeClasses = {
2307
2681
  sm: "max-w-md",
@@ -2360,7 +2734,13 @@ function Dialog(props) {
2360
2734
  }
2361
2735
  )
2362
2736
  ] }),
2363
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-6 bg-gray-50", children }),
2737
+ /* @__PURE__ */ jsx(
2738
+ "div",
2739
+ {
2740
+ className: `${fixedContentHeight ? "h-[70vh]" : "flex-1"} overflow-y-auto ${fixedContentHeight ? "p-0" : "p-6"} bg-gray-50`,
2741
+ children
2742
+ }
2743
+ ),
2364
2744
  actions.length > 0 && /* @__PURE__ */ jsx("div", { className: "px-6 py-4 border-t border-gray-200 bg-white flex justify-end gap-2", children: actions.map((action, index) => renderActionButton(action, index)) })
2365
2745
  ]
2366
2746
  }
@@ -2621,6 +3001,13 @@ function BaseLayout(props) {
2621
3001
  {
2622
3002
  dangerouslySetInnerHTML: {
2623
3003
  __html: `
3004
+ /* \u5BB9\u5668\u67E5\u8BE2\u652F\u6301 - \u5982\u679C Tailwind CDN \u4E0D\u652F\u6301\uFF0C\u4F7F\u7528\u539F\u751F CSS \u5BB9\u5668\u67E5\u8BE2 */
3005
+ @supports (container-type: inline-size) {
3006
+ .\\@container {
3007
+ container-type: inline-size;
3008
+ }
3009
+ }
3010
+
2624
3011
  @keyframes fadeIn {
2625
3012
  from { opacity: 0;}
2626
3013
  to { opacity: 1;}
@@ -3143,9 +3530,9 @@ async function renderResult(ctx, context, result, renderOptions) {
3143
3530
  headers
3144
3531
  );
3145
3532
  }
3146
- if (context.redirectUrl) {
3533
+ if (context.redirectUrl && !context.refresh) {
3147
3534
  logger.info(
3148
- `[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest})`
3535
+ `[ResponseRenderer] Redirect URL found: ${context.redirectUrl} (isHtmxRequest: ${context.isHtmxRequest}, isDialog: ${context.isDialog})`
3149
3536
  );
3150
3537
  if (context.isHtmxRequest) {
3151
3538
  return ctx.html(/* @__PURE__ */ jsx("div", {}), 200, {
@@ -3154,7 +3541,11 @@ async function renderResult(ctx, context, result, renderOptions) {
3154
3541
  } else {
3155
3542
  return ctx.redirect(context.redirectUrl);
3156
3543
  }
3157
- } else {
3544
+ } else if (context.redirectUrl && context.refresh) {
3545
+ logger.info(
3546
+ `[ResponseRenderer] Both redirect URL and refresh are set, using refresh (isDialog: ${context.isDialog})`
3547
+ );
3548
+ } else if (!context.redirectUrl) {
3158
3549
  logger.info(
3159
3550
  `[ResponseRenderer] No redirect URL found (result: ${result === null ? "null" : typeof result}, isHtmxRequest: ${context.isHtmxRequest})`
3160
3551
  );
@@ -3205,6 +3596,8 @@ async function renderResult(ctx, context, result, renderOptions) {
3205
3596
  if (context.isDialog) {
3206
3597
  const dialogSize = renderOptions.feature?.dialogSize || "lg";
3207
3598
  const closeOnBackdropClick = renderOptions.feature?.closeOnBackdropClick ?? true;
3599
+ const isFormFeature = renderOptions.feature?.type === "create" || renderOptions.feature?.type === "edit";
3600
+ const fixedContentHeight = isFormFeature;
3208
3601
  return ctx.html(
3209
3602
  /* @__PURE__ */ jsxs(Fragment, { children: [
3210
3603
  /* @__PURE__ */ jsx(
@@ -3214,6 +3607,7 @@ async function renderResult(ctx, context, result, renderOptions) {
3214
3607
  size: dialogSize,
3215
3608
  closeOnBackdropClick,
3216
3609
  actions,
3610
+ fixedContentHeight,
3217
3611
  children: result
3218
3612
  }
3219
3613
  ),
@@ -3474,6 +3868,28 @@ function registerPageRoutes(page, options) {
3474
3868
  });
3475
3869
  };
3476
3870
  options.hono[route.method](fullPath, handler);
3871
+ if (route.method === "put" || route.method === "delete") {
3872
+ const postHandler = async (ctx) => {
3873
+ const methodOverride = ctx.req.header("X-HTTP-Method-Override");
3874
+ const expectedMethod = route.method.toUpperCase();
3875
+ if (methodOverride === expectedMethod) {
3876
+ logger.info(
3877
+ `[HtmxAdminPlugin] Method override detected: POST ${fullPath} -> ${expectedMethod} (feature: ${feature.name})`
3878
+ );
3879
+ return handleRequest(ctx, page, feature, {
3880
+ options: options.options
3881
+ });
3882
+ }
3883
+ logger.warn(
3884
+ `[HtmxAdminPlugin] POST request to ${fullPath} without matching X-HTTP-Method-Override header (got: ${methodOverride || "none"}, expected: ${expectedMethod})`
3885
+ );
3886
+ return ctx.text("Method Not Allowed", 405);
3887
+ };
3888
+ logger.info(
3889
+ `[HtmxAdminPlugin] Registering POST route for method override: POST ${fullPath} (actual method: ${route.method.toUpperCase()}, feature: ${feature.name})`
3890
+ );
3891
+ options.hono.post(fullPath, postHandler);
3892
+ }
3477
3893
  }
3478
3894
  }
3479
3895
  }
@@ -3563,5 +3979,80 @@ var HtmxAdminPlugin = class {
3563
3979
  registerHomeRedirect(this.pages, routeOptions);
3564
3980
  }
3565
3981
  };
3982
+ function createFormFieldXData(options) {
3983
+ const {
3984
+ fieldName,
3985
+ dataKey,
3986
+ defaultValue = [],
3987
+ customData = {},
3988
+ customMethods = {}
3989
+ } = options;
3990
+ const dataEntries = [];
3991
+ dataEntries.push(`${dataKey}: ${JSON.stringify(defaultValue)}`);
3992
+ for (const [key, value] of Object.entries(customData)) {
3993
+ dataEntries.push(`${key}: ${JSON.stringify(value)}`);
3994
+ }
3995
+ const methodEntries = [];
3996
+ methodEntries.push(`init() {
3997
+ const dataAttr = this.$el.getAttribute('data-initial-value');
3998
+ if (dataAttr) {
3999
+ try {
4000
+ this.${dataKey} = JSON.parse(dataAttr);
4001
+ } catch (e) {
4002
+ console.error('Failed to parse initial value:', e);
4003
+ this.${dataKey} = ${JSON.stringify(defaultValue)};
4004
+ }
4005
+ }
4006
+ this.updateHiddenField();
4007
+ }`);
4008
+ methodEntries.push(`updateHiddenField() {
4009
+ const hiddenInput = this.$el.querySelector('input[name="${fieldName}"][type="hidden"]');
4010
+ if (hiddenInput) {
4011
+ hiddenInput.value = JSON.stringify(this.${dataKey});
4012
+ }
4013
+ }`);
4014
+ for (const [methodName, methodBody] of Object.entries(customMethods)) {
4015
+ methodEntries.push(`${methodName}${methodBody}`);
4016
+ }
4017
+ const dataStr = dataEntries.join(",\n ");
4018
+ const methodsStr = methodEntries.join(",\n ");
4019
+ return `{
4020
+ ${dataStr},
4021
+ ${methodsStr}
4022
+ }`;
4023
+ }
4024
+ function FormFieldWrapper(props) {
4025
+ const {
4026
+ fieldName,
4027
+ initialValue,
4028
+ xData,
4029
+ autoSync = false,
4030
+ children,
4031
+ className = "space-y-4"
4032
+ } = props;
4033
+ const initialValueJson = JSON.stringify(initialValue);
4034
+ return /* @__PURE__ */ jsxs(
4035
+ "div",
4036
+ {
4037
+ "x-data": xData,
4038
+ "data-initial-value": initialValueJson,
4039
+ "x-init": "init()",
4040
+ ...autoSync ? { "x-effect": "updateHiddenField()" } : {},
4041
+ className,
4042
+ children: [
4043
+ /* @__PURE__ */ jsx(
4044
+ "input",
4045
+ {
4046
+ type: "hidden",
4047
+ name: fieldName,
4048
+ value: "",
4049
+ "data-testid": `hidden-${fieldName}`
4050
+ }
4051
+ ),
4052
+ children
4053
+ ]
4054
+ }
4055
+ );
4056
+ }
3566
4057
 
3567
- export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, HtmxAdminPlugin, LoadingBar, PageModel, checkUserPermission, getUserInfo, modelNameToPath, parseListParams };
4058
+ export { BaseFeature, CustomFeature, DefaultCreateFeature, DefaultDeleteFeature, DefaultDetailFeature, DefaultEditFeature, DefaultListFeature, Dialog, ErrorAlert, FormFieldWrapper, HtmxAdminPlugin, LoadingBar, PageModel, checkUserPermission, createFormFieldXData, getUserInfo, modelNameToPath, parseListParams };