@nubitio/crud 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5310,12 +5310,61 @@ function useCrudPage(resource, externalFormRef) {
5310
5310
  }
5311
5311
  //#endregion
5312
5312
  //#region packages/crud/crud/AuditTrailPanel.tsx
5313
- const ACTION_COLORS = {
5314
- create: "#2e7d32",
5315
- update: "#1565c0",
5316
- delete: "#c62828"
5313
+ const ACTION_BADGE = {
5314
+ create: "success",
5315
+ update: "info",
5316
+ delete: "danger"
5317
5317
  };
5318
- function DefaultEntryRenderer({ entry }) {
5318
+ const ACTION_TONE = {
5319
+ create: "success",
5320
+ update: "info",
5321
+ delete: "danger"
5322
+ };
5323
+ function formatAuditValue(value, yesLabel, noLabel) {
5324
+ if (value == null || value === "") return "—";
5325
+ if (typeof value === "boolean") return value ? yesLabel : noLabel;
5326
+ if (typeof value === "object") try {
5327
+ return JSON.stringify(value);
5328
+ } catch {
5329
+ return String(value);
5330
+ }
5331
+ return String(value);
5332
+ }
5333
+ function DefaultEntryContent({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5334
+ const changeKeys = Object.keys(entry.changes);
5335
+ if (changeKeys.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, {});
5336
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5337
+ className: "nb-audit-trail__changes",
5338
+ children: changeKeys.map((field) => {
5339
+ const { before, after } = entry.changes[field];
5340
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5341
+ className: "nb-audit-trail__change",
5342
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5343
+ className: "nb-audit-trail__field",
5344
+ children: resolveFieldLabel(field)
5345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5346
+ className: "nb-audit-trail__diff",
5347
+ children: [
5348
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5349
+ className: "nb-audit-trail__value nb-audit-trail__value--before",
5350
+ children: formatAuditValue(before, yesLabel, noLabel)
5351
+ }),
5352
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5353
+ className: "nb-audit-trail__arrow",
5354
+ "aria-hidden": "true",
5355
+ children: "→"
5356
+ }),
5357
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5358
+ className: "nb-audit-trail__value nb-audit-trail__value--after",
5359
+ children: formatAuditValue(after, yesLabel, noLabel)
5360
+ })
5361
+ ]
5362
+ })]
5363
+ }, field);
5364
+ })
5365
+ });
5366
+ }
5367
+ function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5319
5368
  const { t } = (0, _nubitio_core.useCoreTranslation)();
5320
5369
  const date = new Date(entry.timestamp);
5321
5370
  const formatted = Number.isNaN(date.getTime()) ? entry.timestamp : date.toLocaleString((0, _nubitio_core.getCoreLocale)(), { timeZone: (0, _nubitio_core.getCoreTimezone)() });
@@ -5324,177 +5373,159 @@ function DefaultEntryRenderer({ entry }) {
5324
5373
  update: t("auditTrail.action.update"),
5325
5374
  delete: t("auditTrail.action.delete")
5326
5375
  };
5327
- const changeKeys = Object.keys(entry.changes);
5328
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5329
- style: {
5330
- borderBottom: "1px solid #e0e0e0",
5331
- padding: "10px 0",
5332
- listStyle: "none"
5333
- },
5334
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5335
- style: {
5336
- display: "flex",
5337
- alignItems: "center",
5338
- gap: 8,
5339
- marginBottom: 4
5340
- },
5341
- children: [
5342
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5343
- style: {
5344
- fontSize: 12,
5345
- color: "#757575"
5346
- },
5347
- children: formatted
5348
- }),
5349
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5350
- style: {
5351
- fontSize: 12,
5352
- color: "#424242"
5353
- },
5354
- children: entry.user
5355
- }),
5356
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5357
- style: {
5358
- fontSize: 11,
5359
- fontWeight: 600,
5360
- padding: "1px 6px",
5361
- borderRadius: 4,
5362
- backgroundColor: ACTION_COLORS[entry.action],
5363
- color: "#fff"
5364
- },
5365
- children: actionLabels[entry.action]
5366
- })
5367
- ]
5368
- }), changeKeys.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5369
- style: {
5370
- margin: "4px 0 0 0",
5371
- padding: "0 0 0 16px"
5372
- },
5373
- children: changeKeys.map((field) => {
5374
- const { before, after } = entry.changes[field];
5375
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
5376
- style: {
5377
- fontSize: 12,
5378
- color: "#616161"
5379
- },
5380
- children: [
5381
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("strong", { children: [field, ":"] }),
5382
- " ",
5383
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5384
- style: { color: "#c62828" },
5385
- children: String(before ?? "—")
5386
- }),
5387
- " → ",
5388
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5389
- style: { color: "#2e7d32" },
5390
- children: String(after ?? "—")
5391
- })
5392
- ]
5393
- }, field);
5376
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.TimelineItem, {
5377
+ status: "complete",
5378
+ tone: ACTION_TONE[entry.action],
5379
+ title: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5380
+ className: "nb-audit-trail__meta",
5381
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5382
+ className: "nb-audit-trail__user",
5383
+ children: entry.user
5384
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
5385
+ variant: ACTION_BADGE[entry.action],
5386
+ size: "sm",
5387
+ pill: true,
5388
+ children: actionLabels[entry.action]
5389
+ })]
5390
+ }),
5391
+ timestamp: formatted,
5392
+ dateTime: entry.timestamp,
5393
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultEntryContent, {
5394
+ entry,
5395
+ resolveFieldLabel,
5396
+ yesLabel,
5397
+ noLabel
5398
+ })
5399
+ });
5400
+ }
5401
+ function AuditTrailSkeleton() {
5402
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5403
+ className: "nb-audit-trail__skeleton",
5404
+ "aria-busy": "true",
5405
+ children: [
5406
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
5407
+ variant: "rect",
5408
+ height: 72
5409
+ }),
5410
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
5411
+ variant: "rect",
5412
+ height: 72
5413
+ }),
5414
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Skeleton, {
5415
+ variant: "rect",
5416
+ height: 72
5394
5417
  })
5395
- })]
5418
+ ]
5396
5419
  });
5397
5420
  }
