2026-02-20 13:35:15 -06:00
|
|
|
import {
|
|
|
|
|
forwardRef,
|
|
|
|
|
useCallback,
|
|
|
|
|
useEffect,
|
|
|
|
|
useImperativeHandle,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
type DragEvent,
|
|
|
|
|
} from "react";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import {
|
2026-03-02 16:44:03 -06:00
|
|
|
CodeMirrorEditor,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
MDXEditor,
|
2026-02-26 16:33:48 -06:00
|
|
|
codeBlockPlugin,
|
|
|
|
|
codeMirrorPlugin,
|
2026-03-02 16:44:03 -06:00
|
|
|
type CodeBlockEditorDescriptor,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
type MDXEditorMethods,
|
|
|
|
|
headingsPlugin,
|
|
|
|
|
imagePlugin,
|
|
|
|
|
linkDialogPlugin,
|
|
|
|
|
linkPlugin,
|
|
|
|
|
listsPlugin,
|
|
|
|
|
markdownShortcutPlugin,
|
|
|
|
|
quotePlugin,
|
2026-02-26 16:33:48 -06:00
|
|
|
tablePlugin,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
thematicBreakPlugin,
|
|
|
|
|
type RealmPlugin,
|
|
|
|
|
} from "@mdxeditor/editor";
|
2026-03-21 16:39:12 -05:00
|
|
|
import { LinkNode } from "@lexical/link";
|
2026-03-21 14:48:10 -05:00
|
|
|
import { buildAgentMentionHref, buildProjectMentionHref } from "@paperclipai/shared";
|
|
|
|
|
import { AgentIcon } from "./AgentIconPicker";
|
|
|
|
|
import { applyMentionChipDecoration, clearMentionChipDecoration, parseMentionChipHref } from "../lib/mention-chips";
|
2026-03-22 06:34:15 -05:00
|
|
|
import { mentionDeletionPlugin } from "../lib/mention-deletion";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { cn } from "../lib/utils";
|
|
|
|
|
|
2026-03-21 16:39:12 -05:00
|
|
|
/* ---- Allow custom mention URL schemes in Lexical's LinkNode ---- */
|
|
|
|
|
// Lexical only allows http(s)/mailto/sms/tel by default, converting
|
|
|
|
|
// everything else to about:blank. We need agent:// and project://
|
|
|
|
|
// to survive the markdown→Lexical import so mention chips render.
|
|
|
|
|
const _origSanitizeUrl = LinkNode.prototype.sanitizeUrl;
|
|
|
|
|
LinkNode.prototype.sanitizeUrl = function sanitizeUrl(url: string): string {
|
|
|
|
|
if (/^(agent|project):\/\//.test(url)) return url;
|
|
|
|
|
return _origSanitizeUrl.call(this, url);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
/* ---- Mention types ---- */
|
|
|
|
|
|
|
|
|
|
export interface MentionOption {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
2026-03-02 13:31:58 -06:00
|
|
|
kind?: "agent" | "project";
|
2026-03-21 14:48:10 -05:00
|
|
|
agentId?: string;
|
|
|
|
|
agentIcon?: string | null;
|
2026-03-02 13:31:58 -06:00
|
|
|
projectId?: string;
|
|
|
|
|
projectColor?: string | null;
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Editor props ---- */
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
interface MarkdownEditorProps {
|
|
|
|
|
value: string;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
className?: string;
|
|
|
|
|
contentClassName?: string;
|
|
|
|
|
onBlur?: () => void;
|
|
|
|
|
imageUploadHandler?: (file: File) => Promise<string>;
|
|
|
|
|
bordered?: boolean;
|
2026-03-02 13:31:58 -06:00
|
|
|
/** List of mentionable entities. Enables @-mention autocomplete. */
|
2026-02-20 13:35:15 -06:00
|
|
|
mentions?: MentionOption[];
|
|
|
|
|
/** Called on Cmd/Ctrl+Enter */
|
|
|
|
|
onSubmit?: () => void;
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MarkdownEditorRef {
|
|
|
|
|
focus: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 09:21:44 -05:00
|
|
|
function escapeRegExp(value: string): string {
|
|
|
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:59:20 -05:00
|
|
|
function isSafeMarkdownLinkUrl(url: string): boolean {
|
|
|
|
|
const trimmed = url.trim();
|
|
|
|
|
if (!trimmed) return true;
|
|
|
|
|
return !/^(javascript|data|vbscript):/i.test(trimmed);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
/* ---- Mention detection helpers ---- */
|
|
|
|
|
|
|
|
|
|
interface MentionState {
|
|
|
|
|
query: string;
|
|
|
|
|
top: number;
|
|
|
|
|
left: number;
|
|
|
|
|
textNode: Text;
|
|
|
|
|
atPos: number;
|
|
|
|
|
endPos: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const CODE_BLOCK_LANGUAGES: Record<string, string> = {
|
|
|
|
|
txt: "Text",
|
|
|
|
|
md: "Markdown",
|
|
|
|
|
js: "JavaScript",
|
|
|
|
|
jsx: "JavaScript (JSX)",
|
|
|
|
|
ts: "TypeScript",
|
|
|
|
|
tsx: "TypeScript (TSX)",
|
|
|
|
|
json: "JSON",
|
|
|
|
|
bash: "Bash",
|
|
|
|
|
sh: "Shell",
|
|
|
|
|
python: "Python",
|
|
|
|
|
go: "Go",
|
|
|
|
|
rust: "Rust",
|
|
|
|
|
sql: "SQL",
|
|
|
|
|
html: "HTML",
|
|
|
|
|
css: "CSS",
|
|
|
|
|
yaml: "YAML",
|
|
|
|
|
yml: "YAML",
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 16:44:03 -06:00
|
|
|
const FALLBACK_CODE_BLOCK_DESCRIPTOR: CodeBlockEditorDescriptor = {
|
|
|
|
|
// Keep this lower than codeMirrorPlugin's descriptor priority so known languages
|
|
|
|
|
// still use the standard matching path; this catches malformed/unknown fences.
|
|
|
|
|
priority: 0,
|
|
|
|
|
match: () => true,
|
|
|
|
|
Editor: CodeMirrorEditor,
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
function detectMention(container: HTMLElement): MentionState | null {
|
|
|
|
|
const sel = window.getSelection();
|
|
|
|
|
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return null;
|
|
|
|
|
|
|
|
|
|
const range = sel.getRangeAt(0);
|
|
|
|
|
const textNode = range.startContainer;
|
|
|
|
|
if (textNode.nodeType !== Node.TEXT_NODE) return null;
|
|
|
|
|
if (!container.contains(textNode)) return null;
|
|
|
|
|
|
|
|
|
|
const text = textNode.textContent ?? "";
|
|
|
|
|
const offset = range.startOffset;
|
|
|
|
|
|
|
|
|
|
// Walk backwards from cursor to find @
|
|
|
|
|
let atPos = -1;
|
|
|
|
|
for (let i = offset - 1; i >= 0; i--) {
|
|
|
|
|
const ch = text[i];
|
|
|
|
|
if (ch === "@") {
|
|
|
|
|
if (i === 0 || /\s/.test(text[i - 1])) {
|
|
|
|
|
atPos = i;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (/\s/.test(ch)) break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (atPos === -1) return null;
|
|
|
|
|
|
|
|
|
|
const query = text.slice(atPos + 1, offset);
|
|
|
|
|
|
|
|
|
|
// Get position relative to container
|
|
|
|
|
const tempRange = document.createRange();
|
|
|
|
|
tempRange.setStart(textNode, atPos);
|
|
|
|
|
tempRange.setEnd(textNode, atPos + 1);
|
|
|
|
|
const rect = tempRange.getBoundingClientRect();
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
query,
|
|
|
|
|
top: rect.bottom - containerRect.top,
|
|
|
|
|
left: rect.left - containerRect.left,
|
|
|
|
|
textNode: textNode as Text,
|
|
|
|
|
atPos,
|
|
|
|
|
endPos: offset,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
function mentionMarkdown(option: MentionOption): string {
|
|
|
|
|
if (option.kind === "project" && option.projectId) {
|
|
|
|
|
return `[@${option.name}](${buildProjectMentionHref(option.projectId, option.projectColor ?? null)}) `;
|
|
|
|
|
}
|
2026-03-21 14:48:10 -05:00
|
|
|
const agentId = option.agentId ?? option.id.replace(/^agent:/, "");
|
|
|
|
|
return `[@${option.name}](${buildAgentMentionHref(agentId, option.agentIcon ?? null)}) `;
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Replace `@<query>` in the markdown string with the selected mention token. */
|
2026-02-20 14:53:46 -06:00
|
|
|
function applyMention(markdown: string, query: string, option: MentionOption): string {
|
|
|
|
|
const search = `@${query}`;
|
2026-03-02 13:31:58 -06:00
|
|
|
const replacement = mentionMarkdown(option);
|
2026-02-20 14:53:46 -06:00
|
|
|
const idx = markdown.lastIndexOf(search);
|
|
|
|
|
if (idx === -1) return markdown;
|
|
|
|
|
return markdown.slice(0, idx) + replacement + markdown.slice(idx + search.length);
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---- Component ---- */
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(function MarkdownEditor({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
placeholder,
|
|
|
|
|
className,
|
|
|
|
|
contentClassName,
|
|
|
|
|
onBlur,
|
|
|
|
|
imageUploadHandler,
|
|
|
|
|
bordered = true,
|
2026-02-20 13:35:15 -06:00
|
|
|
mentions,
|
|
|
|
|
onSubmit,
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
}: MarkdownEditorProps, forwardedRef) {
|
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const ref = useRef<MDXEditorMethods>(null);
|
|
|
|
|
const latestValueRef = useRef(value);
|
|
|
|
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
|
|
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
|
|
|
const dragDepthRef = useRef(0);
|
|
|
|
|
|
2026-03-03 09:36:49 -06:00
|
|
|
// Stable ref for imageUploadHandler so plugins don't recreate on every render
|
|
|
|
|
const imageUploadHandlerRef = useRef(imageUploadHandler);
|
|
|
|
|
imageUploadHandlerRef.current = imageUploadHandler;
|
|
|
|
|
|
2026-02-20 14:59:20 -06:00
|
|
|
// Mention state (ref kept in sync so callbacks always see the latest value)
|
2026-02-20 13:35:15 -06:00
|
|
|
const [mentionState, setMentionState] = useState<MentionState | null>(null);
|
2026-02-20 14:59:20 -06:00
|
|
|
const mentionStateRef = useRef<MentionState | null>(null);
|
2026-02-20 13:35:15 -06:00
|
|
|
const [mentionIndex, setMentionIndex] = useState(0);
|
|
|
|
|
const mentionActive = mentionState !== null && mentions && mentions.length > 0;
|
2026-03-21 14:48:10 -05:00
|
|
|
const mentionOptionByKey = useMemo(() => {
|
|
|
|
|
const map = new Map<string, MentionOption>();
|
2026-03-02 13:31:58 -06:00
|
|
|
for (const mention of mentions ?? []) {
|
2026-03-21 14:48:10 -05:00
|
|
|
if (mention.kind === "agent") {
|
|
|
|
|
const agentId = mention.agentId ?? mention.id.replace(/^agent:/, "");
|
|
|
|
|
map.set(`agent:${agentId}`, mention);
|
|
|
|
|
}
|
2026-03-02 13:31:58 -06:00
|
|
|
if (mention.kind === "project" && mention.projectId) {
|
2026-03-21 14:48:10 -05:00
|
|
|
map.set(`project:${mention.projectId}`, mention);
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}, [mentions]);
|
2026-02-20 13:35:15 -06:00
|
|
|
|
|
|
|
|
const filteredMentions = useMemo(() => {
|
|
|
|
|
if (!mentionState || !mentions) return [];
|
|
|
|
|
const q = mentionState.query.toLowerCase();
|
|
|
|
|
return mentions.filter((m) => m.name.toLowerCase().includes(q)).slice(0, 8);
|
|
|
|
|
}, [mentionState?.query, mentions]);
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
useImperativeHandle(forwardedRef, () => ({
|
|
|
|
|
focus: () => {
|
|
|
|
|
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
|
|
|
|
},
|
|
|
|
|
}), []);
|
|
|
|
|
|
2026-03-03 09:36:49 -06:00
|
|
|
// Whether the image plugin should be included (boolean is stable across renders
|
|
|
|
|
// as long as the handler presence doesn't toggle)
|
|
|
|
|
const hasImageUpload = Boolean(imageUploadHandler);
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const plugins = useMemo<RealmPlugin[]>(() => {
|
2026-03-03 09:36:49 -06:00
|
|
|
const imageHandler = hasImageUpload
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
? async (file: File) => {
|
2026-03-03 09:36:49 -06:00
|
|
|
const handler = imageUploadHandlerRef.current;
|
|
|
|
|
if (!handler) throw new Error("No image upload handler");
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
try {
|
2026-03-03 09:36:49 -06:00
|
|
|
const src = await handler(file);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
setUploadError(null);
|
2026-03-16 16:50:18 -05:00
|
|
|
// After MDXEditor inserts the image, ensure two newlines follow it
|
|
|
|
|
// so the cursor isn't stuck right next to the image.
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const current = latestValueRef.current;
|
2026-03-17 09:21:44 -05:00
|
|
|
const escapedSrc = escapeRegExp(src);
|
2026-03-16 16:50:18 -05:00
|
|
|
const updated = current.replace(
|
2026-03-17 09:21:44 -05:00
|
|
|
new RegExp(`(!\\[[^\\]]*\\]\\(${escapedSrc}\\))(?!\\n\\n)`, "g"),
|
|
|
|
|
"$1\n\n",
|
2026-03-16 16:50:18 -05:00
|
|
|
);
|
|
|
|
|
if (updated !== current) {
|
|
|
|
|
latestValueRef.current = updated;
|
|
|
|
|
ref.current?.setMarkdown(updated);
|
|
|
|
|
onChange(updated);
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
ref.current?.focus(undefined, { defaultSelection: "rootEnd" });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, 100);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
return src;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const message = err instanceof Error ? err.message : "Image upload failed";
|
|
|
|
|
setUploadError(message);
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
const all: RealmPlugin[] = [
|
|
|
|
|
headingsPlugin(),
|
|
|
|
|
listsPlugin(),
|
|
|
|
|
quotePlugin(),
|
2026-02-26 16:33:48 -06:00
|
|
|
tablePlugin(),
|
2026-03-21 14:59:20 -05:00
|
|
|
linkPlugin({ validateUrl: isSafeMarkdownLinkUrl }),
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
linkDialogPlugin(),
|
2026-03-22 06:34:15 -05:00
|
|
|
mentionDeletionPlugin(),
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
thematicBreakPlugin(),
|
2026-03-02 16:44:03 -06:00
|
|
|
codeBlockPlugin({
|
|
|
|
|
defaultCodeBlockLanguage: "txt",
|
|
|
|
|
codeBlockEditorDescriptors: [FALLBACK_CODE_BLOCK_DESCRIPTOR],
|
|
|
|
|
}),
|
2026-02-26 16:33:48 -06:00
|
|
|
codeMirrorPlugin({ codeBlockLanguages: CODE_BLOCK_LANGUAGES }),
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
markdownShortcutPlugin(),
|
|
|
|
|
];
|
|
|
|
|
if (imageHandler) {
|
|
|
|
|
all.push(imagePlugin({ imageUploadHandler: imageHandler }));
|
|
|
|
|
}
|
|
|
|
|
return all;
|
2026-03-03 09:36:49 -06:00
|
|
|
}, [hasImageUpload]);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (value !== latestValueRef.current) {
|
|
|
|
|
ref.current?.setMarkdown(value);
|
|
|
|
|
latestValueRef.current = value;
|
|
|
|
|
}
|
|
|
|
|
}, [value]);
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
const decorateProjectMentions = useCallback(() => {
|
|
|
|
|
const editable = containerRef.current?.querySelector('[contenteditable="true"]');
|
|
|
|
|
if (!editable) return;
|
|
|
|
|
const links = editable.querySelectorAll("a");
|
|
|
|
|
for (const node of links) {
|
|
|
|
|
const link = node as HTMLAnchorElement;
|
2026-03-21 14:48:10 -05:00
|
|
|
const parsed = parseMentionChipHref(link.getAttribute("href") ?? "");
|
2026-03-02 13:31:58 -06:00
|
|
|
if (!parsed) {
|
2026-03-21 14:48:10 -05:00
|
|
|
clearMentionChipDecoration(link);
|
2026-03-02 13:31:58 -06:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
if (parsed.kind === "project") {
|
|
|
|
|
const option = mentionOptionByKey.get(`project:${parsed.projectId}`);
|
|
|
|
|
applyMentionChipDecoration(link, {
|
|
|
|
|
...parsed,
|
|
|
|
|
color: parsed.color ?? option?.projectColor ?? null,
|
|
|
|
|
});
|
|
|
|
|
continue;
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
2026-03-21 14:48:10 -05:00
|
|
|
|
|
|
|
|
const option = mentionOptionByKey.get(`agent:${parsed.agentId}`);
|
|
|
|
|
applyMentionChipDecoration(link, {
|
|
|
|
|
...parsed,
|
|
|
|
|
icon: parsed.icon ?? option?.agentIcon ?? null,
|
|
|
|
|
});
|
2026-03-02 13:31:58 -06:00
|
|
|
}
|
2026-03-21 14:48:10 -05:00
|
|
|
}, [mentionOptionByKey]);
|
2026-03-02 13:31:58 -06:00
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
// Mention detection: listen for selection changes and input events
|
|
|
|
|
const checkMention = useCallback(() => {
|
|
|
|
|
if (!mentions || mentions.length === 0 || !containerRef.current) {
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
setMentionState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const result = detectMention(containerRef.current);
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = result;
|
2026-02-20 13:35:15 -06:00
|
|
|
if (result) {
|
|
|
|
|
setMentionState(result);
|
|
|
|
|
setMentionIndex(0);
|
|
|
|
|
} else {
|
|
|
|
|
setMentionState(null);
|
|
|
|
|
}
|
|
|
|
|
}, [mentions]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!mentions || mentions.length === 0) return;
|
|
|
|
|
|
2026-02-20 14:53:46 -06:00
|
|
|
const el = containerRef.current;
|
|
|
|
|
// Listen for input events on the container so mention detection
|
|
|
|
|
// also fires after typing (e.g. space to dismiss).
|
|
|
|
|
const onInput = () => requestAnimationFrame(checkMention);
|
|
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
document.addEventListener("selectionchange", checkMention);
|
2026-02-20 14:53:46 -06:00
|
|
|
el?.addEventListener("input", onInput, true);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("selectionchange", checkMention);
|
|
|
|
|
el?.removeEventListener("input", onInput, true);
|
|
|
|
|
};
|
2026-02-20 13:35:15 -06:00
|
|
|
}, [checkMention, mentions]);
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
const editable = containerRef.current?.querySelector('[contenteditable="true"]');
|
|
|
|
|
if (!editable) return;
|
|
|
|
|
decorateProjectMentions();
|
|
|
|
|
const observer = new MutationObserver(() => {
|
|
|
|
|
decorateProjectMentions();
|
|
|
|
|
});
|
|
|
|
|
observer.observe(editable, {
|
|
|
|
|
subtree: true,
|
|
|
|
|
childList: true,
|
|
|
|
|
characterData: true,
|
|
|
|
|
});
|
|
|
|
|
return () => observer.disconnect();
|
|
|
|
|
}, [decorateProjectMentions, value]);
|
|
|
|
|
|
2026-02-20 13:35:15 -06:00
|
|
|
const selectMention = useCallback(
|
|
|
|
|
(option: MentionOption) => {
|
2026-02-20 14:59:20 -06:00
|
|
|
// Read from ref to avoid stale-closure issues (selectionchange can
|
|
|
|
|
// update state between the last render and this callback firing).
|
|
|
|
|
const state = mentionStateRef.current;
|
|
|
|
|
if (!state) return;
|
2026-03-21 14:48:10 -05:00
|
|
|
const current = latestValueRef.current;
|
|
|
|
|
const next = applyMention(current, state.query, option);
|
|
|
|
|
if (next !== current) {
|
|
|
|
|
latestValueRef.current = next;
|
|
|
|
|
ref.current?.setMarkdown(next);
|
|
|
|
|
onChange(next);
|
|
|
|
|
}
|
2026-02-20 16:07:37 -06:00
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
requestAnimationFrame(() => {
|
2026-03-02 13:32:15 -06:00
|
|
|
requestAnimationFrame(() => {
|
2026-03-21 14:48:10 -05:00
|
|
|
const editable = containerRef.current?.querySelector('[contenteditable="true"]');
|
|
|
|
|
if (!(editable instanceof HTMLElement)) return;
|
2026-03-02 13:32:15 -06:00
|
|
|
decorateProjectMentions();
|
2026-03-21 14:48:10 -05:00
|
|
|
editable.focus();
|
2026-03-02 13:32:15 -06:00
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
const mentionHref = option.kind === "project" && option.projectId
|
|
|
|
|
? buildProjectMentionHref(option.projectId, option.projectColor ?? null)
|
|
|
|
|
: buildAgentMentionHref(
|
|
|
|
|
option.agentId ?? option.id.replace(/^agent:/, ""),
|
|
|
|
|
option.agentIcon ?? null,
|
|
|
|
|
);
|
|
|
|
|
const matchingMentions = Array.from(editable.querySelectorAll("a"))
|
|
|
|
|
.filter((node): node is HTMLAnchorElement => node instanceof HTMLAnchorElement)
|
|
|
|
|
.filter((link) => {
|
|
|
|
|
const href = link.getAttribute("href") ?? "";
|
|
|
|
|
return href === mentionHref && link.textContent === `@${option.name}`;
|
|
|
|
|
});
|
|
|
|
|
const containerRect = containerRef.current?.getBoundingClientRect();
|
|
|
|
|
const target = matchingMentions.sort((a, b) => {
|
|
|
|
|
const rectA = a.getBoundingClientRect();
|
|
|
|
|
const rectB = b.getBoundingClientRect();
|
|
|
|
|
const leftA = containerRect ? rectA.left - containerRect.left : rectA.left;
|
|
|
|
|
const topA = containerRect ? rectA.top - containerRect.top : rectA.top;
|
|
|
|
|
const leftB = containerRect ? rectB.left - containerRect.left : rectB.left;
|
|
|
|
|
const topB = containerRect ? rectB.top - containerRect.top : rectB.top;
|
|
|
|
|
const distA = Math.hypot(leftA - state.left, topA - state.top);
|
|
|
|
|
const distB = Math.hypot(leftB - state.left, topB - state.top);
|
|
|
|
|
return distA - distB;
|
|
|
|
|
})[0] ?? null;
|
|
|
|
|
if (!target) return;
|
|
|
|
|
|
|
|
|
|
const selection = window.getSelection();
|
|
|
|
|
if (!selection) return;
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
const nextSibling = target.nextSibling;
|
|
|
|
|
if (nextSibling?.nodeType === Node.TEXT_NODE) {
|
|
|
|
|
const text = nextSibling.textContent ?? "";
|
|
|
|
|
if (text.startsWith(" ")) {
|
|
|
|
|
range.setStart(nextSibling, 1);
|
|
|
|
|
range.collapse(true);
|
|
|
|
|
selection.removeAllRanges();
|
|
|
|
|
selection.addRange(range);
|
2026-02-20 16:07:37 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 14:48:10 -05:00
|
|
|
range.setStartAfter(target);
|
|
|
|
|
range.collapse(true);
|
|
|
|
|
selection.removeAllRanges();
|
|
|
|
|
selection.addRange(range);
|
|
|
|
|
});
|
2026-03-02 13:31:58 -06:00
|
|
|
});
|
|
|
|
|
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
setMentionState(null);
|
|
|
|
|
},
|
2026-03-02 13:31:58 -06:00
|
|
|
[decorateProjectMentions, onChange],
|
2026-02-20 13:35:15 -06:00
|
|
|
);
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
function hasFilePayload(evt: DragEvent<HTMLDivElement>) {
|
|
|
|
|
return Array.from(evt.dataTransfer?.types ?? []).includes("Files");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const canDropImage = Boolean(imageUploadHandler);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={containerRef}
|
|
|
|
|
className={cn(
|
|
|
|
|
"relative paperclip-mdxeditor-scope",
|
|
|
|
|
bordered ? "rounded-md border border-border bg-transparent" : "bg-transparent",
|
|
|
|
|
isDragOver && "ring-1 ring-primary/60 bg-accent/20",
|
|
|
|
|
className,
|
|
|
|
|
)}
|
2026-02-20 13:35:15 -06:00
|
|
|
onKeyDownCapture={(e) => {
|
|
|
|
|
// Cmd/Ctrl+Enter to submit
|
|
|
|
|
if (onSubmit && e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onSubmit();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 14:59:20 -06:00
|
|
|
// Mention keyboard handling
|
|
|
|
|
if (mentionActive) {
|
|
|
|
|
// Space dismisses the popup (let the character be typed normally)
|
|
|
|
|
if (e.key === " ") {
|
|
|
|
|
mentionStateRef.current = null;
|
|
|
|
|
setMentionState(null);
|
2026-02-20 13:35:15 -06:00
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 14:59:20 -06:00
|
|
|
// Escape always dismisses
|
2026-02-20 13:35:15 -06:00
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2026-02-20 14:59:20 -06:00
|
|
|
mentionStateRef.current = null;
|
2026-02-20 13:35:15 -06:00
|
|
|
setMentionState(null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-20 14:59:20 -06:00
|
|
|
// Arrow / Enter / Tab only when there are filtered results
|
|
|
|
|
if (filteredMentions.length > 0) {
|
|
|
|
|
if (e.key === "ArrowDown") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setMentionIndex((prev) => Math.min(prev + 1, filteredMentions.length - 1));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "ArrowUp") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setMentionIndex((prev) => Math.max(prev - 1, 0));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (e.key === "Enter" || e.key === "Tab") {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
selectMention(filteredMentions[mentionIndex]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-20 13:35:15 -06:00
|
|
|
}
|
|
|
|
|
}}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
onDragEnter={(evt) => {
|
|
|
|
|
if (!canDropImage || !hasFilePayload(evt)) return;
|
|
|
|
|
dragDepthRef.current += 1;
|
|
|
|
|
setIsDragOver(true);
|
|
|
|
|
}}
|
|
|
|
|
onDragOver={(evt) => {
|
|
|
|
|
if (!canDropImage || !hasFilePayload(evt)) return;
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
evt.dataTransfer.dropEffect = "copy";
|
|
|
|
|
}}
|
2026-02-20 13:35:15 -06:00
|
|
|
onDragLeave={() => {
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
if (!canDropImage) return;
|
|
|
|
|
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
|
|
|
|
if (dragDepthRef.current === 0) setIsDragOver(false);
|
|
|
|
|
}}
|
|
|
|
|
onDrop={() => {
|
|
|
|
|
dragDepthRef.current = 0;
|
|
|
|
|
setIsDragOver(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<MDXEditor
|
|
|
|
|
ref={ref}
|
|
|
|
|
markdown={value}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
onChange={(next) => {
|
|
|
|
|
latestValueRef.current = next;
|
|
|
|
|
onChange(next);
|
|
|
|
|
}}
|
|
|
|
|
onBlur={() => onBlur?.()}
|
|
|
|
|
className={cn("paperclip-mdxeditor", !bordered && "paperclip-mdxeditor--borderless")}
|
|
|
|
|
contentEditableClassName={cn(
|
2026-03-04 12:20:29 -06:00
|
|
|
"paperclip-mdxeditor-content focus:outline-none [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:list-item",
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
contentClassName,
|
|
|
|
|
)}
|
|
|
|
|
plugins={plugins}
|
|
|
|
|
/>
|
2026-02-20 13:35:15 -06:00
|
|
|
|
|
|
|
|
{/* Mention dropdown */}
|
|
|
|
|
{mentionActive && filteredMentions.length > 0 && (
|
|
|
|
|
<div
|
|
|
|
|
className="absolute z-50 min-w-[180px] max-h-[200px] overflow-y-auto rounded-md border border-border bg-popover shadow-md"
|
|
|
|
|
style={{ top: mentionState.top + 4, left: mentionState.left }}
|
|
|
|
|
>
|
|
|
|
|
{filteredMentions.map((option, i) => (
|
|
|
|
|
<button
|
|
|
|
|
key={option.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-3 py-1.5 text-sm text-left hover:bg-accent/50 transition-colors",
|
|
|
|
|
i === mentionIndex && "bg-accent",
|
|
|
|
|
)}
|
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
e.preventDefault(); // prevent blur
|
|
|
|
|
selectMention(option);
|
|
|
|
|
}}
|
|
|
|
|
onMouseEnter={() => setMentionIndex(i)}
|
|
|
|
|
>
|
2026-03-02 13:31:58 -06:00
|
|
|
{option.kind === "project" && option.projectId ? (
|
|
|
|
|
<span
|
|
|
|
|
className="inline-flex h-2 w-2 rounded-full border border-border/50"
|
|
|
|
|
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
2026-03-21 14:48:10 -05:00
|
|
|
<AgentIcon
|
|
|
|
|
icon={option.agentIcon}
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
|
|
|
|
/>
|
2026-03-02 13:31:58 -06:00
|
|
|
)}
|
2026-02-20 13:35:15 -06:00
|
|
|
<span>{option.name}</span>
|
2026-03-02 13:31:58 -06:00
|
|
|
{option.kind === "project" && option.projectId && (
|
|
|
|
|
<span className="ml-auto text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
Project
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-02-20 13:35:15 -06:00
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
{isDragOver && canDropImage && (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"pointer-events-none absolute inset-1 z-40 flex items-center justify-center rounded-md border border-dashed border-primary/80 bg-primary/10 text-xs font-medium text-primary",
|
|
|
|
|
!bordered && "inset-0 rounded-sm",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
Drop image to upload
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{uploadError && (
|
|
|
|
|
<p className="px-3 pb-2 text-xs text-destructive">{uploadError}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|