@abraca/nuxt 2.10.0 → 2.11.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 +14 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/assets/editor.css +1 -1
- package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
- package/dist/runtime/components/AConnectionBadge.vue +79 -0
- package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
- package/dist/runtime/components/AEditor.d.vue.ts +2 -2
- package/dist/runtime/components/AEditor.vue +11 -1
- package/dist/runtime/components/AEditor.vue.d.ts +2 -2
- package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
- package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
- package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
- package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
- package/dist/runtime/components/AModalShell.vue +105 -0
- package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
- package/dist/runtime/components/ANodePanel.d.vue.ts +8 -6
- package/dist/runtime/components/ANodePanel.vue +25 -0
- package/dist/runtime/components/ANodePanel.vue.d.ts +8 -6
- package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
- package/dist/runtime/components/ANodePanelHeader.vue +17 -3
- package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
- package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
- package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
- package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
- package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
- package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
- package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
- package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +2 -2
- package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +2 -2
- package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
- package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
- package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
- package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
- package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
- package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
- package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
- package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
- package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
- package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
- package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
- package/dist/runtime/components/shell/AUserMenu.d.vue.ts +2 -2
- package/dist/runtime/components/shell/AUserMenu.vue.d.ts +2 -2
- package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
- package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
- package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
- package/dist/runtime/composables/useDocSnapshots.js +5 -0
- package/dist/runtime/composables/useEditor.d.ts +1 -1
- package/dist/runtime/composables/useEditor.js +120 -0
- package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
- package/dist/runtime/composables/useEditorToolbar.js +78 -56
- package/dist/runtime/composables/useNodeContextMenu.d.ts +10 -0
- package/dist/runtime/composables/useNodeContextMenu.js +41 -1
- package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
- package/dist/runtime/composables/useSwipeGesture.js +140 -0
- package/dist/runtime/extensions/document-header.js +16 -6
- package/dist/runtime/extensions/document-meta.js +344 -19
- package/dist/runtime/extensions/meta-field.js +42 -0
- package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
- package/dist/runtime/extensions/views/FieldView.vue +51 -19
- package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
- package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
- package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
- package/dist/runtime/plugin-abracadabra.client.js +12 -2
- package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
- package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
- package/package.json +1 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref, shallowRef, watch, onUnmounted } from "vue";
|
|
2
|
+
import { Extension } from "@tiptap/core";
|
|
2
3
|
import { XmlElement } from "yjs";
|
|
3
4
|
import { useNuxtApp } from "#imports";
|
|
4
5
|
export function useEditor(options) {
|
|
@@ -7,10 +8,21 @@ export function useEditor(options) {
|
|
|
7
8
|
const connectedUsers = ref([]);
|
|
8
9
|
const extensions = shallowRef([]);
|
|
9
10
|
const ready = ref(false);
|
|
11
|
+
const activeColors = ref(/* @__PURE__ */ new Set());
|
|
12
|
+
const colorTimeouts = /* @__PURE__ */ new Map();
|
|
13
|
+
const CARET_ACTIVITY_TIMEOUT = 3e3;
|
|
10
14
|
let cleanup = null;
|
|
15
|
+
let cursorBroadcastTimer = null;
|
|
11
16
|
watch(childProvider, async (prov) => {
|
|
12
17
|
cleanup?.();
|
|
13
18
|
cleanup = null;
|
|
19
|
+
if (cursorBroadcastTimer) {
|
|
20
|
+
clearTimeout(cursorBroadcastTimer);
|
|
21
|
+
cursorBroadcastTimer = null;
|
|
22
|
+
}
|
|
23
|
+
colorTimeouts.forEach((t) => clearTimeout(t));
|
|
24
|
+
colorTimeouts.clear();
|
|
25
|
+
activeColors.value = /* @__PURE__ */ new Set();
|
|
14
26
|
if (!prov) {
|
|
15
27
|
ready.value = false;
|
|
16
28
|
extensions.value = [];
|
|
@@ -34,6 +46,23 @@ export function useEditor(options) {
|
|
|
34
46
|
CollaborationCaret.configure({
|
|
35
47
|
provider: prov,
|
|
36
48
|
user,
|
|
49
|
+
// Custom caret element: a thin coloured bar with a floating name label.
|
|
50
|
+
// Visibility is activity-gated — the `.is-active` class (added here when
|
|
51
|
+
// the author is in activeColors, and toggled live by the watcher below)
|
|
52
|
+
// is what makes the caret visible (editor.css defaults it to opacity:0).
|
|
53
|
+
render: (u) => {
|
|
54
|
+
const cursor = document.createElement("span");
|
|
55
|
+
cursor.classList.add("collaboration-carets__caret");
|
|
56
|
+
cursor.setAttribute("style", `border-color: ${u.color}`);
|
|
57
|
+
cursor.dataset.userColor = u.color;
|
|
58
|
+
const label = document.createElement("div");
|
|
59
|
+
label.classList.add("collaboration-carets__label");
|
|
60
|
+
label.setAttribute("style", `background-color: ${u.color}`);
|
|
61
|
+
label.insertBefore(document.createTextNode(u.name), null);
|
|
62
|
+
cursor.insertBefore(label, null);
|
|
63
|
+
if (activeColors.value.has(u.color)) cursor.classList.add("is-active");
|
|
64
|
+
return cursor;
|
|
65
|
+
},
|
|
37
66
|
selectionRender: (u) => {
|
|
38
67
|
const color = u.color || "#999";
|
|
39
68
|
const selectionColor = color.startsWith("hsl") ? color.replace("hsl", "hsla").replace(")", ", 0.4)") : `${color}66`;
|
|
@@ -43,6 +72,20 @@ export function useEditor(options) {
|
|
|
43
72
|
};
|
|
44
73
|
}
|
|
45
74
|
}),
|
|
75
|
+
// Broadcast the local cursor position to child awareness (throttled,
|
|
76
|
+
// 150ms trailing) so presence/follow consumers can track the caret
|
|
77
|
+
// without coupling to the CollaborationCaret extension internals.
|
|
78
|
+
Extension.create({
|
|
79
|
+
name: "cursor-position-broadcast",
|
|
80
|
+
onSelectionUpdate() {
|
|
81
|
+
const { from, to } = this.editor.state.selection;
|
|
82
|
+
if (cursorBroadcastTimer) clearTimeout(cursorBroadcastTimer);
|
|
83
|
+
cursorBroadcastTimer = setTimeout(() => {
|
|
84
|
+
cursorBroadcastTimer = null;
|
|
85
|
+
prov.awareness?.setLocalStateField("cursorPos", { from, to });
|
|
86
|
+
}, 150);
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
46
89
|
...extraExtensions
|
|
47
90
|
];
|
|
48
91
|
const ydoc = prov.document;
|
|
@@ -92,6 +135,13 @@ export function useEditor(options) {
|
|
|
92
135
|
dedupeDocumentMeta();
|
|
93
136
|
ensureDocumentMeta();
|
|
94
137
|
}
|
|
138
|
+
const onProviderSynced = () => {
|
|
139
|
+
dedupeHeaders();
|
|
140
|
+
dedupeDocumentMeta();
|
|
141
|
+
ensureDocumentMeta();
|
|
142
|
+
};
|
|
143
|
+
prov.on?.("synced", onProviderSynced);
|
|
144
|
+
if (prov.isSynced) Promise.resolve().then(onProviderSynced);
|
|
95
145
|
ready.value = true;
|
|
96
146
|
const updateUsers = () => {
|
|
97
147
|
const states = prov.awareness?.getStates() ?? /* @__PURE__ */ new Map();
|
|
@@ -104,14 +154,84 @@ export function useEditor(options) {
|
|
|
104
154
|
});
|
|
105
155
|
connectedUsers.value = users;
|
|
106
156
|
};
|
|
157
|
+
const markActive = (color) => {
|
|
158
|
+
activeColors.value.add(color);
|
|
159
|
+
activeColors.value = new Set(activeColors.value);
|
|
160
|
+
if (colorTimeouts.has(color)) clearTimeout(colorTimeouts.get(color));
|
|
161
|
+
colorTimeouts.set(color, setTimeout(() => {
|
|
162
|
+
activeColors.value.delete(color);
|
|
163
|
+
activeColors.value = new Set(activeColors.value);
|
|
164
|
+
colorTimeouts.delete(color);
|
|
165
|
+
}, CARET_ACTIVITY_TIMEOUT));
|
|
166
|
+
};
|
|
167
|
+
const onDocUpdate = (_update, _origin, _doc, transaction) => {
|
|
168
|
+
const beforeState = transaction.beforeState;
|
|
169
|
+
const afterState = transaction.afterState;
|
|
170
|
+
if (!beforeState || !afterState) return;
|
|
171
|
+
const localClientId = ydoc.clientID;
|
|
172
|
+
const states = prov.awareness?.getStates() ?? /* @__PURE__ */ new Map();
|
|
173
|
+
for (const [clientId, clock] of afterState) {
|
|
174
|
+
if (clientId === localClientId) continue;
|
|
175
|
+
const beforeClock = beforeState.get(clientId) ?? 0;
|
|
176
|
+
if (clock > beforeClock) {
|
|
177
|
+
const state = states.get(clientId);
|
|
178
|
+
const color = state?.user?.color;
|
|
179
|
+
if (color) markActive(color);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
ydoc?.on?.("update", onDocUpdate);
|
|
107
184
|
prov.awareness?.on("change", updateUsers);
|
|
108
185
|
updateUsers();
|
|
186
|
+
const fadeOutTimeouts = /* @__PURE__ */ new Map();
|
|
187
|
+
let caretSyncTimer = null;
|
|
188
|
+
const stopActiveWatch = watch(activeColors, (colors) => {
|
|
189
|
+
if (caretSyncTimer) clearTimeout(caretSyncTimer);
|
|
190
|
+
caretSyncTimer = setTimeout(() => {
|
|
191
|
+
caretSyncTimer = null;
|
|
192
|
+
const carets = document.querySelectorAll(".collaboration-carets__caret[data-user-color]");
|
|
193
|
+
carets.forEach((el) => {
|
|
194
|
+
const color = el.dataset.userColor;
|
|
195
|
+
if (!color) return;
|
|
196
|
+
if (colors.has(color)) {
|
|
197
|
+
if (fadeOutTimeouts.has(color)) {
|
|
198
|
+
clearTimeout(fadeOutTimeouts.get(color));
|
|
199
|
+
fadeOutTimeouts.delete(color);
|
|
200
|
+
}
|
|
201
|
+
el.classList.remove("is-hidden");
|
|
202
|
+
requestAnimationFrame(() => el.classList.add("is-active"));
|
|
203
|
+
} else {
|
|
204
|
+
el.classList.remove("is-active");
|
|
205
|
+
if (!fadeOutTimeouts.has(color)) {
|
|
206
|
+
fadeOutTimeouts.set(color, setTimeout(() => {
|
|
207
|
+
el.classList.add("is-hidden");
|
|
208
|
+
fadeOutTimeouts.delete(color);
|
|
209
|
+
}, 300));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}, 100);
|
|
214
|
+
});
|
|
109
215
|
cleanup = () => {
|
|
216
|
+
ydoc?.off?.("update", onDocUpdate);
|
|
217
|
+
prov.off?.("synced", onProviderSynced);
|
|
110
218
|
prov.awareness?.off("change", updateUsers);
|
|
219
|
+
stopActiveWatch();
|
|
220
|
+
if (caretSyncTimer) {
|
|
221
|
+
clearTimeout(caretSyncTimer);
|
|
222
|
+
caretSyncTimer = null;
|
|
223
|
+
}
|
|
224
|
+
fadeOutTimeouts.forEach((t) => clearTimeout(t));
|
|
225
|
+
fadeOutTimeouts.clear();
|
|
111
226
|
if (fragment) {
|
|
112
227
|
fragment.unobserve(dedupeHeaders);
|
|
113
228
|
fragment.unobserve(dedupeDocumentMeta);
|
|
114
229
|
}
|
|
230
|
+
if (cursorBroadcastTimer) {
|
|
231
|
+
clearTimeout(cursorBroadcastTimer);
|
|
232
|
+
cursorBroadcastTimer = null;
|
|
233
|
+
}
|
|
234
|
+
prov.awareness?.setLocalStateField("cursorPos", null);
|
|
115
235
|
};
|
|
116
236
|
}, { immediate: true });
|
|
117
237
|
onUnmounted(() => cleanup?.());
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useEditorToolbar
|
|
3
3
|
*
|
|
4
|
-
* Returns pre-built toolbar item groups for UEditorToolbar,
|
|
5
|
-
*
|
|
4
|
+
* Returns pre-built toolbar item groups for UEditorToolbar, merging the plugin
|
|
5
|
+
* registry's contributions with standard formatting items.
|
|
6
|
+
*
|
|
7
|
+
* Three building blocks, all individually usable:
|
|
8
|
+
* - `items` full toolbar incl. undo/redo — for a docked/main toolbar.
|
|
9
|
+
* - `bubbleItems` selection bubble — same formatting groups WITHOUT undo/redo
|
|
10
|
+
* (undo/redo belongs in the panel header, not a text bubble).
|
|
11
|
+
* - `getTableToolbarItems(editor)` table-context actions (row/column/table ops).
|
|
6
12
|
*
|
|
7
13
|
* Usage:
|
|
8
|
-
* const {
|
|
9
|
-
* <UEditorToolbar :editor="editor" :items="
|
|
14
|
+
* const { bubbleItems } = useEditorToolbar({ editor, docId })
|
|
15
|
+
* <UEditorToolbar :editor="editor" :items="bubbleItems" layout="bubble" />
|
|
10
16
|
*/
|
|
11
17
|
import type { Editor } from '@tiptap/vue-3';
|
|
12
18
|
export interface UseEditorToolbarOptions {
|
|
@@ -19,4 +25,6 @@ export interface UseEditorToolbarOptions {
|
|
|
19
25
|
}
|
|
20
26
|
export declare function useEditorToolbar(options?: UseEditorToolbarOptions): {
|
|
21
27
|
items: import("vue").ComputedRef<any[][]>;
|
|
28
|
+
bubbleItems: import("vue").ComputedRef<any[][]>;
|
|
29
|
+
getTableToolbarItems: (editor: Editor) => any[][];
|
|
22
30
|
};
|
|
@@ -1,72 +1,94 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
2
|
import { useAbracadabra } from "./useAbracadabra.js";
|
|
3
3
|
import { usePluginRegistry } from "./usePluginRegistry.js";
|
|
4
|
+
const UNDO_REDO_GROUP = [
|
|
5
|
+
{ kind: "undo", icon: "i-lucide-undo", tooltip: { text: "Undo" } },
|
|
6
|
+
{ kind: "redo", icon: "i-lucide-redo", tooltip: { text: "Redo" } }
|
|
7
|
+
];
|
|
8
|
+
const TURN_INTO_GROUP = [
|
|
9
|
+
{
|
|
10
|
+
label: "Turn into",
|
|
11
|
+
trailingIcon: "i-lucide-chevron-down",
|
|
12
|
+
activeColor: "neutral",
|
|
13
|
+
activeVariant: "ghost",
|
|
14
|
+
tooltip: { text: "Turn into" },
|
|
15
|
+
content: { align: "start" },
|
|
16
|
+
ui: { label: "text-xs" },
|
|
17
|
+
items: [
|
|
18
|
+
{ type: "label", label: "Turn into" },
|
|
19
|
+
{ kind: "paragraph", label: "Paragraph", icon: "i-lucide-type" },
|
|
20
|
+
{ kind: "heading", level: 1, label: "Heading 1", icon: "i-lucide-heading-1" },
|
|
21
|
+
{ kind: "heading", level: 2, label: "Heading 2", icon: "i-lucide-heading-2" },
|
|
22
|
+
{ kind: "heading", level: 3, label: "Heading 3", icon: "i-lucide-heading-3" },
|
|
23
|
+
{ kind: "bulletList", label: "Bullet List", icon: "i-lucide-list" },
|
|
24
|
+
{ kind: "orderedList", label: "Ordered List", icon: "i-lucide-list-ordered" },
|
|
25
|
+
{ kind: "taskList", label: "Task List", icon: "i-lucide-list-check" },
|
|
26
|
+
{ kind: "blockquote", label: "Blockquote", icon: "i-lucide-text-quote" },
|
|
27
|
+
{ kind: "codeBlock", label: "Code Block", icon: "i-lucide-square-code" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
];
|
|
31
|
+
const MARKS_GROUP = [
|
|
32
|
+
{ kind: "mark", mark: "bold", icon: "i-lucide-bold", tooltip: { text: "Bold" } },
|
|
33
|
+
{ kind: "mark", mark: "italic", icon: "i-lucide-italic", tooltip: { text: "Italic" } },
|
|
34
|
+
{ kind: "mark", mark: "underline", icon: "i-lucide-underline", tooltip: { text: "Underline" } },
|
|
35
|
+
{ kind: "mark", mark: "strike", icon: "i-lucide-strikethrough", tooltip: { text: "Strikethrough" } },
|
|
36
|
+
{ kind: "mark", mark: "code", icon: "i-lucide-code", tooltip: { text: "Inline code" } }
|
|
37
|
+
];
|
|
38
|
+
const SLOTS_GROUP = [
|
|
39
|
+
{ slot: "link" },
|
|
40
|
+
{ slot: "doc-link" },
|
|
41
|
+
{ slot: "create-child-doc" },
|
|
42
|
+
{ slot: "send-to-chat" }
|
|
43
|
+
];
|
|
4
44
|
export function useEditorToolbar(options = {}) {
|
|
5
45
|
const registry = usePluginRegistry();
|
|
6
46
|
const abra = useAbracadabra();
|
|
7
|
-
|
|
8
|
-
const editor = options.editor ?? null;
|
|
47
|
+
function pluginGroups() {
|
|
9
48
|
const ctx = {
|
|
10
|
-
editor,
|
|
49
|
+
editor: options.editor ?? null,
|
|
11
50
|
docId: options.docId,
|
|
12
51
|
abracadabra: abra
|
|
13
52
|
};
|
|
14
|
-
const
|
|
15
|
-
{ kind: "undo", icon: "i-lucide-undo", tooltip: { text: "Undo" } },
|
|
16
|
-
{ kind: "redo", icon: "i-lucide-redo", tooltip: { text: "Redo" } }
|
|
17
|
-
], [
|
|
18
|
-
{
|
|
19
|
-
label: "Turn into",
|
|
20
|
-
trailingIcon: "i-lucide-chevron-down",
|
|
21
|
-
activeColor: "neutral",
|
|
22
|
-
activeVariant: "ghost",
|
|
23
|
-
tooltip: { text: "Turn into" },
|
|
24
|
-
content: { align: "start" },
|
|
25
|
-
ui: { label: "text-xs" },
|
|
26
|
-
items: [
|
|
27
|
-
{ type: "label", label: "Turn into" },
|
|
28
|
-
{ kind: "paragraph", label: "Paragraph", icon: "i-lucide-type" },
|
|
29
|
-
{ kind: "heading", level: 1, label: "Heading 1", icon: "i-lucide-heading-1" },
|
|
30
|
-
{ kind: "heading", level: 2, label: "Heading 2", icon: "i-lucide-heading-2" },
|
|
31
|
-
{ kind: "heading", level: 3, label: "Heading 3", icon: "i-lucide-heading-3" },
|
|
32
|
-
{ kind: "bulletList", label: "Bullet List", icon: "i-lucide-list" },
|
|
33
|
-
{ kind: "orderedList", label: "Ordered List", icon: "i-lucide-list-ordered" },
|
|
34
|
-
{ kind: "taskList", label: "Task List", icon: "i-lucide-list-check" },
|
|
35
|
-
{ kind: "blockquote", label: "Blockquote", icon: "i-lucide-text-quote" },
|
|
36
|
-
{ kind: "codeBlock", label: "Code Block", icon: "i-lucide-square-code" }
|
|
37
|
-
]
|
|
38
|
-
}
|
|
39
|
-
], [
|
|
40
|
-
{ kind: "mark", mark: "bold", icon: "i-lucide-bold", tooltip: { text: "Bold" } },
|
|
41
|
-
{ kind: "mark", mark: "italic", icon: "i-lucide-italic", tooltip: { text: "Italic" } },
|
|
42
|
-
{ kind: "mark", mark: "underline", icon: "i-lucide-underline", tooltip: { text: "Underline" } },
|
|
43
|
-
{ kind: "mark", mark: "strike", icon: "i-lucide-strikethrough", tooltip: { text: "Strikethrough" } },
|
|
44
|
-
{ kind: "mark", mark: "code", icon: "i-lucide-code", tooltip: { text: "Inline code" } }
|
|
45
|
-
], [
|
|
46
|
-
// Slot-based items — UEditorToolbar renders whatever the consumer
|
|
47
|
-
// supplies via <template #link>, <template #doc-link>, etc. AEditor
|
|
48
|
-
// provides defaults for `link` and `doc-link` (the popover primitives);
|
|
49
|
-
// `create-child-doc` and `send-to-chat` render nothing unless the
|
|
50
|
-
// consuming app fills the slot.
|
|
51
|
-
{ slot: "link" },
|
|
52
|
-
{ slot: "doc-link" },
|
|
53
|
-
{ slot: "create-child-doc" },
|
|
54
|
-
{ slot: "send-to-chat" }
|
|
55
|
-
]];
|
|
53
|
+
const out = [];
|
|
56
54
|
try {
|
|
57
|
-
const
|
|
58
|
-
for (const group of
|
|
59
|
-
if (group.items?.length)
|
|
60
|
-
base.push(group.items);
|
|
61
|
-
}
|
|
55
|
+
const groups = registry.getAllToolbarItems(ctx);
|
|
56
|
+
for (const group of groups) {
|
|
57
|
+
if (group.items?.length) out.push(group.items);
|
|
62
58
|
}
|
|
63
59
|
} catch (e) {
|
|
64
60
|
if (import.meta.dev) console.warn("[abracadabra] toolbar: failed to load plugin toolbar items:", e);
|
|
65
61
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
const items = computed(() => [
|
|
65
|
+
UNDO_REDO_GROUP,
|
|
66
|
+
TURN_INTO_GROUP,
|
|
67
|
+
MARKS_GROUP,
|
|
68
|
+
SLOTS_GROUP,
|
|
69
|
+
...pluginGroups(),
|
|
70
|
+
...options.extraItems ?? []
|
|
71
|
+
]);
|
|
72
|
+
const bubbleItems = computed(() => [
|
|
73
|
+
TURN_INTO_GROUP,
|
|
74
|
+
MARKS_GROUP,
|
|
75
|
+
SLOTS_GROUP,
|
|
76
|
+
...pluginGroups(),
|
|
77
|
+
...options.extraItems ?? []
|
|
78
|
+
]);
|
|
79
|
+
function getTableToolbarItems(editor) {
|
|
80
|
+
const chain = () => editor.chain().focus();
|
|
81
|
+
return [[
|
|
82
|
+
{ icon: "i-lucide-between-vertical-start", tooltip: { text: "Add row above" }, onClick: () => chain().addRowBefore().run() },
|
|
83
|
+
{ icon: "i-lucide-between-vertical-end", tooltip: { text: "Add row below" }, onClick: () => chain().addRowAfter().run() },
|
|
84
|
+
{ icon: "i-lucide-between-horizontal-start", tooltip: { text: "Add column before" }, onClick: () => chain().addColumnBefore().run() },
|
|
85
|
+
{ icon: "i-lucide-between-horizontal-end", tooltip: { text: "Add column after" }, onClick: () => chain().addColumnAfter().run() }
|
|
86
|
+
], [
|
|
87
|
+
{ icon: "i-lucide-rows-3", tooltip: { text: "Delete row" }, onClick: () => chain().deleteRow().run() },
|
|
88
|
+
{ icon: "i-lucide-columns-3", tooltip: { text: "Delete column" }, onClick: () => chain().deleteColumn().run() }
|
|
89
|
+
], [
|
|
90
|
+
{ icon: "i-lucide-trash", tooltip: { text: "Delete table" }, onClick: () => chain().deleteTable().run() }
|
|
91
|
+
]];
|
|
92
|
+
}
|
|
93
|
+
return { items, bubbleItems, getTableToolbarItems };
|
|
72
94
|
}
|
|
@@ -14,6 +14,16 @@ interface NodeContextMenuOptions {
|
|
|
14
14
|
onNavigate?: (id: string) => void;
|
|
15
15
|
onCopyLink?: (id: string) => void;
|
|
16
16
|
onChangeType?: (id: string, newType: string) => void;
|
|
17
|
+
/** Open the doc — renders an "Open" item (e.g. navigate to the full page). */
|
|
18
|
+
onOpen?: (id: string) => void;
|
|
19
|
+
/** Open the doc in a floating window — renders "Open as window". */
|
|
20
|
+
onOpenInWindow?: (id: string) => void;
|
|
21
|
+
/** Create a child page under this node — renders "Add child page". */
|
|
22
|
+
onAddChild?: (parentId: string) => void;
|
|
23
|
+
/** Export the doc — renders an "Export" submenu (Markdown / HTML). Wire to
|
|
24
|
+
* `useDocExport().exportDoc` (kept out of this composable so it stays
|
|
25
|
+
* decoupled from the export pipeline + its lazy jszip import). */
|
|
26
|
+
onExport?: (id: string, format: 'markdown' | 'html') => void;
|
|
17
27
|
favorites?: {
|
|
18
28
|
isFavorite: (id: string) => boolean;
|
|
19
29
|
toggleFavorite: (id: string) => void;
|
|
@@ -3,6 +3,25 @@ import { UI_COLORS } from "../types.js";
|
|
|
3
3
|
export function useNodeContextMenu(opts) {
|
|
4
4
|
const { nodeId, label: _label, type, tree, registry } = opts;
|
|
5
5
|
const docTypes = getAvailableDocTypes(registry);
|
|
6
|
+
const openGroup = [];
|
|
7
|
+
if (opts.onOpen) {
|
|
8
|
+
openGroup.push({
|
|
9
|
+
label: "Open",
|
|
10
|
+
icon: "i-lucide-square-arrow-out-up-right",
|
|
11
|
+
onSelect() {
|
|
12
|
+
opts.onOpen(nodeId);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (opts.onOpenInWindow) {
|
|
17
|
+
openGroup.push({
|
|
18
|
+
label: "Open as window",
|
|
19
|
+
icon: "i-lucide-app-window",
|
|
20
|
+
onSelect() {
|
|
21
|
+
opts.onOpenInWindow(nodeId);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
6
25
|
const editGroup = [
|
|
7
26
|
{
|
|
8
27
|
label: "Rename",
|
|
@@ -39,6 +58,15 @@ export function useNodeContextMenu(opts) {
|
|
|
39
58
|
}))
|
|
40
59
|
}
|
|
41
60
|
];
|
|
61
|
+
if (opts.onAddChild) {
|
|
62
|
+
editGroup.push({
|
|
63
|
+
label: "Add child page",
|
|
64
|
+
icon: "i-lucide-file-plus",
|
|
65
|
+
onSelect() {
|
|
66
|
+
opts.onAddChild(nodeId);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
42
70
|
const colorItems = UI_COLORS.slice(0, 12).map((colorName) => ({
|
|
43
71
|
label: colorName.charAt(0).toUpperCase() + colorName.slice(1),
|
|
44
72
|
icon: "i-lucide-circle",
|
|
@@ -82,6 +110,16 @@ export function useNodeContextMenu(opts) {
|
|
|
82
110
|
}
|
|
83
111
|
});
|
|
84
112
|
}
|
|
113
|
+
if (opts.onExport) {
|
|
114
|
+
actionsGroup.push({
|
|
115
|
+
label: "Export",
|
|
116
|
+
icon: "i-lucide-download",
|
|
117
|
+
children: [
|
|
118
|
+
{ label: "Markdown", icon: "i-lucide-file-text", onSelect: () => opts.onExport(nodeId, "markdown") },
|
|
119
|
+
{ label: "HTML", icon: "i-lucide-file-code", onSelect: () => opts.onExport(nodeId, "html") }
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
85
123
|
const dangerGroup = [
|
|
86
124
|
{
|
|
87
125
|
label: "Move to trash",
|
|
@@ -92,7 +130,9 @@ export function useNodeContextMenu(opts) {
|
|
|
92
130
|
}
|
|
93
131
|
}
|
|
94
132
|
];
|
|
95
|
-
const items = [
|
|
133
|
+
const items = [];
|
|
134
|
+
if (openGroup.length > 0) items.push(openGroup);
|
|
135
|
+
items.push(editGroup, appearanceGroup);
|
|
96
136
|
if (actionsGroup.length > 0) items.push(actionsGroup);
|
|
97
137
|
items.push(dangerGroup);
|
|
98
138
|
return { items };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type MaybeRefOrGetter, type Ref } from 'vue';
|
|
2
|
+
export interface SwipeGestureOptions {
|
|
3
|
+
/** Element to attach listeners to */
|
|
4
|
+
el: Ref<HTMLElement | null>;
|
|
5
|
+
/** Callback when user swipes left (navigate forward) */
|
|
6
|
+
onSwipeLeft?: () => void;
|
|
7
|
+
/** Callback when user swipes right (navigate back) */
|
|
8
|
+
onSwipeRight?: () => void;
|
|
9
|
+
/** Callback when user swipes up */
|
|
10
|
+
onSwipeUp?: () => void;
|
|
11
|
+
/** Callback when user swipes down */
|
|
12
|
+
onSwipeDown?: () => void;
|
|
13
|
+
/** Whether swiping left is currently valid (affects damping) */
|
|
14
|
+
canSwipeLeft?: MaybeRefOrGetter<boolean>;
|
|
15
|
+
/** Whether swiping right is currently valid (affects damping) */
|
|
16
|
+
canSwipeRight?: MaybeRefOrGetter<boolean>;
|
|
17
|
+
/** Whether swiping up is currently valid */
|
|
18
|
+
canSwipeUp?: MaybeRefOrGetter<boolean>;
|
|
19
|
+
/** Whether swiping down is currently valid */
|
|
20
|
+
canSwipeDown?: MaybeRefOrGetter<boolean>;
|
|
21
|
+
/** Disable swipe detection entirely */
|
|
22
|
+
disabled?: MaybeRefOrGetter<boolean>;
|
|
23
|
+
/** Restrict to a single axis. Gestures on the other axis pass through. */
|
|
24
|
+
axis?: 'x' | 'y';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Swipe gesture detection for both touch (mobile) and trackpad/wheel (desktop).
|
|
28
|
+
*
|
|
29
|
+
* Touch: locks axis after 10px, applies damped offset, fires on touchend.
|
|
30
|
+
* Wheel: accumulates deltaX/deltaY, fires after threshold, debounce resets.
|
|
31
|
+
*
|
|
32
|
+
* A child with `data-swipe-ignore` lets its own touch-scroll pass through.
|
|
33
|
+
*
|
|
34
|
+
* Ported 1:1 from cou-sh/app/composables/useSwipeGesture.ts.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useSwipeGesture(options: SwipeGestureOptions): {
|
|
37
|
+
swipeOffset: Ref<{
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
}, {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
} | {
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
}>;
|
|
47
|
+
isSwiping: Ref<boolean, boolean>;
|
|
48
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ref, toValue, watchEffect } from "vue";
|
|
2
|
+
export function useSwipeGesture(options) {
|
|
3
|
+
const swipeOffset = ref({ x: 0, y: 0 });
|
|
4
|
+
const isSwiping = ref(false);
|
|
5
|
+
let touchStartX = 0;
|
|
6
|
+
let touchStartY = 0;
|
|
7
|
+
let touchLockedAxis = null;
|
|
8
|
+
let touchIgnored = false;
|
|
9
|
+
const LOCK_THRESHOLD = 10;
|
|
10
|
+
const SWIPE_TRIGGER = 24;
|
|
11
|
+
const DAMP_VALID = 0.4;
|
|
12
|
+
const DAMP_INVALID = 0.1;
|
|
13
|
+
function isInsideSwipeIgnore(target, boundary) {
|
|
14
|
+
let el = target;
|
|
15
|
+
while (el && el !== boundary) {
|
|
16
|
+
if (el instanceof HTMLElement && el.hasAttribute("data-swipe-ignore")) return true;
|
|
17
|
+
el = el.parentElement;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
function onTouchStart(e) {
|
|
22
|
+
if (toValue(options.disabled)) {
|
|
23
|
+
touchIgnored = true;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (options.el.value && isInsideSwipeIgnore(e.target, options.el.value)) {
|
|
27
|
+
touchIgnored = true;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
touchIgnored = false;
|
|
31
|
+
const touch = e.touches[0];
|
|
32
|
+
touchStartX = touch.clientX;
|
|
33
|
+
touchStartY = touch.clientY;
|
|
34
|
+
touchLockedAxis = null;
|
|
35
|
+
isSwiping.value = false;
|
|
36
|
+
swipeOffset.value = { x: 0, y: 0 };
|
|
37
|
+
}
|
|
38
|
+
function onTouchMove(e) {
|
|
39
|
+
if (touchIgnored) return;
|
|
40
|
+
const touch = e.touches[0];
|
|
41
|
+
const dx = touch.clientX - touchStartX;
|
|
42
|
+
const dy = touch.clientY - touchStartY;
|
|
43
|
+
if (!touchLockedAxis) {
|
|
44
|
+
if (Math.abs(dx) < LOCK_THRESHOLD && Math.abs(dy) < LOCK_THRESHOLD) return;
|
|
45
|
+
const detected = Math.abs(dx) >= Math.abs(dy) ? "x" : "y";
|
|
46
|
+
if (options.axis && detected !== options.axis) return;
|
|
47
|
+
touchLockedAxis = detected;
|
|
48
|
+
}
|
|
49
|
+
isSwiping.value = true;
|
|
50
|
+
if (touchLockedAxis === "x") {
|
|
51
|
+
const canL = toValue(options.canSwipeLeft) ?? !!options.onSwipeLeft;
|
|
52
|
+
const canR = toValue(options.canSwipeRight) ?? !!options.onSwipeRight;
|
|
53
|
+
const valid = dx < 0 && canL || dx > 0 && canR;
|
|
54
|
+
swipeOffset.value = { x: dx * (valid ? DAMP_VALID : DAMP_INVALID), y: 0 };
|
|
55
|
+
} else {
|
|
56
|
+
const canU = toValue(options.canSwipeUp) ?? !!options.onSwipeUp;
|
|
57
|
+
const canD = toValue(options.canSwipeDown) ?? !!options.onSwipeDown;
|
|
58
|
+
const valid = dy < 0 && canU || dy > 0 && canD;
|
|
59
|
+
swipeOffset.value = { x: 0, y: dy * (valid ? DAMP_VALID : DAMP_INVALID) };
|
|
60
|
+
}
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
}
|
|
63
|
+
function onTouchEnd() {
|
|
64
|
+
if (touchIgnored) return;
|
|
65
|
+
if (touchLockedAxis === "x" && Math.abs(swipeOffset.value.x) > SWIPE_TRIGGER) {
|
|
66
|
+
if (swipeOffset.value.x < 0) options.onSwipeLeft?.();
|
|
67
|
+
else options.onSwipeRight?.();
|
|
68
|
+
} else if (touchLockedAxis === "y" && Math.abs(swipeOffset.value.y) > SWIPE_TRIGGER) {
|
|
69
|
+
if (swipeOffset.value.y < 0) options.onSwipeUp?.();
|
|
70
|
+
else options.onSwipeDown?.();
|
|
71
|
+
}
|
|
72
|
+
swipeOffset.value = { x: 0, y: 0 };
|
|
73
|
+
isSwiping.value = false;
|
|
74
|
+
touchLockedAxis = null;
|
|
75
|
+
}
|
|
76
|
+
let wheelAccumX = 0;
|
|
77
|
+
let wheelAccumY = 0;
|
|
78
|
+
let wheelTimer = null;
|
|
79
|
+
let wheelCooldown = false;
|
|
80
|
+
const WHEEL_THRESHOLD = 60;
|
|
81
|
+
function resetWheel() {
|
|
82
|
+
wheelAccumX = 0;
|
|
83
|
+
wheelAccumY = 0;
|
|
84
|
+
wheelCooldown = false;
|
|
85
|
+
wheelTimer = null;
|
|
86
|
+
}
|
|
87
|
+
function onWheel(e) {
|
|
88
|
+
if (toValue(options.disabled)) return;
|
|
89
|
+
if (e.ctrlKey) return;
|
|
90
|
+
wheelAccumX += e.deltaX;
|
|
91
|
+
wheelAccumY += e.deltaY;
|
|
92
|
+
if (wheelTimer) clearTimeout(wheelTimer);
|
|
93
|
+
wheelTimer = setTimeout(resetWheel, 300);
|
|
94
|
+
if (wheelCooldown) return;
|
|
95
|
+
if (Math.abs(wheelAccumX) > WHEEL_THRESHOLD && options.axis !== "y" && Math.abs(wheelAccumX) >= Math.abs(wheelAccumY)) {
|
|
96
|
+
if (wheelAccumX > 0) {
|
|
97
|
+
const canL = toValue(options.canSwipeLeft) ?? !!options.onSwipeLeft;
|
|
98
|
+
if (canL) options.onSwipeLeft?.();
|
|
99
|
+
} else {
|
|
100
|
+
const canR = toValue(options.canSwipeRight) ?? !!options.onSwipeRight;
|
|
101
|
+
if (canR) options.onSwipeRight?.();
|
|
102
|
+
}
|
|
103
|
+
wheelCooldown = true;
|
|
104
|
+
wheelAccumX = 0;
|
|
105
|
+
wheelAccumY = 0;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (Math.abs(wheelAccumY) > WHEEL_THRESHOLD && options.axis !== "x" && Math.abs(wheelAccumY) >= Math.abs(wheelAccumX)) {
|
|
109
|
+
if (wheelAccumY > 0) {
|
|
110
|
+
const canU = toValue(options.canSwipeUp) ?? !!options.onSwipeUp;
|
|
111
|
+
if (canU) options.onSwipeUp?.();
|
|
112
|
+
} else {
|
|
113
|
+
const canD = toValue(options.canSwipeDown) ?? !!options.onSwipeDown;
|
|
114
|
+
if (canD) options.onSwipeDown?.();
|
|
115
|
+
}
|
|
116
|
+
wheelCooldown = true;
|
|
117
|
+
wheelAccumX = 0;
|
|
118
|
+
wheelAccumY = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
watchEffect((onCleanup) => {
|
|
122
|
+
const el = options.el.value;
|
|
123
|
+
if (!el) return;
|
|
124
|
+
el.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
125
|
+
el.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
126
|
+
el.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
127
|
+
el.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
128
|
+
el.addEventListener("wheel", onWheel, { passive: true });
|
|
129
|
+
onCleanup(() => {
|
|
130
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
131
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
132
|
+
el.removeEventListener("touchend", onTouchEnd);
|
|
133
|
+
el.removeEventListener("touchcancel", onTouchEnd);
|
|
134
|
+
el.removeEventListener("wheel", onWheel);
|
|
135
|
+
if (wheelTimer) clearTimeout(wheelTimer);
|
|
136
|
+
resetWheel();
|
|
137
|
+
});
|
|
138
|
+
}, { flush: "post" });
|
|
139
|
+
return { swipeOffset, isSwiping };
|
|
140
|
+
}
|
|
@@ -18,14 +18,18 @@ export const DocumentHeader = Node.create({
|
|
|
18
18
|
},
|
|
19
19
|
addKeyboardShortcuts() {
|
|
20
20
|
return {
|
|
21
|
-
// Enter inside the header →
|
|
21
|
+
// Enter inside the header → skip past documentMeta to the first body block
|
|
22
22
|
Enter: ({ editor }) => {
|
|
23
23
|
const $head = editor.state.selection.$head;
|
|
24
24
|
if ($head.parent.type.name !== "documentHeader") return false;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
let firstBodyPos = -1;
|
|
26
|
+
editor.state.doc.forEach((node, offset) => {
|
|
27
|
+
if (firstBodyPos === -1 && node.type.name !== "documentHeader" && node.type.name !== "documentMeta") {
|
|
28
|
+
firstBodyPos = offset + 1;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
if (firstBodyPos > 0) return editor.commands.setTextSelection(firstBodyPos);
|
|
32
|
+
return false;
|
|
29
33
|
},
|
|
30
34
|
// Backspace at the very start of the header → swallow (don't delete the node)
|
|
31
35
|
Backspace: ({ editor }) => {
|
|
@@ -47,10 +51,16 @@ export const DocumentHeader = Node.create({
|
|
|
47
51
|
if (!(view.state.selection instanceof AllSelection)) return false;
|
|
48
52
|
const { tr, schema } = view.state;
|
|
49
53
|
const headerType = schema.nodes.documentHeader;
|
|
54
|
+
const metaType = schema.nodes.documentMeta;
|
|
50
55
|
const paragraphType = schema.nodes.paragraph;
|
|
51
|
-
if (!headerType || !paragraphType) return false;
|
|
56
|
+
if (!headerType || !metaType || !paragraphType) return false;
|
|
57
|
+
let existingMeta = null;
|
|
58
|
+
tr.doc.forEach((node) => {
|
|
59
|
+
if (node.type.name === "documentMeta") existingMeta = node;
|
|
60
|
+
});
|
|
52
61
|
const newContent = Fragment.from([
|
|
53
62
|
headerType.create(null, text ? [schema.text(text)] : []),
|
|
63
|
+
existingMeta ?? metaType.create(),
|
|
54
64
|
paragraphType.create()
|
|
55
65
|
]);
|
|
56
66
|
tr.replaceWith(0, tr.doc.content.size, newContent);
|