@openpresentation/opf-editor 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +97 -9
- package/dist/workspace.js +325 -59
- package/package.json +29 -18
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,7 +1020,7 @@ 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
|
|
1023
|
+
help: "Picture bullets need a logo",
|
|
1022
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([
|
|
@@ -1028,7 +1030,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1028
1030
|
]);
|
|
1029
1031
|
const accent = textField("accent-font", "Accent font", {
|
|
1030
1032
|
placeholder: "Font family name",
|
|
1031
|
-
help: "
|
|
1033
|
+
help: "For accent text such as tags. Enter applies.",
|
|
1032
1034
|
onCommit: (value) => run(() => setDesignOption(editor, "accentFont", value.trim() === "" ? null : value.trim(), scoped()), value.trim() === "" ? "Accent font removed." : "Accent font changed."),
|
|
1033
1035
|
});
|
|
1034
1036
|
const watermarkSource = imageCluster("watermark", "Watermark", {
|
|
@@ -1041,7 +1043,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1041
1043
|
// FA-13: a watermark is an image or a text stamp; the text field and the image cluster replace each other.
|
|
1042
1044
|
const watermarkText = textField("watermark-text", "Watermark text", {
|
|
1043
1045
|
placeholder: "DRAFT",
|
|
1044
|
-
help: "A text stamp
|
|
1046
|
+
help: "A diagonal text stamp; replaces a watermark image",
|
|
1045
1047
|
onCommit: (value) => run(() => {
|
|
1046
1048
|
const text = value.trim();
|
|
1047
1049
|
const current = getDesignOption(editor.presentation, "watermark", scoped()).value;
|
|
@@ -1050,13 +1052,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
1050
1052
|
return setDesignOption(editor, "watermark", { text, opacity }, scoped());
|
|
1051
1053
|
}, value.trim() === "" ? "Watermark removed." : "Watermark text set."),
|
|
1052
1054
|
});
|
|
1053
|
-
const watermarkOpacity = numberField("watermark-opacity", "Watermark opacity
|
|
1055
|
+
const watermarkOpacity = numberField("watermark-opacity", "Watermark opacity", {
|
|
1054
1056
|
min: 0,
|
|
1055
1057
|
max: 1,
|
|
1056
1058
|
step: 0.05,
|
|
1057
1059
|
onCommit: (value) => run(() => setDesignOption(editor, "watermark", { opacity: value === null ? 0.1 : value }, scoped()), "Watermark opacity changed."),
|
|
1058
1060
|
});
|
|
1059
|
-
const watermarkOff = checkField("watermark-off", "Hide
|
|
1061
|
+
const watermarkOff = checkField("watermark-off", "Hide inherited watermark", {
|
|
1060
1062
|
onChange: (checked) => run(() => setDesignOption(editor, "watermark", checked ? false : null, scoped()), checked ? "Watermark hidden." : "Watermark restored."),
|
|
1061
1063
|
});
|
|
1062
1064
|
body.append(organizationLogoGroup, logoChoiceGroup, bullet.wrap, accent.wrap, watermarkSource.wrap, watermarkText.wrap, watermarkOpacity.wrap, watermarkOff.wrap);
|
|
@@ -1102,9 +1104,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
1102
1104
|
};
|
|
1103
1105
|
enumField("titleAlignment", "Title alignment", ["left", "center", "right"], {});
|
|
1104
1106
|
enumField("contentAlignment", "Content alignment", ["left", "center", "right"], {});
|
|
1105
|
-
enumField("contentDirection", "Content direction", ["horizontal", "vertical"], { horizontal: "Horizontal (row)", vertical: "Vertical (column)" }, "
|
|
1106
|
-
enumField("chartPrimary", "Primary chart position", ["none", "top", "bottom", "left", "right"], { none: "None (equal)" }, "Where the main chart sits
|
|
1107
|
-
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");
|
|
1108
1110
|
const box = selectField("opt-contentBox", "Content box", {
|
|
1109
1111
|
empty: "Default",
|
|
1110
1112
|
onChange: (value) => run(() => setDesignOption(editor, "contentBox", value === "" ? null : value === "yes", scoped()), "Content box changed."),
|
|
@@ -1132,13 +1134,13 @@ export function createDesignControls(container, options = {}) {
|
|
|
1132
1134
|
field.set(currentSwitchValue(editor.presentation, dimension).value);
|
|
1133
1135
|
});
|
|
1134
1136
|
};
|
|
1135
|
-
single("narrative", "Narrative", "narratives", "
|
|
1137
|
+
single("narrative", "Narrative", "narratives", "Authoring metadata; slides do not change");
|
|
1136
1138
|
single("tone", "Tone", "tones");
|
|
1137
|
-
single("purpose", "Purpose", "purposes", "
|
|
1139
|
+
single("purpose", "Purpose", "purposes", "Authoring metadata; slides do not change");
|
|
1138
1140
|
const audience = selectField("audience", "Audience", {
|
|
1139
1141
|
multiple: true,
|
|
1140
1142
|
size: 6,
|
|
1141
|
-
help: "
|
|
1143
|
+
help: "Ctrl or Shift selects several",
|
|
1142
1144
|
onChange: (values) => {
|
|
1143
1145
|
if (!values.length) return complain("Choose at least one audience.");
|
|
1144
1146
|
run(() => switchDimension(editor, "audiences", values, catalogOptions()), "Audience changed.");
|
|
@@ -1155,7 +1157,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1155
1157
|
|
|
1156
1158
|
const owner = selectField("social-owner", "Socials belong to");
|
|
1157
1159
|
const platform = selectField("social-platform", "Platform");
|
|
1158
|
-
const handle = textField("social-handle", "Handle or address", { placeholder: "@handle"
|
|
1160
|
+
const handle = textField("social-handle", "Handle or address", { placeholder: "@handle" });
|
|
1159
1161
|
const applySocial = () => {
|
|
1160
1162
|
const value = handle.input.value.trim();
|
|
1161
1163
|
if (!value) return complain("Enter a handle for the platform.");
|
|
@@ -1184,20 +1186,17 @@ export function createDesignControls(container, options = {}) {
|
|
|
1184
1186
|
const dynamic = {};
|
|
1185
1187
|
if (built.selection) {
|
|
1186
1188
|
const { body, details } = built.selection;
|
|
1187
|
-
const summary = h("p", { class: "opf-dc-help", "data-role": "selection-summary" });
|
|
1188
1189
|
const convert = selectField("convert", "Content type", {
|
|
1189
|
-
help: "Converting keeps your text and never adds content. What a type cannot carry is named below.",
|
|
1190
1190
|
onChange: (value) => {
|
|
1191
1191
|
if (!value) return;
|
|
1192
1192
|
const path = dynamic.blockPath;
|
|
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
|
+
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(", ")}.` : ""}`);
|
|
1194
1194
|
reselect(path);
|
|
1195
1195
|
},
|
|
1196
1196
|
});
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
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",
|
|
1201
1200
|
onChange: (value) => {
|
|
1202
1201
|
if (!value) return;
|
|
1203
1202
|
const path = dynamic.blockPath;
|
|
@@ -1207,11 +1206,20 @@ export function createDesignControls(container, options = {}) {
|
|
|
1207
1206
|
});
|
|
1208
1207
|
replace.setOptions(BLOCK_REPLACEMENTS.map((value) => ({ value, label: BLOCK_KIND_LABELS[value] })));
|
|
1209
1208
|
const chartType = selectField("chart-type", "Chart type", {
|
|
1210
|
-
help: "
|
|
1209
|
+
help: "Types this data supports",
|
|
1211
1210
|
onChange: (value) => run(() => switchDimension(editor, "charts", value, { slideIndex: getSlide(), path: dynamic.blockPath, ...catalogOptions() }), `Chart type set to ${chartType.select.selectedOptions[0]?.textContent ?? value}.`),
|
|
1212
1211
|
});
|
|
1213
|
-
|
|
1214
|
-
|
|
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 };
|
|
1215
1223
|
}
|
|
1216
1224
|
|
|
1217
1225
|
if (built["slide-content"]) built["slide-content"].body.append(contentControls.slideNode);
|
|
@@ -1220,11 +1228,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
1220
1228
|
|
|
1221
1229
|
if (built.image) {
|
|
1222
1230
|
const { body, details } = built.image;
|
|
1223
|
-
const summary = h("p", { class: "opf-dc-help", "data-role": "image-summary" });
|
|
1224
1231
|
const treat = (fields, done) => run(() => setImageTreatment(editor, dynamic.imagePath, fields), done);
|
|
1225
1232
|
const fit = selectField("img-fit", "Fit", {
|
|
1226
1233
|
empty: "Presentation default",
|
|
1227
|
-
help: "Cover
|
|
1234
|
+
help: "Cover crops to fill; contain shows it all; stretch distorts",
|
|
1228
1235
|
onChange: (value) => treat({ fit: value === "" ? null : value }, "Image fit changed."),
|
|
1229
1236
|
});
|
|
1230
1237
|
fit.setOptions(IMAGE_FITS.map((value) => ({ value, label: titleCase(value) })));
|
|
@@ -1232,20 +1239,20 @@ export function createDesignControls(container, options = {}) {
|
|
|
1232
1239
|
const x = focusX.input.value, y = focusY.input.value;
|
|
1233
1240
|
treat({ focus: x === "" && y === "" ? null : { x: x === "" ? 0.5 : Number(x), y: y === "" ? 0.5 : Number(y) } }, "Focus point changed.");
|
|
1234
1241
|
};
|
|
1235
|
-
const focusX = numberField("img-focus-x", "Focus
|
|
1236
|
-
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 });
|
|
1237
1244
|
const shape = selectField("img-shape", "Shape", { empty: "Rectangle", onChange: (value) => treat({ shape: value === "" ? null : value }, "Image shape changed.") });
|
|
1238
1245
|
shape.setOptions(IMAGE_SHAPES.filter((value) => value !== "rectangle").map((value) => ({ value, label: titleCase(value) })));
|
|
1239
|
-
const radius = numberField("img-radius", "Corner radius
|
|
1240
|
-
const aspect = numberField("img-aspect", "
|
|
1241
|
-
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.") });
|
|
1242
1249
|
const borderCommit = () => {
|
|
1243
1250
|
const ref = borderColor.input.value.trim();
|
|
1244
1251
|
treat({ border: ref ? { color: ref, width: borderWidth.input.value === "" ? 2 : Number(borderWidth.input.value) } : null }, ref ? "Border changed." : "Border removed.");
|
|
1245
1252
|
};
|
|
1246
|
-
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 });
|
|
1247
1254
|
borderColor.setList(COLOR_NAMES);
|
|
1248
|
-
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 });
|
|
1249
1256
|
const recolorCommit = () => {
|
|
1250
1257
|
const kind = recolor.select.value;
|
|
1251
1258
|
if (kind === "duotone" && (!duoDark.input.value.trim() || !duoLight.input.value.trim())) return say("Enter the dark and light duotone colors to apply it.");
|
|
@@ -1269,19 +1276,18 @@ export function createDesignControls(container, options = {}) {
|
|
|
1269
1276
|
);
|
|
1270
1277
|
};
|
|
1271
1278
|
const placeEdge = selectField("img-place-edge", "Bleed to an edge", {
|
|
1272
|
-
empty: "
|
|
1273
|
-
help: "
|
|
1279
|
+
empty: "None",
|
|
1280
|
+
help: "Takes a band along that slide edge",
|
|
1274
1281
|
onChange: placementCommit,
|
|
1275
1282
|
});
|
|
1276
|
-
const placeSize = numberField("img-place-size", "
|
|
1277
|
-
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 });
|
|
1278
1285
|
const placement = h("fieldset", { class: "opf-dc-fieldset", "data-role": "img-placement" }, h("legend", {}, "Placement"), placeEdge.wrap, placeSize.wrap, placeInset.wrap);
|
|
1279
|
-
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);
|
|
1280
1287
|
dynamic.image = {
|
|
1281
1288
|
details,
|
|
1282
1289
|
load(path) {
|
|
1283
1290
|
const current = readImageTreatments(editor.presentation, path);
|
|
1284
|
-
summary.textContent = `Image block (${current.path})`;
|
|
1285
1291
|
fit.set(current.fit ?? "");
|
|
1286
1292
|
focusX.set(current.focus ? String(current.focus.x) : "");
|
|
1287
1293
|
focusY.set(current.focus ? String(current.focus.y) : "");
|
|
@@ -1299,7 +1305,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1299
1305
|
for (const field of [duoDark, duoLight]) field.wrap.hidden = !(duotone || recolor.select.value === "duotone");
|
|
1300
1306
|
overlay.set(current.overlay);
|
|
1301
1307
|
placement.hidden = !current.placeable;
|
|
1302
|
-
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 })));
|
|
1303
1309
|
placeEdge.set(current.placement?.edge ?? "");
|
|
1304
1310
|
placeSize.set(current.placement?.size === undefined ? "" : String(current.placement.size));
|
|
1305
1311
|
placeInset.set(current.placement?.inset === true);
|
|
@@ -1313,7 +1319,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
1313
1319
|
if (built.catalog) {
|
|
1314
1320
|
const { body } = built.catalog;
|
|
1315
1321
|
const findings = h("ul", { class: "opf-dc-warnings", "aria-label": "Catalog reference problems", "data-role": "reference-findings" });
|
|
1316
|
-
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.");
|
|
1317
1322
|
const updates = h("ul", { class: "opf-dc-summary", "aria-label": "Catalog updates", "data-role": "catalog-updates" });
|
|
1318
1323
|
const review = { changes: [] };
|
|
1319
1324
|
const check = button("Check for catalog updates", () => {
|
|
@@ -1347,7 +1352,8 @@ export function createDesignControls(container, options = {}) {
|
|
|
1347
1352
|
applyButton.hidden = review.changes.length === 0;
|
|
1348
1353
|
};
|
|
1349
1354
|
renderUpdates();
|
|
1350
|
-
|
|
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);
|
|
1351
1357
|
syncs.push(() => {
|
|
1352
1358
|
const list = referenceFindings(editor.validation);
|
|
1353
1359
|
findings.replaceChildren(
|
|
@@ -1373,10 +1379,9 @@ export function createDesignControls(container, options = {}) {
|
|
|
1373
1379
|
|
|
1374
1380
|
if (built.table) {
|
|
1375
1381
|
const { body, details } = built.table;
|
|
1376
|
-
const summary = h("p", { class: "opf-dc-help", "data-role": "table-summary" });
|
|
1377
1382
|
const applyStyle = (change, done) => run(() => setTableStyle(editor, dynamic.tablePath, change), done);
|
|
1378
1383
|
const headerStyle = selectField("table-header", "Header row", {
|
|
1379
|
-
help: "
|
|
1384
|
+
help: "Theme uses the primary color",
|
|
1380
1385
|
onChange: (value) => applyStyle({ ...dynamic.tableStyle, header: value }, "Table header changed."),
|
|
1381
1386
|
});
|
|
1382
1387
|
headerStyle.setOptions([
|
|
@@ -1385,7 +1390,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
1385
1390
|
{ value: "accent", label: "Accent color" },
|
|
1386
1391
|
]);
|
|
1387
1392
|
const banding = checkField("table-banding", "Banded rows", {
|
|
1388
|
-
help: "Alternates the fill of body rows. Text color adjusts automatically.",
|
|
1389
1393
|
onChange: (checked) => applyStyle({ ...dynamic.tableStyle, banding: checked }, checked ? "Banded rows on." : "Banded rows off."),
|
|
1390
1394
|
});
|
|
1391
1395
|
const borders = selectField("table-borders", "Borders", {
|
|
@@ -1398,10 +1402,10 @@ export function createDesignControls(container, options = {}) {
|
|
|
1398
1402
|
{ value: "none", label: "None" },
|
|
1399
1403
|
]);
|
|
1400
1404
|
const resetStyle = button("Reset table style", () => applyStyle("theme", "Table style reset to the theme."), { quiet: true });
|
|
1401
|
-
const cellLabel = h("p", { class: "opf-dc-
|
|
1405
|
+
const cellLabel = h("p", { class: "opf-dc-cell-label", "data-role": "cell-summary" });
|
|
1402
1406
|
const columns = numberField("span-columns", "Columns to merge", { min: 1, step: 1 });
|
|
1403
1407
|
const rows = numberField("span-rows", "Rows to merge", { min: 1, step: 1 });
|
|
1404
|
-
const join = checkField("merge-join", "Keep
|
|
1408
|
+
const join = checkField("merge-join", "Keep merged text", { help: "Otherwise merging cells that hold text is refused" });
|
|
1405
1409
|
const merge = button("Merge cells", () => {
|
|
1406
1410
|
const cell = dynamic.cell;
|
|
1407
1411
|
if (!cell) return;
|
|
@@ -1419,7 +1423,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1419
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);
|
|
1420
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.
|
|
1421
1425
|
const alt = textField("table-alt", "Alt text", {
|
|
1422
|
-
help: "
|
|
1426
|
+
help: "Its point and key numbers. Enter applies.",
|
|
1423
1427
|
placeholder: "What the table shows",
|
|
1424
1428
|
onCommit: (value) => run(() => setTableAlt(editor, dynamic.tablePath, { alt: value }), "Table alt text changed."),
|
|
1425
1429
|
});
|
|
@@ -1428,8 +1432,8 @@ export function createDesignControls(container, options = {}) {
|
|
|
1428
1432
|
onChange: (checked) => run(() => setTableAlt(editor, dynamic.tablePath, { decorative: checked }), checked ? "Table marked decorative." : "The table is no longer decorative."),
|
|
1429
1433
|
});
|
|
1430
1434
|
const styleControls = [headerStyle.wrap, banding.wrap, borders.wrap, resetStyle, cellBox];
|
|
1431
|
-
body.append(
|
|
1432
|
-
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 };
|
|
1433
1437
|
}
|
|
1434
1438
|
|
|
1435
1439
|
// --- sync -----------------------------------------------------------------------------------------
|
|
@@ -1440,24 +1444,31 @@ export function createDesignControls(container, options = {}) {
|
|
|
1440
1444
|
const blockPath = selected ? blockPathForSelection(document_, selected) : undefined;
|
|
1441
1445
|
const content = blockPath ? readBlockContent(document_, blockPath) : undefined;
|
|
1442
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;
|
|
1443
1449
|
const ui = dynamic.selection;
|
|
1444
1450
|
if (ui) {
|
|
1445
|
-
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;
|
|
1446
1458
|
if (content) {
|
|
1447
1459
|
const targets = blockConversionTargets(document_, blockPath);
|
|
1448
|
-
|
|
1460
|
+
// Every type is listed; one this content cannot become is disabled, with the reason as its tooltip and accessible description.
|
|
1449
1461
|
ui.convert.setOptions([
|
|
1450
1462
|
{ value: "", label: `${BLOCK_KIND_LABELS[content.kind] ?? content.kind} (current)` },
|
|
1451
1463
|
...targets.map((target) => ({
|
|
1452
1464
|
value: target.kind,
|
|
1453
|
-
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,
|
|
1454
1466
|
disabled: !target.available,
|
|
1455
|
-
title: target.reason
|
|
1467
|
+
title: target.available ? undefined : `${target.label}: ${shortReason(target.reason)}`,
|
|
1456
1468
|
})),
|
|
1457
1469
|
]);
|
|
1458
1470
|
ui.convert.set("");
|
|
1459
1471
|
ui.convert.wrap.hidden = targets.length === 0;
|
|
1460
|
-
ui.unavailable.replaceChildren(...targets.filter((target) => !target.available).map((target) => h("li", {}, `${target.label}: ${target.reason}`)));
|
|
1461
1472
|
ui.replace.set("");
|
|
1462
1473
|
const charts = content.kind === "chart" ? compatibleChartTypes(document_, { slideIndex: getSlide(), path: blockPath, ...catalogOptions() }) : [];
|
|
1463
1474
|
ui.chartType.wrap.hidden = charts.length === 0;
|
|
@@ -1486,7 +1497,6 @@ export function createDesignControls(container, options = {}) {
|
|
|
1486
1497
|
const inline = Array.isArray(tableValue?.rows);
|
|
1487
1498
|
tableUi.details.hidden = !tablePath || !(inline || typeof tableValue?.dataset === "string");
|
|
1488
1499
|
if (tablePath && !tableUi.details.hidden) {
|
|
1489
|
-
tableUi.summary.textContent = `Table (${tablePath})`;
|
|
1490
1500
|
const textAlternative = readTableAlt(tableValue);
|
|
1491
1501
|
tableUi.alt.set(textAlternative.alt);
|
|
1492
1502
|
tableUi.alt.input.disabled = textAlternative.decorative;
|
|
@@ -1526,7 +1536,7 @@ export function createDesignControls(container, options = {}) {
|
|
|
1526
1536
|
}
|
|
1527
1537
|
}
|
|
1528
1538
|
if (contentControls) {
|
|
1529
|
-
contentControls.sync({ blockPath, content, selectedPath: selected, slideIndex: getSlide() });
|
|
1539
|
+
contentControls.sync({ blockPath, groupPath, content, selectedPath: selected, slideIndex: getSlide() });
|
|
1530
1540
|
const slideContent = built["slide-content"];
|
|
1531
1541
|
if (slideContent) slideContent.details.hidden = [...contentControls.slideNode.children].every((node) => node.hidden);
|
|
1532
1542
|
}
|