@abraca/nuxt 1.6.0 → 1.8.0

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 (43) hide show
  1. package/dist/module.d.mts +6 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +16 -2
  4. package/dist/runtime/assets/sources.css +1 -0
  5. package/dist/runtime/components/ADocumentTree.d.vue.ts +11 -1
  6. package/dist/runtime/components/ADocumentTree.vue +13 -6
  7. package/dist/runtime/components/ADocumentTree.vue.d.ts +11 -1
  8. package/dist/runtime/components/renderers/AChecklistRenderer.vue +22 -4
  9. package/dist/runtime/components/renderers/ADashboardRenderer.vue +4 -2
  10. package/dist/runtime/components/renderers/AGalleryRenderer.vue +97 -70
  11. package/dist/runtime/components/renderers/AGraphRenderer.vue +209 -58
  12. package/dist/runtime/components/renderers/AKanbanRenderer.vue +145 -34
  13. package/dist/runtime/components/renderers/AMediaRenderer.vue +27 -17
  14. package/dist/runtime/components/renderers/AOutlineRenderer.vue +38 -23
  15. package/dist/runtime/components/renderers/ASlidesRenderer.d.vue.ts +21 -0
  16. package/dist/runtime/components/renderers/ASlidesRenderer.vue +591 -0
  17. package/dist/runtime/components/renderers/ASlidesRenderer.vue.d.ts +21 -0
  18. package/dist/runtime/components/renderers/ASpatialRenderer.vue +23 -0
  19. package/dist/runtime/components/renderers/ATableRenderer.vue +20 -391
  20. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.d.vue.ts +40 -0
  21. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue +227 -0
  22. package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue.d.ts +40 -0
  23. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +16 -0
  24. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +66 -0
  25. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +16 -0
  26. package/dist/runtime/components/renderers/table/ATableFlatMode.d.vue.ts +2 -0
  27. package/dist/runtime/components/renderers/table/ATableFlatMode.vue +184 -21
  28. package/dist/runtime/components/renderers/table/ATableFlatMode.vue.d.ts +2 -0
  29. package/dist/runtime/components/renderers/table/ATableHierarchyMode.d.vue.ts +26 -0
  30. package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue +662 -0
  31. package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue.d.ts +26 -0
  32. package/dist/runtime/composables/useAwareness.js +14 -3
  33. package/dist/runtime/composables/useBackgroundSync.js +19 -1
  34. package/dist/runtime/composables/useFileIndex.js +38 -17
  35. package/dist/runtime/composables/useSearchIndex.js +41 -16
  36. package/dist/runtime/composables/useSlidesNavigation.d.ts +45 -0
  37. package/dist/runtime/composables/useSlidesNavigation.js +185 -0
  38. package/dist/runtime/composables/useYDoc.d.ts +1 -1
  39. package/dist/runtime/composables/useYDoc.js +47 -9
  40. package/dist/runtime/locale.d.ts +38 -0
  41. package/dist/runtime/locale.js +41 -3
  42. package/dist/runtime/utils/docTypes.js +17 -0
  43. package/package.json +3 -3
