@abraca/nuxt 2.13.0 → 2.15.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 (45) hide show
  1. package/dist/module.d.mts +15 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -0
  4. package/dist/runtime/assets/editor.css +3 -1
  5. package/dist/runtime/components/ACodeEditor.vue +138 -23
  6. package/dist/runtime/components/ADocViewToggle.d.vue.ts +40 -0
  7. package/dist/runtime/components/ADocViewToggle.vue +234 -0
  8. package/dist/runtime/components/ADocViewToggle.vue.d.ts +40 -0
  9. package/dist/runtime/components/ADocumentTree.vue +1 -1
  10. package/dist/runtime/components/AEditor.vue +183 -15
  11. package/dist/runtime/components/ANodePanel.vue +91 -91
  12. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  13. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  14. package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
  15. package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
  16. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  17. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  18. package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
  19. package/dist/runtime/components/editor/ADocSuggestMenu.d.vue.ts +7 -0
  20. package/dist/runtime/components/editor/ADocSuggestMenu.vue +68 -0
  21. package/dist/runtime/components/editor/ADocSuggestMenu.vue.d.ts +7 -0
  22. package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
  23. package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
  24. package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
  25. package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
  26. package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
  27. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  28. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  29. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  30. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  31. package/dist/runtime/composables/useDocLinkPick.d.ts +9 -8
  32. package/dist/runtime/composables/useDocLinkPick.js +7 -18
  33. package/dist/runtime/composables/useDocSuggest.d.ts +34 -0
  34. package/dist/runtime/composables/useDocSuggest.js +56 -0
  35. package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
  36. package/dist/runtime/composables/useTouchDrag.js +30 -0
  37. package/dist/runtime/extensions/doc-link-drop.js +2 -2
  38. package/dist/runtime/extensions/doc-suggest.d.ts +28 -0
  39. package/dist/runtime/extensions/doc-suggest.js +85 -0
  40. package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
  41. package/dist/runtime/utils/codeHighlightStyle.d.ts +15 -0
  42. package/dist/runtime/utils/codeHighlightStyle.js +34 -0
  43. package/dist/runtime/utils/loadCodeMirror.d.ts +1 -0
  44. package/dist/runtime/utils/loadCodeMirror.js +6 -3
  45. package/package.json +2 -1
package/dist/module.d.mts CHANGED
@@ -59,6 +59,7 @@ declare module '@nuxt/schema' {
59
59
  };
60
60
  debug: boolean;
61
61
  docBasePath: string;
