@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,227 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ import { getMetaColor } from "../../../utils/getMetaColor";
4
+ const props = defineProps({
5
+ item: { type: Object, required: true },
6
+ galleryAspect: { type: String, required: true },
7
+ cardStyle: { type: String, required: true },
8
+ showLabels: { type: Boolean, required: true },
9
+ isDragging: { type: Boolean, required: true },
10
+ isDragOver: { type: Boolean, required: true },
11
+ focusers: { type: Array, required: true },
12
+ canWrite: { type: Boolean, required: true },
13
+ renameId: { type: [String, null], required: true },
14
+ renameValue: { type: String, required: true }
15
+ });
16
+ const emit = defineEmits(["commit-rename", "cancel-rename", "update:rename-value"]);
17
+ const meta = computed(() => props.item.meta ?? {});
18
+ const metaColor = computed(() => getMetaColor(meta.value));
19
+ const coverFailed = ref(false);
20
+ const hasCover = computed(() => !!meta.value.coverUploadId && !coverFailed.value);
21
+ const hasIcon = computed(() => !!meta.value.icon);
22
+ const hasColor = computed(() => !!metaColor.value);
23
+ const ratingStars = computed(() => {
24
+ const r = meta.value.rating ?? 0;
25
+ return r > 0 ? r : 0;
26
+ });
27
+ const visibleTags = computed(() => {
28
+ const tags = meta.value.tags ?? [];
29
+ return tags.slice(0, 2);
30
+ });
31
+ const overflowTagCount = computed(() => {
32
+ const tags = meta.value.tags ?? [];
33
+ return Math.max(0, tags.length - 2);
34
+ });
35
+ const formattedDate = computed(() => {
36
+ const raw = meta.value.datetimeStart ?? meta.value.dateStart ?? meta.value.dateTaken;
37
+ if (!raw) return null;
38
+ try {
39
+ const d = new Date(raw);
40
+ return d.toLocaleDateString(void 0, { month: "short", day: "numeric" });
41
+ } catch {
42
+ return null;
43
+ }
44
+ });
45
+ const priority = computed(() => meta.value.priority ?? 0);
46
+ function onCoverError() {
47
+ coverFailed.value = true;
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <div
53
+ class="group relative rounded-lg border border-(--ui-border) hover:border-(--ui-primary) transition-colors overflow-hidden cursor-pointer"
54
+ :class="{
55
+ 'opacity-40 scale-95': isDragging,
56
+ 'ring-2 ring-(--ui-primary)/40': isDragOver
57
+ }"
58
+ :style="[
59
+ focusers.length ? { boxShadow: `0 0 0 2px ${focusers[0].user?.color ?? '#888'}` } : {},
60
+ metaColor ? { borderBottom: `3px solid ${metaColor}` } : {}
61
+ ]"
62
+ >
63
+ <!-- Remote user badge -->
64
+ <div
65
+ v-if="focusers.length"
66
+ class="absolute top-1 right-1 z-10 flex gap-0.5"
67
+ >
68
+ <span
69
+ v-for="f in focusers"
70
+ :key="f.clientId"
71
+ class="text-[10px] leading-none px-1.5 py-0.5 rounded-full text-white truncate max-w-20"
72
+ :style="{ backgroundColor: f.user?.color ?? '#888' }"
73
+ >
74
+ {{ f.user?.name ?? "User" }}
75
+ </span>
76
+ </div>
77
+
78
+ <!-- Cover zone -->
79
+ <div
80
+ class="bg-(--ui-bg-elevated) flex items-center justify-center overflow-hidden relative"
81
+ :style="{ aspectRatio: galleryAspect }"
82
+ >
83
+ <AGalleryCoverImage
84
+ v-if="hasCover"
85
+ :upload-id="meta.coverUploadId"
86
+ :doc-id="meta.coverDocId ?? item.id"
87
+ :mime-type="meta.coverMimeType"
88
+ class="w-full h-full"
89
+ @error="onCoverError"
90
+ />
91
+
92
+ <!-- Icon fallback (with optional color tint) -->
93
+ <div
94
+ v-else-if="hasIcon"
95
+ class="w-full h-full flex items-center justify-center"
96
+ :style="metaColor ? { backgroundColor: `${metaColor}18` } : {}"
97
+ >
98
+ <UIcon
99
+ :name="`i-lucide-${meta.icon}`"
100
+ class="size-12"
101
+ :style="metaColor ? { color: metaColor } : {}"
102
+ :class="!metaColor ? 'text-(--ui-text-dimmed) opacity-60' : ''"
103
+ />
104
+ </div>
105
+
106
+ <!-- Color swatch fallback -->
107
+ <div
108
+ v-else-if="hasColor"
109
+ class="w-full h-full"
110
+ :style="{ backgroundColor: `${metaColor}20` }"
111
+ >
112
+ <div class="w-full h-full flex items-center justify-center">
113
+ <div
114
+ class="size-10 rounded-full"
115
+ :style="{ backgroundColor: metaColor }"
116
+ />
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Default placeholder -->
121
+ <UIcon
122
+ v-else
123
+ name="i-lucide-file-text"
124
+ class="size-8 text-(--ui-text-dimmed) opacity-40"
125
+ />
126
+
127
+ <!-- Detailed overlays -->
128
+ <template v-if="cardStyle === 'detailed'">
129
+ <div
130
+ v-if="ratingStars > 0"
131
+ class="absolute right-1.5 flex gap-px bg-black/40 rounded px-1 py-0.5"
132
+ :class="focusers.length ? 'top-7' : 'top-1.5'"
133
+ >
134
+ <UIcon
135
+ v-for="i in ratingStars"
136
+ :key="i"
137
+ name="i-lucide-star"
138
+ class="size-3 text-amber-400"
139
+ />
140
+ </div>
141
+
142
+ <div
143
+ v-if="visibleTags.length"
144
+ class="absolute bottom-1.5 left-1.5 flex gap-1 max-w-[70%]"
145
+ >
146
+ <UBadge
147
+ v-for="tag in visibleTags"
148
+ :key="tag"
149
+ size="xs"
150
+ variant="subtle"
151
+ class="truncate max-w-24 backdrop-blur-sm"
152
+ >
153
+ {{ tag }}
154
+ </UBadge>
155
+ <UBadge
156
+ v-if="overflowTagCount > 0"
157
+ size="xs"
158
+ variant="subtle"
159
+ class="backdrop-blur-sm"
160
+ >
161
+ +{{ overflowTagCount }}
162
+ </UBadge>
163
+ </div>
164
+
165
+ <UBadge
166
+ v-if="formattedDate"
167
+ size="xs"
168
+ variant="subtle"
169
+ class="absolute bottom-1.5 right-1.5 backdrop-blur-sm"
170
+ >
171
+ {{ formattedDate }}
172
+ </UBadge>
173
+ </template>
174
+ </div>
175
+
176
+ <!-- Label zone -->
177
+ <div
178
+ v-if="showLabels"
179
+ class="p-2 border-t border-(--ui-border) flex items-center justify-between gap-1"
180
+ >
181
+ <div class="flex-1 min-w-0">
182
+ <UInput
183
+ v-if="canWrite && renameId === item.id"
184
+ :model-value="renameValue"
185
+ size="xs"
186
+ variant="none"
187
+ class="flex-1 -mx-1"
188
+ autofocus
189
+ @update:model-value="emit('update:rename-value', $event)"
190
+ @keydown.enter="emit('commit-rename')"
191
+ @keydown.escape="emit('cancel-rename')"
192
+ @blur="emit('commit-rename')"
193
+ @click.stop
194
+ />
195
+ <template v-else>
196
+ <div class="flex items-center gap-1">
197
+ <UIcon
198
+ v-if="hasIcon && hasCover"
199
+ :name="`i-lucide-${meta.icon}`"
200
+ class="size-3.5 shrink-0 text-(--ui-text-muted)"
201
+ />
202
+ <p class="text-xs font-medium truncate">
203
+ {{ item.label }}
204
+ </p>
205
+ <UIcon
206
+ v-if="priority >= 2"
207
+ name="i-lucide-flag"
208
+ class="size-3 shrink-0"
209
+ :class="{
210
+ 'text-amber-400': priority === 2,
211
+ 'text-red-400': priority >= 3
212
+ }"
213
+ />
214
+ </div>
215
+ <p
216
+ v-if="meta.subtitle"
217
+ class="text-[10px] text-(--ui-text-dimmed) truncate mt-0.5"
218
+ >
219
+ {{ meta.subtitle }}
220
+ </p>
221
+ </template>
222
+ </div>
223
+
224
+ <slot name="actions" />
225
+ </div>
226
+ </div>
227
+ </template>
@@ -0,0 +1,40 @@
1
+ import type { TreeEntry } from '../../../composables/useChildTree.js';
2
+ type __VLS_Props = {
3
+ item: TreeEntry;
4
+ galleryAspect: string;
5
+ cardStyle: string;
6
+ showLabels: boolean;
7
+ isDragging: boolean;
8
+ isDragOver: boolean;
9
+ focusers: Array<{
10
+ clientId: number;
11
+ user?: {
12
+ name?: string;
13
+ color?: string;
14
+ };
15
+ }>;
16
+ canWrite: boolean;
17
+ renameId: string | null;
18
+ renameValue: string;
19
+ };
20
+ declare var __VLS_62: {};
21
+ type __VLS_Slots = {} & {
22
+ actions?: (props: typeof __VLS_62) => any;
23
+ };
24
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
25
+ "commit-rename": () => any;
26
+ "cancel-rename": () => any;
27
+ "update:rename-value": (value: string) => any;
28
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
29
+ "onCommit-rename"?: (() => any) | undefined;
30
+ "onCancel-rename"?: (() => any) | undefined;
31
+ "onUpdate:rename-value"?: ((value: string) => any) | undefined;
32
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
36
+ type __VLS_WithSlots<T, S> = T & {
37
+ new (): {
38
+ $slots: S;
39
+ };
40
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SpatialTransformInputs — internal sub-component for ASpatialRenderer.
3
+ *
4
+ * Position / Rotation / Scale numeric inputs for a selected spatial object.
5
+ * Ported from cou-sh/app/components/spatial/SpatialTransformInputs.vue.
6
+ * Not auto-imported.
7
+ */
8
+ import type { TreeEntry } from '../../../composables/useChildTree.js';
9
+ import type { useChildTree } from '../../../composables/useChildTree.js';
10
+ type __VLS_Props = {
11
+ entry: TreeEntry;
12
+ tree: ReturnType<typeof useChildTree>;
13
+ };
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,66 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ entry: { type: Object, required: true },
4
+ tree: { type: null, required: true }
5
+ });
6
+ function update(key, value) {
7
+ const num = Number.parseFloat(value);
8
+ if (Number.isFinite(num)) {
9
+ props.tree.updateMeta(props.entry.id, { [key]: num });
10
+ }
11
+ }
12
+ const rows = [
13
+ { label: "Pos", fields: [
14
+ { key: "spX", axis: "X" },
15
+ { key: "spY", axis: "Y" },
16
+ { key: "spZ", axis: "Z" }
17
+ ] },
18
+ { label: "Rot", fields: [
19
+ { key: "spRX", axis: "X" },
20
+ { key: "spRY", axis: "Y" },
21
+ { key: "spRZ", axis: "Z" }
22
+ ] },
23
+ { label: "Scale", fields: [
24
+ { key: "spSX", axis: "X" },
25
+ { key: "spSY", axis: "Y" },
26
+ { key: "spSZ", axis: "Z" }
27
+ ] }
28
+ ];
29
+ function getVal(key) {
30
+ const v = props.entry.meta?.[key];
31
+ if (typeof v === "number") return v;
32
+ if (key.startsWith("spS")) return 1;
33
+ return 0;
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <div class="sp-transform-inputs">
39
+ <div
40
+ v-for="row in rows"
41
+ :key="row.label"
42
+ class="sp-row"
43
+ >
44
+ <span class="sp-label">{{ row.label }}</span>
45
+ <div
46
+ v-for="f in row.fields"
47
+ :key="f.key"
48
+ class="sp-field"
49
+ >
50
+ <span class="sp-axis">{{ f.axis }}</span>
51
+ <UInput
52
+ type="number"
53
+ size="xs"
54
+ :model-value="String(getVal(f.key))"
55
+ step="0.1"
56
+ class="sp-input"
57
+ @update:model-value="update(f.key, String($event))"
58
+ />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </template>
63
+
64
+ <style scoped>
65
+ .sp-transform-inputs{display:flex;flex-direction:column;gap:4px;margin-top:8px}.sp-row{align-items:center;display:flex;gap:4px}.sp-label{color:var(--ui-text-dimmed);flex-shrink:0;font-size:.7rem;font-weight:600;width:36px}.sp-field{align-items:center;display:flex;flex:1;gap:2px;min-width:0}.sp-axis{color:var(--ui-text-muted);flex-shrink:0;font-size:.65rem;font-weight:600;text-align:center;width:10px}.sp-input{flex:1;min-width:0}.sp-input :deep(input){font-size:.7rem;padding:2px 4px;text-align:right}
66
+ </style>
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SpatialTransformInputs — internal sub-component for ASpatialRenderer.
3
+ *
4
+ * Position / Rotation / Scale numeric inputs for a selected spatial object.
5
+ * Ported from cou-sh/app/components/spatial/SpatialTransformInputs.vue.
6
+ * Not auto-imported.
7
+ */
8
+ import type { TreeEntry } from '../../../composables/useChildTree.js';
9
+ import type { useChildTree } from '../../../composables/useChildTree.js';
10
+ type __VLS_Props = {
11
+ entry: TreeEntry;
12
+ tree: ReturnType<typeof useChildTree>;
13
+ };
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -1,4 +1,5 @@
1
1
  import type { useTableView } from '../../../composables/useTableView.js';
2
+ import type { AbracadabraLocale } from '../../../locale.js';
2
3
  type __VLS_Props = {
3
4
  tree: ReturnType<typeof import('../../../composables/useChildTree').useChildTree>;
4
5
  tableView: ReturnType<typeof useTableView>;
@@ -6,6 +7,7 @@ type __VLS_Props = {
6
7
  myClientId: number;
7
8
  setLocalState: (state: Record<string, any>) => void;
8
9
  editable?: boolean;
10
+ labels?: Partial<AbracadabraLocale['renderers']['table']>;
9
11
  };
10
12
  declare function addRow(): void;
11
13
  declare function addMetaColumn(): void;
@@ -1,6 +1,8 @@
1
1
  <script setup>
2
2
  import { ref, computed, onBeforeUnmount } from "vue";
3
+ import { useEventListener } from "@vueuse/core";
3
4
  import { useTouchDrag } from "../../../composables/useTouchDrag";
5
+ import { DEFAULT_LOCALE } from "../../../locale";
4
6
  import ATableColumnHeader from "./ATableColumnHeader.vue";
5
7
  import ATableCell from "./cells/ATableCell.vue";
6
8
  const props = defineProps({
@@ -9,9 +11,14 @@ const props = defineProps({
9
11
  states: { type: Array, required: true },
10
12
  myClientId: { type: Number, required: true },
11
13
  setLocalState: { type: Function, required: true },
12
- editable: { type: Boolean, required: false }
14
+ editable: { type: Boolean, required: false },
15
+ labels: { type: Object, required: false }
13
16
  });
14
17
  const emit = defineEmits(["openNode"]);
18
+ const labels = computed(() => ({
19
+ ...DEFAULT_LOCALE.renderers.table,
20
+ ...props.labels ?? {}
21
+ }));
15
22
  function camelToTitle(s) {
16
23
  return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase());
17
24
  }
@@ -219,9 +226,96 @@ function cellEditor(rowId, fieldId) {
219
226
  (s) => s["table:editing"]?.rowId === rowId && s["table:editing"]?.fieldId === fieldId
220
227
  );
221
228
  }
229
+ function onRowPointerEnter(rowId) {
230
+ if (rowDragId.value) return;
231
+ props.setLocalState({ "table:hovering": rowId });
232
+ }
233
+ function onRowPointerLeave() {
234
+ if (rowDragId.value) return;
235
+ props.setLocalState({ "table:hovering": null });
236
+ }
237
+ function rowHoverers(rowId) {
238
+ const result = [];
239
+ for (const s of props.states) {
240
+ if (s.clientId === props.myClientId) continue;
241
+ if (s["table:hovering"] === rowId && s.user) {
242
+ result.push({ name: s.user.name ?? "", color: s.user.color ?? "#888" });
243
+ }
244
+ }
245
+ return result;
246
+ }
247
+ function rowEditors(rowId) {
248
+ const result = [];
249
+ for (const s of props.states) {
250
+ if (s.clientId === props.myClientId) continue;
251
+ if (s["table:editing"]?.rowId === rowId && s.user) {
252
+ result.push({ name: s.user.name ?? "", color: s.user.color ?? "#888" });
253
+ }
254
+ }
255
+ return result;
256
+ }
257
+ function remoteResizeColor(colId) {
258
+ for (const s of props.states) {
259
+ if (s.clientId === props.myClientId) continue;
260
+ if (s["table:resizing"] === colId && s.user) {
261
+ return s.user.color ?? null;
262
+ }
263
+ }
264
+ return null;
265
+ }
266
+ const ctxRow = ref(null);
267
+ const ctxPos = ref({ x: 0, y: 0 });
268
+ const showCtx = ref(false);
269
+ function onRowContextMenu(e, row) {
270
+ if (props.editable === false) return;
271
+ e.preventDefault();
272
+ ctxRow.value = row;
273
+ ctxPos.value = { x: e.clientX, y: e.clientY };
274
+ showCtx.value = true;
275
+ }
276
+ useEventListener(typeof window !== "undefined" ? window : null, "pointerdown", () => {
277
+ showCtx.value = false;
278
+ });
279
+ let longPressTimer = null;
280
+ function startLongPress(e, row) {
281
+ if (props.editable === false) return;
282
+ longPressTimer = setTimeout(() => {
283
+ const touch = e.touches[0];
284
+ if (touch) {
285
+ ctxRow.value = row;
286
+ ctxPos.value = { x: touch.clientX, y: touch.clientY };
287
+ showCtx.value = true;
288
+ }
289
+ longPressTimer = null;
290
+ }, 500);
291
+ }
292
+ function cancelLongPress() {
293
+ if (longPressTimer) {
294
+ clearTimeout(longPressTimer);
295
+ longPressTimer = null;
296
+ }
297
+ }
298
+ const ctxItems = computed(() => {
299
+ if (!ctxRow.value) return [];
300
+ const row = ctxRow.value;
301
+ return [
302
+ [
303
+ { label: labels.value.open, icon: "i-lucide-external-link", onSelect: () => emit("openNode", row.id, row.label) },
304
+ { label: labels.value.rename, icon: "i-lucide-pencil", onSelect: () => {
305
+ const nameCol = allColumns.value.find((c) => c.type === "label");
306
+ if (nameCol) startEdit(row, nameCol);
307
+ } },
308
+ { label: labels.value.duplicate, icon: "i-lucide-copy", onSelect: () => props.tree.duplicateEntry(row.id) }
309
+ ],
310
+ [
311
+ { label: labels.value.delete, icon: "i-lucide-trash-2", color: "error", onSelect: () => deleteRow(row) }
312
+ ]
313
+ ];
314
+ });
222
315
  onBeforeUnmount(() => {
223
316
  props.setLocalState({
224
317
  "table:editing": null,
318
+ "table:hovering": null,
225
319
  "table:resizing": null
226
320
  });
227
321
  });
@@ -239,12 +333,12 @@ defineExpose({ addMetaColumn, addRow });
239
333
  class="size-10 text-(--ui-text-dimmed)"
240
334
  />
241
335
  <p class="text-sm text-(--ui-text-muted)">
242
- No rows yet
336
+ {{ labels.noRows }}
243
337
  </p>
244
338
  <UButton
245
339
  v-if="editable !== false"
246
340
  icon="i-lucide-plus"
247
- label="Add row"
341
+ :label="labels.addRow"
248
342
  size="sm"
249
343
  @click="addRow"
250
344
  />
@@ -278,6 +372,7 @@ defineExpose({ addMetaColumn, addRow });
278
372
  colDragOverId === col.id ? 'border-l-2 border-(--ui-primary)' : '',
279
373
  colDragId === col.id ? 'opacity-30' : ''
280
374
  ]"
375
+ :style="remoteResizeColor(col.id) ? { backgroundColor: remoteResizeColor(col.id) + '20' } : {}"
281
376
  @rename="renameColumn(col, $event)"
282
377
  @delete="deleteColumn(col)"
283
378
  @sort="handleSort(col.id === '__label' ? '__label' : col.metaKey ?? col.id, $event)"
@@ -299,8 +394,29 @@ defineExpose({ addMetaColumn, addRow });
299
394
  rowDragOverId === row.id ? 'border-t-2 border-(--ui-primary)' : 'border-b border-(--ui-border)',
300
395
  rowDragId === row.id ? 'opacity-30' : ''
301
396
  ]"
