@openpresentation/opf-editor 0.27.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 +23 -9
- 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 +136 -125
- package/dist/design-options.js +1 -1
- package/dist/exact-source.d.ts +30 -0
- package/dist/font-gate.js +20 -1
- package/dist/json-editor.d.ts +33 -0
- package/dist/json-editor.js +91 -4
- package/dist/json-options.d.ts +2 -1
- package/dist/json-options.js +7 -6
- package/dist/layout-adapter.js +96 -0
- package/dist/layout-canvas.js +122 -232
- package/dist/layout-editor-ui.js +46 -185
- package/dist/layout-inspector.js +36 -99
- package/dist/layout-layers.js +75 -92
- package/dist/layout-model.js +62 -103
- 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 +16 -4
- package/dist/react.js +47 -6
- 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-color-scheme.js +60 -0
- package/dist/source-completions.js +6 -4
- package/dist/source-pane.d.ts +103 -6
- package/dist/source-pane.js +196 -9
- 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/commands.d.ts +38 -0
- package/dist/studio-kit/commands.js +148 -0
- package/dist/studio-kit/dom.d.ts +52 -0
- package/dist/studio-kit/dom.js +198 -0
- package/dist/studio-kit/drag.d.ts +26 -0
- package/dist/studio-kit/drag.js +127 -0
- package/dist/studio-kit/gesture.d.ts +29 -0
- package/dist/studio-kit/gesture.js +87 -0
- package/dist/studio-kit/index.d.ts +29 -0
- package/dist/studio-kit/index.js +12 -0
- package/dist/studio-kit/inspector.d.ts +22 -0
- package/dist/studio-kit/inspector.js +119 -0
- package/dist/studio-kit/overlay.d.ts +29 -0
- package/dist/studio-kit/overlay.js +182 -0
- package/dist/studio-kit/selection.d.ts +30 -0
- package/dist/studio-kit/selection.js +106 -0
- package/dist/studio-kit/tree.d.ts +9 -0
- package/dist/studio-kit/tree.js +82 -0
- package/dist/studio-kit/viewport.d.ts +29 -0
- package/dist/studio-kit/viewport.js +153 -0
- 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 +1573 -123
- 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 +114 -10
- package/dist/workspace.js +223 -49
- package/package.json +30 -19
package/dist/design-controls.js
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "./design-options.js";
|
|
44
44
|
import { ACCENT_SLOTS } from "./color-slots.js";
|
|
45
45
|
import { readDesignVariant, setDesignVariant, variantSwatches } from "./design-variants.js";
|
|
46
|
-
import { createContentControls } from "./content-controls.js";
|
|
46
|
+
import { createContentControls, iconNode, shortReason } from "./content-controls.js";
|
|
47
47
|
import { IMAGE_EDGES, IMAGE_FITS, IMAGE_SHAPES, readImageTreatments, setImageTreatment } from "./image-options.js";
|
|
48
48
|
import { applyCatalogUpdate, catalogRecordAt, checkCatalogUpdates, mergeCatalogs, moveToCustom, referenceFindings } from "./catalogs.js";
|
|
49
49
|
import { describeTableCell, mergeTableCells, parseTableCellPath, readTableAlt, readTableStyle, setTableAlt, setTableCellStyle, setTableStyle, splitTableCell } from "./table-options.js";
|
|
@@ -94,6 +94,14 @@ const BLOCK_REPLACEMENTS = ["text", "list", "chart", "table", "metric", "quote",
|
|
|
94
94
|
let instances = 0;
|
|
95
95
|
|
|
96
96
|
const messageOf = (error) => error?.issues?.[0]?.message ?? error?.message ?? String(error);
|
|
97
|
+
/** Whether `path` (an OPF path such as slides.0.blocks.1) is an explicit block that is a group of blocks. */
|
|
98
|
+
function isGroupBlock(presentation, path) {
|
|
99
|
+
const parts = String(path).split(".");
|
|
100
|
+
if (parts.at(-2) !== "blocks" || !/^\d+$/.test(parts.at(-1) ?? "")) return false;
|
|
101
|
+
let value = presentation;
|
|
102
|
+
for (const part of parts) value = value?.[/^\d+$/.test(part) ? Number(part) : part];
|
|
103
|
+
return Boolean(value) && typeof value === "object" && Array.isArray(value.blocks);
|
|
104
|
+
}
|
|
97
105
|
const titleCase = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
98
106
|
|
|
99
107
|
/**
|
|
@@ -105,7 +113,12 @@ const titleCase = (text) => text.charAt(0).toUpperCase() + text.slice(1);
|
|
|
105
113
|
* registered ones), `vocabularies` (display metadata for languages, chart types and social platforms: `catalogDisplay`
|
|
106
114
|
* from `@openpresentation/gallery`), `onChange(change)`, `onStatus(message, { error })` and `onSelectPath(path)`, called
|
|
107
115
|
* after a content conversion or replacement with the path to keep selected (the block, or the inline
|
|
108
|
-
* payload field), because the host's old selection path may no longer exist.
|
|
116
|
+
* payload field), because the host's old selection path may no longer exist. `showPath` (default true) shows the
|
|
117
|
+
* selected block's path as a copyable chip under the selection section's Advanced; `blockActions` (`{ before, after }`)
|
|
118
|
+
* adds host actions to the selected block's arrange toolbar.
|
|
119
|
+
*
|
|
120
|
+
* Every option is a visible control with a short label. A hint is the control's tooltip and accessible description, never a
|
|
121
|
+
* paragraph; an option that does not apply is shown disabled, with its reason as its tooltip.
|
|
109
122
|
*/
|
|
110
123
|
export function createDesignControls(container, options = {}) {
|
|
111
124
|
const editor = options.editor;
|
|
@@ -173,7 +186,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
173
186
|
// An edit of a catalog's record forked it into the deck's own records: say so (FA-23).
|
|
174
187
|
const notice = change?.notice ? ` ${change.notice}` : "";
|
|
175
188
|
if (change && change.changed === false) say("Already set. Nothing changed.");
|
|
176
|
-
else say(`${typeof done === "function" ? done(change) : (done ?? "Changed.")}${warnings}${notice}
|
|
189
|
+
else say(`${typeof done === "function" ? done(change) : (done ?? "Changed.")}${warnings}${notice}`);
|
|
177
190
|
options.onChange?.(change);
|
|
178
191
|
} catch (error) {
|
|
179
192
|
complain(messageOf(error));
|
|
@@ -191,12 +204,12 @@ export function createDesignControls(container, options = {}) {
|
|
|
191
204
|
let fieldCount = 0;
|
|
192
205
|
const nextId = (name) => `${uid}-${name}-${++fieldCount}`;
|
|
193
206
|
|
|
207
|
+
// A hint is a tooltip on the field (and the control's accessible description), never a paragraph under it.
|
|
194
208
|
function wrapField(id, label, control, { help, extra } = {}) {
|
|
195
209
|
const note = h("span", { class: "opf-dc-note" });
|
|
196
210
|
const labelEl = h("label", { for: id }, label, note);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const wrap = h("div", { class: "opf-dc-field" }, labelEl, control, extra, helpEl);
|
|
211
|
+
if (help) control.title = help;
|
|
212
|
+
const wrap = h("div", { class: "opf-dc-field", title: help }, labelEl, control, extra);
|
|
200
213
|
return { wrap, note, label: labelEl };
|
|
201
214
|
}
|
|
202
215
|
|
|
@@ -275,19 +288,18 @@ export function createDesignControls(container, options = {}) {
|
|
|
275
288
|
return api;
|
|
276
289
|
}
|
|
277
290
|
|
|
278
|
-
|
|
279
|
-
|
|
291
|
+
// A range is the field's placeholder (a unit hint), so the label stays short.
|
|
292
|
+
function numberField(name, label, { min, max, step, help, placeholder, onCommit } = {}) {
|
|
293
|
+
const field = textField(name, label, { help, placeholder: placeholder ?? (min !== undefined && max !== undefined ? `${min}–${max}` : undefined), type: "number", onCommit: onCommit ? (value) => onCommit(value === "" ? null : Number(value)) : undefined });
|
|
280
294
|
for (const [key, value] of Object.entries({ min, max, step })) if (value !== undefined) field.input.setAttribute(key, String(value));
|
|
281
295
|
return field;
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
function checkField(name, label, { help, onChange } = {}) {
|
|
285
299
|
const id = nextId(name);
|
|
286
|
-
const input = h("input", { id, type: "checkbox" });
|
|
287
|
-
const helpEl = help ? h("p", { class: "opf-dc-help", id: `${id}-help` }, help) : undefined;
|
|
288
|
-
if (helpEl) input.setAttribute("aria-describedby", helpEl.id);
|
|
300
|
+
const input = h("input", { id, type: "checkbox", title: help });
|
|
289
301
|
const note = h("span", { class: "opf-dc-note" });
|
|
290
|
-
const wrap = h("div", { class: "opf-dc-field opf-dc-check" }, input, h("label", { for: id }, label, note)
|
|
302
|
+
const wrap = h("div", { class: "opf-dc-field opf-dc-check", title: help }, input, h("label", { for: id }, label, note));
|
|
291
303
|
if (onChange) input.addEventListener("change", () => onChange(input.checked));
|
|
292
304
|
return {
|
|
293
305
|
wrap,
|
|
@@ -307,12 +319,12 @@ export function createDesignControls(container, options = {}) {
|
|
|
307
319
|
*/
|
|
308
320
|
function overlayFields(name, label, { onCommit } = {}) {
|
|
309
321
|
const commit = () => onCommit?.();
|
|
310
|
-
const color = textField(`${name}-color`, `${label} color`, { list: true, placeholder: "dark1 or #000000", help: "
|
|
322
|
+
const color = textField(`${name}-color`, `${label} color`, { list: true, placeholder: "dark1 or #000000", help: "Theme color, hex or var:<id>. Clear to remove.", onCommit: commit });
|
|
311
323
|
color.setList(COLOR_NAMES);
|
|
312
|
-
const opacity = numberField(`${name}-opacity`, `${label} opacity
|
|
313
|
-
const edge = selectField(`${name}-edge`, `${label} band`, { empty: "Whole picture", help: "Cover
|
|
324
|
+
const opacity = numberField(`${name}-opacity`, `${label} opacity`, { min: 0, max: 1, step: 0.05, onCommit: commit });
|
|
325
|
+
const edge = selectField(`${name}-edge`, `${label} band`, { empty: "Whole picture", help: "Cover a band along one edge only", onChange: commit });
|
|
314
326
|
edge.setOptions(IMAGE_EDGES.map((value) => ({ value, label: `Along the ${value}` })));
|
|
315
|
-
const size = numberField(`${name}-size`, `${label} band size
|
|
327
|
+
const size = numberField(`${name}-size`, `${label} band size`, { min: 0.05, max: 1, step: 0.05, onCommit: commit });
|
|
316
328
|
const wrap = h("fieldset", { class: "opf-dc-fieldset", "data-role": name }, h("legend", {}, label), color.wrap, opacity.wrap, edge.wrap, size.wrap);
|
|
317
329
|
return {
|
|
318
330
|
wrap,
|
|
@@ -344,25 +356,20 @@ export function createDesignControls(container, options = {}) {
|
|
|
344
356
|
const source = textField(`${name}-source`, `${label} source`, {
|
|
345
357
|
list: true,
|
|
346
358
|
placeholder: "asset:photo or https://…",
|
|
347
|
-
help: help ?? "
|
|
359
|
+
help: help ?? "Asset, URL or data URL. Enter applies; clear to remove.",
|
|
348
360
|
onCommit: (value) => {
|
|
349
361
|
const ref = value.trim();
|
|
350
362
|
run(() => apply(ref === "" ? null : ref), ref === "" ? `${label} removed.` : `${label} changed.`);
|
|
351
363
|
},
|
|
352
364
|
});
|
|
353
365
|
const fileId = nextId(`${name}-file`);
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
{ class: "opf-dc-field" },
|
|
358
|
-
h("label", { for: fileId }, `Upload ${label.toLowerCase()} file`),
|
|
359
|
-
input,
|
|
360
|
-
h("p", { class: "opf-dc-help", id: `${fileId}-help` }, `PNG, JPEG, GIF, WebP or SVG, up to ${sizeLabel}. It is added to the presentation's assets (or handed to your app) and used here in one undoable step.`),
|
|
361
|
-
);
|
|
366
|
+
const fileHint = `PNG, JPEG, GIF, WebP or SVG, up to ${sizeLabel}`;
|
|
367
|
+
const input = h("input", { id: fileId, type: "file", accept: IMAGE_ACCEPT, title: fileHint });
|
|
368
|
+
const fileWrap = h("div", { class: "opf-dc-field", title: fileHint }, h("label", { for: fileId }, `Upload ${label.toLowerCase()} file`), input);
|
|
362
369
|
let lastRef = "";
|
|
363
370
|
let lastKey = "";
|
|
364
371
|
const alt = textField(`${name}-alt`, `${label} alt text`, {
|
|
365
|
-
help: "
|
|
372
|
+
help: "Read by screen readers. Enter applies.",
|
|
366
373
|
onCommit: (value) => {
|
|
367
374
|
const id = assetIdOf(lastRef);
|
|
368
375
|
if (!id || editor.presentation.assets?.[id] === undefined) return;
|
|
@@ -390,7 +397,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
390
397
|
try {
|
|
391
398
|
const change = await applyImageUpload(editor, file, build, { alt: alt.input.value, maxBytes: maxImageBytes, onAddAsset: options.onAddAsset });
|
|
392
399
|
const warnings = change.prepared?.warnings?.length ? ` ${change.prepared.warnings.map((warning) => warning.message).join(" ")}` : "";
|
|
393
|
-
say(`${label} set to ${file.name}${change.assetId ? `, added to assets as ${change.assetId}` : ""}.${warnings}
|
|
400
|
+
say(`${label} set to ${file.name}${change.assetId ? `, added to assets as ${change.assetId}` : ""}.${warnings}`);
|
|
394
401
|
options.onChange?.(change);
|
|
395
402
|
} catch (error) {
|
|
396
403
|
if (destroyed) return;
|
|
@@ -438,7 +445,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
438
445
|
let scopeField;
|
|
439
446
|
if (scopeNeeded) {
|
|
440
447
|
scopeField = selectField("scope", "Applies to", {
|
|
441
|
-
help: "Choose the whole presentation, or only the slide you are looking at.",
|
|
442
448
|
onChange: (value) => {
|
|
443
449
|
state.scope = value === "slide" ? "slide" : "deck";
|
|
444
450
|
sync();
|
|
@@ -459,7 +465,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
459
465
|
const built = {};
|
|
460
466
|
for (const section of sections) built[section] = group(section);
|
|
461
467
|
// RR-26: list levels, grouping, regions and images between content and design (content-controls.js).
|
|
462
|
-
const contentControls = built.selection || built["slide-content"] ? createContentControls({ editor, h, selectField, run, nextId, getSlide, reselect }) : undefined;
|
|
468
|
+
const contentControls = built.selection || built["slide-content"] ? createContentControls({ editor, doc, h, selectField, run, nextId, getSlide, reselect, blockActions: options.blockActions }) : undefined;
|
|
463
469
|
|
|
464
470
|
// --- look and language --------------------------------------------------------------------------
|
|
465
471
|
|
|
@@ -478,13 +484,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
478
484
|
});
|
|
479
485
|
return field;
|
|
480
486
|
};
|
|
481
|
-
catalogField("theme", "Theme", "themes", { help: "
|
|
487
|
+
catalogField("theme", "Theme", "themes", { help: "Brings its own colors, fonts and background" });
|
|
482
488
|
catalogField("color-scheme", "Color scheme", "color-schemes");
|
|
483
|
-
catalogField("font-scheme", "Font scheme", "font-schemes", { help: "
|
|
484
|
-
catalogField("language", "Language", "languages", { perSlide: false, help: "
|
|
489
|
+
catalogField("font-scheme", "Font scheme", "font-schemes", { help: "Unbundled fonts preview as an open look-alike" });
|
|
490
|
+
catalogField("language", "Language", "languages", { perSlide: false, help: "Whole presentation, with its script fonts" });
|
|
485
491
|
// RR-41: one size for the whole presentation; the slides recompose and the export writes the matching slide size.
|
|
486
492
|
const slideSize = selectField("slide-size", "Slide size", {
|
|
487
|
-
help: "
|
|
493
|
+
help: "Every slide",
|
|
488
494
|
onChange: (value) => run(() => switchDimension(editor, "slide-sizes", value), `Slide size set to ${slideSize.select.selectedOptions[0]?.textContent ?? value}.`),
|
|
489
495
|
});
|
|
490
496
|
body.append(slideSize.wrap);
|
|
@@ -495,7 +501,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
495
501
|
});
|
|
496
502
|
|
|
497
503
|
const layout = selectField("layout", "Layout of this slide", {
|
|
498
|
-
help: "Arranges this slide's content in the chosen layout's regions.",
|
|
499
504
|
onChange: (value) => run(() => switchDimension(editor, "layouts", value, { slideIndex: getSlide(), ...catalogOptions() }), `Layout changed to ${layout.select.selectedOptions[0]?.textContent ?? value}.`),
|
|
500
505
|
});
|
|
501
506
|
body.append(layout.wrap);
|
|
@@ -578,7 +583,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
578
583
|
|
|
579
584
|
if (built.background) {
|
|
580
585
|
const { body } = built.background;
|
|
581
|
-
const typeSelect = selectField("bg-type", "Background type", { help: "
|
|
586
|
+
const typeSelect = selectField("bg-type", "Background type", { help: "Applies with Apply background" });
|
|
582
587
|
typeSelect.setOptions([
|
|
583
588
|
{ value: "theme", label: "Theme color" },
|
|
584
589
|
{ value: "solid", label: "Solid color" },
|
|
@@ -588,24 +593,24 @@ export function createDesignControls(container, options = {}) {
|
|
|
588
593
|
]);
|
|
589
594
|
const panel = (type, title) => h("fieldset", { class: "opf-dc-fieldset", "data-bg-type": type }, h("legend", {}, title));
|
|
590
595
|
const themePanel = panel("theme", "Theme color");
|
|
591
|
-
const slot = selectField("bg-slot", "Theme slot", { help: "Follows the color scheme
|
|
596
|
+
const slot = selectField("bg-slot", "Theme slot", { help: "Follows the color scheme" });
|
|
592
597
|
slot.setOptions(THEME_BACKGROUND_SLOTS.map((value) => ({ value, label: value.replace(/(\D+)(\d)/, (_, name, n) => `${titleCase(name)} ${n}`) })));
|
|
593
598
|
themePanel.append(slot.wrap);
|
|
594
599
|
|
|
595
600
|
const colorList = [...COLOR_NAMES];
|
|
596
601
|
const solidPanel = panel("solid", "Solid color");
|
|
597
|
-
const solidColor = textField("bg-color", "Color", { list: true, placeholder: "#1F2937 or accent1", help: "
|
|
602
|
+
const solidColor = textField("bg-color", "Color", { list: true, placeholder: "#1F2937 or accent1", help: "Hex, scheme color (accent1, surface) or var:<id>" });
|
|
598
603
|
solidColor.setList(colorList);
|
|
599
604
|
solidPanel.append(solidColor.wrap);
|
|
600
605
|
|
|
601
606
|
const gradientPanel = panel("gradient", "Linear gradient");
|
|
602
|
-
const angle = numberField("bg-angle", "Angle
|
|
607
|
+
const angle = numberField("bg-angle", "Angle", { step: 1, placeholder: "degrees", help: "0° left to right, 90° top to bottom" });
|
|
603
608
|
const stopsBox = h("div", { class: "opf-dc-stops", role: "group", "aria-label": "Gradient stops" });
|
|
604
609
|
const stopRows = [];
|
|
605
610
|
const renderStops = () => {
|
|
606
611
|
stopsBox.replaceChildren(
|
|
607
612
|
...stopRows.map((row, index) => {
|
|
608
|
-
for (const [field, text] of [[row.color, `Stop ${index + 1} color`], [row.position, `Stop ${index + 1} position
|
|
613
|
+
for (const [field, text] of [[row.color, `Stop ${index + 1} color`], [row.position, `Stop ${index + 1} position`]]) field.wrap.querySelector("label").firstChild.textContent = text;
|
|
609
614
|
const remove = h("button", { type: "button", class: "quiet", disabled: stopRows.length <= 2 || undefined, "aria-label": `Remove stop ${index + 1}`, onclick: () => { stopRows.splice(index, 1); backgroundDirty = true; renderStops(); } }, "Remove");
|
|
610
615
|
return h("div", { class: "opf-dc-stop" }, row.color.wrap, row.position.wrap, remove);
|
|
611
616
|
}),
|
|
@@ -626,15 +631,15 @@ export function createDesignControls(container, options = {}) {
|
|
|
626
631
|
gradientPanel.append(angle.wrap, stopsBox, addStopButton);
|
|
627
632
|
|
|
628
633
|
const imagePanel = panel("image", "Image");
|
|
629
|
-
const fit = selectField("bg-fit", "Fit", { help: "Cover
|
|
634
|
+
const fit = selectField("bg-fit", "Fit", { help: "Cover crops to fill; contain shows it all; tile repeats it" });
|
|
630
635
|
fit.setOptions(IMAGE_BACKGROUND_FITS.map((value) => ({ value, label: titleCase(value) })));
|
|
631
636
|
const bgAlt = textField("bg-alt", "Background alt text", {
|
|
632
|
-
help: "
|
|
637
|
+
help: "Leave empty for a decorative background",
|
|
633
638
|
});
|
|
634
|
-
const focusX = numberField("bg-focus-x", "Focus
|
|
635
|
-
const focusY = numberField("bg-focus-y", "Focus
|
|
639
|
+
const focusX = numberField("bg-focus-x", "Focus X", { min: 0, max: 1, step: 0.05, help: "0 left to 1 right" });
|
|
640
|
+
const focusY = numberField("bg-focus-y", "Focus Y", { min: 0, max: 1, step: 0.05, help: "0 top to 1 bottom" });
|
|
636
641
|
const overlay = overlayFields("bg-overlay", "Overlay");
|
|
637
|
-
const bgRecolor = selectField("bg-recolor", "Recolor", { empty: "None", help: "
|
|
642
|
+
const bgRecolor = selectField("bg-recolor", "Recolor", { empty: "None", help: "Changes the picture, not the overlay" });
|
|
638
643
|
bgRecolor.setOptions([
|
|
639
644
|
{ value: "grayscale", label: "Grayscale" },
|
|
640
645
|
{ value: "duotone", label: "Duotone" },
|
|
@@ -665,12 +670,12 @@ export function createDesignControls(container, options = {}) {
|
|
|
665
670
|
const spec = imageSpec(undefined), target = scoped();
|
|
666
671
|
return (ref, doc) => prepareBackground(doc, { ...spec, src: ref }, target);
|
|
667
672
|
},
|
|
668
|
-
help: "
|
|
673
|
+
help: "Asset, URL or data URL. Enter applies.",
|
|
669
674
|
});
|
|
670
675
|
imagePanel.append(bgImage.wrap, fit.wrap, bgAlt.wrap, focusX.wrap, focusY.wrap, bgRecolor.wrap, bgDuoDark.wrap, bgDuoLight.wrap, overlay.wrap);
|
|
671
676
|
|
|
672
677
|
const patternPanel = panel("pattern", "Pattern");
|
|
673
|
-
const preset = selectField("bg-preset", "Pattern", { help: "
|
|
678
|
+
const preset = selectField("bg-preset", "Pattern", { help: "PowerPoint presets, exported as native fills" });
|
|
674
679
|
preset.setOptions(Object.entries(PATTERN_GROUPS).flatMap(([name, ids]) => ids.map((id) => ({ value: id, label: id, group: name }))));
|
|
675
680
|
const patternFg = textField("bg-pattern-fg", "Pattern color", { list: true, placeholder: "#000000 or text" });
|
|
676
681
|
patternFg.setList(colorList);
|
|
@@ -678,7 +683,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
678
683
|
patternBg.setList(colorList);
|
|
679
684
|
patternPanel.append(preset.wrap, patternFg.wrap, patternBg.wrap);
|
|
680
685
|
|
|
681
|
-
const opacity = numberField("bg-opacity", "Opacity
|
|
686
|
+
const opacity = numberField("bg-opacity", "Opacity", { min: 0, max: 1, step: 0.05 });
|
|
682
687
|
const opacityValue = () => (opacity.input.value === "" ? undefined : Number(opacity.input.value));
|
|
683
688
|
const panels = { theme: themePanel, solid: solidPanel, gradient: gradientPanel, image: imagePanel, pattern: patternPanel };
|
|
684
689
|
|
|
@@ -762,13 +767,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
762
767
|
const pick = { which: "footer", zone: "center" };
|
|
763
768
|
const which = () => pick.which;
|
|
764
769
|
const zone = () => pick.zone;
|
|
765
|
-
const furniture = selectField("hf-which", "Edit"
|
|
770
|
+
const furniture = selectField("hf-which", "Edit");
|
|
766
771
|
furniture.setOptions([
|
|
767
772
|
{ value: "header", label: "Header" },
|
|
768
773
|
{ value: "footer", label: "Footer" },
|
|
769
774
|
]);
|
|
770
775
|
furniture.set(pick.which);
|
|
771
|
-
const zoneSelect = selectField("hf-zone", "Zone", { help: "
|
|
776
|
+
const zoneSelect = selectField("hf-zone", "Zone", { help: "Parts sit side by side from the edge: image, text, socials, date" });
|
|
772
777
|
zoneSelect.setOptions(HEADER_FOOTER_ZONES.map((value) => ({ value, label: titleCase(value) })));
|
|
773
778
|
zoneSelect.set(pick.zone);
|
|
774
779
|
furniture.select.addEventListener("change", () => {
|
|
@@ -781,7 +786,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
781
786
|
});
|
|
782
787
|
// Hiding is for a slide that would otherwise show the deck's furniture; at the deck, clear the zones instead.
|
|
783
788
|
const hide = checkField("hf-hide", "Hide it", {
|
|
784
|
-
help: "
|
|
789
|
+
help: "Hide the presentation's on this slide",
|
|
785
790
|
onChange: (checked) => run(() => switchDimension(editor, "headers-footers", { [which()]: checked ? false : null }, scoped()), checked ? `${titleCase(which())} hidden on this slide.` : `${titleCase(which())} shown again.`),
|
|
786
791
|
});
|
|
787
792
|
const edit = (fields, done) => run(() => setHeaderFooterZone(editor, which(), zone(), fields, scoped()), done);
|
|
@@ -792,7 +797,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
792
797
|
};
|
|
793
798
|
const text = textField("hf-text", "Text", {
|
|
794
799
|
multiline: true,
|
|
795
|
-
help: "
|
|
800
|
+
help: "Shift+Enter for a new line. Clear to remove.",
|
|
796
801
|
onCommit: (value) => {
|
|
797
802
|
if (value === currentText()) return;
|
|
798
803
|
edit({ text: value }, `${where()} text changed.`);
|
|
@@ -801,7 +806,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
801
806
|
// The menu inserts a variable token at the caret (or over the selection) as one edit, together with any text typed but not yet committed.
|
|
802
807
|
const insertValue = selectField("hf-insert-value", "Insert value", {
|
|
803
808
|
empty: "Choose a value…",
|
|
804
|
-
help: "Adds a
|
|
809
|
+
help: "Adds a {{ }} variable at the cursor",
|
|
805
810
|
});
|
|
806
811
|
insertValue.setOptions(ZONE_VALUES.map((value) => ({ value: value.name, label: value.label, title: value.token })));
|
|
807
812
|
insertValue.select.addEventListener("change", () => {
|
|
@@ -827,37 +832,36 @@ export function createDesignControls(container, options = {}) {
|
|
|
827
832
|
const kind = which(), place = zone(), target = scoped();
|
|
828
833
|
return (ref, doc) => prepareHeaderFooterZone(doc, kind, place, { image: ref }, target);
|
|
829
834
|
},
|
|
830
|
-
help: "
|
|
835
|
+
help: "Asset, URL or data URL, or Insert logo. Enter applies.",
|
|
831
836
|
});
|
|
832
837
|
// RR-71: a logo is the zone's image, written as a reference to the organization's logo (var:organization.logo.icon). Which artwork is drawn (light or dark) follows each slide's background.
|
|
833
|
-
const logoShape = selectField("hf-logo-shape", "Logo shape", { help: "Icon
|
|
838
|
+
const logoShape = selectField("hf-logo-shape", "Logo shape", { help: "Icon fits a zone best" });
|
|
834
839
|
logoShape.setOptions(LOGO_SHAPES.map((value) => ({ value, label: LOGO_SHAPE_LABELS[value] })));
|
|
835
840
|
logoShape.set("icon");
|
|
836
|
-
const logoOrganization = selectField("hf-logo-organization", "Logo of", { empty: "Primary organization"
|
|
841
|
+
const logoOrganization = selectField("hf-logo-organization", "Logo of", { empty: "Primary organization" });
|
|
837
842
|
const insertLogoButton = button("Insert logo", () => {
|
|
838
843
|
const shape = logoShape.select.value;
|
|
839
844
|
const organization = logoOrganization.select.value || undefined;
|
|
840
845
|
run(() => insertZoneLogo(editor, which(), zone(), { ...scoped(), shape, ...(organization ? { organization } : {}) }), `${where()} now shows the ${shape} logo (${logoReference({ ...(organization ? { organization } : {}), ...(shape !== "full" ? { shape } : {}) })}), replacing any image.`);
|
|
841
846
|
});
|
|
842
847
|
insertLogoButton.dataset.action = "insert-logo";
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
help: "Needs the host to supply today's date when it renders or exports; otherwise the part is reported as unresolved.",
|
|
848
|
+
insertLogoButton.title = "Uses the organization's logo as this zone's image";
|
|
849
|
+
const logo = { wrap: h("fieldset", { class: "opf-dc-fieldset", "data-role": "hf-logo" }, h("legend", {}, "Logo"), logoShape.wrap, logoOrganization.wrap, h("div", { class: "opf-dc-actions" }, insertLogoButton)) };
|
|
850
|
+
const dateNow = checkField("hf-date-now", "Today's date", {
|
|
851
|
+
help: "The host supplies the date when it renders or exports",
|
|
848
852
|
onChange: (checked) => edit({ date: checked }, `${where()} date ${checked ? "shown" : "removed"}.`),
|
|
849
853
|
});
|
|
850
854
|
const dateFixed = textField("hf-date-fixed", "Fixed date", {
|
|
851
855
|
placeholder: "2026-10-01",
|
|
852
|
-
help: "
|
|
856
|
+
help: "YYYY-MM-DD, or text",
|
|
853
857
|
onCommit: (value) => edit({ date: value.trim() }, `${where()} date changed.`),
|
|
854
858
|
});
|
|
855
859
|
const dateFormat = textField("hf-date-format", "Date format", {
|
|
856
860
|
placeholder: "MMMM d, yyyy",
|
|
857
|
-
help: `Tokens: ${DATE_FORMAT_TOKENS.join(", ")}.
|
|
861
|
+
help: `Tokens: ${DATE_FORMAT_TOKENS.join(", ")}. 'Quoted' text is literal.`,
|
|
858
862
|
onCommit: (value) => edit({ dateFormat: value.trim() }, `${where()} date format changed.`),
|
|
859
863
|
});
|
|
860
|
-
const socials = checkField("hf-socials", "
|
|
864
|
+
const socials = checkField("hf-socials", "Social profiles", { onChange: (checked) => edit({ socials: checked }, `${where()} social profiles ${checked ? "shown" : "removed"}.`) });
|
|
861
865
|
let shownContext;
|
|
862
866
|
const summary = h("ul", { class: "opf-dc-summary", "aria-label": "Zones in use" });
|
|
863
867
|
const warnings = h("ul", { class: "opf-dc-warnings", "aria-label": "Header and footer warnings" });
|
|
@@ -916,17 +920,16 @@ export function createDesignControls(container, options = {}) {
|
|
|
916
920
|
// RR-71: logos live on the organization. One image serves every shape, or each of the four shapes (full, stacked, icon,
|
|
917
921
|
// wordmark) has its own, optionally split by the background it is drawn on (light or dark).
|
|
918
922
|
const logoOrganizationPick = selectField("org-logo-organization", "Organization to edit", {
|
|
919
|
-
help: "Whose logo to edit. The primary organization's full logo is drawn on cover and section slides.",
|
|
920
923
|
onChange: () => sync(),
|
|
921
924
|
});
|
|
922
925
|
const logoShapePick = selectField("org-logo-shape", "Shape to edit", {
|
|
923
|
-
help: "
|
|
926
|
+
help: "A missing shape falls back to the full logo",
|
|
924
927
|
onChange: () => sync(),
|
|
925
928
|
});
|
|
926
929
|
logoShapePick.setOptions(["all", ...LOGO_SHAPES].map((value) => ({ value, label: LOGO_SHAPE_LABELS[value] })));
|
|
927
930
|
logoShapePick.set("all");
|
|
928
931
|
const logoBackgroundPick = selectField("org-logo-background", "Background to edit", {
|
|
929
|
-
help: "
|
|
932
|
+
help: "Each slide draws the one that suits its background",
|
|
930
933
|
onChange: () => sync(),
|
|
931
934
|
});
|
|
932
935
|
logoBackgroundPick.setOptions(LOGO_BACKGROUNDS.map((value) => ({ value, label: LOGO_BACKGROUND_LABELS[value] })));
|
|
@@ -941,7 +944,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
941
944
|
const { shape, ...rest } = logoTarget();
|
|
942
945
|
return (ref, doc) => prepareOrganizationLogo(doc, shape, ref, rest);
|
|
943
946
|
},
|
|
944
|
-
help: "
|
|
947
|
+
help: "Asset, URL or data URL. Enter applies; clear to remove.",
|
|
945
948
|
});
|
|
946
949
|
const logoSummary = h("ul", { class: "opf-dc-summary", "aria-label": "Logo shapes set" });
|
|
947
950
|
const organizationLogoGroup = h("fieldset", { class: "opf-dc-fieldset", "data-role": "organization-logo" }, h("legend", {}, "Organization logo"), logoOrganizationPick.wrap, logoShapePick.wrap, logoBackgroundPick.wrap, orgLogo.wrap, logoSummary);
|
|
@@ -980,12 +983,11 @@ export function createDesignControls(container, options = {}) {
|
|
|
980
983
|
// The logo that covers, sections and picture bullets draw: the primary organization's by default, another organization's or shape, or none.
|
|
981
984
|
const choiceOrganizationPick = selectField("design-logo-organization", "Logo for covers, sections and bullets", {
|
|
982
985
|
empty: "Primary organization",
|
|
983
|
-
help: "Which organization's logo covers and sections draw, and picture bullets use. Choose None to draw no logo.",
|
|
984
986
|
onChange: () => applyLogoChoice(),
|
|
985
987
|
});
|
|
986
988
|
const choiceShapePick = selectField("design-logo-shape", "Shape to draw", {
|
|
987
989
|
empty: "Default (full; icon for bullets)",
|
|
988
|
-
help: "
|
|
990
|
+
help: "A missing shape falls back to the full logo",
|
|
989
991
|
onChange: () => applyLogoChoice(),
|
|
990
992
|
});
|
|
991
993
|
choiceShapePick.setOptions(LOGO_SHAPES.map((value) => ({ value, label: LOGO_SHAPE_LABELS[value] })));
|
|
@@ -1018,16 +1020,17 @@ export function createDesignControls(container, options = {}) {
|
|
|
1018
1020
|
};
|
|
1019
1021
|
const bullet = selectField("list-bullet", "List bullets", {
|
|
1020
1022
|
empty: "Default (character)",
|
|
1021
|
-
help: "Picture bullets
|
|
1022
|
-
onChange: (value) => run(() => setDesignOption(editor, "listBullet", value === "" ? null : value, scoped()), value === "image" ? "Picture bullets on." : "Character bullets."),
|
|
1023
|
+
help: "Picture bullets need a logo",
|
|
1024
|
+
onChange: (value) => run(() => setDesignOption(editor, "listBullet", value === "" ? null : value, scoped()), value === "image" ? "Picture bullets on." : value === "none" ? "No bullets." : "Character bullets."),
|
|
1023
1025
|
});
|
|
1024
1026
|
bullet.setOptions([
|
|
1025
1027
|
{ value: "character", label: "Character" },
|
|
1026
1028
|
{ value: "image", label: "Picture (the logo)" },
|
|
1029
|
+
{ value: "none", label: "None (no bullets)" },
|
|
1027
1030
|
]);
|
|
1028
1031
|
const accent = textField("accent-font", "Accent font", {
|
|
1029
1032
|
placeholder: "Font family name",
|
|
1030
|
-
help: "
|
|
1033
|
+
help: "For accent text such as tags. Enter applies.",
|
|
1031
1034
|
onCommit: (value) => run(() => setDesignOption(editor, "accentFont", value.trim() === "" ? null : value.trim(), scoped()), value.trim() === "" ? "Accent font removed." : "Accent font changed."),
|
|
1032
1035
|
});
|
|
1033
1036
|
const watermarkSource = imageCluster("watermark", "Watermark", {
|
|
@@ -1040,7 +1043,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1040
1043
|
// FA-13: a watermark is an image or a text stamp; the text field and the image cluster replace each other.
|
|
1041
1044
|
const watermarkText = textField("watermark-text", "Watermark text", {
|
|
1042
1045
|
placeholder: "DRAFT",
|
|
1043
|
-
help: "A text stamp
|
|
1046
|
+
help: "A diagonal text stamp; replaces a watermark image",
|
|
1044
1047
|
onCommit: (value) => run(() => {
|
|
1045
1048
|
const text = value.trim();
|
|
1046
1049
|
const current = getDesignOption(editor.presentation, "watermark", scoped()).value;
|
|
@@ -1049,13 +1052,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
1049
1052
|
return setDesignOption(editor, "watermark", { text, opacity }, scoped());
|
|
1050
1053
|
}, value.trim() === "" ? "Watermark removed." : "Watermark text set."),
|
|
1051
1054
|
});
|
|
1052
|
-
const watermarkOpacity = numberField("watermark-opacity", "Watermark opacity
|
|
1055
|
+
const watermarkOpacity = numberField("watermark-opacity", "Watermark opacity", {
|
|
1053
1056
|
min: 0,
|
|
1054
1057
|
max: 1,
|
|
1055
1058
|
step: 0.05,
|
|
1056
1059
|
onCommit: (value) => run(() => setDesignOption(editor, "watermark", { opacity: value === null ? 0.1 : value }, scoped()), "Watermark opacity changed."),
|
|
1057
1060
|
});
|
|
1058
|
-
const watermarkOff = checkField("watermark-off", "Hide
|
|
1061
|
+
const watermarkOff = checkField("watermark-off", "Hide inherited watermark", {
|
|
1059
1062
|
onChange: (checked) => run(() => setDesignOption(editor, "watermark", checked ? false : null, scoped()), checked ? "Watermark hidden." : "Watermark restored."),
|
|
1060
1063
|
});
|
|
1061
1064
|
body.append(organizationLogoGroup, logoChoiceGroup, bullet.wrap, accent.wrap, watermarkSource.wrap, watermarkText.wrap, watermarkOpacity.wrap, watermarkOff.wrap);
|
|
@@ -1101,9 +1104,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
1101
1104
|
};
|
|
1102
1105
|
enumField("titleAlignment", "Title alignment", ["left", "center", "right"], {});
|
|
1103
1106
|
enumField("contentAlignment", "Content alignment", ["left", "center", "right"], {});
|
|
1104
|
-
enumField("contentDirection", "Content direction", ["horizontal", "vertical"], { horizontal: "Horizontal (row)", vertical: "Vertical (column)" }, "
|
|
1105
|
-
enumField("chartPrimary", "Primary chart position", ["none", "top", "bottom", "left", "right"], { none: "None (equal)" }, "Where the main chart sits
|
|
1106
|
-
enumField("imageFit", "Image fit", IMAGE_FITS, { cover: "Cover (crop to fill)", contain: "Contain (whole image)", stretch: "Stretch to the frame" }, "
|
|
1107
|
+
enumField("contentDirection", "Content direction", ["horizontal", "vertical"], { horizontal: "Horizontal (row)", vertical: "Vertical (column)" }, "When a slide sets no arrangement");
|
|
1108
|
+
enumField("chartPrimary", "Primary chart position", ["none", "top", "bottom", "left", "right"], { none: "None (equal)" }, "Where the main chart sits");
|
|
1109
|
+
enumField("imageFit", "Image fit", IMAGE_FITS, { cover: "Cover (crop to fill)", contain: "Contain (whole image)", stretch: "Stretch to the frame" }, "When an image block sets no fit");
|
|
1107
1110
|
const box = selectField("opt-contentBox", "Content box", {
|
|
1108
1111
|
empty: "Default",
|
|
1109
1112
|
onChange: (value) => run(() => setDesignOption(editor, "contentBox", value === "" ? null : value === "yes", scoped()), "Content box changed."),
|
|
@@ -1131,13 +1134,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
1131
1134
|
field.set(currentSwitchValue(editor.presentation, dimension).value);
|
|
1132
1135
|
});
|
|
1133
1136
|
};
|
|
1134
|
-
single("narrative", "Narrative", "narratives", "
|
|
1137
|
+
single("narrative", "Narrative", "narratives", "Authoring metadata; slides do not change");
|
|
1135
1138
|
single("tone", "Tone", "tones");
|
|
1136
|
-
single("purpose", "Purpose", "purposes", "
|
|
1139
|
+
single("purpose", "Purpose", "purposes", "Authoring metadata; slides do not change");
|
|
1137
1140
|
const audience = selectField("audience", "Audience", {
|
|
1138
1141
|
multiple: true,
|
|
1139
1142
|
size: 6,
|
|
1140
|
-
help: "
|
|
1143
|
+
help: "Ctrl or Shift selects several",
|
|
1141
1144
|
onChange: (values) => {
|
|
1142
1145
|
if (!values.length) return complain("Choose at least one audience.");
|
|
1143
1146
|
run(() => switchDimension(editor, "audiences", values, catalogOptions()), "Audience changed.");
|
|
@@ -1154,7 +1157,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1154
1157
|
|
|
1155
1158
|
const owner = selectField("social-owner", "Socials belong to");
|
|
1156
1159
|
const platform = selectField("social-platform", "Platform");
|
|
1157
|
-
const handle = textField("social-handle", "Handle or address", { placeholder: "@handle"
|
|
1160
|
+
const handle = textField("social-handle", "Handle or address", { placeholder: "@handle" });
|
|
1158
1161
|
const applySocial = () => {
|
|
1159
1162
|
const value = handle.input.value.trim();
|
|
1160
1163
|
if (!value) return complain("Enter a handle for the platform.");
|
|
@@ -1183,20 +1186,17 @@ export function createDesignControls(container, options = {}) {
|
|
|
1183
1186
|
const dynamic = {};
|
|
1184
1187
|
if (built.selection) {
|
|
1185
1188
|
const { body, details } = built.selection;
|
|
1186
|
-
const summary = h("p", { class: "opf-dc-help", "data-role": "selection-summary" });
|
|
1187
1189
|
const convert = selectField("convert", "Content type", {
|
|
1188
|
-
help: "Converting keeps your text and never adds content. What a type cannot carry is named below.",
|
|
1189
1190
|
onChange: (value) => {
|
|
1190
1191
|
if (!value) return;
|
|
1191
1192
|
const path = dynamic.blockPath;
|
|
1192
|
-
run(() => switchDimension(editor, "blocks", value, { path, convert: true }), (change) => `Converted to ${BLOCK_KIND_LABELS[value]?.toLowerCase() ?? value}.${change.loss?.length ? ` Not carried over: ${change.loss.join(", ")}.` : "
|
|
1193
|
+
run(() => switchDimension(editor, "blocks", value, { path, convert: true }), (change) => `Converted to ${BLOCK_KIND_LABELS[value]?.toLowerCase() ?? value}.${change.loss?.length ? ` Not carried over: ${change.loss.join(", ")}.` : ""}`);
|
|
1193
1194
|
reselect(path);
|
|
1194
1195
|
},
|
|
1195
1196
|
});
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
help: "Starts from sample content. The old content is discarded; Undo brings it back.",
|
|
1197
|
+
const replace = selectField("replace", "Replace with", {
|
|
1198
|
+
empty: "New content…",
|
|
1199
|
+
help: "Replaces this content with a sample",
|
|
1200
1200
|
onChange: (value) => {
|
|
1201
1201
|
if (!value) return;
|
|
1202
1202
|
const path = dynamic.blockPath;
|
|
@@ -1206,11 +1206,20 @@ export function createDesignControls(container, options = {}) {
|
|
|
1206
1206
|
});
|
|
1207
1207
|
replace.setOptions(BLOCK_REPLACEMENTS.map((value) => ({ value, label: BLOCK_KIND_LABELS[value] })));
|
|
1208
1208
|
const chartType = selectField("chart-type", "Chart type", {
|
|
1209
|
-
help: "
|
|
1209
|
+
help: "Types this data supports",
|
|
1210
1210
|
onChange: (value) => run(() => switchDimension(editor, "charts", value, { slideIndex: getSlide(), path: dynamic.blockPath, ...catalogOptions() }), `Chart type set to ${chartType.select.selectedOptions[0]?.textContent ?? value}.`),
|
|
1211
1211
|
});
|
|
1212
|
-
|
|
1213
|
-
|
|
1212
|
+
// The block's path is internal: it is a copyable chip under Advanced (a host with its own Advanced, such as the Studio, turns it off).
|
|
1213
|
+
const pathChip = h("button", { type: "button", class: "opf-dc-path", "data-role": "selection-path", title: "Copy path" });
|
|
1214
|
+
pathChip.addEventListener("click", () => {
|
|
1215
|
+
const path = pathChip.dataset.path;
|
|
1216
|
+
const clipboard = doc.defaultView?.navigator?.clipboard;
|
|
1217
|
+
if (!path || !clipboard?.writeText) return complain("Copying is not available here.");
|
|
1218
|
+
clipboard.writeText(path).then(() => say(`Copied ${path}.`), () => complain("Could not copy the path."));
|
|
1219
|
+
});
|
|
1220
|
+
const advanced = h("details", { class: "opf-dc-advanced", "data-role": "selection-advanced", hidden: options.showPath === false || undefined }, h("summary", {}, "Advanced"), pathChip);
|
|
1221
|
+
body.append(contentControls.nodes.arrange, contentControls.nodes.list, convert.wrap, contentControls.nodes.metrics, chartType.wrap, replace.wrap, contentControls.nodes.image, advanced);
|
|
1222
|
+
dynamic.selection = { details, convert, replace, chartType, pathChip, advanced };
|
|
1214
1223
|
}
|
|
1215
1224
|
|
|
1216
1225
|
if (built["slide-content"]) built["slide-content"].body.append(contentControls.slideNode);
|
|
@@ -1219,11 +1228,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
1219
1228
|
|
|
1220
1229
|
if (built.image) {
|
|
1221
1230
|
const { body, details } = built.image;
|
|
1222
|
-
const summary = h("p", { class: "opf-dc-help", "data-role": "image-summary" });
|
|
1223
1231
|
const treat = (fields, done) => run(() => setImageTreatment(editor, dynamic.imagePath, fields), done);
|
|
1224
1232
|
const fit = selectField("img-fit", "Fit", {
|
|
1225
1233
|
empty: "Presentation default",
|
|
1226
|
-
help: "Cover
|
|
1234
|
+
help: "Cover crops to fill; contain shows it all; stretch distorts",
|
|
1227
1235
|
onChange: (value) => treat({ fit: value === "" ? null : value }, "Image fit changed."),
|
|
1228
1236
|
});
|
|
1229
1237
|
fit.setOptions(IMAGE_FITS.map((value) => ({ value, label: titleCase(value) })));
|
|
@@ -1231,20 +1239,20 @@ export function createDesignControls(container, options = {}) {
|
|
|
1231
1239
|
const x = focusX.input.value, y = focusY.input.value;
|
|
1232
1240
|
treat({ focus: x === "" && y === "" ? null : { x: x === "" ? 0.5 : Number(x), y: y === "" ? 0.5 : Number(y) } }, "Focus point changed.");
|
|
1233
1241
|
};
|
|
1234
|
-
const focusX = numberField("img-focus-x", "Focus
|
|
1235
|
-
const focusY = numberField("img-focus-y", "Focus
|
|
1242
|
+
const focusX = numberField("img-focus-x", "Focus X", { min: 0, max: 1, step: 0.05, help: "0 left to 1 right; the point a cover fit keeps in view", onCommit: focusCommit });
|
|
1243
|
+
const focusY = numberField("img-focus-y", "Focus Y", { min: 0, max: 1, step: 0.05, help: "0 top to 1 bottom", onCommit: focusCommit });
|
|
1236
1244
|
const shape = selectField("img-shape", "Shape", { empty: "Rectangle", onChange: (value) => treat({ shape: value === "" ? null : value }, "Image shape changed.") });
|
|
1237
1245
|
shape.setOptions(IMAGE_SHAPES.filter((value) => value !== "rectangle").map((value) => ({ value, label: titleCase(value) })));
|
|
1238
|
-
const radius = numberField("img-radius", "Corner radius
|
|
1239
|
-
const aspect = numberField("img-aspect", "
|
|
1240
|
-
const opacity = numberField("img-opacity", "Opacity
|
|
1246
|
+
const radius = numberField("img-radius", "Corner radius", { min: 0, max: 0.5, step: 0.02, help: "Share of the shorter side", onCommit: (value) => treat({ cornerRadius: value }, "Corner radius changed.") });
|
|
1247
|
+
const aspect = numberField("img-aspect", "Aspect ratio", { min: 0.1, max: 10, step: 0.01, placeholder: "width / height", help: "1.78 for 16:9, 1 for a square. Clear to fill the region.", onCommit: (value) => treat({ aspectRatio: value }, "Frame aspect ratio changed.") });
|
|
1248
|
+
const opacity = numberField("img-opacity", "Opacity", { min: 0, max: 1, step: 0.05, onCommit: (value) => treat({ opacity: value }, "Image opacity changed.") });
|
|
1241
1249
|
const borderCommit = () => {
|
|
1242
1250
|
const ref = borderColor.input.value.trim();
|
|
1243
1251
|
treat({ border: ref ? { color: ref, width: borderWidth.input.value === "" ? 2 : Number(borderWidth.input.value) } : null }, ref ? "Border changed." : "Border removed.");
|
|
1244
1252
|
};
|
|
1245
|
-
const borderColor = textField("img-border-color", "Border color", { list: true, placeholder: "dark1 or #1F2937", help: "Clear
|
|
1253
|
+
const borderColor = textField("img-border-color", "Border color", { list: true, placeholder: "dark1 or #1F2937", help: "Clear to remove", onCommit: borderCommit });
|
|
1246
1254
|
borderColor.setList(COLOR_NAMES);
|
|
1247
|
-
const borderWidth = numberField("img-border-width", "Border width
|
|
1255
|
+
const borderWidth = numberField("img-border-width", "Border width", { min: 0, max: 64, step: 1, onCommit: borderCommit });
|
|
1248
1256
|
const recolorCommit = () => {
|
|
1249
1257
|
const kind = recolor.select.value;
|
|
1250
1258
|
if (kind === "duotone" && (!duoDark.input.value.trim() || !duoLight.input.value.trim())) return say("Enter the dark and light duotone colors to apply it.");
|
|
@@ -1268,19 +1276,18 @@ export function createDesignControls(container, options = {}) {
|
|
|
1268
1276
|
);
|
|
1269
1277
|
};
|
|
1270
1278
|
const placeEdge = selectField("img-place-edge", "Bleed to an edge", {
|
|
1271
|
-
empty: "
|
|
1272
|
-
help: "
|
|
1279
|
+
empty: "None",
|
|
1280
|
+
help: "Takes a band along that slide edge",
|
|
1273
1281
|
onChange: placementCommit,
|
|
1274
1282
|
});
|
|
1275
|
-
const placeSize = numberField("img-place-size", "
|
|
1276
|
-
const placeInset = checkField("img-place-inset", "Inset
|
|
1283
|
+
const placeSize = numberField("img-place-size", "Band size", { min: 0.1, max: 0.9, step: 0.05, onCommit: placementCommit });
|
|
1284
|
+
const placeInset = checkField("img-place-inset", "Inset in padding", { onChange: placementCommit });
|
|
1277
1285
|
const placement = h("fieldset", { class: "opf-dc-fieldset", "data-role": "img-placement" }, h("legend", {}, "Placement"), placeEdge.wrap, placeSize.wrap, placeInset.wrap);
|
|
1278
|
-
body.append(
|
|
1286
|
+
body.append(fit.wrap, focusX.wrap, focusY.wrap, shape.wrap, radius.wrap, aspect.wrap, opacity.wrap, borderColor.wrap, borderWidth.wrap, recolor.wrap, duoDark.wrap, duoLight.wrap, overlay.wrap, placement);
|
|
1279
1287
|
dynamic.image = {
|
|
1280
1288
|
details,
|
|
1281
1289
|
load(path) {
|
|
1282
1290
|
const current = readImageTreatments(editor.presentation, path);
|
|
1283
|
-
summary.textContent = `Image block (${current.path})`;
|
|
1284
1291
|
fit.set(current.fit ?? "");
|
|
1285
1292
|
focusX.set(current.focus ? String(current.focus.x) : "");
|
|
1286
1293
|
focusY.set(current.focus ? String(current.focus.y) : "");
|
|
@@ -1298,7 +1305,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1298
1305
|
for (const field of [duoDark, duoLight]) field.wrap.hidden = !(duotone || recolor.select.value === "duotone");
|
|
1299
1306
|
overlay.set(current.overlay);
|
|
1300
1307
|
placement.hidden = !current.placeable;
|
|
1301
|
-
placeEdge.setOptions(IMAGE_EDGES.map((edge) => ({ value: edge, label: titleCase(edge), disabled: current.usedEdges.includes(edge), title: current.usedEdges.includes(edge) ? "Another image
|
|
1308
|
+
placeEdge.setOptions(IMAGE_EDGES.map((edge) => ({ value: edge, label: titleCase(edge), disabled: current.usedEdges.includes(edge), title: current.usedEdges.includes(edge) ? "Another image bleeds to this edge" : undefined })));
|
|
1302
1309
|
placeEdge.set(current.placement?.edge ?? "");
|
|
1303
1310
|
placeSize.set(current.placement?.size === undefined ? "" : String(current.placement.size));
|
|
1304
1311
|
placeInset.set(current.placement?.inset === true);
|
|
@@ -1312,7 +1319,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
1312
1319
|
if (built.catalog) {
|
|
1313
1320
|
const { body } = built.catalog;
|
|
1314
1321
|
const findings = h("ul", { class: "opf-dc-warnings", "aria-label": "Catalog reference problems", "data-role": "reference-findings" });
|
|
1315
|
-
const intro = h("p", { class: "opf-dc-help" }, "The document embeds every catalog record it uses when it is saved. Updating replaces an embedded record with the catalog's current one, only after you approve it.");
|
|
1316
1322
|
const updates = h("ul", { class: "opf-dc-summary", "aria-label": "Catalog updates", "data-role": "catalog-updates" });
|
|
1317
1323
|
const review = { changes: [] };
|
|
1318
1324
|
const check = button("Check for catalog updates", () => {
|
|
@@ -1346,7 +1352,8 @@ export function createDesignControls(container, options = {}) {
|
|
|
1346
1352
|
applyButton.hidden = review.changes.length === 0;
|
|
1347
1353
|
};
|
|
1348
1354
|
renderUpdates();
|
|
1349
|
-
|
|
1355
|
+
check.title = "Compares embedded records with the catalog; nothing changes until you apply";
|
|
1356
|
+
body.append(findings, h("div", { class: "opf-dc-actions" }, check, applyButton), updates);
|
|
1350
1357
|
syncs.push(() => {
|
|
1351
1358
|
const list = referenceFindings(editor.validation);
|
|
1352
1359
|
findings.replaceChildren(
|
|
@@ -1372,10 +1379,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
1372
1379
|
|
|
1373
1380
|
if (built.table) {
|
|
1374
1381
|
const { body, details } = built.table;
|
|
1375
|
-
const summary = h("p", { class: "opf-dc-help", "data-role": "table-summary" });
|
|
1376
1382
|
const applyStyle = (change, done) => run(() => setTableStyle(editor, dynamic.tablePath, change), done);
|
|
1377
1383
|
const headerStyle = selectField("table-header", "Header row", {
|
|
1378
|
-
help: "
|
|
1384
|
+
help: "Theme uses the primary color",
|
|
1379
1385
|
onChange: (value) => applyStyle({ ...dynamic.tableStyle, header: value }, "Table header changed."),
|
|
1380
1386
|
});
|
|
1381
1387
|
headerStyle.setOptions([
|
|
@@ -1384,7 +1390,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
1384
1390
|
{ value: "accent", label: "Accent color" },
|
|
1385
1391
|
]);
|
|
1386
1392
|
const banding = checkField("table-banding", "Banded rows", {
|
|
1387
|
-
help: "Alternates the fill of body rows. Text color adjusts automatically.",
|
|
1388
1393
|
onChange: (checked) => applyStyle({ ...dynamic.tableStyle, banding: checked }, checked ? "Banded rows on." : "Banded rows off."),
|
|
1389
1394
|
});
|
|
1390
1395
|
const borders = selectField("table-borders", "Borders", {
|
|
@@ -1397,10 +1402,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
1397
1402
|
{ value: "none", label: "None" },
|
|
1398
1403
|
]);
|
|
1399
1404
|
const resetStyle = button("Reset table style", () => applyStyle("theme", "Table style reset to the theme."), { quiet: true });
|
|
1400
|
-
const cellLabel = h("p", { class: "opf-dc-
|
|
1405
|
+
const cellLabel = h("p", { class: "opf-dc-cell-label", "data-role": "cell-summary" });
|
|
1401
1406
|
const columns = numberField("span-columns", "Columns to merge", { min: 1, step: 1 });
|
|
1402
1407
|
const rows = numberField("span-rows", "Rows to merge", { min: 1, step: 1 });
|
|
1403
|
-
const join = checkField("merge-join", "Keep
|
|
1408
|
+
const join = checkField("merge-join", "Keep merged text", { help: "Otherwise merging cells that hold text is refused" });
|
|
1404
1409
|
const merge = button("Merge cells", () => {
|
|
1405
1410
|
const cell = dynamic.cell;
|
|
1406
1411
|
if (!cell) return;
|
|
@@ -1418,7 +1423,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1418
1423
|
const cellBox = h("div", { class: "opf-dc-cell" }, cellLabel, columns.wrap, rows.wrap, join.wrap, h("div", { class: "opf-dc-actions" }, merge, split), fill.wrap, align.wrap, bold.wrap);
|
|
1419
1424
|
// FA-27: the table's text alternative, like a chart's. It belongs to the table, so a dataset-backed table has it too.
|
|
1420
1425
|
const alt = textField("table-alt", "Alt text", {
|
|
1421
|
-
help: "
|
|
1426
|
+
help: "Its point and key numbers. Enter applies.",
|
|
1422
1427
|
placeholder: "What the table shows",
|
|
1423
1428
|
onCommit: (value) => run(() => setTableAlt(editor, dynamic.tablePath, { alt: value }), "Table alt text changed."),
|
|
1424
1429
|
});
|
|
@@ -1427,8 +1432,8 @@ export function createDesignControls(container, options = {}) {
|
|
|
1427
1432
|
onChange: (checked) => run(() => setTableAlt(editor, dynamic.tablePath, { decorative: checked }), checked ? "Table marked decorative." : "The table is no longer decorative."),
|
|
1428
1433
|
});
|
|
1429
1434
|
const styleControls = [headerStyle.wrap, banding.wrap, borders.wrap, resetStyle, cellBox];
|
|
1430
|
-
body.append(
|
|
1431
|
-
dynamic.table = { details,
|
|
1435
|
+
body.append(alt.wrap, decorative.wrap, ...styleControls);
|
|
1436
|
+
dynamic.table = { details, alt, decorative, styleControls, headerStyle, banding, borders, cellBox, cellLabel, columns, rows, merge, split, fill, align, bold, join };
|
|
1432
1437
|
}
|
|
1433
1438
|
|
|
1434
1439
|
// --- sync -----------------------------------------------------------------------------------------
|
|
@@ -1439,24 +1444,31 @@ export function createDesignControls(container, options = {}) {
|
|
|
1439
1444
|
const blockPath = selected ? blockPathForSelection(document_, selected) : undefined;
|
|
1440
1445
|
const content = blockPath ? readBlockContent(document_, blockPath) : undefined;
|
|
1441
1446
|
dynamic.blockPath = blockPath;
|
|
1447
|
+
// A selected group has no content of its own, but it can be arranged (grouped with the next block, ungrouped).
|
|
1448
|
+
const groupPath = !blockPath && selected && isGroupBlock(document_, selected) ? selected : undefined;
|
|
1442
1449
|
const ui = dynamic.selection;
|
|
1443
1450
|
if (ui) {
|
|
1444
|
-
ui.details.hidden = !content;
|
|
1451
|
+
ui.details.hidden = !content && !groupPath;
|
|
1452
|
+
const path = blockPath ?? groupPath;
|
|
1453
|
+
ui.pathChip.dataset.path = path ?? "";
|
|
1454
|
+
ui.pathChip.replaceChildren(h("code", {}, path ?? ""), iconNode(doc, "Copy", 14));
|
|
1455
|
+
ui.pathChip.setAttribute("aria-label", `Copy path ${path ?? ""}`);
|
|
1456
|
+
for (const field of [ui.convert, ui.replace]) field.wrap.hidden = !content;
|
|
1457
|
+
ui.chartType.wrap.hidden = true;
|
|
1445
1458
|
if (content) {
|
|
1446
1459
|
const targets = blockConversionTargets(document_, blockPath);
|
|
1447
|
-
|
|
1460
|
+
// Every type is listed; one this content cannot become is disabled, with the reason as its tooltip and accessible description.
|
|
1448
1461
|
ui.convert.setOptions([
|
|
1449
1462
|
{ value: "", label: `${BLOCK_KIND_LABELS[content.kind] ?? content.kind} (current)` },
|
|
1450
1463
|
...targets.map((target) => ({
|
|
1451
1464
|
value: target.kind,
|
|
1452
|
-
label: target.available ? `${target.label}${target.lossless ? "" : ` (loses ${target.loss.join(", ")})`}` :
|
|
1465
|
+
label: target.available ? `${target.label}${target.lossless ? "" : ` (loses ${target.loss.join(", ")})`}` : target.label,
|
|
1453
1466
|
disabled: !target.available,
|
|
1454
|
-
title: target.reason
|
|
1467
|
+
title: target.available ? undefined : `${target.label}: ${shortReason(target.reason)}`,
|
|
1455
1468
|
})),
|
|
1456
1469
|
]);
|
|
1457
1470
|
ui.convert.set("");
|
|
1458
1471
|
ui.convert.wrap.hidden = targets.length === 0;
|
|
1459
|
-
ui.unavailable.replaceChildren(...targets.filter((target) => !target.available).map((target) => h("li", {}, `${target.label}: ${target.reason}`)));
|
|
1460
1472
|
ui.replace.set("");
|
|
1461
1473
|
const charts = content.kind === "chart" ? compatibleChartTypes(document_, { slideIndex: getSlide(), path: blockPath, ...catalogOptions() }) : [];
|
|
1462
1474
|
ui.chartType.wrap.hidden = charts.length === 0;
|
|
@@ -1485,7 +1497,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
1485
1497
|
const inline = Array.isArray(tableValue?.rows);
|
|
1486
1498
|
tableUi.details.hidden = !tablePath || !(inline || typeof tableValue?.dataset === "string");
|
|
1487
1499
|
if (tablePath && !tableUi.details.hidden) {
|
|
1488
|
-
tableUi.summary.textContent = `Table (${tablePath})`;
|
|
1489
1500
|
const textAlternative = readTableAlt(tableValue);
|
|
1490
1501
|
tableUi.alt.set(textAlternative.alt);
|
|
1491
1502
|
tableUi.alt.input.disabled = textAlternative.decorative;
|
|
@@ -1525,7 +1536,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1525
1536
|
}
|
|
1526
1537
|
}
|
|
1527
1538
|
if (contentControls) {
|
|
1528
|
-
contentControls.sync({ blockPath, content, selectedPath: selected, slideIndex: getSlide() });
|
|
1539
|
+
contentControls.sync({ blockPath, groupPath, content, selectedPath: selected, slideIndex: getSlide() });
|
|
1529
1540
|
const slideContent = built["slide-content"];
|
|
1530
1541
|
if (slideContent) slideContent.details.hidden = [...contentControls.slideNode.children].every((node) => node.hidden);
|
|
1531
1542
|
}
|