@dmitryvim/form-builder 0.1.4 → 0.1.5

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.html CHANGED
@@ -4,120 +4,104 @@
4
4
  <meta charset="utf-8" />
5
5
  <title>Form Builder - JSON Schema to Dynamic Forms</title>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ darkMode: 'media',
11
+ theme: {
12
+ extend: {
13
+ fontFamily: {
14
+ mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace']
15
+ }
16
+ }
17
+ }
18
+ }
19
+ </script>
7
20
  <style>
8
- :root {
9
- --bg: #f8fafc; --fg: #1e293b; --muted: #64748b; --accent: #3b82f6;
10
- --bad: #ef4444; --good: #10b981; --card: #ffffff; --border: #e2e8f0;
11
- --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono","Courier New", monospace;
12
- --sans: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
21
+ /* Custom styles for form validation states */
22
+ .invalid {
23
+ @apply border-red-500 !important;
13
24
  }
14
- @media (prefers-color-scheme: dark) {
15
- :root {
16
- --bg: #0f1115; --fg: #e7e7e7; --muted: #9aa0a6; --accent: #7dd3fc;
17
- --bad: #ff6b6b; --good: #34d399; --card: #151922; --border: #2a2f3a;
18
- }
25
+ .field-hint {
26
+ @apply text-gray-500 text-xs mt-1;
27
+ }
28
+ .error-message {
29
+ @apply text-red-500 text-xs mt-1;
30
+ }
31
+ .file-preview-container {
32
+ @apply mb-3 p-3 border border-dashed border-gray-300 rounded-lg min-h-[60px] flex items-center justify-center bg-blue-50;
33
+ }
34
+ .dark .file-preview-container {
35
+ @apply bg-blue-900/20 border-gray-600;
19
36
  }
20
- * { box-sizing: border-box; }
21
- html, body { height: 100%; background: var(--bg); color: var(--fg); margin: 0; font-family: var(--sans); }
22
- h1 { margin: 16px; font-size: 24px; font-weight: 600; color: var(--fg); text-align: center; }
23
- .subtitle { text-align: center; color: var(--muted); margin: -8px 16px 24px; font-size: 14px; }
24
- .grid { display: grid; grid-template-columns: 1fr 1fr; grid-auto-rows: minmax(300px, auto); gap: 12px; padding: 12px; }
25
- @media (min-width: 1400px) { .grid { grid-template-columns: 1fr 1fr 1fr 1fr; } }
26
- @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
27
- .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; display: flex; flex-direction: column; min-height: 320px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
28
- .card h2 { margin: 0 0 12px 0; font-size: 18px; color: var(--fg); font-weight: 600; }
29
- .toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
30
- .btn { background: var(--card); color: var(--fg); border: 1px solid var(--border); padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; }
31
- .btn:hover { border-color: var(--accent); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
32
- .btn.good { background: var(--good); color: white; border-color: var(--good); }
33
- .btn.good:hover { filter: brightness(0.9); }
34
- .btn.bad { background: var(--bad); color: white; border-color: var(--bad); }
35
- .btn.bad:hover { filter: brightness(0.9); }
36
- .btn.ghost { background: transparent; }
37
- .btn:disabled { opacity: 0.5; cursor: not-allowed; }
38
- .textarea, .json, input[type="text"], textarea, select, input[type="number"], input[type="file"] {
39
- width: 100%; border: 1px solid var(--border); background: var(--card); color: var(--fg); border-radius: 8px; padding: 10px; font-family: var(--mono); font-size: 13px; transition: border-color 0.2s;
37
+ .resource-pill {
38
+ @apply inline-flex items-center gap-1.5 bg-blue-50 border border-gray-300 rounded-full px-2.5 py-1 font-mono text-xs m-0.5;
39
+ }
40
+ .dark .resource-pill {
41
+ @apply bg-blue-900/20 border-gray-600;
40
42
  }
41
- .json, .textarea { flex: 1; resize: vertical; min-height: 220px; }
42
- input[type="text"], input[type="number"], select { font-family: var(--sans); }
43
- input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
44
- .hint { color: var(--muted); font-size: 12px; margin: 6px 0 0; }
45
- .errors { color: var(--bad); font-size: 12px; white-space: pre-wrap; margin-top: 8px; background: rgba(239, 68, 68, 0.1); padding: 8px; border-radius: 6px; border-left: 3px solid var(--bad); }
46
- .field { margin-bottom: 16px; }
47
- .label { font-size: 14px; margin-bottom: 8px; color: var(--fg); display: flex; align-items: center; gap: 8px; font-weight: 500; }
48
- .label .req { color: var(--bad); font-weight: 600; }
49
- .invalid { border-color: var(--bad) !important; }
50
- .msg { font-size: 12px; color: var(--bad); margin-top: 6px; }
51
- .ok { color: var(--good); }
52
- .muted { color: var(--muted); }
53
- .pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(59, 130, 246, 0.1); border: 1px solid var(--border); border-radius: 999px; padding: 4px 10px; font-family: var(--mono); font-size: 12px; margin: 2px; }
54
- .list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
55
- .groupHeader { display: flex; align-items: center; justify-content: space-between; margin: 8px 0 12px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
56
- .groupItem { border: 1px dashed var(--border); border-radius: 10px; padding: 12px; margin-bottom: 12px; background: rgba(59, 130, 246, 0.02); }
57
- .footer { margin-top: auto; font-size: 11px; color: var(--muted); padding-top: 12px; border-top: 1px solid var(--border); }
58
- .url-params { background: rgba(59, 130, 246, 0.1); border: 1px solid var(--accent); border-radius: 8px; padding: 12px; margin: 16px; font-size: 13px; }
59
- .url-params code { background: rgba(0,0,0,0.1); padding: 2px 4px; border-radius: 4px; font-family: var(--mono); }
60
43
  </style>
61
44
  </head>
62
- <body>
63
- <h1>🚀 Form Builder</h1>
64
- <div class="subtitle">JSON Schema → Dynamic Forms → Structured Output</div>
45
+ <body class="min-h-screen bg-slate-50 text-slate-800 dark:bg-slate-900 dark:text-slate-200">
46
+ <h1 class="text-center text-2xl font-semibold m-4">🚀 Form Builder</h1>
47
+ <div class="text-center text-slate-600 dark:text-slate-400 text-sm mx-4 -mt-2 mb-6">JSON Schema → Dynamic Forms → Structured Output</div>
65
48
 
66
- <div class="url-params" id="urlInfo" style="display: none;">
67
- <strong>💡 URL Parameters:</strong> Add <code>?schema=BASE64_ENCODED_SCHEMA</code> to auto-load a schema.
49
+ <div class="hidden bg-blue-50 dark:bg-blue-900/20 border border-blue-300 dark:border-blue-600 rounded-lg p-3 mx-4 mb-4 text-sm" id="urlInfo">
50
+ <strong>💡 URL Parameters:</strong> Add <code class="bg-black/10 dark:bg-white/10 px-1 py-0.5 rounded font-mono">?schema=BASE64_ENCODED_SCHEMA</code> to auto-load a schema.
68
51
  </div>
69
52
 
70
- <div class="grid">
71
- <section class="card" id="schemaCard">
72
- <h2>1️⃣ JSON Schema</h2>
73
- <div class="toolbar">
74
- <button class="btn good" id="applySchemaBtn">Apply Schema</button>
75
- <button class="btn ghost" id="resetSchemaBtn">Reset to Example</button>
76
- <button class="btn ghost" id="prettySchemaBtn">Format JSON</button>
77
- <button class="btn ghost" id="downloadSchemaBtn">Download</button>
53
+ <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3 p-3 auto-rows-[minmax(300px,auto)]">
54
+ <section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="schemaCard">
55
+ <h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">1️⃣ JSON Schema</h2>
56
+ <div class="flex flex-wrap gap-2 mb-3">
57
+ <button class="bg-emerald-600 hover:bg-emerald-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="applySchemaBtn">Apply Schema</button>
58
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="resetSchemaBtn">Reset to Example</button>
59
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="prettySchemaBtn">Format JSON</button>
60
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="downloadSchemaBtn">Download</button>
78
61
  </div>
79
- <textarea id="schemaInput" class="json" spellcheck="false" placeholder="Paste your JSON schema here..."></textarea>
80
- <div id="schemaErrors" class="errors" style="display: none;"></div>
81
- <div class="footer">
62
+ <textarea id="schemaInput" class="flex-1 w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 font-mono text-xs resize-y min-h-[220px] focus:outline-none focus:border-blue-500" spellcheck="false" placeholder="Paste your JSON schema here..."></textarea>
63
+ <div id="schemaErrors" class="hidden text-red-500 text-xs whitespace-pre-wrap mt-2 bg-red-50 dark:bg-red-900/20 p-2 rounded-md border-l-4 border-red-500"></div>
64
+ <div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">
82
65
  Schema defines form structure. Supports: text, textarea, number, select, file, files, and nested groups.
83
66
  </div>
84
67
  </section>
85
68
 
86
- <section class="card" id="formCard">
87
- <h2>2️⃣ Generated Form</h2>
88
- <div id="formContainer" style="max-height: 400px; overflow-y: auto;">
89
- <div style="text-align: center; color: var(--muted); padding: 40px;">
69
+ <section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="formCard">
70
+ <h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">2️⃣ Generated Form</h2>
71
+ <div id="formContainer" class="max-h-[400px] overflow-y-auto">
72
+ <div class="text-center text-slate-500 dark:text-slate-400 py-10">
90
73
  Apply a schema to generate the form
91
74
  </div>
92
75
  </div>
93
- <div class="toolbar">
94
- <button class="btn good" id="submitBtn">Submit Form</button>
95
- <button class="btn ghost" id="clearFormBtn">Clear Values</button>
76
+ <div class="flex flex-wrap gap-2 mb-3">
77
+ <button class="bg-emerald-600 hover:bg-emerald-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="submitBtn">Submit Form</button>
78
+ <button class="bg-slate-600 hover:bg-slate-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="saveDraftBtn">Save Draft</button>
79
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="clearFormBtn">Clear Values</button>
96
80
  </div>
97
- <div id="formErrors" class="errors" style="display: none;"></div>
98
- <div class="footer">Interactive form generated from your schema. Files are simulated with resource IDs.</div>
81
+ <div id="formErrors" class="hidden text-red-500 text-xs whitespace-pre-wrap mt-2 bg-red-50 dark:bg-red-900/20 p-2 rounded-md border-l-4 border-red-500"></div>
82
+ <div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">Interactive form generated from your schema. Files are simulated with resource IDs.</div>
99
83
  </section>
100
84
 
101
- <section class="card" id="outputCard">
102
- <h2>3️⃣ Form Output</h2>
103
- <div class="toolbar">
104
- <button class="btn" id="copyOutputBtn">Copy JSON</button>
105
- <button class="btn ghost" id="downloadOutputBtn">Download</button>
106
- <button class="btn ghost" id="shareUrlBtn">Share URL</button>
85
+ <section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="outputCard">
86
+ <h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">3️⃣ Form Output</h2>
87
+ <div class="flex flex-wrap gap-2 mb-3">
88
+ <button class="bg-slate-600 hover:bg-slate-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="copyOutputBtn">Copy JSON</button>
89
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="downloadOutputBtn">Download</button>
90
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="shareUrlBtn">Share URL</button>
107
91
  </div>
108
- <textarea id="outputJson" class="json" readonly placeholder="Submit the form to see the output JSON here..."></textarea>
109
- <div class="footer">Structured output matching your schema. Ready for API consumption or processing.</div>
92
+ <textarea id="outputJson" class="flex-1 w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 font-mono text-xs resize-y min-h-[220px] focus:outline-none focus:border-blue-500" readonly placeholder="Submit the form to see the output JSON here..."></textarea>
93
+ <div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">Structured output matching your schema. Ready for API consumption or processing.</div>
110
94
  </section>
111
95
 
112
- <section class="card" id="prefillCard">
113
- <h2>4️⃣ Prefill Data</h2>
114
- <div class="toolbar">
115
- <button class="btn" id="loadPrefillBtn">Load Prefill</button>
116
- <button class="btn ghost" id="copyTemplateBtn">Generate Template</button>
96
+ <section class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 flex flex-col min-h-[320px] shadow-sm" id="prefillCard">
97
+ <h2 class="text-lg font-semibold mb-3 text-slate-800 dark:text-slate-200">4️⃣ Prefill Data</h2>
98
+ <div class="flex flex-wrap gap-2 mb-3">
99
+ <button class="bg-slate-600 hover:bg-slate-700 text-white border-0 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all" id="loadPrefillBtn">Load Prefill</button>
100
+ <button class="bg-transparent hover:border-blue-500 text-slate-700 dark:text-slate-300 border border-slate-300 dark:border-slate-600 px-4 py-2 rounded-lg cursor-pointer text-xs font-medium transition-all hover:shadow-sm" id="copyTemplateBtn">Generate Template</button>
117
101
  </div>
118
- <textarea id="prefillInput" class="json" spellcheck="false" placeholder='{"field1": "value1", "field2": "value2", ...}'></textarea>
119
- <div id="prefillErrors" class="errors" style="display: none;"></div>
120
- <div class="footer">Prefill form fields with existing data. Useful for editing or setting default values.</div>
102
+ <textarea id="prefillInput" class="flex-1 w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 font-mono text-xs resize-y min-h-[220px] focus:outline-none focus:border-blue-500" spellcheck="false" placeholder='{"field1": "value1", "field2": "value2", ...}'></textarea>
103
+ <div id="prefillErrors" class="hidden text-red-500 text-xs whitespace-pre-wrap mt-2 bg-red-50 dark:bg-red-900/20 p-2 rounded-md border-l-4 border-red-500"></div>
104
+ <div class="mt-auto text-xs text-slate-500 dark:text-slate-400 pt-3 border-t border-slate-200 dark:border-slate-700">Prefill form fields with existing data. Useful for editing or setting default values.</div>
121
105
  </section>
122
106
  </div>
123
107
 
@@ -150,6 +134,7 @@
150
134
  formContainer: document.getElementById('formContainer'),
151
135
  formErrors: document.getElementById('formErrors'),
152
136
  submitBtn: document.getElementById('submitBtn'),
137
+ saveDraftBtn: document.getElementById('saveDraftBtn'),
153
138
  clearFormBtn: document.getElementById('clearFormBtn'),
154
139
  outputJson: document.getElementById('outputJson'),
155
140
  copyOutputBtn: document.getElementById('copyOutputBtn'),
@@ -162,59 +147,102 @@
162
147
  urlInfo: document.getElementById('urlInfo')
163
148
  };
164
149
 
165
- // Example schema for demonstration
150
+ // Example schema for demonstration (from docs/13_form_builder.html)
166
151
  const EXAMPLE_SCHEMA = {
167
152
  "version": "0.3",
168
- "title": "User Profile Form",
153
+ "title": "Asset Uploader with Slides",
169
154
  "elements": [
170
155
  {
171
- "type": "text",
172
- "key": "name",
173
- "label": "Full Name",
156
+ "type": "file",
157
+ "key": "cover",
158
+ "label": "Cover image",
174
159
  "required": true,
175
- "minLength": 2,
176
- "maxLength": 50,
177
- "default": ""
160
+ "accept": {
161
+ "extensions": ["png", "jpg", "jpeg"],
162
+ "mime": ["image/png", "image/jpeg"]
163
+ },
164
+ "maxSizeMB": 25
178
165
  },
179
166
  {
180
- "type": "textarea",
181
- "key": "bio",
182
- "label": "Biography",
167
+ "type": "files",
168
+ "key": "assets",
169
+ "label": "Additional images",
183
170
  "required": false,
184
- "maxLength": 500,
185
- "default": ""
171
+ "accept": {
172
+ "extensions": ["png", "jpg"],
173
+ "mime": ["image/png", "image/jpeg"]
174
+ },
175
+ "minCount": 0,
176
+ "maxCount": 10,
177
+ "maxSizeMB": 25
186
178
  },
187
179
  {
188
- "type": "number",
189
- "key": "age",
190
- "label": "Age",
180
+ "type": "text",
181
+ "key": "title",
182
+ "label": "Project title",
191
183
  "required": true,
192
- "min": 18,
193
- "max": 120,
194
- "decimals": 0
184
+ "minLength": 1,
185
+ "maxLength": 120,
186
+ "pattern": "^[A-Za-z0-9 _-]+$",
187
+ "default": "My Project"
188
+ },
189
+ {
190
+ "type": "textarea",
191
+ "key": "description",
192
+ "label": "Description",
193
+ "required": false,
194
+ "minLength": 0,
195
+ "maxLength": 2000,
196
+ "pattern": null,
197
+ "default": ""
195
198
  },
196
199
  {
197
200
  "type": "select",
198
- "key": "role",
199
- "label": "Role",
201
+ "key": "theme",
202
+ "label": "Theme",
200
203
  "required": true,
201
204
  "options": [
202
- {"value": "user", "label": "User"},
203
- {"value": "admin", "label": "Administrator"},
204
- {"value": "moderator", "label": "Moderator"}
205
+ {"value": "light", "label": "Light"},
206
+ {"value": "dark", "label": "Dark"}
205
207
  ],
206
- "default": "user"
208
+ "default": "dark"
207
209
  },
208
210
  {
209
- "type": "file",
210
- "key": "avatar",
211
- "label": "Profile Picture",
212
- "required": false,
213
- "accept": {
214
- "extensions": ["jpg", "jpeg", "png"],
215
- "mime": ["image/jpeg", "image/png"]
216
- },
217
- "maxSizeMB": 5
211
+ "type": "number",
212
+ "key": "opacity",
213
+ "label": "Opacity",
214
+ "required": true,
215
+ "min": 0,
216
+ "max": 1,
217
+ "decimals": 2,
218
+ "step": 0.01,
219
+ "default": 0.85
220
+ },
221
+ {
222
+ "type": "group",
223
+ "key": "slides",
224
+ "label": "Slides",
225
+ "repeat": {"min": 1, "max": 5},
226
+ "elements": [
227
+ {
228
+ "type": "text",
229
+ "key": "title",
230
+ "label": "Slide title",
231
+ "required": true,
232
+ "minLength": 1,
233
+ "maxLength": 80,
234
+ "default": ""
235
+ },
236
+ {
237
+ "type": "textarea",
238
+ "key": "body",
239
+ "label": "Slide text",
240
+ "required": true,
241
+ "minLength": 1,
242
+ "maxLength": 1000,
243
+ "default": ""
244
+ }
245
+ ]
218
246
  }
219
247
  ]
220
248
  };
@@ -253,12 +281,12 @@
253
281
  }
254
282
 
255
283
  function showError(container, message) {
256
- container.style.display = 'block';
284
+ container.classList.remove('hidden');
257
285
  container.textContent = message;
258
286
  }
259
287
 
260
288
  function clearError(container) {
261
- container.style.display = 'none';
289
+ container.classList.add('hidden');
262
290
  container.textContent = '';
263
291
  }
264
292
 
@@ -362,17 +390,17 @@
362
390
 
363
391
  function renderElement(element, ctx) {
364
392
  const wrapper = document.createElement('div');
365
- wrapper.className = 'field';
393
+ wrapper.className = 'mb-4';
366
394
 
367
395
  const label = document.createElement('div');
368
- label.className = 'label';
396
+ label.className = 'text-sm mb-2 text-slate-800 dark:text-slate-200 flex items-center gap-2 font-medium';
369
397
  const title = document.createElement('span');
370
398
  title.textContent = element.label || element.key;
371
399
  label.appendChild(title);
372
400
 
373
401
  if (element.required) {
374
402
  const req = document.createElement('span');
375
- req.className = 'req';
403
+ req.className = 'text-red-500 font-semibold';
376
404
  req.textContent = '*';
377
405
  label.appendChild(req);
378
406
  }
@@ -386,6 +414,7 @@
386
414
  input.type = 'text';
387
415
  input.name = pathKey;
388
416
  input.dataset.type = 'text';
417
+ input.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500';
389
418
  setTextValueFromPrefill(input, element, ctx.prefill, element.key);
390
419
  input.addEventListener('input', () => markValidity(input, null));
391
420
  wrapper.appendChild(input);
@@ -397,6 +426,7 @@
397
426
  ta.name = pathKey;
398
427
  ta.rows = 4;
399
428
  ta.dataset.type = 'textarea';
429
+ ta.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500 resize-y';
400
430
  setTextValueFromPrefill(ta, element, ctx.prefill, element.key);
401
431
  ta.addEventListener('input', () => markValidity(ta, null));
402
432
  wrapper.appendChild(ta);
@@ -408,6 +438,7 @@
408
438
  input.type = 'number';
409
439
  input.name = pathKey;
410
440
  input.dataset.type = 'number';
441
+ input.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500';
411
442
  if (element.step != null) input.step = String(element.step);
412
443
  if (element.min != null) input.min = String(element.min);
413
444
  if (element.max != null) input.max = String(element.max);
@@ -428,6 +459,7 @@
428
459
  const sel = document.createElement('select');
429
460
  sel.name = pathKey;
430
461
  sel.dataset.type = 'select';
462
+ sel.className = 'w-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-200 rounded-lg p-2.5 text-sm focus:outline-none focus:border-blue-500';
431
463
 
432
464
  if (!element.required) {
433
465
  const opt = document.createElement('option');
@@ -517,7 +549,7 @@
517
549
  container.appendChild(hid);
518
550
 
519
551
  wrapper.appendChild(container);
520
- wrapper.appendChild(makeFieldHint(element, 'Files are uploaded to S3 and referenced by URL'));
552
+ wrapper.appendChild(makeFieldHint(element, 'Returns resource ID for download/submission'));
521
553
  break;
522
554
  }
523
555
  case 'files': {
@@ -581,7 +613,7 @@
581
613
  wrapper.appendChild(picker);
582
614
  wrapper.appendChild(list);
583
615
  wrapper.appendChild(hid);
584
- wrapper.appendChild(makeFieldHint(element, 'Multiple files resource ID array'));
616
+ wrapper.appendChild(makeFieldHint(element, 'Multiple files return resource ID array'));
585
617
  break;
586
618
  }
587
619
  case 'group': {
@@ -590,7 +622,7 @@
590
622
 
591
623
  const groupWrap = document.createElement('div');
592
624
  const header = document.createElement('div');
593
- header.className = 'groupHeader';
625
+ header.className = 'flex items-center justify-between my-2 pb-2 border-b border-slate-200 dark:border-slate-700';
594
626
 
595
627
  const left = document.createElement('div');
596
628
  left.innerHTML = `<span>${element.label || element.key}</span>`;
@@ -609,7 +641,7 @@
609
641
 
610
642
  const addBtn = document.createElement('button');
611
643
  addBtn.type = 'button';
612
- addBtn.className = 'btn';
644
+ addBtn.className = 'bg-blue-600 hover:bg-blue-700 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors';
613
645
  addBtn.textContent = 'Add';
614
646
  right.appendChild(addBtn);
615
647
  header.appendChild(right);
@@ -618,7 +650,7 @@
618
650
  const refreshControls = () => {
619
651
  const n = countItems();
620
652
  addBtn.disabled = n >= max;
621
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="muted" style="font-size: 12px;">[${n} / ${max === Infinity ? '∞' : max}, min=${min}]</span>`;
653
+ left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-slate-500 dark:text-slate-400 text-xs">[${n} / ${max === Infinity ? '∞' : max}, min=${min}]</span>`;
622
654
  };
623
655
 
624
656
  const addItem = (prefillObj) => {
@@ -679,7 +711,7 @@
679
711
 
680
712
  function makeFieldHint(element, extra = '') {
681
713
  const hint = document.createElement('div');
682
- hint.className = 'hint';
714
+ hint.className = 'text-slate-500 dark:text-slate-400 text-xs mt-1';
683
715
  const bits = [];
684
716
 
685
717
  if (element.required) bits.push('required');
@@ -805,15 +837,17 @@
805
837
 
806
838
  function renderResourcePills(container, rids, onRemove) {
807
839
  clear(container);
840
+ container.className = 'flex flex-wrap gap-1.5 mt-2';
841
+
808
842
  rids.forEach(rid => {
809
843
  const meta = state.resourceIndex.get(rid);
810
844
  const pill = document.createElement('span');
811
- pill.className = 'pill';
845
+ pill.className = 'resource-pill';
812
846
  pill.textContent = rid;
813
847
 
814
848
  if (meta) {
815
849
  const small = document.createElement('span');
816
- small.className = 'muted';
850
+ small.className = 'text-slate-500 dark:text-slate-400';
817
851
  small.textContent = ` (${meta.name ?? 'file'}, ${formatFileSize(meta.size ?? 0)})`;
818
852
  pill.appendChild(small);
819
853
  }
@@ -821,10 +855,8 @@
821
855
  if (onRemove) {
822
856
  const x = document.createElement('button');
823
857
  x.type = 'button';
824
- x.className = 'btn bad';
858
+ x.className = 'bg-red-500 hover:bg-red-600 text-white text-xs px-1.5 py-0.5 rounded ml-1.5';
825
859
  x.textContent = '×';
826
- x.style.padding = '2px 6px';
827
- x.style.marginLeft = '6px';
828
860
  x.addEventListener('click', () => onRemove(rid));
829
861
  pill.appendChild(x);
830
862
  }
@@ -842,14 +874,14 @@
842
874
  }
843
875
 
844
876
  function markValidity(input, msg) {
845
- const prev = input?.parentElement?.querySelector?.('.msg');
877
+ const prev = input?.parentElement?.querySelector?.('.error-message');
846
878
  if (prev) prev.remove();
847
879
 
848
880
  if (input) input.classList.toggle('invalid', !!msg);
849
881
 
850
882
  if (msg && input?.parentElement) {
851
883
  const m = document.createElement('div');
852
- m.className = 'msg';
884
+ m.className = 'error-message text-red-500 text-xs mt-1';
853
885
  m.textContent = msg;
854
886
  input.parentElement.appendChild(m);
855
887
  }
@@ -905,7 +937,7 @@
905
937
  }
906
938
 
907
939
  // Form data collection and validation
908
- function collectAndValidate(schema) {
940
+ function collectAndValidate(schema, skipValidation = false) {
909
941
  const form = document.getElementById('dynamicForm');
910
942
  const errors = [];
911
943
 
@@ -917,10 +949,10 @@
917
949
  case 'textarea': {
918
950
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
919
951
  const val = (input?.value ?? '').trim();
920
- if (element.required && val === '') {
952
+ if (!skipValidation && element.required && val === '') {
921
953
  errors.push(`${key}: required`);
922
954
  markValidity(input, 'required');
923
- } else if (val !== '') {
955
+ } else if (!skipValidation && val !== '') {
924
956
  if (element.minLength != null && val.length < element.minLength) {
925
957
  errors.push(`${key}: minLength=${element.minLength}`);
926
958
  markValidity(input, `minLength=${element.minLength}`);
@@ -941,6 +973,8 @@
941
973
  markValidity(input, 'invalid pattern');
942
974
  }
943
975
  }
976
+ } else if (skipValidation) {
977
+ markValidity(input, null);
944
978
  } else {
945
979
  markValidity(input, null);
946
980
  }
@@ -949,7 +983,7 @@
949
983
  case 'number': {
950
984
  const input = scopeRoot.querySelector(`[name$="${key}"]`);
951
985
  const raw = input?.value ?? '';
952
- if (element.required && raw === '') {
986
+ if (!skipValidation && element.required && raw === '') {
953
987
  errors.push(`${key}: required`);
954
988
  markValidity(input, 'required');
955
989
  return null;
@@ -959,16 +993,16 @@
959
993
  return null;
960
994
  }
961
995
  const v = parseFloat(raw);
962
- if (!Number.isFinite(v)) {
996
+ if (!skipValidation && !Number.isFinite(v)) {
963
997
  errors.push(`${key}: not a number`);
964
998
  markValidity(input, 'not a number');
965
999
  return null;
966
1000
  }
967
- if (element.min != null && v < element.min) {
1001
+ if (!skipValidation && element.min != null && v < element.min) {
968
1002
  errors.push(`${key}: < min=${element.min}`);
969
1003
  markValidity(input, `< min=${element.min}`);
970
1004
  }
971
- if (element.max != null && v > element.max) {
1005
+ if (!skipValidation && element.max != null && v > element.max) {
972
1006
  errors.push(`${key}: > max=${element.max}`);
973
1007
  markValidity(input, `> max=${element.max}`);
974
1008
  }
@@ -982,12 +1016,12 @@
982
1016
  const sel = scopeRoot.querySelector(`select[name$="${key}"]`);
983
1017
  const val = sel?.value ?? '';
984
1018
  const values = new Set(element.options.map(o => String(o.value)));
985
- if (element.required && val === '') {
1019
+ if (!skipValidation && element.required && val === '') {
986
1020
  errors.push(`${key}: required`);
987
1021
  markValidity(sel, 'required');
988
1022
  return '';
989
1023
  }
990
- if (val !== '' && !values.has(String(val))) {
1024
+ if (!skipValidation && val !== '' && !values.has(String(val))) {
991
1025
  errors.push(`${key}: value not in options`);
992
1026
  markValidity(sel, 'not in options');
993
1027
  } else {
@@ -998,7 +1032,7 @@
998
1032
  case 'file': {
999
1033
  const hid = scopeRoot.querySelector(`input[type="hidden"][name$="${key}"]`);
1000
1034
  const rid = hid?.value ?? '';
1001
- if (element.required && !rid) {
1035
+ if (!skipValidation && element.required && !rid) {
1002
1036
  errors.push(`${key}: required (file missing)`);
1003
1037
  const picker = hid?.previousElementSibling;
1004
1038
  if (picker) markValidity(picker, 'required');
@@ -1011,11 +1045,11 @@
1011
1045
  const hid = scopeRoot.querySelector(`input[type="hidden"][name$="${key}"]`);
1012
1046
  const arr = parseJSONSafe(hid?.value ?? '[]', []);
1013
1047
  const count = Array.isArray(arr) ? arr.length : 0;
1014
- if (!Array.isArray(arr)) errors.push(`${key}: internal value corrupted`);
1015
- if (element.minCount != null && count < element.minCount) {
1048
+ if (!skipValidation && !Array.isArray(arr)) errors.push(`${key}: internal value corrupted`);
1049
+ if (!skipValidation && element.minCount != null && count < element.minCount) {
1016
1050
  errors.push(`${key}: < minCount=${element.minCount}`);
1017
1051
  }
1018
- if (element.maxCount != null && count > element.maxCount) {
1052
+ if (!skipValidation && element.maxCount != null && count > element.maxCount) {
1019
1053
  errors.push(`${key}: > maxCount=${element.maxCount}`);
1020
1054
  }
1021
1055
  if (hid?.previousElementSibling) markValidity(hid.previousElementSibling, null);
@@ -1039,8 +1073,8 @@
1039
1073
  const n = items.length;
1040
1074
  const min = element.repeat.min ?? 0;
1041
1075
  const max = element.repeat.max ?? Infinity;
1042
- if (n < min) errors.push(`${key}: count < min=${min}`);
1043
- if (n > max) errors.push(`${key}: count > max=${max}`);
1076
+ if (!skipValidation && n < min) errors.push(`${key}: count < min=${min}`);
1077
+ if (!skipValidation && n > max) errors.push(`${key}: count > max=${max}`);
1044
1078
  items.forEach(item => {
1045
1079
  const obj = {};
1046
1080
  element.elements.forEach(child => {
@@ -1077,7 +1111,7 @@
1077
1111
  const schemaParam = params.get('schema');
1078
1112
 
1079
1113
  if (schemaParam) {
1080
- el.urlInfo.style.display = 'block';
1114
+ el.urlInfo.classList.remove('hidden');
1081
1115
  try {
1082
1116
  const schemaJson = atob(schemaParam);
1083
1117
  const schema = JSON.parse(schemaJson);
@@ -1132,19 +1166,7 @@
1132
1166
  downloadFile('schema.json', el.schemaInput.value || pretty(EXAMPLE_SCHEMA));
1133
1167
  });
1134
1168
 
1135
- el.submitBtn.addEventListener('click', () => {
1136
- clearError(el.formErrors);
1137
- if (!state.schema) {
1138
- showError(el.formErrors, 'Schema not applied');
1139
- return;
1140
- }
1141
- const { result, errors } = collectAndValidate(state.schema);
1142
- if (errors.length) {
1143
- showError(el.formErrors, errors.join('\n'));
1144
- return;
1145
- }
1146
- el.outputJson.value = pretty(result);
1147
- });
1169
+ // Submit handler is now defined as submitFormEnhanced below
1148
1170
 
1149
1171
  el.clearFormBtn.addEventListener('click', () => {
1150
1172
  if (!state.schema) return;
@@ -1261,12 +1283,265 @@
1261
1283
  Object.assign(state.config, config);
1262
1284
  }
1263
1285
  };
1286
+
1287
+ // Geppetto Integration - Message Handler
1288
+ function setupGeppettoMessaging() {
1289
+ // Listen for messages from parent window (Geppetto SPA)
1290
+ window.addEventListener('message', (event) => {
1291
+ // Security check - adjust origins as needed for your deployment
1292
+ const allowedOrigins = [
1293
+ 'https://your-geppetto-domain.com',
1294
+ 'http://localhost:3000',
1295
+ 'http://localhost:8080',
1296
+ window.location.origin
1297
+ ];
1298
+
1299
+ if (!allowedOrigins.some(origin => event.origin.startsWith(origin))) {
1300
+ return;
1301
+ }
1302
+
1303
+ handleGeppettoMessage(event.data);
1304
+ });
1305
+
1306
+ // Notify parent that form builder is ready
1307
+ if (window.parent !== window) {
1308
+ window.parent.postMessage({
1309
+ type: 'formBuilderReady'
1310
+ }, '*');
1311
+ }
1312
+ }
1313
+
1314
+ function handleGeppettoMessage(data) {
1315
+ switch (data.type) {
1316
+ case 'configure':
1317
+ if (data.config) {
1318
+ if (data.config.uploadHandler) {
1319
+ state.config.uploadFile = data.config.uploadHandler;
1320
+ }
1321
+ if (data.config.downloadHandler) {
1322
+ state.config.downloadFile = data.config.downloadHandler;
1323
+ }
1324
+ if (data.config.thumbnailHandler) {
1325
+ state.config.getThumbnail = data.config.thumbnailHandler;
1326
+ }
1327
+ Object.assign(state.config, data.config);
1328
+ }
1329
+
1330
+ if (data.options) {
1331
+ if (data.options.readonly) {
1332
+ enableReadOnlyMode();
1333
+ }
1334
+ if (data.options.theme) {
1335
+ setTheme(data.options.theme);
1336
+ }
1337
+ }
1338
+ break;
1339
+
1340
+ case 'setSchema':
1341
+ if (data.schema) {
1342
+ el.schemaInput.value = pretty(data.schema);
1343
+ applySchema();
1344
+ }
1345
+ break;
1346
+
1347
+ case 'setData':
1348
+ if (data.data && state.formRoot) {
1349
+ loadFormData(data.data);
1350
+ }
1351
+ break;
1352
+
1353
+ case 'getData':
1354
+ const currentData = getFormData();
1355
+ window.parent.postMessage({
1356
+ type: 'currentData',
1357
+ data: currentData
1358
+ }, '*');
1359
+ break;
1360
+
1361
+ case 'validate':
1362
+ const isValid = validateForm();
1363
+ window.parent.postMessage({
1364
+ type: 'validationResult',
1365
+ isValid: isValid
1366
+ }, '*');
1367
+ break;
1368
+ }
1369
+ }
1370
+
1371
+ function enableReadOnlyMode() {
1372
+ state.config.readonly = true;
1373
+
1374
+ // Hide editing controls
1375
+ const editingElements = [
1376
+ el.applySchemaBtn, el.resetSchemaBtn, el.prettySchemaBtn,
1377
+ el.downloadSchemaBtn, el.submitBtn, el.clearFormBtn,
1378
+ el.loadPrefillBtn, el.copyTemplateBtn
1379
+ ];
1380
+
1381
+ editingElements.forEach(element => {
1382
+ if (element) element.style.display = 'none';
1383
+ });
1384
+
1385
+ // Make schema input readonly
1386
+ if (el.schemaInput) {
1387
+ el.schemaInput.readOnly = true;
1388
+ el.schemaInput.style.backgroundColor = 'var(--muted)';
1389
+ }
1390
+
1391
+ // Disable form inputs
1392
+ if (state.formRoot) {
1393
+ const inputs = state.formRoot.querySelectorAll('input, textarea, select, button');
1394
+ inputs.forEach(input => {
1395
+ input.disabled = true;
1396
+ input.style.opacity = '0.6';
1397
+ });
1398
+ }
1399
+ }
1400
+
1401
+ function setTheme(theme) {
1402
+ document.documentElement.setAttribute('data-theme', theme);
1403
+
1404
+ if (theme === 'dark') {
1405
+ document.documentElement.style.colorScheme = 'dark';
1406
+ } else if (theme === 'light') {
1407
+ document.documentElement.style.colorScheme = 'light';
1408
+ } else {
1409
+ document.documentElement.style.colorScheme = 'light dark';
1410
+ }
1411
+ }
1412
+
1413
+ function getFormData() {
1414
+ if (!state.formRoot) return {};
1415
+
1416
+ try {
1417
+ const { result } = collectAndValidate(state.schema);
1418
+ return result;
1419
+ } catch (error) {
1420
+ console.error('Error getting form data:', error);
1421
+ return {};
1422
+ }
1423
+ }
1424
+
1425
+ function loadFormData(data) {
1426
+ if (!state.formRoot || !data) return;
1427
+
1428
+ Object.keys(data).forEach(key => {
1429
+ const input = state.formRoot.querySelector(`[data-key="${key}"]`);
1430
+ if (!input) return;
1431
+
1432
+ if (input.type === 'checkbox') {
1433
+ input.checked = Boolean(data[key]);
1434
+ } else if (input.type === 'file') {
1435
+ // For file inputs, store the value in dataset
1436
+ if (data[key]) {
1437
+ input.dataset.fileInfo = JSON.stringify(data[key]);
1438
+ // Trigger preview update if file preview is available
1439
+ const event = new Event('change');
1440
+ input.dispatchEvent(event);
1441
+ }
1442
+ } else {
1443
+ input.value = data[key] || '';
1444
+ }
1445
+ });
1446
+ }
1447
+
1448
+ function validateForm() {
1449
+ if (!state.formRoot || !state.schema) return true;
1450
+
1451
+ try {
1452
+ const { errors } = collectAndValidate(state.schema);
1453
+ return errors.length === 0;
1454
+ } catch {
1455
+ return false;
1456
+ }
1457
+ }
1458
+
1459
+ // Enhanced form submission to notify parent
1460
+ function submitFormEnhanced() {
1461
+ clearError(el.formErrors);
1462
+ if (!state.schema) {
1463
+ showError(el.formErrors, 'Schema not applied');
1464
+ return;
1465
+ }
1466
+
1467
+ try {
1468
+ const { result, errors } = collectAndValidate(state.schema);
1469
+
1470
+ if (errors.length > 0) {
1471
+ showError(el.formErrors, errors.join('\n'));
1472
+ return;
1473
+ }
1474
+
1475
+ // Update output display
1476
+ el.outputJson.value = pretty(result);
1477
+
1478
+ // Notify parent window if embedded
1479
+ if (window.parent !== window) {
1480
+ window.parent.postMessage({
1481
+ type: 'formSubmit',
1482
+ data: result,
1483
+ schema: state.schema
1484
+ }, '*');
1485
+ }
1486
+
1487
+ // Dispatch custom event for direct integration
1488
+ document.dispatchEvent(new CustomEvent('formSubmit', {
1489
+ detail: { data: result, schema: state.schema }
1490
+ }));
1491
+
1492
+ } catch (error) {
1493
+ showError(el.formErrors, 'Submission error: ' + error.message);
1494
+ }
1495
+ }
1496
+
1497
+ // Replace original submit handler
1498
+ el.submitBtn.addEventListener('click', submitFormEnhanced);
1499
+
1500
+ // Draft save handler
1501
+ el.saveDraftBtn.addEventListener('click', () => {
1502
+ clearError(el.formErrors);
1503
+ if (!state.schema) {
1504
+ showError(el.formErrors, 'Schema not applied');
1505
+ return;
1506
+ }
1507
+
1508
+ try {
1509
+ const { result } = collectAndValidate(state.schema, true); // Skip validation for drafts
1510
+
1511
+ // Update output display
1512
+ el.outputJson.value = pretty(result);
1513
+
1514
+ // Notify parent window if embedded
1515
+ if (window.parent !== window) {
1516
+ window.parent.postMessage({
1517
+ type: 'draftSaved',
1518
+ data: result,
1519
+ schema: state.schema
1520
+ }, '*');
1521
+ }
1522
+
1523
+ // Dispatch custom event for direct integration
1524
+ document.dispatchEvent(new CustomEvent('draftSaved', {
1525
+ detail: { data: result, schema: state.schema }
1526
+ }));
1527
+
1528
+ // Visual feedback
1529
+ el.saveDraftBtn.textContent = 'Draft Saved!';
1530
+ setTimeout(() => {
1531
+ el.saveDraftBtn.textContent = 'Save Draft';
1532
+ }, 2000);
1533
+
1534
+ } catch (error) {
1535
+ showError(el.formErrors, 'Draft save error: ' + error.message);
1536
+ }
1537
+ });
1264
1538
 
1265
1539
  // Initialize
1266
1540
  function init() {
1267
1541
  el.schemaInput.value = pretty(EXAMPLE_SCHEMA);
1268
1542
  renderForm(EXAMPLE_SCHEMA, {});
1269
1543
  loadSchemaFromURL();
1544
+ setupGeppettoMessaging();
1270
1545
 
1271
1546
  // Expose configuration API
1272
1547
  window.dispatchEvent(new CustomEvent('formBuilderReady', { detail: window.FormBuilderConfig }));