397
+ @contextmenu="onRowContextMenu($event, row)"
398
+ @touchstart.passive="startLongPress($event, row)"
399
+ @touchend="cancelLongPress"
400
+ @touchmove="cancelLongPress"
401
+ @pointerenter="onRowPointerEnter(row.id)"
402
+ @pointerleave="onRowPointerLeave"
302
403
  >
303
- <td class="w-0 p-0" />
404
+ <td class="w-0 p-0 relative overflow-visible">
405
+ <!-- Remote editing indicator (left bar) -->
406
+ <div
407
+ v-if="rowEditors(row.id).length"
408
+ class="absolute left-0 top-0 bottom-0 w-0.5 rounded-full z-10"
409
+ :style="{ background: rowEditors(row.id)[0].color }"
410
+ />
411
+ <!-- Remote hover name badge -->
412
+ <span
413
+ v-if="rowHoverers(row.id).length"
414
+ class="absolute -top-2.5 left-6 text-[10px] px-1 rounded text-white leading-tight z-10 whitespace-nowrap pointer-events-none"
415
+ :style="{ backgroundColor: rowHoverers(row.id)[0].color }"
416
+ >
417
+ {{ rowHoverers(row.id).map((h) => h.name).join(", ") }}
418
+ </span>
419
+ </td>
304
420
  <td
