@agent-native/core 0.123.1 → 0.124.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +20 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +38 -21
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +23 -20
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -56,7 +56,7 @@ import { buildRegistrySlashItems } from "./registrySlashItems";
|
|
|
56
56
|
interface SlashCommandMenuProps {
|
|
57
57
|
editor: Editor;
|
|
58
58
|
documentId?: string;
|
|
59
|
-
onDraftCommitted?: () => void | Promise<void>;
|
|
59
|
+
onDraftCommitted?: () => boolean | void | Promise<boolean | void>;
|
|
60
60
|
onDraftPersisted?: (markdown: string) => boolean | Promise<boolean>;
|
|
61
61
|
/**
|
|
62
62
|
* The open document's linked Notion page id, when it has one. When set, the
|
|
@@ -69,10 +69,76 @@ interface SlashCommandMenuProps {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
interface EditorMenuPosition {
|
|
72
|
-
top
|
|
72
|
+
top?: number;
|
|
73
|
+
bottom?: number;
|
|
73
74
|
left: number;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
const SLASH_MENU_PREFERRED_HEIGHT = 360;
|
|
78
|
+
const SLASH_MENU_GAP = 4;
|
|
79
|
+
|
|
80
|
+
export function getSlashMenuVerticalPosition(
|
|
81
|
+
anchor: { top: number; bottom: number },
|
|
82
|
+
container: { top: number; bottom: number },
|
|
83
|
+
viewportHeight: number,
|
|
84
|
+
): Pick<EditorMenuPosition, "top" | "bottom"> {
|
|
85
|
+
const spaceBelow = viewportHeight - anchor.bottom;
|
|
86
|
+
const spaceAbove = anchor.top;
|
|
87
|
+
if (spaceBelow < SLASH_MENU_PREFERRED_HEIGHT && spaceAbove > spaceBelow) {
|
|
88
|
+
return {
|
|
89
|
+
bottom: container.bottom - anchor.top + SLASH_MENU_GAP,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
top: anchor.bottom - container.top + SLASH_MENU_GAP,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getSlashMenuPosition(editor: Editor): EditorMenuPosition {
|
|
98
|
+
const wrapper = editor.view.dom.closest(".visual-editor-wrapper");
|
|
99
|
+
const containerRect = (
|
|
100
|
+
wrapper instanceof HTMLElement ? wrapper : editor.view.dom
|
|
101
|
+
).getBoundingClientRect();
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const coords = editor.view.coordsAtPos(editor.state.selection.from);
|
|
105
|
+
return {
|
|
106
|
+
...getSlashMenuVerticalPosition(
|
|
107
|
+
coords,
|
|
108
|
+
containerRect,
|
|
109
|
+
window.innerHeight,
|
|
110
|
+
),
|
|
111
|
+
left: coords.left - containerRect.left,
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
// Collaborative reconciliation can briefly leave ProseMirror's DOM mapping
|
|
115
|
+
// behind the document selection. The slash transaction is still valid; use
|
|
116
|
+
// its nearest DOM node (or the editor origin) so the command menu remains
|
|
117
|
+
// available instead of turning the user's slash into inert text.
|
|
118
|
+
try {
|
|
119
|
+
const domAtSelection = editor.view.domAtPos(editor.state.selection.from);
|
|
120
|
+
const element =
|
|
121
|
+
domAtSelection.node instanceof Element
|
|
122
|
+
? domAtSelection.node
|
|
123
|
+
: domAtSelection.node.parentElement;
|
|
124
|
+
const rect = element?.getBoundingClientRect();
|
|
125
|
+
if (rect) {
|
|
126
|
+
return {
|
|
127
|
+
...getSlashMenuVerticalPosition(
|
|
128
|
+
rect,
|
|
129
|
+
containerRect,
|
|
130
|
+
window.innerHeight,
|
|
131
|
+
),
|
|
132
|
+
left: rect.left - containerRect.left,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// The editor origin below is a safe, visible final fallback.
|
|
137
|
+
}
|
|
138
|
+
return { top: 4, left: 0 };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
76
142
|
interface EquationDraft {
|
|
77
143
|
displayMode: boolean;
|
|
78
144
|
insertionRange: { from: number; to: number };
|
|
@@ -93,6 +159,33 @@ export interface CommandItem {
|
|
|
93
159
|
) => void | boolean | Promise<void>;
|
|
94
160
|
}
|
|
95
161
|
|
|
162
|
+
export function excludeCommandsWithDuplicateTitles<T extends { title: string }>(
|
|
163
|
+
primaryCommands: readonly T[],
|
|
164
|
+
candidateCommands: readonly T[],
|
|
165
|
+
): T[] {
|
|
166
|
+
const primaryTitles = new Set(
|
|
167
|
+
primaryCommands.map((command) => command.title.trim().toLocaleLowerCase()),
|
|
168
|
+
);
|
|
169
|
+
return candidateCommands.filter(
|
|
170
|
+
(command) => !primaryTitles.has(command.title.trim().toLocaleLowerCase()),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type MediaPlaceholderType = "image" | "video" | "audio";
|
|
175
|
+
|
|
176
|
+
export function insertMediaPlaceholder(
|
|
177
|
+
editor: Editor,
|
|
178
|
+
type: MediaPlaceholderType,
|
|
179
|
+
) {
|
|
180
|
+
const attrs =
|
|
181
|
+
type === "image"
|
|
182
|
+
? { src: null, alt: "" }
|
|
183
|
+
: type === "video"
|
|
184
|
+
? { src: null, sourcePanelOpen: true }
|
|
185
|
+
: { src: null };
|
|
186
|
+
return editor.chain().focus().insertContent({ type, attrs }).run();
|
|
187
|
+
}
|
|
188
|
+
|
|
96
189
|
function getActiveSlashCommandRange(editor: Editor) {
|
|
97
190
|
const { state } = editor;
|
|
98
191
|
if (!state.selection.empty) return null;
|
|
@@ -219,7 +312,16 @@ export function parseInlineGeneratePrompt(textBeforeCursor: string) {
|
|
|
219
312
|
}
|
|
220
313
|
|
|
221
314
|
export function parseSlashCommandQuery(textBeforeCursor: string) {
|
|
222
|
-
|
|
315
|
+
const match = textBeforeCursor.match(
|
|
316
|
+
/^\s*\/([a-zA-Z0-9][a-zA-Z0-9 _-]*|)\s*$/,
|
|
317
|
+
);
|
|
318
|
+
if (!match) return null;
|
|
319
|
+
const rawQuery = match[1] ?? "";
|
|
320
|
+
// `/generate <prompt>` intentionally leaves the menu so Enter can submit the
|
|
321
|
+
// inline prompt. Other multi-word labels (for example `/heading 2`) remain
|
|
322
|
+
// searchable instead of turning into literal editor text at the first space.
|
|
323
|
+
if (/^generate\s+/i.test(rawQuery)) return null;
|
|
324
|
+
return rawQuery.trim();
|
|
223
325
|
}
|
|
224
326
|
|
|
225
327
|
export function inlineDatabaseBlockContent(
|
|
@@ -293,6 +395,15 @@ export function getEquationInsertionRange(
|
|
|
293
395
|
: slashRange;
|
|
294
396
|
}
|
|
295
397
|
|
|
398
|
+
export function setCodeBlockFromSlashCommand(
|
|
399
|
+
editor: Editor,
|
|
400
|
+
slashRange: { from: number; to: number } | null,
|
|
401
|
+
) {
|
|
402
|
+
const chain = editor.chain().focus();
|
|
403
|
+
if (slashRange) chain.deleteRange(slashRange);
|
|
404
|
+
return chain.setCodeBlock().run();
|
|
405
|
+
}
|
|
406
|
+
|
|
296
407
|
const commands: CommandTemplate[] = [
|
|
297
408
|
{
|
|
298
409
|
titleKey: "editor.slash.text",
|
|
@@ -345,7 +456,9 @@ const commands: CommandTemplate[] = [
|
|
|
345
456
|
descriptionKey: "editor.slash.codeBlockDescription",
|
|
346
457
|
shortcut: "```",
|
|
347
458
|
icon: IconCode,
|
|
348
|
-
|
|
459
|
+
preserveSlashRange: true,
|
|
460
|
+
action: (editor, { slashRange }) =>
|
|
461
|
+
setCodeBlockFromSlashCommand(editor, slashRange),
|
|
349
462
|
},
|
|
350
463
|
{
|
|
351
464
|
titleKey: "editor.slash.quote",
|
|
@@ -450,7 +563,9 @@ const turnIntoCommands: CommandTemplate[] = [
|
|
|
450
563
|
descriptionKey: "editor.slash.codeBlockDescription",
|
|
451
564
|
shortcut: "```",
|
|
452
565
|
icon: IconCode,
|
|
453
|
-
|
|
566
|
+
preserveSlashRange: true,
|
|
567
|
+
action: (editor, { slashRange }) =>
|
|
568
|
+
setCodeBlockFromSlashCommand(editor, slashRange),
|
|
454
569
|
},
|
|
455
570
|
{
|
|
456
571
|
titleKey: "editor.slash.quote",
|
|
@@ -545,16 +660,7 @@ export function SlashCommandMenu({
|
|
|
545
660
|
);
|
|
546
661
|
|
|
547
662
|
const getSelectionMenuPosition = useCallback(() => {
|
|
548
|
-
|
|
549
|
-
const editorRect = editor.view.dom
|
|
550
|
-
.closest(".visual-editor-wrapper")
|
|
551
|
-
?.getBoundingClientRect();
|
|
552
|
-
if (!editorRect) return null;
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
top: coords.bottom - editorRect.top + 4,
|
|
556
|
-
left: coords.left - editorRect.left,
|
|
557
|
-
};
|
|
663
|
+
return getSlashMenuPosition(editor);
|
|
558
664
|
}, [editor]);
|
|
559
665
|
|
|
560
666
|
const openGeneratePopover = useCallback(
|
|
@@ -598,11 +704,7 @@ export function SlashCommandMenu({
|
|
|
598
704
|
description: t("editor.slash.imageDescription"),
|
|
599
705
|
icon: IconPhoto,
|
|
600
706
|
action: (editor) => {
|
|
601
|
-
editor
|
|
602
|
-
.chain()
|
|
603
|
-
.focus()
|
|
604
|
-
.insertContent({ type: "image", attrs: { src: null, alt: "" } })
|
|
605
|
-
.run();
|
|
707
|
+
insertMediaPlaceholder(editor, "image");
|
|
606
708
|
},
|
|
607
709
|
};
|
|
608
710
|
|
|
@@ -611,11 +713,7 @@ export function SlashCommandMenu({
|
|
|
611
713
|
description: t("editor.slash.videoDescription"),
|
|
612
714
|
icon: IconVideo,
|
|
613
715
|
action: (editor) => {
|
|
614
|
-
editor
|
|
615
|
-
.chain()
|
|
616
|
-
.focus()
|
|
617
|
-
.insertContent({ type: "video", attrs: { src: null } })
|
|
618
|
-
.run();
|
|
716
|
+
insertMediaPlaceholder(editor, "video");
|
|
619
717
|
},
|
|
620
718
|
};
|
|
621
719
|
|
|
@@ -624,11 +722,7 @@ export function SlashCommandMenu({
|
|
|
624
722
|
description: t("editor.slash.audioDescription"),
|
|
625
723
|
icon: IconMusic,
|
|
626
724
|
action: (editor) => {
|
|
627
|
-
editor
|
|
628
|
-
.chain()
|
|
629
|
-
.focus()
|
|
630
|
-
.insertContent({ type: "audio", attrs: { src: null } })
|
|
631
|
-
.run();
|
|
725
|
+
insertMediaPlaceholder(editor, "audio");
|
|
632
726
|
},
|
|
633
727
|
};
|
|
634
728
|
|
|
@@ -855,6 +949,10 @@ export function SlashCommandMenu({
|
|
|
855
949
|
...(isTurnInto ? turnIntoCommands : commands).map(localizeCommand),
|
|
856
950
|
...equationCommands,
|
|
857
951
|
];
|
|
952
|
+
const uniqueRegistryCommands = excludeCommandsWithDuplicateTitles(
|
|
953
|
+
blockCommands,
|
|
954
|
+
registryCommands,
|
|
955
|
+
);
|
|
858
956
|
const pageCommands = isTurnInto ? [] : [pageCommand, databaseCommand];
|
|
859
957
|
const mediaCommands = isTurnInto
|
|
860
958
|
? []
|
|
@@ -866,11 +964,20 @@ export function SlashCommandMenu({
|
|
|
866
964
|
cmd.searchText?.toLowerCase().includes(normalizedQuery);
|
|
867
965
|
const filteredAiCommands = aiCommands.filter(commandMatchesQuery);
|
|
868
966
|
const filteredBlockCommands = blockCommands.filter(commandMatchesQuery);
|
|
869
|
-
const filteredRegistryCommands =
|
|
967
|
+
const filteredRegistryCommands =
|
|
968
|
+
uniqueRegistryCommands.filter(commandMatchesQuery);
|
|
870
969
|
const filteredLocalComponentCommands =
|
|
871
970
|
localComponentCommands.filter(commandMatchesQuery);
|
|
872
971
|
const filteredPageCommands = pageCommands.filter(commandMatchesQuery);
|
|
873
972
|
const filteredMediaCommands = mediaCommands.filter(commandMatchesQuery);
|
|
973
|
+
const allCommands = [
|
|
974
|
+
...aiCommands,
|
|
975
|
+
...blockCommands,
|
|
976
|
+
...uniqueRegistryCommands,
|
|
977
|
+
...localComponentCommands,
|
|
978
|
+
...mediaCommands,
|
|
979
|
+
...pageCommands,
|
|
980
|
+
];
|
|
874
981
|
const filteredCommands = [
|
|
875
982
|
...filteredAiCommands,
|
|
876
983
|
...filteredBlockCommands,
|
|
@@ -899,6 +1006,8 @@ export function SlashCommandMenu({
|
|
|
899
1006
|
|
|
900
1007
|
const executeCommand = useCallback(
|
|
901
1008
|
async (cmd: CommandItem) => {
|
|
1009
|
+
if (editor.isDestroyed) return;
|
|
1010
|
+
const beforeDoc = editor.state.doc;
|
|
902
1011
|
const slashRange =
|
|
903
1012
|
getActiveSlashCommandRange(editor) ??
|
|
904
1013
|
(slashPosRef.current !== null
|
|
@@ -912,14 +1021,52 @@ export function SlashCommandMenu({
|
|
|
912
1021
|
setQuery("");
|
|
913
1022
|
slashPosRef.current = null;
|
|
914
1023
|
await cmd.action(editor, { slashRange });
|
|
1024
|
+
// Structural slash commands (especially an empty table) can be followed
|
|
1025
|
+
// immediately by another modal command or navigation before the normal
|
|
1026
|
+
// debounced onUpdate save settles. Persist the completed command now so
|
|
1027
|
+
// the durable snapshot cannot omit the block. Media placeholders are
|
|
1028
|
+
// still held by VisualEditor's pending-media guard until they have a src.
|
|
1029
|
+
if (!editor.isDestroyed && !editor.state.doc.eq(beforeDoc)) {
|
|
1030
|
+
const persisted = await onDraftCommitted?.();
|
|
1031
|
+
if (persisted === false) {
|
|
1032
|
+
toast.error(t("empty.genericError"));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
915
1035
|
},
|
|
916
|
-
[editor],
|
|
1036
|
+
[editor, onDraftCommitted, t],
|
|
917
1037
|
);
|
|
918
1038
|
|
|
919
1039
|
useEffect(() => {
|
|
920
1040
|
if (!editor) return;
|
|
921
1041
|
|
|
922
1042
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
1043
|
+
if (
|
|
1044
|
+
e.key === "Enter" &&
|
|
1045
|
+
!e.shiftKey &&
|
|
1046
|
+
!e.metaKey &&
|
|
1047
|
+
!e.ctrlKey &&
|
|
1048
|
+
!e.altKey
|
|
1049
|
+
) {
|
|
1050
|
+
const slashRange = getActiveSlashCommandRange(editor);
|
|
1051
|
+
const liveQuery = slashRange
|
|
1052
|
+
? editor.state.doc
|
|
1053
|
+
.textBetween(slashRange.from + 1, slashRange.to, "\n")
|
|
1054
|
+
.trim()
|
|
1055
|
+
.toLowerCase()
|
|
1056
|
+
: "";
|
|
1057
|
+
const exactCommand = liveQuery
|
|
1058
|
+
? allCommands.find(
|
|
1059
|
+
(command) => command.title.toLowerCase() === liveQuery,
|
|
1060
|
+
)
|
|
1061
|
+
: undefined;
|
|
1062
|
+
if (exactCommand) {
|
|
1063
|
+
e.preventDefault();
|
|
1064
|
+
e.stopPropagation();
|
|
1065
|
+
void executeCommand(exactCommand);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
923
1070
|
if (!isOpen) {
|
|
924
1071
|
if (
|
|
925
1072
|
e.key === "Enter" &&
|
|
@@ -986,6 +1133,7 @@ export function SlashCommandMenu({
|
|
|
986
1133
|
openGeneratePopover,
|
|
987
1134
|
readInlineGenerateCommand,
|
|
988
1135
|
submitGeneratePrompt,
|
|
1136
|
+
allCommands,
|
|
989
1137
|
]);
|
|
990
1138
|
|
|
991
1139
|
useEffect(() => {
|
|
@@ -1043,16 +1191,7 @@ export function SlashCommandMenu({
|
|
|
1043
1191
|
const slashAtBlockStart = offsetInParent === 0;
|
|
1044
1192
|
setIsTurnInto(slashAtBlockStart && blockHasOtherContent);
|
|
1045
1193
|
|
|
1046
|
-
|
|
1047
|
-
const editorRect = editor.view.dom
|
|
1048
|
-
.closest(".visual-editor-wrapper")
|
|
1049
|
-
?.getBoundingClientRect();
|
|
1050
|
-
if (editorRect) {
|
|
1051
|
-
setPosition({
|
|
1052
|
-
top: coords.bottom - editorRect.top + 4,
|
|
1053
|
-
left: coords.left - editorRect.left,
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1194
|
+
setPosition(getSlashMenuPosition(editor));
|
|
1056
1195
|
setIsOpen(true);
|
|
1057
1196
|
} else {
|
|
1058
1197
|
if (isOpen) {
|
|
@@ -1070,6 +1209,36 @@ export function SlashCommandMenu({
|
|
|
1070
1209
|
};
|
|
1071
1210
|
}, [editor, isOpen]);
|
|
1072
1211
|
|
|
1212
|
+
useEffect(() => {
|
|
1213
|
+
if (!isOpen || editor.isDestroyed) return;
|
|
1214
|
+
|
|
1215
|
+
let frame = 0;
|
|
1216
|
+
const updatePosition = () => {
|
|
1217
|
+
cancelAnimationFrame(frame);
|
|
1218
|
+
frame = requestAnimationFrame(() => {
|
|
1219
|
+
if (!editor.isDestroyed) {
|
|
1220
|
+
setPosition(getSlashMenuPosition(editor));
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// ProseMirror can scroll any ancestor after the slash transaction to reveal
|
|
1226
|
+
// the caret. Recalculate after layout and capture those non-bubbling scroll
|
|
1227
|
+
// events so a menu near the viewport edge flips using current geometry.
|
|
1228
|
+
updatePosition();
|
|
1229
|
+
document.addEventListener("scroll", updatePosition, true);
|
|
1230
|
+
window.addEventListener("resize", updatePosition);
|
|
1231
|
+
window.visualViewport?.addEventListener("resize", updatePosition);
|
|
1232
|
+
window.visualViewport?.addEventListener("scroll", updatePosition);
|
|
1233
|
+
return () => {
|
|
1234
|
+
cancelAnimationFrame(frame);
|
|
1235
|
+
document.removeEventListener("scroll", updatePosition, true);
|
|
1236
|
+
window.removeEventListener("resize", updatePosition);
|
|
1237
|
+
window.visualViewport?.removeEventListener("resize", updatePosition);
|
|
1238
|
+
window.visualViewport?.removeEventListener("scroll", updatePosition);
|
|
1239
|
+
};
|
|
1240
|
+
}, [editor, isOpen]);
|
|
1241
|
+
|
|
1073
1242
|
return (
|
|
1074
1243
|
<>
|
|
1075
1244
|
{/* Slash command menu */}
|
|
@@ -1080,8 +1249,11 @@ export function SlashCommandMenu({
|
|
|
1080
1249
|
style={{
|
|
1081
1250
|
position: "absolute",
|
|
1082
1251
|
top: position.top,
|
|
1252
|
+
bottom: position.bottom,
|
|
1083
1253
|
left: 0,
|
|
1084
1254
|
right: 0,
|
|
1255
|
+
maxHeight: "min(360px, calc(100vh - 2rem))",
|
|
1256
|
+
overflowY: "auto",
|
|
1085
1257
|
maxWidth: "min(330px, calc(100vw - 2rem))",
|
|
1086
1258
|
marginLeft: Math.min(position.left, 16),
|
|
1087
1259
|
zIndex: 50,
|
|
@@ -1293,7 +1465,7 @@ export function SlashCommandMenu({
|
|
|
1293
1465
|
);
|
|
1294
1466
|
}
|
|
1295
1467
|
|
|
1296
|
-
function CommandButton({
|
|
1468
|
+
export function CommandButton({
|
|
1297
1469
|
cmd,
|
|
1298
1470
|
isSelected,
|
|
1299
1471
|
buttonRef,
|
|
@@ -1306,13 +1478,31 @@ function CommandButton({
|
|
|
1306
1478
|
onExecute: () => void;
|
|
1307
1479
|
onHover: () => void;
|
|
1308
1480
|
}) {
|
|
1481
|
+
const pendingExecutionRef = useRef(false);
|
|
1482
|
+
const onExecuteRef = useRef(onExecute);
|
|
1483
|
+
onExecuteRef.current = onExecute;
|
|
1484
|
+
|
|
1485
|
+
const executeOnce = () => {
|
|
1486
|
+
if (pendingExecutionRef.current) return;
|
|
1487
|
+
pendingExecutionRef.current = true;
|
|
1488
|
+
// Pointer selection can close and unmount the menu before the browser
|
|
1489
|
+
// dispatches `click`. Start the command during mouse down, while the
|
|
1490
|
+
// editor selection and button are both still alive. Keep `onClick` as the
|
|
1491
|
+
// keyboard-generated click fallback and dedupe the normal pointer click.
|
|
1492
|
+
onExecuteRef.current();
|
|
1493
|
+
queueMicrotask(() => {
|
|
1494
|
+
pendingExecutionRef.current = false;
|
|
1495
|
+
});
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1309
1498
|
return (
|
|
1310
1499
|
<button
|
|
1311
1500
|
ref={buttonRef}
|
|
1312
1501
|
onMouseDown={(event) => {
|
|
1313
1502
|
event.preventDefault();
|
|
1314
|
-
|
|
1503
|
+
if (event.button === 0) executeOnce();
|
|
1315
1504
|
}}
|
|
1505
|
+
onClick={executeOnce}
|
|
1316
1506
|
onMouseEnter={onHover}
|
|
1317
1507
|
className={cn(
|
|
1318
1508
|
"flex min-h-9 w-full items-center gap-3 px-3 py-1 text-left transition-colors",
|