@lapismd/mira-editor 0.0.1

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE.md +4 -0
  3. package/README.md +70 -0
  4. package/dist/assets/logo.png +0 -0
  5. package/dist/assets.d.ts +4 -0
  6. package/dist/features.d.ts +164 -0
  7. package/dist/features.d.ts.map +1 -0
  8. package/dist/features.js +348 -0
  9. package/dist/index.d.ts +12 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +7 -0
  12. package/dist/index.test.d.ts +2 -0
  13. package/dist/index.test.d.ts.map +1 -0
  14. package/dist/index.test.js +179 -0
  15. package/dist/mira-editor-about-dialog.svelte +28 -0
  16. package/dist/mira-editor-about-dialog.svelte.d.ts +7 -0
  17. package/dist/mira-editor-about-dialog.svelte.d.ts.map +1 -0
  18. package/dist/mira-editor-toolbar.svelte +805 -0
  19. package/dist/mira-editor-toolbar.svelte.d.ts +6 -0
  20. package/dist/mira-editor-toolbar.svelte.d.ts.map +1 -0
  21. package/dist/mira-editor-toolbar.test.d.ts +2 -0
  22. package/dist/mira-editor-toolbar.test.d.ts.map +1 -0
  23. package/dist/mira-editor-toolbar.test.js +199 -0
  24. package/dist/mira-editor.svelte +492 -0
  25. package/dist/mira-editor.svelte.d.ts +24 -0
  26. package/dist/mira-editor.svelte.d.ts.map +1 -0
  27. package/dist/mira-editor.test.d.ts +2 -0
  28. package/dist/mira-editor.test.d.ts.map +1 -0
  29. package/dist/mira-editor.test.js +208 -0
  30. package/dist/styles.css +165 -0
  31. package/dist/test-setup.d.ts +8 -0
  32. package/dist/test-setup.d.ts.map +1 -0
  33. package/dist/test-setup.js +31 -0
  34. package/dist/toolbar-model.d.ts +32 -0
  35. package/dist/toolbar-model.d.ts.map +1 -0
  36. package/dist/toolbar-model.js +148 -0
  37. package/dist/toolbar-model.test.d.ts +2 -0
  38. package/dist/toolbar-model.test.d.ts.map +1 -0
  39. package/dist/toolbar-model.test.js +46 -0
  40. package/dist/types.d.ts +88 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +1 -0
  43. package/dist/version.d.ts +3 -0
  44. package/dist/version.d.ts.map +1 -0
  45. package/dist/version.js +2 -0
  46. package/package.json +56 -0
