@declarion/react 0.2.0 → 0.2.1

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.
Files changed (33) hide show
  1. package/dist-lib/DraggableChildrenTable-C5cpmctN.js +261 -0
  2. package/dist-lib/DraggableChildrenTable-C5cpmctN.js.map +1 -0
  3. package/dist-lib/components/detail-layout/ChildrenBlock.d.ts +2 -1
  4. package/dist-lib/components/detail-layout/DetailAuditStrip.d.ts +13 -0
  5. package/dist-lib/components/detail-layout/DetailHighlights.d.ts +15 -0
  6. package/dist-lib/components/detail-layout/ErrorSummary.d.ts +11 -0
  7. package/dist-lib/components/detail-layout/FieldRow.d.ts +21 -0
  8. package/dist-lib/components/detail-layout/LayoutRenderer.d.ts +2 -0
  9. package/dist-lib/components/detail-layout/LayoutTabs.d.ts +1 -3
  10. package/dist-lib/components/detail-layout/blockMeta.d.ts +4 -0
  11. package/dist-lib/components/detail-layout/index.d.ts +4 -0
  12. package/dist-lib/components/fields/JsonField.d.ts +1 -0
  13. package/dist-lib/components/fields/cells/factories/emptyCell.d.ts +2 -0
  14. package/dist-lib/components/primitives/SectionCard.d.ts +6 -1
  15. package/dist-lib/components/primitives/dataTableStyles.d.ts +4 -0
  16. package/dist-lib/components/primitives/index.d.ts +1 -1
  17. package/dist-lib/components/shared/AuditMetaStrip.d.ts +11 -0
  18. package/dist-lib/components/shared/HighlightsStrip.d.ts +10 -0
  19. package/dist-lib/components/shared/RecordHeader.d.ts +15 -0
  20. package/dist-lib/components/shared/StatusStepper.d.ts +13 -0
  21. package/dist-lib/components/shared/toast/DeclarionToast.d.ts +31 -0
  22. package/dist-lib/{useUserLocale-GBIXbEE6.js → dataTableStyles-DiIApmjb.js} +1669 -1570
  23. package/dist-lib/dataTableStyles-DiIApmjb.js.map +1 -0
  24. package/dist-lib/declarion-react.css +1 -1
  25. package/dist-lib/index.js +6126 -5138
  26. package/dist-lib/index.js.map +1 -1
  27. package/dist-lib/lib/toast.d.ts +49 -16
  28. package/dist-lib/stores/appearance.d.ts +3 -0
  29. package/dist-lib/types/schema.d.ts +13 -0
  30. package/package.json +1 -1
  31. package/dist-lib/DraggableChildrenTable-IfspXSFA.js +0 -281
  32. package/dist-lib/DraggableChildrenTable-IfspXSFA.js.map +0 -1
  33. package/dist-lib/useUserLocale-GBIXbEE6.js.map +0 -1
