@openpresentation/opf-editor 0.28.0 → 0.29.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/README.md +20 -6
- package/dist/canvas.d.ts +13 -1
- package/dist/canvas.js +39 -9
- package/dist/chart-options-panel.js +16 -16
- package/dist/codemirror-guard.js +69 -0
- package/dist/content-controls.js +127 -74
- package/dist/data-grid.js +15 -7
- package/dist/design-controls.d.ts +13 -0
- package/dist/design-controls.js +134 -124
- package/dist/exact-source.d.ts +30 -0
- package/dist/font-gate.js +20 -1
- package/dist/json-editor.js +8 -2
- package/dist/json-options.d.ts +2 -1
- package/dist/json-options.js +7 -6
- package/dist/layout-templates.d.ts +8 -3
- package/dist/layout-templates.js +30 -16
- package/dist/numbering-panel.js +2 -3
- package/dist/react.d.ts +12 -4
- package/dist/react.js +39 -5
- package/dist/slide-adapter.d.ts +49 -0
- package/dist/slide-adapter.js +289 -0
- package/dist/slide-edits.d.ts +46 -0
- package/dist/slide-edits.js +629 -0
- package/dist/slide-manager.js +2 -1
- package/dist/slide-scene.d.ts +96 -0
- package/dist/slide-scene.js +413 -0
- package/dist/source-checker.js +45 -6
- package/dist/source-completions.js +6 -4
- package/dist/source-pane.d.ts +18 -6
- package/dist/source-pane.js +45 -0
- package/dist/source-worker.js +8 -4
- package/dist/studio-canvas.d.ts +34 -0
- package/dist/studio-canvas.js +586 -0
- package/dist/studio-commands.d.ts +15 -0
- package/dist/studio-commands.js +155 -0
- package/dist/studio-design.d.ts +16 -0
- package/dist/studio-design.js +354 -0
- package/dist/studio-kit/dom.d.ts +16 -0
- package/dist/studio-kit/dom.js +42 -6
- package/dist/studio-layers.d.ts +12 -0
- package/dist/studio-layers.js +218 -0
- package/dist/studio.d.ts +31 -0
- package/dist/studio.js +1608 -0
- package/dist/styles.css +1546 -117
- package/dist/thumbnail-cache.d.ts +2 -0
- package/dist/thumbnail-cache.js +10 -1
- package/dist/workspace-measure.js +6 -4
- package/dist/workspace-parts.js +20 -0
- package/dist/workspace-shell.js +25 -15
- package/dist/workspace-theme.js +3 -0
- package/dist/workspace.d.ts +88 -9
- package/dist/workspace.js +210 -49
- package/package.json +29 -18
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// CE-20: the Studio's commands (STUDIO-SPEC.md sections 6 and 14). One registry is the source of the keyboard map, the context menu, the bar's
|
|
2
|
+
// disabled states, the shortcut sheet and a host's palette (`editor.commands.list()`), so they cannot drift. Each command's `when(studio)`
|
|
3
|
+
// returns true, or a reason string that disables it and is shown as its title and spoken in the live region; `run(studio)` asks the Studio to
|
|
4
|
+
// act. Nothing here touches the DOM or the document: the Studio (src/studio.js) does, through the slide adapter (one undo step per command).
|
|
5
|
+
import { createCommandRegistry } from "./studio-kit/commands.js";
|
|
6
|
+
|
|
7
|
+
/** The block kinds Insert offers, in menu order (the layout region kinds, with Group last). */
|
|
8
|
+
export const STUDIO_INSERT_KINDS = Object.freeze(["text", "list", "image", "video", "chart", "table", "code", "metric", "quote", "timeline", "group"]);
|
|
9
|
+
export const STUDIO_INSERT_LABELS = Object.freeze({ text: "Text", list: "List", image: "Image", video: "Video", chart: "Chart", table: "Table", code: "Code", metric: "Metric", quote: "Quote", timeline: "Timeline", group: "Group" });
|
|
10
|
+
export const STUDIO_INSERT_HINTS = Object.freeze({
|
|
11
|
+
text: "A paragraph of text",
|
|
12
|
+
list: "Bulleted or numbered points",
|
|
13
|
+
image: "A picture, with alt text",
|
|
14
|
+
video: "A video with a poster frame",
|
|
15
|
+
chart: "Column, bar, line or pie chart from a small table",
|
|
16
|
+
table: "Rows and columns of values",
|
|
17
|
+
code: "Source code with highlighting",
|
|
18
|
+
metric: "One number with its label",
|
|
19
|
+
quote: "A quotation and its source",
|
|
20
|
+
timeline: "Dated steps in order",
|
|
21
|
+
group: "A row of blocks you arrange together",
|
|
22
|
+
});
|
|
23
|
+
/** The download formats of File (`studio.file.download.<format>`). */
|
|
24
|
+
export const STUDIO_DOWNLOAD_FORMATS = Object.freeze(["json", "yaml", "markdown"]);
|
|
25
|
+
|
|
26
|
+
const READ_ONLY = "This editor is read-only.";
|
|
27
|
+
const FONTS = "Fonts are loading.";
|
|
28
|
+
const SOURCE = "Fix the source text first: the slide shows the last valid document.";
|
|
29
|
+
|
|
30
|
+
// The common gates of a command that changes the document.
|
|
31
|
+
function editable(s) {
|
|
32
|
+
if (s.readOnly) return READ_ONLY;
|
|
33
|
+
if (!s.inSync) return SOURCE;
|
|
34
|
+
if (s.fontsLoading) return FONTS;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const and = (...checks) => (s) => {
|
|
38
|
+
for (const check of checks) {
|
|
39
|
+
const verdict = check(s);
|
|
40
|
+
if (verdict !== true) return verdict;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
// A selection of blocks (or implicit content) on the canvas: not a heading, part, region or slide.
|
|
45
|
+
const blocks = (action) => (s) => {
|
|
46
|
+
const nodes = s.selectedNodes();
|
|
47
|
+
if (!nodes.length) return `Select a block to ${action}.`;
|
|
48
|
+
if (nodes.some((node) => node.kind === "heading")) return `A heading cannot ${action === "move" ? "move" : `be ${action === "copy" ? "copied as a block" : `${action}d`}`}.`;
|
|
49
|
+
if (nodes.some((node) => node.kind === "part")) return `Select the whole block to ${action} it.`;
|
|
50
|
+
if (nodes.some((node) => node.kind === "region")) return "A region belongs to the slide's layout. Edit the layout to change it.";
|
|
51
|
+
if (nodes.some((node) => node.kind === "slide")) return "Select a block on the slide.";
|
|
52
|
+
return true;
|
|
53
|
+
};
|
|
54
|
+
const slides = (s) => (s.slideScope() ? true : "Select a slide first.");
|
|
55
|
+
const blocksOrSlides = (action) => (s) => (s.slideScope() ? true : blocks(action)(s));
|
|
56
|
+
const primaryKind = (kinds, message) => (s) => (kinds.includes(s.primaryNode()?.type) && s.primaryNode()?.kind !== "part" ? true : message);
|
|
57
|
+
const playground = (s) => (s.layout === "studio" ? true : "Open the Studio for this.");
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The command definitions. Keys are CodeMirror-style ("Mod-z"); Mod is Cmd on macOS and Ctrl elsewhere. `scope` "canvas" commands run
|
|
61
|
+
* only while focus is on the slide, the layers or the Studio's own chrome, never in a field or the source pane (the kit's typing guard).
|
|
62
|
+
*/
|
|
63
|
+
export function studioCommandDefinitions() {
|
|
64
|
+
const list = [
|
|
65
|
+
// Tools
|
|
66
|
+
{ id: "studio.tool.select", label: "Select tool", group: "Tools", keys: ["v"], when: playground, run: (s) => s.setTool("select") },
|
|
67
|
+
{ id: "studio.tool.hand", label: "Hand tool", group: "Tools", keys: ["h"], when: playground, run: (s) => s.setTool("hand") },
|
|
68
|
+
{ id: "studio.tool.text", label: "Text tool", group: "Tools", keys: ["t"], when: and(playground, editable), run: (s) => s.setTool("text") },
|
|
69
|
+
// View
|
|
70
|
+
{ id: "studio.zoom.in", label: "Zoom in", group: "View", keys: ["Mod-=", "Mod-+", "Shift-Mod-=", "Shift-Mod-+"], run: (s) => s.zoomBy(1) },
|
|
71
|
+
{ id: "studio.zoom.out", label: "Zoom out", group: "View", keys: ["Mod--"], run: (s) => s.zoomBy(-1) },
|
|
72
|
+
{ id: "studio.zoom.100", label: "Zoom to 100%", group: "View", keys: ["Shift-0", "Shift-)"], run: (s) => s.setZoom(1) },
|
|
73
|
+
{ id: "studio.zoom.fit", label: "Zoom to fit", group: "View", keys: ["Shift-1", "Shift-!"], run: (s) => s.fit("slide") },
|
|
74
|
+
{ id: "studio.panels.toggle", label: "Show or hide panels", group: "View", keys: ["Mod-\\"], when: playground, run: (s) => s.togglePanels() },
|
|
75
|
+
{ id: "studio.source.toggle", label: "Show or hide the source", group: "View", keys: ["Ctrl-`"], when: (s) => (s.hasSource ? true : "This editor has no source pane."), run: (s) => s.toggleSource() },
|
|
76
|
+
{ id: "studio.layout.playground", label: "Switch to the Playground", group: "View", when: (s) => (s.layout === "playground" ? "The Playground is open." : true), run: (s) => s.setLayout("playground") },
|
|
77
|
+
{ id: "studio.layout.studio", label: "Open in Studio", group: "View", when: (s) => (s.layout === "studio" ? "The Studio is open." : true), run: (s) => s.setLayout("studio") },
|
|
78
|
+
// Selection
|
|
79
|
+
{ id: "studio.select.parent", label: "Select parent", group: "Selection", keys: ["Shift-Enter"], scope: "canvas", when: (s) => (s.selectedKeys().length ? true : "Nothing is selected."), run: (s) => s.selectParent() },
|
|
80
|
+
{ id: "studio.select.children", label: "Select children", group: "Selection", keys: ["Enter"], scope: "canvas", when: (s) => (s.primaryNode()?.children?.length ? true : "This has nothing inside it."), run: (s) => s.selectChildren() },
|
|
81
|
+
{ id: "studio.select.next", label: "Select next", group: "Selection", keys: ["Tab", "ArrowRight", "ArrowDown"], scope: "canvas", when: (s) => s.hasSibling(1), run: (s) => s.selectSibling(1) },
|
|
82
|
+
{ id: "studio.select.previous", label: "Select previous", group: "Selection", keys: ["Shift-Tab", "ArrowLeft", "ArrowUp"], scope: "canvas", when: (s) => s.hasSibling(-1), run: (s) => s.selectSibling(-1) },
|
|
83
|
+
{ id: "studio.select.siblings", label: "Select all", group: "Selection", keys: ["Mod-a"], scope: "canvas", when: (s) => (s.selectedKeys().length ? true : "Select something first."), run: (s) => s.selectSiblings() },
|
|
84
|
+
// Edit
|
|
85
|
+
{ id: "studio.cut", label: "Cut", group: "Edit", keys: ["Mod-x"], scope: "canvas", when: and(editable, blocksOrSlides("cut")), run: (s) => s.cut() },
|
|
86
|
+
{ id: "studio.copy", label: "Copy", group: "Edit", keys: ["Mod-c"], scope: "canvas", when: blocksOrSlides("copy"), run: (s) => s.copy() },
|
|
87
|
+
{ id: "studio.paste", label: "Paste", group: "Edit", keys: ["Mod-v"], scope: "canvas", when: and(editable, (s) => (s.hasClipboard() ? true : "The clipboard is empty.")), run: (s) => s.paste() },
|
|
88
|
+
{ id: "studio.duplicate", label: "Duplicate", group: "Edit", keys: ["Mod-d"], scope: "canvas", when: and(editable, blocksOrSlides("duplicate")), run: (s) => s.duplicate() },
|
|
89
|
+
{ id: "studio.delete", label: "Delete", group: "Edit", keys: ["Delete", "Backspace"], scope: "canvas", when: and(editable, blocksOrSlides("delete")), run: (s) => s.remove() },
|
|
90
|
+
{ id: "studio.undo", label: "Undo", group: "Edit", keys: ["Mod-z"], when: and((s) => (s.readOnly ? READ_ONLY : true), (s) => (s.canUndo() ? true : "Nothing to undo.")), run: (s) => s.undo() },
|
|
91
|
+
{ id: "studio.redo", label: "Redo", group: "Edit", keys: ["Shift-Mod-z", "Ctrl-y"], when: and((s) => (s.readOnly ? READ_ONLY : true), (s) => (s.canRedo() ? true : "Nothing to redo.")), run: (s) => s.redo() },
|
|
92
|
+
// Structure
|
|
93
|
+
{ id: "studio.move.earlier", label: "Move up", group: "Structure", keys: ["[", "Alt-ArrowUp", "Alt-ArrowLeft"], scope: "canvas", when: and(editable, blocks("move")), run: (s) => s.moveBy(-1) },
|
|
94
|
+
{ id: "studio.move.later", label: "Move down", group: "Structure", keys: ["]", "Alt-ArrowDown", "Alt-ArrowRight"], scope: "canvas", when: and(editable, blocks("move")), run: (s) => s.moveBy(1) },
|
|
95
|
+
{ id: "studio.move.intoPrevious", label: "Move into the group before", group: "Structure", keys: ["Shift-[", "Shift-{"], scope: "canvas", when: and(editable, blocks("move"), (s) => s.groupBeside(-1)), run: (s) => s.moveInto(-1) },
|
|
96
|
+
{ id: "studio.move.intoNext", label: "Move into the group after", group: "Structure", keys: ["Shift-]", "Shift-}"], scope: "canvas", when: and(editable, blocks("move"), (s) => s.groupBeside(1)), run: (s) => s.moveInto(1) },
|
|
97
|
+
{ id: "studio.group", label: "Group", group: "Structure", keys: ["Mod-g"], scope: "canvas", when: and(editable, blocks("group")), run: (s) => s.group() },
|
|
98
|
+
{ id: "studio.ungroup", label: "Ungroup", group: "Structure", keys: ["Shift-Mod-g"], scope: "canvas", when: and(editable, (s) => (s.primaryNode()?.kind === "group" && s.selectedKeys().length === 1 ? true : "Select a group to ungroup.")), run: (s) => s.ungroup() },
|
|
99
|
+
{ id: "studio.regions.place", label: "Place in regions…", group: "Structure", when: and(editable, (s) => s.canPlaceInRegions()), run: (s) => s.placeInRegions() },
|
|
100
|
+
{ id: "studio.regions.release", label: "Back to blocks", group: "Structure", when: and(editable, (s) => s.canReleaseRegions()), run: (s) => s.releaseRegions() },
|
|
101
|
+
// Content
|
|
102
|
+
{ id: "studio.text.edit", label: "Edit text", group: "Content", keys: ["F2"], scope: "canvas", when: and(editable, (s) => (s.textTarget() ? true : "Select text to edit it.")), run: (s) => s.editText() },
|
|
103
|
+
{ id: "studio.data.edit", label: "Edit data", group: "Content", when: and(editable, primaryKind(["chart", "table"], "Select a chart or a table.")), run: (s) => s.editData() },
|
|
104
|
+
{ id: "studio.image.crop", label: "Crop picture", group: "Content", when: and(editable, primaryKind(["image"], "Select a picture.")), run: (s) => s.cropImage() },
|
|
105
|
+
// Slides
|
|
106
|
+
{ id: "studio.slide.new", label: "New slide", group: "Slides", when: editable, run: (s) => s.newSlide() },
|
|
107
|
+
{ id: "studio.slide.duplicate", label: "Duplicate slide", group: "Slides", when: editable, run: (s) => s.duplicateSlide() },
|
|
108
|
+
{ id: "studio.slide.delete", label: "Delete slide", group: "Slides", when: and(editable, (s) => (s.slideCount() > 1 ? true : "A presentation keeps at least one slide.")), run: (s) => s.deleteSlide() },
|
|
109
|
+
{ id: "studio.slide.hide", label: "Hide slide", group: "Slides", when: editable, run: (s) => s.hideSlide() },
|
|
110
|
+
{ id: "studio.slide.next", label: "Next slide", group: "Slides", keys: ["PageDown"], when: (s) => (s.slideIndex < s.slideCount() - 1 ? true : "This is the last slide."), run: (s) => s.goSlide(1) },
|
|
111
|
+
{ id: "studio.slide.previous", label: "Previous slide", group: "Slides", keys: ["PageUp"], when: (s) => (s.slideIndex > 0 ? true : "This is the first slide."), run: (s) => s.goSlide(-1) },
|
|
112
|
+
{ id: "studio.layout.edit", label: "Edit layout", group: "Slides", when: and(editable, (s) => s.canEditLayout()), run: (s) => s.editLayout() },
|
|
113
|
+
// Deck
|
|
114
|
+
{ id: "studio.present", label: "Present from the start", group: "Deck", keys: ["F5"], when: (s) => (s.inSync ? true : SOURCE), run: (s) => s.present(true) },
|
|
115
|
+
{ id: "studio.present.current", label: "Present from this slide", group: "Deck", keys: ["Shift-F5", "Mod-Alt-Enter"], when: (s) => (s.inSync ? true : SOURCE), run: (s) => s.present(false) },
|
|
116
|
+
{ id: "studio.find", label: "Find", group: "Deck", hint: "", when: (s) => (s.hasFind ? true : "Find is not part of this editor."), run: (s) => s.find(false) },
|
|
117
|
+
{ id: "studio.replace", label: "Find and replace", group: "Deck", hint: "", when: and(editable, (s) => (s.hasFind ? true : "Find is not part of this editor.")), run: (s) => s.find(true) },
|
|
118
|
+
{ id: "studio.export", label: "Export…", group: "Deck", when: (s) => (s.hasExport ? (s.inSync ? true : SOURCE) : "Export is not part of this editor."), run: (s) => s.openExport() },
|
|
119
|
+
{ id: "studio.file.open", label: "Open…", group: "Deck", when: (s) => (s.readOnly ? READ_ONLY : true), run: (s) => s.openFile() },
|
|
120
|
+
{ id: "studio.file.new", label: "New presentation", group: "Deck", when: (s) => (s.readOnly ? READ_ONLY : true), run: (s) => s.newDocument() },
|
|
121
|
+
...STUDIO_DOWNLOAD_FORMATS.map((format) => ({ id: `studio.file.download.${format}`, label: `Download as ${format === "markdown" ? "Markdown" : format.toUpperCase()}`, group: "Deck", when: (s) => (s.inSync ? true : SOURCE), run: (s) => s.download(format) })),
|
|
122
|
+
{ id: "studio.file.copy", label: "Copy as OPF JSON", group: "Deck", when: (s) => (s.inSync ? true : SOURCE), run: (s) => s.copyDocument() },
|
|
123
|
+
{ id: "studio.shortcuts", label: "Keyboard shortcuts", group: "Deck", keys: ["?", "Shift-?", "F1", "Mod-/"], run: (s) => s.openShortcuts() },
|
|
124
|
+
];
|
|
125
|
+
for (const kind of STUDIO_INSERT_KINDS) {
|
|
126
|
+
list.push({ id: `studio.insert.${kind}`, label: `Insert ${STUDIO_INSERT_LABELS[kind].toLowerCase()}`, group: "Content", when: and(editable, playground, (s) => s.canInsert(kind)), run: (s) => s.insert(kind) });
|
|
127
|
+
}
|
|
128
|
+
// Convert to: one command per kind, enabled when the primary block converts to it.
|
|
129
|
+
for (const kind of STUDIO_INSERT_KINDS.filter((entry) => entry !== "group" && entry !== "video")) {
|
|
130
|
+
list.push({ id: `studio.convert.${kind}`, label: `Convert to ${STUDIO_INSERT_LABELS[kind].toLowerCase()}`, group: "Structure", when: and(editable, blocks("convert"), (s) => s.canConvert(kind)), run: (s) => s.convert(kind) });
|
|
131
|
+
}
|
|
132
|
+
return list;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The ids every Studio has (STUDIO-SPEC section 14), for the coverage test and a host palette. */
|
|
136
|
+
export const STUDIO_COMMAND_IDS = Object.freeze(studioCommandDefinitions().map((command) => command.id));
|
|
137
|
+
|
|
138
|
+
/** A registry with the Studio's commands, then `extra` host commands, with the host's `keymap` overrides (`{ id: keys | null }`). */
|
|
139
|
+
export function createStudioCommands({ extra = [], keymap } = {}) {
|
|
140
|
+
const registry = createCommandRegistry({ commands: studioCommandDefinitions() });
|
|
141
|
+
for (const command of extra) registry.register(command);
|
|
142
|
+
if (keymap) registry.setKeymap(keymap);
|
|
143
|
+
return registry;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The context menu of a selection (STUDIO-SPEC 4.7), as groups of command ids; the Studio adds the "Move into" and "Convert to" submenus. */
|
|
147
|
+
export function contextMenuGroups({ slide = false } = {}) {
|
|
148
|
+
const groups = [
|
|
149
|
+
["studio.cut", "studio.copy", "studio.paste", "studio.duplicate", "studio.delete"],
|
|
150
|
+
["studio.group", "studio.ungroup", "studio.move.earlier", "studio.move.later"],
|
|
151
|
+
["studio.text.edit", "studio.data.edit", "studio.image.crop"],
|
|
152
|
+
];
|
|
153
|
+
if (slide) groups.push(["studio.regions.place", "studio.regions.release"], ["studio.slide.new", "studio.slide.duplicate", "studio.slide.hide", "studio.layout.edit"]);
|
|
154
|
+
return groups;
|
|
155
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** CE-20 (internal): the Studio's Design panel, its sections chosen by the selection. */
|
|
2
|
+
import type { Catalog } from "@openpresentation/opf";
|
|
3
|
+
import type { KitDom } from "./studio-kit/dom.js";
|
|
4
|
+
import type { EditorSession } from "./index.js";
|
|
5
|
+
import type { SceneNode } from "./slide-scene.js";
|
|
6
|
+
|
|
7
|
+
export type StudioDesignMode = "deck" | "slide" | "group" | "region" | "promoted" | "heading" | "part" | "multi" | "block" | "image" | "chart" | "table" | "list";
|
|
8
|
+
export declare function designMode(nodes: SceneNode[]): StudioDesignMode;
|
|
9
|
+
export interface StudioDesign {
|
|
10
|
+
element: HTMLElement;
|
|
11
|
+
refresh(): void;
|
|
12
|
+
readonly mode: StudioDesignMode | null;
|
|
13
|
+
showSection(name: string): void;
|
|
14
|
+
destroy(): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function createStudioDesign(container: HTMLElement, options: { dom: KitDom; ui: EditorSession; studio: any; catalogs?: readonly Catalog[]; designOptions?: Record<string, unknown>; onAddAsset?: unknown }): StudioDesign;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// CE-20: the Studio's Design panel, bound to the selection (STUDIO-SPEC section 8). The existing `design-controls` sections are reused as
|
|
2
|
+
// they are, by name, in panels made once per arrangement and shown for the selection that needs them; the Studio adds a Layout row (with the
|
|
3
|
+
// layout's diagram, Change layout and Edit layout), the composition of a group, a promoted region or a slide (mode, columns, gap, padding and
|
|
4
|
+
// one weight per track, linked to the dividers), a region's read-only rules, the multiple-selection actions, speaker notes, and Advanced (the
|
|
5
|
+
// selection's path as a copyable chip, and the schema inspector, opened on demand). Every field commits one undoable change through the session.
|
|
6
|
+
// The panel shows options, not explanations: every option is a visible control with a short label, a hint is a tooltip, and an action that
|
|
7
|
+
// does not apply is shown disabled with its reason (and its shortcut when it has one) in its tooltip.
|
|
8
|
+
import { createDesignControls } from "./design-controls.js";
|
|
9
|
+
import { setActionState } from "./content-controls.js";
|
|
10
|
+
import { createChartOptionsPanel } from "./chart-options-panel.js";
|
|
11
|
+
import { createNumberingPanel } from "./numbering-panel.js";
|
|
12
|
+
import { buildLayoutDiagram } from "./layout-diagram.js";
|
|
13
|
+
import { listSwitchOptions } from "./switches.js";
|
|
14
|
+
import { dottedPath } from "./studio-canvas.js";
|
|
15
|
+
|
|
16
|
+
const MODES = [["auto", "Auto", "Layers"], ["grid", "Grid", "Table"], ["row", "Row", "Columns2"], ["column", "Column", "Rows2"]];
|
|
17
|
+
// Which existing sections each selection shows (STUDIO-SPEC 8), in order.
|
|
18
|
+
const ARRANGEMENTS = {
|
|
19
|
+
deck: ["look", "variant", "background", "header-footer", "brand", "info"],
|
|
20
|
+
slide: ["layout-options", "slide-content", "background"],
|
|
21
|
+
block: ["selection"],
|
|
22
|
+
image: ["selection", "image"],
|
|
23
|
+
table: ["selection", "table"],
|
|
24
|
+
};
|
|
25
|
+
// The selected block's structural actions, around the panel's own Group with the next block and Ungroup: [command, label, icon, role].
|
|
26
|
+
const STRUCTURE_BEFORE = [["studio.move.earlier", "Move up", "ArrowUp", "move-up"], ["studio.move.later", "Move down", "ArrowDown", "move-down"]];
|
|
27
|
+
const STRUCTURE_AFTER = [["studio.duplicate", "Duplicate", "Copy", "duplicate"], ["studio.delete", "Delete", "Trash2", "delete"]];
|
|
28
|
+
// Modes whose selection section (the arrange toolbar and the content type) comes before the Studio's own sections.
|
|
29
|
+
const SHARED_FIRST = new Set(["block", "list", "chart", "group", "promoted", "image", "table"]);
|
|
30
|
+
|
|
31
|
+
/** The arrangement a selection gets: deck, slide, group, region, multi, or a block kind. */
|
|
32
|
+
export function designMode(nodes) {
|
|
33
|
+
if (!nodes.length) return "deck";
|
|
34
|
+
if (nodes.length > 1) return "multi";
|
|
35
|
+
const node = nodes[0];
|
|
36
|
+
if (node.kind === "slide") return "slide";
|
|
37
|
+
if (node.kind === "group") return "group";
|
|
38
|
+
if (node.kind === "region") return node.virtual ? "region" : "promoted";
|
|
39
|
+
if (node.kind === "heading") return "heading";
|
|
40
|
+
if (node.kind === "part") return "part";
|
|
41
|
+
return ["image", "chart", "table", "list"].includes(node.type) ? node.type : "block";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Mount the panel in `container`. Returns `{ element, refresh, showSection, destroy }`. */
|
|
45
|
+
export function createStudioDesign(container, { dom, ui, studio, catalogs, designOptions = {}, onAddAsset }) {
|
|
46
|
+
const doc = container.ownerDocument;
|
|
47
|
+
const { el } = dom;
|
|
48
|
+
const p = dom.prefix;
|
|
49
|
+
const head = el("div", `${p}-design-head`);
|
|
50
|
+
const notice = el("p", `${p}-design-notice`, { role: "note" });
|
|
51
|
+
notice.hidden = true;
|
|
52
|
+
const own = el("div", `${p}-design-own`);
|
|
53
|
+
const shared = el("div", `${p}-design-shared`);
|
|
54
|
+
const advanced = el("details", `${p}-design-advanced`);
|
|
55
|
+
const advancedSummary = el("summary", "", { text: "Advanced" });
|
|
56
|
+
// The selection's path is internal: a chip under Advanced that copies it.
|
|
57
|
+
const pathChip = el("button", `${p}-path`, { type: "button", title: "Copy path" });
|
|
58
|
+
pathChip.addEventListener("click", () => {
|
|
59
|
+
const path = pathChip.dataset.path;
|
|
60
|
+
const clipboard = doc.defaultView?.navigator?.clipboard;
|
|
61
|
+
if (!path || !clipboard?.writeText) { studio.status("Copying is not available here."); return; }
|
|
62
|
+
clipboard.writeText(path).then(() => studio.status(`Copied ${path}`), () => studio.status("Could not copy the path."));
|
|
63
|
+
});
|
|
64
|
+
const advancedBody = el("div", `${p}-design-advanced-body`);
|
|
65
|
+
advanced.append(advancedSummary, pathChip, advancedBody);
|
|
66
|
+
const live = el("p", `${p}-design-live`, { role: "status", "aria-live": "polite" });
|
|
67
|
+
container.append(head, notice, own, shared, advanced, live);
|
|
68
|
+
const panels = new Map();
|
|
69
|
+
let chartPanel = null, numberingPanel = null, inspector = null, inspectorPath = null;
|
|
70
|
+
let mode = null;
|
|
71
|
+
let lastKey = null;
|
|
72
|
+
|
|
73
|
+
const selectedPath = () => {
|
|
74
|
+
const node = studio.primaryNode();
|
|
75
|
+
if (!node || node.kind === "slide" || node.kind === "region" && node.virtual) return undefined;
|
|
76
|
+
return dottedPath(node.path);
|
|
77
|
+
};
|
|
78
|
+
// One design-controls panel per arrangement, made the first time it is needed and kept (hidden) afterwards.
|
|
79
|
+
function panel(name) {
|
|
80
|
+
if (panels.has(name)) return panels.get(name);
|
|
81
|
+
const host = el("div", `${p}-design-panel`, { "data-arrangement": name });
|
|
82
|
+
shared.append(host);
|
|
83
|
+
const hostAction = ([id, label, icon, role]) => ({ role, label, icon, state: () => studio.commandState(id), run: () => studio.run(id) });
|
|
84
|
+
const controls = createDesignControls(host, {
|
|
85
|
+
...designOptions,
|
|
86
|
+
showPath: false,
|
|
87
|
+
...(name === "deck" || name === "slide" ? {} : { blockActions: { before: STRUCTURE_BEFORE.map(hostAction), after: STRUCTURE_AFTER.map(hostAction) } }),
|
|
88
|
+
editor: ui,
|
|
89
|
+
sections: ARRANGEMENTS[name],
|
|
90
|
+
scope: name === "deck" ? "deck" : "slide",
|
|
91
|
+
getSlideIndex: () => studio.slideIndex,
|
|
92
|
+
getSelectedPath: selectedPath,
|
|
93
|
+
catalogs,
|
|
94
|
+
...(onAddAsset ? { onAddAsset } : {}),
|
|
95
|
+
onSelectPath: (path) => studio.revealPath(path),
|
|
96
|
+
onStatus: (message) => studio.status(message),
|
|
97
|
+
});
|
|
98
|
+
const entry = { host, controls };
|
|
99
|
+
panels.set(name, entry);
|
|
100
|
+
return entry;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const plural = (count, one) => `${count} ${one}${count === 1 ? "" : "s"}`;
|
|
104
|
+
// A hint is the field's tooltip and the control's accessible description, never a paragraph under it.
|
|
105
|
+
const field = (label, control, help) => {
|
|
106
|
+
const wrap = el("div", `${p}-field`, { title: help });
|
|
107
|
+
const id = `${p}-f-${Math.random().toString(36).slice(2, 9)}`;
|
|
108
|
+
control.id ||= id;
|
|
109
|
+
if (help) control.title = help;
|
|
110
|
+
wrap.append(el("label", `${p}-field-label`, { for: control.id, text: label }), control);
|
|
111
|
+
return wrap;
|
|
112
|
+
};
|
|
113
|
+
/** An action of the Studio's command registry: an icon button (or a text button with `text`), disabled with its reason, with its shortcut in its tooltip. */
|
|
114
|
+
const commandButton = (id, label, icon, { text = false, className = text ? `${p}-action` : "opf-dc-tool" } = {}) => {
|
|
115
|
+
const button = dom.button(className, { ...(text ? { text: label } : { label }), iconName: icon, size: text ? 14 : 16, attributes: { "data-command": id } });
|
|
116
|
+
setActionState(button, { label, ...studio.commandState(id) });
|
|
117
|
+
button.addEventListener("click", () => { if (button.getAttribute("aria-disabled") !== "true") studio.run(id); });
|
|
118
|
+
return button;
|
|
119
|
+
};
|
|
120
|
+
const toolbar = (label, buttons) => { const row = el("div", "opf-dc-toolbar", { role: "group", "aria-label": label }); row.append(...buttons); return row; };
|
|
121
|
+
const section = (title) => { const node = el("section", `${p}-section`); node.append(el("h3", `${p}-section-title`, { text: title })); return node; };
|
|
122
|
+
// A number field: the arrow keys step it (Shift for 10 steps), a drag on its label scrubs it (as in Figma), Enter or leaving commits it.
|
|
123
|
+
function numberField(label, value, { min, max, step = 1, onCommit, help, name }) {
|
|
124
|
+
const input = el("input", `${p}-number`, { type: "number", inputmode: "decimal", min, max, step, value: value ?? "", "data-field": name, placeholder: "auto" });
|
|
125
|
+
let base = input.value;
|
|
126
|
+
const commit = () => { if (input.value === base) return; base = input.value; onCommit(input.value === "" ? undefined : Number(input.value)); };
|
|
127
|
+
input.addEventListener("change", commit);
|
|
128
|
+
input.addEventListener("keydown", (event) => {
|
|
129
|
+
if (event.key === "Enter") { event.preventDefault(); commit(); }
|
|
130
|
+
if ((event.key === "ArrowUp" || event.key === "ArrowDown") && event.shiftKey) {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
const next = Math.max(min ?? -Infinity, Math.min(max ?? Infinity, Number(input.value || 0) + (event.key === "ArrowUp" ? 10 : -10) * step));
|
|
133
|
+
input.value = String(Math.round(next * 1000) / 1000);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const wrap = field(label, input, help);
|
|
137
|
+
const caption = wrap.querySelector("label");
|
|
138
|
+
caption.classList.add(`${p}-scrub`);
|
|
139
|
+
caption.addEventListener("pointerdown", (event) => {
|
|
140
|
+
if (event.button !== 0 || studio.readOnly) return;
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
const start = { x: event.clientX, value: Number(input.value || 0) };
|
|
143
|
+
try { caption.setPointerCapture?.(event.pointerId); } catch { /* a pointer that cannot be captured (a synthetic one) */ }
|
|
144
|
+
const move = (next) => {
|
|
145
|
+
const delta = Math.round((next.clientX - start.x) / 4) * step;
|
|
146
|
+
input.value = String(Math.max(min ?? -Infinity, Math.min(max ?? Infinity, Math.round((start.value + delta) * 1000) / 1000)));
|
|
147
|
+
};
|
|
148
|
+
const up = () => { caption.removeEventListener("pointermove", move); caption.removeEventListener("pointerup", up); commit(); };
|
|
149
|
+
caption.addEventListener("pointermove", move);
|
|
150
|
+
caption.addEventListener("pointerup", up);
|
|
151
|
+
});
|
|
152
|
+
return wrap;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- the composition of a container ---------------------------------------------------------------------------------------------------------
|
|
156
|
+
function compositionSection(node) {
|
|
157
|
+
const body = section(node.kind === "slide" ? "Slide composition" : "Composition");
|
|
158
|
+
const pointer = `${node.path}/composition`;
|
|
159
|
+
const current = ui.get(pointer) ?? {};
|
|
160
|
+
const container = node.container ?? {};
|
|
161
|
+
const write = (next, label) => {
|
|
162
|
+
const clean = Object.fromEntries(Object.entries(next).filter(([, value]) => value !== undefined));
|
|
163
|
+
studio.writeComposition(node, Object.keys(clean).length ? clean : undefined, label);
|
|
164
|
+
};
|
|
165
|
+
const mode = current.mode ?? "auto";
|
|
166
|
+
const segmented = el("div", `${p}-segmented`, { role: "group", "aria-label": "Arrangement" });
|
|
167
|
+
for (const [value, text, icon] of MODES) {
|
|
168
|
+
const button = dom.button(`${p}-segment`, { label: text, iconName: icon, size: 16, pressed: value === mode, attributes: { "data-field": `mode-${value}` } });
|
|
169
|
+
button.addEventListener("click", () => {
|
|
170
|
+
if (value === mode) return;
|
|
171
|
+
const next = { ...current, mode: value };
|
|
172
|
+
if (value !== "grid") delete next.columns;
|
|
173
|
+
// Weights belong to the tracks of one arrangement; a new arrangement starts equal.
|
|
174
|
+
delete next.weights;
|
|
175
|
+
write(next, `Arrangement set to ${text}`);
|
|
176
|
+
});
|
|
177
|
+
segmented.append(button);
|
|
178
|
+
}
|
|
179
|
+
const modeWrap = el("div", `${p}-field`);
|
|
180
|
+
modeWrap.append(el("span", `${p}-field-label`, { text: "Arrangement" }), segmented);
|
|
181
|
+
body.append(modeWrap);
|
|
182
|
+
if (mode === "grid") body.append(numberField("Columns", current.columns ?? container.columns, { min: 1, max: 6, name: "columns", onCommit: (value) => write({ ...current, columns: value, weights: undefined }, `Columns set to ${value ?? "auto"}`) }));
|
|
183
|
+
body.append(
|
|
184
|
+
numberField("Gap", current.gap, { min: 0, max: 0.5, step: 0.01, name: "gap", help: "Share of the container's shorter side (0 to 0.5)", onCommit: (value) => write({ ...current, gap: value }, `Gap set to ${value ?? "auto"}`) }),
|
|
185
|
+
numberField("Padding", current.padding, { min: 0, max: 0.4, step: 0.01, name: "padding", help: "Share of the container's shorter side (0 to 0.4)", onCommit: (value) => write({ ...current, padding: value }, `Padding set to ${value ?? "none"}`) }),
|
|
186
|
+
);
|
|
187
|
+
// Track weights exist in Row, Column and Grid; Auto has none to set.
|
|
188
|
+
if (mode !== "auto" && container.tracks?.length >= 2) {
|
|
189
|
+
const weights = el("div", `${p}-weights`, { role: "group", "aria-label": "Track weights" });
|
|
190
|
+
const tracks = container.tracks;
|
|
191
|
+
const values = tracks.map((track, index) => current.weights?.[index] ?? track.weight ?? 1);
|
|
192
|
+
tracks.forEach((_, index) => {
|
|
193
|
+
weights.append(numberField(`${container.direction === "column" ? "Row" : "Column"} ${index + 1}`, values[index], {
|
|
194
|
+
min: 0.1, max: 100, step: 0.5, name: `weight-${index}`,
|
|
195
|
+
onCommit: (value) => { const next = [...values]; next[index] = value ?? 1; studio.setWeights(node.key, next); },
|
|
196
|
+
}));
|
|
197
|
+
});
|
|
198
|
+
body.append(weights);
|
|
199
|
+
}
|
|
200
|
+
return body;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- the slide's layout row -------------------------------------------------------------------------------------------------------------
|
|
204
|
+
function layoutSection(node) {
|
|
205
|
+
const body = section("Layout");
|
|
206
|
+
const slide = ui.get(node.path) ?? {};
|
|
207
|
+
const id = typeof slide.layout === "string" ? slide.layout : undefined;
|
|
208
|
+
const choice = id ? listSwitchOptions(ui.presentation, "layouts", { catalogs: studio.catalogs() }).find((entry) => entry.id === id) : undefined;
|
|
209
|
+
const row = el("div", `${p}-layout-row`);
|
|
210
|
+
if (choice?.record) row.append(buildLayoutDiagram(doc, choice.record));
|
|
211
|
+
row.append(el("span", `${p}-layout-name`, { text: choice?.label ?? id ?? "Automatic (no layout)" }));
|
|
212
|
+
body.append(row);
|
|
213
|
+
const actions = el("div", `${p}-actions`);
|
|
214
|
+
const change = dom.button(`${p}-action`, { text: "Change layout", iconName: "Columns2", size: 14, attributes: { "aria-haspopup": "menu" } });
|
|
215
|
+
change.addEventListener("click", () => studio.openSlideMenu(change, { apply: true }));
|
|
216
|
+
actions.append(change, commandButton("studio.layout.edit", "Edit layout", "Pencil", { text: true }));
|
|
217
|
+
body.append(actions);
|
|
218
|
+
return body;
|
|
219
|
+
}
|
|
220
|
+
function regionSection(node) {
|
|
221
|
+
const body = section(node.label);
|
|
222
|
+
const rules = el("dl", `${p}-rules`);
|
|
223
|
+
const add = (term, value) => rules.append(el("dt", "", { text: term }), el("dd", "", { text: value }));
|
|
224
|
+
add("Accepts", node.accepts?.length ? node.accepts.join(", ") : "Any content");
|
|
225
|
+
add("Most blocks", node.max ? String(node.max) : "No limit");
|
|
226
|
+
add("Holds now", plural(node.children.length, "block"));
|
|
227
|
+
const edit = commandButton("studio.layout.edit", "Edit layout", "Pencil", { text: true });
|
|
228
|
+
// A region's rules belong to the layout: the tooltip says where they change.
|
|
229
|
+
if (edit.getAttribute("aria-disabled") !== "true") edit.title = "Edit layout: the region's rules come from it";
|
|
230
|
+
body.append(rules, edit);
|
|
231
|
+
return body;
|
|
232
|
+
}
|
|
233
|
+
function notesSection(node) {
|
|
234
|
+
const body = section("Notes");
|
|
235
|
+
const input = el("textarea", `${p}-notes`, { rows: 4, "data-field": "notes", placeholder: "Speaker notes" });
|
|
236
|
+
const pointer = `${node.path}/notes`;
|
|
237
|
+
const value = ui.get(pointer);
|
|
238
|
+
input.value = typeof value === "string" ? value : "";
|
|
239
|
+
let base = input.value;
|
|
240
|
+
input.addEventListener("change", () => {
|
|
241
|
+
if (input.value === base) return;
|
|
242
|
+
base = input.value;
|
|
243
|
+
studio.writeValue(pointer, input.value === "" ? undefined : input.value, "Notes changed");
|
|
244
|
+
});
|
|
245
|
+
body.append(field("Speaker notes", input));
|
|
246
|
+
return body;
|
|
247
|
+
}
|
|
248
|
+
function multiSection(nodes) {
|
|
249
|
+
const body = section(plural(nodes.length, "block"));
|
|
250
|
+
body.append(toolbar("Arrange", [["studio.group", "Group", "Group"], ["studio.duplicate", "Duplicate", "Copy"], ["studio.delete", "Delete", "Trash2"]].map(([id, label, icon]) => commandButton(id, label, icon))));
|
|
251
|
+
return body;
|
|
252
|
+
}
|
|
253
|
+
function dataSection(node) {
|
|
254
|
+
const body = section("Data");
|
|
255
|
+
const button = commandButton("studio.data.edit", "Edit data", "Table", { text: true });
|
|
256
|
+
if (button.getAttribute("aria-disabled") !== "true") button.title = `Edit the ${node.type === "chart" ? "chart's" : "table's"} values under the slide`;
|
|
257
|
+
body.append(button);
|
|
258
|
+
return body;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Show the sections for the selection now. */
|
|
262
|
+
function refresh() {
|
|
263
|
+
const nodes = studio.selectedNodes();
|
|
264
|
+
const next = designMode(nodes);
|
|
265
|
+
const node = nodes[0];
|
|
266
|
+
const key = nodes.map((entry) => entry.key).join("|");
|
|
267
|
+
// Typing in one of the Studio's own fields keeps it: only a selection change rebuilds them while focus is inside.
|
|
268
|
+
const typing = own.contains(doc.activeElement) && key === lastKey && next === mode;
|
|
269
|
+
mode = next;
|
|
270
|
+
lastKey = key;
|
|
271
|
+
container.dataset.mode = next;
|
|
272
|
+
head.replaceChildren(el("h2", `${p}-design-title`, { text: next === "deck" ? "Presentation" : next === "multi" ? plural(nodes.length, "block") : node.kind === "slide" ? studio.slideLabel(node) : node.label }));
|
|
273
|
+
notice.hidden = !(studio.readOnly || !studio.inSync);
|
|
274
|
+
notice.textContent = studio.readOnly ? "Read-only" : !studio.inSync ? "Source has errors: showing the last valid version" : "";
|
|
275
|
+
container.toggleAttribute("inert", studio.readOnly || !studio.inSync);
|
|
276
|
+
if (!typing) {
|
|
277
|
+
own.replaceChildren();
|
|
278
|
+
if (next === "slide") {
|
|
279
|
+
own.append(layoutSection(node));
|
|
280
|
+
if (!studio.slideUsesLayout() && node.container && !node.container.promoted) own.append(compositionSection(node));
|
|
281
|
+
} else if (next === "group" || next === "promoted") own.append(compositionSection(node));
|
|
282
|
+
else if (next === "region") own.append(regionSection(node));
|
|
283
|
+
else if (next === "multi") own.append(multiSection(nodes));
|
|
284
|
+
else if (next === "chart" || next === "table") own.append(dataSection(node));
|
|
285
|
+
else if (next === "heading") own.append(toolbar("Heading", [commandButton("studio.text.edit", "Edit text", "Pencil", { text: true })]));
|
|
286
|
+
else if (next === "part") own.append(toolbar("Part", [commandButton("studio.select.parent", "Select block", "MousePointer2", { text: true })]));
|
|
287
|
+
}
|
|
288
|
+
// The arrange toolbar and the content type come first for a block; a slide's layout and the deck's look come before the shared sections.
|
|
289
|
+
const sharedFirst = SHARED_FIRST.has(next);
|
|
290
|
+
// 4 is DOCUMENT_POSITION_FOLLOWING: `own` comes after `shared`.
|
|
291
|
+
if (sharedFirst !== Boolean(shared.compareDocumentPosition(own) & 4)) {
|
|
292
|
+
if (sharedFirst) container.insertBefore(shared, own);
|
|
293
|
+
else container.insertBefore(own, shared);
|
|
294
|
+
}
|
|
295
|
+
const arrangement = next === "deck" ? "deck" : next === "slide" ? "slide" : next === "image" ? "image" : next === "table" ? "table" : ["block", "list", "chart", "group", "promoted"].includes(next) ? "block" : null;
|
|
296
|
+
for (const [name, entry] of panels) entry.host.hidden = name !== arrangement;
|
|
297
|
+
if (arrangement) { const entry = panel(arrangement); entry.host.hidden = false; entry.controls.refresh(); }
|
|
298
|
+
// The chart's options and a list's numbering follow the selection in their own panels.
|
|
299
|
+
if (next === "chart") {
|
|
300
|
+
chartPanel ??= createChartOptionsPanel(el("div", `${p}-design-chart`), { editor: ui, getSelectedPath: selectedPath, onStatus: (message) => studio.status(message) });
|
|
301
|
+
own.append(chartPanel.element.parentElement ?? chartPanel.element);
|
|
302
|
+
chartPanel.refresh();
|
|
303
|
+
}
|
|
304
|
+
if (next === "list") {
|
|
305
|
+
numberingPanel ??= createNumberingPanel(el("div", `${p}-design-numbering`), { editor: ui, getTarget: selectedPath, getSlideIndex: () => studio.slideIndex, onStatus: (message) => studio.status(message) });
|
|
306
|
+
own.append(numberingPanel.element.parentElement ?? numberingPanel.element);
|
|
307
|
+
numberingPanel.setTarget();
|
|
308
|
+
}
|
|
309
|
+
if (next === "slide" && !typing) own.append(notesSection(node));
|
|
310
|
+
// Advanced: the node's other fields through the schema inspector, made when opened.
|
|
311
|
+
const path = node && node.kind !== "region" ? dottedPath(node.path) : "";
|
|
312
|
+
advanced.hidden = next === "multi" || next === "region";
|
|
313
|
+
pathChip.hidden = !path;
|
|
314
|
+
pathChip.dataset.path = path;
|
|
315
|
+
pathChip.setAttribute("aria-label", `Copy path ${path}`);
|
|
316
|
+
pathChip.replaceChildren(el("code", "", { text: path }), dom.icon("Copy", 14));
|
|
317
|
+
if (advanced.open && path !== inspectorPath) mountInspector(path);
|
|
318
|
+
}
|
|
319
|
+
async function mountInspector(path) {
|
|
320
|
+
inspectorPath = path;
|
|
321
|
+
inspector?.destroy();
|
|
322
|
+
inspector = null;
|
|
323
|
+
advancedBody.replaceChildren();
|
|
324
|
+
const { createSchemaInspector } = await import("./schema-inspector.js");
|
|
325
|
+
if (!advanced.open || inspectorPath !== path) return;
|
|
326
|
+
inspector = createSchemaInspector(advancedBody, { editor: ui, path: path || undefined, onError: (error) => studio.status(error.message) });
|
|
327
|
+
}
|
|
328
|
+
advanced.addEventListener("toggle", () => {
|
|
329
|
+
if (advanced.open) {
|
|
330
|
+
const node = studio.primaryNode();
|
|
331
|
+
mountInspector(node && node.kind !== "region" ? dottedPath(node.path) : "");
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
element: container,
|
|
337
|
+
refresh,
|
|
338
|
+
get mode() { return mode; },
|
|
339
|
+
/** Scroll a section into view (the layers' Background row). */
|
|
340
|
+
showSection(name) {
|
|
341
|
+
const entry = panels.get(mode === "slide" ? "slide" : "deck");
|
|
342
|
+
const target = entry?.host.querySelector(`[data-section="${name}"], details:has([data-section="${name}"])`) ?? entry?.host;
|
|
343
|
+
target?.scrollIntoView?.({ block: "start" });
|
|
344
|
+
if (target?.tagName === "DETAILS") target.open = true;
|
|
345
|
+
},
|
|
346
|
+
destroy() {
|
|
347
|
+
for (const entry of panels.values()) entry.controls.destroy();
|
|
348
|
+
chartPanel?.destroy();
|
|
349
|
+
numberingPanel?.destroy();
|
|
350
|
+
inspector?.destroy();
|
|
351
|
+
container.replaceChildren();
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
package/dist/studio-kit/dom.d.ts
CHANGED
|
@@ -31,6 +31,22 @@ export interface KitMenuItem {
|
|
|
31
31
|
role?: "menuitem" | "menuitemcheckbox";
|
|
32
32
|
checked?: boolean;
|
|
33
33
|
hint?: string;
|
|
34
|
+
/** CE-20: a Lucide icon name drawn before the label. */
|
|
35
|
+
icon?: string;
|
|
36
|
+
/** CE-20: an element drawn before the label instead of an icon (a layout diagram). */
|
|
37
|
+
media?: HTMLElement;
|
|
38
|
+
/** CE-20: a second, quieter line under the label. */
|
|
39
|
+
description?: string;
|
|
40
|
+
/** CE-20: the item's title (why it is disabled). */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** CE-20: `aria-keyshortcuts`. */
|
|
43
|
+
keyshortcuts?: string;
|
|
44
|
+
/** CE-20: consecutive items with the same group sit in a `role="group"` under a visible heading. */
|
|
45
|
+
group?: string;
|
|
46
|
+
/** CE-20: a separator instead of an item. */
|
|
47
|
+
separator?: boolean;
|
|
48
|
+
/** CE-20: a press on the item (an item that can be dragged out of the menu); `close()` closes the menu without restoring focus. */
|
|
49
|
+
pointerdown?(event: PointerEvent, item: KitMenuItem, menu: { close(): void }): void;
|
|
34
50
|
}
|
|
35
51
|
export interface KitMenu { element: HTMLElement; close(options?: { restore?: boolean }): void }
|
|
36
52
|
export declare function openMenu(dom: KitDom, host: HTMLElement, options: { x: number; y: number; items: KitMenuItem[]; label: string; returnFocus?: HTMLElement | null; onClose?(): void; className?: string }): KitMenu;
|