/** * `` — true-WYSIWYG markdown editing on the native * `` element. * * The external contract is **markdown**: `value` in, `onChange(markdown)` out. * Internally the editor converts markdown ↔ the element's `RichDoc` span model * (`convert/mdToDoc`, `convert/docToMd`) and drives formatting through * fire-and-forget commands; the element is the single source of truth for live * text and selection (lightly-controlled — keystrokes are never echoed back). * * ### Echo / IME rules (JS side) * - An incoming `value` identical to the last markdown we emitted is our own * echo → ignored (string compare; exact). * - Otherwise it's compared structurally against the element's last document — * only genuinely different content is pushed via `setDocument`. * - While the IME is composing, external values are buffered and applied on * the composition-end change; `onChange` is also suppressed mid-composition. * * Sizing: `minLines`/`maxLines` × line height drive the element's auto-grow * window (`mode="auto"`, chat-style 1 → N lines then internal scroll); * `mode="fixed"` pins the height at `maxLines`; `mode="fullscreen"` fills the * parent. */ import { type Define } from '@sigx/lynx'; import { type SelectionState } from '@sigx/lynx-richtext'; import { type ToolbarRenderItem } from './toolbar/Toolbar.js'; import { type ToolbarItem } from './toolbar/items.js'; import type { MarkdownEditorPlugin } from './plugin.js'; import { type SuggestionPopupStyle } from './trigger/SuggestionPopup.js'; export type MarkdownEditorMode = 'auto' | 'fixed' | 'fullscreen'; /** Imperative command surface — what toolbars and plugins drive. */ export interface MarkdownEditorController { toggleBold(): void; toggleItalic(): void; toggleStrike(): void; toggleCode(): void; /** 1–6 sets a heading; 0 reverts to paragraph. */ setHeading(level: 0 | 1 | 2 | 3 | 4 | 5 | 6): void; /** * Set the selected paragraph(s)' list type; `'none'` reverts to * paragraph. A new `'task'` line starts unchecked. */ setList(kind: 'bullet' | 'ordered' | 'task' | 'none'): void; /** Toggle blockquote on the selected paragraph(s). */ toggleQuote(): void; /** * Insert or wrap a link. Non-empty selection → the selection becomes the * link text; collapsed → `text` (or the href itself) is inserted and * linked. The href is trusted as-is (parse-side `sanitizeHref` and the * serializer's destination escaping are the safety nets); offsets come * from the last selection event — same fire-and-forget assumption as * `replaceRange`. No-op before the first selection event (the caret * position is unknown). */ insertLink(href: string, text?: string): void; insertText(text: string): void; /** * Replace `[start, end)` (UTF-16 offsets in the document text) with * `text`, leaving the caret after it. What trigger plugins use to swap * the typed query for the selected suggestion. */ replaceRange(start: number, end: number, text: string): void; /** * Insert an atomic mention chip (one U+FFFC carrying a `mention` span — * see lynx-richtext's chip invariant). `replace` removes `[from, to)` * first, typically the trigger query run. A dedicated native op: * `replaceRange`/`insertText` can't attach a span to the inserted char. */ insertChip(chip: { id: string; label: string; kind?: string; }, replace?: { from: number; to: number; }): void; /** Clear the document (chat send). */ clear(): void; /** * Expand the editor into an absolute-inset overlay (and back). The same * mounted element is restyled — never re-parented — so the native * document, selection, focus and keyboard all survive the transition * (re-parenting would recreate the native view and lose its state). * Style the overlay surface via the `fullscreenClass` prop. */ openFullscreen(): void; closeFullscreen(): void; isFullscreen(): boolean; focus(): void; blur(): void; /** The current markdown (as of the last element change). */ getMarkdown(): string; /** The current selection state (as of the last selection event). */ getSelection(): SelectionState | null; } export type MarkdownEditorProps = Define.Prop<'value', string, false> & Define.Prop<'placeholder', string, false> & Define.Prop<'minLines', number, false> & Define.Prop<'maxLines', number, false> & Define.Prop<'mode', MarkdownEditorMode, false> & Define.Prop<'fontSize', number, false> & Define.Prop<'textColor', string, false> & Define.Prop<'accentColor', string, false> & Define.Prop<'placeholderColor', string, false> & Define.Prop<'confirmType', 'send' | 'search' | 'next' | 'go' | 'done', false> & Define.Prop<'autoFocus', boolean, false> & Define.Prop<'disabled', boolean, false> & Define.Prop<'class', string, false> /** * Built-in formatting toolbar. `true` ≡ `'bottom'` — below the input is * the common chat placement (selection handles and the iOS edit menu pop * up *above* the selection, so a toolbar on top would sit under them). */ & Define.Prop<'toolbar', boolean | 'top' | 'bottom', false> /** Override the built-in toolbar's items (defaults to `defaultToolbarItems`). */ & Define.Prop<'toolbarItems', ToolbarItem[], false> /** Re-skin the built-in toolbar's item rendering (what daisyUI does). */ & Define.Prop<'renderToolbarItem', ToolbarRenderItem, false> /** * Editor plugins ({@link MarkdownEditorPlugin}) — inline syntax, trigger * suggestions, extra toolbar items. Pass a stable array (e.g. a module * constant); the set is captured at mount. */ & Define.Prop<'plugins', MarkdownEditorPlugin[], false> /** * Style the trigger suggestion popup (mentions, etc.) — surface/border/ * active/text colors, width, maxHeight, class. Generic to *any* trigger; * omitted fields keep neutral defaults. daisyUI wires this from the active * theme via `useMarkdownEditorTheme().suggestionPopup`, mirroring how * `textColor`/`accentColor` theme the editor body. Per-row content stays a * plugin concern (`trigger.renderItem`). */ & Define.Prop<'suggestionPopup', SuggestionPopupStyle, false> /** * Extra root classes applied only while the fullscreen overlay is open — * the consumer owns the surface (e.g. daisyUI `bg-base-100`). Without * it the overlay falls back to a plain white background. */ & Define.Prop<'fullscreenClass', string, false> /** Fullscreen overlay opened/closed (controller or the ✕ affordance). */ & Define.Prop<'onFullscreenChange', (open: boolean) => void, false> & Define.Prop<'onChange', (markdown: string) => void, false> & Define.Prop<'onSelectionChange', (sel: SelectionState) => void, false> & Define.Prop<'onFocus', () => void, false> & Define.Prop<'onBlur', () => void, false> /** Receives the imperative controller once on mount. */ & Define.Prop<'controllerRef', (ctrl: MarkdownEditorController) => void, false>; export declare const MarkdownEditor: import("@sigx/runtime-core").ComponentFactory;