@@ -1,16 +1,49 @@
1
- /**
2
- * Shared toast configuration constants.
3
- *
4
- * The toaster is mounted once in `app.tsx` (sonner `<Toaster>` with
5
- * `closeButton` + `visibleToasts={5}`). Every toast emitter - the central
6
- * `ActionRunner` and the handful of direct `toast.error` callsites in
7
- * `useMutations`, `useVersionWatcher`, and form gates in `SmartDetailPage` -
8
- * MUST import this constant rather than hardcoding `Infinity` (which
9
- * piles toasts up indefinitely and overwhelms the surface).
10
- *
11
- * 60s is the chosen balance: well past human read time, short enough that
12
- * walking away does not produce a stale wall of red on return. `closeButton`
13
- * on the Toaster and the in-description Copy button cover the explicit
14
- * read-and-keep flow for users who need longer than 60s.
15
- */
16
- export declare const ERROR_TOAST_DURATION_MS = 60000;
1
+ import { type ToastType } from "../components/shared/toast/DeclarionToast";
2
+ export type { ToastType, ToastAction } from "../components/shared/toast/DeclarionToast";
3
+ /** Per-call toast options. Severity sets the defaults; these override them. */
4
+ export interface ToastOptions {
5
+ /** Auto-dismiss after this many ms. 0 = sticky. Defaults to the variant's. */
6
+ duration?: number;
7
+ /** Force sticky (never auto-dismiss) regardless of duration. */
8
+ sticky?: boolean;
9
+ /** Trailing action button - e.g. View / Retry / Undo / Reload. */
10
+ action?: {
11
+ label: string;
12
+ onClick: () => void;
13
+ };
14
+ /** Correlation id, shown top-right in mono, included when the user copies. */
15
+ requestId?: string;
16
+ /** Expandable detail block (stack trace, server payload). */
17
+ details?: string;
18
+ /** Repeats sharing a `group` roll up into one toast with a ×N badge. */
19
+ group?: string;
20
+ /** Explicit id - reuse to replace an existing toast in place (idempotent). */
21
+ id?: string;
22
+ }
23
+ /** Fields a loading toast can morph into when its async work resolves. */
24
+ export interface ToastUpdate {
25
+ type?: ToastType;
26
+ title?: string;
27
+ message?: string;
28
+ duration?: number;
29
+ sticky?: boolean;
30
+ action?: {
31
+ label: string;
32
+ onClick: () => void;
33
+ };
34
+ requestId?: string;
35
+ details?: string;
36
+ }
37
+ /** Morph an existing toast in place - the loading -> success/error path. */
38
+ declare function update(id: string, patch: ToastUpdate): void;
39
+ /** The app-wide toast API. Import this - not `sonner` - everywhere. */
40
+ export declare const toast: {
41
+ success: (title: string, message?: string, opts?: ToastOptions) => string;
42
+ info: (title: string, message?: string, opts?: ToastOptions) => string;
43
+ warning: (title: string, message?: string, opts?: ToastOptions) => string;
44
+ error: (title: string, message?: string, opts?: ToastOptions) => string;
45
+ loading: (title: string, message?: string, opts?: ToastOptions) => string;
46
+ update: typeof update;
47
+ dismiss: (id: string) => string | number;
48
+ dismissAll: () => string | number;
49
+ };
@@ -5,6 +5,7 @@ export type SidebarMode = "wide" | "collapsed";
5
5
  export type TabsPosition = "top" | "bottom";
6
6
  export type AiSurface = "command" | "panel" | "both" | "off";
7
7
  export type FaviconSource = "google" | "direct" | "none";