5398
- function AuditTrailPanel({ url, renderEntry, visible, onClose }) {
5421
+ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, resolveFieldLabel, drawerSize = "sm" }) {
5399
5422
  const { t } = (0, _nubitio_core.useCoreTranslation)();
5400
5423
  const httpClient = (0, _nubitio_core.useCoreHttpClient)();
5401
5424
  const [fetchState, setFetchState] = (0, react.useState)({ status: "idle" });
5402
- (0, react.useEffect)(() => {
5403
- if (!visible || url === null) {
5425
+ const yesLabel = t("common.yes");
5426
+ const noLabel = t("common.no");
5427
+ const loadEntries = (0, react.useCallback)(() => {
5428
+ if (url === null) {
5404
5429
  setFetchState({ status: "idle" });
5405
5430
  return;
5406
5431
  }
5407
- let cancelled = false;
5408
5432
  setFetchState({ status: "loading" });
5409
5433
  httpClient.get(url).then((response) => {
5410
- if (!cancelled) setFetchState({
5434
+ setFetchState({
5411
5435
  status: "success",
5412
5436
  entries: response.data
5413
5437
  });
5414
5438
  }).catch(() => {
5415
- if (!cancelled) setFetchState({ status: "error" });
5439
+ setFetchState({ status: "error" });
5416
5440
  });
5417
- return () => {
5418
- cancelled = true;
5419
- };
5420
- }, [
5421
- httpClient,
5422
- url,
5423
- visible
5424
- ]);
5425
- if (!visible) return null;
5426
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
5427
- className: "nb-audit-trail-panel",
5428
- style: {
5429
- border: "1px solid #e0e0e0",
5430
- borderRadius: 4,
5431
- padding: "12px 16px",
5432
- backgroundColor: "#fafafa",
5433
- minWidth: 280
5434
- },
5435
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5436
- style: {
5437
- display: "flex",
5438
- alignItems: "center",
5439
- justifyContent: "space-between",
5440
- marginBottom: 12
5441
- },
5442
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
5443
- style: { fontSize: 14 },
5444
- children: t("auditTrail.title")
5445
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5446
- type: "button",
5447
- onClick: onClose,
5448
- "aria-label": t("auditTrail.closeButton"),
5449
- style: {
5450
- background: "none",
5451
- border: "none",
5452
- cursor: "pointer",
5453
- fontSize: 18,
5454
- lineHeight: 1,
5455
- padding: "0 4px",
5456
- color: "#616161"
5457
- },
5458
- children: "×"
5459
- })]
5460
- }), url === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5461
- style: {
5462
- fontSize: 13,
5463
- color: "#757575",
5464
- margin: 0
5465
- },
5466
- children: t("auditTrail.selectRecord")
5467
- }) : fetchState.status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5468
- style: {
5469
- fontSize: 13,
5470
- color: "#757575",
5471
- margin: 0
5472
- },
5473
- children: t("auditTrail.loading")
5474
- }) : fetchState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5475
- style: {
5476
- fontSize: 13,
5477
- color: "#c62828",
5478
- margin: 0
5479
- },
5480
- children: t("auditTrail.error")
5481
- }) : fetchState.status === "success" ? fetchState.entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5482
- style: {
5483
- fontSize: 13,
5484
- color: "#757575",
5485
- margin: 0
5486
- },
5487
- children: t("auditTrail.empty")
5488
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5489
- style: {
5490
- margin: 0,
5491
- padding: 0
5492
- },
5493
- children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultEntryRenderer, { entry }, String(entry.id)))
5494
- }) : null]
5441
+ }, [httpClient, url]);
5442
+ (0, react.useEffect)(() => {
5443
+ if (!visible) {
5444
+ setFetchState({ status: "idle" });
5445
+ return;
5446
+ }
5447
+ loadEntries();
5448
+ }, [loadEntries, visible]);
5449
+ const drawerTitle = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("auditTrail.title") }), recordSubtitle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5450
+ className: "nb-audit-trail__subtitle",
5451
+ children: recordSubtitle
5452
+ })] });
5453
+ const body = (() => {
5454
+ if (url === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5455
+ fill: true,
5456
+ icon: "cursor-click",
5457
+ title: t("auditTrail.selectRecord"),
5458
+ description: t("auditTrail.selectRecordHint"),
5459
+ size: "sm"
5460
+ });
5461
+ if (fetchState.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuditTrailSkeleton, {});
5462
+ if (fetchState.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5463
+ className: "nb-audit-trail__error",
5464
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5465
+ fill: true,
5466
+ variant: "danger",
5467
+ icon: "warning-circle",
5468
+ title: t("auditTrail.error"),
5469
+ size: "sm",
5470
+ action: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Button, {
5471
+ variant: "secondary",
5472
+ size: "sm",
5473
+ onClick: loadEntries,
5474
+ children: t("auditTrail.retry")
5475
+ })
5476
+ })
5477
+ });
5478
+ if (fetchState.status === "success" && fetchState.entries.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.EmptyState, {
5479
+ fill: true,
5480
+ icon: "clock-counter-clockwise",
5481
+ title: t("auditTrail.empty"),
5482
+ description: t("auditTrail.emptyHint"),
5483
+ size: "sm"
5484
+ });
5485
+ if (fetchState.status === "success") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Timeline, {
5486
+ variant: "log",
5487
+ "aria-label": t("auditTrail.title"),
5488
+ children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.default.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultEntryRenderer, {
5489
+ entry,
5490
+ resolveFieldLabel,
5491
+ yesLabel,
5492
+ noLabel
5493
+ }, String(entry.id)))
5494
+ });
5495
+ return null;
5496
+ })();
5497
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Drawer, {
5498
+ isOpen: visible,
5499
+ onClose,
5500
+ title: drawerTitle,
5501
+ width: resolveDrawerWidth({ drawerSize }),
5502
+ side: "right",
5503
+ scrim: "subtle",
5504
+ closeLabel: t("auditTrail.closeButton"),
5505
+ "aria-label": t("auditTrail.title"),
5506
+ className: "nb-audit-trail-drawer",
5507
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5508
+ className: "nb-audit-trail",
5509
+ children: body
5510
+ })
5495
5511
  });
5496
5512
  }
5497
5513
  //#endregion
5514
+ //#region packages/crud/crud/AuditTrail.ts
5515
+ function createAuditFieldLabelResolver(config, fields) {
5516
+ const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5517
+ return (field) => {
5518
+ const fromConfig = config?.fieldLabels;
5519
+ if (fromConfig) {
5520
+ if (typeof fromConfig === "function") {
5521
+ const resolved = fromConfig(field);
5522
+ if (resolved) return resolved;
5523
+ } else if (fromConfig[field]) return fromConfig[field];
5524
+ }
5525
+ return fieldLabelByName.get(field) ?? field;
5526
+ };
5527
+ }
5528
+ //#endregion
5498
5529
  //#region packages/crud/crud/dialogStore.ts
