@agent-native/toolkit 0.6.0 → 0.8.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/README.md +17 -1
- package/agent-native.eject.json +411 -0
- package/dist/chat-history/ChatHistoryList.d.ts +103 -0
- package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryList.js +109 -0
- package/dist/chat-history/ChatHistoryList.js.map +1 -0
- package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
- package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryList.spec.js +283 -0
- package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
- package/dist/chat-history/index.d.ts +2 -0
- package/dist/chat-history/index.d.ts.map +1 -0
- package/dist/chat-history/index.js +2 -0
- package/dist/chat-history/index.js.map +1 -0
- package/dist/chat-history.css +314 -0
- package/package.json +20 -1
- package/src/app-shell/header-actions.tsx +84 -0
- package/src/app-shell/index.ts +1 -0
- package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
- package/src/chat-history/ChatHistoryList.tsx +469 -0
- package/src/chat-history/index.ts +7 -0
- package/src/chat-history.css +314 -0
- package/src/collab-ui/AgentPresenceChip.tsx +69 -0
- package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
- package/src/collab-ui/PresenceBar.tsx +346 -0
- package/src/collab-ui/RecentEditHighlights.tsx +225 -0
- package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
- package/src/collab-ui/agent-identity.ts +16 -0
- package/src/collab-ui/index.ts +39 -0
- package/src/collab-ui/lead-client.ts +32 -0
- package/src/collab-ui/types.spec.ts +18 -0
- package/src/collab-ui/types.ts +96 -0
- package/src/composer/AgentComposerFrame.tsx +59 -0
- package/src/composer/ComposerPlusMenu.tsx +983 -0
- package/src/composer/MentionPopover.tsx +412 -0
- package/src/composer/PastedTextChip.tsx +150 -0
- package/src/composer/PromptComposer.spec.ts +73 -0
- package/src/composer/PromptComposer.tsx +718 -0
- package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
- package/src/composer/RealtimeVoiceMode.tsx +856 -0
- package/src/composer/TiptapComposer.spec.ts +390 -0
- package/src/composer/TiptapComposer.tsx +2936 -0
- package/src/composer/VoiceButton.spec.ts +33 -0
- package/src/composer/VoiceButton.tsx +255 -0
- package/src/composer/asset-picker-url.spec.ts +56 -0
- package/src/composer/asset-picker-url.ts +80 -0
- package/src/composer/attachment-accept.spec.ts +22 -0
- package/src/composer/attachment-accept.ts +71 -0
- package/src/composer/draft-key.spec.ts +19 -0
- package/src/composer/draft-key.ts +7 -0
- package/src/composer/extensions/FileReference.tsx +55 -0
- package/src/composer/extensions/MentionReference.tsx +93 -0
- package/src/composer/extensions/SkillReference.tsx +56 -0
- package/src/composer/index.ts +79 -0
- package/src/composer/pasted-text.spec.ts +162 -0
- package/src/composer/pasted-text.ts +160 -0
- package/src/composer/prompt-attachments.spec.ts +80 -0
- package/src/composer/prompt-attachments.ts +93 -0
- package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
- package/src/composer/realtime-voice-audio-level.ts +64 -0
- package/src/composer/realtime-voice-transcript.spec.ts +95 -0
- package/src/composer/realtime-voice-transcript.ts +134 -0
- package/src/composer/runtime-adapters.tsx +343 -0
- package/src/composer/types.ts +66 -0
- package/src/composer/use-file-search.ts +64 -0
- package/src/composer/use-mention-search.ts +90 -0
- package/src/composer/use-skills.ts +45 -0
- package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
- package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
- package/src/composer/useVoiceDictation.spec.ts +45 -0
- package/src/composer/useVoiceDictation.ts +1191 -0
- package/src/context-ui/ContextMeter.tsx +101 -0
- package/src/context-ui/ContextSegmentRow.tsx +184 -0
- package/src/context-ui/ContextTreemap.tsx +125 -0
- package/src/context-ui/ContextXRayPanel.tsx +377 -0
- package/src/context-ui/format.ts +42 -0
- package/src/context-ui/index.ts +15 -0
- package/src/context-ui/types.ts +43 -0
- package/src/design-tweaks/index.ts +1 -0
- package/src/design-tweaks/visual-style-controls.tsx +1084 -0
- package/src/editor/BubbleToolbar.tsx +253 -0
- package/src/editor/CodeBlockNode.tsx +254 -0
- package/src/editor/DragHandle.spec.ts +588 -0
- package/src/editor/DragHandle.ts +1437 -0
- package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
- package/src/editor/ImageExtension.ts +306 -0
- package/src/editor/RegistryBlockContext.tsx +107 -0
- package/src/editor/RegistryBlockNode.spec.ts +279 -0
- package/src/editor/RegistryBlockNode.tsx +652 -0
- package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
- package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
- package/src/editor/RichMarkdownEditor.tsx +81 -0
- package/src/editor/RunId.ts +61 -0
- package/src/editor/SharedRichEditor.tsx +321 -0
- package/src/editor/SlashCommandMenu.tsx +310 -0
- package/src/editor/disableHistory.spec.ts +85 -0
- package/src/editor/extensions.ts +402 -0
- package/src/editor/gfmDoc.spec.ts +230 -0
- package/src/editor/gfmDoc.ts +92 -0
- package/src/editor/index.ts +93 -0
- package/src/editor/registrySlashCommands.ts +131 -0
- package/src/editor/structuralReorderUndo.spec.ts +144 -0
- package/src/editor/surgical-apply.spec.ts +279 -0
- package/src/editor/surgical-apply.ts +192 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
- package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
- package/src/editor/useCollabReconcile.ts +589 -0
- package/src/editor.css +510 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +188 -0
- package/src/index.ts +8 -0
- package/src/onboarding/index.ts +1 -0
- package/src/provider.tsx +47 -0
- package/src/sharing/VisibilityBadge.spec.ts +96 -0
- package/src/sharing/VisibilityBadge.tsx +37 -0
- package/src/sharing/index.ts +4 -0
- package/src/styles.css +21 -0
- package/src/ui/accordion.tsx +56 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/aspect-ratio.tsx +5 -0
- package/src/ui/avatar.tsx +48 -0
- package/src/ui/badge.tsx +37 -0
- package/src/ui/breadcrumb.tsx +115 -0
- package/src/ui/button.tsx +78 -0
- package/src/ui/calendar.tsx +214 -0
- package/src/ui/card.tsx +86 -0
- package/src/ui/carousel.tsx +260 -0
- package/src/ui/chart.tsx +375 -0
- package/src/ui/checkbox.tsx +28 -0
- package/src/ui/collapsible.tsx +9 -0
- package/src/ui/command.spec.tsx +51 -0
- package/src/ui/command.tsx +174 -0
- package/src/ui/context-menu.tsx +202 -0
- package/src/ui/date-picker.tsx +64 -0
- package/src/ui/dialog.spec.tsx +43 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/drawer.tsx +126 -0
- package/src/ui/dropdown-menu.tsx +218 -0
- package/src/ui/form.tsx +177 -0
- package/src/ui/hover-card.tsx +27 -0
- package/src/ui/index.ts +52 -0
- package/src/ui/input-otp.tsx +69 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/logo.tsx +36 -0
- package/src/ui/menubar.tsx +235 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.tsx +121 -0
- package/src/ui/popover.tsx +44 -0
- package/src/ui/progress.tsx +26 -0
- package/src/ui/radio-group.tsx +42 -0
- package/src/ui/resizable.tsx +43 -0
- package/src/ui/scroll-area.tsx +46 -0
- package/src/ui/select.tsx +158 -0
- package/src/ui/separator.tsx +29 -0
- package/src/ui/sheet.tsx +160 -0
- package/src/ui/sidebar.tsx +777 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/slider.tsx +26 -0
- package/src/ui/sonner.tsx +58 -0
- package/src/ui/spinner.tsx +17 -0
- package/src/ui/switch.tsx +27 -0
- package/src/ui/table.tsx +117 -0
- package/src/ui/tabs.tsx +53 -0
- package/src/ui/textarea.tsx +23 -0
- package/src/ui/toast.tsx +127 -0
- package/src/ui/toaster.tsx +33 -0
- package/src/ui/toggle-group.tsx +59 -0
- package/src/ui/toggle.tsx +43 -0
- package/src/ui/tooltip.tsx +53 -0
- package/src/ui/use-toast.ts +3 -0
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
2
|
+
import type { Editor } from "@tiptap/react";
|
|
3
|
+
import { useEffect, useState, type CSSProperties } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../utils.js";
|
|
6
|
+
|
|
7
|
+
/** A bubble-toolbar button or a divider. */
|
|
8
|
+
export type BubbleToolbarItem =
|
|
9
|
+
| {
|
|
10
|
+
/** Short label/glyph shown on the button. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Accessible/title text. */
|
|
13
|
+
title: string;
|
|
14
|
+
action: () => void;
|
|
15
|
+
isActive: () => boolean;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
| { type: "divider" };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds the default selection-toolbar items (Plan's current set): bold,
|
|
22
|
+
* italic, strike, code, headings 1-3, and a link toggle. `toggleLink` is
|
|
23
|
+
* supplied by the toolbar so the link-editor input can be opened.
|
|
24
|
+
*/
|
|
25
|
+
export function buildDefaultBubbleItems(
|
|
26
|
+
editor: Editor,
|
|
27
|
+
toggleLink: () => void,
|
|
28
|
+
): BubbleToolbarItem[] {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
label: "B",
|
|
32
|
+
title: "Bold",
|
|
33
|
+
action: () => editor.chain().focus().toggleBold().run(),
|
|
34
|
+
isActive: () => editor.isActive("bold"),
|
|
35
|
+
style: { fontWeight: 700 },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: "I",
|
|
39
|
+
title: "Italic",
|
|
40
|
+
action: () => editor.chain().focus().toggleItalic().run(),
|
|
41
|
+
isActive: () => editor.isActive("italic"),
|
|
42
|
+
style: { fontStyle: "italic" },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "S",
|
|
46
|
+
title: "Strikethrough",
|
|
47
|
+
action: () => editor.chain().focus().toggleStrike().run(),
|
|
48
|
+
isActive: () => editor.isActive("strike"),
|
|
49
|
+
style: { textDecoration: "line-through" },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "<>",
|
|
53
|
+
title: "Code",
|
|
54
|
+
action: () => editor.chain().focus().toggleCode().run(),
|
|
55
|
+
isActive: () => editor.isActive("code"),
|
|
56
|
+
style: { fontFamily: "monospace", fontSize: 11 },
|
|
57
|
+
},
|
|
58
|
+
{ type: "divider" },
|
|
59
|
+
{
|
|
60
|
+
label: "H1",
|
|
61
|
+
title: "Heading 1",
|
|
62
|
+
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
63
|
+
isActive: () => editor.isActive("heading", { level: 1 }),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "H2",
|
|
67
|
+
title: "Heading 2",
|
|
68
|
+
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
69
|
+
isActive: () => editor.isActive("heading", { level: 2 }),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: "H3",
|
|
73
|
+
title: "Heading 3",
|
|
74
|
+
action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
75
|
+
isActive: () => editor.isActive("heading", { level: 3 }),
|
|
76
|
+
},
|
|
77
|
+
{ type: "divider" },
|
|
78
|
+
{
|
|
79
|
+
label: "Link",
|
|
80
|
+
title: "Link",
|
|
81
|
+
action: toggleLink,
|
|
82
|
+
isActive: () => editor.isActive("link"),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BubbleToolbarProps {
|
|
88
|
+
editor: Editor;
|
|
89
|
+
/**
|
|
90
|
+
* Custom item builder. Receives the editor and the `toggleLink` helper (so a
|
|
91
|
+
* custom set can still open the built-in link editor). Defaults to
|
|
92
|
+
* {@link buildDefaultBubbleItems}.
|
|
93
|
+
*/
|
|
94
|
+
buildItems?: (editor: Editor, toggleLink: () => void) => BubbleToolbarItem[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The shared floating selection toolbar. Tracks the current text selection and
|
|
99
|
+
* positions a fixed toolbar above it, with an inline link editor. Extracted
|
|
100
|
+
* from the inline plan toolbar so embedders share one implementation; apps swap
|
|
101
|
+
* the item set via `buildItems`.
|
|
102
|
+
*/
|
|
103
|
+
export function BubbleToolbar({
|
|
104
|
+
editor,
|
|
105
|
+
buildItems = buildDefaultBubbleItems,
|
|
106
|
+
}: BubbleToolbarProps) {
|
|
107
|
+
const [visible, setVisible] = useState(false);
|
|
108
|
+
const [coords, setCoords] = useState({ top: 0, left: 0 });
|
|
109
|
+
const [showLinkInput, setShowLinkInput] = useState(false);
|
|
110
|
+
const [linkUrl, setLinkUrl] = useState("");
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const update = () => {
|
|
114
|
+
const selectionState = editor.state.selection;
|
|
115
|
+
const { from, to } = selectionState;
|
|
116
|
+
const selection = window.getSelection();
|
|
117
|
+
const selectionInsideEditor =
|
|
118
|
+
!!selection?.anchorNode &&
|
|
119
|
+
!!selection.focusNode &&
|
|
120
|
+
editor.view.dom.contains(selection.anchorNode) &&
|
|
121
|
+
editor.view.dom.contains(selection.focusNode);
|
|
122
|
+
if (
|
|
123
|
+
!(selectionState instanceof TextSelection) ||
|
|
124
|
+
from === to ||
|
|
125
|
+
!selectionInsideEditor
|
|
126
|
+
) {
|
|
127
|
+
setVisible(false);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!selection || selection.rangeCount === 0) {
|
|
131
|
+
setVisible(false);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const rect = selection.getRangeAt(0).getBoundingClientRect();
|
|
135
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
136
|
+
setVisible(false);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setCoords({
|
|
140
|
+
top: rect.top - 8,
|
|
141
|
+
left: rect.left + rect.width / 2,
|
|
142
|
+
});
|
|
143
|
+
setVisible(true);
|
|
144
|
+
};
|
|
145
|
+
editor.on("selectionUpdate", update);
|
|
146
|
+
editor.on("transaction", update);
|
|
147
|
+
document.addEventListener("selectionchange", update);
|
|
148
|
+
const onBlur = () => {
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
if (!editor.isFocused) setVisible(false);
|
|
151
|
+
}, 140);
|
|
152
|
+
};
|
|
153
|
+
editor.on("blur", onBlur);
|
|
154
|
+
return () => {
|
|
155
|
+
editor.off("selectionUpdate", update);
|
|
156
|
+
editor.off("transaction", update);
|
|
157
|
+
document.removeEventListener("selectionchange", update);
|
|
158
|
+
editor.off("blur", onBlur);
|
|
159
|
+
};
|
|
160
|
+
}, [editor]);
|
|
161
|
+
|
|
162
|
+
const handleSetLink = () => {
|
|
163
|
+
if (linkUrl.trim()) {
|
|
164
|
+
editor
|
|
165
|
+
.chain()
|
|
166
|
+
.focus()
|
|
167
|
+
.extendMarkRange("link")
|
|
168
|
+
.setLink({ href: linkUrl.trim() })
|
|
169
|
+
.run();
|
|
170
|
+
} else {
|
|
171
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
172
|
+
}
|
|
173
|
+
setShowLinkInput(false);
|
|
174
|
+
setLinkUrl("");
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const toggleLink = () => {
|
|
178
|
+
if (editor.isActive("link")) {
|
|
179
|
+
editor.chain().focus().unsetLink().run();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
setLinkUrl(editor.getAttributes("link").href || "");
|
|
183
|
+
setShowLinkInput(true);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const items = buildItems(editor, toggleLink);
|
|
187
|
+
|
|
188
|
+
if (!visible) return null;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div
|
|
192
|
+
className="an-rich-md-bubble-toolbar"
|
|
193
|
+
style={{
|
|
194
|
+
position: "fixed",
|
|
195
|
+
top: coords.top,
|
|
196
|
+
left: coords.left,
|
|
197
|
+
transform: "translate(-50%, -100%)",
|
|
198
|
+
}}
|
|
199
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
200
|
+
data-plan-interactive
|
|
201
|
+
>
|
|
202
|
+
{showLinkInput ? (
|
|
203
|
+
<div className="an-rich-md-link-editor">
|
|
204
|
+
<input
|
|
205
|
+
autoFocus
|
|
206
|
+
type="url"
|
|
207
|
+
placeholder="Paste link..."
|
|
208
|
+
value={linkUrl}
|
|
209
|
+
onChange={(event) => setLinkUrl(event.target.value)}
|
|
210
|
+
onKeyDown={(event) => {
|
|
211
|
+
if (event.key === "Enter") handleSetLink();
|
|
212
|
+
if (event.key === "Escape") {
|
|
213
|
+
setShowLinkInput(false);
|
|
214
|
+
setLinkUrl("");
|
|
215
|
+
}
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
<button type="button" onClick={handleSetLink}>
|
|
219
|
+
Apply
|
|
220
|
+
</button>
|
|
221
|
+
</div>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="an-rich-md-bubble-items">
|
|
224
|
+
{items.map((item, index) => {
|
|
225
|
+
if ("type" in item) {
|
|
226
|
+
return (
|
|
227
|
+
<span
|
|
228
|
+
key={`divider-${index}`}
|
|
229
|
+
className="an-rich-md-bubble-divider"
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return (
|
|
234
|
+
<button
|
|
235
|
+
key={item.title}
|
|
236
|
+
type="button"
|
|
237
|
+
title={item.title}
|
|
238
|
+
className={cn(
|
|
239
|
+
"an-rich-md-bubble-button",
|
|
240
|
+
item.isActive() && "an-rich-md-bubble-button--active",
|
|
241
|
+
)}
|
|
242
|
+
style={item.style}
|
|
243
|
+
onClick={item.action}
|
|
244
|
+
>
|
|
245
|
+
{item.label}
|
|
246
|
+
</button>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
|
2
|
+
import type { Node as TiptapNode } from "@tiptap/core";
|
|
3
|
+
import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight";
|
|
4
|
+
import {
|
|
5
|
+
NodeViewContent,
|
|
6
|
+
NodeViewWrapper,
|
|
7
|
+
ReactNodeViewRenderer,
|
|
8
|
+
type NodeViewProps,
|
|
9
|
+
} from "@tiptap/react";
|
|
10
|
+
import type { createLowlight } from "lowlight";
|
|
11
|
+
import { useMemo, useState } from "react";
|
|
12
|
+
|
|
13
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
|
|
14
|
+
import { cn } from "../utils.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A selectable language for the code-block picker. `value === null` is the
|
|
18
|
+
* "Auto" sentinel: the block stores no language and the lowlight plugin
|
|
19
|
+
* auto-detects (biased toward the editor's registered, web-leaning grammar
|
|
20
|
+
* set). Keeping it first makes Auto the default, matching how plans skew toward
|
|
21
|
+
* JS/TS/JSON without the author having to choose.
|
|
22
|
+
*/
|
|
23
|
+
export interface CodeLanguageOption {
|
|
24
|
+
value: string | null;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Default picker list — Auto first, then the web-leaning grammars plans use. */
|
|
29
|
+
export const DEFAULT_CODE_LANGUAGES: CodeLanguageOption[] = [
|
|
30
|
+
{ value: null, label: "Auto" },
|
|
31
|
+
{ value: "typescript", label: "TypeScript" },
|
|
32
|
+
{ value: "javascript", label: "JavaScript" },
|
|
33
|
+
{ value: "tsx", label: "TSX" },
|
|
34
|
+
{ value: "jsx", label: "JSX" },
|
|
35
|
+
{ value: "json", label: "JSON" },
|
|
36
|
+
{ value: "html", label: "HTML" },
|
|
37
|
+
{ value: "css", label: "CSS" },
|
|
38
|
+
{ value: "bash", label: "Bash" },
|
|
39
|
+
{ value: "python", label: "Python" },
|
|
40
|
+
{ value: "sql", label: "SQL" },
|
|
41
|
+
{ value: "yaml", label: "YAML" },
|
|
42
|
+
{ value: "markdown", label: "Markdown" },
|
|
43
|
+
{ value: "graphql", label: "GraphQL" },
|
|
44
|
+
{ value: "go", label: "Go" },
|
|
45
|
+
{ value: "rust", label: "Rust" },
|
|
46
|
+
{ value: "diff", label: "Diff" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/** Class hooks so each app themes the shared node with its own palette. */
|
|
50
|
+
export interface CodeBlockClassNames {
|
|
51
|
+
wrapper: string;
|
|
52
|
+
header: string;
|
|
53
|
+
langButton: string;
|
|
54
|
+
langButtonReadonly: string;
|
|
55
|
+
picker: string;
|
|
56
|
+
search: string;
|
|
57
|
+
list: string;
|
|
58
|
+
option: string;
|
|
59
|
+
optionActive: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DEFAULT_CLASS_NAMES: CodeBlockClassNames = {
|
|
63
|
+
wrapper: "an-code-block",
|
|
64
|
+
header: "an-code-block__header",
|
|
65
|
+
langButton: "an-code-block__lang",
|
|
66
|
+
langButtonReadonly: "an-code-block__lang--readonly",
|
|
67
|
+
picker: "an-code-block__picker",
|
|
68
|
+
search: "an-code-block__search",
|
|
69
|
+
list: "an-code-block__list",
|
|
70
|
+
option: "an-code-block__option",
|
|
71
|
+
optionActive: "is-active",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface CreateCodeBlockNodeOptions {
|
|
75
|
+
/** Lowlight instance backing syntax highlighting (same one the app registers). */
|
|
76
|
+
lowlight: ReturnType<typeof createLowlight>;
|
|
77
|
+
/** Picker languages. Defaults to {@link DEFAULT_CODE_LANGUAGES} (Auto first). */
|
|
78
|
+
languages?: CodeLanguageOption[];
|
|
79
|
+
/** Class hooks for per-app theming. Defaults to the shared `an-code-block__*`. */
|
|
80
|
+
classNames?: Partial<CodeBlockClassNames>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function optionLabel(
|
|
84
|
+
language: string | null,
|
|
85
|
+
languages: CodeLanguageOption[],
|
|
86
|
+
): string {
|
|
87
|
+
if (!language) return languages[0]?.label ?? "Auto";
|
|
88
|
+
const match = languages.find((option) => option.value === language);
|
|
89
|
+
return match?.label ?? language;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface CodeBlockNodeViewExtraOptions {
|
|
93
|
+
languagePickerOptions: CodeLanguageOption[];
|
|
94
|
+
codeBlockClassNames: CodeBlockClassNames;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function CodeBlockView({
|
|
98
|
+
node,
|
|
99
|
+
updateAttributes,
|
|
100
|
+
editor,
|
|
101
|
+
extension,
|
|
102
|
+
}: NodeViewProps) {
|
|
103
|
+
const options = extension.options as unknown as CodeBlockNodeViewExtraOptions;
|
|
104
|
+
const languages = options.languagePickerOptions;
|
|
105
|
+
const classNames = options.codeBlockClassNames;
|
|
106
|
+
|
|
107
|
+
const [open, setOpen] = useState(false);
|
|
108
|
+
const [filter, setFilter] = useState("");
|
|
109
|
+
const isEditable = editor.isEditable;
|
|
110
|
+
|
|
111
|
+
const current = (node.attrs.language as string | null) || null;
|
|
112
|
+
const label = optionLabel(current, languages);
|
|
113
|
+
|
|
114
|
+
const filtered = useMemo(() => {
|
|
115
|
+
const query = filter.trim().toLowerCase();
|
|
116
|
+
if (!query) return languages;
|
|
117
|
+
return languages.filter((option) =>
|
|
118
|
+
option.label.toLowerCase().includes(query),
|
|
119
|
+
);
|
|
120
|
+
}, [filter, languages]);
|
|
121
|
+
|
|
122
|
+
const select = (value: string | null) => {
|
|
123
|
+
// Store `""` for Auto so tiptap-markdown emits a bare fence (no language),
|
|
124
|
+
// matching the auto-detect read path.
|
|
125
|
+
updateAttributes({ language: value ?? "" });
|
|
126
|
+
setFilter("");
|
|
127
|
+
setOpen(false);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<NodeViewWrapper className={classNames.wrapper}>
|
|
132
|
+
<div className={classNames.header} contentEditable={false}>
|
|
133
|
+
{isEditable ? (
|
|
134
|
+
<Popover
|
|
135
|
+
open={open}
|
|
136
|
+
onOpenChange={(next) => {
|
|
137
|
+
setOpen(next);
|
|
138
|
+
if (!next) setFilter("");
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<PopoverTrigger asChild>
|
|
142
|
+
<button type="button" className={classNames.langButton}>
|
|
143
|
+
{label}
|
|
144
|
+
<IconChevronDown className="size-3" />
|
|
145
|
+
</button>
|
|
146
|
+
</PopoverTrigger>
|
|
147
|
+
<PopoverContent align="start" side="bottom" className="w-52 p-0">
|
|
148
|
+
<input
|
|
149
|
+
autoFocus
|
|
150
|
+
type="text"
|
|
151
|
+
value={filter}
|
|
152
|
+
onChange={(event) => setFilter(event.target.value)}
|
|
153
|
+
onKeyDown={(event) => {
|
|
154
|
+
if (event.key === "Enter" && filtered.length > 0) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
select(filtered[0].value);
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
placeholder="Search languages…"
|
|
160
|
+
className={classNames.search}
|
|
161
|
+
/>
|
|
162
|
+
<div className={classNames.list}>
|
|
163
|
+
{filtered.map((option) => {
|
|
164
|
+
const active =
|
|
165
|
+
option.value === current || (!option.value && !current);
|
|
166
|
+
return (
|
|
167
|
+
<button
|
|
168
|
+
key={option.value ?? "auto"}
|
|
169
|
+
type="button"
|
|
170
|
+
className={cn(
|
|
171
|
+
classNames.option,
|
|
172
|
+
active && classNames.optionActive,
|
|
173
|
+
)}
|
|
174
|
+
onClick={() => select(option.value)}
|
|
175
|
+
>
|
|
176
|
+
{option.label}
|
|
177
|
+
{active && <IconCheck className="size-3.5" />}
|
|
178
|
+
</button>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
</PopoverContent>
|
|
183
|
+
</Popover>
|
|
184
|
+
) : (
|
|
185
|
+
label && (
|
|
186
|
+
<span
|
|
187
|
+
className={cn(
|
|
188
|
+
classNames.langButton,
|
|
189
|
+
classNames.langButtonReadonly,
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
{label}
|
|
193
|
+
</span>
|
|
194
|
+
)
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
<pre>
|
|
198
|
+
<NodeViewContent as={"code" as never} />
|
|
199
|
+
</pre>
|
|
200
|
+
</NodeViewWrapper>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The shared Notion-style code block: {@link CodeBlockLowlight} (so fenced
|
|
206
|
+
* markdown round-trips byte-identically — the node name and `language` attr are
|
|
207
|
+
* unchanged) plus a React node view that adds a language picker header instead
|
|
208
|
+
* of a bare highlighted `<pre>`. "Auto" stores no language and the lowlight
|
|
209
|
+
* plugin auto-detects.
|
|
210
|
+
*
|
|
211
|
+
* Lifted from the Content editor's bespoke code block so Plans (and any app
|
|
212
|
+
* opting into `features.codeBlock`) share one implementation. Theming is fully
|
|
213
|
+
* class-driven via {@link CreateCodeBlockNodeOptions.classNames}, so each app
|
|
214
|
+
* maps the hooks onto its own palette.
|
|
215
|
+
*/
|
|
216
|
+
export function createCodeBlockNode({
|
|
217
|
+
lowlight,
|
|
218
|
+
languages = DEFAULT_CODE_LANGUAGES,
|
|
219
|
+
classNames,
|
|
220
|
+
}: CreateCodeBlockNodeOptions): TiptapNode {
|
|
221
|
+
const resolvedClassNames: CodeBlockClassNames = {
|
|
222
|
+
...DEFAULT_CLASS_NAMES,
|
|
223
|
+
...(classNames ?? {}),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return CodeBlockLowlight.extend({
|
|
227
|
+
addOptions() {
|
|
228
|
+
return {
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
230
|
+
...this.parent!(),
|
|
231
|
+
languagePickerOptions: languages,
|
|
232
|
+
codeBlockClassNames: resolvedClassNames,
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
addNodeView() {
|
|
236
|
+
return ReactNodeViewRenderer(CodeBlockView);
|
|
237
|
+
},
|
|
238
|
+
addKeyboardShortcuts() {
|
|
239
|
+
return {
|
|
240
|
+
...this.parent?.(),
|
|
241
|
+
Tab: ({ editor }) => {
|
|
242
|
+
if (editor.isActive(this.name)) {
|
|
243
|
+
editor.commands.insertContent("\t");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
}).configure({
|
|
251
|
+
lowlight,
|
|
252
|
+
defaultLanguage: null,
|
|
253
|
+
}) as unknown as TiptapNode;
|
|
254
|
+
}
|