From 98e01f28d4cbbeabfb16107580292bb5f781fab8 Mon Sep 17 00:00:00 2001 From: Einar Date: Wed, 30 Sep 2026 20:00:15 +0200 Subject: [PATCH 1/4] Add MarkdownEditor with formatting, preview, completion and uploads A controlled markdown editor published as the ./MarkdownEditor subpath: one bordered box with a formatting toolbar and a round corner toggle that swaps it to a host-rendered preview. - Completion is generic: a MarkdownCompletion names a string or RegExp trigger and a suggest callback. The editor recognizes the trigger at the caret, debounces, aborts and drops stale answers, renders the list at the caret above any dialog, and inserts the picked suggestion. - Pasted and dropped files upload through an uploadFile callback behind a placeholder that is resolved by value, or removed on failure. - No markdown renderer ships: renderPreview hands rendering and sanitization to the host, so the subpath adds no dependencies. - Stable parts, labels with English defaults, and a toolbar with one Tab stop and Control/Command shortcuts. --- Migrator/test/transform.test.js | 4 + Source/MarkdownEditor/CaretPosition.ts | 16 + Source/MarkdownEditor/MarkdownCompletion.ts | 75 ++++ .../MarkdownCompletionContext.ts | 13 + .../MarkdownEditor/MarkdownCompletionMatch.ts | 24 ++ Source/MarkdownEditor/MarkdownEditor.css | 224 ++++++++++++ Source/MarkdownEditor/MarkdownEditor.tsx | 329 ++++++++++++++++++ Source/MarkdownEditor/MarkdownEditorLabels.ts | 66 ++++ Source/MarkdownEditor/MarkdownEditorMode.ts | 14 + Source/MarkdownEditor/MarkdownEditorParts.ts | 45 +++ Source/MarkdownEditor/MarkdownFormat.ts | 41 +++ Source/MarkdownEditor/MarkdownLineBlock.ts | 16 + Source/MarkdownEditor/MarkdownModeToggle.tsx | 60 ++++ Source/MarkdownEditor/MarkdownSuggestion.ts | 24 ++ .../MarkdownEditor/MarkdownSuggestionList.tsx | 124 +++++++ Source/MarkdownEditor/MarkdownTextEdit.ts | 16 + Source/MarkdownEditor/MarkdownToolbar.tsx | 112 ++++++ Source/MarkdownEditor/MarkdownWritingArea.tsx | 156 +++++++++ Source/MarkdownEditor/README.md | 40 +++ Source/MarkdownEditor/applyMarkdownFormat.ts | 59 ++++ Source/MarkdownEditor/caretPositionIn.ts | 71 ++++ .../defaultMarkdownEditorLabels.ts | 26 ++ .../MarkdownEditor/defaultMarkdownFormats.ts | 14 + Source/MarkdownEditor/filesIn.ts | 13 + Source/MarkdownEditor/findCompletionMatch.ts | 66 ++++ .../given/an_editor_in_the_dom.ts | 136 ++++++++ .../when_completing_a_trigger.ts | 83 +++++ .../for_MarkdownEditor/when_formatting.ts | 58 +++ .../when_toggling_the_preview.ts | 61 ++++ .../when_uploading_a_pasted_file.ts | 72 ++++ .../when_inserting_a_link.ts | 31 ++ .../when_toggling_a_code_block.ts | 31 ++ .../when_toggling_a_line_format.ts | 47 +++ .../when_toggling_an_inline_format.ts | 47 +++ .../for_findCompletionMatch/given.ts | 15 + .../when_a_pattern_trigger_is_typed.ts | 34 ++ .../when_a_string_trigger_is_typed.ts | 61 ++++ .../when_several_triggers_match.ts | 15 + .../when_toggling.ts | 12 + .../when_resolving_an_upload_placeholder.ts | 28 ++ .../when_replacing_a_range.ts | 14 + Source/MarkdownEditor/formatIcons.tsx | 33 ++ Source/MarkdownEditor/formatShortcuts.ts | 11 + Source/MarkdownEditor/index.ts | 16 + Source/MarkdownEditor/insertLink.ts | 40 +++ Source/MarkdownEditor/lineBlockAround.ts | 24 ++ .../MarkdownEditor/nextMarkdownEditorMode.ts | 12 + Source/MarkdownEditor/replacePlaceholder.ts | 17 + Source/MarkdownEditor/replaceRange.ts | 22 ++ Source/MarkdownEditor/toggleCodeBlock.ts | 42 +++ Source/MarkdownEditor/toggleInlineMarker.ts | 46 +++ Source/MarkdownEditor/toggleLinePrefix.ts | 44 +++ Source/MarkdownEditor/uploadPlaceholderFor.ts | 13 + .../MarkdownEditor/useMarkdownCompletion.ts | 165 +++++++++ Source/MarkdownEditor/useMarkdownUploads.ts | 102 ++++++ Source/api-surface.json | 20 +- Source/package.json | 5 + Source/scripts/export-inventory.json | 2 + Source/scripts/generate-parts.mjs | 42 +++ Source/scripts/parts-inventory.json | 20 ++ Source/styles.css | 1 + Source/types/parts.ts | 12 + 62 files changed, 3080 insertions(+), 2 deletions(-) create mode 100644 Source/MarkdownEditor/CaretPosition.ts create mode 100644 Source/MarkdownEditor/MarkdownCompletion.ts create mode 100644 Source/MarkdownEditor/MarkdownCompletionContext.ts create mode 100644 Source/MarkdownEditor/MarkdownCompletionMatch.ts create mode 100644 Source/MarkdownEditor/MarkdownEditor.css create mode 100644 Source/MarkdownEditor/MarkdownEditor.tsx create mode 100644 Source/MarkdownEditor/MarkdownEditorLabels.ts create mode 100644 Source/MarkdownEditor/MarkdownEditorMode.ts create mode 100644 Source/MarkdownEditor/MarkdownEditorParts.ts create mode 100644 Source/MarkdownEditor/MarkdownFormat.ts create mode 100644 Source/MarkdownEditor/MarkdownLineBlock.ts create mode 100644 Source/MarkdownEditor/MarkdownModeToggle.tsx create mode 100644 Source/MarkdownEditor/MarkdownSuggestion.ts create mode 100644 Source/MarkdownEditor/MarkdownSuggestionList.tsx create mode 100644 Source/MarkdownEditor/MarkdownTextEdit.ts create mode 100644 Source/MarkdownEditor/MarkdownToolbar.tsx create mode 100644 Source/MarkdownEditor/MarkdownWritingArea.tsx create mode 100644 Source/MarkdownEditor/README.md create mode 100644 Source/MarkdownEditor/applyMarkdownFormat.ts create mode 100644 Source/MarkdownEditor/caretPositionIn.ts create mode 100644 Source/MarkdownEditor/defaultMarkdownEditorLabels.ts create mode 100644 Source/MarkdownEditor/defaultMarkdownFormats.ts create mode 100644 Source/MarkdownEditor/filesIn.ts create mode 100644 Source/MarkdownEditor/findCompletionMatch.ts create mode 100644 Source/MarkdownEditor/for_MarkdownEditor/given/an_editor_in_the_dom.ts create mode 100644 Source/MarkdownEditor/for_MarkdownEditor/when_completing_a_trigger.ts create mode 100644 Source/MarkdownEditor/for_MarkdownEditor/when_formatting.ts create mode 100644 Source/MarkdownEditor/for_MarkdownEditor/when_toggling_the_preview.ts create mode 100644 Source/MarkdownEditor/for_MarkdownEditor/when_uploading_a_pasted_file.ts create mode 100644 Source/MarkdownEditor/for_applyMarkdownFormat/when_inserting_a_link.ts create mode 100644 Source/MarkdownEditor/for_applyMarkdownFormat/when_toggling_a_code_block.ts create mode 100644 Source/MarkdownEditor/for_applyMarkdownFormat/when_toggling_a_line_format.ts create mode 100644 Source/MarkdownEditor/for_applyMarkdownFormat/when_toggling_an_inline_format.ts create mode 100644 Source/MarkdownEditor/for_findCompletionMatch/given.ts create mode 100644 Source/MarkdownEditor/for_findCompletionMatch/when_a_pattern_trigger_is_typed.ts create mode 100644 Source/MarkdownEditor/for_findCompletionMatch/when_a_string_trigger_is_typed.ts create mode 100644 Source/MarkdownEditor/for_findCompletionMatch/when_several_triggers_match.ts create mode 100644 Source/MarkdownEditor/for_nextMarkdownEditorMode/when_toggling.ts create mode 100644 Source/MarkdownEditor/for_replacePlaceholder/when_resolving_an_upload_placeholder.ts create mode 100644 Source/MarkdownEditor/for_replaceRange/when_replacing_a_range.ts create mode 100644 Source/MarkdownEditor/formatIcons.tsx create mode 100644 Source/MarkdownEditor/formatShortcuts.ts create mode 100644 Source/MarkdownEditor/index.ts create mode 100644 Source/MarkdownEditor/insertLink.ts create mode 100644 Source/MarkdownEditor/lineBlockAround.ts create mode 100644 Source/MarkdownEditor/nextMarkdownEditorMode.ts create mode 100644 Source/MarkdownEditor/replacePlaceholder.ts create mode 100644 Source/MarkdownEditor/replaceRange.ts create mode 100644 Source/MarkdownEditor/toggleCodeBlock.ts create mode 100644 Source/MarkdownEditor/toggleInlineMarker.ts create mode 100644 Source/MarkdownEditor/toggleLinePrefix.ts create mode 100644 Source/MarkdownEditor/uploadPlaceholderFor.ts create mode 100644 Source/MarkdownEditor/useMarkdownCompletion.ts create mode 100644 Source/MarkdownEditor/useMarkdownUploads.ts diff --git a/Migrator/test/transform.test.js b/Migrator/test/transform.test.js index b81c5a2f..024c3cbe 100644 --- a/Migrator/test/transform.test.js +++ b/Migrator/test/transform.test.js @@ -176,6 +176,9 @@ describe('root namespace maps', () => { './CommandStepper', // Nested refinement of the historical CommandForm namespace. './CommandForm/fields', + // Added after Components 4.0 and never a Components 3 root namespace, so no Components 3 + // import can name it and the codemod has nothing to rewrite. + './MarkdownEditor', // Per-area CSS entry points (feat/per-area-stylesheets): side-effect stylesheet // imports, not symbol imports, so the codemod has nothing to rewrite for them — // same as the pre-existing aggregate './styles' above. @@ -192,6 +195,7 @@ describe('root namespace maps', () => { './Display/styles', './Dropdown/styles', './Filter/styles', + './MarkdownEditor/styles', './Notifications/styles', './ObjectContentEditor/styles', './ObjectNavigationalBar/styles', diff --git a/Source/MarkdownEditor/CaretPosition.ts b/Source/MarkdownEditor/CaretPosition.ts new file mode 100644 index 00000000..e7026101 --- /dev/null +++ b/Source/MarkdownEditor/CaretPosition.ts @@ -0,0 +1,16 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * Where the caret of a writing area is on screen, in viewport coordinates. + */ +export interface CaretPosition { + /** The left edge of the caret. */ + left: number; + + /** The top of the line the caret is on. */ + top: number; + + /** The bottom of the line the caret is on. */ + bottom: number; +} diff --git a/Source/MarkdownEditor/MarkdownCompletion.ts b/Source/MarkdownEditor/MarkdownCompletion.ts new file mode 100644 index 00000000..7b794e2e --- /dev/null +++ b/Source/MarkdownEditor/MarkdownCompletion.ts @@ -0,0 +1,75 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import type { ReactNode } from 'react'; +import type { MarkdownCompletionContext } from './MarkdownCompletionContext'; +import type { MarkdownSuggestion } from './MarkdownSuggestion'; + +/** + * Configures autocompletion for a {@link MarkdownEditor}: what starts it, and where the suggestions + * come from. The editor recognizes the trigger at the caret, asks {@link suggest} for suggestions, + * renders the list, and writes the picked suggestion's text over the trigger and what followed it. + * + * ```ts + * const issues: MarkdownCompletion = { + * trigger: '#', + * allowSpaces: true, + * label: 'Issues', + * suggest: async (query, { signal }) => (await searchIssues(query, signal)).map(issue => ({ + * id: issue.id, + * insertText: `#${issue.number}`, + * label: issue.title, + * detail: `#${issue.number}`, + * })), + * }; + * ``` + */ +export interface MarkdownCompletion { + /** + * What starts a completion. + * + * A string, such as `'#'` or `'@'`, starts one when it is typed at the start of a line or after + * whitespace; what follows it up to the caret, on the same line, is the query. + * + * A regular expression is matched against the current line up to the caret and starts a completion + * when its match ends at the caret. The whole match is what a picked suggestion replaces, and the + * query is the named group `query` when there is one, otherwise the first group, otherwise the + * whole match. Anchor it with `$` and use a lookbehind such as `(?<=^|\s)` for word boundaries. + */ + trigger: string | RegExp; + + /** + * Whether the query of a string trigger may contain spaces - searching by title rather than by a + * single word. Leading spaces are dropped from the query. Defaults to `false`. + */ + allowSpaces?: boolean; + + /** How many characters must follow the trigger before suggestions are asked for. Defaults to `0`. */ + minimumQueryLength?: number; + + /** How long typing must pause, in milliseconds, before suggestions are asked for. Defaults to `150`. */ + debounce?: number; + + /** + * Returns the suggestions for what has been typed after the trigger. May answer synchronously or + * with a promise; an answer for a query the person has already typed past is dropped, and so is a + * rejected one. An empty answer closes the list. + * @param query What has been typed after the trigger. + * @param context The {@link MarkdownCompletionContext} of the request. + * @returns The suggestions to offer, in the order to offer them. + */ + suggest: ( + query: string, + context: MarkdownCompletionContext, + ) => readonly MarkdownSuggestion[] | Promise; + + /** Accessible name of the suggestion list. Defaults to the editor's `suggestions` label. */ + label?: string; + + /** + * Renders the content of one suggestion in place of the default detail, label and annotation. + * @param suggestion The suggestion to render. + * @returns What to show for it. + */ + renderSuggestion?: (suggestion: MarkdownSuggestion) => ReactNode; +} diff --git a/Source/MarkdownEditor/MarkdownCompletionContext.ts b/Source/MarkdownEditor/MarkdownCompletionContext.ts new file mode 100644 index 00000000..f8b9a1c2 --- /dev/null +++ b/Source/MarkdownEditor/MarkdownCompletionContext.ts @@ -0,0 +1,13 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +/** + * What a {@link MarkdownCompletion} is told alongside the query when it is asked for suggestions. + */ +export interface MarkdownCompletionContext { + /** + * Aborted as soon as the answer is no longer wanted - the person typed on, moved the caret away or + * picked something. Pass it to `fetch` or check it before doing more work. + */ + signal: AbortSignal; +} diff --git a/Source/MarkdownEditor/MarkdownCompletionMatch.ts b/Source/MarkdownEditor/MarkdownCompletionMatch.ts new file mode 100644 index 00000000..803ff1df --- /dev/null +++ b/Source/MarkdownEditor/MarkdownCompletionMatch.ts @@ -0,0 +1,24 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import type { MarkdownCompletion } from './MarkdownCompletion'; + +/** + * A completion trigger found at the caret, and the query typed after it. + */ +export interface MarkdownCompletionMatch { + /** The completion whose trigger was found. */ + completion: MarkdownCompletion; + + /** The position of the matched completion in the list it was configured in. */ + completionIndex: number; + + /** Where the trigger starts - the start of what a picked suggestion replaces. */ + start: number; + + /** The caret - the end of what a picked suggestion replaces. */ + end: number; + + /** What has been typed after the trigger. */ + query: string; +} diff --git a/Source/MarkdownEditor/MarkdownEditor.css b/Source/MarkdownEditor/MarkdownEditor.css new file mode 100644 index 00000000..41abe67f --- /dev/null +++ b/Source/MarkdownEditor/MarkdownEditor.css @@ -0,0 +1,224 @@ +/* Copyright (c) Cratis. All rights reserved. */ +/* Licensed under the MIT license. See LICENSE file in the project root for full license information. */ + +.cratis-markdown-editor { + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; + width: 100%; + height: 100%; + overflow: hidden; + background: var(--cratis-surface-ground); + border: 1px solid var(--cratis-surface-border); + border-radius: var(--cratis-border-radius, 6px); + color: var(--cratis-text-color); +} + +.cratis-markdown-editor:focus-within { + border-color: var(--cratis-primary-color); +} + +.cratis-markdown-editor[data-invalid] { + border-color: var(--cratis-red-500); +} + +.cratis-markdown-editor[data-disabled] { + opacity: var(--cratis-disabled-opacity, 0.6); +} + +.cratis-markdown-editor__toolbar { + display: flex; + flex-wrap: wrap; + flex-shrink: 0; + align-items: center; + gap: 2px; + padding: 4px 6px; + background: var(--cratis-surface-card); + border-bottom: 1px solid var(--cratis-surface-border); +} + +.cratis-markdown-editor--toggleable .cratis-markdown-editor__toolbar { + padding-right: 2.75rem; +} + +.cratis-markdown-editor__separator { + width: 1px; + height: 1rem; + margin: 0 4px; + background: var(--cratis-surface-border); +} + +.cratis-markdown-editor__format { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + padding: 0; + color: var(--cratis-text-color-secondary); + background: transparent; + border: 0; + border-radius: 4px; + font-size: 0.8125rem; +} + +.cratis-markdown-editor__format:hover:not(:disabled) { + color: var(--cratis-highlight-text-color); + background: var(--cratis-highlight-bg); + cursor: pointer; +} + +.cratis-markdown-editor__format:focus-visible, +.cratis-markdown-editor__toggle:focus-visible { + outline: none; + box-shadow: var(--cratis-focus-ring); +} + +.cratis-markdown-editor__toggle { + position: absolute; + top: 0.3rem; + right: 0.35rem; + z-index: 1; + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + padding: 0; + color: var(--cratis-text-color-secondary); + background: var(--cratis-surface-card); + border: 1px solid var(--cratis-surface-border); + border-radius: 50%; + cursor: pointer; + font-size: 0.875rem; +} + +.cratis-markdown-editor__toggle:hover { + color: var(--cratis-text-color); + background: var(--cratis-surface-hover); +} + +.cratis-markdown-editor__toggle[data-pressed], +.cratis-markdown-editor__toggle[data-pressed]:hover { + color: var(--cratis-primary-color-text); + background: var(--cratis-primary-color); + border-color: var(--cratis-primary-color); +} + +.cratis-markdown-editor__body { + position: relative; + flex: 1 1 auto; + min-height: 4rem; +} + +.cratis-markdown-editor__textarea, +.cratis-markdown-editor__preview { + position: absolute; + inset: 0; + box-sizing: border-box; + width: 100%; + height: 100%; + margin: 0; + padding: 0.75rem; + overflow: auto; + color: var(--cratis-text-color); + background: transparent; +} + +.cratis-markdown-editor__textarea, +.cratis-markdown-editor__textarea:hover, +.cratis-markdown-editor__textarea:focus { + border: 0; + border-radius: 0; + outline: none; + box-shadow: none; + resize: none; + font-family: var(--cratis-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); + font-size: 0.8125rem; + line-height: 1.5; +} + +.cratis-markdown-editor__textarea::placeholder, +.cratis-markdown-editor__empty { + color: var(--cratis-text-color-secondary); +} + +.cratis-markdown-editor__preview { + padding-right: 3rem; +} + +.cratis-markdown-editor__empty { + margin: 0; + font-style: italic; +} + +.cratis-markdown-editor__status { + flex-shrink: 0; + padding: 4px 8px; + color: var(--cratis-text-color-secondary); + background: var(--cratis-surface-card); + border-top: 1px solid var(--cratis-surface-border); + font-size: 0.75rem; +} + +.cratis-markdown-editor__suggestions { + position: fixed; + display: flex; + flex-direction: column; + gap: 2px; + box-sizing: border-box; + width: min(32rem, calc(100vw - 1rem)); + max-height: 18rem; + padding: 4px; + overflow-y: auto; + background: var(--cratis-surface-overlay); + border: 1px solid var(--cratis-surface-border); + border-radius: 6px; + box-shadow: var(--cratis-shadow-overlay); +} + +.cratis-markdown-editor__suggestion { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + color: var(--cratis-text-color); + border-radius: 4px; + cursor: pointer; + font-size: 0.8125rem; +} + +.cratis-markdown-editor__suggestion[data-selected] { + color: var(--cratis-highlight-text-color); + background: var(--cratis-highlight-bg); +} + +.cratis-markdown-editor__suggestion-detail { + flex-shrink: 0; + color: var(--cratis-text-color-secondary); + font-family: var(--cratis-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); +} + +.cratis-markdown-editor__suggestion-label { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cratis-markdown-editor__suggestion-annotation { + flex-shrink: 0; + color: var(--cratis-text-color-secondary); + font-size: 0.6875rem; +} + +@media (forced-colors: active) { + .cratis-markdown-editor__suggestion[data-selected], + .cratis-markdown-editor__toggle[data-pressed] { + forced-color-adjust: none; + color: HighlightText; + background: Highlight; + } +} diff --git a/Source/MarkdownEditor/MarkdownEditor.tsx b/Source/MarkdownEditor/MarkdownEditor.tsx new file mode 100644 index 00000000..4e698bdd --- /dev/null +++ b/Source/MarkdownEditor/MarkdownEditor.tsx @@ -0,0 +1,329 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { useId, useLayoutEffect, useRef, useState, type ReactNode } from 'react'; +import { classNames } from '../ClassNames/classNames'; +import { unstable_useOverlayEnvironment } from '../renderer/RendererContext'; +import { OVERLAY_OFFSET, zIndexAboveDialog } from '../renderer/dialogStack'; +import { useNearestDialogZIndex } from '../renderer/DialogStackContext'; +import type { ExactPartKeys } from '../types/ExactPartKeys'; +import type { PartsOf } from '../types/parts'; +import type { CaretPosition } from './CaretPosition'; +import type { MarkdownCompletion } from './MarkdownCompletion'; +import type { MarkdownEditorLabels } from './MarkdownEditorLabels'; +import { MarkdownEditorMode } from './MarkdownEditorMode'; +import type { MarkdownEditorParts } from './MarkdownEditorParts'; +import type { MarkdownFormat } from './MarkdownFormat'; +import type { MarkdownTextEdit } from './MarkdownTextEdit'; +import { MarkdownModeToggle } from './MarkdownModeToggle'; +import { MarkdownSuggestionList } from './MarkdownSuggestionList'; +import { MarkdownToolbar } from './MarkdownToolbar'; +import { MarkdownWritingArea } from './MarkdownWritingArea'; +import { applyMarkdownFormat } from './applyMarkdownFormat'; +import { caretPositionIn } from './caretPositionIn'; +import { defaultMarkdownEditorLabels } from './defaultMarkdownEditorLabels'; +import { defaultMarkdownFormats } from './defaultMarkdownFormats'; +import { useMarkdownCompletion } from './useMarkdownCompletion'; +import { useMarkdownUploads } from './useMarkdownUploads'; + +const markdownEditorPartsMatchManifest: ExactPartKeys> = true; +void markdownEditorPartsMatchManifest; + +/** The height the editor falls back to when its container leaves its height to its content. */ +const defaultHeight = 320; + +/** Props for {@link MarkdownEditor}. */ +export interface MarkdownEditorProps { + /** The markdown being edited. */ + value: string; + + /** Called with the markdown as it is written, formatted, completed or uploaded into. */ + onChange: (value: string) => void; + + /** Called when focus leaves the writing area. */ + onBlur?: () => void; + + /** Placeholder shown while the markdown is empty. */ + placeholder?: string; + + /** + * The height the editor takes when its container leaves its height to its content - a number of + * pixels or any CSS length. Inside a container with a height of its own the editor fills that + * instead. Defaults to 320 pixels. + */ + height?: number | string; + + /** The mode the editor opens in. Defaults to {@link MarkdownEditorMode.Write}. */ + initialMode?: MarkdownEditorMode; + + /** Focuses the writing area as the editor mounts. */ + autoFocus?: boolean; + + /** Disables writing, formatting and uploading. */ + disabled?: boolean; + + /** Shows the markdown without letting it be changed. */ + readOnly?: boolean; + + /** Marks the markdown as invalid, as a failed validation does. */ + invalid?: boolean; + + /** + * The formatting toolbar, as groups of formats separated from each other. Pass `false` for no + * toolbar. Defaults to text styles, blocks and links, and lists. + */ + formats?: readonly (readonly MarkdownFormat[])[] | false; + + /** + * Renders the preview of the markdown. The editor ships no markdown renderer - rendering, and what + * HTML is allowed through, belongs to the host - so the preview toggle only shows when this is given. + * @param markdown The markdown to render. Never empty: the editor shows its own empty text instead. + * @returns The rendered markdown. + */ + renderPreview?: (markdown: string) => ReactNode; + + /** Autocompletions offered while their trigger is typed, such as `#` for issues or `@` for people. */ + completions?: readonly MarkdownCompletion[]; + + /** + * Uploads a file pasted or dropped into the writing area and returns the markdown that stands for + * it, such as an image or a link. Omit to leave pasting and dropping files to the browser. + * @param file The file to upload. + * @returns The markdown that replaces the file's placeholder. + */ + uploadFile?: (file: File) => Promise; + + /** + * Told about a file whose upload failed. Its placeholder has already been taken out again. + * @param file The file that failed to upload. + * @param error What {@link uploadFile} rejected with. + */ + onUploadFailed?: (file: File, error: unknown) => void; + + /** Overrides for the editor's labels. Unset fields fall back to English. */ + labels?: MarkdownEditorLabels; + + /** The writing area's element id, for a `