@openpresentation/opf-editor 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +20 -6
  2. package/dist/canvas.d.ts +13 -1
  3. package/dist/canvas.js +39 -9
  4. package/dist/chart-options-panel.js +16 -16
  5. package/dist/codemirror-guard.js +69 -0
  6. package/dist/content-controls.js +127 -74
  7. package/dist/data-grid.js +15 -7
  8. package/dist/design-controls.d.ts +13 -0
  9. package/dist/design-controls.js +134 -124
  10. package/dist/exact-source.d.ts +30 -0
  11. package/dist/font-gate.js +20 -1
  12. package/dist/json-editor.js +8 -2
  13. package/dist/json-options.d.ts +2 -1
  14. package/dist/json-options.js +7 -6
  15. package/dist/layout-templates.d.ts +8 -3
  16. package/dist/layout-templates.js +30 -16
  17. package/dist/numbering-panel.js +2 -3
  18. package/dist/react.d.ts +12 -4
  19. package/dist/react.js +39 -5
  20. package/dist/slide-adapter.d.ts +49 -0
  21. package/dist/slide-adapter.js +289 -0
  22. package/dist/slide-edits.d.ts +46 -0
  23. package/dist/slide-edits.js +629 -0
  24. package/dist/slide-manager.js +2 -1
  25. package/dist/slide-scene.d.ts +96 -0
  26. package/dist/slide-scene.js +413 -0
  27. package/dist/source-checker.js +45 -6
  28. package/dist/source-completions.js +6 -4
  29. package/dist/source-pane.d.ts +18 -6
  30. package/dist/source-pane.js +45 -0
  31. package/dist/source-worker.js +8 -4
  32. package/dist/studio-canvas.d.ts +34 -0
  33. package/dist/studio-canvas.js +586 -0
  34. package/dist/studio-commands.d.ts +15 -0
  35. package/dist/studio-commands.js +155 -0
  36. package/dist/studio-design.d.ts +16 -0
  37. package/dist/studio-design.js +354 -0
  38. package/dist/studio-kit/dom.d.ts +16 -0
  39. package/dist/studio-kit/dom.js +42 -6
  40. package/dist/studio-layers.d.ts +12 -0
  41. package/dist/studio-layers.js +218 -0
  42. package/dist/studio.d.ts +31 -0
  43. package/dist/studio.js +1608 -0
  44. package/dist/styles.css +1546 -117
  45. package/dist/thumbnail-cache.d.ts +2 -0
  46. package/dist/thumbnail-cache.js +10 -1
  47. package/dist/workspace-measure.js +6 -4
  48. package/dist/workspace-parts.js +20 -0
  49. package/dist/workspace-shell.js +25 -15
  50. package/dist/workspace-theme.js +3 -0
  51. package/dist/workspace.d.ts +88 -9
  52. package/dist/workspace.js +210 -49
  53. package/package.json +29 -18
package/dist/workspace.js CHANGED
@@ -7,29 +7,13 @@
7
7
  // A host picks a layout: `"full"` (a bar, the source, the slide, the slide row and a side panel are built for it), `"canvas"`, `"source"`,
8
8
  // or `slots`, an object of its own elements, one per part, for a host that arranges the page itself. Each part is optional (`panels`).
9
9
  // Nothing here fetches anything, stores anything on its own or talks to a service; the host supplies fonts, catalogs and storage.
10
- import { toSvg } from "@openpresentation/opf-render/svg";
11
10
  import { OPFEditorError, createEditorSession, mergeCatalogs } from "./index.js";
12
- import { createCanvasEditor } from "./canvas.js";
13
11
  import { whenFontsReady } from "./font-gate.js";
14
12
  import { createSourcePane } from "./source-pane.js";
15
- import { createSlideNavigator } from "./slide-navigator.js";
16
- import { createThumbnailStrip } from "./thumbnail-strip.js";
17
- import { createThumbnailCache } from "./thumbnail-cache.js";
18
- import { createSlideManager } from "./slide-manager.js";
19
- import { createOutlineView } from "./outline-view.js";
20
- import { createExportMenu } from "./export-menu.js";
21
- import { bindPresentShortcut, createPresentButton, createPresenter } from "./presenter.js";
22
- import { createReviewPanel } from "./review-panel.js";
23
- import { createFindPanel, installFindShortcuts } from "./find-panel.js";
24
- import { createDesignControls } from "./design-controls.js";
25
13
  import { createPersistence } from "./persistence.js";
26
14
  import { createPersistenceUi } from "./persistence-ui.js";