@@ -0,0 +1,6 @@
1
+ import type { Component } from "svelte";
2
+ import type { MiraEditorToolbarProps } from "./types";
3
+ declare const MiraEditorToolbar: Component<MiraEditorToolbarProps, {}, "mode">;
4
+ type MiraEditorToolbar = ReturnType<typeof MiraEditorToolbar>;
5
+ export default MiraEditorToolbar;
6
+ //# sourceMappingURL=mira-editor-toolbar.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mira-editor-toolbar.svelte.d.ts","sourceRoot":"","sources":["../src/mira-editor-toolbar.svelte.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AA8BxC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAurBtD,QAAA,MAAM,iBAAiB,+CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mira-editor-toolbar.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mira-editor-toolbar.test.d.ts","sourceRoot":"","sources":["../src/mira-editor-toolbar.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,199 @@
1
+ import { tick } from "svelte";
2
+ import { mount, unmount } from "svelte";
3
+ import BoldIcon from "@lucide/svelte/icons/bold";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import MiraEditorToolbar from "./mira-editor-toolbar.svelte";
6
+ import { MiraFeature, } from "./features";
7
+ import { MIRA_EDITOR_VERSION } from "./version";
8
+ const noInsertItems = {
9
+ [MiraFeature.Formatting]: false,
10
+ [MiraFeature.Headings]: false,
11
+ [MiraFeature.Lists]: false,
12
+ [MiraFeature.Links]: false,
13
+ [MiraFeature.Images]: false,
14
+ [MiraFeature.Tables]: false,
15
+ [MiraFeature.GridTables]: false,
16
+ [MiraFeature.Code]: false,
17
+ [MiraFeature.Math]: false,
18
+ [MiraFeature.Mermaid]: false,
19
+ };
20
+ async function renderToolbar(props = {}) {
21
+ const target = document.createElement("div");
22
+ document.body.append(target);
23
+ const component = mount(MiraEditorToolbar, {
24
+ target,
25
+ props,
26
+ });
27
+ await tick();
28
+ return {
29
+ target,
30
+ separators() {
31
+ return target.querySelectorAll(".mira-editor__separator");
32
+ },
33
+ destroy() {
34
+ unmount(component);
35
+ target.remove();
36
+ },
37
+ };
38
+ }
39
+ function toolbarAction(id) {
40
+ return {
41
+ id,
42
+ label: id,
43
+ icon: BoldIcon,
44
+ run: () => undefined,
45
+ };
46
+ }
47
+ function actionContext(overrides = {}) {
48
+ return {
49
+ value: "word",
50
+ mode: "source",
51
+ readonly: false,
52
+ focus: () => undefined,
53
+ getMarkdown: () => "word",
54
+ getMode: () => "source",
55
+ getSelection: () => null,
56
+ insertMarkdown: () => undefined,
57
+ setMarkdown: () => undefined,
58
+ setMode: () => undefined,
59
+ setReadonly: () => undefined,
60
+ setSelection: () => undefined,
61
+ ...overrides,
62
+ };
63
+ }
64
+ describe("default Markdown toolbar actions", () => {
65
+ it("renders only actions explicitly placed in the top toolbar", async () => {
66
+ const toolbar = await renderToolbar({
67
+ toolbarActions: [
68
+ { ...toolbarAction("block only"), placements: ["block-menu"] },
69
+ {
70
+ ...toolbarAction("both places"),
71
+ placements: ["toolbar", "block-menu"],
72
+ },
73
+ ],
74
+ });
75
+ expect(toolbar.target.querySelector('[aria-label="block only"]')).toBeNull();
76
+ expect(toolbar.target.querySelector('[aria-label="both places"]')).toBeTruthy();
77
+ toolbar.destroy();
78
+ });
79
+ it("delegates smart actions through an integrated editor context", async () => {
80
+ const applyMarkdownAction = vi.fn(() => true);
81
+ const insertMarkdown = vi.fn();
82
+ const toolbar = await renderToolbar({
83
+ context: actionContext({ applyMarkdownAction, insertMarkdown }),
84
+ featureConfigs: {
85
+ [MiraFeature.Toolbar]: {
86
+ items: ["heading", "strikethrough", "inlineCode", "numberedList"],
87
+ },
88
+ },
89
+ });
90
+ toolbar.target
91
+ .querySelector('[aria-label="Strikethrough"]')
92
+ ?.click();
93
+ expect(applyMarkdownAction).toHaveBeenCalledWith("strikethrough");
94
+ expect(insertMarkdown).not.toHaveBeenCalled();
95
+ expect(toolbar.target.querySelector('[aria-label="Inline code"]')).toBeTruthy();
96
+ expect(toolbar.target.querySelector('[aria-label="Numbered list"]')).toBeTruthy();
97
+ toolbar.destroy();
98
+ });
99
+ it("retains template insertion for a standalone toolbar", async () => {
100
+ const onInsertMarkdown = vi.fn();
101
+ const toolbar = await renderToolbar({
102
+ onInsertMarkdown,
103
+ featureConfigs: {
104
+ [MiraFeature.Toolbar]: { items: ["bold"] },
105
+ },
106
+ });
107
+ toolbar.target
108
+ .querySelector('[aria-label="Bold"]')
109
+ ?.click();
110
+ expect(onInsertMarkdown).toHaveBeenCalledWith("**strong**");
111
+ toolbar.destroy();
112
+ });
113
+ it("disables built-in actions in readonly and reading modes", async () => {
114
+ const preview = await renderToolbar({ mode: "preview" });
115
+ expect(preview.target.querySelector('[aria-label="Bold"]')
116
+ ?.disabled).toBe(true);
117
+ preview.destroy();
118
+ const readonly = await renderToolbar({ readonly: true });
119
+ expect(readonly.target.querySelector('[aria-label="Bold"]')
120
+ ?.disabled).toBe(true);
121
+ readonly.destroy();
122
+ });
123
+ });
124
+ describe("default toolbar separators", () => {
125
+ it("does not render separators when only view controls are visible", async () => {
126
+ const toolbar = await renderToolbar({ features: noInsertItems });
127
+ expect(toolbar.separators()).toHaveLength(0);
128
+ toolbar.destroy();
129
+ });
130
+ it("renders one separator between leading content and view controls", async () => {
131
+ const toolbar = await renderToolbar({
132
+ features: noInsertItems,
133
+ toolbars: [
134
+ {
135
+ id: "start",
136
+ items: [toolbarAction("start-action")],
137
+ },
138
+ ],
139
+ });
140
+ expect(toolbar.separators()).toHaveLength(1);
141
+ toolbar.destroy();
142
+ });
143
+ it("ignores empty custom toolbar groups for separator placement", async () => {
144
+ const toolbar = await renderToolbar({
145
+ features: noInsertItems,
146
+ toolbars: [
147
+ { id: "empty-start", items: [] },
148
+ { id: "empty-end", align: "end", items: [] },
149
+ ],
150
+ });
151
+ expect(toolbar.separators()).toHaveLength(0);
152
+ toolbar.destroy();
153
+ });
154
+ it("does not duplicate separators between end groups and view controls", async () => {
155
+ const toolbar = await renderToolbar({
156
+ features: noInsertItems,
157
+ toolbars: [
158
+ {
159
+ id: "first-end",
160
+ align: "end",
161
+ items: [toolbarAction("first-end-action")],
162
+ },
163
+ {
164
+ id: "second-end",
165
+ align: "end",
166
+ items: [toolbarAction("second-end-action")],
167
+ },
168
+ ],
169
+ });
170
+ expect(toolbar.separators()).toHaveLength(2);
171
+ toolbar.destroy();
172
+ });
173
+ });
174
+ describe("toolbar About dialog", () => {
175
+ it("opens the package-branded version dialog from View options", async () => {
176
+ const toolbar = await renderToolbar({ features: noInsertItems });
177
+ toolbar.target
178
+ .querySelector('[aria-label="View options"]')
179
+ ?.click();
180
+ await tick();
181
+ const aboutItem = Array.from(document.body.querySelectorAll('[role="menuitem"]')).find((item) => item.textContent?.includes("About Mira"));
182
+ expect(aboutItem).toBeTruthy();
183
+ aboutItem?.click();
184
+ await tick();
185
+ const dialog = document.body.querySelector('[role="dialog"]');
186
+ expect(dialog?.getAttribute("aria-labelledby")).toBeTruthy();
187
+ expect(dialog?.textContent).toContain("About Mira");
188
+ expect(dialog?.textContent).toContain(`Version ${MIRA_EDITOR_VERSION}`);
189
+ expect(dialog?.querySelector('img[alt="Mira MDE logo"]')).toBeTruthy();
190
+ dialog
191
+ ?.querySelector('[data-slot="dialog-close"]')
192
+ ?.click();
193
+ await tick();
194
+ expect(dialog?.dataset.state).toBe("closed");
195
+ await new Promise((resolve) => window.setTimeout(resolve, 250));
196
+ expect(document.body.querySelector('[role="dialog"]')).toBeNull();
197
+ toolbar.destroy();
198
+ });
199
+ });
@@ -0,0 +1,492 @@
1
+ <script lang="ts">
2
+ import CheckIcon from "@lucide/svelte/icons/check";
3
+ import CodeIcon from "@lucide/svelte/icons/code";
4
+ import CommandIcon from "@lucide/svelte/icons/command";
5
+ import ImageIcon from "@lucide/svelte/icons/image";
6
+ import LinkIcon from "@lucide/svelte/icons/link";
7
+ import PlayIcon from "@lucide/svelte/icons/play";
8
+ import RotateCcwIcon from "@lucide/svelte/icons/rotate-ccw";
9
+ import SaveIcon from "@lucide/svelte/icons/save";
10
+ import SparklesIcon from "@lucide/svelte/icons/sparkles";
11
+ import TableIcon from "@lucide/svelte/icons/table-2";
12
+ import WandSparklesIcon from "@lucide/svelte/icons/wand-sparkles";
13
+ import { Mira, type MiraHandle } from "@lapismd/mira";
14
+ import type {
15
+ MiraEditorSelection,
16
+ MiraMarkdownActionId,
17
+ } from "@lapismd/mira/core";
18
+ import {
19
+ defineMiraExtension,
20
+ resolveMiraExtensions,
21
+ type MiraBlockActionContext,
22
+ type MiraExtension,
23
+ type MiraMode,
24
+ type MiraTemplateSelection,
25
+ type MiraToolbarIconName,
26
+ } from "@lapismd/mira/extensions";
27
+ import { mount, unmount, type Component } from "svelte";
28
+ import {
29
+ miraColorModeAttribute,
30
+ miraColorModeClassName,
31
+ normalizeMiraTheme,
32
+ provideMiraAppearance,
33
+ } from "@lapismd/mira/ui/appearance";
34
+ import MiraEditorToolbar from "./mira-editor-toolbar.svelte";
35
+ import {
36
+ createMiraEditorExtensions,
37
+ defaultMiraEditorEditMode,
38
+ MiraFeature,
39
+ resolveMiraEditorBlockControls,
40
+ resolveMiraEditorFeatures,
41
+ resolveMiraEditorEditMode,
42
+ resolveMiraEditorModes,
43
+ resolveMiraEditorToolbarActions,
44
+ resolveMiraEditorToolbarDefinitions,
45
+ type MiraEditorToolbarButtonAction,
46
+ type MiraEditorToolbarActionContext,
47
+ type MiraEditorToolbarDefinition,
48
+ } from "./features";
49
+ import type { MiraEditorProps } from "./types";
50
+
51
+ let {
52
+ value = $bindable(""),
53
+ defaultEditMode = defaultMiraEditorEditMode,
54
+ mode = $bindable(defaultEditMode),
55
+ readonly = false,
56
+ placeholder = "Start writing Markdown...",
57
+ lineWrapping = true,
58
+ spellcheck = true,
59
+ indentGuides = true,
60
+ indentWithTabs = true,
61
+ indentWidth = 4,
62
+ theme,
63
+ colorMode = "inherit",
64
+ sourcePath,
65
+ class: className = "",
66
+ editorClass = "",
67
+ features = {},
68
+ featureConfigs = {},
69
+ toolbarActions = [],
70
+ toolbars = [],
71
+ extensions = [],
72
+ linkResolver,
73
+ assetResolver,
74
+ fileAdapter,
75
+ imageConfig,
76
+ authoring,
77
+ frontmatterOpen = true,
78
+ frontmatterConfig,
79
+ headingIds = false,
80
+ headingIdPrefix = "",
81
+ htmlPolicy = "trusted",
82
+ emoji = false,
83
+ outline = false,
84
+ outlineVariant = "floating",
85
+ onChange,
86
+ onModeChange,
87
+ onReadonlyChange,
88
+ onIndentGuidesChange,
89
+ onIndentWidthChange,
90
+ onIndentWithTabsChange,
91
+ onFrontmatterChange,
92
+ }: MiraEditorProps = $props();
93
+
94
+ provideMiraAppearance({
95
+ get theme() {
96
+ return normalizeMiraTheme(theme);
97
+ },
98
+ get colorMode() {
99
+ return colorMode;
100
+ },
101
+ });
102
+
103
+ const themeAttribute = $derived(normalizeMiraTheme(theme));
104
+ const colorModeAttribute = $derived(miraColorModeAttribute(colorMode));
105
+ const colorModeClass = $derived(miraColorModeClassName(colorMode));
106
+
107
+ let editor: MiraHandle | null = $state(null);
108
+
109
+ const resolvedFeatures = $derived(resolveMiraEditorFeatures(features));
110
+ const modeOptions = $derived(resolveMiraEditorModes(features));
111
+ const resolvedDefaultEditMode = $derived(
112
+ resolveMiraEditorEditMode(defaultEditMode, modeOptions),
113
+ );
114
+ const frameworkBlockMenuActions = $derived.by(
115
+ resolveFrameworkBlockMenuActions,
116
+ );
117
+ const frameworkBlockMenuExtension = $derived(
118
+ createFrameworkBlockMenuExtension(frameworkBlockMenuActions),
119
+ );
120
+ const activeExtensions = $derived([
121
+ ...createMiraEditorExtensions({ features, featureConfigs }),
122
+ ...(frameworkBlockMenuExtension ? [frameworkBlockMenuExtension] : []),
123
+ ...extensions,
124
+ ]);
125
+ const resolvedExtensionContributions = $derived(
126
+ resolveMiraExtensions(activeExtensions, {
127
+ mode,
128
+ readonly,
129
+ sourcePath,
130
+ }),
131
+ );
132
+ const extensionToolbars = $derived.by(createExtensionToolbars);
133
+ const activeToolbars = $derived([...extensionToolbars, ...toolbars]);
134
+
135
+ const toolbarVisible = $derived(resolvedFeatures[MiraFeature.Toolbar]);
136
+ const toolbarContext = $derived(createToolbarActionContext());
137
+ const blockControls = $derived(
138
+ resolveMiraEditorBlockControls({ featureConfigs, features }),
139
+ );
140
+
141
+ type FrameworkBlockMenuAction = {
142
+ action: MiraEditorToolbarButtonAction;
143
+ group: string;
144
+ };
145
+
146
+ function resolveFrameworkBlockMenuActions(): FrameworkBlockMenuAction[] {
147
+ const actions = resolveMiraEditorToolbarActions({
148
+ featureConfigs,
149
+ toolbarActions,
150
+ })
151
+ .filter(isBlockMenuButton)
152
+ .map((action) => ({ action, group: action.group ?? "Actions" }));
153
+ const definitions = resolveMiraEditorToolbarDefinitions({
154
+ featureConfigs,
155
+ toolbars,
156
+ }).flatMap((toolbar) =>
157
+ toolbar.items.filter(isBlockMenuButton).map((action) => ({
158
+ action,
159
+ group: action.group ?? toolbar.label ?? "Actions",
160
+ })),
161
+ );
162
+ return [...actions, ...definitions];
163
+ }
164
+
165
+ function isBlockMenuButton(
166
+ action: import("./features").MiraEditorToolbarAction,
167
+ ): action is MiraEditorToolbarButtonAction {
168
+ return (
169
+ action.type !== "dropdown" &&
170
+ action.placements?.includes("block-menu") === true
171
+ );
172
+ }
173
+
174
+ function createFrameworkBlockMenuExtension(
175
+ actions: FrameworkBlockMenuAction[],
176
+ ): MiraExtension | null {
177
+ if (actions.length === 0) {
178
+ return null;
179
+ }
180
+ return defineMiraExtension({
181
+ name: "mira-editor-block-menu-actions",
182
+ blockActions: actions.map(({ action, group }) => ({
183
+ id: `mira-editor-${action.id}`,
184
+ label: action.label,
185
+ group,
186
+ placements: ["block-menu"],
187
+ shortcut: action.shortcut,
188
+ disabled(context) {
189
+ const disabled = action.disabled;
190
+ const actionContext = createBlockToolbarActionContext(context);
191
+ return typeof disabled === "function"
192
+ ? disabled(actionContext)
193
+ : (disabled ?? false);
194
+ },
195
+ renderIcon(target) {
196
+ const component = mount(action.icon, {
197
+ target,
198
+ props: {
199
+ class: "mira-editor__icon",
200
+ "aria-hidden": "true",
201
+ },
202
+ });
203
+ return () => {
204
+ void unmount(component);
205
+ };
206
+ },
207
+ run(context) {
208
+ action.run(createBlockToolbarActionContext(context));
209
+ },
210
+ })),
211
+ });
212
+ }
213
+
214
+ function createBlockToolbarActionContext(
215
+ context: MiraBlockActionContext,
216
+ ): MiraEditorToolbarActionContext {
217
+ return {
218
+ ...createToolbarActionContext(),
219
+ block: context.block,
220
+ blocks: context.blocks,
221
+ handle: context.handle,
222
+ affectedRange: context.affectedRange,
223
+ replaceRange: context.replaceRange,
224
+ };
225
+ }
226
+
227
+ function handleChange(nextValue: string): void {
228
+ value = nextValue;
229
+ onChange?.(nextValue);
230
+ }
231
+
232
+ function applyMode(nextMode: MiraMode): void {
233
+ if (!modeOptions.includes(nextMode)) {
234
+ return;
235
+ }
236
+ mode = nextMode;
237
+ onModeChange?.(nextMode);
238
+ }
239
+
240
+ function applyReadonly(nextReadonly: boolean): void {
241
+ readonly = nextReadonly;
242
+ onReadonlyChange?.(nextReadonly);
243
+ }
244
+
245
+ function applyIndentGuides(nextEnabled: boolean): void {
246
+ indentGuides = nextEnabled;
247
+ onIndentGuidesChange?.(nextEnabled);
248
+ }
249
+
250
+ function applyIndentWidth(nextWidth: number): void {
251
+ indentWidth = nextWidth;
252
+ onIndentWidthChange?.(nextWidth);
253
+ }
254
+
255
+ function applyIndentWithTabs(nextEnabled: boolean): void {
256
+ indentWithTabs = nextEnabled;
257
+ onIndentWithTabsChange?.(nextEnabled);
258
+ }
259
+
260
+ function createToolbarActionContext(): MiraEditorToolbarActionContext {
261
+ return {
262
+ value,
263
+ mode,
264
+ readonly,
265
+ applyMarkdownAction,
266
+ focus,
267
+ getIndentGuides: () => indentGuides,
268
+ getIndentWidth: () => indentWidth,
269
+ getIndentWithTabs: () => indentWithTabs,
270
+ getMarkdown,
271
+ getMode,
272
+ getSelection,
273
+ insertImage,
274
+ insertMarkdown,
275
+ setIndentGuides: applyIndentGuides,
276
+ setIndentWidth: applyIndentWidth,
277
+ setIndentWithTabs: applyIndentWithTabs,
278
+ setMarkdown,
279
+ setMode,
280
+ setReadonly,
281
+ setSelection,
282
+ };
283
+ }
284
+
285
+ export function focus(): void {
286
+ editor?.focus();
287
+ }
288
+
289
+ export function executeCommand(commandId: string): boolean {
290
+ return editor?.executeCommand(commandId) ?? false;
291
+ }
292
+
293
+ export function getCommands(): ReturnType<MiraHandle["getCommands"]> {
294
+ return editor?.getCommands() ?? resolvedExtensionContributions.commands;
295
+ }
296
+
297
+ export function isCommandEnabled(commandId: string): boolean {
298
+ return editor?.isCommandEnabled(commandId) ?? false;
299
+ }
300
+
301
+ export function getMarkdown(): string {
302
+ return editor?.getMarkdown() ?? value;
303
+ }
304
+
305
+ export function setMarkdown(markdown: string): void {
306
+ value = markdown;
307
+ editor?.setMarkdown(markdown);
308
+ }
309
+
310
+ export function getMode(): MiraMode {
311
+ return mode;
312
+ }
313
+
314
+ export function setMode(nextMode: MiraMode): void {
315
+ if (!modeOptions.includes(nextMode)) {
316
+ return;
317
+ }
318
+ applyMode(nextMode);
319
+ editor?.setMode(nextMode);
320
+ }
321
+
322
+ export function setReadonly(nextReadonly: boolean): void {
323
+ applyReadonly(nextReadonly);
324
+ editor?.setReadonly(nextReadonly);
325
+ }
326
+
327
+ export function getSelection(): MiraEditorSelection | null {
328
+ return editor?.getSelection() ?? null;
329
+ }
330
+
331
+ export function setSelection(selection: MiraEditorSelection): void {
332
+ editor?.setSelection(selection);
333
+ }
334
+
335
+ export function insertMarkdown(
336
+ markdown: string,
337
+ selection?: MiraTemplateSelection,
338
+ ): void {
339
+ editor?.insertMarkdown(markdown, selection);
340
+ }
341
+
342
+ export function applyMarkdownAction(action: MiraMarkdownActionId): boolean {
343
+ if (readonly || mode === "preview") {
344
+ return false;
345
+ }
346
+ return editor?.applyMarkdownAction(action) ?? false;
347
+ }
348
+
349
+ export function insertImage(): void {
350
+ editor?.insertImage();
351
+ }
352
+
353
+ function createExtensionToolbars(): MiraEditorToolbarDefinition[] {
354
+ const groups = new Map<string, MiraEditorToolbarDefinition>();
355
+
356
+ for (const item of resolvedExtensionContributions.toolbarItems) {
357
+ if (item.placements && !item.placements.includes("toolbar")) {
358
+ continue;
359
+ }
360
+ const align = item.align ?? "start";
361
+ const label = item.group ?? "Extensions";
362
+ const key = `${align}:${label}`;
363
+ let group = groups.get(key);
364
+ if (!group) {
365
+ group = {
366
+ id: `extension-${slugify(label)}-${align}`,
367
+ label,
368
+ align,
369
+ items: [],
370
+ };
371
+ groups.set(key, group);
372
+ }
373
+
374
+ group.items.push({
375
+ id: `extension-${item.id}`,
376
+ label: item.label,
377
+ tooltip: item.tooltip,
378
+ icon: extensionToolbarIcon(item.icon),
379
+ disabled: () => (editor ? !isCommandEnabled(item.command) : false),
380
+ run: () => {
381
+ executeCommand(item.command);
382
+ },
383
+ });
384
+ }
385
+
386
+ return [...groups.values()];
387
+ }
388
+
389
+ function extensionToolbarIcon(
390
+ icon: MiraToolbarIconName | undefined,
391
+ ): Component<Record<string, unknown>> {
392
+ return extensionToolbarIcons[icon ?? "command"] ?? CommandIcon;
393
+ }
394
+
395
+ function slugify(value: string): string {
396
+ return (
397
+ value
398
+ .toLowerCase()
399
+ .replace(/[^a-z0-9]+/g, "-")
400
+ .replace(/(^-|-$)/g, "") || "extensions"
401
+ );
402
+ }
403
+
404
+ const extensionToolbarIcons: Record<
405
+ MiraToolbarIconName,
406
+ Component<Record<string, unknown>>
407
+ > = {
408
+ check: CheckIcon,
409
+ code: CodeIcon,
410
+ command: CommandIcon,
411
+ image: ImageIcon,
412
+ link: LinkIcon,
413
+ play: PlayIcon,
414
+ "rotate-ccw": RotateCcwIcon,
415
+ save: SaveIcon,
416
+ sparkles: SparklesIcon,
417
+ table: TableIcon,
418
+ "wand-sparkles": WandSparklesIcon,
419
+ };
420
+
421
+ $effect(() => {
422
+ if (!modeOptions.includes(mode) && modeOptions[0]) {
423
+ applyMode(modeOptions[0]);
424
+ }
425
+ });
426
+ </script>
427
+
428
+ <div
429
+ class={`mira-editor ${colorModeClass ?? ""} ${className}`.trim()}
430
+ data-mira-theme={themeAttribute}
431
+ data-mira-color-mode={colorModeAttribute}
432
+ data-mode={mode}
433
+ data-readonly={readonly}
434
+ >
435
+ {#if toolbarVisible}
436
+ <MiraEditorToolbar
437
+ bind:mode
438
+ {value}
439
+ {readonly}
440
+ {features}
441
+ {featureConfigs}
442
+ defaultEditMode={resolvedDefaultEditMode}
443
+ {toolbarActions}
444
+ toolbars={activeToolbars}
445
+ context={toolbarContext}
446
+ {indentGuides}
447
+ {indentWidth}
448
+ {indentWithTabs}
449
+ onIndentGuidesChange={applyIndentGuides}
450
+ onIndentWidthChange={applyIndentWidth}
451
+ onIndentWithTabsChange={applyIndentWithTabs}
452
+ />
453
+ {/if}
454
+
455
+ <div class="mira-editor__editor">
456
+ <Mira
457
+ {...{ frontmatterConfig } as any}
458
+ bind:this={editor}
459
+ bind:value
460
+ bind:mode
461
+ class={editorClass}
462
+ {readonly}
463
+ {placeholder}
464
+ {lineWrapping}
465
+ {spellcheck}
466
+ {indentGuides}
467
+ {indentWithTabs}
468
+ {indentWidth}
469
+ {theme}
470
+ {colorMode}
471
+ {sourcePath}
472
+ {blockControls}
473
+ toolbar={false}
474
+ extensions={activeExtensions}
475
+ {linkResolver}
476
+ {assetResolver}
477
+ {fileAdapter}
478
+ {imageConfig}
479
+ {authoring}
480
+ frontmatterOpen={resolvedFeatures[MiraFeature.Frontmatter] &&
481
+ frontmatterOpen}
482
+ {headingIds}
483
+ {headingIdPrefix}
484
+ {htmlPolicy}
485
+ {emoji}
486
+ {outline}
487
+ {outlineVariant}
488
+ onChange={handleChange}
489
+ {onFrontmatterChange}
490
+ />
491
+ </div>
492
+ </div>