@braedonsaunders/appkit-forms 0.2.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.
Files changed (120) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +74 -0
  3. package/canvas-editor.d.ts +24 -0
  4. package/canvas-editor.d.ts.map +1 -0
  5. package/canvas-editor.js +115 -0
  6. package/canvas-editor.js.map +1 -0
  7. package/canvas-runtime.d.ts +12 -0
  8. package/canvas-runtime.d.ts.map +1 -0
  9. package/canvas-runtime.js +54 -0
  10. package/canvas-runtime.js.map +1 -0
  11. package/element-preview.d.ts +9 -0
  12. package/element-preview.d.ts.map +1 -0
  13. package/element-preview.js +167 -0
  14. package/element-preview.js.map +1 -0
  15. package/form-designer.d.ts +29 -0
  16. package/form-designer.d.ts.map +1 -0
  17. package/form-designer.js +214 -0
  18. package/form-designer.js.map +1 -0
  19. package/form-flow-validation.d.ts +5 -0
  20. package/form-flow-validation.d.ts.map +1 -0
  21. package/form-flow-validation.js +154 -0
  22. package/form-flow-validation.js.map +1 -0
  23. package/form-renderer.d.ts +37 -0
  24. package/form-renderer.d.ts.map +1 -0
  25. package/form-renderer.js +282 -0
  26. package/form-renderer.js.map +1 -0
  27. package/formula-builder.d.ts +25 -0
  28. package/formula-builder.d.ts.map +1 -0
  29. package/formula-builder.js +310 -0
  30. package/formula-builder.js.map +1 -0
  31. package/generated-copy.d.ts +17 -0
  32. package/generated-copy.d.ts.map +1 -0
  33. package/generated-copy.js +396 -0
  34. package/generated-copy.js.map +1 -0
  35. package/index.d.ts +27 -0
  36. package/index.d.ts.map +1 -0
  37. package/index.js +25 -0
  38. package/index.js.map +1 -0
  39. package/logic-builder.d.ts +27 -0
  40. package/logic-builder.d.ts.map +1 -0
  41. package/logic-builder.js +106 -0
  42. package/logic-builder.js.map +1 -0
  43. package/package.json +54 -0
  44. package/photo-field-state.d.ts +9 -0
  45. package/photo-field-state.d.ts.map +1 -0
  46. package/photo-field-state.js +11 -0
  47. package/photo-field-state.js.map +1 -0
  48. package/production-designer-adapter.d.ts +100 -0
  49. package/production-designer-adapter.d.ts.map +1 -0
  50. package/production-designer-adapter.js +2 -0
  51. package/production-designer-adapter.js.map +1 -0
  52. package/production-file-upload.d.ts +27 -0
  53. package/production-file-upload.d.ts.map +1 -0
  54. package/production-file-upload.js +105 -0
  55. package/production-file-upload.js.map +1 -0
  56. package/production-form-designer.d.ts +3 -0
  57. package/production-form-designer.d.ts.map +1 -0
  58. package/production-form-designer.js +1124 -0
  59. package/production-form-designer.js.map +1 -0
  60. package/production-form-renderer.d.ts +16 -0
  61. package/production-form-renderer.d.ts.map +1 -0
  62. package/production-form-renderer.js +2134 -0
  63. package/production-form-renderer.js.map +1 -0
  64. package/production-runtime-adapter.d.ts +187 -0
  65. package/production-runtime-adapter.d.ts.map +1 -0
  66. package/production-runtime-adapter.js +2 -0
  67. package/production-runtime-adapter.js.map +1 -0
  68. package/production-runtime-context.d.ts +29 -0
  69. package/production-runtime-context.d.ts.map +1 -0
  70. package/production-runtime-context.js +35 -0
  71. package/production-runtime-context.js.map +1 -0
  72. package/production-runtime-layout.d.ts +21 -0
  73. package/production-runtime-layout.d.ts.map +1 -0
  74. package/production-runtime-layout.js +33 -0
  75. package/production-runtime-layout.js.map +1 -0
  76. package/properties.d.ts +36 -0
  77. package/properties.d.ts.map +1 -0
  78. package/properties.js +133 -0
  79. package/properties.js.map +1 -0
  80. package/record-actions-panel.d.ts +28 -0
  81. package/record-actions-panel.d.ts.map +1 -0
  82. package/record-actions-panel.js +136 -0
  83. package/record-actions-panel.js.map +1 -0
  84. package/record-behavior-panel.d.ts +15 -0
  85. package/record-behavior-panel.d.ts.map +1 -0
  86. package/record-behavior-panel.js +59 -0
  87. package/record-behavior-panel.js.map +1 -0
  88. package/record-config.d.ts +44 -0
  89. package/record-config.d.ts.map +1 -0
  90. package/record-config.js +2 -0
  91. package/record-config.js.map +1 -0
  92. package/record-list-panel.d.ts +16 -0
  93. package/record-list-panel.d.ts.map +1 -0
  94. package/record-list-panel.js +86 -0
  95. package/record-list-panel.js.map +1 -0
  96. package/risk-matrix.d.ts +93 -0
  97. package/risk-matrix.d.ts.map +1 -0
  98. package/risk-matrix.js +239 -0
  99. package/risk-matrix.js.map +1 -0
  100. package/runtime-controls.d.ts +19 -0
  101. package/runtime-controls.d.ts.map +1 -0
  102. package/runtime-controls.js +61 -0
  103. package/runtime-controls.js.map +1 -0
  104. package/sketch-pad-client.d.ts +4 -0
  105. package/sketch-pad-client.d.ts.map +1 -0
  106. package/sketch-pad-client.js +54 -0
  107. package/sketch-pad-client.js.map +1 -0
  108. package/sketch-pad.d.ts +13 -0
  109. package/sketch-pad.d.ts.map +1 -0
  110. package/sketch-pad.js +17 -0
  111. package/sketch-pad.js.map +1 -0
  112. package/styles.css +7 -0
  113. package/text.d.ts +5 -0
  114. package/text.d.ts.map +1 -0
  115. package/text.js +10 -0
  116. package/text.js.map +1 -0
  117. package/workflow-properties.d.ts +16 -0
  118. package/workflow-properties.d.ts.map +1 -0
  119. package/workflow-properties.js +31 -0
  120. package/workflow-properties.js.map +1 -0