5499
5530
  function initialDialogState() {
5500
5531
  return {
@@ -5842,13 +5873,30 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5842
5873
  if (hasAuditTrail) {
5843
5874
  const first = rows[0];
5844
5875
  if (first != null) setSelectedRowId(first["id"] ?? first["ID"] ?? null);
5876
+ else setSelectedRowId(null);
5845
5877
  }
5846
5878
  };
5879
+ const selectedRow = selectedRows[0] ?? null;
5847
5880
  const auditUrl = (0, react.useMemo)(() => {
5848
5881
  if (!resolvedResource.auditTrail?.enabled || selectedRowId == null) return null;
5849
5882
  const { apiUrl } = resolvedResource.auditTrail;
5850
5883
  return typeof apiUrl === "function" ? apiUrl(selectedRowId) : `${apiUrl}${selectedRowId}`;
5851
5884
  }, [resolvedResource.auditTrail, selectedRowId]);
5885
+ const resolveAuditFieldLabel = (0, react.useMemo)(() => createAuditFieldLabelResolver(resolvedResource.auditTrail, routeAwareGridFields), [resolvedResource.auditTrail, routeAwareGridFields]);
5886
+ const auditRecordSubtitle = (0, react.useMemo)(() => {
5887
+ if (!resolvedResource.auditTrail?.recordSubtitle || selectedRow == null) return void 0;
5888
+ return resolvedResource.auditTrail.recordSubtitle(selectedRow);
5889
+ }, [resolvedResource.auditTrail, selectedRow]);
5890
+ const openAuditTrail = (0, react.useCallback)((row) => {
5891
+ if (row != null) {
5892
+ const id = row["id"] ?? row["ID"] ?? null;
5893
+ if (id != null) {
5894
+ setSelectedRowId(id);
5895
+ setSelectedRows([row]);
5896
+ }
5897
+ }
5898
+ setAuditOpen(true);
5899
+ }, []);
5852
5900
  const executeBulkAction = (0, react.useCallback)(async (action) => {
5853
5901
  await action.onAction(selectionState.selectedIds);
5854
5902
  selectionState.clearSelection();
@@ -5862,7 +5910,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5862
5910
  else executeBulkAction(action);
5863
5911
  }, [executeBulkAction]);
5864
5912
  const toolbar = (0, react.useMemo)(() => {
5865
- return resolveResourceToolbar(resolvedResource, {
5913
+ const base = resolveResourceToolbar(resolvedResource, {
5866
5914
  resource: resolvedResource,
5867
5915
  selectedRow: selectedRows[0],
5868
5916
  selectedRows,
@@ -5870,14 +5918,48 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5870
5918
  formRef,
5871
5919
  events,
5872
5920
  emit
5873
- });
5921
+ }) ?? {};
5922
+ if (!hasAuditTrail) return base;
5923
+ return {
5924
+ ...base,
5925
+ utility: [...base.utility ?? [], {
5926
+ key: "audit-trail",
5927
+ text: t("crudPage.auditTrailButton"),
5928
+ icon: "ph-clock-counter-clockwise",
5929
+ hint: t("auditTrail.toolbarHint"),
5930
+ disabled: selectedRowId == null,
5931
+ onClick: () => openAuditTrail()
5932
+ }]
5933
+ };
5874
5934
  }, [
5875
5935
  emit,
5876
5936
  events,
5877
5937
  formRef,
5878
5938
  gridRef,
5939
+ hasAuditTrail,
5940
+ openAuditTrail,
5879
5941
  resolvedResource,
5880
- selectedRows
5942
+ selectedRowId,
5943
+ selectedRows,
5944
+ t
5945
+ ]);
5946
+ const rowActions = (0, react.useMemo)(() => {
5947
+ const base = resolvedResource.rowActions;
5948
+ if (!hasAuditTrail || resolvedResource.auditTrail?.rowAction === false) return base;
5949
+ const auditAction = (row) => ({
5950
+ key: "audit-trail",
5951
+ text: t("auditTrail.rowAction"),
5952
+ icon: "ph-clock-counter-clockwise",
5953
+ onClick: () => openAuditTrail(row)
5954
+ });
5955
+ if (typeof base === "function") return (row) => [...base(row), auditAction(row)];
5956
+ return (row) => [...base ?? [], auditAction(row)];
5957
+ }, [
5958
+ hasAuditTrail,
5959
+ openAuditTrail,
5960
+ resolvedResource.auditTrail?.rowAction,
5961
+ resolvedResource.rowActions,
5962
+ t
5881
5963
  ]);
5882
5964
  /** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
5883
5965
  const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ColumnPresetSelector, {
@@ -5890,7 +5972,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5890
5972
  }) : void 0;
5891
5973
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5892
5974
  id: "wrapper",
5893
- className: [auditOpen && "wrapper--with-audit", viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5975
+ className: [viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5894
5976
  children: [
5895
5977
  (resolvedResource.bulkActions?.length ?? 0) > 0 && selectionState.hasSelection && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5896
5978
  className: "nb-bulk-toolbar",
@@ -5907,16 +5989,6 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5907
5989
  children: action.label
5908
5990
  }, action.key))]
5909
5991
  }),
5910
- hasAuditTrail && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5911
- className: "nb-audit-trail-toolbar",
5912
- style: { marginBottom: 8 },
5913
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5914
- type: "button",
5915
- onClick: () => setAuditOpen((prev) => !prev),
5916
- "aria-pressed": auditOpen,
5917
- children: t("crudPage.auditTrailButton")
5918
- })
5919
- }),
5920
5992
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NativeDataGridView, {
5921
5993
  ref: gridRef,
5922
5994
  id: resolvedResource.id,
@@ -5941,7 +6013,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5941
6013
  mode: resolvedResource.mode,
5942
6014
  stateStoringEnabled: resolvedResource.stateStoring,
5943
6015
  toolbar,
5944
- rowActions: resolvedResource.rowActions,
6016
+ rowActions,
5945
6017
  onAdd: requestNew,
5946
6018
  onEdit: requestEdit,
5947
6019
  onView: requestView,
@@ -6014,7 +6086,10 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
6014
6086
  url: auditUrl,
6015
6087
  renderEntry: resolvedResource.auditTrail.renderEntry,
6016
6088
  visible: auditOpen,
6017
- onClose: () => setAuditOpen(false)
6089
+ onClose: () => setAuditOpen(false),
6090
+ recordSubtitle: auditRecordSubtitle,
6091
+ resolveFieldLabel: resolveAuditFieldLabel,
6092
+ drawerSize: resolvedResource.auditTrail?.drawerSize
6018
6093
  }),
6019
6094
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ConfirmDialog, {
6020
6095
  open: confirmState.open,
package/dist/index.d.cts CHANGED
@@ -495,10 +495,23 @@ interface AuditEntry {
495
495
  after: unknown;
496
496
  }>;
497
497
  }
498
- interface AuditTrailConfig {
498
+ type AuditFieldLabelResolver = (field: string) => string;
499
+ interface AuditTrailConfig<T extends DataRecord$1 = DataRecord$1> {
499
500
  enabled: boolean;
500
501
  apiUrl: string | ((id: string | number) => string);
502
+ /** Custom entry renderer. Receives resolved field labels via context in the panel. */
501
503
  renderEntry?: (entry: AuditEntry) => ReactNode;
504
+ /**
505
+ * Maps API field keys to display labels. When omitted, CrudPage falls back to
506
+ * matching grid field labels from the active resource schema.
507
+ */
508
+ fieldLabels?: Record<string, string> | ((field: string) => string | undefined);
509
+ /** One-line context shown under the drawer title for the selected row. */
510
+ recordSubtitle?: (row: T) => string | undefined;
511
+ /** Drawer width token. Default `sm` (480px) — read-only secondary panel. */
512
+ drawerSize?: CrudDrawerSize;
513
+ /** Adds a row-menu action that opens the audit drawer. Default true. */
514
+ rowAction?: boolean;
502
515
  }
503
516
  //#endregion
504
517
  //#region packages/crud/crud/fieldOperationSemantics.d.ts
