@moloodev/editor-sdk 0.10.0 → 0.11.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.
- package/package.json +7 -7
- package/src/chrome-manifest.ts +2 -0
- package/src/commands/icons/catalog.ts +16 -0
- package/src/commands/manifest/editor-document.ts +26 -4
- package/src/commands/manifest/merge.ts +1 -0
- package/src/editor-common.ts +4 -4
- package/src/host/host-chrome-manifest.ts +22 -2
- package/src/host/host-chrome.tsx +36 -11
- package/src/layout/chrome/editor-menu.tsx +4 -2
- package/src/view/file-types-context.ts +5 -0
- package/src/view/hooks.ts +6 -5
- package/src/view/mount-editor-view.ts +3 -1
- package/src/view/port-binding.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moloodev/editor-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/view.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"y-protocols": "^1.0.7",
|
|
30
30
|
"yjs": "^13.6.32",
|
|
31
31
|
"zod": "^4.5.4",
|
|
32
|
-
"@moloodev/platform": "0.
|
|
33
|
-
"@moloodev/ui": "0.
|
|
32
|
+
"@moloodev/platform": "0.11.0",
|
|
33
|
+
"@moloodev/ui": "0.11.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": "^19.0.0",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"jsdom": "30.1.1",
|
|
47
47
|
"react": "^19.2.8",
|
|
48
48
|
"react-dom": "^19.2.8",
|
|
49
|
-
"
|
|
49
|
+
"vitest": "5.1.0",
|
|
50
50
|
"typescript": "^7.0.2",
|
|
51
|
-
"@moloodev/typescript-config": "0.
|
|
51
|
+
"@moloodev/typescript-config": "0.11.0"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
],
|
|
61
61
|
"scripts": {
|
|
62
62
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.origin-headers.json",
|
|
63
|
-
"test": "
|
|
64
|
-
"test:browser": "
|
|
63
|
+
"test": "vitest run --root ../.. --project @moloodev/editor-sdk --project @moloodev/editor-sdk:browser",
|
|
64
|
+
"test:browser": "vitest run --root ../.. --project @moloodev/editor-sdk:browser"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/chrome-manifest.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
2
|
import { parseTablerIconUri } from "../manifest/editor-document";
|
|
3
3
|
import {
|
|
4
|
+
IconArchive,
|
|
5
|
+
IconCube,
|
|
6
|
+
IconDatabase,
|
|
7
|
+
IconFile,
|
|
8
|
+
IconFolder,
|
|
9
|
+
IconHelp,
|
|
10
|
+
IconMovie,
|
|
11
|
+
IconTypography,
|
|
4
12
|
IconArrowBackUp,
|
|
5
13
|
IconArrowForwardUp,
|
|
6
14
|
IconBold,
|
|
@@ -69,6 +77,14 @@ export type EditorIconComponent = ComponentType<IconProps>;
|
|
|
69
77
|
|
|
70
78
|
/** Host-resolved Tabler icon names. */
|
|
71
79
|
export const EDITOR_ICON_CATALOG = {
|
|
80
|
+
archive: IconArchive,
|
|
81
|
+
cube: IconCube,
|
|
82
|
+
database: IconDatabase,
|
|
83
|
+
file: IconFile,
|
|
84
|
+
folder: IconFolder,
|
|
85
|
+
help: IconHelp,
|
|
86
|
+
movie: IconMovie,
|
|
87
|
+
typography: IconTypography,
|
|
72
88
|
menu: IconMenu2,
|
|
73
89
|
"arrow-back-up": IconArrowBackUp,
|
|
74
90
|
restore: IconRestore,
|
|
@@ -10,7 +10,7 @@ import { validateManifest } from "./validate-manifest";
|
|
|
10
10
|
|
|
11
11
|
export const TABLER_ICON_URI_PREFIX = "tabler:";
|
|
12
12
|
const TABLER_ICON_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
13
|
-
const
|
|
13
|
+
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
14
14
|
|
|
15
15
|
export function parseTablerIconUri(value: string): string | null {
|
|
16
16
|
if (!value.startsWith(TABLER_ICON_URI_PREFIX)) return null;
|
|
@@ -28,6 +28,8 @@ export type EditorContributes = Pick<
|
|
|
28
28
|
>;
|
|
29
29
|
|
|
30
30
|
export type EditorFileMatcher = {
|
|
31
|
+
/** Explicit asset kind id for type-only displays, independent of acceptedTypes. */
|
|
32
|
+
type?: string;
|
|
31
33
|
/** File extensions with leading dot, supporting compound extensions (e.g. [".layout.json"], [".md", ".markdown"]). */
|
|
32
34
|
extensions?: string[];
|
|
33
35
|
/** MIME types or wildcard families (e.g. ["text/markdown"], ["application/json"]). */
|
|
@@ -56,6 +58,8 @@ export type EditorManifest = {
|
|
|
56
58
|
categories?: string[];
|
|
57
59
|
/** Host-resolved Tabler icon URI (`tabler:{name}`). */
|
|
58
60
|
icon?: string;
|
|
61
|
+
/** Editor identity tint (`#rrggbb`), independent of fileTypes. */
|
|
62
|
+
color?: string;
|
|
59
63
|
/** Optional host I/O the editor implements (`editor.import` / `editor.download`). */
|
|
60
64
|
capabilities?: EditorCapability[];
|
|
61
65
|
importAccept?: string;
|
|
@@ -117,6 +121,13 @@ function parseEditorFileMatchers(input: unknown): EditorFileMatcher[] {
|
|
|
117
121
|
`Invalid editor manifest: fileTypes.${index} must be an object`,
|
|
118
122
|
);
|
|
119
123
|
}
|
|
124
|
+
let type: string | undefined;
|
|
125
|
+
if (entry.type != null) {
|
|
126
|
+
type = requireString(entry.type, `fileTypes.${index}.type`);
|
|
127
|
+
if (!isQualifiedName(type)) {
|
|
128
|
+
throw new Error(`Invalid editor manifest: fileTypes.${index}.type must be publisher.name`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
120
131
|
let extensions: string[] | undefined;
|
|
121
132
|
if (entry.extensions != null) {
|
|
122
133
|
if (!Array.isArray(entry.extensions) || entry.extensions.length === 0) {
|
|
@@ -198,18 +209,19 @@ function parseEditorFileMatchers(input: unknown): EditorFileMatcher[] {
|
|
|
198
209
|
let icon: AssetTypeIcon | undefined;
|
|
199
210
|
if (entry.icon != null) {
|
|
200
211
|
const rawIcon = requireString(entry.icon, `fileTypes.${index}.icon`);
|
|
201
|
-
|
|
212
|
+
const name = parseTablerIconUri(rawIcon);
|
|
213
|
+
if (name === null) {
|
|
202
214
|
throw new Error(
|
|
203
215
|
`Invalid editor manifest: fileTypes.${index}.icon must be tabler:{name}`,
|
|
204
216
|
);
|
|
205
217
|
}
|
|
206
|
-
icon =
|
|
218
|
+
icon = `tabler:${name}`;
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
let color: string | undefined;
|
|
210
222
|
if (entry.color != null) {
|
|
211
223
|
const rawColor = requireString(entry.color, `fileTypes.${index}.color`);
|
|
212
|
-
if (!
|
|
224
|
+
if (!HEX_COLOR_PATTERN.test(rawColor)) {
|
|
213
225
|
throw new Error(
|
|
214
226
|
`Invalid editor manifest: fileTypes.${index}.color must be #rrggbb`,
|
|
215
227
|
);
|
|
@@ -218,6 +230,7 @@ function parseEditorFileMatchers(input: unknown): EditorFileMatcher[] {
|
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
return {
|
|
233
|
+
...(type ? { type } : {}),
|
|
221
234
|
...(extensions ? { extensions } : {}),
|
|
222
235
|
...(mimeTypes ? { mimeTypes } : {}),
|
|
223
236
|
...(schema ? { schema } : {}),
|
|
@@ -370,6 +383,14 @@ export function validateEditorManifest(
|
|
|
370
383
|
throw new Error("Invalid editor manifest: icon must be tabler:{name}");
|
|
371
384
|
}
|
|
372
385
|
}
|
|
386
|
+
let color: string | undefined;
|
|
387
|
+
if (input.color != null) {
|
|
388
|
+
const rawColor = requireString(input.color, "color");
|
|
389
|
+
if (!HEX_COLOR_PATTERN.test(rawColor)) {
|
|
390
|
+
throw new Error("Invalid editor manifest: color must be #rrggbb");
|
|
391
|
+
}
|
|
392
|
+
color = rawColor.toLowerCase();
|
|
393
|
+
}
|
|
373
394
|
if ("order" in input) {
|
|
374
395
|
throw new Error(
|
|
375
396
|
"Invalid editor manifest: order belongs on workspace favorites, not editor documents",
|
|
@@ -438,6 +459,7 @@ export function validateEditorManifest(
|
|
|
438
459
|
...(fileTypes ? { fileTypes } : {}),
|
|
439
460
|
...(templates ? { templates } : {}),
|
|
440
461
|
...(icon ? { icon } : {}),
|
|
462
|
+
...(color ? { color } : {}),
|
|
441
463
|
...(categories ? { categories } : {}),
|
|
442
464
|
...(capabilities ? { capabilities } : {}),
|
|
443
465
|
...(importAccept ? { importAccept } : {}),
|
|
@@ -111,6 +111,7 @@ function buildMenuTree(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const SEGMENT_META: Record<string, { label: string; order: number }> = {
|
|
114
|
+
app: { label: "앱", order: 0 },
|
|
114
115
|
file: { label: "파일", order: 10 },
|
|
115
116
|
edit: { label: "편집", order: 30 },
|
|
116
117
|
insert: { label: "삽입", order: 35 },
|
package/src/editor-common.ts
CHANGED
|
@@ -63,7 +63,7 @@ export const EDITOR_COMMON_CONTRIBUTES = {
|
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
id: "editor.preferences",
|
|
66
|
-
title: "에디터
|
|
66
|
+
title: "에디터 설정…",
|
|
67
67
|
group: "앱",
|
|
68
68
|
},
|
|
69
69
|
{
|
|
@@ -140,10 +140,10 @@ export const EDITOR_COMMON_CONTRIBUTES = {
|
|
|
140
140
|
shortcut: "Backspace",
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
|
-
path: ["
|
|
143
|
+
path: ["app", "preferences"],
|
|
144
144
|
commandId: "editor.preferences",
|
|
145
|
-
group: "
|
|
146
|
-
order:
|
|
145
|
+
group: "2_settings",
|
|
146
|
+
order: 10,
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
149
|
path: ["view", "resetLayout"],
|
|
@@ -22,6 +22,9 @@ export function isHostReservedKey(key: string): boolean {
|
|
|
22
22
|
|
|
23
23
|
const HOST_CHROME_SCOPE = "host-chrome";
|
|
24
24
|
|
|
25
|
+
/** The host owns this label; published views still send their own title ("환경설정"). */
|
|
26
|
+
export const PREFERENCES_LABEL = "에디터 설정…";
|
|
27
|
+
|
|
25
28
|
export function scopeManifest(
|
|
26
29
|
manifest: ChromeManifest,
|
|
27
30
|
activation?: "exact",
|
|
@@ -33,7 +36,13 @@ export function scopeManifest(
|
|
|
33
36
|
? { commands: manifest.contributes.commands }
|
|
34
37
|
: {}),
|
|
35
38
|
...(manifest.contributes?.menus
|
|
36
|
-
? { menus: manifest.contributes.menus
|
|
39
|
+
? { menus: manifest.contributes.menus.map((menu) =>
|
|
40
|
+
// Compat for published views that still contribute the old Edit-menu path.
|
|
41
|
+
// Remove once no published editor targets an SDK older than this release.
|
|
42
|
+
menu.commandId === "editor.preferences"
|
|
43
|
+
? { ...menu, path: ["app", "preferences"], group: "2_settings", order: 10, label: PREFERENCES_LABEL }
|
|
44
|
+
: menu,
|
|
45
|
+
) }
|
|
37
46
|
: {}),
|
|
38
47
|
...(manifest.contributes?.keybindings
|
|
39
48
|
? { keybindings: manifest.contributes.keybindings }
|
|
@@ -43,8 +52,19 @@ export function scopeManifest(
|
|
|
43
52
|
|
|
44
53
|
export function hostChromeMergeInput(
|
|
45
54
|
manifest: ChromeManifest,
|
|
55
|
+
options?: { displayName: string },
|
|
46
56
|
): ContributionManifest[] {
|
|
47
|
-
return [
|
|
57
|
+
return [{
|
|
58
|
+
id: "host-app-menu",
|
|
59
|
+
commands: [
|
|
60
|
+
{ id: "editor.about", title: `${options?.displayName ?? "앱"}에 대하여` },
|
|
61
|
+
{ id: "editor.pinToSidebar", title: "사이드바에 고정" },
|
|
62
|
+
],
|
|
63
|
+
menus: [
|
|
64
|
+
{ path: ["app", "about"], commandId: "editor.about", group: "1_about", order: 10 },
|
|
65
|
+
{ path: ["app", "pin"], commandId: "editor.pinToSidebar", group: "3_sidebar", order: 10 },
|
|
66
|
+
],
|
|
67
|
+
}, scopeManifest(manifest)];
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
export type CommandStateBody = Extract<
|
package/src/host/host-chrome.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { KindIcon } from "@moloodev/ui/kind-icon";
|
|
|
14
14
|
import { TooltipProvider } from "@moloodev/ui";
|
|
15
15
|
import {
|
|
16
16
|
hostChromeMergeInput,
|
|
17
|
+
PREFERENCES_LABEL,
|
|
17
18
|
scopeManifest,
|
|
18
19
|
validateHostChrome,
|
|
19
20
|
type CommandStateBody,
|
|
@@ -25,6 +26,8 @@ export type HostChromeHandlers = {
|
|
|
25
26
|
openStartDialog: () => void;
|
|
26
27
|
openBrowser: () => void;
|
|
27
28
|
openPreferences?: () => void;
|
|
29
|
+
openAbout?: () => void;
|
|
30
|
+
togglePinToSidebar?: () => void;
|
|
28
31
|
/** When set, `editor.import` opens the host file picker instead of reaching the view. */
|
|
29
32
|
openImport?: () => void;
|
|
30
33
|
/** Runs before `editor.resetLayout` is forwarded to the view. */
|
|
@@ -33,6 +36,7 @@ export type HostChromeHandlers = {
|
|
|
33
36
|
|
|
34
37
|
export type EditorHostChromeProps = {
|
|
35
38
|
value: unknown;
|
|
39
|
+
appMenu?: { displayName: string; pinned: boolean };
|
|
36
40
|
host: HostChromeHandlers;
|
|
37
41
|
onViewCommand: (commandId: string) => void;
|
|
38
42
|
commandState?: Readonly<Record<string, CommandStateBody>>;
|
|
@@ -104,7 +108,7 @@ function overlayCommandState(
|
|
|
104
108
|
visible: state.visible,
|
|
105
109
|
disabled: state.disabled,
|
|
106
110
|
pressed: state.pressed,
|
|
107
|
-
label: state.label,
|
|
111
|
+
label: id === "editor.preferences" ? PREFERENCES_LABEL : state.label,
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
114
|
return next ?? commands;
|
|
@@ -117,6 +121,14 @@ export function dispatchHostCommand(
|
|
|
117
121
|
onViewCommand: (commandId: string) => void,
|
|
118
122
|
): void {
|
|
119
123
|
if (!declared.has(commandId)) return;
|
|
124
|
+
if (commandId === "editor.about") {
|
|
125
|
+
host.openAbout?.();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (commandId === "editor.pinToSidebar") {
|
|
129
|
+
host.togglePinToSidebar?.();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
120
132
|
if (commandId === "editor.new") {
|
|
121
133
|
host.openStartDialog();
|
|
122
134
|
return;
|
|
@@ -152,18 +164,16 @@ export function chromeCommandHandlers(
|
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
function HostCommandHandlers({
|
|
155
|
-
manifest,
|
|
156
167
|
host,
|
|
157
168
|
onViewCommand,
|
|
158
169
|
}: {
|
|
159
|
-
manifest: ChromeManifest;
|
|
160
170
|
host: HostChromeHandlers;
|
|
161
171
|
onViewCommand: (commandId: string) => void;
|
|
162
172
|
}) {
|
|
163
|
-
const { registerHandler } = useCommands();
|
|
173
|
+
const { commands, registerHandler } = useCommands();
|
|
164
174
|
const commandIds = useMemo(
|
|
165
|
-
() =>
|
|
166
|
-
[
|
|
175
|
+
() => commands.map((command) => command.id),
|
|
176
|
+
[commands],
|
|
167
177
|
);
|
|
168
178
|
const declared = useMemo(() => new Set(commandIds), [commandIds]);
|
|
169
179
|
useEffect(() => {
|
|
@@ -198,18 +208,20 @@ function HostCommands({
|
|
|
198
208
|
<ScopeProvider manifest={scopeManifest(manifest, "exact")}>
|
|
199
209
|
{children}
|
|
200
210
|
</ScopeProvider>
|
|
201
|
-
<HostCommandHandlers
|
|
211
|
+
<HostCommandHandlers host={host} onViewCommand={onViewCommand} />
|
|
202
212
|
</>
|
|
203
213
|
);
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
function ChromeFrame({
|
|
207
217
|
spec,
|
|
218
|
+
appMenu,
|
|
208
219
|
host,
|
|
209
220
|
onViewCommand,
|
|
210
221
|
commandState,
|
|
211
222
|
}: {
|
|
212
223
|
spec: Spec;
|
|
224
|
+
appMenu?: EditorHostChromeProps["appMenu"];
|
|
213
225
|
host: HostChromeHandlers;
|
|
214
226
|
onViewCommand: (commandId: string) => void;
|
|
215
227
|
commandState?: Readonly<Record<string, CommandStateBody>>;
|
|
@@ -221,8 +233,18 @@ function ChromeFrame({
|
|
|
221
233
|
[commands],
|
|
222
234
|
);
|
|
223
235
|
const layoutCommands = useMemo(
|
|
224
|
-
() =>
|
|
225
|
-
|
|
236
|
+
() => ({
|
|
237
|
+
...overlayCommandState(commandLayoutState(commands, ctx).commands, commandState),
|
|
238
|
+
"editor.about": {
|
|
239
|
+
visible: true, disabled: !host.openAbout, pressed: false,
|
|
240
|
+
label: `${appMenu?.displayName ?? "앱"}에 대하여`,
|
|
241
|
+
},
|
|
242
|
+
"editor.pinToSidebar": {
|
|
243
|
+
visible: true, disabled: !host.togglePinToSidebar,
|
|
244
|
+
pressed: appMenu?.pinned ?? false, label: "사이드바에 고정",
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
[appMenu, commandState, commands, ctx, host.openAbout, host.togglePinToSidebar],
|
|
226
248
|
);
|
|
227
249
|
const store = useMemo(() => createStateStore({ commands: layoutCommands }), []);
|
|
228
250
|
useLayoutEffect(() => {
|
|
@@ -264,6 +286,7 @@ function ChromeFrame({
|
|
|
264
286
|
return (
|
|
265
287
|
<LayoutBindingProvider value={binding}>
|
|
266
288
|
<EditorMenuBar
|
|
289
|
+
appMenuLabel={appMenu?.displayName}
|
|
267
290
|
commandState={layoutCommands}
|
|
268
291
|
onExecute={(commandId) => handlers.executeCommand({ commandId })}
|
|
269
292
|
/>
|
|
@@ -273,14 +296,15 @@ function ChromeFrame({
|
|
|
273
296
|
|
|
274
297
|
export function EditorHostChrome({
|
|
275
298
|
value,
|
|
299
|
+
appMenu,
|
|
276
300
|
host,
|
|
277
301
|
onViewCommand,
|
|
278
302
|
commandState,
|
|
279
303
|
}: EditorHostChromeProps) {
|
|
280
304
|
const validated = useMemo(() => validateHostChrome(value), [value]);
|
|
281
305
|
const seeded = useMemo(
|
|
282
|
-
() => (validated.ok ? hostChromeMergeInput(validated.manifest) : []),
|
|
283
|
-
[validated],
|
|
306
|
+
() => (validated.ok ? hostChromeMergeInput(validated.manifest, appMenu) : []),
|
|
307
|
+
[validated, appMenu?.displayName],
|
|
284
308
|
);
|
|
285
309
|
if (!validated.ok) return null;
|
|
286
310
|
return (
|
|
@@ -293,6 +317,7 @@ export function EditorHostChrome({
|
|
|
293
317
|
>
|
|
294
318
|
<ChromeFrame
|
|
295
319
|
spec={validated.spec}
|
|
320
|
+
appMenu={appMenu}
|
|
296
321
|
host={host}
|
|
297
322
|
onViewCommand={onViewCommand}
|
|
298
323
|
commandState={commandState}
|
|
@@ -34,9 +34,11 @@ type MenuCommandState = Readonly<Record<string, CommandLayoutEntry>>;
|
|
|
34
34
|
|
|
35
35
|
/** Horizontal editor menubar driven by merged contribution manifests. */
|
|
36
36
|
export function EditorMenuBar({
|
|
37
|
+
appMenuLabel,
|
|
37
38
|
commandState,
|
|
38
39
|
onExecute,
|
|
39
40
|
}: {
|
|
41
|
+
appMenuLabel?: string;
|
|
40
42
|
commandState?: MenuCommandState;
|
|
41
43
|
onExecute?: (commandId: string) => unknown;
|
|
42
44
|
} = {}) {
|
|
@@ -103,7 +105,7 @@ export function EditorMenuBar({
|
|
|
103
105
|
/>
|
|
104
106
|
}
|
|
105
107
|
>
|
|
106
|
-
{node.label}
|
|
108
|
+
{node.segment === "app" ? appMenuLabel ?? node.label : node.label}
|
|
107
109
|
</DropdownMenuTrigger>
|
|
108
110
|
<DropdownMenuContent
|
|
109
111
|
align="start"
|
|
@@ -298,7 +300,7 @@ export function MenuLeaf({
|
|
|
298
300
|
</>
|
|
299
301
|
);
|
|
300
302
|
|
|
301
|
-
if (isPanelToggleCommand(node.commandId)) {
|
|
303
|
+
if (isPanelToggleCommand(node.commandId) || node.commandId === "editor.pinToSidebar") {
|
|
302
304
|
return (
|
|
303
305
|
<DropdownMenuCheckboxItem
|
|
304
306
|
className={`${editorMenuItemClass} pr-7`}
|
package/src/view/hooks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, createElement, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import { KNOWN_PATTERN_IDS,
|
|
2
|
+
import { KNOWN_PATTERN_IDS, resolveTypeAppearance, type FileAppearanceMatcher } from "@moloodev/platform";
|
|
3
3
|
import type { Doc } from "../crdt";
|
|
4
4
|
import type { Composition, Publication } from "@moloodev/platform";
|
|
5
5
|
import type {
|
|
@@ -120,8 +120,8 @@ export function shouldShowContentError(
|
|
|
120
120
|
return contentError && isEditorContentReady(mode, hasDoc);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
export function kindOf(type: string) {
|
|
124
|
-
const entry =
|
|
123
|
+
export function kindOf(type: string, matchers: readonly FileAppearanceMatcher[] = []) {
|
|
124
|
+
const entry = resolveTypeAppearance(type, matchers);
|
|
125
125
|
return {
|
|
126
126
|
id: type,
|
|
127
127
|
label: entry.label,
|
|
@@ -130,8 +130,9 @@ export function kindOf(type: string) {
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
export function kindsOf() {
|
|
134
|
-
|
|
133
|
+
export function kindsOf(matchers: readonly FileAppearanceMatcher[] = []) {
|
|
134
|
+
const types = new Set<string>([...KNOWN_PATTERN_IDS, ...matchers.flatMap((rule) => rule.type ? [rule.type] : [])]);
|
|
135
|
+
return [...types].map((type) => kindOf(type, matchers));
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export function useTheme() {
|
|
@@ -2,6 +2,7 @@ import { createElement, type ComponentType } from "react";
|
|
|
2
2
|
import type { EditorManifest } from "../commands";
|
|
3
3
|
import { connectEditorView, type EditorView } from "../view";
|
|
4
4
|
import { bindThemeClass } from "./theme-class";
|
|
5
|
+
import { FileTypesContext } from "./file-types-context";
|
|
5
6
|
import { createEditorChrome } from "./editor-chrome";
|
|
6
7
|
|
|
7
8
|
/** Imports outside an iframe stay inert, including server and DOM unit tests. */
|
|
@@ -24,7 +25,8 @@ export function mountEditorView(options: {
|
|
|
24
25
|
root.id = "root";
|
|
25
26
|
document.body.append(root);
|
|
26
27
|
void import("react-dom/client").then(({ createRoot }) => {
|
|
27
|
-
createRoot(root).render(createElement(
|
|
28
|
+
createRoot(root).render(createElement(FileTypesContext.Provider, { value: options.manifest.fileTypes ?? [] },
|
|
29
|
+
createElement(options.Root, { view })));
|
|
28
30
|
});
|
|
29
31
|
void view.handshake.then(async (reply) => {
|
|
30
32
|
if (!reply.ok) throw new Error(reply.code);
|
package/src/view/port-binding.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createElement, useCallback, useMemo, useSyncExternalStore, type ReactNode } from "react";
|
|
1
|
+
import { createElement, useContext, useCallback, useMemo, useSyncExternalStore, type ReactNode } from "react";
|
|
2
2
|
import type { EditorOperation } from "../index";
|
|
3
|
+
import { FileTypesContext } from "./file-types-context";
|
|
3
4
|
import { RenderFailure, renderCode } from "../render-result";
|
|
4
5
|
import type { EditorView } from "../view";
|
|
5
6
|
import { EditorViewProvider, kindOf, type EditorOpenBrowserArgs, type EditorViewBinding } from "./hooks";
|
|
@@ -66,7 +67,8 @@ function createPortBinding(view: EditorView, acceptedTypes: readonly string[]):
|
|
|
66
67
|
},
|
|
67
68
|
useKind(type) {
|
|
68
69
|
useSnapshot();
|
|
69
|
-
|
|
70
|
+
const matchers = useContext(FileTypesContext);
|
|
71
|
+
return useMemo(() => kindOf(type, matchers), [type, matchers]);
|
|
70
72
|
},
|
|
71
73
|
useKinds: () => unavailable("useKinds"),
|
|
72
74
|
useList: () => unavailable("useList"),
|