@openpresentation/opf-editor 0.27.0 → 0.28.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 (40) hide show
  1. package/README.md +4 -4
  2. package/dist/design-controls.js +3 -2
  3. package/dist/design-options.js +1 -1
  4. package/dist/json-editor.d.ts +33 -0
  5. package/dist/json-editor.js +83 -2
  6. package/dist/layout-adapter.js +96 -0
  7. package/dist/layout-canvas.js +122 -232
  8. package/dist/layout-editor-ui.js +46 -185
  9. package/dist/layout-inspector.js +36 -99
  10. package/dist/layout-layers.js +75 -92
  11. package/dist/layout-model.js +62 -103
  12. package/dist/react.d.ts +4 -0
  13. package/dist/react.js +9 -2
  14. package/dist/source-color-scheme.js +60 -0
  15. package/dist/source-pane.d.ts +85 -0
  16. package/dist/source-pane.js +151 -9
  17. package/dist/studio-kit/commands.d.ts +38 -0
  18. package/dist/studio-kit/commands.js +148 -0
  19. package/dist/studio-kit/dom.d.ts +36 -0
  20. package/dist/studio-kit/dom.js +162 -0
  21. package/dist/studio-kit/drag.d.ts +26 -0
  22. package/dist/studio-kit/drag.js +127 -0
  23. package/dist/studio-kit/gesture.d.ts +29 -0
  24. package/dist/studio-kit/gesture.js +87 -0
  25. package/dist/studio-kit/index.d.ts +29 -0
  26. package/dist/studio-kit/index.js +12 -0
  27. package/dist/studio-kit/inspector.d.ts +22 -0
  28. package/dist/studio-kit/inspector.js +119 -0
  29. package/dist/studio-kit/overlay.d.ts +29 -0
  30. package/dist/studio-kit/overlay.js +182 -0
  31. package/dist/studio-kit/selection.d.ts +30 -0
  32. package/dist/studio-kit/selection.js +106 -0
  33. package/dist/studio-kit/tree.d.ts +9 -0
  34. package/dist/studio-kit/tree.js +82 -0
  35. package/dist/studio-kit/viewport.d.ts +29 -0
  36. package/dist/studio-kit/viewport.js +153 -0
  37. package/dist/styles.css +28 -7
  38. package/dist/workspace.d.ts +26 -1
  39. package/dist/workspace.js +13 -0
  40. package/package.json +10 -10
package/dist/styles.css CHANGED
@@ -19,7 +19,7 @@
19
19
  --opf-ui-border-strong: hsl(214 22% 72%);
20
20
  --opf-ui-ring: hsl(214 57% 28%);
21
21
  --opf-ui-danger: hsl(0 72% 45%);
22
- --opf-ui-warn: hsl(28 75% 38%);
22
+ --opf-ui-warn: hsl(28 75% 30%);
23
23
  --opf-ui-success: hsl(152 76% 26%);
24
24
  --opf-ui-code-bg: hsl(220 30% 96%);
25
25
  --opf-ui-code-border: hsl(214 18% 84%);
@@ -90,7 +90,7 @@
90
90
  --opf-ui-border-strong: hsl(214 22% 72%);
91
91
  --opf-ui-ring: hsl(214 57% 28%);
92
92
  --opf-ui-danger: hsl(0 72% 45%);
93
- --opf-ui-warn: hsl(28 75% 38%);
93
+ --opf-ui-warn: hsl(28 75% 30%);
94
94
  --opf-ui-success: hsl(152 76% 26%);
95
95
  --opf-ui-code-bg: hsl(220 30% 96%);
96
96
  --opf-ui-code-border: hsl(214 18% 84%);
@@ -3673,9 +3673,21 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
3673
3673
  background: color-mix(in srgb, var(--opf-ui-accent) 8%, transparent);
3674
3674
  }
3675
3675
 