@@ -1149,13 +1162,19 @@ interface AuditTrailPanelProps {
1149
1162
  renderEntry?: (entry: AuditEntry) => ReactNode;
1150
1163
  visible: boolean;
1151
1164
  onClose: () => void;
1165
+ recordSubtitle?: string;
1166
+ resolveFieldLabel: AuditFieldLabelResolver;
1167
+ drawerSize?: CrudDrawerSize;
1152
1168
  }
1153
1169
  declare function AuditTrailPanel({
1154
1170
  url,
1155
1171
  renderEntry,
1156
1172
  visible,
1157
- onClose
1158
- }: AuditTrailPanelProps): React.JSX.Element | null;
1173
+ onClose,
1174
+ recordSubtitle,
1175
+ resolveFieldLabel,
1176
+ drawerSize
1177
+ }: AuditTrailPanelProps): React.JSX.Element;
1159
1178
  //#endregion
1160
1179
  //#region packages/crud/field/FieldBuilders.d.ts
1161
1180
  /**
@@ -1936,4 +1955,4 @@ declare function ResourceStoreProvider({
1936
1955
  }: ResourceStoreProviderProps): React.JSX.Element;
1937
1956
  declare function useResourceStoreFactory(): ResourceStoreFactory;
1938
1957
  //#endregion
1939
- export { type AuditEntry, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
1958
+ export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
package/dist/index.d.mts CHANGED
@@ -495,10 +495,23 @@ interface AuditEntry {
495
495
  after: unknown;
496
496
  }>;
497
497
  }
498
- interface AuditTrailConfig {
498
+ type AuditFieldLabelResolver = (field: string) => string;
499
+ interface AuditTrailConfig<T extends DataRecord$1 = DataRecord$1> {
499
500
  enabled: boolean;
500
501
  apiUrl: string | ((id: string | number) => string);
502
+ /** Custom entry renderer. Receives resolved field labels via context in the panel. */
501
503
  renderEntry?: (entry: AuditEntry) => ReactNode;
504
+ /**
505
+ * Maps API field keys to display labels. When omitted, CrudPage falls back to
506
+ * matching grid field labels from the active resource schema.
507
+ */
508
+ fieldLabels?: Record<string, string> | ((field: string) => string | undefined);
509
+ /** One-line context shown under the drawer title for the selected row. */
510
+ recordSubtitle?: (row: T) => string | undefined;
511
+ /** Drawer width token. Default `sm` (480px) — read-only secondary panel. */
512
+ drawerSize?: CrudDrawerSize;
513
+ /** Adds a row-menu action that opens the audit drawer. Default true. */
514
+ rowAction?: boolean;
502
515
  }
503
516
  //#endregion
504
517
  //#region packages/crud/crud/fieldOperationSemantics.d.ts
@@ -1149,13 +1162,19 @@ interface AuditTrailPanelProps {
1149
1162
  renderEntry?: (entry: AuditEntry) => ReactNode;
1150
1163
  visible: boolean;
1151
1164
  onClose: () => void;
1165
+ recordSubtitle?: string;
1166
+ resolveFieldLabel: AuditFieldLabelResolver;
1167
+ drawerSize?: CrudDrawerSize;
1152
1168
  }
1153
1169
  declare function AuditTrailPanel({
1154
1170
  url,
1155
1171
  renderEntry,
1156
1172
  visible,
1157
- onClose
1158
- }: AuditTrailPanelProps): React.JSX.Element | null;
1173
+ onClose,
1174
+ recordSubtitle,
1175
+ resolveFieldLabel,
1176
+ drawerSize
1177
+ }: AuditTrailPanelProps): React.JSX.Element;
1159
1178
  //#endregion
1160
1179
  //#region packages/crud/field/FieldBuilders.d.ts
