@openpresentation/opf-editor 0.14.2 → 0.16.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 +93 -35
- 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 +25 -10
- package/dist/catalog-control.js +21 -0
- 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 +286 -82
- package/dist/design-options.d.ts +4 -4
- package/dist/design-options.js +6 -26
- package/dist/export.d.ts +8 -1
- package/dist/export.js +14 -4
- package/dist/find-replace.js +1 -0
- package/dist/font-gate.js +0 -11
- 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 +26 -8
- package/dist/index.js +107 -51
- 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 +37 -47
- package/dist/layout-handles.js +13 -0
- package/dist/layout-placeholders.js +15 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +6 -2
- 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 +4 -2
- package/dist/slides.d.ts +1 -1
- package/dist/slides.js +6 -5
- package/dist/svelte.d.ts +1 -0
- package/dist/svelte.js +4 -4
- package/dist/switches.d.ts +31 -16
- package/dist/switches.js +194 -92
- package/dist/table-options.d.ts +6 -1
- package/dist/table-options.js +43 -0
- package/dist/transfer.d.ts +12 -1
- package/dist/transfer.js +87 -131
- package/package.json +22 -9
package/README.md
CHANGED
|
@@ -2,7 +2,48 @@
|
|
|
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
|
-
|
|
5
|
+
## Using the editor with opf-render 0.16: what to install (RR-63, unreleased)
|
|
6
|
+
|
|
7
|
+
`@openpresentation/opf-render` is an optional peer of the editor, and from render 0.16 its heavy pieces are optional peers too, so install only what you use. The editor itself draws with `@openpresentation/opf-render/svg` and loads fonts with `/fonts-browser`, which need nothing else. Add:
|
|
8
|
+
|
|
9
|
+
| You use | Also install |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| The canvas, the preview, SVG downloads in a browser (the host serves its own font files) | nothing |
|
|
12
|
+
| `loadFonts` from `/fonts-node` (a server, a test, a build script such as `npm run build:playground`) | the font packages of the pack: `@expo-google-fonts/roboto` and `roboto-mono` for `base`; also `arimo`, `caladea`, `cousine`, `gelasio`, `tinos` and `noto-sans` for `office`; a script face (`scripts: 'auto'`) needs its `@expo-google-fonts/noto-*` package. A missing one fails with `font-resource-unavailable` and names it |
|
|
13
|
+
| `svgToPng` / `svgToPdf` from the Node entries (`/png`, `/pdf`) | `@resvg/resvg-js` (and `sharp`, `pdf-lib` as the renderer's README says); a missing one fails with `converter-missing` |
|
|
14
|
+
| `exportDeck` PDF (vector), PNG, SVG | nothing: the browser entry `@openpresentation/opf-render/export-browser` imports no converter |
|
|
15
|
+
| `exportDeck` raster PDF (`pdfMode: "raster"`) | `pdf-lib`, imported by the page and passed as the `pdfLib` option (`import * as pdfLib from "pdf-lib"`); without it the export rejects with `converter-missing` |
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const pdfLib = await import("pdf-lib");
|
|
19
|
+
await exportDeck(editor.presentation, { format: "pdf", pdfMode: "raster", pdfLib, fonts });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The playground imports `pdf-lib` for you when its raster PDF choice is used.
|
|
23
|
+
|
|
24
|
+
## OPF 0.15: catalogs from the host, backgrounds and image blocks (FA-23)
|
|
25
|
+
|
|
26
|
+
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`.
|
|
27
|
+
|
|
28
|
+
- **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:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { createEditorSession, saveDocument } from "@openpresentation/opf-editor";
|
|
32
|
+
import { defaultCatalog, catalogDisplay } from "@openpresentation/opf/catalog"; // the host app's choice, not the library's
|
|
33
|
+
|
|
34
|
+
const editor = createEditorSession(doc, { catalogs: [defaultCatalog, acmeCatalog] }); // the first is the host default
|
|
35
|
+
editor.catalogs; // the frozen list; editor.setCatalogs([...]) replaces it (one "catalogs" event)
|
|
36
|
+
const { document } = saveDocument(editor); // core embed: every referenced record embedded once, renders with no catalog
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`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`.
|
|
40
|
+
- **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.
|
|
41
|
+
- **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.
|
|
42
|
+
- **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)`).
|
|
43
|
+
- **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`.
|
|
44
|
+
- **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.
|
|
45
|
+
|
|
46
|
+
Editor 0.15.0 uses core `^0.15.1` and the optional renderer peer `^0.15.0`. Install renderer and PPTX 0.15 together for the coordinated canvas and export workflow. The shared APIs are core's `validate`, `Finding`, `resolveSlideContext` and the catalog helpers above, and the renderer's `loadFonts()` handle passed as `fonts` to the canvas, export, composition and pagination. Historical version changes are recorded in `CHANGELOG.md`.
|
|
6
47
|
|
|
7
48
|
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.
|
|
8
49
|
|
|
@@ -22,7 +63,7 @@ const control = mountJsonCodeEditor(container, {
|
|
|
22
63
|
code: source,
|
|
23
64
|
label: 'OPF JSON document',
|
|
24
65
|
lineNumbers: true,
|
|
25
|
-
catalogs:
|
|
66
|
+
catalogs: editor.catalogs, // the host's Catalog[] (for example [defaultCatalog])
|
|
26
67
|
onChange: nextSource => receiveDraft(nextSource),
|
|
27
68
|
onError: error => showError(error),
|
|
28
69
|
});
|
|
@@ -36,7 +77,7 @@ The host owns the source draft, OPF validation, slide preview, persistence and s
|
|
|
36
77
|
|
|
37
78
|
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
79
|
|
|
39
|
-
Click a categorical property/value or press `Ctrl+Space`, `Cmd+Space` or `Alt+Down` to open its choices.
|
|
80
|
+
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
81
|
|
|
41
82
|
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
83
|
|
|
@@ -52,7 +93,7 @@ Version 0.7.0 uses core 0.10.0 and renderer 0.8.0. Code source/metadata edits pr
|
|
|
52
93
|
- Compatibility target: `@openpresentation/opf`
|
|
53
94
|
- Renderer relationship: built on `@openpresentation/opf-render` trace output
|
|
54
95
|
- Headless JSON edit bindings for renderer trace attributes
|
|
55
|
-
- Structured catalog controls that only commit
|
|
96
|
+
- Structured catalog controls that only commit catalog references that resolve (in the document or the host's registered catalogs)
|
|
56
97
|
- JSON Patch state transitions with inverse patches for undo/redo
|
|
57
98
|
- Optional DOM controls plus React and Svelte bindings in separate embeddable entry points
|
|
58
99
|
- 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 +131,13 @@ import { createCanvasEditor, whenFontsReady } from '@openpresentation/opf-editor
|
|
|
90
131
|
import { loadFonts } from '@openpresentation/opf-render/fonts-browser';
|
|
91
132
|
|
|
92
133
|
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, {
|
|
134
|
+
// The canvas draws with the session's registered catalogs (plus renderOptions.catalogs) and hands the same list to the handle, so
|
|
135
|
+
// font schemes only your catalogs know resolve in the registry too.
|
|
136
|
+
const canvas = createCanvasEditor(container, { editor, fonts });
|
|
96
137
|
// Other renders of your own: whenFontsReady(fonts, presentation, { ready: draw, failed: showError, loading: showSpinner })
|
|
97
138
|
```
|
|
98
139
|
|
|
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.
|
|
140
|
+
`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
141
|
|
|
101
142
|
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
143
|
|
|
@@ -186,7 +227,7 @@ editor.redo();
|
|
|
186
227
|
|
|
187
228
|
## Catalog Controls
|
|
188
229
|
|
|
189
|
-
Catalog helpers commit catalog
|
|
230
|
+
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
231
|
|
|
191
232
|
```js
|
|
192
233
|
import {
|
|
@@ -194,7 +235,7 @@ import {
|
|
|
194
235
|
getCatalogOptions
|
|
195
236
|
} from "@openpresentation/opf-editor";
|
|
196
237
|
|
|
197
|
-
getCatalogOptions("themes").map((option) => option.id);
|
|
238
|
+
getCatalogOptions("themes", { editor }).map((option) => option.id); // references, embedded records first
|
|
198
239
|
|
|
199
240
|
const themeSelect = createCatalogSelect(editor, {
|
|
200
241
|
path: "design.theme",
|
|
@@ -205,7 +246,7 @@ const themeSelect = createCatalogSelect(editor, {
|
|
|
205
246
|
toolbar.append(themeSelect);
|
|
206
247
|
```
|
|
207
248
|
|
|
208
|
-
|
|
249
|
+
References that resolve nowhere throw before they reach the document. Existing object-form references keep their sibling override fields and replace only `id`.
|
|
209
250
|
|
|
210
251
|
## Dimension switches
|
|
211
252
|
|
|
@@ -225,18 +266,19 @@ const { patches, presentation } = prepareDimensionSwitch(editor.presentation, "t
|
|
|
225
266
|
| --- | --- | --- |
|
|
226
267
|
| `layouts` | `/slides/N/layout` | Needs `slideIndex`. Adds the blank payloads the layout declares, like the JSON editor's layout choice; existing content stays. |
|
|
227
268
|
| `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
|
-
| `
|
|
269
|
+
| `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. |
|
|
270
|
+
| `languages` | `/language` | A BCP-47 tag (`en-US`, `ja`) or a language object; `listSwitchOptions` offers core's `LANGUAGES`, labelled from `vocabularies.languages`. |
|
|
271
|
+
| `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. |
|
|
272
|
+
| `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
273
|
| `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. |
|
|
274
|
+
| `image-treatments` | `<image block>/fit`, `/focus`, `/shape`, ... | Needs `path` (the image block). The fields of `setImageTreatment`; `null` removes one. |
|
|
275
|
+
| `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
276
|
| `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
277
|
| `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
278
|
| `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
|
|
279
|
+
| `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
280
|
|
|
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`
|
|
281
|
+
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
282
|
|
|
241
283
|
### Content-type conversion (RR-06, RR-26)
|
|
242
284
|
|
|
@@ -280,14 +322,14 @@ Images, videos and any other group have no conversion. Everything else is replac
|
|
|
280
322
|
| 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
323
|
| 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
324
|
| 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, "
|
|
325
|
+
| 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
326
|
| 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
327
|
|
|
286
328
|
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
329
|
|
|
288
330
|
### Pickers: options, chart types and current values
|
|
289
331
|
|
|
290
|
-
`listSwitchOptions(presentation, dimension,
|
|
332
|
+
`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
333
|
|
|
292
334
|
## Design options (RR-06)
|
|
293
335
|
|
|
@@ -309,11 +351,11 @@ setHeaderFooterZone(editor, "footer", "right", { slideNumber: true, logo: true }
|
|
|
309
351
|
| `chartPrimary` | `design.chartPrimary` | `none`, `top`, `bottom`, `left`, `right` |
|
|
310
352
|
| `listBullet` | `design.listBullet` | `character`, `image` (picture bullets draw the logo) |
|
|
311
353
|
| `contentBox` | `design.contentBox` | `true`, `false` |
|
|
354
|
+
| `imageFit` | `design.imageFit` | `cover`, `contain`, `stretch`: the fit of image blocks that choose none |
|
|
312
355
|
| `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
356
|
| `logo` | `design.logo` | a source, an Asset object or a LogoSet; `setLogoVariant` edits one of the 12 variants |
|
|
314
357
|
| `organizationLogo` | `organization.logo` (deck only; `index` picks an organization) | a source or Asset object |
|
|
315
358
|
| `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
359
|
| 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
360
|
|
|
319
361
|
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 +372,18 @@ const change = await applyImageUpload(editor, file, (reference, presentation) =>
|
|
|
330
372
|
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
373
|
```
|
|
332
374
|
|
|
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,
|
|
375
|
+
`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
376
|
|
|
335
377
|
### Backgrounds (RR-06)
|
|
336
378
|
|
|
337
|
-
`@openpresentation/opf-editor/backgrounds` covers every background form the schema has: a theme slot, a solid color, a linear gradient, an image
|
|
379
|
+
`@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).
|
|
380
|
+
|
|
381
|
+
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.
|
|
382
|
+
|
|
383
|
+
### Image blocks (FA-22, OPF 0.15)
|
|
384
|
+
|
|
385
|
+
`@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.
|
|
386
|
+
|
|
338
387
|
|
|
339
388
|
## Footnotes, citations and captions (RR-34)
|
|
340
389
|
|
|
@@ -368,6 +417,8 @@ setTableCellStyle(editor, "slides.4.blocks.1.table", [{ section: "header", colum
|
|
|
368
417
|
|
|
369
418
|
Styles use scheme roles, so they follow the color scheme, and the renderers keep the text readable on any fill. A style sets the header fill (`theme`, `plain`, `accent`), banded rows and borders (`theme`, `none`, `horizontal`, `grid`); named presets are `theme`, `banded`, `grid`, `minimal` and `open`, and `theme` removes a previous style. A style owns only the fill and border fields of a cell: text color, alignment, padding, values and merges are kept. Merging never hides text: covered cells that hold text refuse with `merge-would-lose-content` unless you pass `join: true`, which joins the words into the anchor with a space and keeps run formatting. Other refusals are `merge-overlap` (the region crosses another merge), `invalid-table-span` (outside the table, or a header cell spanning into the body) and `table-cell-covered`. Splitting leaves the formerly covered cells as empty text. `parseTableCellPath` maps a canvas selection to a table and cell, and `describeTableCell` and `readTableStyle` read the current state.
|
|
370
419
|
|
|
420
|
+
Table alt text (FA-27): `setTableAlt(editor, tablePath, { alt: "North America leads EMEA in Q4." })` writes the table's text alternative (`Table.alt`) as one undoable patch, like a chart's. `alt` is trimmed and an empty string or `null` removes it; `{ decorative: true }` writes the empty alt (decorative, and it wins over `alt`) and `{ decorative: false }` removes an empty alt. It works on a table that shows a shared dataset too, since the alt belongs to the table and not to its data. `readTableAlt(table)` gives `{ alt, decorative }` for a form and `prepareTableAlt` is the headless patch. The table section of the design controls has an "Alt text" field and a "Decorative (no alt text)" box, and find and replace searches `table.alt`. `validate` does not ask for a table alt, so the field is optional.
|
|
421
|
+
|
|
371
422
|
## Slide management (RR-21)
|
|
372
423
|
|
|
373
424
|
`@openpresentation/opf-editor/slides` adds, duplicates, deletes, reorders, hides and sections slides. Every function has a `prepare…` form that returns the JSON Patch for a document without touching a session (`{ presentation, patches, changed, selection }`), and an apply form that commits it as ONE validated, undoable change, so a single Undo restores the deck exactly and the preview, thumbnails and PPTX export follow from the document. An operation that changes nothing commits nothing (`changed: false`).
|
|
@@ -441,13 +492,14 @@ const controls = createDesignControls(designPanel, {
|
|
|
441
492
|
editor,
|
|
442
493
|
getSlideIndex: () => slideIndex,
|
|
443
494
|
getSelectedPath: () => selectedPath,
|
|
444
|
-
sections: ["look", "
|
|
495
|
+
sections: ["look", "background", "header-footer", "brand", "layout-options", "info", "catalog"],
|
|
496
|
+
vocabularies: catalogDisplay, // from @openpresentation/opf/catalog, in the host app
|
|
445
497
|
});
|
|
446
|
-
const selectionControls = createDesignControls(contentPanel, { editor, getSlideIndex, getSelectedPath, sections: ["selection", "table", "slide-content"], onSelectPath: select });
|
|
498
|
+
const selectionControls = createDesignControls(contentPanel, { editor, getSlideIndex, getSelectedPath, sections: ["selection", "image", "table", "slide-content"], onSelectPath: select });
|
|
447
499
|
controls.refresh(); // when the slide or the selection changes
|
|
448
500
|
```
|
|
449
501
|
|
|
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), `
|
|
502
|
+
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
503
|
|
|
452
504
|
In React (or Svelte, or anything else) mount it into a ref and destroy it on unmount; it needs no framework runtime:
|
|
453
505
|
|
|
@@ -459,13 +511,13 @@ useEffect(() => {
|
|
|
459
511
|
// call controls.refresh() (keep it in a ref) when slide or selected changes
|
|
460
512
|
```
|
|
461
513
|
|
|
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,
|
|
514
|
+
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
515
|
|
|
464
516
|
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
517
|
|
|
466
518
|
### What a switch does not establish
|
|
467
519
|
|
|
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
|
|
520
|
+
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
521
|
|
|
470
522
|
## Autosave and restore (RR-22)
|
|
471
523
|
|
|
@@ -523,9 +575,9 @@ The headless pieces are usable on their own: `listTemplateFields(presentation, v
|
|
|
523
575
|
|
|
524
576
|
## Image crop and focal point (RR-25)
|
|
525
577
|
|
|
526
|
-
Select a picture on the canvas and use **Crop picture** (or `canvas.cropImage(path, { tool: "focus" })`, or the inspector's Picture section
|
|
578
|
+
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
579
|
|
|
528
|
-
**What is written.** The OPF schema has no crop rectangle
|
|
580
|
+
**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
581
|
|
|
530
582
|
## Phone and tablet (RR-25)
|
|
531
583
|
|
|
@@ -564,7 +616,7 @@ import { exportDeck } from "@openpresentation/opf-editor/export";
|
|
|
564
616
|
const result = await exportDeck(editor.presentation, {
|
|
565
617
|
format: "pdf", // "pdf" | "png" | "svg"
|
|
566
618
|
slides: "all", // or "current" with slideIndex, or [slide numbers]; hidden slides only with includeHidden
|
|
567
|
-
pdfMode: "vector", // or "raster" (an image per slide); PNG and raster density: scale 1 to 4
|
|
619
|
+
pdfMode: "vector", // or "raster" (an image per slide; also pass pdfLib: import * as pdfLib from "pdf-lib"); PNG and raster density: scale 1 to 4
|
|
568
620
|
fonts, // the renderer's fonts handle (loadFonts): measures, loads the faces the deck needs, supplies the faces to embed
|
|
569
621
|
renderOptions, // catalogs, date, ...
|
|
570
622
|
signal, onProgress, onDiagnostic,
|
|
@@ -578,7 +630,11 @@ const result = await exportDeck(editor.presentation, {
|
|
|
578
630
|
- **Names.** `exportFileName(deck, ext, suffix)` uses the deck's `filename` (a trailing .pptx/.pdf/.png/.svg dropped), else the slugified `name`, else `presentation`; slides are `name-01.png`, archives `name-png.zip`.
|
|
579
631
|
- **Progress and cancel.** `onProgress({ stage, done, total, message })` reports fonts, drawing, per-page conversion and packing; an aborted `signal` rejects with `export-aborted` between pages and slides.
|
|
580
632
|
- **Diagnostics.** `describeDiagnostic` normalises renderer and converter notes into `{ code, severity, message, slide? }`: `pdf-font-substituted`, `pdf-glyph-missing`, `pdf-raster-fallback` and the renderer's own are `warning`; `pdf-font-embedded` is `info`.
|
|
581
|
-
- PDF and PNG need `@openpresentation/opf-render` with its `export-browser` entry (RR-23, opf-render#105); without it they reject with `export-unavailable` and SVG still works. Verified in Chromium; Safari and Firefox are not exercised in CI. The playground bundle grows by the PDF writer, fontkit shaping and the bidi algorithm.
|
|
633
|
+
- PDF and PNG need `@openpresentation/opf-render` with its `export-browser` entry (RR-23, opf-render#105); without it they reject with `export-unavailable` and SVG still works. The entry imports no PDF library (opf-render 0.16): the vector PDF, PNG and SVG need nothing else, and `pdfMode: "raster"` needs the `pdfLib` option. Verified in Chromium; Safari and Firefox are not exercised in CI. The playground bundle grows by the PDF writer, fontkit shaping and the bidi algorithm.
|
|
634
|
+
|
|
635
|
+
Catalog controls initialize an absent deck or slide `design` for known theme, color-scheme and font-scheme fields as one validated transaction. Undo removes a newly created parent; existing design fields are retained. DOM, React and Svelte catalog controls accept `onError(error)` and emit a bubbling `opferror` event on rejected edits, restore the committed selection and expose native validity feedback. General `editor.set` and JSON Patch require existing parents.
|
|
636
|
+
|
|
637
|
+
Optional downloads can be deferred until requested: dynamically import `@openpresentation/opf-editor/export` inside the download handler. For a PowerPoint download, dynamically import `@openpresentation/opf-pptx` inside that handler too; the `/export` helper covers PDF, PNG and SVG. This defers conversion code, while canvas rendering and font measurement still have their own startup costs.
|
|
582
638
|
|
|
583
639
|
## Optional React Bindings
|
|
584
640
|
|
|
@@ -626,7 +682,7 @@ Public npm package publication is handled by `.github/workflows/release.yml` thr
|
|
|
626
682
|
|
|
627
683
|
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See the sibling OPF repo's `docs/dynamic-composition.md` for the complete contract.
|
|
628
684
|
|
|
629
|
-
|
|
685
|
+
The current editor requires core `^0.15.1`; canvas rendering uses the optional renderer peer `^0.15.0`, and playground PPTX import/export uses `@openpresentation/opf-pptx@^0.15.0`. Clean registry installs support composition without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
630
686
|
|
|
631
687
|
## Local interactive demo
|
|
632
688
|
|
|
@@ -638,7 +694,7 @@ Nested content groups expose their bounds through `editor.composeSlide(index).gr
|
|
|
638
694
|
|
|
639
695
|
`editor.paginateSlide(index, {minFontSize:24})` splits a crowded draft into ordinary OPF slides as one undoable transaction. It returns the change and source mappings; failed pagination leaves the document unchanged. The playground’s Split overflow action demonstrates the workflow.
|
|
640
696
|
|
|
641
|
-
`composeSlide(index, { fonts })` and `paginateSlide(index, { fonts })` accept the same fonts handle as preview and export. The local playground now uses bundled, embedded fonts and reports substitutions. The font a user selects (for example Calibri or Aptos) is the source of truth and stays in the document; because license-restricted (proprietary) fonts are never bundled, the canvas draws an open look-alike (a metric-compatible one such as Carlito for Calibri where it exists,
|
|
697
|
+
`composeSlide(index, { fonts })` and `paginateSlide(index, { fonts })` accept the same fonts handle as preview and export. The local playground now uses bundled, embedded fonts and reports substitutions. The font a user selects (for example Calibri or Aptos) is the source of truth and stays in the document; because license-restricted (proprietary) fonts are never bundled, the canvas draws an open look-alike (a metric-compatible one such as Carlito for Calibri where it exists, the metric-compatible Intos family for Aptos) and the substitution report says so. Since 0.14, PPTX export names the selected font, while preview substitutions remain visible in the font report. See the [OPF font policy](https://github.com/OpenPresentation/opf/blob/main/docs/font-fidelity.md#font-policy-ff-31).
|
|
642
698
|
|
|
643
699
|
|
|
644
700
|
## Copy, paste, and gallery imports
|
|
@@ -649,7 +705,7 @@ The `/galleries` export provides `loadOpfGallery` and `loadOpfGalleryItem`, acce
|
|
|
649
705
|
|
|
650
706
|
The playground includes Copy OPF and Import dialogs with paste, file/drop, gallery search, and URL tabs. Files accept OPF JSON and PowerPoint `.pptx` up to 20 MB. PPTX conversion runs locally using `@openpresentation/opf-pptx`; review the converted preview and diagnostics before inserting slides or opening a presentation. Applying an import is one undoable edit. Custom registries persist in local storage; defaults are configured in `examples/galleries.json`. Copy permissions may require using the Select all fallback. Pasting within text fields keeps normal text editing. Cmd/Ctrl+Shift+C opens copying and Cmd/Ctrl+O opens file import.
|
|
651
707
|
|
|
652
|
-
The **PowerPoint** button commits the current canvas draft and prepares an editable `.pptx` download from that snapshot, using the preview's text measurements. Conversion diagnostics remain visible in the download dialog. Missing/remote images require an explicit host resolver or embedded data; export does not silently fetch them or omit them. Save OPF separately to retain the complete original source.
|
|
708
|
+
The **PowerPoint** button commits the current canvas draft and prepares an editable `.pptx` download from that snapshot, using the preview's text measurements. Conversion diagnostics remain visible in the download dialog. Missing/remote images require an explicit host resolver or embedded data; export does not silently fetch them or omit them. Save OPF separately to retain the complete original source. Since 0.14, PPTX export names the font the user selected, so PowerPoint uses it when installed or available as a Microsoft 365 cloud font. The download does not embed font binaries, and license-restricted (proprietary) fonts are never embedded. Arbitrary native positions and unsupported PowerPoint features can change during reimport.
|
|
653
709
|
|
|
654
710
|
Run `npm ci`, `npm run build:playground`, then serve `artifacts/playground` with a local static server. This example uses the published core/render/PPTX packages and this checkout's editor source; it does not require an account, AI provider or paid service. `npm run test:playground` builds and executes a real browser flow (local Edge on Windows, installed Chromium in CI): author JSON, inline edit, export a native merged table, validate/reimport the actual downloaded file, undo/redo, save OPF and reject malformed input without changing the document. It is browser behavior evidence, not a claim of native PowerPoint raster equivalence.
|
|
655
711
|
|
|
@@ -685,6 +741,8 @@ The canvas can show draggable, keyboard-accessible dividers for root and nested
|
|
|
685
741
|
|
|
686
742
|
### Move complete blocks
|
|
687
743
|
|
|
744
|
+
When the slide's layout record has placeholder groups (FA-26, `{ "type": "group", "composition", "placeholders" }`), Arrange also outlines every slot of the record at its cell (`geometry.slots`): groups, filled regions and labelled empty regions. The slots belong to the record, so they are shown, not edited; the slide's content in them keeps its own selection and handles. Layout pickers and the JSON field menu describe such a record as `title, column (text, text), chart` and compare layouts by their leaf regions.
|
|
745
|
+
|
|
688
746
|
Arrange mode also shows numbered block handles. Drag to reorder siblings, use arrow keys for earlier/later, or click a handle to choose an existing destination group/slide and insertion position. The move preserves the whole block, including rich text, table/chart data, and nested groups. Each move is validated, preflighted by the renderer, and undoable. Parent weights stay with layout positions; moves that leave an empty container or create a containment cycle are rejected.
|
|
689
747
|
|
|
690
748
|
Agents can import `prepareBlockMove` and `listBlockContainers` from `@openpresentation/opf-editor/layout`. The prepared result includes a full candidate document, guarded atomic patches, the new block path, and a changed flag. Destination indexes refer to the pre-removal document. Use `canvas.openBlockMenu(path)` for the corresponding browser controls.
|
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>;
|