@abraca/nuxt 0.1.0 → 0.1.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.
- package/README.md +216 -56
- package/dist/module.json +1 -1
- package/dist/runtime/components/ADocumentTree.d.vue.ts +8 -43
- package/dist/runtime/components/ADocumentTree.vue +1239 -274
- package/dist/runtime/components/ADocumentTree.vue.d.ts +8 -43
- package/dist/runtime/components/AEditor.d.vue.ts +5 -0
- package/dist/runtime/components/AEditor.vue +85 -29
- package/dist/runtime/components/AEditor.vue.d.ts +5 -0
- package/dist/runtime/components/ANodePanel.vue +1 -1
- package/dist/runtime/composables/useConnectionStatus.d.ts +5 -1
- package/dist/runtime/composables/useConnectionStatus.js +36 -11
- package/dist/runtime/composables/useEditorSuggestions.js +10 -0
- package/dist/runtime/extensions/meta-field.d.ts +16 -0
- package/dist/runtime/extensions/meta-field.js +110 -0
- package/dist/runtime/extensions/views/MetaFieldView.d.vue.ts +4 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue +489 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue.d.ts +4 -0
- package/dist/runtime/plugin-abracadabra.client.js +55 -4
- package/dist/runtime/plugins/core.plugin.js +7 -3
- package/dist/runtime/server/plugins/abracadabra-service.d.ts +1 -1
- package/dist/runtime/server/plugins/abracadabra-service.js +3 -0
- package/dist/runtime/utils/docDragDrop.d.ts +13 -0
- package/dist/runtime/utils/docDragDrop.js +26 -0
- package/package.json +14 -12
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, nextTick } from "vue";
|
|
3
|
+
import { NodeViewWrapper } from "@tiptap/vue-3";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
decorations: { type: Array, required: true },
|
|
6
|
+
selected: { type: Boolean, required: true },
|
|
7
|
+
updateAttributes: { type: Function, required: true },
|
|
8
|
+
deleteNode: { type: Function, required: true },
|
|
9
|
+
node: { type: null, required: true },
|
|
10
|
+
view: { type: null, required: true },
|
|
11
|
+
getPos: { type: null, required: true },
|
|
12
|
+
innerDecorations: { type: null, required: true },
|
|
13
|
+
editor: { type: Object, required: true },
|
|
14
|
+
extension: { type: Object, required: true },
|
|
15
|
+
HTMLAttributes: { type: Object, required: true }
|
|
16
|
+
});
|
|
17
|
+
function storage() {
|
|
18
|
+
return props.editor.storage?.metaField;
|
|
19
|
+
}
|
|
20
|
+
const fieldType = computed(() => props.node.attrs.fieldType);
|
|
21
|
+
const fieldLabel = computed(() => props.node.attrs.fieldLabel);
|
|
22
|
+
const metaKey = computed(() => props.node.attrs.metaKey);
|
|
23
|
+
const startKey = computed(() => props.node.attrs.startKey);
|
|
24
|
+
const endKey = computed(() => props.node.attrs.endKey);
|
|
25
|
+
const allDayKey = computed(() => props.node.attrs.allDayKey);
|
|
26
|
+
const presets = computed(() => {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(props.node.attrs.presets ?? "[]");
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
const options = computed(() => {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(props.node.attrs.options ?? "[]");
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const sliderMin = computed(() => props.node.attrs.sliderMin ?? 0);
|
|
41
|
+
const sliderMax = computed(() => props.node.attrs.sliderMax ?? 100);
|
|
42
|
+
const sliderStep = computed(() => props.node.attrs.sliderStep ?? 1);
|
|
43
|
+
const unit = computed(() => props.node.attrs.unit ?? "");
|
|
44
|
+
function getStr(key) {
|
|
45
|
+
return storage()?.pageMeta?.[key] ?? "";
|
|
46
|
+
}
|
|
47
|
+
function getStrArr(key) {
|
|
48
|
+
const v = storage()?.pageMeta?.[key];
|
|
49
|
+
return Array.isArray(v) ? v : [];
|
|
50
|
+
}
|
|
51
|
+
function getNum(key, fallback = 0) {
|
|
52
|
+
return storage()?.pageMeta?.[key] ?? fallback;
|
|
53
|
+
}
|
|
54
|
+
function getBool(key) {
|
|
55
|
+
return Boolean(storage()?.pageMeta?.[key]);
|
|
56
|
+
}
|
|
57
|
+
function patch(update) {
|
|
58
|
+
storage()?.updateMeta?.(update);
|
|
59
|
+
}
|
|
60
|
+
const labelDraft = ref("");
|
|
61
|
+
function commitLabel() {
|
|
62
|
+
if (labelDraft.value !== fieldLabel.value) {
|
|
63
|
+
props.updateAttributes({ fieldLabel: labelDraft.value });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function deleteChip() {
|
|
67
|
+
props.deleteNode();
|
|
68
|
+
}
|
|
69
|
+
const defaultPresets = ["#6366f1", "#f97316", "#22c55e", "#ef4444", "#a855f7", "#06b6d4", "#f59e0b", "#ec4899"];
|
|
70
|
+
const allPresets = computed(() => presets.value.length ? presets.value : defaultPresets);
|
|
71
|
+
function toggleSelect(opt) {
|
|
72
|
+
patch({ [metaKey.value]: opt === getStr(metaKey.value) ? "" : opt });
|
|
73
|
+
}
|
|
74
|
+
function toggleMultiSelect(opt) {
|
|
75
|
+
const current = getStrArr(metaKey.value);
|
|
76
|
+
const next = current.includes(opt) ? current.filter((x) => x !== opt) : [...current, opt];
|
|
77
|
+
patch({ [metaKey.value]: next });
|
|
78
|
+
}
|
|
79
|
+
function toggleTag(tag) {
|
|
80
|
+
const current = getStrArr(metaKey.value);
|
|
81
|
+
const next = current.includes(tag) ? current.filter((x) => x !== tag) : [...current, tag];
|
|
82
|
+
patch({ [metaKey.value]: next });
|
|
83
|
+
}
|
|
84
|
+
function fmtDate(iso) {
|
|
85
|
+
if (!iso) return "";
|
|
86
|
+
try {
|
|
87
|
+
return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
88
|
+
} catch {
|
|
89
|
+
return iso;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function fmtDatetime(iso) {
|
|
93
|
+
if (!iso) return "";
|
|
94
|
+
try {
|
|
95
|
+
return new Date(iso).toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
96
|
+
} catch {
|
|
97
|
+
return iso;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const displayValue = computed(() => {
|
|
101
|
+
const t = fieldType.value;
|
|
102
|
+
if (t === "toggle") return getBool(metaKey.value) ? "Yes" : "No";
|
|
103
|
+
if (t === "colorPreset" || t === "colorPicker") return getStr(metaKey.value) ? "" : "None";
|
|
104
|
+
if (t === "date") return fmtDate(getStr(metaKey.value)) || "Set";
|
|
105
|
+
if (t === "datetime") return fmtDatetime(getStr(metaKey.value)) || "Set";
|
|
106
|
+
if (t === "daterange") {
|
|
107
|
+
const s = getStr(startKey.value);
|
|
108
|
+
const e = getStr(endKey.value);
|
|
109
|
+
return s && e ? `${fmtDate(s)} \u2013 ${fmtDate(e)}` : "Set";
|
|
110
|
+
}
|
|
111
|
+
if (t === "datetimerange") {
|
|
112
|
+
const s = getStr(startKey.value);
|
|
113
|
+
const e = getStr(endKey.value);
|
|
114
|
+
return s && e ? `${fmtDatetime(s)} \u2013 ${fmtDatetime(e)}` : "Set";
|
|
115
|
+
}
|
|
116
|
+
if (t === "timerange") {
|
|
117
|
+
const s = getStr(startKey.value);
|
|
118
|
+
const e = getStr(endKey.value);
|
|
119
|
+
return s && e ? `${s} \u2013 ${e}` : "Set";
|
|
120
|
+
}
|
|
121
|
+
if (t === "time") return getStr(metaKey.value) || "Set";
|
|
122
|
+
if (t === "slider" || t === "number") {
|
|
123
|
+
const v = getNum(metaKey.value, sliderMin.value);
|
|
124
|
+
return unit.value ? `${v} ${unit.value}` : String(v);
|
|
125
|
+
}
|
|
126
|
+
if (t === "rating") {
|
|
127
|
+
const v = getNum(metaKey.value);
|
|
128
|
+
return v ? `${v}/${sliderMax.value}` : "Rate";
|
|
129
|
+
}
|
|
130
|
+
if (t === "url") {
|
|
131
|
+
const v = getStr(metaKey.value);
|
|
132
|
+
return v ? new URL(v).hostname : "Add URL";
|
|
133
|
+
}
|
|
134
|
+
if (t === "select") return getStr(metaKey.value) || "Select";
|
|
135
|
+
if (t === "multiselect") {
|
|
136
|
+
const arr = getStrArr(metaKey.value);
|
|
137
|
+
return arr.length ? arr.join(", ") : "Select";
|
|
138
|
+
}
|
|
139
|
+
if (t === "tags") {
|
|
140
|
+
const arr = getStrArr(metaKey.value);
|
|
141
|
+
return arr.length ? arr.join(", ") : "Add tags";
|
|
142
|
+
}
|
|
143
|
+
if (t === "textarea") {
|
|
144
|
+
const v = getStr(metaKey.value);
|
|
145
|
+
return v ? v.slice(0, 24) + (v.length > 24 ? "\u2026" : "") : "Add text";
|
|
146
|
+
}
|
|
147
|
+
if (t === "location") {
|
|
148
|
+
const lat = getNum(metaKey.value);
|
|
149
|
+
return lat ? `${getNum(props.node.attrs.latKey)?.toFixed(2)}, ${getNum(props.node.attrs.lngKey)?.toFixed(2)}` : "Set";
|
|
150
|
+
}
|
|
151
|
+
return getStr(metaKey.value) || "\u2014";
|
|
152
|
+
});
|
|
153
|
+
const chipColor = computed(() => {
|
|
154
|
+
const t = fieldType.value;
|
|
155
|
+
if (t === "colorPreset" || t === "colorPicker") return getStr(metaKey.value) || "";
|
|
156
|
+
return "";
|
|
157
|
+
});
|
|
158
|
+
const popoverOpen = ref(false);
|
|
159
|
+
const editingText = ref(false);
|
|
160
|
+
const textDraft = ref("");
|
|
161
|
+
const newOption = ref("");
|
|
162
|
+
function openEditor() {
|
|
163
|
+
labelDraft.value = fieldLabel.value;
|
|
164
|
+
popoverOpen.value = true;
|
|
165
|
+
}
|
|
166
|
+
function startTextEdit() {
|
|
167
|
+
textDraft.value = getStr(metaKey.value);
|
|
168
|
+
editingText.value = true;
|
|
169
|
+
nextTick(() => document.querySelector(".meta-text-input")?.focus());
|
|
170
|
+
}
|
|
171
|
+
function commitText() {
|
|
172
|
+
patch({ [metaKey.value]: textDraft.value });
|
|
173
|
+
editingText.value = false;
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<template>
|
|
178
|
+
<NodeViewWrapper as="span" class="inline-flex items-center">
|
|
179
|
+
<!-- ── Chip wrapper ─────────────────────────────────────────────────────── -->
|
|
180
|
+
<UPopover v-model:open="popoverOpen" :content="{ side: 'bottom', align: 'start' }">
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
class="inline-flex items-center gap-1 rounded-md border border-default bg-muted px-2 py-0.5 text-xs font-medium text-default hover:bg-elevated cursor-pointer select-none mx-0.5"
|
|
184
|
+
@mousedown.prevent
|
|
185
|
+
@click="openEditor"
|
|
186
|
+
>
|
|
187
|
+
<!-- Color swatch for color fields -->
|
|
188
|
+
<span
|
|
189
|
+
v-if="chipColor"
|
|
190
|
+
class="size-3 rounded-full border border-white/20 shrink-0"
|
|
191
|
+
:style="`background: ${chipColor}`"
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
<!-- Label -->
|
|
195
|
+
<span class="text-muted">{{ fieldLabel || fieldType }}</span>
|
|
196
|
+
|
|
197
|
+
<span class="text-default">
|
|
198
|
+
<!-- Toggle shows a switch icon -->
|
|
199
|
+
<span v-if="fieldType === 'toggle'">
|
|
200
|
+
<UIcon :name="getBool(metaKey) ? 'i-lucide-toggle-right' : 'i-lucide-toggle-left'" class="size-3.5 align-middle" />
|
|
201
|
+
</span>
|
|
202
|
+
<!-- Rating shows stars -->
|
|
203
|
+
<span v-else-if="fieldType === 'rating'" class="flex gap-0.5">
|
|
204
|
+
<UIcon
|
|
205
|
+
v-for="i in sliderMax"
|
|
206
|
+
:key="i"
|
|
207
|
+
:name="i <= getNum(metaKey) ? 'i-lucide-star' : 'i-lucide-star'"
|
|
208
|
+
:class="i <= getNum(metaKey) ? 'text-amber-400' : 'text-muted'"
|
|
209
|
+
class="size-3"
|
|
210
|
+
/>
|
|
211
|
+
</span>
|
|
212
|
+
<!-- Everything else: text display -->
|
|
213
|
+
<span v-else>{{ displayValue }}</span>
|
|
214
|
+
</span>
|
|
215
|
+
|
|
216
|
+
<!-- Delete button -->
|
|
217
|
+
<UButton
|
|
218
|
+
icon="i-lucide-x"
|
|
219
|
+
size="xs"
|
|
220
|
+
color="neutral"
|
|
221
|
+
variant="ghost"
|
|
222
|
+
class="size-3.5 p-0 opacity-50 hover:opacity-100 -mr-0.5"
|
|
223
|
+
@click.stop="deleteChip"
|
|
224
|
+
/>
|
|
225
|
+
</button>
|
|
226
|
+
|
|
227
|
+
<!-- ── Popover content ─────────────────────────────────────────────── -->
|
|
228
|
+
<template #content>
|
|
229
|
+
<div class="p-2 min-w-48 max-w-xs space-y-2">
|
|
230
|
+
<!-- Field label editor -->
|
|
231
|
+
<div class="flex items-center gap-1 border-b border-default pb-2">
|
|
232
|
+
<input
|
|
233
|
+
v-model="labelDraft"
|
|
234
|
+
:placeholder="fieldType"
|
|
235
|
+
class="flex-1 text-xs font-medium bg-transparent border-none outline-none"
|
|
236
|
+
@blur="commitLabel"
|
|
237
|
+
@keydown.enter.prevent="commitLabel"
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<!-- ── Toggle ──────────────────────────────────────────────────── -->
|
|
242
|
+
<div v-if="fieldType === 'toggle'" class="flex items-center justify-between">
|
|
243
|
+
<span class="text-xs text-muted">{{ getBool(metaKey) ? "On" : "Off" }}</span>
|
|
244
|
+
<USwitch
|
|
245
|
+
:model-value="getBool(metaKey)"
|
|
246
|
+
@update:model-value="(v) => patch({ [metaKey]: v })"
|
|
247
|
+
/>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<!-- ── Color presets ───────────────────────────────────────────── -->
|
|
251
|
+
<div v-else-if="fieldType === 'colorPreset'" class="flex flex-wrap gap-1.5">
|
|
252
|
+
<button
|
|
253
|
+
v-for="c in allPresets"
|
|
254
|
+
:key="c"
|
|
255
|
+
class="size-5 rounded-full border-2 cursor-pointer"
|
|
256
|
+
:style="`background: ${c}; border-color: ${getStr(metaKey) === c ? c : 'transparent'}`"
|
|
257
|
+
:class="getStr(metaKey) === c ? 'ring-2 ring-offset-1 ring-current' : ''"
|
|
258
|
+
@click="patch({ [metaKey]: getStr(metaKey) === c ? '' : c })"
|
|
259
|
+
/>
|
|
260
|
+
<!-- Clear -->
|
|
261
|
+
<button
|
|
262
|
+
v-if="getStr(metaKey)"
|
|
263
|
+
class="size-5 rounded-full border border-default flex items-center justify-center text-muted hover:bg-muted"
|
|
264
|
+
@click="patch({ [metaKey]: '' })"
|
|
265
|
+
>
|
|
266
|
+
<UIcon name="i-lucide-x" class="size-3" />
|
|
267
|
+
</button>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- ── Color picker ────────────────────────────────────────────── -->
|
|
271
|
+
<div v-else-if="fieldType === 'colorPicker'" class="space-y-1">
|
|
272
|
+
<input
|
|
273
|
+
type="color"
|
|
274
|
+
:value="getStr(metaKey) || '#6366f1'"
|
|
275
|
+
class="w-full h-8 rounded cursor-pointer"
|
|
276
|
+
@input="(e) => patch({ [metaKey]: e.target.value })"
|
|
277
|
+
/>
|
|
278
|
+
<UButton
|
|
279
|
+
v-if="getStr(metaKey)"
|
|
280
|
+
icon="i-lucide-x"
|
|
281
|
+
size="xs"
|
|
282
|
+
color="neutral"
|
|
283
|
+
variant="ghost"
|
|
284
|
+
label="Clear"
|
|
285
|
+
class="w-full justify-start"
|
|
286
|
+
@click="patch({ [metaKey]: '' })"
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<!-- ── Date ───────────────────────────────────────────────────── -->
|
|
291
|
+
<div v-else-if="fieldType === 'date'">
|
|
292
|
+
<input
|
|
293
|
+
type="date"
|
|
294
|
+
:value="getStr(metaKey)"
|
|
295
|
+
class="w-full text-xs rounded border border-default bg-transparent px-2 py-1 outline-none"
|
|
296
|
+
@change="(e) => patch({ [metaKey]: e.target.value })"
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<!-- ── Datetime ───────────────────────────────────────────────── -->
|
|
301
|
+
<div v-else-if="fieldType === 'datetime'">
|
|
302
|
+
<input
|
|
303
|
+
type="datetime-local"
|
|
304
|
+
:value="getStr(metaKey)"
|
|
305
|
+
class="w-full text-xs rounded border border-default bg-transparent px-2 py-1 outline-none"
|
|
306
|
+
@change="(e) => patch({ [metaKey]: e.target.value })"
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<!-- ── Date range ─────────────────────────────────────────────── -->
|
|
311
|
+
<div v-else-if="fieldType === 'daterange'" class="space-y-1">
|
|
312
|
+
<div class="flex items-center gap-1 text-xs text-muted"><span class="w-8">Start</span><input type="date" :value="getStr(startKey)" class="flex-1 text-xs rounded border border-default bg-transparent px-2 py-1 outline-none" @change="(e) => patch({ [startKey]: e.target.value })" /></div>
|
|
313
|
+
<div class="flex items-center gap-1 text-xs text-muted"><span class="w-8">End</span><input type="date" :value="getStr(endKey)" class="flex-1 text-xs rounded border border-default bg-transparent px-2 py-1 outline-none" @change="(e) => patch({ [endKey]: e.target.value })" /></div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<!-- ── Datetime range ─────────────────────────────────────────── -->
|
|
317
|
+
<div v-else-if="fieldType === 'datetimerange'" class="space-y-1">
|
|
318
|
+
<div class="flex items-center gap-1 text-xs text-muted"><span class="w-8">Start</span><input type="datetime-local" :value="getStr(startKey)" class="flex-1 text-xs rounded border border-default bg-transparent px-2 py-1 outline-none" @change="(e) => patch({ [startKey]: e.target.value })" /></div>
|
|
319
|
+
<div class="flex items-center gap-1 text-xs text-muted"><span class="w-8">End</span><input type="datetime-local" :value="getStr(endKey)" class="flex-1 text-xs rounded border border-default bg-transparent px-2 py-1 outline-none" @change="(e) => patch({ [endKey]: e.target.value })" /></div>
|
|
320
|
+
<div class="flex items-center gap-2 text-xs"><USwitch :model-value="getBool(allDayKey)" @update:model-value="(v) => patch({ [allDayKey]: v })" /><span class="text-muted">All day</span></div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<!-- ── Time ───────────────────────────────────────────────────── -->
|
|
324
|
+
<div v-else-if="fieldType === 'time' || fieldType === 'timerange'">
|
|
325
|
+
<div class="space-y-1">
|
|
326
|
+
<input type="time" :value="fieldType === 'time' ? getStr(metaKey) : getStr(startKey)" class="w-full text-xs rounded border border-default bg-transparent px-2 py-1 outline-none" @change="(e) => patch({ [fieldType === 'time' ? metaKey : startKey]: e.target.value })" />
|
|
327
|
+
<input v-if="fieldType === 'timerange'" type="time" :value="getStr(endKey)" class="w-full text-xs rounded border border-default bg-transparent px-2 py-1 outline-none" @change="(e) => patch({ [endKey]: e.target.value })" />
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<!-- ── Slider ─────────────────────────────────────────────────── -->
|
|
332
|
+
<div v-else-if="fieldType === 'slider'" class="space-y-1">
|
|
333
|
+
<div class="flex items-center gap-2">
|
|
334
|
+
<USlider
|
|
335
|
+
:model-value="getNum(metaKey, sliderMin)"
|
|
336
|
+
:min="sliderMin"
|
|
337
|
+
:max="sliderMax"
|
|
338
|
+
:step="sliderStep"
|
|
339
|
+
class="flex-1"
|
|
340
|
+
@update:model-value="(v) => patch({ [metaKey]: v })"
|
|
341
|
+
/>
|
|
342
|
+
<span class="text-xs w-10 text-right tabular-nums">{{ getNum(metaKey, sliderMin) }}</span>
|
|
343
|
+
</div>
|
|
344
|
+
<div class="flex justify-between text-xs text-muted">
|
|
345
|
+
<span>{{ sliderMin }}</span><span>{{ sliderMax }}</span>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<!-- ── Number ─────────────────────────────────────────────────── -->
|
|
350
|
+
<div v-else-if="fieldType === 'number'" class="flex items-center gap-2">
|
|
351
|
+
<UInputNumber
|
|
352
|
+
:model-value="getNum(metaKey)"
|
|
353
|
+
:min="sliderMin"
|
|
354
|
+
:max="sliderMax"
|
|
355
|
+
:step="sliderStep"
|
|
356
|
+
class="flex-1"
|
|
357
|
+
size="xs"
|
|
358
|
+
@update:model-value="(v) => patch({ [metaKey]: v })"
|
|
359
|
+
/>
|
|
360
|
+
<span v-if="unit" class="text-xs text-muted">{{ unit }}</span>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<!-- ── Rating ─────────────────────────────────────────────────── -->
|
|
364
|
+
<div v-else-if="fieldType === 'rating'" class="flex gap-1">
|
|
365
|
+
<button
|
|
366
|
+
v-for="i in sliderMax"
|
|
367
|
+
:key="i"
|
|
368
|
+
class="p-0.5 hover:scale-110 transition-transform"
|
|
369
|
+
@click="patch({ [metaKey]: i === getNum(metaKey) ? 0 : i })"
|
|
370
|
+
>
|
|
371
|
+
<UIcon
|
|
372
|
+
name="i-lucide-star"
|
|
373
|
+
:class="i <= getNum(metaKey) ? 'text-amber-400' : 'text-muted'"
|
|
374
|
+
class="size-5"
|
|
375
|
+
/>
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<!-- ── Select ─────────────────────────────────────────────────── -->
|
|
380
|
+
<div v-else-if="fieldType === 'select'" class="space-y-1">
|
|
381
|
+
<button
|
|
382
|
+
v-for="opt in options"
|
|
383
|
+
:key="opt"
|
|
384
|
+
class="flex items-center gap-2 w-full px-2 py-1 rounded text-xs hover:bg-muted text-left"
|
|
385
|
+
:class="getStr(metaKey) === opt ? 'bg-primary/10 text-primary' : ''"
|
|
386
|
+
@click="toggleSelect(opt)"
|
|
387
|
+
>
|
|
388
|
+
<UIcon v-if="getStr(metaKey) === opt" name="i-lucide-check" class="size-3" />
|
|
389
|
+
<span :class="getStr(metaKey) === opt ? '' : 'ml-5'">{{ opt }}</span>
|
|
390
|
+
</button>
|
|
391
|
+
<div v-if="!options.length" class="text-xs text-muted px-2">No options yet</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<!-- ── Multi select ───────────────────────────────────────────── -->
|
|
395
|
+
<div v-else-if="fieldType === 'multiselect'" class="space-y-1">
|
|
396
|
+
<button
|
|
397
|
+
v-for="opt in options"
|
|
398
|
+
:key="opt"
|
|
399
|
+
class="flex items-center gap-2 w-full px-2 py-1 rounded text-xs hover:bg-muted text-left"
|
|
400
|
+
:class="getStrArr(metaKey).includes(opt) ? 'bg-primary/10 text-primary' : ''"
|
|
401
|
+
@click="toggleMultiSelect(opt)"
|
|
402
|
+
>
|
|
403
|
+
<UIcon :name="getStrArr(metaKey).includes(opt) ? 'i-lucide-check-square' : 'i-lucide-square'" class="size-3" />
|
|
404
|
+
{{ opt }}
|
|
405
|
+
</button>
|
|
406
|
+
<div v-if="!options.length" class="text-xs text-muted px-2">No options yet</div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<!-- ── Tags ───────────────────────────────────────────────────── -->
|
|
410
|
+
<div v-else-if="fieldType === 'tags'" class="space-y-2">
|
|
411
|
+
<div class="flex flex-wrap gap-1">
|
|
412
|
+
<span
|
|
413
|
+
v-for="tag in getStrArr(metaKey)"
|
|
414
|
+
:key="tag"
|
|
415
|
+
class="inline-flex items-center gap-0.5 rounded-full bg-primary/10 text-primary px-2 py-0.5 text-xs"
|
|
416
|
+
>
|
|
417
|
+
{{ tag }}
|
|
418
|
+
<button @click="toggleTag(tag)"><UIcon name="i-lucide-x" class="size-3" /></button>
|
|
419
|
+
</span>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="flex gap-1">
|
|
422
|
+
<UInput v-model="newOption" size="xs" placeholder="Add tag…" class="flex-1" @keydown.enter.prevent="() => {
|
|
423
|
+
if (newOption.trim()) {
|
|
424
|
+
toggleTag(newOption.trim());
|
|
425
|
+
newOption = '';
|
|
426
|
+
}
|
|
427
|
+
}" />
|
|
428
|
+
<UButton size="xs" icon="i-lucide-plus" @click="() => {
|
|
429
|
+
if (newOption.trim()) {
|
|
430
|
+
toggleTag(newOption.trim());
|
|
431
|
+
newOption = '';
|
|
432
|
+
}
|
|
433
|
+
}" />
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<!-- ── URL ────────────────────────────────────────────────────── -->
|
|
438
|
+
<div v-else-if="fieldType === 'url'" class="space-y-1">
|
|
439
|
+
<UInput
|
|
440
|
+
:model-value="getStr(metaKey)"
|
|
441
|
+
size="xs"
|
|
442
|
+
placeholder="https://…"
|
|
443
|
+
leading-icon="i-lucide-link"
|
|
444
|
+
@update:model-value="(v) => patch({ [metaKey]: v })"
|
|
445
|
+
/>
|
|
446
|
+
<a v-if="getStr(metaKey)" :href="getStr(metaKey)" target="_blank" class="text-xs text-primary hover:underline flex items-center gap-1">
|
|
447
|
+
<UIcon name="i-lucide-external-link" class="size-3" />
|
|
448
|
+
Open
|
|
449
|
+
</a>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<!-- ── Textarea ───────────────────────────────────────────────── -->
|
|
453
|
+
<div v-else-if="fieldType === 'textarea'">
|
|
454
|
+
<UTextarea
|
|
455
|
+
:model-value="getStr(metaKey)"
|
|
456
|
+
size="xs"
|
|
457
|
+
:rows="3"
|
|
458
|
+
placeholder="Add text…"
|
|
459
|
+
@update:model-value="(v) => patch({ [metaKey]: v })"
|
|
460
|
+
/>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<!-- ── Location ───────────────────────────────────────────────── -->
|
|
464
|
+
<div v-else-if="fieldType === 'location'" class="space-y-1">
|
|
465
|
+
<div class="flex items-center gap-1">
|
|
466
|
+
<span class="text-xs text-muted w-7">Lat</span>
|
|
467
|
+
<UInputNumber :model-value="getNum(props.node.attrs.latKey)" size="xs" class="flex-1" :step="1e-4" @update:model-value="(v) => patch({ [props.node.attrs.latKey]: v })" />
|
|
468
|
+
</div>
|
|
469
|
+
<div class="flex items-center gap-1">
|
|
470
|
+
<span class="text-xs text-muted w-7">Lng</span>
|
|
471
|
+
<UInputNumber :model-value="getNum(props.node.attrs.lngKey)" size="xs" class="flex-1" :step="1e-4" @update:model-value="(v) => patch({ [props.node.attrs.lngKey]: v })" />
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<!-- ── Icon ───────────────────────────────────────────────────── -->
|
|
476
|
+
<div v-else-if="fieldType === 'icon'" class="space-y-1">
|
|
477
|
+
<AIconPicker
|
|
478
|
+
:model-value="getStr(metaKey)"
|
|
479
|
+
@update:model-value="(v) => patch({ [metaKey]: v })"
|
|
480
|
+
/>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
<!-- Fallback -->
|
|
484
|
+
<div v-else class="text-xs text-muted">{{ fieldType }}</div>
|
|
485
|
+
</div>
|
|
486
|
+
</template>
|
|
487
|
+
</UPopover>
|
|
488
|
+
</NodeViewWrapper>
|
|
489
|
+
</template>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { NodeViewProps } from '@tiptap/vue-3';
|
|
2
|
+
declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
3
|
+
declare const _default: typeof __VLS_export;
|
|
4
|
+
export default _default;
|
|
@@ -34,6 +34,57 @@ import {
|
|
|
34
34
|
_initFileIndex,
|
|
35
35
|
_destroyFileIndex
|
|
36
36
|
} from "./composables/useFileIndex.js";
|
|
37
|
+
const NUXT_UI_PRIMARY_COLORS = /* @__PURE__ */ new Set([
|
|
38
|
+
"red",
|
|
39
|
+
"orange",
|
|
40
|
+
"amber",
|
|
41
|
+
"yellow",
|
|
42
|
+
"lime",
|
|
43
|
+
"green",
|
|
44
|
+
"emerald",
|
|
45
|
+
"teal",
|
|
46
|
+
"cyan",
|
|
47
|
+
"sky",
|
|
48
|
+
"blue",
|
|
49
|
+
"indigo",
|
|
50
|
+
"violet",
|
|
51
|
+
"purple",
|
|
52
|
+
"fuchsia",
|
|
53
|
+
"pink",
|
|
54
|
+
"rose"
|
|
55
|
+
]);
|
|
56
|
+
const NUXT_UI_NEUTRAL_COLORS = /* @__PURE__ */ new Set(["slate", "gray", "zinc", "neutral", "stone"]);
|
|
57
|
+
const CUSTOM_TO_NUXT_UI_PRIMARY = {
|
|
58
|
+
grass: "green",
|
|
59
|
+
diamond: "cyan",
|
|
60
|
+
gold: "amber",
|
|
61
|
+
redstone: "red",
|
|
62
|
+
lapis: "blue",
|
|
63
|
+
wood: "orange",
|
|
64
|
+
discord: "indigo",
|
|
65
|
+
steam: "lime",
|
|
66
|
+
oxidized: "teal"
|
|
67
|
+
};
|
|
68
|
+
const CUSTOM_TO_NUXT_UI_NEUTRAL = {
|
|
69
|
+
cobblestone: "stone",
|
|
70
|
+
bedrock: "zinc",
|
|
71
|
+
cream: "stone",
|
|
72
|
+
sage: "slate",
|
|
73
|
+
lavender: "slate",
|
|
74
|
+
blush: "gray",
|
|
75
|
+
copper: "stone",
|
|
76
|
+
oxidized: "slate",
|
|
77
|
+
mint: "slate",
|
|
78
|
+
peach: "stone",
|
|
79
|
+
mist: "gray",
|
|
80
|
+
mauve: "gray"
|
|
81
|
+
};
|
|
82
|
+
function toNuxtUIPrimary(name) {
|
|
83
|
+
return NUXT_UI_PRIMARY_COLORS.has(name) ? name : CUSTOM_TO_NUXT_UI_PRIMARY[name] ?? "blue";
|
|
84
|
+
}
|
|
85
|
+
function toNuxtUINeutral(name) {
|
|
86
|
+
return NUXT_UI_NEUTRAL_COLORS.has(name) ? name : CUSTOM_TO_NUXT_UI_NEUTRAL[name] ?? "zinc";
|
|
87
|
+
}
|
|
37
88
|
const STORAGE_KEY_EXTERNAL_PLUGINS = "abracadabra_external_plugins";
|
|
38
89
|
const STORAGE_KEY_DISABLED_BUILTINS = "abracadabra_disabled_builtins";
|
|
39
90
|
const CLAIMED_FLAG_KEY = "abracadabra_was_claimed";
|
|
@@ -264,14 +315,14 @@ export default defineNuxtPlugin({
|
|
|
264
315
|
userColorName.value = colorName;
|
|
265
316
|
localStorage.setItem("abracadabra_usercolor", colorName);
|
|
266
317
|
const appConfig = useAppConfig();
|
|
267
|
-
if (appConfig.ui?.colors) appConfig.ui.colors.primary = colorName;
|
|
318
|
+
if (appConfig.ui?.colors) appConfig.ui.colors.primary = toNuxtUIPrimary(colorName);
|
|
268
319
|
provider.value?.setAwarenessField("user", { name: userName.value, color: hsl, publicKey: publicKeyB64.value });
|
|
269
320
|
}
|
|
270
321
|
function setNeutralColor(colorName) {
|
|
271
322
|
userNeutralColorName.value = colorName;
|
|
272
323
|
localStorage.setItem("abracadabra_neutralcolor", colorName);
|
|
273
324
|
const appConfig = useAppConfig();
|
|
274
|
-
if (appConfig.ui?.colors) appConfig.ui.colors.neutral = colorName;
|
|
325
|
+
if (appConfig.ui?.colors) appConfig.ui.colors.neutral = toNuxtUINeutral(colorName);
|
|
275
326
|
}
|
|
276
327
|
function setRequirePasskey(enabled) {
|
|
277
328
|
requirePasskeyOnLogin.value = enabled;
|
|
@@ -544,8 +595,8 @@ export default defineNuxtPlugin({
|
|
|
544
595
|
try {
|
|
545
596
|
const appConfig = useAppConfig();
|
|
546
597
|
if (appConfig.ui?.colors) {
|
|
547
|
-
appConfig.ui.colors.primary = userColorName.value;
|
|
548
|
-
appConfig.ui.colors.neutral = userNeutralColorName.value;
|
|
598
|
+
appConfig.ui.colors.primary = toNuxtUIPrimary(userColorName.value);
|
|
599
|
+
appConfig.ui.colors.neutral = toNuxtUINeutral(userNeutralColorName.value);
|
|
549
600
|
}
|
|
550
601
|
} catch {
|
|
551
602
|
}
|
|
@@ -32,7 +32,8 @@ async function loadClientExtensions() {
|
|
|
32
32
|
{ Badge },
|
|
33
33
|
{ Kbd },
|
|
34
34
|
{ ProseIcon },
|
|
35
|
-
{ FileBlock }
|
|
35
|
+
{ FileBlock },
|
|
36
|
+
{ MetaField }
|
|
36
37
|
] = await Promise.all([
|
|
37
38
|
import("@tiptap/extension-task-list"),
|
|
38
39
|
import("@tiptap/extension-task-item"),
|
|
@@ -65,7 +66,8 @@ async function loadClientExtensions() {
|
|
|
65
66
|
import("../extensions/badge.js"),
|
|
66
67
|
import("../extensions/kbd.js"),
|
|
67
68
|
import("../extensions/prose-icon.js"),
|
|
68
|
-
import("../extensions/file-block.js")
|
|
69
|
+
import("../extensions/file-block.js"),
|
|
70
|
+
import("../extensions/meta-field.js")
|
|
69
71
|
]);
|
|
70
72
|
const lowlight = createLowlight(common);
|
|
71
73
|
return [
|
|
@@ -108,7 +110,9 @@ async function loadClientExtensions() {
|
|
|
108
110
|
Kbd,
|
|
109
111
|
ProseIcon,
|
|
110
112
|
// File block
|
|
111
|
-
FileBlock
|
|
113
|
+
FileBlock,
|
|
114
|
+
// Meta field chips (inline properties in documentMeta)
|
|
115
|
+
MetaField
|
|
112
116
|
];
|
|
113
117
|
}
|
|
114
118
|
async function loadServerExtensions() {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default:
|
|
1
|
+
declare const _default: import("nitropack").NitroAppPlugin;
|
|
2
2
|
export default _default;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { defineNitroPlugin } from "nitropack/runtime/plugin";
|
|
2
|
+
import { useRuntimeConfig } from "nitropack/runtime/config";
|
|
3
|
+
import { useStorage } from "nitropack/runtime/storage";
|
|
1
4
|
import { registerServerPlugin, bootRunners, shutdownAllRunners } from "../utils/serverRunner.js";
|
|
2
5
|
import { createDocCacheAPI } from "../utils/docCache.js";
|
|
3
6
|
import { docTreeCacheRunner } from "../runners/doc-tree-cache.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const DOC_DRAG_MIME = "application/x-abracadabra-doc";
|
|
2
|
+
export interface DocDragPayload {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
/** Check if a drag event carries a doc drag payload (works during dragover) */
|
|
7
|
+
export declare function isDocDrag(e: DragEvent): boolean;
|
|
8
|
+
/** Read the doc drag payload (only works during drop — getData is blocked during dragover) */
|
|
9
|
+
export declare function parseDocDragPayload(e: DragEvent): DocDragPayload | null;
|
|
10
|
+
/** Walk tree-map data to check if `nodeId` is a descendant of `ancestorId` */
|
|
11
|
+
export declare function isDescendantInMap(data: Record<string, {
|
|
12
|
+
parentId: string | null;
|
|
13
|
+
}>, ancestorId: string, nodeId: string): boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const DOC_DRAG_MIME = "application/x-abracadabra-doc";
|
|
2
|
+
export function isDocDrag(e) {
|
|
3
|
+
return !!e.dataTransfer?.types.includes(DOC_DRAG_MIME);
|
|
4
|
+
}
|
|
5
|
+
export function parseDocDragPayload(e) {
|
|
6
|
+
const raw = e.dataTransfer?.getData(DOC_DRAG_MIME);
|
|
7
|
+
if (!raw) return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isDescendantInMap(data, ancestorId, nodeId) {
|
|
15
|
+
let current = nodeId;
|
|
16
|
+
const visited = /* @__PURE__ */ new Set();
|
|
17
|
+
while (current) {
|
|
18
|
+
if (current === ancestorId) return true;
|
|
19
|
+
if (visited.has(current)) break;
|
|
20
|
+
visited.add(current);
|
|
21
|
+
const entry = data[current];
|
|
22
|
+
if (!entry?.parentId) break;
|
|
23
|
+
current = entry.parentId;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|