305
421
  class="px-1 py-1.5 text-(--ui-text-dimmed) opacity-0 group-hover:opacity-60 cursor-grab w-6 touch-none"
306
422
  @pointerdown="editable !== false && handleRowPointerDown($event, row.id)"
@@ -313,23 +429,33 @@ defineExpose({ addMetaColumn, addRow });
313
429
  <td class="px-2 py-1.5 text-xs text-(--ui-text-dimmed) select-none w-20">
314
430
  <div class="flex items-center gap-0.5">
315
431
  <span class="w-4 text-center">{{ idx + 1 }}</span>
316
- <UButton
317
- icon="i-lucide-external-link"
318
- size="xs"
319
- variant="ghost"
320
- color="neutral"
321
- class="opacity-0 group-hover:opacity-100"
322
- @click="emit('openNode', row.id, row.label)"
323
- />
324
- <UButton
432
+ <UTooltip
433
+ :text="labels.openAsSlideover"
434
+ :content="{ side: 'bottom' }"
435
+ >
436
+ <UButton
437
+ icon="i-lucide-external-link"
438
+ size="xs"
439
+ variant="ghost"
440
+ color="neutral"
441
+ class="opacity-0 group-hover:opacity-100"
442
+ @click="emit('openNode', row.id, row.label)"
443
+ />
444
+ </UTooltip>
445
+ <UTooltip
325
446
  v-if="editable !== false"