@@ -0,0 +1,154 @@
1
+ import { FORM_STATUS_VALUES, FORM_TEMPLATE_ACTIONS, FORM_TEMPLATE_TRIGGERS, SKIP_FIELD_TYPES, hasPhotosCompanion, isAttachmentArrayField, labelText, lintAutomationGraph, storesResponseValue, } from '@braedonsaunders/appkit-forms-core';
2
+ import { DEFAULT_LOCALE } from '@braedonsaunders/appkit-i18n';
3
+ function fieldKind(field) {
4
+ if (field.type === 'number' ||
5
+ field.type === 'currency' ||
6
+ field.type === 'percentage' ||
7
+ field.type === 'slider' ||
8
+ field.type === 'rating')
9
+ return 'number';
10
+ if (field.type === 'date' || field.type === 'datetime' || field.type === 'time')
11
+ return 'date';
12
+ if (field.type === 'person_picker' || field.type === 'multi_person_picker')
13
+ return 'person';
14
+ if (['customer_picker', 'project_picker', 'site_picker', 'area_picker'].includes(field.type)) {
15
+ return 'org_unit';
16
+ }
17
+ if (['select', 'radio', 'pass_fail_na', 'rating', 'traffic_light', 'multi_select'].includes(field.type)) {
18
+ return 'enum';
19
+ }
20
+ return 'text';
21
+ }
22
+ export function formFlowProfile(templateId, label, schema, locale = DEFAULT_LOCALE, defaultLocale = DEFAULT_LOCALE) {
23
+ const fields = schema.sections.flatMap((section) => section.repeating
24
+ ? []
25
+ : section.fields
26
+ .filter((field) => !SKIP_FIELD_TYPES.has(field.type))
27
+ .map((field) => ({
28
+ key: field.id,
29
+ label: labelText(field.label, field.id, locale, defaultLocale),
30
+ kind: fieldKind(field),
31
+ writable: storesResponseValue(field),
32
+ photoSource: isAttachmentArrayField(field.type) || hasPhotosCompanion(field.type),
33
+ textOutput: storesResponseValue(field) && ['text', 'long_text', 'rich_text'].includes(field.type),
34
+ })));
35
+ fields.push({
36
+ key: 'compliance_score',
37
+ label: 'Compliance score',
38
+ kind: 'number',
39
+ writable: false,
40
+ photoSource: false,
41
+ textOutput: false,
42
+ }, {
43
+ key: 'compliance_status',
44
+ label: 'Compliance status',
45
+ kind: 'enum',
46
+ writable: false,
47
+ photoSource: false,
48
+ textOutput: false,
49
+ });
50
+ return {
51
+ subjectType: 'form_template',
52
+ subjectKey: templateId,
53
+ label,
54
+ triggers: FORM_TEMPLATE_TRIGGERS,
55
+ actions: FORM_TEMPLATE_ACTIONS,
56
+ statusValues: FORM_STATUS_VALUES,
57
+ fields,
58
+ };
59
+ }
60
+ function actionFieldReferences(graph) {
61
+ const refs = [];
62
+ for (const node of graph.nodes) {
63
+ if (node.data.kind === 'gate' && node.data.gate.assignee.type === 'field') {
64
+ refs.push({ where: `Gate ${node.id} assignee`, field: node.data.gate.assignee.field });
65
+ continue;
66
+ }
67
+ if (node.data.kind !== 'action')
68
+ continue;
69
+ const action = node.data.action;
70
+ if (action.action === 'send_email') {
71
+ for (const target of action.to) {
72
+ if (target.type === 'field') {
73
+ refs.push({ where: `Action ${node.id} recipient`, field: target.field });
74
+ }
75
+ }
76
+ }
77
+ else if (action.action === 'create_capa' && action.assignee?.type === 'field') {
78
+ refs.push({ where: `Action ${node.id} assignee`, field: action.assignee.field });
79
+ }
80
+ }
81
+ return refs;
82
+ }
83
+ export function lintFormFlowGraph(graph, templateId, label, schema) {
84
+ const profile = formFlowProfile(templateId, label, schema);
85
+ const fields = new Map();
86
+ for (const section of schema.sections) {
87
+ if (section.repeating)
88
+ continue;
89
+ for (const field of section.fields)
90
+ fields.set(field.id, field);
91
+ }
92
+ const readableIds = new Set(profile.fields.map((field) => field.key));
93
+ const writableIds = new Set([...fields.values()].filter((field) => storesResponseValue(field)).map((field) => field.id));
94
+ const errors = lintAutomationGraph(graph, readableIds, profile);
95
+ for (const ref of actionFieldReferences(graph)) {
96
+ if (!readableIds.has(ref.field))
97
+ errors.push(`${ref.where} references unknown field "${ref.field}"`);
98
+ }
99
+ for (const node of graph.nodes) {
100
+ if (node.data.kind === 'trigger' && node.data.trigger.trigger === 'manual') {
101
+ if (!node.data.trigger.buttonId.trim() || !node.data.trigger.label.trim()) {
102
+ errors.push(`Trigger ${node.id}: manual buttons require an id and label.`);
103
+ }
104
+ }
105
+ if (node.data.kind === 'gate') {
106
+ if (!node.data.gate.title.trim())
107
+ errors.push(`Gate ${node.id}: title is required.`);
108
+ continue;
109
+ }
110
+ if (node.data.kind !== 'action')
111
+ continue;
112
+ const action = node.data.action;
113
+ if (action.action === 'set_field' && !writableIds.has(action.field)) {
114
+ errors.push(`Action ${node.id}: set_field must target a stored, top-level response field.`);
115
+ }
116
+ else if (action.action === 'analyze_photos') {
117
+ const photoField = fields.get(action.fieldId);
118
+ if (!photoField ||
119
+ (!isAttachmentArrayField(photoField.type) && !hasPhotosCompanion(photoField.type))) {
120
+ errors.push(`Action ${node.id}: analyze_photos requires a top-level photo field.`);
121
+ }
122
+ if (action.storeInField) {
123
+ const output = fields.get(action.storeInField);
124
+ if (!output ||
125
+ !storesResponseValue(output) ||
126
+ !['text', 'long_text', 'rich_text'].includes(output.type)) {
127
+ errors.push(`Action ${node.id}: photo analysis output requires a text response field.`);
128
+ }
129
+ }
130
+ }
131
+ else if (action.action === 'start_monitored_session') {
132
+ for (const [name, fieldId] of [
133
+ ['interval', action.intervalFieldKey],
134
+ ['grace', action.graceFieldKey],
135
+ ['duration', action.durationFieldKey],
136
+ ]) {
137
+ if (fieldId && !['number', 'slider'].includes(fields.get(fieldId)?.type ?? '')) {
138
+ errors.push(`Action ${node.id}: ${name} binding requires a top-level number field.`);
139
+ }
140
+ }
141
+ }
142
+ else if (action.action === 'create_capa' && !action.titleTemplate.trim()) {
143
+ errors.push(`Action ${node.id}: CAPA title is required.`);
144
+ }
145
+ else if (action.action === 'create_incident' && !action.titleTemplate.trim()) {
146
+ errors.push(`Action ${node.id}: incident title is required.`);
147
+ }
148
+ else if (action.action === 'notify_role' && (!action.role.trim() || !action.message.trim())) {
149
+ errors.push(`Action ${node.id}: notification role and message are required.`);
150
+ }
151
+ }
152
+ return Array.from(new Set(errors));
153
+ }
154
+ //# sourceMappingURL=form-flow-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-flow-validation.js","sourceRoot":"","sources":["../src/form-flow-validation.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n FORM_STATUS_VALUES,\n FORM_TEMPLATE_ACTIONS,\n FORM_TEMPLATE_TRIGGERS,\n SKIP_FIELD_TYPES,\n hasPhotosCompanion,\n isAttachmentArrayField,\n labelText,\n lintAutomationGraph,\n storesResponseValue,\n type AutomationGraph,\n type FlowFieldKind,\n type FlowSubjectProfile,\n type FormField,\n type FormSchemaV1,\n} from '@braedonsaunders/appkit-forms-core'\nimport { DEFAULT_LOCALE, type AppLocale } from '@braedonsaunders/appkit-i18n'\n\nfunction fieldKind(field: FormField): FlowFieldKind {\n if (\n field.type === 'number' ||\n field.type === 'currency' ||\n field.type === 'percentage' ||\n field.type === 'slider' ||\n field.type === 'rating'\n ) return 'number'\n if (field.type === 'date' || field.type === 'datetime' || field.type === 'time') return 'date'\n if (field.type === 'person_picker' || field.type === 'multi_person_picker') return 'person'\n if (['customer_picker', 'project_picker', 'site_picker', 'area_picker'].includes(field.type)) {\n return 'org_unit'\n }\n if (\n ['select', 'radio', 'pass_fail_na', 'rating', 'traffic_light', 'multi_select'].includes(\n field.type,\n )\n ) {\n return 'enum'\n }\n return 'text'\n}\n\nexport function formFlowProfile(\n templateId: string,\n label: string,\n schema: FormSchemaV1,\n locale: AppLocale = DEFAULT_LOCALE,\n defaultLocale: AppLocale = DEFAULT_LOCALE,\n): FlowSubjectProfile {\n const fields = schema.sections.flatMap((section) =>\n section.repeating\n ? []\n : section.fields\n .filter((field) => !SKIP_FIELD_TYPES.has(field.type))\n .map((field) => ({\n key: field.id,\n label: labelText(field.label, field.id, locale, defaultLocale),\n kind: fieldKind(field),\n writable: storesResponseValue(field),\n photoSource: isAttachmentArrayField(field.type) || hasPhotosCompanion(field.type),\n textOutput:\n storesResponseValue(field) && ['text', 'long_text', 'rich_text'].includes(field.type),\n })),\n )\n fields.push(\n {\n key: 'compliance_score',\n label: 'Compliance score',\n kind: 'number',\n writable: false,\n photoSource: false,\n textOutput: false,\n },\n {\n key: 'compliance_status',\n label: 'Compliance status',\n kind: 'enum',\n writable: false,\n photoSource: false,\n textOutput: false,\n },\n )\n return {\n subjectType: 'form_template',\n subjectKey: templateId,\n label,\n triggers: FORM_TEMPLATE_TRIGGERS,\n actions: FORM_TEMPLATE_ACTIONS,\n statusValues: FORM_STATUS_VALUES,\n fields,\n }\n}\n\nfunction actionFieldReferences(graph: AutomationGraph): Array<{ where: string; field: string }> {\n const refs: Array<{ where: string; field: string }> = []\n for (const node of graph.nodes) {\n if (node.data.kind === 'gate' && node.data.gate.assignee.type === 'field') {\n refs.push({ where: `Gate ${node.id} assignee`, field: node.data.gate.assignee.field })\n continue\n }\n if (node.data.kind !== 'action') continue\n const action = node.data.action\n if (action.action === 'send_email') {\n for (const target of action.to) {\n if (target.type === 'field') {\n refs.push({ where: `Action ${node.id} recipient`, field: target.field })\n }\n }\n } else if (action.action === 'create_capa' && action.assignee?.type === 'field') {\n refs.push({ where: `Action ${node.id} assignee`, field: action.assignee.field })\n }\n }\n return refs\n}\n\nexport function lintFormFlowGraph(\n graph: AutomationGraph,\n templateId: string,\n label: string,\n schema: FormSchemaV1,\n): string[] {\n const profile = formFlowProfile(templateId, label, schema)\n const fields = new Map<string, FormField>()\n for (const section of schema.sections) {\n if (section.repeating) continue\n for (const field of section.fields) fields.set(field.id, field)\n }\n const readableIds = new Set(profile.fields.map((field) => field.key))\n const writableIds = new Set(\n [...fields.values()].filter((field) => storesResponseValue(field)).map((field) => field.id),\n )\n const errors = lintAutomationGraph(graph, readableIds, profile)\n\n for (const ref of actionFieldReferences(graph)) {\n if (!readableIds.has(ref.field))\n errors.push(`${ref.where} references unknown field \"${ref.field}\"`)\n }\n\n for (const node of graph.nodes) {\n if (node.data.kind === 'trigger' && node.data.trigger.trigger === 'manual') {\n if (!node.data.trigger.buttonId.trim() || !node.data.trigger.label.trim()) {\n errors.push(`Trigger ${node.id}: manual buttons require an id and label.`)\n }\n }\n if (node.data.kind === 'gate') {\n if (!node.data.gate.title.trim()) errors.push(`Gate ${node.id}: title is required.`)\n continue\n }\n if (node.data.kind !== 'action') continue\n const action = node.data.action\n if (action.action === 'set_field' && !writableIds.has(action.field)) {\n errors.push(`Action ${node.id}: set_field must target a stored, top-level response field.`)\n } else if (action.action === 'analyze_photos') {\n const photoField = fields.get(action.fieldId)\n if (\n !photoField ||\n (!isAttachmentArrayField(photoField.type) && !hasPhotosCompanion(photoField.type))\n ) {\n errors.push(`Action ${node.id}: analyze_photos requires a top-level photo field.`)\n }\n if (action.storeInField) {\n const output = fields.get(action.storeInField)\n if (\n !output ||\n !storesResponseValue(output) ||\n !['text', 'long_text', 'rich_text'].includes(output.type)\n ) {\n errors.push(`Action ${node.id}: photo analysis output requires a text response field.`)\n }\n }\n } else if (action.action === 'start_monitored_session') {\n for (const [name, fieldId] of [\n ['interval', action.intervalFieldKey],\n ['grace', action.graceFieldKey],\n ['duration', action.durationFieldKey],\n ] as const) {\n if (fieldId && !['number', 'slider'].includes(fields.get(fieldId)?.type ?? '')) {\n errors.push(`Action ${node.id}: ${name} binding requires a top-level number field.`)\n }\n }\n } else if (action.action === 'create_capa' && !action.titleTemplate.trim()) {\n errors.push(`Action ${node.id}: CAPA title is required.`)\n } else if (action.action === 'create_incident' && !action.titleTemplate.trim()) {\n errors.push(`Action ${node.id}: incident title is required.`)\n } else if (action.action === 'notify_role' && (!action.role.trim() || !action.message.trim())) {\n errors.push(`Action ${node.id}: notification role and message are required.`)\n }\n }\n return Array.from(new Set(errors))\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import * as React from 'react';
2
+ import { type EntityAttrsByField, type EvalContext, type FieldType, type FormField, type FormSchemaV1 } from '@braedonsaunders/appkit-forms-core';
3
+ import { type AppLocale } from '@braedonsaunders/appkit-i18n';
4
+ import type { RiskMatrixConfig } from './risk-matrix.js';
5
+ export type FormValues = Record<string, unknown>;
6
+ export type FormOption = {
7
+ value: string;
8
+ label: string;
9
+ hint?: string;
10
+ };
11
+ export type FormFieldAdapterProps = {
12
+ field: FormField;
13
+ value: unknown;
14
+ onChange: (value: unknown) => void;
15
+ error?: string;
16
+ disabled: boolean;
17
+ locale: AppLocale;
18
+ };
19
+ export type FormFieldAdapter = (props: FormFieldAdapterProps) => React.ReactNode;
20
+ export type FormRendererProps = {
21
+ schema: FormSchemaV1;
22
+ values?: FormValues;
23
+ defaultValues?: FormValues;
24
+ onChange?: (values: FormValues) => void;
25
+ onSubmit?: (values: FormValues) => void | Promise<void>;
26
+ locale?: AppLocale;
27
+ disabled?: boolean;
28
+ submitLabel?: string;
29
+ optionsByField?: Readonly<Record<string, readonly FormOption[]>>;
30
+ fieldAdapters?: Partial<Record<FieldType, FormFieldAdapter>>;
31
+ className?: string;
32
+ requestContext?: EvalContext['requestContext'];
33
+ entities?: EntityAttrsByField;
34
+ riskMatrix?: RiskMatrixConfig;
35
+ };
36
+ export declare function FormRenderer({ schema, values: controlledValues, defaultValues, onChange, onSubmit, locale, disabled, submitLabel, optionsByField, fieldAdapters, className, requestContext, entities, riskMatrix, }: FormRendererProps): React.JSX.Element;
37
+ //# sourceMappingURL=form-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-renderer.d.ts","sourceRoot":"","sources":["../src/form-renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAKL,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,YAAY,EAElB,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAc7E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAChD,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAExE,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,KAAK,CAAC,SAAS,CAAA;AAEhF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,YAAY,CAAA;IACpB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,aAAa,CAAC,EAAE,UAAU,CAAA;IAC1B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAA;IACvC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvD,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC,CAAC,CAAA;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,cAAc,CAAC,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAA;IAC9C,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAA;CAC9B,CAAA;AAeD,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,MAAM,EAAE,gBAAgB,EACxB,aAAkB,EAClB,QAAQ,EACR,QAAQ,EACR,MAAuB,EACvB,QAAgB,EAChB,WAAsB,EACtB,cAAmB,EACnB,aAAkB,EAClB,SAAS,EACT,cAAc,EACd,QAAa,EACb,UAAU,GACX,EAAE,iBAAiB,qBA+HnB"}
@@ -0,0 +1,282 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from 'react';
4
+ import { ArrowDown, ArrowUp, Plus, Trash2 } from 'lucide-react';
5
+ import { evaluateLogicRule, evaluateFormulaTree, resolveDefaultValue, validateResponse, } from '@braedonsaunders/appkit-forms-core';
6
+ import { DEFAULT_LOCALE } from '@braedonsaunders/appkit-i18n';
7
+ import { Alert, AlertDescription, Button, Checkbox, Input, Label, Textarea, cn, } from '@braedonsaunders/appkit-ui';
8
+ import { RichTextEditor } from '@braedonsaunders/appkit-editor';
9
+ import { readText } from './text.js';
10
+ import { AddressField, GpsField, MatrixField, RiskField, SignatureField, SketchField, TableField } from './runtime-controls.js';
11
+ const ADAPTER_FIELD_TYPES = new Set([
12
+ 'photo',
13
+ 'photo_upload',
14
+ 'photo_ai',
15
+ 'photo_annotated',
16
+ 'file',
17
+ 'video',
18
+ 'audio',
19
+ 'lookup',
20
+ 'data_table',
21
+ 'metric',
22
+ ]);
23
+ export function FormRenderer({ schema, values: controlledValues, defaultValues = {}, onChange, onSubmit, locale = DEFAULT_LOCALE, disabled = false, submitLabel = 'Submit', optionsByField = {}, fieldAdapters = {}, className, requestContext, entities = {}, riskMatrix, }) {
24
+ const initialValues = React.useMemo(() => applySchemaDefaults(schema, defaultValues, requestContext, entities), [defaultValues, entities, requestContext, schema]);
25
+ const [internalValues, setInternalValues] = React.useState(initialValues);
26
+ const [errors, setErrors] = React.useState([]);
27
+ const [submitting, setSubmitting] = React.useState(false);
28
+ const [stepIndex, setStepIndex] = React.useState(0);
29
+ const [activeTabId, setActiveTabId] = React.useState(schema.tabs?.[0]?.id ?? '');
30
+ const values = controlledValues ?? internalValues;
31
+ const errorMap = new Map(errors.map((error) => [error.fieldId, error.message]));
32
+ const rows = Object.fromEntries(schema.sections.filter((section) => section.repeating).map((section) => [section.id, Array.isArray(values[section.id]) ? values[section.id] : []]));
33
+ const evalContext = { values, rows, entities, requestContext };
34
+ const steps = schema.workflow?.steps ?? [];
35
+ const currentStep = steps[Math.min(stepIndex, Math.max(0, steps.length - 1))];
36
+ const stepSections = currentStep
37
+ ? schema.sections.filter((section) => (section.step ?? steps[0]?.key) === currentStep.key)
38
+ : schema.sections;
39
+ const tabs = schema.tabs ?? [];
40
+ const visibleSections = tabs.length > 1 && steps.length <= 1
41
+ ? stepSections.filter((section) => (section.tabId ?? tabs[0]?.id) === activeTabId)
42
+ : stepSections;
43
+ function update(fieldId, next) {
44
+ const updated = { ...values, [fieldId]: next };
45
+ if (controlledValues === undefined)
46
+ setInternalValues(updated);
47
+ onChange?.(updated);
48
+ if (errorMap.has(fieldId))
49
+ setErrors((current) => current.filter((error) => error.fieldId !== fieldId));
50
+ }
51
+ async function submit(event) {
52
+ event.preventDefault();
53
+ const nextErrors = validateResponse(schema, values, 'submit');
54
+ setErrors(nextErrors);
55
+ if (nextErrors.length > 0 || !onSubmit)
56
+ return;
57
+ setSubmitting(true);
58
+ try {
59
+ await onSubmit(values);
60
+ }
61
+ finally {
62
+ setSubmitting(false);
63
+ }
64
+ }
65
+ function nextStep() {
66
+ const sectionIds = new Set(stepSections.map((section) => section.id));
67
+ const fieldIds = new Set(stepSections.flatMap((section) => section.fields.map((field) => field.id)));
68
+ const nextErrors = validateResponse(schema, values, 'submit').filter((error) => {
69
+ if (error.fieldId.startsWith('__section_'))
70
+ return sectionIds.has(error.fieldId.slice(10));
71
+ if (fieldIds.has(error.fieldId))
72
+ return true;
73
+ return [...sectionIds].some((sectionId) => error.fieldId.startsWith(`${sectionId}.`));
74
+ });
75
+ setErrors(nextErrors);
76
+ if (nextErrors.length === 0)
77
+ setStepIndex((current) => Math.min(steps.length - 1, current + 1));
78
+ }
79
+ return (_jsxs("form", { onSubmit: submit, className: cn('space-y-6', className), noValidate: true, children: [_jsxs("header", { className: "space-y-1", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight text-fg", children: readText(schema.title, locale, 'Untitled form') }), schema.description ? _jsx("p", { className: "text-sm text-fg-muted", children: readText(schema.description, locale) }) : null] }), steps.length > 1 ? _jsx("nav", { "aria-label": "Form progress", className: "grid gap-2", style: { gridTemplateColumns: `repeat(${steps.length}, minmax(0, 1fr))` }, children: steps.map((step, index) => _jsx("button", { type: "button", onClick: () => index <= stepIndex && setStepIndex(index), disabled: index > stepIndex, className: cn('rounded-md border px-3 py-2 text-left text-xs', index === stepIndex ? 'border-primary bg-primary-subtle text-primary' : index < stepIndex ? 'border-success bg-success-subtle text-success' : 'border-border bg-surface text-fg-muted'), children: _jsxs("span", { className: "block font-semibold", children: [index + 1, ". ", readText(step.title, locale, step.key)] }) }, step.key)) }) : null, tabs.length > 1 && steps.length <= 1 ? _jsx("div", { role: "tablist", className: "flex gap-1 overflow-x-auto border-b border-border", children: tabs.map((tab) => _jsx("button", { type: "button", role: "tab", "aria-selected": activeTabId === tab.id, onClick: () => setActiveTabId(tab.id), className: cn('border-b-2 px-3 py-2 text-sm font-medium', activeTabId === tab.id ? 'border-primary text-primary' : 'border-transparent text-fg-muted hover:text-fg'), children: readText(tab.title, locale, tab.id) }, tab.id)) }) : null, errors.length > 0 ? (_jsx(Alert, { variant: "destructive", role: "alert", children: _jsxs(AlertDescription, { children: ["Review ", errors.length, " field", errors.length === 1 ? '' : 's', " before submitting."] }) })) : null, visibleSections.map((section) => {
80
+ const visible = !section.showIf || evaluateLogicRule(section.showIf, evalContext);
81
+ if (!visible)
82
+ return null;
83
+ if (section.repeating) {
84
+ const rows = Array.isArray(values[section.id])
85
+ ? values[section.id]
86
+ : [];
87
+ return (_jsx(RepeatingSection, { section: section, rows: rows, onChange: (next) => update(section.id, next), errorMap: errorMap, disabled: disabled || submitting, locale: locale, optionsByField: optionsByField, fieldAdapters: fieldAdapters, evalContext: evalContext, riskMatrix: riskMatrix }, section.id));
88
+ }
89
+ return (_jsxs("section", { className: "rounded-xl border border-border bg-surface shadow-sm", children: [(section.title || section.description) ? (_jsxs("div", { className: "border-b border-border bg-bg-subtle px-5 py-4", children: [section.title ? _jsx("h2", { className: "font-semibold text-fg", children: readText(section.title, locale) }) : null, section.description ? _jsx("p", { className: "mt-1 text-sm text-fg-muted", children: readText(section.description, locale) }) : null] })) : null, _jsx("div", { className: cn('grid gap-5 p-5', section.canvas ? 'grid-cols-12 auto-rows-[40px]' : section.layout?.columns === 2 ? 'sm:grid-cols-2' : section.layout?.columns === 3 ? 'sm:grid-cols-2 lg:grid-cols-3' : section.layout?.columns === 4 ? 'sm:grid-cols-2 lg:grid-cols-4' : ''), children: section.fields.map((field) => {
90
+ const fieldVisible = !field.showIf || evaluateLogicRule(field.showIf, evalContext);
91
+ if (!fieldVisible)
92
+ return null;
93
+ const canvasItem = section.canvas?.items.find((item) => item.i === field.id);
94
+ const fieldValue = field.type === 'formula' && field.formula
95
+ ? evaluateFormulaTree(field.formula, evalContext)
96
+ : values[field.id];
97
+ return (_jsx("div", { className: cn(!canvasItem && field.config?.width === 'full' && 'col-span-full'), style: canvasItem ? { gridColumn: `${canvasItem.x + 1} / span ${canvasItem.w}`, gridRow: `${canvasItem.y + 1} / span ${canvasItem.h}` } : undefined, children: _jsx(FieldControl, { field: field, value: fieldValue, onChange: (next) => update(field.id, next), error: errorMap.get(field.id), disabled: disabled || submitting, locale: locale, options: optionsByField[field.id] ?? [], adapter: fieldAdapters[field.type], riskMatrix: riskMatrix }) }, field.id));
98
+ }) })] }, section.id));
99
+ }), onSubmit ? _jsxs("div", { className: "flex justify-between gap-2", children: [steps.length > 1 && stepIndex > 0 ? _jsx(Button, { type: "button", variant: "outline", disabled: disabled || submitting, onClick: () => setStepIndex((current) => Math.max(0, current - 1)), children: "Back" }) : _jsx("span", {}), steps.length > 1 && stepIndex < steps.length - 1 ? _jsx(Button, { type: "button", disabled: disabled || submitting, onClick: nextStep, children: "Next" }) : _jsx(Button, { type: "submit", disabled: disabled || submitting, children: submitting ? 'Submitting…' : submitLabel })] }) : null] }));
100
+ }
101
+ function RepeatingSection({ section, rows, onChange, errorMap, disabled, locale, optionsByField, fieldAdapters, evalContext, riskMatrix }) {
102
+ const canAdd = section.maxRows === undefined || rows.length < section.maxRows;
103
+ const canRemove = rows.length > (section.minRows ?? 0);
104
+ function patchRow(rowIndex, fieldId, value) {
105
+ const next = rows.map((row, index) => index === rowIndex ? { ...row, [fieldId]: value } : row);
106
+ onChange(next);
107
+ }
108
+ return (_jsxs("section", { className: "rounded-xl border border-border bg-surface shadow-sm", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-border bg-bg-subtle px-5 py-4", children: [_jsxs("div", { children: [section.title ? _jsx("h2", { className: "font-semibold text-fg", children: readText(section.title, locale) }) : null, section.description ? _jsx("p", { className: "mt-1 text-sm text-fg-muted", children: readText(section.description, locale) }) : null] }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", disabled: disabled || !canAdd, onClick: () => onChange([...rows, {}]), children: [_jsx(Plus, { size: 15 }), "Add row"] })] }), _jsxs("div", { className: "space-y-4 p-5", children: [errorMap.get(`__section_${section.id}`) ? _jsx("p", { className: "text-sm text-danger", children: errorMap.get(`__section_${section.id}`) }) : null, rows.length === 0 ? _jsx("p", { className: "rounded-lg border border-dashed border-border-strong px-4 py-6 text-center text-sm text-fg-muted", children: "No rows yet." }) : null, rows.map((row, rowIndex) => (_jsxs("div", { className: "rounded-lg border border-border bg-bg p-4", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-semibold text-fg", children: rowLabel(section.rowLabelTemplate, rowIndex, row) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", "aria-label": `Remove row ${rowIndex + 1}`, disabled: disabled || !canRemove, onClick: () => onChange(rows.filter((_, index) => index !== rowIndex)), children: _jsx(Trash2, { size: 15 }) })] }), _jsx("div", { className: "grid gap-5 sm:grid-cols-2", children: section.fields.map((field) => {
109
+ const rowContext = { ...evalContext, values: { ...evalContext.values, ...row } };
110
+ const visible = !field.showIf || evaluateLogicRule(field.showIf, rowContext);
111
+ if (!visible)
112
+ return null;
113
+ const fieldValue = field.type === 'formula' && field.formula ? evaluateFormulaTree(field.formula, rowContext) : row[field.id];
114
+ return _jsx(FieldControl, { field: field, value: fieldValue, onChange: (next) => patchRow(rowIndex, field.id, next), error: errorMap.get(`${section.id}.${rowIndex}.${field.id}`), disabled: disabled, locale: locale, options: optionsByField[field.id] ?? [], adapter: fieldAdapters[field.type], riskMatrix: riskMatrix }, field.id);
115
+ }) })] }, rowIndex)))] })] }));
116
+ }
117
+ function rowLabel(template, index, row) {
118
+ if (!template)
119
+ return `Row ${index + 1}`;
120
+ return template
121
+ .replaceAll('{index+1}', String(index + 1))
122
+ .replaceAll('{index}', String(index))
123
+ .replace(/\{([A-Za-z0-9_-]+)\}/g, (_, key) => String(row[key] ?? ''));
124
+ }
125
+ function FieldControl({ field, value, onChange, error, disabled, locale, options, adapter, riskMatrix }) {
126
+ if (adapter)
127
+ return _jsxs("div", { className: "space-y-2", children: [fieldLabel(field, locale), adapter({ field, value, onChange, error, disabled, locale }), fieldHelp(field, locale, error)] });
128
+ if (field.type === 'heading')
129
+ return _jsx("h3", { className: "col-span-full text-lg font-semibold text-fg", children: readText(field.label, locale) });
130
+ if (field.type === 'paragraph')
131
+ return _jsx("p", { className: "col-span-full text-sm text-fg-muted", children: readText(field.helpText ?? field.label, locale) });
132
+ if (field.type === 'divider')
133
+ return _jsx("hr", { className: "col-span-full border-border" });
134
+ if (field.type === 'formula')
135
+ return _jsx(OutputField, { field: field, value: value, locale: locale });
136
+ const nativeProps = { field, value, onChange, disabled };
137
+ if (field.type === 'gps')
138
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, children: _jsx(GpsField, { ...nativeProps }) });
139
+ if (field.type === 'address')
140
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, children: _jsx(AddressField, { ...nativeProps }) });
141
+ if (field.type === 'table')
142
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, children: _jsx(TableField, { ...nativeProps }) });
143
+ if (field.type === 'matrix')
144
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, children: _jsx(MatrixField, { ...nativeProps }) });
145
+ if (field.type === 'risk_matrix')
146
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, hideLabel: true, children: _jsx(RiskField, { ...nativeProps, matrix: riskMatrix }) });
147
+ if (field.type === 'signature')
148
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, children: _jsx(SignatureField, { ...nativeProps }) });
149
+ if (field.type === 'sketch')
150
+ return _jsx(FieldFrame, { field: field, locale: locale, error: error, children: _jsx(SketchField, { ...nativeProps }) });
151
+ if (ADAPTER_FIELD_TYPES.has(field.type))
152
+ throw new Error(`Form field ${field.id} (${field.type}) requires a field adapter`);
153
+ const common = { id: field.id, disabled, 'aria-invalid': Boolean(error), 'aria-describedby': `${field.id}-help` };
154
+ const choiceOptions = field.validation?.options?.map((option) => ({ value: option.value, label: readText(option.label, locale) })) ?? options;
155
+ let control;
156
+ switch (field.type) {
157
+ case 'long_text':
158
+ control = _jsx(Textarea, { ...common, value: typeof value === 'string' ? value : '', onChange: (event) => onChange(event.target.value) });
159
+ break;
160
+ case 'rich_text':
161
+ control = _jsx(RichTextEditor, { value: typeof value === 'string' ? value : '', disabled: disabled, onChange: onChange, normalizeLink: safeLink });
162
+ break;
163
+ case 'number':
164
+ case 'currency':
165
+ case 'percentage':
166
+ control = _jsx(Input, { ...common, type: "number", inputMode: "decimal", value: typeof value === 'number' ? value : '', min: numberConfig(field, 'min'), max: numberConfig(field, 'max'), step: numberConfig(field, 'step') ?? (field.type === 'currency' ? 0.01 : 'any'), onChange: (event) => onChange(event.target.value === '' ? undefined : Number(event.target.value)) });
167
+ break;
168
+ case 'slider': {
169
+ const min = numberConfig(field, 'min') ?? 0;
170
+ const max = numberConfig(field, 'max') ?? 10;
171
+ control = _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("input", { ...common, className: "h-2 flex-1 accent-primary", type: "range", min: min, max: max, step: numberConfig(field, 'step') ?? 1, value: typeof value === 'number' ? value : min, onChange: (event) => onChange(Number(event.target.value)) }), _jsx("output", { className: "w-10 text-right text-sm tabular-nums text-fg", children: typeof value === 'number' ? value : min })] });
172
+ break;
173
+ }
174
+ case 'rating': {
175
+ const max = numberConfig(field, 'max') ?? 5;
176
+ control = _jsx("div", { className: "flex flex-wrap gap-2", children: Array.from({ length: max }, (_, index) => index + 1).map((rating) => _jsx(Button, { type: "button", size: "icon", variant: value === rating ? 'default' : 'outline', disabled: disabled, onClick: () => onChange(rating), "aria-label": `Rate ${rating} of ${max}`, children: rating }, rating)) });
177
+ break;
178
+ }
179
+ case 'radio':
180
+ case 'pass_fail_na':
181
+ case 'traffic_light': {
182
+ const items = field.type === 'pass_fail_na' ? [{ value: 'pass', label: 'Pass' }, { value: 'fail', label: 'Fail' }, { value: 'n_a', label: 'N/A' }] : field.type === 'traffic_light' ? [{ value: 'green', label: 'Green' }, { value: 'yellow', label: 'Yellow' }, { value: 'red', label: 'Red' }] : choiceOptions;
183
+ control = _jsx("div", { className: "flex flex-wrap gap-2", children: items.map((option) => _jsx(Button, { type: "button", size: "sm", variant: value === option.value ? 'default' : 'outline', disabled: disabled, onClick: () => onChange(option.value), children: option.label }, option.value)) });
184
+ break;
185
+ }
186
+ case 'checkbox_group':
187
+ case 'multi_select':
188
+ case 'multi_person_picker': {
189
+ const selected = Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
190
+ control = _jsx("div", { className: "space-y-2", children: choiceOptions.map((option) => _jsxs("label", { className: "flex items-center gap-2 text-sm text-fg", children: [_jsx(Checkbox, { disabled: disabled, checked: selected.includes(option.value), onChange: (event) => onChange(event.target.checked ? [...selected, option.value] : selected.filter((entry) => entry !== option.value)) }), option.label] }, option.value)) });
191
+ break;
192
+ }
193
+ case 'ranking': {
194
+ const ordered = Array.isArray(value) && value.length > 0
195
+ ? value.filter((item) => typeof item === 'string')
196
+ : choiceOptions.map((option) => option.value);
197
+ control = _jsx("div", { className: "space-y-1", children: ordered.map((entry, index) => { const option = choiceOptions.find((item) => item.value === entry); return _jsxs("div", { className: "flex items-center gap-2 rounded-md border border-border bg-bg-subtle px-2 py-1.5", children: [_jsx("span", { className: "w-5 text-center text-xs tabular-nums text-fg-muted", children: index + 1 }), _jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-fg", children: option?.label ?? entry }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", "aria-label": `Move ${option?.label ?? entry} up`, disabled: disabled || index === 0, onClick: () => onChange(move(ordered, index, index - 1)), children: _jsx(ArrowUp, { size: 14 }) }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", "aria-label": `Move ${option?.label ?? entry} down`, disabled: disabled || index === ordered.length - 1, onClick: () => onChange(move(ordered, index, index + 1)), children: _jsx(ArrowDown, { size: 14 }) })] }, entry); }) });
198
+ break;
199
+ }
200
+ case 'select':
201
+ case 'person_picker':
202
+ case 'customer_picker':
203
+ case 'project_picker':
204
+ case 'site_picker':
205
+ case 'area_picker':
206
+ case 'gl_account':
207
+ case 'party':
208
+ control = _jsxs("select", { ...common, className: "h-10 w-full rounded-md border border-border bg-surface px-3 text-sm text-fg focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/40", value: typeof value === 'string' ? value : '', onChange: (event) => onChange(event.target.value || undefined), children: [_jsx("option", { value: "", children: "Select\u2026" }), choiceOptions.map((option) => _jsx("option", { value: option.value, children: option.label }, option.value))] });
209
+ break;
210
+ case 'yes_no_comment': {
211
+ const current = typeof value === 'object' && value !== null ? value : {};
212
+ control = _jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "flex gap-2", children: ['yes', 'no'].map((answer) => _jsx(Button, { type: "button", size: "sm", variant: current.answer === answer ? 'default' : 'outline', disabled: disabled, onClick: () => onChange({ ...current, answer }), children: answer === 'yes' ? 'Yes' : 'No' }, answer)) }), current.answer === 'no' ? _jsx(Textarea, { value: current.comment ?? '', placeholder: "Add a comment\u2026", disabled: disabled, onChange: (event) => onChange({ ...current, comment: event.target.value }) }) : null] });
213
+ break;
214
+ }
215
+ case 'typed_attestation': {
216
+ const current = typeof value === 'object' && value !== null ? value : {};
217
+ control = _jsxs("div", { className: "space-y-2", children: [_jsx(Input, { ...common, value: current.name ?? '', placeholder: "Type your full name", onChange: (event) => onChange({ ...current, name: event.target.value }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-fg", children: [_jsx(Checkbox, { disabled: disabled, checked: current.agreed ?? false, onChange: (event) => onChange({ ...current, agreed: event.target.checked }) }), "I confirm this attestation."] })] });
218
+ break;
219
+ }
220
+ default: {
221
+ const inputType = field.type === 'email' ? 'email' : field.type === 'phone' ? 'tel' : field.type === 'url' ? 'url' : field.type === 'date' ? 'date' : field.type === 'datetime' ? 'datetime-local' : field.type === 'time' ? 'time' : 'text';
222
+ control = _jsx(Input, { ...common, type: inputType, value: typeof value === 'string' ? value : '', onChange: (event) => onChange(event.target.value) });
223
+ }
224
+ }
225
+ return _jsxs("div", { className: cn('space-y-2', (field.colSpan ?? 1) > 1 && 'sm:col-span-2'), children: [fieldLabel(field, locale), control, fieldHelp(field, locale, error)] });
226
+ }
227
+ function FieldFrame({ field, locale, error, hideLabel = false, children }) {
228
+ return _jsxs("div", { className: cn('space-y-2', (field.colSpan ?? 1) > 1 && 'sm:col-span-2'), children: [hideLabel ? null : fieldLabel(field, locale), children, fieldHelp(field, locale, error)] });
229
+ }
230
+ function fieldLabel(field, locale) {
231
+ return _jsxs(Label, { htmlFor: field.id, children: [readText(field.label, locale), field.required || field.validation?.required ? _jsx("span", { className: "ml-1 text-danger", "aria-hidden": "true", children: "*" }) : null] });
232
+ }
233
+ function fieldHelp(field, locale, error) {
234
+ return _jsx("p", { id: `${field.id}-help`, className: cn('text-xs', error ? 'text-danger' : 'text-fg-muted'), children: error ?? readText(field.helpText, locale) });
235
+ }
236
+ function OutputField({ field, value, locale }) {
237
+ return _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { children: readText(field.label, locale) }), _jsx("output", { className: "block rounded-md border border-border bg-bg-subtle px-3 py-2 text-sm font-medium text-fg", children: value === undefined || value === null ? '—' : String(value) })] });
238
+ }
239
+ function numberConfig(field, key) {
240
+ const value = field.config?.[key];
241
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
242
+ }
243
+ function move(values, from, to) {
244
+ if (to < 0 || to >= values.length)
245
+ return values;
246
+ const next = [...values];
247
+ const [item] = next.splice(from, 1);
248
+ next.splice(to, 0, item);
249
+ return next;
250
+ }
251
+ function safeLink(value) {
252
+ try {
253
+ const url = new URL(value, window.location.origin);
254
+ return ['http:', 'https:', 'mailto:'].includes(url.protocol) ? url.href : null;
255
+ }
256
+ catch {
257
+ return null;
258
+ }
259
+ }
260
+ function applySchemaDefaults(schema, supplied, requestContext, entities) {
261
+ const values = structuredClone(supplied);
262
+ const rows = Object.fromEntries(schema.sections
263
+ .filter((section) => section.repeating)
264
+ .map((section) => [
265
+ section.id,
266
+ Array.isArray(values[section.id]) ? values[section.id] : [],
267
+ ]));
268
+ const context = { values, rows, entities, requestContext };
269
+ for (const section of schema.sections) {
270
+ if (section.repeating)
271
+ continue;
272
+ for (const field of section.fields) {
273
+ if (values[field.id] !== undefined || !field.defaultValue)
274
+ continue;
275
+ const resolved = resolveDefaultValue(field.defaultValue, context);
276
+ if (resolved !== undefined && resolved !== null)
277
+ values[field.id] = resolved;
278
+ }
279
+ }
280
+ return values;
281
+ }
282
+ //# sourceMappingURL=form-renderer.js.map