27
- import { prepareDimensionSwitch } from "./switches.js";
28
- import { mergeSlides, splitSlideByBlocks } from "./content-actions.js";
29
- import { installCanvasVariableEditing } from "./canvas-variables.js";
30
- import { createLayoutStudio } from "./workspace-layouts.js";
31
- import { fontSubstitutionNotes, layoutOptionsFor } from "./workspace-measure.js";
32
15
  import { buildWorkspaceShell } from "./workspace-shell.js";
16
+ import { fontSubstitutionNotes, layoutOptionsFor } from "./workspace-measure.js";
33
17
  import { SOURCE_CHECKS, copyText, downloadSourceFile, lf, reportSource } from "./workspace-source.js";
34
18
  import { createThemeController } from "./workspace-theme.js";
35
19
  import { retainedSlideIndex } from "./slide-retention.js";
@@ -39,6 +23,34 @@ export { THEME_TOKENS, createThemeController, normalizeTheme, normalizeThemeToke
39
23
  // The script-aware measurement options for composing one slide: a host that calls `composeSlide` or `paginateSlide` on the session passes these (the playground does).
40
24
  export { layoutOptionsFor } from "./workspace-measure.js";
41
25
 
26
+ // The slide and everything around it (the renderer, the canvas, the slide row, the presenter, Export, Review, Find, Design and the views) load
27
+ // on demand, in one chunk (`workspace-parts.js`): a host that mounts only the source pane never loads the renderer. Its factories are bound
28
+ // here once the chunk has arrived, before any part is mounted.
29
+ let toSvg, createCanvasEditor, createSlideNavigator, createThumbnailStrip, createThumbnailCache, createSlideManager, createOutlineView, createExportMenu;
30
+ let bindPresentShortcut, createPresentButton, createPresenter, createReviewPanel, createFindPanel, installFindShortcuts, createDesignControls;
31
+ let prepareDimensionSwitch, mergeSlides, splitSlideByBlocks, installCanvasVariableEditing, createLayoutStudio, buildStudioShell, createStudio;
32
+ let partsModule;
33
+ let partsLoaded = false;
34
+ function loadParts() {
35
+ partsModule ??= import("./workspace-parts.js").then((parts) => {
36
+ ({ toSvg, createCanvasEditor, createSlideNavigator, createThumbnailStrip, createThumbnailCache, createSlideManager, createOutlineView, createExportMenu } = parts);
37
+ ({ bindPresentShortcut, createPresentButton, createPresenter, createReviewPanel, createFindPanel, installFindShortcuts, createDesignControls } = parts);
38
+ ({ prepareDimensionSwitch, mergeSlides, splitSlideByBlocks, installCanvasVariableEditing, createLayoutStudio, buildStudioShell, createStudio } = parts);
39
+ partsLoaded = true;
40
+ });
41
+ // A chunk that failed to load is asked for again by the next editor.
42
+ partsModule.catch(() => {
43
+ partsModule = undefined;
44
+ });
45
+ return partsModule;
46
+ }
47
+ /**
48
+ * Whether the parts to mount need the slide chunk: any part but the source pane, its autosave and the history buttons, that has an element to
49
+ * mount in (the presenter mounts in the overlay).
50
+ */
51
+ const SOURCE_ONLY_PARTS = new Set(["source", "persistence", "history"]);
52
+ const needsSlideParts = (parts, slots) => Object.entries(parts).some(([name, on]) => on && !SOURCE_ONLY_PARTS.has(name) && (name === "present" || Boolean(slots[name])));
53
+
42
54
  const VIEWS = Object.freeze(["slide", "sorter", "outline", "layouts"]);
43
55
  const MUTATORS = new Set(["set", "setGroupComposition", "setCatalog", "setCatalogs", "applyPatch", "replaceDocument", "restoreState", "undo", "redo", "setComposition", "paginateSlide"]);
44
56
  const STARTER = Object.freeze({ name: "Untitled presentation", slides: [{ id: "slide-1", title: "Untitled slide" }] });
@@ -57,7 +69,11 @@ const LAYOUT_PARTS = {
57
69
  full: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, sorter: true, outline: true },
58
70
  canvas: { navigator: true, thumbnails: true, history: true },
59
71
  source: { source: true },
72
+ // CE-20: the Studio and its Playground preset arrange the same parts; the Studio mounts its own Design panel.
73
+ studio: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, slides: true },
74
+ playground: { source: true, navigator: true, thumbnails: true, history: true, find: true, present: true, export: true, review: true, layouts: true, slides: true },
60
75
  };
76
+ const STUDIO_LAYOUTS = new Set(["studio", "playground"]);
61
77
 
62
78
  const isThenable = (value) => value !== null && typeof value === "object" && typeof value.then === "function";