1161
1180
  /**
@@ -1936,4 +1955,4 @@ declare function ResourceStoreProvider({
1936
1955
  }: ResourceStoreProviderProps): React.JSX.Element;
1937
1956
  declare function useResourceStoreFactory(): ResourceStoreFactory;
1938
1957
  //#endregion
1939
- export { type AuditEntry, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
1958
+ export { type AuditEntry, type AuditFieldLabelResolver, type AuditTrailConfig, AuditTrailPanel, type AuditTrailPanelProps, type BackendAdapter, type BulkAction, type ColSpan, type ColumnPreset, ColumnPresetSelector, type ColumnPresetState, CrudDialogShell, CrudDrawerShell, type CrudDrawerSize, type CrudDrawerViewEvents, type CrudDrawerViewOptions, CrudFormShell, type CrudFormShellProps, CrudPage, CrudPageShell, type CrudPageViewEvents, type CrudPageViewOptions, type CrudViewMode, type CrudViewModeConfig, DATA_GRID_EVENTS, DEFAULT_DRAWER_SIZE, DEFAULT_DRAWER_WIDTH, DRAWER_WIDTHS, type DataGridSelectionChangedEvent, type DataGridSummaryItem, NativeDataGridView as DataGridView, type DataGridViewOptions, type DataRecord, type DetailSummaryOptions, CrudDialogView as DialogView, type DrawerSize, CrudDrawerView as DrawerView, type EnumOption, FORM_EVENTS, type Field, FieldBuilder, type FieldColSpanContext, type FieldDef, type FieldInput, type FieldOverride, FieldType, type FilterRule, type FormHandle, type FormLayout, type FormLayoutHint, type FormOnChangeFn, type FormPresentationContext, type FormPresentationMode, type FormSection, type FormTab, NativeFormView as FormView, type FormViewOptions, type FormatterFn, type GridCellContext, type GridData, type GridHandle, type GridOnChangeFn, HydraAdapter, type ItemFormatterFn, type LoadOption, type OnChangeFn, CrudPageView as PageView, type ResolvedViewMode, type ResourceConfig, type ResourceEmptyState, type ResourceFilterDescriptor, type ResourceFilterRule, type ResourceFormDetail, type ResourceGridDetail, type ResourceLoadOption, type ResourceLoadOptions, type ResourcePermissions, type ResourceRouting, type ResourceRowActions, ResourceSchemaProvider, type ResourceSchemaProviderProps, type ResourceSchemaResolution, type ResourceSchemaResolver, type ResourceSortDescriptor, type ResourceStore, type ResourceStoreFactory, type ResourceStoreOptions, ResourceStoreProvider, type ResourceStoreProviderProps, type ResourceToolbar, type ResourceToolbarAction, type ResourceToolbarActionVariant, type ResourceToolbarContext, type ResourceToolbarItems, RestAdapter, type SmartCrudFieldContract, type SmartCrudFieldOperation, type SmartCrudFieldPatch, type SmartCrudHydraFieldContract, type SmartCrudHydraFieldDirective, type SmartCrudManualField, type SmartCrudManualFieldContract, type SmartCrudOperation, SmartCrudPage, SmartCrudRolesProvider, type SummaryCalculateContext, type SummaryFormat, type SummaryItem, type SummaryTextContext, type SummaryType, ToolbarSelect, type ToolbarSelectOption, type ToolbarSelectProps, type ValidationRule, buildFieldColSpanContext, buildFields, checkboxField, computeSummaryValue, createCrudEvents, crudRoute, currencyField, dateField, datetimeField, defineFieldContract, defineFields, defineResource, entityField, enumField, fileField, formatSummaryValue, identityField, imageField, isLongTextField, isShortField, noneField, numberField, parseDrawerWidthPx, passwordField, resolveDrawerLayoutBucket, resolveDrawerSize, resolveDrawerWidth, resolveFieldColSpan, resolveFieldsColSpans, resolveSummaryText, resolveViewMode, selectField, switchField, textField, textareaField, useColumnPreset, useResourceStoreFactory, useSmartCrudRoles, validateFieldContract };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
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, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, IconButton, Skeleton } from "@nubitio/ui";
4
+ import { AppDialog, AppDropdown, Badge, Button, ConfirmDialog, DatePicker, DateRangePicker, Drawer, EmptyState, 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
7
  import { useDropzone } from "react-dropzone";
@@ -5286,12 +5286,61 @@ function useCrudPage(resource, externalFormRef) {
5286
5286
  }
5287
5287
  //#endregion
5288
5288
  //#region packages/crud/crud/AuditTrailPanel.tsx
5289
- const ACTION_COLORS = {
5290
- create: "#2e7d32",
5291
- update: "#1565c0",
5292
- delete: "#c62828"
5289
+ const ACTION_BADGE = {
5290
+ create: "success",
5291
+ update: "info",
5292
+ delete: "danger"
5293
5293
  };
5294
- function DefaultEntryRenderer({ entry }) {
5294
+ const ACTION_TONE = {
5295
+ create: "success",
5296
+ update: "info",
5297
+ delete: "danger"
5298
+ };
5299
+ function formatAuditValue(value, yesLabel, noLabel) {
5300
+ if (value == null || value === "") return "—";
5301
+ if (typeof value === "boolean") return value ? yesLabel : noLabel;
5302
+ if (typeof value === "object") try {
5303
+ return JSON.stringify(value);
5304
+ } catch {
5305
+ return String(value);
5306
+ }
5307
+ return String(value);
5308
+ }
5309
+ function DefaultEntryContent({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5310
+ const changeKeys = Object.keys(entry.changes);
5311
+ if (changeKeys.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
5312
+ return /* @__PURE__ */ jsx("ul", {
5313
+ className: "nb-audit-trail__changes",
5314
+ children: changeKeys.map((field) => {
5315
+ const { before, after } = entry.changes[field];
5316
+ return /* @__PURE__ */ jsxs("li", {
5317
+ className: "nb-audit-trail__change",
5318
+ children: [/* @__PURE__ */ jsx("span", {
5319
+ className: "nb-audit-trail__field",
5320
+ children: resolveFieldLabel(field)
5321
+ }), /* @__PURE__ */ jsxs("div", {
5322
+ className: "nb-audit-trail__diff",
5323
+ children: [
5324
+ /* @__PURE__ */ jsx("span", {
5325
+ className: "nb-audit-trail__value nb-audit-trail__value--before",
5326
+ children: formatAuditValue(before, yesLabel, noLabel)
5327
+ }),
5328
+ /* @__PURE__ */ jsx("span", {
5329
+ className: "nb-audit-trail__arrow",
5330
+ "aria-hidden": "true",
5331
+ children: "→"
5332
+ }),
5333
+ /* @__PURE__ */ jsx("span", {
5334
+ className: "nb-audit-trail__value nb-audit-trail__value--after",
5335
+ children: formatAuditValue(after, yesLabel, noLabel)
5336
+ })
5337
+ ]
5338
+ })]
5339
+ }, field);
5340
+ })
5341
+ });
5342
+ }
5343
+ function DefaultEntryRenderer({ entry, resolveFieldLabel, yesLabel, noLabel }) {
5295
5344
  const { t } = useCoreTranslation();
5296
5345
  const date = new Date(entry.timestamp);
5297
5346
  const formatted = Number.isNaN(date.getTime()) ? entry.timestamp : date.toLocaleString(getCoreLocale(), { timeZone: getCoreTimezone() });
@@ -5300,177 +5349,159 @@ function DefaultEntryRenderer({ entry }) {
5300
5349
  update: t("auditTrail.action.update"),
5301
5350
  delete: t("auditTrail.action.delete")
5302
5351
  };
5303
- const changeKeys = Object.keys(entry.changes);
5304
- return /* @__PURE__ */ jsxs("li", {
5305
- style: {
5306
- borderBottom: "1px solid #e0e0e0",
5307
- padding: "10px 0",
5308
- listStyle: "none"
5309
- },
5310
- children: [/* @__PURE__ */ jsxs("div", {
5311
- style: {
5312
- display: "flex",
5313
- alignItems: "center",
5314
- gap: 8,
5315
- marginBottom: 4
5316
- },
5317
- children: [
5318
- /* @__PURE__ */ jsx("span", {
5319
- style: {
5320
- fontSize: 12,
5321
- color: "#757575"
5322
- },
5323
- children: formatted
5324
- }),
5325
- /* @__PURE__ */ jsx("span", {
5326
- style: {
5327
- fontSize: 12,
5328
- color: "#424242"
5329
- },
5330
- children: entry.user
5331
- }),
5332
- /* @__PURE__ */ jsx("span", {
5333
- style: {
5334
- fontSize: 11,
5335
- fontWeight: 600,
5336
- padding: "1px 6px",
5337
- borderRadius: 4,
5338
- backgroundColor: ACTION_COLORS[entry.action],
5339
- color: "#fff"
5340
- },
5341
- children: actionLabels[entry.action]
5342
- })
5343
- ]
5344
- }), changeKeys.length > 0 && /* @__PURE__ */ jsx("ul", {
5345
- style: {
5346
- margin: "4px 0 0 0",
5347
- padding: "0 0 0 16px"
5348
- },
5349
- children: changeKeys.map((field) => {
5350
- const { before, after } = entry.changes[field];
5351
- return /* @__PURE__ */ jsxs("li", {
5352
- style: {
5353
- fontSize: 12,
5354
- color: "#616161"
5355
- },
5356
- children: [
5357
- /* @__PURE__ */ jsxs("strong", { children: [field, ":"] }),
5358
- " ",
5359
- /* @__PURE__ */ jsx("span", {
5360
- style: { color: "#c62828" },
5361
- children: String(before ?? "—")
5362
- }),
5363
- " → ",
5364
- /* @__PURE__ */ jsx("span", {
5365
- style: { color: "#2e7d32" },
5366
- children: String(after ?? "—")
5367
- })
5368
- ]
5369
- }, field);
5352
+ return /* @__PURE__ */ jsx(TimelineItem, {
5353
+ status: "complete",
5354
+ tone: ACTION_TONE[entry.action],
5355
+ title: /* @__PURE__ */ jsxs("span", {
5356
+ className: "nb-audit-trail__meta",
5357
+ children: [/* @__PURE__ */ jsx("span", {
5358
+ className: "nb-audit-trail__user",
5359
+ children: entry.user
5360
+ }), /* @__PURE__ */ jsx(Badge, {
5361
+ variant: ACTION_BADGE[entry.action],
5362
+ size: "sm",
5363
+ pill: true,
5364
+ children: actionLabels[entry.action]
5365
+ })]
5366
+ }),
5367
+ timestamp: formatted,
5368
+ dateTime: entry.timestamp,
5369
+ children: /* @__PURE__ */ jsx(DefaultEntryContent, {
5370
+ entry,
5371
+ resolveFieldLabel,
5372
+ yesLabel,
5373
+ noLabel
5374
+ })
5375
+ });
5376
+ }
5377
+ function AuditTrailSkeleton() {
5378
+ return /* @__PURE__ */ jsxs("div", {
5379
+ className: "nb-audit-trail__skeleton",
5380
+ "aria-busy": "true",
5381
+ children: [
5382
+ /* @__PURE__ */ jsx(Skeleton, {
5383
+ variant: "rect",
5384
+ height: 72
5385
+ }),
5386
+ /* @__PURE__ */ jsx(Skeleton, {
5387
+ variant: "rect",
5388
+ height: 72
5389
+ }),
5390
+ /* @__PURE__ */ jsx(Skeleton, {
5391
+ variant: "rect",
5392
+ height: 72
5370
5393
  })
5371
- })]
5394
+ ]
5372
5395
  });
5373
5396
  }
5374
- function AuditTrailPanel({ url, renderEntry, visible, onClose }) {
5397
+ function AuditTrailPanel({ url, renderEntry, visible, onClose, recordSubtitle, resolveFieldLabel, drawerSize = "sm" }) {
5375
5398
  const { t } = useCoreTranslation();
5376
5399
  const httpClient = useCoreHttpClient();
5377
5400
  const [fetchState, setFetchState] = useState({ status: "idle" });
5378
- useEffect(() => {
5379
- if (!visible || url === null) {
5401
+ const yesLabel = t("common.yes");
5402
+ const noLabel = t("common.no");
5403
+ const loadEntries = useCallback(() => {
5404
+ if (url === null) {
5380
5405
  setFetchState({ status: "idle" });
5381
5406
  return;
5382
5407
  }
5383
- let cancelled = false;
5384
5408
  setFetchState({ status: "loading" });
5385
5409
  httpClient.get(url).then((response) => {
5386
- if (!cancelled) setFetchState({
5410
+ setFetchState({
5387
5411
  status: "success",
5388
5412
  entries: response.data
5389
5413
  });
5390
5414
  }).catch(() => {
5391
- if (!cancelled) setFetchState({ status: "error" });
5415
+ setFetchState({ status: "error" });
5392
5416
  });
5393
- return () => {
5394
- cancelled = true;
5395
- };
5396
- }, [
5397
- httpClient,
5398
- url,
5399
- visible
5400
- ]);
5401
- if (!visible) return null;
5402
- return /* @__PURE__ */ jsxs("aside", {
5403
- className: "nb-audit-trail-panel",
5404
- style: {
5405
- border: "1px solid #e0e0e0",
5406
- borderRadius: 4,
5407
- padding: "12px 16px",
5408
- backgroundColor: "#fafafa",
5409
- minWidth: 280
5410
- },
5411
- children: [/* @__PURE__ */ jsxs("div", {
5412
- style: {
5413
- display: "flex",
5414
- alignItems: "center",
5415
- justifyContent: "space-between",
5416
- marginBottom: 12
5417
- },
5418
- children: [/* @__PURE__ */ jsx("strong", {
5419
- style: { fontSize: 14 },
5420
- children: t("auditTrail.title")
5421
- }), /* @__PURE__ */ jsx("button", {
5422
- type: "button",
5423
- onClick: onClose,
5424
- "aria-label": t("auditTrail.closeButton"),
5425
- style: {
5426
- background: "none",
5427
- border: "none",
5428
- cursor: "pointer",
5429
- fontSize: 18,
5430
- lineHeight: 1,
5431
- padding: "0 4px",
5432
- color: "#616161"
5433
- },
5434
- children: "×"
5435
- })]
5436
- }), url === null ? /* @__PURE__ */ jsx("p", {
5437
- style: {
5438
- fontSize: 13,
5439
- color: "#757575",
5440
- margin: 0
5441
- },
5442
- children: t("auditTrail.selectRecord")
5443
- }) : fetchState.status === "loading" ? /* @__PURE__ */ jsx("p", {
5444
- style: {
5445
- fontSize: 13,
5446
- color: "#757575",
5447
- margin: 0
5448
- },
5449
- children: t("auditTrail.loading")
5450
- }) : fetchState.status === "error" ? /* @__PURE__ */ jsx("p", {
5451
- style: {
5452
- fontSize: 13,
5453
- color: "#c62828",
5454
- margin: 0
5455
- },
5456
- children: t("auditTrail.error")
5457
- }) : fetchState.status === "success" ? fetchState.entries.length === 0 ? /* @__PURE__ */ jsx("p", {
5458
- style: {
5459
- fontSize: 13,
5460
- color: "#757575",
5461
- margin: 0
5462
- },
5463
- children: t("auditTrail.empty")
5464
- }) : /* @__PURE__ */ jsx("ul", {
5465
- style: {
5466
- margin: 0,
5467
- padding: 0
5468
- },
5469
- children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ jsx(React.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ jsx(DefaultEntryRenderer, { entry }, String(entry.id)))
5470
- }) : null]
5417
+ }, [httpClient, url]);
5418
+ useEffect(() => {
5419
+ if (!visible) {
5420
+ setFetchState({ status: "idle" });
5421
+ return;
5422
+ }
5423
+ loadEntries();
5424
+ }, [loadEntries, visible]);
5425
+ const drawerTitle = /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: t("auditTrail.title") }), recordSubtitle && /* @__PURE__ */ jsx("p", {
5426
+ className: "nb-audit-trail__subtitle",
5427
+ children: recordSubtitle
5428
+ })] });
5429
+ const body = (() => {
5430
+ if (url === null) return /* @__PURE__ */ jsx(EmptyState, {
5431
+ fill: true,
5432
+ icon: "cursor-click",
5433
+ title: t("auditTrail.selectRecord"),
5434
+ description: t("auditTrail.selectRecordHint"),
5435
+ size: "sm"
5436
+ });
5437
+ if (fetchState.status === "loading") return /* @__PURE__ */ jsx(AuditTrailSkeleton, {});
5438
+ if (fetchState.status === "error") return /* @__PURE__ */ jsx("div", {
5439
+ className: "nb-audit-trail__error",
5440
+ children: /* @__PURE__ */ jsx(EmptyState, {
5441
+ fill: true,
5442
+ variant: "danger",
5443
+ icon: "warning-circle",
5444
+ title: t("auditTrail.error"),
5445
+ size: "sm",
5446
+ action: /* @__PURE__ */ jsx(Button, {
5447
+ variant: "secondary",
5448
+ size: "sm",
5449
+ onClick: loadEntries,
5450
+ children: t("auditTrail.retry")
5451
+ })
5452
+ })
5453
+ });
5454
+ if (fetchState.status === "success" && fetchState.entries.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
5455
+ fill: true,
5456
+ icon: "clock-counter-clockwise",
5457
+ title: t("auditTrail.empty"),
5458
+ description: t("auditTrail.emptyHint"),
5459
+ size: "sm"
5460
+ });
5461
+ if (fetchState.status === "success") return /* @__PURE__ */ jsx(Timeline, {
5462
+ variant: "log",
5463
+ "aria-label": t("auditTrail.title"),
5464
+ children: fetchState.entries.map((entry) => renderEntry ? /* @__PURE__ */ jsx(React.Fragment, { children: renderEntry(entry) }, String(entry.id)) : /* @__PURE__ */ jsx(DefaultEntryRenderer, {
5465
+ entry,
5466
+ resolveFieldLabel,
5467
+ yesLabel,
5468
+ noLabel
5469
+ }, String(entry.id)))
5470
+ });
5471
+ return null;
5472
+ })();
5473
+ return /* @__PURE__ */ jsx(Drawer, {
5474
+ isOpen: visible,
5475
+ onClose,
5476
+ title: drawerTitle,
5477
+ width: resolveDrawerWidth({ drawerSize }),
5478
+ side: "right",
5479
+ scrim: "subtle",
5480
+ closeLabel: t("auditTrail.closeButton"),
5481
+ "aria-label": t("auditTrail.title"),
5482
+ className: "nb-audit-trail-drawer",
5483
+ children: /* @__PURE__ */ jsx("div", {
5484
+ className: "nb-audit-trail",
5485
+ children: body
5486
+ })
5471
5487
  });
5472
5488
  }
5473
5489
  //#endregion
5490
+ //#region packages/crud/crud/AuditTrail.ts
5491
+ function createAuditFieldLabelResolver(config, fields) {
5492
+ const fieldLabelByName = new Map(fields.map((field) => [field.name, field.label || field.name]));
5493
+ return (field) => {
5494
+ const fromConfig = config?.fieldLabels;
5495
+ if (fromConfig) {
5496
+ if (typeof fromConfig === "function") {
5497
+ const resolved = fromConfig(field);
5498
+ if (resolved) return resolved;
5499
+ } else if (fromConfig[field]) return fromConfig[field];
5500
+ }
5501
+ return fieldLabelByName.get(field) ?? field;
5502
+ };
5503
+ }
5504
+ //#endregion
5474
5505
  //#region packages/crud/crud/dialogStore.ts
5475
5506
  function initialDialogState() {
5476
5507
  return {
@@ -5818,13 +5849,30 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5818
5849
  if (hasAuditTrail) {
5819
5850
  const first = rows[0];
5820
5851
  if (first != null) setSelectedRowId(first["id"] ?? first["ID"] ?? null);
5852
+ else setSelectedRowId(null);
5821
5853
  }
5822
5854
  };
5855
+ const selectedRow = selectedRows[0] ?? null;
5823
5856
  const auditUrl = useMemo(() => {
5824
5857
  if (!resolvedResource.auditTrail?.enabled || selectedRowId == null) return null;
5825
5858
  const { apiUrl } = resolvedResource.auditTrail;
5826
5859
  return typeof apiUrl === "function" ? apiUrl(selectedRowId) : `${apiUrl}${selectedRowId}`;
5827
5860
  }, [resolvedResource.auditTrail, selectedRowId]);
5861
+ const resolveAuditFieldLabel = useMemo(() => createAuditFieldLabelResolver(resolvedResource.auditTrail, routeAwareGridFields), [resolvedResource.auditTrail, routeAwareGridFields]);
5862
+ const auditRecordSubtitle = useMemo(() => {
5863
+ if (!resolvedResource.auditTrail?.recordSubtitle || selectedRow == null) return void 0;
5864
+ return resolvedResource.auditTrail.recordSubtitle(selectedRow);
5865
+ }, [resolvedResource.auditTrail, selectedRow]);
5866
+ const openAuditTrail = useCallback((row) => {
5867
+ if (row != null) {
5868
+ const id = row["id"] ?? row["ID"] ?? null;
5869
+ if (id != null) {
5870
+ setSelectedRowId(id);
5871
+ setSelectedRows([row]);
5872
+ }
5873
+ }
5874
+ setAuditOpen(true);
5875
+ }, []);
5828
5876
  const executeBulkAction = useCallback(async (action) => {
5829
5877
  await action.onAction(selectionState.selectedIds);
5830
5878
  selectionState.clearSelection();
@@ -5838,7 +5886,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5838
5886
  else executeBulkAction(action);
5839
5887
  }, [executeBulkAction]);
5840
5888
  const toolbar = useMemo(() => {
5841
- return resolveResourceToolbar(resolvedResource, {
5889
+ const base = resolveResourceToolbar(resolvedResource, {
5842
5890
  resource: resolvedResource,
5843
5891
  selectedRow: selectedRows[0],
5844
5892
  selectedRows,
@@ -5846,14 +5894,48 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5846
5894
  formRef,
5847
5895
  events,
5848
5896
  emit
5849
- });
5897
+ }) ?? {};
5898
+ if (!hasAuditTrail) return base;
5899
+ return {
5900
+ ...base,
5901
+ utility: [...base.utility ?? [], {
5902
+ key: "audit-trail",
5903
+ text: t("crudPage.auditTrailButton"),
5904
+ icon: "ph-clock-counter-clockwise",
5905
+ hint: t("auditTrail.toolbarHint"),
5906
+ disabled: selectedRowId == null,
5907
+ onClick: () => openAuditTrail()
5908
+ }]
5909
+ };
5850
5910
  }, [
5851
5911
  emit,
5852
5912
  events,
5853
5913
  formRef,
5854
5914
  gridRef,
5915
+ hasAuditTrail,
5916
+ openAuditTrail,
5855
5917
  resolvedResource,
5856
- selectedRows
5918
+ selectedRowId,
5919
+ selectedRows,
5920
+ t
5921
+ ]);
5922
+ const rowActions = useMemo(() => {
5923
+ const base = resolvedResource.rowActions;
5924
+ if (!hasAuditTrail || resolvedResource.auditTrail?.rowAction === false) return base;
5925
+ const auditAction = (row) => ({
5926
+ key: "audit-trail",
5927
+ text: t("auditTrail.rowAction"),
5928
+ icon: "ph-clock-counter-clockwise",
5929
+ onClick: () => openAuditTrail(row)
5930
+ });
5931
+ if (typeof base === "function") return (row) => [...base(row), auditAction(row)];
5932
+ return (row) => [...base ?? [], auditAction(row)];
5933
+ }, [
5934
+ hasAuditTrail,
5935
+ openAuditTrail,
5936
+ resolvedResource.auditTrail?.rowAction,
5937
+ resolvedResource.rowActions,
5938
+ t
5857
5939
  ]);
5858
5940
  /** Preset selector rendered as a toolbar slot via `beforeToolbar`. */
5859
5941
  const renderPresetSelector = hasPresets ? () => /* @__PURE__ */ jsx(ColumnPresetSelector, {
@@ -5866,7 +5948,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5866
5948
  }) : void 0;
5867
5949
  return /* @__PURE__ */ jsxs("div", {
5868
5950
  id: "wrapper",
5869
- className: [auditOpen && "wrapper--with-audit", viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5951
+ className: [viewMode.mode === "page" && dialogIsOpen && "wrapper--with-page"].filter(Boolean).join(" ") || void 0,
5870
5952
  children: [
5871
5953
  (resolvedResource.bulkActions?.length ?? 0) > 0 && selectionState.hasSelection && /* @__PURE__ */ jsxs("div", {
5872
5954
  className: "nb-bulk-toolbar",
@@ -5883,16 +5965,6 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5883
5965
  children: action.label
5884
5966
  }, action.key))]
5885
5967
  }),
5886
- hasAuditTrail && /* @__PURE__ */ jsx("div", {
5887
- className: "nb-audit-trail-toolbar",
5888
- style: { marginBottom: 8 },
5889
- children: /* @__PURE__ */ jsx("button", {
5890
- type: "button",
5891
- onClick: () => setAuditOpen((prev) => !prev),
5892
- "aria-pressed": auditOpen,
5893
- children: t("crudPage.auditTrailButton")
5894
- })
5895
- }),
5896
5968
  /* @__PURE__ */ jsx(NativeDataGridView, {
5897
5969
  ref: gridRef,
5898
5970
  id: resolvedResource.id,
@@ -5917,7 +5989,7 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5917
5989
  mode: resolvedResource.mode,
5918
5990
  stateStoringEnabled: resolvedResource.stateStoring,
5919
5991
  toolbar,
5920
- rowActions: resolvedResource.rowActions,
5992
+ rowActions,
5921
5993
  onAdd: requestNew,
5922
5994
  onEdit: requestEdit,
5923
5995
  onView: requestView,
@@ -5990,7 +6062,10 @@ const CrudPageInner = ({ resource, onFormDataChange, initialRecordId = null, ini
5990
6062
  url: auditUrl,
5991
6063
  renderEntry: resolvedResource.auditTrail.renderEntry,
5992
6064
  visible: auditOpen,
5993
- onClose: () => setAuditOpen(false)
6065
+ onClose: () => setAuditOpen(false),
6066
+ recordSubtitle: auditRecordSubtitle,
6067
+ resolveFieldLabel: resolveAuditFieldLabel,
6068
+ drawerSize: resolvedResource.auditTrail?.drawerSize
5994
6069
  }),
5995
6070
  /* @__PURE__ */ jsx(ConfirmDialog, {
5996
6071
  open: confirmState.open,
package/dist/style.css CHANGED
@@ -3276,8 +3276,7 @@ html[data-density=compact] .nb-datagrid .nb-badge {
3276
3276
 
3277
3277
  #wrapper.wrapper--with-page > .data-grid-view,
3278
3278
  #wrapper.wrapper--with-page > .view.datagrid-list,
3279
- #wrapper.wrapper--with-page > .nb-bulk-toolbar,
3280
- #wrapper.wrapper--with-page > .nb-audit-trail-toolbar {
3279
+ #wrapper.wrapper--with-page > .nb-bulk-toolbar {
3281
3280
  display: none;
3282
3281
  }
3283
3282
 
@@ -3292,6 +3291,99 @@ html[data-density=compact] .nb-crud-page-shell__footer {
3292
3291
  gap: var(--space-1);
3293
3292
  padding: var(--space-2) var(--space-3);
3294
3293
  }
3294
+ .nb-audit-trail {
3295
+ display: flex;
3296
+ flex-direction: column;
3297
+ gap: var(--space-3);
3298
+ min-height: 0;
3299
+ }
3300
+
3301
+ .nb-audit-trail__subtitle {
3302
+ color: var(--text-secondary);
3303
+ font-size: var(--font-size-sm, 0.8125rem);
3304
+ line-height: 1.4;
3305
+ margin: calc(var(--space-1) * -1) 0 0;
3306
+ }
3307
+
3308
+ .nb-audit-trail__meta {
3309
+ align-items: center;
3310
+ display: flex;
3311
+ flex-wrap: wrap;
3312
+ gap: var(--space-2);
3313
+ }
3314
+
3315
+ .nb-audit-trail__user {
3316
+ color: var(--text-primary);
3317
+ font-size: var(--font-size-sm, 0.8125rem);
3318
+ font-weight: 500;
3319
+ }
3320
+
3321
+ .nb-audit-trail__changes {
3322
+ display: flex;
3323
+ flex-direction: column;
3324
+ gap: var(--space-2);
3325
+ list-style: none;
3326
+ margin: 0;
3327
+ padding: 0;
3328
+ }
3329
+
3330
+ .nb-audit-trail__change {
3331
+ background: var(--surface-2);
3332
+ border: 1px solid var(--border-color);
3333
+ border-radius: var(--radius-md, 6px);
3334
+ display: grid;
3335
+ gap: var(--space-1);
3336
+ padding: var(--space-2) var(--space-3);
3337
+ }
3338
+
3339
+ .nb-audit-trail__field {
3340
+ color: var(--text-primary);
3341
+ font-size: var(--font-size-sm, 0.8125rem);
3342
+ font-weight: 600;
3343
+ }
3344
+
3345
+ .nb-audit-trail__diff {
3346
+ align-items: center;
3347
+ color: var(--text-secondary);
3348
+ display: flex;
3349
+ flex-wrap: wrap;
3350
+ font-size: var(--font-size-sm, 0.8125rem);
3351
+ gap: var(--space-2);
3352
+ }
3353
+
3354
+ .nb-audit-trail__value {
3355
+ font-variant-numeric: tabular-nums;
3356
+ max-width: 100%;
3357
+ overflow-wrap: anywhere;
3358
+ }
3359
+
3360
+ .nb-audit-trail__value--before {
3361
+ color: var(--danger-color);
3362
+ text-decoration: line-through;
3363
+ text-decoration-color: color-mix(in srgb, var(--danger-color) 55%, transparent);
3364
+ }
3365
+
3366
+ .nb-audit-trail__value--after {
3367
+ color: var(--success-color);
3368
+ font-weight: 500;
3369
+ }
3370
+
3371
+ .nb-audit-trail__arrow {
3372
+ color: var(--text-tertiary);
3373
+ flex: 0 0 auto;
3374
+ }
3375
+
3376
+ .nb-audit-trail__skeleton {
3377
+ display: flex;
3378
+ flex-direction: column;
3379
+ gap: var(--space-3);
3380
+ }
3381
+
3382
+ .nb-audit-trail__error {
3383
+ display: flex;
3384
+ flex-direction: column;
3385
+ gap: var(--space-3);
3386
+ }
3295
3387
  .nb-smart-crud-fallback {
3296
3388
  display: flex;
3297
3389
  flex-direction: column;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/crud",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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,8 +56,8 @@
56
56
  "react-dom": "^19.0.0",
57
57
  "react-i18next": "^14.0.0",
58
58
  "react-router-dom": "^6.0.0",
59
- "@nubitio/ui": "^0.5.0",
60
- "@nubitio/core": "^0.5.0"
59
+ "@nubitio/core": "^0.5.2",
60
+ "@nubitio/ui": "^0.5.2"
61
61
  },
62
62
  "dependencies": {
63
63
  "react-dropzone": "^15.0.0"