3676
- :is(.opf-ui, .opf-json, .opf-code) .cm-selectionBackground,
3677
- :is(.opf-ui, .opf-json, .opf-code) .cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
3678
- background: color-mix(in srgb, var(--opf-ui-accent) 20%, transparent);
3676
+ /* The selection is a layer behind the text, so the text keeps its own colors: the tint is light enough that every token color (test/selection-contrast-browser.mjs
3677
+ measures each) keeps 4.5:1 against it, focused or not, in the light and the dark scheme. The focused rule of CodeMirror's base theme
3678
+ (`&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground`, `#d7d4f0` for light and `#233` for dark) has five classes, so
3679
+ the second selector has six and wins whichever stylesheet loads last; src/source-color-scheme.js flags the view dark so the rest of the base theme follows. */
3680
+ :is(.opf-ui, .opf-json, .opf-code) .cm-editor .cm-selectionLayer .cm-selectionBackground,
3681
+ :is(.opf-ui, .opf-json, .opf-code) .cm-editor.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
3682
+ background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent);
3683
+ }
3684
+
3685
+ /* A selection that is not drawn by CodeMirror (the handoff prompt, the data grid's text areas, the numbering preview) uses the browser's own, and a browser's
3686
+ default highlight is not made for our surfaces: the same tint, with the text color the surface already has, is 4.5:1 or better in both schemes. */
3687
+ :is(.opf-json, .opf-code, .opf-source-code, .opf-handoff-box, .opf-numbering-preview) ::selection,
3688
+ :is(.opf-handoff-prompt, .opf-numbering-preview, .opf-grid details textarea)::selection {
3689
+ background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent);
3690
+ color: var(--opf-ui-fg);
3679
3691
  }
3680
3692
 
3681
3693
  :is(.opf-ui, .opf-json, .opf-code) .cm-selectionMatch {
@@ -4518,8 +4530,7 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4518
4530
  .opf-source-code .cm-gutters { border: 0; background: transparent; color: color-mix(in srgb, var(--opf-ui-muted-fg) 60%, transparent); }
4519
4531
  .opf-source-code .cm-lineNumbers .cm-gutterElement { min-width: 30px; padding: 0 12px 0 0; text-align: right; font-size: 11px; }
4520
4532
  .opf-source-code .cm-activeLine { background: transparent; }
4521
- .opf-source-code .cm-selectionBackground,
4522
- .opf-source-code .cm-focused .cm-selectionBackground { background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent); }
4533
+ /* The selection color is the shared one in panels.css (JSON editor section). */
4523
4534
  .opf-source-code .cm-cursor { border-left-color: var(--opf-ui-fg); }
4524
4535
  .opf-source-code .tok-key { color: var(--opf-ui-accent-strong); }
4525
4536
  .opf-source-code .tok-str { color: var(--opf-ui-fg); }
@@ -4532,6 +4543,16 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4532
4543
  .opf-source-code .tok-comment { font-style: italic; }
4533
4544
  .opf-source-code .code-key.is-active { background: color-mix(in srgb, var(--opf-ui-accent) 10%, transparent); border-radius: var(--opf-ui-radius-sm); }
4534
4545
  .opf-source-code .cm-line.is-hover { background: color-mix(in srgb, var(--opf-ui-accent) 6%, transparent); }
4546
+ /* CE-17: decorations a host draws (setDecorations) and the flash of revealPath; the host styles any other kind as .opf-source-decoration-<kind>. */
4547
+ .opf-source-code .opf-source-decoration { background: color-mix(in srgb, var(--opf-ui-accent) 16%, transparent); border-radius: var(--opf-ui-radius-sm); }
4548
+ .opf-source-code .opf-source-decoration-error { background: color-mix(in srgb, var(--opf-ui-danger) 18%, transparent); }
4549
+ .opf-source-code .opf-source-decoration-warning { background: color-mix(in srgb, var(--opf-ui-warn) 20%, transparent); }
4550
+ .opf-source-code .opf-source-decoration-info { background: color-mix(in srgb, var(--opf-ui-accent) 10%, transparent); }
4551
+ .opf-source-code .opf-source-decoration-added { background: color-mix(in srgb, var(--opf-ui-success) 20%, transparent); }
4552
+ .opf-source-code .opf-source-decoration-changed { background: color-mix(in srgb, var(--opf-ui-warn) 14%, transparent); }
4553
+ .opf-source-code .cm-line.opf-source-flash { animation: opf-source-flash 1.2s ease-out; }
4554
+ @keyframes opf-source-flash { from { background: color-mix(in srgb, var(--opf-ui-accent) 32%, transparent); } to { background: transparent; } }
4555
+ @media (prefers-reduced-motion: reduce) { .opf-source-code .cm-line.opf-source-flash { animation: none; background: color-mix(in srgb, var(--opf-ui-accent) 20%, transparent); } }
4535
4556
  .opf-source-code .cm-panels { background: var(--opf-ui-surface); color: var(--opf-ui-fg); border-color: var(--opf-ui-border); }