@@ -0,0 +1,26 @@
1
+ import type { useTableView } from '../../../composables/useTableView.js';
2
+ import type { AbracadabraLocale } from '../../../locale.js';
3
+ type TableLabels = AbracadabraLocale['renderers']['table'];
4
+ type __VLS_Props = {
5
+ tree: ReturnType<typeof import('../../../composables/useChildTree').useChildTree>;
6
+ tableView: ReturnType<typeof useTableView>;
7
+ states: Array<Record<string, any>>;
8
+ myClientId: number;
9
+ setLocalState: (state: Record<string, any>) => void;
10
+ editable?: boolean;
11
+ labels?: Partial<TableLabels>;
12
+ };
13
+ declare function addColumn(): void;
14
+ declare function addRow(): void;
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
16
+ addColumn: typeof addColumn;
17
+ addRow: typeof addRow;
18
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
+ openNode: (id: string, label: string) => any;
20
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
+ onOpenNode?: ((id: string, label: string) => any) | undefined;
22
+ }>, {
23
+ editable: boolean;
24
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
25
+ declare const _default: typeof __VLS_export;
26
+ export default _default;
@@ -0,0 +1,662 @@
1
+ <script setup>
2
+ import { ref, computed, onBeforeUnmount } from "vue";
3
+ import { useEventListener } from "@vueuse/core";
4
+ import { useTouchDrag } from "../../../composables/useTouchDrag";
5
+ import { DEFAULT_LOCALE } from "../../../locale";
6
+ import ATableColumnHeader from "./ATableColumnHeader.vue";
7
+ import ATableCell from "./cells/ATableCell.vue";
8
+ const props = defineProps({
9
+ tree: { type: null, required: true },
10
+ tableView: { type: null, required: true },
11
+ states: { type: Array, required: true },
12
+ myClientId: { type: Number, required: true },
13
+ setLocalState: { type: Function, required: true },
14
+ editable: { type: Boolean, required: false, default: true },
15
+ labels: { type: Object, required: false }
16
+ });
17
+ const emit = defineEmits(["openNode"]);
18
+ const labels = computed(() => ({
19
+ ...DEFAULT_LOCALE.renderers.table,
20
+ ...props.labels ?? {}
21
+ }));
22
+ const INTERNAL_META_KEYS = /* @__PURE__ */ new Set([
23
+ "_metaFields",
24
+ "_metaInitialized",
25
+ "coverUploadId",
26
+ "coverDocId",
27
+ "coverMimeType",
28
+ "geoType",
29
+ "geoLat",
30
+ "geoLng",
31
+ "icon",
32
+ "color",
33
+ "geoDescription"
34
+ ]);
35
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
36
+ function camelToTitle(s) {
37
+ return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
38
+ }
39
+ const columns = computed(
40
+ () => props.tree.childrenOf(null).sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
41
+ );
42
+ const orderedColumns = computed(() => {
43
+ const order = props.tableView.columnOrder.value;
44
+ if (!order.length) return columns.value;
45
+ const colMap = new Map(columns.value.map((c) => [c.id, c]));
46
+ const ordered = [];
47
+ for (const id of order) {
48
+ const col = colMap.get(id);
49
+ if (col) {
50
+ ordered.push(col);
51
+ colMap.delete(id);
52
+ }
53
+ }
54
+ for (const col of colMap.values()) ordered.push(col);
55
+ return ordered;
56
+ });
57
+ const colChildren = computed(() => {
58
+ const map = /* @__PURE__ */ new Map();
59
+ for (const col of columns.value) {
60
+ map.set(col.id, props.tree.childrenOf(col.id).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)));
61
+ }
62
+ return map;
63
+ });
64
+ const rowCount = computed(
65
+ () => Math.max(0, ...columns.value.map((c) => colChildren.value.get(c.id)?.length ?? 0))
66
+ );
67
+ function getCellEntry(colId, rowIdx) {
68
+ return colChildren.value.get(colId)?.[rowIdx];
69
+ }
70
+ function getRowRep(rowIdx) {
71
+ for (const col of columns.value) {
72
+ const entry = getCellEntry(col.id, rowIdx);
73
+ if (entry) return entry;
74
+ }
75
+ return void 0;
76
+ }
77
+ const allCellEntries = computed(() => {
78
+ const result = [];
79
+ for (const children of colChildren.value.values()) result.push(...children);
80
+ return result;
81
+ });
82
+ const metaColumns = computed(() => {
83
+ const colIds = new Set(columns.value.map((c) => c.id));
84
+ const keys = /* @__PURE__ */ new Set();
85
+ for (const col of columns.value) {
86
+ const children = colChildren.value.get(col.id) ?? [];
87
+ for (const cell of children) {
88
+ if (!cell.meta) continue;
89
+ for (const key of Object.keys(cell.meta)) {
90
+ if (INTERNAL_META_KEYS.has(key)) continue;
91
+ if (UUID_RE.test(key)) continue;
92
+ if (colIds.has(key)) continue;
93
+ keys.add(key);
94
+ }
95
+ }
96
+ }
97
+ return [...keys].sort().map((key) => ({
98
+ id: `__meta_${key}`,
99
+ label: camelToTitle(key),
100
+ metaKey: key,
101
+ metaType: props.tableView.inferColumnType(allCellEntries.value, key)
102
+ }));
103
+ });
104
+ const TYPE_MIN_WIDTHS = {
105
+ text: 180,
106
+ number: 100,
107
+ date: 150,
108
+ datetime: 170,
109
+ select: 130,
110
+ multiselect: 160,
111
+ tags: 160,
112
+ toggle: 80,
113
+ rating: 140,
114
+ url: 180,
115
+ color: 100,
116
+ colorPicker: 100,
117
+ slider: 150,
118
+ members: 140,
119
+ priority: 130,
120
+ status: 130
121
+ };
122
+ function getDocColumnWidth(colId) {
123
+ return props.tableView.columnWidths.value[colId] ?? 180;
124
+ }
125
+ function getMetaColumnWidth(mc) {
126
+ return props.tableView.columnWidths.value[mc.id] ?? TYPE_MIN_WIDTHS[mc.metaType] ?? 140;
127
+ }
128
+ function getCellValue(colId, rowIdx) {
129
+ const entry = getCellEntry(colId, rowIdx);
130
+ return entry?.label && entry.label !== "Untitled" ? entry.label : "";
131
+ }
132
+ function getMetaCellValue(rowIdx, metaKey) {
133
+ for (const col of columns.value) {
134
+ const entry = getCellEntry(col.id, rowIdx);
135
+ if (entry?.meta?.[metaKey] !== void 0) return entry.meta[metaKey];
136
+ }
137
+ return null;
138
+ }
139
+ function setCellValue(colId, rowIdx, value) {
140
+ if (!props.editable) return;
141
+ const str = typeof value === "string" ? value.trim() : String(value ?? "");
142
+ const entry = getCellEntry(colId, rowIdx);
143
+ if (entry) {
144
+ props.tree.renameEntry(entry.id, str || "Untitled");
145
+ } else {
146
+ props.tree.createChild(colId, str || "Untitled");
147
+ }
148
+ }
149
+ function setMetaCellValue(rowIdx, metaKey, value) {
150
+ if (!props.editable) return;
151
+ const rep = getRowRep(rowIdx);
152
+ if (rep) props.tree.updateMeta(rep.id, { [metaKey]: value });
153
+ }
154
+ function addColumn() {
155
+ if (!props.editable) return;
156
+ props.tree.createChild(null, labels.value.column || "New Column");
157
+ }
158
+ function deleteColumn(col) {
159
+ if (!props.editable) return;
160
+ props.tree.deleteEntry(col.id);
161
+ }
162
+ function deleteMetaColumn(metaKey) {
163
+ if (!props.editable) return;
164
+ for (const col of columns.value) {
165
+ const children = colChildren.value.get(col.id) ?? [];
166
+ for (const cell of children) {
167
+ if (cell.meta?.[metaKey] !== void 0) {
168
+ const newMeta = { ...cell.meta };
169
+ delete newMeta[metaKey];
170
+ props.tree.treeMap.set(cell.id, { ...props.tree.treeMap.get(cell.id), meta: newMeta });
171
+ }
172
+ }
173
+ }
174
+ }
175
+ function addRow() {
176
+ if (!props.editable) return;
177
+ if (!columns.value[0]) return;
178
+ props.tree.createChild(columns.value[0].id, "");
179
+ }
180
+ function deleteRow(rowIdx) {
181
+ if (!props.editable) return;
182
+ for (const col of columns.value) {
183
+ const entry = getCellEntry(col.id, rowIdx);
184
+ if (entry) props.tree.deleteEntry(entry.id);
185
+ }
186
+ }
187
+ const editingCell = ref(null);
188
+ function startEditDocCol(rowIdx, col) {
189
+ if (!props.editable) return;
190
+ editingCell.value = { rowIdx, colId: col.id };
191
+ props.setLocalState({ "table:editing": { rowIdx, fieldId: col.id } });
192
+ }
193
+ function startEditMetaCol(rowIdx, mc) {
194
+ if (!props.editable) return;
195
+ editingCell.value = { rowIdx, colId: mc.id, metaKey: mc.metaKey };
196
+ props.setLocalState({ "table:editing": { rowIdx, fieldId: mc.id } });
197
+ }
198
+ function stopEdit() {
199
+ editingCell.value = null;
200
+ props.setLocalState({ "table:editing": null });
201
+ }
202
+ function onDocCellUpdate(colId, rowIdx, value) {
203
+ setCellValue(colId, rowIdx, value);
204
+ }
205
+ function onMetaCellUpdate(rowIdx, metaKey, value) {
206
+ setMetaCellValue(rowIdx, metaKey, value);
207
+ }
208
+ function renameColumn(colId, label) {
209
+ if (!props.editable) return;
210
+ props.tree.renameEntry(colId, label);
211
+ }
212
+ function onResizeStart(e, colId) {
213
+ if (!props.editable) return;
214
+ const startX = e.clientX;
215
+ const startWidth = props.tableView.columnWidths.value[colId] ?? 120;
216
+ props.setLocalState({ "table:resizing": colId });
217
+ function onMove(ev) {
218
+ const newWidth = Math.max(80, startWidth + (ev.clientX - startX));
219
+ props.tableView.setColumnWidth(colId, newWidth);
220
+ }
221
+ function onUp() {
222
+ document.removeEventListener("pointermove", onMove);
223
+ document.removeEventListener("pointerup", onUp);
224
+ document.body.style.userSelect = "";
225
+ document.body.style.cursor = "";
226
+ props.setLocalState({ "table:resizing": null });
227
+ }
228
+ document.addEventListener("pointermove", onMove);
229
+ document.addEventListener("pointerup", onUp);
230
+ document.body.style.userSelect = "none";
231
+ document.body.style.cursor = "col-resize";
232
+ }
233
+ function rowIdxFromDragId(dragId) {
234
+ return Number.parseInt(dragId.replace("row-", ""), 10);
235
+ }
236
+ const {
237
+ dragId: rowDragId,
238
+ dragOverId: rowDragOverId,
239
+ handlePointerDown: handleRowPointerDown
240
+ } = useTouchDrag({
241
+ idAttr: "data-drag-id",
242
+ onDragStart: (dragId) => {
243
+ if (!props.editable) return;
244
+ props.setLocalState({ "table:dragging": rowIdxFromDragId(dragId) });
245
+ },
246
+ onDragEnd: () => {
247
+ props.setLocalState({ "table:dragging": null });
248
+ },
249
+ onDrop: (srcDragId, targetDragId) => {
250
+ if (!props.editable) return;
251
+ const srcIdx = rowIdxFromDragId(srcDragId);
252
+ const targetIdx = rowIdxFromDragId(targetDragId);
253
+ if (srcIdx === targetIdx) return;
254
+ for (const col of columns.value) {
255
+ const children = colChildren.value.get(col.id) ?? [];
256
+ const srcEntry = children[srcIdx];
257
+ if (!srcEntry) continue;
258
+ const prev = children[targetIdx - (targetIdx > srcIdx ? 0 : 1)];
259
+ const next = children[targetIdx + (targetIdx > srcIdx ? 1 : 0)];
260
+ let newOrder;
261
+ if (!prev && !next) newOrder = Date.now();
262
+ else if (!prev) newOrder = next.order - 1e3;
263
+ else if (!next) newOrder = prev.order + 1e3;
264
+ else newOrder = (prev.order + next.order) / 2;
265
+ props.tree.moveEntry(srcEntry.id, col.id, newOrder);
266
+ }
267
+ props.setLocalState({ "table:dragging": null });
268
+ }
269
+ });
270
+ const {
271
+ dragId: colDragId,
272
+ dragOverId: colDragOverId,
273
+ handlePointerDown: handleColPointerDown
274
+ } = useTouchDrag({
275
+ idAttr: "data-col-drag-id",
276
+ onDrop: (srcId, targetId) => {
277
+ if (!props.editable) return;
278
+ if (srcId === targetId) return;
279
+ const cols = orderedColumns.value;
280
+ const srcIdx = cols.findIndex((c) => c.id === srcId);
281
+ const targetIdx = cols.findIndex((c) => c.id === targetId);
282
+ if (srcIdx === -1 || targetIdx === -1) return;
283
+ const reordered = [...cols];
284
+ const [moved] = reordered.splice(srcIdx, 1);
285
+ reordered.splice(targetIdx, 0, moved);
286
+ props.tableView.setColumnOrder(reordered.map((c) => c.id));
287
+ const prev = reordered[targetIdx - 1];
288
+ const next = reordered[targetIdx + 1];
289
+ let newOrder;
290
+ if (!prev && !next) newOrder = Date.now();
291
+ else if (!prev) newOrder = next.order - 1e3;
292
+ else if (!next) newOrder = prev.order + 1e3;
293
+ else newOrder = (prev.order + next.order) / 2;
294
+ props.tree.moveEntry(srcId, null, newOrder);
295
+ }
296
+ });
297
+ function handleSort(key, dir) {
298
+ if (dir === "clear") props.tableView.setSort(void 0);
299
+ else props.tableView.setSort(key, dir);
300
+ }
301
+ const ctxCell = ref(null);
302
+ const ctxPos = ref({ x: 0, y: 0 });
303
+ const showCtx = ref(false);
304
+ function onCellContextMenu(e, entry) {
305
+ if (!entry) return;
306
+ e.preventDefault();
307
+ ctxCell.value = entry;
308
+ ctxPos.value = { x: e.clientX, y: e.clientY };
309
+ showCtx.value = true;
310
+ }
311
+ useEventListener(window, "pointerdown", () => {
312
+ showCtx.value = false;
313
+ });
314
+ let longPressTimer = null;
315
+ function startLongPress(e, rowIdx) {
316
+ const entry = getRowRep(rowIdx);
317
+ if (!entry) return;
318
+ longPressTimer = setTimeout(() => {
319
+ const touch = e.touches[0];
320
+ if (touch) {
321
+ ctxCell.value = entry;
322
+ ctxPos.value = { x: touch.clientX, y: touch.clientY };
323
+ showCtx.value = true;
324
+ }
325
+ longPressTimer = null;
326
+ }, 500);
327
+ }
328
+ function cancelLongPress() {
329
+ if (longPressTimer) {
330
+ clearTimeout(longPressTimer);
331
+ longPressTimer = null;
332
+ }
333
+ }
334
+ const ctxItems = computed(() => {
335
+ if (!ctxCell.value) return [];
336
+ const cell = ctxCell.value;
337
+ return [
338
+ [
339
+ { label: labels.value.open, icon: "i-lucide-external-link", onSelect: () => emit("openNode", cell.id, cell.label) },
340
+ { label: labels.value.rename, icon: "i-lucide-pencil", onSelect: () => {
341
+ for (const col of columns.value) {
342
+ const children = colChildren.value.get(col.id) ?? [];
343
+ const rowIdx = children.findIndex((c) => c.id === cell.id);
344
+ if (rowIdx !== -1) {
345
+ startEditDocCol(rowIdx, col);
346
+ break;
347
+ }
348
+ }
349
+ } },
350
+ { label: labels.value.duplicate, icon: "i-lucide-copy", onSelect: () => props.tree.duplicateEntry(cell.id) }
351
+ ],
352
+ [
353
+ { label: labels.value.delete, icon: "i-lucide-trash-2", color: "error", onSelect: () => props.tree.deleteEntry(cell.id) }
354
+ ]
355
+ ];
356
+ });
357
+ function cellEditor(rowIdx, fieldId) {
358
+ return props.states.filter((s) => s.clientId !== props.myClientId).find(
359
+ (s) => s["table:editing"]?.rowIdx === rowIdx && s["table:editing"]?.fieldId === fieldId
360
+ );
361
+ }
362
+ function onRowPointerEnter(rowIdx) {
363
+ if (rowDragId.value) return;
364
+ props.setLocalState({ "table:hovering": rowIdx });
365
+ }
366
+ function onRowPointerLeave() {
367
+ if (rowDragId.value) return;
368
+ props.setLocalState({ "table:hovering": null });
369
+ }
370
+ function rowHoverers(rowIdx) {
371
+ const result = [];
372
+ for (const s of props.states) {
373
+ if (s.clientId === props.myClientId) continue;
374
+ if (s["table:hovering"] === rowIdx && s.user) {
375
+ result.push({ name: s.user.name ?? "", color: s.user.color ?? "#888" });
376
+ }
377
+ }
378
+ return result;
379
+ }
380
+ function rowEditors(rowIdx) {
381
+ const result = [];
382
+ for (const s of props.states) {
383
+ if (s.clientId === props.myClientId) continue;
384
+ if (s["table:editing"]?.rowIdx === rowIdx && s.user) {
385
+ result.push({ name: s.user.name ?? "", color: s.user.color ?? "#888" });
386
+ }
387
+ }
388
+ return result;
389
+ }
390
+ function remoteResizeColor(colId) {
391
+ for (const s of props.states) {
392
+ if (s.clientId === props.myClientId) continue;
393
+ if (s["table:resizing"] === colId && s.user) {
394
+ return s.user.color ?? null;
395
+ }
396
+ }
397
+ return null;
398
+ }
399
+ onBeforeUnmount(() => {
400
+ props.setLocalState({
401
+ "table:editing": null,
402
+ "table:hovering": null,
403
+ "table:dragging": null,
404
+ "table:resizing": null
405
+ });
406
+ });
407
+ defineExpose({ addColumn, addRow });
408
+ </script>
409
+
410
+ <template>
411
+ <div class="flex-1 overflow-auto">
412
+ <!-- Empty state -->
413
+ <div
414
+ v-if="columns.length === 0"
415
+ class="flex flex-col items-center justify-center h-full gap-3 text-center py-12"
416
+ >
417
+ <UIcon
418
+ name="i-lucide-table"
419
+ class="size-10 text-(--ui-text-dimmed)"
420
+ />
421
+ <p class="text-sm text-(--ui-text-muted)">
422
+ {{ labels.noRows }}
423
+ </p>
424
+ <UButton
425
+ v-if="editable"
426
+ icon="i-lucide-plus"
427
+ :label="labels.addColumn"
428
+ size="sm"
429
+ @click="addColumn"
430
+ />
431
+ </div>
432
+
433
+ <template v-else>
434
+ <table
435
+ class="text-sm border-collapse"
436
+ style="table-layout: fixed; min-width: 100%;"
437
+ >
438
+ <thead class="sticky top-0 bg-(--ui-bg) z-10">
439
+ <tr class="border-b border-(--ui-border)">
440
+ <th class="w-0 p-0" />
441
+ <th class="w-6 px-1">
442
+ <!-- grip -->
443
+ </th>
444
+ <th class="text-left px-2 py-2 text-xs font-medium text-(--ui-text-muted) w-20">
445
+ #
446
+ </th>
447
+ <!-- Document columns -->
448
+ <ATableColumnHeader
449
+ v-for="col in orderedColumns"
450
+ :key="col.id"
451
+ :column-id="col.id"
452
+ :label="col.label"
453
+ :width="getDocColumnWidth(col.id)"
454
+ :sort-key="tableView.sortKey.value"
455
+ :sort-dir="tableView.sortDir.value"
456
+ :can-rename="editable !== false"
457
+ :can-delete="editable !== false"
458
+ :can-drag="editable !== false"
459
+ :data-col-drag-id="col.id"
460
+ :class="[
461
+ colDragOverId === col.id ? 'border-l-2 border-(--ui-primary)' : '',
462
+ colDragId === col.id ? 'opacity-30' : ''
463
+ ]"
464
+ :style="remoteResizeColor(col.id) ? { backgroundColor: remoteResizeColor(col.id) + '20' } : {}"
465
+ @rename="renameColumn(col.id, $event)"
466
+ @delete="deleteColumn(col)"
467
+ @sort="handleSort(col.id, $event)"
468
+ @resize-start="onResizeStart($event, col.id)"
469
+ @drag-pointer-down="handleColPointerDown($event, col.id)"
470
+ />
471
+ <!-- Auto-detected meta columns -->
472
+ <ATableColumnHeader
473
+ v-for="mc in metaColumns"
474
+ :key="mc.id"
475
+ :column-id="mc.id"
476
+ :label="mc.label"
477
+ :width="getMetaColumnWidth(mc)"
478
+ :sort-key="tableView.sortKey.value"
479
+ :sort-dir="tableView.sortDir.value"
480
+ :can-rename="false"
481
+ :can-delete="editable !== false"
482
+ :can-drag="false"
483
+ @delete="deleteMetaColumn(mc.metaKey)"
484
+ @sort="handleSort(mc.id, $event)"
485
+ />
486
+ </tr>
487
+ </thead>
488
+ <TransitionGroup
489
+ name="trow"
490
+ tag="tbody"
491
+ >
492
+ <tr
493
+ v-for="idx in rowCount"
494
+ :key="getRowRep(idx - 1)?.id ?? `row-${idx}`"
495
+ :data-drag-id="`row-${idx - 1}`"
496
+ class="group hover:bg-(--ui-bg-elevated)"
497
+ :class="[
498
+ rowDragOverId === `row-${idx - 1}` ? 'border-t-2 border-(--ui-primary)' : 'border-b border-(--ui-border)',
499
+ rowDragId === `row-${idx - 1}` ? 'opacity-30' : ''
500
+ ]"
501
+ @contextmenu="editable !== false && onCellContextMenu($event, getRowRep(idx - 1))"
502
+ @touchstart.passive="editable !== false && startLongPress($event, idx - 1)"
503
+ @touchend="cancelLongPress"
504
+ @touchmove="cancelLongPress"
505
+ @pointerenter="onRowPointerEnter(idx - 1)"
506
+ @pointerleave="onRowPointerLeave"
507
+ >
508
+ <td class="w-0 p-0 relative overflow-visible">
509
+ <div
510
+ v-if="rowEditors(idx - 1).length"
511
+ class="absolute left-0 top-0 bottom-0 w-0.5 rounded-full z-10"
512
+ :style="{ background: rowEditors(idx - 1)[0].color }"
513
+ />
514
+ <span
515
+ v-if="rowHoverers(idx - 1).length"
516
+ class="absolute -top-2.5 left-6 text-[10px] px-1 rounded text-white leading-tight z-10 whitespace-nowrap pointer-events-none"
517
+ :style="{ backgroundColor: rowHoverers(idx - 1)[0].color }"
518
+ >
519
+ {{ rowHoverers(idx - 1).map((h) => h.name).join(", ") }}
520
+ </span>
521
+ </td>
522
+ <td
523
+ class="px-1 py-1.5 text-(--ui-text-dimmed) opacity-0 group-hover:opacity-60 cursor-grab w-6 touch-none"
524
+ @pointerdown="handleRowPointerDown($event, `row-${idx - 1}`)"
525
+ >
526
+ <div class="flex items-center justify-center">
527
+ <UIcon
528
+ name="i-lucide-grip-vertical"
529
+ class="size-4"
530
+ />
531
+ </div>
532
+ </td>
533
+ <td class="px-2 py-1.5 text-xs text-(--ui-text-dimmed) select-none w-20">
534
+ <div class="flex items-center gap-0.5">
535
+ <span class="w-4 text-center">{{ idx }}</span>
536
+ <UTooltip
537
+ v-if="getRowRep(idx - 1)"
538
+ :text="labels.openAsSlideover"
539
+ :content="{ side: 'bottom' }"
540
+ >
541
+ <UButton
542
+ icon="i-lucide-external-link"
543
+ size="xs"
544
+ variant="ghost"
545
+ color="neutral"
546
+ class="opacity-0 group-hover:opacity-100"
547
+ @click="emit('openNode', getRowRep(idx - 1).id, getRowRep(idx - 1).label)"
548
+ />
549
+ </UTooltip>
550
+ <UTooltip
551
+ v-if="editable !== false"
552
+ :text="labels.deleteRow"
553
+ :content="{ side: 'bottom' }"
554
+ >
555
+ <UButton
556
+ icon="i-lucide-trash-2"
557
+ size="xs"
558
+ variant="ghost"
559
+ color="error"
560
+ class="opacity-0 group-hover:opacity-100"
561
+ @click="deleteRow(idx - 1)"
562
+ />
563
+ </UTooltip>
564
+ </div>
565
+ </td>
566
+ <!-- Document column cells -->
567
+ <td
568
+ v-for="col in orderedColumns"
569
+ :key="col.id"
570
+ class="px-1 py-0.5"
571
+ :style="
572
+ cellEditor(idx - 1, col.id) ? { outline: `2px solid ${cellEditor(idx - 1, col.id).user?.color}` } : {}
573
+ "
574
+ >
575
+ <ATableCell
576
+ :value="getCellValue(col.id, idx - 1)"
577
+ meta-type="text"
578
+ :editing="editingCell?.rowIdx === idx - 1 && editingCell?.colId === col.id"
579
+ :column="{ id: col.id, type: 'label', label: col.label }"
580
+ @update:model-value="onDocCellUpdate(col.id, idx - 1, $event)"
581
+ @start-edit="startEditDocCol(idx - 1, col)"
582
+ @stop-edit="stopEdit"
583
+ />
584
+ </td>
585
+ <!-- Auto-detected meta column cells -->
586
+ <td
587
+ v-for="mc in metaColumns"
588
+ :key="mc.id"
589
+ class="px-1 py-0.5"
590
+ :style="
591
+ cellEditor(idx - 1, mc.id) ? { outline: `2px solid ${cellEditor(idx - 1, mc.id).user?.color}` } : {}
592
+ "
593
+ >
594
+ <ATableCell
595
+ :value="getMetaCellValue(idx - 1, mc.metaKey)"
596
+ :meta-type="mc.metaType"
597
+ :editing="editingCell?.rowIdx === idx - 1 && editingCell?.colId === mc.id"
598
+ :column="{ id: mc.id, type: 'meta', metaKey: mc.metaKey, metaType: mc.metaType, label: mc.label }"
599
+ @update:model-value="onMetaCellUpdate(idx - 1, mc.metaKey, $event)"
600
+ @start-edit="startEditMetaCol(idx - 1, mc)"
601
+ @stop-edit="stopEdit"
602
+ />
603
+ </td>
604
+ </tr>
605
+ </TransitionGroup>
606
+ <tfoot v-if="editable !== false">
607
+ <tr>
608
+ <td :colspan="orderedColumns.length + metaColumns.length + 3">
609
+ <button
610
+ class="w-full text-left px-3 py-2 text-xs text-(--ui-text-dimmed) hover:bg-(--ui-bg-elevated) border-b border-(--ui-border) transition-colors"
611
+ @click="addRow"
612
+ >
613
+ + {{ labels.addRow }}
614
+ </button>
615
+ </td>
616
+ </tr>
617
+ </tfoot>
618
+ </table>
619
+ </template>
620
+
621
+ <!-- Cell context menu -->
622
+ <Teleport
623
+ v-if="editable !== false"
624
+ to="body"
625
+ >
626
+ <div
627
+ v-if="showCtx && ctxItems.length"
628
+ class="fixed z-50 min-w-40 bg-(--ui-bg) border border-(--ui-border) rounded-lg shadow-xl py-1 text-sm"
629
+ :style="{ left: ctxPos.x + 'px', top: ctxPos.y + 'px' }"
630
+ >
631
+ <template
632
+ v-for="(group, gi) in ctxItems"
633
+ :key="gi"
634
+ >
635
+ <div
636
+ v-if="gi > 0"
637
+ class="my-1 border-t border-(--ui-border)"
638
+ />
639
+ <button
640
+ v-for="item in group"
641
+ :key="item.label"
642
+ class="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-(--ui-bg-elevated) text-left cursor-default"
643
+ :class="item.color === 'error' ? 'text-(--ui-color-error-500)' : 'text-(--ui-text)'"
644
+ @pointerdown.stop
645
+ @click="item.onSelect();
646
+ showCtx = false"
647
+ >
648
+ <UIcon
649
+ :name="item.icon"
650
+ class="size-3.5 shrink-0 opacity-70"
651
+ />
652
+ {{ item.label }}
653
+ </button>
654
+ </template>
655
+ </div>
656
+ </Teleport>
657
+ </div>
658
+ </template>
659
+
660
+ <style scoped>
661
+ .trow-move{transition:transform .25s ease}.trow-enter-active{transition:opacity .18s ease,transform .18s ease}.trow-enter-from{opacity:0;transform:translateY(-6px) scale(.97)}.trow-leave-active{transition:opacity .15s ease}.trow-leave-to{opacity:0}
662
+ </style>