@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.
@@ -50,8 +50,10 @@ function addRangeHint(element, parts, state) {
50
50
  }
51
51
  }
52
52
  function addFileSizeHint(element, parts, state) {
53
- if (element.maxSizeMB) {
54
- parts.push(t("hintMaxSize", state, { size: element.maxSizeMB }));
53
+ var _a;
54
+ const sizeMB = (_a = element.maxSize) != null ? _a : element.maxSizeMB;
55
+ if (sizeMB && sizeMB !== Infinity) {
56
+ parts.push(t("hintMaxSize", state, { size: sizeMB }));
55
57
  }
56
58
  }
57
59
  function addFormatHint(element, parts, state) {
@@ -125,6 +127,25 @@ function validateSchema(schema) {
125
127
  });
126
128
  }
127
129
  }
130
+ function validateContainerProps(element, elementPath, errors2) {
131
+ if ("columns" in element && element.columns !== void 0) {
132
+ const columns = element.columns;
133
+ const validColumns = [1, 2, 3, 4];
134
+ if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
135
+ errors2.push(
136
+ `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
137
+ );
138
+ }
139
+ }
140
+ if ("displayMode" in element && element.displayMode !== void 0) {
141
+ const displayMode = element.displayMode;
142
+ if (displayMode !== "stack" && displayMode !== "slides") {
143
+ errors2.push(
144
+ `${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
145
+ );
146
+ }
147
+ }
148
+ }
128
149
  function checkFlatOutputCollisions(elements, scopePath) {
129
150
  var _a, _b;
130
151
  const allOutputKeys = /* @__PURE__ */ new Set();
@@ -159,12 +180,14 @@ function validateSchema(schema) {
159
180
  allOutputKeys.add(textKey);
160
181
  allOutputKeys.add(filesKey);
161
182
  } else {
162
- if (allOutputKeys.has(el.key)) {
163
- errors.push(
164
- `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
165
- );
183
+ if (el.key) {
184
+ if (allOutputKeys.has(el.key)) {
185
+ errors.push(
186
+ `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
187
+ );
188
+ }
189
+ allOutputKeys.add(el.key);
166
190
  }
167
- allOutputKeys.add(el.key);
168
191
  }
169
192
  }
170
193
  }
