@nubitio/crud 0.5.14 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -28,7 +28,6 @@ let react_dom = require("react-dom");
28
28
  let _nubitio_ui = require("@nubitio/ui");
29
29
  let react_jsx_runtime = require("react/jsx-runtime");
30
30
  let _nubitio_core = require("@nubitio/core");
31
- let react_dropzone = require("react-dropzone");
32
31
  let _tanstack_react_query = require("@tanstack/react-query");
33
32
  //#region packages/crud/crud/defineResource.ts
34
33
  const stringResourceCache = /* @__PURE__ */ new Map();
@@ -1243,9 +1242,6 @@ const enumTypeModule = {
1243
1242
  };
1244
1243
  //#endregion
1245
1244
  //#region packages/crud/form/FileUploadField.tsx
1246
- function cx$1(...values) {
1247
- return values.filter(Boolean).join(" ");
1248
- }
1249
1245
  function resolveMediaPath(media) {
1250
1246
  if (!media) return null;
1251
1247
  const path = media["path"];
@@ -1271,27 +1267,6 @@ function resolveMediaIri(uploadUrl, media) {
1271
1267
  function isImageMimeType(mimeType) {
1272
1268
  return !!mimeType && mimeType.startsWith("image/");
1273
1269
  }
1274
- function buildDropzoneAccept(accept) {
1275
- if (!accept || accept === "*/*" || accept === "*") return void 0;
1276
- if (accept === "image/*") return {
1277
- "image/png": [".png"],
1278
- "image/jpeg": [".jpg", ".jpeg"],
1279
- "image/webp": [".webp"],
1280
- "image/gif": [".gif"]
1281
- };
1282
- if (accept.includes(",")) return accept.split(",").reduce((acc, token) => {
1283
- const trimmed = token.trim();
1284
- if (!trimmed) return acc;
1285
- if (trimmed.startsWith(".")) {
1286
- acc["application/octet-stream"] = [...acc["application/octet-stream"] ?? [], trimmed];
1287
- return acc;
1288
- }
1289
- acc[trimmed] = [];
1290
- return acc;
1291
- }, {});
1292
- if (accept.startsWith(".")) return { "application/octet-stream": [accept] };
1293
- return { [accept]: [] };
1294
- }
1295
1270
  async function uploadMediaFile(file, uploadUrl, httpClient) {
1296
1271
  const body = new FormData();
1297
1272
  body.append("file", file);
@@ -1371,17 +1346,6 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1371
1346
  t,
1372
1347
  uploadUrl
1373
1348
  ]);
1374
- const { getRootProps, getInputProps, isDragActive, open } = (0, react_dropzone.useDropzone)({
1375
- accept: buildDropzoneAccept(field.accept),
1376
- disabled: disabled || readOnly || status === "uploading",
1377
- multiple: false,
1378
- noClick: !!(previewUrl || fileName),
1379
- noKeyboard: !!(previewUrl || fileName),
1380
- onDrop: (acceptedFiles) => {
1381
- const file = acceptedFiles[0];
1382
- if (file) uploadFile(file);
1383
- }
1384
- });
1385
1349
  const handleClear = () => {
1386
1350
  revokeLocalPreview();
1387
1351
  setPreviewUrl(null);
@@ -1391,104 +1355,35 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1391
1355
  setErrorMessage(null);
1392
1356
  onCleared(field.name);
1393
1357
  };
1394
- const isInteractive = !disabled && !readOnly;
1395
- const hasContent = !!(previewUrl || fileName);
1396
- const placeholderIcon = imageMode ? "ph-image" : "ph-file-arrow-up";
1397
- const placeholderTitle = isDragActive ? t("form.fileUploadDrop") : imageMode ? t("form.imageUploadPrompt") : t("form.fileUploadPrompt");
1398
- const placeholderHint = imageMode ? t("form.imageUploadHint") : t("form.fileUploadHint");
1399
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1400
- className: cx$1("nb-form__file-upload", imageMode && "nb-form__file-upload--image", invalid && "nb-form__file-upload--invalid"),
1401
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1402
- ...getRootProps({ className: cx$1("nb-form__file-upload-zone", isDragActive && "nb-form__file-upload-zone--active", hasContent && "nb-form__file-upload-zone--filled", status === "uploading" && "nb-form__file-upload-zone--uploading", !isInteractive && "nb-form__file-upload-zone--disabled") }),
1403
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps({
1404
- id: `nb-form-${field.name}`,
1405
- "aria-label": field.label
1406
- }) }), hasContent ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1407
- previewUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1408
- className: "nb-form__file-upload-preview",
1409
- src: previewUrl,
1410
- alt: field.label
1411
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1412
- className: "nb-form__file-upload-file",
1413
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1414
- className: "nb-form__file-upload-file-icon",
1415
- "aria-hidden": "true",
1416
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "ph ph-file" })
1417
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1418
- className: "nb-form__file-upload-file-meta",
1419
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1420
- className: "nb-form__file-upload-file-name",
1421
- children: fileName
1422
- }), fileUrl && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1423
- className: "nb-form__file-upload-file-link",
1424
- href: fileUrl,
1425
- target: "_blank",
1426
- rel: "noreferrer",
1427
- onClick: (event) => event.stopPropagation(),
1428
- children: t("form.fileUploadOpen")
1429
- })]
1430
- })]
1431
- }),
1432
- status === "uploading" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1433
- className: "nb-form__file-upload-overlay",
1434
- "aria-live": "polite",
1435
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1436
- className: "nb-form__file-upload-spinner",
1437
- "aria-hidden": "true"
1438
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("form.fileUploading") })]
1439
- }),
1440
- isInteractive && status !== "uploading" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1441
- className: "nb-form__file-upload-actions",
1442
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1443
- type: "button",
1444
- className: "nb-form__file-upload-action",
1445
- onClick: (event) => {
1446
- event.stopPropagation();
1447
- open();
1448
- },
1449
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1450
- className: "ph ph-arrows-clockwise",
1451
- "aria-hidden": "true"
1452
- }), t("form.fileUploadReplace")]
1453
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1454
- type: "button",
1455
- className: "nb-form__file-upload-action nb-form__file-upload-action--danger",
1456
- onClick: (event) => {
1457
- event.stopPropagation();
1458
- handleClear();
1459
- },
1460
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1461
- className: "ph ph-trash",
1462
- "aria-hidden": "true"
1463
- }), t("form.fileUploadRemove")]
1464
- })]
1465
- })
1466
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1467
- className: "nb-form__file-upload-placeholder",
1468
- children: [
1469
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1470
- className: "nb-form__file-upload-icon",
1471
- "aria-hidden": "true",
1472
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: `ph ${placeholderIcon}` })
1473
- }),
1474
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1475
- className: "nb-form__file-upload-title",
1476
- children: placeholderTitle
1477
- }),
1478
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1479
- className: "nb-form__file-upload-hint",
1480
- children: placeholderHint
1481
- })
1482
- ]
1483
- })]
1484
- }), errorMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1485
- className: "nb-form__file-upload-error",
1486
- role: "alert",
1487
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
1488
- className: "ph ph-warning-circle",
1489
- "aria-hidden": "true"
1490
- }), errorMessage]
1491
- })]
1358
+ const value = {
1359
+ fileName,
1360
+ fileUrl,
1361
+ previewUrl
1362
+ };
1363
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.FileDropzone, {
1364
+ accept: field.accept,
1365
+ disabled,
1366
+ readOnly,
1367
+ invalid,
1368
+ image: imageMode,
1369
+ value,
1370
+ uploading: status === "uploading",
1371
+ error: errorMessage,
1372
+ inputId: `nb-form-${field.name}`,
1373
+ inputLabel: field.label,
1374
+ labels: {
1375
+ dropPrompt: t("form.fileUploadDrop"),
1376
+ prompt: t("form.fileUploadPrompt"),
1377
+ imagePrompt: t("form.imageUploadPrompt"),
1378
+ hint: t("form.fileUploadHint"),
1379
+ imageHint: t("form.imageUploadHint"),
1380
+ uploading: t("form.fileUploading"),
1381
+ replace: t("form.fileUploadReplace"),
1382
+ remove: t("form.fileUploadRemove"),
1383
+ open: t("form.fileUploadOpen")
1384
+ },
1385
+ onFileSelect: (file) => void uploadFile(file),
1386
+ onClear: handleClear
1492
1387
  });
