@script-it/json-views-react 0.1.0-beta.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 (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +301 -0
  3. package/THIRD_PARTY_NOTICES.txt +4917 -0
  4. package/dist/browser-device.d.ts +21 -0
  5. package/dist/browser-device.js +22 -0
  6. package/dist/file-content-edit-state.d.ts +10 -0
  7. package/dist/file-content-edit-state.js +1 -0
  8. package/dist/file-content-view.d.ts +8 -0
  9. package/dist/file-content-view.js +7 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +8 -0
  12. package/dist/json/copy-json.d.ts +15 -0
  13. package/dist/json/copy-json.js +52 -0
  14. package/dist/json/html-preview.d.ts +5 -0
  15. package/dist/json/html-preview.js +12 -0
  16. package/dist/json/html-view/native-control.d.ts +44 -0
  17. package/dist/json/html-view/native-control.js +96 -0
  18. package/dist/json/html-view/settings.d.ts +2 -0
  19. package/dist/json/html-view/settings.js +58 -0
  20. package/dist/json/html-view/template.d.ts +12 -0
  21. package/dist/json/html-view/template.js +55 -0
  22. package/dist/json/html-view.d.ts +9 -0
  23. package/dist/json/html-view.js +163 -0
  24. package/dist/json/json-content.d.ts +61 -0
  25. package/dist/json/json-content.js +433 -0
  26. package/dist/json/json-source-editor.d.ts +36 -0
  27. package/dist/json/json-source-editor.js +203 -0
  28. package/dist/json/json-view.d.ts +3 -0
  29. package/dist/json/json-view.js +322 -0
  30. package/dist/json/kanban-view.d.ts +20 -0
  31. package/dist/json/kanban-view.js +328 -0
  32. package/dist/json/long-text.d.ts +3 -0
  33. package/dist/json/long-text.js +6 -0
  34. package/dist/json/record-view.d.ts +38 -0
  35. package/dist/json/record-view.js +85 -0
  36. package/dist/json/row-mutations.d.ts +7 -0
  37. package/dist/json/row-mutations.js +93 -0
  38. package/dist/json/schema-value.d.ts +57 -0
  39. package/dist/json/schema-value.js +294 -0
  40. package/dist/json/source-default.d.ts +1 -0
  41. package/dist/json/source-default.js +1 -0
  42. package/dist/json/source-edit-capabilities.d.ts +8 -0
  43. package/dist/json/source-edit-capabilities.js +22 -0
  44. package/dist/json/source-literals.d.ts +15 -0
  45. package/dist/json/source-literals.js +24 -0
  46. package/dist/json/source-recovery.d.ts +32 -0
  47. package/dist/json/source-recovery.js +38 -0
  48. package/dist/json/source-selection.d.ts +3 -0
  49. package/dist/json/source-selection.js +16 -0
  50. package/dist/json/table-view.d.ts +20 -0
  51. package/dist/json/table-view.js +300 -0
  52. package/dist/json/use-kanban-card-drag.d.ts +13 -0
  53. package/dist/json/use-kanban-card-drag.js +255 -0
  54. package/dist/json/use-kanban-column-drag.d.ts +14 -0
  55. package/dist/json/use-kanban-column-drag.js +179 -0
  56. package/dist/json/view-diagnostics.d.ts +4 -0
  57. package/dist/json/view-diagnostics.js +26 -0
  58. package/dist/json/view-model.d.ts +27 -0
  59. package/dist/json/view-model.js +180 -0
  60. package/dist/json/view-save-status.d.ts +12 -0
  61. package/dist/json/view-save-status.js +38 -0
  62. package/dist/json/view-settings.d.ts +36 -0
  63. package/dist/json/view-settings.js +661 -0
  64. package/dist/json/view-types.d.ts +47 -0
  65. package/dist/json/view-types.js +4 -0
  66. package/dist/json/viewers/general/general-json-view.d.ts +46 -0
  67. package/dist/json/viewers/general/general-json-view.js +163 -0
  68. package/dist/json/viewers/general/json-flatten.d.ts +33 -0
  69. package/dist/json/viewers/general/json-flatten.js +51 -0
  70. package/dist/json/viewers/general/json-node-view.d.ts +43 -0
  71. package/dist/json/viewers/general/json-node-view.js +215 -0
  72. package/dist/lib/clipboard.d.ts +1 -0
  73. package/dist/lib/clipboard.js +24 -0
  74. package/dist/lib/cn.d.ts +2 -0
  75. package/dist/lib/cn.js +5 -0
  76. package/dist/lib/file-data.d.ts +7 -0
  77. package/dist/lib/file-data.js +5 -0
  78. package/dist/lib/floating-portal-selectors.d.ts +4 -0
  79. package/dist/lib/floating-portal-selectors.js +4 -0
  80. package/dist/lib/time.d.ts +24 -0
  81. package/dist/lib/time.js +60 -0
  82. package/dist/lib/ui-size.d.ts +28 -0
  83. package/dist/lib/ui-size.js +39 -0
  84. package/dist/lib/use-anchored-position.d.ts +5 -0
  85. package/dist/lib/use-anchored-position.js +65 -0
  86. package/dist/lib/use-dismiss.d.ts +31 -0
  87. package/dist/lib/use-dismiss.js +65 -0
  88. package/dist/primitives/boolean-toggle.d.ts +10 -0
  89. package/dist/primitives/boolean-toggle.js +11 -0
  90. package/dist/primitives/button.d.ts +11 -0
  91. package/dist/primitives/button.js +34 -0
  92. package/dist/primitives/checkbox.d.ts +4 -0
  93. package/dist/primitives/checkbox.js +8 -0
  94. package/dist/primitives/choice-control.d.ts +29 -0
  95. package/dist/primitives/choice-control.js +131 -0
  96. package/dist/primitives/dialog.d.ts +29 -0
  97. package/dist/primitives/dialog.js +25 -0
  98. package/dist/primitives/dropdown-menu.d.ts +37 -0
  99. package/dist/primitives/dropdown-menu.js +39 -0
  100. package/dist/primitives/inline-feedback-action.d.ts +13 -0
  101. package/dist/primitives/inline-feedback-action.js +6 -0
  102. package/dist/primitives/menu-select.d.ts +12 -0
  103. package/dist/primitives/menu-select.js +7 -0
  104. package/dist/primitives/multi-select.d.ts +18 -0
  105. package/dist/primitives/multi-select.js +7 -0
  106. package/dist/primitives/number-input.d.ts +10 -0
  107. package/dist/primitives/number-input.js +8 -0
  108. package/dist/primitives/option-pill.d.ts +13 -0
  109. package/dist/primitives/option-pill.js +50 -0
  110. package/dist/primitives/tabs.d.ts +7 -0
  111. package/dist/primitives/tabs.js +12 -0
  112. package/dist/primitives/textarea.d.ts +4 -0
  113. package/dist/primitives/textarea.js +8 -0
  114. package/dist/primitives/tooltip.d.ts +31 -0
  115. package/dist/primitives/tooltip.js +37 -0
  116. package/dist/standalone.css +113 -0
  117. package/dist/structured-data/atomic-value-editor.d.ts +33 -0
  118. package/dist/structured-data/atomic-value-editor.js +195 -0
  119. package/dist/structured-data/atomic-value.d.ts +3 -0
  120. package/dist/structured-data/atomic-value.js +6 -0
  121. package/dist/structured-data/column-controls.d.ts +21 -0
  122. package/dist/structured-data/column-controls.js +180 -0
  123. package/dist/structured-data/copy-json-action.d.ts +10 -0
  124. package/dist/structured-data/copy-json-action.js +87 -0
  125. package/dist/structured-data/delete-row-action.d.ts +10 -0
  126. package/dist/structured-data/delete-row-action.js +37 -0
  127. package/dist/structured-data/edit-base.d.ts +3 -0
  128. package/dist/structured-data/edit-base.js +8 -0
  129. package/dist/structured-data/filter-value.d.ts +2 -0
  130. package/dist/structured-data/filter-value.js +28 -0
  131. package/dist/structured-data/row-selection-icon.d.ts +9 -0
  132. package/dist/structured-data/row-selection-icon.js +6 -0
  133. package/dist/structured-data/structured-cell-fill-context.d.ts +6 -0
  134. package/dist/structured-data/structured-cell-fill-context.js +7 -0
  135. package/dist/structured-data/structured-data-content.d.ts +1 -0
  136. package/dist/structured-data/structured-data-content.js +1 -0
  137. package/dist/structured-data/table-types.d.ts +108 -0
  138. package/dist/structured-data/table-types.js +1 -0
  139. package/dist/structured-data/use-metadata-persistence.d.ts +15 -0
  140. package/dist/structured-data/use-metadata-persistence.js +18 -0
  141. package/dist/structured-data/use-optimistic-text-document.d.ts +22 -0
  142. package/dist/structured-data/use-optimistic-text-document.js +30 -0
  143. package/dist/structured-data/value-cell.d.ts +15 -0
  144. package/dist/structured-data/value-cell.js +81 -0
  145. package/dist/styles.css +2 -0
  146. package/dist/surface.d.ts +14 -0
  147. package/dist/surface.js +73 -0
  148. package/dist/tabular-data-view.d.ts +5 -0
  149. package/dist/tabular-data-view.js +493 -0
  150. package/dist/viewer-state.d.ts +72 -0
  151. package/dist/viewer-state.js +79 -0
  152. package/dist/widget-registry.d.ts +60 -0
  153. package/dist/widget-registry.js +126 -0
  154. package/dist/widgets/boolean-widget.d.ts +2 -0
  155. package/dist/widgets/boolean-widget.js +15 -0
  156. package/dist/widgets/date-picker-widget.d.ts +18 -0
  157. package/dist/widgets/date-picker-widget.js +265 -0
  158. package/dist/widgets/dropdown-widget.d.ts +10 -0
  159. package/dist/widgets/dropdown-widget.js +19 -0
  160. package/dist/widgets/multi-select-widget.d.ts +11 -0
  161. package/dist/widgets/multi-select-widget.js +46 -0
  162. package/dist/widgets/number-widget.d.ts +2 -0
  163. package/dist/widgets/number-widget.js +19 -0
  164. package/dist/widgets/read-only-field.d.ts +36 -0
  165. package/dist/widgets/read-only-field.js +39 -0
  166. package/dist/widgets/text-widget.d.ts +2 -0
  167. package/dist/widgets/text-widget.js +19 -0
  168. package/dist/widgets/textarea-widget.d.ts +2 -0
  169. package/dist/widgets/textarea-widget.js +34 -0
  170. package/dist/widgets/types.d.ts +53 -0
  171. package/dist/widgets/types.js +1 -0
  172. package/package.json +83 -0
@@ -0,0 +1,328 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useJsonSourceLiterals } from './source-literals.js';
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { File, FileText, GripVertical } from 'lucide-react';
5
+ import { useJsonViewsRegistries } from '../widget-registry.js';
6
+ import { cn } from '../lib/cn.js';
7
+ import {} from '@script-it/json-views-core';
8
+ import { JSON_VIEW_PATH_MISSING, resolveJsonViewRowPath, schemaForJsonViewPath } from '@script-it/json-views-core';
9
+ import { JsonViewSchemaValueCell } from './schema-value.js';
10
+ import { applyJsonViewViewRows, projectJsonViewCollection } from '@script-it/json-views-core';
11
+ import { GeneralJsonView } from './viewers/general/general-json-view.js';
12
+ import { OptionPill, optionColorsForValues } from '../primitives/option-pill.js';
13
+ import {} from './view-types.js';
14
+ import { matchesSearch, inferredDescriptor, rowHasLongText, titleForRow, titleFieldForRow, rowAtSourcePath, sameJsonViewPath } from './view-model.js';
15
+ import { GeneralRecordView, RecordView } from './record-view.js';
16
+ import { useKanbanColumnDrag } from './use-kanban-column-drag.js';
17
+ import { useKanbanCardDrag } from './use-kanban-card-drag.js';
18
+ const KANBAN_GROUP_COLORS = {
19
+ gray: 'border-border bg-muted/40',
20
+ blue: 'border-border bg-muted/40',
21
+ green: 'border-border bg-muted/40',
22
+ yellow: 'border-border bg-muted/40',
23
+ orange: 'border-border bg-muted/40',
24
+ red: 'border-border bg-muted/40',
25
+ purple: 'border-border bg-muted/40',
26
+ pink: 'border-border bg-muted/40',
27
+ };
28
+ function kanbanGroup(value) {
29
+ if (value === JSON_VIEW_PATH_MISSING || value == null || value === '') {
30
+ return { key: 'empty', label: 'No value', value: '' };
31
+ }
32
+ return {
33
+ key: `value:${typeof value}:${JSON.stringify(value)}`,
34
+ label: String(value),
35
+ value,
36
+ };
37
+ }
38
+ function groupValues(compiled, view, rows) {
39
+ if (!view.groupBy)
40
+ return [];
41
+ const samplePath = rows[0]
42
+ ? resolveJsonViewRowPath(compiled.root, rows[0], view.groupBy).sourcePath
43
+ : view.groupBy.segments.map((segment) => {
44
+ if (segment.kind === 'property')
45
+ return segment.key;
46
+ if (segment.kind === 'index')
47
+ return segment.index;
48
+ return 0;
49
+ });
50
+ const configured = schemaForJsonViewPath(compiled.schema, samplePath)?.descriptor.options;
51
+ const values = new Map();
52
+ view.groupOrder?.forEach((item) => {
53
+ const group = kanbanGroup(item);
54
+ values.set(group.key, group);
55
+ });
56
+ if (Array.isArray(configured)) {
57
+ configured.filter((item) => typeof item === 'string').forEach((item) => {
58
+ const group = kanbanGroup(item);
59
+ values.set(group.key, group);
60
+ });
61
+ }
62
+ for (const row of rows) {
63
+ const value = resolveJsonViewRowPath(compiled.root, row, view.groupBy).value;
64
+ const group = kanbanGroup(value);
65
+ values.set(group.key, group);
66
+ }
67
+ const positions = new Map(view.groupOrder?.map((value, index) => [kanbanGroup(value).key, index]));
68
+ return Array.from(values.values()).sort((left, right) => {
69
+ const leftPosition = positions.get(left.key);
70
+ const rightPosition = positions.get(right.key);
71
+ if (leftPosition !== undefined || rightPosition !== undefined) {
72
+ return (leftPosition ?? Infinity) - (rightPosition ?? Infinity);
73
+ }
74
+ if (left.key === 'empty' || right.key === 'empty')
75
+ return left.key === 'empty' ? 1 : -1;
76
+ if (typeof left.value === 'number' && typeof right.value === 'number')
77
+ return left.value - right.value;
78
+ return left.label.localeCompare(right.label, undefined, { numeric: true, sensitivity: 'base' });
79
+ });
80
+ }
81
+ function rowIdentity(row) {
82
+ return JSON.stringify(row.sourcePath);
83
+ }
84
+ function groupValueForRow(root, view, row) {
85
+ if (!view.groupBy)
86
+ return kanbanGroup(JSON_VIEW_PATH_MISSING);
87
+ const value = resolveJsonViewRowPath(root, row, view.groupBy).value;
88
+ return kanbanGroup(value);
89
+ }
90
+ function orderRows(root, view, rows, temporaryOrder) {
91
+ if (view.orderPath) {
92
+ return rows.map((row, index) => ({ row, index })).sort((left, right) => {
93
+ const leftValue = resolveJsonViewRowPath(root, left.row, view.orderPath).value;
94
+ const rightValue = resolveJsonViewRowPath(root, right.row, view.orderPath).value;
95
+ if (typeof leftValue === 'number' && typeof rightValue === 'number')
96
+ return leftValue - rightValue || left.index - right.index;
97
+ if (typeof leftValue === 'number')
98
+ return -1;
99
+ if (typeof rightValue === 'number')
100
+ return 1;
101
+ return left.index - right.index;
102
+ }).map(({ row }) => row);
103
+ }
104
+ if (!temporaryOrder)
105
+ return rows;
106
+ const positions = new Map(temporaryOrder.map((id, index) => [id, index]));
107
+ return rows.map((row, index) => ({ row, index })).sort((left, right) => ((positions.get(rowIdentity(left.row)) ?? Number.MAX_SAFE_INTEGER)
108
+ - (positions.get(rowIdentity(right.row)) ?? Number.MAX_SAFE_INTEGER)
109
+ || left.index - right.index)).map(({ row }) => row);
110
+ }
111
+ export function KanbanView({ compiled, copyFileName, editing, filePath, fillHeight, query, onCurrentPathChange, onSaveView, rawView, renderExternalWidget, renderMarkdown, uiSize, view, }) {
112
+ const { types } = useJsonViewsRegistries();
113
+ const sourceLiteral = useJsonSourceLiterals();
114
+ const rowTitle = (row) => sourceLiteral(titleFieldForRow(compiled.root, view, row)?.path) ?? titleForRow(compiled.root, view, row);
115
+ const [openRowPath, setOpenRowPath] = useState();
116
+ const [pendingGroups, setPendingGroups] = useState();
117
+ const savingGroups = useRef(false);
118
+ const [dragError, setDragError] = useState();
119
+ const boardRef = useRef(null);
120
+ const boardPosition = useRef({ left: 0, top: 0 });
121
+ useEffect(() => {
122
+ if (openRowPath)
123
+ return;
124
+ const board = boardRef.current;
125
+ if (board) {
126
+ board.scrollLeft = boardPosition.current.left;
127
+ board.scrollTop = boardPosition.current.top;
128
+ }
129
+ }, [openRowPath]);
130
+ const [moveNotice, setMoveNotice] = useState('');
131
+ const [temporaryOrder, setTemporaryOrder] = useState({});
132
+ const completeRows = useMemo(() => applyJsonViewViewRows(compiled.root, view, compiled.schema, types, sourceLiteral), [compiled.root, compiled.schema, view, types, types.version, sourceLiteral]);
133
+ const rows = useMemo(() => completeRows.filter((row) => matchesSearch(row.value, query)), [completeRows, query]);
134
+ const openRow = rowAtSourcePath(compiled.root, openRowPath);
135
+ const reportedPath = openRow?.sourcePath ?? view.sourcePath;
136
+ const reportedPathKey = JSON.stringify(reportedPath);
137
+ const savedGroups = useMemo(() => groupValues(compiled, view, completeRows), [compiled, view, completeRows]);
138
+ const groups = pendingGroups ?? savedGroups;
139
+ const hasGroupReordering = rawView !== undefined && onSaveView !== undefined && groups.length > 1;
140
+ const canReorderGroups = hasGroupReordering && !pendingGroups && !editing?.saving;
141
+ const moveGroup = async (sourceKey, targetKey, position) => {
142
+ if (!canReorderGroups || savingGroups.current || sourceKey === targetKey)
143
+ return;
144
+ const ordered = [...groups];
145
+ const sourceIndex = ordered.findIndex((group) => group.key === sourceKey);
146
+ const targetIndex = ordered.findIndex((group) => group.key === targetKey);
147
+ if (sourceIndex < 0 || targetIndex < 0)
148
+ return;
149
+ const [moved] = ordered.splice(sourceIndex, 1);
150
+ const updatedTargetIndex = ordered.findIndex((group) => group.key === targetKey);
151
+ ordered.splice(updatedTargetIndex + (position === 'after' ? 1 : 0), 0, moved);
152
+ if (ordered.every((group, index) => group.key === groups[index].key))
153
+ return;
154
+ setDragError(undefined);
155
+ savingGroups.current = true;
156
+ setPendingGroups(ordered);
157
+ try {
158
+ await onSaveView({ ...rawView, groupOrder: ordered.map((group) => group.value) });
159
+ }
160
+ catch (error) {
161
+ setDragError(error instanceof Error ? error.message : 'Could not reorder these columns');
162
+ }
163
+ finally {
164
+ savingGroups.current = false;
165
+ setPendingGroups(undefined);
166
+ }
167
+ };
168
+ const columnDrag = useKanbanColumnDrag({
169
+ enabled: canReorderGroups && !openRow,
170
+ scope: JSON.stringify([filePath, view.id, view.groupBy?.source, savedGroups.map(group => group.key)]),
171
+ onMove: (source, target, position) => { void moveGroup(source, target, position); },
172
+ });
173
+ const projected = useMemo(() => projectJsonViewCollection(compiled.root, view, compiled.schema, types, sourceLiteral), [compiled, view, types, types.version, sourceLiteral]);
174
+ const cardFields = useMemo(() => projected.columns.filter((column) => {
175
+ if (sameJsonViewPath(column.path, projected.columns[0]?.path) || sameJsonViewPath(column.path, view.groupBy))
176
+ return false;
177
+ return !projected.rows.some((row) => {
178
+ const resolved = resolveJsonViewRowPath(compiled.root, row, column.path);
179
+ if (resolved.value === JSON_VIEW_PATH_MISSING)
180
+ return false;
181
+ const descriptor = schemaForJsonViewPath(compiled.schema, resolved.sourcePath)?.descriptor;
182
+ if (Array.isArray(resolved.value))
183
+ return descriptor?.type !== 'multi-select';
184
+ return resolved.value !== null && typeof resolved.value === 'object';
185
+ });
186
+ }).slice(0, 3), [projected, compiled, view]);
187
+ const rowsByGroup = useMemo(() => {
188
+ const buckets = new Map();
189
+ for (const row of rows) {
190
+ const key = groupValueForRow(compiled.root, view, row).key;
191
+ const bucket = buckets.get(key);
192
+ if (bucket)
193
+ bucket.push(row);
194
+ else
195
+ buckets.set(key, [row]);
196
+ }
197
+ for (const [key, bucket] of buckets)
198
+ buckets.set(key, orderRows(compiled.root, view, bucket, temporaryOrder[key]));
199
+ return buckets;
200
+ }, [compiled.root, view, rows, temporaryOrder]);
201
+ const sourcePaths = !openRow && view.groupBy && (view.filter?.rules.length || query.trim())
202
+ ? groups.flatMap((group) => (rowsByGroup.get(group.key) ?? []).map((row) => row.sourcePath))
203
+ : undefined;
204
+ const sourcePathsKey = JSON.stringify(sourcePaths);
205
+ useEffect(() => {
206
+ onCurrentPathChange?.(reportedPath, sourcePaths);
207
+ }, [onCurrentPathChange, reportedPathKey, sourcePathsKey]);
208
+ const cardDrag = useKanbanCardDrag({
209
+ boardRef: columnDrag.boardRef,
210
+ enabled: !!editing && !editing.saving && !openRow,
211
+ scope: JSON.stringify([filePath, view.id, view.groupBy?.source, groups.map(group => group.key), rows.map(row => [rowIdentity(row), row.value]), temporaryOrder]),
212
+ onMove: (id, key, before) => {
213
+ const row = completeRows.find(row => rowIdentity(row) === id);
214
+ const group = groups.find(group => group.key === key);
215
+ if (row && group)
216
+ void place(row, group, completeRows.find(row => rowIdentity(row) === before));
217
+ },
218
+ });
219
+ if (openRow)
220
+ return _jsx(RecordView, { compiled: compiled, copyFileName: copyFileName, editing: editing, filePath: filePath, fillHeight: fillHeight, onBack: () => setOpenRowPath(undefined), onCurrentPathChange: onCurrentPathChange, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, row: openRow, uiSize: uiSize, view: view });
221
+ if (!view.groupBy)
222
+ return _jsx(GeneralJsonView, { root: compiled.root, initialPath: view.sourcePath, defaultKey: view.name, editing: editing, fillHeight: fillHeight, uiSize: uiSize, renderRecord: (target) => _jsx(GeneralRecordView, { ...target, compiled: compiled, copyFileName: copyFileName, editing: editing, filePath: filePath, fillHeight: fillHeight, name: view.name, onCurrentPathChange: onCurrentPathChange, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, uiSize: uiSize }) });
223
+ const place = async (row, group, beforeRow) => {
224
+ const resolved = resolveJsonViewRowPath(compiled.root, row, view.groupBy);
225
+ if (!editing || editing.saving || editing.canReplace?.(resolved.sourcePath) === false || resolved.value === JSON_VIEW_PATH_MISSING)
226
+ return;
227
+ const currentGroup = groupValueForRow(compiled.root, view, row);
228
+ const targetRows = orderRows(compiled.root, view, completeRows.filter((candidate) => groupValueForRow(compiled.root, view, candidate).key === group.key && rowIdentity(candidate) !== rowIdentity(row)), temporaryOrder[group.key]);
229
+ const targetIndex = beforeRow ? targetRows.findIndex((candidate) => rowIdentity(candidate) === rowIdentity(beforeRow)) : -1;
230
+ const insertAt = targetIndex < 0 ? targetRows.length : targetIndex;
231
+ targetRows.splice(insertAt, 0, row);
232
+ setDragError(undefined);
233
+ try {
234
+ setMoveNotice('');
235
+ if (!view.orderPath) {
236
+ // Keep the exact previewed slot even when a cross-column move only
237
+ // persists the grouping field (there is no numeric orderPath).
238
+ setTemporaryOrder((current) => ({ ...current, [group.key]: targetRows.map(rowIdentity) }));
239
+ if (currentGroup.key === group.key) {
240
+ setMoveNotice(`${rowTitle(row)} reordered in ${group.label}`);
241
+ return;
242
+ }
243
+ await editing.replace(resolved.sourcePath, group.value);
244
+ setMoveNotice(`${rowTitle(row)} moved to ${group.label}`);
245
+ return;
246
+ }
247
+ const affectedGroups = currentGroup.key === group.key ? [targetRows] : [
248
+ orderRows(compiled.root, view, completeRows.filter((candidate) => groupValueForRow(compiled.root, view, candidate).key === currentGroup.key && rowIdentity(candidate) !== rowIdentity(row))),
249
+ targetRows,
250
+ ];
251
+ const replacements = [];
252
+ if (currentGroup.key !== group.key)
253
+ replacements.push({ path: resolved.sourcePath, value: group.value });
254
+ for (const groupRows of affectedGroups) {
255
+ for (const [index, candidate] of groupRows.entries()) {
256
+ const order = resolveJsonViewRowPath(compiled.root, candidate, view.orderPath);
257
+ if (order.value === JSON_VIEW_PATH_MISSING || typeof order.value !== 'number') {
258
+ throw new Error(`orderPath must point to an existing number on every card`);
259
+ }
260
+ if (order.value !== index)
261
+ replacements.push({ path: order.sourcePath, value: index });
262
+ }
263
+ }
264
+ if (replacements.some(({ path }) => editing.canReplace?.(path) === false))
265
+ throw new Error('One of the affected fields is read-only');
266
+ if (replacements.length > 0)
267
+ await editing.replaceMany(replacements);
268
+ setMoveNotice(`${rowTitle(row)} moved in ${group.label}`);
269
+ }
270
+ catch (error) {
271
+ setDragError(error instanceof Error ? error.message : 'Could not move this card');
272
+ }
273
+ };
274
+ return (_jsxs("div", { "data-id": "jsonView-kanban-layout", className: cn('min-w-0 max-w-full', fillHeight && 'flex min-h-0 flex-1 flex-col'), children: [_jsx("span", { role: "status", className: moveNotice ? 'mb-2 text-xs text-muted-foreground' : 'sr-only', children: moveNotice }), dragError && _jsx("p", { role: "alert", className: "mb-2 rounded-md border border-destructive-border bg-destructive-surface px-3 py-2 text-xs text-destructive-foreground", children: dragError }), _jsx("div", { ref: (node) => { boardRef.current = node; columnDrag.boardRef.current = node; }, "data-id": "jsonView-kanban", onScroll: () => {
275
+ const board = boardRef.current;
276
+ if (!board)
277
+ return;
278
+ boardPosition.current = { left: board.scrollLeft, top: board.scrollTop };
279
+ }, onDragEnterCapture: event => { columnDrag.onDragEnterCapture(event); cardDrag.onDragEnterCapture(event); }, onDragOverCapture: event => { columnDrag.onDragOverCapture(event); cardDrag.onDragOverCapture(event); }, onDropCapture: event => { columnDrag.onDropCapture(event); cardDrag.onDropCapture(event); }, onDragLeave: event => { columnDrag.onDragLeave(event); cardDrag.onDragLeave(event); }, className: cn('min-w-0 max-w-full overflow-x-auto overscroll-x-contain', fillHeight && 'min-h-0 flex-1 overflow-y-auto'), children: _jsxs("div", { className: "relative flex w-max min-w-full items-start gap-3 py-3 pr-3", children: [_jsxs("div", { ref: columnDrag.markerRef, "data-id": "jsonView-kanban-column-drop-indicator", hidden: true, "aria-hidden": true, className: "pointer-events-none absolute left-0 z-20 w-[3px] rounded-full bg-blue-500 ring-2 ring-background", children: [_jsx("span", { className: "absolute -left-[3px] -top-1 size-[9px] rounded-full bg-blue-500" }), _jsx("span", { className: "absolute -bottom-1 -left-[3px] size-[9px] rounded-full bg-blue-500" })] }), groups.map((group) => {
280
+ const groupDescriptor = schemaForJsonViewPath(compiled.schema, view.groupBy.segments.map((segment) => (segment.kind === 'property' ? segment.key : segment.kind === 'index' ? segment.index : 0)))?.descriptor;
281
+ const groupColors = optionColorsForValues([...(groupDescriptor?.options ?? []), ...groups.filter((entry) => entry.key !== 'empty').map((entry) => entry.label)], groupDescriptor?.optionColors);
282
+ const groupColor = group.key === 'empty' ? 'gray' : groupColors[group.label];
283
+ const grouped = rowsByGroup.get(group.key) ?? [];
284
+ return (_jsxs("section", { "data-id": "jsonView-kanban-column", "data-group-value": group.label, "data-group-key": group.key, "data-group-color": groupColor, className: cn('relative w-64 shrink-0 rounded-lg border p-2.5 transition-colors data-[column-dragging=true]:opacity-40', KANBAN_GROUP_COLORS[groupColor]), children: [_jsxs("header", { "data-id": "jsonView-kanban-column-header", draggable: canReorderGroups, className: cn('flex h-8 select-none items-center justify-between gap-2 px-1 text-xs font-medium', canReorderGroups && 'cursor-grab active:cursor-grabbing'), onDragStart: (event) => columnDrag.start(event, group.key), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-1", children: [hasGroupReordering && (_jsx("button", { type: "button", "data-id": "jsonView-kanban-column-move", draggable: canReorderGroups, disabled: !canReorderGroups, "aria-label": `Reorder ${group.label} column`, title: "Drag or use arrow keys to reorder this column", className: "shrink-0 cursor-grab rounded-sm text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-wait disabled:opacity-50", onKeyDown: (event) => {
285
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
286
+ return;
287
+ event.preventDefault();
288
+ const offset = event.key === 'ArrowLeft' ? -1 : 1;
289
+ const target = groups[groups.indexOf(group) + offset];
290
+ if (target)
291
+ void moveGroup(group.key, target.key, event.key === 'ArrowLeft' ? 'before' : 'after');
292
+ }, children: _jsx(GripVertical, { "aria-hidden": true, className: "h-3.5 w-3.5" }) })), _jsx(OptionPill, { label: group.label, color: groupColor, className: "min-w-0 max-w-[10rem]", labelClassName: "truncate" })] }), _jsx("span", { className: "font-mono text-muted-foreground", children: grouped.length })] }), _jsxs("div", { "data-id": "jsonView-kanban-cards", className: "grid gap-2.5 pt-1.5", children: [grouped.length === 0 && _jsx("p", { className: "px-2 py-5 text-center text-xs text-muted-foreground", children: query ? 'No matching cards' : 'No cards' }), grouped.map((row, rowIndex) => {
293
+ const groupPath = resolveJsonViewRowPath(compiled.root, row, view.groupBy);
294
+ const canMove = editing !== undefined && editing.canReplace?.(groupPath.sourcePath) !== false && groupPath.value !== JSON_VIEW_PATH_MISSING;
295
+ const hasLongText = rowHasLongText(compiled, row.sourcePath);
296
+ const PageIcon = hasLongText ? FileText : File;
297
+ const rowId = rowIdentity(row);
298
+ return (_jsxs("article", { "data-id": "jsonView-kanban-card", "data-row-id": rowId, draggable: canMove && !editing.saving, className: "min-w-0 overflow-hidden rounded-md border border-border bg-card p-3 shadow-sm", onDragStart: (event) => {
299
+ const target = event.target instanceof Element ? event.target : null;
300
+ if (!canMove || (target?.closest('a, button, input, select, textarea, summary') && !target.closest('[data-id="jsonView-kanban-move"]'))) {
301
+ event.preventDefault();
302
+ return;
303
+ }
304
+ cardDrag.start(event, rowId);
305
+ }, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-1.5", children: [_jsx("button", { type: "button", "data-id": "jsonView-kanban-move", draggable: canMove && !editing.saving, "aria-label": `Reorder ${rowTitle(row)}`, title: "Drag to move; use arrow keys when focused", disabled: !canMove || editing?.saving, className: "mt-0.5 shrink-0 cursor-grab rounded-sm text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-default", onKeyDown: (event) => {
306
+ if (!canMove || !['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key))
307
+ return;
308
+ event.preventDefault();
309
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
310
+ const target = groups[groups.indexOf(group) + (event.key === 'ArrowLeft' ? -1 : 1)];
311
+ if (target)
312
+ void place(row, target);
313
+ }
314
+ else if (event.key === 'ArrowUp' && rowIndex > 0)
315
+ void place(row, group, grouped[rowIndex - 1]);
316
+ else if (event.key === 'ArrowDown' && rowIndex < grouped.length - 1)
317
+ void place(row, group, grouped[rowIndex + 2]);
318
+ }, children: _jsx(GripVertical, { "aria-hidden": true, className: "h-3.5 w-3.5" }) }), _jsx("button", { type: "button", className: "min-w-0 flex-1 text-left text-xs font-medium leading-5 hover:underline", onClick: () => setOpenRowPath(row.sourcePath), children: _jsx("span", { className: "line-clamp-3", children: rowTitle(row) }) }), _jsx(PageIcon, { "data-id": "jsonView-row-page-icon", "data-page-content": hasLongText ? 'text' : 'empty', className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" })] }), _jsx("div", { className: "mt-2 grid gap-1.5 text-xs text-muted-foreground", children: cardFields.map((column) => {
319
+ const resolved = resolveJsonViewRowPath(compiled.root, row, column.path);
320
+ if (resolved.value === JSON_VIEW_PATH_MISSING)
321
+ return null;
322
+ const schema = schemaForJsonViewPath(compiled.schema, resolved.sourcePath);
323
+ const descriptor = schema?.descriptor ?? inferredDescriptor(resolved.value);
324
+ return (_jsx("div", { className: "min-w-0", children: _jsx(JsonViewSchemaValueCell, { sourcePath: resolved.sourcePath, compact: true, descriptor: descriptor, suppressRequiredValidation: editing?.isDraftRow?.(row.sourcePath) === true, onOptionColorChange: editing?.setOptionColor && schema ? (optionValue, color) => editing.setOptionColor(schema.declaration, optionValue, color) : undefined, inputKey: String(resolved.sourcePath[resolved.sourcePath.length - 1] ?? column.label), label: descriptor.title ?? column.label, value: resolved.value, onOpen: () => setOpenRowPath(row.sourcePath), onClear: editing && resolved.value !== JSON_VIEW_PATH_MISSING ? () => editing.clear(resolved.sourcePath) : undefined, onCommit: editing && editing.canReplace?.(resolved.sourcePath) !== false && resolved.value !== JSON_VIEW_PATH_MISSING ? (next) => editing.replace(resolved.sourcePath, next) : undefined, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, saving: editing?.saving }) }, column.id));
325
+ }) })] }, JSON.stringify(row.sourcePath)));
326
+ })] })] }, group.key));
327
+ })] }) })] }));
328
+ }
@@ -0,0 +1,3 @@
1
+ import type { JsonViewSchemaDescriptor } from '@script-it/json-views-core';
2
+ /** Presentation only: never changes the stored value or inferred data type. */
3
+ export declare function isLongText(value: unknown, descriptor?: JsonViewSchemaDescriptor): value is string;
@@ -0,0 +1,6 @@
1
+ /** Presentation only: never changes the stored value or inferred data type. */
2
+ export function isLongText(value, descriptor) {
3
+ return typeof value === 'string'
4
+ && (!descriptor || descriptor.type === 'text' || descriptor.type === 'body' || descriptor.type === 'markdown' || descriptor.type === 'html')
5
+ && (descriptor?.multiline === true || descriptor?.type === 'body' || descriptor?.type === 'markdown' || descriptor?.type === 'html' || value.length > 120 || /[\r\n]/.test(value));
6
+ }
@@ -0,0 +1,38 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { UiSize } from '../lib/ui-size.js';
3
+ import { type ValuePath } from '@script-it/json-views-core';
4
+ import { type CompiledJsonViewMetadata, type CompiledJsonViewView, type JsonViewViewRow } from '@script-it/json-views-core';
5
+ import { type JsonViewSchemaExternalWidgetRenderer } from './schema-value.js';
6
+ import { type GeneralJsonRecordTarget } from './viewers/general/general-json-view.js';
7
+ import { type JsonViewJsonEditing } from './view-types.js';
8
+ interface GeneralRecordViewProps extends GeneralJsonRecordTarget {
9
+ compiled: CompiledJsonViewMetadata;
10
+ editing?: JsonViewJsonEditing;
11
+ filePath?: string;
12
+ copyFileName: string;
13
+ fillHeight: boolean;
14
+ name: string;
15
+ onCurrentPathChange?: (path: ValuePath) => void;
16
+ renderExternalWidget?: JsonViewSchemaExternalWidgetRenderer;
17
+ renderMarkdown?: (content: string) => ReactNode;
18
+ uiSize?: UiSize;
19
+ }
20
+ export declare function GeneralRecordView({ path, titleKey, name, ...props }: GeneralRecordViewProps): import("react").JSX.Element | null;
21
+ export declare function RecordView({ compiled, editing, filePath, copyFileName, fillHeight, onBack, breadcrumbLabels, backLabel, editPath, onCurrentPathChange, renderExternalWidget, renderMarkdown, row, uiSize, view, }: {
22
+ compiled: CompiledJsonViewMetadata;
23
+ editing?: JsonViewJsonEditing;
24
+ filePath?: string;
25
+ copyFileName: string;
26
+ fillHeight: boolean;
27
+ editPath?: ValuePath;
28
+ onBack?: () => void;
29
+ breadcrumbLabels?: readonly string[];
30
+ backLabel?: string;
31
+ onCurrentPathChange?: (path: ValuePath) => void;
32
+ renderExternalWidget?: JsonViewSchemaExternalWidgetRenderer;
33
+ renderMarkdown?: (content: string) => ReactNode;
34
+ row: JsonViewViewRow;
35
+ uiSize?: UiSize;
36
+ view: CompiledJsonViewView;
37
+ }): import("react").JSX.Element;
38
+ export {};
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useJsonSourceLiterals } from './source-literals.js';
3
+ import { useEffect, useState } from 'react';
4
+ import { cn } from '../lib/cn.js';
5
+ import { AtomicValueEditor } from '../structured-data/atomic-value-editor.js';
6
+ import { isEditableAtomicValue } from '../structured-data/atomic-value.js';
7
+ import { ValueCell, ValueCellContent } from '../tabular-data-view.js';
8
+ import {} from '@script-it/json-views-core';
9
+ import { getValueAtPath, VALUE_PATH_MISSING } from '@script-it/json-views-core';
10
+ import { schemaForJsonViewPath } from '@script-it/json-views-core';
11
+ import { JsonViewSchemaValueCell } from './schema-value.js';
12
+ import { isLongText } from './long-text.js';
13
+ import { GeneralJsonView, NavigationBreadcrumb } from './viewers/general/general-json-view.js';
14
+ import {} from './view-types.js';
15
+ import { nestedRecordView, titleFieldForRow, titleForRow } from './view-model.js';
16
+ import { jsonPathReference, jsonRecordCopyText } from './copy-json.js';
17
+ import { CopyJsonAction } from '../structured-data/copy-json-action.js';
18
+ import { useViewerState } from '../viewer-state.js';
19
+ export function GeneralRecordView({ path, titleKey, name, ...props }) {
20
+ const target = nestedRecordView(props.compiled.root, path, titleKey, name);
21
+ if (!target)
22
+ return null;
23
+ return _jsx(RecordView, { ...props, row: target.row, view: target.view });
24
+ }
25
+ export function RecordView({ compiled, editing, filePath, copyFileName, fillHeight, onBack, breadcrumbLabels, backLabel, editPath, onCurrentPathChange, renderExternalWidget, renderMarkdown, row, uiSize, view, }) {
26
+ const sourceLiteral = useJsonSourceLiterals();
27
+ const { navigation } = useViewerState();
28
+ const suppressRequiredValidation = editing?.isDraftRow?.(row.sourcePath) === true;
29
+ const nestedStateKey = `${filePath ?? 'json'}#record:${JSON.stringify(row.sourcePath)}`;
30
+ const [nestedPath, setNestedPath] = useState(() => {
31
+ const restored = filePath ? navigation.get(nestedStateKey)?.currentPath : undefined;
32
+ return restored && restored.length > row.sourcePath.length
33
+ && row.sourcePath.every((segment, index) => segment === restored[index])
34
+ && getValueAtPath(compiled.root, restored) !== VALUE_PATH_MISSING ? restored : undefined;
35
+ });
36
+ useEffect(() => {
37
+ if (nestedPath && (getValueAtPath(compiled.root, nestedPath) === VALUE_PATH_MISSING
38
+ || !row.sourcePath.every((segment, index) => segment === nestedPath[index])))
39
+ setNestedPath(undefined);
40
+ }, [compiled.root, nestedPath, row.sourcePath]);
41
+ useEffect(() => {
42
+ if (!filePath)
43
+ return;
44
+ const currentPath = nestedPath ?? row.sourcePath;
45
+ if (JSON.stringify(navigation.get(nestedStateKey)?.currentPath) !== JSON.stringify(currentPath)) {
46
+ navigation.set(nestedStateKey, { stack: [], currentPath });
47
+ }
48
+ }, [filePath, navigation, nestedPath, nestedStateKey, row.sourcePath]);
49
+ useEffect(() => {
50
+ if (!nestedPath)
51
+ onCurrentPathChange?.(row.sourcePath);
52
+ }, [nestedPath, onCurrentPathChange, row.sourcePath]);
53
+ const titleField = titleFieldForRow(compiled.root, view, row);
54
+ const rowTitle = titleField ? sourceLiteral(titleField.path) ?? titleForRow(compiled.root, view, row) : '';
55
+ const titleSchema = titleField ? schemaForJsonViewPath(compiled.schema, titleField.path) : undefined;
56
+ const titleDescriptor = titleSchema?.descriptor;
57
+ const titleTopLevelKey = titleField
58
+ && titleField.path.length === row.sourcePath.length + 1
59
+ && titleField.path.slice(0, row.sourcePath.length).every((segment, index) => segment === row.sourcePath[index])
60
+ && typeof titleField.path[titleField.path.length - 1] === 'string'
61
+ ? titleField.path[titleField.path.length - 1]
62
+ : undefined;
63
+ if (nestedPath) {
64
+ return (_jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: _jsx(GeneralJsonView, { breadcrumb: { ancestors: row.sourcePath.map(String), backLabel: 'Back to record', onBack: () => setNestedPath(undefined) }, root: compiled.root, initialPath: nestedPath, defaultKey: String(nestedPath[nestedPath.length - 1] ?? view.name), filePath: `${filePath ?? 'json'}#${view.id}:nested:${JSON.stringify(nestedPath)}`, editing: editing, onCurrentPathChange: onCurrentPathChange, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, schemaForPath: (path) => schemaForJsonViewPath(compiled.schema, path)?.descriptor, uiSize: uiSize, fillHeight: fillHeight, renderRecord: (target) => (_jsx(GeneralRecordView, { ...target, compiled: compiled, copyFileName: copyFileName, editing: editing, filePath: filePath, fillHeight: fillHeight, name: view.name, onCurrentPathChange: onCurrentPathChange, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, uiSize: uiSize })) }) }));
65
+ }
66
+ const entries = Object.entries(row.value).filter(([key]) => key !== titleTopLevelKey && key !== '$jsonviews')
67
+ .sort(([left, leftValue], [right, rightValue]) => Number(isLongText(leftValue, schemaForJsonViewPath(compiled.schema, [...row.sourcePath, left])?.descriptor)) - Number(isLongText(rightValue, schemaForJsonViewPath(compiled.schema, [...row.sourcePath, right])?.descriptor)));
68
+ return (_jsxs("div", { "data-id": "jsonView-record-view", className: cn('flex min-h-0 flex-1 flex-col', fillHeight && 'overflow-y-auto'), children: [_jsx(NavigationBreadcrumb, { labels: breadcrumbLabels ?? row.sourcePath.map(String), onBack: onBack, backLabel: backLabel }), _jsxs("article", { className: "mx-auto w-full max-w-2xl px-3 pb-6 sm:px-6", children: [titleField && _jsx("div", { "data-id": "jsonView-record-title-label", title: String(titleField.path[titleField.path.length - 1]), className: "mb-1 text-xs text-muted-foreground", children: titleDescriptor?.title ?? titleField.label.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').replace(/^./, (letter) => letter.toUpperCase()) }), _jsxs("div", { className: "-mx-1.5 flex min-w-0 items-start justify-end gap-1.5", children: [titleField && _jsx("div", { "data-id": "jsonView-record-title", role: "heading", "aria-level": 2, className: "min-w-0 max-w-full flex-1 text-xl font-semibold tracking-tight", children: titleField && titleDescriptor ? (_jsx(JsonViewSchemaValueCell, { sourcePath: titleField.path, descriptor: titleDescriptor, suppressRequiredValidation: suppressRequiredValidation, onOptionColorChange: editing?.setOptionColor && titleSchema ? (optionValue, color) => editing.setOptionColor(titleSchema.declaration, optionValue, color) : undefined, inputKey: String(titleField.path[titleField.path.length - 1] ?? 'title'), label: titleDescriptor.title ?? titleField.label, value: titleField.value, onOpen: () => undefined, onClear: editing ? () => editing.clear(titleField.path) : undefined, onCommit: editing && editing.canReplace?.(titleField.path) !== false ? (next) => editing.replace(titleField.path, next) : undefined, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, saving: editing?.saving, variant: "record-title" })) : titleField && editing && editing.canReplace?.(titleField.path) !== false && isEditableAtomicValue(titleField.value) ? (_jsx(AtomicValueEditor, { className: "min-h-10 w-fit max-w-full px-1.5 py-1", editorClassName: "min-h-10 text-xl font-semibold tracking-tight", label: titleField.label, preserveLayout: true, value: titleField.value, saving: editing.saving, onCommit: (next) => editing.replace(titleField.path, next), children: _jsx("span", { className: "break-words", children: rowTitle }) })) : _jsx("span", { className: "block px-1.5 py-1", children: rowTitle }) }), _jsx(CopyJsonAction, { className: "mt-1.5", dataId: "jsonView-copy-record", getPathText: () => jsonPathReference(copyFileName, row.sourcePath), getRecordText: () => jsonRecordCopyText(row.value, row.sourcePath, sourceLiteral), label: "Copy path", pathLabel: "path", recordLabel: "record" })] }), _jsx("dl", { className: cn("grid grid-cols-[minmax(6rem,10rem)_minmax(0,1fr)] gap-x-4 text-xs", titleField && "mt-5"), children: entries.map(([key, value]) => {
69
+ const path = [...row.sourcePath, key];
70
+ const schema = schemaForJsonViewPath(compiled.schema, path);
71
+ const descriptor = schema?.descriptor;
72
+ const focusedNested = editPath && editPath.length > path.length && path.every((part, index) => editPath[index] === part)
73
+ ? nestedRecordView(compiled.root, path, undefined, key) : undefined;
74
+ if (focusedNested)
75
+ return _jsxs("div", { className: "col-span-2 min-w-0 py-3", children: [_jsx("dt", { className: "mb-2 text-muted-foreground", children: descriptor?.title ?? key }), _jsx("dd", { children: _jsx(RecordView, { compiled: compiled, row: focusedNested.row, view: focusedNested.view, editPath: editPath, editing: editing, filePath: filePath, copyFileName: copyFileName, fillHeight: false, renderMarkdown: renderMarkdown, renderExternalWidget: renderExternalWidget, uiSize: uiSize }) })] }, key);
76
+ if (typeof value === 'string' && (!descriptor || ['text', 'markdown', 'html'].includes(descriptor.type))) {
77
+ const wide = isLongText(value, descriptor);
78
+ return _jsx(RecordTextProperty, { wide: wide, value: value, descriptor: descriptor?.type === 'markdown' || descriptor?.type === 'html' ? descriptor : { ...descriptor, type: 'text', multiline: wide }, path: path, label: descriptor?.title ?? key, editing: editing, initiallyEditing: JSON.stringify(editPath) === JSON.stringify(path), renderMarkdown: renderMarkdown }, key);
79
+ }
80
+ return (_jsxs("div", { className: "contents", children: [_jsx("dt", { className: "min-w-0 break-words py-3 text-muted-foreground", children: descriptor?.title ?? key }), _jsx("dd", { className: "min-w-0 overflow-hidden py-1.5", children: descriptor ? (_jsx(JsonViewSchemaValueCell, { sourcePath: path, descriptor: descriptor, suppressRequiredValidation: suppressRequiredValidation, onOptionColorChange: editing?.setOptionColor && schema ? (optionValue, color) => editing.setOptionColor(schema.declaration, optionValue, color) : undefined, label: descriptor.title ?? key, value: value, inputKey: key, onOpen: () => setNestedPath(path), onClear: editing ? () => editing.clear(path) : undefined, onCommit: editing && editing.canReplace?.(path) !== false ? (next) => editing.replace(path, next) : undefined, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, saving: editing?.saving })) : isEditableAtomicValue(value) && editing && editing.canReplace?.(path) !== false ? (_jsx(AtomicValueEditor, { label: key, value: value, saving: editing.saving, onCommit: (next) => editing.replace(path, next), children: _jsx(ValueCellContent, { sourcePath: path, value: value, onOpen: () => undefined }) })) : (_jsx(ValueCell, { sourcePath: path, value: value, onOpen: () => setNestedPath(path) })) })] }, key));
81
+ }) })] })] }));
82
+ }
83
+ function RecordTextProperty({ value, descriptor, path, label, editing, initiallyEditing, renderMarkdown, wide }) {
84
+ return _jsxs("div", { className: wide ? 'col-span-2 min-w-0 py-3' : 'contents', children: [_jsx("dt", { className: cn('min-w-0 text-muted-foreground', wide ? 'mb-2 flex items-center justify-between gap-2' : 'py-3'), children: _jsx("span", { className: "break-words", children: label }) }), _jsx("dd", { className: cn('min-w-0', !wide && 'py-1.5'), children: _jsx(JsonViewSchemaValueCell, { renderMarkdown: renderMarkdown, descriptor: descriptor, sourcePath: path, value: value, label: label, initiallyEditing: initiallyEditing, onOpen: () => undefined, onCommit: editing && editing.canReplace?.(path) !== false ? next => editing.replace(path, next) : undefined, onClear: editing ? () => editing.clear(path) : undefined, saving: editing?.saving }) })] });
85
+ }
@@ -0,0 +1,7 @@
1
+ import { type ValuePath } from '@script-it/json-views-core';
2
+ import { type CompiledJsonViewMetadata, type CompiledJsonViewView, type JsonViewPath, type JsonViewSchemaDescriptor } from '@script-it/json-views-core';
3
+ export declare function blankValue(descriptor: JsonViewSchemaDescriptor | undefined, sample: unknown): unknown;
4
+ export declare function samePathSegment(left: JsonViewPath['segments'][number], right: JsonViewPath['segments'][number] | undefined): boolean;
5
+ export declare function rowFieldPath(view: CompiledJsonViewView, field: JsonViewPath): string[] | undefined;
6
+ export declare function blankRow(compiled: CompiledJsonViewMetadata, view: CompiledJsonViewView): Record<string, unknown>;
7
+ export declare function removalOrder(paths: ValuePath[]): ValuePath[];
@@ -0,0 +1,93 @@
1
+ import {} from '@script-it/json-views-core';
2
+ import { setJsonValueAtPath, schemaForJsonViewPath } from '@script-it/json-views-core';
3
+ export function blankValue(descriptor, sample) {
4
+ if (descriptor) {
5
+ if (descriptor.type === 'checkbox')
6
+ return false;
7
+ if (descriptor.type === 'number') {
8
+ if (typeof descriptor.minimum === 'number')
9
+ return descriptor.minimum;
10
+ if (typeof descriptor.maximum === 'number' && descriptor.maximum < 0)
11
+ return descriptor.maximum;
12
+ return 0;
13
+ }
14
+ if (descriptor.type === 'multi-select')
15
+ return [];
16
+ if (descriptor.type === 'select')
17
+ return descriptor.required ? descriptor.options?.[0] ?? '' : null;
18
+ if (descriptor.type === 'date')
19
+ return null;
20
+ if (['text', 'url', 'email', 'body', 'markdown', 'html'].includes(descriptor.type))
21
+ return '';
22
+ }
23
+ if (Array.isArray(sample))
24
+ return [];
25
+ if (sample !== null && typeof sample === 'object')
26
+ return Object.fromEntries(Object.entries(sample).map(([key, value]) => [key, blankValue(undefined, value)]));
27
+ if (typeof sample === 'boolean')
28
+ return false;
29
+ if (typeof sample === 'number')
30
+ return 0;
31
+ if (typeof sample === 'string')
32
+ return '';
33
+ return sample ?? null;
34
+ }
35
+ export function samePathSegment(left, right) {
36
+ if (!right || left.kind !== right.kind)
37
+ return false;
38
+ if (left.kind === 'property')
39
+ return left.key === right.key;
40
+ if (left.kind === 'index')
41
+ return left.index === right.index;
42
+ return true;
43
+ }
44
+ export function rowFieldPath(view, field) {
45
+ if (!view.path.segments.every((segment, index) => samePathSegment(segment, field.segments[index])))
46
+ return undefined;
47
+ if (field.segments[view.path.segments.length]?.kind !== 'wildcard')
48
+ return undefined;
49
+ const relative = field.segments.slice(view.path.segments.length + 1);
50
+ if (relative.length === 0 || relative.some((segment) => segment.kind !== 'property'))
51
+ return undefined;
52
+ return relative.map((segment) => segment.kind === 'property' ? segment.key : '');
53
+ }
54
+ export function blankRow(compiled, view) {
55
+ const collection = view.value;
56
+ if (!Array.isArray(collection))
57
+ throw new TypeError('Rows can only be added to JSON arrays');
58
+ const sampleIndex = collection.findIndex((value) => value !== null && typeof value === 'object' && !Array.isArray(value));
59
+ if (sampleIndex >= 0) {
60
+ const sample = collection[sampleIndex];
61
+ return Object.fromEntries(Object.entries(sample).map(([key, value]) => {
62
+ const path = [...view.sourcePath, sampleIndex, key];
63
+ return [key, blankValue(schemaForJsonViewPath(compiled.schema, path)?.descriptor, value)];
64
+ }));
65
+ }
66
+ const result = {};
67
+ const fields = [
68
+ ...(view.columns ?? []).map((column) => column.path),
69
+ ...compiled.schema.map((entry) => entry.path),
70
+ ...(view.groupBy ? [view.groupBy] : []),
71
+ ];
72
+ const seen = new Set();
73
+ fields.forEach((field) => {
74
+ const relative = rowFieldPath(view, field);
75
+ if (!relative || seen.has(JSON.stringify(relative)))
76
+ return;
77
+ seen.add(JSON.stringify(relative));
78
+ const sourcePath = [...view.sourcePath, 0, ...relative];
79
+ setJsonValueAtPath(result, relative, blankValue(schemaForJsonViewPath(compiled.schema, sourcePath)?.descriptor, undefined));
80
+ });
81
+ return result;
82
+ }
83
+ export function removalOrder(paths) {
84
+ return [...paths].sort((left, right) => {
85
+ if (left.length !== right.length)
86
+ return right.length - left.length;
87
+ const leftLast = left[left.length - 1];
88
+ const rightLast = right[right.length - 1];
89
+ if (typeof leftLast === 'number' && typeof rightLast === 'number')
90
+ return rightLast - leftLast;
91
+ return 0;
92
+ });
93
+ }