@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.
Files changed (174) hide show
  1. package/README.md +17 -1
  2. package/agent-native.eject.json +411 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  4. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  5. package/dist/chat-history/ChatHistoryList.js +109 -0
  6. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  8. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  10. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  11. package/dist/chat-history/index.d.ts +2 -0
  12. package/dist/chat-history/index.d.ts.map +1 -0
  13. package/dist/chat-history/index.js +2 -0
  14. package/dist/chat-history/index.js.map +1 -0
  15. package/dist/chat-history.css +314 -0
  16. package/package.json +20 -1
  17. package/src/app-shell/header-actions.tsx +84 -0
  18. package/src/app-shell/index.ts +1 -0
  19. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  20. package/src/chat-history/ChatHistoryList.tsx +469 -0
  21. package/src/chat-history/index.ts +7 -0
  22. package/src/chat-history.css +314 -0
  23. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  24. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  25. package/src/collab-ui/PresenceBar.tsx +346 -0
  26. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  27. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  28. package/src/collab-ui/agent-identity.ts +16 -0
  29. package/src/collab-ui/index.ts +39 -0
  30. package/src/collab-ui/lead-client.ts +32 -0
  31. package/src/collab-ui/types.spec.ts +18 -0
  32. package/src/collab-ui/types.ts +96 -0
  33. package/src/composer/AgentComposerFrame.tsx +59 -0
  34. package/src/composer/ComposerPlusMenu.tsx +983 -0
  35. package/src/composer/MentionPopover.tsx +412 -0
  36. package/src/composer/PastedTextChip.tsx +150 -0
  37. package/src/composer/PromptComposer.spec.ts +73 -0
  38. package/src/composer/PromptComposer.tsx +718 -0
  39. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  40. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  41. package/src/composer/TiptapComposer.spec.ts +390 -0
  42. package/src/composer/TiptapComposer.tsx +2936 -0
  43. package/src/composer/VoiceButton.spec.ts +33 -0
  44. package/src/composer/VoiceButton.tsx +255 -0
  45. package/src/composer/asset-picker-url.spec.ts +56 -0
  46. package/src/composer/asset-picker-url.ts +80 -0
  47. package/src/composer/attachment-accept.spec.ts +22 -0
  48. package/src/composer/attachment-accept.ts +71 -0
  49. package/src/composer/draft-key.spec.ts +19 -0
  50. package/src/composer/draft-key.ts +7 -0
  51. package/src/composer/extensions/FileReference.tsx +55 -0
  52. package/src/composer/extensions/MentionReference.tsx +93 -0
  53. package/src/composer/extensions/SkillReference.tsx +56 -0
  54. package/src/composer/index.ts +79 -0
  55. package/src/composer/pasted-text.spec.ts +162 -0
  56. package/src/composer/pasted-text.ts +160 -0
  57. package/src/composer/prompt-attachments.spec.ts +80 -0
  58. package/src/composer/prompt-attachments.ts +93 -0
  59. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  60. package/src/composer/realtime-voice-audio-level.ts +64 -0
  61. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  62. package/src/composer/realtime-voice-transcript.ts +134 -0
  63. package/src/composer/runtime-adapters.tsx +343 -0
  64. package/src/composer/types.ts +66 -0
  65. package/src/composer/use-file-search.ts +64 -0
  66. package/src/composer/use-mention-search.ts +90 -0
  67. package/src/composer/use-skills.ts +45 -0
  68. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  69. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  70. package/src/composer/useVoiceDictation.spec.ts +45 -0
  71. package/src/composer/useVoiceDictation.ts +1191 -0
  72. package/src/context-ui/ContextMeter.tsx +101 -0
  73. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  74. package/src/context-ui/ContextTreemap.tsx +125 -0
  75. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  76. package/src/context-ui/format.ts +42 -0
  77. package/src/context-ui/index.ts +15 -0
  78. package/src/context-ui/types.ts +43 -0
  79. package/src/design-tweaks/index.ts +1 -0
  80. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  81. package/src/editor/BubbleToolbar.tsx +253 -0
  82. package/src/editor/CodeBlockNode.tsx +254 -0
  83. package/src/editor/DragHandle.spec.ts +588 -0
  84. package/src/editor/DragHandle.ts +1437 -0
  85. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  86. package/src/editor/ImageExtension.ts +306 -0
  87. package/src/editor/RegistryBlockContext.tsx +107 -0
  88. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  89. package/src/editor/RegistryBlockNode.tsx +652 -0
  90. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  91. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  92. package/src/editor/RichMarkdownEditor.tsx +81 -0
  93. package/src/editor/RunId.ts +61 -0
  94. package/src/editor/SharedRichEditor.tsx +321 -0
  95. package/src/editor/SlashCommandMenu.tsx +310 -0
  96. package/src/editor/disableHistory.spec.ts +85 -0
  97. package/src/editor/extensions.ts +402 -0
  98. package/src/editor/gfmDoc.spec.ts +230 -0
  99. package/src/editor/gfmDoc.ts +92 -0
  100. package/src/editor/index.ts +93 -0
  101. package/src/editor/registrySlashCommands.ts +131 -0
  102. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  103. package/src/editor/surgical-apply.spec.ts +279 -0
  104. package/src/editor/surgical-apply.ts +192 -0
  105. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  106. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  107. package/src/editor/useCollabReconcile.ts +589 -0
  108. package/src/editor.css +510 -0
  109. package/src/hooks/index.ts +2 -0
  110. package/src/hooks/use-mobile.tsx +21 -0
  111. package/src/hooks/use-toast.ts +188 -0
  112. package/src/index.ts +8 -0
  113. package/src/onboarding/index.ts +1 -0
  114. package/src/provider.tsx +47 -0
  115. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  116. package/src/sharing/VisibilityBadge.tsx +37 -0
  117. package/src/sharing/index.ts +4 -0
  118. package/src/styles.css +21 -0
  119. package/src/ui/accordion.tsx +56 -0
  120. package/src/ui/alert-dialog.tsx +141 -0
  121. package/src/ui/alert.tsx +59 -0
  122. package/src/ui/aspect-ratio.tsx +5 -0
  123. package/src/ui/avatar.tsx +48 -0
  124. package/src/ui/badge.tsx +37 -0
  125. package/src/ui/breadcrumb.tsx +115 -0
  126. package/src/ui/button.tsx +78 -0
  127. package/src/ui/calendar.tsx +214 -0
  128. package/src/ui/card.tsx +86 -0
  129. package/src/ui/carousel.tsx +260 -0
  130. package/src/ui/chart.tsx +375 -0
  131. package/src/ui/checkbox.tsx +28 -0
  132. package/src/ui/collapsible.tsx +9 -0
  133. package/src/ui/command.spec.tsx +51 -0
  134. package/src/ui/command.tsx +174 -0
  135. package/src/ui/context-menu.tsx +202 -0
  136. package/src/ui/date-picker.tsx +64 -0
  137. package/src/ui/dialog.spec.tsx +43 -0
  138. package/src/ui/dialog.tsx +146 -0
  139. package/src/ui/drawer.tsx +126 -0
  140. package/src/ui/dropdown-menu.tsx +218 -0
  141. package/src/ui/form.tsx +177 -0
  142. package/src/ui/hover-card.tsx +27 -0
  143. package/src/ui/index.ts +52 -0
  144. package/src/ui/input-otp.tsx +69 -0
  145. package/src/ui/input.tsx +22 -0
  146. package/src/ui/label.tsx +24 -0
  147. package/src/ui/logo.tsx +36 -0
  148. package/src/ui/menubar.tsx +235 -0
  149. package/src/ui/navigation-menu.tsx +128 -0
  150. package/src/ui/pagination.tsx +121 -0
  151. package/src/ui/popover.tsx +44 -0
  152. package/src/ui/progress.tsx +26 -0
  153. package/src/ui/radio-group.tsx +42 -0
  154. package/src/ui/resizable.tsx +43 -0
  155. package/src/ui/scroll-area.tsx +46 -0
  156. package/src/ui/select.tsx +158 -0
  157. package/src/ui/separator.tsx +29 -0
  158. package/src/ui/sheet.tsx +160 -0
  159. package/src/ui/sidebar.tsx +777 -0
  160. package/src/ui/skeleton.tsx +18 -0
  161. package/src/ui/slider.tsx +26 -0
  162. package/src/ui/sonner.tsx +58 -0
  163. package/src/ui/spinner.tsx +17 -0
  164. package/src/ui/switch.tsx +27 -0
  165. package/src/ui/table.tsx +117 -0
  166. package/src/ui/tabs.tsx +53 -0
  167. package/src/ui/textarea.tsx +23 -0
  168. package/src/ui/toast.tsx +127 -0
  169. package/src/ui/toaster.tsx +33 -0
  170. package/src/ui/toggle-group.tsx +59 -0
  171. package/src/ui/toggle.tsx +43 -0
  172. package/src/ui/tooltip.tsx +53 -0
  173. package/src/ui/use-toast.ts +3 -0
  174. 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
+ }