62
+ docPicker: 'inline' | 'command-palette';
62
63
  guestName: {
63
64
  adjectives: string[];
64
65
  nouns: string[];
@@ -258,6 +259,20 @@ interface ModuleOptions {
258
259
  * docBasePath: '' → /{docId}
259
260
  */
260
261
  docBasePath?: string;
262
+ /**
263
+ * How documents are picked for cross-document references (doc links/embeds).
264
+ *
265
+ * - `'inline'` (default) — typing `[[` opens an inline mention-style doc-link
266
+ * popup, and `![[` an inline doc-embed popup (flat search + ancestor-path
267
+ * prefix). The slash commands + doc-link toolbar button still open the
268
+ * command palette.
269
+ * - `'command-palette'` — disables the inline `[[` / `![[` triggers; the only
270
+ * doc-reference entry points are the slash commands + toolbar button, which
271
+ * open a `UCommandPalette` modal.
272
+ *
273
+ * Default: 'inline'.
274
+ */
275
+ docPicker?: 'inline' | 'command-palette';
261
276
  /**
262
277
  * Mapbox GL access token for the Map renderer.
263
278
  * Can be overridden at runtime via NUXT_PUBLIC_ABRACADABRA_MAPBOX_TOKEN.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "2.13.0",
7
+ "version": "2.15.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -50,6 +50,7 @@ const module$1 = defineNuxtModule({
50
50
  },
51
51
  debug: false,
52
52
  docBasePath: "/doc",
53
+ docPicker: "inline",
53
54
  guestName: {},
54
55
  webrtc: {
55
56
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
@@ -96,6 +97,7 @@ const module$1 = defineNuxtModule({
96
97
  server: options.server ?? {},
97
98
  debug: options.debug ?? false,
98
99
  docBasePath: options.docBasePath ?? "/doc",
100
+ docPicker: options.docPicker ?? "inline",
99
101
  mapboxToken: options.mapboxToken ?? "",
100
102
  guestName: {
101
103
  adjectives: options.guestName?.adjectives ?? [],
@@ -1 +1,3 @@
1
- html.dark .tiptap .shiki,html.dark .tiptap .shiki span{background-color:var(--ui-bg-muted)!important;color:var(--shiki-dark)!important}.collaboration-carets__caret{border-left:1px solid #0d0d0d;border-right:1px solid #0d0d0d;margin-left:-1px;margin-right:-1px;opacity:0;pointer-events:none;position:relative;transition:opacity .3s ease;word-break:normal}.collaboration-carets__caret.is-hidden{display:none}.collaboration-carets__caret.is-active{opacity:1}.collaboration-carets__label{border-radius:3px 3px 3px 0;color:#0d0d0d;font-size:12px;font-style:normal;font-weight:600;left:-1px;line-height:normal;padding:.1rem .3rem;position:absolute;top:-1.4em;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.ProseMirror-yjs-selection,.collaboration-carets__selection{background-color:var(--collaboration-selection-color)!important;pointer-events:none}.search-highlight{background-color:color-mix(in srgb,var(--color-primary-400) 35%,transparent);border-radius:2px;padding:0 1px}.doc-passage-highlight{background-color:color-mix(in srgb,var(--color-success-400) 35%,transparent);border-radius:2px;padding:0 1px}.tiptap{min-height:100%;padding-bottom:8rem}.tiptap.file-drop-active{border-radius:4px;outline:2px dashed var(--ui-primary);outline-offset:4px}.tiptap .document-header{font-size:2.5rem;font-weight:800;letter-spacing:-.025em;line-height:1.2;margin-bottom:1rem}.tiptap [data-type=document-meta]{align-items:center;display:flex;flex-wrap:wrap;font-size:.875rem;gap:.375rem;line-height:1.75rem;margin-bottom:1.5rem;min-height:1.75rem}.tiptap [data-type=document-meta][data-cursor-in-meta=true][data-empty=true]:after{color:var(--ui-text-dimmed);content:"Type '/' to add a property…";pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.prose-variant .tiptap [data-type=document-meta]{display:none}
1
+ html .cm-editor,html .cm-editor .cm-content,html .cm-editor .cm-gutters,html .cm-editor .cm-scroller,html .cm-editor .cm-tooltip{font-family:var(
2
+ --font-code,ui-monospace,"SF Mono",Menlo,Monaco,Consolas,monospace
3
+ )!important}html.dark .tiptap .shiki,html.dark .tiptap .shiki span{background-color:var(--ui-bg-muted)!important;color:var(--shiki-dark)!important}.collaboration-carets__caret{border-left:1px solid #0d0d0d;border-right:1px solid #0d0d0d;margin-left:-1px;margin-right:-1px;opacity:0;pointer-events:none;position:relative;transition:opacity .3s ease;word-break:normal}.collaboration-carets__caret.is-hidden{display:none}.collaboration-carets__caret.is-active{opacity:1}.collaboration-carets__label{border-radius:3px 3px 3px 0;color:#0d0d0d;font-size:12px;font-style:normal;font-weight:600;left:-1px;line-height:normal;padding:.1rem .3rem;position:absolute;top:-1.4em;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.ProseMirror-yjs-selection,.collaboration-carets__selection{background-color:var(--collaboration-selection-color)!important;pointer-events:none}.search-highlight{background-color:color-mix(in srgb,var(--color-primary-400) 35%,transparent);border-radius:2px;padding:0 1px}.doc-passage-highlight{background-color:color-mix(in srgb,var(--color-success-400) 35%,transparent);border-radius:2px;padding:0 1px}.tiptap{min-height:100%;padding-bottom:8rem}.tiptap.file-drop-active{border-radius:4px;outline:2px dashed var(--ui-primary);outline-offset:4px}.tiptap .document-header{font-size:2.5rem;font-weight:800;letter-spacing:-.025em;line-height:1.2;margin-bottom:1rem}.tiptap [data-type=document-meta]{align-items:center;display:flex;flex-wrap:wrap;font-size:.875rem;gap:.375rem;line-height:1.75rem;margin-bottom:1.5rem;min-height:1.75rem}.tiptap [data-type=document-meta][data-cursor-in-meta=true][data-empty=true]:after{color:var(--ui-text-dimmed);content:"Type '/' to add a property…";pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.prose-variant .tiptap [data-type=document-meta]{display:none}
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { ref, shallowRef, watch, computed, onMounted, onBeforeUnmount } from "vue";
3
3
  import { loadCodeMirror } from "../utils/loadCodeMirror";
4
+ import { buildAtomOneNuxtHighlight } from "../utils/codeHighlightStyle";
4
5
  import { useNuxtApp } from "#imports";
5
6
  const props = defineProps({
6
7
  provider: { type: null, required: true },
@@ -47,60 +48,148 @@ function buildTheme(bundle) {
47
48
  backgroundColor: "var(--ui-bg)",
48
49
  color: "var(--ui-text-highlighted)",
49
50
  height: "100%",
50
- fontSize: "13px",
51
- fontFamily: 'ui-monospace, "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", Menlo, Monaco, Consolas, monospace'
51
+ fontSize: "13px"
52
52
  },
53
+ // Honour a host-defined code font (`--font-code`) when present; falls back
54
+ // to the system monospace stack. `!important` + the `.cm-content`/
55
+ // `.cm-gutters` specificity guards against an aggressive host body-font
56
+ // rule (e.g. `html[data-font] *`) leaking the UI sans-serif into the editor.
53
57
  ".cm-content": {
54
- fontFamily: "inherit",
58
+ fontFamily: 'var(--font-code, ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace) !important',
55
59
  caretColor: "var(--ui-text-highlighted)",
56
- padding: "8px 0"
60
+ // Top padding leaves room for a remote caret's name badge on the FIRST
61
+ // line — y-codemirror positions it at `top: -1.4em`, so without headroom
62
+ // it renders above the content box and is clipped by the toolbar above.
63
+ padding: "1.6em 0 8px"
57
64
  },
58
- ".cm-cursor": {
59
- borderLeftColor: "var(--ui-text-highlighted)"
65
+ // Local caret — `drawSelection()` hides the native caret and draws this
66
+ // bar, so style it to read like the native caret used elsewhere in the
67
+ // app: a crisp 2px stroke in the body text colour.
68
+ ".cm-cursor, .cm-dropCursor": {
69
+ borderLeftColor: "var(--ui-text-highlighted)",
70
+ borderLeftWidth: "2px"
60
71
  },
61
72
  ".cm-activeLine": {
62
73
  backgroundColor: "color-mix(in srgb, var(--ui-bg-elevated) 50%, transparent)"
63
74
  },
64
75
  ".cm-activeLineGutter": {
65
- backgroundColor: "color-mix(in srgb, var(--ui-bg-elevated) 50%, transparent)"
76
+ backgroundColor: "color-mix(in srgb, var(--ui-bg-elevated) 50%, transparent)",
77
+ color: "var(--ui-text)"
66
78
  },
67
79
  ".cm-gutters": {
68
- fontFamily: "inherit",
80
+ fontFamily: 'var(--font-code, ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace) !important',
69
81
  backgroundColor: "var(--ui-bg)",
70
82
  color: "var(--ui-text-dimmed)",
71
83
  border: "none",
72
84
  borderRight: "1px solid var(--ui-border)"
73
85
  },
86
+ ".cm-foldPlaceholder": {
87
+ backgroundColor: "var(--ui-bg-elevated)",
88
+ color: "var(--ui-text-muted)",
89
+ border: "1px solid var(--ui-border)"
90
+ },
91
+ // Local selection — translucent primary tint, brighter while focused.
74
92
  ".cm-selectionBackground": {
75
93
  backgroundColor: "color-mix(in srgb, var(--ui-primary) 25%, transparent) !important"
76
94
  },
77
95
  "&.cm-focused .cm-selectionBackground": {
78
- backgroundColor: "color-mix(in srgb, var(--ui-primary) 30%, transparent) !important"
96
+ backgroundColor: "color-mix(in srgb, var(--ui-primary) 40%, transparent) !important"
79
97
  },
80
98
  ".cm-matchingBracket": {
81
99
  backgroundColor: "color-mix(in srgb, var(--ui-primary) 20%, transparent)",
82
100
  outline: "1px solid color-mix(in srgb, var(--ui-primary) 40%, transparent)"
83
101
  },
102
+ // Occurrences of the current selection (highlightSelectionMatches).
103
+ ".cm-selectionMatch": {
104
+ backgroundColor: "color-mix(in srgb, var(--ui-primary) 15%, transparent)"
105
+ },
106
+ // Find/replace panel + search hits — themed to the UI tokens.
107
+ ".cm-panels": {
108
+ backgroundColor: "var(--ui-bg-elevated)",
109
+ color: "var(--ui-text)"
110
+ },
111
+ ".cm-panels.cm-panels-top": { borderBottom: "1px solid var(--ui-border)" },
112
+ ".cm-panels.cm-panels-bottom": { borderTop: "1px solid var(--ui-border)" },
113
+ ".cm-searchMatch": {
114
+ backgroundColor: "color-mix(in srgb, var(--ui-primary) 25%, transparent)",
115
+ outline: "1px solid color-mix(in srgb, var(--ui-primary) 45%, transparent)",
116
+ borderRadius: "2px"
117
+ },
118
+ ".cm-searchMatch-selected": {
119
+ backgroundColor: "color-mix(in srgb, var(--ui-primary) 45%, transparent) !important"
120
+ },
121
+ ".cm-textfield": {
122
+ backgroundColor: "var(--ui-bg)",
123
+ color: "var(--ui-text)",
124
+ border: "1px solid var(--ui-border)",
125
+ borderRadius: "4px"
126
+ },
127
+ ".cm-button": {
128
+ backgroundColor: "var(--ui-bg)",
129
+ backgroundImage: "none",
130
+ color: "var(--ui-text)",
131
+ border: "1px solid var(--ui-border)",
132
+ borderRadius: "4px"
133
+ },
134
+ // Autocomplete tooltip.
135
+ ".cm-tooltip": {
136
+ backgroundColor: "var(--ui-bg-elevated)",
137
+ color: "var(--ui-text)",
138
+ border: "1px solid var(--ui-border)",
139
+ borderRadius: "6px"
140
+ },
141
+ ".cm-tooltip-autocomplete > ul > li[aria-selected]": {
142
+ backgroundColor: "var(--ui-primary)",
143
+ color: "var(--ui-bg)"
144
+ },
145
+ // ── Remote presence (y-codemirror.next) — matches the TipTap editor's
146
+ // `.collaboration-carets__*` look so multiplayer feels identical across
147
+ // both editors. Per-user colour stays inline (set from awareness
148
+ // `user.color`); we restyle shape only. The caret already matches TipTap
149
+ // (1px border bars, -1px margins); we only fix the label badge + hide the
150
+ // caret dot.
151
+ ".cm-ySelection": {
152
+ borderRadius: "1px",
153
+ // Fallback selection fill. y-codemirror paints the band via an inline
154
+ // `background-color: <user.colorLight>`; a valid inline value wins by
155
+ // specificity and keeps the per-user colour. But peers that broadcast
156
+ // only `user.color` (older builds, native gpui/apertura/aperio clients)
157
+ // leave colorLight as y-codemirror's invalid `color + '33'` default,
158
+ // which the browser drops — making the band invisible while the caret
159
+ // (which uses `color` directly) still shows. This neutral tint then
160
+ // applies, so a remote selection is always visible.
161
+ backgroundColor: "color-mix(in srgb, var(--ui-primary) 30%, transparent)"
162
+ },
163
+ ".cm-ySelectionCaretDot": { display: "none" },
84
164
  ".cm-ySelectionInfo": {
85
- fontSize: "10px",
86
- fontFamily: "system-ui, sans-serif",
87
- padding: "1px 4px",
88
- borderRadius: "3px",
89
- opacity: "0.9",
90
- position: "absolute",
91
- top: "-1.3em",
165
+ opacity: "1",
166
+ top: "-1.4em",
92
167
  left: "-1px",
168
+ padding: "0.1rem 0.3rem",
169
+ borderRadius: "3px 3px 3px 0",
170
+ fontFamily: "system-ui, sans-serif",
171
+ fontSize: "12px",
172
+ fontStyle: "normal",
173
+ fontWeight: "600",
174
+ lineHeight: "normal",
175
+ color: "#0d0d0d",
93
176
  whiteSpace: "nowrap",
94
- color: "#fff"
95
- }
177
+ transitionDelay: "0s"
178
+ },
179
+ ".cm-ySelectionCaret:hover > .cm-ySelectionInfo": { opacity: "1" }
96
180
  }, { dark: true });
97
181
  }
98
182
  let xmlObserverCleanup = null;
183
+ let syncedOff = null;
99
184
  function destroyEditor() {
100
185
  if (xmlObserverCleanup) {
101
186
  xmlObserverCleanup();
102
187
  xmlObserverCleanup = null;
103
188
  }
189
+ if (syncedOff) {
190
+ syncedOff();
191
+ syncedOff = null;
192
+ }
104
193
  if (editorView.value) {
105
194
  editorView.value.destroy();
106
195
  editorView.value = null;
@@ -110,7 +199,7 @@ function createEditor(bundle, container) {
110
199
  const { EditorView, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, rectangularSelection, crosshairCursor, highlightSpecialChars, dropCursor, keymap } = bundle.view;
111
200
  const { EditorState } = bundle.state;
112
201
  const { defaultKeymap, history, historyKeymap, indentWithTab } = bundle.commands;
113
- const { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap } = bundle.language;
202
+ const { syntaxHighlighting, HighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap } = bundle.language;
114
203
  const { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } = bundle.autocomplete;
115
204
  const { searchKeymap, highlightSelectionMatches } = bundle.search;
116
205
  const baseExtensions = [
@@ -122,7 +211,7 @@ function createEditor(bundle, container) {
122
211
  dropCursor(),
123
212
  EditorState.allowMultipleSelections.of(true),
124
213
  indentOnInput(),
125
- syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
214
+ syntaxHighlighting(buildAtomOneNuxtHighlight(HighlightStyle, bundle.lezerHighlight.tags), { fallback: true }),
126
215
  bracketMatching(),
127
216
  closeBrackets(),
128
217
  autocompletion(),
@@ -168,9 +257,14 @@ function createEditor(bundle, container) {
168
257
  const ytext = ydoc.getText(props.fieldName);
169
258
  const awareness = prov.awareness;
170
259
  if (awareness && userName?.value && publicKeyB64?.value) {
260
+ const color = userColor?.value ?? "#888";
171
261
  awareness.setLocalStateField("user", {
172
262
  name: userName.value,
173
- color: userColor?.value ?? "#888",
263
+ color,
264
+ // Translucent fill for remote selection blocks (y-codemirror.next).
265
+ // Its default `color + '33'` is invalid for hsl()/named/var() colours,
266
+ // leaving the selection invisible — supply a format-agnostic value.
267
+ colorLight: `color-mix(in srgb, ${color} 30%, transparent)`,
174
268
  publicKey: publicKeyB64.value
175
269
  });
176
270
  }
@@ -180,10 +274,25 @@ function createEditor(bundle, container) {
180
274
  } else {
181
275
  extensions.push(history());
182
276
  }
183
- return new EditorView({
277
+ const view = new EditorView({
184
278
  state: EditorState.create({ doc: ytext.toString(), extensions }),
185
279
  parent: container
186
280
  });
281
+ let rebuiltOnSync = false;
282
+ const onSynced = () => {
283
+ if (rebuiltOnSync) return;
284
+ const v = editorView.value;
285
+ const liveYtext = prov.document?.getText?.(props.fieldName);
286
+ if (!v || !liveYtext) return;
287
+ if (liveYtext.toString() !== v.state.doc.toString()) {
288
+ rebuiltOnSync = true;
289
+ void mount();
290
+ }
291
+ };
292
+ prov.on?.("synced", onSynced);
293
+ syncedOff = () => prov.off?.("synced", onSynced);
294
+ if (prov.isSynced) Promise.resolve().then(onSynced);
295
+ return view;
187
296
  }
188
297
  return new EditorView({
189
298
  state: EditorState.create({ doc: "", extensions: [...baseExtensions, history()] }),
@@ -224,7 +333,13 @@ watch(
224
333
  ([n, c, k]) => {
225
334
  const awareness = props.provider?.awareness;
226
335
  if (!awareness || !n || !k) return;
227
- awareness.setLocalStateField("user", { name: n, color: c ?? "#888", publicKey: k });
336
+ const color = c ?? "#888";
337
+ awareness.setLocalStateField("user", {
338
+ name: n,
339
+ color,
340
+ colorLight: `color-mix(in srgb, ${color} 30%, transparent)`,
341
+ publicKey: k
342
+ });
228
343
  }
229
344
  );
230
345
  onBeforeUnmount(destroyEditor);
@@ -0,0 +1,40 @@
1
+ export type DocViewTab = 'pageType' | 'editor' | 'chat' | 'settings';
2
+ type __VLS_Props = {
3
+ /** Active tab. `null` (or absent) = nothing highlighted (e.g. another tab is active). */
4
+ modelValue?: DocViewTab | null;
5
+ docId: string;
6
+ docType?: string;
7
+ chatUnread?: number;
8
+ /** Disable the editor segment (e.g. main view is already the editor). */
9
+ editorDisabled?: boolean;
10
+ /** Disable the page-type segment (e.g. provider not ready). */
11
+ pageTypeDisabled?: boolean;
12
+ /** Whether the user may assign/change the page type (gates the + dropdown). */
13
+ canChangeType?: boolean;
14
+ /** Render the chat slot. Off → omitted (e.g. features.chat disabled). */
15
+ showChat?: boolean;
16
+ /** Render the settings slot. Off → omitted. */
17
+ showSettings?: boolean;
18
+ /** Suppress tooltips (touch devices / during open animations). */
19
+ disableTooltips?: boolean;
20
+ size?: 'sm' | 'md';
21
+ };
22
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
+ select: (tab: DocViewTab) => any;
24
+ "update:modelValue": (tab: DocViewTab) => any;
25
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
+ onSelect?: ((tab: DocViewTab) => any) | undefined;
27
+ "onUpdate:modelValue"?: ((tab: DocViewTab) => any) | undefined;
28
+ }>, {
29
+ size: "sm" | "md";
30
+ modelValue: DocViewTab | null;
31
+ chatUnread: number;
32
+ editorDisabled: boolean;
33
+ pageTypeDisabled: boolean;
34
+ canChangeType: boolean;
35
+ showChat: boolean;
36
+ showSettings: boolean;
37
+ disableTooltips: boolean;
38
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
39
+ declare const _default: typeof __VLS_export;
40
+ export default _default;
@@ -0,0 +1,234 @@
1
+ <script setup>
2
+ import { ref, computed, onBeforeUnmount } from "vue";
3
+ import { useAbracadabra } from "../composables/useAbracadabra";
4
+ import { useSyncedMap } from "../composables/useYDoc";
5
+ import { useAbraLocale } from "../composables/useAbraLocale";
6
+ import { resolveDocType, getAvailableDocTypes } from "../utils/docTypes";
7
+ const props = defineProps({
8
+ modelValue: { type: [String, null], required: false, default: null },
9
+ docId: { type: String, required: true },
10
+ docType: { type: String, required: false },
11
+ chatUnread: { type: Number, required: false, default: 0 },
12
+ editorDisabled: { type: Boolean, required: false, default: false },
13
+ pageTypeDisabled: { type: Boolean, required: false, default: false },
14
+ canChangeType: { type: Boolean, required: false, default: true },
15
+ showChat: { type: Boolean, required: false, default: true },
16
+ showSettings: { type: Boolean, required: false, default: true },
17
+ disableTooltips: { type: Boolean, required: false, default: false },
18
+ size: { type: String, required: false, default: "md" }
19
+ });
20
+ const emit = defineEmits(["update:modelValue", "select"]);
21
+ const locale = computed(() => useAbraLocale("nodePanel"));
22
+ const { doc, registry } = useAbracadabra();
23
+ const treeMap = useSyncedMap(doc, "doc-tree");
24
+ const docTypeDef = computed(() => resolveDocType(props.docType, registry));
25
+ const hasPageType = computed(() => !!(props.docType && props.docType !== "doc"));
26
+ const docTypeItems = computed(
27
+ () => getAvailableDocTypes(registry).filter((dt) => dt.key !== props.docType).map((dt) => ({
28
+ label: dt.label,
29
+ icon: dt.icon,
30
+ onSelect() {
31
+ const entry = treeMap.get(props.docId);
32
+ if (!entry) return;
33
+ if (entry.type === dt.key) return;
34
+ treeMap.set(props.docId, { ...entry, type: dt.key, updatedAt: Date.now() });
35
+ }
36
+ }))
37
+ );
38
+ const visibleTabs = computed(() => {
39
+ const t = ["pageType", "editor"];
40
+ if (props.showChat) t.push("chat");
41
+ if (props.showSettings) t.push("settings");
42
+ return t;
43
+ });
44
+ function select(tab) {
45
+ emit("update:modelValue", tab);
46
+ emit("select", tab);
47
+ }
48
+ const typeMenuOpen = ref(false);
49
+ let pressTimer = null;
50
+ let longFired = false;
51
+ function onTypePointerDown() {
52
+ longFired = false;
53
+ cancelTypePress();
54
+ pressTimer = setTimeout(() => {
55
+ longFired = true;
56
+ typeMenuOpen.value = true;
57
+ }, 400);
58
+ }
59
+ function cancelTypePress() {
60
+ if (pressTimer) {
61
+ clearTimeout(pressTimer);
62
+ pressTimer = null;
63
+ }
64
+ }
65
+ function onTypeClick() {
66
+ cancelTypePress();
67
+ if (longFired) {
68
+ longFired = false;
69
+ return;
70
+ }
71
+ select("pageType");
72
+ }
73
+ onBeforeUnmount(cancelTypePress);
74
+ const activeIndex = computed(() => {
75
+ const v = props.modelValue;
76
+ if (!v) return -1;
77
+ if (v === "pageType" && !hasPageType.value) return -1;
78
+ return visibleTabs.value.indexOf(v);
79
+ });
80
+ const indicatorVisible = computed(() => activeIndex.value >= 0);
81
+ const segPx = computed(() => props.size === "sm" ? 24 : 28);
82
+ const iconClass = computed(() => props.size === "sm" ? "size-3" : "size-3.5");
83
+ </script>
84
+
85
+ <template>
86
+ <div
87
+ class="dvt"
88
+ :class="`dvt-${size}`"
89
+ :style="{ '--dvt-seg': segPx + 'px' }"
90
+ >
91
+ <!-- Sliding active pill — like Nuxt UI tabs. Equal-width segments make the
92
+ offset a simple multiple of the segment size, no DOM measuring. -->
93
+ <span
94
+ class="dvt-indicator"
95
+ :class="{ 'dvt-indicator-hidden': !indicatorVisible }"
96
+ :style="{ transform: `translateX(${Math.max(activeIndex, 0) * 100}%)` }"
97
+ aria-hidden="true"
98
+ />
99
+
100
+ <!-- Slot 1: page type (selectable tab) or + (assign-type dropdown) -->
101
+ <UDropdownMenu
102
+ v-if="!hasPageType"
103
+ :items="[docTypeItems]"
104
+ :disabled="!canChangeType"
105
+ >
106
+ <UTooltip
107
+ :text="locale.docType"
108
+ :content="{ side: 'bottom' }"
109
+ :disabled="disableTooltips"
110
+ >
111
+ <button
112
+ type="button"
113
+ class="dvt-seg"
114
+ :disabled="!canChangeType"
115
+ >
116
+ <UIcon
117
+ name="i-lucide-plus"
118
+ :class="iconClass"
119
+ />
120
+ </button>
121
+ </UTooltip>
122
+ </UDropdownMenu>
123
+ <div
124
+ v-else
125
+ class="dvt-seg-host"
126
+ >
127
+ <UTooltip
128
+ :text="docTypeDef.label"
129
+ :content="{ side: 'bottom' }"
130
+ :disabled="disableTooltips || typeMenuOpen"
131
+ >
132
+ <button
133
+ type="button"
134
+ class="dvt-seg"
135
+ :class="{ active: modelValue === 'pageType' }"
136
+ :disabled="pageTypeDisabled"
137
+ @pointerdown="onTypePointerDown"
138
+ @pointerup="onTypeClick"
139
+ @pointerleave="cancelTypePress"
140
+ @pointercancel="cancelTypePress"
141
+ @contextmenu.prevent="canChangeType && (typeMenuOpen = true)"
142
+ >
143
+ <UIcon
144
+ :name="docTypeDef.icon"
145
+ :class="iconClass"
146
+ />
147
+ </button>
148
+ </UTooltip>
149
+ <!-- Hidden anchor: the menu is opened programmatically by long-press so
150
+ the trigger never steals the click. -->
151
+ <UDropdownMenu
152
+ v-if="canChangeType"
153
+ v-model:open="typeMenuOpen"
154
+ :items="[docTypeItems]"
155
+ :content="{ side: 'bottom', align: 'center' }"
156
+ >
157
+ <span
158
+ class="dvt-anchor"
159
+ aria-hidden="true"
160
+ />
161
+ </UDropdownMenu>
162
+ </div>
163
+
164
+ <!-- Slot 2: editor -->
165
+ <UTooltip
166
+ :text="locale.editorTab"
167
+ :content="{ side: 'bottom' }"
168
+ :disabled="disableTooltips"
169
+ >
170
+ <button
171
+ type="button"
172
+ class="dvt-seg"
173
+ :class="{ active: modelValue === 'editor' }"
174
+ :disabled="editorDisabled"
175
+ @click="select('editor')"
176
+ >
177
+ <UIcon
178
+ name="i-lucide-text-cursor"
179
+ :class="iconClass"
180
+ />
181
+ </button>
182
+ </UTooltip>
183
+
184
+ <!-- Slot 3: chat (with unread badge) -->
185
+ <UTooltip
186
+ v-if="showChat"
187
+ :text="locale.chatTab"
188
+ :content="{ side: 'bottom' }"
189
+ :disabled="disableTooltips"
190
+ >
191
+ <button
192
+ type="button"
193
+ class="dvt-seg"
194
+ :class="{ active: modelValue === 'chat' }"
195
+ @click="select('chat')"
196
+ >
197
+ <span class="relative inline-flex">
198
+ <UIcon
199
+ name="i-lucide-message-circle"
200
+ :class="iconClass"
201
+ />
202
+ <span
203
+ v-if="chatUnread > 0 && modelValue !== 'chat'"
204
+ class="dvt-badge"
205
+ >{{ chatUnread > 99 ? "99+" : chatUnread }}</span>
206
+ </span>
207
+ </button>
208
+ </UTooltip>
209
+
210
+ <!-- Slot 4: settings -->
211
+ <UTooltip
212
+ v-if="showSettings"
213
+ :text="locale.settingsTab"
214
+ :content="{ side: 'bottom' }"
215
+ :disabled="disableTooltips"
216
+ >
217
+ <button
218
+ type="button"
219
+ class="dvt-seg"
220
+ :class="{ active: modelValue === 'settings' }"
221
+ @click="select('settings')"
222
+ >
223
+ <UIcon
224
+ name="i-lucide-settings-2"
225
+ :class="iconClass"
226
+ />
227
+ </button>
228
+ </UTooltip>
229
+ </div>
230
+ </template>
231
+
232
+ <style scoped>
233
+ .dvt{align-items:center;background:var(--ui-bg-elevated);border-radius:calc(var(--ui-radius)*1.5);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--ui-border-accented),#fff 25%);display:inline-flex;flex-shrink:0;padding:2px;position:relative}.dvt-indicator{background:var(--ui-bg);border-radius:var(--ui-radius);box-shadow:0 1px 2px rgba(0,0,0,.08),inset 0 0 0 1px var(--ui-border-accented);height:calc(100% - 4px);left:2px;pointer-events:none;position:absolute;top:2px;transition:transform .25s cubic-bezier(.4,0,.2,1),opacity .15s ease;width:var(--dvt-seg)}.dvt-indicator-hidden{opacity:0}.dvt-seg-host{display:flex;position:relative}.dvt-anchor{bottom:0;height:0;left:50%;position:absolute;width:0}.dvt-seg{align-items:center;border-radius:var(--ui-radius);color:var(--ui-text-muted);cursor:pointer;display:flex;height:var(--dvt-seg);justify-content:center;position:relative;transition:color .15s ease;width:var(--dvt-seg);z-index:1}.dvt-seg:hover:not(:disabled){color:var(--ui-text)}.dvt-seg.active{color:var(--ui-text-highlighted)}.dvt-seg:disabled{cursor:default;opacity:.4}.dvt-badge{background:var(--ui-color-error-500);border-radius:999px;color:#fff;font-size:9px;font-weight:700;height:15px;line-height:15px;min-width:15px;padding:0 3px;pointer-events:none;position:absolute;right:-7px;text-align:center;top:-6px}
234
+ </style>
@@ -0,0 +1,40 @@
1
+ export type DocViewTab = 'pageType' | 'editor' | 'chat' | 'settings';
2
+ type __VLS_Props = {
3
+ /** Active tab. `null` (or absent) = nothing highlighted (e.g. another tab is active). */
4
+ modelValue?: DocViewTab | null;
5
+ docId: string;
6
+ docType?: string;
7
+ chatUnread?: number;
8
+ /** Disable the editor segment (e.g. main view is already the editor). */
9
+ editorDisabled?: boolean;
10
+ /** Disable the page-type segment (e.g. provider not ready). */
11
+ pageTypeDisabled?: boolean;
12
+ /** Whether the user may assign/change the page type (gates the + dropdown). */
13
+ canChangeType?: boolean;
14
+ /** Render the chat slot. Off → omitted (e.g. features.chat disabled). */
15
+ showChat?: boolean;
16
+ /** Render the settings slot. Off → omitted. */
17
+ showSettings?: boolean;
18
+ /** Suppress tooltips (touch devices / during open animations). */
19
+ disableTooltips?: boolean;
20
+ size?: 'sm' | 'md';
21
+ };
22
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
+ select: (tab: DocViewTab) => any;
24
+ "update:modelValue": (tab: DocViewTab) => any;
25
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
26
+ onSelect?: ((tab: DocViewTab) => any) | undefined;
27
+ "onUpdate:modelValue"?: ((tab: DocViewTab) => any) | undefined;
28
+ }>, {
29
+ size: "sm" | "md";
30
+ modelValue: DocViewTab | null;
31
+ chatUnread: number;
32
+ editorDisabled: boolean;
33
+ pageTypeDisabled: boolean;
34
+ canChangeType: boolean;
35
+ showChat: boolean;
36
+ showSettings: boolean;
37
+ disableTooltips: boolean;
38
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
39
+ declare const _default: typeof __VLS_export;
40
+ export default _default;
@@ -424,7 +424,7 @@ function onDragStart(e, item) {
424
424
  if (!multiSelected.value.has(item.id)) multiSelected.value = /* @__PURE__ */ new Set();
425
425
  dragId.value = item.id;
426
426
  if (e.dataTransfer) {
427
- e.dataTransfer.effectAllowed = "move";
427
+ e.dataTransfer.effectAllowed = "all";
428
428
  e.dataTransfer.setData("text/plain", item.id);
429
429
  e.dataTransfer.setData(DOC_DRAG_MIME, JSON.stringify({ id: item.id, label: item.name }));
430
430
  }