@@ -174,9 +197,17 @@ function validateSchema(schema) {
174
197
  if (!element.type) {
175
198
  errors.push(`${elementPath}: missing type`);
176
199
  }
177
- if (!element.key) {
200
+ if (!element.key && element.type !== "markdown") {
178
201
  errors.push(`${elementPath}: missing key`);
179
202
  }
203
+ if (element.type === "markdown") {
204
+ const content = element.content;
205
+ if (typeof content !== "string") {
206
+ errors.push(
207
+ `${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
208
+ );
209
+ }
210
+ }
180
211
  if (element.enableIf) {
181
212
  const enableIf = element.enableIf;
182
213
  if (!enableIf.key || typeof enableIf.key !== "string") {
@@ -195,15 +226,7 @@ function validateSchema(schema) {
195
226
  validateElements(element.elements, `${elementPath}.elements`);
196
227
  }
197
228
  if (element.type === "container" && element.elements) {
198
- if ("columns" in element && element.columns !== void 0) {
199
- const columns = element.columns;
200
- const validColumns = [1, 2, 3, 4];
201
- if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
202
- errors.push(
203
- `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
204
- );
205
- }
206
- }
229
+ validateContainerProps(element, elementPath, errors);
207
230
  if ("prefillHints" in element && element.prefillHints) {
208
231
  const prefillHints = element.prefillHints;
209
232
  if (Array.isArray(prefillHints)) {
@@ -269,6 +292,18 @@ function escapeHtml(text) {
269
292
  div.textContent = text;
270
293
  return div.innerHTML;
271
294
  }
295
+ function getElementLookupKey(element, state) {
296
+ if (element.key) {
297
+ return element.key;
298
+ }
299
+ const cached = state.syntheticElementIds.get(element);
300
+ if (cached !== void 0) {
301
+ return cached;
302
+ }
303
+ const id = `fb-synthetic-${state.syntheticElementIdCounter++}`;
304
+ state.syntheticElementIds.set(element, id);
305
+ return id;
306
+ }
272
307
  function pathJoin(base, key) {
273
308
  return base ? `${base}.${key}` : key;
274
309
  }
@@ -829,8 +864,12 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
829
864
  const textareaWrapper = document.createElement("div");
830
865
  textareaWrapper.style.cssText = "position: relative;";
831
866
  const textareaInput = document.createElement("textarea");
832
- 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";
833
- textareaInput.style.cssText = "padding-bottom: 24px;";
867
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
868
+ textareaInput.style.cssText = `
869
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
870
+ font-size: var(--fb-font-size);
871
+ font-family: var(--fb-font-family);
872
+ `;
834
873
  textareaInput.name = pathKey;
835
874
  textareaInput.placeholder = element.placeholder || "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
836
875
  textareaInput.rows = element.rows || 4;
@@ -883,8 +922,12 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
883
922
  const textareaContainer = document.createElement("div");
884
923
  textareaContainer.style.cssText = "position: relative;";
885
924
  const textareaInput = document.createElement("textarea");
886
- 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";
887
- textareaInput.style.cssText = "padding-bottom: 24px;";
925
+ textareaInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
926
+ textareaInput.style.cssText = `
927
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x) 24px var(--fb-input-padding-x);
928
+ font-size: var(--fb-font-size);
929
+ font-family: var(--fb-font-family);
930
+ `;
888
931
  textareaInput.placeholder = element.placeholder || t("placeholderText", state);
889
932
  textareaInput.rows = element.rows || 4;
890
933
  textareaInput.value = value;
@@ -1070,8 +1113,14 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1070
1113
  inputWrapper.style.cssText = "position: relative;";
1071
1114
  const numberInput = document.createElement("input");
1072
1115
  numberInput.type = "number";
1073
- 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";
1074
- numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
1116
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1117
+ numberInput.style.cssText = `
1118
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1119
+ font-size: var(--fb-font-size);
1120
+ font-family: var(--fb-font-family);
1121
+ width: 100%;
1122
+ box-sizing: border-box;
1123
+ `;
1075
1124
  numberInput.name = pathKey;
1076
1125
  numberInput.placeholder = element.placeholder || "0";
1077
1126
  if (element.min !== void 0) numberInput.min = element.min.toString();
@@ -1124,8 +1173,14 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1124
1173
  inputContainer.style.cssText = "position: relative; flex: 1;";
1125
1174
  const numberInput = document.createElement("input");
1126
1175
  numberInput.type = "number";
1127
- 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";
1128
- numberInput.style.cssText = "padding-right: 60px; width: 100%; box-sizing: border-box;";
1176
+ numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1177
+ numberInput.style.cssText = `
1178
+ padding: var(--fb-input-padding-y) 60px var(--fb-input-padding-y) var(--fb-input-padding-x);
1179
+ font-size: var(--fb-font-size);
1180
+ font-family: var(--fb-font-family);
1181
+ width: 100%;
1182
+ box-sizing: border-box;
1183
+ `;
1129
1184
  numberInput.placeholder = element.placeholder || "0";
1130
1185
  if (element.min !== void 0) numberInput.min = element.min.toString();
1131
1186
  if (element.max !== void 0) numberInput.max = element.max.toString();
@@ -1399,7 +1454,12 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1399
1454
  const state = ctx.state;
1400
1455
  const readonly = isElementReadonly(element, state, ctx);
1401
1456
  const selectInput = document.createElement("select");
1402
- 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";
1457
+ selectInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1458
+ selectInput.style.cssText = `
1459
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1460
+ font-size: var(--fb-font-size);
1461
+ font-family: var(--fb-font-family);
1462
+ `;
1403
1463
  selectInput.name = pathKey;
1404
1464
  selectInput.disabled = readonly;
1405
1465
  (element.options || []).forEach((option) => {
@@ -1452,7 +1512,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1452
1512
  const itemWrapper = document.createElement("div");
1453
1513
  itemWrapper.className = "multiple-select-item flex items-center gap-2";
1454
1514
  const selectInput = document.createElement("select");
1455
- 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";
1515
+ selectInput.className = "flex-1 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1516
+ selectInput.style.cssText = `
1517
+ padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
1518
+ font-size: var(--fb-font-size);
1519
+ font-family: var(--fb-font-family);
1520
+ `;
1456
1521
  selectInput.disabled = readonly;
1457
1522
  (element.options || []).forEach((option) => {
1458
1523
  const optionElement = document.createElement("option");
@@ -2203,7 +2268,13 @@ function ensureFileStyles() {
2203
2268
  style.textContent = `
2204
2269
  @keyframes fb-spin { to { transform: rotate(360deg); } }
2205
2270
 
2206
- /* Spinner used during single-file and multi-file upload */
2271
+ /* \u2500\u2500\u2500 Checker background utility \u2500\u2500\u2500 */
2272
+ /* Neutral diagonal-stripe background for image previews (never crops) */
2273
+ .fb-checker {
2274
+ background-image: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
2275
+ }
2276
+
2277
+ /* \u2500\u2500\u2500 Spinner \u2500\u2500\u2500 */
2207
2278
  .fb-spinner {
2208
2279
  width: 36px;
2209
2280
  height: 36px;
@@ -2214,207 +2285,271 @@ function ensureFileStyles() {
2214
2285
  flex-shrink: 0;
2215
2286
  }
2216
2287
 
2217
- /* Base tile: fixed 160\xD7160 square, theme-aware background */
2218
- .fb-tile {
2219
- width: var(--fb-tile-size, 160px);
2220
- height: var(--fb-tile-size, 160px);
2221
- flex-shrink: 0;
2222
- position: relative;
2288
+ /* \u2500\u2500\u2500 Wide single-file add tile (empty state) \u2500\u2500\u2500 */
2289
+ .fb-wide-tile {
2290
+ width: 100%;
2291
+ border-radius: 0.75rem;
2292
+ border: 1px dashed #60a5fa;
2293
+ background: rgba(239,246,255,0.5);
2294
+ display: flex;
2223
2295
  overflow: hidden;
2224
- border-radius: var(--fb-border-radius, 0.5rem);
2225
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2296
+ height: 180px;
2297
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2298
+ cursor: pointer;
2226
2299
  }
2227
-
2228
- /* Uploaded resource tile \u2014 adds a visible border */
2229
- .fb-tile-resource {
2230
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2300
+ .fb-wide-tile:hover {
2301
+ background: #eff6ff;
2231
2302
  }
2232
-
2233
- /* Uploading placeholder tile \u2014 dashed border, uploading indicator */
2234
- .fb-tile-uploading {
2235
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2303
+ .fb-wide-tile.fb-drag-over {
2304
+ border-color: #3b82f6;
2305
+ border-width: 2px;
2306
+ background: #eff6ff;
2307
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2236
2308
  }
2237
2309
 
2238
- /* "+" add-more tile */
2239
- .fb-tile-add {
2240
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2310
+ /* Upload zone inside wide tile */
2311
+ .fb-wide-tile-upload {
2312
+ flex: 1;
2241
2313
  display: flex;
2314
+ flex-direction: column;
2242
2315
  align-items: center;
2243
2316
  justify-content: center;
2317
+ gap: 8px;
2318
+ color: #2563eb;
2319
+ padding: 16px;
2320
+ transition: background 150ms;
2244
2321
  cursor: pointer;
2245
- font-size: 32px;
2246
- color: var(--fb-file-upload-text-color, #9ca3af);
2247
- transition:
2248
- border-color var(--fb-transition-duration, 200ms),
2249
- color var(--fb-transition-duration, 200ms);
2322
+ background: transparent;
2323
+ border: none;
2324
+ font-family: inherit;
2250
2325
  }
2251
- .fb-tile-add:hover {
2252
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2253
- color: var(--fb-text-color, #1f2937);
2326
+ .fb-wide-tile-upload:hover {
2327
+ background: rgba(191,219,254,0.25);
2254
2328
  }
2255
2329
 
2256
- /* Count chip shown when at maxCount */
2257
- .fb-tile-counter {
2258
- font-size: 11px;
2259
- color: var(--fb-text-secondary-color, #6b7280);
2260
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2261
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2262
- border-radius: 4px;
2263
- padding: 2px 6px;
2264
- align-self: flex-end;
2265
- margin-bottom: 4px;
2330
+ /* Vertical dashed divider between upload and library zones */
2331
+ .fb-wide-tile-divider {
2332
+ width: 1px;
2333
+ margin: 16px 0;
2334
+ border-left: 1px dashed rgba(96,165,250,0.5);
2335
+ background: transparent;
2336
+ flex-shrink: 0;
2266
2337
  }
2267
2338
 
2268
- /* Empty-state dropzone */
2269
- .fb-file-dropzone {
2270
- width: 100%;
2271
- height: 128px;
2272
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2273
- border-radius: var(--fb-border-radius, 0.5rem);
2339
+ /* Library zone inside wide tile */
2340
+ .fb-wide-tile-library {
2341
+ width: 176px;
2342
+ flex-shrink: 0;
2274
2343
  display: flex;
2275
2344
  flex-direction: column;
2276
2345
  align-items: center;
2277
2346
  justify-content: center;
2278
- gap: 4px;
2347
+ gap: 8px;
2348
+ color: #2563eb;
2349
+ padding: 12px;
2350
+ transition: background 150ms;
2279
2351
  cursor: pointer;
2280
- transition:
2281
- border-color var(--fb-transition-duration, 200ms),
2282
- background var(--fb-transition-duration, 200ms);
2352
+ background: transparent;
2353
+ border: none;
2354
+ font-family: inherit;
2283
2355
  }
2284
- .fb-file-dropzone:hover {
2285
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2286
- background: var(--fb-background-hover-color, #f9fafb);
2356
+ .fb-wide-tile-library:hover {
2357
+ background: rgba(191,219,254,0.25);
2287
2358
  }
2288
2359
 
2289
- /* Inline text inside tiles */
2290
- .fb-tile-label {
2291
- font-size: 9px;
2292
- color: var(--fb-text-secondary-color, #6b7280);
2293
- text-align: center;
2294
- overflow: hidden;
2295
- word-break: break-all;
2296
- max-height: 28px;
2360
+ /* \u2500\u2500\u2500 Multi-file outer grid container \u2500\u2500\u2500 */
2361
+ .fb-multi-outer {
2362
+ border-radius: 0.75rem;
2363
+ border: 1px dashed #cbd5e1;
2364
+ background: rgba(248,250,252,0.4);
2365
+ padding: 12px;
2366
+ transition: border-color 150ms, background 150ms, box-shadow 150ms;
2367
+ }
2368
+ .fb-multi-outer.fb-drag-over {
2369
+ border-width: 2px;
2370
+ border-color: #3b82f6;
2371
+ background: rgba(239,246,255,0.4);
2372
+ box-shadow: 0 0 0 4px rgba(191,219,254,0.7);
2297
2373
  }
2298
- .fb-tile-uploading-text {
2299
- font-size: 8px;
2300
- color: var(--fb-file-upload-text-color, #9ca3af);
2374
+
2375
+ /* With files present: white solid border */
2376
+ .fb-multi-outer.fb-multi-has-files {
2377
+ border-style: solid;
2378
+ border-color: #e2e8f0;
2379
+ background: #fff;
2301
2380
  }
2302
- .fb-tile-hint {
2303
- font-size: 11px;
2304
- color: var(--fb-file-upload-text-color, #9ca3af);
2305
- margin-top: 4px;
2381
+
2382
+ /* The CSS grid inside */
2383
+ .fb-multi-grid {
2384
+ display: grid;
2385
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2386
+ gap: 10px;
2306
2387
  }
2307
- .fb-tile-empty-text {
2308
- font-size: 12px;
2309
- color: var(--fb-text-secondary-color, #6b7280);
2310
- padding: 4px 0;
2388
+
2389
+ /* \u2500\u2500\u2500 Multi square add-tile (combined upload + library) \u2500\u2500\u2500 */
2390
+ .fb-multi-add-tile {
2391
+ aspect-ratio: 1 / 1;
2392
+ border-radius: 0.5rem;
2393
+ border: 1px dashed #60a5fa;
2394
+ background: rgba(239,246,255,0.5);
2395
+ display: flex;
2396
+ flex-direction: column;
2397
+ overflow: hidden;
2398
+ transition: background 150ms;
2311
2399
  }
2312
- .fb-dropzone-primary-text {
2313
- font-size: 13px;
2314
- color: var(--fb-text-secondary-color, #6b7280);
2400
+ .fb-multi-add-tile:hover {
2401
+ background: #eff6ff;
2315
2402
  }
2316
- .fb-dropzone-hint-text {
2317
- font-size: 11px;
2318
- color: var(--fb-file-upload-text-color, #9ca3af);
2403
+ .fb-multi-add-tile.fb-drag-over-tile {
2404
+ border-width: 2px;
2405
+ border-color: #3b82f6;
2406
+ background: rgba(255,255,255,0.8);
2319
2407
  }
2320
2408
 
2321
- /* Hover overlay + X-button on resource tiles */
2322
- .fb-tile-overlay {
2323
- position: absolute;
2324
- inset: 0;
2325
- background: transparent;
2326
- transition: background var(--fb-transition-duration, 200ms);
2327
- display: flex;
2328
- align-items: flex-start;
2329
- justify-content: flex-end;
2330
- }
2331
- .fb-tile-resource:hover .fb-tile-overlay {
2332
- background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.4));
2333
- }
2334
- .fb-tile-x-btn {
2335
- margin: 3px;
2336
- width: 18px;
2337
- height: 18px;
2338
- background: var(--fb-error-color, #ef4444);
2339
- color: var(--fb-file-bg-color, #fff);
2340
- border: none;
2341
- border-radius: 50%;
2342
- font-size: 11px;
2343
- line-height: 1;
2344
- cursor: pointer;
2409
+ /* Upload half of add-tile */
2410
+ .fb-multi-add-upload {
2411
+ flex: 1;
2345
2412
  display: flex;
2413
+ flex-direction: column;
2346
2414
  align-items: center;
2347
2415
  justify-content: center;
2348
- opacity: 0;
2349
- transition: opacity var(--fb-transition-duration, 200ms);
2416
+ gap: 4px;
2417
+ color: #2563eb;
2418
+ cursor: pointer;
2419
+ background: transparent;
2420
+ border: none;
2421
+ font-family: inherit;
2422
+ width: 100%;
2423
+ transition: background 150ms;
2350
2424
  }
2351
- .fb-tile-resource:hover .fb-tile-x-btn {
2352
- opacity: 1;
2425
+ .fb-multi-add-upload:hover {
2426
+ background: rgba(191,219,254,0.35);
2353
2427
  }
2354
2428
 
2355
- /* Video play button overlay (readonly tiles with video thumbnails) */
2356
- .fb-video-overlay {
2357
- position: absolute;
2358
- inset: 0;
2359
- display: flex;
2360
- align-items: center;
2361
- justify-content: center;
2362
- background: var(--fb-tile-hover-overlay-color, rgba(0,0,0,0.25));
2429
+ /* Horizontal dashed divider inside add-tile */
2430
+ .fb-multi-add-divider {
2431
+ border-top: 1px dashed rgba(96,165,250,0.5);
2432
+ margin: 0;
2433
+ flex-shrink: 0;
2363
2434
  }
2364
- .fb-play-btn {
2365
- background: var(--fb-file-bg-color, rgba(255,255,255,0.9));
2366
- border-radius: 50%;
2435
+
2436
+ /* Library strip at bottom of add-tile */
2437
+ .fb-multi-add-library {
2438
+ padding: 6px 0;
2367
2439
  display: flex;
2368
2440
  align-items: center;
2369
2441
  justify-content: center;
2442
+ gap: 4px;
2443
+ color: #2563eb;
2444
+ font-size: 11px;
2445
+ font-weight: 500;
2446
+ cursor: pointer;
2447
+ background: transparent;
2448
+ border: none;
2449
+ font-family: inherit;
2450
+ width: 100%;
2451
+ transition: background 150ms;
2452
+ flex-shrink: 0;
2453
+ }
2454
+ .fb-multi-add-library:hover {
2455
+ background: rgba(191,219,254,0.35);
2370
2456
  }
2371
2457
 
2372
- /* Edit-mode local video preview wrapper */
2373
- .fb-video-preview-wrap {
2458
+ /* \u2500\u2500\u2500 Capacity placeholder squares \u2500\u2500\u2500 */
2459
+ .fb-multi-placeholder {
2460
+ aspect-ratio: 1 / 1;
2461
+ border-radius: 0.5rem;
2462
+ border: 1px solid #e2e8f0;
2463
+ }
2464
+ .fb-multi-placeholder.fb-drag-over {
2465
+ border-width: 2px;
2466
+ border-style: dashed;
2467
+ border-color: #93c5fd;
2468
+ background: rgba(219,234,254,0.6);
2469
+ }
2470
+
2471
+ /* \u2500\u2500\u2500 Filled preview tile \u2500\u2500\u2500 */
2472
+ .fb-preview-tile {
2473
+ aspect-ratio: 1 / 1;
2474
+ border-radius: 0.5rem;
2475
+ border: 1px solid #e2e8f0;
2476
+ overflow: hidden;
2374
2477
  position: relative;
2478
+ cursor: pointer;
2479
+ }
2480
+ .fb-preview-tile img {
2375
2481
  width: 100%;
2376
2482
  height: 100%;
2483
+ object-fit: contain;
2484
+ display: block;
2377
2485
  }
2378
2486
 
2379
- /* Hover overlay for edit-mode local video (Remove / Change buttons) */
2380
- .fb-video-btn-overlay {
2381
- position: absolute;
2382
- top: 8px;
2383
- right: 8px;
2384
- z-index: 10;
2487
+ /* \u2500\u2500\u2500 Uploading placeholder tile \u2500\u2500\u2500 */
2488
+ .fb-uploading-tile {
2489
+ aspect-ratio: 1 / 1;
2490
+ border-radius: 0.5rem;
2491
+ border: 2px dashed #d1d5db;
2385
2492
  display: flex;
2386
- gap: 4px;
2387
- opacity: 0;
2388
- transition: opacity var(--fb-transition-duration, 200ms);
2389
- pointer-events: none;
2493
+ flex-direction: column;
2494
+ align-items: center;
2495
+ justify-content: center;
2496
+ gap: 6px;
2497
+ padding: 6px;
2390
2498
  }
2391
- .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2392
- opacity: 1;
2393
- pointer-events: auto;
2499
+
2500
+ /* \u2500\u2500\u2500 Meta line below multi grid \u2500\u2500\u2500 */
2501
+ .fb-meta-line {
2502
+ margin-top: 10px;
2503
+ display: flex;
2504
+ align-items: center;
2505
+ justify-content: space-between;
2506
+ gap: 8px;
2507
+ flex-wrap: wrap;
2394
2508
  }
2395
- .fb-video-btn {
2396
- border: none;
2397
- border-radius: var(--fb-border-radius, 4px);
2398
- font-size: 11px;
2399
- padding: 4px 8px;
2400
- cursor: pointer;
2401
- color: #fff;
2402
- line-height: 1.2;
2509
+ .fb-meta-text {
2510
+ font-size: 12px;
2511
+ color: #94a3b8;
2512
+ display: flex;
2513
+ align-items: center;
2514
+ gap: 8px;
2515
+ flex-wrap: wrap;
2403
2516
  }
2404
- .fb-video-btn-delete {
2405
- background: rgba(220, 38, 38, 0.85);
2517
+ .fb-meta-dot {
2518
+ width: 4px;
2519
+ height: 4px;
2520
+ border-radius: 50%;
2521
+ background: #cbd5e1;
2522
+ flex-shrink: 0;
2523
+ }
2524
+ .fb-meta-mono {
2525
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
2526
+ font-size: 11px;
2527
+ letter-spacing: -0.02em;
2406
2528
  }
2407
- .fb-video-btn-delete:hover {
2408
- background: rgba(185, 28, 28, 0.95);
2529
+ .fb-clear-all-btn {
2530
+ font-size: 12px;
2531
+ color: #94a3b8;
2532
+ background: none;
2533
+ border: none;
2534
+ cursor: pointer;
2535
+ padding: 0;
2536
+ font-family: inherit;
2537
+ transition: color 150ms;
2538
+ white-space: nowrap;
2539
+ flex-shrink: 0;
2409
2540
  }
2410
- .fb-video-btn-change {
2411
- background: rgba(31, 41, 55, 0.85);
2541
+ .fb-clear-all-btn:hover {
2542
+ color: #dc2626;
2412
2543
  }
2413
- .fb-video-btn-change:hover {
2414
- background: rgba(17, 24, 39, 0.95);
2544
+
2545
+ /* \u2500\u2500\u2500 Empty text (readonly) \u2500\u2500\u2500 */
2546
+ .fb-tile-empty-text {
2547
+ font-size: 11px;
2548
+ color: var(--fb-text-secondary-color, #6b7280);
2549
+ padding: 4px 0;
2415
2550
  }
2416
2551
 
2417
- /* Tile action icon buttons (download / open / remove) \u2014 shown on tile hover */
2552
+ /* \u2500\u2500\u2500 Tile action buttons (for zoom popup, compat) \u2500\u2500\u2500 */
2418
2553
  .fb-tile-actions {
2419
2554
  position: absolute;
2420
2555
  top: 3px;
@@ -2426,37 +2561,35 @@ function ensureFileStyles() {
2426
2561
  transition: opacity var(--fb-transition-duration, 200ms);
2427
2562
  z-index: 10;
2428
2563
  }
2429
- .fb-tile-resource:hover .fb-tile-actions {
2564
+ .fb-preview-tile:hover .fb-tile-actions {
2430
2565
  opacity: 1;
2431
2566
  }
2432
2567
  .fb-tile-action-btn {
2433
- width: 28px;
2434
- height: 28px;
2568
+ width: 24px;
2569
+ height: 24px;
2435
2570
  display: flex;
2436
2571
  align-items: center;
2437
2572
  justify-content: center;
2438
- border: none;
2439
- border-radius: 50%;
2573
+ border: 1px solid rgba(15,23,42,0.08);
2574
+ border-radius: 0.375rem;
2440
2575
  cursor: pointer;
2441
- background: rgba(31, 41, 55, 0.75);
2442
- color: #fff;
2576
+ background: rgba(255,255,255,0.92);
2577
+ color: #374151;
2443
2578
  padding: 0;
2444
2579
  flex-shrink: 0;
2445
- transition:
2446
- background var(--fb-transition-duration, 200ms),
2447
- opacity var(--fb-transition-duration, 200ms);
2580
+ box-shadow: 0 1px 2px rgba(0,0,0,0.06);
2581
+ transition: background var(--fb-transition-duration, 200ms),
2582
+ color var(--fb-transition-duration, 200ms);
2448
2583
  }
2449
2584
  .fb-tile-action-btn:hover {
2450
- background: rgba(17, 24, 39, 0.95);
2451
- }
2452
- .fb-tile-action-remove {
2453
- background: rgba(220, 38, 38, 0.8);
2585
+ background: #ffffff;
2586
+ color: #0f172a;
2454
2587
  }
2455
2588
  .fb-tile-action-remove:hover {
2456
- background: rgba(185, 28, 28, 0.95);
2589
+ color: #dc2626;
2457
2590
  }
2458
2591
 
2459
- /* Actions row inside zoom popup \u2014 always visible while popup is shown */
2592
+ /* Zoom popup action buttons always visible */
2460
2593
  .fb-tile-zoom-preview .fb-tile-actions {
2461
2594
  position: absolute;
2462
2595
  top: 6px;
@@ -2465,116 +2598,145 @@ function ensureFileStyles() {
2465
2598
  z-index: 10000;
2466
2599
  }
2467
2600
 
2468
- /* Two-card empty-state layout (upload card + library card) */
2469
- .fb-file-card-row {
2470
- display: flex;
2471
- gap: 8px;
2472
- align-items: stretch;
2601
+ /* \u2500\u2500\u2500 Hover zoom preview popup \u2500\u2500\u2500 */
2602
+ .fb-tile-zoom-preview {
2603
+ position: fixed;
2604
+ z-index: 9999;
2605
+ background: var(--fb-background-color, #fff);
2606
+ border: 1px solid #e2e8f0;
2607
+ border-radius: 0.5rem;
2608
+ box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2609
+ padding: 4px;
2610
+ width: 350px;
2611
+ height: 350px;
2612
+ pointer-events: none;
2613
+ opacity: 0;
2614
+ transition: opacity 150ms ease;
2473
2615
  }
2474
- .fb-file-card-row .fb-file-dropzone,
2475
- .fb-file-card-row .fb-file-library-card {
2476
- flex: 1;
2477
- min-width: 0;
2616
+ .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2617
+ opacity: 1;
2618
+ }
2619
+ .fb-tile-zoom-preview-img {
2620
+ width: 100%;
2621
+ height: 100%;
2622
+ object-fit: contain;
2623
+ display: block;
2624
+ border-radius: calc(0.5rem - 2px);
2478
2625
  }
2479
2626
 
2480
- /* Library picker card \u2014 mirrors .fb-file-dropzone styling */
2481
- .fb-file-library-card {
2482
- height: 128px;
2483
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2484
- border-radius: var(--fb-border-radius, 0.5rem);
2627
+ /* \u2500\u2500\u2500 Single-file uploading state \u2500\u2500\u2500 */
2628
+ .fb-single-uploading {
2629
+ height: 180px;
2630
+ border-radius: 0.75rem;
2631
+ border: 1px dashed #60a5fa;
2632
+ background: rgba(239,246,255,0.5);
2485
2633
  display: flex;
2486
2634
  flex-direction: column;
2487
2635
  align-items: center;
2488
2636
  justify-content: center;
2489
- gap: 4px;
2490
- cursor: pointer;
2491
- background: none;
2492
- padding: 0;
2493
- transition:
2494
- border-color var(--fb-transition-duration, 200ms),
2495
- background var(--fb-transition-duration, 200ms);
2496
- width: 100%;
2637
+ gap: 8px;
2497
2638
  }
2498
- .fb-file-library-card:hover,
2499
- .fb-file-library-card:focus-visible {
2500
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2501
- background: var(--fb-background-hover-color, #f9fafb);
2502
- outline: none;
2639
+
2640
+ /* \u2500\u2500\u2500 Video overlays \u2500\u2500\u2500 */
2641
+ .fb-video-overlay {
2642
+ position: absolute;
2643
+ inset: 0;
2644
+ display: flex;
2645
+ align-items: center;
2646
+ justify-content: center;
2647
+ background: rgba(0,0,0,0.25);
2503
2648
  }
2504
- .fb-file-library-card-icon {
2505
- font-size: 24px;
2506
- line-height: 1;
2507
- flex-shrink: 0;
2649
+ .fb-play-btn {
2650
+ background: rgba(255,255,255,0.9);
2651
+ border-radius: 50%;
2652
+ display: flex;
2653
+ align-items: center;
2654
+ justify-content: center;
2508
2655
  }
2509
- .fb-file-library-card-label {
2510
- font-size: 13px;
2511
- color: var(--fb-text-secondary-color, #6b7280);
2656
+ .fb-video-preview-wrap {
2657
+ position: relative;
2658
+ width: 100%;
2659
+ height: 100%;
2660
+ }
2661
+ .fb-video-btn-overlay {
2662
+ position: absolute;
2663
+ top: 8px;
2664
+ right: 8px;
2665
+ z-index: 10;
2666
+ display: flex;
2667
+ gap: 4px;
2668
+ opacity: 0;
2669
+ transition: opacity 150ms;
2670
+ pointer-events: none;
2512
2671
  }
2513
- .fb-file-library-card-hint {
2672
+ .fb-video-preview-wrap:hover .fb-video-btn-overlay {
2673
+ opacity: 1;
2674
+ pointer-events: auto;
2675
+ }
2676
+ .fb-video-btn {
2677
+ border: none;
2678
+ border-radius: 4px;
2514
2679
  font-size: 11px;
2515
- color: var(--fb-file-upload-text-color, #9ca3af);
2680
+ padding: 4px 8px;
2681
+ cursor: pointer;
2682
+ color: #fff;
2683
+ line-height: 1.2;
2516
2684
  }
2685
+ .fb-video-btn-delete { background: rgba(220,38,38,0.85); }
2686
+ .fb-video-btn-delete:hover { background: rgba(185,28,28,0.95); }
2687
+ .fb-video-btn-change { background: rgba(31,41,55,0.85); }
2688
+ .fb-video-btn-change:hover { background: rgba(17,24,39,0.95); }
2517
2689
 
2518
- /* Library "\u{1F4DA}" add-tile \u2014 same size/style as the "+" add tile */
2519
- .fb-tile-add-library {
2520
- border: 2px dashed var(--fb-file-upload-border-color, #d1d5db);
2521
- display: flex;
2522
- align-items: center;
2523
- justify-content: center;
2524
- cursor: pointer;
2525
- font-size: 24px;
2526
- color: var(--fb-file-upload-text-color, #9ca3af);
2527
- transition:
2528
- border-color var(--fb-transition-duration, 200ms),
2529
- color var(--fb-transition-duration, 200ms);
2530
- background: none;
2531
- padding: 0;
2532
- width: var(--fb-tile-size, 160px);
2533
- height: var(--fb-tile-size, 160px);
2534
- flex-shrink: 0;
2535
- position: relative;
2690
+ /* \u2500\u2500\u2500 Readonly readonly tile \u2500\u2500\u2500 */
2691
+ .fb-readonly-tile {
2692
+ aspect-ratio: 1 / 1;
2693
+ border-radius: 0.5rem;
2694
+ border: 1px solid #e2e8f0;
2536
2695
  overflow: hidden;
2537
- border-radius: var(--fb-border-radius, 0.5rem);
2696
+ position: relative;
2697
+ cursor: pointer;
2538
2698
  }
2539
- .fb-tile-add-library:hover,
2540
- .fb-tile-add-library:focus-visible {
2541
- border-color: var(--fb-file-upload-hover-border-color, #3b82f6);
2542
- color: var(--fb-text-color, #1f2937);
2543
- outline: none;
2699
+ .fb-readonly-tile img {
2700
+ width: 100%;
2701
+ height: 100%;
2702
+ object-fit: contain;
2703
+ display: block;
2544
2704
  }
2545
-
2546
- /* Hover zoom preview popup for image tiles \u2014 appended to document.body (fixed) */
2547
- .fb-tile-zoom-preview {
2548
- position: fixed;
2549
- z-index: 9999;
2550
- background: var(--fb-background-color, #fff);
2551
- border: 1px solid var(--fb-file-upload-border-color, #d1d5db);
2552
- border-radius: var(--fb-border-radius, 0.5rem);
2553
- box-shadow: 0 4px 16px rgba(0,0,0,0.18);
2554
- padding: 4px;
2555
- width: 350px;
2556
- height: 350px;
2557
- pointer-events: none;
2705
+ .fb-readonly-tile .fb-tile-actions {
2558
2706
  opacity: 0;
2559
- transition: opacity 150ms ease;
2560
2707
  }
2561
- .fb-tile-zoom-preview.fb-tile-zoom-preview--visible {
2708
+ .fb-readonly-tile:hover .fb-tile-actions {
2562
2709
  opacity: 1;
2563
2710
  }
2564
- .fb-tile-zoom-preview-img {
2711
+
2712
+ /* \u2500\u2500\u2500 Readonly single-file filled \u2500\u2500\u2500 */
2713
+ .fb-single-readonly-filled {
2714
+ position: relative;
2715
+ border-radius: 0.75rem;
2716
+ border: 1px solid #e2e8f0;
2717
+ overflow: hidden;
2718
+ height: 220px;
2719
+ display: block;
2720
+ cursor: pointer;
2721
+ }
2722
+ .fb-single-readonly-filled img {
2565
2723
  width: 100%;
2566
2724
  height: 100%;
2567
2725
  object-fit: contain;
2568
2726
  display: block;
2569
- background: var(--fb-file-upload-bg-color, #f3f4f6);
2570
- border-radius: calc(var(--fb-border-radius, 0.5rem) - 2px);
2727
+ }
2728
+
2729
+ /* \u2500\u2500\u2500 Readonly multi grid \u2500\u2500\u2500 */
2730
+ .fb-multi-readonly-grid {
2731
+ display: grid;
2732
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
2733
+ gap: 10px;
2571
2734
  }
2572
2735
  `;
2573
2736
  document.head.appendChild(style);
2574
2737
  }
2575
2738
 
2576
2739
  // src/components/file/dom.ts
2577
- var TILE_SIZE = "160px";
2578
2740
  function createFileTile() {
2579
2741
  ensureFileStyles();
2580
2742
  const tile = document.createElement("div");
@@ -2583,7 +2745,7 @@ function createFileTile() {
2583
2745
  }
2584
2746
  function showFileError(container, message) {
2585
2747
  var _a, _b;
2586
- const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2748
+ const existing = (_a = container.closest("[data-files-wrapper]")) == null ? void 0 : _a.querySelector(".file-error-message");
2587
2749
  if (existing) existing.remove();
2588
2750
  const errorEl = document.createElement("div");
2589
2751
  errorEl.className = "file-error-message error-message";
@@ -2593,11 +2755,11 @@ function showFileError(container, message) {
2593
2755
  margin-top: 0.25rem;
2594
2756
  `;
2595
2757
  errorEl.textContent = message;
2596
- (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
2758
+ (_b = container.closest("[data-files-wrapper]")) == null ? void 0 : _b.appendChild(errorEl);
2597
2759
  }
2598
2760
  function clearFileError(container) {
2599
2761
  var _a;
2600
- const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2762
+ const existing = (_a = container.closest("[data-files-wrapper]")) == null ? void 0 : _a.querySelector(".file-error-message");
2601
2763
  if (existing) existing.remove();
2602
2764
  }
2603
2765
  function addDeleteButton(container, state, onDelete) {
@@ -2615,14 +2777,6 @@ function addDeleteButton(container, state, onDelete) {
2615
2777
  overlay.appendChild(deleteBtn);
2616
2778
  container.appendChild(overlay);
2617
2779
  }
2618
- function findFilePicker(container) {
2619
- var _a;
2620
- let el = container.parentElement;
2621
- while (el && !el.dataset.filesWrapper) {
2622
- el = el.parentElement;
2623
- }
2624
- return (_a = el == null ? void 0 : el.querySelector('input[type="file"]')) != null ? _a : null;
2625
- }
2626
2780
  function createUploadingTile(fileName, state) {
2627
2781
  ensureFileStyles();
2628
2782
  const tile = createFileTile();
@@ -2638,10 +2792,14 @@ function createUploadingTile(fileName, state) {
2638
2792
  return tile;
2639
2793
  }
2640
2794
  function ensureTilesWrap(list) {
2795
+ var _a, _b, _c;
2796
+ const existingGrid = list.querySelector(".fb-multi-grid");
2797
+ if (existingGrid) return existingGrid;
2641
2798
  const existing = list.querySelector(".fb-tiles-wrap");
2642
2799
  if (existing) return existing;
2643
- const dropzone = list.querySelector(".fb-file-dropzone");
2644
- if (dropzone) dropzone.remove();
2800
+ (_a = list.querySelector(".fb-file-dropzone")) == null ? void 0 : _a.remove();
2801
+ (_b = list.querySelector(".fb-wide-tile")) == null ? void 0 : _b.remove();
2802
+ (_c = list.querySelector(".fb-multi-outer")) == null ? void 0 : _c.remove();
2645
2803
  const tilesWrap = document.createElement("div");
2646
2804
  tilesWrap.className = "fb-tiles-wrap";
2647
2805
  tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
@@ -2653,7 +2811,7 @@ function ensureTilesWrap(list) {
2653
2811
  return tilesWrap;
2654
2812
  }
2655
2813
  function setEmptyFileContainer(fileContainer, state, hint) {
2656
- const hintHtml = hint ? `<div class="text-xs text-gray-500 mt-1">${escapeHtml(hint)}</div>` : "";
2814
+ const hintHtml = "";
2657
2815
  fileContainer.innerHTML = `
2658
2816
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
2659
2817
  <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
@@ -2696,9 +2854,11 @@ function setupDragAndDrop(element, dropHandler) {
2696
2854
  }
2697
2855
 
2698
2856
  // src/components/file/preview.ts
2699
- 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>`;
2700
- 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>`;
2701
- 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>`;
2857
+ 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>`;
2858
+ 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>`;
2859
+ 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>`;
2860
+ 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>`;
2861
+ 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>`;
2702
2862
  function canDownload(state, meta) {
2703
2863
  return Boolean(
2704
2864
  state.config.downloadFile || state.config.getDownloadUrl || state.config.getThumbnail || (meta == null ? void 0 : meta.file)
@@ -2710,7 +2870,16 @@ function canOpenInTab(state, meta) {
2710
2870
  );
2711
2871
  }
2712
2872
  function createTileActions(options) {
2713
- const { canRemove, removeHandler, state, resourceId, fileName, meta } = options;
2873
+ const {
2874
+ canRemove,
2875
+ removeHandler,
2876
+ state,
2877
+ resourceId,
2878
+ fileName,
2879
+ meta,
2880
+ replaceHandler,
2881
+ libraryHandler
2882
+ } = options;
2714
2883
  const group = document.createElement("div");
2715
2884
  group.className = "fb-tile-actions";
2716
2885
  const makeBtn = (icon, label, cls) => {
@@ -2725,6 +2894,16 @@ function createTileActions(options) {
2725
2894
  });
2726
2895
  return btn;
2727
2896
  };
2897
+ if (replaceHandler) {
2898
+ const replaceBtn = makeBtn(ICON_REPLACE, t("replaceFile", state), "fb-tile-action-replace");
2899
+ replaceBtn.addEventListener("click", () => replaceHandler());
2900
+ group.appendChild(replaceBtn);
2901
+ }
2902
+ if (libraryHandler) {
2903
+ const libBtn = makeBtn(ICON_LIBRARY, t("fromLibrary", state), "fb-tile-action-library");
2904
+ libBtn.addEventListener("click", () => libraryHandler());
2905
+ group.appendChild(libBtn);
2906
+ }
2728
2907
  if (canDownload(state, meta)) {
2729
2908
  const dlBtn = makeBtn(ICON_DOWNLOAD, t("downloadFile", state), "fb-tile-action-download");
2730
2909
  dlBtn.addEventListener("click", () => {
@@ -2910,8 +3089,7 @@ function attachClonedActionListeners(cloned, original) {
2910
3089
  }
2911
3090
  function renderLocalImagePreview(container, file, fileName, state) {
2912
3091
  const img = document.createElement("img");
2913
- img.className = "w-full h-full object-contain";
2914
- img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
3092
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
2915
3093
  img.alt = fileName || t("previewAlt", state);
2916
3094
  const reader = new FileReader();
2917
3095
  reader.onload = (e) => {
@@ -2934,7 +3112,7 @@ function renderLocalVideoPreview(container, file, videoType, resourceId, state,
2934
3112
  const newContainer = setupDragDropless(container);
2935
3113
  newContainer.innerHTML = `
2936
3114
  <div class="fb-video-preview-wrap">
2937
- <video class="w-full h-full object-contain" controls preload="auto" muted src="${videoUrl}">
3115
+ <video style="width:100%;height:100%;object-fit:contain;" controls preload="auto" muted src="${videoUrl}">
2938
3116
  ${escapeHtml(t("videoNotSupported", state))}
2939
3117
  </video>
2940
3118
  <div class="fb-video-btn-overlay">
@@ -2980,11 +3158,11 @@ function handleVideoDelete(container, resourceId, state, deps) {
2980
3158
  container.onclick = deps.fileUploadHandler;
2981
3159
  }
2982
3160
  container.innerHTML = `
2983
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2984
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3161
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3162
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
2985
3163
  <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"/>
2986
3164
  </svg>
2987
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3165
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
2988
3166
  </div>
2989
3167
  `;
2990
3168
  if (deps == null ? void 0 : deps.setupDrop) {
@@ -3002,11 +3180,11 @@ function renderDeleteButton(container, resourceId, state) {
3002
3180
  hiddenInput.value = "";
3003
3181
  }
3004
3182
  container.innerHTML = `
3005
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
3006
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3183
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3184
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
3007
3185
  <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"/>
3008
3186
  </svg>
3009
- <div class="text-sm text-center">${escapeHtml(t("clickDragText", state))}</div>
3187
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(t("clickDragText", state))}</div>
3010
3188
  </div>
3011
3189
  `;
3012
3190
  });
@@ -3026,7 +3204,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
3026
3204
  deps
3027
3205
  );
3028
3206
  } else {
3029
- 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>`;
3207
+ 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>`;
3030
3208
  }
3031
3209
  if (!isReadonly && !((_c = meta.type) == null ? void 0 : _c.startsWith("video/"))) {
3032
3210
  renderDeleteButton(container, resourceId, state);
@@ -3034,7 +3212,7 @@ async function renderLocalFilePreview(container, meta, fileName, resourceId, isR
3034
3212
  }
3035
3213
  function renderUploadedVideoPreview(container, thumbnailUrl, state) {
3036
3214
  const video = document.createElement("video");
3037
- video.className = "w-full h-full object-contain";
3215
+ video.style.cssText = "width:100%;height:100%;object-fit:contain;";
3038
3216
  video.controls = true;
3039
3217
  video.preload = "metadata";
3040
3218
  video.muted = true;
@@ -3056,8 +3234,7 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3056
3234
  renderUploadedVideoPreview(container, thumbnailUrl, state);
3057
3235
  } else {
3058
3236
  const img = document.createElement("img");
3059
- img.className = "w-full h-full object-contain";
3060
- img.style.background = "var(--fb-file-upload-bg-color,#f3f4f6)";
3237
+ img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3061
3238
  img.alt = fileName || t("previewAlt", state);
3062
3239
  img.src = thumbnailUrl;
3063
3240
  container.appendChild(img);
@@ -3068,11 +3245,11 @@ async function renderUploadedFilePreview(container, resourceId, fileName, meta,
3068
3245
  } catch (error) {
3069
3246
  console.error("Failed to get thumbnail:", error);
3070
3247
  container.innerHTML = `
3071
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
3072
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
3248
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--fb-text-secondary-color,#9ca3af);">
3249
+ <svg style="width:1.5rem;height:1.5rem;margin-bottom:0.5rem;" fill="currentColor" viewBox="0 0 24 24">
3073
3250
  <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"/>
3074
3251
  </svg>
3075
- <div class="text-sm text-center">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3252
+ <div style="font-size:0.875rem;text-align:center;">${escapeHtml(fileName || t("previewUnavailable", state))}</div>
3076
3253
  </div>
3077
3254
  `;
3078
3255
  }
@@ -3245,20 +3422,15 @@ async function renderSingleFileEditTile(fileContainer, resourceId, state, deps)
3245
3422
  fileContainer.appendChild(tile);
3246
3423
  }
3247
3424
  async function fillTileContent(tile, rid, meta, state, actionsEl) {
3248
- var _a, _b, _c;
3425
+ var _a, _b;
3249
3426
  if ((_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("image/")) {
3250
3427
  if (meta.file && meta.file instanceof File) {
3251
3428
  const img = document.createElement("img");
3252
3429
  img.style.cssText = "width:100%;height:100%;object-fit:contain;background:var(--fb-file-upload-bg-color,#f3f4f6);";
3253
3430
  img.alt = meta.name;
3254
- const reader = new FileReader();
3255
- reader.onload = (e) => {
3256
- var _a2;
3257
- img.src = ((_a2 = e.target) == null ? void 0 : _a2.result) || "";
3258
- attachZoomHover(tile, img.src, meta.name, actionsEl != null ? actionsEl : null);
3259
- };
3260
- reader.readAsDataURL(meta.file);
3431
+ img.src = getLocalFileUrl(meta.file);
3261
3432
  tile.appendChild(img);
3433
+ attachZoomHover(tile, img.src, meta.name, actionsEl != null ? actionsEl : null);
3262
3434
  } else if (state.config.getThumbnail) {
3263
3435
  try {
3264
3436
  const url = await state.config.getThumbnail(rid);
@@ -3315,17 +3487,21 @@ async function fillTileContent(tile, rid, meta, state, actionsEl) {
3315
3487
  }
3316
3488
  if (actionsEl) tile.appendChild(actionsEl);
3317
3489
  } else {
3318
- const name = (_c = meta == null ? void 0 : meta.name) != null ? _c : "";
3319
- const hasExtension = name.includes(".");
3320
- const captionHtml = hasExtension ? `<div class="fb-tile-label">${escapeHtml(name.length > 10 ? name.substring(0, 8) + "\u2026" : name)}</div>` : "";
3321
- tile.innerHTML = `
3322
- <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3323
- <div style="font-size:36px;">\u{1F4C1}</div>
3324
- ${captionHtml}
3325
- </div>`;
3326
- if (actionsEl) tile.appendChild(actionsEl);
3490
+ fillDocumentFallback(tile, rid, meta, actionsEl);
3327
3491
  }
3328
3492
  }
3493
+ function fillDocumentFallback(tile, rid, meta, actionsEl) {
3494
+ var _a, _b;
3495
+ const fileName = (_b = (_a = meta == null ? void 0 : meta.name) != null ? _a : rid.split("/").pop()) != null ? _b : "";
3496
+ if (fileName) tile.title = fileName;
3497
+ 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>` : "";
3498
+ tile.innerHTML = `
3499
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:6px;gap:4px;">
3500
+ <div style="font-size:36px;">\u{1F4C1}</div>
3501
+ ${labelHtml}
3502
+ </div>`;
3503
+ if (actionsEl) tile.appendChild(actionsEl);
3504
+ }
3329
3505
  async function forceDownload(resourceId, fileName, state) {
3330
3506
  try {
3331
3507
  let fileUrl = null;
@@ -3390,7 +3566,7 @@ async function uploadSingleFile(file, state) {
3390
3566
  }
3391
3567
  }
3392
3568
  async function handleFileSelect(opts) {
3393
- var _a, _b;
3569
+ var _a, _b, _c;
3394
3570
  const {
3395
3571
  file,
3396
3572
  container,
@@ -3426,6 +3602,10 @@ async function handleFileSelect(opts) {
3426
3602
  return;
3427
3603
  }
3428
3604
  clearFileError(container);
3605
+ const existingHiddenInput = (_a = container.parentElement) == null ? void 0 : _a.querySelector(
3606
+ 'input[type="hidden"]'
3607
+ );
3608
+ const previousRid = (existingHiddenInput == null ? void 0 : existingHiddenInput.value) || null;
3429
3609
  ensureFileStyles();
3430
3610
  container.innerHTML = `
3431
3611
  <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;padding:6px;">
@@ -3436,7 +3616,13 @@ async function handleFileSelect(opts) {
3436
3616
  try {
3437
3617
  rid = await uploadSingleFile(file, state);
3438
3618
  } catch (error) {
3439
- setEmptyFileContainer(container, state);
3619
+ if (previousRid && (deps == null ? void 0 : deps.onAfterUpload)) {
3620
+ deps.onAfterUpload(container, previousRid);
3621
+ } else if (deps == null ? void 0 : deps.onRemove) {
3622
+ deps.onRemove();
3623
+ } else {
3624
+ setEmptyFileContainer(container, state);
3625
+ }
3440
3626
  throw error;
3441
3627
  }
3442
3628
  state.resourceIndex.set(rid, {
@@ -3446,18 +3632,21 @@ async function handleFileSelect(opts) {
3446
3632
  uploadedAt: /* @__PURE__ */ new Date(),
3447
3633
  file
3448
3634
  });
3449
- let hiddenInput = (_a = container.parentElement) == null ? void 0 : _a.querySelector(
3450
- 'input[type="hidden"]'
3451
- );
3635
+ if (previousRid && previousRid !== rid) {
3636
+ releaseLocalFileUrl((_b = state.resourceIndex.get(previousRid)) == null ? void 0 : _b.file);
3637
+ }
3638
+ let hiddenInput = existingHiddenInput;
3452
3639
  if (!hiddenInput) {
3453
3640
  hiddenInput = document.createElement("input");
3454
3641
  hiddenInput.type = "hidden";
3455
3642
  hiddenInput.name = fieldName;
3456
- (_b = container.parentElement) == null ? void 0 : _b.appendChild(hiddenInput);
3643
+ (_c = container.parentElement) == null ? void 0 : _c.appendChild(hiddenInput);
3457
3644
  }
3458
3645
  hiddenInput.value = rid;
3459
3646
  const isVideo = file.type.startsWith("video/");
3460
- if (!isVideo && deps) {
3647
+ if (!isVideo && (deps == null ? void 0 : deps.onAfterUpload)) {
3648
+ deps.onAfterUpload(container, rid);
3649
+ } else if (!isVideo && deps) {
3461
3650
  renderSingleFileEditTile(container, rid, state, deps).catch(console.error);
3462
3651
  } else {
3463
3652
  renderFilePreview(container, rid, state, {
@@ -3518,17 +3707,18 @@ function filterAndSlice(allFiles, currentCount, constraints, state) {
3518
3707
  return { accepted, errorMessage: errorParts.join(" \u2022 ") };
3519
3708
  }
3520
3709
  async function uploadBatch(accepted, resourceIds, listEl, state) {
3710
+ var _a;
3711
+ if (listEl) {
3712
+ const tilesWrap = ensureTilesWrap(listEl);
3713
+ const addTile = (_a = tilesWrap.querySelector(".fb-multi-add-tile-js")) != null ? _a : tilesWrap.querySelector(".fb-tile-add");
3714
+ if (addTile) addTile.style.display = "none";
3715
+ }
3521
3716
  await Promise.all(
3522
3717
  accepted.map(async (file) => {
3523
3718
  const placeholder = createUploadingTile(file.name, state);
3524
3719
  if (listEl) {
3525
3720
  const tilesWrap = ensureTilesWrap(listEl);
3526
- const addTile = tilesWrap.querySelector(".fb-tile-add");
3527
- if (addTile) {
3528
- tilesWrap.insertBefore(placeholder, addTile);
3529
- } else {
3530
- tilesWrap.appendChild(placeholder);
3531
- }
3721
+ tilesWrap.appendChild(placeholder);
3532
3722
  }
3533
3723
  try {
3534
3724
  const rid = await uploadSingleFile(file, state);
@@ -3571,7 +3761,7 @@ function setupFilesDropHandler(filesContainer, resourceIds, state, updateCallbac
3571
3761
  function setupFilesPickerHandler(filesPicker, resourceIds, state, updateCallback, constraints, pathKey, instance) {
3572
3762
  filesPicker.onchange = async () => {
3573
3763
  if (!filesPicker.files) return;
3574
- const wrapperEl = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3764
+ const wrapperEl = filesPicker.closest("[data-files-wrapper]") || filesPicker.parentElement;
3575
3765
  const { accepted, errorMessage } = filterAndSlice(
3576
3766
  Array.from(filesPicker.files),
3577
3767
  resourceIds.length,
@@ -3706,7 +3896,7 @@ async function handleLibraryPickMulti(state, element, wrapper, fieldPath, resour
3706
3896
  }
3707
3897
  }
3708
3898
  async function handleLibraryPickSingle(state, element, container, fileWrapper, pathKey, fieldPath, renderCallback, instance) {
3709
- var _a;
3899
+ var _a, _b;
3710
3900
  if (!state.config.pickExistingFiles) return;
3711
3901
  const allowedExtensions = getAllowedExtensions(element.accept);
3712
3902
  const allowedMimes = getAllowedMimes(element.accept);
@@ -3740,6 +3930,10 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3740
3930
  hiddenInput.name = pathKey;
3741
3931
  fileWrapper.appendChild(hiddenInput);
3742
3932
  }
3933
+ const previousRid = hiddenInput.value || null;
3934
+ if (previousRid && previousRid !== first.resourceId) {
3935
+ releaseLocalFileUrl((_b = state.resourceIndex.get(previousRid)) == null ? void 0 : _b.file);
3936
+ }
3743
3937
  hiddenInput.value = first.resourceId;
3744
3938
  await renderCallback(first.resourceId);
3745
3939
  if (!state.config.readonly) {
@@ -3748,7 +3942,9 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
3748
3942
  }
3749
3943
 
3750
3944
  // src/components/file/render-edit.ts
3751
- function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps) {
3945
+ 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>`;
3946
+ 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>`;
3947
+ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, state, deps, extras) {
3752
3948
  var _a;
3753
3949
  seedInferredResource(initial, state.resourceIndex);
3754
3950
  const meta = state.resourceIndex.get(initial);
@@ -3760,7 +3956,7 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3760
3956
  deps
3761
3957
  }).catch(console.error);
3762
3958
  } else {
3763
- renderSingleFileEditTile(fileContainer, initial, state, deps).catch(console.error);
3959
+ renderSingleFileFilled(fileContainer, initial, state, deps, extras);
3764
3960
  }
3765
3961
  const hiddenInput = document.createElement("input");
3766
3962
  hiddenInput.type = "hidden";
@@ -3768,38 +3964,252 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
3768
3964
  hiddenInput.value = initial;
3769
3965
  fileWrapper.appendChild(hiddenInput);
3770
3966
  }
3771
- 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);">
3772
- <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"/>
3773
- </svg>`;
3774
- function buildEmptyDropzone(state, primaryText, subHint, openPicker) {
3775
- const dropzone = document.createElement("div");
3776
- dropzone.className = "fb-file-dropzone";
3777
- dropzone.innerHTML = `
3778
- ${UPLOAD_SVG}
3779
- <div class="fb-dropzone-primary-text">${escapeHtml(primaryText)}</div>
3780
- ${subHint ? `<div class="fb-dropzone-hint-text">${escapeHtml(subHint)}</div>` : ""}
3781
- `;
3782
- dropzone.onclick = openPicker;
3783
- return dropzone;
3967
+ function buildWideTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false, constraintHint = "") {
3968
+ ensureFileStyles();
3969
+ const outer = document.createElement("div");
3970
+ outer.className = `fb-wide-tile${hasLibrary ? " fb-file-card-row" : ""}${isDragOver ? " fb-drag-over" : ""}`;
3971
+ const uploadBtn = document.createElement("button");
3972
+ uploadBtn.type = "button";
3973
+ uploadBtn.className = "fb-wide-tile-upload fb-file-dropzone";
3974
+ const cloudIcon = document.createElement("span");
3975
+ cloudIcon.style.cssText = "width:36px;height:36px;display:block;flex-shrink:0;";
3976
+ cloudIcon.innerHTML = ICON_CLOUD;
3977
+ uploadBtn.appendChild(cloudIcon);
3978
+ const primaryText = document.createElement("div");
3979
+ primaryText.className = "fb-wide-tile-label";
3980
+ primaryText.style.cssText = "font-size:14px;font-weight:600;";
3981
+ primaryText.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragText", state);
3982
+ uploadBtn.appendChild(primaryText);
3983
+ if (constraintHint) {
3984
+ const hintEl = document.createElement("div");
3985
+ hintEl.style.cssText = "font-size:11px;opacity:0.65;margin-top:2px;";
3986
+ hintEl.textContent = constraintHint;
3987
+ uploadBtn.appendChild(hintEl);
3988
+ }
3989
+ uploadBtn.onclick = (e) => {
3990
+ e.stopPropagation();
3991
+ onUploadClick();
3992
+ };
3993
+ outer.appendChild(uploadBtn);
3994
+ if (hasLibrary && onLibraryClick) {
3995
+ const divider = document.createElement("div");
3996
+ divider.className = "fb-wide-tile-divider";
3997
+ outer.appendChild(divider);
3998
+ const libBtn = document.createElement("button");
3999
+ libBtn.type = "button";
4000
+ libBtn.className = "fb-wide-tile-library fb-file-library-card";
4001
+ const libIcon = document.createElement("span");
4002
+ libIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4003
+ libIcon.innerHTML = ICON_LIBRARY2;
4004
+ libBtn.appendChild(libIcon);
4005
+ const libLabel = document.createElement("div");
4006
+ libLabel.style.cssText = "font-size:13px;font-weight:600;text-align:center;";
4007
+ libLabel.textContent = t("fromLibrary", state);
4008
+ libBtn.appendChild(libLabel);
4009
+ const libHint = document.createElement("div");
4010
+ libHint.style.cssText = "font-size:11px;opacity:0.75;text-align:center;";
4011
+ libHint.textContent = t("libraryHint", state);
4012
+ libBtn.appendChild(libHint);
4013
+ libBtn.onclick = (e) => {
4014
+ e.stopPropagation();
4015
+ onLibraryClick();
4016
+ };
4017
+ outer.appendChild(libBtn);
4018
+ }
4019
+ attachDragOverFeedback(outer, {
4020
+ onEnter: () => {
4021
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
4022
+ if (primaryText2) primaryText2.textContent = t("dropToUpload", state);
4023
+ },
4024
+ onLeave: () => {
4025
+ const primaryText2 = outer.querySelector(".fb-wide-tile-label");
4026
+ if (primaryText2) primaryText2.textContent = t("clickDragText", state);
4027
+ },
4028
+ activeClass: "fb-drag-over"
4029
+ });
4030
+ return outer;
3784
4031
  }
3785
- function buildLibraryButton(variant, state, onClick) {
3786
- const btn = document.createElement("button");
3787
- btn.type = "button";
3788
- btn.className = variant === "card" ? "fb-file-library-card" : "fb-tile fb-tile-add-library";
3789
- if (variant === "card") {
3790
- btn.innerHTML = `
3791
- <span class="fb-file-library-card-icon" aria-hidden="true">\u{1F4DA}</span>
3792
- <span class="fb-file-library-card-label">${escapeHtml(t("fromLibrary", state))}</span>
3793
- <span class="fb-file-library-card-hint">${escapeHtml(t("libraryHint", state))}</span>
3794
- `;
4032
+ function attachDragOverFeedback(el, hooks) {
4033
+ let depth = 0;
4034
+ el.addEventListener("dragover", (e) => {
4035
+ e.preventDefault();
4036
+ });
4037
+ el.addEventListener("dragenter", (e) => {
4038
+ e.preventDefault();
4039
+ depth++;
4040
+ if (depth === 1) {
4041
+ el.classList.add(hooks.activeClass);
4042
+ hooks.onEnter();
4043
+ }
4044
+ });
4045
+ el.addEventListener("dragleave", (e) => {
4046
+ e.preventDefault();
4047
+ depth = Math.max(0, depth - 1);
4048
+ if (depth === 0) {
4049
+ el.classList.remove(hooks.activeClass);
4050
+ hooks.onLeave();
4051
+ }
4052
+ });
4053
+ el.addEventListener("drop", () => {
4054
+ depth = 0;
4055
+ el.classList.remove(hooks.activeClass);
4056
+ hooks.onLeave();
4057
+ });
4058
+ }
4059
+ function renderSingleFileFilled(fileContainer, resourceId, state, deps, extras) {
4060
+ var _a, _b;
4061
+ const meta = state.resourceIndex.get(resourceId);
4062
+ const isVideo = (_a = meta == null ? void 0 : meta.type) == null ? void 0 : _a.startsWith("video/");
4063
+ if (isVideo) {
4064
+ renderFilePreview(fileContainer, resourceId, state, {
4065
+ fileName: (_b = meta == null ? void 0 : meta.name) != null ? _b : "",
4066
+ isReadonly: false,
4067
+ deps
4068
+ }).catch(console.error);
4069
+ return;
4070
+ }
4071
+ ensureFileStyles();
4072
+ const outer = document.createElement("div");
4073
+ outer.className = "fb-multi-outer fb-multi-has-files";
4074
+ const grid = document.createElement("div");
4075
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4076
+ outer.appendChild(grid);
4077
+ const tile = buildPreviewTile(
4078
+ resourceId,
4079
+ state,
4080
+ Boolean(deps.onRemove),
4081
+ deps.onRemove ? () => {
4082
+ var _a2;
4083
+ return (_a2 = deps.onRemove) == null ? void 0 : _a2.call(deps);
4084
+ } : null,
4085
+ extras
4086
+ );
4087
+ grid.appendChild(tile);
4088
+ fileContainer.className = "file-preview-container";
4089
+ fileContainer.removeAttribute("style");
4090
+ while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4091
+ fileContainer.appendChild(outer);
4092
+ }
4093
+ function buildMultiAddTile(state, hasLibrary, onUploadClick, onLibraryClick, isDragOver = false) {
4094
+ const tile = document.createElement("div");
4095
+ tile.className = `fb-multi-add-tile fb-multi-add-tile-js${isDragOver ? " fb-drag-over-tile" : ""}`;
4096
+ const uploadBtn = document.createElement("button");
4097
+ uploadBtn.type = "button";
4098
+ uploadBtn.className = "fb-multi-add-upload fb-tile-add fb-file-dropzone";
4099
+ const cloudIcon = document.createElement("span");
4100
+ cloudIcon.style.cssText = "width:28px;height:28px;display:block;flex-shrink:0;";
4101
+ cloudIcon.innerHTML = ICON_CLOUD;
4102
+ uploadBtn.appendChild(cloudIcon);
4103
+ const uploadLabel = document.createElement("span");
4104
+ uploadLabel.className = "fb-multi-add-label";
4105
+ uploadLabel.style.cssText = "font-size:11px;font-weight:600;";
4106
+ uploadLabel.textContent = isDragOver ? t("dropToUpload", state) : t("clickDragTextMultiple", state);
4107
+ uploadBtn.appendChild(uploadLabel);
4108
+ uploadBtn.onclick = (e) => {
4109
+ e.stopPropagation();
4110
+ onUploadClick();
4111
+ };
4112
+ tile.appendChild(uploadBtn);
4113
+ if (hasLibrary && onLibraryClick) {
4114
+ const divider = document.createElement("div");
4115
+ divider.className = "fb-multi-add-divider";
4116
+ tile.appendChild(divider);
4117
+ const libBtn = document.createElement("button");
4118
+ libBtn.type = "button";
4119
+ libBtn.className = "fb-multi-add-library fb-tile-add-library fb-file-library-card";
4120
+ libBtn.setAttribute("aria-label", t("fromLibrary", state));
4121
+ const libIcon = document.createElement("span");
4122
+ libIcon.style.cssText = "width:14px;height:14px;display:block;flex-shrink:0;";
4123
+ libIcon.innerHTML = ICON_LIBRARY2;
4124
+ libBtn.appendChild(libIcon);
4125
+ libBtn.appendChild(document.createTextNode(t("fromLibrary", state)));
4126
+ libBtn.onclick = (e) => {
4127
+ e.stopPropagation();
4128
+ onLibraryClick();
4129
+ };
4130
+ tile.appendChild(libBtn);
4131
+ }
4132
+ return tile;
4133
+ }
4134
+ function buildPreviewTile(rid, state, canRemove, onRemove, extras) {
4135
+ var _a, _b, _c;
4136
+ ensureFileStyles();
4137
+ const meta = state.resourceIndex.get(rid);
4138
+ const tile = document.createElement("div");
4139
+ tile.className = "fb-preview-tile fb-checker fb-tile-resource resource-pill";
4140
+ tile.dataset.resourceId = rid;
4141
+ const actionsEl = createTileActions({
4142
+ canRemove: canRemove && onRemove !== null,
4143
+ removeHandler: onRemove,
4144
+ state,
4145
+ resourceId: rid,
4146
+ fileName: (_a = meta == null ? void 0 : meta.name) != null ? _a : "",
4147
+ meta,
4148
+ replaceHandler: (_b = extras == null ? void 0 : extras.replaceHandler) != null ? _b : null,
4149
+ libraryHandler: (_c = extras == null ? void 0 : extras.libraryHandler) != null ? _c : null
4150
+ });
4151
+ fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
4152
+ console.error("Failed to render tile:", err);
4153
+ });
4154
+ return tile;
4155
+ }
4156
+ function buildPlaceholderTile(isDragOver = false) {
4157
+ const div = document.createElement("div");
4158
+ div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
4159
+ return div;
4160
+ }
4161
+ function buildMetaLine(state, element, ridCount, maxCount, canClearAll, onClearAll) {
4162
+ const line = document.createElement("div");
4163
+ line.className = "fb-meta-line";
4164
+ const metaText = document.createElement("div");
4165
+ metaText.className = "fb-meta-text";
4166
+ if (element.maxSize && element.maxSize !== Infinity) {
4167
+ const sizeSpan = document.createElement("span");
4168
+ sizeSpan.textContent = t("hintMaxSize", state, { size: element.maxSize });
4169
+ metaText.appendChild(sizeSpan);
4170
+ metaText.appendChild(buildMetaDot());
4171
+ }
4172
+ const exts = getAllowedExtensions(element.accept);
4173
+ if (exts.length > 0) {
4174
+ const fmtSpan = document.createElement("span");
4175
+ fmtSpan.className = "fb-meta-mono";
4176
+ fmtSpan.textContent = exts.map((e) => e.toUpperCase()).join(", ");
4177
+ metaText.appendChild(fmtSpan);
4178
+ metaText.appendChild(buildMetaDot());
4179
+ }
4180
+ const countSpan = document.createElement("span");
4181
+ if (maxCount < Infinity) {
4182
+ countSpan.textContent = t("fileCountWithMax", state, {
4183
+ count: ridCount,
4184
+ max: maxCount
4185
+ });
3795
4186
  } else {
3796
- btn.innerHTML = `<span aria-hidden="true">\u{1F4DA}</span>`;
3797
- btn.title = t("fromLibrary", state);
3798
- btn.setAttribute("aria-label", t("fromLibrary", state));
4187
+ const countKey = ridCount === 1 ? "fileCountSingle" : "fileCountPlural";
4188
+ countSpan.textContent = t(countKey, state, { count: ridCount });
4189
+ }
4190
+ metaText.appendChild(countSpan);
4191
+ line.appendChild(metaText);
4192
+ if (canClearAll && ridCount > 1) {
4193
+ const clearBtn = document.createElement("button");
4194
+ clearBtn.type = "button";
4195
+ clearBtn.className = "fb-clear-all-btn";
4196
+ clearBtn.textContent = t("clearAll", state);
4197
+ clearBtn.onclick = (e) => {
4198
+ e.stopPropagation();
4199
+ if (window.confirm(t("clearAll", state) + "?")) {
4200
+ onClearAll();
4201
+ }
4202
+ };
4203
+ line.appendChild(clearBtn);
3799
4204
  }
3800
- btn.addEventListener("click", onClick);
3801
- return btn;
4205
+ return line;
3802
4206
  }
4207
+ function buildMetaDot() {
4208
+ const dot = document.createElement("span");
4209
+ dot.className = "fb-meta-dot";
4210
+ return dot;
4211
+ }
4212
+ var gridResizeObservers = /* @__PURE__ */ new WeakMap();
3803
4213
  function renderResourcePills(opts) {
3804
4214
  var _a;
3805
4215
  const {
@@ -3807,130 +4217,185 @@ function renderResourcePills(opts) {
3807
4217
  rids,
3808
4218
  state,
3809
4219
  onRemove,
3810
- hint,
3811
- countInfo,
3812
4220
  maxCount,
3813
4221
  isReadonly = false,
3814
- onLibraryPick
4222
+ onLibraryPick,
4223
+ element,
4224
+ onClearAll,
4225
+ openPicker: openPickerProp
3815
4226
  } = opts;
3816
4227
  ensureFileStyles();
3817
4228
  const wrapper = container.closest("[data-files-wrapper]");
3818
4229
  if (wrapper) {
3819
4230
  wrapper.dataset.resourceIds = JSON.stringify(rids != null ? rids : []);
3820
4231
  }
4232
+ const previousObserver = gridResizeObservers.get(container);
4233
+ if (previousObserver) {
4234
+ previousObserver.disconnect();
4235
+ gridResizeObservers.delete(container);
4236
+ }
3821
4237
  while (container.firstChild) container.removeChild(container.firstChild);
3822
4238
  const ridList = rids != null ? rids : [];
3823
- const atMax = maxCount !== void 0 && ridList.length >= maxCount;
4239
+ const effectiveMax = maxCount != null ? maxCount : Infinity;
4240
+ const atMax = effectiveMax !== Infinity && ridList.length >= effectiveMax;
3824
4241
  const hasLibrary = !isReadonly && typeof onLibraryPick === "function";
3825
- const buildSubHint = () => {
3826
- const parts = [];
3827
- if (hint) parts.push(hint);
3828
- if (countInfo) parts.push(countInfo);
3829
- return parts.join(" \u2022 ");
3830
- };
3831
- const openPicker = () => {
3832
- const picker = findFilePicker(container);
3833
- if (picker) picker.click();
3834
- };
3835
- if (ridList.length === 0) {
3836
- if (isReadonly) {
4242
+ const openPicker = openPickerProp != null ? openPickerProp : (() => {
4243
+ var _a2;
4244
+ const pickerEl = (_a2 = container.closest("[data-files-wrapper]")) == null ? void 0 : _a2.querySelector('input[type="file"]');
4245
+ if (pickerEl) pickerEl.click();
4246
+ });
4247
+ if (isReadonly) {
4248
+ if (ridList.length === 0) {
3837
4249
  const emptyEl = document.createElement("div");
3838
4250
  emptyEl.className = "fb-tile-empty-text";
3839
4251
  emptyEl.textContent = t("noFilesSelected", state);
3840
4252
  container.appendChild(emptyEl);
3841
- } else if (hasLibrary) {
3842
- const row = document.createElement("div");
3843
- row.className = "fb-file-card-row";
3844
- const dropzone = buildEmptyDropzone(
3845
- state,
3846
- t("clickDragTextMultiple", state),
3847
- buildSubHint(),
3848
- openPicker
3849
- );
3850
- const libraryBtn = buildLibraryButton("card", state, onLibraryPick);
3851
- row.appendChild(dropzone);
3852
- row.appendChild(libraryBtn);
3853
- container.appendChild(row);
3854
4253
  } else {
3855
- const dropzone = buildEmptyDropzone(
3856
- state,
3857
- t("clickDragTextMultiple", state),
3858
- buildSubHint(),
3859
- openPicker
3860
- );
3861
- container.appendChild(dropzone);
4254
+ const grid2 = document.createElement("div");
4255
+ grid2.className = "fb-multi-readonly-grid";
4256
+ container.appendChild(grid2);
4257
+ for (const rid of ridList) {
4258
+ const meta = state.resourceIndex.get(rid);
4259
+ const tile = document.createElement("div");
4260
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4261
+ tile.dataset.resourceId = rid;
4262
+ const actionsEl = createTileActions({
4263
+ canRemove: false,
4264
+ removeHandler: null,
4265
+ state,
4266
+ resourceId: rid,
4267
+ fileName: (_a = meta == null ? void 0 : meta.name) != null ? _a : "",
4268
+ meta
4269
+ });
4270
+ fillTileContent(tile, rid, meta, state, actionsEl).catch(console.error);
4271
+ tile.onclick = async () => {
4272
+ var _a2;
4273
+ let url = null;
4274
+ if (state.config.getDownloadUrl) {
4275
+ url = state.config.getDownloadUrl(rid);
4276
+ } else if (state.config.getThumbnail) {
4277
+ url = await state.config.getThumbnail(rid);
4278
+ } else if ((meta == null ? void 0 : meta.file) instanceof File) {
4279
+ url = URL.createObjectURL(meta.file);
4280
+ }
4281
+ if (url) {
4282
+ window.open(url, "_blank");
4283
+ } else if (state.config.downloadFile) {
4284
+ state.config.downloadFile(rid, (_a2 = meta == null ? void 0 : meta.name) != null ? _a2 : "");
4285
+ }
4286
+ };
4287
+ grid2.appendChild(tile);
4288
+ }
3862
4289
  }
3863
4290
  return;
3864
4291
  }
3865
- const tilesWrap = document.createElement("div");
3866
- tilesWrap.className = "fb-tiles-wrap";
3867
- tilesWrap.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;align-items:flex-start;";
3868
- for (const rid of ridList) {
3869
- const meta = state.resourceIndex.get(rid);
3870
- const tile = createFileTile();
3871
- tile.classList.add("fb-tile-resource", "resource-pill");
3872
- tile.dataset.resourceId = rid;
3873
- const actionsEl = createTileActions({
3874
- canRemove: !isReadonly && onRemove !== null,
3875
- removeHandler: onRemove ? () => onRemove(rid) : null,
4292
+ const outerDiv = document.createElement("div");
4293
+ outerDiv.className = `fb-multi-outer${ridList.length > 0 ? " fb-multi-has-files" : ""}`;
4294
+ const grid = document.createElement("div");
4295
+ grid.className = "fb-multi-grid fb-tiles-wrap";
4296
+ outerDiv.appendChild(grid);
4297
+ container.appendChild(outerDiv);
4298
+ for (let i = 0; i < ridList.length; i++) {
4299
+ const rid = ridList[i];
4300
+ const tile = buildPreviewTile(
4301
+ rid,
3876
4302
  state,
3877
- resourceId: rid,
3878
- fileName: (_a = meta == null ? void 0 : meta.name) != null ? _a : ""
3879
- });
3880
- fillTileContent(tile, rid, meta, state, actionsEl).catch((err) => {
3881
- console.error("Failed to render tile:", err);
3882
- });
3883
- tilesWrap.appendChild(tile);
3884
- }
3885
- if (!isReadonly && !atMax) {
3886
- const addTile = document.createElement("div");
3887
- addTile.className = "fb-tile fb-tile-add";
3888
- addTile.innerHTML = "+";
3889
- addTile.onclick = openPicker;
3890
- tilesWrap.appendChild(addTile);
3891
- if (hasLibrary) {
3892
- const libraryTile = buildLibraryButton("tile", state, onLibraryPick);
3893
- tilesWrap.appendChild(libraryTile);
3894
- }
3895
- } else if (!isReadonly && atMax) {
3896
- const chip = document.createElement("div");
3897
- chip.className = "fb-tile-counter";
3898
- chip.textContent = t("filesCounter", state, {
3899
- count: ridList.length,
3900
- max: maxCount
3901
- });
3902
- tilesWrap.appendChild(chip);
4303
+ onRemove !== null,
4304
+ onRemove ? () => onRemove(rid) : null
4305
+ );
4306
+ grid.appendChild(tile);
3903
4307
  }
3904
- container.appendChild(tilesWrap);
3905
- const subHint = buildSubHint();
3906
- if (subHint) {
3907
- const hintEl = document.createElement("div");
3908
- hintEl.className = "fb-tile-hint";
3909
- hintEl.textContent = subHint;
3910
- container.appendChild(hintEl);
4308
+ if (!atMax) {
4309
+ const addTile = buildMultiAddTile(
4310
+ state,
4311
+ hasLibrary,
4312
+ openPicker,
4313
+ onLibraryPick != null ? onLibraryPick : null
4314
+ );
4315
+ grid.appendChild(addTile);
4316
+ }
4317
+ const occupied = ridList.length + (atMax ? 0 : 1);
4318
+ const adjustPlaceholders = () => {
4319
+ const tpl = getComputedStyle(grid).gridTemplateColumns;
4320
+ const cols = tpl ? tpl.split(" ").filter(Boolean).length : 0;
4321
+ if (!cols) return;
4322
+ const remainder = occupied % cols;
4323
+ const rowFill = remainder === 0 ? 0 : cols - remainder;
4324
+ const capacityRemaining = effectiveMax === Infinity ? rowFill : Math.max(0, effectiveMax - occupied);
4325
+ const needed = Math.min(rowFill, capacityRemaining);
4326
+ const existing = grid.querySelectorAll(".fb-multi-placeholder");
4327
+ if (existing.length > needed) {
4328
+ for (let i = existing.length - 1; i >= needed; i--) existing[i].remove();
4329
+ } else if (existing.length < needed) {
4330
+ for (let i = existing.length; i < needed; i++) {
4331
+ grid.appendChild(buildPlaceholderTile());
4332
+ }
4333
+ }
4334
+ };
4335
+ if (effectiveMax === Infinity || effectiveMax > occupied) {
4336
+ grid.appendChild(buildPlaceholderTile());
4337
+ }
4338
+ requestAnimationFrame(adjustPlaceholders);
4339
+ if (typeof ResizeObserver !== "undefined") {
4340
+ const ro = new ResizeObserver(() => adjustPlaceholders());
4341
+ ro.observe(grid);
4342
+ gridResizeObservers.set(container, ro);
4343
+ }
4344
+ attachDragOverFeedback(outerDiv, {
4345
+ activeClass: "fb-drag-over",
4346
+ onEnter: () => {
4347
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4348
+ p.classList.add("fb-drag-over");
4349
+ });
4350
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4351
+ if (addTile) {
4352
+ addTile.classList.add("fb-drag-over-tile");
4353
+ const label = addTile.querySelector(".fb-multi-add-label");
4354
+ if (label) label.textContent = t("dropToUpload", state);
4355
+ }
4356
+ },
4357
+ onLeave: () => {
4358
+ grid.querySelectorAll(".fb-multi-placeholder").forEach((p) => {
4359
+ p.classList.remove("fb-drag-over");
4360
+ });
4361
+ const addTile = grid.querySelector(".fb-multi-add-tile-js");
4362
+ if (addTile) {
4363
+ addTile.classList.remove("fb-drag-over-tile");
4364
+ const label = addTile.querySelector(".fb-multi-add-label");
4365
+ if (label) label.textContent = t("clickDragTextMultiple", state);
4366
+ }
4367
+ }
4368
+ });
4369
+ if (element) {
4370
+ const metaLine = buildMetaLine(
4371
+ state,
4372
+ element,
4373
+ ridList.length,
4374
+ effectiveMax,
4375
+ Boolean(onClearAll),
4376
+ onClearAll != null ? onClearAll : (() => {
4377
+ })
4378
+ );
4379
+ container.appendChild(metaLine);
3911
4380
  }
3912
4381
  }
3913
4382
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3914
- var _a, _b, _c, _d, _e;
4383
+ var _a, _b;
3915
4384
  const state = ctx.state;
3916
4385
  const fileWrapper = document.createElement("div");
3917
4386
  fileWrapper.className = "space-y-2";
4387
+ fileWrapper.dataset.filesWrapper = pathKey;
3918
4388
  const picker = document.createElement("input");
3919
4389
  picker.type = "file";
3920
4390
  picker.name = pathKey;
3921
4391
  picker.style.display = "none";
3922
- if (element.accept) {
3923
- picker.accept = typeof element.accept === "string" ? element.accept : [
3924
- ...(_b = (_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
3925
- ...(_c = element.accept.mime) != null ? _c : []
3926
- ].join(",") || "";
3927
- }
4392
+ picker.accept = buildAcceptAttribute(element.accept);
3928
4393
  const fileContainer = document.createElement("div");
3929
4394
  fileContainer.className = "file-preview-container";
3930
4395
  const initial = ctx.prefill[element.key];
3931
4396
  const allowedExts = getAllowedExtensions(element.accept);
3932
4397
  const allowedMimes = getAllowedMimes(element.accept);
3933
- const maxSizeMB = (_d = element.maxSize) != null ? _d : Infinity;
4398
+ const maxSizeMB = (_a = element.maxSize) != null ? _a : Infinity;
3934
4399
  const handlers = {
3935
4400
  fileUploadHandler() {
3936
4401
  picker.click();
@@ -3953,14 +4418,6 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3953
4418
  setupDrop(container) {
3954
4419
  setupDragAndDrop(container, handlers.dragHandler);
3955
4420
  },
3956
- restoreDropzone() {
3957
- const hint = makeFieldHint(element, state);
3958
- fileContainer.className = "file-preview-container w-full max-w-md bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
3959
- fileContainer.style.height = "128px";
3960
- setEmptyFileContainer(fileContainer, state, hint);
3961
- fileContainer.onclick = handlers.fileUploadHandler;
3962
- setupDragAndDrop(fileContainer, handlers.dragHandler);
3963
- },
3964
4421
  onRemove() {
3965
4422
  var _a2;
3966
4423
  const hiddenInput = fileWrapper.querySelector('input[type="hidden"]');
@@ -3972,34 +4429,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
3972
4429
  renderEmptySingleState();
3973
4430
  }
3974
4431
  };
3975
- const buildDeps = () => ({
3976
- picker,
3977
- fileUploadHandler: handlers.fileUploadHandler,
3978
- dragHandler: handlers.dragHandler,
3979
- setupDrop: handlers.setupDrop,
3980
- onRemove: handlers.onRemove
3981
- });
3982
- const renderEmptySingleState = () => {
3983
- if (state.config.pickExistingFiles && !element.disableLibrary) {
3984
- fileContainer.className = "file-preview-container";
3985
- fileContainer.removeAttribute("style");
3986
- fileContainer.onclick = null;
3987
- while (fileContainer.firstChild) {
3988
- fileContainer.removeChild(fileContainer.firstChild);
3989
- }
3990
- const row = document.createElement("div");
3991
- row.className = "fb-file-card-row";
3992
- row.style.cssText = "display:flex;gap:8px;align-items:stretch;";
3993
- const hint = makeFieldHint(element, state);
3994
- const uploadCard = buildEmptyDropzone(
3995
- state,
3996
- t("clickDragText", state),
3997
- hint,
3998
- handlers.fileUploadHandler
3999
- );
4000
- uploadCard.style.cssText = "flex:1;min-width:0;height:128px;";
4001
- setupDragAndDrop(uploadCard, handlers.dragHandler);
4002
- const libraryBtn = buildLibraryButton("card", state, () => {
4432
+ const buildSingleExtras = () => {
4433
+ const hasLibrary = Boolean(state.config.pickExistingFiles && !element.disableLibrary);
4434
+ return {
4435
+ replaceHandler: state.config.uploadFile ? () => picker.click() : null,
4436
+ libraryHandler: hasLibrary ? () => {
4003
4437
  handleLibraryPickSingle(
4004
4438
  state,
4005
4439
  element,
@@ -4008,20 +4442,41 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4008
4442
  pathKey,
4009
4443
  pathKey,
4010
4444
  async (rid) => {
4011
- await renderSingleFileEditTile(fileContainer, rid, state, buildDeps());
4445
+ renderSingleFileFilled(fileContainer, rid, state, buildDeps(), buildSingleExtras());
4012
4446
  },
4013
4447
  ctx.instance
4014
4448
  ).catch((err) => {
4015
4449
  console.error("Library pick failed:", err);
4016
4450
  });
4017
- });
4018
- libraryBtn.style.cssText = "flex:1;min-width:0;";
4019
- row.appendChild(uploadCard);
4020
- row.appendChild(libraryBtn);
4021
- fileContainer.appendChild(row);
4022
- } else {
4023
- handlers.restoreDropzone();
4451
+ } : null
4452
+ };
4453
+ };
4454
+ const buildDeps = () => ({
4455
+ picker,
4456
+ fileUploadHandler: handlers.fileUploadHandler,
4457
+ dragHandler: handlers.dragHandler,
4458
+ setupDrop: handlers.setupDrop,
4459
+ onRemove: handlers.onRemove,
4460
+ onAfterUpload: (container, rid) => {
4461
+ renderSingleFileFilled(container, rid, state, buildDeps(), buildSingleExtras());
4024
4462
  }
4463
+ });
4464
+ const renderEmptySingleState = () => {
4465
+ ensureFileStyles();
4466
+ fileContainer.className = "file-preview-container";
4467
+ fileContainer.removeAttribute("style");
4468
+ while (fileContainer.firstChild) fileContainer.removeChild(fileContainer.firstChild);
4469
+ const onLibraryClick = buildSingleExtras().libraryHandler;
4470
+ const wideTile = buildWideTile(
4471
+ state,
4472
+ onLibraryClick !== null,
4473
+ handlers.fileUploadHandler,
4474
+ onLibraryClick,
4475
+ false,
4476
+ makeFieldHint(element, state)
4477
+ );
4478
+ fileContainer.appendChild(wideTile);
4479
+ setupDragAndDrop(fileContainer, handlers.dragHandler);
4025
4480
  };
4026
4481
  if (initial) {
4027
4482
  handleInitialFileData(
@@ -4030,11 +4485,11 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4030
4485
  pathKey,
4031
4486
  fileWrapper,
4032
4487
  state,
4033
- buildDeps()
4488
+ buildDeps(),
4489
+ buildSingleExtras()
4034
4490
  );
4035
4491
  const prefillMeta = state.resourceIndex.get(initial);
4036
- if ((_e = prefillMeta == null ? void 0 : prefillMeta.type) == null ? void 0 : _e.startsWith("video/")) {
4037
- fileContainer.onclick = handlers.fileUploadHandler;
4492
+ if ((_b = prefillMeta == null ? void 0 : prefillMeta.type) == null ? void 0 : _b.startsWith("video/")) {
4038
4493
  setupDragAndDrop(fileContainer, handlers.dragHandler);
4039
4494
  }
4040
4495
  } else {
@@ -4042,116 +4497,25 @@ function renderFileElementEdit(element, ctx, wrapper, pathKey) {
4042
4497
  }
4043
4498
  picker.onchange = () => {
4044
4499
  if (picker.files && picker.files.length > 0) {
4045
- handleFileSelect({
4046
- file: picker.files[0],
4047
- container: fileContainer,
4048
- fieldName: pathKey,
4049
- state,
4050
- deps: buildDeps(),
4051
- instance: ctx.instance,
4052
- allowedExtensions: allowedExts,
4053
- allowedMimes,
4054
- maxSizeMB
4055
- });
4500
+ handlers.dragHandler(picker.files);
4056
4501
  }
4057
4502
  };
4058
4503
  fileWrapper.appendChild(fileContainer);
4059
4504
  fileWrapper.appendChild(picker);
4060
4505
  wrapper.appendChild(fileWrapper);
4061
4506
  }
4062
- function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4063
- var _a, _b, _c, _d;
4064
- const state = ctx.state;
4065
- const filesWrapper = document.createElement("div");
4066
- filesWrapper.className = "space-y-2";
4067
- filesWrapper.dataset.filesWrapper = pathKey;
4068
- const filesPicker = document.createElement("input");
4069
- filesPicker.type = "file";
4070
- filesPicker.name = pathKey;
4071
- filesPicker.multiple = true;
4072
- filesPicker.style.display = "none";
4073
- if (element.accept) {
4074
- filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4075
- ...(_b = (_a = element.accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
4076
- ...(_c = element.accept.mime) != null ? _c : []
4077
- ].join(",") || "";
4078
- }
4079
- const filesContainer = document.createElement("div");
4080
- filesContainer.className = "files-list-wrapper";
4081
- 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);";
4082
- const list = document.createElement("div");
4083
- list.className = "files-list";
4084
- const initialFiles = ctx.prefill[element.key] || [];
4085
- addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4086
- filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4087
- const filesFieldHint = makeFieldHint(element, state);
4088
- const filesConstraints = {
4089
- maxCount: Infinity,
4090
- allowedExtensions: getAllowedExtensions(element.accept),
4091
- allowedMimes: getAllowedMimes(element.accept),
4092
- maxSize: (_d = element.maxSize) != null ? _d : Infinity
4093
- };
4094
- filesContainer.appendChild(list);
4095
- filesWrapper.appendChild(filesPicker);
4096
- filesWrapper.appendChild(filesContainer);
4097
- wrapper.appendChild(filesWrapper);
4098
- const onLibraryPickFiles = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4099
- handleLibraryPickMulti(
4100
- state,
4101
- element,
4102
- filesWrapper,
4103
- pathKey,
4104
- initialFiles,
4105
- Infinity,
4106
- updateFilesList,
4107
- ctx.instance
4108
- ).catch((err) => {
4109
- console.error("Library pick failed:", err);
4110
- });
4111
- } : null;
4112
- function updateFilesList() {
4113
- const currentlyReadonly = isElementReadonly(element, state);
4114
- renderResourcePills({
4115
- container: list,
4116
- rids: initialFiles,
4117
- state,
4118
- onRemove: currentlyReadonly ? null : (ridToRemove) => {
4119
- var _a2;
4120
- releaseLocalFileUrl((_a2 = state.resourceIndex.get(ridToRemove)) == null ? void 0 : _a2.file);
4121
- const index = initialFiles.indexOf(ridToRemove);
4122
- if (index > -1) initialFiles.splice(index, 1);
4123
- updateFilesList();
4124
- },
4125
- hint: filesFieldHint,
4126
- isReadonly: currentlyReadonly,
4127
- onLibraryPick: currentlyReadonly ? null : onLibraryPickFiles
4128
- });
4129
- }
4130
- updateFilesList();
4131
- setupFilesDropHandler(
4132
- filesContainer,
4133
- initialFiles,
4134
- state,
4135
- updateFilesList,
4136
- filesConstraints,
4137
- pathKey,
4138
- ctx.instance
4139
- );
4140
- setupFilesPickerHandler(
4141
- filesPicker,
4142
- initialFiles,
4143
- state,
4144
- updateFilesList,
4145
- filesConstraints,
4146
- pathKey,
4147
- ctx.instance
4148
- );
4149
- }
4150
- function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4151
- var _a, _b, _c, _d, _e, _f;
4507
+ function buildAcceptAttribute(accept) {
4508
+ var _a, _b, _c;
4509
+ if (!accept) return "";
4510
+ if (typeof accept === "string") return accept;
4511
+ return [
4512
+ ...(_b = (_a = accept.extensions) == null ? void 0 : _a.map((ext) => `.${ext}`)) != null ? _b : [],
4513
+ ...(_c = accept.mime) != null ? _c : []
4514
+ ].join(",");
4515
+ }
4516
+ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
4517
+ var _a, _b;
4152
4518
  const state = ctx.state;
4153
- const minFiles = (_a = element.minCount) != null ? _a : 0;
4154
- const maxFiles = (_b = element.maxCount) != null ? _b : Infinity;
4155
4519
  const filesWrapper = document.createElement("div");
4156
4520
  filesWrapper.className = "space-y-2";
4157
4521
  filesWrapper.dataset.filesWrapper = pathKey;
@@ -4160,15 +4524,9 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4160
4524
  filesPicker.name = pathKey;
4161
4525
  filesPicker.multiple = true;
4162
4526
  filesPicker.style.display = "none";
4163
- if (element.accept) {
4164
- filesPicker.accept = typeof element.accept === "string" ? element.accept : [
4165
- ...(_d = (_c = element.accept.extensions) == null ? void 0 : _c.map((ext) => `.${ext}`)) != null ? _d : [],
4166
- ...(_e = element.accept.mime) != null ? _e : []
4167
- ].join(",") || "";
4168
- }
4527
+ filesPicker.accept = buildAcceptAttribute(element.accept);
4169
4528
  const filesContainer = document.createElement("div");
4170
4529
  filesContainer.className = "files-list-wrapper";
4171
- 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);";
4172
4530
  const list = document.createElement("div");
4173
4531
  list.className = "files-list";
4174
4532
  filesWrapper.appendChild(filesPicker);
@@ -4177,19 +4535,18 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4177
4535
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
4178
4536
  addPrefillFilesToIndex(initialFiles, state.resourceIndex);
4179
4537
  filesWrapper.dataset.resourceIds = JSON.stringify(initialFiles);
4180
- const multipleFilesHint = makeFieldHint(element, state);
4181
- const multipleConstraints = {
4538
+ const constraints = {
4182
4539
  maxCount: maxFiles,
4183
4540
  allowedExtensions: getAllowedExtensions(element.accept),
4184
4541
  allowedMimes: getAllowedMimes(element.accept),
4185
- maxSize: (_f = element.maxSize) != null ? _f : Infinity
4542
+ // Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
4543
+ // backward compatibility (matches addFileSizeHint in validation.ts).
4544
+ maxSize: (_b = (_a = element.maxSize) != null ? _a : element.maxSizeMB) != null ? _b : Infinity
4186
4545
  };
4187
- const buildCountInfo = () => {
4188
- const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
4189
- const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
4190
- return countText + minMaxText;
4546
+ const openPicker = () => {
4547
+ filesPicker.click();
4191
4548
  };
4192
- const onLibraryPickMultiple = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4549
+ const onLibraryPick = state.config.pickExistingFiles && !element.disableLibrary ? () => {
4193
4550
  handleLibraryPickMulti(
4194
4551
  state,
4195
4552
  element,
@@ -4203,31 +4560,36 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4203
4560
  console.error("Library pick failed:", err);
4204
4561
  });
4205
4562
  } : null;
4206
- const updateFilesDisplay = () => {
4563
+ function updateFilesDisplay() {
4207
4564
  const currentlyReadonly = isElementReadonly(element, state);
4208
4565
  renderResourcePills({
4209
4566
  container: list,
4210
4567
  rids: initialFiles,
4211
4568
  state,
4212
- onRemove: currentlyReadonly ? null : (index) => {
4569
+ onRemove: currentlyReadonly ? null : (ridToRemove) => {
4213
4570
  var _a2;
4214
- releaseLocalFileUrl((_a2 = state.resourceIndex.get(index)) == null ? void 0 : _a2.file);
4215
- initialFiles.splice(initialFiles.indexOf(index), 1);
4571
+ releaseLocalFileUrl((_a2 = state.resourceIndex.get(ridToRemove)) == null ? void 0 : _a2.file);
4572
+ const index = initialFiles.indexOf(ridToRemove);
4573
+ if (index > -1) initialFiles.splice(index, 1);
4216
4574
  updateFilesDisplay();
4217
4575
  },
4218
- hint: multipleFilesHint,
4219
- countInfo: buildCountInfo(),
4220
4576
  maxCount: maxFiles < Infinity ? maxFiles : void 0,
4221
4577
  isReadonly: currentlyReadonly,
4222
- onLibraryPick: currentlyReadonly ? null : onLibraryPickMultiple
4578
+ onLibraryPick: currentlyReadonly ? null : onLibraryPick,
4579
+ element,
4580
+ onClearAll: currentlyReadonly ? void 0 : () => {
4581
+ initialFiles.splice(0);
4582
+ updateFilesDisplay();
4583
+ },
4584
+ openPicker
4223
4585
  });
4224
- };
4586
+ }
4225
4587
  setupFilesDropHandler(
4226
4588
  filesContainer,
4227
4589
  initialFiles,
4228
4590
  state,
4229
4591
  updateFilesDisplay,
4230
- multipleConstraints,
4592
+ constraints,
4231
4593
  pathKey,
4232
4594
  ctx.instance
4233
4595
  );
@@ -4236,13 +4598,20 @@ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4236
4598
  initialFiles,
4237
4599
  state,
4238
4600
  updateFilesDisplay,
4239
- multipleConstraints,
4601
+ constraints,
4240
4602
  pathKey,
4241
4603
  ctx.instance
4242
4604
  );
4243
4605
  updateFilesDisplay();
4244
4606
  wrapper.appendChild(filesWrapper);
4245
4607
  }
4608
+ function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
4609
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
4610
+ }
4611
+ function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
4612
+ var _a;
4613
+ setupMultiFileEditMode(element, ctx, wrapper, pathKey, (_a = element.maxCount) != null ? _a : Infinity);
4614
+ }
4246
4615
 
4247
4616
  // src/components/file/validate.ts
4248
4617
  function readMultiFileResourceIds(scopeRoot, fullKey) {
@@ -4369,33 +4738,36 @@ function renderFileElementReadonly(element, ctx, wrapper, pathKey) {
4369
4738
  hiddenInput.name = pathKey;
4370
4739
  hiddenInput.value = initial;
4371
4740
  wrapper.appendChild(hiddenInput);
4372
- renderFilePreviewReadonly(initial, state).then((filePreview) => {
4373
- wrapper.appendChild(filePreview);
4374
- }).catch((err) => {
4375
- console.error("Failed to render file preview:", err);
4376
- wrapper.appendChild(buildEmptyReadonlyTile(state));
4377
- });
4741
+ renderFilePreviewReadonly(initial, state).then((tile) => {
4742
+ tile.classList.add(
4743
+ "fb-single-readonly-filled",
4744
+ "fb-readonly-tile",
4745
+ "fb-checker"
4746
+ );
4747
+ wrapper.appendChild(tile);
4748
+ }).catch(console.error);
4378
4749
  } else {
4379
4750
  wrapper.appendChild(buildEmptyReadonlyTile(state));
4380
4751
  }
4381
4752
  }
4382
4753
  function buildEmptyReadonlyTile(state) {
4754
+ ensureFileStyles();
4383
4755
  const emptyState = document.createElement("div");
4384
4756
  emptyState.style.cssText = `
4385
- width:${TILE_SIZE};
4386
- height:${TILE_SIZE};
4757
+ height: 220px;
4387
4758
  display:flex;
4388
4759
  align-items:center;
4389
4760
  justify-content:center;
4390
- background:var(--fb-file-upload-bg-color,#f3f4f6);
4391
- border-radius:var(--fb-border-radius,0.5rem);
4392
- border:1px solid var(--fb-file-upload-border-color,#d1d5db);
4761
+ background: repeating-linear-gradient(45deg, #fafafa 0 6px, #f3f4f6 6px 12px);
4762
+ border-radius:0.75rem;
4763
+ border:1px solid #e2e8f0;
4393
4764
  `;
4394
4765
  emptyState.innerHTML = `<div style="font-size:11px;text-align:center;color:var(--fb-text-secondary-color,#6b7280);">${escapeHtml(t("noFileSelected", state))}</div>`;
4395
4766
  return emptyState;
4396
4767
  }
4397
- function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4768
+ function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
4398
4769
  addPrefillFilesToIndex(rids, state.resourceIndex);
4770
+ ensureFileStyles();
4399
4771
  const filesWrapper = document.createElement("div");
4400
4772
  filesWrapper.dataset.filesWrapper = pathKey;
4401
4773
  filesWrapper.dataset.resourceIds = JSON.stringify(rids);
@@ -4407,22 +4779,28 @@ function renderMultiFileReadonly(rids, state, wrapper, pathKey, marginTop) {
4407
4779
  filesWrapper.appendChild(emptyEl);
4408
4780
  return;
4409
4781
  }
4410
- const tilesWrap = document.createElement("div");
4411
- tilesWrap.style.cssText = `display:flex;flex-wrap:wrap;gap:6px;${marginTop ? `margin-top:${marginTop};` : ""}`;
4412
- filesWrapper.appendChild(tilesWrap);
4782
+ const grid = document.createElement("div");
4783
+ grid.className = "fb-multi-readonly-grid";
4784
+ filesWrapper.appendChild(grid);
4413
4785
  const placeholders = rids.map(() => {
4414
- const placeholder = document.createElement("div");
4415
- placeholder.style.cssText = `width:${TILE_SIZE};height:${TILE_SIZE};`;
4416
- tilesWrap.appendChild(placeholder);
4417
- return placeholder;
4786
+ const ph = document.createElement("div");
4787
+ ph.className = "fb-readonly-tile fb-checker fb-tile";
4788
+ grid.appendChild(ph);
4789
+ return ph;
4418
4790
  });
4419
4791
  for (let i = 0; i < rids.length; i++) {
4420
4792
  const resourceId = rids[i];
4421
4793
  const placeholder = placeholders[i];
4422
- renderFilePreviewReadonly(resourceId, state).then((tileEl) => {
4423
- placeholder.replaceWith(tileEl);
4424
- }).catch((err) => {
4425
- console.error("Failed to render readonly tile:", err);
4794
+ const meta = state.resourceIndex.get(resourceId);
4795
+ renderFilePreviewReadonly(resourceId, state, meta == null ? void 0 : meta.name).then((tile) => {
4796
+ tile.classList.add("fb-readonly-tile", "fb-checker", "fb-tile-resource");
4797
+ tile.dataset.resourceId = resourceId;
4798
+ placeholder.replaceWith(tile);
4799
+ }).catch(() => {
4800
+ const tile = document.createElement("div");
4801
+ tile.className = "fb-readonly-tile fb-checker fb-tile fb-tile-resource";
4802
+ tile.dataset.resourceId = resourceId;
4803
+ placeholder.replaceWith(tile);
4426
4804
  });
4427
4805
  }
4428
4806
  }
@@ -4434,7 +4812,7 @@ function renderFilesElementReadonly(element, ctx, wrapper, pathKey) {
4434
4812
  function renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey) {
4435
4813
  const rawPrefill = ctx.prefill[element.key];
4436
4814
  const initialFiles = Array.isArray(rawPrefill) ? rawPrefill : [];
4437
- renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey, "4px");
4815
+ renderMultiFileReadonly(initialFiles, ctx.state, wrapper, pathKey);
4438
4816
  }
4439
4817
 
4440
4818
  // src/components/file.ts
@@ -5531,7 +5909,7 @@ function updateSliderField(element, fieldPath, value, context) {
5531
5909
  function extractChildDefaults(elements) {
5532
5910
  const defaults = {};
5533
5911
  for (const child of elements) {
5534
- if ("default" in child && child.default !== void 0) {
5912
+ if (child.key && "default" in child && child.default !== void 0) {
5535
5913
  defaults[child.key] = child.default;
5536
5914
  }
5537
5915
  }
@@ -5582,7 +5960,7 @@ function createPrefillHints(element, pathKey) {
5582
5960
  return null;
5583
5961
  }
5584
5962
  const hintsContainer = document.createElement("div");
5585
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
5963
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
5586
5964
  element.prefillHints.forEach((hint, index) => {
5587
5965
  const hintButton = document.createElement("button");
5588
5966
  hintButton.type = "button";
@@ -5598,14 +5976,14 @@ function createPrefillHints(element, pathKey) {
5598
5976
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5599
5977
  var _a, _b;
5600
5978
  const containerWrap = document.createElement("div");
5601
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
5979
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5602
5980
  containerWrap.setAttribute("data-container", pathKey);
5603
5981
  const itemsWrap = document.createElement("div");
5604
5982
  const columns = element.columns || 1;
5605
5983
  if (columns === 1) {
5606
- itemsWrap.className = "space-y-4";
5984
+ itemsWrap.className = "space-y-2";
5607
5985
  } else {
5608
- itemsWrap.className = `grid grid-cols-${columns} gap-4`;
5986
+ itemsWrap.className = `grid grid-cols-${columns} gap-2`;
5609
5987
  }
5610
5988
  const containerIsReadonly = isElementReadonly(element, ctx.state, ctx);
5611
5989
  if (!containerIsReadonly) {
@@ -5628,8 +6006,8 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5628
6006
  };
5629
6007
  element.elements.forEach((child) => {
5630
6008
  var _a2, _b2;
5631
- if (child.hidden || child.type === "hidden") {
5632
- const prefillVal = (_b2 = (_a2 = containerPrefill[child.key]) != null ? _a2 : child.default) != null ? _b2 : null;
6009
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6010
+ const prefillVal = (_b2 = (_a2 = containerPrefill[child.key]) != null ? _a2 : "default" in child ? child.default : null) != null ? _b2 : null;
5633
6011
  itemsWrap.appendChild(
5634
6012
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
5635
6013
  );
@@ -5640,17 +6018,32 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
5640
6018
  containerWrap.appendChild(itemsWrap);
5641
6019
  wrapper.appendChild(containerWrap);
5642
6020
  }
6021
+ function getChildWrapperClass(isSlides, columns) {
6022
+ if (isSlides) {
6023
+ return "space-y-2";
6024
+ }
6025
+ const cols = columns || 1;
6026
+ return cols === 1 ? "space-y-2" : `grid grid-cols-${cols} gap-2`;
6027
+ }
5643
6028
  function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5644
6029
  var _a, _b, _c, _d;
5645
6030
  const state = ctx.state;
5646
6031
  const containerIsReadonly = isElementReadonly(element, state, ctx);
5647
6032
  const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
5648
6033
  const containerWrap = document.createElement("div");
5649
- containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
6034
+ containerWrap.className = "border border-gray-200 rounded-lg p-2 bg-gray-50";
5650
6035
  const countDisplay = document.createElement("span");
5651
6036
  countDisplay.className = "text-sm text-gray-500";
5652
6037
  const itemsWrap = document.createElement("div");
5653
- itemsWrap.className = "space-y-4";
6038
+ const isSlides = element.displayMode === "slides";
6039
+ if (isSlides) {
6040
+ itemsWrap.className = "fb-container-slides";
6041
+ const slideCols = element.columns;
6042
+ const gridTemplateColumns = typeof slideCols === "number" && slideCols > 0 ? `repeat(${slideCols}, 1fr)` : "repeat(auto-fit, minmax(280px, 1fr))";
6043
+ itemsWrap.style.cssText = `display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:start;`;
6044
+ } else {
6045
+ itemsWrap.className = "space-y-2";
6046
+ }
5654
6047
  if (!containerIsReadonly) {
5655
6048
  const hintsElement = createPrefillHints(element, element.key);
5656
6049
  if (hintsElement) {
@@ -5694,22 +6087,17 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5694
6087
  inheritedReadonly: childInheritedReadonly
5695
6088
  };
5696
6089
  const item = document.createElement("div");
5697
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6090
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5698
6091
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5699
6092
  const childWrapper = document.createElement("div");
5700
- const columns = element.columns || 1;
5701
- if (columns === 1) {
5702
- childWrapper.className = "space-y-4";
5703
- } else {
5704
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
5705
- }
6093
+ childWrapper.className = getChildWrapperClass(isSlides, element.columns);
5706
6094
  element.elements.forEach((child) => {
5707
6095
  var _a2;
5708
- if (child.hidden || child.type === "hidden") {
6096
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5709
6097
  childWrapper.appendChild(
5710
6098
  createHiddenInput(
5711
6099
  pathJoin(subCtx.path, child.key),
5712
- (_a2 = child.default) != null ? _a2 : null
6100
+ (_a2 = "default" in child ? child.default : null) != null ? _a2 : null
5713
6101
  )
5714
6102
  );
5715
6103
  } else {
@@ -5773,19 +6161,23 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5773
6161
  inheritedReadonly: childInheritedReadonly
5774
6162
  };
5775
6163
  const item = document.createElement("div");
5776
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6164
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5777
6165
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5778
6166
  const childWrapper = document.createElement("div");
5779
- const columns = element.columns || 1;
5780
- if (columns === 1) {
5781
- childWrapper.className = "space-y-4";
6167
+ if (isSlides) {
6168
+ childWrapper.className = "space-y-2";
5782
6169
  } else {
5783
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6170
+ const columns = element.columns || 1;
6171
+ if (columns === 1) {
6172
+ childWrapper.className = "space-y-2";
6173
+ } else {
6174
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6175
+ }
5784
6176
  }
5785
6177
  element.elements.forEach((child) => {
5786
6178
  var _a3, _b2;
5787
- if (child.hidden || child.type === "hidden") {
5788
- const prefillVal = (_b2 = (_a3 = prefillObj == null ? void 0 : prefillObj[child.key]) != null ? _a3 : child.default) != null ? _b2 : null;
6179
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6180
+ const prefillVal = (_b2 = (_a3 = prefillObj == null ? void 0 : prefillObj[child.key]) != null ? _a3 : "default" in child ? child.default : null) != null ? _b2 : null;
5789
6181
  childWrapper.appendChild(
5790
6182
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
5791
6183
  );
@@ -5830,22 +6222,26 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
5830
6222
  inheritedReadonly: childInheritedReadonly
5831
6223
  };
5832
6224
  const item = document.createElement("div");
5833
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
6225
+ item.className = "containerItem border border-gray-300 rounded-lg p-2 bg-white";
5834
6226
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
5835
6227
  const childWrapper = document.createElement("div");
5836
- const columns = element.columns || 1;
5837
- if (columns === 1) {
5838
- childWrapper.className = "space-y-4";
6228
+ if (isSlides) {
6229
+ childWrapper.className = "space-y-2";
5839
6230
  } else {
5840
- childWrapper.className = `grid grid-cols-${columns} gap-4`;
6231
+ const columns = element.columns || 1;
6232
+ if (columns === 1) {
6233
+ childWrapper.className = "space-y-2";
6234
+ } else {
6235
+ childWrapper.className = `grid grid-cols-${columns} gap-2`;
6236
+ }
5841
6237
  }
5842
6238
  element.elements.forEach((child) => {
5843
6239
  var _a2;
5844
- if (child.hidden || child.type === "hidden") {
6240
+ if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
5845
6241
  childWrapper.appendChild(
5846
6242
  createHiddenInput(
5847
6243
  pathJoin(subCtx.path, child.key),
5848
- (_a2 = child.default) != null ? _a2 : null
6244
+ (_a2 = "default" in child ? child.default : null) != null ? _a2 : null
5849
6245
  )
5850
6246
  );
5851
6247
  } else {
@@ -6035,6 +6431,7 @@ function updateContainerField(element, fieldPath, value, context) {
6035
6431
  if (isPlainObject(itemValue)) {
6036
6432
  element.elements.forEach((childElement) => {
6037
6433
  var _a, _b;
6434
+ if (childElement.type === "markdown" || !childElement.key) return;
6038
6435
  const childPath = `${fieldPath}[${index}].${childElement.key}`;
6039
6436
  if (childElement.type === "richinput" && childElement.flatOutput) {
6040
6437
  const richChild = childElement;
@@ -6075,6 +6472,7 @@ function updateContainerField(element, fieldPath, value, context) {
6075
6472
  }
6076
6473
  element.elements.forEach((childElement) => {
6077
6474
  var _a, _b;
6475
+ if (childElement.type === "markdown" || !childElement.key) return;
6078
6476
  const childPath = `${fieldPath}.${childElement.key}`;
6079
6477
  if (childElement.type === "richinput" && childElement.flatOutput) {
6080
6478
  const richChild = childElement;
@@ -7908,7 +8306,7 @@ function filterFilesForDropdown(query, files, labels) {
7908
8306
  });
7909
8307
  }
7910
8308
  var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
7911
- var TEXTAREA_PADDING = "padding: 12px 52px 12px 14px;";
8309
+ var TEXTAREA_PADDING = "padding: 8px 40px 8px 10px;";
7912
8310
  function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7913
8311
  var _a;
7914
8312
  const state = ctx.state;
@@ -7957,7 +8355,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7957
8355
  });
7958
8356
  const errorEl = document.createElement("div");
7959
8357
  errorEl.className = "fb-richinput-error";
7960
- errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
8358
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 10px 6px;";
7961
8359
  let errorTimer = null;
7962
8360
  function showUploadError(message) {
7963
8361
  errorEl.textContent = message;
@@ -8035,7 +8433,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8035
8433
  });
8036
8434
  const filesRow = document.createElement("div");
8037
8435
  filesRow.className = "fb-richinput-files";
8038
- filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 6px; padding: 10px 14px 0; align-items: center;";
8436
+ filesRow.style.cssText = "display: none; flex-wrap: wrap; gap: 4px; padding: 6px 10px 0; align-items: center;";
8039
8437
  const fileInput = document.createElement("input");
8040
8438
  fileInput.type = "file";
8041
8439
  fileInput.multiple = true;
@@ -8171,13 +8569,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8171
8569
  paperclipBtn.title = t("richinputAttachFile", state);
8172
8570
  paperclipBtn.style.cssText = `
8173
8571
  position: absolute;
8174
- right: 10px;
8175
- bottom: 10px;
8572
+ right: 6px;
8573
+ bottom: 6px;
8176
8574
  z-index: 2;
8177
- width: 32px;
8178
- height: 32px;
8575
+ width: 28px;
8576
+ height: 28px;
8179
8577
  border: none;
8180
- border-radius: 8px;
8578
+ border-radius: 6px;
8181
8579
  background: transparent;
8182
8580
  cursor: pointer;
8183
8581
  display: flex;
@@ -8545,7 +8943,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
8545
8943
  outerDiv.appendChild(errorEl);
8546
8944
  if (element.minLength != null || element.maxLength != null) {
8547
8945
  const counterRow = document.createElement("div");
8548
- counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
8946
+ counterRow.style.cssText = "position: relative; padding: 2px 10px 4px; text-align: right;";
8549
8947
  const counter = createCharCounter(element, textarea, false);
8550
8948
  counter.style.cssText = `
8551
8949
  position: static;
@@ -8842,6 +9240,250 @@ function updateRichInputField(element, fieldPath, value, context) {
8842
9240
  }
8843
9241
  }
8844
9242
 
9243
+ // src/components/markdown/snarkdown.ts
9244
+ var TAGS = {
9245
+ "": ["<em>", "</em>"],
9246
+ _: ["<strong>", "</strong>"],
9247
+ "*": ["<strong>", "</strong>"],
9248
+ "~": ["<s>", "</s>"],
9249
+ "\n": ["<br />"],
9250
+ " ": ["<br />"],
9251
+ "-": ["<hr />"]
9252
+ };
9253
+ function outdent(str) {
9254
+ return str.replace(
9255
+ RegExp("^" + (str.match(/^(\t| )+/) || "")[0], "gm"),
9256
+ ""
9257
+ );
9258
+ }
9259
+ function encodeAttr(str) {
9260
+ return (str + "").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9261
+ }
9262
+ function parse(md, prevLinks) {
9263
+ 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;
9264
+ const context = [];
9265
+ let out = "";
9266
+ const links = prevLinks || {};
9267
+ let last = 0;
9268
+ let chunk;
9269
+ let prev;
9270
+ let token;
9271
+ let inner;
9272
+ let t2;
9273
+ function tag(token2) {
9274
+ const desc = TAGS[token2[1] || ""];
9275
+ const end = context[context.length - 1] === token2;
9276
+ if (!desc) return token2;
9277
+ if (!desc[1]) return desc[0];
9278
+ if (end) context.pop();
9279
+ else context.push(token2);
9280
+ return desc[end ? 1 : 0];
9281
+ }
9282
+ function flush() {
9283
+ let str = "";
9284
+ while (context.length) str += tag(context[context.length - 1]);
9285
+ return str;
9286
+ }
9287
+ md = md.replace(/^\[(.+?)\]:\s*(.+)$/gm, (_s, name, url) => {
9288
+ links[name.toLowerCase()] = url;
9289
+ return "";
9290
+ }).replace(/^\n+|\n+$/g, "");
9291
+ while (token = tokenizer.exec(md)) {
9292
+ prev = md.substring(last, token.index);
9293
+ last = tokenizer.lastIndex;
9294
+ chunk = token[0];
9295
+ if (prev.match(/[^\\](\\\\)*\\$/)) ; else if (t2 = token[3] || token[4]) {
9296
+ 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>";
9297
+ } else if (t2 = token[6]) {
9298
+ if (t2.match(/\./)) {
9299
+ token[5] = token[5].replace(/^\d+/gm, "");
9300
+ }
9301
+ inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, "")));
9302
+ if (t2 === ">") t2 = "blockquote";
9303
+ else {
9304
+ t2 = t2.match(/\./) ? "ol" : "ul";
9305
+ inner = inner.replace(/^(.*)(\n|$)/gm, "<li>$1</li>");
9306
+ }
9307
+ chunk = "<" + t2 + ">" + inner + "</" + t2 + ">";
9308
+ } else if (token[8]) {
9309
+ chunk = `<img src="${encodeAttr(token[8])}" alt="${encodeAttr(token[7])}">`;
9310
+ } else if (token[10]) {
9311
+ out = out.replace(
9312
+ "<a>",
9313
+ `<a href="${encodeAttr(token[11] || links[prev.toLowerCase()])}">`
9314
+ );
9315
+ chunk = flush() + "</a>";
9316
+ } else if (token[9]) {
9317
+ chunk = "<a>";
9318
+ } else if (token[12] || token[14]) {
9319
+ t2 = "h" + (token[14] ? token[14].length : token[13] > "=" ? 1 : 2);
9320
+ chunk = "<" + t2 + ">" + parse(token[12] || token[15], links) + "</" + t2 + ">";
9321
+ } else if (token[16]) {
9322
+ chunk = "<code>" + encodeAttr(token[16]) + "</code>";
9323
+ } else if (token[17] || token[1]) {
9324
+ chunk = tag(token[17] || "--");
9325
+ }
9326
+ out += prev;
9327
+ out += chunk;
9328
+ }
9329
+ return (out + md.substring(last) + flush()).replace(/^\n+|\n+$/g, "");
9330
+ }
9331
+
9332
+ // src/components/markdown/render.ts
9333
+ var STYLE_ID2 = "fb-markdown-styles";
9334
+ function ensureMarkdownStyles() {
9335
+ if (typeof document === "undefined") return;
9336
+ if (document.getElementById(STYLE_ID2)) return;
9337
+ const style = document.createElement("style");
9338
+ style.id = STYLE_ID2;
9339
+ style.setAttribute("data-fb-markdown-styles", "true");
9340
+ style.textContent = `
9341
+ .fb-markdown {
9342
+ font-family: var(--fb-font-family, inherit);
9343
+ font-size: var(--fb-font-size, 1rem);
9344
+ color: var(--fb-text-color, #1f2937);
9345
+ line-height: 1.6;
9346
+ }
9347
+ .fb-markdown h1,
9348
+ .fb-markdown h2,
9349
+ .fb-markdown h3,
9350
+ .fb-markdown h4,
9351
+ .fb-markdown h5,
9352
+ .fb-markdown h6 {
9353
+ font-weight: var(--fb-font-weight-medium, 600);
9354
+ color: var(--fb-text-color, #1f2937);
9355
+ margin-top: 0.75em;
9356
+ margin-bottom: 0.25em;
9357
+ }
9358
+ .fb-markdown h1 { font-size: 1.5rem; }
9359
+ .fb-markdown h2 { font-size: 1.25rem; }
9360
+ .fb-markdown h3 { font-size: 1.1rem; }
9361
+ .fb-markdown h4,
9362
+ .fb-markdown h5,
9363
+ .fb-markdown h6 { font-size: 1rem; }
9364
+ .fb-markdown p {
9365
+ margin-top: 0;
9366
+ margin-bottom: 0.5em;
9367
+ }
9368
+ .fb-markdown ul,
9369
+ .fb-markdown ol {
9370
+ margin: 0.25em 0 0.5em 1.5em;
9371
+ padding: 0;
9372
+ }
9373
+ .fb-markdown li {
9374
+ margin-bottom: 0.15em;
9375
+ }
9376
+ .fb-markdown a {
9377
+ color: var(--fb-primary-color, #2563eb);
9378
+ text-decoration: underline;
9379
+ }
9380
+ .fb-markdown a:hover {
9381
+ opacity: 0.8;
9382
+ }
9383
+ .fb-markdown code {
9384
+ background: var(--fb-input-background-color, #f3f4f6);
9385
+ border-radius: 3px;
9386
+ padding: 0.1em 0.35em;
9387
+ font-size: 0.9em;
9388
+ font-family: monospace;
9389
+ }
9390
+ .fb-markdown pre {
9391
+ background: var(--fb-input-background-color, #f3f4f6);
9392
+ border-radius: var(--fb-border-radius, 0.375rem);
9393
+ padding: 0.75em 1em;
9394
+ overflow-x: auto;
9395
+ margin: 0.5em 0;
9396
+ }
9397
+ .fb-markdown pre code {
9398
+ background: none;
9399
+ padding: 0;
9400
+ border-radius: 0;
9401
+ font-size: inherit;
9402
+ }
9403
+ .fb-markdown blockquote {
9404
+ border-left: 3px solid var(--fb-border-color, #d1d5db);
9405
+ margin: 0.5em 0;
9406
+ padding-left: 1em;
9407
+ color: var(--fb-text-secondary-color, #6b7280);
9408
+ }
9409
+ .fb-markdown hr {
9410
+ border: none;
9411
+ border-top: 1px solid var(--fb-border-color, #d1d5db);
9412
+ margin: 0.75em 0;
9413
+ }
9414
+ .fb-markdown strong { font-weight: var(--fb-font-weight-medium, 600); }
9415
+ .fb-markdown em { font-style: italic; }
9416
+ .fb-markdown s { text-decoration: line-through; }
9417
+ `;
9418
+ document.head.appendChild(style);
9419
+ }
9420
+ var ANCHOR_DANGEROUS_SCHEMES = [
9421
+ "javascript:",
9422
+ "data:",
9423
+ "vbscript:",
9424
+ "blob:"
9425
+ ];
9426
+ var IMG_DANGEROUS_SCHEMES = ["javascript:", "vbscript:", "blob:"];
9427
+ function isImgSrcDangerous(normalized) {
9428
+ if (IMG_DANGEROUS_SCHEMES.some((scheme) => normalized.startsWith(scheme))) {
9429
+ return true;
9430
+ }
9431
+ if (normalized.startsWith("data:") && !normalized.startsWith("data:image/")) {
9432
+ return true;
9433
+ }
9434
+ return false;
9435
+ }
9436
+ function escapeRawHtml(content) {
9437
+ return content.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
9438
+ }
9439
+ function sanitizeElements(container) {
9440
+ const anchors = container.querySelectorAll("a[href]");
9441
+ anchors.forEach((a) => {
9442
+ var _a;
9443
+ const href = (_a = a.getAttribute("href")) != null ? _a : "";
9444
+ const normalized = href.trim().toLowerCase();
9445
+ const isDangerous = ANCHOR_DANGEROUS_SCHEMES.some(
9446
+ (scheme) => normalized.startsWith(scheme)
9447
+ );
9448
+ if (isDangerous) {
9449
+ a.setAttribute("href", "#");
9450
+ }
9451
+ a.setAttribute("target", "_blank");
9452
+ a.setAttribute("rel", "noopener noreferrer");
9453
+ });
9454
+ const images = container.querySelectorAll("img[src]");
9455
+ images.forEach((img) => {
9456
+ var _a;
9457
+ const src = (_a = img.getAttribute("src")) != null ? _a : "";
9458
+ const normalized = src.trim().toLowerCase();
9459
+ if (isImgSrcDangerous(normalized)) {
9460
+ img.setAttribute("src", "");
9461
+ }
9462
+ });
9463
+ }
9464
+ function renderMarkdown(element, _ctx, parent) {
9465
+ if (typeof element.content !== "string") {
9466
+ throw new Error(
9467
+ `renderMarkdown: markdown element${element.key ? ` "${element.key}"` : ""} requires "content" to be a string (got ${element.content === null ? "null" : typeof element.content})`
9468
+ );
9469
+ }
9470
+ ensureMarkdownStyles();
9471
+ const wrapper = document.createElement("div");
9472
+ wrapper.className = "fb-markdown";
9473
+ const escaped = escapeRawHtml(element.content);
9474
+ wrapper.innerHTML = parse(escaped);
9475
+ sanitizeElements(wrapper);
9476
+ parent.appendChild(wrapper);
9477
+ return wrapper;
9478
+ }
9479
+
9480
+ // src/components/markdown/index.ts
9481
+ function validateMarkdown(_element, _key, _context) {
9482
+ return { value: void 0, errors: [], skip: true };
9483
+ }
9484
+ function updateMarkdown(_element, _fieldPath, _value, _context) {
9485
+ }
9486
+
8845
9487
  // src/components/index.ts
8846
9488
  function showTooltip(tooltipId, button) {
8847
9489
  const tooltip = document.getElementById(tooltipId);
@@ -9073,9 +9715,10 @@ function setupEnableIfListeners(wrapper, element, ctx) {
9073
9715
  });
9074
9716
  }
9075
9717
  function createFieldLabel(element) {
9718
+ var _a, _b;
9076
9719
  const title = document.createElement("label");
9077
9720
  title.className = "text-sm font-medium text-gray-900";
9078
- title.textContent = element.label || element.key;
9721
+ title.textContent = (_b = (_a = element.label) != null ? _a : element.key) != null ? _b : "";
9079
9722
  if (element.required) {
9080
9723
  const req = document.createElement("span");
9081
9724
  req.className = "text-red-500 ml-1";
@@ -9105,7 +9748,7 @@ function createInfoButton(element) {
9105
9748
  }
9106
9749
  function createLabelContainer(element) {
9107
9750
  const label = document.createElement("div");
9108
- label.className = "flex items-center mb-2";
9751
+ label.className = "flex items-center mb-1";
9109
9752
  const title = createFieldLabel(element);
9110
9753
  label.appendChild(title);
9111
9754
  if (element.description || element.hint) {
@@ -9203,9 +9846,32 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
9203
9846
  }
9204
9847
  }
9205
9848
  function renderElement2(element, ctx) {
9849
+ if (element.type === "markdown") {
9850
+ if (element.hidden === true) {
9851
+ const placeholder = document.createElement("div");
9852
+ placeholder.style.display = "none";
9853
+ placeholder.setAttribute("data-fb-hidden-markdown", "true");
9854
+ return placeholder;
9855
+ }
9856
+ const initiallyDisabled2 = shouldDisableElement(element, ctx);
9857
+ const outerWrapper = document.createElement("div");
9858
+ outerWrapper.className = "mb-2 fb-field-wrapper fb-markdown-wrapper";
9859
+ outerWrapper.setAttribute(
9860
+ "data-field-key",
9861
+ getElementLookupKey(element, ctx.state)
9862
+ );
9863
+ renderMarkdown(element, ctx, outerWrapper);
9864
+ if (initiallyDisabled2) {
9865
+ outerWrapper.style.display = "none";
9866
+ outerWrapper.classList.add("fb-field-wrapper-disabled");
9867
+ outerWrapper.setAttribute("data-conditionally-disabled", "true");
9868
+ }
9869
+ setupEnableIfListeners(outerWrapper, element, ctx);
9870
+ return outerWrapper;
9871
+ }
9206
9872
  const initiallyDisabled = shouldDisableElement(element, ctx);
9207
9873
  const wrapper = document.createElement("div");
9208
- wrapper.className = "mb-6 fb-field-wrapper";
9874
+ wrapper.className = "mb-2 fb-field-wrapper";
9209
9875
  wrapper.setAttribute("data-field-key", element.key);
9210
9876
  const label = createLabelContainer(element);
9211
9877
  wrapper.appendChild(label);
@@ -9272,12 +9938,16 @@ var defaultConfig = {
9272
9938
  hintPattern: "Format: {pattern}",
9273
9939
  fileCountSingle: "{count} file",
9274
9940
  fileCountPlural: "{count} files",
9941
+ fileCountWithMax: "{count} / {max} files",
9275
9942
  fileCountRange: "({min}-{max})",
9276
9943
  uploadingFile: "Uploading\u2026",
9277
9944
  filesCounter: "{count}/{max}",
9278
9945
  fromLibrary: "From library",
9279
9946
  libraryEmpty: "Library is empty",
9280
9947
  libraryHint: "Choose from previously uploaded files",
9948
+ dropToUpload: "Release to upload",
9949
+ replaceFile: "Replace",
9950
+ clearAll: "Clear all",
9281
9951
  pickerError: "Failed to load files from library",
9282
9952
  // Validation errors
9283
9953
  required: "Required",
@@ -9343,12 +10013,16 @@ var defaultConfig = {
9343
10013
  hintPattern: "\u0424\u043E\u0440\u043C\u0430\u0442: {pattern}",
9344
10014
  fileCountSingle: "{count} \u0444\u0430\u0439\u043B",
9345
10015
  fileCountPlural: "{count} \u0444\u0430\u0439\u043B\u043E\u0432",
10016
+ fileCountWithMax: "{count} / {max} \u0444\u0430\u0439\u043B\u043E\u0432",
9346
10017
  fileCountRange: "({min}-{max})",
9347
10018
  uploadingFile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026",
9348
10019
  filesCounter: "{count}/{max}",
9349
10020
  fromLibrary: "\u0418\u0437 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438",
9350
10021
  libraryEmpty: "\u0411\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0430 \u043F\u0443\u0441\u0442\u0430",
9351
10022
  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",
10023
+ dropToUpload: "\u041E\u0442\u043F\u0443\u0441\u0442\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C",
10024
+ replaceFile: "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C",
10025
+ clearAll: "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435",
9352
10026
  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",
9353
10027
  // Validation errors
9354
10028
  required: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
@@ -9413,7 +10087,9 @@ function createInstanceState(config) {
9413
10087
  translations: mergedTranslations
9414
10088
  },
9415
10089
  debounceTimer: null,
9416
- prefill: {}
10090
+ prefill: {},
10091
+ syntheticElementIds: /* @__PURE__ */ new WeakMap(),
10092
+ syntheticElementIdCounter: 0
9417
10093
  };
9418
10094
  }
9419
10095
  function generateInstanceId() {
@@ -9487,10 +10163,10 @@ var defaultTheme = {
9487
10163
  fileUploadHoverBorderColor: "#3b82f6",
9488
10164
  // blue-500
9489
10165
  // Spacing
9490
- inputPaddingX: "0.75rem",
9491
- // 3 (12px)
9492
- inputPaddingY: "0.5rem",
9493
- // 2 (8px)
10166
+ inputPaddingX: "0.5rem",
10167
+ // 8px (compact density v2)
10168
+ inputPaddingY: "0.25rem",
10169
+ // 4px (compact density v2)
9494
10170
  borderRadius: "0.5rem",
9495
10171
  // rounded-lg (8px)
9496
10172
  borderWidth: "1px",
@@ -9691,6 +10367,11 @@ var componentRegistry = {
9691
10367
  // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
9692
10368
  validate: validateHiddenElement,
9693
10369
  update: updateHiddenField
10370
+ },
10371
+ markdown: {
10372
+ // Display-only element — no value, no errors, skip from form data
10373
+ validate: validateMarkdown,
10374
+ update: updateMarkdown
9694
10375
  }
9695
10376
  };
9696
10377
  function getComponentOperations(elementType) {
@@ -9931,7 +10612,7 @@ var FormBuilderInstance = class {
9931
10612
  existingContainer.remove();
9932
10613
  }
9933
10614
  const actionsContainer = document.createElement("div");
9934
- actionsContainer.className = "form-level-actions-container mt-6 pt-4 flex flex-wrap gap-3 justify-center";
10615
+ actionsContainer.className = "form-level-actions-container mt-3 pt-2 flex flex-wrap gap-2 justify-center";
9935
10616
  actionsContainer.style.cssText = `
9936
10617
  border-top: var(--fb-border-width) solid var(--fb-border-color);
9937
10618
  `;
@@ -10072,7 +10753,7 @@ var FormBuilderInstance = class {
10072
10753
  */
10073
10754
  createRootPrefillHints(hints) {
10074
10755
  const hintsContainer = document.createElement("div");
10075
- hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-4";
10756
+ hintsContainer.className = "fb-prefill-hints flex flex-wrap gap-2 mb-2";
10076
10757
  hints.forEach((hint) => {
10077
10758
  const hintButton = document.createElement("button");
10078
10759
  hintButton.type = "button";
@@ -10105,7 +10786,7 @@ var FormBuilderInstance = class {
10105
10786
  root.setAttribute("data-fb-root", "true");
10106
10787
  injectThemeVariables(root, this.state.config.theme);
10107
10788
  const rootContainer = document.createElement("div");
10108
- rootContainer.className = "space-y-6";
10789
+ rootContainer.className = "space-y-2";
10109
10790
  if (schema.prefillHints && !this.state.config.readonly) {
10110
10791
  const hintsContainer = this.createRootPrefillHints(schema.prefillHints);
10111
10792
  rootContainer.appendChild(hintsContainer);
@@ -10113,13 +10794,13 @@ var FormBuilderInstance = class {
10113
10794
  const fieldsWrapper = document.createElement("div");
10114
10795
  const columns = schema.columns || 1;
10115
10796
  if (columns === 1) {
10116
- fieldsWrapper.className = "space-y-4";
10797
+ fieldsWrapper.className = "space-y-2";
10117
10798
  } else {
10118
- fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
10799
+ fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
10119
10800
  }
10120
10801
  schema.elements.forEach((element) => {
10121
10802
  var _a, _b;
10122
- if (element.hidden || element.type === "hidden") {
10803
+ if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
10123
10804
  const val = (_b = (_a = prefill == null ? void 0 : prefill[element.key]) != null ? _a : element.default) != null ? _b : null;
10124
10805
  fieldsWrapper.appendChild(createHiddenInput(element.key, val));
10125
10806
  return;
@@ -10154,7 +10835,8 @@ var FormBuilderInstance = class {
10154
10835
  const errors = [];
10155
10836
  const data = {};
10156
10837
  const validateElement2 = (element, ctx, customScopeRoot = null) => {
10157
- const key = element.key;
10838
+ var _a;
10839
+ const key = (_a = element.key) != null ? _a : "";
10158
10840
  const scopeRoot = customScopeRoot || this.state.formRoot;
10159
10841
  const componentContext = {
10160
10842
  scopeRoot,
@@ -10172,7 +10854,8 @@ var FormBuilderInstance = class {
10172
10854
  errors.push(...componentResult.errors);
10173
10855
  return {
10174
10856
  value: componentResult.value,
10175
- spread: !!componentResult.spread
10857
+ spread: !!componentResult.spread,
10858
+ skip: !!componentResult.skip
10176
10859
  };
10177
10860
  }
10178
10861
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
@@ -10194,6 +10877,9 @@ var FormBuilderInstance = class {
10194
10877
  );
10195
10878
  }
10196
10879
  }
10880
+ if (element.type === "markdown") {
10881
+ return;
10882
+ }
10197
10883
  if (element.hidden || element.type === "hidden") {
10198
10884
  const hiddenInput = this.state.formRoot.querySelector(
10199
10885
  `input[type="hidden"][data-hidden-field="true"][name="${element.key}"]`
@@ -10206,9 +10892,10 @@ var FormBuilderInstance = class {
10206
10892
  }
10207
10893
  } else {
10208
10894
  const result = validateElement2(element, { path: "" });
10895
+ if (result.skip) return;
10209
10896
  if (result.spread && result.value !== null && typeof result.value === "object") {
10210
10897
  Object.assign(data, result.value);
10211
- } else {
10898
+ } else if (element.key) {
10212
10899
  data[element.key] = result.value;
10213
10900
  }
10214
10901
  }
@@ -10284,6 +10971,7 @@ var FormBuilderInstance = class {
10284
10971
  buildHiddenFieldsData(elements) {
10285
10972
  const data = {};
10286
10973
  for (const element of elements) {
10974
+ if (element.type === "markdown" || !element.key) continue;
10287
10975
  const key = element.key;
10288
10976
  if (element.hidden && element.default !== void 0) {
10289
10977
  data[key] = element.default;
@@ -10405,6 +11093,72 @@ var FormBuilderInstance = class {
10405
11093
  );
10406
11094
  }
10407
11095
  }
11096
+ /**
11097
+ * Find the field wrapper DOM element for a given element+path combo.
11098
+ * Used by reevaluateConditionalFields.
11099
+ */
11100
+ findFieldWrapper(element, currentPath) {
11101
+ const formRoot = this.state.formRoot;
11102
+ const lookupKey = getElementLookupKey(element, this.state);
11103
+ if (!currentPath) {
11104
+ return formRoot.querySelector(`[data-field-key="${lookupKey}"]`);
11105
+ }
11106
+ const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
11107
+ if (pathMatch) {
11108
+ const containerEl2 = formRoot.querySelector(
11109
+ `[data-container-item="${pathMatch[1]}[${pathMatch[2]}]"]`
11110
+ );
11111
+ return containerEl2 ? containerEl2.querySelector(`[data-field-key="${lookupKey}"]`) : null;
11112
+ }
11113
+ const containerEl = formRoot.querySelector(
11114
+ `[data-container="${currentPath}"]`
11115
+ );
11116
+ return containerEl ? containerEl.querySelector(`[data-field-key="${lookupKey}"]`) : null;
11117
+ }
11118
+ /**
11119
+ * Apply enableIf show/hide logic to a single field wrapper.
11120
+ * Extracted to reduce cyclomatic complexity of checkElements.
11121
+ */
11122
+ applyEnableIfVisibility(element, wrapper, currentPath, fullPath, formData) {
11123
+ var _a, _b, _c, _d;
11124
+ try {
11125
+ const scope = (_a = element.enableIf.scope) != null ? _a : "relative";
11126
+ const containerData = scope === "relative" && currentPath ? getValueByPath(formData, currentPath) : void 0;
11127
+ const shouldEnable = evaluateEnableCondition(
11128
+ element.enableIf,
11129
+ formData,
11130
+ containerData
11131
+ );
11132
+ const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
11133
+ if (shouldEnable && isCurrentlyDisabled) {
11134
+ const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
11135
+ const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
11136
+ const newWrapper = renderElement2(element, {
11137
+ path: currentPath,
11138
+ prefill: prefillContext,
11139
+ formData,
11140
+ state: this.state,
11141
+ instance: this
11142
+ });
11143
+ (_b = wrapper.parentNode) == null ? void 0 : _b.replaceChild(newWrapper, wrapper);
11144
+ } else if (!shouldEnable && !isCurrentlyDisabled) {
11145
+ const disabledWrapper = document.createElement("div");
11146
+ disabledWrapper.className = "fb-field-wrapper-disabled";
11147
+ disabledWrapper.style.display = "none";
11148
+ disabledWrapper.setAttribute(
11149
+ "data-field-key",
11150
+ getElementLookupKey(element, this.state)
11151
+ );
11152
+ disabledWrapper.setAttribute("data-conditionally-disabled", "true");
11153
+ (_c = wrapper.parentNode) == null ? void 0 : _c.replaceChild(disabledWrapper, wrapper);
11154
+ }
11155
+ } catch (error) {
11156
+ console.error(
11157
+ `Error re-evaluating enableIf for field "${(_d = element.key) != null ? _d : "<no key>"}" at path "${fullPath}":`,
11158
+ error
11159
+ );
11160
+ }
11161
+ }
10408
11162
  /**
10409
11163
  * Re-evaluate all conditional fields (enableIf) based on current form data
10410
11164
  * This is called automatically when form data changes (via onChange events)
@@ -10414,98 +11168,32 @@ var FormBuilderInstance = class {
10414
11168
  const formData = this.validateForm(true).data;
10415
11169
  const checkElements = (elements, currentPath) => {
10416
11170
  elements.forEach((element) => {
10417
- var _a, _b, _c;
10418
- const fullPath = currentPath ? `${currentPath}.${element.key}` : element.key;
11171
+ var _a, _b;
11172
+ const fullPath = currentPath ? `${currentPath}.${(_a = element.key) != null ? _a : ""}` : (_b = element.key) != null ? _b : "";
10419
11173
  if (element.enableIf) {
10420
- let fieldWrapper = null;
10421
- if (currentPath) {
10422
- const pathMatch = currentPath.match(/^(.+)\[(\d+)\]$/);
10423
- if (pathMatch) {
10424
- const containerKey = pathMatch[1];
10425
- const containerIndex = pathMatch[2];
10426
- const containerElement = this.state.formRoot.querySelector(
10427
- `[data-container-item="${containerKey}[${containerIndex}]"]`
10428
- );
10429
- if (containerElement) {
10430
- fieldWrapper = containerElement.querySelector(
10431
- `[data-field-key="${element.key}"]`
10432
- );
10433
- }
10434
- } else {
10435
- const containerElement = this.state.formRoot.querySelector(
10436
- `[data-container="${currentPath}"]`
10437
- );
10438
- if (containerElement) {
10439
- fieldWrapper = containerElement.querySelector(
10440
- `[data-field-key="${element.key}"]`
10441
- );
10442
- }
10443
- }
10444
- } else {
10445
- fieldWrapper = this.state.formRoot.querySelector(
10446
- `[data-field-key="${element.key}"]`
10447
- );
10448
- }
11174
+ const fieldWrapper = this.findFieldWrapper(element, currentPath);
10449
11175
  if (fieldWrapper) {
10450
- const wrapper = fieldWrapper;
10451
- try {
10452
- let containerData = void 0;
10453
- const scope = (_a = element.enableIf.scope) != null ? _a : "relative";
10454
- if (scope === "relative" && currentPath) {
10455
- containerData = getValueByPath(formData, currentPath);
10456
- }
10457
- const shouldEnable = evaluateEnableCondition(
10458
- element.enableIf,
10459
- formData,
10460
- // Use complete formData for absolute scope
10461
- containerData
10462
- // Use container-specific data for relative scope
10463
- );
10464
- const isCurrentlyDisabled = wrapper.getAttribute("data-conditionally-disabled") === "true";
10465
- if (shouldEnable && isCurrentlyDisabled) {
10466
- const containerPrefill = currentPath ? getValueByPath(formData, currentPath) : formData;
10467
- const prefillContext = containerPrefill && typeof containerPrefill === "object" ? containerPrefill : {};
10468
- const newWrapper = renderElement2(element, {
10469
- path: currentPath,
10470
- // Use container path (empty string for root-level)
10471
- prefill: prefillContext,
10472
- formData,
10473
- // Pass complete formData for enableIf evaluation
10474
- state: this.state,
10475
- instance: this
10476
- });
10477
- (_b = wrapper.parentNode) == null ? void 0 : _b.replaceChild(newWrapper, wrapper);
10478
- } else if (!shouldEnable && !isCurrentlyDisabled) {
10479
- const disabledWrapper = document.createElement("div");
10480
- disabledWrapper.className = "fb-field-wrapper-disabled";
10481
- disabledWrapper.style.display = "none";
10482
- disabledWrapper.setAttribute("data-field-key", element.key);
10483
- disabledWrapper.setAttribute(
10484
- "data-conditionally-disabled",
10485
- "true"
10486
- );
10487
- (_c = wrapper.parentNode) == null ? void 0 : _c.replaceChild(disabledWrapper, wrapper);
10488
- }
10489
- } catch (error) {
10490
- console.error(
10491
- `Error re-evaluating enableIf for field "${element.key}" at path "${fullPath}":`,
10492
- error
10493
- );
10494
- }
11176
+ this.applyEnableIfVisibility(
11177
+ element,
11178
+ fieldWrapper,
11179
+ currentPath,
11180
+ fullPath,
11181
+ formData
11182
+ );
10495
11183
  }
10496
11184
  }
10497
11185
  if ((element.type === "container" || element.type === "group") && "elements" in element && element.elements) {
10498
- const containerData = formData == null ? void 0 : formData[element.key];
11186
+ const containerData = element.key ? formData == null ? void 0 : formData[element.key] : void 0;
10499
11187
  if (Array.isArray(containerData)) {
10500
11188
  const containerItems = this.state.formRoot.querySelectorAll(
10501
11189
  `[data-container-item]`
10502
11190
  );
10503
- const directItems = Array.from(containerItems).filter((el) => {
11191
+ const directItems = fullPath ? Array.from(containerItems).filter((el) => {
10504
11192
  const attr = el.getAttribute("data-container-item") || "";
10505
11193
  if (!attr.startsWith(`${fullPath}[`)) return false;
10506
11194
  const suffix = attr.slice(fullPath.length);
10507
11195
  return /^\[\d+\]$/.test(suffix);
10508
- });
11196
+ }) : [];
10509
11197
  directItems.forEach((el) => {
10510
11198
  const attr = el.getAttribute("data-container-item") || "";
10511
11199
  checkElements(element.elements, attr);