@ctrl-ui/react 0.8.18 → 0.8.20

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 (106) hide show
  1. package/README.md +7 -0
  2. package/dist/action-bar.js +2 -2
  3. package/dist/audio-device-select.js +5 -2
  4. package/dist/blocks/coding-agent.js +22 -12
  5. package/dist/blocks/design-canvas-inspector.js +6 -3
  6. package/dist/blocks/design-canvas.js +20 -10
  7. package/dist/blocks/discussion-markdown-input.d.ts +14 -0
  8. package/dist/blocks/discussion-markdown-input.js +36 -0
  9. package/dist/blocks/discussion.d.ts +9 -3
  10. package/dist/blocks/discussion.js +82 -16
  11. package/dist/blocks/file-explorer.js +46 -24
  12. package/dist/blocks/settings.js +10 -6
  13. package/dist/chat-composer-editor/extensions/mention.d.ts +1 -1
  14. package/dist/chat-message.d.ts +13 -0
  15. package/dist/chat-message.js +198 -3
  16. package/dist/filter-bar/editor.js +5 -2
  17. package/dist/knob-contracts/flow-knobs.d.ts +4 -0
  18. package/dist/knob-contracts/flow-knobs.js +32 -0
  19. package/dist/knob-contracts/item-knobs.d.ts +1 -1
  20. package/dist/knob-contracts/item-knobs.js +4 -1
  21. package/dist/knob-contracts/label-knobs.d.ts +4 -0
  22. package/dist/knob-contracts/label-knobs.js +4 -0
  23. package/dist/knob-contracts/markdown-editor-knobs.d.ts +4 -0
  24. package/dist/knob-contracts/markdown-editor-knobs.js +17 -0
  25. package/dist/knob-contracts/morphing-panel-knobs.d.ts +1 -1
  26. package/dist/knob-contracts/morphing-panel-knobs.js +2 -1
  27. package/dist/knob-contracts/resize-handle-knobs.d.ts +4 -0
  28. package/dist/knob-contracts/resize-handle-knobs.js +12 -0
  29. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  30. package/dist/knob-contracts/sidebar-knobs.js +10 -0
  31. package/dist/knob-contracts/switch-knobs.d.ts +1 -1
  32. package/dist/knob-contracts/switch-knobs.js +1 -0
  33. package/dist/lib/canvas-grid.d.ts +4 -0
  34. package/dist/lib/canvas-grid.js +5 -0
  35. package/dist/markdown-editor/context.d.ts +22 -0
  36. package/dist/markdown-editor/context.js +10 -0
  37. package/dist/markdown-editor/drag-handle.d.ts +6 -0
  38. package/dist/markdown-editor/drag-handle.js +126 -0
  39. package/dist/markdown-editor/extensions.d.ts +10 -0
  40. package/dist/markdown-editor/extensions.js +139 -0
  41. package/dist/markdown-editor/suggestions.d.ts +12 -0
  42. package/dist/markdown-editor/suggestions.js +235 -0
  43. package/dist/markdown-editor/toolbar.d.ts +9 -0
  44. package/dist/markdown-editor/toolbar.js +339 -0
  45. package/dist/markdown-editor/uploads.d.ts +23 -0
  46. package/dist/markdown-editor/uploads.js +32 -0
  47. package/dist/markdown-editor/use-image-uploads.d.ts +12 -0
  48. package/dist/markdown-editor/use-image-uploads.js +120 -0
  49. package/dist/markdown-editor.d.ts +27 -0
  50. package/dist/markdown-editor.js +202 -0
  51. package/dist/styles/index.css +9 -4
  52. package/dist/styles/recipes/chat-message-edit.css +105 -0
  53. package/dist/styles/recipes/code.css +4 -0
  54. package/dist/styles/recipes/field.css +1 -1
  55. package/dist/styles/recipes/flow.css +304 -0
  56. package/dist/styles/recipes/infinite-canvas.css +1 -1
  57. package/dist/styles/recipes/item.css +39 -2
  58. package/dist/styles/recipes/label.css +19 -0
  59. package/dist/styles/recipes/markdown-editor.css +234 -0
  60. package/dist/styles/recipes/morphing-panel.css +53 -12
  61. package/dist/styles/recipes/page-layout.css +11 -1
  62. package/dist/styles/recipes/resize-handle.css +126 -0
  63. package/dist/styles/recipes/sidebar-mobile.css +171 -0
  64. package/dist/styles/recipes/switch.css +17 -5
  65. package/dist/styles/recipes/toolbar.css +8 -1
  66. package/dist/styles/theme.css +1 -0
  67. package/dist/ui/autocomplete.js +1 -1
  68. package/dist/ui/code.js +1 -0
  69. package/dist/ui/color-picker.js +1 -1
  70. package/dist/ui/combobox.d.ts +1 -1
  71. package/dist/ui/combobox.js +1 -1
  72. package/dist/ui/dockable-panel.js +1 -1
  73. package/dist/ui/flow.d.ts +51 -0
  74. package/dist/ui/flow.js +153 -0
  75. package/dist/ui/infinite-canvas.js +3 -3
  76. package/dist/ui/item.d.ts +5 -3
  77. package/dist/ui/item.js +2 -1
  78. package/dist/ui/label.d.ts +8 -0
  79. package/dist/ui/label.js +14 -0
  80. package/dist/ui/markdown.d.ts +2 -1
  81. package/dist/ui/markdown.js +3 -3
  82. package/dist/ui/morphing-panel.d.ts +21 -4
  83. package/dist/ui/morphing-panel.js +152 -36
  84. package/dist/ui/resize-handle.d.ts +11 -0
  85. package/dist/ui/resize-handle.js +26 -0
  86. package/dist/ui/sidebar-mobile.d.ts +17 -0
  87. package/dist/ui/sidebar-mobile.js +70 -0
  88. package/dist/ui/sidebar-provider.d.ts +1 -0
  89. package/dist/ui/sidebar-provider.js +3 -0
  90. package/dist/ui/sidebar.d.ts +3 -1
  91. package/dist/ui/sidebar.js +101 -49
  92. package/dist/ui/toolbar.d.ts +3 -1
  93. package/dist/ui/toolbar.js +2 -1
  94. package/dist/ui/typography.d.ts +47 -0
  95. package/dist/ui/typography.js +61 -0
  96. package/dist/ui/use-switch-drag.js +1 -1
  97. package/package.json +41 -1
  98. package/dist/environment-variables.d.ts +0 -125
  99. package/dist/environment-variables.js +0 -512
  100. package/dist/hooks/use-environment-variables.d.ts +0 -50
  101. package/dist/hooks/use-environment-variables.js +0 -188
  102. package/dist/knob-contracts/environment-variables-knobs.d.ts +0 -4
  103. package/dist/knob-contracts/environment-variables-knobs.js +0 -12
  104. package/dist/lib/env-file.d.ts +0 -27
  105. package/dist/lib/env-file.js +0 -218
  106. package/dist/styles/recipes/environment-variables.css +0 -170
