@declarion/react 0.1.80 → 0.1.81

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 (51) hide show
  1. package/dist-lib/{DraggableChildrenTable-CcXR8Vm6.js → DraggableChildrenTable-XKJWMb1S.js} +2 -2
  2. package/dist-lib/{DraggableChildrenTable-CcXR8Vm6.js.map → DraggableChildrenTable-XKJWMb1S.js.map} +1 -1
  3. package/dist-lib/{Grid-CseTLMLv.js → Grid-By_Ere-W.js} +2 -2
  4. package/dist-lib/{Grid-CseTLMLv.js.map → Grid-By_Ere-W.js.map} +1 -1
  5. package/dist-lib/{JsonEditor-Tew1oNqZ.js → JsonEditor-CexL5HXN.js} +17 -15
  6. package/dist-lib/JsonEditor-CexL5HXN.js.map +1 -0
  7. package/dist-lib/api/events.d.ts +5 -0
  8. package/dist-lib/components/fields/BoolField.d.ts +1 -1
  9. package/dist-lib/components/fields/EmailField.d.ts +1 -1
  10. package/dist-lib/components/fields/EnumField.d.ts +1 -1
  11. package/dist-lib/components/fields/IntArrayField.d.ts +1 -1
  12. package/dist-lib/components/fields/JsonEditor.d.ts +2 -1
  13. package/dist-lib/components/fields/JsonField.d.ts +1 -1
  14. package/dist-lib/components/fields/MultilangField.d.ts +1 -1
  15. package/dist-lib/components/fields/MultilangTextField.d.ts +1 -1
  16. package/dist-lib/components/fields/NumberField.d.ts +1 -1
  17. package/dist-lib/components/fields/PasswordField.d.ts +1 -1
  18. package/dist-lib/components/fields/PhoneField.d.ts +1 -1
  19. package/dist-lib/components/fields/RefField.d.ts +1 -1
  20. package/dist-lib/components/fields/SecretField.d.ts +1 -1
  21. package/dist-lib/components/fields/StringArrayField.d.ts +1 -1
  22. package/dist-lib/components/fields/StringField.d.ts +1 -1
  23. package/dist-lib/components/fields/TextField.d.ts +1 -1
  24. package/dist-lib/components/fields/TimestampField.d.ts +1 -1
  25. package/dist-lib/components/fields/UrlField.d.ts +1 -1
  26. package/dist-lib/components/fields/index.d.ts +2 -0
  27. package/dist-lib/components/file-widgets/host.d.ts +2 -0
  28. package/dist-lib/components/file-widgets/types.d.ts +1 -0
  29. package/dist-lib/components/pages/list/DataCell.d.ts +7 -0
  30. package/dist-lib/components/pages/list/ListBulkBar.d.ts +27 -0
  31. package/dist-lib/components/pages/list/ListHeader.d.ts +23 -0
  32. package/dist-lib/components/pages/list/RowActionsCell.d.ts +38 -0
  33. package/dist-lib/components/pages/list/SaveViewDialog.d.ts +6 -0
  34. package/dist-lib/components/pages/list/columns.d.ts +42 -0
  35. package/dist-lib/components/pages/list/filter-meta.d.ts +9 -0
  36. package/dist-lib/components/primitives/BoolToggle.d.ts +3 -1
  37. package/dist-lib/components/primitives/Fields.d.ts +12 -3
  38. package/dist-lib/index.js +4189 -4066
  39. package/dist-lib/index.js.map +1 -1
  40. package/dist-lib/lib/action-dispatch.d.ts +7 -0
  41. package/dist-lib/stores/realtime.d.ts +9 -0
  42. package/dist-lib/types/schema.d.ts +11 -1
  43. package/dist-lib/{useUserLocale-D9wc5kzQ.js → useUserLocale-CSS6fwi8.js} +1110 -1003
  44. package/dist-lib/useUserLocale-CSS6fwi8.js.map +1 -0
  45. package/dist-lib/{value-FIp50R1-.js → value-DgzPSFZM.js} +149 -196
  46. package/dist-lib/value-DgzPSFZM.js.map +1 -0
  47. package/package.json +1 -1
  48. package/dist-lib/JsonEditor-Tew1oNqZ.js.map +0 -1
  49. package/dist-lib/components/shared/RowActionItem.d.ts +0 -11
  50. package/dist-lib/useUserLocale-D9wc5kzQ.js.map +0 -1
  51. package/dist-lib/value-FIp50R1-.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { Gt as e, Ut as t, Wt as n, at as r, n as i, t as a } from "./useUserLocale-D9wc5kzQ.js";
1
+ import { Gt as e, Ut as t, Wt as n, at as r, n as i, t as a } from "./useUserLocale-CSS6fwi8.js";
2
2
  import { useCallback as o, useMemo as s, useRef as c } from "react";
3
3
  import { jsx as l, jsxs as u } from "react/jsx-runtime";
4
4
  import { DndContext as d, PointerSensor as f, closestCenter as p, useSensor as m, useSensors as h } from "@dnd-kit/core";
