@abraca/nuxt 0.1.0 → 0.2.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.
@@ -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: any;
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
+ }