@abraca/nuxt 2.13.0 → 2.14.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 +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/components/ACodeEditor.vue +123 -22
- package/dist/runtime/components/ADocViewToggle.d.vue.ts +40 -0
- package/dist/runtime/components/ADocViewToggle.vue +234 -0
- package/dist/runtime/components/ADocViewToggle.vue.d.ts +40 -0
- package/dist/runtime/components/ADocumentTree.vue +1 -1
- package/dist/runtime/components/AEditor.vue +183 -15
- package/dist/runtime/components/ANodePanel.vue +84 -86
- package/dist/runtime/components/editor/ADocSuggestMenu.d.vue.ts +7 -0
- package/dist/runtime/components/editor/ADocSuggestMenu.vue +68 -0
- package/dist/runtime/components/editor/ADocSuggestMenu.vue.d.ts +7 -0
- package/dist/runtime/composables/useDocLinkPick.d.ts +9 -8
- package/dist/runtime/composables/useDocLinkPick.js +7 -18
- package/dist/runtime/composables/useDocSuggest.d.ts +34 -0
- package/dist/runtime/composables/useDocSuggest.js +56 -0
- package/dist/runtime/extensions/doc-link-drop.js +2 -2
- package/dist/runtime/extensions/doc-suggest.d.ts +28 -0
- package/dist/runtime/extensions/doc-suggest.js +85 -0
- package/dist/runtime/utils/codeHighlightStyle.d.ts +15 -0
- package/dist/runtime/utils/codeHighlightStyle.js +34 -0
- package/dist/runtime/utils/loadCodeMirror.d.ts +1 -0
- package/dist/runtime/utils/loadCodeMirror.js +6 -3
- 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
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,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,134 @@ 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: "
|
|
58
|
+
fontFamily: 'var(--font-code, ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace) !important',
|
|
55
59
|
caretColor: "var(--ui-text-highlighted)",
|
|
56
60
|
padding: "8px 0"
|
|
57
61
|
},
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
// Local caret — `drawSelection()` hides the native caret and draws this
|
|
63
|
+
// bar, so style it to read like the native caret used elsewhere in the
|
|
64
|
+
// app: a crisp 2px stroke in the body text colour.
|
|
65
|
+
".cm-cursor, .cm-dropCursor": {
|
|
66
|
+
borderLeftColor: "var(--ui-text-highlighted)",
|
|
67
|
+
borderLeftWidth: "2px"
|
|
60
68
|
},
|
|
61
69
|
".cm-activeLine": {
|
|
62
70
|
backgroundColor: "color-mix(in srgb, var(--ui-bg-elevated) 50%, transparent)"
|
|
63
71
|
},
|
|
64
72
|
".cm-activeLineGutter": {
|
|
65
|
-
backgroundColor: "color-mix(in srgb, var(--ui-bg-elevated) 50%, transparent)"
|
|
73
|
+
backgroundColor: "color-mix(in srgb, var(--ui-bg-elevated) 50%, transparent)",
|
|
74
|
+
color: "var(--ui-text)"
|
|
66
75
|
},
|
|
67
76
|
".cm-gutters": {
|
|
68
|
-
fontFamily: "
|
|
77
|
+
fontFamily: 'var(--font-code, ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace) !important',
|
|
69
78
|
backgroundColor: "var(--ui-bg)",
|
|
70
79
|
color: "var(--ui-text-dimmed)",
|
|
71
80
|
border: "none",
|
|
72
81
|
borderRight: "1px solid var(--ui-border)"
|
|
73
82
|
},
|
|
83
|
+
".cm-foldPlaceholder": {
|
|
84
|
+
backgroundColor: "var(--ui-bg-elevated)",
|
|
85
|
+
color: "var(--ui-text-muted)",
|
|
86
|
+
border: "1px solid var(--ui-border)"
|
|
87
|
+
},
|
|
88
|
+
// Local selection — translucent primary tint, brighter while focused.
|
|
74
89
|
".cm-selectionBackground": {
|
|
75
90
|
backgroundColor: "color-mix(in srgb, var(--ui-primary) 25%, transparent) !important"
|
|
76
91
|
},
|
|
77
92
|
"&.cm-focused .cm-selectionBackground": {
|
|
78
|
-
backgroundColor: "color-mix(in srgb, var(--ui-primary)
|
|
93
|
+
backgroundColor: "color-mix(in srgb, var(--ui-primary) 40%, transparent) !important"
|
|
79
94
|
},
|
|
80
95
|
".cm-matchingBracket": {
|
|
81
96
|
backgroundColor: "color-mix(in srgb, var(--ui-primary) 20%, transparent)",
|
|
82
97
|
outline: "1px solid color-mix(in srgb, var(--ui-primary) 40%, transparent)"
|
|
83
98
|
},
|
|
99
|
+
// Occurrences of the current selection (highlightSelectionMatches).
|
|
100
|
+
".cm-selectionMatch": {
|
|
101
|
+
backgroundColor: "color-mix(in srgb, var(--ui-primary) 15%, transparent)"
|
|
102
|
+
},
|
|
103
|
+
// Find/replace panel + search hits — themed to the UI tokens.
|
|
104
|
+
".cm-panels": {
|
|
105
|
+
backgroundColor: "var(--ui-bg-elevated)",
|
|
106
|
+
color: "var(--ui-text)"
|
|
107
|
+
},
|
|
108
|
+
".cm-panels.cm-panels-top": { borderBottom: "1px solid var(--ui-border)" },
|
|
109
|
+
".cm-panels.cm-panels-bottom": { borderTop: "1px solid var(--ui-border)" },
|
|
110
|
+
".cm-searchMatch": {
|
|
111
|
+
backgroundColor: "color-mix(in srgb, var(--ui-primary) 25%, transparent)",
|
|
112
|
+
outline: "1px solid color-mix(in srgb, var(--ui-primary) 45%, transparent)",
|
|
113
|
+
borderRadius: "2px"
|
|
114
|
+
},
|
|
115
|
+
".cm-searchMatch-selected": {
|
|
116
|
+
backgroundColor: "color-mix(in srgb, var(--ui-primary) 45%, transparent) !important"
|
|
117
|
+
},
|
|
118
|
+
".cm-textfield": {
|
|
119
|
+
backgroundColor: "var(--ui-bg)",
|
|
120
|
+
color: "var(--ui-text)",
|
|
121
|
+
border: "1px solid var(--ui-border)",
|
|
122
|
+
borderRadius: "4px"
|
|
123
|
+
},
|
|
124
|
+
".cm-button": {
|
|
125
|
+
backgroundColor: "var(--ui-bg)",
|
|
126
|
+
backgroundImage: "none",
|
|
127
|
+
color: "var(--ui-text)",
|
|
128
|
+
border: "1px solid var(--ui-border)",
|
|
129
|
+
borderRadius: "4px"
|
|
130
|
+
},
|
|
131
|
+
// Autocomplete tooltip.
|
|
132
|
+
".cm-tooltip": {
|
|
133
|
+
backgroundColor: "var(--ui-bg-elevated)",
|
|
134
|
+
color: "var(--ui-text)",
|
|
135
|
+
border: "1px solid var(--ui-border)",
|
|
136
|
+
borderRadius: "6px"
|
|
137
|
+
},
|
|
138
|
+
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
|
|
139
|
+
backgroundColor: "var(--ui-primary)",
|
|
140
|
+
color: "var(--ui-bg)"
|
|
141
|
+
},
|
|
142
|
+
// ── Remote presence (y-codemirror.next) — matches the TipTap editor's
|
|
143
|
+
// `.collaboration-carets__*` look so multiplayer feels identical across
|
|
144
|
+
// both editors. Per-user colour stays inline (set from awareness
|
|
145
|
+
// `user.color`); we restyle shape only. The caret already matches TipTap
|
|
146
|
+
// (1px border bars, -1px margins); we only fix the label badge + hide the
|
|
147
|
+
// caret dot.
|
|
148
|
+
".cm-ySelection": { borderRadius: "1px" },
|
|
149
|
+
".cm-ySelectionCaretDot": { display: "none" },
|
|
84
150
|
".cm-ySelectionInfo": {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
padding: "1px 4px",
|
|
88
|
-
borderRadius: "3px",
|
|
89
|
-
opacity: "0.9",
|
|
90
|
-
position: "absolute",
|
|
91
|
-
top: "-1.3em",
|
|
151
|
+
opacity: "1",
|
|
152
|
+
top: "-1.4em",
|
|
92
153
|
left: "-1px",
|
|
154
|
+
padding: "0.1rem 0.3rem",
|
|
155
|
+
borderRadius: "3px 3px 3px 0",
|
|
156
|
+
fontFamily: "system-ui, sans-serif",
|
|
157
|
+
fontSize: "12px",
|
|
158
|
+
fontStyle: "normal",
|
|
159
|
+
fontWeight: "600",
|
|
160
|
+
lineHeight: "normal",
|
|
161
|
+
color: "#0d0d0d",
|
|
93
162
|
whiteSpace: "nowrap",
|
|
94
|
-
|
|
95
|
-
}
|
|
163
|
+
transitionDelay: "0s"
|
|
164
|
+
},
|
|
165
|
+
".cm-ySelectionCaret:hover > .cm-ySelectionInfo": { opacity: "1" }
|
|
96
166
|
}, { dark: true });
|
|
97
167
|
}
|
|
98
168
|
let xmlObserverCleanup = null;
|
|
169
|
+
let syncedOff = null;
|
|
99
170
|
function destroyEditor() {
|
|
100
171
|
if (xmlObserverCleanup) {
|
|
101
172
|
xmlObserverCleanup();
|
|
102
173
|
xmlObserverCleanup = null;
|
|
103
174
|
}
|
|
175
|
+
if (syncedOff) {
|
|
176
|
+
syncedOff();
|
|
177
|
+
syncedOff = null;
|
|
178
|
+
}
|
|
104
179
|
if (editorView.value) {
|
|
105
180
|
editorView.value.destroy();
|
|
106
181
|
editorView.value = null;
|
|
@@ -110,7 +185,7 @@ function createEditor(bundle, container) {
|
|
|
110
185
|
const { EditorView, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection, rectangularSelection, crosshairCursor, highlightSpecialChars, dropCursor, keymap } = bundle.view;
|
|
111
186
|
const { EditorState } = bundle.state;
|
|
112
187
|
const { defaultKeymap, history, historyKeymap, indentWithTab } = bundle.commands;
|
|
113
|
-
const { syntaxHighlighting,
|
|
188
|
+
const { syntaxHighlighting, HighlightStyle, indentOnInput, bracketMatching, foldGutter, foldKeymap } = bundle.language;
|
|
114
189
|
const { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } = bundle.autocomplete;
|
|
115
190
|
const { searchKeymap, highlightSelectionMatches } = bundle.search;
|
|
116
191
|
const baseExtensions = [
|
|
@@ -122,7 +197,7 @@ function createEditor(bundle, container) {
|
|
|
122
197
|
dropCursor(),
|
|
123
198
|
EditorState.allowMultipleSelections.of(true),
|
|
124
199
|
indentOnInput(),
|
|
125
|
-
syntaxHighlighting(
|
|
200
|
+
syntaxHighlighting(buildAtomOneNuxtHighlight(HighlightStyle, bundle.lezerHighlight.tags), { fallback: true }),
|
|
126
201
|
bracketMatching(),
|
|
127
202
|
closeBrackets(),
|
|
128
203
|
autocompletion(),
|
|
@@ -168,9 +243,14 @@ function createEditor(bundle, container) {
|
|
|
168
243
|
const ytext = ydoc.getText(props.fieldName);
|
|
169
244
|
const awareness = prov.awareness;
|
|
170
245
|
if (awareness && userName?.value && publicKeyB64?.value) {
|
|
246
|
+
const color = userColor?.value ?? "#888";
|
|
171
247
|
awareness.setLocalStateField("user", {
|
|
172
248
|
name: userName.value,
|
|
173
|
-
color
|
|
249
|
+
color,
|
|
250
|
+
// Translucent fill for remote selection blocks (y-codemirror.next).
|
|
251
|
+
// Its default `color + '33'` is invalid for hsl()/named/var() colours,
|
|
252
|
+
// leaving the selection invisible — supply a format-agnostic value.
|
|
253
|
+
colorLight: `color-mix(in srgb, ${color} 30%, transparent)`,
|
|
174
254
|
publicKey: publicKeyB64.value
|
|
175
255
|
});
|
|
176
256
|
}
|
|
@@ -180,10 +260,25 @@ function createEditor(bundle, container) {
|
|
|
180
260
|
} else {
|
|
181
261
|
extensions.push(history());
|
|
182
262
|
}
|
|
183
|
-
|
|
263
|
+
const view = new EditorView({
|
|
184
264
|
state: EditorState.create({ doc: ytext.toString(), extensions }),
|
|
185
265
|
parent: container
|
|
186
266
|
});
|
|
267
|
+
let rebuiltOnSync = false;
|
|
268
|
+
const onSynced = () => {
|
|
269
|
+
if (rebuiltOnSync) return;
|
|
270
|
+
const v = editorView.value;
|
|
271
|
+
const liveYtext = prov.document?.getText?.(props.fieldName);
|
|
272
|
+
if (!v || !liveYtext) return;
|
|
273
|
+
if (liveYtext.toString() !== v.state.doc.toString()) {
|
|
274
|
+
rebuiltOnSync = true;
|
|
275
|
+
void mount();
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
prov.on?.("synced", onSynced);
|
|
279
|
+
syncedOff = () => prov.off?.("synced", onSynced);
|
|
280
|
+
if (prov.isSynced) Promise.resolve().then(onSynced);
|
|
281
|
+
return view;
|
|
187
282
|
}
|
|
188
283
|
return new EditorView({
|
|
189
284
|
state: EditorState.create({ doc: "", extensions: [...baseExtensions, history()] }),
|
|
@@ -224,7 +319,13 @@ watch(
|
|
|
224
319
|
([n, c, k]) => {
|
|
225
320
|
const awareness = props.provider?.awareness;
|
|
226
321
|
if (!awareness || !n || !k) return;
|
|
227
|
-
|
|
322
|
+
const color = c ?? "#888";
|
|
323
|
+
awareness.setLocalStateField("user", {
|
|
324
|
+
name: n,
|
|
325
|
+
color,
|
|
326
|
+
colorLight: `color-mix(in srgb, ${color} 30%, transparent)`,
|
|
327
|
+
publicKey: k
|
|
328
|
+
});
|
|
228
329
|
}
|
|
229
330
|
);
|
|
230
331
|
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 = "
|
|
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
|
}
|