@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.
- package/README.md +7 -0
- package/dist/action-bar.js +2 -2
- package/dist/audio-device-select.js +5 -2
- package/dist/blocks/coding-agent.js +22 -12
- package/dist/blocks/design-canvas-inspector.js +6 -3
- package/dist/blocks/design-canvas.js +20 -10
- package/dist/blocks/discussion-markdown-input.d.ts +14 -0
- package/dist/blocks/discussion-markdown-input.js +36 -0
- package/dist/blocks/discussion.d.ts +9 -3
- package/dist/blocks/discussion.js +82 -16
- package/dist/blocks/file-explorer.js +46 -24
- package/dist/blocks/settings.js +10 -6
- package/dist/chat-composer-editor/extensions/mention.d.ts +1 -1
- package/dist/chat-message.d.ts +13 -0
- package/dist/chat-message.js +198 -3
- package/dist/filter-bar/editor.js +5 -2
- package/dist/knob-contracts/flow-knobs.d.ts +4 -0
- package/dist/knob-contracts/flow-knobs.js +32 -0
- package/dist/knob-contracts/item-knobs.d.ts +1 -1
- package/dist/knob-contracts/item-knobs.js +4 -1
- package/dist/knob-contracts/label-knobs.d.ts +4 -0
- package/dist/knob-contracts/label-knobs.js +4 -0
- package/dist/knob-contracts/markdown-editor-knobs.d.ts +4 -0
- package/dist/knob-contracts/markdown-editor-knobs.js +17 -0
- package/dist/knob-contracts/morphing-panel-knobs.d.ts +1 -1
- package/dist/knob-contracts/morphing-panel-knobs.js +2 -1
- package/dist/knob-contracts/resize-handle-knobs.d.ts +4 -0
- package/dist/knob-contracts/resize-handle-knobs.js +12 -0
- package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
- package/dist/knob-contracts/sidebar-knobs.js +10 -0
- package/dist/knob-contracts/switch-knobs.d.ts +1 -1
- package/dist/knob-contracts/switch-knobs.js +1 -0
- package/dist/lib/canvas-grid.d.ts +4 -0
- package/dist/lib/canvas-grid.js +5 -0
- package/dist/markdown-editor/context.d.ts +22 -0
- package/dist/markdown-editor/context.js +10 -0
- package/dist/markdown-editor/drag-handle.d.ts +6 -0
- package/dist/markdown-editor/drag-handle.js +126 -0
- package/dist/markdown-editor/extensions.d.ts +10 -0
- package/dist/markdown-editor/extensions.js +139 -0
- package/dist/markdown-editor/suggestions.d.ts +12 -0
- package/dist/markdown-editor/suggestions.js +235 -0
- package/dist/markdown-editor/toolbar.d.ts +9 -0
- package/dist/markdown-editor/toolbar.js +339 -0
- package/dist/markdown-editor/uploads.d.ts +23 -0
- package/dist/markdown-editor/uploads.js +32 -0
- package/dist/markdown-editor/use-image-uploads.d.ts +12 -0
- package/dist/markdown-editor/use-image-uploads.js +120 -0
- package/dist/markdown-editor.d.ts +27 -0
- package/dist/markdown-editor.js +202 -0
- package/dist/styles/index.css +9 -4
- package/dist/styles/recipes/chat-message-edit.css +105 -0
- package/dist/styles/recipes/code.css +4 -0
- package/dist/styles/recipes/field.css +1 -1
- package/dist/styles/recipes/flow.css +304 -0
- package/dist/styles/recipes/infinite-canvas.css +1 -1
- package/dist/styles/recipes/item.css +39 -2
- package/dist/styles/recipes/label.css +19 -0
- package/dist/styles/recipes/markdown-editor.css +234 -0
- package/dist/styles/recipes/morphing-panel.css +53 -12
- package/dist/styles/recipes/page-layout.css +11 -1
- package/dist/styles/recipes/resize-handle.css +126 -0
- package/dist/styles/recipes/sidebar-mobile.css +171 -0
- package/dist/styles/recipes/switch.css +17 -5
- package/dist/styles/recipes/toolbar.css +8 -1
- package/dist/styles/theme.css +1 -0
- package/dist/ui/autocomplete.js +1 -1
- package/dist/ui/code.js +1 -0
- package/dist/ui/color-picker.js +1 -1
- package/dist/ui/combobox.d.ts +1 -1
- package/dist/ui/combobox.js +1 -1
- package/dist/ui/dockable-panel.js +1 -1
- package/dist/ui/flow.d.ts +51 -0
- package/dist/ui/flow.js +153 -0
- package/dist/ui/infinite-canvas.js +3 -3
- package/dist/ui/item.d.ts +5 -3
- package/dist/ui/item.js +2 -1
- package/dist/ui/label.d.ts +8 -0
- package/dist/ui/label.js +14 -0
- package/dist/ui/markdown.d.ts +2 -1
- package/dist/ui/markdown.js +3 -3
- package/dist/ui/morphing-panel.d.ts +21 -4
- package/dist/ui/morphing-panel.js +152 -36
- package/dist/ui/resize-handle.d.ts +11 -0
- package/dist/ui/resize-handle.js +26 -0
- package/dist/ui/sidebar-mobile.d.ts +17 -0
- package/dist/ui/sidebar-mobile.js +70 -0
- package/dist/ui/sidebar-provider.d.ts +1 -0
- package/dist/ui/sidebar-provider.js +3 -0
- package/dist/ui/sidebar.d.ts +3 -1
- package/dist/ui/sidebar.js +101 -49
- package/dist/ui/toolbar.d.ts +3 -1
- package/dist/ui/toolbar.js +2 -1
- package/dist/ui/typography.d.ts +47 -0
- package/dist/ui/typography.js +61 -0
- package/dist/ui/use-switch-drag.js +1 -1
- package/package.json +41 -1
- package/dist/environment-variables.d.ts +0 -125
- package/dist/environment-variables.js +0 -512
- package/dist/hooks/use-environment-variables.d.ts +0 -50
- package/dist/hooks/use-environment-variables.js +0 -188
- package/dist/knob-contracts/environment-variables-knobs.d.ts +0 -4
- package/dist/knob-contracts/environment-variables-knobs.js +0 -12
- package/dist/lib/env-file.d.ts +0 -27
- package/dist/lib/env-file.js +0 -218
- 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 ? `` : ``;
|
|
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" && [" ", "\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
|