@openpresentation/opf-editor 0.14.2 → 0.15.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 +60 -29
- package/dist/assets.js +1 -1
- package/dist/background-options.d.ts +10 -3
- package/dist/background-options.js +38 -13
- package/dist/canvas.d.ts +8 -1
- package/dist/canvas.js +12 -9
- package/dist/catalogs.d.ts +48 -0
- package/dist/catalogs.js +217 -0
- package/dist/checks.js +9 -7
- package/dist/content-actions.d.ts +2 -2
- package/dist/content-actions.js +3 -3
- package/dist/content-controls.js +9 -9
- package/dist/design-controls.d.ts +12 -5
- package/dist/design-controls.js +260 -76
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +2 -0
- package/dist/export.js +8 -3
- package/dist/galleries.js +32 -67
- package/dist/image-crop.js +8 -9
- package/dist/image-options.d.ts +45 -0
- package/dist/image-options.js +195 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +91 -47
- package/dist/json-editor.js +2 -2
- package/dist/json-menu.js +2 -2
- package/dist/json-options.d.ts +3 -3
- package/dist/json-options.js +30 -41
- package/dist/react.js +1 -1
- package/dist/review-panel.js +8 -2
- package/dist/schema-inspector.js +1 -1
- package/dist/schema.js +1 -1
- package/dist/slide-manager.js +1 -1
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.js +1 -2
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +191 -90
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Unfinished prepared shaping work is preserved in the [September 15 roadmap](docs/roadmap-shaping-20260915.md); it is not part of the published runtime.
|
|
4
4
|
|
|
5
|
+
## OPF 0.15: catalogs from the host, backgrounds and image blocks (FA-23)
|
|
6
|
+
|
|
7
|
+
The release after 0.14.2 moves the editor onto core 0.15 (a breaking, clean spec: no migration). See `changes/fa-23-opf-0-15.md`.
|
|
8
|
+
|
|
9
|
+
- **Catalogs come from the host.** Core ships no catalog records; the editor library imports no catalog data either. The host registers its catalogs once, on the session, and the editor hands that one list to core (resolution, validation, `embed`, `copySlides`, `updateFromCatalog`), every picker, the canvas renderer and the exports:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { createEditorSession, saveDocument } from "@openpresentation/opf-editor";
|
|
13
|
+
import { defaultCatalog, catalogDisplay } from "@openpresentation/opf/catalog"; // the host app's choice, not the library's
|
|
14
|
+
|
|
15
|
+
const editor = createEditorSession(doc, { catalogs: [defaultCatalog, acmeCatalog] }); // the first is the host default
|
|
16
|
+
editor.catalogs; // the frozen list; editor.setCatalogs([...]) replaces it (one "catalogs" event)
|
|
17
|
+
const { document } = saveDocument(editor); // core embed: every referenced record embedded once, renders with no catalog
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`mergeCatalogs(...lists)` is the one merge (the session's list first, the first catalog per `source` wins); `catalogsFor(editor, options)` is the list for one call. Pickers list core's `catalogRecords` and write the reference it gives, `id` or `name:id`.
|
|
21
|
+
- **Save and export embed.** `prepareSave`/`saveDocument`, `serializeOpfTransfer` (presentation and slide scope) and `exportDeck` call core's `embed`; the playground's OPF download and PPTX export do too.
|
|
22
|
+
- **Paste** (`prepareOpfImport`, insert mode) uses core's `copySlides`: groups match by `source`, identical records are reused, and the result's `renamed` (`custom-conflict`, `catalog-revision`, `group-name`) and `addedGroups` tell the user what changed.
|
|
23
|
+
- **Update from catalog** is an explicit action: `checkCatalogUpdates(presentation, { catalogs })` lists the embedded records whose catalog record differs, and `applyCatalogUpdate(editor, { refs })` applies only the approved ones as one undoable step. The design panel's `catalog` section runs both and lists the `opf/unresolved-reference` and `opf/undeclared-catalog` findings (`referenceFindings(editor.validation)`).
|
|
24
|
+
- **Editing a catalog's record forks it.** An edit of a layout, theme, colour scheme or font scheme embedded under `catalogs.default` or a named group (from a panel, Properties, Source or any `applyPatch`) moves it into `catalogs.custom` as `<id>-custom` (or the id in `meta.forkIds["<group>:<kind>:<id>"]`), rewrites every reference to it and drops the original, in the same undo step. The change and the session event carry `forked` and a `notice` ("… is now this presentation's own; it no longer receives catalog updates"); approved catalog updates and `moveToCustom` change records in place (`meta.catalogRecordEdit: "in-place"`). `moveToCustom(editor, { group, kind, id }, { id })` is the explicit form, offered for `opf/catalog-record-not-in-source`.
|
|
25
|
+
- **Backgrounds** take the flat image form `{ type: "image", src, alt, fit, focus, opacity, recolor, overlay }` (fits `cover`, `contain`, `stretch`, `tile`) and the image-source shorthand, on the deck and on a slide. **Image blocks** carry their own fit, focus, treatments and placement (`/image-options`, the panel's `image` section): a full-bleed photo is a background, a picture beside the content is a placed image block. `design.imageFit` is the default fit of image blocks.
|
|
26
|
+
|
|
5
27
|
The release after 0.12.1 (RR-55) moves the editor onto core 0.14, renderer 0.14 and PPTX 0.14, which ship together: core's one `validate` and `Finding` format, `resolveSlideContext`, and the renderer's `loadFonts()` handle as the `fonts` option of the canvas, the export and `composeSlide`/`paginateSlide`. See `changes/rr-55-short-verb-api.md` for what was renamed or removed (the font gate, `validateOpfDocument`, `resolveSlideFonts`, `runAudit`, the `*Available` checks). Version 0.11.2 adds the slide-size and purpose switches (additive; same requirements as 0.11.0). Version 0.11.0 requires `@openpresentation/opf` ^0.12.0 (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored; install it with renderer 0.12.0 and PPTX 0.12.0 so the canvas, preview and export resolve the same core) and ships the new entry points listed in the changelog. Version 0.10.6 changes no editor source: it requires `@openpresentation/opf` ^0.11.4 (the design fields compose, cover tag and subtitle follow the title alignment, picture bullets and furniture images change geometry; install it with renderer 0.11.9 and PPTX 0.11.7 so the canvas, preview and export resolve the same core). Version 0.10.5 changes no editor source: its playground example loads its base faces through the renderer's `extraLazyFonts` (renderer 0.11.7 or later; the editor package itself runs with the optional peer ^0.11.0 as before). Version 0.10.4 changes no editor behavior: it requires `@openpresentation/opf` ^0.11.3 (the 70 legacy gallery layout ids and their geometry; install it with renderer 0.11.6 and PPTX 0.11.4 so the canvas, preview and export resolve the same core). Version 0.10.3 passes the host's render options (`catalogs`) through the font gate to the registry, so layouts and font schemes that only the host's catalogs know load their faces, and asks the script and vendored-face loaders separately; it keeps the 0.10.2 requirements and gates nothing new on renderers before 0.11.5. Version 0.10.2 adds pointer caret entry on the canvas (one click puts the caret where you click; new `textEntry` option; keyboard entry still selects all) and keeps the 0.10.1 and 0.10.0 requirements. Version 0.10.1 adds the opt-in font gate (`createFontGate`, FF-41) and keeps the 0.10.0 requirements; develop and test it against renderer 0.11.2 and PPTX 0.11.1. Version 0.10.0 requires `@openpresentation/opf` ^0.11.2, renderer ^0.11.0, and PPTX ^0.11.0 (install them together so cover centering resolves the same core everywhere; renderer 0.11.0 also activates the playground's automatic script fonts and lazy Intos fonts). Version 0.9.0 required `@openpresentation/opf` ^0.11.1, renderer ^0.10.0, and PPTX ^0.10.0. Composition and slide transfer fall back to the shared `aptos` font scheme and report unknown scheme ids through `onDiagnostic`; gallery apply keeps every font-scheme role.
|
|
6
28
|
|
|
7
29
|
Version 0.8.0 required `@openpresentation/opf` ^0.11.0, renderer ^0.9.0, and PPTX ^0.9.0. Named ColorRefs (scheme slots, roles, and `var:<id>`) paint through the published renderer and hex-resolve on export. Payload ids share the slide id namespace for pagination and transfer. Native `schemeClr`, theme write, and `p:hf` remain out of scope.
|
|
@@ -22,7 +44,7 @@ const control = mountJsonCodeEditor(container, {
|
|
|
22
44
|
code: source,
|
|
23
45
|
label: 'OPF JSON document',
|
|
24
46
|
lineNumbers: true,
|
|
25
|
-
catalogs:
|
|
47
|
+
catalogs: editor.catalogs, // the host's Catalog[] (for example [defaultCatalog])
|
|
26
48
|
onChange: nextSource => receiveDraft(nextSource),
|
|
27
49
|
onError: error => showError(error),
|
|
28
50
|
});
|
|
@@ -36,7 +58,7 @@ The host owns the source draft, OPF validation, slide preview, persistence and s
|
|
|
36
58
|
|
|
37
59
|
The control provides indentation, paired quotes/brackets, folding shortcuts, search, undo/redo, JSON syntax diagnostics and explicit formatting (`Cmd/Ctrl+Shift+F`). Enter at the end of a nonempty string array item supplies indentation, quotes and commas. Other Enter positions use ordinary JSON indentation. Source offsets are UTF-16 in the original authored string. Existing CRLF, CR and LF spellings survive ordinary edits and undo; new typed lines use the first existing separator. Paste retains the text delivered by the browser clipboard event. Explicit formatting deliberately standardizes whitespace, preserving JSON values and numeric lexemes.
|
|
38
60
|
|
|
39
|
-
Click a categorical property/value or press `Ctrl+Space`, `Cmd+Space` or `Alt+Down` to open its choices.
|
|
61
|
+
Click a categorical property/value or press `Ctrl+Space`, `Cmd+Space` or `Alt+Down` to open its choices. Records the document embeds come first, then those of the catalogs the host passes as `catalogs` (`Catalog[]`, the session's registered list); there are no built-in records. The actual schema supplies enums and booleans. Prose and extension fields do not gain menus just because their keys resemble schema fields. External catalog URLs are not fetched. `setCatalogs(list)` replaces the host's catalogs and closes stale choices. Supply `onOptions(left?, top?)` to retain an existing menu UI.
|
|
40
62
|
|
|
41
63
|
In version 0.7.1, layout choices add blank payloads for missing declared placeholders, preserving existing content. Repeated slots use separate blocks; subtitle slots use `subtitle`, and metric slots use `{ "metric": { "value": "", "label": "" } }`. Charts receive the smallest valid data scaffold rather than invented numeric values. Existing positioned content stays in its regions; additional body slots append inside the last region. Non-layout options still change only their value. Custom menus must apply the full source returned by `replaceFieldOption`, or use `fieldOptionEdit(context, value)` and pass its `from`, `to`, and `insert` to `control.api.replace` as one undoable edit. Always check that `context.source` still matches the editor first. Version 0.7.0 only changed the selected token. Version 0.7.1 requires core 0.10.1 for metric, quote and timeline placeholder contracts.
|
|
42
64
|
|
|
@@ -52,7 +74,7 @@ Version 0.7.0 uses core 0.10.0 and renderer 0.8.0. Code source/metadata edits pr
|
|
|
52
74
|
- Compatibility target: `@openpresentation/opf`
|
|
53
75
|
- Renderer relationship: built on `@openpresentation/opf-render` trace output
|
|
54
76
|
- Headless JSON edit bindings for renderer trace attributes
|
|
55
|
-
- Structured catalog controls that only commit
|
|
77
|
+
- Structured catalog controls that only commit catalog references that resolve (in the document or the host's registered catalogs)
|
|
56
78
|
- JSON Patch state transitions with inverse patches for undo/redo
|
|
57
79
|
- Optional DOM controls plus React and Svelte bindings in separate embeddable entry points
|
|
58
80
|
- Dimension switches, safe block conversion and content actions (list levels, grouping, regions, images to design, slide split and merge), design-level options, table style and cell merge as headless APIs (`/switches`, `/block-convert`, `/content-actions`, `/design-options`, `/tables`, `/assets`, `/backgrounds`) and one accessible DOM panel (`/design-controls`)
|
|
@@ -90,13 +112,13 @@ import { createCanvasEditor, whenFontsReady } from '@openpresentation/opf-editor
|
|
|
90
112
|
import { loadFonts } from '@openpresentation/opf-render/fonts-browser';
|
|
91
113
|
|
|
92
114
|
const fonts = await loadFonts({ faces, scriptBaseUrl, lazyFontsBaseUrl }); // a handle with no faces to load gates nothing
|
|
93
|
-
// The canvas
|
|
94
|
-
//
|
|
95
|
-
const canvas = createCanvasEditor(container, {
|
|
115
|
+
// The canvas draws with the session's registered catalogs (plus renderOptions.catalogs) and hands the same list to the handle, so
|
|
116
|
+
// font schemes only your catalogs know resolve in the registry too.
|
|
117
|
+
const canvas = createCanvasEditor(container, { editor, fonts });
|
|
96
118
|
// Other renders of your own: whenFontsReady(fonts, presentation, { ready: draw, failed: showError, loading: showSpinner })
|
|
97
119
|
```
|
|
98
120
|
|
|
99
|
-
`session.composeSlide(index, { fonts })` and `session.paginateSlide(index, { fonts })` take the same handle and resolve the slide's canvas, layout, theme, colour scheme and font families with core's `resolveSlideContext` (slide design, then deck design, then theme, then the default: the order the renderer and the PowerPoint export use), so the editor composes what is drawn and exported.
|
|
121
|
+
`session.composeSlide(index, { fonts })` and `session.paginateSlide(index, { fonts })` take the same handle and resolve the slide's canvas, layout, theme, colour scheme and font families with core's `resolveSlideContext` (slide design, then deck design, then theme, then the engine default: the order the renderer and the PowerPoint export use) and the session's registered catalogs, so the editor composes what is drawn and exported. A reference that resolves nowhere never throws: the slide composes automatically or with core's `ENGINE_DEFAULT_*`, and `onDiagnostic` hears each `unresolved-reference` diagnostic. The handle's `textMeasurement` is strict: for a document with a script the design font lacks (Japanese under Aptos) pass `{ fonts: { textMeasurement: createScriptTextMeasurement(fonts.textMeasurement, resolveScriptFonts(presentation, { slideIndex })) } }` (`createScriptTextMeasurement` is from `@openpresentation/opf-render/fonts`, `resolveScriptFonts` from `@openpresentation/opf/composition`), as `renderSvg` does internally.
|
|
100
122
|
|
|
101
123
|
These APIs were introduced in 0.1.0. Version 0.7.0 requires core 0.10.0 and renderer 0.8.0 for the canvas, including shared accepted geometry, styled/merged cells, rich table values, headers and content-aware row heights. See the OPF repository’s `docs/live-editor.md` for setup, the support matrix and roadmap. `pnpm pack:ecosystem` in that repository also prepares local preview tarballs for coordinated development.
|
|
102
124
|
|
|
@@ -186,7 +208,7 @@ editor.redo();
|
|
|
186
208
|
|
|
187
209
|
## Catalog Controls
|
|
188
210
|
|
|
189
|
-
Catalog helpers commit catalog
|
|
211
|
+
Catalog helpers commit catalog references (`id` or `name:id`) that resolve in the document or the session's registered catalogs, instead of accepting freeform incompatible values:
|
|
190
212
|
|
|
191
213
|
```js
|
|
192
214
|
import {
|
|
@@ -194,7 +216,7 @@ import {
|
|
|
194
216
|
getCatalogOptions
|
|
195
217
|
} from "@openpresentation/opf-editor";
|
|
196
218
|
|
|
197
|
-
getCatalogOptions("themes").map((option) => option.id);
|
|
219
|
+
getCatalogOptions("themes", { editor }).map((option) => option.id); // references, embedded records first
|
|
198
220
|
|
|
199
221
|
const themeSelect = createCatalogSelect(editor, {
|
|
200
222
|
path: "design.theme",
|
|
@@ -205,7 +227,7 @@ const themeSelect = createCatalogSelect(editor, {
|
|
|
205
227
|
toolbar.append(themeSelect);
|
|
206
228
|
```
|
|
207
229
|
|
|
208
|
-
|
|
230
|
+
References that resolve nowhere throw before they reach the document. Existing object-form references keep their sibling override fields and replace only `id`.
|
|
209
231
|
|
|
210
232
|
## Dimension switches
|
|
211
233
|
|
|
@@ -225,18 +247,19 @@ const { patches, presentation } = prepareDimensionSwitch(editor.presentation, "t
|
|
|
225
247
|
| --- | --- | --- |
|
|
226
248
|
| `layouts` | `/slides/N/layout` | Needs `slideIndex`. Adds the blank payloads the layout declares, like the JSON editor's layout choice; existing content stays. |
|
|
227
249
|
| `color-schemes`, `font-schemes` | `/design/colorScheme`, `/design/fontScheme` | Deck by default, one slide with `slideIndex`. An inline object value is replaced by the bare id, except an accent font in a font scheme object, which stays (`{ id, accent }`). |
|
|
228
|
-
| `themes` | `/design/theme`
|
|
229
|
-
| `languages
|
|
230
|
-
| `
|
|
250
|
+
| `themes` | `/design/theme` | The theme's own color scheme, font scheme, background (and, at the deck, size) apply: overrides of those at the switched scope are removed, nothing is copied out of the record. On a slide whose deck sets one of them, the theme's value is written on the slide (its references qualified with the theme's catalog group). `bundle: false` changes only the reference. |
|
|
251
|
+
| `languages` | `/language` | A BCP-47 tag (`en-US`, `ja`) or a language object; `listSwitchOptions` offers core's `LANGUAGES`, labelled from `vocabularies.languages`. |
|
|
252
|
+
| `narratives`, `tones`, `audiences` | `/narrative`, `/tone`, `/audience` | Catalog references; `audiences` accepts one or an array. A custom narrative is a `catalogs.custom.narratives` record; slides keep their `beat` links. |
|
|
253
|
+
| `backgrounds` | `/design/background` | A background object or a shorthand string (theme slot such as `dark1`, a hex color, or an image source: a cover image). |
|
|
231
254
|
| `headers-footers` | `/design/header`, `/design/footer` | `{header?, footer?}`: an absent field stays, `null` removes it. |
|
|
232
|
-
| `image-treatments` | `/
|
|
233
|
-
| `socials` | `/speaker/socials/<platform>` or the `organization` | `{platform, handle}` with `owner` and `index`; the owner must exist. |
|
|
255
|
+
| `image-treatments` | `<image block>/fit`, `/focus`, `/shape`, ... | Needs `path` (the image block). The fields of `setImageTreatment`; `null` removes one. |
|
|
256
|
+
| `socials` | `/speaker/socials/<platform>` or the `organization` | `{platform, handle}` with `owner` and `index`; the owner must exist. The platform is a key of core's `SOCIAL_PLATFORMS`. |
|
|
234
257
|
| `charts` | `<chart owner>/chart/type` | The slide's first chart, or the block named by `path`. The data is kept as it is and only the document schema is checked: switching does not verify that the data suits the new type (`compatibleChartTypes` lists the types the data can use; map types, for example, expect their own data). |
|
|
235
258
|
| `blocks` | replaces one block, or with `convert: true` moves its content to the new kind | `path` names a complete `blocks/N` block or a slide/region with one content field. The value is a block kind or a block object. See *Content-type conversion*. |
|
|
236
259
|
| `slide-sizes` | `/design/dimensions` | One of the schema's ten presets (`SLIDE_SIZE_PRESETS`: `16:9`, `4:3`, `16:10`, the social-feed ratios `1:1`, `4:5` and `9:16`, `letter`, `a4`, `widescreen`, `standard`). Deck only (`slideIndex` is refused: a presentation has one slide size). The preview recomposes at the new canvas and the PowerPoint export writes the matching `p:sldSz`; a custom size (inches) is replaced by the preset. `listSwitchOptions` labels each preset with its inches; `currentSwitchValue` reads `design.dimensions`, else the theme's size. A slide's design cannot set `dimensions`, so nothing shadows the deck's size. A deck-scope theme switch still writes its own size; a slide-scope one does not. |
|
|
237
|
-
| `purposes` | `/purpose` | A `purposes` catalog
|
|
260
|
+
| `purposes` | `/purpose` | A `purposes` catalog reference, any free-form goal text (not checked against the catalogs), or a Purpose object. `record` embeds a gallery item's purpose record. Authoring metadata: the preview does not change. |
|
|
238
261
|
|
|
239
|
-
A deck-level design switch cannot reach a slide that carries its own value for that key. The result lists those slides in `shadowed`; `clearSlideOverrides: true` removes the overrides in the same transaction. `record`
|
|
262
|
+
A deck-level design switch cannot reach a slide that carries its own value for that key. The result lists those slides in `shadowed`; `clearSlideOverrides: true` removes the overrides in the same transaction. `record` embeds a gallery item's catalog record in the same transaction, under the group whose source is `recordSource` (else `custom`), when the document does not embed it yet. `catalogs` are the host's catalogs (merged after the session's when a panel calls the switch). Every switch is validated: an unknown catalog reference, an invalid value or an invalid resulting document throws before anything changes, and switching to the current value commits nothing. The editor session emits the usual `patch`, `undo` and `redo` events with `meta.source: "dimension-switch"` and `meta.dimension`, so the canvas and any host preview recompose from the switched document. `resolveSlideContext(presentation, slideIndex).options.fontFamilies` (core) returns the heading, body and code families the preview measures and the export names.
|
|
240
263
|
|
|
241
264
|
### Content-type conversion (RR-06, RR-26)
|
|
242
265
|
|
|
@@ -280,14 +303,14 @@ Images, videos and any other group have no conversion. Everything else is replac
|
|
|
280
303
|
| List levels | `shiftListItems(editor, listPath, [indexes], delta)` | Indent (`1`) or outdent (`-1`) items; a level is at most one deeper than the item above, items under a moved item move with it, nothing is lost. |
|
|
281
304
|
| Group | `groupBlocks(editor, containerPath, [indexes])`, `ungroupBlock(editor, groupPath)` | Wrap blocks of a slide or group in a group, or dissolve one (its composition and id are reported). |
|
|
282
305
|
| Regions | `placeBlocksInRegions`, `regionsAsBlocks`, `moveSlideRegion` | Give each block its own named region (no overlap), turn regions back into blocks in reading order (placement is reported), move or swap a region. |
|
|
283
|
-
| Images | `moveImageToDesign(editor, blockPath, "
|
|
306
|
+
| Images | `moveImageToDesign(editor, blockPath, "background" \| "watermark", options)`, `moveImageToContent(editor, slideIndex, source)` | Promote an image block to the slide's background (it keeps alt, fit, focus, opacity and overlay) or watermark and back; what the target cannot hold is reported. Bleeding an image to an edge beside the content is the block's `placement`, not a move. |
|
|
284
307
|
| Slides | `splitSlideByBlocks`, `splitSlideOnOverflow`, `mergeSlides`, `unpaginateSlides` | Split a slide by its blocks, or where it overflows through the existing pagination (the change carries `pages`), merge consecutive slides, and put paginated slides back together from `pages`. Each is one undo step made of per-slide `test`, `replace`, `remove` and `add` operations. |
|
|
285
308
|
|
|
286
309
|
The playground's Content tab mounts the block-level and slide-structure actions (`slide-content` section of `/design-controls`). Slide-level split and merge are in the playground's slide menu (the slide manager's `contentActions` option) as well as in the API.
|
|
287
310
|
|
|
288
311
|
### Pickers: options, chart types and current values
|
|
289
312
|
|
|
290
|
-
`listSwitchOptions(presentation, dimension,
|
|
313
|
+
`listSwitchOptions(presentation, dimension, { catalogs, vocabularies })` lists what a dimension offers: a catalog dimension lists core's `catalogRecords` (the document's embedded records first, then the host catalogs'), each with the reference to write; `charts`, `socials` and `languages` list core's vocabularies, labelled from `vocabularies` (`catalogDisplay` from `@openpresentation/opf/catalog`, supplied by the host). `compatibleChartTypes(presentation, { slideIndex, path })` lists the chart types the chart's inline data can use as it is, by data shape: the first column labels the categories and each further column is a series (how the renderers read it), a type with N series needs exactly N value columns, except that stacked types and the combo chart take that many or more (combo: two or more), and the single-series, distribution and geographic types are offered only where they fit. Switching a combo chart to another type removes its `line`, `secondaryAxis` and secondary axis title. It is data-shape compatibility, not a claim that an engine draws the type. `currentSwitchValue(presentation, dimension, { slideIndex })` reads the value back as `{ value, scope }`.
|
|
291
314
|
|
|
292
315
|
## Design options (RR-06)
|
|
293
316
|
|
|
@@ -309,11 +332,11 @@ setHeaderFooterZone(editor, "footer", "right", { slideNumber: true, logo: true }
|
|
|
309
332
|
| `chartPrimary` | `design.chartPrimary` | `none`, `top`, `bottom`, `left`, `right` |
|
|
310
333
|
| `listBullet` | `design.listBullet` | `character`, `image` (picture bullets draw the logo) |
|
|
311
334
|
| `contentBox` | `design.contentBox` | `true`, `false` |
|
|
335
|
+
| `imageFit` | `design.imageFit` | `cover`, `contain`, `stretch`: the fit of image blocks that choose none |
|
|
312
336
|
| `accentFont` | `design.fontScheme.accent` | a family name; the font scheme becomes its object form and collapses back to the bare id when the accent is cleared |
|
|
313
337
|
| `logo` | `design.logo` | a source, an Asset object or a LogoSet; `setLogoVariant` edits one of the 12 variants |
|
|
314
338
|
| `organizationLogo` | `organization.logo` (deck only; `index` picks an organization) | a source or Asset object |
|
|
315
339
|
| `watermark` | `design.watermark` | `false`, a source, `{ src, opacity }` or the text stamp `{ text, opacity }` (fields merge; a lone source stays a bare source; setting `text` drops `src` and setting `src` drops `text`: a watermark is one or the other) |
|
|
316
|
-
| `slideImage` | `design.slideImage` | a source or `{ src, position, size, fill, shape, inset, ... }` (fields merge; `position` defaults to `background` because the object form requires it) |
|
|
317
340
|
| header and footer zones | `design.header` / `design.footer` `.left/.center/.right` | `setHeaderFooterZone` merges every part a zone can hold (`text`, `logo`, `image`, `slideNumber` and `slideNumberFormat` (must contain `{current}`), `date` (true, or a fixed date) and `dateFormat`, `organization`, `speaker` (the first speaker's name and title), `socials`, `section`; `ZONE_FIELDS`) into one zone, checking each value; a removed field, an emptied zone and an emptied header are all deleted rather than left as `{}`. A slide's own header or footer replaces the deck's whole one, so the first edit on a slide starts from a copy of the deck's (its other zones stay), and a slide emptied that way hides the furniture (`false`) instead of inheriting it again |
|
|
318
341
|
|
|
319
342
|
A deck-scope change reports `shadowed` slides whose own design hides it (`clearSlideOverrides: true` removes those values in the same transaction). Results carry `warnings`: a header or footer zone with `logo: true`, or picture bullets, with no logo to draw (no slide, deck or primary-organization logo) is reported as `unresolved-logo`, and a zone that shows the organization or its social profiles when there are none as `unresolved-content`, before export, as `designWarnings(presentation, slideIndex)` does for the current document. `DESIGN_OPTIONS` describes every option for a generic panel, and `getDesignOption` reads `{ value, scope, inherited }`.
|
|
@@ -330,11 +353,18 @@ const change = await applyImageUpload(editor, file, (reference, presentation) =>
|
|
|
330
353
|
change.assetId; // "acme-logo": the document now has assets["acme-logo"] = { src: "data:image/png;base64,...", mediaType, title, alt } and design.logo.light = "asset:acme-logo"
|
|
331
354
|
```
|
|
332
355
|
|
|
333
|
-
`build(reference, presentation)` is any `prepare...` function of this package, so the same upload works for the logo (all 12 variants), organization logo, watermark,
|
|
356
|
+
`build(reference, presentation)` is any `prepare...` function of this package, so the same upload works for the logo (all 12 variants), organization logo, watermark, a background (`prepareBackground`) and a header/footer zone image. The file is checked before anything changes: PNG, JPEG, GIF, WebP or SVG by its bytes (a `.jpg` that is really a PNG, a text file, an empty file and an SVG with script are refused), and at most `maxBytes` (default 2 MiB, `DEFAULT_MAX_IMAGE_BYTES`) with a message that says what to do. A host that stores images elsewhere passes `onAddAsset({ name, mediaType, bytes, size, alt, file })` and returns the reference to use (a web address, or an `asset:` id it added); nothing is then added to `assets`. `setAssetAlt` edits an asset's alt text as one step.
|
|
334
357
|
|
|
335
358
|
### Backgrounds (RR-06)
|
|
336
359
|
|
|
337
|
-
`@openpresentation/opf-editor/backgrounds` covers every background form the schema has: a theme slot, a solid color, a linear gradient, an image
|
|
360
|
+
`@openpresentation/opf-editor/backgrounds` covers every background form the schema has: a theme slot, a solid color, a linear gradient, an image and a pattern, each with an optional opacity. The image form is flat (OPF 0.15): `{ type: "image", src, alt, fit, focus: { x, y }, opacity, overlay: { color, opacity, edge?, size? } }`, with the fits `cover`, `contain`, `stretch` and `tile`; a plain cover image is stored as the shorthand string (an `asset:`, `https://`, `data:`, `./` or `../` source). A background never moves content; with `alt` it is meaningful (PowerPoint export draws a picture at the back that carries the text).
|
|
361
|
+
|
|
362
|
+
Colors are ColorRefs: hex, a scheme slot or role (`accent1`, `surface`, ...) or `var:<id>`. `normalizeBackground` validates with sentences a person can act on (at least two stops, positions 0 to 1, a color that is none of the above), `setBackground(editor, spec, { slideIndex })` is one undoable patch through the `backgrounds` switch, `null` removes the background so the theme's (or the deck's) shows again, and `readBackground` flattens the current one for a form. `PATTERN_PRESETS` is the 54 DrawingML presets (`PATTERN_GROUPS` groups them in five families); PPTX export writes them as native pattern fills and import returns the same name. Radial gradients are not part of the OPF schema (a gradient has an angle and stops), so there is no radial control.
|
|
363
|
+
|
|
364
|
+
### Image blocks (FA-22, OPF 0.15)
|
|
365
|
+
|
|
366
|
+
`@openpresentation/opf-editor/image-options` frames one image block: `setImageTreatment(editor, "slides.1.blocks.0", { fit: "contain", focus: { x: 0.5, y: 0.3 }, shape: "rounded", cornerRadius: 0.1, border: { color: "dark1", width: 4 }, opacity: 0.9, recolor: "grayscale", overlay: { color: "dark1", opacity: 0.4, edge: "bottom", size: 0.25 }, aspectRatio: 1.78, placement: { edge: "right", size: 0.45, inset: false } })` is one validated, undoable patch (`null` removes a field), and `readImageTreatments(presentation, path)` reads them back with `placeable` and the `usedEdges` other placed blocks take. Placement bleeds the block to one slide edge and composes the title and content in the rest; only a top-level block (`slides.N.blocks.I`) can be placed, at most one per edge, and the slide's own `image` takes no framing fields (`placement-not-top-level`, `placement-edge-taken`). The design panel's `image` section shows these controls for the selected image block.
|
|
367
|
+
|
|
338
368
|
|
|
339
369
|
## Footnotes, citations and captions (RR-34)
|
|
340
370
|
|
|
@@ -441,13 +471,14 @@ const controls = createDesignControls(designPanel, {
|
|
|
441
471
|
editor,
|
|
442
472
|
getSlideIndex: () => slideIndex,
|
|
443
473
|
getSelectedPath: () => selectedPath,
|
|
444
|
-
sections: ["look", "
|
|
474
|
+
sections: ["look", "background", "header-footer", "brand", "layout-options", "info", "catalog"],
|
|
475
|
+
vocabularies: catalogDisplay, // from @openpresentation/opf/catalog, in the host app
|
|
445
476
|
});
|
|
446
|
-
const selectionControls = createDesignControls(contentPanel, { editor, getSlideIndex, getSelectedPath, sections: ["selection", "table", "slide-content"], onSelectPath: select });
|
|
477
|
+
const selectionControls = createDesignControls(contentPanel, { editor, getSlideIndex, getSelectedPath, sections: ["selection", "image", "table", "slide-content"], onSelectPath: select });
|
|
447
478
|
controls.refresh(); // when the slide or the selection changes
|
|
448
479
|
```
|
|
449
480
|
|
|
450
|
-
Sections: `look` (theme, color scheme, font scheme, language, this slide's layout), `background` (type, theme slot, solid color, gradient with its stops, image, any of the 54 patterns, opacity; a draft until Apply, with Remove), `
|
|
481
|
+
Sections: `look` (theme, color scheme, font scheme, language, this slide's layout), `background` (type, theme slot, solid color, gradient with its stops, image with fit, focus, alt text, recolor and overlay, any of the 54 patterns, opacity; a draft until Apply, with Remove), `header-footer` (choose header or footer and a zone, then every part the zone supports: text, logo, image, organization, socials, section, slide number and format, current or fixed date and format, with a summary of the zones in use), `brand` (logo variants, organization logo, picture bullets, accent font, watermark), `layout-options` (alignment, direction, primary chart, content box, default image fit), `info` (narrative, tone, audience, socials), `selection` (content type with a loss report, list levels, group of metrics, group and ungroup, image to background or watermark, replacement, chart type), `image` (the selected image block's fit, focus, shape, corner radius, border, opacity, recolor, overlay, aspect ratio and placement), `catalog` (reference findings and the explicit "Update from catalog"), `slide-content` (blocks to regions and back, design images back into the content) and `table` (style, merge, split, cell fill and alignment). The panel is native `details`, `fieldset`, `select`, checkbox and text controls with a label on each, so it works with the keyboard and with screen readers: groups open with Enter or Space, a select changes with the arrow keys, text fields commit on Enter or when you leave them, and results and refusals are announced in `role="status"` and `role="alert"` regions. A refused change (an invalid value, text a merge would hide) is explained, changes nothing, and puts the field back. "Applies to" switches the design controls between the whole presentation and the current slide, and a control shows when a slide value is "set on this slide" or "from the presentation". The panel offers only conversions that exist, shows what a conversion loses before you choose it, and lists why an unavailable one is unavailable.
|
|
451
482
|
|
|
452
483
|
In React (or Svelte, or anything else) mount it into a ref and destroy it on unmount; it needs no framework runtime:
|
|
453
484
|
|
|
@@ -459,13 +490,13 @@ useEffect(() => {
|
|
|
459
490
|
// call controls.refresh() (keep it in a ref) when slide or selected changes
|
|
460
491
|
```
|
|
461
492
|
|
|
462
|
-
The playground mounts both panels (the Design tab, and the selection area of the Content tab). One click on text still enters text editing; the panels follow the selection after Escape. Every image field (logo variants, organization logo, watermark,
|
|
493
|
+
The playground mounts both panels (the Design tab, and the selection area of the Content tab). One click on text still enters text editing; the panels follow the selection after Escape. Every image field (logo variants, organization logo, watermark, background image and header/footer zone image) takes an asset reference (suggested from the document's assets), a web address or a data address, and has a file picker and an alt-text field: the chosen file is validated (PNG, JPEG, GIF, WebP or SVG, up to `maxImageBytes`, 2 MiB by default), added to `assets` and used in one undo step, or handed to your app through `onAddAsset`. Alt text is saved on the asset; typed before an upload it is used for that upload.
|
|
463
494
|
|
|
464
495
|
Run `npm run test:design-controls-browser` (after `npm run build:playground`) for the real-browser check of every control, its keyboard operation and its undo.
|
|
465
496
|
|
|
466
497
|
### What a switch does not establish
|
|
467
498
|
|
|
468
|
-
A switch changes the document; it does not change what the engines support. Language changes recompose fonts only as far as the installed core, renderer and PPTX packages implement the language and script model (FF-18, FF-19); the editor's own composition measures the Latin families. `image-treatments` previews the
|
|
499
|
+
A switch changes the document; it does not change what the engines support. Language changes recompose fonts only as far as the installed core, renderer and PPTX packages implement the language and script model (FF-18, FF-19); the editor's own composition measures the Latin families. `image-treatments` previews the image block treatments the renderer draws. `test/switches.mjs` checks the patch, one undo step, undo/redo, and preview refresh for all 16 dimensions (the 14 gallery dimensions, slide sizes and purposes), and for every slide-size preset the composed canvas and the preview viewBox. `test/switches-export.mjs` exports after each switch, undo and redo, checks that every slide-size preset writes the matching `p:sldSz` and re-imports as that preset, and applies opf-pptx's FF-08 typeface check (`checkTypefaces`) to every package it writes.
|
|
469
500
|
|
|
470
501
|
## Autosave and restore (RR-22)
|
|
471
502
|
|
|
@@ -523,9 +554,9 @@ The headless pieces are usable on their own: `listTemplateFields(presentation, v
|
|
|
523
554
|
|
|
524
555
|
## Image crop and focal point (RR-25)
|
|
525
556
|
|
|
526
|
-
Select a picture on the canvas and use **Crop picture** (or `canvas.cropImage(path, { tool: "focus" })`, or the inspector's Picture section
|
|
557
|
+
Select a picture on the canvas and use **Crop picture** (or `canvas.cropImage(path, { tool: "focus" })`, or the inspector's Picture section). The crop layer shows the whole picture with a crop rectangle: eight drag handles, an aspect lock (Free, Original, **Frame shape**, 1:1, 4:3, 3:2, 16:9 and the portrait ratios; Shift while dragging keeps the current ratio), exact pixel fields, Reset, Cancel and Apply. The **Focal point** tool cuts the frame's own shape around a point you click or drag, with a zoom. Keyboard: the crop area takes arrow keys (move), Shift+arrows (resize), plus and minus (scale), Enter (apply) and Esc (cancel); the layer is a modal dialog that keeps Tab inside it. On a phone or tablet the layer fills the screen and the handles have finger-sized targets. Each Apply is one undoable change.
|
|
527
558
|
|
|
528
|
-
**What is written.** The OPF schema has no crop rectangle
|
|
559
|
+
**What is written.** The OPF schema has no crop rectangle (`Asset` is `src`, `alt`, `title`, `description`, `mediaType`, `format`). An image block has a `fit` (cover, contain, stretch) and, since OPF 0.15, a `focus` point a cover fit keeps in view, set in the "Selected image" section (`/image-options`). So the editor writes the crop into the picture itself: the cropped pixels (at the picture's own resolution; JPEG stays JPEG, everything else PNG) become a new entry of `assets` with `description: "Cropped from asset:<id>"`, and the image points at it, in one patch. The preview and the PPTX export both read those pixels, so they cannot disagree about what is shown, and the fit the document already has places the cropped picture in its frame as before (opf-pptx writes its usual `a:srcRect` for a covering placement and none for a contained one; `test/image-crop-browser.mjs` exports a cropped deck and checks that the media is the cropped asset and that the placement matches the preview in both fills). The original asset stays so **Restore original** can put it back (one undo step); a crop of a crop points at the first original and the unused intermediate asset is removed. A vector (SVG) picture is not croppable (it has no pixels; use Fit); a picture hosted on another site is cropped only if the host allows its pixels to be read (otherwise upload it first); a GIF becomes its first frame. The model is `@openpresentation/opf-editor/image-crop` (`describeImage`, `prepareCrop`, `prepareRestore`, `applyCrop`, `cropImagePixels`, and the pure rectangle maths `resizeRect`, `fitAspect`, `focalWindow`) and the layer is `/image-cropper`.
|
|
529
560
|
|
|
530
561
|
## Phone and tablet (RR-25)
|
|
531
562
|
|
package/dist/assets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Image assets for the design controls (RR-06): turn a local file into an entry of the document's
|
|
2
|
-
// `assets` map and reference it from a logo, watermark,
|
|
2
|
+
// `assets` map and reference it from a logo, watermark, background or header/footer
|
|
3
3
|
// zone in the same undoable patch. The file is validated before anything changes: the type must be
|
|
4
4
|
// PNG, JPEG, GIF, WebP or SVG, its bytes must match that type, and its size must be under a cap with a
|
|
5
5
|
// clear error. A host that keeps images elsewhere passes `onAddAsset` and gets the bytes instead.
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { EditorSession } from "./index.js";
|
|
2
|
+
import type { ImageFocus, ImageOverlay } from "./image-options.js";
|
|
2
3
|
import type { DimensionSwitchChange, DimensionSwitchOptions, PreparedDimensionSwitch } from "./switches.js";
|
|
3
4
|
|
|
4
5
|
export declare const BACKGROUND_TYPES: readonly ["theme", "solid", "gradient", "image", "pattern"];
|
|
5
6
|
export declare const THEME_BACKGROUND_SLOTS: readonly ["light1", "light2", "dark1", "dark2"];
|
|
6
|
-
export declare const IMAGE_BACKGROUND_FITS: readonly ["cover", "contain", "tile"];
|
|
7
|
+
export declare const IMAGE_BACKGROUND_FITS: readonly ["cover", "contain", "stretch", "tile"];
|
|
8
|
+
/** Image sources the background string shorthand accepts: `asset:`, `https://`, `data:`, `./`, `../`. */
|
|
9
|
+
export declare const IMAGE_SOURCE: RegExp;
|
|
7
10
|
/** Scheme slots and roles a ColorRef may name. */
|
|
8
11
|
export declare const COLOR_NAMES: readonly string[];
|
|
9
12
|
/** The 54 DrawingML preset patterns by family. */
|
|
@@ -19,12 +22,12 @@ export type BackgroundSpec =
|
|
|
19
22
|
| { type: "theme"; slot: (typeof THEME_BACKGROUND_SLOTS)[number] }
|
|
20
23
|
| { type: "solid"; color: string; opacity?: number }
|
|
21
24
|
| { type: "gradient"; gradient: { angle?: number; stops: GradientStop[] }; opacity?: number }
|
|
22
|
-
| { type: "image";
|
|
25
|
+
| { type: "image"; src: string; alt?: string; fit?: (typeof IMAGE_BACKGROUND_FITS)[number]; focus?: ImageFocus; opacity?: number; recolor?: "grayscale" | { dark: string; light: string }; overlay?: ImageOverlay }
|
|
23
26
|
| { type: "pattern"; pattern: { preset: string; foregroundColor?: string; backgroundColor?: string }; opacity?: number };
|
|
24
27
|
|
|
25
28
|
/** Whether `value` is a ColorRef: a hex color, a scheme slot or role name, or `var:<id>`. */
|
|
26
29
|
export declare function isColorRef(value: unknown): boolean;
|
|
27
|
-
/** Validate a background description and return the value to store (a theme slot
|
|
30
|
+
/** Validate a background description and return the value to store (a theme slot, a plain hex color or a plain cover image stays the shorthand string). Throws `invalid-background`. */
|
|
28
31
|
export declare function normalizeBackground(spec: BackgroundSpec): string | Record<string, unknown>;
|
|
29
32
|
export declare function prepareBackground(presentation: unknown, spec: BackgroundSpec | null, options?: DimensionSwitchOptions): PreparedDimensionSwitch;
|
|
30
33
|
/** Set the background as one undoable transaction; `null` removes it. */
|
|
@@ -37,7 +40,11 @@ export interface BackgroundState {
|
|
|
37
40
|
angle?: number;
|
|
38
41
|
stops?: GradientStop[];
|
|
39
42
|
src?: string;
|
|
43
|
+
alt?: string;
|
|
40
44
|
fit?: string;
|
|
45
|
+
focus?: ImageFocus;
|
|
46
|
+
recolor?: "grayscale" | { dark: string; light: string };
|
|
47
|
+
overlay?: ImageOverlay;
|
|
41
48
|
preset?: string;
|
|
42
49
|
foregroundColor?: string;
|
|
43
50
|
backgroundColor?: string;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
// Backgrounds (RR-06): every background form the schema has, with validation that explains itself.
|
|
2
|
-
// A background is a theme slot, a solid color, a linear gradient, an image or a pattern, at the deck
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Backgrounds (RR-06, FA-22/FA-23): every background form the schema has, with validation that explains itself.
|
|
2
|
+
// A background is a theme slot, a solid color, a linear gradient, an image or a pattern, at the deck or on one slide. It is
|
|
3
|
+
// the only canvas fill and never moves content. The image form is flat (OPF 0.15): `{ type: "image", src, alt, fit, focus,
|
|
4
|
+
// opacity, recolor, overlay }`; an image source string (`asset:`, `https://`, `data:`, `./`, `../`) is the cover-image shorthand.
|
|
5
|
+
// `setBackground` is one undoable patch through the `backgrounds` switch, so the preview, export and Undo behave like every
|
|
6
|
+
// other dimension.
|
|
5
7
|
import { prepareDimensionSwitch, switchDimension } from "./switches.js";
|
|
6
8
|
import { fail } from "./edit-helpers.js";
|
|
9
|
+
import { normalizeFocus, normalizeOverlay, normalizeRecolor } from "./image-options.js";
|
|
7
10
|
|
|
8
11
|
export const BACKGROUND_TYPES = Object.freeze(["theme", "solid", "gradient", "image", "pattern"]);
|
|
9
12
|
export const THEME_BACKGROUND_SLOTS = Object.freeze(["light1", "light2", "dark1", "dark2"]);
|
|
10
|
-
export const IMAGE_BACKGROUND_FITS = Object.freeze(["cover", "contain", "tile"]);
|
|
13
|
+
export const IMAGE_BACKGROUND_FITS = Object.freeze(["cover", "contain", "stretch", "tile"]);
|
|
14
|
+
/** Image sources the background string shorthand accepts (the schema's ImageSource). */
|
|
15
|
+
export const IMAGE_SOURCE = /^(?:asset:|https:\/\/|data:|\.\/|\.\.\/)/;
|
|
11
16
|
/** Scheme slots and roles a ColorRef may name (the schema's ColorRef enum), besides hex colors and `var:<id>`. */
|
|
12
17
|
export const COLOR_NAMES = Object.freeze([
|
|
13
18
|
"accent1", "accent2", "accent3", "accent4", "accent5", "accent6", "dark1", "dark2", "light1", "light2", "hyperlink", "followedHyperlink",
|
|
@@ -55,7 +60,8 @@ export function normalizeBackground(spec) {
|
|
|
55
60
|
if (typeof spec === "string") {
|
|
56
61
|
if (THEME_BACKGROUND_SLOTS.includes(spec)) return spec;
|
|
57
62
|
if (HEX.test(spec)) return spec.toUpperCase();
|
|
58
|
-
|
|
63
|
+
if (IMAGE_SOURCE.test(spec.trim())) return spec.trim();
|
|
64
|
+
throw fail("invalid-background", `A background shorthand is a theme slot (${THEME_BACKGROUND_SLOTS.join(", ")}), a hex color, or an image source starting with asset:, https://, data:, ./ or ../.`, { spec });
|
|
59
65
|
}
|
|
60
66
|
if (!isObject(spec)) throw fail("invalid-background", "Describe the background as { type, ... } or a theme slot / hex shorthand.", { spec });
|
|
61
67
|
const opacity = opacityOf(spec);
|
|
@@ -83,10 +89,25 @@ export function normalizeBackground(spec) {
|
|
|
83
89
|
return withOpacity({ type: "gradient", gradient: { ...(angle === undefined ? {} : { angle }), stops } });
|
|
84
90
|
}
|
|
85
91
|
case "image": {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
92
|
+
if (typeof spec.src !== "string" || !spec.src.trim()) throw fail("invalid-background", "An image background needs an image: choose a file or enter a source.", { spec });
|
|
93
|
+
const src = spec.src.trim();
|
|
94
|
+
if (!IMAGE_SOURCE.test(src)) throw fail("invalid-background", "An image source starts with asset:, https://, data:, ./ or ../.", { spec });
|
|
95
|
+
const fit = spec.fit === "" || spec.fit === null ? undefined : spec.fit;
|
|
96
|
+
if (fit !== undefined && !IMAGE_BACKGROUND_FITS.includes(fit)) throw fail("invalid-background", `Image fit is one of ${IMAGE_BACKGROUND_FITS.join(", ")}.`, { spec });
|
|
97
|
+
const alt = typeof spec.alt === "string" && spec.alt.trim() ? spec.alt.trim() : undefined;
|
|
98
|
+
const wrap = (call) => {
|
|
99
|
+
try {
|
|
100
|
+
return call();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw fail("invalid-background", error.message, { spec });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const focus = spec.focus === undefined || spec.focus === null ? undefined : wrap(() => normalizeFocus(spec.focus));
|
|
106
|
+
const overlay = spec.overlay === undefined || spec.overlay === null ? undefined : wrap(() => normalizeOverlay(spec.overlay));
|
|
107
|
+
const recolor = spec.recolor === undefined || spec.recolor === null || spec.recolor === "" ? undefined : wrap(() => normalizeRecolor(spec.recolor));
|
|
108
|
+
const value = { type: "image", src, ...(alt ? { alt } : {}), ...(fit ? { fit } : {}), ...(focus ? { focus } : {}), ...(opacity === undefined ? {} : { opacity }), ...(recolor ? { recolor } : {}), ...(overlay ? { overlay } : {}) };
|
|
109
|
+
// A cover image with nothing else is the shorthand string.
|
|
110
|
+
return Object.keys(value).length === 2 ? src : value;
|
|
90
111
|
}
|
|
91
112
|
case "pattern": {
|
|
92
113
|
const pattern = spec.pattern;
|
|
@@ -113,7 +134,7 @@ export function setBackground(editor, spec, options = {}) {
|
|
|
113
134
|
|
|
114
135
|
/**
|
|
115
136
|
* The background that applies at a scope as a flat description for a form:
|
|
116
|
-
* `{ type, slot?, color?, opacity?, angle?, stops?, src?, fit?, preset?, foregroundColor?, backgroundColor?, scope, value }`.
|
|
137
|
+
* `{ type, slot?, color?, opacity?, angle?, stops?, src?, alt?, fit?, focus?, recolor?, overlay?, preset?, foregroundColor?, backgroundColor?, scope, value }`.
|
|
117
138
|
* `type` is undefined when nothing is set; `scope` is "slide" when the slide sets its own, else "deck".
|
|
118
139
|
*/
|
|
119
140
|
export function readBackground(presentation, { slideIndex } = {}) {
|
|
@@ -121,13 +142,17 @@ export function readBackground(presentation, { slideIndex } = {}) {
|
|
|
121
142
|
const value = own !== undefined ? own : presentation.design?.background;
|
|
122
143
|
const scope = own !== undefined ? "slide" : "deck";
|
|
123
144
|
if (value === undefined) return { scope };
|
|
124
|
-
if (typeof value === "string")
|
|
145
|
+
if (typeof value === "string") {
|
|
146
|
+
if (THEME_BACKGROUND_SLOTS.includes(value)) return { type: "theme", slot: value, scope, value };
|
|
147
|
+
if (IMAGE_SOURCE.test(value)) return { type: "image", src: value, fit: "cover", scope, value };
|
|
148
|
+
return { type: "solid", color: value, scope, value };
|
|
149
|
+
}
|
|
125
150
|
if (!isObject(value)) return { scope, value };
|
|
126
151
|
const base = { type: value.type, opacity: value.opacity, scope, value };
|
|
127
152
|
if (value.type === "theme") return { ...base, slot: value.slot };
|
|
128
153
|
if (value.type === "solid") return { ...base, color: value.color };
|
|
129
154
|
if (value.type === "gradient") return { ...base, angle: value.gradient?.angle, stops: structuredClone(value.gradient?.stops ?? []) };
|
|
130
|
-
if (value.type === "image") return { ...base, src: value.
|
|
155
|
+
if (value.type === "image") return { ...base, src: value.src, alt: value.alt, fit: value.fit, focus: value.focus && { ...value.focus }, recolor: value.recolor && structuredClone(value.recolor), overlay: value.overlay && { ...value.overlay } };
|
|
131
156
|
if (value.type === "pattern") return { ...base, preset: value.pattern?.preset, foregroundColor: value.pattern?.foregroundColor, backgroundColor: value.pattern?.backgroundColor };
|
|
132
157
|
return base;
|
|
133
158
|
}
|
package/dist/canvas.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EditorSession } from "./index.js";
|
|
|
2
2
|
import type { RenderSvgOptions } from "@openpresentation/opf-render";
|
|
3
3
|
import type { BrowserFontsHandle } from "@openpresentation/opf-render/fonts-browser";
|
|
4
4
|
import type { SlideComposition } from "@openpresentation/opf/composition";
|
|
5
|
+
import type { Catalog } from "@openpresentation/opf";
|
|
5
6
|
/**
|
|
6
7
|
* The fonts the canvas, the export and `whenFontsReady` take: the renderer's handle from `loadFonts()` (`@openpresentation/opf-render/fonts-browser`).
|
|
7
8
|
* Only `textMeasurement` is required to measure; `pending` and `ensure` make the canvas load script and vendored faces before it draws.
|
|
@@ -30,6 +31,12 @@ export declare function whenFontsReady(
|
|
|
30
31
|
export interface CanvasEditorOptions {
|
|
31
32
|
editor?: EditorSession;
|
|
32
33
|
presentation?: unknown;
|
|
34
|
+
/**
|
|
35
|
+
* The host's catalogs (core `Catalog[]`, for example `[defaultCatalog]` from `@openpresentation/opf/catalog`) for the session the
|
|
36
|
+
* canvas creates from `presentation`. With `editor`, register them on that session instead (`createEditorSession(doc, { catalogs })`);
|
|
37
|
+
* `renderOptions.catalogs` adds catalogs for drawing only, merged after the session's.
|
|
38
|
+
*/
|
|
39
|
+
catalogs?: readonly Catalog[];
|
|
33
40
|
slideIndex?: number;
|
|
34
41
|
/** Show keyboard-accessible dividers for resizing composition tracks. */
|
|
35
42
|
layoutEditing?: boolean;
|
|
@@ -86,7 +93,7 @@ export interface CanvasEditor {
|
|
|
86
93
|
/** True while the crop layer is open. */
|
|
87
94
|
readonly cropping: boolean;
|
|
88
95
|
/**
|
|
89
|
-
* Open the crop layer for the picture at `path` (an image block
|
|
96
|
+
* Open the crop layer for the picture at `path` (an image block or a slide's `image`); `tool: "focus"`
|
|
90
97
|
* starts with the focal point. Apply writes one undoable change (see `@openpresentation/opf-editor/image-crop`). Resolves to whether it opened.
|
|
91
98
|
*/
|
|
92
99
|
cropImage(path: string, options?: { tool?: "crop" | "focus" }): Promise<boolean>;
|
package/dist/canvas.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createEditorSession,
|
|
3
|
+
mergeCatalogs,
|
|
3
4
|
opfPathToJsonPointer,
|
|
4
5
|
applyJsonPatch,
|
|
5
6
|
getValueAtPath,
|
|
@@ -67,12 +68,14 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
67
68
|
win = doc.defaultView;
|
|
68
69
|
const editor =
|
|
69
70
|
options.editor ??
|
|
70
|
-
createEditorSession(options.presentation, { rejectInvalid: true });
|
|
71
|
+
createEditorSession(options.presentation, { rejectInvalid: true, catalogs: options.catalogs });
|
|
71
72
|
// `options.fonts` is the renderer's fonts handle (`loadFonts()`): it measures the text and draws the faces. A document whose faces
|
|
72
73
|
// are not loaded yet is never rendered: the canvas has the handle load them first and shows "Loading fonts…" meanwhile. Without a
|
|
73
74
|
// handle every document renders at once, with core's portable text estimate.
|
|
74
75
|
const handle = options.fonts, fonts = fontGate(handle);
|
|
75
|
-
|
|
76
|
+
// The renderer draws with the session's registered catalogs (plus any `renderOptions.catalogs`): the same list core resolves with.
|
|
77
|
+
const resolveOptions = () => ({ ...renderOptions, catalogs: mergeCatalogs(editor.catalogs, renderOptions.catalogs) });
|
|
78
|
+
const drawOptions = (extra) => ({ ...resolveOptions(), fonts: previewFonts(handle), ...extra });
|
|
76
79
|
let slideIndex = options.slideIndex ?? 0,
|
|
77
80
|
// RR-32: the canvas edits the document as authored, so a template's {{tokens}} stay visible and an inline edit never
|
|
78
81
|
// overwrites one with its resolved text. The Fill template panel previews the resolved deck. Pass `variables` to override.
|
|
@@ -180,11 +183,11 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
180
183
|
// A synchronous render or validation of a document whose faces are still loading fails with a clear "fonts-pending"
|
|
181
184
|
// error and starts the load, so the next attempt succeeds. It never draws glyphs the registry cannot provide.
|
|
182
185
|
function requireFonts(presentation) {
|
|
183
|
-
const pending = fonts?.pending(presentation,
|
|
186
|
+
const pending = fonts?.pending(presentation, resolveOptions()) ?? [];
|
|
184
187
|
if (!pending.length) return;
|
|
185
188
|
notice.hidden = false;
|
|
186
189
|
notice.textContent = pendingMessage;
|
|
187
|
-
fonts.ensure(presentation, { renderOptions }).then(
|
|
190
|
+
fonts.ensure(presentation, { renderOptions: resolveOptions() }).then(
|
|
188
191
|
() => { if (!disposed && notice.textContent === pendingMessage) clearNotice(); },
|
|
189
192
|
(error) => { if (!disposed) report(error); },
|
|
190
193
|
);
|
|
@@ -223,7 +226,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
223
226
|
if (disposed) return;
|
|
224
227
|
const token = ++showToken;
|
|
225
228
|
return whenFontsReady(fonts, target ?? editor.presentation, {
|
|
226
|
-
renderOptions,
|
|
229
|
+
renderOptions: resolveOptions(),
|
|
227
230
|
isCurrent: () => !disposed && token === showToken,
|
|
228
231
|
loading: (pending) => fontsState("loading", pending),
|
|
229
232
|
ready: () => {
|
|
@@ -239,7 +242,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
239
242
|
}
|
|
240
243
|
// Renders now when the document's fonts are loaded (errors throw to the caller), otherwise after loading them.
|
|
241
244
|
function renderFor(presentation) {
|
|
242
|
-
if (fonts?.pending(presentation ?? editor.presentation,
|
|
245
|
+
if (fonts?.pending(presentation ?? editor.presentation, resolveOptions()).length) show(presentation);
|
|
243
246
|
else render(presentation);
|
|
244
247
|
}
|
|
245
248
|
function render(presentation = editor.presentation) {
|
|
@@ -366,11 +369,11 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
366
369
|
}
|
|
367
370
|
// An in-progress edit whose text needs faces that are not loaded yet waits for them, then draws again.
|
|
368
371
|
function deferDraft(draft) {
|
|
369
|
-
if (!fonts?.pending(draft,
|
|
372
|
+
if (!fonts?.pending(draft, resolveOptions()).length) return false;
|
|
370
373
|
active.valid = false;
|
|
371
374
|
notice.hidden = false;
|
|
372
375
|
notice.textContent = pendingMessage;
|
|
373
|
-
fonts.ensure(draft, { renderOptions }).then(
|
|
376
|
+
fonts.ensure(draft, { renderOptions: resolveOptions() }).then(
|
|
374
377
|
() => {
|
|
375
378
|
if (disposed) return;
|
|
376
379
|
if (notice.textContent === pendingMessage) clearNotice();
|
|
@@ -1020,7 +1023,7 @@ export function createCanvasEditor(container, options = {}) {
|
|
|
1020
1023
|
get layoutEditing() { return layoutHandles.enabled; },
|
|
1021
1024
|
/** True while the crop layer is open. */
|
|
1022
1025
|
get cropping() { return !!imageCropper?.isOpen; },
|
|
1023
|
-
/** Open the crop layer for the picture at `path` (an image block
|
|
1026
|
+
/** Open the crop layer for the picture at `path` (an image block or a slide's `image`). `tool: "focus"` starts with the focal point. */
|
|
1024
1027
|
cropImage(path, cropOptions) {
|
|
1025
1028
|
if (!imageCropper) return Promise.resolve(false);
|
|
1026
1029
|
return imageCropper.open(path, cropOptions);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Catalog, CatalogKind, CatalogRecordChange, EmbeddedRecord, UnresolvedReferenceDiagnostic, ValidationReport } from "@openpresentation/opf";
|
|
2
|
+
import type { CatalogOption, EditorChange, EditorSession, JsonPatchOperation } from "./index.js";
|
|
3
|
+
|
|
4
|
+
/** One catalog list: the session's registration first, then each host list; the first catalog with a `source` wins. Throws `invalid-catalogs` for an entry without a source. */
|
|
5
|
+
export declare function mergeCatalogs(...lists: (readonly Catalog[] | Catalog | undefined | null)[]): readonly Catalog[];
|
|
6
|
+
/** The catalogs for one operation: `editor.catalogs`, then `options.catalogs`. */
|
|
7
|
+
export declare function catalogsFor(editor: EditorSession | undefined, options?: { catalogs?: readonly Catalog[] }): readonly Catalog[];
|
|
8
|
+
/** A record's picker label: its name, then its gallery label, then the id. */
|
|
9
|
+
export declare function catalogRecordLabel(record: Record<string, unknown> | undefined, id: string): string;
|
|
10
|
+
/** Core's `catalogRecords` as picker options: embedded records first, then registered ones; `id` is the reference to write. */
|
|
11
|
+
export declare function listCatalogRecords(presentation: unknown, kind: CatalogKind, options?: { catalogs?: readonly Catalog[] }): CatalogOption[];
|
|
12
|
+
|
|
13
|
+
export interface SavedDocument {
|
|
14
|
+
/** The self-contained document: every referenced record embedded once under its group, `x-*` stripped. */
|
|
15
|
+
document: unknown;
|
|
16
|
+
added: EmbeddedRecord[];
|
|
17
|
+
unresolved: UnresolvedReferenceDiagnostic[];
|
|
18
|
+
}
|
|
19
|
+
/** Core's `embed`: the document to save or hand on, which renders the same with no catalog registered. The input is not changed. */
|
|
20
|
+
export declare function prepareSave(presentation: unknown, options?: { catalogs?: readonly Catalog[] }): SavedDocument;
|
|
21
|
+
/** `prepareSave` of the session's document with its registered catalogs (and `options.catalogs`). The session is not changed. */
|
|
22
|
+
export declare function saveDocument(editor: EditorSession, options?: { catalogs?: readonly Catalog[] }): SavedDocument;
|
|
23
|
+
|
|
24
|
+
export interface CatalogUpdateCheck {
|
|
25
|
+
/** One per embedded record (under `default` or a named group) whose registered record differs. */
|
|
26
|
+
changes: CatalogRecordChange[];
|
|
27
|
+
/** RFC 6902; apply only after the author approves (`applyCatalogUpdate`). */
|
|
28
|
+
patch: JsonPatchOperation[];
|
|
29
|
+
}
|
|
30
|
+
/** The "Update from catalog" check (core's `updateFromCatalog`). Applies nothing. `refs` limits it to some references. */
|
|
31
|
+
export declare function checkCatalogUpdates(presentation: unknown, options?: { catalogs?: readonly Catalog[]; refs?: readonly { kind: CatalogKind; reference: string }[] }): CatalogUpdateCheck;
|
|
32
|
+
/** Apply the approved updates (`refs`) as ONE undoable step, recomputed against the session's current document. */
|
|
33
|
+
export declare function applyCatalogUpdate(
|
|
34
|
+
editor: EditorSession,
|
|
35
|
+
options: { refs: readonly { kind: CatalogKind; reference: string }[]; catalogs?: readonly Catalog[]; meta?: Record<string, unknown> },
|
|
36
|
+
): EditorChange & { changed: boolean; changes: CatalogRecordChange[] };
|
|
37
|
+
|
|
38
|
+
export declare const REFERENCE_FINDING_CODES: readonly ["opf/unresolved-reference", "opf/undeclared-catalog", "opf/catalog-record-not-in-source"];
|
|
39
|
+
/** The reference and embedded-record findings (`REFERENCE_FINDING_CODES`) of a report. */
|
|
40
|
+
export declare function referenceFindings(validation: ValidationReport | undefined): ValidationReport["findings"];
|
|
41
|
+
|
|
42
|
+
export interface CatalogRecordRef { group: string; kind: CatalogKind; id: string }
|
|
43
|
+
/** The record a `opf/catalog-record-not-in-source` finding path names (`/catalogs/<group>/<kind>/<id>`). */
|
|
44
|
+
export declare function catalogRecordAt(path: string): CatalogRecordRef | undefined;
|
|
45
|
+
/** Move a record embedded under `default` or a named group into `catalogs.custom`, rewriting the references that resolved to it. */
|
|
46
|
+
export declare function prepareMoveToCustom(presentation: unknown, target: CatalogRecordRef, options?: { catalogs?: readonly Catalog[] }): { presentation: unknown; patches: JsonPatchOperation[]; changed: boolean; from: CatalogRecordRef; to: CatalogRecordRef };
|
|
47
|
+
/** `prepareMoveToCustom` as one undoable step on the session. */
|
|
48
|
+
export declare function moveToCustom(editor: EditorSession, target: CatalogRecordRef, options?: { catalogs?: readonly Catalog[]; meta?: Record<string, unknown> }): EditorChange & { changed: boolean; from: CatalogRecordRef; to: CatalogRecordRef };
|