@marianmeres/stuic 3.108.0 → 3.109.0

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.
@@ -0,0 +1,541 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type { InputWrapClassProps } from "../Input/types.js";
6
+
7
+ type SnippetWithId = Snippet<[{ id: string }]>;
8
+
9
+ export type MarkdownEditorMode = "wysiwyg" | "source";
10
+
11
+ /** A toolbar button key, or `"|"` for a visual separator. */
12
+ export type ToolbarItem =
13
+ | "bold"
14
+ | "italic"
15
+ | "heading1"
16
+ | "heading2"
17
+ | "heading3"
18
+ | "link"
19
+ | "image"
20
+ | "bulletList"
21
+ | "orderedList"
22
+ | "blockquote"
23
+ | "codeBlock"
24
+ | "hr"
25
+ | "hardBreak"
26
+ | "undo"
27
+ | "redo"
28
+ | "|";
29
+
30
+ /** Default toolbar layout used when `toolbar` is `true`. */
31
+ export const DEFAULT_TOOLBAR: ToolbarItem[] = [
32
+ "bold",
33
+ "italic",
34
+ "|",
35
+ "heading1",
36
+ "heading2",
37
+ "|",
38
+ "link",
39
+ "image",
40
+ "|",
41
+ "bulletList",
42
+ "orderedList",
43
+ "blockquote",
44
+ "codeBlock",
45
+ "|",
46
+ "hr",
47
+ "hardBreak",
48
+ "|",
49
+ "undo",
50
+ "redo",
51
+ ];
52
+
53
+ /** Reduced toolbar used on mobile / touch devices (see `mobileToolbar`). */
54
+ export const DEFAULT_MOBILE_TOOLBAR: ToolbarItem[] = [
55
+ "bold",
56
+ "italic",
57
+ "|",
58
+ "heading2",
59
+ "bulletList",
60
+ "orderedList",
61
+ "|",
62
+ "link",
63
+ "|",
64
+ "undo",
65
+ "redo",
66
+ ];
67
+
68
+ export interface Props extends InputWrapClassProps {
69
+ /** Markdown content. Bindable, single source of truth across both modes. */
70
+ value?: string;
71
+ /** Active editing surface. Bindable. Default `"wysiwyg"`. */
72
+ mode?: MarkdownEditorMode;
73
+ /** The editor surface wrapper element. Bindable (read). */
74
+ input?: HTMLDivElement;
75
+ /** Name attribute of the hidden input (markdown form submission). */
76
+ name?: string;
77
+ //
78
+ label?: SnippetWithId | THC;
79
+ description?: SnippetWithId | THC;
80
+ class?: string;
81
+ id?: string;
82
+ renderSize?: "sm" | "md" | "lg" | string;
83
+ required?: boolean;
84
+ disabled?: boolean;
85
+ placeholder?: string;
86
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
87
+ //
88
+ /**
89
+ * Formatting toolbar. `true` (default) shows {@link DEFAULT_TOOLBAR},
90
+ * `false` hides it, or pass an ordered array of {@link ToolbarItem}s
91
+ * (use `"|"` for separators) to choose exactly which buttons appear.
92
+ */
93
+ toolbar?: boolean | ToolbarItem[];
94
+ /**
95
+ * Toolbar used on mobile / touch devices (matched by `mobileQuery`). Same
96
+ * shape as `toolbar`; defaults to the reduced {@link DEFAULT_MOBILE_TOOLBAR}.
97
+ */
98
+ mobileToolbar?: boolean | ToolbarItem[];
99
+ /** Start in source mode on mobile / touch devices. Default `true`. */
100
+ autoSourceOnMobile?: boolean;
101
+ /**
102
+ * Media query defining "mobile" for `mobileToolbar` / `autoSourceOnMobile`.
103
+ * Default `"(pointer: coarse) and (max-width: 640px)"`.
104
+ */
105
+ mobileQuery?: string;
106
+ /**
107
+ * URL prompt used by the link/image buttons. Defaults to the native
108
+ * `window.prompt`. Pass `createPrompt(acpStack)` to use STUIC's ACP dialog
109
+ * (any `window.prompt`-compatible sync/async function works).
110
+ */
111
+ prompt?: PromptFn;
112
+ /** Show the WYSIWYG/Source mode toggle. Default `true`. */
113
+ showModeToggle?: boolean;
114
+ /** Label for the toggle when in WYSIWYG mode (switches to source). */
115
+ sourceLabel?: string;
116
+ /** Label for the toggle when in source mode (switches to WYSIWYG). */
117
+ previewLabel?: string;
118
+ //
119
+ labelAfter?: SnippetWithId | THC;
120
+ below?: SnippetWithId | THC;
121
+ labelLeft?: boolean;
122
+ labelLeftWidth?: "normal" | "wide";
123
+ labelLeftBreakpoint?: number;
124
+ //
125
+ /** Classes for the editor surface element. */
126
+ classInput?: string;
127
+ classToolbar?: string;
128
+ style?: string;
129
+ //
130
+ onChange?: (value: string) => void;
131
+ }
132
+ </script>
133
+
134
+ <script lang="ts">
135
+ import { untrack } from "svelte";
136
+ import { MediaQuery } from "svelte/reactivity";
137
+ import {
138
+ validate as validateAction,
139
+ type ValidationResult,
140
+ } from "../../actions/validate.svelte.js";
141
+ import { getId } from "../../utils/get-id.js";
142
+ import { twMerge } from "../../utils/tw-merge.js";
143
+ import {
144
+ iconBold,
145
+ iconCode,
146
+ iconCornerDownLeft,
147
+ iconHeading1,
148
+ iconHeading2,
149
+ iconHeading3,
150
+ iconImage,
151
+ iconItalic,
152
+ iconLink,
153
+ iconList,
154
+ iconListOrdered,
155
+ iconMinus,
156
+ iconQuote,
157
+ iconRedo,
158
+ iconUndo,
159
+ } from "../../icons/index.js";
160
+ import InputWrap from "../Input/_internal/InputWrap.svelte";
161
+ import type { EditorCommands, EditorHandle, PromptFn } from "./_internal/types.js";
162
+ import "./index.css";
163
+
164
+ // Default URL prompt for link/image — the native `window.prompt`. Opt into
165
+ // STUIC's ACP dialog by passing `prompt={createPrompt(acpStack)}`.
166
+ const defaultPrompt: PromptFn = (message, defaultValue = "") =>
167
+ typeof window !== "undefined" ? window.prompt(message, defaultValue) : null;
168
+
169
+ // Toolbar button registry: lucide icon + tooltip + the command to run. Keep
170
+ // this in sync with the `ToolbarItem` union in the module block.
171
+ type IconFn = (props?: Record<string, unknown>) => string;
172
+ type ToolbarButton = { title: string; icon: IconFn; run: (c: EditorCommands) => void };
173
+ const TOOLBAR_REGISTRY: Record<Exclude<ToolbarItem, "|">, ToolbarButton> = {
174
+ bold: { title: "Bold", icon: iconBold, run: (c) => c.bold() },
175
+ italic: { title: "Italic", icon: iconItalic, run: (c) => c.italic() },
176
+ heading1: { title: "Heading 1", icon: iconHeading1, run: (c) => c.heading(1) },
177
+ heading2: { title: "Heading 2", icon: iconHeading2, run: (c) => c.heading(2) },
178
+ heading3: { title: "Heading 3", icon: iconHeading3, run: (c) => c.heading(3) },
179
+ link: { title: "Link", icon: iconLink, run: (c) => c.link() },
180
+ image: { title: "Image", icon: iconImage, run: (c) => c.image() },
181
+ bulletList: { title: "Bullet list", icon: iconList, run: (c) => c.bulletList() },
182
+ orderedList: {
183
+ title: "Ordered list",
184
+ icon: iconListOrdered,
185
+ run: (c) => c.orderedList(),
186
+ },
187
+ blockquote: { title: "Blockquote", icon: iconQuote, run: (c) => c.blockquote() },
188
+ codeBlock: { title: "Code block", icon: iconCode, run: (c) => c.codeBlock() },
189
+ hr: { title: "Horizontal rule", icon: iconMinus, run: (c) => c.hr() },
190
+ hardBreak: {
191
+ title: "Line break",
192
+ icon: iconCornerDownLeft,
193
+ run: (c) => c.hardBreak(),
194
+ },
195
+ undo: { title: "Undo", icon: iconUndo, run: (c) => c.undo() },
196
+ redo: { title: "Redo", icon: iconRedo, run: (c) => c.redo() },
197
+ };
198
+
199
+ let {
200
+ value = $bindable(""),
201
+ mode = $bindable("wysiwyg"),
202
+ input = $bindable(),
203
+ name,
204
+ //
205
+ label,
206
+ description,
207
+ class: classProp,
208
+ id = getId(),
209
+ renderSize = "md",
210
+ required = false,
211
+ disabled = false,
212
+ placeholder,
213
+ // Renamed to avoid collision with the exported `validate()` below.
214
+ validate: validateProp,
215
+ //
216
+ toolbar = true,
217
+ mobileToolbar = DEFAULT_MOBILE_TOOLBAR,
218
+ autoSourceOnMobile = true,
219
+ mobileQuery = "(pointer: coarse) and (max-width: 640px)",
220
+ prompt = defaultPrompt,
221
+ showModeToggle = true,
222
+ sourceLabel = "Source",
223
+ previewLabel = "Preview",
224
+ //
225
+ labelAfter,
226
+ below,
227
+ labelLeft = false,
228
+ labelLeftWidth = "normal",
229
+ labelLeftBreakpoint = 480,
230
+ //
231
+ classInput,
232
+ classToolbar,
233
+ classLabel,
234
+ classLabelBox,
235
+ classInputBox,
236
+ classInputBoxWrap,
237
+ classInputBoxWrapInvalid,
238
+ classDescBox,
239
+ classDescBoxToggle,
240
+ classBelowBox,
241
+ classValidationBox,
242
+ style,
243
+ //
244
+ onChange,
245
+ }: Props = $props();
246
+
247
+ // Reactive "is this a mobile / touch device" flag (SSR-safe: svelte/reactivity
248
+ // MediaQuery returns the fallback on the server).
249
+ const mobile = new MediaQuery(
250
+ untrack(() => mobileQuery),
251
+ false
252
+ );
253
+ const isMobile = $derived(mobile.current);
254
+
255
+ // Resolve the toolbar config (boolean | item list) into a validated list,
256
+ // swapping to the reduced `mobileToolbar` on touch devices.
257
+ const toolbarItems = $derived.by<ToolbarItem[]>(() => {
258
+ const src = isMobile ? mobileToolbar : toolbar;
259
+ const fallback = isMobile ? DEFAULT_MOBILE_TOOLBAR : DEFAULT_TOOLBAR;
260
+ const list = src === false ? [] : src === true ? fallback : src;
261
+ return list.filter((k) => k === "|" || k in TOOLBAR_REGISTRY);
262
+ });
263
+
264
+ // Default to source mode on mobile — applied once on (client) mount so it
265
+ // doesn't fight the user's later manual toggle or re-fire on resize.
266
+ let mobileModeApplied = false;
267
+ $effect(() => {
268
+ if (mobileModeApplied) return;
269
+ mobileModeApplied = true;
270
+ if (autoSourceOnMobile && untrack(() => mobile.current)) mode = "source";
271
+ });
272
+
273
+ // The editor surface the active backend mounts into.
274
+ let host = $state<HTMLDivElement>();
275
+ $effect(() => {
276
+ input = host;
277
+ });
278
+
279
+ // Handle to whichever backend is currently mounted (null while (re)loading).
280
+ let activeHandle = $state<EditorHandle | undefined>();
281
+
282
+ // Loop guards (intentionally NON-reactive so they don't retrigger effects):
283
+ // - `applyingExternal`: true while we push `value` INTO an editor; the
284
+ // backend's change listener checks it to ignore the echo.
285
+ // - `lastEmitted`: last markdown the editor pushed UP; lets the value-watcher
286
+ // skip changes that originated from the editor itself.
287
+ let applyingExternal = false;
288
+ let lastEmitted = "";
289
+
290
+ // editor -> value
291
+ function onEditorChanged(md: string) {
292
+ if (applyingExternal) return;
293
+ lastEmitted = md;
294
+ value = md;
295
+ onChange?.(md);
296
+ }
297
+
298
+ // Mount / teardown the active backend. Keyed on (mode, host, disabled) ONLY —
299
+ // `value` is read untracked so per-keystroke updates never rebuild the editor.
300
+ $effect(() => {
301
+ const target = mode;
302
+ const el = host;
303
+ const isDisabled = disabled;
304
+ if (!el) return;
305
+
306
+ let disposed = false;
307
+ let handle: EditorHandle | undefined;
308
+
309
+ const v0 = untrack(() => value) ?? "";
310
+ lastEmitted = v0;
311
+
312
+ (async () => {
313
+ try {
314
+ const opts = {
315
+ value: v0,
316
+ onChange: onEditorChanged,
317
+ disabled: isDisabled,
318
+ placeholder,
319
+ prompt,
320
+ };
321
+ if (target === "wysiwyg") {
322
+ const { mountMilkdown } = await import("./_internal/milkdown.js");
323
+ if (disposed) return;
324
+ handle = await mountMilkdown(el, opts);
325
+ } else {
326
+ const { mountCodeMirror } = await import("./_internal/codemirror.js");
327
+ if (disposed) return;
328
+ handle = mountCodeMirror(el, opts);
329
+ }
330
+ if (disposed) {
331
+ handle.destroy();
332
+ handle = undefined;
333
+ return;
334
+ }
335
+ activeHandle = handle;
336
+ } catch (err) {
337
+ // eslint-disable-next-line no-console
338
+ console.error(
339
+ "[MarkdownEditor] failed to load the editor backend. " +
340
+ "Make sure the optional peer dependencies (@milkdown/* and " +
341
+ "@codemirror/*) are installed.",
342
+ err
343
+ );
344
+ }
345
+ })();
346
+
347
+ return () => {
348
+ disposed = true;
349
+ if (handle) {
350
+ // Flush the latest content into `value` BEFORE destroying, so a mode
351
+ // switch never loses the final edits.
352
+ const md = handle.getMarkdown();
353
+ if (md !== untrack(() => value)) {
354
+ lastEmitted = md;
355
+ value = md;
356
+ }
357
+ handle.destroy();
358
+ }
359
+ activeHandle = undefined;
360
+ handle = undefined;
361
+ };
362
+ });
363
+
364
+ // value -> editor (external mutations). Skips editor-originated changes via the
365
+ // `lastEmitted` guard, and the `applyingExternal` flag stops the resulting
366
+ // programmatic replace from echoing back through `onEditorChanged`.
367
+ $effect(() => {
368
+ const v = value ?? "";
369
+ const handle = activeHandle;
370
+ if (!handle) return;
371
+ if (v === lastEmitted) return;
372
+ applyingExternal = true;
373
+ try {
374
+ handle.setMarkdown(v);
375
+ } finally {
376
+ applyingExternal = false;
377
+ }
378
+ lastEmitted = v;
379
+ });
380
+
381
+ function toggleModeInternal() {
382
+ mode = mode === "wysiwyg" ? "source" : "wysiwyg";
383
+ }
384
+
385
+ // --- Validation (hidden-input pattern, mirrors FieldPhoneNumber) ---------
386
+ let hiddenInputEl: HTMLInputElement | undefined = $state();
387
+ let validation: ValidationResult | undefined = $state();
388
+ const setValidationResult = (res: ValidationResult) => (validation = res);
389
+ let _doValidate: (() => void) | undefined = $state();
390
+
391
+ // Re-validate on blur of the editor surface (avoids per-keystroke churn).
392
+ function handleFocusOut() {
393
+ hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
394
+ }
395
+
396
+ /** Trigger validation now. Renders the inline message if invalid. */
397
+ export function validate(): ValidationResult | undefined {
398
+ _doValidate?.();
399
+ return validation;
400
+ }
401
+ /** Clear the inline validation message. */
402
+ export function clearValidation(): void {
403
+ validation = undefined;
404
+ hiddenInputEl?.setCustomValidity?.("");
405
+ }
406
+ /** Current validation state, or undefined if never run. */
407
+ export function getValidation(): ValidationResult | undefined {
408
+ return validation;
409
+ }
410
+ /** Focus the active editor surface. */
411
+ export function focus(): void {
412
+ activeHandle?.focus();
413
+ }
414
+ /** Scroll the editor into view. */
415
+ export function scrollIntoView(opts?: ScrollIntoViewOptions): void {
416
+ host?.scrollIntoView?.({ behavior: "smooth", block: "center", ...opts });
417
+ }
418
+ /** Read the current markdown (from the active editor, falling back to `value`). */
419
+ export function getMarkdown(): string {
420
+ return activeHandle?.getMarkdown() ?? value ?? "";
421
+ }
422
+ /** Toggle between WYSIWYG and source modes. */
423
+ export function toggleMode(): void {
424
+ toggleModeInternal();
425
+ }
426
+ </script>
427
+
428
+ <InputWrap
429
+ {id}
430
+ {label}
431
+ {description}
432
+ {labelAfter}
433
+ {below}
434
+ {required}
435
+ {disabled}
436
+ size={renderSize}
437
+ class={classProp}
438
+ {labelLeft}
439
+ {labelLeftWidth}
440
+ {labelLeftBreakpoint}
441
+ {classLabel}
442
+ {classLabelBox}
443
+ {classInputBox}
444
+ {classInputBoxWrap}
445
+ {classInputBoxWrapInvalid}
446
+ {classDescBox}
447
+ {classDescBoxToggle}
448
+ {classBelowBox}
449
+ {classValidationBox}
450
+ {validation}
451
+ {style}
452
+ >
453
+ <div
454
+ class={twMerge("stuic-markdown-editor", classInput)}
455
+ class:disabled
456
+ data-size={renderSize}
457
+ data-mode={mode}
458
+ >
459
+ {#if toolbarItems.length || showModeToggle}
460
+ <div class={twMerge("stuic-markdown-editor-bar", classToolbar)}>
461
+ {#if toolbarItems.length}
462
+ <div
463
+ class="stuic-markdown-editor-toolbar"
464
+ role="toolbar"
465
+ aria-label="Formatting"
466
+ >
467
+ {#each toolbarItems as item, i (i)}
468
+ {#if item === "|"}
469
+ <span class="stuic-markdown-editor-toolbar-sep" aria-hidden="true"></span>
470
+ {:else}
471
+ {@const btn = TOOLBAR_REGISTRY[item]}
472
+ <button
473
+ type="button"
474
+ data-cmd={item}
475
+ title={btn.title}
476
+ aria-label={btn.title}
477
+ {disabled}
478
+ onpointerdown={(e) => e.preventDefault()}
479
+ onclick={() => activeHandle && btn.run(activeHandle.commands)}
480
+ >
481
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
482
+ {@html btn.icon({ size: 20 })}
483
+ </button>
484
+ {/if}
485
+ {/each}
486
+ </div>
487
+ {/if}
488
+ {#if showModeToggle}
489
+ <button
490
+ type="button"
491
+ class="stuic-markdown-editor-mode"
492
+ {disabled}
493
+ onclick={toggleModeInternal}
494
+ >
495
+ {mode === "wysiwyg" ? sourceLabel : previewLabel}
496
+ </button>
497
+ {/if}
498
+ </div>
499
+ {/if}
500
+
501
+ <!-- Backend mounts here on the client. Empty during SSR. -->
502
+ <div
503
+ bind:this={host}
504
+ class="stuic-markdown-editor-surface"
505
+ onfocusout={handleFocusOut}
506
+ role="textbox"
507
+ aria-multiline="true"
508
+ tabindex="-1"
509
+ ></div>
510
+ </div>
511
+ </InputWrap>
512
+
513
+ <!-- Hidden input for form submission + validation parity. Rendered whenever
514
+ validation is enabled (default) OR `name` is set, mirroring FieldPhoneNumber. -->
515
+ {#if name || validateProp !== false}
516
+ <input
517
+ type="hidden"
518
+ {name}
519
+ value={value ?? ""}
520
+ bind:this={hiddenInputEl}
521
+ use:validateAction={() => {
522
+ const customOpts =
523
+ typeof validateProp === "object" && validateProp ? validateProp : {};
524
+ const inner = customOpts.customValidator;
525
+ return {
526
+ enabled: validateProp !== false,
527
+ ...customOpts,
528
+ // Hidden inputs are barred from native constraint validation, so we
529
+ // enforce `required` ourselves before delegating.
530
+ customValidator(val, ctx, el) {
531
+ if (required && (val == null || String(val).trim() === "")) {
532
+ return "This field requires attention. Please review and try again.";
533
+ }
534
+ return inner ? inner(val, ctx, el) || "" : "";
535
+ },
536
+ setValidationResult,
537
+ setDoValidate: (fn) => (_doValidate = fn),
538
+ };
539
+ }}
540
+ />
541
+ {/if}
@@ -0,0 +1,87 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ValidateOptions } from "../../actions/validate.svelte.js";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ import type { InputWrapClassProps } from "../Input/types.js";
5
+ type SnippetWithId = Snippet<[{
6
+ id: string;
7
+ }]>;
8
+ export type MarkdownEditorMode = "wysiwyg" | "source";
9
+ /** A toolbar button key, or `"|"` for a visual separator. */
10
+ export type ToolbarItem = "bold" | "italic" | "heading1" | "heading2" | "heading3" | "link" | "image" | "bulletList" | "orderedList" | "blockquote" | "codeBlock" | "hr" | "hardBreak" | "undo" | "redo" | "|";
11
+ /** Default toolbar layout used when `toolbar` is `true`. */
12
+ export declare const DEFAULT_TOOLBAR: ToolbarItem[];
13
+ /** Reduced toolbar used on mobile / touch devices (see `mobileToolbar`). */
14
+ export declare const DEFAULT_MOBILE_TOOLBAR: ToolbarItem[];
15
+ export interface Props extends InputWrapClassProps {
16
+ /** Markdown content. Bindable, single source of truth across both modes. */
17
+ value?: string;
18
+ /** Active editing surface. Bindable. Default `"wysiwyg"`. */
19
+ mode?: MarkdownEditorMode;
20
+ /** The editor surface wrapper element. Bindable (read). */
21
+ input?: HTMLDivElement;
22
+ /** Name attribute of the hidden input (markdown form submission). */
23
+ name?: string;
24
+ label?: SnippetWithId | THC;
25
+ description?: SnippetWithId | THC;
26
+ class?: string;
27
+ id?: string;
28
+ renderSize?: "sm" | "md" | "lg" | string;
29
+ required?: boolean;
30
+ disabled?: boolean;
31
+ placeholder?: string;
32
+ validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
33
+ /**
34
+ * Formatting toolbar. `true` (default) shows {@link DEFAULT_TOOLBAR},
35
+ * `false` hides it, or pass an ordered array of {@link ToolbarItem}s
36
+ * (use `"|"` for separators) to choose exactly which buttons appear.
37
+ */
38
+ toolbar?: boolean | ToolbarItem[];
39
+ /**
40
+ * Toolbar used on mobile / touch devices (matched by `mobileQuery`). Same
41
+ * shape as `toolbar`; defaults to the reduced {@link DEFAULT_MOBILE_TOOLBAR}.
42
+ */
43
+ mobileToolbar?: boolean | ToolbarItem[];
44
+ /** Start in source mode on mobile / touch devices. Default `true`. */
45
+ autoSourceOnMobile?: boolean;
46
+ /**
47
+ * Media query defining "mobile" for `mobileToolbar` / `autoSourceOnMobile`.
48
+ * Default `"(pointer: coarse) and (max-width: 640px)"`.
49
+ */
50
+ mobileQuery?: string;
51
+ /**
52
+ * URL prompt used by the link/image buttons. Defaults to the native
53
+ * `window.prompt`. Pass `createPrompt(acpStack)` to use STUIC's ACP dialog
54
+ * (any `window.prompt`-compatible sync/async function works).
55
+ */
56
+ prompt?: PromptFn;
57
+ /** Show the WYSIWYG/Source mode toggle. Default `true`. */
58
+ showModeToggle?: boolean;
59
+ /** Label for the toggle when in WYSIWYG mode (switches to source). */
60
+ sourceLabel?: string;
61
+ /** Label for the toggle when in source mode (switches to WYSIWYG). */
62
+ previewLabel?: string;
63
+ labelAfter?: SnippetWithId | THC;
64
+ below?: SnippetWithId | THC;
65
+ labelLeft?: boolean;
66
+ labelLeftWidth?: "normal" | "wide";
67
+ labelLeftBreakpoint?: number;
68
+ /** Classes for the editor surface element. */
69
+ classInput?: string;
70
+ classToolbar?: string;
71
+ style?: string;
72
+ onChange?: (value: string) => void;
73
+ }
74
+ import { type ValidationResult } from "../../actions/validate.svelte.js";
75
+ import type { PromptFn } from "./_internal/types.js";
76
+ import "./index.css";
77
+ declare const MarkdownEditor: import("svelte").Component<Props, {
78
+ validate: () => ValidationResult | undefined;
79
+ clearValidation: () => void;
80
+ getValidation: () => ValidationResult | undefined;
81
+ focus: () => void;
82
+ scrollIntoView: (opts?: ScrollIntoViewOptions) => void;
83
+ getMarkdown: () => string;
84
+ toggleMode: () => void;
85
+ }, "value" | "input" | "mode">;
86
+ type MarkdownEditor = ReturnType<typeof MarkdownEditor>;
87
+ export default MarkdownEditor;