@abraca/nuxt 2.10.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/module.d.mts +14 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/assets/editor.css +1 -1
  5. package/dist/runtime/components/AConnectionBadge.d.vue.ts +29 -0
  6. package/dist/runtime/components/AConnectionBadge.vue +79 -0
  7. package/dist/runtime/components/AConnectionBadge.vue.d.ts +29 -0
  8. package/dist/runtime/components/ADocPickerModal.d.vue.ts +31 -0
  9. package/dist/runtime/components/ADocPickerModal.vue +191 -0
  10. package/dist/runtime/components/ADocPickerModal.vue.d.ts +31 -0
  11. package/dist/runtime/components/ADocumentTree.vue +65 -0
  12. package/dist/runtime/components/AEditor.d.vue.ts +19 -12
  13. package/dist/runtime/components/AEditor.vue +243 -165
  14. package/dist/runtime/components/AEditor.vue.d.ts +19 -12
  15. package/dist/runtime/components/AEncryptionModePicker.d.vue.ts +33 -0
  16. package/dist/runtime/components/AEncryptionModePicker.vue +211 -0
  17. package/dist/runtime/components/AEncryptionModePicker.vue.d.ts +33 -0
  18. package/dist/runtime/components/AModalShell.d.vue.ts +48 -0
  19. package/dist/runtime/components/AModalShell.vue +105 -0
  20. package/dist/runtime/components/AModalShell.vue.d.ts +48 -0
  21. package/dist/runtime/components/ANodePanel.d.vue.ts +17 -7
  22. package/dist/runtime/components/ANodePanel.vue +550 -451
  23. package/dist/runtime/components/ANodePanel.vue.d.ts +17 -7
  24. package/dist/runtime/components/ANodePanelHeader.d.vue.ts +20 -10
  25. package/dist/runtime/components/ANodePanelHeader.vue +17 -3
  26. package/dist/runtime/components/ANodePanelHeader.vue.d.ts +20 -10
  27. package/dist/runtime/components/ANodeSettingsPanel.d.vue.ts +2 -0
  28. package/dist/runtime/components/ANodeSettingsPanel.vue +21 -1
  29. package/dist/runtime/components/ANodeSettingsPanel.vue.d.ts +2 -0
  30. package/dist/runtime/components/ASnapshotPreviewModal.d.vue.ts +33 -0
  31. package/dist/runtime/components/ASnapshotPreviewModal.vue +430 -0
  32. package/dist/runtime/components/ASnapshotPreviewModal.vue.d.ts +33 -0
  33. package/dist/runtime/components/ATagsEditor.d.vue.ts +19 -0
  34. package/dist/runtime/components/ATagsEditor.vue +60 -0
  35. package/dist/runtime/components/ATagsEditor.vue.d.ts +19 -0
  36. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  37. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  38. package/dist/runtime/components/chat/AChatInput.d.vue.ts +11 -6
  39. package/dist/runtime/components/chat/AChatInput.vue +33 -2
  40. package/dist/runtime/components/chat/AChatInput.vue.d.ts +11 -6
  41. package/dist/runtime/components/chat/AChatList.d.vue.ts +12 -0
  42. package/dist/runtime/components/chat/AChatList.vue +76 -32
  43. package/dist/runtime/components/chat/AChatList.vue.d.ts +12 -0
  44. package/dist/runtime/components/chat/AChatMessages.d.vue.ts +4 -0
  45. package/dist/runtime/components/chat/AChatMessages.vue +57 -4
  46. package/dist/runtime/components/chat/AChatMessages.vue.d.ts +4 -0
  47. package/dist/runtime/components/chat/AChatPanel.d.vue.ts +6 -2
  48. package/dist/runtime/components/chat/AChatPanel.vue +17 -1
  49. package/dist/runtime/components/chat/AChatPanel.vue.d.ts +6 -2
  50. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -1
  51. package/dist/runtime/components/docs/ADocsSearch.d.vue.ts +1 -1
  52. package/dist/runtime/components/docs/ADocsSearch.vue.d.ts +1 -1
  53. package/dist/runtime/components/editor/ALocationPickerPopover.vue +28 -7
  54. package/dist/runtime/components/registry/APluginDetail.d.vue.ts +2 -2
  55. package/dist/runtime/components/registry/APluginDetail.vue.d.ts +2 -2
  56. package/dist/runtime/components/renderers/AChartRenderer.client.d.vue.ts +17 -0
  57. package/dist/runtime/components/renderers/AChartRenderer.client.vue +622 -0
  58. package/dist/runtime/components/renderers/AChartRenderer.client.vue.d.ts +17 -0
  59. package/dist/runtime/components/renderers/AGraphRenderer.vue +64 -15
  60. package/dist/runtime/components/renderers/AProseRenderer.d.vue.ts +2 -2
  61. package/dist/runtime/components/renderers/AProseRenderer.vue.d.ts +2 -2
  62. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  63. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  64. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  65. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  66. package/dist/runtime/components/renderers/sheets/ASheetsGrid.d.vue.ts +2 -2
  67. package/dist/runtime/components/renderers/sheets/ASheetsGrid.vue.d.ts +2 -2
  68. package/dist/runtime/components/settings/ASettingsAppearancePanel.d.vue.ts +3 -0
  69. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue +67 -0
  70. package/dist/runtime/components/settings/ASettingsAppearancePanel.vue.d.ts +3 -0
  71. package/dist/runtime/components/settings/ASettingsGroup.d.vue.ts +24 -0
  72. package/dist/runtime/components/settings/ASettingsGroup.vue +31 -0
  73. package/dist/runtime/components/settings/ASettingsGroup.vue.d.ts +24 -0
  74. package/dist/runtime/components/settings/ASettingsModal.vue +84 -53
  75. package/dist/runtime/components/settings/ASettingsPlaceholder.d.vue.ts +20 -0
  76. package/dist/runtime/components/settings/ASettingsPlaceholder.vue +32 -0
  77. package/dist/runtime/components/settings/ASettingsPlaceholder.vue.d.ts +20 -0
  78. package/dist/runtime/components/settings/ASettingsRow.d.vue.ts +34 -0
  79. package/dist/runtime/components/settings/ASettingsRow.vue +34 -0
  80. package/dist/runtime/components/settings/ASettingsRow.vue.d.ts +34 -0
  81. package/dist/runtime/components/settings/sections.d.ts +37 -0
  82. package/dist/runtime/components/settings/sections.js +45 -0
  83. package/dist/runtime/components/shell/ABreadcrumbForDoc.d.vue.ts +6 -0
  84. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue +75 -3
  85. package/dist/runtime/components/shell/ABreadcrumbForDoc.vue.d.ts +6 -0
  86. package/dist/runtime/components/shell/ADocPanelServerSettings.d.vue.ts +17 -0
  87. package/dist/runtime/components/shell/ADocPanelServerSettings.vue +253 -0
  88. package/dist/runtime/components/shell/ADocPanelServerSettings.vue.d.ts +17 -0
  89. package/dist/runtime/components/shell/ADocPanelSettings.d.vue.ts +2 -0
  90. package/dist/runtime/components/shell/ADocPanelSettings.vue +15 -4
  91. package/dist/runtime/components/shell/ADocPanelSettings.vue.d.ts +2 -0
  92. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  93. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  94. package/dist/runtime/composables/useChat.d.ts +22 -1
  95. package/dist/runtime/composables/useChat.js +79 -8
  96. package/dist/runtime/composables/useDocBreadcrumb.d.ts +17 -2
  97. package/dist/runtime/composables/useDocBreadcrumb.js +17 -3
  98. package/dist/runtime/composables/useDocSnapshots.d.ts +2 -1
  99. package/dist/runtime/composables/useDocSnapshots.js +5 -0
  100. package/dist/runtime/composables/useEditor.d.ts +1 -1
  101. package/dist/runtime/composables/useEditor.js +120 -0
  102. package/dist/runtime/composables/useEditorToolbar.d.ts +12 -4
  103. package/dist/runtime/composables/useEditorToolbar.js +78 -56
  104. package/dist/runtime/composables/useNodeContextMenu.d.ts +14 -0
  105. package/dist/runtime/composables/useNodeContextMenu.js +59 -1
  106. package/dist/runtime/composables/useSettingsModal.d.ts +1 -1
  107. package/dist/runtime/composables/useSwipeGesture.d.ts +48 -0
  108. package/dist/runtime/composables/useSwipeGesture.js +140 -0
  109. package/dist/runtime/extensions/document-header.js +16 -6
  110. package/dist/runtime/extensions/document-meta.js +344 -19
  111. package/dist/runtime/extensions/meta-field.js +42 -0
  112. package/dist/runtime/extensions/views/DocumentMetaView.vue +33 -7
  113. package/dist/runtime/extensions/views/FieldView.vue +51 -19
  114. package/dist/runtime/extensions/views/MetaFieldView.vue +30 -4
  115. package/dist/runtime/locale.d.ts +8 -0
  116. package/dist/runtime/locale.js +9 -1
  117. package/dist/runtime/middleware/abracadabra-auth.d.ts +1 -1
  118. package/dist/runtime/plugin-abracadabra.client.d.ts +1 -1
  119. package/dist/runtime/plugin-abracadabra.client.js +12 -2
  120. package/dist/runtime/plugin-abracadabra.server.d.ts +1 -1
  121. package/dist/runtime/plugin-shared-globals.client.d.ts +1 -1
  122. package/dist/runtime/utils/chatContent.d.ts +20 -2
  123. package/dist/runtime/utils/chatContent.js +20 -1
  124. package/dist/runtime/utils/docTypes.js +43 -0
  125. package/dist/runtime/utils/titleSync.d.ts +130 -0
  126. package/dist/runtime/utils/titleSync.js +53 -0
  127. package/package.json +11 -4