1493
1388
  }
1494
1389
  function isImageFileField(field) {
@@ -3671,6 +3566,7 @@ function normalizeFormData(data, fields, adapter = HydraAdapter, prependDataByFi
3671
3566
  else row[field.name] = null;
3672
3567
  }
3673
3568
  if (field.type === "entity") normalizeEntityField(row, field, adapter, prependDataByField);
3569
+ if (field.type !== "entity" && field.type !== "file" && (Array.isArray(row[field.name]) || row[field.name] !== null && typeof row[field.name] === "object")) delete row[field.name];
3674
3570
  });
3675
3571
  return row;
3676
3572
  }
@@ -4304,9 +4200,20 @@ function validateField(field, value, formData, t) {
4304
4200
  }
4305
4201
  return null;
4306
4202
  }
4203
+ const DETAIL_CURRENCY_COL_WIDTH = 112;
4204
+ const DETAIL_NUMBER_COL_WIDTH = 96;
4205
+ function resolveDetailColWidth(field, colWidths) {
4206
+ if (colWidths[field.name] !== void 0) return colWidths[field.name];
4207
+ if (field.width !== void 0) {
4208
+ const parsed = typeof field.width === "number" ? field.width : Number.parseInt(String(field.width), 10);
4209
+ return Number.isFinite(parsed) ? parsed : void 0;
4210
+ }
4211
+ if (field.type === "currency") return DETAIL_CURRENCY_COL_WIDTH;
4212
+ if (field.type === "number") return DETAIL_NUMBER_COL_WIDTH;
4213
+ }
4307
4214
  function DetailColumnGroup({ allowDeleting, fields, colWidths }) {
4308
4215
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("colgroup", { children: [fields.map((field) => {
4309
- const width = colWidths[field.name] ?? field.width;
4216
+ const width = resolveDetailColWidth(field, colWidths);
4310
4217
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { style: width ? { width } : void 0 }, field.name);
4311
4218
  }), allowDeleting && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("col", { className: "nb-form__col-actions-col" })] });
4312
4219
  }