@@ -0,0 +1,126 @@
1
+ "use client";
2
+ import { Button } from "../ui/button.js";
3
+ import { LiveStatus } from "../ui/live-status.js";
4
+ import { useMarkdownEditor } from "./context.js";
5
+ import { useEffect, useRef, useState } from "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
7
+ import { GripVerticalIcon } from "lucide-react";
8
+ import { NodeSelection } from "@tiptap/pm/state";
9
+ import { closeHistory } from "@tiptap/pm/history";
10
+ //#region src/markdown-editor/drag-handle.tsx
11
+ function MarkdownEditorDragHandle({ container }) {
12
+ const { editor, disabled, source } = useMarkdownEditor();
13
+ const [block, setBlock] = useState(null);
14
+ const [announcement, setAnnouncement] = useState("");
15
+ const button = useRef(null);
16
+ const dragging = useRef(false);
17
+ useEffect(() => {
18
+ const surface = container.current;
19
+ if (!editor || !surface || disabled || source) return;
20
+ function show(blockPosition) {
21
+ if (!editor || !surface) return;
22
+ const node = editor.view.nodeDOM(blockPosition);
23
+ if (!(node instanceof HTMLElement)) return;
24
+ const top = node.getBoundingClientRect().top - surface.getBoundingClientRect().top + surface.scrollTop;
25
+ setBlock((current) => current?.position === blockPosition && current.top === top ? current : {
26
+ position: blockPosition,
27
+ top
28
+ });
29
+ }
30
+ function track(event) {
31
+ if (!editor || dragging.current || button.current?.contains(event.target instanceof Node ? event.target : null)) return;
32
+ const rect = editor.view.dom.getBoundingClientRect();
33
+ const padding = Number.parseFloat(getComputedStyle(editor.view.dom).paddingInlineStart);
34
+ const hit = editor.view.posAtCoords({
35
+ left: Math.max(event.clientX, rect.left + padding),
36
+ top: event.clientY
37
+ });
38
+ if (!hit) return;
39
+ const resolved = editor.state.doc.resolve(hit.pos);
40
+ show(resolved.depth > 0 ? resolved.before(1) : hit.pos);
41
+ }
42
+ function selectionChanged() {
43
+ if (!editor || dragging.current) return;
44
+ const { $from } = editor.state.selection;
45
+ show($from.depth > 0 ? $from.before(1) : $from.pos);
46
+ }
47
+ function hide() {
48
+ if (!dragging.current && document.activeElement !== button.current) setBlock(null);
49
+ }
50
+ surface.addEventListener("mousemove", track);
51
+ surface.addEventListener("mouseleave", hide);
52
+ editor.on("transaction", selectionChanged).on("focus", selectionChanged);
53
+ return () => {
54
+ surface.removeEventListener("mousemove", track);
55
+ surface.removeEventListener("mouseleave", hide);
56
+ editor.off("transaction", selectionChanged).off("focus", selectionChanged);
57
+ editor.view.dragging = null;
58
+ };
59
+ }, [
60
+ editor,
61
+ container,
62
+ disabled,
63
+ source
64
+ ]);
65
+ if (!editor || disabled || source) return null;
66
+ const selected = editor.state.selection.$from;
67
+ const position = block?.position ?? (selected.depth > 0 ? selected.before(1) : selected.pos);
68
+ function move(direction) {
69
+ if (!editor) return;
70
+ const { doc, tr } = editor.state;
71
+ const node = doc.nodeAt(position);
72
+ if (!node) return;
73
+ const adjacent = direction === -1 ? doc.resolve(position).nodeBefore : doc.nodeAt(position + node.nodeSize);
74
+ if (!adjacent) return;
75
+ const target = direction === -1 ? position - adjacent.nodeSize : position + adjacent.nodeSize;
76
+ tr.delete(position, position + node.nodeSize).insert(target, node);
77
+ tr.setSelection(NodeSelection.create(tr.doc, target));
78
+ editor.view.dispatch(closeHistory(tr).scrollIntoView());
79
+ setAnnouncement(`Block moved ${direction === -1 ? "up" : "down"}.`);
80
+ }
81
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
82
+ ref: button,
83
+ type: "button",
84
+ variant: "ghost",
85
+ size: "xs",
86
+ iconOnly: true,
87
+ "data-markdown-drag-handle": "",
88
+ "data-visible": block ? "" : void 0,
89
+ "aria-label": "Drag to reorder block",
90
+ "aria-description": "Drag this block, or use Alt+ArrowUp and Alt+ArrowDown to move it.",
91
+ title: "Drag to reorder · Alt+↑/↓ to move",
92
+ draggable: true,
93
+ style: { top: block?.top },
94
+ onClick: () => editor.commands.setNodeSelection(position),
95
+ onKeyDown: (event) => {
96
+ if (!event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
97
+ event.preventDefault();
98
+ move(event.key === "ArrowUp" ? -1 : 1);
99
+ },
100
+ onDragStart: (event) => {
101
+ const { view } = editor;
102
+ const selection = NodeSelection.create(view.state.doc, position);
103
+ const node = view.nodeDOM(position);
104
+ view.dispatch(view.state.tr.setSelection(selection));
105
+ const { dom, text, slice } = view.serializeForClipboard(selection.content());
106
+ event.dataTransfer.clearData();
107
+ event.dataTransfer.setData("text/html", dom.innerHTML);
108
+ event.dataTransfer.setData("text/plain", text);
109
+ event.dataTransfer.effectAllowed = "copyMove";
110
+ if (node instanceof HTMLElement) event.dataTransfer.setDragImage(node, 0, 0);
111
+ view.dragging = {
112
+ slice,
113
+ move: true
114
+ };
115
+ dragging.current = true;
116
+ },
117
+ onDragEnd: () => {
118
+ editor.view.dragging = null;
119
+ dragging.current = false;
120
+ setBlock(null);
121
+ },
122
+ children: /* @__PURE__ */ jsx(GripVerticalIcon, { "aria-hidden": "true" })
123
+ }), /* @__PURE__ */ jsx(LiveStatus, { message: announcement })] });
124
+ }
125
+ //#endregion
126
+ export { MarkdownEditorDragHandle };
@@ -0,0 +1,10 @@
1
+ import { JSONContent, Node } from "@tiptap/core";
2
+ import { MarkdownManager } from "@tiptap/markdown";
3
+ //#region src/markdown-editor/extensions.d.ts
4
+ export declare function isEditorUrl(value: string, image?: boolean): boolean;
5
+ export declare const ImageUpload: Node<any, any>;
6
+ export declare function markdownExtensions(): (import("@tiptap/core").Extension<import("@tiptap/markdown").MarkdownExtensionOptions, import("@tiptap/markdown").MarkdownExtensionStorage> | import("@tiptap/core").Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | import("@tiptap/core").Extension<import("@tiptap/extension-table").TableKitOptions, any> | Node<any, any> | Node<import("@tiptap/extension-image").ImageOptions, any> | Node<import("@tiptap/extension-paragraph").ParagraphOptions, any> | Node<import("@tiptap/extension-list").TaskItemOptions, any> | Node<import("@tiptap/extension-list").TaskListOptions, any>)[];
7
+ export declare function createMarkdownCodec(): MarkdownManager;
8
+ export declare function requiresMarkdownSource(markdown: string, codec: MarkdownManager): boolean;
9
+ export declare function hasMarkdownContent(document: JSONContent): boolean;
10
+ //#endregion
@@ -0,0 +1,139 @@
1
+ import { Node } from "@tiptap/core";
2
+ import Image from "@tiptap/extension-image";
3
+ import { TaskItem, TaskList } from "@tiptap/extension-list";
4
+ import Paragraph from "@tiptap/extension-paragraph";
5
+ import { TableKit } from "@tiptap/extension-table";
6
+ import { Markdown, MarkdownManager } from "@tiptap/markdown";
7
+ import StarterKit from "@tiptap/starter-kit";
8
+ //#region src/markdown-editor/extensions.ts
9
+ function isEditorUrl(value, image = false) {
10
+ if (!value || /[\s\\]/.test(value) || Array.from(value).some((character) => character.charCodeAt(0) < 32)) return false;
11
+ if (value.startsWith("/") && !value.startsWith("//")) return true;
12
+ if (!image && value.startsWith("#")) return true;
13
+ try {
14
+ const url = new URL(value);
15
+ return [
16
+ "https:",
17
+ "http:",
18
+ ...image ? [] : ["mailto:"]
19
+ ].includes(url.protocol);
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ const SafeImage = Image.extend({
25
+ renderMarkdown: (node) => {
26
+ const src = String(node.attrs?.src ?? "").replace(/[()]/g, (character) => `\\${character}`);
27
+ const alt = String(node.attrs?.alt ?? "").replace(/[\\[\]]/g, (character) => `\\${character}`);
28
+ const title = String(node.attrs?.title ?? "").replace(/[\\"]/g, (character) => `\\${character}`);
29
+ return title ? `![${alt}](${src} "${title}")` : `![${alt}](${src})`;
30
+ },
31
+ addAttributes() {
32
+ return {
33
+ ...this.parent?.(),
34
+ src: {
35
+ default: null,
36
+ parseHTML: (element) => {
37
+ const src = element.getAttribute("src") ?? "";
38
+ return isEditorUrl(src, true) ? src : null;
39
+ },
40
+ renderHTML: (attributes) => ({ src: isEditorUrl(attributes.src ?? "", true) ? attributes.src : void 0 })
41
+ }
42
+ };
43
+ }
44
+ });
45
+ const MarkdownParagraph = Paragraph.extend({ parseMarkdown: (token, helpers) => {
46
+ const content = helpers.parseInline(token.tokens ?? []);
47
+ const first = content[0];
48
+ const empty = content.length === 1 && first?.type === "text" && ["&nbsp;", "\xA0"].includes(first.text ?? "");
49
+ return helpers.createNode("paragraph", void 0, empty ? [] : content);
50
+ } });
51
+ const ImageUpload = Node.create({
52
+ name: "imageUpload",
53
+ group: "block",
54
+ atom: true,
55
+ selectable: true,
56
+ addAttributes: () => ({
57
+ id: { default: null },
58
+ name: { default: "Image" }
59
+ }),
60
+ renderHTML: ({ node }) => [
61
+ "div",
62
+ {
63
+ "data-image-upload": "",
64
+ role: "status"
65
+ },
66
+ `Image: ${node.attrs.name}`
67
+ ],
68
+ renderMarkdown: () => ""
69
+ });
70
+ function markdownExtensions() {
71
+ return [
72
+ StarterKit.configure({
73
+ underline: false,
74
+ paragraph: false,
75
+ link: {
76
+ openOnClick: false,
77
+ isAllowedUri: (url) => isEditorUrl(url)
78
+ }
79
+ }),
80
+ MarkdownParagraph,
81
+ SafeImage.configure({
82
+ allowBase64: false,
83
+ inline: true
84
+ }),
85
+ TaskList,
86
+ TaskItem.configure({
87
+ nested: true,
88
+ HTMLAttributes: { "data-type": "taskItem" }
89
+ }),
90
+ TableKit.configure({ table: { resizable: false } }),
91
+ ImageUpload,
92
+ Markdown
93
+ ];
94
+ }
95
+ function createMarkdownCodec() {
96
+ return new MarkdownManager({ extensions: markdownExtensions() });
97
+ }
98
+ const supportedTokens = /* @__PURE__ */ new Set([
99
+ "space",
100
+ "code",
101
+ "heading",
102
+ "hr",
103
+ "blockquote",
104
+ "list",
105
+ "list_item",
106
+ "paragraph",
107
+ "text",
108
+ "escape",
109
+ "link",
110
+ "image",
111
+ "strong",
112
+ "em",
113
+ "codespan",
114
+ "br",
115
+ "del",
116
+ "table",
117
+ "def",
118
+ "taskList",
119
+ "taskItem"
120
+ ]);
121
+ function unsupportedToken(value) {
122
+ if (Array.isArray(value)) return value.some(unsupportedToken);
123
+ if (typeof value !== "object" || value === null) return false;
124
+ if ("type" in value && typeof value.type === "string") {
125
+ if (!supportedTokens.has(value.type)) return true;
126
+ if ((value.type === "image" || value.type === "link") && "href" in value && typeof value.href === "string" && !isEditorUrl(value.href, value.type === "image")) return true;
127
+ }
128
+ return Object.values(value).some(unsupportedToken);
129
+ }
130
+ function requiresMarkdownSource(markdown, codec) {
131
+ return unsupportedToken(codec.instance.lexer(markdown));
132
+ }
133
+ function hasMarkdownContent(document) {
134
+ if (document.type === "image") return true;
135
+ if (document.type === "text" && document.text?.trim()) return true;
136
+ return document.content?.some(hasMarkdownContent) ?? false;
137
+ }
138
+ //#endregion
139
+ export { ImageUpload, createMarkdownCodec, hasMarkdownContent, isEditorUrl, markdownExtensions, requiresMarkdownSource };
@@ -0,0 +1,12 @@
1
+ import { TriggerMenuItemData } from "../hooks/use-trigger-menu.js";
2
+ //#region src/markdown-editor/suggestions.d.ts
3
+ export type MarkdownEditorMention = TriggerMenuItemData & {
4
+ href: string;
5
+ };
6
+ export type MarkdownEditorSuggestionsProps = {
7
+ /** The application supplies people and their persistent profile URLs. */
8
+ mentions?: readonly MarkdownEditorMention[] | ((query: string) => readonly MarkdownEditorMention[]);
9
+ onMentionSelect?: (mention: MarkdownEditorMention) => void;
10
+ };
11
+ export declare function MarkdownEditorSuggestions({ mentions, onMentionSelect }: MarkdownEditorSuggestionsProps): import("react").JSX.Element;
12
+ //#endregion
@@ -0,0 +1,235 @@
1
+ "use client";
2
+ import { LiveStatus } from "../ui/live-status.js";
3
+ import { isComposingKey, useTriggerMenu } from "../hooks/use-trigger-menu.js";
4
+ import { useMarkdownEditor } from "./context.js";
5
+ import { isEditorUrl } from "./extensions.js";
6
+ import { detectTrigger } from "../lib/trigger-detect.js";
7
+ import { TriggerMenu, TriggerMenuEmpty, TriggerMenuIcon, TriggerMenuItem, TriggerMenuList } from "../ui/trigger-menu.js";
8
+ import { useEffect, useEffectEvent, useRef } from "react";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
+ import { Heading1Icon, Heading2Icon, Heading3Icon, ListChecksIcon, ListIcon, ListOrderedIcon, MinusIcon, QuoteIcon, SquareCodeIcon, TextIcon, UserIcon } from "lucide-react";
11
+ //#region src/markdown-editor/suggestions.tsx
12
+ const blockCommands = [
13
+ {
14
+ id: "paragraph",
15
+ label: "Text",
16
+ keywords: ["paragraph"],
17
+ icon: /* @__PURE__ */ jsx(TextIcon, {}),
18
+ command: (chain) => chain.clearNodes().setParagraph()
19
+ },
20
+ {
21
+ id: "heading-1",
22
+ label: "Heading 1",
23
+ keywords: ["h1", "title"],
24
+ icon: /* @__PURE__ */ jsx(Heading1Icon, {}),
25
+ command: (chain) => chain.clearNodes().setHeading({ level: 1 })
26
+ },
27
+ {
28
+ id: "heading-2",
29
+ label: "Heading 2",
30
+ keywords: ["h2", "subtitle"],
31
+ icon: /* @__PURE__ */ jsx(Heading2Icon, {}),
32
+ command: (chain) => chain.clearNodes().setHeading({ level: 2 })
33
+ },
34
+ {
35
+ id: "heading-3",
36
+ label: "Heading 3",
37
+ keywords: ["h3"],
38
+ icon: /* @__PURE__ */ jsx(Heading3Icon, {}),
39
+ command: (chain) => chain.clearNodes().setHeading({ level: 3 })
40
+ },
41
+ {
42
+ id: "bullet-list",
43
+ label: "Bullet list",
44
+ icon: /* @__PURE__ */ jsx(ListIcon, {}),
45
+ command: (chain) => chain.clearNodes().toggleBulletList()
46
+ },
47
+ {
48
+ id: "ordered-list",
49
+ label: "Numbered list",
50
+ keywords: ["ordered"],
51
+ icon: /* @__PURE__ */ jsx(ListOrderedIcon, {}),
52
+ command: (chain) => chain.clearNodes().toggleOrderedList()
53
+ },
54
+ {
55
+ id: "task-list",
56
+ label: "Task list",
57
+ keywords: ["todo", "checklist"],
58
+ icon: /* @__PURE__ */ jsx(ListChecksIcon, {}),
59
+ command: (chain) => chain.clearNodes().toggleTaskList()
60
+ },
61
+ {
62
+ id: "quote",
63
+ label: "Quote",
64
+ keywords: ["blockquote"],
65
+ icon: /* @__PURE__ */ jsx(QuoteIcon, {}),
66
+ command: (chain) => chain.clearNodes().toggleBlockquote()
67
+ },
68
+ {
69
+ id: "code-block",
70
+ label: "Code block",
71
+ icon: /* @__PURE__ */ jsx(SquareCodeIcon, {}),
72
+ command: (chain) => chain.clearNodes().setCodeBlock()
73
+ },
74
+ {
75
+ id: "divider",
76
+ label: "Divider",
77
+ keywords: ["rule", "separator"],
78
+ icon: /* @__PURE__ */ jsx(MinusIcon, {}),
79
+ command: (chain) => chain.setHorizontalRule()
80
+ }
81
+ ];
82
+ function readTrigger(editor) {
83
+ const { selection } = editor.state;
84
+ const { $from } = selection;
85
+ if (!editor.isFocused || !selection.empty || !$from.parent.isTextblock || $from.parent.type.spec.code || editor.isActive("code") || editor.isActive("link")) return null;
86
+ const text = $from.parent.textBetween(0, $from.parentOffset, void 0, "");
87
+ const match = detectTrigger(text, ["/", "@"]);
88
+ if (!match || match.char === "/" && match.start !== 0) return null;
89
+ return {
90
+ ...match,
91
+ start: $from.start() + match.start,
92
+ end: $from.pos
93
+ };
94
+ }
95
+ function MarkdownEditorSuggestions({ mentions = [], onMentionSelect }) {
96
+ const activeItem = useRef(null);
97
+ const { editor, disabled, source } = useMarkdownEditor();
98
+ const enabled = Boolean(editor) && !disabled && !source;
99
+ const controller = useTriggerMenu({
100
+ triggers: [{
101
+ char: "/",
102
+ items: blockCommands
103
+ }, {
104
+ char: "@",
105
+ items: mentions
106
+ }],
107
+ onCommit: (item) => {
108
+ if (!editor || !enabled || item.disabled) return;
109
+ const match = readTrigger(editor);
110
+ if (!match) return;
111
+ const range = {
112
+ from: match.start,
113
+ to: match.end
114
+ };
115
+ if (match.char === "/" && item.command) item.command(editor.chain().focus().deleteRange(range)).run();
116
+ if (match.char === "@" && item.href && isEditorUrl(item.href)) {
117
+ editor.chain().focus().insertContentAt(range, [{
118
+ type: "text",
119
+ text: `@${item.label}`,
120
+ marks: [{
121
+ type: "link",
122
+ attrs: { href: item.href }
123
+ }]
124
+ }, {
125
+ type: "text",
126
+ text: " "
127
+ }]).run();
128
+ onMentionSelect?.({
129
+ ...item,
130
+ href: item.href
131
+ });
132
+ }
133
+ }
134
+ });
135
+ const report = useEffectEvent(() => {
136
+ if (!editor) return;
137
+ const match = enabled ? readTrigger(editor) : null;
138
+ const coords = match ? editor.view.coordsAtPos(match.end) : null;
139
+ controller.report(match, coords ? new DOMRect(coords.left, coords.top, 1, coords.bottom - coords.top) : null);
140
+ });
141
+ const handleKeyDown = useEffectEvent((event) => {
142
+ if (!editor || !enabled || isComposingKey(event) || event.metaKey || event.ctrlKey || event.altKey || !readTrigger(editor)) return;
143
+ if (controller.handleKeyDown(event.key)) {
144
+ event.preventDefault();
145
+ event.stopImmediatePropagation();
146
+ }
147
+ });
148
+ useEffect(() => {
149
+ if (!editor || !enabled) return;
150
+ const dom = editor.view.dom;
151
+ editor.on("transaction", report).on("focus", report).on("blur", report);
152
+ dom.addEventListener("keydown", handleKeyDown, true);
153
+ return () => {
154
+ editor.off("transaction", report).off("focus", report).off("blur", report);
155
+ dom.removeEventListener("keydown", handleKeyDown, true);
156
+ };
157
+ }, [editor, enabled]);
158
+ const open = enabled && controller.open;
159
+ useEffect(() => {
160
+ if (!editor || !open) return;
161
+ const owner = editor.view.dom.ownerDocument;
162
+ owner.addEventListener("scroll", report, {
163
+ capture: true,
164
+ passive: true
165
+ });
166
+ owner.defaultView?.addEventListener("resize", report);
167
+ return () => {
168
+ owner.removeEventListener("scroll", report, true);
169
+ owner.defaultView?.removeEventListener("resize", report);
170
+ };
171
+ }, [editor, open]);
172
+ const { activeIndex, activeChar, query } = controller;
173
+ useEffect(() => {
174
+ if (open) activeItem.current?.scrollIntoView({ block: "nearest" });
175
+ }, [
176
+ open,
177
+ activeIndex,
178
+ activeChar,
179
+ query
180
+ ]);
181
+ const { "aria-controls": controls, "aria-activedescendant": activeDescendant } = controller.inputAria;
182
+ useEffect(() => {
183
+ if (!editor || !enabled) return;
184
+ const dom = editor.view.dom;
185
+ const attributes = {
186
+ "aria-autocomplete": "list",
187
+ "aria-haspopup": "listbox",
188
+ "aria-controls": controls,
189
+ "aria-activedescendant": activeDescendant
190
+ };
191
+ for (const [name, value] of Object.entries(attributes)) if (value) dom.setAttribute(name, value);
192
+ else dom.removeAttribute(name);
193
+ return () => {
194
+ for (const name of Object.keys(attributes)) dom.removeAttribute(name);
195
+ };
196
+ }, [
197
+ editor,
198
+ enabled,
199
+ controls,
200
+ activeDescendant
201
+ ]);
202
+ const label = controller.activeChar === "@" ? "Mention people" : "Insert block";
203
+ const emptyLabel = controller.activeChar === "@" ? "No people found" : "No commands found";
204
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(LiveStatus, { message: open ? `${controller.items.length} ${label.toLowerCase()} suggestions` : "" }), /* @__PURE__ */ jsxs(TriggerMenu, {
205
+ open,
206
+ onOpenChange: controller.setOpen,
207
+ anchorRect: controller.anchorRect,
208
+ side: "bottom",
209
+ children: [controller.items.length === 0 ? /* @__PURE__ */ jsx(TriggerMenuEmpty, { children: emptyLabel }) : null, /* @__PURE__ */ jsx(TriggerMenuList, {
210
+ id: controller.listId,
211
+ "aria-label": label,
212
+ children: controller.items.map((item, index) => /* @__PURE__ */ jsxs(TriggerMenuItem, {
213
+ id: controller.optionId(index),
214
+ active: index === controller.activeIndex,
215
+ disabled: item.disabled || item.href !== void 0 && !isEditorUrl(item.href),
216
+ ref: index === controller.activeIndex ? activeItem : void 0,
217
+ onPointerMove: () => controller.setActiveIndex(index),
218
+ onClick: () => controller.select(item),
219
+ children: [
220
+ /* @__PURE__ */ jsx(TriggerMenuIcon, { children: item.icon ?? /* @__PURE__ */ jsx(UserIcon, {}) }),
221
+ /* @__PURE__ */ jsx("span", {
222
+ className: "min-w-0 flex-1 truncate",
223
+ children: item.label
224
+ }),
225
+ item.description ? /* @__PURE__ */ jsx("span", {
226
+ className: "truncate text-muted-foreground",
227
+ children: item.description
228
+ }) : null
229
+ ]
230
+ }, item.id))
231
+ })]
232
+ })] });
233
+ }
234
+ //#endregion
235
+ export { MarkdownEditorSuggestions };
@@ -0,0 +1,9 @@
1
+ import { ReactNode } from "react";
2
+ //#region src/markdown-editor/toolbar.d.ts
3
+ export declare function MarkdownEditorToolbar({ children }: {
4
+ children?: ReactNode;
5
+ }): import("react").JSX.Element;
6
+ export declare function MarkdownEditorLink(): import("react").JSX.Element;
7
+ export declare function MarkdownEditorImage(): import("react").JSX.Element;
8
+ export declare function MarkdownEditorUploads(): import("react").JSX.Element;
9
+ //#endregion