@@ -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
- const items = computed(() => {
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 base = [[
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 pluginGroups = registry.getAllToolbarItems(ctx);
58
- for (const group of pluginGroups) {
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
- if (options.extraItems?.length) {
67
- base.push(...options.extraItems);
68
- }
69
- return base;
70
- });
71
- return { items };
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,20 @@ 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
+ /** Reparent this node — renders "Move to…" (e.g. open an <ADocPickerModal>). */
24
+ onMoveTo?: (id: string) => void;
25
+ /** Edit this node's tags — renders "Edit tags…" (e.g. open an <ATagsEditor>). */
26
+ onEditTags?: (id: string) => void;
27
+ /** Export the doc — renders an "Export" submenu (Markdown / HTML). Wire to
28
+ * `useDocExport().exportDoc` (kept out of this composable so it stays
29
+ * decoupled from the export pipeline + its lazy jszip import). */
30
+ onExport?: (id: string, format: 'markdown' | 'html') => void;
17
31
  favorites?: {
18
32
  isFavorite: (id: string) => boolean;
19
33
  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,24 @@ 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
+ }
70
+ if (opts.onMoveTo) {
71
+ editGroup.push({
72
+ label: "Move to\u2026",
73
+ icon: "i-lucide-corner-down-right",
74
+ onSelect() {
75
+ opts.onMoveTo(nodeId);
76
+ }
77
+ });
78
+ }
42
79
  const colorItems = UI_COLORS.slice(0, 12).map((colorName) => ({
43
80
  label: colorName.charAt(0).toUpperCase() + colorName.slice(1),
44
81
  icon: "i-lucide-circle",
@@ -82,6 +119,25 @@ export function useNodeContextMenu(opts) {
82
119
  }
83
120
  });
84
121
  }
122
+ if (opts.onEditTags) {
123
+ actionsGroup.push({
124
+ label: "Edit tags\u2026",
125
+ icon: "i-lucide-tags",
126
+ onSelect() {
127
+ opts.onEditTags(nodeId);
128
+ }
129
+ });
130
+ }
131
+ if (opts.onExport) {
132
+ actionsGroup.push({
133
+ label: "Export",
134
+ icon: "i-lucide-download",
135
+ children: [
136
+ { label: "Markdown", icon: "i-lucide-file-text", onSelect: () => opts.onExport(nodeId, "markdown") },
137
+ { label: "HTML", icon: "i-lucide-file-code", onSelect: () => opts.onExport(nodeId, "html") }
138
+ ]
139
+ });
140
+ }
85
141
  const dangerGroup = [
86
142
  {
87
143
  label: "Move to trash",
@@ -92,7 +148,9 @@ export function useNodeContextMenu(opts) {
92
148
  }
93
149
  }
94
150
  ];
95
- const items = [editGroup, appearanceGroup];
151
+ const items = [];
152
+ if (openGroup.length > 0) items.push(openGroup);
153
+ items.push(editGroup, appearanceGroup);
96
154
  if (actionsGroup.length > 0) items.push(actionsGroup);
97
155
  items.push(dangerGroup);
98
156
  return { items };
@@ -9,7 +9,7 @@
9
9
  * Usage:
10
10
  * const { isOpen, activeTab, openSettings, closeSettings } = useSettingsModal()
11
11
  */
12
- export type SettingsTab = 'profile' | 'connection' | 'spaces' | 'security' | 'invites' | 'members' | 'trash' | 'offline' | 'plugins' | 'admin';
12
+ export type SettingsTab = 'profile' | 'appearance' | 'connection' | 'spaces' | 'security' | 'invites' | 'members' | 'trash' | 'offline' | 'plugins' | 'admin';
13
13
  export declare function useSettingsModal(): {
14
14
  isOpen: import("vue").WritableComputedRef<boolean, boolean>;
15
15
  activeTab: import("vue").WritableComputedRef<SettingsTab, SettingsTab>;
@@ -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 → move cursor to the first body block below
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
- const headerNode = editor.state.doc.firstChild;
26
- if (!headerNode) return false;
27
- const insideNextBlock = headerNode.nodeSize + 1;
28
- return editor.commands.setTextSelection(insideNextBlock);
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);