@openpresentation/opf-editor 0.29.1 → 0.31.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 +19 -0
- package/dist/deck-title.d.ts +59 -0
- package/dist/deck-title.js +184 -0
- package/dist/design-menu.d.ts +90 -0
- package/dist/design-menu.js +462 -0
- package/dist/editor-expand.d.ts +102 -0
- package/dist/editor-expand.js +370 -0
- package/dist/element-define.d.ts +2 -0
- package/dist/element-define.js +7 -0
- package/dist/element.d.ts +94 -0
- package/dist/element.js +585 -0
- package/dist/icons.d.ts +34 -0
- package/dist/icons.js +88 -0
- package/dist/presenter.d.ts +3 -0
- package/dist/presenter.js +6 -2
- package/dist/react.d.ts +9 -0
- package/dist/react.js +18 -0
- package/dist/review-pill.d.ts +83 -0
- package/dist/review-pill.js +137 -0
- package/dist/shortcuts-sheet.d.ts +82 -0
- package/dist/shortcuts-sheet.js +230 -0
- package/dist/slide-navigator.d.ts +3 -0
- package/dist/slide-navigator.js +6 -17
- package/dist/studio.js +13 -16
- package/dist/styles.css +901 -36
- package/dist/styles.layered.css +7782 -0
- package/dist/svelte.d.ts +16 -0
- package/dist/svelte.js +32 -0
- package/dist/thumbnail-strip.d.ts +59 -0
- package/dist/thumbnail-strip.js +399 -6
- package/dist/toolbar-actions.d.ts +59 -0
- package/dist/toolbar-actions.js +136 -0
- package/dist/toolbar.d.ts +113 -0
- package/dist/toolbar.js +398 -0
- package/dist/workspace-parts.js +8 -0
- package/dist/workspace-shell.js +93 -33
- package/dist/workspace-theme.js +11 -0
- package/dist/workspace.d.ts +143 -7
- package/dist/workspace.js +603 -27
- package/package.json +57 -9
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// CE-29, the help item: a button (and the "?" key) that opens the keyboard shortcuts sheet, a modal dialog listing every shortcut the host passes
|
|
2
|
+
// in, grouped and filterable by typing. The rows, the classes and the look are the source pane's shortcuts dialog (source-shortcuts.js,
|
|
3
|
+
// styles/shortcuts.css); the key names come from the studio kit's `formatKey` (⌘ on a Mac, Ctrl elsewhere) so the three sheets agree. Plain DOM,
|
|
4
|
+
// no framework, no network. The icon is the Lucide CircleHelp path (ISC license), inlined.
|
|
5
|
+
import { normalizeIcons, resolveIcon } from "./icons.js";
|
|
6
|
+
import { matches } from "./source-shortcuts.js";
|
|
7
|
+
import { formatKey, isHandled, markHandled } from "./studio-kit/commands.js";
|
|
8
|
+
import { isMac, isTypingTarget } from "./studio-kit/dom.js";
|
|
9
|
+
|
|
10
|
+
const ICON_NAMES = ["help", "close"];
|
|
11
|
+
// The help glyph is Lucide CircleHelp: a ring, a question mark and a dot.
|
|
12
|
+
const HELP_ICON = [["circle", { cx: 12, cy: 12, r: 10 }], ["path", { d: "M9.09 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3" }], ["path", { d: "M12 17h.01" }]];
|
|
13
|
+
const FOCUSABLE = 'button, input, [tabindex]:not([tabindex="-1"])';
|
|
14
|
+
let counter = 0;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_LABELS = {
|
|
17
|
+
/** The accessible name of the toolbar button. */
|
|
18
|
+
button: "Keyboard shortcuts",
|
|
19
|
+
/** The tooltip of the toolbar button (the key is added when it is bound). */
|
|
20
|
+
title: "Keyboard shortcuts",
|
|
21
|
+
/** The heading of the dialog. */
|
|
22
|
+
heading: "Keyboard shortcuts",
|
|
23
|
+
close: "Close",
|
|
24
|
+
/** The accessible name of the filter field. */
|
|
25
|
+
search: "Filter the keyboard shortcuts",
|
|
26
|
+
searchPlaceholder: "Filter shortcuts",
|
|
27
|
+
/** The group of a shortcut that names none. */
|
|
28
|
+
defaultGroup: "General",
|
|
29
|
+
none: "No shortcut matches.",
|
|
30
|
+
empty: "No keyboard shortcuts are available.",
|
|
31
|
+
/** The sentence under the heading. `platform` is "macOS" or "Windows or Linux". */
|
|
32
|
+
description: (platform) => `Keys for ${platform}. Type to filter; Escape closes.`,
|
|
33
|
+
/** The live status line: how many shortcuts show. */
|
|
34
|
+
status: (shown, total) => `${shown === total ? "" : `${shown} of `}${total} ${total === 1 ? "shortcut" : "shortcuts"}${shown === total ? "" : " match"}`,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The strokes of one shortcut as people read them, one string per alternative: `["Mod-z", "Shift-Mod-z"]` becomes `["⌘Z", "⇧⌘Z"]` on a Mac and
|
|
39
|
+
* `["Ctrl+Z", "Ctrl+Shift+Z"]` elsewhere. A key with several strokes ("Mod-k Mod-s") keeps them separated by a space. A name that does not parse
|
|
40
|
+
* is shown as it is.
|
|
41
|
+
*/
|
|
42
|
+
export function formatShortcutKeys(keys, win) {
|
|
43
|
+
return (Array.isArray(keys) ? keys : [keys]).filter((key) => typeof key === "string" && key).map((key) => key.split(" ").map((stroke) => {
|
|
44
|
+
try { return formatKey(stroke, win); } catch { return stroke; }
|
|
45
|
+
}).join(" "));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The sheet's rows from the host's `getShortcuts()`: `{ id, title, group, keys }`, in the order given, with the keys formatted for `win`. */
|
|
49
|
+
export function shortcutRows(shortcuts, win, defaultGroup = DEFAULT_LABELS.defaultGroup) {
|
|
50
|
+
return (Array.isArray(shortcuts) ? shortcuts : []).filter((item) => item && typeof item.label === "string").map((item, index) => ({
|
|
51
|
+
id: item.id ?? `shortcut-${index}`,
|
|
52
|
+
title: item.label,
|
|
53
|
+
group: item.group || defaultGroup,
|
|
54
|
+
keys: formatShortcutKeys(item.keys, win),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Rows that match `query` (every word, in the label, group or keys; the source pane's rule), in their original order. */
|
|
59
|
+
export function filterShortcutRows(rows, query) {
|
|
60
|
+
return query.trim() ? rows.filter((row) => matches(row, query)) : rows;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether a "?" key press should open the sheet: a bare "?" (with or without Shift), not typed into a field, an editor, a menu or a dialog, not handled by another part. */
|
|
64
|
+
export function opensOnKey(event) {
|
|
65
|
+
if (event.key !== "?" || event.ctrlKey || event.metaKey || event.altKey || event.isComposing || event.defaultPrevented || isHandled(event)) return false;
|
|
66
|
+
const target = event.target;
|
|
67
|
+
if (isTypingTarget(target)) return false;
|
|
68
|
+
if (target && typeof target.closest === "function" && target.closest('.cm-editor,[contenteditable=""],[contenteditable="true"],[role="textbox"],[role="combobox"],[role="menu"],[role="dialog"],dialog[open]')) return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function node(doc, tag, className, attributes = {}, text) {
|
|
73
|
+
const made = doc.createElement(tag);
|
|
74
|
+
if (className) made.className = className;
|
|
75
|
+
for (const [key, value] of Object.entries(attributes)) made.setAttribute(key, value);
|
|
76
|
+
if (text !== undefined) made.textContent = text;
|
|
77
|
+
return made;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Mount the help button into `container`. Pressing it (or "?" / "Shift-?" while focus is not in a field) opens the shortcuts sheet in `overlay`.
|
|
82
|
+
*
|
|
83
|
+
* Options: `overlay` (the element the dialog is added to; default the closest `.opf-ui` ancestor, else the document body), `getShortcuts()` (called on
|
|
84
|
+
* every open; returns `[{ label, keys: ["Mod-z", "Shift-Mod-z"], group? }]` with CodeMirror-style key names), `density`, `labels`, `bindKey`
|
|
85
|
+
* (default true: bind "?" on the document).
|
|
86
|
+
*
|
|
87
|
+
* Returns `{ element, open(), close(), setDensity(density), setDisabled(disabled), menuItems(), destroy() }`.
|
|
88
|
+
*/
|
|
89
|
+
export function createHelpButton(container, options = {}) {
|
|
90
|
+
if (!container?.ownerDocument) throw new TypeError("A DOM container is required.");
|
|
91
|
+
const doc = container.ownerDocument;
|
|
92
|
+
const win = doc.defaultView;
|
|
93
|
+
const labels = { ...DEFAULT_LABELS, ...(options.labels ?? {}) };
|
|
94
|
+
const bindKey = options.bindKey !== false;
|
|
95
|
+
const icons = normalizeIcons(options.icons, ICON_NAMES, "createHelpButton icons");
|
|
96
|
+
let density = options.density === "touch" ? "touch" : "comfortable";
|
|
97
|
+
let disabled = false;
|
|
98
|
+
let destroyed = false;
|
|
99
|
+
let sheet = null;
|
|
100
|
+
|
|
101
|
+
const element = node(doc, "button", "opf-help-button", { type: "button", "aria-label": labels.button, "aria-haspopup": "dialog", "aria-expanded": "false" });
|
|
102
|
+
element.title = bindKey ? `${labels.title} (?)` : labels.title;
|
|
103
|
+
element.dataset.density = density;
|
|
104
|
+
element.append(resolveIcon(doc, icons.help, HELP_ICON, { name: "help", className: "opf-help-icon" }));
|
|
105
|
+
container.append(element);
|
|
106
|
+
|
|
107
|
+
const overlayHost = () => options.overlay ?? container.closest?.(".opf-ui") ?? doc.body;
|
|
108
|
+
|
|
109
|
+
function open() {
|
|
110
|
+
if (destroyed || disabled) return false;
|
|
111
|
+
if (sheet) { sheet.focus(); return true; }
|
|
112
|
+
const host = overlayHost();
|
|
113
|
+
const opener = doc.activeElement && doc.activeElement !== doc.body ? doc.activeElement : element;
|
|
114
|
+
const rows = shortcutRows(options.getShortcuts?.(), win, labels.defaultGroup);
|
|
115
|
+
const id = `opf-help-${++counter}`;
|
|
116
|
+
const root = node(doc, "div", `opf-shortcuts opf-help-sheet${host === doc.body || !host.closest?.(".opf-ui") ? " opf-ui" : ""}`);
|
|
117
|
+
root.dataset.density = density;
|
|
118
|
+
const dialog = node(doc, "div", "opf-shortcuts-dialog", { role: "dialog", "aria-modal": "true", "aria-labelledby": `${id}-title`, "aria-describedby": `${id}-help`, tabindex: "-1" });
|
|
119
|
+
const head = node(doc, "div", "opf-shortcuts-head");
|
|
120
|
+
const closeButton = node(doc, "button", "opf-shortcuts-close", { type: "button", "aria-label": labels.close }, "×");
|
|
121
|
+
// The close button reads "×" unless the host gives a `close` icon.
|
|
122
|
+
const closeIcon = resolveIcon(doc, icons.close, null, { name: "close", className: "opf-help-close-icon", size: 16 });
|
|
123
|
+
if (closeIcon) closeButton.replaceChildren(closeIcon);
|
|
124
|
+
head.append(node(doc, "h2", "opf-shortcuts-title", { id: `${id}-title` }, labels.heading), closeButton);
|
|
125
|
+
const help = node(doc, "p", "opf-shortcuts-help", { id: `${id}-help` }, labels.description(isMac(win) ? "macOS" : "Windows or Linux"));
|
|
126
|
+
const search = node(doc, "input", "opf-shortcuts-search", { type: "search", "aria-label": labels.search, placeholder: labels.searchPlaceholder, autocomplete: "off", spellcheck: "false" });
|
|
127
|
+
const status = node(doc, "div", "opf-shortcuts-status", { role: "status", "aria-live": "polite" });
|
|
128
|
+
const list = node(doc, "div", "opf-shortcuts-list", { tabindex: "0", role: "region", "aria-label": labels.heading });
|
|
129
|
+
dialog.append(head, help, search, status, list);
|
|
130
|
+
root.append(dialog);
|
|
131
|
+
host.append(root);
|
|
132
|
+
|
|
133
|
+
function render() {
|
|
134
|
+
const shown = filterShortcutRows(rows, search.value);
|
|
135
|
+
list.replaceChildren();
|
|
136
|
+
let group = null, section = null;
|
|
137
|
+
for (const row of shown) {
|
|
138
|
+
if (row.group !== group) {
|
|
139
|
+
group = row.group;
|
|
140
|
+
section = node(doc, "section", "opf-shortcuts-group", { "aria-label": group });
|
|
141
|
+
section.append(node(doc, "h3", "opf-shortcuts-group-title", {}, group));
|
|
142
|
+
list.append(section);
|
|
143
|
+
}
|
|
144
|
+
const item = node(doc, "div", "opf-shortcuts-row", { "data-id": row.id });
|
|
145
|
+
const keys = node(doc, "span", "opf-shortcuts-keys");
|
|
146
|
+
row.keys.forEach((text, index) => {
|
|
147
|
+
if (index) keys.append(node(doc, "span", "opf-shortcuts-or", {}, "or"));
|
|
148
|
+
for (const stroke of text.split(" ")) keys.append(node(doc, "kbd", "opf-shortcuts-key", {}, stroke));
|
|
149
|
+
});
|
|
150
|
+
item.append(node(doc, "span", "opf-shortcuts-name", {}, row.title), keys);
|
|
151
|
+
section.append(item);
|
|
152
|
+
}
|
|
153
|
+
if (!shown.length) list.append(node(doc, "p", "opf-shortcuts-none", {}, rows.length ? labels.none : labels.empty));
|
|
154
|
+
status.textContent = labels.status(shown.length, rows.length);
|
|
155
|
+
}
|
|
156
|
+
const focusables = () => [...dialog.querySelectorAll(FOCUSABLE)].filter((item) => item.getClientRects().length);
|
|
157
|
+
function onKeydown(event) {
|
|
158
|
+
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); return; }
|
|
159
|
+
if (event.key !== "Tab") return;
|
|
160
|
+
const items = focusables();
|
|
161
|
+
if (!items.length) { event.preventDefault(); dialog.focus(); return; }
|
|
162
|
+
const first = items[0], last = items.at(-1), active = doc.activeElement;
|
|
163
|
+
if (event.shiftKey && (active === first || active === dialog)) { event.preventDefault(); last.focus(); }
|
|
164
|
+
else if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); }
|
|
165
|
+
else if (!dialog.contains(active)) { event.preventDefault(); first.focus(); }
|
|
166
|
+
}
|
|
167
|
+
const onFocusin = (event) => { if (!dialog.contains(event.target)) (focusables()[0] ?? dialog).focus(); };
|
|
168
|
+
const onPointerdown = (event) => { if (event.target === root) close(); };
|
|
169
|
+
function close() {
|
|
170
|
+
if (!sheet) return;
|
|
171
|
+
sheet = null;
|
|
172
|
+
element.setAttribute("aria-expanded", "false");
|
|
173
|
+
doc.removeEventListener("focusin", onFocusin, true);
|
|
174
|
+
root.removeEventListener("keydown", onKeydown);
|
|
175
|
+
root.removeEventListener("pointerdown", onPointerdown);
|
|
176
|
+
root.remove();
|
|
177
|
+
if (opener?.isConnected) opener.focus({ preventScroll: true }); else if (element.isConnected) element.focus({ preventScroll: true });
|
|
178
|
+
}
|
|
179
|
+
sheet = { root, close, focus: () => search.focus() };
|
|
180
|
+
root.addEventListener("keydown", onKeydown);
|
|
181
|
+
root.addEventListener("pointerdown", onPointerdown);
|
|
182
|
+
closeButton.addEventListener("click", close);
|
|
183
|
+
search.addEventListener("input", render);
|
|
184
|
+
doc.addEventListener("focusin", onFocusin, true);
|
|
185
|
+
element.setAttribute("aria-expanded", "true");
|
|
186
|
+
render();
|
|
187
|
+
search.focus();
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const close = () => sheet?.close();
|
|
191
|
+
|
|
192
|
+
const onKey = (event) => {
|
|
193
|
+
if (destroyed || disabled || sheet || !opensOnKey(event)) return;
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
markHandled(event);
|
|
196
|
+
open();
|
|
197
|
+
};
|
|
198
|
+
const onClick = () => { open(); };
|
|
199
|
+
element.addEventListener("click", onClick);
|
|
200
|
+
if (bindKey) doc.addEventListener("keydown", onKey);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
element,
|
|
204
|
+
/** Open the sheet. Returns false when the button is disabled or destroyed. */
|
|
205
|
+
open,
|
|
206
|
+
/** Close the sheet and return the focus to where it was. */
|
|
207
|
+
close,
|
|
208
|
+
setDensity(next) {
|
|
209
|
+
density = next === "touch" ? "touch" : "comfortable";
|
|
210
|
+
element.dataset.density = density;
|
|
211
|
+
if (sheet) sheet.root.dataset.density = density;
|
|
212
|
+
},
|
|
213
|
+
setDisabled(next) {
|
|
214
|
+
disabled = Boolean(next);
|
|
215
|
+
element.disabled = disabled;
|
|
216
|
+
if (disabled) close();
|
|
217
|
+
},
|
|
218
|
+
menuItems() {
|
|
219
|
+
return [{ id: "help", label: labels.button, disabled, run: open }];
|
|
220
|
+
},
|
|
221
|
+
destroy() {
|
|
222
|
+
if (destroyed) return;
|
|
223
|
+
close();
|
|
224
|
+
destroyed = true;
|
|
225
|
+
element.removeEventListener("click", onClick);
|
|
226
|
+
doc.removeEventListener("keydown", onKey);
|
|
227
|
+
element.remove();
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OpfIcons } from "./icons.js";
|
|
1
2
|
import type { EditorSession } from "./index.js";
|
|
2
3
|
|
|
3
4
|
export interface SlideNavigatorLabels {
|
|
@@ -22,6 +23,8 @@ export interface SlideNavigatorOptions {
|
|
|
22
23
|
/** `"comfortable"` (32px controls, default) or `"touch"` (40px). */
|
|
23
24
|
density?: "comfortable" | "touch";
|
|
24
25
|
labels?: SlideNavigatorLabels;
|
|
26
|
+
/** Replace the arrows: `previous` and `next` (`.opf-nav-icon`). See `OpfIcon`. */
|
|
27
|
+
icons?: OpfIcons<"previous" | "next">;
|
|
25
28
|
/** False to stop following the editor on its own; call `refresh()` yourself. Default true. */
|
|
26
29
|
autoRefresh?: boolean;
|
|
27
30
|
}
|
package/dist/slide-navigator.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Slide navigator (RR-98): previous and next buttons around a mono "01 / 03" counter. It owns no document state, only the slide index the host
|
|
2
2
|
// shows; the slide count follows the editor session (or a presentation the host hands in). Plain DOM, no framework, no network.
|
|
3
3
|
// Icons are the Lucide ChevronLeft and ChevronRight paths (ISC license), inlined.
|
|
4
|
-
|
|
4
|
+
import { normalizeIcons, resolveIcon } from "./icons.js";
|
|
5
|
+
|
|
5
6
|
const CHEVRONS = { previous: "m15 18-6-6 6-6", next: "m9 18 6-6-6-6" };
|
|
6
7
|
|
|
7
8
|
const DEFAULT_LABELS = {
|
|
@@ -19,21 +20,8 @@ export function formatSlideCounter(index, count) {
|
|
|
19
20
|
return `${pad(count === 0 ? 0 : index + 1)} / ${pad(count)}`;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
function chevron(name) {
|
|
23
|
-
|
|
24
|
-
svg.setAttribute("class", "opf-nav-icon");
|
|
25
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
26
|
-
svg.setAttribute("aria-hidden", "true");
|
|
27
|
-
svg.setAttribute("focusable", "false");
|
|
28
|
-
svg.setAttribute("fill", "none");
|
|
29
|
-
svg.setAttribute("stroke", "currentColor");
|
|
30
|
-
svg.setAttribute("stroke-width", "2");
|
|
31
|
-
svg.setAttribute("stroke-linecap", "round");
|
|
32
|
-
svg.setAttribute("stroke-linejoin", "round");
|
|
33
|
-
const path = document.createElementNS(SVG_NS, "path");
|
|
34
|
-
path.setAttribute("d", CHEVRONS[name]);
|
|
35
|
-
svg.append(path);
|
|
36
|
-
return svg;
|
|
23
|
+
function chevron(doc, name, given) {
|
|
24
|
+
return resolveIcon(doc, given, CHEVRONS[name], { name, className: "opf-nav-icon" });
|
|
37
25
|
}
|
|
38
26
|
|
|
39
27
|
/** True when a page-level key press belongs to something else: typing, an editor, a menu or a dialog. */
|
|
@@ -72,6 +60,7 @@ export function createSlideNavigator(container, options = {}) {
|
|
|
72
60
|
return Array.isArray(deck?.slides) ? deck.slides.length : 0;
|
|
73
61
|
};
|
|
74
62
|
|
|
63
|
+
const icons = normalizeIcons(options.icons, ["previous", "next"], "createSlideNavigator icons");
|
|
75
64
|
const element = doc.createElement("div");
|
|
76
65
|
element.className = "opf-nav";
|
|
77
66
|
element.dataset.density = density;
|
|
@@ -84,7 +73,7 @@ export function createSlideNavigator(container, options = {}) {
|
|
|
84
73
|
node.dataset.action = name;
|
|
85
74
|
node.setAttribute("aria-label", labels[name]);
|
|
86
75
|
node.title = labels[name];
|
|
87
|
-
node.append(chevron(name));
|
|
76
|
+
node.append(chevron(doc, name, icons[name]));
|
|
88
77
|
return node;
|
|
89
78
|
};
|
|
90
79
|
const previous = button("previous");
|
package/dist/studio.js
CHANGED
|
@@ -68,7 +68,8 @@ export function buildStudioShell(root, { parts, layout, labels = {}, studio: con
|
|
|
68
68
|
// The bar.
|
|
69
69
|
const bar = el("div", `${PREFIX}-bar`, { role: "toolbar", "aria-label": labels.studio ?? "Studio" });
|
|
70
70
|
const file = dom.button(`${PREFIX}-barbutton`, { text: "File", iconName: "Menu", size: 16, attributes: { "aria-haspopup": "menu", "data-studio": "file" } });
|
|
71
|
-
|
|
71
|
+
// CE-28 wave 2: the deck name is the workspace's title part (src/deck-title.js), mounted in this slot; the expand button sits before topbarEnd.
|
|
72
|
+
const name = slot("title", "div", `${PREFIX}-title`);
|
|
72
73
|
const tools = el("div", `${PREFIX}-tools`, { role: "group", "aria-label": "Tools" });
|
|
73
74
|
const toolButtons = {};
|
|
74
75
|
for (const [id, text, icon, key] of [["select", "Select", "MousePointer2", "V"], ["hand", "Hand", "Hand", "H"], ["text", "Text", "Type", "T"]]) {
|
|
@@ -97,6 +98,7 @@ export function buildStudioShell(root, { parts, layout, labels = {}, studio: con
|
|
|
97
98
|
bar.append(file, name, tools, insert, addSlideButton, history, views, spacer, zoom);
|
|
98
99
|
if (parts.source) bar.append(source);
|
|
99
100
|
if (parts.present) bar.append(slot("present", "div", `${PREFIX}-present`));
|
|
101
|
+
bar.append(slot("expand", "div", `${PREFIX}-expand`));
|
|
100
102
|
const topbarEnd = el("div", `${PREFIX}-topbar-end`, { "data-opf-slot": "topbarEnd" });
|
|
101
103
|
if (given.topbarEnd) topbarEnd.append(given.topbarEnd);
|
|
102
104
|
bar.append(topbarEnd);
|
|
@@ -1304,7 +1306,9 @@ export function createStudio(ctx) {
|
|
|
1304
1306
|
dialog.append(head, search, list);
|
|
1305
1307
|
const draw = () => {
|
|
1306
1308
|
const query = search.value.trim().toLowerCase();
|
|
1307
|
-
|
|
1309
|
+
// The workspace's own keys (save, the title, expand) are listed with the commands, as the help item lists them (CE-28).
|
|
1310
|
+
const extra = (ctx.extraShortcuts?.() ?? []).map((entry) => ({ id: entry.id, label: entry.label, group: entry.group ?? "General", keys: entry.keys, enabled: true, plain: true }));
|
|
1311
|
+
const entries = [...commands.list(s, { win }), ...extra].filter((entry) => (entry.keys.length || query) && (!query || `${entry.label} ${entry.group} ${entry.id}`.toLowerCase().includes(query)));
|
|
1308
1312
|
const groups = new Map();
|
|
1309
1313
|
for (const entry of entries) { if (!groups.has(entry.group)) groups.set(entry.group, []); groups.get(entry.group).push(entry); }
|
|
1310
1314
|
list.replaceChildren(...[...groups].map(([group, rows]) => {
|
|
@@ -1314,9 +1318,12 @@ export function createStudio(ctx) {
|
|
|
1314
1318
|
const keys = el("dd", "");
|
|
1315
1319
|
for (const key of row.keys.slice(0, 2)) keys.append(el("kbd", "", { text: formatKey(key, win) }));
|
|
1316
1320
|
const term = el("dt", "");
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1321
|
+
if (row.plain) term.append(el("span", "", { text: row.label }));
|
|
1322
|
+
else {
|
|
1323
|
+
const run = dom.button(`${PREFIX}-linkbutton`, { text: row.label, attributes: { "aria-disabled": row.enabled ? undefined : "true", title: row.reason || undefined } });
|
|
1324
|
+
run.addEventListener("click", () => { if (!row.enabled) { announce(row.reason); return; } closeSheetDialog(); runCommand(row.id); });
|
|
1325
|
+
term.append(run);
|
|
1326
|
+
}
|
|
1320
1327
|
table.append(term, keys);
|
|
1321
1328
|
}
|
|
1322
1329
|
section.append(el("h3", `${PREFIX}-section-title`, { text: group }), table);
|
|
@@ -1434,15 +1441,7 @@ export function createStudio(ctx) {
|
|
|
1434
1441
|
if (!ctx.inSync() && ctx.pane) { if (sourceMode === "off") s.toggleSource(); ctx.pane.editor?.api?.focus?.(); return; }
|
|
1435
1442
|
if (shell.tabs.review) { if (root.dataset.width === "narrow") openSheet("review"); else showTab("review", { focus: true }); }
|
|
1436
1443
|
});
|
|
1437
|
-
// The name: the
|
|
1438
|
-
const deckName = () => { const value = ui.get("/name"); return typeof value === "string" ? value : ""; };
|
|
1439
|
-
listen(shell.name, "change", () => {
|
|
1440
|
-
const value = shell.name.value.trim();
|
|
1441
|
-
if (value === deckName()) return;
|
|
1442
|
-
if (s.writeValue("/name", value || undefined, value ? `Renamed to ${value}` : "Name removed")) return;
|
|
1443
|
-
shell.name.value = deckName();
|
|
1444
|
-
});
|
|
1445
|
-
listen(shell.name, "keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); shell.name.blur(); } if (event.key === "Escape") { shell.name.value = deckName(); shell.name.blur(); } });
|
|
1444
|
+
// The name: the workspace's title part in `shell.name` (CE-28), one undoable change through the session (the source pane keeps its own file name).
|
|
1446
1445
|
function syncBar() {
|
|
1447
1446
|
const list = commands.list(s, { win });
|
|
1448
1447
|
const state = (id) => list.find((entry) => entry.id === id);
|
|
@@ -1452,8 +1451,6 @@ export function createStudio(ctx) {
|
|
|
1452
1451
|
const slideState = state("studio.slide.new");
|
|
1453
1452
|
shell.addSlide.disabled = !slideState?.enabled;
|
|
1454
1453
|
shell.addSlide.title = slideState?.enabled ? "Add a slide" : slideState?.reason ?? "";
|
|
1455
|
-
if (doc.activeElement !== shell.name) shell.name.value = deckName();
|
|
1456
|
-
shell.name.readOnly = s.readOnly;
|
|
1457
1454
|
}
|
|
1458
1455
|
|
|
1459
1456
|
// ---- the status line ----------------------------------------------------------------------------------------------------------------
|