8
+ export type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
8
9
  export interface Appearance {
9
10
  theme: ThemeId;
10
11
  accent: AccentId;
@@ -19,6 +20,8 @@ export interface Appearance {
19
20
  aiSurface: AiSurface;
20
21
  /** Favicon source for unknown hosts in URL cells. See ui_favicon_source. */
21
22
  faviconSource: FaviconSource;
23
+ /** Where toasts appear. Read by <Toaster> in app.tsx. See ui_toast_position. */
24
+ toastPosition: ToastPosition;
22
25
  }
23
26
  export declare function applyAppearance(a: Appearance): void;
24
27
  interface AppearanceState extends Appearance {
@@ -172,6 +172,7 @@ export interface StatusGroupDef {
172
172
  initial: string;
173
173
  statuses: StatusDef[];
174
174
  transitions?: Record<string, string[]>;
175
+ display_mode?: "pill" | "stepper";
175
176
  }
176
177
  export interface PropertyDef {
177
178
  name: LocalizedString;
@@ -338,6 +339,7 @@ export interface SectionNode {
338
339
  columns?: number;
339
340
  icon?: string;
340
341
  badge?: SectionBadgeConfig;
342
+ emphasis?: "primary" | "secondary";
341
343
  }
342
344
  export interface RowConfig {
343
345
  items: LayoutNode[];
@@ -389,6 +391,15 @@ export interface LayoutNode {
389
391
  position_field?: string;
390
392
  interaction?: "navigate" | "inline_edit" | "row_edit" | "readonly";
391
393
  }
394
+ export interface AuditLineDef {
395
+ label: LocalizedString;
396
+ at?: string;
397
+ by?: string;
398
+ }
399
+ export interface AuditConfig {
400
+ enabled?: boolean;
401
+ lines?: AuditLineDef[];
402
+ }
392
403
  export interface Screen {
393
404
  type: "list" | "detail" | "custom" | "record_list";
394
405
  entity?: string;
@@ -409,6 +420,8 @@ export interface Screen {
409
420
  items?: LayoutNode[];
410
421
  sidebar?: LayoutNode[];
411
422
  sidebar_width?: string;
423
+ highlights?: string[];
424
+ audit?: AuditConfig;
412
425
  self?: boolean;
413
426
  list_width?: string;
414
427
  list_resizable?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/react",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "React SDK for Declarion, the schema-driven business apps platform.",
@@ -1,281 +0,0 @@
1
- import { Jt as e, Kt as t, ct as n, n as r, qt as i, t as a } from "./useUserLocale-GBIXbEE6.js";
2
- import { useCallback as o, useMemo as s, useRef as c } from "react";
3
- import { jsx as l, jsxs as u } from "react/jsx-runtime";
4
- import { DndContext as d, PointerSensor as f, closestCenter as p, useSensor as m, useSensors as h } from "@dnd-kit/core";
5
- import { SortableContext as g, useSortable as _, verticalListSortingStrategy as v } from "@dnd-kit/sortable";
6
- import { CSS as y } from "@dnd-kit/utilities";
7
- //#region src/components/detail-layout/DraggableChildrenTable.tsx
8
- function b({ config: t, childEntity: r, schema: a, rows: _, onChange: y, validationErrors: b, refs: S }) {
9
- let C = c(null), w = t.position_field, T = t.columns, E = h(m(f, { activationConstraint: { distance: 5 } })), D = new Set(Array.isArray(t.foreign_key) ? t.foreign_key : [t.foreign_key]), O = T.filter((e) => {
10
- if (D.has(e) || e === w) return !1;
11
- let t = r.fields.get(e);
12
- return !(!t || t.auto || t.primary);
13
- }), k = new Set(T.filter((e) => {
14
- if (e === w) return !0;
15
- let t = r.fields.get(e);
16
- return t && (t.auto || t.primary);
17
- })), A = T.filter((e) => e !== w), j = s(() => _.filter((e) => !e._deleted).map((e) => e._key), [_]), M = o((e, t, n) => {
18
- y(_.map((r) => r._key === e ? {
19
- ...r,
20
- [t]: n,
21
- _dirty: !0
22
- } : r));
23
- }, [_, y]), N = o(() => {
24
- let e = _.filter((e) => !e._deleted).reduce((e, t) => {
25
- let n = typeof t[w] == "number" ? t[w] : -1;
26
- return Math.max(e, n);
27
- }, -1), n = {
28
- _key: crypto.randomUUID(),
29
- _dirty: !0,
30
- [w]: e + 1
31
- }, i = t.quick_add?.defaults ?? {};
32
- for (let e of O) {
33
- let t = r.fields.get(e);
34
- t && (i[e] === void 0 ? t.default === void 0 ? t.type === "bool" && (n[e] = !1) : n[e] = t.default : n[e] = i[e]);
35
- }
36
- y([..._, n]);
37
- }, [
38
- _,
39
- y,
40
- O,
41
- r,
42
- t.quick_add?.defaults,
43
- w
44
- ]), P = o((e) => {
45
- y(_.map((t) => t._key === e ? {
46
- ...t,
47
- _deleted: !0
48
- } : t));
49
- }, [_, y]), F = o((e) => {
50
- y(_.map((t) => t._key === e ? {
51
- ...t,
52
- _deleted: !1
53
- } : t));
54
- }, [_, y]), I = o((e) => {
55
- let { active: t, over: n } = e;
56
- if (!n || t.id === n.id) return;
57
- let r = _.filter((e) => !e._deleted), i = _.filter((e) => e._deleted), a = r.findIndex((e) => e._key === String(t.id)), o = r.findIndex((e) => e._key === String(n.id));
58
- if (a === -1 || o === -1) return;
59
- let s = [...r], [c] = s.splice(a, 1);
60
- s.splice(o, 0, c), y([...s.map((e, t) => ({
61
- ...e,
62
- [w]: t,
63
- _dirty: e[w] === t ? e._dirty : !0
64
- })), ...i]);
65
- }, [
66
- _,
67
- y,
68
- w
69
- ]), L = o((e) => {
70
- let t = e.target, n = t.closest("td");
71
- if (!n) return;
72
- let r = n.closest("tr");
73
- if (!r) return;
74
- let i = r.closest("tbody");
75
- if (!i) return;
76
- let a = Array.from(r.cells).indexOf(n), o = Array.from(i.rows).indexOf(r);
77
- if (e.key === "Tab") {
78
- let t = e.shiftKey ? -1 : 1, n = Array.from(i.rows), r = o, s = a + t;
79
- for (; r >= 0 && r < n.length;) if (s >= 0 && s < n[r].cells.length) {
80
- let i = n[r].cells[s].querySelector("input, select, textarea, [tabindex]");
81
- if (i) {
82
- e.preventDefault(), i.focus();
83
- return;
84
- }
85
- s += t;
86
- } else r += t, s = t > 0 ? 0 : (n[r]?.cells.length ?? 1) - 1;
87
- } else if (e.key === "Enter") {
88
- e.preventDefault();
89
- let t = Array.from(i.rows);
90
- if (o < t.length - 1) {
91
- let e = t[o + 1].cells[a]?.querySelector("input, select, textarea, [tabindex]");
92
- e && e.focus();
93
- } else N(), requestAnimationFrame(() => {
94
- let e = i.querySelectorAll("tr"), t = e[e.length - 1];
95
- if (t) {
96
- let e = t.querySelector("input, select, textarea, [tabindex]");
97
- e && e.focus();
98
- }
99
- });
100
- } else e.key === "Escape" && t.blur?.();
101
- }, [N]), R = {
102
- height: 30,
103
- padding: "0 12px",
104
- textAlign: "left",
105
- fontWeight: 600,
106
- fontSize: 11,
107
- letterSpacing: "0.04em",
108
- color: "var(--text-3)",
109
- textTransform: "uppercase",
110
- borderBottom: "1px solid var(--divider)"
111
- };
112
- return /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("div", {
113
- style: { overflowX: "auto" },
114
- children: /* @__PURE__ */ l(d, {
115
- sensors: E,
116
- collisionDetection: p,
117
- onDragEnd: I,
118
- children: /* @__PURE__ */ u("table", {
119
- ref: C,
120
- style: {
121
- width: "100%",
122
- fontSize: 12.5,
123
- borderCollapse: "separate",
124
- borderSpacing: 0
125
- },
126
- onKeyDown: L,
127
- children: [/* @__PURE__ */ l("thead", { children: /* @__PURE__ */ u("tr", { children: [
128
- /* @__PURE__ */ l("th", { style: {
129
- ...R,
130
- width: 32,
131
- padding: "0 4px"
132
- } }),
133
- A.map((e) => {
134
- let t = r.fields.get(e);
135
- return /* @__PURE__ */ l("th", {
136
- style: R,
137
- children: t ? n(t, e, a.entities) : e
138
- }, e);
139
- }),
140
- /* @__PURE__ */ l("th", { style: {
141
- ...R,
142
- width: 40
143
- } })
144
- ] }) }), /* @__PURE__ */ l(g, {
145
- items: j,
146
- strategy: v,
147
- children: /* @__PURE__ */ l("tbody", { children: _.map((e) => /* @__PURE__ */ l(x, {
148
- row: e,
149
- columns: A,
150
- editableColumns: O,
151
- displayOnlyColumns: k,
152
- childEntity: r,
153
- validationErrors: b,
154
- refs: S,
155
- onCellChange: M,
156
- onDelete: P,
157
- onUndoDelete: F
158
- }, e._key)) })
159
- })]
160
- })
161
- })
162
- }), /* @__PURE__ */ l("div", {
163
- style: { marginTop: 8 },
164
- children: /* @__PURE__ */ l(i, {
165
- kind: "ghost",
166
- size: "xs",
167
- icon: /* @__PURE__ */ l(e.plus, { size: 12 }),
168
- onClick: N,
169
- children: "Add row"
170
- })
171
- })] });
172
- }
173
- function x({ row: n, columns: i, editableColumns: o, displayOnlyColumns: s, childEntity: c, validationErrors: d, refs: f, onCellChange: p, onDelete: m, onUndoDelete: h }) {
174
- let g = a(), v = !!n._deleted, b = d?.[n._key], { attributes: x, listeners: S, setNodeRef: C, transform: w, transition: T, isDragging: E } = _({
175
- id: n._key,
176
- disabled: v
177
- }), D = {
178
- transform: y.Translate.toString(w),
179
- transition: T,
180
- zIndex: E ? 10 : void 0,
181
- opacity: E ? .5 : void 0
182
- }, O = {
183
- padding: "6px 12px",
184
- borderBottom: "1px solid var(--divider)",
185
- verticalAlign: "middle",
186
- color: "var(--text-2)"
187
- }, k = {
188
- padding: "4px 4px",
189
- borderBottom: "1px solid var(--divider)",
190
- verticalAlign: "middle"
191
- };
192
- return /* @__PURE__ */ u("tr", {
193
- ref: C,
194
- style: {
195
- ...D,
196
- opacity: v ? .5 : D?.opacity ?? 1
197
- },
198
- children: [
199
- /* @__PURE__ */ l("td", {
200
- style: {
201
- padding: "4px 4px",
202
- borderBottom: "1px solid var(--divider)",
203
- width: 32,
204
- verticalAlign: "middle"
205
- },
206
- children: !v && /* @__PURE__ */ l("button", {
207
- type: "button",
208
- style: {
209
- cursor: "grab",
210
- padding: 4,
211
- color: "var(--text-3)",
212
- background: "transparent",
213
- border: 0,
214
- display: "inline-flex"
215
- },
216
- ...x,
217
- ...S,
218
- children: /* @__PURE__ */ l(e.grid, { size: 14 })
219
- })
220
- }),
221
- i.map((e) => {
222
- let t = c.fields.get(e), i = b?.includes(e), a = s.has(e), u = o.includes(e);
223
- return v || a || !u ? /* @__PURE__ */ l("td", {
224
- style: {
225
- ...O,
226
- textDecoration: v ? "line-through" : "none"
227
- },
228
- children: t ? r({
229
- field: t,
230
- fieldName: e,
231
- value: n[e],
232
- mode: "display",
233
- record: n,
234
- refs: f,
235
- locale: g
236
- }) : String(n[e] ?? "")
237
- }, e) : /* @__PURE__ */ l("td", {
238
- style: {
239
- ...k,
240
- outline: i ? "1px solid var(--danger)" : "none",
241
- outlineOffset: -1,
242
- borderRadius: i ? 4 : 0
243
- },
244
- title: i ? `${e} is required` : void 0,
245
- children: t ? r({
246
- field: t,
247
- fieldName: e,
248
- value: n[e],
249
- mode: "edit",
250
- onChange: (t) => p(n._key, e, t),
251
- record: n,
252
- refs: f,
253
- locale: g
254
- }) : String(n[e] ?? "")
255
- }, e);
256
- }),
257
- /* @__PURE__ */ l("td", {
258
- style: {
259
- padding: "4px 8px",
260
- borderBottom: "1px solid var(--divider)",
261
- verticalAlign: "middle"
262
- },
263
- children: v ? /* @__PURE__ */ l(t, {
264
- size: 24,
265
- icon: /* @__PURE__ */ l(e.refresh, { size: 12 }),
266
- onClick: () => h(n._key),
267
- title: "Undo delete"
268
- }) : /* @__PURE__ */ l(t, {
269
- size: 24,
270
- icon: /* @__PURE__ */ l(e.trash, { size: 12 }),
271
- onClick: () => m(n._key),
272
- title: "Delete row"
273
- })
274
- })
275
- ]
276
- });
277
- }
278
- //#endregion
279
- export { b as DraggableChildrenTable };
280
-
281
- //# sourceMappingURL=DraggableChildrenTable-IfspXSFA.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"DraggableChildrenTable-IfspXSFA.js","names":[],"sources":["../src/components/detail-layout/DraggableChildrenTable.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef } from \"react\";\nimport {\n DndContext,\n closestCenter,\n PointerSensor,\n useSensor,\n useSensors,\n type DragEndEvent,\n} from \"@dnd-kit/core\";\nimport {\n SortableContext,\n verticalListSortingStrategy,\n useSortable,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Btn, Icon, IconBtn } from \"@/components/primitives\";\nimport { fieldDisplayName } from \"@/types/schema\";\nimport type { Entity, Schema } from \"@/types/schema\";\nimport { renderField } from \"@/components/fields\";\nimport type { RefsMap } from \"@/components/fields\";\nimport { useUserLocale } from \"@/hooks/useUserLocale\";\nimport type { PendingChildRow } from \"./LayoutRenderer\";\nimport type { ResolvedChildConfig } from \"@/lib/child-config\";\n\ninterface DraggableChildrenTableProps {\n config: ResolvedChildConfig;\n childEntity: Entity;\n schema: Schema;\n rows: PendingChildRow[];\n onChange: (rows: PendingChildRow[]) => void;\n validationErrors?: Record<string, string[]>;\n refs?: RefsMap;\n}\n\n// DraggableChildrenTable renders child rows with drag handles for reordering.\n// Used when position_field is configured on a child relation.\nexport function DraggableChildrenTable({\n config,\n childEntity,\n schema,\n rows,\n onChange,\n validationErrors,\n refs,\n}: DraggableChildrenTableProps) {\n const tableRef = useRef<HTMLTableElement>(null);\n const positionField = config.position_field!;\n const columns = config.columns;\n\n // distance: 5 separates click from drag.\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n );\n\n // Editable columns: exclude FK, auto, primary, and position_field.\n const fkSet = new Set(Array.isArray(config.foreign_key) ? config.foreign_key : [config.foreign_key]);\n const editableColumns = columns.filter((col) => {\n if (fkSet.has(col)) return false;\n if (col === positionField) return false;\n const field = childEntity.fields.get(col);\n if (!field) return false;\n if (field.auto || field.primary) return false;\n return true;\n });\n\n // Display-only columns (auto/primary but visible in column list, or position_field).\n const displayOnlyColumns = new Set(\n columns.filter((col) => {\n if (col === positionField) return true;\n const field = childEntity.fields.get(col);\n return field && (field.auto || field.primary);\n }),\n );\n\n // Visible columns: exclude position_field from display since it's managed by drag.\n const visibleColumns = columns.filter((col) => col !== positionField);\n\n // Row IDs for SortableContext - only non-deleted rows participate in drag.\n const rowIds = useMemo(\n () => rows.filter((r) => !r._deleted).map((r) => r._key),\n [rows],\n );\n\n const handleCellChange = useCallback(\n (rowKey: string, fieldName: string, value: unknown) => {\n const updated = rows.map((row) =>\n row._key === rowKey\n ? { ...row, [fieldName]: value, _dirty: true }\n : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleAddRow = useCallback(() => {\n const maxPosition = rows\n .filter((r) => !r._deleted)\n .reduce((max, r) => {\n const pos = typeof r[positionField] === \"number\" ? (r[positionField] as number) : -1;\n return Math.max(max, pos);\n }, -1);\n\n const newRow: PendingChildRow = {\n _key: crypto.randomUUID(),\n _dirty: true,\n [positionField]: maxPosition + 1,\n };\n // Pre-fill from quick_add.defaults first, then field-level defaults.\n const quickDefaults = config.quick_add?.defaults ?? {};\n for (const col of editableColumns) {\n const field = childEntity.fields.get(col);\n if (!field) continue;\n if (quickDefaults[col] !== undefined) {\n newRow[col] = quickDefaults[col];\n } else if (field.default !== undefined) {\n newRow[col] = field.default;\n } else if (field.type === \"bool\") {\n newRow[col] = false;\n }\n }\n onChange([...rows, newRow]);\n }, [rows, onChange, editableColumns, childEntity, config.quick_add?.defaults, positionField]);\n\n const handleDeleteRow = useCallback(\n (rowKey: string) => {\n const updated = rows.map((row) =>\n row._key === rowKey ? { ...row, _deleted: true } : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleUndoDelete = useCallback(\n (rowKey: string) => {\n const updated = rows.map((row) =>\n row._key === rowKey ? { ...row, _deleted: false } : row,\n );\n onChange(updated);\n },\n [rows, onChange],\n );\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n\n // Reorder non-deleted rows.\n const nonDeleted = rows.filter((r) => !r._deleted);\n const deleted = rows.filter((r) => r._deleted);\n\n const oldIndex = nonDeleted.findIndex((r) => r._key === String(active.id));\n const newIndex = nonDeleted.findIndex((r) => r._key === String(over.id));\n if (oldIndex === -1 || newIndex === -1) return;\n\n // Move the row.\n const reordered = [...nonDeleted];\n const [moved] = reordered.splice(oldIndex, 1);\n reordered.splice(newIndex, 0, moved);\n\n // Re-assign sequential position values and mark dirty only if position changed.\n const withPositions = reordered.map((row, index) => ({\n ...row,\n [positionField]: index,\n _dirty: row[positionField] !== index ? true : row._dirty,\n }));\n\n // Append deleted rows at the end (they'll be excluded from payload).\n onChange([...withPositions, ...deleted]);\n },\n [rows, onChange, positionField],\n );\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLTableElement>) => {\n const target = e.target as HTMLElement;\n const cell = target.closest(\"td\");\n if (!cell) return;\n const row = cell.closest(\"tr\");\n if (!row) return;\n const tbody = row.closest(\"tbody\");\n if (!tbody) return;\n\n const cellIndex = Array.from(row.cells).indexOf(cell as HTMLTableCellElement);\n const rowIndex = Array.from(tbody.rows).indexOf(row as HTMLTableRowElement);\n\n if (e.key === \"Tab\") {\n const direction = e.shiftKey ? -1 : 1;\n const allRows = Array.from(tbody.rows);\n let nextRow = rowIndex;\n let nextCell = cellIndex + direction;\n\n while (nextRow >= 0 && nextRow < allRows.length) {\n if (nextCell >= 0 && nextCell < allRows[nextRow].cells.length) {\n const nextTd = allRows[nextRow].cells[nextCell];\n const input = nextTd.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (input) {\n e.preventDefault();\n input.focus();\n return;\n }\n nextCell += direction;\n } else {\n nextRow += direction;\n nextCell = direction > 0 ? 0 : (allRows[nextRow]?.cells.length ?? 1) - 1;\n }\n }\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n const allRows = Array.from(tbody.rows);\n if (rowIndex < allRows.length - 1) {\n const nextTd = allRows[rowIndex + 1].cells[cellIndex];\n const input = nextTd?.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (input) input.focus();\n } else {\n handleAddRow();\n requestAnimationFrame(() => {\n const newRows = tbody.querySelectorAll(\"tr\");\n const lastRow = newRows[newRows.length - 1];\n if (lastRow) {\n const firstInput = lastRow.querySelector(\"input, select, textarea, [tabindex]\") as HTMLElement | null;\n if (firstInput) firstInput.focus();\n }\n });\n }\n } else if (e.key === \"Escape\") {\n (target as HTMLElement).blur?.();\n }\n },\n [handleAddRow],\n );\n\n const thStyle: React.CSSProperties = {\n height: 30,\n padding: \"0 12px\",\n textAlign: \"left\",\n fontWeight: 600,\n fontSize: 11,\n letterSpacing: \"0.04em\",\n color: \"var(--text-3)\",\n textTransform: \"uppercase\",\n borderBottom: \"1px solid var(--divider)\",\n };\n\n return (\n <div>\n <div style={{ overflowX: \"auto\" }}>\n <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>\n <table\n ref={tableRef}\n style={{ width: \"100%\", fontSize: 12.5, borderCollapse: \"separate\", borderSpacing: 0 }}\n onKeyDown={handleKeyDown}\n >\n <thead>\n <tr>\n <th style={{ ...thStyle, width: 32, padding: \"0 4px\" }} />\n {visibleColumns.map((col) => {\n const field = childEntity.fields.get(col);\n return (\n <th key={col} style={thStyle}>\n {field ? fieldDisplayName(field, col, schema.entities) : col}\n </th>\n );\n })}\n <th style={{ ...thStyle, width: 40 }} />\n </tr>\n </thead>\n <SortableContext items={rowIds} strategy={verticalListSortingStrategy}>\n <tbody>\n {rows.map((row) => (\n <SortableRow\n key={row._key}\n row={row}\n columns={visibleColumns}\n editableColumns={editableColumns}\n displayOnlyColumns={displayOnlyColumns}\n childEntity={childEntity}\n validationErrors={validationErrors}\n refs={refs}\n onCellChange={handleCellChange}\n onDelete={handleDeleteRow}\n onUndoDelete={handleUndoDelete}\n />\n ))}\n </tbody>\n </SortableContext>\n </table>\n </DndContext>\n </div>\n <div style={{ marginTop: 8 }}>\n <Btn kind=\"ghost\" size=\"xs\" icon={<Icon.plus size={12} />} onClick={handleAddRow}>\n Add row\n </Btn>\n </div>\n </div>\n );\n}\n\n// SortableRow wraps a table row with drag-and-drop support.\ninterface SortableRowProps {\n row: PendingChildRow;\n columns: string[];\n editableColumns: string[];\n displayOnlyColumns: Set<string>;\n childEntity: Entity;\n validationErrors?: Record<string, string[]>;\n refs?: RefsMap;\n onCellChange: (rowKey: string, fieldName: string, value: unknown) => void;\n onDelete: (rowKey: string) => void;\n onUndoDelete: (rowKey: string) => void;\n}\n\nfunction SortableRow({\n row,\n columns,\n editableColumns,\n displayOnlyColumns,\n childEntity,\n validationErrors,\n refs,\n onCellChange,\n onDelete,\n onUndoDelete,\n}: SortableRowProps) {\n const userLocale = useUserLocale();\n const isDeleted = !!row._deleted;\n const rowErrors = validationErrors?.[row._key];\n\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: row._key, disabled: isDeleted });\n\n const style = {\n transform: CSS.Translate.toString(transform),\n transition,\n zIndex: isDragging ? 10 : undefined,\n opacity: isDragging ? 0.5 : undefined,\n };\n\n const cellStyle: React.CSSProperties = {\n padding: \"6px 12px\",\n borderBottom: \"1px solid var(--divider)\",\n verticalAlign: \"middle\",\n color: \"var(--text-2)\",\n };\n const editCellStyle: React.CSSProperties = {\n padding: \"4px 4px\",\n borderBottom: \"1px solid var(--divider)\",\n verticalAlign: \"middle\",\n };\n\n return (\n <tr ref={setNodeRef} style={{ ...style, opacity: isDeleted ? 0.5 : style?.opacity ?? 1 }}>\n <td style={{ padding: \"4px 4px\", borderBottom: \"1px solid var(--divider)\", width: 32, verticalAlign: \"middle\" }}>\n {!isDeleted && (\n <button\n type=\"button\"\n style={{\n cursor: \"grab\",\n padding: 4,\n color: \"var(--text-3)\",\n background: \"transparent\",\n border: 0,\n display: \"inline-flex\",\n }}\n {...attributes}\n {...listeners}\n >\n <Icon.grid size={14} />\n </button>\n )}\n </td>\n {columns.map((col) => {\n const field = childEntity.fields.get(col);\n const hasError = rowErrors?.includes(col);\n const isDisplayOnly = displayOnlyColumns.has(col);\n const isEditable = editableColumns.includes(col);\n\n if (isDeleted || isDisplayOnly || !isEditable) {\n return (\n <td\n key={col}\n style={{\n ...cellStyle,\n textDecoration: isDeleted ? \"line-through\" : \"none\",\n }}\n >\n {field\n ? renderField({\n field,\n fieldName: col,\n value: row[col],\n mode: \"display\",\n record: row as Record<string, unknown>,\n refs,\n locale: userLocale,\n })\n : String(row[col] ?? \"\")}\n </td>\n );\n }\n\n return (\n <td\n key={col}\n style={{\n ...editCellStyle,\n outline: hasError ? \"1px solid var(--danger)\" : \"none\",\n outlineOffset: -1,\n borderRadius: hasError ? 4 : 0,\n }}\n title={hasError ? `${col} is required` : undefined}\n >\n {field\n ? renderField({\n field,\n fieldName: col,\n value: row[col],\n mode: \"edit\",\n onChange: (val) => onCellChange(row._key, col, val),\n record: row as Record<string, unknown>,\n refs,\n locale: userLocale,\n })\n : String(row[col] ?? \"\")}\n </td>\n );\n })}\n <td style={{ padding: \"4px 8px\", borderBottom: \"1px solid var(--divider)\", verticalAlign: \"middle\" }}>\n {isDeleted ? (\n <IconBtn\n size={24}\n icon={<Icon.refresh size={12} />}\n onClick={() => onUndoDelete(row._key)}\n title=\"Undo delete\"\n />\n ) : (\n <IconBtn\n size={24}\n icon={<Icon.trash size={12} />}\n onClick={() => onDelete(row._key)}\n title=\"Delete row\"\n />\n )}\n </td>\n </tr>\n );\n}\n"],"mappings":"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"}