@dmitryvim/form-builder 0.2.29 → 0.2.32

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/esm/index.js CHANGED
@@ -46,8 +46,9 @@ function addRangeHint(element, parts, state) {
46
46
  }
47
47
  }
48
48
  function addFileSizeHint(element, parts, state) {
49
- if (element.maxSizeMB) {
50
- parts.push(t("hintMaxSize", state, { size: element.maxSizeMB }));
49
+ const sizeMB = element.maxSize ?? element.maxSizeMB;
50
+ if (sizeMB && sizeMB !== Infinity) {
51
+ parts.push(t("hintMaxSize", state, { size: sizeMB }));
51
52
  }
52
53
  }
53
54
  function addFormatHint(element, parts, state) {
@@ -120,6 +121,25 @@ function validateSchema(schema) {
120
121
  });
121
122
  }
122
123
  }
124
+ function validateContainerProps(element, elementPath, errors2) {
125
+ if ("columns" in element && element.columns !== void 0) {
126
+ const columns = element.columns;
127
+ const validColumns = [1, 2, 3, 4];
128
+ if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
129
+ errors2.push(
130
+ `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
131
+ );
132
+ }
133
+ }
134
+ if ("displayMode" in element && element.displayMode !== void 0) {
135
+ const displayMode = element.displayMode;
136
+ if (displayMode !== "stack" && displayMode !== "slides") {
137
+ errors2.push(
138
+ `${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
139
+ );
140
+ }
141
+ }
142
+ }
123
143
  function checkFlatOutputCollisions(elements, scopePath) {
124
144
  const allOutputKeys = /* @__PURE__ */ new Set();
125
145
  for (const el of elements) {
@@ -153,12 +173,14 @@ function validateSchema(schema) {
153
173
  allOutputKeys.add(textKey);
154
174
  allOutputKeys.add(filesKey);
155
175
  } else {
156
- if (allOutputKeys.has(el.key)) {
157
- errors.push(
158
- `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
159
- );
176
+ if (el.key) {
177
+ if (allOutputKeys.has(el.key)) {
178
+ errors.push(
179
+ `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
180
+ );
181
+ }
182
+ allOutputKeys.add(el.key);
160
183
  }
161
- allOutputKeys.add(el.key);
162
184
  }
163
185
  }
164
186
  }