63
79
 
@@ -110,7 +126,9 @@ function resolveParts(options, layout, slots) {
110
126
  }
111
127
 
112
128
  /**
113
- * Mount the editor in `element`. Returns the handle at once; with a `fonts` promise the parts mount when it resolves (`ready` settles then).
129
+ * Mount the editor in `element`. Returns the handle at once; the parts mount once a `fonts` promise has resolved and, for a layout that shows
130
+ * slides, once the slide chunk has loaded (`mounted` is emitted then, and `ready` settles after the first update). A source-only editor whose
131
+ * fonts are at hand mounts before this returns.
114
132
  * See `workspace.d.ts` for the options and the handle.
115
133
  */
116
134
  export function createOpfEditor(element, options = {}) {
@@ -165,8 +183,9 @@ export function createOpfEditor(element, options = {}) {
165
183
 
166
184
  // ---- layout, parts and slots ------------------------------------------------------------------------------------------------
167
185
  const given = options.slots ?? {};
168
- const layout = options.slots ? "slots" : (options.layout ?? "full");
169
- if (!["full", "canvas", "source", "slots"].includes(layout)) throw new RangeError('layout must be "full", "canvas" or "source", or pass `slots`.');
186
+ const layout = options.slots && !STUDIO_LAYOUTS.has(options.layout) ? "slots" : (options.layout ?? "full");
187
+ if (!["full", "canvas", "source", "slots", "studio", "playground"].includes(layout)) throw new RangeError('layout must be "full", "canvas", "source", "studio" or "playground", or pass `slots`.');
188
+ const studioLayout = STUDIO_LAYOUTS.has(layout);
170
189
  const parts = resolveParts(options, layout, given);
171
190
  const views = VIEWS.filter((view) => view === "slide" || parts[view]);
172
191
  if (options.view !== undefined && !views.includes(options.view)) throw new RangeError(`view "${options.view}" is not available in this layout (${views.join(", ")}).`);
@@ -183,13 +202,27 @@ export function createOpfEditor(element, options = {}) {
183
202
  const themeController = createThemeController(element, options.theme);
184
203
  let slots = { ...given };
185
204
  let controls = {};
205
+ let studioShell = null;
206
+ let overlay = root;
207
+ // CE-20: the Studio builds its own shell; a host's slots add parts to it (`topbarEnd`, `overlay`). Its code is in the slide chunk, so the Studio
208
+ // shell is built once that has loaded, right before the parts mount.
209
+ function buildStudio() {
210
+ const built = buildStudioShell(root, { parts, layout, labels: options.labels, studio: options.studio, slots: given });
211
+ const { topbarEnd: _placed, ...rest } = given;
212
+ slots = { ...built.slots, ...rest };
213
+ controls = built.controls;
214
+ studioShell = built.shell;
215
+ overlay = slots.overlay ?? root;
216
+ }
186
217
  if (layout === "slots") root.setAttribute("data-opf-layout", "slots");
187
- else {
218
+ else if (!studioLayout) {
188
219
  const built = buildWorkspaceShell(root, { parts, views, layout, labels: options.labels });
189
220
  slots = { ...built.slots, ...given };
190
221
  controls = built.controls;
191
222
  }
192
- const overlay = slots.overlay ?? root;
223
+ overlay = slots.overlay ?? root;
224
+ // The slide chunk is loaded before the parts mount, unless only the source pane (with its autosave) is mounted. The Studio always needs it.
225
+ const slideParts = studioLayout || needsSlideParts(parts, slots);
193
226
 
194
227
  // ---- state ------------------------------------------------------------------------------------------------------------------
195
228
  let fonts;
@@ -201,6 +234,8 @@ export function createOpfEditor(element, options = {}) {
201
234
  let sorter;
202
235
  let outline;
203
236
  let studio;
237
+ let studioCtl;
238
+ let fontsPending = false;
204
239
  let presenter;
205
240
  let presentButton;
206
241
  let exportMenu;
@@ -234,7 +269,7 @@ export function createOpfEditor(element, options = {}) {
234
269
  let frameSize = { width: 0, height: 0 };
235
270
  const draftMemory = createSourceMemory();
236
271
  const variablesMode = options.variables === "resolved" ? "resolved" : "authored";
237
- const fitMode = options.fit === undefined ? (layout === "slots" ? false : "contain") : options.fit;
272
+ const fitMode = studioLayout ? false : options.fit === undefined ? (layout === "slots" ? false : "contain") : options.fit;
238
273
 
239
274
  const say = (message) => {
240
275
  statusText = message;
@@ -255,7 +290,8 @@ export function createOpfEditor(element, options = {}) {
255
290
 
256
291
  // ---- navigation and the refresh loop ----------------------------------------------------------------------------------------
257
292
  function renderViews(deck) {
258
- thumbnailCache?.prepare(deck);
293
+ // The Studio's filmstrip draws the thumbnails it shows itself, as they scroll in.
294
+ if (!studioLayout) thumbnailCache?.prepare(deck);
259
295
  slideManager?.render();
260
296
  if (activeView === "sorter") sorter?.render();
261
297
  if (activeView === "outline") outline?.render();
@@ -283,7 +319,8 @@ export function createOpfEditor(element, options = {}) {
283
319
  }
284
320
  function sync() {
285
321
  fonts?.registry?.clearSubstitutions?.();
286
- const deck = session.presentation;
322
+ // The frozen snapshot (one object per version): the session's `presentation` copies the whole deck on every read.
323
+ const deck = session.snapshot().presentation;
287
324
  slideIndex = Math.max(0, Math.min(slideIndex, deck.slides.length - 1));
288
325
  renderViews(deck);
289
326
  slideNav?.setSlide(slideIndex);
@@ -300,16 +337,18 @@ export function createOpfEditor(element, options = {}) {
300
337
  if (!mounted || destroyed) return Promise.resolve();
301
338
  const token = ++refreshToken;
302
339
  let loaded = false;
303
- lastRefresh = whenFontsReady(fonts, session.presentation, {
340
+ lastRefresh = whenFontsReady(fonts, session.snapshot().presentation, {
304
341
  renderOptions: renderOptions(),
305
342
  isCurrent: () => token === refreshToken && !destroyed,
306
343
  loading: () => {
307
344
  loaded = true;
345
+ fontsPending = true;
308
346
  emit("fonts", { state: "loading" });
309
347
  say(MESSAGES.loadingFonts);
310
348
  },
311
349
  ready: () => {
312
350
  fontsFailure = undefined;
351
+ fontsPending = false;
313
352
  if (loaded) {
314
353
  thumbnailCache?.clear();
315
354
  emit("fonts", { state: "ready" });
@@ -354,7 +393,8 @@ export function createOpfEditor(element, options = {}) {
354
393
  }
355
394
  function goTo(index, path) {
356
395
  if (index !== slideIndex && !go(index)) return false;
357
- if (path) canvas?.reveal(path);
396
+ if (studioCtl) studioCtl.revealPath(path ?? `slides.${index}`);
397
+ else if (path) canvas?.reveal(path);
358
398
  return true;
359
399
  }
360
400
 
@@ -367,7 +407,8 @@ export function createOpfEditor(element, options = {}) {
367
407
  if (controls.redo) controls.redo.disabled = !inSync || readOnly || !session.canRedo;
368
408
  // Text in the source pane that is not a valid deck: the slide shows the last valid one and is not editable until the text is fixed.
369
409
  const frame = slots.canvas && (slots.slide ?? slots.canvas);
370
- if (frame && (!inSync || readOnly)) frame.setAttribute("inert", "");
410
+ // The Studio keeps selection, zoom and Present while read-only (its commands say why they are off); only invalid text locks the slide.
411
+ if (frame && (!inSync || (readOnly && !studioLayout))) frame.setAttribute("inert", "");
371
412
  else frame?.removeAttribute("inert");
372
413
  }
373
414
  function applyFit() {
@@ -393,7 +434,8 @@ export function createOpfEditor(element, options = {}) {
393
434
  setSlideIndex: (index) => {
394
435
  go(index);
395
436
  },
396
- renderThumbnail: (deck, index) => thumbnailCache.html(deck, index),
437
+ // The Studio's filmstrip draws only the thumbnails it shows (an IntersectionObserver fills the rest as they scroll in).
438
+ renderThumbnail: studioLayout ? (deck, index) => thumbnailCache.peek(deck, index) ?? "" : (deck, index) => thumbnailCache.html(deck, index),
397
439
  renderLayoutPreview: (deck, index, choice) => {
398
440
  const candidate = prepareDimensionSwitch(deck, "layouts", choice.id, { slideIndex: index, catalogs: catalogs() }).presentation;
399
441
  return toSvg(candidate, index + 1, { ...renderOptions(), fonts, text: "system", trace: false });
@@ -454,6 +496,7 @@ export function createOpfEditor(element, options = {}) {
454
496
  return true;
455
497
  }
456
498
  function showPanel(name, focus = false) {
499
+ if (studioCtl) return studioCtl.showTab(name, { focus });
457
500
  const tabs = controls.tabs;
458
501
  if (!tabs?.[name]) return false;
459
502
  for (const [tabName, tab] of Object.entries(tabs)) {
@@ -475,6 +518,8 @@ export function createOpfEditor(element, options = {}) {
475
518
  strip?.destroy();
476
519
  slideNav = undefined;
477
520
  strip = undefined;
521
+ // The Studio shows its filmstrip; the slide row is the Playground preset's, mounted only while that preset is on.
522
+ if (studioLayout && (studioCtl?.layout ?? layout) !== "playground") return;
478
523
  if (parts.navigator && slots.navigator) {
479
524
  slideNav = createSlideNavigator(slots.navigator, {
480
525
  editor: ui,
@@ -625,6 +670,7 @@ export function createOpfEditor(element, options = {}) {
625
670
  );
626
671
  canvas = createCanvasEditor(slots.canvas, {
627
672
  ...configured,
673
+ ...(studioLayout ? { textEntry: layout === "playground" ? "click" : "dblclick", targets: layout === "playground" ? "interactive" : "passive", escape: "commit" } : {}),
628
674
  editor: ui,
629
675
  fonts,
630
676
  renderOptions: renderOptions(),
@@ -635,17 +681,22 @@ export function createOpfEditor(element, options = {}) {
635
681
  if (event.state === "error") {
636
682
  fontsFailure = event.error;
637
683
  say(event.error.message);
638
- } else if (event.state === "loading") say(MESSAGES.loadingFonts);
684
+ } else if (event.state === "loading") {
685
+ fontsPending = true;
686
+ say(MESSAGES.loadingFonts);
687
+ }
639
688
  else if (fontsFailure) {
640
689
  fontsFailure = undefined;
641
690
  refresh();
642
691
  }
692
+ if (event.state === "ready") fontsPending = false;
643
693
  emit("fonts", event);
644
694
  configured.onFonts?.(event);
645
695
  },
646
696
  onSelect: (value) => {
647
697
  selection = { path: value.path, value: value.value };
648
- emit("selection", { path: value.path, value: value.value, slideIndex, element: value.element });
698
+ // The Studio emits its own selection (a list of nodes); the canvas's single selection is internal there.
699
+ if (!studioLayout) emit("selection", { path: value.path, value: value.value, slideIndex, element: value.element });
649
700
  design?.refresh();
650
701
  configured.onSelect?.(value);
651
702
  },
@@ -783,7 +834,7 @@ export function createOpfEditor(element, options = {}) {
783
834
  onFocusField: ({ fix, target }) => emit("review-focus", { fix, target }),
784
835
  onStatus: say,
785
836
  onChange: (counts) => {
786
- const tab = controls.tabs?.review;
837
+ const tab = controls.tabs?.review ?? studioShell?.tabs.review?.tab;
787
838
  if (tab) {
788
839
  const name = options.labels?.review ?? "Review";
789
840
  const found = !counts.unavailable && counts.total;
@@ -795,13 +846,13 @@ export function createOpfEditor(element, options = {}) {
795
846
  });
796
847
  }
797
848
 
798
- function present() {
849
+ function present(from) {
799
850
  if (destroyed || !mounted || !presenter || presenter.isRunning() || !inSync || slides.length === 0) return false;
800
851
  // An open canvas edit is committed, and text typed a moment ago is applied, before the document is shown; a source that is not a valid deck waits.
801
852
  if (canvas?.commit() === false) return false;
802
853
  pane?.getText();
803
854
  if (draftInvalid) return false;
804
- void presenter.start(canvas?.slideIndex ?? slideIndex);
855
+ void presenter.start(Number.isInteger(from) ? from : (canvas?.slideIndex ?? slideIndex));
805
856
  return true;
806
857
  }
807
858
 
@@ -809,7 +860,7 @@ export function createOpfEditor(element, options = {}) {
809
860
  slides = session.presentation.slides ?? [];
810
861
  authoredTemplate = session.presentation.template === true;
811
862
  if (parts.canvas && slots.canvas) mountCanvas();
812
- thumbnailCache = createThumbnailCache({ toSvg, getOptions: () => ({ ...renderOptions(), fonts, text: "system", trace: false }) });
863
+ if (slideParts) thumbnailCache = createThumbnailCache({ toSvg, getOptions: () => ({ ...renderOptions(), fonts, text: "system", trace: false }) });
813
864
  mountSlideRow();
814
865
  if (parts.slides && slots.slides) slideManager = createSlideManager(slots.slides, { ...slideManagerOptions(), ...(slots.slideToolbar ? { toolbar: slots.slideToolbar } : {}) });
815
866
  if (parts.source && slots.source) {
@@ -867,6 +918,7 @@ export function createOpfEditor(element, options = {}) {
867
918
  if (parts.persistence) mountPersistence();
868
919
  mountControls();
869
920
  mountEnvironment();
921
+ if (studioShell && canvas) mountStudio();
870
922
  // These come last, so a host hears a change after every part has. The refresh runs for every event, the catalogs' included.
871
923
  cleanups.push(
872
924
  session.subscribe((event) => {
@@ -890,6 +942,66 @@ export function createOpfEditor(element, options = {}) {
890
942
  );
891
943
  }
892
944
 
945
+ // CE-20: the Studio over the parts mounted above (src/studio.js).
946
+ function mountStudio() {
947
+ studioCtl = createStudio({
948
+ root,
949
+ element,
950
+ shell: studioShell,
951
+ slots,
952
+ session,
953
+ ui,
954
+ canvas,
955
+ options,
956
+ layout,
957
+ hostCatalogs,
958
+ get pane() {
959
+ return pane;
960
+ },
961
+ get find() {
962
+ return find;
963
+ },
964
+ get exportMenu() {
965
+ return exportMenu;
966
+ },
967
+ get slideManager() {
968
+ return slideManager;
969
+ },
970
+ get thumbnailCache() {
971
+ return thumbnailCache;
972
+ },
973
+ get persistence() {
974
+ return persistence;
975
+ },
976
+ get handle() {
977
+ return handle;
978
+ },
979
+ slideIndex: () => slideIndex,
980
+ go: (index) => go(index),
981
+ present: (from) => present(from),
982
+ say,
983
+ emit,
984
+ report,
985
+ on,
986
+ setView: (view) => setView(view),
987
+ readOnly: () => readOnly,
988
+ inSync: () => inSync,
989
+ fontsLoading: () => fontsPending,
990
+ catalogs,
991
+ layoutFor: (deck, index) => layoutFor(deck, index),
992
+ setDocument: (document, openOptions) => setDocument(document, openOptions),
993
+ openFind: (openOptions) => {
994
+ if (!find) return false;
995
+ find.openPanel(openOptions ?? {});
996
+ return true;
997
+ },
998
+ getName: () => pane?.getName(),
999
+ setName: (name) => pane?.setName(name),
1000
+ layoutStudio: () => studio,
1001
+ mountSlideRow: () => mountSlideRow(),
1002
+ });
1003
+ }
1004
+
893
1005
  // The shell's own controls: history, views, tabs.
894
1006
  function mountControls() {
895
1007
  controls.undo?.addEventListener("click", () => undo());
@@ -977,6 +1089,7 @@ export function createOpfEditor(element, options = {}) {
977
1089
  studio?.setReadOnly(readOnly);
978
1090
  root.toggleAttribute("data-readonly", readOnly);
979
1091
  syncChrome();
1092
+ studioCtl?.refresh();
980
1093
  }
981
1094
  function setDensity(setting) {
982
1095
  const next = densityFor(setting);
@@ -1014,16 +1127,29 @@ export function createOpfEditor(element, options = {}) {
1014
1127
  } catch (error) {
1015
1128
  if (error?.code !== "invalid-opf-edit") throw error;
1016
1129
  // A document the session refuses is no document to open: show its text with its findings, as a draft the person can fix.
1017
- if (pane && text !== undefined) {
1018
- if (openOptions.format && pane.getFormat() !== openOptions.format) pane.setFormat(openOptions.format);
1019
- const api = pane.editor.api;
1020
- api.replace(0, api.getValue().length, text);
1021
- pane.getText();
1022
- if (openOptions.name !== undefined) pane.setName(openOptions.name);
1023
- }
1130
+ if (pane && text !== undefined) pane.setSourceText(text, { ...(openOptions.format ? { format: openOptions.format } : {}), ...(openOptions.name !== undefined ? { name: openOptions.name } : {}) });
1024
1131
  return false;
1025
1132
  }
1026
1133
  }
1134
+ // Raw text in the source pane (a file as it was read, a draft that does not parse), without converting it: what the person would get by
1135
+ // pasting it over everything. True when the session now holds its document; false when it stays a draft with its problems.
1136
+ function setSourceText(text, openOptions = {}) {
1137
+ if (destroyed || !mounted) throw new OPFEditorError("not-ready", "The editor is not mounted yet. Await `ready` first.");
1138
+ if (!pane) throw new OPFEditorError("no-source-pane", "This editor has no source pane.");
1139
+ if (readOnly) throw new OPFEditorError("read-only", "This editor is read-only.");
1140
+ if (typeof text !== "string") throw new TypeError("setSourceText needs the source text as a string.");
1141
+ // An open canvas edit is committed first; the text then replaces it.
1142
+ if (canvas?.commit() === false) return false;
1143
+ drafting = false;
1144
+ openedText = undefined;
1145
+ const applied = pane.setSourceText(text, {
1146
+ ...(openOptions.format ? { format: openOptions.format } : {}),
1147
+ ...(openOptions.name !== undefined ? { name: openOptions.name } : {}),
1148
+ });
1149
+ // Text the host loaded is not the person's work: it is not autosaved or warned about until it is edited.
1150
+ if (applied && openOptions.rebase !== false) persistence?.rebase();
1151
+ return applied;
1152
+ }
1027
1153
 
1028
1154
  let fontsRef = { value: undefined };
1029
1155
  let releaseFonts;
@@ -1084,8 +1210,34 @@ export function createOpfEditor(element, options = {}) {
1084
1210
  get slideIndex() {
1085
1211
  return slideIndex;
1086
1212
  },
1213
+ /** The selection: the canvas's `{ path, value }` (or null); in the Studio, the list `[{ key, path, idPath, kind }]`. */
1087
1214
  get selection() {
1088
- return selection;
1215
+ return studioCtl ? studioCtl.selection : selection;
1216
+ },
1217
+ /** CE-20: the arrangement ("full", "canvas", "source", "slots", or the Studio's "studio" and "playground"). */
1218
+ get layout() {
1219
+ return studioCtl?.layout ?? layout;
1220
+ },
1221
+ /** CE-20: switch the Studio between "studio" and "playground" in place (same session, history, slide and selection). */
1222
+ setLayout(next) {
1223
+ if (!studioCtl) return false;
1224
+ return studioCtl.setLayout(next);
1225
+ },
1226
+ get zoom() {
1227
+ return studioCtl?.zoom ?? 1;
1228
+ },
1229
+ setZoom(value) {
1230
+ studioCtl?.setZoom(value);
1231
+ },
1232
+ fit(mode) {
1233
+ studioCtl?.fit(mode);
1234
+ },
1235
+ openLayout(layoutId, openOptions = {}) {
1236
+ return studioCtl ? studioCtl.openLayout(layoutId, openOptions) : false;
1237
+ },
1238
+ /** CE-20: the Studio's command registry (`list`, `run`, `register`); empty outside the Studio. */
1239
+ get commands() {
1240
+ return studioCtl?.commands ?? { list: () => [], run: () => false, register: () => () => {} };
1089
1241
  },
1090
1242
  get view() {
1091
1243
  return activeView;
@@ -1116,11 +1268,13 @@ export function createOpfEditor(element, options = {}) {
1116
1268
  return session.snapshot().presentation;
1117
1269
  },
1118
1270
  setDocument,
1271
+ setSourceText,
1119
1272
  setView,
1120
1273
  setSlide: go,
1121
1274
  goTo,
1122
- select(path, { focus = false } = {}) {
1123
- return canvas?.reveal(path, { focus }) ?? null;
1275
+ select(path, selectOptions = {}) {
1276
+ if (studioCtl) return studioCtl.selectTargets(path, selectOptions);
1277
+ return canvas?.reveal(path, { focus: Boolean(selectOptions.focus) }) ?? null;
1124
1278
  },
1125
1279
  showPanel,
1126
1280
  present,
@@ -1187,6 +1341,7 @@ export function createOpfEditor(element, options = {}) {
1187
1341
  /* already gone */
1188
1342
  }
1189
1343
  }
1344
+ studioCtl?.destroy();
1190
1345
  unbindKeys?.();
1191
1346
  checking?.disconnect();
1192
1347
  persistence?.destroy();
@@ -1214,6 +1369,7 @@ export function createOpfEditor(element, options = {}) {
1214
1369
 
1215
1370
  // ---- start ------------------------------------------------------------------------------------------------------------------
1216
1371
  function mount(resolved) {
1372
+ if (studioLayout && !studioShell) buildStudio();
1217
1373
  fonts = resolved;
1218
1374
  density = densityFor(options.density);
1219
1375
  exportMode = exportModeFor(options.exportMode);
@@ -1254,15 +1410,20 @@ export function createOpfEditor(element, options = {}) {
1254
1410
  };
1255
1411
  try {
1256
1412
  fontsRef = fontsSource(options.fonts);
1257
- if (isThenable(fontsRef.value)) {
1258
- say(MESSAGES.loadingFonts);
1259
- Promise.resolve(fontsRef.value).then((resolved) => {
1413
+ const chunk = slideParts && !partsLoaded ? loadParts() : null;
1414
+ if (isThenable(fontsRef.value) || chunk) {
1415
+ if (isThenable(fontsRef.value)) say(MESSAGES.loadingFonts);
1416
+ Promise.all([fontsRef.value, chunk]).then(([resolved]) => {
1260
1417
  try {
1261
1418
  start(resolved);
1262
1419
  } catch (error) {
1263
1420
  fail(error);
1264
1421
  }
1265
- }, fail);
1422
+ }, (error) => {
1423
+ // The slide chunk did not load: fonts that did arrive are given back.
1424
+ Promise.resolve(fontsRef.value).then((resolved) => fontsRef.use(resolved).release?.(), () => {});
1425
+ fail(error);
1426
+ });
1266
1427
  } else start(fontsRef.value);
1267
1428
  } catch (error) {
1268
1429
  fail(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -239,6 +239,10 @@
239
239
  "types": "./dist/source-pane.d.ts",
240
240
  "default": "./dist/source-pane.js"
241
241
  },
242
+ "./exact-source": {
243
+ "types": "./dist/exact-source.d.ts",
244
+ "default": "./dist/exact-source.js"
245
+ },
242
246
  "./presenter": {
243
247
  "types": "./dist/presenter.d.ts",
244
248
  "default": "./dist/presenter.js"
@@ -281,25 +285,29 @@
281
285
  }
282
286
  },
283
287
  "types": "./dist/index.d.ts",
284
- "sideEffects": false,
288
+ "sideEffects": [
289
+ "./dist/source-worker.js",
290
+ "./dist/styles.css"
291
+ ],
285
292
  "dependencies": {
286
- "@codemirror/autocomplete": "6.20.4",
287
- "@codemirror/commands": "6.11.1",
288
- "@codemirror/lang-json": "6.0.2",
289
- "@codemirror/lang-yaml": "6.1.3",
290
- "@codemirror/language": "6.13.1",
291
- "@codemirror/lint": "6.9.7",
292
- "@codemirror/search": "6.7.2",
293
- "@codemirror/state": "6.7.6",
294
- "@codemirror/view": "6.43.15",
295
- "@lezer/highlight": "1.2.5",
293
+ "@codemirror/autocomplete": "^6.20.4",
294
+ "@codemirror/commands": "^6.11.1",
295
+ "@codemirror/lang-json": "^6.0.2",
296
+ "@codemirror/lang-yaml": "^6.1.3",
297
+ "@codemirror/language": "^6.13.1",
298
+ "@codemirror/lint": "^6.9.7",
299
+ "@codemirror/search": "^6.7.2",
300
+ "@codemirror/state": "^6.7.6",
301
+ "@codemirror/view": "^6.43.15",
302
+ "@lezer/highlight": "^1.2.5",
296
303
  "jsonc-parser": "3.3.1",
297
304
  "yaml": "^2.9.1",
298
- "@openpresentation/opf": "^0.28.0"
305
+ "@openpresentation/opf": "^0.29.0"
299
306
  },
300
307
  "peerDependencies": {
301
308
  "react": ">=18",
302
- "@openpresentation/opf-render": "^0.28.0"
309
+ "react-dom": ">=18",
310
+ "@openpresentation/opf-render": "^0.29.0"
303
311
  },
304
312
  "peerDependenciesMeta": {
305
313
  "@openpresentation/opf-render": {
@@ -307,6 +315,9 @@
307
315
  },
308
316
  "react": {
309
317
  "optional": true
318
+ },
319
+ "react-dom": {
320
+ "optional": true
310
321
  }
311
322
  },
312
323
  "devDependencies": {
@@ -365,9 +376,9 @@
365
376
  "react-dom": "19.3.1",
366
377
  "react-18": "npm:react@18.3.1",
367
378
  "react-dom-18": "npm:react-dom@18.3.1",
368
- "@openpresentation/gallery": "0.28.0",
369
- "@openpresentation/opf-pptx": "0.28.0",
370
- "@openpresentation/opf-render": "0.28.0"
379
+ "@openpresentation/gallery": "0.29.0",
380
+ "@openpresentation/opf-pptx": "0.29.0",
381
+ "@openpresentation/opf-render": "0.29.0"
371
382
  },
372
383
  "scripts": {
373
384
  "build": "node scripts/build.mjs",
@@ -420,5 +431,5 @@
420
431
  "test:workspace-browser": "node test/workspace-browser.mjs",
421
432
  "test:workspace-react-browser": "node test/workspace-react-browser.mjs"
422
433
  },
423
- "gitHead": "abc56634a584db38a305a27edc3bc76b4116637b"
434
+ "gitHead": "464e10d3c15647b0e43326ac7e6761e34789991a"
424
435
  }