@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.
- package/dist/module.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -2
- package/dist/runtime/assets/sources.css +1 -0
- package/dist/runtime/components/ADocumentTree.d.vue.ts +11 -1
- package/dist/runtime/components/ADocumentTree.vue +13 -6
- package/dist/runtime/components/ADocumentTree.vue.d.ts +11 -1
- package/dist/runtime/components/renderers/AChecklistRenderer.vue +22 -4
- package/dist/runtime/components/renderers/ADashboardRenderer.vue +4 -2
- package/dist/runtime/components/renderers/AGalleryRenderer.vue +97 -70
- package/dist/runtime/components/renderers/AGraphRenderer.vue +209 -58
- package/dist/runtime/components/renderers/AKanbanRenderer.vue +145 -34
- package/dist/runtime/components/renderers/AMediaRenderer.vue +27 -17
- package/dist/runtime/components/renderers/AOutlineRenderer.vue +38 -23
- package/dist/runtime/components/renderers/ASlidesRenderer.d.vue.ts +21 -0
- package/dist/runtime/components/renderers/ASlidesRenderer.vue +591 -0
- package/dist/runtime/components/renderers/ASlidesRenderer.vue.d.ts +21 -0
- package/dist/runtime/components/renderers/ASpatialRenderer.vue +23 -0
- package/dist/runtime/components/renderers/ATableRenderer.vue +20 -391
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.d.vue.ts +40 -0
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue +227 -0
- package/dist/runtime/components/renderers/gallery/AGalleryItemCard.vue.d.ts +40 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +16 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +66 -0
- package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +16 -0
- package/dist/runtime/components/renderers/table/ATableFlatMode.d.vue.ts +2 -0
- package/dist/runtime/components/renderers/table/ATableFlatMode.vue +184 -21
- package/dist/runtime/components/renderers/table/ATableFlatMode.vue.d.ts +2 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.d.vue.ts +26 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue +662 -0
- package/dist/runtime/components/renderers/table/ATableHierarchyMode.vue.d.ts +26 -0
- package/dist/runtime/composables/useAwareness.js +14 -3
- package/dist/runtime/composables/useBackgroundSync.js +19 -1
- package/dist/runtime/composables/useFileIndex.js +38 -17
- package/dist/runtime/composables/useSearchIndex.js +41 -16
- package/dist/runtime/composables/useSlidesNavigation.d.ts +45 -0
- package/dist/runtime/composables/useSlidesNavigation.js +185 -0
- package/dist/runtime/composables/useYDoc.d.ts +1 -1
- package/dist/runtime/composables/useYDoc.js +47 -9
- package/dist/runtime/locale.d.ts +38 -0
- package/dist/runtime/locale.js +41 -3
- package/dist/runtime/utils/docTypes.js +17 -0
- 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
|
-
|
|
336
|
+
{{ labels.noRows }}
|
|
243
337
|
</p>
|
|
244
338
|
<UButton
|
|
245
339
|
v-if="editable !== false"
|
|
246
340
|
icon="i-lucide-plus"
|
|
247
|
-
label="
|
|
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
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
+
|
|
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;
|