@@ -4316,7 +4223,6 @@ function DetailSummaryFooter({ allowDeleting, colWidths, fields, rows, scrollRef
4316
4223
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4317
4224
  ref: scrollRef,
4318
4225
  className: "nb-form__detail-summary-wrap",
4319
- "aria-hidden": "true",
4320
4226
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
4321
4227
  className: "nb-form__detail-table nb-form__detail-summary-table",
4322
4228
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailColumnGroup, {
@@ -4328,11 +4234,16 @@ function DetailSummaryFooter({ allowDeleting, colWidths, fields, rows, scrollRef
4328
4234
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [fields.map((field) => {
4329
4235
  const item = itemsByColumn.get(field.name);
4330
4236
  const align = item?.align ?? field.align;
4237
+ const alignItems = align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start";
4238
+ const colWidth = item ? resolveDetailColWidth(field, colWidths) : void 0;
4331
4239
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
4332
- style: align ? { textAlign: align } : void 0,
4240
+ style: {
4241
+ ...align ? { textAlign: align } : void 0,
4242
+ ...colWidth ? { minWidth: colWidth } : void 0
4243
+ },
4333
4244
  children: item && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4334
4245
  className: "nb-form__detail-summary-cell",
4335
- style: { justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start" },
4246
+ style: { alignItems },
4336
4247
  children: [item.label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4337
4248
  className: "nb-form__detail-summary-label",
4338
4249
  children: item.label
@@ -7221,6 +7132,22 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7221
7132
  };
7222
7133
  }
7223
7134
  //#endregion
7135
+ //#region packages/crud/crud/applyFormDetailFormFieldOverrides.ts
7136
+ /**
7137
+ * Hides the header form field that mirrors `formDetail.propertyName`.
7138
+ * OneToMany collections serialized as object arrays must not render as a
7139
+ * plain text input ([object Object]) when line items are edited via formDetail.
7140
+ */
7141
+ function applyFormDetailFormFieldOverrides(fields, formDetail) {
7142
+ const propertyName = formDetail?.propertyName?.trim();
7143
+ if (!propertyName) return fields;
7144
+ return fields.map((field) => field.name === propertyName ? {
7145
+ ...field,
7146
+ visibleOnForm: false,
7147
+ hidden: true
7148
+ } : field);
7149
+ }
7150
+ //#endregion
7224
7151
  //#region packages/crud/crud/fieldValidation.ts
7225
7152
  var SmartCrudFieldContractError = class extends Error {
7226
7153
  issues;
@@ -7663,6 +7590,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7663
7590
  const { activeOperation, formData, handleFormDataChange, startCreate, startEdit, resetOperation } = useSmartCrudOperation(void 0, routingState);
7664
7591
  const roles = useSmartCrudRoles();
7665
7592
  const { gridFields, processedFields, computedValues } = useSmartCrudFields(fields, activeOperation, formData, (0, react.useMemo)(() => roles ?? [], [roles]));
7593
+ const formFields = (0, react.useMemo)(() => applyFormDetailFormFieldOverrides(processedFields, resolvedBaseResource.formDetail), [processedFields, resolvedBaseResource.formDetail]);
7666
7594
  (0, _nubitio_core.useMercureSubscription)(resource.apiUrl, () => {
7667
7595
  effectiveGridRef.current?.refresh();
7668
7596
  }, resolvedBaseResource.mercure !== false);
@@ -7672,7 +7600,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7672
7600
  ...!hasManualFields ? { fields: gridFields } : {},
7673
7601
  apiUrl: normalizedApiUrl,
7674
7602
  fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7675
- formFields: processedFields,
7603
+ formFields,
7676
7604
  formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7677
7605
  _supportedOperations: supportedOperations
7678
7606
  }), [
@@ -7681,7 +7609,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7681
7609
  hasManualFields,
7682
7610
  inferredFormLayout,
7683
7611
  normalizedApiUrl,
7684
- processedFields,
7612
+ formFields,
7685
7613
  resolvedBaseResource,
7686
7614
  resource.fields,
7687
7615
  supportedOperations
package/dist/index.mjs CHANGED
@@ -1,10 +1,9 @@
1
1
  import React, { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
2
2
  import { Route, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom";
3
3
  import { createPortal } from "react-dom";
4
- import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton, Timeline, TimelineItem } from "@nubitio/ui";
4
+ import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, FileDropzone, IconButton, Skeleton, Timeline, TimelineItem } from "@nubitio/ui";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { createCrudEvents, createScopedEventBus, getCoreCurrency, getCoreLocale, getCoreTimezone, useCoreHttpClient, useCoreRuntime, useCoreTranslation, useEvents, useMercureSubscription } from "@nubitio/core";
7
- import { useDropzone } from "react-dropzone";
8
7
  import { useQueryClient } from "@tanstack/react-query";
9
8
  //#region packages/crud/crud/defineResource.ts
10
9
  const stringResourceCache = /* @__PURE__ */ new Map();
@@ -1219,9 +1218,6 @@ const enumTypeModule = {
1219
1218
  };
1220
1219
  //#endregion
1221
1220
  //#region packages/crud/form/FileUploadField.tsx
1222
- function cx$1(...values) {
1223
- return values.filter(Boolean).join(" ");
1224
- }
1225
1221
  function resolveMediaPath(media) {
1226
1222
  if (!media) return null;
1227
1223
  const path = media["path"];
@@ -1247,27 +1243,6 @@ function resolveMediaIri(uploadUrl, media) {
1247
1243
  function isImageMimeType(mimeType) {
1248
1244
  return !!mimeType && mimeType.startsWith("image/");
1249
1245
  }
1250
- function buildDropzoneAccept(accept) {
1251
- if (!accept || accept === "*/*" || accept === "*") return void 0;
1252
- if (accept === "image/*") return {
1253
- "image/png": [".png"],
1254
- "image/jpeg": [".jpg", ".jpeg"],
1255
- "image/webp": [".webp"],
1256
- "image/gif": [".gif"]
1257
- };
1258
- if (accept.includes(",")) return accept.split(",").reduce((acc, token) => {
1259
- const trimmed = token.trim();
1260
- if (!trimmed) return acc;
1261
- if (trimmed.startsWith(".")) {
1262
- acc["application/octet-stream"] = [...acc["application/octet-stream"] ?? [], trimmed];
1263
- return acc;
1264
- }
1265
- acc[trimmed] = [];
1266
- return acc;
1267
- }, {});
1268
- if (accept.startsWith(".")) return { "application/octet-stream": [accept] };
1269
- return { [accept]: [] };
1270
- }
1271
1246
  async function uploadMediaFile(file, uploadUrl, httpClient) {
1272
1247
  const body = new FormData();
1273
1248
  body.append("file", file);
@@ -1347,17 +1322,6 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1347
1322
  t,
1348
1323
  uploadUrl
1349
1324
  ]);
1350
- const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
1351
- accept: buildDropzoneAccept(field.accept),
1352
- disabled: disabled || readOnly || status === "uploading",
1353
- multiple: false,
1354
- noClick: !!(previewUrl || fileName),
1355
- noKeyboard: !!(previewUrl || fileName),
1356
- onDrop: (acceptedFiles) => {
1357
- const file = acceptedFiles[0];
1358
- if (file) uploadFile(file);
1359
- }
1360
- });
1361
1325
  const handleClear = () => {
1362
1326
  revokeLocalPreview();
1363
1327
  setPreviewUrl(null);
@@ -1367,104 +1331,35 @@ function FileUploadField({ field, disabled = false, readOnly = false, invalid =
1367
1331
  setErrorMessage(null);
1368
1332
  onCleared(field.name);
1369
1333
  };
1370
- const isInteractive = !disabled && !readOnly;
1371
- const hasContent = !!(previewUrl || fileName);
1372
- const placeholderIcon = imageMode ? "ph-image" : "ph-file-arrow-up";
1373
- const placeholderTitle = isDragActive ? t("form.fileUploadDrop") : imageMode ? t("form.imageUploadPrompt") : t("form.fileUploadPrompt");
1374
- const placeholderHint = imageMode ? t("form.imageUploadHint") : t("form.fileUploadHint");
1375
- return /* @__PURE__ */ jsxs("div", {
1376
- className: cx$1("nb-form__file-upload", imageMode && "nb-form__file-upload--image", invalid && "nb-form__file-upload--invalid"),
1377
- children: [/* @__PURE__ */ jsxs("div", {
1378
- ...getRootProps({ className: cx$1("nb-form__file-upload-zone", isDragActive && "nb-form__file-upload-zone--active", hasContent && "nb-form__file-upload-zone--filled", status === "uploading" && "nb-form__file-upload-zone--uploading", !isInteractive && "nb-form__file-upload-zone--disabled") }),
1379
- children: [/* @__PURE__ */ jsx("input", { ...getInputProps({
1380
- id: `nb-form-${field.name}`,
1381
- "aria-label": field.label
1382
- }) }), hasContent ? /* @__PURE__ */ jsxs(Fragment, { children: [
1383
- previewUrl ? /* @__PURE__ */ jsx("img", {
1384
- className: "nb-form__file-upload-preview",
1385
- src: previewUrl,
1386
- alt: field.label
1387
- }) : /* @__PURE__ */ jsxs("div", {
1388
- className: "nb-form__file-upload-file",
1389
- children: [/* @__PURE__ */ jsx("span", {
1390
- className: "nb-form__file-upload-file-icon",
1391
- "aria-hidden": "true",
1392
- children: /* @__PURE__ */ jsx("i", { className: "ph ph-file" })
1393
- }), /* @__PURE__ */ jsxs("div", {
1394
- className: "nb-form__file-upload-file-meta",
1395
- children: [/* @__PURE__ */ jsx("span", {
1396
- className: "nb-form__file-upload-file-name",
1397
- children: fileName
1398
- }), fileUrl && /* @__PURE__ */ jsx("a", {
1399
- className: "nb-form__file-upload-file-link",
1400
- href: fileUrl,
1401
- target: "_blank",
1402
- rel: "noreferrer",
1403
- onClick: (event) => event.stopPropagation(),
1404
- children: t("form.fileUploadOpen")
1405
- })]
1406
- })]
1407
- }),
1408
- status === "uploading" && /* @__PURE__ */ jsxs("div", {
1409
- className: "nb-form__file-upload-overlay",
1410
- "aria-live": "polite",
1411
- children: [/* @__PURE__ */ jsx("span", {
1412
- className: "nb-form__file-upload-spinner",
1413
- "aria-hidden": "true"
1414
- }), /* @__PURE__ */ jsx("span", { children: t("form.fileUploading") })]
1415
- }),
1416
- isInteractive && status !== "uploading" && /* @__PURE__ */ jsxs("div", {
1417
- className: "nb-form__file-upload-actions",
1418
- children: [/* @__PURE__ */ jsxs("button", {
1419
- type: "button",
1420
- className: "nb-form__file-upload-action",
1421
- onClick: (event) => {
1422
- event.stopPropagation();
1423
- open();
1424
- },
1425
- children: [/* @__PURE__ */ jsx("i", {
1426
- className: "ph ph-arrows-clockwise",
1427
- "aria-hidden": "true"
1428
- }), t("form.fileUploadReplace")]
1429
- }), /* @__PURE__ */ jsxs("button", {
1430
- type: "button",
1431
- className: "nb-form__file-upload-action nb-form__file-upload-action--danger",
1432
- onClick: (event) => {
1433
- event.stopPropagation();
1434
- handleClear();
1435
- },
1436
- children: [/* @__PURE__ */ jsx("i", {
1437
- className: "ph ph-trash",
1438
- "aria-hidden": "true"
1439
- }), t("form.fileUploadRemove")]
1440
- })]
1441
- })
1442
- ] }) : /* @__PURE__ */ jsxs("div", {
1443
- className: "nb-form__file-upload-placeholder",
1444
- children: [
1445
- /* @__PURE__ */ jsx("span", {
1446
- className: "nb-form__file-upload-icon",
1447
- "aria-hidden": "true",
1448
- children: /* @__PURE__ */ jsx("i", { className: `ph ${placeholderIcon}` })
1449
- }),
1450
- /* @__PURE__ */ jsx("span", {
1451
- className: "nb-form__file-upload-title",
1452
- children: placeholderTitle
1453
- }),
1454
- /* @__PURE__ */ jsx("span", {
1455
- className: "nb-form__file-upload-hint",
1456
- children: placeholderHint
1457
- })
1458
- ]
1459
- })]
1460
- }), errorMessage && /* @__PURE__ */ jsxs("span", {
1461
- className: "nb-form__file-upload-error",
1462
- role: "alert",
1463
- children: [/* @__PURE__ */ jsx("i", {
1464
- className: "ph ph-warning-circle",
1465
- "aria-hidden": "true"
1466
- }), errorMessage]
1467
- })]
1334
+ const value = {
1335
+ fileName,
1336
+ fileUrl,
1337
+ previewUrl
1338
+ };
1339
+ return /* @__PURE__ */ jsx(FileDropzone, {
1340
+ accept: field.accept,
1341
+ disabled,
1342
+ readOnly,
1343
+ invalid,
1344
+ image: imageMode,
1345
+ value,
1346
+ uploading: status === "uploading",
1347
+ error: errorMessage,
1348
+ inputId: `nb-form-${field.name}`,
1349
+ inputLabel: field.label,
1350
+ labels: {
1351
+ dropPrompt: t("form.fileUploadDrop"),
1352
+ prompt: t("form.fileUploadPrompt"),
1353
+ imagePrompt: t("form.imageUploadPrompt"),
1354
+ hint: t("form.fileUploadHint"),
1355
+ imageHint: t("form.imageUploadHint"),
1356
+ uploading: t("form.fileUploading"),
1357
+ replace: t("form.fileUploadReplace"),
1358
+ remove: t("form.fileUploadRemove"),
1359
+ open: t("form.fileUploadOpen")
1360
+ },
1361
+ onFileSelect: (file) => void uploadFile(file),
1362
+ onClear: handleClear
1468
1363
  });
1469
1364
  }
1470
1365
  function isImageFileField(field) {
@@ -3647,6 +3542,7 @@ function normalizeFormData(data, fields, adapter = HydraAdapter, prependDataByFi
3647
3542
  else row[field.name] = null;
3648
3543
  }
3649
3544
  if (field.type === "entity") normalizeEntityField(row, field, adapter, prependDataByField);
3545
+ if (field.type !== "entity" && field.type !== "file" && (Array.isArray(row[field.name]) || row[field.name] !== null && typeof row[field.name] === "object")) delete row[field.name];
3650
3546
  });
3651
3547
  return row;
3652
3548
  }
@@ -4280,9 +4176,20 @@ function validateField(field, value, formData, t) {
4280
4176
  }
4281
4177
  return null;
4282
4178
  }
4179
+ const DETAIL_CURRENCY_COL_WIDTH = 112;
4180
+ const DETAIL_NUMBER_COL_WIDTH = 96;
4181
+ function resolveDetailColWidth(field, colWidths) {
4182
+ if (colWidths[field.name] !== void 0) return colWidths[field.name];
4183
+ if (field.width !== void 0) {
4184
+ const parsed = typeof field.width === "number" ? field.width : Number.parseInt(String(field.width), 10);
4185
+ return Number.isFinite(parsed) ? parsed : void 0;
4186
+ }
4187
+ if (field.type === "currency") return DETAIL_CURRENCY_COL_WIDTH;
4188
+ if (field.type === "number") return DETAIL_NUMBER_COL_WIDTH;
4189
+ }
4283
4190
  function DetailColumnGroup({ allowDeleting, fields, colWidths }) {
4284
4191
  return /* @__PURE__ */ jsxs("colgroup", { children: [fields.map((field) => {
4285
- const width = colWidths[field.name] ?? field.width;
4192
+ const width = resolveDetailColWidth(field, colWidths);
4286
4193
  return /* @__PURE__ */ jsx("col", { style: width ? { width } : void 0 }, field.name);
4287
4194
  }), allowDeleting && /* @__PURE__ */ jsx("col", { className: "nb-form__col-actions-col" })] });
4288
4195
  }
@@ -4292,7 +4199,6 @@ function DetailSummaryFooter({ allowDeleting, colWidths, fields, rows, scrollRef
4292
4199
  return /* @__PURE__ */ jsx("div", {
4293
4200
  ref: scrollRef,
4294
4201
  className: "nb-form__detail-summary-wrap",
4295
- "aria-hidden": "true",
4296
4202
  children: /* @__PURE__ */ jsxs("table", {
4297
4203
  className: "nb-form__detail-table nb-form__detail-summary-table",
4298
4204
  children: [/* @__PURE__ */ jsx(DetailColumnGroup, {
@@ -4304,11 +4210,16 @@ function DetailSummaryFooter({ allowDeleting, colWidths, fields, rows, scrollRef
4304
4210
  children: /* @__PURE__ */ jsxs("tr", { children: [fields.map((field) => {
4305
4211
  const item = itemsByColumn.get(field.name);
4306
4212
  const align = item?.align ?? field.align;
4213
+ const alignItems = align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start";
4214
+ const colWidth = item ? resolveDetailColWidth(field, colWidths) : void 0;
4307
4215
  return /* @__PURE__ */ jsx("td", {
4308
- style: align ? { textAlign: align } : void 0,
4216
+ style: {
4217
+ ...align ? { textAlign: align } : void 0,
4218
+ ...colWidth ? { minWidth: colWidth } : void 0
4219
+ },
4309
4220
  children: item && /* @__PURE__ */ jsxs("div", {
4310
4221
  className: "nb-form__detail-summary-cell",
4311
- style: { justifyContent: align === "center" ? "center" : align === "right" ? "flex-end" : "flex-start" },
4222
+ style: { alignItems },
4312
4223
  children: [item.label && /* @__PURE__ */ jsx("span", {
4313
4224
  className: "nb-form__detail-summary-label",
4314
4225
  children: item.label
@@ -7197,6 +7108,22 @@ function useSmartCrudFields(fields, activeOperation, formData, roles) {
7197
7108
  };
7198
7109
  }
7199
7110
  //#endregion
7111
+ //#region packages/crud/crud/applyFormDetailFormFieldOverrides.ts
7112
+ /**
7113
+ * Hides the header form field that mirrors `formDetail.propertyName`.
7114
+ * OneToMany collections serialized as object arrays must not render as a
7115
+ * plain text input ([object Object]) when line items are edited via formDetail.
7116
+ */
7117
+ function applyFormDetailFormFieldOverrides(fields, formDetail) {
7118
+ const propertyName = formDetail?.propertyName?.trim();
7119
+ if (!propertyName) return fields;
7120
+ return fields.map((field) => field.name === propertyName ? {
7121
+ ...field,
7122
+ visibleOnForm: false,
7123
+ hidden: true
7124
+ } : field);
7125
+ }
7126
+ //#endregion
7200
7127
  //#region packages/crud/crud/fieldValidation.ts
7201
7128
  var SmartCrudFieldContractError = class extends Error {
7202
7129
  issues;
@@ -7639,6 +7566,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7639
7566
  const { activeOperation, formData, handleFormDataChange, startCreate, startEdit, resetOperation } = useSmartCrudOperation(void 0, routingState);
7640
7567
  const roles = useSmartCrudRoles();
7641
7568
  const { gridFields, processedFields, computedValues } = useSmartCrudFields(fields, activeOperation, formData, useMemo(() => roles ?? [], [roles]));
7569
+ const formFields = useMemo(() => applyFormDetailFormFieldOverrides(processedFields, resolvedBaseResource.formDetail), [processedFields, resolvedBaseResource.formDetail]);
7642
7570
  useMercureSubscription(resource.apiUrl, () => {
7643
7571
  effectiveGridRef.current?.refresh();
7644
7572
  }, resolvedBaseResource.mercure !== false);
@@ -7648,7 +7576,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7648
7576
  ...!hasManualFields ? { fields: gridFields } : {},
7649
7577
  apiUrl: normalizedApiUrl,
7650
7578
  fields: hasManualFields ? buildFields(resource.fields) : gridFields,
7651
- formFields: processedFields,
7579
+ formFields,
7652
7580
  formLayout: resolvedBaseResource.formLayout ?? inferredFormLayout,
7653
7581
  _supportedOperations: supportedOperations
7654
7582
  }), [
@@ -7657,7 +7585,7 @@ function SmartCrudPage({ resource, fieldOverrides, formRef, onSelectionChanged,
7657
7585
  hasManualFields,
7658
7586
  inferredFormLayout,
7659
7587
  normalizedApiUrl,
7660
- processedFields,
7588
+ formFields,
7661
7589
  resolvedBaseResource,
7662
7590
  resource.fields,
7663
7591
  supportedOperations
package/dist/style.css CHANGED
@@ -1705,471 +1705,6 @@ html[data-density=compact] .nb-datagrid .nb-badge {
1705
1705
  box-shadow: none;
1706
1706
  }
1707
1707
  }
1708
- .nb-form__file-upload {
1709
- display: flex;
1710
- flex-direction: column;
1711
- gap: var(--space-1);
1712
- width: 100%;
1713
- }
1714
-
1715
- .nb-form__file-upload-zone {
1716
- align-items: center;
1717
- background: var(--surface-1);
1718
- border: 1px dashed var(--border-color);
1719
- border-radius: var(--radius-lg);
1720
- box-sizing: border-box;
1721
- cursor: pointer;
1722
- display: flex;
1723
- justify-content: center;
1724
- min-height: 112px;
1725
- overflow: hidden;
1726
- position: relative;
1727
- transition: border-color var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
1728
- width: 100%;
1729
- }
1730
- .nb-form__file-upload-zone:hover:not(.nb-form__file-upload-zone--disabled):not(.nb-form__file-upload-zone--filled) {
1731
- background: color-mix(in srgb, var(--accent-color) 4%, var(--surface-1));
1732
- border-color: var(--accent-color);
1733
- }
1734
- .nb-form__file-upload-zone--active {
1735
- background: color-mix(in srgb, var(--accent-color) 8%, var(--surface-1));
1736
- border-color: var(--accent-color);
1737
- }
1738
- .nb-form__file-upload-zone--filled {
1739
- border-style: solid;
1740
- cursor: default;
1741
- min-height: 88px;
1742
- }
1743
- .nb-form__file-upload-zone--uploading {
1744
- pointer-events: none;
1745
- }
1746
- .nb-form__file-upload-zone--disabled {
1747
- cursor: not-allowed;
1748
- opacity: 0.72;
1749
- }
1750
- .nb-form__file-upload-zone:focus-visible {
1751
- box-shadow: 0 0 0 3px var(--focus-ring-color);
1752
- outline: none;
1753
- }
1754
-
1755
- .nb-form__file-upload--image .nb-form__file-upload-zone {
1756
- min-height: 168px;
1757
- }
1758
-
1759
- .nb-form__file-upload--image .nb-form__file-upload-zone--filled {
1760
- min-height: 180px;
1761
- }
1762
-
1763
- .nb-form__file-upload--invalid .nb-form__file-upload-zone {
1764
- border-color: var(--error-color);
1765
- }
1766
-
1767
- .nb-form__file-upload-placeholder {
1768
- align-items: center;
1769
- display: flex;
1770
- flex-direction: column;
1771
- gap: var(--space-2);
1772
- max-width: 320px;
1773
- padding: var(--space-4);
1774
- text-align: center;
1775
- }
1776
-
1777
- .nb-form__file-upload-icon {
1778
- align-items: center;
1779
- background: color-mix(in srgb, var(--accent-color) 10%, transparent);
1780
- border-radius: 999px;
1781
- color: var(--accent-color);
1782
- display: inline-flex;
1783
- font-size: 24px;
1784
- height: 48px;
1785
- justify-content: center;
1786
- width: 48px;
1787
- }
1788
-
1789
- .nb-form__file-upload-title {
1790
- color: var(--text-primary);
1791
- font-size: var(--font-size-sm);
1792
- font-weight: var(--font-weight-semibold);
1793
- }
1794
-
1795
- .nb-form__file-upload-hint {
1796
- color: var(--text-tertiary);
1797
- font-size: var(--font-size-xs);
1798
- line-height: var(--line-height-tight);
1799
- }
1800
-
1801
- .nb-form__file-upload-preview {
1802
- display: block;
1803
- height: 100%;
1804
- max-height: 220px;
1805
- object-fit: contain;
1806
- width: 100%;
1807
- }
1808
-
1809
- .nb-form__file-upload-file {
1810
- align-items: center;
1811
- display: flex;
1812
- gap: var(--space-3);
1813
- max-width: 100%;
1814
- padding: var(--space-3) var(--space-4);
1815
- width: 100%;
1816
- }
1817
-
1818
- .nb-form__file-upload-file-icon {
1819
- align-items: center;
1820
- background: var(--surface-0);
1821
- border: 1px solid var(--border-subtle);
1822
- border-radius: var(--radius-md);
1823
- color: var(--accent-color);
1824
- display: inline-flex;
1825
- flex: 0 0 auto;
1826
- font-size: 22px;
1827
- height: 44px;
1828
- justify-content: center;
1829
- width: 44px;
1830
- }
1831
-
1832
- .nb-form__file-upload-file-meta {
1833
- display: flex;
1834
- flex: 1 1 auto;
1835
- flex-direction: column;
1836
- gap: 2px;
1837
- min-width: 0;
1838
- }
1839
-
1840
- .nb-form__file-upload-file-name {
1841
- color: var(--text-primary);
1842
- font-size: var(--font-size-sm);
1843
- font-weight: var(--font-weight-medium);
1844
- overflow: hidden;
1845
- text-overflow: ellipsis;
1846
- white-space: nowrap;
1847
- }
1848
-
1849
- .nb-form__file-upload-file-link {
1850
- color: var(--accent-color);
1851
- font-size: var(--font-size-xs);
1852
- text-decoration: none;
1853
- }
1854
- .nb-form__file-upload-file-link:hover {
1855
- text-decoration: underline;
1856
- }
1857
-
1858
- .nb-form__file-upload-overlay {
1859
- align-items: center;
1860
- background: rgba(0, 0, 0, 0.42);
1861
- color: #fff;
1862
- display: flex;
1863
- flex-direction: column;
1864
- font-size: var(--font-size-sm);
1865
- gap: var(--space-2);
1866
- inset: 0;
1867
- justify-content: center;
1868
- position: absolute;
1869
- }
1870
-
1871
- .nb-form__file-upload-spinner {
1872
- animation: nb-form-file-spin 700ms linear infinite;
1873
- border: 2px solid rgba(255, 255, 255, 0.35);
1874
- border-radius: 999px;
1875
- border-top-color: #fff;
1876
- height: 24px;
1877
- width: 24px;
1878
- }
1879
-
1880
- @keyframes nb-form-file-spin {
1881
- to {
1882
- transform: rotate(360deg);
1883
- }
1884
- }
1885
- .nb-form__file-upload-actions {
1886
- align-items: center;
1887
- background: linear-gradient(to top, rgba(0, 0, 0, 0.58), transparent);
1888
- bottom: 0;
1889
- display: flex;
1890
- gap: var(--space-2);
1891
- inset-inline: 0;
1892
- justify-content: center;
1893
- opacity: 0;
1894
- padding: var(--space-3);
1895
- position: absolute;
1896
- transition: opacity var(--transition-base);
1897
- }
1898
-
1899
- .nb-form__file-upload-zone--filled:hover .nb-form__file-upload-actions,
1900
- .nb-form__file-upload-zone--filled:focus-within .nb-form__file-upload-actions {
1901
- opacity: 1;
1902
- }
1903
-
1904
- .nb-form__file-upload-action {
1905
- align-items: center;
1906
- background: var(--surface-1);
1907
- border: 1px solid var(--border-subtle);
1908
- border-radius: var(--radius-md);
1909
- color: var(--text-primary);
1910
- cursor: pointer;
1911
- display: inline-flex;
1912
- font: inherit;
1913
- font-size: var(--font-size-xs);
1914
- font-weight: var(--font-weight-medium);
1915
- gap: var(--space-1);
1916
- min-height: 28px;
1917
- padding: 0 var(--space-2);
1918
- transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
1919
- }
1920
- .nb-form__file-upload-action:hover {
1921
- border-color: var(--accent-color);
1922
- color: var(--accent-color);
1923
- }
1924
- .nb-form__file-upload-action--danger:hover {
1925
- border-color: var(--error-color);
1926
- color: var(--error-color);
1927
- }
1928
- .nb-form__file-upload-action:focus-visible {
1929
- box-shadow: 0 0 0 2px var(--focus-ring-color);
1930
- outline: none;
1931
- }
1932
-
1933
- .nb-form__file-upload-error {
1934
- align-items: center;
1935
- color: var(--error-color);
1936
- display: inline-flex;
1937
- font-size: var(--font-size-xs);
1938
- gap: var(--space-1);
1939
- }
1940
- .nb-form__file-upload {
1941
- display: flex;
1942
- flex-direction: column;
1943
- gap: var(--space-1);
1944
- width: 100%;
1945
- }
1946
-
1947
- .nb-form__file-upload-zone {
1948
- align-items: center;
1949
- background: var(--surface-1);
1950
- border: 1px dashed var(--border-color);
1951
- border-radius: var(--radius-lg);
1952
- box-sizing: border-box;
1953
- cursor: pointer;
1954
- display: flex;
1955
- justify-content: center;
1956
- min-height: 112px;
1957
- overflow: hidden;
1958
- position: relative;
1959
- transition: border-color var(--transition-base), background var(--transition-base), box-shadow var(--transition-base);
1960
- width: 100%;
1961
- }
1962
- .nb-form__file-upload-zone:hover:not(.nb-form__file-upload-zone--disabled):not(.nb-form__file-upload-zone--filled) {
1963
- background: color-mix(in srgb, var(--accent-color) 4%, var(--surface-1));
1964
- border-color: var(--accent-color);
1965
- }
1966
- .nb-form__file-upload-zone--active {
1967
- background: color-mix(in srgb, var(--accent-color) 8%, var(--surface-1));
1968
- border-color: var(--accent-color);
1969
- }
1970
- .nb-form__file-upload-zone--filled {
1971
- border-style: solid;
1972
- cursor: default;
1973
- min-height: 88px;
1974
- }
1975
- .nb-form__file-upload-zone--uploading {
1976
- pointer-events: none;
1977
- }
1978
- .nb-form__file-upload-zone--disabled {
1979
- cursor: not-allowed;
1980
- opacity: 0.72;
1981
- }
1982
- .nb-form__file-upload-zone:focus-visible {
1983
- box-shadow: 0 0 0 3px var(--focus-ring-color);
1984
- outline: none;
1985
- }
1986
-
1987
- .nb-form__file-upload--image .nb-form__file-upload-zone {
1988
- min-height: 168px;
1989
- }
1990
-
1991
- .nb-form__file-upload--image .nb-form__file-upload-zone--filled {
1992
- min-height: 180px;
1993
- }
1994
-
1995
- .nb-form__file-upload--invalid .nb-form__file-upload-zone {
1996
- border-color: var(--error-color);
1997
- }
1998
-
1999
- .nb-form__file-upload-placeholder {
2000
- align-items: center;
2001
- display: flex;
2002
- flex-direction: column;
2003
- gap: var(--space-2);
2004
- max-width: 320px;
2005
- padding: var(--space-4);
2006
- text-align: center;
2007
- }
2008
-
2009
- .nb-form__file-upload-icon {
2010
- align-items: center;
2011
- background: color-mix(in srgb, var(--accent-color) 10%, transparent);
2012
- border-radius: 999px;
2013
- color: var(--accent-color);
2014
- display: inline-flex;
2015
- font-size: 24px;
2016
- height: 48px;
2017
- justify-content: center;
2018
- width: 48px;
2019
- }
2020
-
2021
- .nb-form__file-upload-title {
2022
- color: var(--text-primary);
2023
- font-size: var(--font-size-sm);
2024
- font-weight: var(--font-weight-semibold);
2025
- }
2026
-
2027
- .nb-form__file-upload-hint {
2028
- color: var(--text-tertiary);
2029
- font-size: var(--font-size-xs);
2030
- line-height: var(--line-height-tight);
2031
- }
2032
-
2033
- .nb-form__file-upload-preview {
2034
- display: block;
2035
- height: 100%;
2036
- max-height: 220px;
2037
- object-fit: contain;
2038
- width: 100%;
2039
- }
2040
-
2041
- .nb-form__file-upload-file {
2042
- align-items: center;
2043
- display: flex;
2044
- gap: var(--space-3);
2045
- max-width: 100%;
2046
- padding: var(--space-3) var(--space-4);
2047
- width: 100%;
2048
- }
2049
-
2050
- .nb-form__file-upload-file-icon {
2051
- align-items: center;
2052
- background: var(--surface-0);
2053
- border: 1px solid var(--border-subtle);
2054
- border-radius: var(--radius-md);
2055
- color: var(--accent-color);
2056
- display: inline-flex;
2057
- flex: 0 0 auto;
2058
- font-size: 22px;
2059
- height: 44px;
2060
- justify-content: center;
2061
- width: 44px;
2062
- }
2063
-
2064
- .nb-form__file-upload-file-meta {
2065
- display: flex;
2066
- flex: 1 1 auto;
2067
- flex-direction: column;
2068
- gap: 2px;
2069
- min-width: 0;
2070
- }
2071
-
2072
- .nb-form__file-upload-file-name {
2073
- color: var(--text-primary);
2074
- font-size: var(--font-size-sm);
2075
- font-weight: var(--font-weight-medium);
2076
- overflow: hidden;
2077
- text-overflow: ellipsis;
2078
- white-space: nowrap;
2079
- }
2080
-
2081
- .nb-form__file-upload-file-link {
2082
- color: var(--accent-color);
2083
- font-size: var(--font-size-xs);
2084
- text-decoration: none;
2085
- }
2086
- .nb-form__file-upload-file-link:hover {
2087
- text-decoration: underline;
2088
- }
2089
-
2090
- .nb-form__file-upload-overlay {
2091
- align-items: center;
2092
- background: rgba(0, 0, 0, 0.42);
2093
- color: #fff;
2094
- display: flex;
2095
- flex-direction: column;
2096
- font-size: var(--font-size-sm);
2097
- gap: var(--space-2);
2098
- inset: 0;
2099
- justify-content: center;
2100
- position: absolute;
2101
- }
2102
-
2103
- .nb-form__file-upload-spinner {
2104
- animation: nb-form-file-spin 700ms linear infinite;
2105
- border: 2px solid rgba(255, 255, 255, 0.35);
2106
- border-radius: 999px;
2107
- border-top-color: #fff;
2108
- height: 24px;
2109
- width: 24px;
2110
- }
2111
-
2112
- @keyframes nb-form-file-spin {
2113
- to {
2114
- transform: rotate(360deg);
2115
- }
2116
- }
2117
- .nb-form__file-upload-actions {
2118
- align-items: center;
2119
- background: linear-gradient(to top, rgba(0, 0, 0, 0.58), transparent);
2120
- bottom: 0;
2121
- display: flex;
2122
- gap: var(--space-2);
2123
- inset-inline: 0;
2124
- justify-content: center;
2125
- opacity: 0;
2126
- padding: var(--space-3);
2127
- position: absolute;
2128
- transition: opacity var(--transition-base);
2129
- }
2130
-
2131
- .nb-form__file-upload-zone--filled:hover .nb-form__file-upload-actions,
2132
- .nb-form__file-upload-zone--filled:focus-within .nb-form__file-upload-actions {
2133
- opacity: 1;
2134
- }
2135
-
2136
- .nb-form__file-upload-action {
2137
- align-items: center;
2138
- background: var(--surface-1);
2139
- border: 1px solid var(--border-subtle);
2140
- border-radius: var(--radius-md);
2141
- color: var(--text-primary);
2142
- cursor: pointer;
2143
- display: inline-flex;
2144
- font: inherit;
2145
- font-size: var(--font-size-xs);
2146
- font-weight: var(--font-weight-medium);
2147
- gap: var(--space-1);
2148
- min-height: 28px;
2149
- padding: 0 var(--space-2);
2150
- transition: background var(--transition-base), border-color var(--transition-base), color var(--transition-base);
2151
- }
2152
- .nb-form__file-upload-action:hover {
2153
- border-color: var(--accent-color);
2154
- color: var(--accent-color);
2155
- }
2156
- .nb-form__file-upload-action--danger:hover {
2157
- border-color: var(--error-color);
2158
- color: var(--error-color);
2159
- }
2160
- .nb-form__file-upload-action:focus-visible {
2161
- box-shadow: 0 0 0 2px var(--focus-ring-color);
2162
- outline: none;
2163
- }
2164
-
2165
- .nb-form__file-upload-error {
2166
- align-items: center;
2167
- color: var(--error-color);
2168
- display: inline-flex;
2169
- font-size: var(--font-size-xs);
2170
- gap: var(--space-1);
2171
- }
2172
-
2173
1708
  .nb-form {
2174
1709
  color: var(--text-primary);
2175
1710
  display: flex;
@@ -2926,6 +2461,7 @@ html[data-density=compact] .nb-datagrid .nb-badge {
2926
2461
  font-size: var(--font-size-sm);
2927
2462
  font-weight: var(--font-weight-semibold);
2928
2463
  min-height: 38px;
2464
+ overflow: visible;
2929
2465
  padding: 8px;
2930
2466
  vertical-align: middle;
2931
2467
  }
@@ -2949,7 +2485,8 @@ html[data-density=compact] .nb-datagrid .nb-badge {
2949
2485
  background: var(--surface-2);
2950
2486
  border-top: 0;
2951
2487
  flex: 0 0 auto;
2952
- overflow: hidden;
2488
+ overflow-x: auto;
2489
+ overflow-y: hidden;
2953
2490
  scrollbar-width: none;
2954
2491
  }
2955
2492
  .nb-form__detail-summary-wrap::-webkit-scrollbar {
@@ -2962,10 +2499,12 @@ html[data-density=compact] .nb-datagrid .nb-badge {
2962
2499
  }
2963
2500
 
2964
2501
  .nb-form__detail-summary-cell {
2965
- align-items: baseline;
2502
+ align-items: flex-end;
2966
2503
  display: inline-flex;
2967
- gap: var(--space-2);
2504
+ flex-direction: column;
2505
+ gap: 2px;
2968
2506
  min-width: 100%;
2507
+ overflow: visible;
2969
2508
  }
2970
2509
 
2971
2510
  .nb-form__detail-summary-label {
@@ -2978,9 +2517,12 @@ html[data-density=compact] .nb-datagrid .nb-badge {
2978
2517
 
2979
2518
  .nb-form__detail-summary-value {
2980
2519
  color: var(--text-primary);
2520
+ display: block;
2981
2521
  font-size: var(--font-size-md);
2982
2522
  font-weight: var(--font-weight-bold);
2523
+ max-width: none;
2983
2524
  white-space: nowrap;
2525
+ width: max-content;
2984
2526
  }
2985
2527
 
2986
2528
  .nb-form__detail-table td .nb-form__control,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/crud",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "type": "module",
5
5
  "description": "Declarative CRUD engine with field DSL, forms, datagrids, RBAC, conditional logic and pluggable adapters (Hydra/REST).",
6
6
  "license": "MIT",
@@ -56,10 +56,7 @@
56
56
  "react-dom": "^19.0.0",
57
57
  "react-i18next": "^14.0.0",
58
58
  "react-router-dom": "^6.0.0",
59
- "@nubitio/core": "^0.5.14",
60
- "@nubitio/ui": "^0.5.14"
61
- },
62
- "dependencies": {
63
- "react-dropzone": "^15.0.0"
59
+ "@nubitio/core": "^0.5.16",
60
+ "@nubitio/ui": "^0.5.16"
64
61
  }
65
62
  }