326
- icon="i-lucide-trash-2"
327
- size="xs"
328
- variant="ghost"
329
- color="error"
330
- class="opacity-0 group-hover:opacity-100"
331
- @click="deleteRow(row)"
332
- />
447
+ :text="labels.deleteRow"
448
+ :content="{ side: 'bottom' }"
449
+ >
450
+ <UButton
451
+ icon="i-lucide-trash-2"
452
+ size="xs"
453
+ variant="ghost"
454
+ color="error"
455
+ class="opacity-0 group-hover:opacity-100"
456
+ @click="deleteRow(row)"
457
+ />
458
+ </UTooltip>
333
459
  </div>
334
460
  </td>
335
461
  <td
@@ -359,9 +485,46 @@ defineExpose({ addMetaColumn, addRow });
359
485
  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"
360
486
  @click="addRow"
361
487
  >
362
- + Add row
488
+ + {{ labels.addRow }}
363
489
  </button>
364
490
  </template>
491
+
492
+ <!-- Row context menu -->
493
+ <Teleport
494
+ v-if="editable !== false"
495
+ to="body"
496
+ >
497
+ <div
498
+ v-if="showCtx && ctxItems.length"
499
+ class="fixed z-50 min-w-40 bg-(--ui-bg) border border-(--ui-border) rounded-lg shadow-xl py-1 text-sm"
500
+ :style="{ left: ctxPos.x + 'px', top: ctxPos.y + 'px' }"
501
+ >
502
+ <template
503
+ v-for="(group, gi) in ctxItems"
504
+ :key="gi"
505
+ >
506
+ <div
507
+ v-if="gi > 0"
508
+ class="my-1 border-t border-(--ui-border)"
509
+ />
510
+ <button
511
+ v-for="item in group"
512
+ :key="item.label"
513
+ class="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-(--ui-bg-elevated) text-left cursor-default"
514
+ :class="item.color === 'error' ? 'text-(--ui-color-error-500)' : 'text-(--ui-text)'"
515
+ @pointerdown.stop
516
+ @click="item.onSelect();
517
+ showCtx = false"
518
+ >
519
+ <UIcon
520
+ :name="item.icon"
521
+ class="size-3.5 shrink-0 opacity-70"
522
+ />
523
+ {{ item.label }}
524
+ </button>
525
+ </template>
526
+ </div>
527
+ </Teleport>
365
528
  </div>
366
529
  </template>
367
530
 
@@ -1,4 +1,5 @@
1
1
  import type { useTableView } from '../../../composables/useTableView.js';
2
+ import type { AbracadabraLocale } from '../../../locale.js';
2
3
  type __VLS_Props = {
3
4
  tree: ReturnType<typeof import('../../../composables/useChildTree').useChildTree>;
4
5
  tableView: ReturnType<typeof useTableView>;
@@ -6,6 +7,7 @@ type __VLS_Props = {
6
7
  myClientId: number;
7
8
  setLocalState: (state: Record<string, any>) => void;
8
9
  editable?: boolean;
10
+ labels?: Partial<AbracadabraLocale['renderers']['table']>;
9
11
  };
10
12
  declare function addRow(): void;
11
13
  declare function addMetaColumn(): void;