4536
4557
  .opf-source-code .cm-tooltip { background: var(--opf-ui-surface); border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius); color: var(--opf-ui-fg); }
4537
4558
  .opf-source-code .cm-lintRange-error { background-image: none; text-decoration: underline wavy var(--opf-ui-danger); }
@@ -11,7 +11,7 @@ import type { Presenter } from "./presenter.js";
11
11
  import type { ReviewFinding, ReviewHook } from "./review.js";
12
12
  import type { ReviewPanel, ReviewPanelOptions } from "./review-panel.js";
13
13
  import type { SlideManager } from "./slide-manager.js";
14
- import type { SourceFormat, SourceFile, SourcePane, SourcePaneOptions } from "./source-pane.js";
14
+ import type { SourceCursor, SourceFormat, SourceFile, SourcePane, SourcePaneOptions } from "./source-pane.js";
15
15
 
16
16
  export type OpfEditorLayout = "full" | "canvas" | "source";
17
17
  export type OpfEditorView = "slide" | "sorter" | "outline" | "layouts";
@@ -77,6 +77,19 @@ export interface ThemeController {
77
77
  /** Apply a theme to the editor mounted in `root`. `createOpfEditor` does this for `options.theme` and `handle.setTheme`. */
78
78
  export declare function createThemeController(root: HTMLElement, theme?: OpfEditorTheme): ThemeController;
79
79
 
80
+ /**
81
+ * The options for composing slide `slideIndex` of `deck` with the fonts handle `fonts`: its text measurement, made script-aware for this document
82
+ * (Japanese under Aptos measures with Noto Sans JP, where the plain strict measurement would throw). Pass the result to the session's
83
+ * `composeSlide` and `paginateSlide`, and the same `catalogs` the editor was given. Without a handle it is `{}`, and a handle without a
84
+ * measurement is passed through.
85
+ */
86
+ export declare function layoutOptionsFor(
87
+ fonts: EditorFonts | undefined,
88
+ deck: unknown,
89
+ slideIndex: number,
90
+ catalogs?: readonly Catalog[],
91
+ ): { fonts?: EditorFonts };
92
+
80
93
  /** What a host's own acquire function returns: the handle and the way to give it back. */
81
94
  export interface OpfEditorFontsLease {
82
95
  fonts: EditorFonts;
@@ -168,6 +181,8 @@ export interface OpfEditorSourceOptions {
168
181
  checks?: { only?: readonly string[]; [key: string]: unknown };
169
182
  outline?: SourcePaneOptions["outline"];
170
183
  applyDelay?: number;
184
+ /** Milliseconds of quiet before a `cursor` event (default 50). */
185
+ cursorDelay?: number;
171
186
  completions?: boolean;
172
187
  label?: string;
173
188
  catalogs?: SourcePaneOptions["catalogs"];
@@ -240,6 +255,8 @@ export interface OpfEditorOptions {
240
255
 
241
256
  onChange?(detail: OpfEditorEvents["change"]): void;
242
257
  onSelection?(detail: OpfEditorEvents["selection"]): void;
258
+ /** The caret of the source pane moved to another place in the text. The same as `on: { cursor }`. */
259
+ onCursor?(detail: OpfEditorEvents["cursor"]): void;
243
260
  onError?(error: Error, detail: Omit<OpfEditorEvents["error"], "error">): void;
244
261
  onReady?(detail: OpfEditorEvents["ready"]): void;
245
262
  /** Listeners by event name; the same as calling `on` before anything happens. */
@@ -261,6 +278,12 @@ export interface OpfEditorEvents {
261
278
  /** The document changed (an edit in the canvas or the source, undo, redo, a loaded document). Not fired for a catalogs change. */
262
279
  change: { document: unknown; canUndo: boolean; canRedo: boolean; event: EditorEvent };
263
280
  selection: { path: string; value: unknown; slideIndex: number; element: Element | undefined };
281
+ /**
282
+ * The caret or selection of the source pane moved, in JSON, YAML or Markdown, also while the pane has no focus: the path under it as a JSON Pointer
283
+ * and by id, and the selection in source offsets. Quiet for `source.cursorDelay` first and not repeated for a place already reported; the detail is
284
+ * frozen. For the rest of the pane (`revealPath`, `setDecorations`, `getCursor`) use `editor.sourcePane`.
285
+ */
286
+ cursor: SourceCursor;
264
287
  /** The slide changed (0-based). */
265
288
  slide: { index: number; count: number };
266
289
  view: { view: OpfEditorView; previous: OpfEditorView };
@@ -333,6 +356,8 @@ export interface OpfEditor {
333
356
  readonly readOnly: boolean;
334
357
  /** False while the text in the source pane is not a valid deck: the slide shows the last valid one. */
335
358
  readonly inSync: boolean;
359
+ /** The latest `cursor` event, or `null` before the first (and without a source pane). The same object until the next event. */
360
+ readonly sourceCursor: SourceCursor | null;
336
361
  /** The latest verdict on the text of the source pane (what the last `source` event carried), or `null` before the first. */
337
362
  readonly sourceState: OpfEditorSourceState | null;
338
363
  readonly lastError: Error | undefined;
package/dist/workspace.js CHANGED
@@ -36,6 +36,8 @@ import { retainedSlideIndex } from "./slide-retention.js";
36
36
  import { createSourceMemory, updateJsonSource } from "./exact-source.js";
37
37
 
38
38
  export { THEME_TOKENS, createThemeController, normalizeTheme, normalizeThemeTokens } from "./workspace-theme.js";
39
+ // 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
+ export { layoutOptionsFor } from "./workspace-measure.js";
39
41
 
40
42
  const VIEWS = Object.freeze(["slide", "sorter", "outline", "layouts"]);
41
43
  const MUTATORS = new Set(["set", "setGroupComposition", "setCatalog", "setCatalogs", "applyPatch", "replaceDocument", "restoreState", "undo", "redo", "setComposition", "paginateSlide"]);
@@ -150,6 +152,7 @@ export function createOpfEditor(element, options = {}) {
150
152
  for (const [name, listener] of Object.entries(options.on ?? {})) on(name, listener);
151
153
  if (options.onChange) on("change", options.onChange);
152
154
  if (options.onSelection) on("selection", options.onSelection);
155
+ if (options.onCursor) on("cursor", options.onCursor);
153
156
  if (options.onError) on("error", ({ error, ...detail }) => options.onError(error, detail));
154
157
  if (options.onReady) on("ready", options.onReady);
155
158
 
@@ -220,6 +223,7 @@ export function createOpfEditor(element, options = {}) {
220
223
  let drafting = false;
221
224
  let openedText;
222
225
  let sourceState = null;
226
+ let sourceCursor = null;
223
227
  let fontsFailure;
224
228
  let refreshToken = 0;
225
229
  let lastRefresh = Promise.resolve();
@@ -538,6 +542,7 @@ export function createOpfEditor(element, options = {}) {
538
542
  return {
539
543
  ...(configured.outline !== undefined ? { outline: configured.outline } : {}),
540
544
  ...(configured.applyDelay !== undefined ? { applyDelay: configured.applyDelay } : {}),
545
+ ...(configured.cursorDelay !== undefined ? { cursorDelay: configured.cursorDelay } : {}),
541
546
  ...(configured.completions !== undefined ? { completions: configured.completions } : {}),
542
547
  ...(configured.catalogs !== undefined ? { catalogs: configured.catalogs } : {}),
543
548
  ...(configured.name !== undefined ? { name: configured.name } : {}),
@@ -553,6 +558,11 @@ export function createOpfEditor(element, options = {}) {
553
558
  configured.onRename?.(name);
554
559
  emit("rename", { name });
555
560
  },
561
+ // CE-17: the path under the caret of the source pane, as `cursor` events.
562
+ onCursor: (detail) => {
563
+ sourceCursor = Object.freeze(detail);
564
+ emit("cursor", sourceCursor);
565
+ },
556
566
  getFindings: ({ format, text: shown }) => {
557
567
  reportedText = shown;
558
568
  const made = reportSource(format, shown, { catalogs: catalogs(), checks: configured.checks ?? SOURCE_CHECKS });
@@ -1090,6 +1100,9 @@ export function createOpfEditor(element, options = {}) {
1090
1100
  return inSync;
1091
1101
  },
1092
1102
  /** The latest verdict on the text of the source pane (what the last `source` event carried), or `null` before the first. */
1103
+ get sourceCursor() {
1104
+ return sourceCursor;
1105
+ },
1093
1106
  get sourceState() {
1094
1107
  return sourceState;
1095
1108
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -283,7 +283,7 @@
283
283
  "types": "./dist/index.d.ts",
284
284
  "sideEffects": false,
285
285
  "dependencies": {
286
- "@codemirror/autocomplete": "6.20.3",
286
+ "@codemirror/autocomplete": "6.20.4",
287
287
  "@codemirror/commands": "6.11.1",
288
288
  "@codemirror/lang-json": "6.0.2",
289
289
  "@codemirror/lang-yaml": "6.1.3",
@@ -291,15 +291,15 @@
291
291
  "@codemirror/lint": "6.9.7",
292
292
  "@codemirror/search": "6.7.2",
293
293
  "@codemirror/state": "6.7.6",
294
- "@codemirror/view": "6.43.14",
294
+ "@codemirror/view": "6.43.15",
295
295
  "@lezer/highlight": "1.2.5",
296
296
  "jsonc-parser": "3.3.1",
297
297
  "yaml": "^2.9.1",
298
- "@openpresentation/opf": "^0.27.0"
298
+ "@openpresentation/opf": "^0.28.0"
299
299
  },
300
300
  "peerDependencies": {
301
301
  "react": ">=18",
302
- "@openpresentation/opf-render": "^0.27.0"
302
+ "@openpresentation/opf-render": "^0.28.0"
303
303
  },
304
304
  "peerDependenciesMeta": {
305
305
  "@openpresentation/opf-render": {
@@ -360,14 +360,14 @@
360
360
  "jszip": "3.10.2",
361
361
  "pdf-lib": "1.17.1",
362
362
  "pdfjs-dist": "6.4.299",
363
- "playwright": "1.64.0",
363
+ "playwright": "1.64.1",
364
364
  "react": "19.3.1",
365
365
  "react-dom": "19.3.1",
366
366
  "react-18": "npm:react@18.3.1",
367
367
  "react-dom-18": "npm:react-dom@18.3.1",
368
- "@openpresentation/opf-pptx": "0.27.0",
369
- "@openpresentation/gallery": "0.27.0",
370
- "@openpresentation/opf-render": "0.27.0"
368
+ "@openpresentation/gallery": "0.28.0",
369
+ "@openpresentation/opf-pptx": "0.28.0",
370
+ "@openpresentation/opf-render": "0.28.0"
371
371
  },
372
372
  "scripts": {
373
373
  "build": "node scripts/build.mjs",
@@ -420,5 +420,5 @@
420
420
  "test:workspace-browser": "node test/workspace-browser.mjs",
421
421
  "test:workspace-react-browser": "node test/workspace-react-browser.mjs"
422
422
  },
423
- "gitHead": "f90536d0e9a7a16b25aa718e03ced4e00a7316e1"
423
+ "gitHead": "abc56634a584db38a305a27edc3bc76b4116637b"
424
424
  }