@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,300 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { isLongText } from './long-text.js';
3
+ import { useJsonSourceLiterals } from './source-literals.js';
4
+ import { useCallback, useEffect, useMemo, useState } from 'react';
5
+ import { useJsonViewsRegistries } from '../widget-registry.js';
6
+ import { AtomicValueEditor } from '../structured-data/atomic-value-editor.js';
7
+ import { isEditableAtomicValue } from '../structured-data/atomic-value.js';
8
+ import { TabularObjectArrayView, ValueCell, ValueCellContent } from '../tabular-data-view.js';
9
+ import {} from '@script-it/json-views-core';
10
+ import { JSON_VIEW_PATH_MISSING, resolveJsonViewRowPath, schemaForJsonViewPath } from '@script-it/json-views-core';
11
+ import { JsonViewSchemaValueCell, JsonViewSchemaValueDisplay, withColumnOptionColors } from './schema-value.js';
12
+ import { projectJsonViewCollection } from '@script-it/json-views-core';
13
+ import { GeneralJsonView } from './viewers/general/general-json-view.js';
14
+ import {} from './view-types.js';
15
+ import { matchesSearch, titleForRow, titleFieldForRow, rowAtSourcePath, sameJsonViewPath } from './view-model.js';
16
+ import { blankRow, removalOrder, rowFieldPath } from './row-mutations.js';
17
+ import { GeneralRecordView, RecordView } from './record-view.js';
18
+ import { jsonRowsCopyText, jsonRowsReferenceText } from './copy-json.js';
19
+ const FILTER_OPERATORS = [
20
+ { value: 'eq', label: 'is' },
21
+ { value: 'neq', label: 'is not' },
22
+ { value: 'in', label: 'is any of' },
23
+ { value: 'notIn', label: 'is none of' },
24
+ { value: 'gt', label: 'is greater than' },
25
+ { value: 'gte', label: 'is at least' },
26
+ { value: 'lt', label: 'is less than' },
27
+ { value: 'lte', label: 'is at most' },
28
+ { value: 'contains', label: 'contains' },
29
+ { value: 'notContains', label: 'does not contain' },
30
+ { value: 'isEmpty', label: 'is empty' },
31
+ { value: 'isNotEmpty', label: 'is not empty' },
32
+ ];
33
+ const DATE_FILTER_OPERATORS = [
34
+ { value: 'eq', label: 'is' },
35
+ { value: 'neq', label: 'is not' },
36
+ { value: 'lt', label: 'is before' },
37
+ { value: 'lte', label: 'is on or before' },
38
+ { value: 'gt', label: 'is after' },
39
+ { value: 'gte', label: 'is on or after' },
40
+ { value: 'isEmpty', label: 'is empty' },
41
+ { value: 'isNotEmpty', label: 'is not empty' },
42
+ ];
43
+ export function ProjectedTableView({ compiled, editing, filePath, copyFileName, fillHeight, onSaveView, onCurrentPathChange, query, rawView, renderExternalWidget, renderMarkdown, uiSize, view, }) {
44
+ const { types } = useJsonViewsRegistries();
45
+ const sourceLiteral = useJsonSourceLiterals();
46
+ const [editPath, setEditPath] = useState();
47
+ const [openRowPath, setOpenRowPath] = useState();
48
+ const [openCell, setOpenCell] = useState();
49
+ const unfilteredProjection = useMemo(() => projectJsonViewCollection(compiled.root, { ...view, filter: undefined }, compiled.schema, types, sourceLiteral), [compiled.root, compiled.schema, view, types, types.version, sourceLiteral]);
50
+ const completeProjection = useMemo(() => projectJsonViewCollection(compiled.root, view, compiled.schema, types, sourceLiteral), [compiled.root, compiled.schema, view, types, types.version, sourceLiteral]);
51
+ const projected = useMemo(() => {
52
+ const indices = completeProjection.rows.flatMap((row, index) => matchesSearch(row.value, query) ? [index] : []);
53
+ return {
54
+ columns: completeProjection.columns,
55
+ rows: indices.map((index) => completeProjection.rows[index]),
56
+ records: indices.map((index) => completeProjection.records[index]),
57
+ };
58
+ }, [completeProjection, query]);
59
+ const tableModel = useMemo(() => {
60
+ const visibleColumns = [...completeProjection.columns];
61
+ const fieldOccurrences = new Map();
62
+ const stateKeys = Object.fromEntries(visibleColumns.map((column) => {
63
+ const field = JSON.stringify(column.path.segments);
64
+ const occurrence = fieldOccurrences.get(field) ?? 0;
65
+ fieldOccurrences.set(field, occurrence + 1);
66
+ return [column.id, JSON.stringify([field, occurrence])];
67
+ }));
68
+ const descriptors = Object.fromEntries(visibleColumns.map((column) => {
69
+ const descriptor = unfilteredProjection.rows.flatMap((row) => {
70
+ const path = resolveJsonViewRowPath(compiled.root, row, column.path).sourcePath;
71
+ const schema = schemaForJsonViewPath(compiled.schema, path);
72
+ return schema ? [schema.descriptor] : [];
73
+ })[0] ?? compiled.schema.find((schema) => sameJsonViewPath(schema.path, column.path))?.descriptor;
74
+ return [column.id, descriptor];
75
+ }));
76
+ const columnFilterOperators = Object.fromEntries(visibleColumns.map((column) => {
77
+ const descriptor = descriptors[column.id];
78
+ const defaults = descriptor?.type === 'date' ? DATE_FILTER_OPERATORS : FILTER_OPERATORS;
79
+ const declared = descriptor ? types.filterOperators(descriptor) : undefined;
80
+ return [column.id, declared ? [
81
+ ...defaults.filter((operator) => declared.includes(operator.value)),
82
+ ...declared.filter((value) => !defaults.some((operator) => operator.value === value)).map((value) => ({ value, label: value })),
83
+ ] : defaults];
84
+ }));
85
+ return {
86
+ visibleColumns,
87
+ records: projected.records.map((record) => Object.fromEntries(visibleColumns.flatMap((column) => column.id in record ? [[column.id, record[column.id]]] : []))),
88
+ labels: Object.fromEntries(visibleColumns.map((column) => [column.id, column.label])),
89
+ stateKeys,
90
+ columnFilterOperators,
91
+ columnFilterValues: Object.fromEntries(visibleColumns.map((column) => [
92
+ column.id,
93
+ unfilteredProjection.records.flatMap((record) => column.id in record ? [record[column.id]] : []),
94
+ ])),
95
+ titleColumn: visibleColumns[0],
96
+ };
97
+ }, [compiled.root, compiled.schema, completeProjection, projected.records, types, unfilteredProjection]);
98
+ const declaredColumnActions = rawView && onSaveView ? {
99
+ activeFilters: view.filter?.rules.flatMap((rule) => {
100
+ const column = tableModel.visibleColumns.find((candidate) => sameJsonViewPath(candidate.path, rule.path));
101
+ return column ? [{
102
+ column: column.id,
103
+ operator: rule.operator,
104
+ ...(rule.value === undefined ? {} : { value: rule.value }),
105
+ }] : [];
106
+ }) ?? [],
107
+ activeSort: (() => {
108
+ for (const sort of view.sort) {
109
+ const column = tableModel.visibleColumns.find((candidate) => sameJsonViewPath(candidate.path, sort.path));
110
+ if (column)
111
+ return { column: column.id, direction: sort.direction };
112
+ }
113
+ return null;
114
+ })(),
115
+ rename: async (columnId, label) => {
116
+ const columns = tableModel.visibleColumns.map((column) => ({
117
+ label: column.id === columnId ? label : column.label,
118
+ path: column.path.source,
119
+ }));
120
+ await onSaveView({ ...rawView, columns });
121
+ },
122
+ hide: async (columnId) => {
123
+ const columns = tableModel.visibleColumns
124
+ .filter((column) => column.id !== columnId)
125
+ .map((column) => ({ label: column.label, path: column.path.source }));
126
+ if (columns.length === 0)
127
+ throw new Error('Keep at least one column visible');
128
+ await onSaveView({ ...rawView, columns });
129
+ },
130
+ move: async (columnId, targetId, position) => {
131
+ if (columnId === targetId)
132
+ return;
133
+ const columns = [...tableModel.visibleColumns];
134
+ const sourceIndex = columns.findIndex((column) => column.id === columnId);
135
+ const targetIndex = columns.findIndex((column) => column.id === targetId);
136
+ if (sourceIndex < 0 || targetIndex < 0)
137
+ return;
138
+ const [moved] = columns.splice(sourceIndex, 1);
139
+ const updatedTargetIndex = columns.findIndex((column) => column.id === targetId);
140
+ columns.splice(updatedTargetIndex + (position === 'after' ? 1 : 0), 0, moved);
141
+ await onSaveView({ ...rawView, columns: columns.map((column) => ({ label: column.label, path: column.path.source })) });
142
+ },
143
+ filter: async (columnId, filter) => {
144
+ const target = tableModel.visibleColumns.find((column) => column.id === columnId);
145
+ if (!target)
146
+ return;
147
+ const replacement = filter ? {
148
+ path: target.path.source,
149
+ operator: filter.operator,
150
+ ...((filter.operator === 'isEmpty' || filter.operator === 'isNotEmpty') ? {} : { value: filter.value }),
151
+ } : undefined;
152
+ let inserted = false;
153
+ const rules = (view.filter?.rules ?? []).flatMap((rule) => {
154
+ if (!sameJsonViewPath(rule.path, target.path))
155
+ return [{
156
+ path: rule.path.source,
157
+ operator: rule.operator,
158
+ ...((rule.operator === 'isEmpty' || rule.operator === 'isNotEmpty') ? {} : { value: rule.value }),
159
+ }];
160
+ if (!replacement || inserted)
161
+ return [];
162
+ inserted = true;
163
+ return [replacement];
164
+ });
165
+ if (replacement && !inserted)
166
+ rules.push(replacement);
167
+ const next = { ...rawView };
168
+ if (rules.length > 0)
169
+ next.filter = {
170
+ ...(view.filter?.match === 'any' ? { match: 'any' } : {}),
171
+ rules,
172
+ };
173
+ else
174
+ delete next.filter;
175
+ await onSaveView(next);
176
+ },
177
+ sort: async (columnId, direction) => {
178
+ const target = tableModel.visibleColumns.find((column) => column.id === columnId);
179
+ if (!target)
180
+ return;
181
+ const remaining = view.sort
182
+ .filter((sort) => !sameJsonViewPath(sort.path, target.path))
183
+ .map((sort) => ({ path: sort.path.source, direction: sort.direction }));
184
+ const next = { ...rawView };
185
+ const sort = direction ? [{ path: target.path.source, direction }, ...remaining] : remaining;
186
+ if (sort.length > 0)
187
+ next.sort = sort;
188
+ else
189
+ delete next.sort;
190
+ await onSaveView(next);
191
+ },
192
+ } : undefined;
193
+ const getCellSourcePath = useCallback((rowIndex, columnId) => {
194
+ const column = tableModel.visibleColumns.find((candidate) => candidate.id === columnId);
195
+ return column && resolveJsonViewRowPath(compiled.root, projected.rows[rowIndex], column.path).sourcePath;
196
+ }, [compiled.root, projected.rows, tableModel.visibleColumns]);
197
+ const openRow = rowAtSourcePath(compiled.root, openRowPath);
198
+ const reportedPath = openRow?.sourcePath ?? openCell?.path ?? view.sourcePath;
199
+ const reportedPathKey = JSON.stringify(reportedPath);
200
+ useEffect(() => {
201
+ if (openRow || openCell)
202
+ onCurrentPathChange?.(reportedPath);
203
+ }, [onCurrentPathChange, reportedPathKey, Boolean(openRow), Boolean(openCell)]);
204
+ const reportVisibleRows = useCallback((indices, filtered) => {
205
+ const narrowed = filtered || Boolean(view.filter?.rules.length) || query.trim().length > 0;
206
+ onCurrentPathChange?.(view.sourcePath, narrowed
207
+ ? indices.map((index) => projected.rows[index].sourcePath)
208
+ : undefined);
209
+ }, [onCurrentPathChange, projected.rows, query, view.filter, view.sourcePath]);
210
+ if (openRow)
211
+ return _jsx(RecordView, { editPath: editPath, compiled: compiled, editing: editing, filePath: filePath, copyFileName: copyFileName, fillHeight: fillHeight, onBack: () => { setOpenRowPath(undefined); setEditPath(undefined); }, onCurrentPathChange: onCurrentPathChange, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, row: openRow, uiSize: uiSize, view: view });
212
+ if (openCell) {
213
+ return (_jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: _jsx(GeneralJsonView, { breadcrumb: { ancestors: [view.name, openCell.rowLabel], backLabel: `Back to ${view.name} table`, onBack: () => setOpenCell(undefined) }, root: compiled.root, initialPath: openCell.path, defaultKey: openCell.fieldLabel, filePath: `${filePath ?? 'json'}#${view.id}:cell:${JSON.stringify(openCell.path)}`, 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 }) }) }));
214
+ }
215
+ const { columnFilterOperators, columnFilterValues, labels, records, stateKeys, titleColumn, visibleColumns } = tableModel;
216
+ const collection = view.value;
217
+ const addRow = editing && Array.isArray(collection) ? async () => {
218
+ const sourcePath = [...view.sourcePath, collection.length];
219
+ await editing.append(view.sourcePath, blankRow(compiled, view));
220
+ editing.registerDraftRow?.(sourcePath);
221
+ setOpenRowPath(sourcePath);
222
+ } : undefined;
223
+ const addColumn = editing?.addColumn && rawView && view.declarationIndex >= 0 && Array.isArray(collection) ? async ({ label, type }) => {
224
+ let base = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'property';
225
+ if (/^[0-9]/.test(base))
226
+ base = `property_${base}`;
227
+ const existing = new Set(collection.flatMap((row) => row && typeof row === 'object' && !Array.isArray(row) ? Object.keys(row) : []));
228
+ let property = base;
229
+ let suffix = 2;
230
+ while (existing.has(property))
231
+ property = `${base}_${suffix++}`;
232
+ const schemaPath = `${view.path.source}[*].${property}`;
233
+ const descriptor = { type, title: label };
234
+ if (type === 'select' || type === 'multi-select')
235
+ descriptor.options = [];
236
+ await editing.addColumn(view.sourcePath, property, schemaPath, descriptor, view.declarationIndex);
237
+ } : undefined;
238
+ const deleteColumn = editing?.deleteColumn && rawView && Array.isArray(collection) ? async (columnId) => {
239
+ const column = tableModel.visibleColumns.find((candidate) => candidate.id === columnId);
240
+ const relative = column ? rowFieldPath(view, column.path) : undefined;
241
+ if (!column || !relative || relative.length !== 1)
242
+ throw new Error('Only direct row properties can be deleted');
243
+ await editing.deleteColumn(view.sourcePath, relative[0], column.path.source);
244
+ } : undefined;
245
+ return (_jsx(TabularObjectArrayView, { arr: records, getCellSourcePath: getCellSourcePath, rowKeys: projected.rows.map((row) => JSON.stringify(row.sourcePath)), onVisibleRowsChange: reportVisibleRows, columns: visibleColumns.map(({ id }) => id), columnLabels: labels, filePath: filePath, columnStateKeys: stateKeys, viewStateKey: `jsonView:${view.id}`, presentationControls: view.declarationIndex < 0 || compiled.recognized === false, activeColumnFilters: declaredColumnActions?.activeFilters, activeColumnSort: declaredColumnActions?.activeSort, columnFilterOperators: columnFilterOperators, columnFilterValues: columnFilterValues, onColumnFilter: declaredColumnActions?.filter, onColumnRename: declaredColumnActions?.rename, onColumnHide: declaredColumnActions?.hide, onColumnMove: declaredColumnActions?.move, onColumnDelete: deleteColumn, onAddColumn: addColumn, onColumnSort: declaredColumnActions?.sort, fillHeight: fillHeight, uiSize: uiSize, recordNavigation: titleColumn ? {
246
+ titleColumn: titleColumn.id,
247
+ getLabel: (rowIndex) => {
248
+ const row = projected.rows[rowIndex];
249
+ return sourceLiteral(titleFieldForRow(compiled.root, view, row)?.path) ?? titleForRow(compiled.root, view, row);
250
+ },
251
+ onOpen: (rowIndex) => setOpenRowPath(projected.rows[rowIndex].sourcePath),
252
+ renderTitle: ({ value, rowIndex }) => {
253
+ const row = projected.rows[rowIndex];
254
+ const resolved = resolveJsonViewRowPath(compiled.root, row, titleColumn.path);
255
+ const descriptor = schemaForJsonViewPath(compiled.schema, resolved.sourcePath)?.descriptor;
256
+ const coloredDescriptor = descriptor && withColumnOptionColors(descriptor, columnFilterValues[titleColumn.id] ?? [value], visibleColumns.findIndex((candidate) => candidate.id === titleColumn.id));
257
+ if (resolved.value === JSON_VIEW_PATH_MISSING)
258
+ return _jsx("span", { className: "italic text-muted-foreground", children: "Untitled" });
259
+ return coloredDescriptor
260
+ ? _jsx(JsonViewSchemaValueDisplay, { sourcePath: resolved.sourcePath, compact: true, decorative: true, passive: true, descriptor: coloredDescriptor, value: value, onOpen: () => undefined, renderMarkdown: renderMarkdown })
261
+ : _jsx(ValueCellContent, { containerInteractive: false, sourcePath: resolved.sourcePath, value: value, onOpen: () => undefined });
262
+ },
263
+ } : undefined, onOpenCell: ({ rowIndex, column }) => {
264
+ const displayColumn = visibleColumns.find((candidate) => candidate.id === column);
265
+ const row = projected.rows[rowIndex];
266
+ if (!displayColumn || !row)
267
+ return;
268
+ const resolved = resolveJsonViewRowPath(compiled.root, row, displayColumn.path);
269
+ if (resolved.value !== JSON_VIEW_PATH_MISSING)
270
+ setOpenCell({
271
+ fieldLabel: displayColumn.label,
272
+ path: resolved.sourcePath,
273
+ rowLabel: titleForRow(compiled.root, view, row),
274
+ });
275
+ }, renderCell: ({ value, rowIndex, column, open }) => {
276
+ const displayColumn = visibleColumns.find((candidate) => candidate.id === column);
277
+ const row = projected.rows[rowIndex];
278
+ if (!displayColumn || !row)
279
+ return null;
280
+ const resolved = resolveJsonViewRowPath(compiled.root, row, displayColumn.path);
281
+ const schema = schemaForJsonViewPath(compiled.schema, resolved.sourcePath);
282
+ const descriptor = schema?.descriptor;
283
+ const coloredDescriptor = descriptor && withColumnOptionColors(descriptor, columnFilterValues[column] ?? [value], visibleColumns.findIndex((candidate) => candidate.id === column));
284
+ if (isLongText(value, descriptor))
285
+ return (_jsx("button", { type: "button", className: "block h-full w-full min-w-0 truncate px-1.5 py-1 text-left hover:bg-accent focus-visible:ring-1 focus-visible:ring-ring", "aria-label": `Open ${displayColumn.label} in record`, onClick: () => { setEditPath(resolved.sourcePath); setOpenRowPath(row.sourcePath); }, children: value || '—' }));
286
+ if (coloredDescriptor) {
287
+ return (_jsx(JsonViewSchemaValueCell, { sourcePath: resolved.sourcePath, descriptor: coloredDescriptor, 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] ?? displayColumn.label), label: descriptor.title ?? displayColumn.label, value: value, onOpen: open, 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 }));
288
+ }
289
+ if (editing && editing.canReplace?.(resolved.sourcePath) !== false && resolved.value !== JSON_VIEW_PATH_MISSING && isEditableAtomicValue(value)) {
290
+ return (_jsx(AtomicValueEditor, { label: displayColumn.label, value: value, saving: editing.saving, onCommit: (next) => editing.replace(resolved.sourcePath, next), children: _jsx(ValueCellContent, { sourcePath: resolved.sourcePath, value: value, onOpen: open }) }));
291
+ }
292
+ return _jsx(ValueCell, { sourcePath: resolved.sourcePath, value: value, onOpen: open });
293
+ }, onAddRow: addRow, getRowsCopyText: (rowIndices) => jsonRowsCopyText(rowIndices.flatMap((index) => {
294
+ const row = projected.rows[index];
295
+ return row ? [{ value: row.value, sourcePath: row.sourcePath }] : [];
296
+ }), sourceLiteral), getRowsReferenceText: (rowIndices) => jsonRowsReferenceText(copyFileName, rowIndices.flatMap((index) => {
297
+ const row = projected.rows[index];
298
+ return row ? [{ sourcePath: row.sourcePath }] : [];
299
+ })), onDeleteRows: editing ? (rowIndices) => editing.removeMany(removalOrder(rowIndices.map((index) => projected.rows[index]?.sourcePath).filter((path) => path !== undefined))) : undefined, rowAdditionDisabled: editing?.saving, rowDeletionDisabled: editing?.saving }));
300
+ }
@@ -0,0 +1,13 @@
1
+ import { type DragEvent, type RefObject } from 'react';
2
+ export declare function useKanbanCardDrag({ boardRef, enabled, scope, onMove }: {
3
+ boardRef: RefObject<HTMLDivElement | null>;
4
+ enabled: boolean;
5
+ scope: string;
6
+ onMove: (id: string, group: string, before?: string) => void;
7
+ }): {
8
+ start: (event: DragEvent<HTMLElement>, id: string) => void;
9
+ onDragEnterCapture(event: DragEvent<HTMLDivElement>): void;
10
+ onDragOverCapture(event: DragEvent<HTMLDivElement>): void;
11
+ onDropCapture(event: DragEvent<HTMLDivElement>): void;
12
+ onDragLeave(event: DragEvent<HTMLDivElement>): void;
13
+ };
@@ -0,0 +1,255 @@
1
+ import { useEffect, useRef } from 'react';
2
+ // Geometry is measured once, then projected from the current slot. Card widgets
3
+ // never rerender on hover; transform-only movement is batched to one frame.
4
+ export function useKanbanCardDrag({ boardRef, enabled, scope, onMove }) {
5
+ const sessionRef = useRef(undefined);
6
+ const moveRef = useRef(onMove);
7
+ moveRef.current = onMove;
8
+ function clear() {
9
+ const session = sessionRef.current;
10
+ sessionRef.current = undefined;
11
+ if (!session)
12
+ return;
13
+ if (session.frame !== undefined)
14
+ cancelAnimationFrame(session.frame);
15
+ session.cleanup();
16
+ session.restore.reverse().forEach(restore => restore());
17
+ session.card.element.removeAttribute('aria-grabbed');
18
+ }
19
+ useEffect(() => clear, [scope, enabled]);
20
+ function cardsIn(session, lane) {
21
+ return lane.cards.filter(card => card !== session.card);
22
+ }
23
+ function destination(session) {
24
+ const board = boardRef.current;
25
+ const bounds = board.getBoundingClientRect();
26
+ if (session.x < bounds.left || session.x > bounds.right || session.y < bounds.top || session.y > bounds.bottom)
27
+ return;
28
+ const x = session.x - bounds.left + board.scrollLeft;
29
+ const y = session.y - bounds.top + board.scrollTop;
30
+ // Include the narrow gutters, but not arbitrary space beyond the board.
31
+ const lane = session.lanes.find(lane => x >= lane.left - 6 && x <= lane.right + 6);
32
+ if (!lane)
33
+ return;
34
+ const cards = cardsIn(session, lane);
35
+ let top = lane.top + lane.padding;
36
+ for (const [index, card] of cards.entries()) {
37
+ if (session.slot?.lane === lane && session.slot.index === index)
38
+ top += session.card.height + lane.gap;
39
+ if (y < top + card.height / 2)
40
+ return { lane, index };
41
+ top += card.height + lane.gap;
42
+ }
43
+ return { lane, index: cards.length };
44
+ }
45
+ function saveStyle(session, element) {
46
+ const original = element.getAttribute('style');
47
+ session.restore.push(() => {
48
+ if (original === null)
49
+ element.removeAttribute('style');
50
+ else
51
+ element.setAttribute('style', original);
52
+ });
53
+ }
54
+ function prepare(session) {
55
+ if (session.prepared)
56
+ return;
57
+ session.prepared = true;
58
+ const board = boardRef.current;
59
+ saveStyle(session, board);
60
+ // Keep short/empty lanes reachable while their cards move to another lane.
61
+ board.style.minHeight = `${board.clientHeight}px`;
62
+ for (const lane of session.lanes) {
63
+ saveStyle(session, lane.list);
64
+ Object.assign(lane.list.style, { display: 'block', position: 'relative' });
65
+ let top = lane.padding;
66
+ for (const card of lane.cards) {
67
+ saveStyle(session, card.element);
68
+ Object.assign(card.element.style, { position: 'absolute', left: '0', top: '0', width: '100%', height: `${card.height}px`, transform: `translateY(${top}px)` });
69
+ top += card.height + lane.gap;
70
+ }
71
+ }
72
+ session.card.element.style.opacity = '0';
73
+ // Commit the starting geometry before enabling transitions.
74
+ void board.offsetHeight;
75
+ board.setAttribute('data-card-reordering', 'true');
76
+ }
77
+ function project(session, slot) {
78
+ if (session.projected && session.slot?.lane === slot?.lane && session.slot?.index === slot?.index)
79
+ return;
80
+ session.projected = true;
81
+ session.slot = slot;
82
+ // Outside the board, restore the original arrangement until the drag returns.
83
+ const landing = slot ?? session.original;
84
+ for (const lane of session.lanes) {
85
+ const cards = cardsIn(session, lane);
86
+ let top = lane.padding;
87
+ for (let index = 0; index <= cards.length; index++) {
88
+ if (landing.lane === lane && landing.index === index) {
89
+ lane.list.appendChild(session.preview);
90
+ session.preview.style.transform = `translateY(${top}px)`;
91
+ top += session.card.height + lane.gap;
92
+ }
93
+ const card = cards[index];
94
+ if (card) {
95
+ card.element.style.transform = `translateY(${top}px)`;
96
+ top += card.height + lane.gap;
97
+ }
98
+ }
99
+ lane.list.style.height = `${Math.max(lane.padding, top - lane.gap)}px`;
100
+ }
101
+ session.preview.hidden = !slot;
102
+ session.card.element.style.opacity = slot ? '0' : '';
103
+ }
104
+ function paint(time) {
105
+ const session = sessionRef.current;
106
+ const board = boardRef.current;
107
+ if (!session || !board)
108
+ return;
109
+ session.frame = undefined;
110
+ prepare(session);
111
+ const bounds = board.getBoundingClientRect();
112
+ const inside = session.x >= bounds.left && session.x <= bounds.right && session.y >= bounds.top && session.y <= bounds.bottom;
113
+ const speed = (point, start, end) => {
114
+ const edge = Math.min(56, (end - start) / 4);
115
+ return point < start + edge ? -Math.min(1, (start + edge - point) / edge)
116
+ : point > end - edge ? Math.min(1, (point - end + edge) / edge) : 0;
117
+ };
118
+ const elapsed = Math.min(32, session.time === undefined ? 16 : time - session.time);
119
+ session.time = time;
120
+ const left = board.scrollLeft;
121
+ const top = board.scrollTop;
122
+ if (inside) {
123
+ board.scrollLeft = Math.max(0, board.scrollLeft + speed(session.x, bounds.left, bounds.right) * elapsed * .65);
124
+ board.scrollTop = Math.max(0, board.scrollTop + speed(session.y, bounds.top, bounds.bottom) * elapsed * .65);
125
+ }
126
+ project(session, destination(session));
127
+ if (board.scrollLeft !== left || board.scrollTop !== top)
128
+ schedule();
129
+ }
130
+ function schedule() {
131
+ const session = sessionRef.current;
132
+ if (session && session.frame === undefined)
133
+ session.frame = requestAnimationFrame(paint);
134
+ }
135
+ function start(event, id) {
136
+ const board = boardRef.current;
137
+ if (!enabled || !board) {
138
+ event.preventDefault();
139
+ return;
140
+ }
141
+ clear();
142
+ const bounds = board.getBoundingClientRect();
143
+ const lanes = Array.from(board.querySelectorAll('[data-id="jsonView-kanban-column"]')).map(element => {
144
+ const rect = element.getBoundingClientRect();
145
+ const list = element.querySelector('[data-id="jsonView-kanban-cards"]');
146
+ const style = getComputedStyle(list);
147
+ return {
148
+ key: element.dataset.groupKey, list,
149
+ left: rect.left - bounds.left + board.scrollLeft, right: rect.right - bounds.left + board.scrollLeft,
150
+ top: list.getBoundingClientRect().top - bounds.top + board.scrollTop,
151
+ padding: parseFloat(style.paddingTop) || 0, gap: parseFloat(style.rowGap) || 10,
152
+ cards: Array.from(list.querySelectorAll('[data-id="jsonView-kanban-card"]')).map(element => ({ id: element.dataset.rowId, element, height: element.getBoundingClientRect().height })),
153
+ };
154
+ });
155
+ const lane = lanes.find(lane => lane.cards.some(card => card.id === id));
156
+ const card = lane?.cards.find(card => card.id === id);
157
+ if (!lane || !card) {
158
+ event.preventDefault();
159
+ return;
160
+ }
161
+ event.stopPropagation();
162
+ event.dataTransfer.effectAllowed = 'move';
163
+ event.dataTransfer.setData('application/x-jsonView-json-row', id);
164
+ const preview = card.element.cloneNode(true);
165
+ // This is a visual clone, not another interactive or accessible card.
166
+ for (const element of [preview, ...preview.querySelectorAll('*')]) {
167
+ element.removeAttribute('id');
168
+ element.removeAttribute('data-id');
169
+ element.removeAttribute('data-row-id');
170
+ }
171
+ preview.inert = true;
172
+ preview.draggable = false;
173
+ preview.setAttribute('aria-hidden', 'true');
174
+ preview.setAttribute('data-id', 'jsonView-kanban-card-preview');
175
+ Object.assign(preview.style, { position: 'absolute', top: '0', left: '0', width: '100%', height: `${card.height}px` });
176
+ const ghost = preview.cloneNode(true);
177
+ ghost.removeAttribute('data-id');
178
+ Object.assign(ghost.style, { position: 'fixed', left: '-10000px', width: `${card.element.getBoundingClientRect().width}px` });
179
+ board.appendChild(ghost);
180
+ const cardBounds = card.element.getBoundingClientRect();
181
+ event.dataTransfer.setDragImage?.(ghost, Math.max(0, event.clientX - cardBounds.left), Math.max(0, event.clientY - cardBounds.top));
182
+ card.element.setAttribute('aria-grabbed', 'true');
183
+ const doc = board.ownerDocument;
184
+ const escape = (event) => { if (event.key === 'Escape')
185
+ clear(); };
186
+ const resize = () => clear();
187
+ const original = { lane, index: lane.cards.indexOf(card) };
188
+ sessionRef.current = {
189
+ card, lanes, original, slot: original, preview, x: event.clientX, y: event.clientY, prepared: false, projected: false, restore: [],
190
+ cleanup: () => {
191
+ board.removeAttribute('data-card-reordering');
192
+ preview.remove();
193
+ ghost.remove();
194
+ doc.removeEventListener('keydown', escape, true);
195
+ doc.removeEventListener('dragend', clear);
196
+ doc.removeEventListener('drop', clear);
197
+ doc.defaultView?.removeEventListener('blur', clear);
198
+ doc.defaultView?.removeEventListener('resize', resize);
199
+ board.removeEventListener('scroll', schedule);
200
+ },
201
+ };
202
+ doc.addEventListener('keydown', escape, true);
203
+ doc.addEventListener('dragend', clear);
204
+ doc.addEventListener('drop', clear);
205
+ doc.defaultView?.addEventListener('blur', clear);
206
+ doc.defaultView?.addEventListener('resize', resize);
207
+ board.addEventListener('scroll', schedule, { passive: true });
208
+ schedule();
209
+ }
210
+ return {
211
+ start,
212
+ onDragEnterCapture(event) {
213
+ if (!sessionRef.current)
214
+ return;
215
+ event.preventDefault();
216
+ event.stopPropagation();
217
+ },
218
+ onDragOverCapture(event) {
219
+ const session = sessionRef.current;
220
+ if (!session)
221
+ return;
222
+ event.preventDefault();
223
+ event.stopPropagation();
224
+ event.dataTransfer.dropEffect = 'move';
225
+ session.x = event.clientX;
226
+ session.y = event.clientY;
227
+ schedule();
228
+ },
229
+ onDropCapture(event) {
230
+ const session = sessionRef.current;
231
+ if (!session)
232
+ return;
233
+ event.preventDefault();
234
+ event.stopPropagation();
235
+ session.x = event.clientX;
236
+ session.y = event.clientY;
237
+ const slot = destination(session);
238
+ const before = slot && cardsIn(session, slot.lane)[slot.index]?.id;
239
+ clear();
240
+ if (slot && (slot.lane !== session.original.lane || slot.index !== session.original.index))
241
+ moveRef.current(session.card.id, slot.lane.key, before);
242
+ },
243
+ onDragLeave(event) {
244
+ const session = sessionRef.current;
245
+ if (!session || (event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget)))
246
+ return;
247
+ const bounds = event.currentTarget.getBoundingClientRect();
248
+ if (event.clientX >= bounds.left && event.clientX <= bounds.right && event.clientY >= bounds.top && event.clientY <= bounds.bottom)
249
+ return;
250
+ session.x = -Infinity;
251
+ if (session.prepared)
252
+ project(session);
253
+ },
254
+ };
255
+ }
@@ -0,0 +1,14 @@
1
+ import { type DragEvent } from 'react';
2
+ export declare function useKanbanColumnDrag({ enabled, scope, onMove }: {
3
+ enabled: boolean;
4
+ scope: string;
5
+ onMove: (source: string, target: string, position: 'before' | 'after') => void;
6
+ }): {
7
+ boardRef: import("react").RefObject<HTMLDivElement | null>;
8
+ markerRef: import("react").RefObject<HTMLDivElement | null>;
9
+ start: (event: DragEvent<HTMLElement>, key: string) => void;
10
+ onDragEnterCapture(event: DragEvent<HTMLDivElement>): void;
11
+ onDragOverCapture(event: DragEvent<HTMLDivElement>): void;
12
+ onDropCapture(event: DragEvent<HTMLDivElement>): void;
13
+ onDragLeave(event: DragEvent<HTMLDivElement>): void;
14
+ };