@@ -168,9 +190,17 @@ function validateSchema(schema) {
168
190
  if (!element.type) {
169
191
  errors.push(`${elementPath}: missing type`);
170
192
  }
171
- if (!element.key) {
193
+ if (!element.key && element.type !== "markdown") {
172
194
  errors.push(`${elementPath}: missing key`);
173
195
  }
196
+ if (element.type === "markdown") {
197
+ const content = element.content;
198
+ if (typeof content !== "string") {
199
+ errors.push(
200
+ `${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
201
+ );
202
+ }
203
+ }
174
204
  if (element.enableIf) {
175
205
  const enableIf = element.enableIf;
176
206
  if (!enableIf.key || typeof enableIf.key !== "string") {
@@ -189,15 +219,7 @@ function validateSchema(schema) {
189
219
  validateElements(element.elements, `${elementPath}.elements`);
190
220
  }
191
221
  if (element.type === "container" && element.elements) {
192
- if ("columns" in element && element.columns !== void 0) {
193
- const columns = element.columns;
194
- const validColumns = [1, 2, 3, 4];
195
- if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
196
- errors.push(
197
- `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
198
- );
199
- }
200
- }
222
+ validateContainerProps(element, elementPath, errors);
201
223
  if ("prefillHints" in element && element.prefillHints) {
202
224
  const prefillHints = element.prefillHints;
203
225
  if (Array.isArray(prefillHints)) {
@@ -263,6 +285,18 @@ function escapeHtml(text) {
263
285
  div.textContent = text;
264
286
  return div.innerHTML;
265
287
  }
288
+ function getElementLookupKey(element, state) {
289
+ if (element.key) {
290
+ return element.key;
291
+ }
292
+ const cached = state.syntheticElementIds.get(element);
293
+ if (cached !== void 0) {
294
+ return cached;
295
+ }
296
+ const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
297
+ state.syntheticElementIds.set(element, id);
298
+ return id;
299
+ }
266
300
  function pathJoin(base, key) {
267
301
  return base ? `${base}.${key}` : key;
268
302
  }
@@ -818,8 +852,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
818
852
  const textareaWrapper = document.createElement("div");
819
853
  textareaWrapper.style.cssText = "position: relative;";
820
854
  const textareaInput = document.createElement("textarea");
821
- textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
822
- textareaInput.style.cssText = "padding-bottom: 24px;";
855
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
856
+ textareaInput.style.cssText = `
857
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
858
+ font-size: var(--fb-font-size);
859
+ font-family: var(--fb-font-family);
860
+ `;
823
861
  textareaInput.name = pathKey;
824
862
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
825
863
  textareaInput.rows = element.rows || 4;
@@ -871,8 +909,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
871
909
  const textareaContainer = document.createElement("div");
872
910
  textareaContainer.style.cssText = "position: relative;";
873
911
  const textareaInput = document.createElement("textarea");
874
- textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
875
- textareaInput.style.cssText = "padding-bottom: 24px;";
912
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
913
+ textareaInput.style.cssText = `
914
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
915
+ font-size: var(--fb-font-size);
916
+ font-family: var(--fb-font-family);
917
+ `;
876
918
  textareaInput.placeholder = element.placeholder || t("placeholderText", state);
877
919
  textareaInput.rows = element.rows || 4;
878
920
  textareaInput.value = value;
@@ -1058,8 +1100,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1058
1100
  inputWrapper.style.cssText = "position: relative;";
1059
1101
  const numberInput = document.createElement("input");
1060
1102
  numberInput.type = "number";
1061
- numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1062
- numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
1103
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1104
+ numberInput.style.cssText = `
1105
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1106
+ font-size: var(--fb-font-size);
1107
+ font-family: var(--fb-font-family);
1108
+ width: 100%;
1109
+ box-sizing: border-box;
1110
+ `;
1063
1111
  numberInput.name = pathKey;
1064
1112
  numberInput.placeholder = element.placeholder || "0";
1065
1113
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1111,8 +1159,14 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1111
1159
  inputContainer.style.cssText = "position: relative; flex: 1;";
1112
1160
  const numberInput = document.createElement("input");
1113
1161
  numberInput.type = "number";
1114
- numberInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1115
- numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
1162
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1163
+ numberInput.style.cssText = `
1164
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1165
+ font-size: var(--fb-font-size);
1166
+ font-family: var(--fb-font-family);
1167
+ width: 100%;
1168
+ box-sizing: border-box;
1169
+ `;
1116
1170
  numberInput.placeholder = element.placeholder || "0";
1117
1171
  if (element.min !== void 0) numberInput.min = element.min.toString();
1118
1172
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -1383,7 +1437,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1383
1437
  const state = ctx.state;
1384
1438
  const readonly = isElementReadonly(element, state, ctx);
1385
1439
  const selectInput = document.createElement("select");
1386
- selectInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1440
+ selectInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1441
+ selectInput.style.cssText = `
1442
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1443
+ font-size: var(--fb-font-size);
1444
+ font-family: var(--fb-font-family);
1445
+ `;
1387
1446
  selectInput.name = pathKey;
1388
1447
  selectInput.disabled = readonly;
1389
1448
  (element.options || []).forEach((option) => {
@@ -1435,7 +1494,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1435
1494
  const itemWrapper = document.createElement("div");
1436
1495
  itemWrapper.className = "multiple-select-item flex items-center gap-2";
1437
1496
  const selectInput = document.createElement("select");
1438
- selectInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1497
+ selectInput.className = "flex-1 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1498
+ selectInput.style.cssText = `
1499
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1500
+ font-size: var(--fb-font-size);
1501
+ font-family: var(--fb-font-family);
1502
+ `;
1439
1503
  selectInput.disabled = readonly;
1440
1504
  (element.options || []).forEach((option) => {
1441
1505
  const optionElement = document.createElement("option");
@@ -2170,7 +2234,13 @@ function ensureFileStyles() {
2170
2234
  style.textContent = `
2171
2235
  @keyframes fb-spin { to { transform: rotate(360deg); } }
2172
2236
 
2173
- /* Spinner used during single-file and multi-file upload */
2237
+ /* \u2500\u2500\u2500 Checker background utility \u2500\u2500\u2500 */
2238
+ /* Neutral diagonal-stripe background for image previews (never crops) */
2239
+ .fb-checker {
2240
+ background-image: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
2241
+ }
2242
+
2243
+ /* \u2500\u2500\u2500 Spinner \u2500\u2500\u2500 */
2174
2244
  .fb-spinner {
2175
2245
  width: 36px;
2176
2246
  height: 36px;
@@ -2181,207 +2251,271 @@ function ensureFileStyles() {
2181
2251
  flex-shrink: 0;
2182
2252
  }
2183
2253
 
2184
- /* Base tile: fixed 160\xD7160 square, theme-aware background */
2185
- .fb-tile {
2186
- width: var(--fb-tile-size, 160px);
2187
- height: var(--fb-tile-size, 160px);
2188
- flex-shrink: 0;
2189
- position: relative;
2254
+ /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2255
+ .fb-wide-tile {
2256
+ width: 100%;
2257
+ border-radius: 0.75rem;
2258
+ border: 1px dashed #60a5fa;
2259
+ background: rgba(239,246,255,0.5);
2260
+ display: flex;
2190
2261
  overflow: hidden;
2191
- border-radius: var(--fb-border-radius, 0.5rem);
2192
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2262
+ height: 180px;
2263
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2264
+ cursor: pointer;
2193
2265
  }
2194
-
2195
- /* Uploaded resource tile \u2014 adds a visible border */
2196
- .fb-tile-resource {
2197
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2266
+ .fb-wide-tile:hover {
2267
+ background: #eff6ff;
2198
2268
  }
2199
-
2200
- /* Uploading placeholder tile \u2014 dashed border, uploading indicator */
2201
- .fb-tile-uploading {
2202
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2269
+ .fb-wide-tile.fb-drag-over {
2270
+ border-color: #3b82f6;
2271
+ border-width: 2px;
2272
+ background: #eff6ff;
2273
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2203
2274
  }
2204
2275
 
2205
- /* "+" add-more tile */
2206
- .fb-tile-add {
2207
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2276
+ /* Upload zone inside wide tile */
2277
+ .fb-wide-tile-upload {
2278
+ flex: 1;
2208
2279
  display: flex;
2280
+ flex-direction: column;
2209
2281
  align-items: center;
2210
2282
  justify-content: center;
2283
+ gap: 8px;
2284
+ color: #2563eb;
2285
+ padding: 16px;
2286
+ transition: background 150ms;
2211
2287
  cursor: pointer;
2212
- font-size: 32px;
2213
- color: var(--fb-file-upload-text-color, #9ca3af);
2214
- transition:
2215
- border-color var(--fb-transition-duration, 200ms),
2216
- color var(--fb-transition-duration, 200ms);
2288
+ background: transparent;
2289
+ border: none;
2290
+ font-family: inherit;
2217
2291
  }
2218
- .fb-tile-add:hover {
2219
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2220
- color: var(--fb-text-color, #1f2937);
2292
+ .fb-wide-tile-upload:hover {
2293
+ background: rgba(191,219,254,0.25);
2221
2294
  }
2222
2295
 
2223
- /* Count chip shown when at maxCount */
2224
- .fb-tile-counter {
2225
- font-size: 11px;
2226
- color: var(--fb-text-secondary-color, #6b7280);
2227
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2228
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2229
- border-radius: 4px;
2230
- padding: 2px 6px;
2231
- align-self: flex-end;
2232
- margin-bottom: 4px;
2296
+ /* Vertical dashed divider between upload and library zones */
2297
+ .fb-wide-tile-divider {
2298
+ width: 1px;
2299
+ margin: 16px 0;
2300
+ border-left: 1px dashed rgba(96,165,250,0.5);
2301
+ background: transparent;
2302
+ flex-shrink: 0;
2233
2303
  }
2234
2304
 
2235
- /* Empty-state dropzone */
2236
- .fb-file-dropzone {
2237
- width: 100%;
2238
- height: 128px;
2239
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2240
- border-radius: var(--fb-border-radius, 0.5rem);
2305
+ /* Library zone inside wide tile */
2306
+ .fb-wide-tile-library {
2307
+ width: 176px;
2308
+ flex-shrink: 0;
2241
2309
  display: flex;
2242
2310
  flex-direction: column;
2243
2311
  align-items: center;
2244
2312
  justify-content: center;
2245
- gap: 4px;
2313
+ gap: 8px;
2314
+ color: #2563eb;
2315
+ padding: 12px;
2316
+ transition: background 150ms;
2246
2317
  cursor: pointer;
2247
- transition:
2248
- border-color var(--fb-transition-duration, 200ms),
2249
- background var(--fb-transition-duration, 200ms);
2318
+ background: transparent;
2319
+ border: none;
2320
+ font-family: inherit;
2250
2321
  }
2251
- .fb-file-dropzone:hover {
2252
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2253
- background: var(--fb-background-hover-color, #f9fafb);
2322
+ .fb-wide-tile-library:hover {
2323
+ background: rgba(191,219,254,0.25);
2254
2324
  }
2255
2325
 
2256
- /* Inline text inside tiles */
2257
- .fb-tile-label {
2258
- font-size: 9px;
2259
- color: var(--fb-text-secondary-color, #6b7280);
2260
- text-align: center;
2261
- overflow: hidden;
2262
- word-break: break-all;
2263
- max-height: 28px;
2326
+ /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2327
+ .fb-multi-outer {
2328
+ border-radius: 0.75rem;
2329
+ border: 1px dashed #cbd5e1;
2330
+ background: rgba(248,250,252,0.4);
2331
+ padding: 12px;
2332
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2333
+ }
2334
+ .fb-multi-outer.fb-drag-over {
2335
+ border-width: 2px;
2336
+ border-color: #3b82f6;
2337
+ background: rgba(239,246,255,0.4);
2338
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2264
2339
  }
2265
- .fb-tile-uploading-text {
2266
- font-size: 8px;
2267
- color: var(--fb-file-upload-text-color, #9ca3af);
2340
+
2341
+ /* With files present: white solid border */
2342
+ .fb-multi-outer.fb-multi-has-files {
2343
+ border-style: solid;
2344
+ border-color: #e2e8f0;
2345
+ background: #fff;
2268
2346
  }
2269
- .fb-tile-hint {
2270
- font-size: 11px;
2271
- color: var(--fb-file-upload-text-color, #9ca3af);
2272
- margin-top: 4px;
2347
+
2348
+ /* The CSS grid inside */
2349
+ .fb-multi-grid {
2350
+ display: grid;
2351
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2352
+ gap: 10px;
2273
2353
  }
2274
- .fb-tile-empty-text {
2275
- font-size: 12px;
2276
- color: var(--fb-text-secondary-color, #6b7280);
2277
- padding: 4px 0;
2354
+
2355
+ /* \u2500\u2500\u2500 Multi square add-tile (combined upload + library) \u2500\u2500\u2500 */
2356
+ .fb-multi-add-tile {
2357
+ aspect-ratio: 1 / 1;
2358
+ border-radius: 0.5rem;
2359
+ border: 1px dashed #60a5fa;
2360
+ background: rgba(239,246,255,0.5);
2361
+ display: flex;
2362
+ flex-direction: column;
2363
+ overflow: hidden;
2364
+ transition: background 150ms;
2278
2365
  }
2279
- .fb-dropzone-primary-text {
2280
- font-size: 13px;
2281
- color: var(--fb-text-secondary-color, #6b7280);
2366
+ .fb-multi-add-tile:hover {
2367
+ background: #eff6ff;
2282
2368
  }
2283
- .fb-dropzone-hint-text {
2284
- font-size: 11px;
2285
- color: var(--fb-file-upload-text-color, #9ca3af);
2369
+ .fb-multi-add-tile.fb-drag-over-tile {
2370
+ border-width: 2px;
2371
+ border-color: #3b82f6;
2372
+ background: rgba(255,255,255,0.8);
2286
2373
  }
2287
2374
 
2288
- /* Hover overlay + X-button on resource tiles */
2289
- .fb-tile-overlay {
2290
- position: absolute;
2291
- inset: 0;
2292
- background: transparent;
2293
- transition: background var(--fb-transition-duration, 200ms);
2294
- display: flex;
2295
- align-items: flex-start;
2296
- justify-content: flex-end;
2297
- }
2298
- .fb-tile-resource:hover .fb-tile-overlay {
2299
- background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
2300
- }
2301
- .fb-tile-x-btn {
2302
- margin: 3px;
2303
- width: 18px;
2304
- height: 18px;
2305
- background: var(--fb-error-color, #ef4444);
2306
- color: var(--fb-file-bg-color, #fff);
2307
- border: none;
2308
- border-radius: 50%;
2309
- font-size: 11px;
2310
- line-height: 1;
2311
- cursor: pointer;
2375
+ /* Upload half of add-tile */
2376
+ .fb-multi-add-upload {
2377
+ flex: 1;
2312
2378
  display: flex;
2379
+ flex-direction: column;
2313
2380
  align-items: center;
2314
2381
  justify-content: center;
2315
- opacity: 0;
2316
- transition: opacity var(--fb-transition-duration, 200ms);
2382
+ gap: 4px;
2383
+ color: #2563eb;
2384
+ cursor: pointer;
2385
+ background: transparent;
2386
+ border: none;
2387
+ font-family: inherit;
2388
+ width: 100%;
2389
+ transition: background 150ms;
2317
2390
  }
2318
- .fb-tile-resource:hover .fb-tile-x-btn {
2319
- opacity: 1;
2391
+ .fb-multi-add-upload:hover {
2392
+ background: rgba(191,219,254,0.35);
2320
2393
  }
2321
2394
 
2322
- /* Video play button overlay (readonly tiles with video thumbnails) */
2323
- .fb-video-overlay {
2324
- position: absolute;
2325
- inset: 0;
2326
- display: flex;
2327
- align-items: center;
2328
- justify-content: center;
2329
- background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
2395
+ /* Horizontal dashed divider inside add-tile */
2396
+ .fb-multi-add-divider {
2397
+ border-top: 1px dashed rgba(96,165,250,0.5);
2398
+ margin: 0;
2399
+ flex-shrink: 0;
2330
2400
  }
2331
- .fb-play-btn {
2332
- background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
2333
- border-radius: 50%;
2401
+
2402
+ /* Library strip at bottom of add-tile */
2403
+ .fb-multi-add-library {
2404
+ padding: 6px 0;
2334
2405
  display: flex;
2335
2406
  align-items: center;
2336
2407
  justify-content: center;
2408
+ gap: 4px;
2409
+ color: #2563eb;
2410
+ font-size: 11px;
2411
+ font-weight: 500;
2412
+ cursor: pointer;
2413
+ background: transparent;
2414
+ border: none;
2415
+ font-family: inherit;
2416
+ width: 100%;
2417
+ transition: background 150ms;
2418
+ flex-shrink: 0;
2419
+ }
2420
+ .fb-multi-add-library:hover {
2421
+ background: rgba(191,219,254,0.35);
2337
2422
  }
2338
2423
 
2339
- /* Edit-mode local video preview wrapper */
2340
- .fb-video-preview-wrap {
2424
+ /* \u2500\u2500\u2500 Capacity placeholder squares \u2500\u2500\u2500 */
2425
+ .fb-multi-placeholder {
2426
+ aspect-ratio: 1 / 1;
2427
+ border-radius: 0.5rem;
2428
+ border: 1px solid #e2e8f0;
2429
+ }
2430
+ .fb-multi-placeholder.fb-drag-over {
2431
+ border-width: 2px;
2432
+ border-style: dashed;
2433
+ border-color: #93c5fd;
2434
+ background: rgba(219,234,254,0.6);
2435
+ }
2436
+
2437
+ /* \u2500\u2500\u2500 Filled preview tile \u2500\u2500\u2500 */
2438
+ .fb-preview-tile {
2439
+ aspect-ratio: 1 / 1;
2440
+ border-radius: 0.5rem;
2441
+ border: 1px solid #e2e8f0;
2442
+ overflow: hidden;
2341
2443
  position: relative;
2444
+ cursor: pointer;
2445
+ }
2446
+ .fb-preview-tile img {
2342
2447
  width: 100%;
2343
2448
  height: 100%;
2449
+ object-fit: contain;
2450
+ display: block;
2344
2451
  }
2345
2452
 
2346
- /* Hover overlay for edit-mode local video (Remove / Change buttons) */
2347
- .fb-video-btn-overlay {
2348
- position: absolute;
2349
- top: 8px;
2350
- right: 8px;
2351
- z-index: 10;
2453
+ /* \u2500\u2500\u2500 Uploading placeholder tile \u2500\u2500\u2500 */
2454
+ .fb-uploading-tile {
2455
+ aspect-ratio: 1 / 1;
2456
+ border-radius: 0.5rem;
2457
+ border: 2px dashed #d1d5db;
2352
2458
  display: flex;
2353
- gap: 4px;
2354
- opacity: 0;
2355
- transition: opacity var(--fb-transition-duration, 200ms);
2356
- pointer-events: none;
2459
+ flex-direction: column;
2460
+ align-items: center;
2461
+ justify-content: center;
2462
+ gap: 6px;
2463
+ padding: 6px;
2357
2464
  }
2358
- .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2359
- opacity: 1;
2360
- pointer-events: auto;
2465
+
2466
+ /* \u2500\u2500\u2500 Meta line below multi grid \u2500\u2500\u2500 */
2467
+ .fb-meta-line {
2468
+ margin-top: 10px;
2469
+ display: flex;
2470
+ align-items: center;
2471
+ justify-content: space-between;
2472
+ gap: 8px;
2473
+ flex-wrap: wrap;
2361
2474
  }
2362
- .fb-video-btn {
2363
- border: none;
2364
- border-radius: var(--fb-border-radius, 4px);
2365
- font-size: 11px;
2366
- padding: 4px 8px;
2367
- cursor: pointer;
2368
- color: #fff;
2369
- line-height: 1.2;
2475
+ .fb-meta-text {
2476
+ font-size: 12px;
2477
+ color: #94a3b8;
2478
+ display: flex;
2479
+ align-items: center;
2480
+ gap: 8px;
2481
+ flex-wrap: wrap;
2370
2482
  }
2371
- .fb-video-btn-delete {
2372
- background: rgba(220, 38, 38, 0.85);
2483
+ .fb-meta-dot {
2484
+ width: 4px;
2485
+ height: 4px;
2486
+ border-radius: 50%;
2487
+ background: #cbd5e1;
2488
+ flex-shrink: 0;
2373
2489
  }
2374
- .fb-video-btn-delete:hover {
2375
- background: rgba(185, 28, 28, 0.95);
2490
+ .fb-meta-mono {
2491
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
2492
+ font-size: 11px;
2493
+ letter-spacing: -0.02em;
2376
2494
  }
2377
- .fb-video-btn-change {
2378
- background: rgba(31, 41, 55, 0.85);
2495
+ .fb-clear-all-btn {
2496
+ font-size: 12px;
2497
+ color: #94a3b8;
2498
+ background: none;
2499
+ border: none;
2500
+ cursor: pointer;
2501
+ padding: 0;
2502
+ font-family: inherit;
2503
+ transition: color 150ms;
2504
+ white-space: nowrap;
2505
+ flex-shrink: 0;
2379
2506
  }
2380
- .fb-video-btn-change:hover {
2381
- background: rgba(17, 24, 39, 0.95);
2507
+ .fb-clear-all-btn:hover {
2508
+ color: #dc2626;
2509
+ }
2510
+
2511
+ /* \u2500\u2500\u2500 Empty text (readonly) \u2500\u2500\u2500 */
2512
+ .fb-tile-empty-text {
2513
+ font-size: 11px;
2514
+ color: var(--fb-text-secondary-color, #6b7280);
2515
+ padding: 4px 0;
2382
2516
  }
2383
2517
 
2384
- /* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
2518
+ /* \u2500\u2500\u2500 Tile action buttons (for zoom popup, compat) \u2500\u2500\u2500 */
2385
2519
  .fb-tile-actions {
2386
2520
  position: absolute;
2387
2521
  top: 3px;
@@ -2393,37 +2527,35 @@ function ensureFileStyles() {
2393
2527
  transition: opacity var(--fb-transition-duration, 200ms);
2394
2528
  z-index: 10;
2395
2529
  }
2396
- .fb-tile-resource:hover .fb-tile-actions {
2530
+ .fb-preview-tile:hover .fb-tile-actions {
2397
2531
  opacity: 1;
2398
2532
  }
2399
2533
  .fb-tile-action-btn {
2400
- width: 28px;
2401
- height: 28px;
2534
+ width: 24px;
2535
+ height: 24px;
2402
2536
  display: flex;
2403
2537
  align-items: center;
2404
2538
  justify-content: center;
2405
- border: none;
2406
- border-radius: 50%;
2539
+ border: 1px solid rgba(15,23,42,0.08);
2540
+ border-radius: 0.375rem;
2407
2541
  cursor: pointer;
2408
- background: rgba(31, 41, 55, 0.75);
2409
- color: #fff;
2542
+ background: rgba(255,255,255,0.92);
2543
+ color: #374151;
2410
2544
  padding: 0;
2411
2545
  flex-shrink: 0;
2412
- transition:
2413
- background var(--fb-transition-duration, 200ms),
2414
- opacity var(--fb-transition-duration, 200ms);
2546
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
2547
+ transition: background var(--fb-transition-duration, 200ms),
2548
+ color var(--fb-transition-duration, 200ms);
2415
2549
  }
2416
2550
  .fb-tile-action-btn:hover {
2417
- background: rgba(17, 24, 39, 0.95);
2418
- }
2419
- .fb-tile-action-remove {
2420
- background: rgba(220, 38, 38, 0.8);
2551
+ background: #ffffff;
2552
+ color: #0f172a;
2421
2553
  }
2422
2554
  .fb-tile-action-remove:hover {
2423
- background: rgba(185, 28, 28, 0.95);
2555
+ color: #dc2626;
2424
2556
  }
2425
2557
 
2426
- /* Actions row inside zoom popup \u2014 always visible while popup is shown */
2558
+ /* Zoom popup action buttons always visible */
2427
2559
  .fb-tile-zoom-preview .fb-tile-actions {
2428
2560
  position: absolute;
2429
2561
  top: 6px;
@@ -2432,116 +2564,145 @@ function ensureFileStyles() {
2432
2564
  z-index: 10000;
2433
2565
  }
2434
2566
 
2435
- /* Two-card empty-state layout (upload card + library card) */
2436
- .fb-file-card-row {
2437
- display: flex;
2438
- gap: 8px;
2439
- align-items: stretch;
2567
+ /* \u2500\u2500\u2500 Hover zoom preview popup \u2500\u2500\u2500 */
2568
+ .fb-tile-zoom-preview {
2569
+ position: fixed;
2570
+ z-index: 9999;
2571
+ background: var(--fb-background-color, #fff);
2572
+ border: 1px solid #e2e8f0;
2573
+ border-radius: 0.5rem;
2574
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2575
+ padding: 4px;
2576
+ width: 350px;
2577
+ height: 350px;
2578
+ pointer-events: none;
2579
+ opacity: 0;
2580
+ transition: opacity 150ms ease;
2440
2581
  }
2441
- .fb-file-card-row .fb-file-dropzone,
2442
- .fb-file-card-row .fb-file-library-card {
2443
- flex: 1;
2444
- min-width: 0;
2582
+ .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2583
+ opacity: 1;
2584
+ }
2585
+ .fb-tile-zoom-preview-img {
2586
+ width: 100%;
2587
+ height: 100%;
2588
+ object-fit: contain;
2589
+ display: block;
2590
+ border-radius: calc(0.5rem - 2px);
2445
2591
  }
2446
2592
 
2447
- /* Library picker card \u2014 mirrors .fb-file-dropzone styling */
2448
- .fb-file-library-card {
2449
- height: 128px;
2450
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2451
- border-radius: var(--fb-border-radius, 0.5rem);
2593
+ /* \u2500\u2500\u2500 Single-file uploading state \u2500\u2500\u2500 */
2594
+ .fb-single-uploading {
2595
+ height: 180px;
2596
+ border-radius: 0.75rem;
2597
+ border: 1px dashed #60a5fa;
2598
+ background: rgba(239,246,255,0.5);
2452
2599
  display: flex;
2453
2600
  flex-direction: column;
2454
2601
  align-items: center;
2455
2602
  justify-content: center;
2456
- gap: 4px;
2457
- cursor: pointer;
2458
- background: none;
2459
- padding: 0;
2460
- transition:
2461
- border-color var(--fb-transition-duration, 200ms),
2462
- background var(--fb-transition-duration, 200ms);
2463
- width: 100%;
2603
+ gap: 8px;
2464
2604
  }
2465
- .fb-file-library-card:hover,
2466
- .fb-file-library-card:focus-visible {
2467
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2468
- background: var(--fb-background-hover-color, #f9fafb);
2469
- outline: none;
2605
+
2606
+ /* \u2500\u2500\u2500 Video overlays \u2500\u2500\u2500 */
2607
+ .fb-video-overlay {
2608
+ position: absolute;
2609
+ inset: 0;
2610
+ display: flex;
2611
+ align-items: center;
2612
+ justify-content: center;
2613
+ background: rgba(0,0,0,0.25);
2470
2614
  }
2471
- .fb-file-library-card-icon {
2472
- font-size: 24px;
2473
- line-height: 1;
2474
- flex-shrink: 0;
2615
+ .fb-play-btn {
2616
+ background: rgba(255,255,255,0.9);
2617
+ border-radius: 50%;
2618
+ display: flex;
2619
+ align-items: center;
2620
+ justify-content: center;
2475
2621
  }
2476
- .fb-file-library-card-label {
2477
- font-size: 13px;
2478
- color: var(--fb-text-secondary-color, #6b7280);
2622
+ .fb-video-preview-wrap {
2623
+ position: relative;
2624
+ width: 100%;
2625
+ height: 100%;
2479
2626
  }
2480
- .fb-file-library-card-hint {
2627
+ .fb-video-btn-overlay {
2628
+ position: absolute;
2629
+ top: 8px;
2630
+ right: 8px;
2631
+ z-index: 10;
2632
+ display: flex;
2633
+ gap: 4px;
2634
+ opacity: 0;
2635
+ transition: opacity 150ms;
2636
+ pointer-events: none;
2637
+ }
2638
+ .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2639
+ opacity: 1;
2640
+ pointer-events: auto;
2641
+ }
2642
+ .fb-video-btn {
2643
+ border: none;
2644
+ border-radius: 4px;
2481
2645
  font-size: 11px;
2482
- color: var(--fb-file-upload-text-color, #9ca3af);
2646
+ padding: 4px 8px;
2647
+ cursor: pointer;
2648
+ color: #fff;
2649
+ line-height: 1.2;
2483
2650
  }
2651
+ .fb-video-btn-delete { background: rgba(220,38,38,0.85); }
2652
+ .fb-video-btn-delete:hover { background: rgba(185,28,28,0.95); }
2653
+ .fb-video-btn-change { background: rgba(31,41,55,0.85); }
2654
+ .fb-video-btn-change:hover { background: rgba(17,24,39,0.95); }
2484
2655
 
2485
- /* Library "\u{1F4DA}" add-tile \u2014 same size/style as the "+" add tile */
2486
- .fb-tile-add-library {
2487
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2488
- display: flex;
2489
- align-items: center;
2490
- justify-content: center;
2491
- cursor: pointer;
2492
- font-size: 24px;
2493
- color: var(--fb-file-upload-text-color, #9ca3af);
2494
- transition:
2495
- border-color var(--fb-transition-duration, 200ms),
2496
- color var(--fb-transition-duration, 200ms);
2497
- background: none;
2498
- padding: 0;
2499
- width: var(--fb-tile-size, 160px);
2500
- height: var(--fb-tile-size, 160px);
2501
- flex-shrink: 0;
2502
- position: relative;
2656
+ /* \u2500\u2500\u2500 Readonly readonly tile \u2500\u2500\u2500 */
2657
+ .fb-readonly-tile {
2658
+ aspect-ratio: 1 / 1;
2659
+ border-radius: 0.5rem;
2660
+ border: 1px solid #e2e8f0;
2503
2661
  overflow: hidden;
2504
- border-radius: var(--fb-border-radius, 0.5rem);
2662
+ position: relative;
2663
+ cursor: pointer;
2505
2664
  }
2506
- .fb-tile-add-library:hover,
2507
- .fb-tile-add-library:focus-visible {
2508
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2509
- color: var(--fb-text-color, #1f2937);
2510
- outline: none;
2665
+ .fb-readonly-tile img {
2666
+ width: 100%;
2667
+ height: 100%;
2668
+ object-fit: contain;
2669
+ display: block;
2511
2670
  }
2512
-
2513
- /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2514
- .fb-tile-zoom-preview {
2515
- position: fixed;
2516
- z-index: 9999;
2517
- background: var(--fb-background-color, #fff);
2518
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2519
- border-radius: var(--fb-border-radius, 0.5rem);
2520
- box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2521
- padding: 4px;
2522
- width: 350px;
2523
- height: 350px;
2524
- pointer-events: none;
2671
+ .fb-readonly-tile .fb-tile-actions {
2525
2672
  opacity: 0;
2526
- transition: opacity 150ms ease;
2527
2673
  }
2528
- .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2674
+ .fb-readonly-tile:hover .fb-tile-actions {
2529
2675
  opacity: 1;
2530
2676
  }
2531
- .fb-tile-zoom-preview-img {
2677
+
2678
+ /* \u2500\u2500\u2500 Readonly single-file filled \u2500\u2500\u2500 */
2679
+ .fb-single-readonly-filled {
2680
+ position: relative;
2681
+ border-radius: 0.75rem;
2682
+ border: 1px solid #e2e8f0;
2683
+ overflow: hidden;
2684
+ height: 220px;
2685
+ display: block;
2686
+ cursor: pointer;
2687
+ }
2688
+ .fb-single-readonly-filled img {
2532
2689
  width: 100%;
2533
2690
  height: 100%;
2534
2691
  object-fit: contain;
2535
2692
  display: block;
2536
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2537
- border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
2693
+ }
2694
+
2695
+ /* \u2500\u2500\u2500 Readonly multi grid \u2500\u2500\u2500 */
2696
+ .fb-multi-readonly-grid {
2697
+ display: grid;
2698
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2699
+ gap: 10px;
2538
2700
  }
2539
2701
  `;
2540
2702
  document.head.appendChild(style);
2541
2703
  }
2542
2704
 
2543
2705
  // src/components/file/dom.ts
2544
- var TILE_SIZE = "160px";
2545
2706
  function createFileTile() {
2546
2707
  ensureFileStyles();
2547
2708
  const tile = document.createElement("div");
@@ -2549,7 +2710,7 @@ function createFileTile() {
2549
2710
  return tile;
2550
2711
  }
2551
2712
  function showFileError(container, message) {
2552
- const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2713
+ const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
2553
2714
  if (existing) existing.remove();
2554
2715
  const errorEl = document.createElement("div");
2555
2716
  errorEl.className = "file-error-message error-message";
@@ -2559,10 +2720,10 @@ function showFileError(container, message) {
2559
2720
  margin-top: 0.25rem;
2560
2721
  `;
2561
2722
  errorEl.textContent = message;
2562
- container.closest(".space-y-2")?.appendChild(errorEl);
2723
+ container.closest("[data-files-wrapper]")?.appendChild(errorEl);
2563
2724
  }
2564
2725
  function clearFileError(container) {
2565
- const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2726
+ const existing = container.closest("[data-files-wrapper]")?.querySelector(".file-error-message");
2566
2727
  if (existing) existing.remove();
2567
2728
  }
2568
2729
  function addDeleteButton(container, state, onDelete) {
@@ -2580,13 +2741,6 @@ function addDeleteButton(container, state, onDelete) {
2580
2741
  overlay.appendChild(deleteBtn);
2581
2742
  container.appendChild(overlay);
2582
2743
  }
2583
- function findFilePicker(container) {
2584
- let el = container.parentElement;
2585
- while (el && !el.dataset.filesWrapper) {
2586
- el = el.parentElement;
2587
- }
2588
- return el?.querySelector('input[type="file"]') ?? null;
2589
- }
2590
2744
  function createUploadingTile(fileName, state) {
2591
2745
  ensureFileStyles();
2592
2746
  const tile = createFileTile();
@@ -2602,10 +2756,13 @@ function createUploadingTile(fileName, state) {
2602
2756
  return tile;
2603
2757
  }
2604
2758
  function ensureTilesWrap(list) {
2759
+ const existingGrid = list.querySelector(".fb-multi-grid");
2760
+ if (existingGrid) return existingGrid;
2605
2761
  const existing = list.querySelector(".fb-tiles-wrap");
2606
2762
  if (existing) return existing;
2607
- const dropzone = list.querySelector(".fb-file-dropzone");
2608
- if (dropzone) dropzone.remove();
2763
+ list.querySelector(".fb-file-dropzone")?.remove();
2764
+ list.querySelector(".fb-wide-tile")?.remove();
2765
+ list.querySelector(".fb-multi-outer")?.remove();
2609
2766
  const tilesWrap = document.createElement("div");
2610
2767
  tilesWrap.className = "fb-tiles-wrap";
2611
2768
  tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
@@ -2617,7 +2774,7 @@ function ensureTilesWrap(list) {
2617
2774
  return tilesWrap;
2618
2775
  }
2619
2776
  function setEmptyFileContainer(fileContainer, state, hint) {
2620
- const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2777
+ const hintHtml = "";
2621
2778
  fileContainer.innerHTML = `
2622
2779
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
2623
2780
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
@@ -2659,9 +2816,11 @@ function setupDragAndDrop(element, dropHandler) {
2659
2816
  }
2660
2817
 
2661
2818
  // src/components/file/preview.ts
2662
- var ICON_DOWNLOAD = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
2663
- var ICON_OPEN = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
2664
- var ICON_REMOVE = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>`;
2819
+ var ICON_DOWNLOAD = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
2820
+ var ICON_OPEN = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
2821
+ var ICON_REMOVE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/></svg>`;
2822
+ var ICON_REPLACE = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 18a4 4 0 000-8 6 6 0 00-11.5 2A4 4 0 006 20h11M12 12v7M12 12l-3 3M12 12l3 3"/></svg>`;
2823
+ var ICON_LIBRARY = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 5l3.5 1-3 14L13 19"/></svg>`;
2665
2824
  function canDownload(state, meta) {
2666
2825
  return Boolean(
2667
2826
  state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || meta?.file
@@ -2673,7 +2832,16 @@ function canOpenInTab(state, meta) {
2673
2832
  );
2674
2833
  }
2675
2834
  function createTileActions(options) {
2676
- const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
2835
+ const {
2836
+ canRemove,
2837
+ removeHandler,
2838
+ state,
2839
+ resourceId,
2840
+ fileName,
2841
+ meta,
2842
+ replaceHandler,
2843
+ libraryHandler
2844
+ } = options;
2677
2845
  const group = document.createElement("div");
2678
2846
  group.className = "fb-tile-actions";
2679
2847
  const makeBtn = (icon, label, cls) => {
@@ -2688,6 +2856,16 @@ function createTileActions(options) {
2688
2856
  });
2689
2857
  return btn;
2690
2858
  };
2859
+ if (replaceHandler) {
2860
+ const replaceBtn = makeBtn(ICON_REPLACE, t("replaceFile", state), "fb-tile-action-replace");
2861
+ replaceBtn.addEventListener("click", () => replaceHandler());
2862
+ group.appendChild(replaceBtn);
2863
+ }
2864
+ if (libraryHandler) {
2865
+ const libBtn = makeBtn(ICON_LIBRARY, t("fromLibrary", state), "fb-tile-action-library");
2866
+ libBtn.addEventListener("click", () => libraryHandler());
2867
+ group.appendChild(libBtn);
2868
+ }
2691
2869
  if (canDownload(state, meta)) {
2692
2870
  const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
2693
2871
  dlBtn.addEventListener("click", () => {
@@ -2873,8 +3051,7 @@ function attachClonedActionListeners(cloned, original) {
2873
3051
  }
2874
3052
  function renderLocalImagePreview(container, file, fileName, state) {
2875
3053
  const img = document.createElement("img");
2876
- img.className = "w-full h-full object-contain";
2877
- img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
3054
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2878
3055
  img.alt = fileName || t("previewAlt", state);
2879
3056
  const reader = new FileReader();
2880
3057
  reader.onload = (e) => {
@@ -2896,7 +3073,7 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2896
3073
  const newContainer = setupDragDropless(container);
2897
3074
  newContainer.innerHTML = `
2898
3075
  <div class="fb-video-preview-wrap">
2899
- <video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
3076
+ <video style="width:100%;height:100%;object-fit:contain;" controls preload="auto" muted src="${videoUrl}">
2900
3077
  ${escapeHtml(t("videoNotSupported", state))}
2901
3078
  </video>
2902
3079
  <div class="fb-video-btn-overlay">
@@ -2940,11 +3117,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
2940
3117
  container.onclick = deps.fileUploadHandler;
2941
3118
  }
2942
3119
  container.innerHTML = `
2943
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2944
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3120
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3121
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
2945
3122
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2946
3123
  </svg>
2947
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3124
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2948
3125
  </div>
2949
3126
  `;
2950
3127
  if (deps?.setupDrop) {
@@ -2961,11 +3138,11 @@ function renderDeleteButton(container, resourceId, state) {
2961
3138
  hiddenInput.value = "";
2962
3139
  }
2963
3140
  container.innerHTML = `
2964
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2965
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3141
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3142
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
2966
3143
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
2967
3144
  </svg>
2968
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3145
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2969
3146
  </div>
2970
3147
  `;
2971
3148
  });
@@ -2984,7 +3161,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
2984
3161
  deps
2985
3162
  );
2986
3163
  } else {
2987
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div style="font-size:36px;" class="mb-2">\u{1F4C1}</div><div class="text-sm">${escapeHtml(fileName)}</div></div>`;
3164
+ container.innerHTML = `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);"><div style="font-size:36px;margin-bottom:0.5rem;">\u{1F4C1}</div><div style="font-size:0.875rem;">${escapeHtml(fileName)}</div></div>`;
2988
3165
  }
2989
3166
  if (!isReadonly && !meta.type?.startsWith("video/")) {
2990
3167
  renderDeleteButton(container, resourceId, state);
@@ -2992,7 +3169,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
2992
3169
  }
2993
3170
  function renderUploadedVideoPreview(container, thumbnailUrl, state) {
2994
3171
  const video = document.createElement("video");
2995
- video.className = "w-full h-full object-contain";
3172
+ video.style.cssText = "width:100%;height:100%;object-fit:contain;";
2996
3173
  video.controls = true;
2997
3174
  video.preload = "metadata";
2998
3175
  video.muted = true;
@@ -3013,8 +3190,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3013
3190
  renderUploadedVideoPreview(container, thumbnailUrl, state);
3014
3191
  } else {
3015
3192
  const img = document.createElement("img");
3016
- img.className = "w-full h-full object-contain";
3017
- img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
3193
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3018
3194
  img.alt = fileName || t("previewAlt", state);
3019
3195
  img.src = thumbnailUrl;
3020
3196
  container.appendChild(img);
@@ -3025,11 +3201,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3025
3201
  } catch (error) {
3026
3202
  console.error("Failed to get thumbnail:", error);
3027
3203
  container.innerHTML = `
3028
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
3029
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3204
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3205
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
3030
3206
  <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
3031
3207
  </svg>
3032
- <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3208
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3033
3209
  </div>
3034
3210
  `;
3035
3211
  }
@@ -3203,13 +3379,9 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3203
3379
  const img = document.createElement("img");
3204
3380
  img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3205
3381
  img.alt = meta.name;
3206
- const reader = new FileReader();
3207
- reader.onload = (e) => {
3208
- img.src = e.target?.result || "";
3209
- attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
3210
- };
3211
- reader.readAsDataURL(meta.file);
3382
+ img.src = getLocalFileUrl(meta.file);
3212
3383
  tile.appendChild(img);
3384
+ attachZoomHover(tile, img.src, meta.name, actionsEl ?? null);
3213
3385
  } else if (state.config.getThumbnail) {
3214
3386
  try {
3215
3387
  const url = await state.config.getThumbnail(rid);
@@ -3266,17 +3438,20 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3266
3438
  }
3267
3439
  if (actionsEl) tile.appendChild(actionsEl);
3268
3440
  } else {
3269
- const name = meta?.name ?? "";
3270
- const hasExtension = name.includes(".");
3271
- const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
3272
- tile.innerHTML = `
3273
- <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3274
- <div style="font-size:36px;">\u{1F4C1}</div>
3275
- ${captionHtml}
3276
- </div>`;
3277
- if (actionsEl) tile.appendChild(actionsEl);
3441
+ fillDocumentFallback(tile, rid, meta, actionsEl);
3278
3442
  }
3279
3443
  }
3444
+ function fillDocumentFallback(tile, rid, meta, actionsEl) {
3445
+ const fileName = meta?.name ?? rid.split("/").pop() ?? "";
3446
+ if (fileName) tile.title = fileName;
3447
+ const labelHtml = fileName ? `<div style="font-size:11px;line-height:1.2;text-align:center;color:var(--fb-text-secondary-color,#6b7280);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${escapeHtml(fileName)}</div>` : "";
3448
+ tile.innerHTML = `
3449
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3450
+ <div style="font-size:36px;">\u{1F4C1}</div>
3451
+ ${labelHtml}
3452
+ </div>`;
3453
+ if (actionsEl) tile.appendChild(actionsEl);
3454
+ }
3280
3455
  async function forceDownload(resourceId, fileName, state) {
3281
3456
  try {
3282
3457
  let fileUrl = null;
@@ -3376,6 +3551,10 @@ async function handleFileSelect(opts) {
3376
3551
  return;
3377
3552
  }
3378
3553
  clearFileError(container);
3554
+ const existingHiddenInput = container.parentElement?.querySelector(
3555
+ 'input[type="hidden"]'
3556
+ );
3557
+ const previousRid = existingHiddenInput?.value || null;
3379
3558
  ensureFileStyles();
3380
3559
  container.innerHTML = `
3381
3560
  <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
@@ -3386,7 +3565,13 @@ async function handleFileSelect(opts) {
3386
3565
  try {
3387
3566
  rid = await uploadSingleFile(file, state);
3388
3567
  } catch (error) {
3389
- setEmptyFileContainer(container, state);
3568
+ if (previousRid && deps?.onAfterUpload) {
3569
+ deps.onAfterUpload(container, previousRid);
3570
+ } else if (deps?.onRemove) {
3571
+ deps.onRemove();
3572
+ } else {
3573
+ setEmptyFileContainer(container, state);
3574
+ }
3390
3575
  throw error;
3391
3576
  }
3392
3577
  state.resourceIndex.set(rid, {
@@ -3396,9 +3581,10 @@ async function handleFileSelect(opts) {
3396
3581
  uploadedAt: /* @__PURE__ */ new Date(),
3397
3582
  file
3398
3583
  });
3399
- let hiddenInput = container.parentElement?.querySelector(
3400
- 'input[type="hidden"]'
3401
- );
3584
+ if (previousRid && previousRid !== rid) {
3585
+ releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
3586
+ }
3587
+ let hiddenInput = existingHiddenInput;
3402
3588
  if (!hiddenInput) {
3403
3589
  hiddenInput = document.createElement("input");
3404
3590
  hiddenInput.type = "hidden";
@@ -3407,7 +3593,9 @@ async function handleFileSelect(opts) {
3407
3593
  }
3408
3594
  hiddenInput.value = rid;
3409
3595
  const isVideo = file.type.startsWith("video/");
3410
- if (!isVideo && deps) {
3596
+ if (!isVideo && deps?.onAfterUpload) {
3597
+ deps.onAfterUpload(container, rid);
3598
+ } else if (!isVideo && deps) {
3411
3599
  renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
3412
3600
  } else {
3413
3601
  renderFilePreview(container, rid, state, {
@@ -3468,17 +3656,17 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3468
3656
  return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3469
3657
  }
3470
3658
  async function uploadBatch(accepted, resourceIds, listEl, state) {
3659
+ if (listEl) {
3660
+ const tilesWrap = ensureTilesWrap(listEl);
3661
+ const addTile = tilesWrap.querySelector(".fb-multi-add-tile-js") ?? tilesWrap.querySelector(".fb-tile-add");
3662
+ if (addTile) addTile.style.display = "none";
3663
+ }
3471
3664
  await Promise.all(
3472
3665
  accepted.map(async (file) => {
3473
3666
  const placeholder = createUploadingTile(file.name, state);
3474
3667
  if (listEl) {
3475
3668
  const tilesWrap = ensureTilesWrap(listEl);
3476
- const addTile = tilesWrap.querySelector(".fb-tile-add");
3477
- if (addTile) {
3478
- tilesWrap.insertBefore(placeholder, addTile);
3479
- } else {
3480
- tilesWrap.appendChild(placeholder);
3481
- }
3669
+ tilesWrap.appendChild(placeholder);
3482
3670
  }
3483
3671
  try {
3484
3672
  const rid = await uploadSingleFile(file, state);
@@ -3520,7 +3708,7 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3520
3708
  function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3521
3709
  filesPicker.onchange = async () => {
3522
3710
  if (!filesPicker.files) return;
3523
- const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3711
+ const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
3524
3712
  const { accepted, errorMessage } = filterAndSlice(
3525
3713
  Array.from(filesPicker.files),
3526
3714
  resourceIds.length,
@@ -3685,6 +3873,10 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3685
3873
  hiddenInput.name = pathKey;
3686
3874
  fileWrapper.appendChild(hiddenInput);
3687
3875
  }
3876
+ const previousRid = hiddenInput.value || null;
3877
+ if (previousRid && previousRid !== first.resourceId) {
3878
+ releaseLocalFileUrl(state.resourceIndex.get(previousRid)?.file);
3879
+ }
3688
3880
  hiddenInput.value = first.resourceId;
3689
3881
  await renderCallback(first.resourceId);
3690
3882
  if (!state.config.readonly) {
@@ -3693,7 +3885,9 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3693
3885
  }
3694
3886
 
3695
3887
  // src/components/file/render-edit.ts
3696
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3888
+ var ICON_CLOUD = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 18a4 4 0 000-8 6 6 0 00-11.5 2A4 4 0 006 20h11M12 12v7M12 12l-3 3M12 12l3 3"/></svg>`;
3889
+ var ICON_LIBRARY2 = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 5l3.5 1-3 14L13 19"/></svg>`;
3890
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps, extras) {
3697
3891
  seedInferredResource(initial, state.resourceIndex);
3698
3892
  const meta = state.resourceIndex.get(initial);
3699
3893
  const isVideo = meta?.type?.startsWith("video/");
@@ -3704,7 +3898,7 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3704
3898
  deps
3705
3899
  }).catch(console.error);
3706
3900
  } else {
3707
- renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
3901
+ renderSingleFileFilled(fileContainer, initial, state, deps, extras);
3708
3902
  }
3709
3903
  const hiddenInput = document.createElement("input");
3710
3904
  hiddenInput.type = "hidden";
@@ -3712,161 +3906,423 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3712
3906
  hiddenInput.value = initial;
3713
3907
  fileWrapper.appendChild(hiddenInput);
3714
3908
  }
3715
- var UPLOAD_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" style="flex-shrink:0;color:var(--fb-file-upload-text-color,#9ca3af);">
3716
- <path d="M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/>
3717
- </svg>`;
3718
- function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
3719
- const dropzone = document.createElement("div");
3720
- dropzone.className = "fb-file-dropzone";
3721
- dropzone.innerHTML = `
3722
- ${UPLOAD_SVG}
3723
- <div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
3724
- ${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
3725
- `;
3726
- dropzone.onclick = openPicker;
3727
- return dropzone;
3909
+ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false, constraintHint = "") {
3910
+ ensureFileStyles();
3911
+ const outer = document.createElement("div");
3912
+ outer.className = `fb-wide-tile${hasLibrary ? " fb-file-card-row" : ""}${isDragOver ? " fb-drag-over" : ""}`;
3913
+ const uploadBtn = document.createElement("button");
3914
+ uploadBtn.type = "button";
3915
+ uploadBtn.className = "fb-wide-tile-upload fb-file-dropzone";
3916
+ const cloudIcon = document.createElement("span");
3917
+ cloudIcon.style.cssText = "width:36px;height:36px;display:block;flex-shrink:0;";
3918
+ cloudIcon.innerHTML = ICON_CLOUD;
3919
+ uploadBtn.appendChild(cloudIcon);
3920
+ const primaryText = document.createElement("div");
3921
+ primaryText.className = "fb-wide-tile-label";
3922
+ primaryText.style.cssText = "font-size:14px;font-weight:600;";
3923
+ primaryText.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragText", state);
3924
+ uploadBtn.appendChild(primaryText);
3925
+ if (constraintHint) {
3926
+ const hintEl = document.createElement("div");
3927
+ hintEl.style.cssText = "font-size:11px;opacity:0.65;margin-top:2px;";
3928
+ hintEl.textContent = constraintHint;
3929
+ uploadBtn.appendChild(hintEl);
3930
+ }
3931
+ uploadBtn.onclick = (e) => {
3932
+ e.stopPropagation();
3933
+ onUploadClick();
3934
+ };
3935
+ outer.appendChild(uploadBtn);
3936
+ if (hasLibrary && onLibraryClick) {
3937
+ const divider = document.createElement("div");
3938
+ divider.className = "fb-wide-tile-divider";
3939
+ outer.appendChild(divider);
3940
+ const libBtn = document.createElement("button");
3941
+ libBtn.type = "button";
3942
+ libBtn.className = "fb-wide-tile-library fb-file-library-card";
3943
+ const libIcon = document.createElement("span");
3944
+ libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
3945
+ libIcon.innerHTML = ICON_LIBRARY2;
3946
+ libBtn.appendChild(libIcon);
3947
+ const libLabel = document.createElement("div");
3948
+ libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
3949
+ libLabel.textContent = t("fromLibrary", state);
3950
+ libBtn.appendChild(libLabel);
3951
+ const libHint = document.createElement("div");
3952
+ libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
3953
+ libHint.textContent = t("libraryHint", state);
3954
+ libBtn.appendChild(libHint);
3955
+ libBtn.onclick = (e) => {
3956
+ e.stopPropagation();
3957
+ onLibraryClick();
3958
+ };
3959
+ outer.appendChild(libBtn);
3960
+ }
3961
+ attachDragOverFeedback(outer, {
3962
+ onEnter: () => {
3963
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
3964
+ if (primaryText2) primaryText2.textContent = t("dropToUpload", state);
3965
+ },
3966
+ onLeave: () => {
3967
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
3968
+ if (primaryText2) primaryText2.textContent = t("clickDragText", state);
3969
+ },
3970
+ activeClass: "fb-drag-over"
3971
+ });
3972
+ return outer;
3728
3973
  }
3729
- function buildLibraryButton(variant, state, onClick) {
3730
- const btn = document.createElement("button");
3731
- btn.type = "button";
3732
- btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
3733
- if (variant === "card") {
3734
- btn.innerHTML = `
3735
- <span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
3736
- <span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
3737
- <span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
3738
- `;
3974
+ function attachDragOverFeedback(el, hooks) {
3975
+ let depth = 0;
3976
+ el.addEventListener("dragover", (e) => {
3977
+ e.preventDefault();
3978
+ });
3979
+ el.addEventListener("dragenter", (e) => {
3980
+ e.preventDefault();
3981
+ depth++;
3982
+ if (depth === 1) {
3983
+ el.classList.add(hooks.activeClass);
3984
+ hooks.onEnter();
3985
+ }
3986
+ });
3987
+ el.addEventListener("dragleave", (e) => {
3988
+ e.preventDefault();
3989
+ depth = Math.max(0, depth - 1);
3990
+ if (depth === 0) {
3991
+ el.classList.remove(hooks.activeClass);
3992
+ hooks.onLeave();
3993
+ }
3994
+ });
3995
+ el.addEventListener("drop", () => {
3996
+ depth = 0;
3997
+ el.classList.remove(hooks.activeClass);
3998
+ hooks.onLeave();
3999
+ });
4000
+ }
4001
+ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras) {
4002
+ const meta = state.resourceIndex.get(resourceId);
4003
+ const isVideo = meta?.type?.startsWith("video/");
4004
+ if (isVideo) {
4005
+ renderFilePreview(fileContainer, resourceId, state, {
4006
+ fileName: meta?.name ?? "",
4007
+ isReadonly: false,
4008
+ deps
4009
+ }).catch(console.error);
4010
+ return;
4011
+ }
4012
+ ensureFileStyles();
4013
+ const outer = document.createElement("div");
4014
+ outer.className = "fb-multi-outer fb-multi-has-files";
4015
+ const grid = document.createElement("div");
4016
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4017
+ outer.appendChild(grid);
4018
+ const tile = buildPreviewTile(
4019
+ resourceId,
4020
+ state,
4021
+ Boolean(deps.onRemove),
4022
+ deps.onRemove ? () => deps.onRemove?.() : null,
4023
+ extras
4024
+ );
4025
+ grid.appendChild(tile);
4026
+ fileContainer.className = "file-preview-container";
4027
+ fileContainer.removeAttribute("style");
4028
+ while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4029
+ fileContainer.appendChild(outer);
4030
+ }
4031
+ function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
4032
+ const tile = document.createElement("div");
4033
+ tile.className = `fb-multi-add-tile fb-multi-add-tile-js${isDragOver ? " fb-drag-over-tile" : ""}`;
4034
+ const uploadBtn = document.createElement("button");
4035
+ uploadBtn.type = "button";
4036
+ uploadBtn.className = "fb-multi-add-upload fb-tile-add fb-file-dropzone";
4037
+ const cloudIcon = document.createElement("span");
4038
+ cloudIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4039
+ cloudIcon.innerHTML = ICON_CLOUD;
4040
+ uploadBtn.appendChild(cloudIcon);
4041
+ const uploadLabel = document.createElement("span");
4042
+ uploadLabel.className = "fb-multi-add-label";
4043
+ uploadLabel.style.cssText = "font-size:11px;font-weight:600;";
4044
+ uploadLabel.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragTextMultiple", state);
4045
+ uploadBtn.appendChild(uploadLabel);
4046
+ uploadBtn.onclick = (e) => {
4047
+ e.stopPropagation();
4048
+ onUploadClick();
4049
+ };
4050
+ tile.appendChild(uploadBtn);
4051
+ if (hasLibrary && onLibraryClick) {
4052
+ const divider = document.createElement("div");
4053
+ divider.className = "fb-multi-add-divider";
4054
+ tile.appendChild(divider);
4055
+ const libBtn = document.createElement("button");
4056
+ libBtn.type = "button";
4057
+ libBtn.className = "fb-multi-add-library fb-tile-add-library fb-file-library-card";
4058
+ libBtn.setAttribute("aria-label", t("fromLibrary", state));
4059
+ const libIcon = document.createElement("span");
4060
+ libIcon.style.cssText = "width:14px;height:14px;display:block;flex-shrink:0;";
4061
+ libIcon.innerHTML = ICON_LIBRARY2;
4062
+ libBtn.appendChild(libIcon);
4063
+ libBtn.appendChild(document.createTextNode(t("fromLibrary", state)));
4064
+ libBtn.onclick = (e) => {
4065
+ e.stopPropagation();
4066
+ onLibraryClick();
4067
+ };
4068
+ tile.appendChild(libBtn);
4069
+ }
4070
+ return tile;
4071
+ }
4072
+ function buildPreviewTile(rid, state, canRemove, onRemove, extras) {
4073
+ ensureFileStyles();
4074
+ const meta = state.resourceIndex.get(rid);
4075
+ const tile = document.createElement("div");
4076
+ tile.className = "fb-preview-tile fb-checker fb-tile-resource resource-pill";
4077
+ tile.dataset.resourceId = rid;
4078
+ const actionsEl = createTileActions({
4079
+ canRemove: canRemove && onRemove !== null,
4080
+ removeHandler: onRemove,
4081
+ state,
4082
+ resourceId: rid,
4083
+ fileName: meta?.name ?? "",
4084
+ meta,
4085
+ replaceHandler: extras?.replaceHandler ?? null,
4086
+ libraryHandler: extras?.libraryHandler ?? null
4087
+ });
4088
+ fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
4089
+ console.error("Failed to render tile:", err);
4090
+ });
4091
+ return tile;
4092
+ }
4093
+ function buildPlaceholderTile(isDragOver = false) {
4094
+ const div = document.createElement("div");
4095
+ div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
4096
+ return div;
4097
+ }
4098
+ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearAll) {
4099
+ const line = document.createElement("div");
4100
+ line.className = "fb-meta-line";
4101
+ const metaText = document.createElement("div");
4102
+ metaText.className = "fb-meta-text";
4103
+ if (element.maxSize && element.maxSize !== Infinity) {
4104
+ const sizeSpan = document.createElement("span");
4105
+ sizeSpan.textContent = t("hintMaxSize", state, { size: element.maxSize });
4106
+ metaText.appendChild(sizeSpan);
4107
+ metaText.appendChild(buildMetaDot());
4108
+ }
4109
+ const exts = getAllowedExtensions(element.accept);
4110
+ if (exts.length > 0) {
4111
+ const fmtSpan = document.createElement("span");
4112
+ fmtSpan.className = "fb-meta-mono";
4113
+ fmtSpan.textContent = exts.map((e) => e.toUpperCase()).join(", ");
4114
+ metaText.appendChild(fmtSpan);
4115
+ metaText.appendChild(buildMetaDot());
4116
+ }
4117
+ const countSpan = document.createElement("span");
4118
+ if (maxCount < Infinity) {
4119
+ countSpan.textContent = t("fileCountWithMax", state, {
4120
+ count: ridCount,
4121
+ max: maxCount
4122
+ });
3739
4123
  } else {
3740
- btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
3741
- btn.title = t("fromLibrary", state);
3742
- btn.setAttribute("aria-label", t("fromLibrary", state));
4124
+ const countKey = ridCount === 1 ? "fileCountSingle" : "fileCountPlural";
4125
+ countSpan.textContent = t(countKey, state, { count: ridCount });
4126
+ }
4127
+ metaText.appendChild(countSpan);
4128
+ line.appendChild(metaText);
4129
+ if (canClearAll && ridCount > 1) {
4130
+ const clearBtn = document.createElement("button");
4131
+ clearBtn.type = "button";
4132
+ clearBtn.className = "fb-clear-all-btn";
4133
+ clearBtn.textContent = t("clearAll", state);
4134
+ clearBtn.onclick = (e) => {
4135
+ e.stopPropagation();
4136
+ if (window.confirm(t("clearAll", state) + "?")) {
4137
+ onClearAll();
4138
+ }
4139
+ };
4140
+ line.appendChild(clearBtn);
3743
4141
  }
3744
- btn.addEventListener("click", onClick);
3745
- return btn;
4142
+ return line;
4143
+ }
4144
+ function buildMetaDot() {
4145
+ const dot = document.createElement("span");
4146
+ dot.className = "fb-meta-dot";
4147
+ return dot;
3746
4148
  }
4149
+ var gridResizeObservers = /* @__PURE__ */ new WeakMap();
3747
4150
  function renderResourcePills(opts) {
3748
4151
  const {
3749
4152
  container,
3750
4153
  rids,
3751
4154
  state,
3752
4155
  onRemove,
3753
- hint,
3754
- countInfo,
3755
4156
  maxCount,
3756
4157
  isReadonly = false,
3757
- onLibraryPick
4158
+ onLibraryPick,
4159
+ element,
4160
+ onClearAll,
4161
+ openPicker: openPickerProp
3758
4162
  } = opts;
3759
4163
  ensureFileStyles();
3760
4164
  const wrapper = container.closest("[data-files-wrapper]");
3761
4165
  if (wrapper) {
3762
4166
  wrapper.dataset.resourceIds = JSON.stringify(rids ?? []);
3763
4167
  }
4168
+ const previousObserver = gridResizeObservers.get(container);
4169
+ if (previousObserver) {
4170
+ previousObserver.disconnect();
4171
+ gridResizeObservers.delete(container);
4172
+ }
3764
4173
  while (container.firstChild) container.removeChild(container.firstChild);
3765
4174
  const ridList = rids ?? [];
3766
- const atMax = maxCount !== void 0 && ridList.length >= maxCount;
4175
+ const effectiveMax = maxCount ?? Infinity;
4176
+ const atMax = effectiveMax !== Infinity && ridList.length >= effectiveMax;
3767
4177
  const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
3768
- const buildSubHint = () => {
3769
- const parts = [];
3770
- if (hint) parts.push(hint);
3771
- if (countInfo) parts.push(countInfo);
3772
- return parts.join(" \u2022 ");
3773
- };
3774
- const openPicker = () => {
3775
- const picker = findFilePicker(container);
3776
- if (picker) picker.click();
3777
- };
3778
- if (ridList.length === 0) {
3779
- if (isReadonly) {
4178
+ const openPicker = openPickerProp ?? (() => {
4179
+ const pickerEl = container.closest("[data-files-wrapper]")?.querySelector('input[type="file"]');
4180
+ if (pickerEl) pickerEl.click();
4181
+ });
4182
+ if (isReadonly) {
4183
+ if (ridList.length === 0) {
3780
4184
  const emptyEl = document.createElement("div");
3781
4185
  emptyEl.className = "fb-tile-empty-text";
3782
4186
  emptyEl.textContent = t("noFilesSelected", state);
3783
4187
  container.appendChild(emptyEl);
3784
- } else if (hasLibrary) {
3785
- const row = document.createElement("div");
3786
- row.className = "fb-file-card-row";
3787
- const dropzone = buildEmptyDropzone(
3788
- state,
3789
- t("clickDragTextMultiple", state),
3790
- buildSubHint(),
3791
- openPicker
3792
- );
3793
- const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
3794
- row.appendChild(dropzone);
3795
- row.appendChild(libraryBtn);
3796
- container.appendChild(row);
3797
4188
  } else {
3798
- const dropzone = buildEmptyDropzone(
3799
- state,
3800
- t("clickDragTextMultiple", state),
3801
- buildSubHint(),
3802
- openPicker
3803
- );
3804
- container.appendChild(dropzone);
4189
+ const grid2 = document.createElement("div");
4190
+ grid2.className = "fb-multi-readonly-grid";
4191
+ container.appendChild(grid2);
4192
+ for (const rid of ridList) {
4193
+ const meta = state.resourceIndex.get(rid);
4194
+ const tile = document.createElement("div");
4195
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4196
+ tile.dataset.resourceId = rid;
4197
+ const actionsEl = createTileActions({
4198
+ canRemove: false,
4199
+ removeHandler: null,
4200
+ state,
4201
+ resourceId: rid,
4202
+ fileName: meta?.name ?? "",
4203
+ meta
4204
+ });
4205
+ fillTileContent(tile, rid, meta, state, actionsEl).catch(console.error);
4206
+ tile.onclick = async () => {
4207
+ let url = null;
4208
+ if (state.config.getDownloadUrl) {
4209
+ url = state.config.getDownloadUrl(rid);
4210
+ } else if (state.config.getThumbnail) {
4211
+ url = await state.config.getThumbnail(rid);
4212
+ } else if (meta?.file instanceof File) {
4213
+ url = URL.createObjectURL(meta.file);
4214
+ }
4215
+ if (url) {
4216
+ window.open(url, "_blank");
4217
+ } else if (state.config.downloadFile) {
4218
+ state.config.downloadFile(rid, meta?.name ?? "");
4219
+ }
4220
+ };
4221
+ grid2.appendChild(tile);
4222
+ }
3805
4223
  }
3806
4224
  return;
3807
4225
  }
3808
- const tilesWrap = document.createElement("div");
3809
- tilesWrap.className = "fb-tiles-wrap";
3810
- tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
3811
- for (const rid of ridList) {
3812
- const meta = state.resourceIndex.get(rid);
3813
- const tile = createFileTile();
3814
- tile.classList.add("fb-tile-resource", "resource-pill");
3815
- tile.dataset.resourceId = rid;
3816
- const actionsEl = createTileActions({
3817
- canRemove: !isReadonly && onRemove !== null,
3818
- removeHandler: onRemove ? () => onRemove(rid) : null,
4226
+ const outerDiv = document.createElement("div");
4227
+ outerDiv.className = `fb-multi-outer${ridList.length > 0 ? " fb-multi-has-files" : ""}`;
4228
+ const grid = document.createElement("div");
4229
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4230
+ outerDiv.appendChild(grid);
4231
+ container.appendChild(outerDiv);
4232
+ for (let i = 0; i < ridList.length; i++) {
4233
+ const rid = ridList[i];
4234
+ const tile = buildPreviewTile(
4235
+ rid,
3819
4236
  state,
3820
- resourceId: rid,
3821
- fileName: meta?.name ?? ""
3822
- });
3823
- fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
3824
- console.error("Failed to render tile:", err);
3825
- });
3826
- tilesWrap.appendChild(tile);
3827
- }
3828
- if (!isReadonly && !atMax) {
3829
- const addTile = document.createElement("div");
3830
- addTile.className = "fb-tile fb-tile-add";
3831
- addTile.innerHTML = "+";
3832
- addTile.onclick = openPicker;
3833
- tilesWrap.appendChild(addTile);
3834
- if (hasLibrary) {
3835
- const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
3836
- tilesWrap.appendChild(libraryTile);
3837
- }
3838
- } else if (!isReadonly && atMax) {
3839
- const chip = document.createElement("div");
3840
- chip.className = "fb-tile-counter";
3841
- chip.textContent = t("filesCounter", state, {
3842
- count: ridList.length,
3843
- max: maxCount
3844
- });
3845
- tilesWrap.appendChild(chip);
4237
+ onRemove !== null,
4238
+ onRemove ? () => onRemove(rid) : null
4239
+ );
4240
+ grid.appendChild(tile);
3846
4241
  }
3847
- container.appendChild(tilesWrap);
3848
- const subHint = buildSubHint();
3849
- if (subHint) {
3850
- const hintEl = document.createElement("div");
3851
- hintEl.className = "fb-tile-hint";
3852
- hintEl.textContent = subHint;
3853
- container.appendChild(hintEl);
4242
+ if (!atMax) {
4243
+ const addTile = buildMultiAddTile(
4244
+ state,
4245
+ hasLibrary,
4246
+ openPicker,
4247
+ onLibraryPick ?? null
4248
+ );
4249
+ grid.appendChild(addTile);
4250
+ }
4251
+ const occupied = ridList.length + (atMax ? 0 : 1);
4252
+ const adjustPlaceholders = () => {
4253
+ const tpl = getComputedStyle(grid).gridTemplateColumns;
4254
+ const cols = tpl ? tpl.split(" ").filter(Boolean).length : 0;
4255
+ if (!cols) return;
4256
+ const remainder = occupied % cols;
4257
+ const rowFill = remainder === 0 ? 0 : cols - remainder;
4258
+ const capacityRemaining = effectiveMax === Infinity ? rowFill : Math.max(0, effectiveMax - occupied);
4259
+ const needed = Math.min(rowFill, capacityRemaining);
4260
+ const existing = grid.querySelectorAll(".fb-multi-placeholder");
4261
+ if (existing.length > needed) {
4262
+ for (let i = existing.length - 1; i >= needed; i--) existing[i].remove();
4263
+ } else if (existing.length < needed) {
4264
+ for (let i = existing.length; i < needed; i++) {
4265
+ grid.appendChild(buildPlaceholderTile());
4266
+ }
4267
+ }
4268
+ };
4269
+ if (effectiveMax === Infinity || effectiveMax > occupied) {
4270
+ grid.appendChild(buildPlaceholderTile());
4271
+ }
4272
+ requestAnimationFrame(adjustPlaceholders);
4273
+ if (typeof ResizeObserver !== "undefined") {
4274
+ const ro = new ResizeObserver(() => adjustPlaceholders());
4275
+ ro.observe(grid);
4276
+ gridResizeObservers.set(container, ro);
4277
+ }
4278
+ attachDragOverFeedback(outerDiv, {
4279
+ activeClass: "fb-drag-over",
4280
+ onEnter: () => {
4281
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4282
+ p.classList.add("fb-drag-over");
4283
+ });
4284
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4285
+ if (addTile) {
4286
+ addTile.classList.add("fb-drag-over-tile");
4287
+ const label = addTile.querySelector(".fb-multi-add-label");
4288
+ if (label) label.textContent = t("dropToUpload", state);
4289
+ }
4290
+ },
4291
+ onLeave: () => {
4292
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4293
+ p.classList.remove("fb-drag-over");
4294
+ });
4295
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4296
+ if (addTile) {
4297
+ addTile.classList.remove("fb-drag-over-tile");
4298
+ const label = addTile.querySelector(".fb-multi-add-label");
4299
+ if (label) label.textContent = t("clickDragTextMultiple", state);
4300
+ }
4301
+ }
4302
+ });
4303
+ if (element) {
4304
+ const metaLine = buildMetaLine(
4305
+ state,
4306
+ element,
4307
+ ridList.length,
4308
+ effectiveMax,
4309
+ Boolean(onClearAll),
4310
+ onClearAll ?? (() => {
4311
+ })
4312
+ );
4313
+ container.appendChild(metaLine);
3854
4314
  }
3855
4315
  }
3856
4316
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3857
4317
  const state = ctx.state;
3858
4318
  const fileWrapper = document.createElement("div");
3859
4319
  fileWrapper.className = "space-y-2";
4320
+ fileWrapper.dataset.filesWrapper = pathKey;
3860
4321
  const picker = document.createElement("input");
3861
4322
  picker.type = "file";
3862
4323
  picker.name = pathKey;
3863
4324
  picker.style.display = "none";
3864
- if (element.accept) {
3865
- picker.accept = typeof element.accept === "string" ? element.accept : [
3866
- ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
3867
- ...element.accept.mime ?? []
3868
- ].join(",") || "";
3869
- }
4325
+ picker.accept = buildAcceptAttribute(element.accept);
3870
4326
  const fileContainer = document.createElement("div");
3871
4327
  fileContainer.className = "file-preview-container";
3872
4328
  const initial = ctx.prefill[element.key];
@@ -3895,14 +4351,6 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3895
4351
  setupDrop(container) {
3896
4352
  setupDragAndDrop(container, handlers.dragHandler);
3897
4353
  },
3898
- restoreDropzone() {
3899
- const hint = makeFieldHint(element, state);
3900
- fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3901
- fileContainer.style.height = "128px";
3902
- setEmptyFileContainer(fileContainer, state, hint);
3903
- fileContainer.onclick = handlers.fileUploadHandler;
3904
- setupDragAndDrop(fileContainer, handlers.dragHandler);
3905
- },
3906
4354
  onRemove() {
3907
4355
  const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
3908
4356
  const currentRid = hiddenInput?.value;
@@ -3913,34 +4361,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3913
4361
  renderEmptySingleState();
3914
4362
  }
3915
4363
  };
3916
- const buildDeps = () => ({
3917
- picker,
3918
- fileUploadHandler: handlers.fileUploadHandler,
3919
- dragHandler: handlers.dragHandler,
3920
- setupDrop: handlers.setupDrop,
3921
- onRemove: handlers.onRemove
3922
- });
3923
- const renderEmptySingleState = () => {
3924
- if (state.config.pickExistingFiles && !element.disableLibrary) {
3925
- fileContainer.className = "file-preview-container";
3926
- fileContainer.removeAttribute("style");
3927
- fileContainer.onclick = null;
3928
- while (fileContainer.firstChild) {
3929
- fileContainer.removeChild(fileContainer.firstChild);
3930
- }
3931
- const row = document.createElement("div");
3932
- row.className = "fb-file-card-row";
3933
- row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
3934
- const hint = makeFieldHint(element, state);
3935
- const uploadCard = buildEmptyDropzone(
3936
- state,
3937
- t("clickDragText", state),
3938
- hint,
3939
- handlers.fileUploadHandler
3940
- );
3941
- uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
3942
- setupDragAndDrop(uploadCard, handlers.dragHandler);
3943
- const libraryBtn = buildLibraryButton("card", state, () => {
4364
+ const buildSingleExtras = () => {
4365
+ const hasLibrary = Boolean(state.config.pickExistingFiles && !element.disableLibrary);
4366
+ return {
4367
+ replaceHandler: state.config.uploadFile ? () => picker.click() : null,
4368
+ libraryHandler: hasLibrary ? () => {
3944
4369
  handleLibraryPickSingle(
3945
4370
  state,
3946
4371
  element,
@@ -3949,20 +4374,41 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3949
4374
  pathKey,
3950
4375
  pathKey,
3951
4376
  async (rid) => {
3952
- await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
4377
+ renderSingleFileFilled(fileContainer, rid, state, buildDeps(), buildSingleExtras());
3953
4378
  },
3954
4379
  ctx.instance
3955
4380
  ).catch((err) => {
3956
4381
  console.error("Library pick failed:", err);
3957
4382
  });
3958
- });
3959
- libraryBtn.style.cssText = "flex:1;min-width:0;";
3960
- row.appendChild(uploadCard);
3961
- row.appendChild(libraryBtn);
3962
- fileContainer.appendChild(row);
3963
- } else {
3964
- handlers.restoreDropzone();
4383
+ } : null
4384
+ };
4385
+ };
4386
+ const buildDeps = () => ({
4387
+ picker,
4388
+ fileUploadHandler: handlers.fileUploadHandler,
4389
+ dragHandler: handlers.dragHandler,
4390
+ setupDrop: handlers.setupDrop,
4391
+ onRemove: handlers.onRemove,
4392
+ onAfterUpload: (container, rid) => {
4393
+ renderSingleFileFilled(container, rid, state, buildDeps(), buildSingleExtras());
3965
4394
  }
4395
+ });
4396
+ const renderEmptySingleState = () => {
4397
+ ensureFileStyles();
4398
+ fileContainer.className = "file-preview-container";
4399
+ fileContainer.removeAttribute("style");
4400
+ while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4401
+ const onLibraryClick = buildSingleExtras().libraryHandler;
4402
+ const wideTile = buildWideTile(
4403
+ state,
4404
+ onLibraryClick !== null,
4405
+ handlers.fileUploadHandler,
4406
+ onLibraryClick,
4407
+ false,
4408
+ makeFieldHint(element, state)
4409
+ );
4410
+ fileContainer.appendChild(wideTile);
4411
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
3966
4412
  };
3967
4413
  if (initial) {
3968
4414
  handleInitialFileData(
@@ -3971,11 +4417,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3971
4417
  pathKey,
3972
4418
  fileWrapper,
3973
4419
  state,
3974
- buildDeps()
4420
+ buildDeps(),
4421
+ buildSingleExtras()
3975
4422
  );
3976
4423
  const prefillMeta = state.resourceIndex.get(initial);
3977
4424
  if (prefillMeta?.type?.startsWith("video/")) {
3978
- fileContainer.onclick = handlers.fileUploadHandler;
3979
4425
  setupDragAndDrop(fileContainer, handlers.dragHandler);
3980
4426
  }
3981
4427
  } else {
@@ -3983,113 +4429,23 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3983
4429
  }
3984
4430
  picker.onchange = () => {
3985
4431
  if (picker.files && picker.files.length > 0) {
3986
- handleFileSelect({
3987
- file: picker.files[0],
3988
- container: fileContainer,
3989
- fieldName: pathKey,
3990
- state,
3991
- deps: buildDeps(),
3992
- instance: ctx.instance,
3993
- allowedExtensions: allowedExts,
3994
- allowedMimes,
3995
- maxSizeMB
3996
- });
4432
+ handlers.dragHandler(picker.files);
3997
4433
  }
3998
4434
  };
3999
4435
  fileWrapper.appendChild(fileContainer);
4000
4436
  fileWrapper.appendChild(picker);
4001
4437
  wrapper.appendChild(fileWrapper);
4002
4438
  }
4003
- function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4004
- const state = ctx.state;
4005
- const filesWrapper = document.createElement("div");
4006
- filesWrapper.className = "space-y-2";
4007
- filesWrapper.dataset.filesWrapper = pathKey;
4008
- const filesPicker = document.createElement("input");
4009
- filesPicker.type = "file";
4010
- filesPicker.name = pathKey;
4011
- filesPicker.multiple = true;
4012
- filesPicker.style.display = "none";
4013
- if (element.accept) {
4014
- filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4015
- ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
4016
- ...element.accept.mime ?? []
4017
- ].join(",") || "";
4018
- }
4019
- const filesContainer = document.createElement("div");
4020
- filesContainer.className = "files-list-wrapper";
4021
- filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
4022
- const list = document.createElement("div");
4023
- list.className = "files-list";
4024
- const initialFiles = ctx.prefill[element.key] || [];
4025
- addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4026
- filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4027
- const filesFieldHint = makeFieldHint(element, state);
4028
- const filesConstraints = {
4029
- maxCount: Infinity,
4030
- allowedExtensions: getAllowedExtensions(element.accept),
4031
- allowedMimes: getAllowedMimes(element.accept),
4032
- maxSize: element.maxSize ?? Infinity
4033
- };
4034
- filesContainer.appendChild(list);
4035
- filesWrapper.appendChild(filesPicker);
4036
- filesWrapper.appendChild(filesContainer);
4037
- wrapper.appendChild(filesWrapper);
4038
- const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4039
- handleLibraryPickMulti(
4040
- state,
4041
- element,
4042
- filesWrapper,
4043
- pathKey,
4044
- initialFiles,
4045
- Infinity,
4046
- updateFilesList,
4047
- ctx.instance
4048
- ).catch((err) => {
4049
- console.error("Library pick failed:", err);
4050
- });
4051
- } : null;
4052
- function updateFilesList() {
4053
- const currentlyReadonly = isElementReadonly(element, state);
4054
- renderResourcePills({
4055
- container: list,
4056
- rids: initialFiles,
4057
- state,
4058
- onRemove: currentlyReadonly ? null : (ridToRemove) => {
4059
- releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
4060
- const index = initialFiles.indexOf(ridToRemove);
4061
- if (index > -1) initialFiles.splice(index, 1);
4062
- updateFilesList();
4063
- },
4064
- hint: filesFieldHint,
4065
- isReadonly: currentlyReadonly,
4066
- onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
4067
- });
4068
- }
4069
- updateFilesList();
4070
- setupFilesDropHandler(
4071
- filesContainer,
4072
- initialFiles,
4073
- state,
4074
- updateFilesList,
4075
- filesConstraints,
4076
- pathKey,
4077
- ctx.instance
4078
- );
4079
- setupFilesPickerHandler(
4080
- filesPicker,
4081
- initialFiles,
4082
- state,
4083
- updateFilesList,
4084
- filesConstraints,
4085
- pathKey,
4086
- ctx.instance
4087
- );
4439
+ function buildAcceptAttribute(accept) {
4440
+ if (!accept) return "";
4441
+ if (typeof accept === "string") return accept;
4442
+ return [
4443
+ ...accept.extensions?.map((ext) => `.${ext}`) ?? [],
4444
+ ...accept.mime ?? []
4445
+ ].join(",");
4088
4446
  }
4089
- function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4447
+ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
4090
4448
  const state = ctx.state;
4091
- const minFiles = element.minCount ?? 0;
4092
- const maxFiles = element.maxCount ?? Infinity;
4093
4449
  const filesWrapper = document.createElement("div");
4094
4450
  filesWrapper.className = "space-y-2";
4095
4451
  filesWrapper.dataset.filesWrapper = pathKey;
@@ -4098,15 +4454,9 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4098
4454
  filesPicker.name = pathKey;
4099
4455
  filesPicker.multiple = true;
4100
4456
  filesPicker.style.display = "none";
4101
- if (element.accept) {
4102
- filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4103
- ...element.accept.extensions?.map((ext) => `.${ext}`) ?? [],
4104
- ...element.accept.mime ?? []
4105
- ].join(",") || "";
4106
- }
4457
+ filesPicker.accept = buildAcceptAttribute(element.accept);
4107
4458
  const filesContainer = document.createElement("div");
4108
4459
  filesContainer.className = "files-list-wrapper";
4109
- filesContainer.style.cssText = "border:2px dashed var(--fb-file-upload-border-color,#d1d5db);border-radius:var(--fb-border-radius,0.5rem);padding:8px;transition:border-color var(--fb-transition-duration,200ms),background var(--fb-transition-duration,200ms);";
4110
4460
  const list = document.createElement("div");
4111
4461
  list.className = "files-list";
4112
4462
  filesWrapper.appendChild(filesPicker);
@@ -4115,19 +4465,18 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4115
4465
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
4116
4466
  addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4117
4467
  filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4118
- const multipleFilesHint = makeFieldHint(element, state);
4119
- const multipleConstraints = {
4468
+ const constraints = {
4120
4469
  maxCount: maxFiles,
4121
4470
  allowedExtensions: getAllowedExtensions(element.accept),
4122
4471
  allowedMimes: getAllowedMimes(element.accept),
4123
- maxSize: element.maxSize ?? Infinity
4472
+ // Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
4473
+ // backward compatibility (matches addFileSizeHint in validation.ts).
4474
+ maxSize: element.maxSize ?? element.maxSizeMB ?? Infinity
4124
4475
  };
4125
- const buildCountInfo = () => {
4126
- const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
4127
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
4128
- return countText + minMaxText;
4476
+ const openPicker = () => {
4477
+ filesPicker.click();
4129
4478
  };
4130
- const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4479
+ const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4131
4480
  handleLibraryPickMulti(
4132
4481
  state,
4133
4482
  element,
@@ -4141,30 +4490,35 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4141
4490
  console.error("Library pick failed:", err);
4142
4491
  });
4143
4492
  } : null;
4144
- const updateFilesDisplay = () => {
4493
+ function updateFilesDisplay() {
4145
4494
  const currentlyReadonly = isElementReadonly(element, state);
4146
4495
  renderResourcePills({
4147
4496
  container: list,
4148
4497
  rids: initialFiles,
4149
4498
  state,
4150
- onRemove: currentlyReadonly ? null : (index) => {
4151
- releaseLocalFileUrl(state.resourceIndex.get(index)?.file);
4152
- initialFiles.splice(initialFiles.indexOf(index), 1);
4499
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
4500
+ releaseLocalFileUrl(state.resourceIndex.get(ridToRemove)?.file);
4501
+ const index = initialFiles.indexOf(ridToRemove);
4502
+ if (index > -1) initialFiles.splice(index, 1);
4153
4503
  updateFilesDisplay();
4154
4504
  },
4155
- hint: multipleFilesHint,
4156
- countInfo: buildCountInfo(),
4157
4505
  maxCount: maxFiles < Infinity ? maxFiles : void 0,
4158
4506
  isReadonly: currentlyReadonly,
4159
- onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4507
+ onLibraryPick: currentlyReadonly ? null : onLibraryPick,
4508
+ element,
4509
+ onClearAll: currentlyReadonly ? void 0 : () => {
4510
+ initialFiles.splice(0);
4511
+ updateFilesDisplay();
4512
+ },
4513
+ openPicker
4160
4514
  });
4161
- };
4515
+ }
4162
4516
  setupFilesDropHandler(
4163
4517
  filesContainer,
4164
4518
  initialFiles,
4165
4519
  state,
4166
4520
  updateFilesDisplay,
4167
- multipleConstraints,
4521
+ constraints,
4168
4522
  pathKey,
4169
4523
  ctx.instance
4170
4524
  );
@@ -4173,13 +4527,19 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4173
4527
  initialFiles,
4174
4528
  state,
4175
4529
  updateFilesDisplay,
4176
- multipleConstraints,
4530
+ constraints,
4177
4531
  pathKey,
4178
4532
  ctx.instance
4179
4533
  );
4180
4534
  updateFilesDisplay();
4181
4535
  wrapper.appendChild(filesWrapper);
4182
4536
  }
4537
+ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4538
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
4539
+ }
4540
+ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4541
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, element.maxCount ?? Infinity);
4542
+ }
4183
4543
 
4184
4544
  // src/components/file/validate.ts
4185
4545
  function readMultiFileResourceIds(scopeRoot, fullKey) {
@@ -4302,33 +4662,36 @@ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
4302
4662
  hiddenInput.name = pathKey;
4303
4663
  hiddenInput.value = initial;
4304
4664
  wrapper.appendChild(hiddenInput);
4305
- renderFilePreviewReadonly(initial, state).then((filePreview) => {
4306
- wrapper.appendChild(filePreview);
4307
- }).catch((err) => {
4308
- console.error("Failed to render file preview:", err);
4309
- wrapper.appendChild(buildEmptyReadonlyTile(state));
4310
- });
4665
+ renderFilePreviewReadonly(initial, state).then((tile) => {
4666
+ tile.classList.add(
4667
+ "fb-single-readonly-filled",
4668
+ "fb-readonly-tile",
4669
+ "fb-checker"
4670
+ );
4671
+ wrapper.appendChild(tile);
4672
+ }).catch(console.error);
4311
4673
  } else {
4312
4674
  wrapper.appendChild(buildEmptyReadonlyTile(state));
4313
4675
  }
4314
4676
  }
4315
4677
  function buildEmptyReadonlyTile(state) {
4678
+ ensureFileStyles();
4316
4679
  const emptyState = document.createElement("div");
4317
4680
  emptyState.style.cssText = `
4318
- width:${TILE_SIZE};
4319
- height:${TILE_SIZE};
4681
+ height: 220px;
4320
4682
  display:flex;
4321
4683
  align-items:center;
4322
4684
  justify-content:center;
4323
- background:var(--fb-file-upload-bg-color,#f3f4f6);
4324
- border-radius:var(--fb-border-radius,0.5rem);
4325
- border:1px solid var(--fb-file-upload-border-color,#d1d5db);
4685
+ background: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
4686
+ border-radius:0.75rem;
4687
+ border:1px solid #e2e8f0;
4326
4688
  `;
4327
4689
  emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
4328
4690
  return emptyState;
4329
4691
  }
4330
- function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4692
+ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
4331
4693
  addPrefillFilesToIndex(rids, state.resourceIndex);
4694
+ ensureFileStyles();
4332
4695
  const filesWrapper = document.createElement("div");
4333
4696
  filesWrapper.dataset.filesWrapper = pathKey;
4334
4697
  filesWrapper.dataset.resourceIds = JSON.stringify(rids);
@@ -4340,22 +4703,28 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4340
4703
  filesWrapper.appendChild(emptyEl);
4341
4704
  return;
4342
4705
  }
4343
- const tilesWrap = document.createElement("div");
4344
- tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
4345
- filesWrapper.appendChild(tilesWrap);
4706
+ const grid = document.createElement("div");
4707
+ grid.className = "fb-multi-readonly-grid";
4708
+ filesWrapper.appendChild(grid);
4346
4709
  const placeholders = rids.map(() => {
4347
- const placeholder = document.createElement("div");
4348
- placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
4349
- tilesWrap.appendChild(placeholder);
4350
- return placeholder;
4710
+ const ph = document.createElement("div");
4711
+ ph.className = "fb-readonly-tile fb-checker fb-tile";
4712
+ grid.appendChild(ph);
4713
+ return ph;
4351
4714
  });
4352
4715
  for (let i = 0; i < rids.length; i++) {
4353
4716
  const resourceId = rids[i];
4354
4717
  const placeholder = placeholders[i];
4355
- renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
4356
- placeholder.replaceWith(tileEl);
4357
- }).catch((err) => {
4358
- console.error("Failed to render readonly tile:", err);
4718
+ const meta = state.resourceIndex.get(resourceId);
4719
+ renderFilePreviewReadonly(resourceId, state, meta?.name).then((tile) => {
4720
+ tile.classList.add("fb-readonly-tile", "fb-checker", "fb-tile-resource");
4721
+ tile.dataset.resourceId = resourceId;
4722
+ placeholder.replaceWith(tile);
4723
+ }).catch(() => {
4724
+ const tile = document.createElement("div");
4725
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4726
+ tile.dataset.resourceId = resourceId;
4727
+ placeholder.replaceWith(tile);
4359
4728
  });
4360
4729
  }
4361
4730
  }
@@ -4367,7 +4736,7 @@ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
4367
4736
  function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
4368
4737
  const rawPrefill = ctx.prefill[element.key];
4369
4738
  const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4370
- renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
4739
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
4371
4740
  }
4372
4741
 
4373
4742
  // src/components/file.ts
@@ -5454,7 +5823,7 @@ function updateSliderField(element, fieldPath, value, context) {
5454
5823
  function extractChildDefaults(elements) {
5455
5824
  const defaults = {};
5456
5825
  for (const child of elements) {
5457
- if ("default" in child && child.default !== void 0) {
5826
+ if (child.key && "default" in child && child.default !== void 0) {
5458
5827
  defaults[child.key] = child.default;
5459
5828
  }
5460
5829
  }
@@ -5505,7 +5874,7 @@ function createPrefillHints(element, pathKey) {
5505
5874
  return null;
5506
5875
  }
5507
5876
  const hintsContainer = document.createElement("div");
5508
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
5877
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
5509
5878
  element.prefillHints.forEach((hint, index) => {
5510
5879
  const hintButton = document.createElement("button");
5511
5880
  hintButton.type = "button";
@@ -5520,14 +5889,14 @@ function createPrefillHints(element, pathKey) {
5520
5889
  }
5521
5890
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5522
5891
  const containerWrap = document.createElement("div");
5523
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
5892
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5524
5893
  containerWrap.setAttribute("data-container", pathKey);
5525
5894
  const itemsWrap = document.createElement("div");
5526
5895
  const columns = element.columns || 1;
5527
5896
  if (columns === 1) {
5528
- itemsWrap.className = "space-y-4";
5897
+ itemsWrap.className = "space-y-2";
5529
5898
  } else {
5530
- itemsWrap.className = `grid grid-cols-${columns} gap-4`;
5899
+ itemsWrap.className = `grid grid-cols-${columns} gap-2`;
5531
5900
  }
5532
5901
  const containerIsReadonly = isElementReadonly(element, ctx.state, ctx);
5533
5902
  if (!containerIsReadonly) {
@@ -5549,8 +5918,8 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5549
5918
  inheritedReadonly: containerIsReadonly || ctx.inheritedReadonly
5550
5919
  };
5551
5920
  element.elements.forEach((child) => {
5552
- if (child.hidden || child.type === "hidden") {
5553
- const prefillVal = containerPrefill[child.key] ?? child.default ?? null;
5921
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5922
+ const prefillVal = containerPrefill[child.key] ?? ("default" in child ? child.default : null) ?? null;
5554
5923
  itemsWrap.appendChild(
5555
5924
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
5556
5925
  );
@@ -5561,16 +5930,31 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5561
5930
  containerWrap.appendChild(itemsWrap);
5562
5931
  wrapper.appendChild(containerWrap);
5563
5932
  }
5933
+ function getChildWrapperClass(isSlides, columns) {
5934
+ if (isSlides) {
5935
+ return "space-y-2";
5936
+ }
5937
+ const cols = columns || 1;
5938
+ return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
5939
+ }
5564
5940
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5565
5941
  const state = ctx.state;
5566
5942
  const containerIsReadonly = isElementReadonly(element, state, ctx);
5567
5943
  const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
5568
5944
  const containerWrap = document.createElement("div");
5569
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
5945
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5570
5946
  const countDisplay = document.createElement("span");
5571
5947
  countDisplay.className = "text-sm text-gray-500";
5572
5948
  const itemsWrap = document.createElement("div");
5573
- itemsWrap.className = "space-y-4";
5949
+ const isSlides = element.displayMode === "slides";
5950
+ if (isSlides) {
5951
+ itemsWrap.className = "fb-container-slides";
5952
+ const slideCols = element.columns;
5953
+ const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
5954
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
5955
+ } else {
5956
+ itemsWrap.className = "space-y-2";
5957
+ }
5574
5958
  if (!containerIsReadonly) {
5575
5959
  const hintsElement = createPrefillHints(element, element.key);
5576
5960
  if (hintsElement) {
@@ -5614,21 +5998,16 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5614
5998
  inheritedReadonly: childInheritedReadonly
5615
5999
  };
5616
6000
  const item = document.createElement("div");
5617
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6001
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5618
6002
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5619
6003
  const childWrapper = document.createElement("div");
5620
- const columns = element.columns || 1;
5621
- if (columns === 1) {
5622
- childWrapper.className = "space-y-4";
5623
- } else {
5624
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
5625
- }
6004
+ childWrapper.className = getChildWrapperClass(isSlides, element.columns);
5626
6005
  element.elements.forEach((child) => {
5627
- if (child.hidden || child.type === "hidden") {
6006
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5628
6007
  childWrapper.appendChild(
5629
6008
  createHiddenInput(
5630
6009
  pathJoin(subCtx.path, child.key),
5631
- child.default ?? null
6010
+ ("default" in child ? child.default : null) ?? null
5632
6011
  )
5633
6012
  );
5634
6013
  } else {
@@ -5691,18 +6070,22 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5691
6070
  inheritedReadonly: childInheritedReadonly
5692
6071
  };
5693
6072
  const item = document.createElement("div");
5694
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6073
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5695
6074
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5696
6075
  const childWrapper = document.createElement("div");
5697
- const columns = element.columns || 1;
5698
- if (columns === 1) {
5699
- childWrapper.className = "space-y-4";
6076
+ if (isSlides) {
6077
+ childWrapper.className = "space-y-2";
5700
6078
  } else {
5701
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6079
+ const columns = element.columns || 1;
6080
+ if (columns === 1) {
6081
+ childWrapper.className = "space-y-2";
6082
+ } else {
6083
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6084
+ }
5702
6085
  }
5703
6086
  element.elements.forEach((child) => {
5704
- if (child.hidden || child.type === "hidden") {
5705
- const prefillVal = prefillObj?.[child.key] ?? child.default ?? null;
6087
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6088
+ const prefillVal = prefillObj?.[child.key] ?? ("default" in child ? child.default : null) ?? null;
5706
6089
  childWrapper.appendChild(
5707
6090
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
5708
6091
  );
@@ -5747,21 +6130,25 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5747
6130
  inheritedReadonly: childInheritedReadonly
5748
6131
  };
5749
6132
  const item = document.createElement("div");
5750
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6133
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5751
6134
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5752
6135
  const childWrapper = document.createElement("div");
5753
- const columns = element.columns || 1;
5754
- if (columns === 1) {
5755
- childWrapper.className = "space-y-4";
6136
+ if (isSlides) {
6137
+ childWrapper.className = "space-y-2";
5756
6138
  } else {
5757
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6139
+ const columns = element.columns || 1;
6140
+ if (columns === 1) {
6141
+ childWrapper.className = "space-y-2";
6142
+ } else {
6143
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6144
+ }
5758
6145
  }
5759
6146
  element.elements.forEach((child) => {
5760
- if (child.hidden || child.type === "hidden") {
6147
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5761
6148
  childWrapper.appendChild(
5762
6149
  createHiddenInput(
5763
6150
  pathJoin(subCtx.path, child.key),
5764
- child.default ?? null
6151
+ ("default" in child ? child.default : null) ?? null
5765
6152
  )
5766
6153
  );
5767
6154
  } else {
@@ -5947,6 +6334,7 @@ function updateContainerField(element, fieldPath, value, context) {
5947
6334
  value.forEach((itemValue, index) => {
5948
6335
  if (isPlainObject(itemValue)) {
5949
6336
  element.elements.forEach((childElement) => {
6337
+ if (childElement.type === "markdown" || !childElement.key) return;
5950
6338
  const childPath = `${fieldPath}[${index}].${childElement.key}`;
5951
6339
  if (childElement.type === "richinput" && childElement.flatOutput) {
5952
6340
  const richChild = childElement;
@@ -5986,6 +6374,7 @@ function updateContainerField(element, fieldPath, value, context) {
5986
6374
  return;
5987
6375
  }
5988
6376
  element.elements.forEach((childElement) => {
6377
+ if (childElement.type === "markdown" || !childElement.key) return;
5989
6378
  const childPath = `${fieldPath}.${childElement.key}`;
5990
6379
  if (childElement.type === "richinput" && childElement.flatOutput) {
5991
6380
  const richChild = childElement;
@@ -7780,7 +8169,7 @@ function filterFilesForDropdown(query, files, labels) {
7780
8169
  });
7781
8170
  }
7782
8171
  var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
7783
- var TEXTAREA_PADDING = "padding: 12px 52px 12px 14px;";
8172
+ var TEXTAREA_PADDING = "padding: 8px 40px 8px 10px;";
7784
8173
  function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7785
8174
  const state = ctx.state;
7786
8175
  const files = [...initialValue.files];
@@ -7827,7 +8216,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7827
8216
  });
7828
8217
  const errorEl = document.createElement("div");
7829
8218
  errorEl.className = "fb-richinput-error";
7830
- errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
8219
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 10px 6px;";
7831
8220
  let errorTimer = null;
7832
8221
  function showUploadError(message) {
7833
8222
  errorEl.textContent = message;
@@ -7903,7 +8292,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7903
8292
  });
7904
8293
  const filesRow = document.createElement("div");
7905
8294
  filesRow.className = "fb-richinput-files";
7906
- filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
8295
+ filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 4px; padding: 6px 10px 0; align-items: center;";
7907
8296
  const fileInput = document.createElement("input");
7908
8297
  fileInput.type = "file";
7909
8298
  fileInput.multiple = true;
@@ -8033,13 +8422,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8033
8422
  paperclipBtn.title = t("richinputAttachFile", state);
8034
8423
  paperclipBtn.style.cssText = `
8035
8424
  position: absolute;
8036
- right: 10px;
8037
- bottom: 10px;
8425
+ right: 6px;
8426
+ bottom: 6px;
8038
8427
  z-index: 2;
8039
- width: 32px;
8040
- height: 32px;
8428
+ width: 28px;
8429
+ height: 28px;
8041
8430
  border: none;
8042
- border-radius: 8px;
8431
+ border-radius: 6px;
8043
8432
  background: transparent;
8044
8433
  cursor: pointer;
8045
8434
  display: flex;
@@ -8393,7 +8782,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8393
8782
  outerDiv.appendChild(errorEl);
8394
8783
  if (element.minLength != null || element.maxLength != null) {
8395
8784
  const counterRow = document.createElement("div");
8396
- counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
8785
+ counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8397
8786
  const counter = createCharCounter(element, textarea, false);
8398
8787
  counter.style.cssText = `
8399
8788
  position: static;
@@ -8685,6 +9074,248 @@ function updateRichInputField(element, fieldPath, value, context) {
8685
9074
  }
8686
9075
  }
8687
9076
 
9077
+ // src/components/markdown/snarkdown.ts
9078
+ var TAGS = {
9079
+ "": ["<em>", "</em>"],
9080
+ _: ["<strong>", "</strong>"],
9081
+ "*": ["<strong>", "</strong>"],
9082
+ "~": ["<s>", "</s>"],
9083
+ "\n": ["<br />"],
9084
+ " ": ["<br />"],
9085
+ "-": ["<hr />"]
9086
+ };
9087
+ function outdent(str) {
9088
+ return str.replace(
9089
+ RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
9090
+ ""
9091
+ );
9092
+ }
9093
+ function encodeAttr(str) {
9094
+ return (str + "").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9095
+ }
9096
+ function parse(md, prevLinks) {
9097
+ const tokenizer = /((?:^|\n+)(?:\n---+|\*[ ]\*(?:[ ]\*)+)\n)|(?:^```[ ]*(\w*)\n([\s\S]*?)\n```$)|((?:(?:^|\n+)(?:\t|[ ]{2,}).+)+\n*)|((?:(?:^|\n)([>*+-]|\d+\.)\s+.*)+)|(?:!\[([^\]]*?)\]\(([^)]+?)\))|(\[)|(\](?:\(([^)]+?)\))?)|(?:(?:^|\n+)([^\s].*)\n(-{3,}|={3,})(?:\n+|$))|(?:(?:^|\n+)(#{1,6})\s*(.+)(?:\n+|$))|(?:`([^`].*?)`)|([ ]{2}\n\n*|\n{2,}|__|\*\*|[_*]|~~)/gm;
9098
+ const context = [];
9099
+ let out = "";
9100
+ const links = prevLinks || {};
9101
+ let last = 0;
9102
+ let chunk;
9103
+ let prev;
9104
+ let token;
9105
+ let inner;
9106
+ let t2;
9107
+ function tag(token2) {
9108
+ const desc = TAGS[token2[1] || ""];
9109
+ const end = context[context.length - 1] === token2;
9110
+ if (!desc) return token2;
9111
+ if (!desc[1]) return desc[0];
9112
+ if (end) context.pop();
9113
+ else context.push(token2);
9114
+ return desc[end ? 1 : 0];
9115
+ }
9116
+ function flush() {
9117
+ let str = "";
9118
+ while (context.length) str += tag(context[context.length - 1]);
9119
+ return str;
9120
+ }
9121
+ md = md.replace(/^\[(.+?)\]:\s*(.+)$/gm, (_s, name, url) => {
9122
+ links[name.toLowerCase()] = url;
9123
+ return "";
9124
+ }).replace(/^\n+|\n+$/g, "");
9125
+ while (token = tokenizer.exec(md)) {
9126
+ prev = md.substring(last, token.index);
9127
+ last = tokenizer.lastIndex;
9128
+ chunk = token[0];
9129
+ if (prev.match(/[^\\](\\\\)*\\$/)) ; else if (t2 = token[3] || token[4]) {
9130
+ chunk = '<pre class="code ' + (token[4] ? "poetry" : token[2].toLowerCase()) + '"><code' + (token[2] ? ` class="language-${token[2].toLowerCase()}"` : "") + ">" + outdent(encodeAttr(t2).replace(/^\n+|\n+$/g, "")) + "</code></pre>";
9131
+ } else if (t2 = token[6]) {
9132
+ if (t2.match(/\./)) {
9133
+ token[5] = token[5].replace(/^\d+/gm, "");
9134
+ }
9135
+ inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, "")));
9136
+ if (t2 === ">") t2 = "blockquote";
9137
+ else {
9138
+ t2 = t2.match(/\./) ? "ol" : "ul";
9139
+ inner = inner.replace(/^(.*)(\n|$)/gm, "<li>$1</li>");
9140
+ }
9141
+ chunk = "<" + t2 + ">" + inner + "</" + t2 + ">";
9142
+ } else if (token[8]) {
9143
+ chunk = `<img src="${encodeAttr(token[8])}" alt="${encodeAttr(token[7])}">`;
9144
+ } else if (token[10]) {
9145
+ out = out.replace(
9146
+ "<a>",
9147
+ `<a href="${encodeAttr(token[11] || links[prev.toLowerCase()])}">`
9148
+ );
9149
+ chunk = flush() + "</a>";
9150
+ } else if (token[9]) {
9151
+ chunk = "<a>";
9152
+ } else if (token[12] || token[14]) {
9153
+ t2 = "h" + (token[14] ? token[14].length : token[13] > "=" ? 1 : 2);
9154
+ chunk = "<" + t2 + ">" + parse(token[12] || token[15], links) + "</" + t2 + ">";
9155
+ } else if (token[16]) {
9156
+ chunk = "<code>" + encodeAttr(token[16]) + "</code>";
9157
+ } else if (token[17] || token[1]) {
9158
+ chunk = tag(token[17] || "--");
9159
+ }
9160
+ out += prev;
9161
+ out += chunk;
9162
+ }
9163
+ return (out + md.substring(last) + flush()).replace(/^\n+|\n+$/g, "");
9164
+ }
9165
+
9166
+ // src/components/markdown/render.ts
9167
+ var STYLE_ID2 = "fb-markdown-styles";
9168
+ function ensureMarkdownStyles() {
9169
+ if (typeof document === "undefined") return;
9170
+ if (document.getElementById(STYLE_ID2)) return;
9171
+ const style = document.createElement("style");
9172
+ style.id = STYLE_ID2;
9173
+ style.setAttribute("data-fb-markdown-styles", "true");
9174
+ style.textContent = `
9175
+ .fb-markdown {
9176
+ font-family: var(--fb-font-family, inherit);
9177
+ font-size: var(--fb-font-size, 1rem);
9178
+ color: var(--fb-text-color, #1f2937);
9179
+ line-height: 1.6;
9180
+ }
9181
+ .fb-markdown h1,
9182
+ .fb-markdown h2,
9183
+ .fb-markdown h3,
9184
+ .fb-markdown h4,
9185
+ .fb-markdown h5,
9186
+ .fb-markdown h6 {
9187
+ font-weight: var(--fb-font-weight-medium, 600);
9188
+ color: var(--fb-text-color, #1f2937);
9189
+ margin-top: 0.75em;
9190
+ margin-bottom: 0.25em;
9191
+ }
9192
+ .fb-markdown h1 { font-size: 1.5rem; }
9193
+ .fb-markdown h2 { font-size: 1.25rem; }
9194
+ .fb-markdown h3 { font-size: 1.1rem; }
9195
+ .fb-markdown h4,
9196
+ .fb-markdown h5,
9197
+ .fb-markdown h6 { font-size: 1rem; }
9198
+ .fb-markdown p {
9199
+ margin-top: 0;
9200
+ margin-bottom: 0.5em;
9201
+ }
9202
+ .fb-markdown ul,
9203
+ .fb-markdown ol {
9204
+ margin: 0.25em 0 0.5em 1.5em;
9205
+ padding: 0;
9206
+ }
9207
+ .fb-markdown li {
9208
+ margin-bottom: 0.15em;
9209
+ }
9210
+ .fb-markdown a {
9211
+ color: var(--fb-primary-color, #2563eb);
9212
+ text-decoration: underline;
9213
+ }
9214
+ .fb-markdown a:hover {
9215
+ opacity: 0.8;
9216
+ }
9217
+ .fb-markdown code {
9218
+ background: var(--fb-input-background-color, #f3f4f6);
9219
+ border-radius: 3px;
9220
+ padding: 0.1em 0.35em;
9221
+ font-size: 0.9em;
9222
+ font-family: monospace;
9223
+ }
9224
+ .fb-markdown pre {
9225
+ background: var(--fb-input-background-color, #f3f4f6);
9226
+ border-radius: var(--fb-border-radius, 0.375rem);
9227
+ padding: 0.75em 1em;
9228
+ overflow-x: auto;
9229
+ margin: 0.5em 0;
9230
+ }
9231
+ .fb-markdown pre code {
9232
+ background: none;
9233
+ padding: 0;
9234
+ border-radius: 0;
9235
+ font-size: inherit;
9236
+ }
9237
+ .fb-markdown blockquote {
9238
+ border-left: 3px solid var(--fb-border-color, #d1d5db);
9239
+ margin: 0.5em 0;
9240
+ padding-left: 1em;
9241
+ color: var(--fb-text-secondary-color, #6b7280);
9242
+ }
9243
+ .fb-markdown hr {
9244
+ border: none;
9245
+ border-top: 1px solid var(--fb-border-color, #d1d5db);
9246
+ margin: 0.75em 0;
9247
+ }
9248
+ .fb-markdown strong { font-weight: var(--fb-font-weight-medium, 600); }
9249
+ .fb-markdown em { font-style: italic; }
9250
+ .fb-markdown s { text-decoration: line-through; }
9251
+ `;
9252
+ document.head.appendChild(style);
9253
+ }
9254
+ var ANCHOR_DANGEROUS_SCHEMES = [
9255
+ "javascript:",
9256
+ "data:",
9257
+ "vbscript:",
9258
+ "blob:"
9259
+ ];
9260
+ var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
9261
+ function isImgSrcDangerous(normalized) {
9262
+ if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
9263
+ return true;
9264
+ }
9265
+ if (normalized.startsWith("data:") && !normalized.startsWith("data:image/")) {
9266
+ return true;
9267
+ }
9268
+ return false;
9269
+ }
9270
+ function escapeRawHtml(content) {
9271
+ return content.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9272
+ }
9273
+ function sanitizeElements(container) {
9274
+ const anchors = container.querySelectorAll("a[href]");
9275
+ anchors.forEach((a) => {
9276
+ const href = a.getAttribute("href") ?? "";
9277
+ const normalized = href.trim().toLowerCase();
9278
+ const isDangerous = ANCHOR_DANGEROUS_SCHEMES.some(
9279
+ (scheme) => normalized.startsWith(scheme)
9280
+ );
9281
+ if (isDangerous) {
9282
+ a.setAttribute("href", "#");
9283
+ }
9284
+ a.setAttribute("target", "_blank");
9285
+ a.setAttribute("rel", "noopener noreferrer");
9286
+ });
9287
+ const images = container.querySelectorAll("img[src]");
9288
+ images.forEach((img) => {
9289
+ const src = img.getAttribute("src") ?? "";
9290
+ const normalized = src.trim().toLowerCase();
9291
+ if (isImgSrcDangerous(normalized)) {
9292
+ img.setAttribute("src", "");
9293
+ }
9294
+ });
9295
+ }
9296
+ function renderMarkdown(element, _ctx, parent) {
9297
+ if (typeof element.content !== "string") {
9298
+ throw new Error(
9299
+ `renderMarkdown: markdown element${element.key ? ` "${element.key}"` : ""} requires "content" to be a string (got ${element.content === null ? "null" : typeof element.content})`
9300
+ );
9301
+ }
9302
+ ensureMarkdownStyles();
9303
+ const wrapper = document.createElement("div");
9304
+ wrapper.className = "fb-markdown";
9305
+ const escaped = escapeRawHtml(element.content);
9306
+ wrapper.innerHTML = parse(escaped);
9307
+ sanitizeElements(wrapper);
9308
+ parent.appendChild(wrapper);
9309
+ return wrapper;
9310
+ }
9311
+
9312
+ // src/components/markdown/index.ts
9313
+ function validateMarkdown(_element, _key, _context) {
9314
+ return { value: void 0, errors: [], skip: true };
9315
+ }
9316
+ function updateMarkdown(_element, _fieldPath, _value, _context) {
9317
+ }
9318
+
8688
9319
  // src/components/index.ts
8689
9320
  function showTooltip(tooltipId, button) {
8690
9321
  const tooltip = document.getElementById(tooltipId);
@@ -8915,7 +9546,7 @@ function setupEnableIfListeners(wrapper, element, ctx) {
8915
9546
  function createFieldLabel(element) {
8916
9547
  const title = document.createElement("label");
8917
9548
  title.className = "text-sm font-medium text-gray-900";
8918
- title.textContent = element.label || element.key;
9549
+ title.textContent = element.label ?? element.key ?? "";
8919
9550
  if (element.required) {
8920
9551
  const req = document.createElement("span");
8921
9552
  req.className = "text-red-500 ml-1";
@@ -8945,7 +9576,7 @@ function createInfoButton(element) {
8945
9576
  }
8946
9577
  function createLabelContainer(element) {
8947
9578
  const label = document.createElement("div");
8948
- label.className = "flex items-center mb-2";
9579
+ label.className = "flex items-center mb-1";
8949
9580
  const title = createFieldLabel(element);
8950
9581
  label.appendChild(title);
8951
9582
  if (element.description || element.hint) {
@@ -9043,9 +9674,32 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9043
9674
  }
9044
9675
  }
9045
9676
  function renderElement2(element, ctx) {
9677
+ if (element.type === "markdown") {
9678
+ if (element.hidden === true) {
9679
+ const placeholder = document.createElement("div");
9680
+ placeholder.style.display = "none";
9681
+ placeholder.setAttribute("data-fb-hidden-markdown", "true");
9682
+ return placeholder;
9683
+ }
9684
+ const initiallyDisabled2 = shouldDisableElement(element, ctx);
9685
+ const outerWrapper = document.createElement("div");
9686
+ outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
9687
+ outerWrapper.setAttribute(
9688
+ "data-field-key",
9689
+ getElementLookupKey(element, ctx.state)
9690
+ );
9691
+ renderMarkdown(element, ctx, outerWrapper);
9692
+ if (initiallyDisabled2) {
9693
+ outerWrapper.style.display = "none";
9694
+ outerWrapper.classList.add("fb-field-wrapper-disabled");
9695
+ outerWrapper.setAttribute("data-conditionally-disabled", "true");
9696
+ }
9697
+ setupEnableIfListeners(outerWrapper, element, ctx);
9698
+ return outerWrapper;
9699
+ }
9046
9700
  const initiallyDisabled = shouldDisableElement(element, ctx);
9047
9701
  const wrapper = document.createElement("div");
9048
- wrapper.className = "mb-6 fb-field-wrapper";
9702
+ wrapper.className = "mb-2 fb-field-wrapper";
9049
9703
  wrapper.setAttribute("data-field-key", element.key);
9050
9704
  const label = createLabelContainer(element);
9051
9705
  wrapper.appendChild(label);
@@ -9112,12 +9766,16 @@ var defaultConfig = {
9112
9766
  hintPattern: "Format: {pattern}",
9113
9767
  fileCountSingle: "{count} file",
9114
9768
  fileCountPlural: "{count} files",
9769
+ fileCountWithMax: "{count} / {max} files",
9115
9770
  fileCountRange: "({min}-{max})",
9116
9771
  uploadingFile: "Uploading\u2026",
9117
9772
  filesCounter: "{count}/{max}",
9118
9773
  fromLibrary: "From library",
9119
9774
  libraryEmpty: "Library is empty",
9120
9775
  libraryHint: "Choose from previously uploaded files",
9776
+ dropToUpload: "Release to upload",
9777
+ replaceFile: "Replace",
9778
+ clearAll: "Clear all",
9121
9779
  pickerError: "Failed to load files from library",
9122
9780
  // Validation errors
9123
9781
  required: "Required",
@@ -9183,12 +9841,16 @@ var defaultConfig = {
9183
9841
  hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
9184
9842
  fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
9185
9843
  fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
9844
+ fileCountWithMax: "{count} / {max} \u0444\u0430\u0439\u043B\u043E\u0432",
9186
9845
  fileCountRange: "({min}-{max})",
9187
9846
  uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
9188
9847
  filesCounter: "{count}/{max}",
9189
9848
  fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9190
9849
  libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9191
9850
  libraryHint: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0438\u0437 \u0440\u0430\u043D\u0435\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432",
9851
+ dropToUpload: "\u041E\u0442\u043F\u0443\u0441\u0442\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C",
9852
+ replaceFile: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C",
9853
+ clearAll: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435",
9192
9854
  pickerError: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u0438\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9193
9855
  // Validation errors
9194
9856
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
@@ -9253,7 +9915,9 @@ function createInstanceState(config) {
9253
9915
  translations: mergedTranslations
9254
9916
  },
9255
9917
  debounceTimer: null,
9256
- prefill: {}
9918
+ prefill: {},
9919
+ syntheticElementIds: /* @__PURE__ */ new WeakMap(),
9920
+ syntheticElementIdCounter: 0
9257
9921
  };
9258
9922
  }
9259
9923
  function generateInstanceId() {
@@ -9327,10 +9991,10 @@ var defaultTheme = {
9327
9991
  fileUploadHoverBorderColor: "#3b82f6",
9328
9992
  // blue-500
9329
9993
  // Spacing
9330
- inputPaddingX: "0.75rem",
9331
- // 3 (12px)
9332
- inputPaddingY: "0.5rem",
9333
- // 2 (8px)
9994
+ inputPaddingX: "0.5rem",
9995
+ // 8px (compact density v2)
9996
+ inputPaddingY: "0.25rem",
9997
+ // 4px (compact density v2)
9334
9998
  borderRadius: "0.5rem",
9335
9999
  // rounded-lg (8px)
9336
10000
  borderWidth: "1px",
@@ -9530,6 +10194,11 @@ var componentRegistry = {
9530
10194
  // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
9531
10195
  validate: validateHiddenElement,
9532
10196
  update: updateHiddenField
10197
+ },
10198
+ markdown: {
10199
+ // Display-only element — no value, no errors, skip from form data
10200
+ validate: validateMarkdown,
10201
+ update: updateMarkdown
9533
10202
  }
9534
10203
  };
9535
10204
  function getComponentOperations(elementType) {
@@ -9770,7 +10439,7 @@ var FormBuilderInstance = class {
9770
10439
  existingContainer.remove();
9771
10440
  }
9772
10441
  const actionsContainer = document.createElement("div");
9773
- actionsContainer.className = "form-level-actions-container mt-6 pt-4 flex flex-wrap gap-3 justify-center";
10442
+ actionsContainer.className = "form-level-actions-container mt-3 pt-2 flex flex-wrap gap-2 justify-center";
9774
10443
  actionsContainer.style.cssText = `
9775
10444
  border-top: var(--fb-border-width) solid var(--fb-border-color);
9776
10445
  `;
@@ -9911,7 +10580,7 @@ var FormBuilderInstance = class {
9911
10580
  */
9912
10581
  createRootPrefillHints(hints) {
9913
10582
  const hintsContainer = document.createElement("div");
9914
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
10583
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
9915
10584
  hints.forEach((hint) => {
9916
10585
  const hintButton = document.createElement("button");
9917
10586
  hintButton.type = "button";
@@ -9944,7 +10613,7 @@ var FormBuilderInstance = class {
9944
10613
  root.setAttribute("data-fb-root", "true");
9945
10614
  injectThemeVariables(root, this.state.config.theme);
9946
10615
  const rootContainer = document.createElement("div");
9947
- rootContainer.className = "space-y-6";
10616
+ rootContainer.className = "space-y-2";
9948
10617
  if (schema.prefillHints && !this.state.config.readonly) {
9949
10618
  const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
9950
10619
  rootContainer.appendChild(hintsContainer);
@@ -9952,12 +10621,12 @@ var FormBuilderInstance = class {
9952
10621
  const fieldsWrapper = document.createElement("div");
9953
10622
  const columns = schema.columns || 1;
9954
10623
  if (columns === 1) {
9955
- fieldsWrapper.className = "space-y-4";
10624
+ fieldsWrapper.className = "space-y-2";
9956
10625
  } else {
9957
- fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
10626
+ fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
9958
10627
  }
9959
10628
  schema.elements.forEach((element) => {
9960
- if (element.hidden || element.type === "hidden") {
10629
+ if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
9961
10630
  const val = prefill?.[element.key] ?? element.default ?? null;
9962
10631
  fieldsWrapper.appendChild(createHiddenInput(element.key, val));
9963
10632
  return;
@@ -9992,7 +10661,7 @@ var FormBuilderInstance = class {
9992
10661
  const errors = [];
9993
10662
  const data = {};
9994
10663
  const validateElement2 = (element, ctx, customScopeRoot = null) => {
9995
- const key = element.key;
10664
+ const key = element.key ?? "";
9996
10665
  const scopeRoot = customScopeRoot || this.state.formRoot;
9997
10666
  const componentContext = {
9998
10667
  scopeRoot,
@@ -10010,7 +10679,8 @@ var FormBuilderInstance = class {
10010
10679
  errors.push(...componentResult.errors);
10011
10680
  return {
10012
10681
  value: componentResult.value,
10013
- spread: !!componentResult.spread
10682
+ spread: !!componentResult.spread,
10683
+ skip: !!componentResult.skip
10014
10684
  };
10015
10685
  }
10016
10686
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
@@ -10031,6 +10701,9 @@ var FormBuilderInstance = class {
10031
10701
  );
10032
10702
  }
10033
10703
  }
10704
+ if (element.type === "markdown") {
10705
+ return;
10706
+ }
10034
10707
  if (element.hidden || element.type === "hidden") {
10035
10708
  const hiddenInput = this.state.formRoot.querySelector(
10036
10709
  `input[type="hidden"][data-hidden-field="true"][name="${element.key}"]`
@@ -10043,9 +10716,10 @@ var FormBuilderInstance = class {
10043
10716
  }
10044
10717
  } else {
10045
10718
  const result = validateElement2(element, { path: "" });
10719
+ if (result.skip) return;
10046
10720
  if (result.spread && result.value !== null && typeof result.value === "object") {
10047
10721
  Object.assign(data, result.value);
10048
- } else {
10722
+ } else if (element.key) {
10049
10723
  data[element.key] = result.value;
10050
10724
  }
10051
10725
  }
@@ -10121,6 +10795,7 @@ var FormBuilderInstance = class {
10121
10795
  buildHiddenFieldsData(elements) {
10122
10796
  const data = {};
10123
10797
  for (const element of elements) {
10798
+ if (element.type === "markdown" || !element.key) continue;
10124
10799
  const key = element.key;
10125
10800
  if (element.hidden && element.default !== void 0) {
10126
10801
  data[key] = element.default;
@@ -10241,6 +10916,71 @@ var FormBuilderInstance = class {
10241
10916
  );
10242
10917
  }
10243
10918
  }
10919
+ /**
10920
+ * Find the field wrapper DOM element for a given element+path combo.
10921
+ * Used by reevaluateConditionalFields.
10922
+ */
10923
+ findFieldWrapper(element, currentPath) {
10924
+ const formRoot = this.state.formRoot;
10925
+ const lookupKey = getElementLookupKey(element, this.state);
10926
+ if (!currentPath) {
10927
+ return formRoot.querySelector(`[data-field-key="${lookupKey}"]`);
10928
+ }
10929
+ const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
10930
+ if (pathMatch) {
10931
+ const containerEl2 = formRoot.querySelector(
10932
+ `[data-container-item="${pathMatch[1]}[${pathMatch[2]}]"]`
10933
+ );
10934
+ return containerEl2 ? containerEl2.querySelector(`[data-field-key="${lookupKey}"]`) : null;
10935
+ }
10936
+ const containerEl = formRoot.querySelector(
10937
+ `[data-container="${currentPath}"]`
10938
+ );
10939
+ return containerEl ? containerEl.querySelector(`[data-field-key="${lookupKey}"]`) : null;
10940
+ }
10941
+ /**
10942
+ * Apply enableIf show/hide logic to a single field wrapper.
10943
+ * Extracted to reduce cyclomatic complexity of checkElements.
10944
+ */
10945
+ applyEnableIfVisibility(element, wrapper, currentPath, fullPath, formData) {
10946
+ try {
10947
+ const scope = element.enableIf.scope ?? "relative";
10948
+ const containerData = scope === "relative" && currentPath ? getValueByPath(formData, currentPath) : void 0;
10949
+ const shouldEnable = evaluateEnableCondition(
10950
+ element.enableIf,
10951
+ formData,
10952
+ containerData
10953
+ );
10954
+ const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
10955
+ if (shouldEnable && isCurrentlyDisabled) {
10956
+ const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
10957
+ const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
10958
+ const newWrapper = renderElement2(element, {
10959
+ path: currentPath,
10960
+ prefill: prefillContext,
10961
+ formData,
10962
+ state: this.state,
10963
+ instance: this
10964
+ });
10965
+ wrapper.parentNode?.replaceChild(newWrapper, wrapper);
10966
+ } else if (!shouldEnable && !isCurrentlyDisabled) {
10967
+ const disabledWrapper = document.createElement("div");
10968
+ disabledWrapper.className = "fb-field-wrapper-disabled";
10969
+ disabledWrapper.style.display = "none";
10970
+ disabledWrapper.setAttribute(
10971
+ "data-field-key",
10972
+ getElementLookupKey(element, this.state)
10973
+ );
10974
+ disabledWrapper.setAttribute("data-conditionally-disabled", "true");
10975
+ wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
10976
+ }
10977
+ } catch (error) {
10978
+ console.error(
10979
+ `Error re-evaluating enableIf for field "${element.key ?? "<no key>"}" at path "${fullPath}":`,
10980
+ error
10981
+ );
10982
+ }
10983
+ }
10244
10984
  /**
10245
10985
  * Re-evaluate all conditional fields (enableIf) based on current form data
10246
10986
  * This is called automatically when form data changes (via onChange events)
@@ -10250,97 +10990,31 @@ var FormBuilderInstance = class {
10250
10990
  const formData = this.validateForm(true).data;
10251
10991
  const checkElements = (elements, currentPath) => {
10252
10992
  elements.forEach((element) => {
10253
- const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
10993
+ const fullPath = currentPath ? `${currentPath}.${element.key ?? ""}` : element.key ?? "";
10254
10994
  if (element.enableIf) {
10255
- let fieldWrapper = null;
10256
- if (currentPath) {
10257
- const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
10258
- if (pathMatch) {
10259
- const containerKey = pathMatch[1];
10260
- const containerIndex = pathMatch[2];
10261
- const containerElement = this.state.formRoot.querySelector(
10262
- `[data-container-item="${containerKey}[${containerIndex}]"]`
10263
- );
10264
- if (containerElement) {
10265
- fieldWrapper = containerElement.querySelector(
10266
- `[data-field-key="${element.key}"]`
10267
- );
10268
- }
10269
- } else {
10270
- const containerElement = this.state.formRoot.querySelector(
10271
- `[data-container="${currentPath}"]`
10272
- );
10273
- if (containerElement) {
10274
- fieldWrapper = containerElement.querySelector(
10275
- `[data-field-key="${element.key}"]`
10276
- );
10277
- }
10278
- }
10279
- } else {
10280
- fieldWrapper = this.state.formRoot.querySelector(
10281
- `[data-field-key="${element.key}"]`
10282
- );
10283
- }
10995
+ const fieldWrapper = this.findFieldWrapper(element, currentPath);
10284
10996
  if (fieldWrapper) {
10285
- const wrapper = fieldWrapper;
10286
- try {
10287
- let containerData = void 0;
10288
- const scope = element.enableIf.scope ?? "relative";
10289
- if (scope === "relative" && currentPath) {
10290
- containerData = getValueByPath(formData, currentPath);
10291
- }
10292
- const shouldEnable = evaluateEnableCondition(
10293
- element.enableIf,
10294
- formData,
10295
- // Use complete formData for absolute scope
10296
- containerData
10297
- // Use container-specific data for relative scope
10298
- );
10299
- const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
10300
- if (shouldEnable && isCurrentlyDisabled) {
10301
- const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
10302
- const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
10303
- const newWrapper = renderElement2(element, {
10304
- path: currentPath,
10305
- // Use container path (empty string for root-level)
10306
- prefill: prefillContext,
10307
- formData,
10308
- // Pass complete formData for enableIf evaluation
10309
- state: this.state,
10310
- instance: this
10311
- });
10312
- wrapper.parentNode?.replaceChild(newWrapper, wrapper);
10313
- } else if (!shouldEnable && !isCurrentlyDisabled) {
10314
- const disabledWrapper = document.createElement("div");
10315
- disabledWrapper.className = "fb-field-wrapper-disabled";
10316
- disabledWrapper.style.display = "none";
10317
- disabledWrapper.setAttribute("data-field-key", element.key);
10318
- disabledWrapper.setAttribute(
10319
- "data-conditionally-disabled",
10320
- "true"
10321
- );
10322
- wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
10323
- }
10324
- } catch (error) {
10325
- console.error(
10326
- `Error re-evaluating enableIf for field "${element.key}" at path "${fullPath}":`,
10327
- error
10328
- );
10329
- }
10997
+ this.applyEnableIfVisibility(
10998
+ element,
10999
+ fieldWrapper,
11000
+ currentPath,
11001
+ fullPath,
11002
+ formData
11003
+ );
10330
11004
  }
10331
11005
  }
10332
11006
  if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
10333
- const containerData = formData?.[element.key];
11007
+ const containerData = element.key ? formData?.[element.key] : void 0;
10334
11008
  if (Array.isArray(containerData)) {
10335
11009
  const containerItems = this.state.formRoot.querySelectorAll(
10336
11010
  `[data-container-item]`
10337
11011
  );
10338
- const directItems = Array.from(containerItems).filter((el) => {
11012
+ const directItems = fullPath ? Array.from(containerItems).filter((el) => {
10339
11013
  const attr = el.getAttribute("data-container-item") || "";
10340
11014
  if (!attr.startsWith(`${fullPath}[`)) return false;
10341
11015
  const suffix = attr.slice(fullPath.length);
10342
11016
  return /^\[\d+\]$/.test(suffix);
10343
- });
11017
+ }) : [];
10344
11018
  directItems.forEach((el) => {
10345
11019
  const attr = el.getAttribute("data-container-item") || "";
10346
11020
  checkElements(element.elements, attr);