@@ -278,4 +278,4 @@ function x({ row: n, columns: r, editableColumns: o, displayOnlyColumns: s, chil
278
278
  //#endregion
279
279
  export { b as DraggableChildrenTable };
280
280
 
281
- //# sourceMappingURL=DraggableChildrenTable-CcXR8Vm6.js.map
281
+ //# sourceMappingURL=DraggableChildrenTable-XKJWMb1S.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DraggableChildrenTable-CcXR8Vm6.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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"}
1
+ {"version":3,"file":"DraggableChildrenTable-XKJWMb1S.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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"}
@@ -1,4 +1,4 @@
1
- import { _ as e, g as t, h as n, i as r, l as i, m as a, t as o, v as s } from "./value-FIp50R1-.js";
1
+ import { _ as e, g as t, h as n, i as r, l as i, m as a, t as o, v as s } from "./value-DgzPSFZM.js";
2
2
  import { useCallback as c, useEffect as l, useMemo as u, useRef as d, useState as f } from "react";
3
3
  import { Fragment as p, jsx as m, jsxs as h } from "react/jsx-runtime";
4
4
  import { DndContext as g, PointerSensor as _, closestCenter as v, useSensor as y, useSensors as ee } from "@dnd-kit/core";
@@ -402,4 +402,4 @@ function D({ file: e, hasPrev: n, hasNext: r, onPrev: i, onNext: a, onClose: o }
402
402
  //#endregion
403
403
  export { T as Grid };
404
404
 
405
- //# sourceMappingURL=Grid-CseTLMLv.js.map
405
+ //# sourceMappingURL=Grid-By_Ere-W.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Grid-CseTLMLv.js","names":[],"sources":["../src/components/file-widgets/Grid.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { CSSProperties, DragEvent, KeyboardEvent } 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 rectSortingStrategy,\n useSortable,\n arrayMove,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n uploadFile,\n discardFile,\n validateClientSide,\n formatBytes,\n UploadError,\n} from \"./upload\";\nimport { useDraftRegistration } from \"./draft-scope\";\nimport { preferredListThumbnailURL } from \"./helpers\";\nimport { fileArrayFromValue } from \"./value\";\nimport type {\n FileWidgetProps,\n HydratedFile,\n UploadProgress,\n} from \"./types\";\n\n// Grid widget: thumbnail grid for `mode: array` files. Per einstein §6.4.\n//\n// Behaviors:\n// - drag-drop multi-upload (each file gets its own progress tile)\n// - per-item hover overlay (View / Metadata / Delete)\n// - drag-handle reorder via @dnd-kit\n// - multi-select toolbar with Bulk Delete\n// - click → lightbox with prev/next + keyboard nav\n// - IntersectionObserver lazy thumbnail loading\n// - per-tile thumbnail prefers the schema-selected list derivation, then\n// falls back to built-in thumbs or the primary file URL\n//\n// Value contract: array of HydratedFile. onChange emits the full new array\n// (callers persist it via the entity's $files payload on save).\n\ninterface InFlightTile {\n // Local-only tiles representing uploads in progress. Once finished they\n // are appended to the value array and removed from this list.\n localId: string;\n filename: string;\n progress: UploadProgress | null;\n error: { code: string; message: string } | null;\n abort: () => void;\n}\n\nfunction thumbUrl(f: HydratedFile, preferredDerivationKey?: string): string | undefined {\n return preferredListThumbnailURL(f, preferredDerivationKey);\n}\n\nexport function Grid(props: FileWidgetProps) {\n const { field, fieldName, value, onChange, mode, disabled } = props;\n const items = fileArrayFromValue(value);\n const [tiles, setTiles] = useState<InFlightTile[]>([]);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [lightbox, setLightbox] = useState<number | null>(null);\n const { register, unregister, isDraft } = useDraftRegistration();\n const cancelledUploadsRef = useRef<Set<string>>(new Set());\n const tilesRef = useRef<InFlightTile[]>([]);\n tilesRef.current = tiles;\n // valueRef snapshots the current value array so parallel uploads append\n // against the most recent state, not the closure they were created with.\n const valueRef = useRef<HydratedFile[]>(items);\n valueRef.current = items;\n\n // Cleanup all in-flight uploads on unmount.\n useEffect(() => {\n return () => {\n tilesRef.current.forEach((t) => t.abort());\n };\n }, []);\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n );\n\n const ariaLabel = field.display?.placeholder || fieldName;\n const accept = (field.accepts ?? [\"image/*\"]).join(\",\");\n const reorderable = field.display?.reorderable !== false;\n const preferredThumbnail = field.display?.listable_thumbnail;\n\n const startUpload = useCallback(\n async (raw: File) => {\n const v = validateClientSide(raw, field.accepts, field.max_size_bytes);\n const localId = `tile-${Math.random().toString(36).slice(2)}-${Date.now()}`;\n const ctrl = new AbortController();\n\n if (v) {\n setTiles((prev) => [\n ...prev,\n {\n localId,\n filename: raw.name,\n progress: null,\n error: v,\n abort: () => {},\n },\n ]);\n return;\n }\n\n setTiles((prev) => [\n ...prev,\n {\n localId,\n filename: raw.name,\n progress: { loaded: 0, total: raw.size, pct: 0 },\n error: null,\n abort: () => ctrl.abort(),\n },\n ]);\n\n try {\n const result = await uploadFile({\n file: raw,\n entityCode: props.entityCode,\n fieldCode: fieldName,\n signal: ctrl.signal,\n onProgress: (p) =>\n setTiles((prev) =>\n prev.map((t) =>\n t.localId === localId ? { ...t, progress: p } : t,\n ),\n ),\n });\n if (cancelledUploadsRef.current.delete(localId)) {\n void discardFile(result.id).catch(() => {});\n unregister(result.id);\n return;\n }\n register(result.id, () => discardFile(result.id));\n const next: HydratedFile = {\n id: result.id,\n filename: result.filename,\n content_type: result.content_type,\n size_bytes: result.size_bytes,\n sha256: result.sha256,\n width: result.width,\n height: result.height,\n url: result.url,\n metadata: result.metadata,\n };\n const merged = [...valueRef.current, next];\n valueRef.current = merged;\n onChange?.(merged);\n setTiles((prev) => prev.filter((t) => t.localId !== localId));\n } catch (err) {\n if (err instanceof UploadError && err.code === \"ABORTED\") {\n setTiles((prev) => prev.filter((t) => t.localId !== localId));\n return;\n }\n const e =\n err instanceof UploadError\n ? { code: err.code, message: err.message }\n : {\n code: \"UNKNOWN\",\n message: err instanceof Error ? err.message : \"Upload failed\",\n };\n setTiles((prev) =>\n prev.map((t) =>\n t.localId === localId\n ? { ...t, error: e, progress: null, abort: () => {} }\n : t,\n ),\n );\n }\n },\n [field.accepts, field.max_size_bytes, fieldName, onChange, props.entityCode, register, unregister],\n );\n\n const handleFiles = useCallback(\n (files: FileList | File[]) => {\n const arr = Array.from(files);\n arr.forEach((f) => void startUpload(f));\n },\n [startUpload],\n );\n\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n const onPick = useCallback(() => {\n if (disabled) return;\n fileInputRef.current?.click();\n }, [disabled]);\n\n const onDragOver = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n if (disabled) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n },\n [disabled],\n );\n\n const onDrop = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n if (disabled) return;\n e.preventDefault();\n const files = e.dataTransfer.files;\n if (files && files.length > 0) handleFiles(files);\n },\n [disabled, handleFiles],\n );\n\n const onChangeFile = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n e.target.value = \"\";\n if (files && files.length > 0) handleFiles(files);\n },\n [handleFiles],\n );\n\n const handleRemove = useCallback(\n (id: string) => {\n if (isDraft(id)) {\n void discardFile(id).catch(() => {});\n unregister(id);\n }\n const next = items.filter((f) => f.id !== id);\n onChange?.(next);\n setSelected((prev) => {\n const n = new Set(prev);\n n.delete(id);\n return n;\n });\n },\n [isDraft, items, onChange, unregister],\n );\n\n const handleBulkDelete = useCallback(() => {\n if (selected.size === 0) return;\n const ids = Array.from(selected);\n ids.forEach((id) => {\n if (isDraft(id)) {\n void discardFile(id).catch(() => {});\n unregister(id);\n }\n });\n onChange?.(items.filter((f) => !selected.has(f.id)));\n setSelected(new Set());\n }, [isDraft, items, selected, onChange, unregister]);\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const oldIdx = items.findIndex((f) => f.id === active.id);\n const newIdx = items.findIndex((f) => f.id === over.id);\n if (oldIdx < 0 || newIdx < 0) return;\n onChange?.(arrayMove(items, oldIdx, newIdx));\n },\n [items, onChange],\n );\n\n const toggleSelect = useCallback((id: string) => {\n setSelected((prev) => {\n const n = new Set(prev);\n if (n.has(id)) n.delete(id);\n else n.add(id);\n return n;\n });\n }, []);\n\n const lightboxFile = lightbox != null ? items[lightbox] : null;\n const lightboxNext = useCallback(() => {\n setLightbox((cur) =>\n cur == null ? cur : Math.min(items.length - 1, cur + 1),\n );\n }, [items.length]);\n const lightboxPrev = useCallback(() => {\n setLightbox((cur) => (cur == null ? cur : Math.max(0, cur - 1)));\n }, []);\n const lightboxClose = useCallback(() => setLightbox(null), []);\n\n useEffect(() => {\n if (lightbox == null) return;\n const handler = (e: globalThis.KeyboardEvent) => {\n if (e.key === \"Escape\") lightboxClose();\n else if (e.key === \"ArrowRight\") lightboxNext();\n else if (e.key === \"ArrowLeft\") lightboxPrev();\n };\n window.addEventListener(\"keydown\", handler);\n return () => window.removeEventListener(\"keydown\", handler);\n }, [lightbox, lightboxClose, lightboxNext, lightboxPrev]);\n\n const onGridKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n if (e.target === e.currentTarget) {\n e.preventDefault();\n onPick();\n }\n }\n },\n [disabled, onPick],\n );\n\n const isDisplay = mode === \"display\";\n\n const sortableIds = useMemo(() => items.map((f) => f.id), [items]);\n\n return (\n <div data-testid={`file-widget-${fieldName}`} className=\"flex flex-col gap-2\">\n {!isDisplay && selected.size > 0 && (\n <div\n className=\"flex items-center gap-2 rounded-md border border-border bg-surface-2 p-2 text-sm\"\n role=\"toolbar\"\n aria-label=\"Bulk actions\"\n >\n <span className=\"text-foreground\">{selected.size} selected</span>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-1 text-xs text-destructive hover:bg-surface-2\"\n onClick={handleBulkDelete}\n data-testid=\"bulk-delete\"\n >\n Delete\n </button>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-1 text-xs text-foreground hover:bg-surface-2\"\n onClick={() => setSelected(new Set())}\n >\n Clear\n </button>\n </div>\n )}\n\n <DndContext\n sensors={sensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={sortableIds} strategy={rectSortingStrategy}>\n <div\n className=\"grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 rounded-md border border-dashed border-border bg-surface p-3 outline-none focus-visible:outline-2 focus-visible:outline-accent\"\n role=\"grid\"\n aria-label={ariaLabel}\n tabIndex={isDisplay || disabled ? -1 : 0}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onKeyDown={onGridKeyDown}\n data-testid=\"grid-container\"\n >\n {items.map((f, idx) => (\n <SortableTile\n key={f.id}\n file={f}\n selected={selected.has(f.id)}\n disabled={disabled || isDisplay}\n reorderable={reorderable && !isDisplay}\n preferredThumbnail={preferredThumbnail}\n onSelect={() => toggleSelect(f.id)}\n onRemove={() => handleRemove(f.id)}\n onPreview={() => setLightbox(idx)}\n />\n ))}\n {tiles.map((t) => (\n <PendingTile\n key={t.localId}\n tile={t}\n onCancel={() => {\n t.abort();\n cancelledUploadsRef.current.add(t.localId);\n setTiles((prev) =>\n prev.filter((x) => x.localId !== t.localId),\n );\n }}\n onDismiss={() =>\n setTiles((prev) =>\n prev.filter((x) => x.localId !== t.localId),\n )\n }\n />\n ))}\n {items.length === 0 && tiles.length === 0 && (\n <div\n className=\"col-span-full flex h-24 cursor-pointer items-center justify-center text-sm text-muted-foreground\"\n onClick={onPick}\n data-testid=\"grid-empty\"\n >\n {field.display?.placeholder || \"Drop files here or click to browse\"}\n </div>\n )}\n </div>\n </SortableContext>\n </DndContext>\n\n {!isDisplay && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n onChange={onChangeFile}\n style={{ display: \"none\" }}\n data-testid={`file-input-${fieldName}`}\n />\n )}\n\n {lightboxFile && (\n <Lightbox\n file={lightboxFile}\n hasPrev={lightbox! > 0}\n hasNext={lightbox! < items.length - 1}\n onPrev={lightboxPrev}\n onNext={lightboxNext}\n onClose={lightboxClose}\n />\n )}\n </div>\n );\n}\n\nfunction SortableTile({\n file,\n selected,\n disabled,\n reorderable,\n preferredThumbnail,\n onSelect,\n onRemove,\n onPreview,\n}: {\n file: HydratedFile;\n selected: boolean;\n disabled?: boolean;\n reorderable: boolean;\n preferredThumbnail?: string;\n onSelect: () => void;\n onRemove: () => void;\n onPreview: () => void;\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: file.id, disabled: !reorderable });\n const style: CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n };\n const url = thumbUrl(file, preferredThumbnail);\n return (\n <div\n ref={setNodeRef}\n style={style}\n className={`group relative aspect-square overflow-hidden rounded-md border ${\n selected ? \"border-accent\" : \"border-border\"\n } bg-surface-2`}\n data-testid={`grid-tile-${file.id}`}\n data-selected={selected || undefined}\n >\n {/* Lazy thumb: native loading=\"lazy\" approximates IntersectionObserver\n and is what the rest of the SDK uses (see ImageWidget). */}\n {url ? (\n <img\n src={url}\n alt={file.filename}\n className=\"h-full w-full cursor-pointer object-cover\"\n loading=\"lazy\"\n onClick={onPreview}\n />\n ) : (\n <div\n className=\"flex h-full w-full cursor-pointer items-center justify-center bg-surface-3 text-xs text-muted-foreground\"\n onClick={onPreview}\n >\n {file.filename}\n </div>\n )}\n {!disabled && (\n <>\n <input\n type=\"checkbox\"\n className=\"absolute left-1 top-1 h-4 w-4 cursor-pointer\"\n checked={selected}\n onChange={onSelect}\n aria-label={`Select ${file.filename}`}\n data-testid={`select-${file.id}`}\n onClick={(e) => e.stopPropagation()}\n />\n {reorderable && (\n <button\n type=\"button\"\n className=\"absolute right-1 top-1 cursor-grab rounded-sm border border-border bg-card px-1 text-xs\"\n aria-label={`Drag to reorder ${file.filename}`}\n data-testid={`drag-${file.id}`}\n {...attributes}\n {...listeners}\n >\n ⋮⋮\n </button>\n )}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center justify-end gap-1 bg-black/50 p-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100\">\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-0.5 text-xs\"\n onClick={onPreview}\n >\n View\n </button>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-0.5 text-xs text-destructive\"\n onClick={onRemove}\n data-testid={`remove-${file.id}`}\n >\n Delete\n </button>\n </div>\n </>\n )}\n </div>\n );\n}\n\nfunction PendingTile({\n tile,\n onCancel,\n onDismiss,\n}: {\n tile: InFlightTile;\n onCancel: () => void;\n onDismiss: () => void;\n}) {\n return (\n <div\n className={`relative flex aspect-square flex-col items-center justify-center rounded-md border ${\n tile.error ? \"border-destructive\" : \"border-border\"\n } bg-surface-2 p-2 text-center text-xs`}\n data-testid={`pending-tile-${tile.localId}`}\n >\n <span className=\"truncate\" title={tile.filename}>\n {tile.filename}\n </span>\n {tile.progress && !tile.error && (\n <span\n className=\"mt-1 text-muted-foreground\"\n role=\"progressbar\"\n aria-valuenow={tile.progress.pct}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n {tile.progress.pct}%\n </span>\n )}\n {tile.error && (\n <div role=\"alert\" className=\"mt-1 text-destructive\">\n {tile.error.message}\n </div>\n )}\n <button\n type=\"button\"\n className=\"mt-1 rounded-sm border border-border bg-card px-2 py-0.5\"\n onClick={tile.error ? onDismiss : onCancel}\n >\n {tile.error ? \"Dismiss\" : \"Cancel\"}\n </button>\n </div>\n );\n}\n\nfunction Lightbox({\n file,\n hasPrev,\n hasNext,\n onPrev,\n onNext,\n onClose,\n}: {\n file: HydratedFile;\n hasPrev: boolean;\n hasNext: boolean;\n onPrev: () => void;\n onNext: () => void;\n onClose: () => void;\n}) {\n const url = file.presigned_url ?? file.url ?? thumbUrl(file);\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/80\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={file.filename}\n data-testid=\"lightbox\"\n onClick={onClose}\n >\n <button\n type=\"button\"\n className=\"absolute right-4 top-4 rounded-sm border border-border bg-card px-3 py-1 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onClose();\n }}\n data-testid=\"lightbox-close\"\n >\n Close\n </button>\n {hasPrev && (\n <button\n type=\"button\"\n className=\"absolute left-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onPrev();\n }}\n data-testid=\"lightbox-prev\"\n >\n ‹\n </button>\n )}\n {hasNext && (\n <button\n type=\"button\"\n className=\"absolute right-4 bottom-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onNext();\n }}\n data-testid=\"lightbox-next\"\n >\n ›\n </button>\n )}\n {url ? (\n <img\n src={url}\n alt={file.filename}\n className=\"max-h-[90vh] max-w-[90vw] object-contain\"\n onClick={(e) => e.stopPropagation()}\n />\n ) : (\n <div className=\"rounded-md bg-card p-4 text-foreground\">\n {file.filename} ({formatBytes(file.size_bytes)})\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Grid-By_Ere-W.js","names":[],"sources":["../src/components/file-widgets/Grid.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { CSSProperties, DragEvent, KeyboardEvent } 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 rectSortingStrategy,\n useSortable,\n arrayMove,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport {\n uploadFile,\n discardFile,\n validateClientSide,\n formatBytes,\n UploadError,\n} from \"./upload\";\nimport { useDraftRegistration } from \"./draft-scope\";\nimport { preferredListThumbnailURL } from \"./helpers\";\nimport { fileArrayFromValue } from \"./value\";\nimport type {\n FileWidgetProps,\n HydratedFile,\n UploadProgress,\n} from \"./types\";\n\n// Grid widget: thumbnail grid for `mode: array` files. Per einstein §6.4.\n//\n// Behaviors:\n// - drag-drop multi-upload (each file gets its own progress tile)\n// - per-item hover overlay (View / Metadata / Delete)\n// - drag-handle reorder via @dnd-kit\n// - multi-select toolbar with Bulk Delete\n// - click → lightbox with prev/next + keyboard nav\n// - IntersectionObserver lazy thumbnail loading\n// - per-tile thumbnail prefers the schema-selected list derivation, then\n// falls back to built-in thumbs or the primary file URL\n//\n// Value contract: array of HydratedFile. onChange emits the full new array\n// (callers persist it via the entity's $files payload on save).\n\ninterface InFlightTile {\n // Local-only tiles representing uploads in progress. Once finished they\n // are appended to the value array and removed from this list.\n localId: string;\n filename: string;\n progress: UploadProgress | null;\n error: { code: string; message: string } | null;\n abort: () => void;\n}\n\nfunction thumbUrl(f: HydratedFile, preferredDerivationKey?: string): string | undefined {\n return preferredListThumbnailURL(f, preferredDerivationKey);\n}\n\nexport function Grid(props: FileWidgetProps) {\n const { field, fieldName, value, onChange, mode, disabled } = props;\n const items = fileArrayFromValue(value);\n const [tiles, setTiles] = useState<InFlightTile[]>([]);\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [lightbox, setLightbox] = useState<number | null>(null);\n const { register, unregister, isDraft } = useDraftRegistration();\n const cancelledUploadsRef = useRef<Set<string>>(new Set());\n const tilesRef = useRef<InFlightTile[]>([]);\n tilesRef.current = tiles;\n // valueRef snapshots the current value array so parallel uploads append\n // against the most recent state, not the closure they were created with.\n const valueRef = useRef<HydratedFile[]>(items);\n valueRef.current = items;\n\n // Cleanup all in-flight uploads on unmount.\n useEffect(() => {\n return () => {\n tilesRef.current.forEach((t) => t.abort());\n };\n }, []);\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),\n );\n\n const ariaLabel = field.display?.placeholder || fieldName;\n const accept = (field.accepts ?? [\"image/*\"]).join(\",\");\n const reorderable = field.display?.reorderable !== false;\n const preferredThumbnail = field.display?.listable_thumbnail;\n\n const startUpload = useCallback(\n async (raw: File) => {\n const v = validateClientSide(raw, field.accepts, field.max_size_bytes);\n const localId = `tile-${Math.random().toString(36).slice(2)}-${Date.now()}`;\n const ctrl = new AbortController();\n\n if (v) {\n setTiles((prev) => [\n ...prev,\n {\n localId,\n filename: raw.name,\n progress: null,\n error: v,\n abort: () => {},\n },\n ]);\n return;\n }\n\n setTiles((prev) => [\n ...prev,\n {\n localId,\n filename: raw.name,\n progress: { loaded: 0, total: raw.size, pct: 0 },\n error: null,\n abort: () => ctrl.abort(),\n },\n ]);\n\n try {\n const result = await uploadFile({\n file: raw,\n entityCode: props.entityCode,\n fieldCode: fieldName,\n signal: ctrl.signal,\n onProgress: (p) =>\n setTiles((prev) =>\n prev.map((t) =>\n t.localId === localId ? { ...t, progress: p } : t,\n ),\n ),\n });\n if (cancelledUploadsRef.current.delete(localId)) {\n void discardFile(result.id).catch(() => {});\n unregister(result.id);\n return;\n }\n register(result.id, () => discardFile(result.id));\n const next: HydratedFile = {\n id: result.id,\n filename: result.filename,\n content_type: result.content_type,\n size_bytes: result.size_bytes,\n sha256: result.sha256,\n width: result.width,\n height: result.height,\n url: result.url,\n metadata: result.metadata,\n };\n const merged = [...valueRef.current, next];\n valueRef.current = merged;\n onChange?.(merged);\n setTiles((prev) => prev.filter((t) => t.localId !== localId));\n } catch (err) {\n if (err instanceof UploadError && err.code === \"ABORTED\") {\n setTiles((prev) => prev.filter((t) => t.localId !== localId));\n return;\n }\n const e =\n err instanceof UploadError\n ? { code: err.code, message: err.message }\n : {\n code: \"UNKNOWN\",\n message: err instanceof Error ? err.message : \"Upload failed\",\n };\n setTiles((prev) =>\n prev.map((t) =>\n t.localId === localId\n ? { ...t, error: e, progress: null, abort: () => {} }\n : t,\n ),\n );\n }\n },\n [field.accepts, field.max_size_bytes, fieldName, onChange, props.entityCode, register, unregister],\n );\n\n const handleFiles = useCallback(\n (files: FileList | File[]) => {\n const arr = Array.from(files);\n arr.forEach((f) => void startUpload(f));\n },\n [startUpload],\n );\n\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n const onPick = useCallback(() => {\n if (disabled) return;\n fileInputRef.current?.click();\n }, [disabled]);\n\n const onDragOver = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n if (disabled) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = \"copy\";\n },\n [disabled],\n );\n\n const onDrop = useCallback(\n (e: DragEvent<HTMLDivElement>) => {\n if (disabled) return;\n e.preventDefault();\n const files = e.dataTransfer.files;\n if (files && files.length > 0) handleFiles(files);\n },\n [disabled, handleFiles],\n );\n\n const onChangeFile = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n e.target.value = \"\";\n if (files && files.length > 0) handleFiles(files);\n },\n [handleFiles],\n );\n\n const handleRemove = useCallback(\n (id: string) => {\n if (isDraft(id)) {\n void discardFile(id).catch(() => {});\n unregister(id);\n }\n const next = items.filter((f) => f.id !== id);\n onChange?.(next);\n setSelected((prev) => {\n const n = new Set(prev);\n n.delete(id);\n return n;\n });\n },\n [isDraft, items, onChange, unregister],\n );\n\n const handleBulkDelete = useCallback(() => {\n if (selected.size === 0) return;\n const ids = Array.from(selected);\n ids.forEach((id) => {\n if (isDraft(id)) {\n void discardFile(id).catch(() => {});\n unregister(id);\n }\n });\n onChange?.(items.filter((f) => !selected.has(f.id)));\n setSelected(new Set());\n }, [isDraft, items, selected, onChange, unregister]);\n\n const handleDragEnd = useCallback(\n (event: DragEndEvent) => {\n const { active, over } = event;\n if (!over || active.id === over.id) return;\n const oldIdx = items.findIndex((f) => f.id === active.id);\n const newIdx = items.findIndex((f) => f.id === over.id);\n if (oldIdx < 0 || newIdx < 0) return;\n onChange?.(arrayMove(items, oldIdx, newIdx));\n },\n [items, onChange],\n );\n\n const toggleSelect = useCallback((id: string) => {\n setSelected((prev) => {\n const n = new Set(prev);\n if (n.has(id)) n.delete(id);\n else n.add(id);\n return n;\n });\n }, []);\n\n const lightboxFile = lightbox != null ? items[lightbox] : null;\n const lightboxNext = useCallback(() => {\n setLightbox((cur) =>\n cur == null ? cur : Math.min(items.length - 1, cur + 1),\n );\n }, [items.length]);\n const lightboxPrev = useCallback(() => {\n setLightbox((cur) => (cur == null ? cur : Math.max(0, cur - 1)));\n }, []);\n const lightboxClose = useCallback(() => setLightbox(null), []);\n\n useEffect(() => {\n if (lightbox == null) return;\n const handler = (e: globalThis.KeyboardEvent) => {\n if (e.key === \"Escape\") lightboxClose();\n else if (e.key === \"ArrowRight\") lightboxNext();\n else if (e.key === \"ArrowLeft\") lightboxPrev();\n };\n window.addEventListener(\"keydown\", handler);\n return () => window.removeEventListener(\"keydown\", handler);\n }, [lightbox, lightboxClose, lightboxNext, lightboxPrev]);\n\n const onGridKeyDown = useCallback(\n (e: KeyboardEvent<HTMLDivElement>) => {\n if (disabled) return;\n if (e.key === \"Enter\" || e.key === \" \") {\n if (e.target === e.currentTarget) {\n e.preventDefault();\n onPick();\n }\n }\n },\n [disabled, onPick],\n );\n\n const isDisplay = mode === \"display\";\n\n const sortableIds = useMemo(() => items.map((f) => f.id), [items]);\n\n return (\n <div data-testid={`file-widget-${fieldName}`} className=\"flex flex-col gap-2\">\n {!isDisplay && selected.size > 0 && (\n <div\n className=\"flex items-center gap-2 rounded-md border border-border bg-surface-2 p-2 text-sm\"\n role=\"toolbar\"\n aria-label=\"Bulk actions\"\n >\n <span className=\"text-foreground\">{selected.size} selected</span>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-1 text-xs text-destructive hover:bg-surface-2\"\n onClick={handleBulkDelete}\n data-testid=\"bulk-delete\"\n >\n Delete\n </button>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-1 text-xs text-foreground hover:bg-surface-2\"\n onClick={() => setSelected(new Set())}\n >\n Clear\n </button>\n </div>\n )}\n\n <DndContext\n sensors={sensors}\n collisionDetection={closestCenter}\n onDragEnd={handleDragEnd}\n >\n <SortableContext items={sortableIds} strategy={rectSortingStrategy}>\n <div\n className=\"grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 rounded-md border border-dashed border-border bg-surface p-3 outline-none focus-visible:outline-2 focus-visible:outline-accent\"\n role=\"grid\"\n aria-label={ariaLabel}\n tabIndex={isDisplay || disabled ? -1 : 0}\n onDragOver={onDragOver}\n onDrop={onDrop}\n onKeyDown={onGridKeyDown}\n data-testid=\"grid-container\"\n >\n {items.map((f, idx) => (\n <SortableTile\n key={f.id}\n file={f}\n selected={selected.has(f.id)}\n disabled={disabled || isDisplay}\n reorderable={reorderable && !isDisplay}\n preferredThumbnail={preferredThumbnail}\n onSelect={() => toggleSelect(f.id)}\n onRemove={() => handleRemove(f.id)}\n onPreview={() => setLightbox(idx)}\n />\n ))}\n {tiles.map((t) => (\n <PendingTile\n key={t.localId}\n tile={t}\n onCancel={() => {\n t.abort();\n cancelledUploadsRef.current.add(t.localId);\n setTiles((prev) =>\n prev.filter((x) => x.localId !== t.localId),\n );\n }}\n onDismiss={() =>\n setTiles((prev) =>\n prev.filter((x) => x.localId !== t.localId),\n )\n }\n />\n ))}\n {items.length === 0 && tiles.length === 0 && (\n <div\n className=\"col-span-full flex h-24 cursor-pointer items-center justify-center text-sm text-muted-foreground\"\n onClick={onPick}\n data-testid=\"grid-empty\"\n >\n {field.display?.placeholder || \"Drop files here or click to browse\"}\n </div>\n )}\n </div>\n </SortableContext>\n </DndContext>\n\n {!isDisplay && (\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept}\n multiple\n onChange={onChangeFile}\n style={{ display: \"none\" }}\n data-testid={`file-input-${fieldName}`}\n />\n )}\n\n {lightboxFile && (\n <Lightbox\n file={lightboxFile}\n hasPrev={lightbox! > 0}\n hasNext={lightbox! < items.length - 1}\n onPrev={lightboxPrev}\n onNext={lightboxNext}\n onClose={lightboxClose}\n />\n )}\n </div>\n );\n}\n\nfunction SortableTile({\n file,\n selected,\n disabled,\n reorderable,\n preferredThumbnail,\n onSelect,\n onRemove,\n onPreview,\n}: {\n file: HydratedFile;\n selected: boolean;\n disabled?: boolean;\n reorderable: boolean;\n preferredThumbnail?: string;\n onSelect: () => void;\n onRemove: () => void;\n onPreview: () => void;\n}) {\n const {\n attributes,\n listeners,\n setNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: file.id, disabled: !reorderable });\n const style: CSSProperties = {\n transform: CSS.Transform.toString(transform),\n transition,\n opacity: isDragging ? 0.5 : 1,\n };\n const url = thumbUrl(file, preferredThumbnail);\n return (\n <div\n ref={setNodeRef}\n style={style}\n className={`group relative aspect-square overflow-hidden rounded-md border ${\n selected ? \"border-accent\" : \"border-border\"\n } bg-surface-2`}\n data-testid={`grid-tile-${file.id}`}\n data-selected={selected || undefined}\n >\n {/* Lazy thumb: native loading=\"lazy\" approximates IntersectionObserver\n and is what the rest of the SDK uses (see ImageWidget). */}\n {url ? (\n <img\n src={url}\n alt={file.filename}\n className=\"h-full w-full cursor-pointer object-cover\"\n loading=\"lazy\"\n onClick={onPreview}\n />\n ) : (\n <div\n className=\"flex h-full w-full cursor-pointer items-center justify-center bg-surface-3 text-xs text-muted-foreground\"\n onClick={onPreview}\n >\n {file.filename}\n </div>\n )}\n {!disabled && (\n <>\n <input\n type=\"checkbox\"\n className=\"absolute left-1 top-1 h-4 w-4 cursor-pointer\"\n checked={selected}\n onChange={onSelect}\n aria-label={`Select ${file.filename}`}\n data-testid={`select-${file.id}`}\n onClick={(e) => e.stopPropagation()}\n />\n {reorderable && (\n <button\n type=\"button\"\n className=\"absolute right-1 top-1 cursor-grab rounded-sm border border-border bg-card px-1 text-xs\"\n aria-label={`Drag to reorder ${file.filename}`}\n data-testid={`drag-${file.id}`}\n {...attributes}\n {...listeners}\n >\n ⋮⋮\n </button>\n )}\n <div className=\"absolute inset-x-0 bottom-0 flex items-center justify-end gap-1 bg-black/50 p-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100\">\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-0.5 text-xs\"\n onClick={onPreview}\n >\n View\n </button>\n <button\n type=\"button\"\n className=\"rounded-sm border border-border bg-card px-2 py-0.5 text-xs text-destructive\"\n onClick={onRemove}\n data-testid={`remove-${file.id}`}\n >\n Delete\n </button>\n </div>\n </>\n )}\n </div>\n );\n}\n\nfunction PendingTile({\n tile,\n onCancel,\n onDismiss,\n}: {\n tile: InFlightTile;\n onCancel: () => void;\n onDismiss: () => void;\n}) {\n return (\n <div\n className={`relative flex aspect-square flex-col items-center justify-center rounded-md border ${\n tile.error ? \"border-destructive\" : \"border-border\"\n } bg-surface-2 p-2 text-center text-xs`}\n data-testid={`pending-tile-${tile.localId}`}\n >\n <span className=\"truncate\" title={tile.filename}>\n {tile.filename}\n </span>\n {tile.progress && !tile.error && (\n <span\n className=\"mt-1 text-muted-foreground\"\n role=\"progressbar\"\n aria-valuenow={tile.progress.pct}\n aria-valuemin={0}\n aria-valuemax={100}\n >\n {tile.progress.pct}%\n </span>\n )}\n {tile.error && (\n <div role=\"alert\" className=\"mt-1 text-destructive\">\n {tile.error.message}\n </div>\n )}\n <button\n type=\"button\"\n className=\"mt-1 rounded-sm border border-border bg-card px-2 py-0.5\"\n onClick={tile.error ? onDismiss : onCancel}\n >\n {tile.error ? \"Dismiss\" : \"Cancel\"}\n </button>\n </div>\n );\n}\n\nfunction Lightbox({\n file,\n hasPrev,\n hasNext,\n onPrev,\n onNext,\n onClose,\n}: {\n file: HydratedFile;\n hasPrev: boolean;\n hasNext: boolean;\n onPrev: () => void;\n onNext: () => void;\n onClose: () => void;\n}) {\n const url = file.presigned_url ?? file.url ?? thumbUrl(file);\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/80\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={file.filename}\n data-testid=\"lightbox\"\n onClick={onClose}\n >\n <button\n type=\"button\"\n className=\"absolute right-4 top-4 rounded-sm border border-border bg-card px-3 py-1 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onClose();\n }}\n data-testid=\"lightbox-close\"\n >\n Close\n </button>\n {hasPrev && (\n <button\n type=\"button\"\n className=\"absolute left-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onPrev();\n }}\n data-testid=\"lightbox-prev\"\n >\n ‹\n </button>\n )}\n {hasNext && (\n <button\n type=\"button\"\n className=\"absolute right-4 bottom-4 rounded-sm border border-border bg-card px-3 py-2 text-foreground\"\n onClick={(e) => {\n e.stopPropagation();\n onNext();\n }}\n data-testid=\"lightbox-next\"\n >\n ›\n </button>\n )}\n {url ? (\n <img\n src={url}\n alt={file.filename}\n className=\"max-h-[90vh] max-w-[90vw] object-contain\"\n onClick={(e) => e.stopPropagation()}\n />\n ) : (\n <div className=\"rounded-md bg-card p-4 text-foreground\">\n {file.filename} ({formatBytes(file.size_bytes)})\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -12,12 +12,12 @@ function a(e) {
12
12
  }
13
13
  return { json: e };
14
14
  }
15
- function o({ value: o, onChange: s, readOnly: c }) {
16
- let l = t(null), u = t(null), d = t(s);
15
+ function o({ value: o, onChange: s, readOnly: c, id: l }) {
16
+ let u = t(null), d = t(null), f = t(s);
17
17
  return e(() => {
18
- d.current = s;
18
+ f.current = s;
19
19
  }, [s]), e(() => {
20
- if (!l.current) return;
20
+ if (!u.current) return;
21
21
  let e = {
22
22
  content: a(o),
23
23
  readOnly: c ?? !1,
@@ -26,18 +26,18 @@ function o({ value: o, onChange: s, readOnly: c }) {
26
26
  navigationBar: !1,
27
27
  statusBar: !1,
28
28
  onChange: (e, t, n) => {
29
- if (!(c || !d.current) && !n.contentErrors) {
30
- if ("json" in e) d.current(e.json);
29
+ if (!(c || !f.current) && !n.contentErrors) {
30
+ if ("json" in e) f.current(e.json);
31
31
  else if ("text" in e) try {
32
- d.current(JSON.parse(e.text));
32
+ f.current(JSON.parse(e.text));
33
33
  } catch {
34
- d.current(e.text);
34
+ f.current(e.text);
35
35
  }
36
36
  }
37
37
  }
38
38
  }, t = () => {
39
39
  let e = document.documentElement.dataset.theme === "dark";
40
- l.current?.classList.toggle("jse-theme-dark", e);
40
+ u.current?.classList.toggle("jse-theme-dark", e);
41
41
  };
42
42
  t();
43
43
  let n = new MutationObserver(t);
@@ -46,20 +46,22 @@ function o({ value: o, onChange: s, readOnly: c }) {
46
46
  attributeFilter: ["data-theme"]
47
47
  });
48
48
  let s = i({
49
- target: l.current,
49
+ target: u.current,
50
50
  props: e
51
51
  });
52
- return u.current = s, () => {
53
- n.disconnect(), s.destroy(), u.current = null;
52
+ return d.current = s, () => {
53
+ n.disconnect(), s.destroy(), d.current = null;
54
54
  };
55
55
  }, [c]), e(() => {
56
- !u.current || !c || u.current.updateProps({ content: a(o) });
56
+ !d.current || !c || d.current.updateProps({ content: a(o) });
57
57
  }, [o, c]), /* @__PURE__ */ n("div", {
58
- ref: l,
58
+ ref: u,
59
+ id: l,
60
+ role: "group",
59
61
  className: c ? "json-editor-readonly" : "json-editor-editable"
60
62
  });
61
63
  }
62
64
  //#endregion
63
65
  export { o as JsonEditor };
64
66
 
65
- //# sourceMappingURL=JsonEditor-Tew1oNqZ.js.map
67
+ //# sourceMappingURL=JsonEditor-CexL5HXN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsonEditor-CexL5HXN.js","names":[],"sources":["../src/components/fields/JsonEditor.tsx"],"sourcesContent":["import { useRef, useEffect } from \"react\";\nimport {\n createJSONEditor,\n Mode,\n type Content,\n type OnChangeStatus,\n type JSONEditorPropsOptional,\n} from \"vanilla-jsoneditor\";\n// Dark theme CSS vars for vanilla-jsoneditor (sets --jse-* custom properties)\nimport \"vanilla-jsoneditor/themes/jse-theme-dark.css\";\n\ninterface JsonEditorProps {\n value: unknown;\n onChange?: (value: unknown) => void;\n readOnly?: boolean;\n // Future: JSON Schema for in-editor validation (Ajv). Accept now, wire later.\n schema?: Record<string, unknown>;\n // id (Finding 2): set on the editor container so the form's <label>\n // association resolves to a real element. The container also carries\n // role=\"group\" so assistive tech treats the editor as one labelled region.\n id?: string;\n}\n\nfunction toContent(v: unknown): Content {\n if (v == null) return { text: \"\" };\n if (typeof v === \"string\") {\n try {\n return { json: JSON.parse(v) };\n } catch {\n return { text: v };\n }\n }\n return { json: v };\n}\n\nexport function JsonEditor({ value, onChange, readOnly, id }: JsonEditorProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const editorRef = useRef<ReturnType<typeof createJSONEditor> | null>(null);\n const onChangeRef = useRef(onChange);\n useEffect(() => { onChangeRef.current = onChange; }, [onChange]);\n\n // Create / recreate editor when readOnly changes\n useEffect(() => {\n if (!containerRef.current) return;\n\n const props: JSONEditorPropsOptional = {\n content: toContent(value),\n readOnly: readOnly ?? false,\n mode: readOnly ? Mode.tree : Mode.text,\n mainMenuBar: !readOnly,\n navigationBar: false,\n statusBar: false,\n onChange: (content: Content, _prev: Content, status: OnChangeStatus) => {\n if (readOnly || !onChangeRef.current) return;\n if (status.contentErrors) return;\n if (\"json\" in content) {\n onChangeRef.current(content.json);\n } else if (\"text\" in content) {\n try {\n onChangeRef.current(JSON.parse(content.text));\n } catch {\n onChangeRef.current(content.text);\n }\n }\n },\n };\n\n const syncDarkClass = () => {\n const isDark = document.documentElement.dataset.theme === \"dark\";\n containerRef.current?.classList.toggle(\"jse-theme-dark\", isDark);\n };\n syncDarkClass();\n\n // Watch for theme attribute changes on <html>.\n const observer = new MutationObserver(syncDarkClass);\n observer.observe(document.documentElement, {\n attributes: true,\n attributeFilter: [\"data-theme\"],\n });\n\n const editor = createJSONEditor({\n target: containerRef.current,\n props,\n });\n editorRef.current = editor;\n\n return () => {\n observer.disconnect();\n editor.destroy();\n editorRef.current = null;\n };\n }, [readOnly]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Update content when value changes externally (read-only mode only)\n useEffect(() => {\n if (!editorRef.current || !readOnly) return;\n editorRef.current.updateProps({ content: toContent(value) });\n }, [value, readOnly]);\n\n return (\n <div\n ref={containerRef}\n id={id}\n role=\"group\"\n className={readOnly ? \"json-editor-readonly\" : \"json-editor-editable\"}\n />\n );\n}\n"],"mappings":";;;;;AAuBA,SAAS,EAAU,GAAqB;AACtC,KAAI,KAAK,KAAM,QAAO,EAAE,MAAM,IAAI;AAClC,KAAI,OAAO,KAAM,SACf,KAAI;AACF,SAAO,EAAE,MAAM,KAAK,MAAM,EAAE,EAAE;SACxB;AACN,SAAO,EAAE,MAAM,GAAG;;AAGtB,QAAO,EAAE,MAAM,GAAG;;AAGpB,SAAgB,EAAW,EAAE,UAAO,aAAU,aAAU,SAAuB;CAC7E,IAAM,IAAe,EAAuB,KAAK,EAC3C,IAAY,EAAmD,KAAK,EACpE,IAAc,EAAO,EAAS;AA6DpC,QA5DA,QAAgB;AAAE,IAAY,UAAU;IAAa,CAAC,EAAS,CAAC,EAGhE,QAAgB;AACd,MAAI,CAAC,EAAa,QAAS;EAE3B,IAAM,IAAiC;GACrC,SAAS,EAAU,EAAM;GACzB,UAAU,KAAY;GACtB,MAAM,IAAW,EAAK,OAAO,EAAK;GAClC,aAAa,CAAC;GACd,eAAe;GACf,WAAW;GACX,WAAW,GAAkB,GAAgB,MAA2B;AAClE,eAAY,CAAC,EAAY,YACzB,GAAO,eACX;SAAI,UAAU,EACZ,GAAY,QAAQ,EAAQ,KAAK;cACxB,UAAU,EACnB,KAAI;AACF,QAAY,QAAQ,KAAK,MAAM,EAAQ,KAAK,CAAC;aACvC;AACN,QAAY,QAAQ,EAAQ,KAAK;;;;GAIxC,EAEK,UAAsB;GAC1B,IAAM,IAAS,SAAS,gBAAgB,QAAQ,UAAU;AAC1D,KAAa,SAAS,UAAU,OAAO,kBAAkB,EAAO;;AAElE,KAAe;EAGf,IAAM,IAAW,IAAI,iBAAiB,EAAc;AACpD,IAAS,QAAQ,SAAS,iBAAiB;GACzC,YAAY;GACZ,iBAAiB,CAAC,aAAa;GAChC,CAAC;EAEF,IAAM,IAAS,EAAiB;GAC9B,QAAQ,EAAa;GACrB;GACD,CAAC;AAGF,SAFA,EAAU,UAAU,SAEP;AAGX,GAFA,EAAS,YAAY,EACrB,EAAO,SAAS,EAChB,EAAU,UAAU;;IAErB,CAAC,EAAS,CAAC,EAGd,QAAgB;AACV,GAAC,EAAU,WAAW,CAAC,KAC3B,EAAU,QAAQ,YAAY,EAAE,SAAS,EAAU,EAAM,EAAE,CAAC;IAC3D,CAAC,GAAO,EAAS,CAAC,EAGnB,kBAAC,OAAD;EACE,KAAK;EACD;EACJ,MAAK;EACL,WAAW,IAAW,yBAAyB;EAC/C,CAAA"}
@@ -11,6 +11,11 @@ export type SSEEvent = {
11
11
  } | {
12
12
  type: "checkpoint";
13
13
  eventID: string;
14
+ } | {
15
+ type: "realtime_degraded";
16
+ eventID?: string;
17
+ entity: string;
18
+ degradedSeq?: number;
14
19
  } | {
15
20
  type: "cache_reset";
16
21
  eventID?: string;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function BoolField({ value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function BoolField({ value, mode, onChange, fieldId, labelId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function EmailField({ value, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function EmailField({ field, fieldName, value, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function EnumField({ field, value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function EnumField({ field, value, mode, onChange, fieldId, labelId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function IntArrayField({ value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function IntArrayField({ field, fieldName, value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -4,6 +4,7 @@ interface JsonEditorProps {
4
4
  onChange?: (value: unknown) => void;
5
5
  readOnly?: boolean;
6
6
  schema?: Record<string, unknown>;
7
+ id?: string;
7
8
  }
8
- export declare function JsonEditor({ value, onChange, readOnly }: JsonEditorProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function JsonEditor({ value, onChange, readOnly, id }: JsonEditorProps): import("react/jsx-runtime").JSX.Element;
9
10
  export {};
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function JsonField({ value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function JsonField({ value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function MultilangField({ value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function MultilangField({ field, fieldName, value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function MultilangTextField({ value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function MultilangTextField({ field, fieldName, value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function NumberField({ field, fieldName, value, mode, onChange, record }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function NumberField({ field, fieldName, value, mode, onChange, record, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function PasswordField({ value, mode, onChange, record }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function PasswordField({ field, fieldName, value, mode, onChange, record, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function PhoneField({ value, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function PhoneField({ field, fieldName, value, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function RefField({ field, value, mode, onChange, refs }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function RefField({ field, value, mode, onChange, refs, fieldId, labelId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function SecretField({ value, mode, onChange, record }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function SecretField({ field, fieldName, value, mode, onChange, record, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function StringArrayField({ value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function StringArrayField({ field, fieldName, value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function StringField({ field, value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function StringField({ field, fieldName, value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function TextField({ field, value, mode, onChange, fill, compact }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function TextField({ field, fieldName, value, mode, onChange, fill, compact, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function TimestampField({ field, value, mode, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function TimestampField({ field, fieldName, value, mode, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -1,2 +1,2 @@
1
1
  import type { FieldRendererProps } from "./index";
2
- export declare function UrlField({ value, onChange }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function UrlField({ field, fieldName, value, onChange, fieldId }: FieldRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -8,6 +8,8 @@ export interface FieldRendererProps {
8
8
  value: unknown;
9
9
  mode: "display" | "edit";
10
10
  onChange?: (value: unknown) => void;
11
+ fieldId?: string;
12
+ labelId?: string;
11
13
  record?: Record<string, unknown>;
12
14
  refs?: RefsMap;
13
15
  fill?: boolean;
@@ -21,4 +21,6 @@ export declare function renderSchemaField(props: {
21
21
  screens?: Record<string, Screen>;
22
22
  navigate?: (href: string) => void;
23
23
  locale: string;
24
+ fieldId?: string;
25
+ labelId?: string;
24
26
  }): ReactNode;
@@ -11,5 +11,6 @@ export interface FileWidgetProps {
11
11
  alt?: string;
12
12
  uploadFieldCode?: string;
13
13
  uploadSlotCode?: string;
14
+ fieldId?: string;
14
15
  }
15
16
  export type FileWidget = React.ComponentType<FileWidgetProps>;
@@ -0,0 +1,7 @@
1
+ import type { Cell } from "@tanstack/react-table";
2
+ type RowData = Record<string, unknown>;
3
+ export declare function DataCell({ cell, isRowSelected, }: {
4
+ cell: Cell<RowData, unknown>;
5
+ isRowSelected: boolean;
6
+ }): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,27 @@
1
+ import { type ResolvedActionLayoutNode } from "../../../lib/actions";
2
+ import { type WidgetContext } from "../../../components/shared/ResponsiveActionBar";
3
+ import type { Action } from "../../../types/schema";
4
+ export interface ListBulkBarProps {
5
+ selectedIds: string[];
6
+ /** Actions whose visibility includes `list_floatbar` - the bulk pill slot.
7
+ * Reason: the parent owns slot filtering via `getScreenActions(...)`;
8
+ * passing `toolbarActions` (list_toolbar slice) here silently dropped
9
+ * __delete (which is list_floatbar/list_row/detail, NOT list_toolbar)
10
+ * and rendered an empty pill. */
11
+ floatbarActions: {
12
+ code: string;
13
+ action: Action;
14
+ }[];
15
+ /** Server-emitted action_layout from screen (highest precedence per plan §3.9). */
16
+ screenActionLayout?: ResolvedActionLayoutNode[];
17
+ /** Server-emitted action_layout from entity. Used when screen has none. */
18
+ entityActionLayout?: ResolvedActionLayoutNode[];
19
+ data: Record<string, unknown>[];
20
+ rowKey: (row: Record<string, unknown>) => string;
21
+ entityCode?: string;
22
+ onClear: () => void;
23
+ onActionSuccess: () => void;
24
+ /** Dispatch surface threaded into ResponsiveActionBar (plan §8 Task 10). */
25
+ widgetContext: WidgetContext;
26
+ }
27
+ export declare function ListBulkBar({ selectedIds, floatbarActions, screenActionLayout, entityActionLayout, data, rowKey, entityCode, onClear, onActionSuccess, widgetContext, }: ListBulkBarProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,23 @@
1
+ import { type ResolvedActionLayoutNode } from "../../../lib/actions";
2
+ import { type WidgetContext } from "../../../components/shared/ResponsiveActionBar";
3
+ import type { Action } from "../../../types/schema";
4
+ export interface ListHeaderProps {
5
+ title: string;
6
+ total: number | undefined;
7
+ loaded: number;
8
+ toolbarActions: {
9
+ code: string;
10
+ action: Action;
11
+ }[];
12
+ /** Server-emitted action_layout from screen (highest precedence per plan §3.9). */
13
+ screenActionLayout?: ResolvedActionLayoutNode[];
14
+ /** Server-emitted action_layout from entity. Used when screen has none. */
15
+ entityActionLayout?: ResolvedActionLayoutNode[];
16
+ selectedIds: string[];
17
+ data: Record<string, unknown>[];
18
+ rowKey: (row: Record<string, unknown>) => string;
19
+ onActionSuccess: () => void;
20
+ /** Dispatch surface threaded into ResponsiveActionBar (plan §8 Task 10). */
21
+ widgetContext: WidgetContext;
22
+ }
23
+ export declare function ListHeader({ title, total, loaded, toolbarActions, screenActionLayout, entityActionLayout, selectedIds, data, rowKey, onActionSuccess, widgetContext, }: ListHeaderProps): import("react/jsx-runtime").JSX.Element;