@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.
- package/CHANGELOG.md +7 -0
- package/LICENSE.md +4 -0
- package/README.md +70 -0
- package/dist/assets/logo.png +0 -0
- package/dist/assets.d.ts +4 -0
- package/dist/features.d.ts +164 -0
- package/dist/features.d.ts.map +1 -0
- package/dist/features.js +348 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +179 -0
- package/dist/mira-editor-about-dialog.svelte +28 -0
- package/dist/mira-editor-about-dialog.svelte.d.ts +7 -0
- package/dist/mira-editor-about-dialog.svelte.d.ts.map +1 -0
- package/dist/mira-editor-toolbar.svelte +805 -0
- package/dist/mira-editor-toolbar.svelte.d.ts +6 -0
- package/dist/mira-editor-toolbar.svelte.d.ts.map +1 -0
- package/dist/mira-editor-toolbar.test.d.ts +2 -0
- package/dist/mira-editor-toolbar.test.d.ts.map +1 -0
- package/dist/mira-editor-toolbar.test.js +199 -0
- package/dist/mira-editor.svelte +492 -0
- package/dist/mira-editor.svelte.d.ts +24 -0
- package/dist/mira-editor.svelte.d.ts.map +1 -0
- package/dist/mira-editor.test.d.ts +2 -0
- package/dist/mira-editor.test.d.ts.map +1 -0
- package/dist/mira-editor.test.js +208 -0
- package/dist/styles.css +165 -0
- package/dist/test-setup.d.ts +8 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +31 -0
- package/dist/toolbar-model.d.ts +32 -0
- package/dist/toolbar-model.d.ts.map +1 -0
- package/dist/toolbar-model.js +148 -0
- package/dist/toolbar-model.test.d.ts +2 -0
- package/dist/toolbar-model.test.d.ts.map +1 -0
- package/dist/toolbar-model.test.js +46 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/package.json +56 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import BoldIcon from "@lucide/svelte/icons/bold";
|
|
3
|
+
import BookOpenIcon from "@lucide/svelte/icons/book-open";
|
|
4
|
+
import BracesIcon from "@lucide/svelte/icons/braces";
|
|
5
|
+
import CheckIcon from "@lucide/svelte/icons/check";
|
|
6
|
+
import CodeIcon from "@lucide/svelte/icons/code";
|
|
7
|
+
import CodeXmlIcon from "@lucide/svelte/icons/code-xml";
|
|
8
|
+
import Columns2Icon from "@lucide/svelte/icons/columns-2";
|
|
9
|
+
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
|
|
10
|
+
import FileCodeIcon from "@lucide/svelte/icons/file-code";
|
|
11
|
+
import Heading1Icon from "@lucide/svelte/icons/heading-1";
|
|
12
|
+
import ImageIcon from "@lucide/svelte/icons/image";
|
|
13
|
+
import InfoIcon from "@lucide/svelte/icons/info";
|
|
14
|
+
import IndentIncreaseIcon from "@lucide/svelte/icons/indent-increase";
|
|
15
|
+
import ItalicIcon from "@lucide/svelte/icons/italic";
|
|
16
|
+
import LinkIcon from "@lucide/svelte/icons/link";
|
|
17
|
+
import ListIcon from "@lucide/svelte/icons/list";
|
|
18
|
+
import ListChecksIcon from "@lucide/svelte/icons/list-checks";
|
|
19
|
+
import ListOrderedIcon from "@lucide/svelte/icons/list-ordered";
|
|
20
|
+
import ListTreeIcon from "@lucide/svelte/icons/list-tree";
|
|
21
|
+
import PencilLineIcon from "@lucide/svelte/icons/pencil-line";
|
|
22
|
+
import QuoteIcon from "@lucide/svelte/icons/quote";
|
|
23
|
+
import SpaceIcon from "@lucide/svelte/icons/space";
|
|
24
|
+
import StrikethroughIcon from "@lucide/svelte/icons/strikethrough";
|
|
25
|
+
import Table2Icon from "@lucide/svelte/icons/table-2";
|
|
26
|
+
import TableCellsSplitIcon from "@lucide/svelte/icons/table-cells-split";
|
|
27
|
+
import WorkflowIcon from "@lucide/svelte/icons/workflow";
|
|
28
|
+
import { Separator } from "@lapismd/mira/ui/separator";
|
|
29
|
+
import * as DropdownMenu from "@lapismd/mira/ui/dropdown-menu";
|
|
30
|
+
import * as Toolbar from "@lapismd/mira/ui/toolbar";
|
|
31
|
+
import * as Tooltip from "@lapismd/mira/ui/tooltip";
|
|
32
|
+
import type { Component } from "svelte";
|
|
33
|
+
import type { MiraEditorSelection } from "@lapismd/mira/core";
|
|
34
|
+
import type { MiraMode } from "@lapismd/mira/extensions";
|
|
35
|
+
import {
|
|
36
|
+
MiraFeature,
|
|
37
|
+
defaultMiraEditorEditMode,
|
|
38
|
+
resolveMiraEditorFeatures,
|
|
39
|
+
resolveMiraEditorEditMode,
|
|
40
|
+
resolveMiraEditorModes,
|
|
41
|
+
resolveMiraEditorToolbarActions,
|
|
42
|
+
resolveMiraEditorToolbarDefinitions,
|
|
43
|
+
resolveMiraEditorToolbarItems,
|
|
44
|
+
type MiraEditorToolbarAction,
|
|
45
|
+
type MiraEditorToolbarActionContext,
|
|
46
|
+
type MiraEditorToolbarDefinition,
|
|
47
|
+
type MiraEditorToolbarDropdownAction,
|
|
48
|
+
type MiraEditorToolbarItem,
|
|
49
|
+
type MiraEditorToolbarMenuItem,
|
|
50
|
+
} from "./features";
|
|
51
|
+
import {
|
|
52
|
+
isMiraEditMode,
|
|
53
|
+
markdownActionForMiraToolbarItem,
|
|
54
|
+
miraEditorToolbarItemLabels,
|
|
55
|
+
miraViewOptionsLabel,
|
|
56
|
+
miraViewToggleLabel,
|
|
57
|
+
resolveMiraModeAfterSplit,
|
|
58
|
+
resolveMiraViewModeMenuItems,
|
|
59
|
+
resolveMiraViewToggleMode,
|
|
60
|
+
templateForMiraToolbarItem,
|
|
61
|
+
} from "./toolbar-model";
|
|
62
|
+
import type { MiraEditorToolbarProps } from "./types";
|
|
63
|
+
import MiraEditorAboutDialog from "./mira-editor-about-dialog.svelte";
|
|
64
|
+
|
|
65
|
+
let {
|
|
66
|
+
value = "",
|
|
67
|
+
mode = $bindable("live-preview"),
|
|
68
|
+
defaultEditMode = defaultMiraEditorEditMode,
|
|
69
|
+
readonly = false,
|
|
70
|
+
features = {},
|
|
71
|
+
featureConfigs = {},
|
|
72
|
+
toolbarActions = [],
|
|
73
|
+
toolbars = [],
|
|
74
|
+
modeOptions: modeOptionsProp,
|
|
75
|
+
showModeSwitch = true,
|
|
76
|
+
indentGuides = true,
|
|
77
|
+
indentWithTabs = true,
|
|
78
|
+
indentWidth = 4,
|
|
79
|
+
context,
|
|
80
|
+
class: className = "",
|
|
81
|
+
onModeChange,
|
|
82
|
+
onInsertMarkdown,
|
|
83
|
+
onInsertImage,
|
|
84
|
+
onIndentGuidesChange,
|
|
85
|
+
onIndentWidthChange,
|
|
86
|
+
onIndentWithTabsChange,
|
|
87
|
+
}: MiraEditorToolbarProps = $props();
|
|
88
|
+
|
|
89
|
+
const resolvedFeatures = $derived(resolveMiraEditorFeatures(features));
|
|
90
|
+
const modeOptions = $derived(
|
|
91
|
+
modeOptionsProp ?? resolveMiraEditorModes(features),
|
|
92
|
+
);
|
|
93
|
+
const toolbarItems = $derived(
|
|
94
|
+
resolveMiraEditorToolbarItems({ features, featureConfigs }),
|
|
95
|
+
);
|
|
96
|
+
const customToolbarActions = $derived(
|
|
97
|
+
resolveMiraEditorToolbarActions({ featureConfigs, toolbarActions }).filter(
|
|
98
|
+
hasTopToolbarPlacement,
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
const customToolbars = $derived(
|
|
102
|
+
resolveMiraEditorToolbarDefinitions({ featureConfigs, toolbars }).map(
|
|
103
|
+
(toolbar) => ({
|
|
104
|
+
...toolbar,
|
|
105
|
+
items: toolbar.items.filter(hasTopToolbarPlacement),
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
const startToolbars = $derived(
|
|
110
|
+
customToolbars.filter((toolbar) => toolbar.align !== "end"),
|
|
111
|
+
);
|
|
112
|
+
const endToolbars = $derived(
|
|
113
|
+
customToolbars.filter((toolbar) => toolbar.align === "end"),
|
|
114
|
+
);
|
|
115
|
+
const visibleStartToolbars = $derived(
|
|
116
|
+
startToolbars.filter(toolbarHasRenderableActions),
|
|
117
|
+
);
|
|
118
|
+
const visibleEndToolbars = $derived(
|
|
119
|
+
endToolbars.filter(toolbarHasRenderableActions),
|
|
120
|
+
);
|
|
121
|
+
const toolbarItemsVisible = $derived(toolbarItems.length > 0);
|
|
122
|
+
const customToolbarActionsVisible = $derived(customToolbarActions.length > 0);
|
|
123
|
+
|
|
124
|
+
const modeSwitchVisible = $derived(
|
|
125
|
+
showModeSwitch &&
|
|
126
|
+
resolvedFeatures[MiraFeature.ModeSwitch] &&
|
|
127
|
+
modeOptions.length > 1,
|
|
128
|
+
);
|
|
129
|
+
const toolbarHasLeadingContent = $derived(
|
|
130
|
+
visibleStartToolbars.length > 0 ||
|
|
131
|
+
toolbarItemsVisible ||
|
|
132
|
+
customToolbarActionsVisible,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
let lastNonSplitMode = $state<MiraMode | null>(null);
|
|
136
|
+
let aboutDialogOpen = $state(false);
|
|
137
|
+
|
|
138
|
+
function hasTopToolbarPlacement(action: MiraEditorToolbarAction): boolean {
|
|
139
|
+
return (
|
|
140
|
+
action.type === "dropdown" ||
|
|
141
|
+
!action.placements ||
|
|
142
|
+
action.placements.includes("toolbar")
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const modeIcons = {
|
|
147
|
+
source: FileCodeIcon,
|
|
148
|
+
"live-preview": PencilLineIcon,
|
|
149
|
+
preview: BookOpenIcon,
|
|
150
|
+
split: Columns2Icon,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const toolbarItemIcons = {
|
|
154
|
+
heading: Heading1Icon,
|
|
155
|
+
bold: BoldIcon,
|
|
156
|
+
italic: ItalicIcon,
|
|
157
|
+
strikethrough: StrikethroughIcon,
|
|
158
|
+
inlineCode: CodeXmlIcon,
|
|
159
|
+
quote: QuoteIcon,
|
|
160
|
+
bulletList: ListIcon,
|
|
161
|
+
numberedList: ListOrderedIcon,
|
|
162
|
+
taskList: ListChecksIcon,
|
|
163
|
+
link: LinkIcon,
|
|
164
|
+
image: ImageIcon,
|
|
165
|
+
table: Table2Icon,
|
|
166
|
+
gridTable: TableCellsSplitIcon,
|
|
167
|
+
code: CodeIcon,
|
|
168
|
+
math: BracesIcon,
|
|
169
|
+
mermaid: WorkflowIcon,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const resolvedDefaultEditMode = $derived(
|
|
173
|
+
resolveMiraEditorEditMode(defaultEditMode, modeOptions),
|
|
174
|
+
);
|
|
175
|
+
const editModeOptions = $derived(
|
|
176
|
+
modeOptions.filter((modeOption) => isMiraEditMode(modeOption)),
|
|
177
|
+
);
|
|
178
|
+
const viewModeMenuItems = $derived(
|
|
179
|
+
resolveMiraViewModeMenuItems({
|
|
180
|
+
mode,
|
|
181
|
+
modeOptions,
|
|
182
|
+
resolvedDefaultEditMode,
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
const viewModeToggleVisible = $derived(
|
|
186
|
+
modeSwitchVisible &&
|
|
187
|
+
modeOptions.includes("preview") &&
|
|
188
|
+
editModeOptions.length > 0,
|
|
189
|
+
);
|
|
190
|
+
const splitModeVisible = $derived(
|
|
191
|
+
modeSwitchVisible && modeOptions.includes("split"),
|
|
192
|
+
);
|
|
193
|
+
const editorSettingsMenuVisible = $derived(true);
|
|
194
|
+
const viewModeOverflowVisible = $derived(
|
|
195
|
+
(modeSwitchVisible && viewModeMenuItems.length > 0) ||
|
|
196
|
+
editorSettingsMenuVisible,
|
|
197
|
+
);
|
|
198
|
+
const viewControlsVisible = $derived(
|
|
199
|
+
viewModeToggleVisible || splitModeVisible || viewModeOverflowVisible,
|
|
200
|
+
);
|
|
201
|
+
const tabSizeOptions = [2, 4, 8];
|
|
202
|
+
|
|
203
|
+
function toolbarHasRenderableActions(
|
|
204
|
+
toolbar: MiraEditorToolbarDefinition,
|
|
205
|
+
): boolean {
|
|
206
|
+
return toolbar.items.length > 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function fallbackContext(): MiraEditorToolbarActionContext {
|
|
210
|
+
return {
|
|
211
|
+
value,
|
|
212
|
+
mode,
|
|
213
|
+
readonly,
|
|
214
|
+
focus: () => undefined,
|
|
215
|
+
getIndentGuides: () => indentGuides,
|
|
216
|
+
getIndentWidth: () => indentWidth,
|
|
217
|
+
getIndentWithTabs: () => indentWithTabs,
|
|
218
|
+
getMarkdown: () => value,
|
|
219
|
+
getMode: () => mode,
|
|
220
|
+
getSelection: () => null,
|
|
221
|
+
insertMarkdown(markdown) {
|
|
222
|
+
onInsertMarkdown?.(markdown);
|
|
223
|
+
},
|
|
224
|
+
insertImage() {
|
|
225
|
+
onInsertImage?.();
|
|
226
|
+
},
|
|
227
|
+
setIndentGuides(nextEnabled) {
|
|
228
|
+
indentGuides = nextEnabled;
|
|
229
|
+
onIndentGuidesChange?.(nextEnabled);
|
|
230
|
+
},
|
|
231
|
+
setIndentWidth(nextWidth) {
|
|
232
|
+
indentWidth = nextWidth;
|
|
233
|
+
onIndentWidthChange?.(nextWidth);
|
|
234
|
+
},
|
|
235
|
+
setIndentWithTabs(nextEnabled) {
|
|
236
|
+
indentWithTabs = nextEnabled;
|
|
237
|
+
onIndentWithTabsChange?.(nextEnabled);
|
|
238
|
+
},
|
|
239
|
+
setMarkdown: () => undefined,
|
|
240
|
+
setMode(nextMode) {
|
|
241
|
+
mode = nextMode;
|
|
242
|
+
onModeChange?.(nextMode);
|
|
243
|
+
},
|
|
244
|
+
setReadonly: () => undefined,
|
|
245
|
+
setSelection: () => undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function actionContext(): MiraEditorToolbarActionContext {
|
|
250
|
+
return context ?? fallbackContext();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function dynamicBoolean(
|
|
254
|
+
value:
|
|
255
|
+
| boolean
|
|
256
|
+
| ((context: MiraEditorToolbarActionContext) => boolean)
|
|
257
|
+
| undefined,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (typeof value === "function") {
|
|
260
|
+
return value(actionContext());
|
|
261
|
+
}
|
|
262
|
+
return value ?? false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isDropdownAction(
|
|
266
|
+
action: MiraEditorToolbarAction,
|
|
267
|
+
): action is MiraEditorToolbarDropdownAction {
|
|
268
|
+
return action.type === "dropdown";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isToolbarActionDisabled(action: MiraEditorToolbarAction): boolean {
|
|
272
|
+
return dynamicBoolean(action.disabled);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isToolbarActionPressed(action: MiraEditorToolbarAction): boolean {
|
|
276
|
+
return !isDropdownAction(action) && dynamicBoolean(action.pressed);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function runToolbarAction(action: MiraEditorToolbarAction): void {
|
|
280
|
+
if (isDropdownAction(action) || isToolbarActionDisabled(action)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
action.run(actionContext());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function findToolbarAction(
|
|
287
|
+
actionId: string,
|
|
288
|
+
): MiraEditorToolbarAction | undefined {
|
|
289
|
+
return [
|
|
290
|
+
...customToolbarActions,
|
|
291
|
+
...customToolbars.flatMap((toolbar) => toolbar.items),
|
|
292
|
+
].find((action) => action.id === actionId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function handleToolbarActionButtonClick(event: MouseEvent): void {
|
|
296
|
+
const actionId = (event.currentTarget as HTMLElement | null)?.dataset
|
|
297
|
+
.toolbarActionId;
|
|
298
|
+
const action = actionId ? findToolbarAction(actionId) : undefined;
|
|
299
|
+
if (action) {
|
|
300
|
+
runToolbarAction(action);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function modeToolbarDelegate(node: HTMLElement): { destroy: () => void } {
|
|
305
|
+
node.dataset.miraToolbarDelegate = "ready";
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
destroy() {
|
|
309
|
+
delete node.dataset.miraToolbarDelegate;
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function menuItemKey(item: MiraEditorToolbarMenuItem, index: number): string {
|
|
315
|
+
const fallback = item.type === "separator" ? index : item.label;
|
|
316
|
+
return `${item.type ?? "item"}-${item.id ?? fallback}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isMenuItemDisabled(item: MiraEditorToolbarMenuItem): boolean {
|
|
320
|
+
return (
|
|
321
|
+
item.type !== "label" &&
|
|
322
|
+
item.type !== "separator" &&
|
|
323
|
+
dynamicBoolean(item.disabled)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isMenuItemChecked(item: MiraEditorToolbarMenuItem): boolean {
|
|
328
|
+
return (
|
|
329
|
+
item.type !== "label" &&
|
|
330
|
+
item.type !== "separator" &&
|
|
331
|
+
dynamicBoolean(item.checked)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function menuItemIcon(
|
|
336
|
+
item: MiraEditorToolbarMenuItem,
|
|
337
|
+
): Component<Record<string, unknown>> | undefined {
|
|
338
|
+
if (item.type === "label" || item.type === "separator") {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return item.icon;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function runMenuItem(item: MiraEditorToolbarMenuItem): void {
|
|
345
|
+
if (
|
|
346
|
+
item.type === "label" ||
|
|
347
|
+
item.type === "separator" ||
|
|
348
|
+
isMenuItemDisabled(item)
|
|
349
|
+
) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
item.run(actionContext());
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function withTooltipClickHandler(
|
|
356
|
+
props: Record<string, unknown>,
|
|
357
|
+
handler: (event: MouseEvent) => void,
|
|
358
|
+
): Record<string, unknown> {
|
|
359
|
+
const originalClick =
|
|
360
|
+
typeof props.onclick === "function"
|
|
361
|
+
? (props.onclick as (event: MouseEvent) => void)
|
|
362
|
+
: undefined;
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
...props,
|
|
366
|
+
onclick(event: MouseEvent) {
|
|
367
|
+
handler(event);
|
|
368
|
+
originalClick?.(event);
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function applyMode(nextMode: string): void {
|
|
374
|
+
const typedMode = nextMode as MiraMode;
|
|
375
|
+
if (!modeOptions.includes(typedMode)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
mode = typedMode;
|
|
379
|
+
context?.setMode(typedMode);
|
|
380
|
+
onModeChange?.(typedMode);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function getIndentGuidesSetting(): boolean {
|
|
384
|
+
return actionContext().getIndentGuides?.() ?? indentGuides;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function getIndentWidthSetting(): number {
|
|
388
|
+
return actionContext().getIndentWidth?.() ?? indentWidth;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getIndentWithTabsSetting(): boolean {
|
|
392
|
+
return actionContext().getIndentWithTabs?.() ?? indentWithTabs;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function setIndentGuidesSetting(nextEnabled: boolean): void {
|
|
396
|
+
const ctx = actionContext();
|
|
397
|
+
if (ctx.setIndentGuides) {
|
|
398
|
+
ctx.setIndentGuides(nextEnabled);
|
|
399
|
+
} else {
|
|
400
|
+
indentGuides = nextEnabled;
|
|
401
|
+
onIndentGuidesChange?.(nextEnabled);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function setIndentWidthSetting(nextWidth: number): void {
|
|
406
|
+
const ctx = actionContext();
|
|
407
|
+
if (ctx.setIndentWidth) {
|
|
408
|
+
ctx.setIndentWidth(nextWidth);
|
|
409
|
+
} else {
|
|
410
|
+
indentWidth = nextWidth;
|
|
411
|
+
onIndentWidthChange?.(nextWidth);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function setIndentWithTabsSetting(nextEnabled: boolean): void {
|
|
416
|
+
const ctx = actionContext();
|
|
417
|
+
if (ctx.setIndentWithTabs) {
|
|
418
|
+
ctx.setIndentWithTabs(nextEnabled);
|
|
419
|
+
} else {
|
|
420
|
+
indentWithTabs = nextEnabled;
|
|
421
|
+
onIndentWithTabsChange?.(nextEnabled);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function switchToSplitMode(): void {
|
|
426
|
+
if (mode === "split") {
|
|
427
|
+
applyMode(
|
|
428
|
+
resolveMiraModeAfterSplit({
|
|
429
|
+
lastNonSplitMode,
|
|
430
|
+
modeOptions,
|
|
431
|
+
resolvedDefaultEditMode,
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
applyMode("split");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function viewToggleIcon(): Component<Record<string, unknown>> {
|
|
440
|
+
return mode === "preview" ? PencilLineIcon : BookOpenIcon;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function handleViewToggle(): void {
|
|
444
|
+
applyMode(resolveMiraViewToggleMode(mode, resolvedDefaultEditMode));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
$effect(() => {
|
|
448
|
+
if (
|
|
449
|
+
mode === "preview" &&
|
|
450
|
+
!modeOptions.includes("preview") &&
|
|
451
|
+
editModeOptions.length > 0
|
|
452
|
+
) {
|
|
453
|
+
applyMode(resolvedDefaultEditMode);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
$effect(() => {
|
|
458
|
+
if (mode !== "split" && modeOptions.includes(mode)) {
|
|
459
|
+
lastNonSplitMode = mode;
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
function insertTemplate(item: MiraEditorToolbarItem): void {
|
|
464
|
+
const activeContext = actionContext();
|
|
465
|
+
if (item === "image" && activeContext.insertImage) {
|
|
466
|
+
activeContext.insertImage();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const action = markdownActionForMiraToolbarItem(item);
|
|
470
|
+
if (action && activeContext.applyMarkdownAction) {
|
|
471
|
+
activeContext.applyMarkdownAction(action);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const markdown = templateForMiraToolbarItem(item);
|
|
475
|
+
if (markdown) {
|
|
476
|
+
activeContext.insertMarkdown(markdown);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
</script>
|
|
480
|
+
|
|
481
|
+
{#snippet renderAction(action: MiraEditorToolbarAction)}
|
|
482
|
+
{@const Icon = action.icon}
|
|
483
|
+
{#if isDropdownAction(action)}
|
|
484
|
+
<DropdownMenu.Root>
|
|
485
|
+
<Tooltip.Root>
|
|
486
|
+
<Tooltip.Trigger>
|
|
487
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
488
|
+
<DropdownMenu.Trigger
|
|
489
|
+
{...props}
|
|
490
|
+
class="mira-toolbar__button"
|
|
491
|
+
aria-label={action.label}
|
|
492
|
+
disabled={isToolbarActionDisabled(action)}
|
|
493
|
+
>
|
|
494
|
+
<Icon class="mira-editor__icon" aria-hidden="true" />
|
|
495
|
+
</DropdownMenu.Trigger>
|
|
496
|
+
{/snippet}
|
|
497
|
+
</Tooltip.Trigger>
|
|
498
|
+
<Tooltip.Content>{action.tooltip ?? action.label}</Tooltip.Content>
|
|
499
|
+
</Tooltip.Root>
|
|
500
|
+
<DropdownMenu.Content align="start">
|
|
501
|
+
{#each action.items as item, index (menuItemKey(item, index))}
|
|
502
|
+
{#if item.type === "separator"}
|
|
503
|
+
<DropdownMenu.Separator />
|
|
504
|
+
{:else if item.type === "label"}
|
|
505
|
+
<DropdownMenu.Label>{item.label}</DropdownMenu.Label>
|
|
506
|
+
{:else}
|
|
507
|
+
{@const ItemIcon = menuItemIcon(item)}
|
|
508
|
+
<DropdownMenu.Item
|
|
509
|
+
disabled={isMenuItemDisabled(item)}
|
|
510
|
+
onclick={() => runMenuItem(item)}
|
|
511
|
+
>
|
|
512
|
+
{#if ItemIcon}
|
|
513
|
+
<ItemIcon
|
|
514
|
+
class="mira-editor-toolbar__menu-icon"
|
|
515
|
+
aria-hidden="true"
|
|
516
|
+
/>
|
|
517
|
+
{:else}
|
|
518
|
+
<span
|
|
519
|
+
class="mira-editor-toolbar__menu-icon-placeholder"
|
|
520
|
+
aria-hidden="true"
|
|
521
|
+
></span>
|
|
522
|
+
{/if}
|
|
523
|
+
<span>{item.label}</span>
|
|
524
|
+
{#if item.shortcut}
|
|
525
|
+
<DropdownMenu.Shortcut>{item.shortcut}</DropdownMenu.Shortcut>
|
|
526
|
+
{/if}
|
|
527
|
+
{#if isMenuItemChecked(item)}
|
|
528
|
+
<CheckIcon
|
|
529
|
+
class="mira-editor-toolbar__menu-check"
|
|
530
|
+
aria-hidden="true"
|
|
531
|
+
/>
|
|
532
|
+
{/if}
|
|
533
|
+
</DropdownMenu.Item>
|
|
534
|
+
{/if}
|
|
535
|
+
{/each}
|
|
536
|
+
</DropdownMenu.Content>
|
|
537
|
+
</DropdownMenu.Root>
|
|
538
|
+
{:else}
|
|
539
|
+
<Tooltip.Root>
|
|
540
|
+
<Tooltip.Trigger>
|
|
541
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
542
|
+
{@const triggerProps = withTooltipClickHandler(
|
|
543
|
+
props,
|
|
544
|
+
handleToolbarActionButtonClick,
|
|
545
|
+
)}
|
|
546
|
+
<Toolbar.Button
|
|
547
|
+
{...triggerProps}
|
|
548
|
+
aria-label={action.label}
|
|
549
|
+
aria-pressed={isToolbarActionPressed(action) ? "true" : undefined}
|
|
550
|
+
data-toolbar-action-id={action.id}
|
|
551
|
+
disabled={isToolbarActionDisabled(action)}
|
|
552
|
+
>
|
|
553
|
+
<Icon class="mira-editor__icon" aria-hidden="true" />
|
|
554
|
+
</Toolbar.Button>
|
|
555
|
+
{/snippet}
|
|
556
|
+
</Tooltip.Trigger>
|
|
557
|
+
<Tooltip.Content>{action.tooltip ?? action.label}</Tooltip.Content>
|
|
558
|
+
</Tooltip.Root>
|
|
559
|
+
{/if}
|
|
560
|
+
{/snippet}
|
|
561
|
+
|
|
562
|
+
{#snippet renderToolbarSection(toolbar: MiraEditorToolbarDefinition)}
|
|
563
|
+
<div
|
|
564
|
+
class="mira-editor__toolbar-section"
|
|
565
|
+
role="group"
|
|
566
|
+
aria-label={toolbar.label ?? toolbar.id}
|
|
567
|
+
>
|
|
568
|
+
{#each toolbar.items as action (action.id)}
|
|
569
|
+
{@render renderAction(action)}
|
|
570
|
+
{/each}
|
|
571
|
+
</div>
|
|
572
|
+
{/snippet}
|
|
573
|
+
|
|
574
|
+
{#snippet renderViewModeControl()}
|
|
575
|
+
{@const ToggleIcon = viewToggleIcon()}
|
|
576
|
+
<Tooltip.Root>
|
|
577
|
+
<Tooltip.Trigger>
|
|
578
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
579
|
+
{@const triggerProps = withTooltipClickHandler(props, () =>
|
|
580
|
+
handleViewToggle(),
|
|
581
|
+
)}
|
|
582
|
+
<Toolbar.Button
|
|
583
|
+
{...triggerProps}
|
|
584
|
+
aria-label={miraViewToggleLabel(mode)}
|
|
585
|
+
>
|
|
586
|
+
<ToggleIcon class="mira-editor__icon" aria-hidden="true" />
|
|
587
|
+
</Toolbar.Button>
|
|
588
|
+
{/snippet}
|
|
589
|
+
</Tooltip.Trigger>
|
|
590
|
+
<Tooltip.Content>{miraViewToggleLabel(mode)}</Tooltip.Content>
|
|
591
|
+
</Tooltip.Root>
|
|
592
|
+
{/snippet}
|
|
593
|
+
|
|
594
|
+
{#snippet renderSplitModeButton()}
|
|
595
|
+
<Tooltip.Root>
|
|
596
|
+
<Tooltip.Trigger>
|
|
597
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
598
|
+
{@const triggerProps = withTooltipClickHandler(props, () =>
|
|
599
|
+
switchToSplitMode(),
|
|
600
|
+
)}
|
|
601
|
+
<Toolbar.Button
|
|
602
|
+
{...triggerProps}
|
|
603
|
+
aria-label="Split"
|
|
604
|
+
aria-pressed={mode === "split" ? "true" : undefined}
|
|
605
|
+
>
|
|
606
|
+
<Columns2Icon class="mira-editor__icon" aria-hidden="true" />
|
|
607
|
+
</Toolbar.Button>
|
|
608
|
+
{/snippet}
|
|
609
|
+
</Tooltip.Trigger>
|
|
610
|
+
<Tooltip.Content>Split</Tooltip.Content>
|
|
611
|
+
</Tooltip.Root>
|
|
612
|
+
{/snippet}
|
|
613
|
+
|
|
614
|
+
{#snippet renderViewModeOverflow()}
|
|
615
|
+
<DropdownMenu.Root>
|
|
616
|
+
<Tooltip.Root>
|
|
617
|
+
<Tooltip.Trigger>
|
|
618
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
619
|
+
<DropdownMenu.Trigger
|
|
620
|
+
{...props}
|
|
621
|
+
class="mira-toolbar__button"
|
|
622
|
+
aria-label={miraViewOptionsLabel}
|
|
623
|
+
>
|
|
624
|
+
<EllipsisIcon class="mira-editor__icon" aria-hidden="true" />
|
|
625
|
+
</DropdownMenu.Trigger>
|
|
626
|
+
{/snippet}
|
|
627
|
+
</Tooltip.Trigger>
|
|
628
|
+
<Tooltip.Content>{miraViewOptionsLabel}</Tooltip.Content>
|
|
629
|
+
</Tooltip.Root>
|
|
630
|
+
<DropdownMenu.Content align="end">
|
|
631
|
+
{#if viewModeMenuItems.length > 0}
|
|
632
|
+
{#each viewModeMenuItems as item (item.mode)}
|
|
633
|
+
{@const ItemIcon = modeIcons[item.mode]}
|
|
634
|
+
<DropdownMenu.Item onclick={() => applyMode(item.mode)}>
|
|
635
|
+
<ItemIcon
|
|
636
|
+
class="mira-editor-toolbar__menu-icon"
|
|
637
|
+
aria-hidden="true"
|
|
638
|
+
/>
|
|
639
|
+
<span>{item.label}</span>
|
|
640
|
+
{#if item.checked}
|
|
641
|
+
<CheckIcon
|
|
642
|
+
class="mira-editor-toolbar__menu-check"
|
|
643
|
+
aria-hidden="true"
|
|
644
|
+
/>
|
|
645
|
+
{/if}
|
|
646
|
+
</DropdownMenu.Item>
|
|
647
|
+
{/each}
|
|
648
|
+
<DropdownMenu.Separator />
|
|
649
|
+
{/if}
|
|
650
|
+
<DropdownMenu.Label>Editor</DropdownMenu.Label>
|
|
651
|
+
<DropdownMenu.Item
|
|
652
|
+
onclick={() => setIndentGuidesSetting(!getIndentGuidesSetting())}
|
|
653
|
+
>
|
|
654
|
+
<ListTreeIcon
|
|
655
|
+
class="mira-editor-toolbar__menu-icon"
|
|
656
|
+
aria-hidden="true"
|
|
657
|
+
/>
|
|
658
|
+
<span>Indentation guides</span>
|
|
659
|
+
{#if getIndentGuidesSetting()}
|
|
660
|
+
<CheckIcon
|
|
661
|
+
class="mira-editor-toolbar__menu-check"
|
|
662
|
+
aria-hidden="true"
|
|
663
|
+
/>
|
|
664
|
+
{/if}
|
|
665
|
+
</DropdownMenu.Item>
|
|
666
|
+
<DropdownMenu.Item
|
|
667
|
+
onclick={() => setIndentWithTabsSetting(!getIndentWithTabsSetting())}
|
|
668
|
+
>
|
|
669
|
+
<IndentIncreaseIcon
|
|
670
|
+
class="mira-editor-toolbar__menu-icon"
|
|
671
|
+
aria-hidden="true"
|
|
672
|
+
/>
|
|
673
|
+
<span>Use tabs for indentation</span>
|
|
674
|
+
{#if getIndentWithTabsSetting()}
|
|
675
|
+
<CheckIcon
|
|
676
|
+
class="mira-editor-toolbar__menu-check"
|
|
677
|
+
aria-hidden="true"
|
|
678
|
+
/>
|
|
679
|
+
{/if}
|
|
680
|
+
</DropdownMenu.Item>
|
|
681
|
+
<DropdownMenu.Separator />
|
|
682
|
+
<DropdownMenu.Label>Tab size</DropdownMenu.Label>
|
|
683
|
+
{#each tabSizeOptions as size}
|
|
684
|
+
<DropdownMenu.Item onclick={() => setIndentWidthSetting(size)}>
|
|
685
|
+
<SpaceIcon
|
|
686
|
+
class="mira-editor-toolbar__menu-icon"
|
|
687
|
+
aria-hidden="true"
|
|
688
|
+
/>
|
|
689
|
+
<span>{size} spaces</span>
|
|
690
|
+
{#if getIndentWidthSetting() === size}
|
|
691
|
+
<CheckIcon
|
|
692
|
+
class="mira-editor-toolbar__menu-check"
|
|
693
|
+
aria-hidden="true"
|
|
694
|
+
/>
|
|
695
|
+
{/if}
|
|
696
|
+
</DropdownMenu.Item>
|
|
697
|
+
{/each}
|
|
698
|
+
<DropdownMenu.Separator />
|
|
699
|
+
<DropdownMenu.Item onclick={() => (aboutDialogOpen = true)}>
|
|
700
|
+
<InfoIcon class="mira-editor-toolbar__menu-icon" aria-hidden="true" />
|
|
701
|
+
<span>About Mira</span>
|
|
702
|
+
</DropdownMenu.Item>
|
|
703
|
+
</DropdownMenu.Content>
|
|
704
|
+
</DropdownMenu.Root>
|
|
705
|
+
{/snippet}
|
|
706
|
+
|
|
707
|
+
<Tooltip.Provider delayDuration={350}>
|
|
708
|
+
<div
|
|
709
|
+
class="mira-editor-toolbar__event-root"
|
|
710
|
+
role="presentation"
|
|
711
|
+
use:modeToolbarDelegate
|
|
712
|
+
>
|
|
713
|
+
<Toolbar.Root
|
|
714
|
+
class={`mira-editor-toolbar mira-editor__toolbar ${className}`.trim()}
|
|
715
|
+
aria-label="Markdown editor toolbar"
|
|
716
|
+
>
|
|
717
|
+
{#each visibleStartToolbars as toolbar, index (toolbar.id)}
|
|
718
|
+
{#if index > 0}
|
|
719
|
+
<Separator orientation="vertical" class="mira-editor__separator" />
|
|
720
|
+
{/if}
|
|
721
|
+
{@render renderToolbarSection(toolbar)}
|
|
722
|
+
{/each}
|
|
723
|
+
|
|
724
|
+
{#if toolbarItemsVisible}
|
|
725
|
+
{#if visibleStartToolbars.length > 0}
|
|
726
|
+
<Separator orientation="vertical" class="mira-editor__separator" />
|
|
727
|
+
{/if}
|
|
728
|
+
<div
|
|
729
|
+
class="mira-editor__toolbar-section"
|
|
730
|
+
role="group"
|
|
731
|
+
aria-label="Insert"
|
|
732
|
+
>
|
|
733
|
+
{#each toolbarItems as item}
|
|
734
|
+
{@const Icon = toolbarItemIcons[item]}
|
|
735
|
+
<Tooltip.Root>
|
|
736
|
+
<Tooltip.Trigger>
|
|
737
|
+
{#snippet child({ props }: { props: Record<string, unknown> })}
|
|
738
|
+
{@const triggerProps = withTooltipClickHandler(props, () =>
|
|
739
|
+
insertTemplate(item),
|
|
740
|
+
)}
|
|
741
|
+
<Toolbar.Button
|
|
742
|
+
{...triggerProps}
|
|
743
|
+
aria-label={miraEditorToolbarItemLabels[item]}
|
|
744
|
+
disabled={readonly || mode === "preview"}
|
|
745
|
+
>
|
|
746
|
+
<Icon class="mira-editor__icon" aria-hidden="true" />
|
|
747
|
+
</Toolbar.Button>
|
|
748
|
+
{/snippet}
|
|
749
|
+
</Tooltip.Trigger>
|
|
750
|
+
<Tooltip.Content
|
|
751
|
+
>{miraEditorToolbarItemLabels[item]}</Tooltip.Content
|
|
752
|
+
>
|
|
753
|
+
</Tooltip.Root>
|
|
754
|
+
{/each}
|
|
755
|
+
</div>
|
|
756
|
+
{/if}
|
|
757
|
+
|
|
758
|
+
{#if customToolbarActionsVisible}
|
|
759
|
+
{#if visibleStartToolbars.length > 0 || toolbarItemsVisible}
|
|
760
|
+
<Separator orientation="vertical" class="mira-editor__separator" />
|
|
761
|
+
{/if}
|
|
762
|
+
<div
|
|
763
|
+
class="mira-editor__toolbar-section"
|
|
764
|
+
role="group"
|
|
765
|
+
aria-label="Custom actions"
|
|
766
|
+
>
|
|
767
|
+
{#each customToolbarActions as action (action.id)}
|
|
768
|
+
{@render renderAction(action)}
|
|
769
|
+
{/each}
|
|
770
|
+
</div>
|
|
771
|
+
{/if}
|
|
772
|
+
|
|
773
|
+
<div class="mira-editor-toolbar__spacer"></div>
|
|
774
|
+
|
|
775
|
+
{#each visibleEndToolbars as toolbar, index (toolbar.id)}
|
|
776
|
+
{#if index > 0}
|
|
777
|
+
<Separator orientation="vertical" class="mira-editor__separator" />
|
|
778
|
+
{/if}
|
|
779
|
+
{@render renderToolbarSection(toolbar)}
|
|
780
|
+
{/each}
|
|
781
|
+
|
|
782
|
+
{#if viewControlsVisible}
|
|
783
|
+
{#if toolbarHasLeadingContent || visibleEndToolbars.length > 0}
|
|
784
|
+
<Separator orientation="vertical" class="mira-editor__separator" />
|
|
785
|
+
{/if}
|
|
786
|
+
<div
|
|
787
|
+
class="mira-editor__toolbar-section"
|
|
788
|
+
role="group"
|
|
789
|
+
aria-label="View controls"
|
|
790
|
+
>
|
|
791
|
+
{#if viewModeToggleVisible}
|
|
792
|
+
{@render renderViewModeControl()}
|
|
793
|
+
{/if}
|
|
794
|
+
{#if splitModeVisible}
|
|
795
|
+
{@render renderSplitModeButton()}
|
|
796
|
+
{/if}
|
|
797
|
+
{#if viewModeOverflowVisible}
|
|
798
|
+
{@render renderViewModeOverflow()}
|
|
799
|
+
{/if}
|
|
800
|
+
</div>
|
|
801
|
+
{/if}
|
|
802
|
+
</Toolbar.Root>
|
|
803
|
+
</div>
|
|
804
|
+
<MiraEditorAboutDialog bind:open={aboutDialogOpen} />
|
|
805
|
+
</Tooltip.Provider>
|