@openpresentation/opf-editor 0.18.0 → 0.18.2

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 CHANGED
@@ -158,6 +158,19 @@ Plain-text carets are resolved from the rendered SVG glyphs: each line carries i
158
158
 
159
159
  The canvas retains canonical SVG glyphs while a transparent native input supplies the caret. Advanced shaping, freeform object positioning, all chart/media treatments, and cross-engine pixel identity remain work in progress. The Source dialog in the playground now provides a live JSON preview; changes are validated before committing.
160
160
 
161
+ ### Input methods (IME): not certified
162
+
163
+ IME input is **not certified**. Real Japanese (Kana to Kanji), Chinese (pinyin) and Korean (Hangul) input methods have not been tested in this editor, on any browser or operating system; no run with a real input method is recorded (opf-editor#137). Only synthetic `CompositionEvent`s are tested (`test/rich-input-browser.mjs` dispatches `compositionstart`, an `insertCompositionText` input and `compositionend` on the rich-text input and checks that the canvas does not commit midway and that one Undo restores the original). Synthetic events do not reproduce a candidate window, a re-render during a composition, or the order of `keydown`, `compositionend` and `input` that real browsers and input methods produce, so treat inline editing with an input method as unverified until a pass is recorded.
164
+
165
+ What exists in the code (written for input methods, not verified with one):
166
+
167
+ - **Canvas** (`src/canvas.js`): the inline text input marks the field as composing between `compositionstart` and `compositionend` (no draft and no commit are made meanwhile, and the input is repositioned), ignores `keydown` while `isComposing` (so Enter and Escape that act on a composition do not commit or cancel the edit), and queues a draft when the composition ends.
168
+ - **Rich-text input** (`src/rich-text-input.js`): tracks composition, keeps the pre-composition value so one history step covers the composed text, and ignores `isComposing` key events.
169
+ - **Data grid** (`src/data-grid.js`): an Enter or Tab that confirms a composition is not treated as a command (`isComposing`).
170
+ - **Outline view** (`src/outline-view.js`) and the **design controls** (`src/design-controls.js`): Enter during a composition does not commit (`isComposing`).
171
+
172
+ Not tested at all, even with synthetic events: the data grid, the outline view and the design controls. Failures with a real input method are reported as defects with the input method named.
173
+
161
174
  ## List numbering (RR-33)
162
175
 
163
176
  `@openpresentation/opf-editor/numbering` is the headless model and `/numbering-panel` the DOM control for the `numbering` field of `items` and `bullets` payloads (a style name, a `{ style, start, suffix }` object, or an array per list level). `setNumbering(editor, path, value)` numbers the list a path points at or into (`undefined` turns numbering off and drops the entry `start` values), `setEntryStart(editor, itemPath, start)` restarts the count at an entry, `numberingValue(levels)` writes the shortest form, `numberingState(presentation, path)` reads the settings and the markers the list draws, and `findNumberableLists(presentation, slideIndex)` lists a slide's lists. Each write is one validated, undoable session edit. `createNumberingPanel(container, { editor, getTarget, getSlideIndex, onStatus })` mounts the control: number this list, style, start and suffix, different numbering per level, the markers it will draw, and a restart at the selected entry. The playground opens it from **List numbering**. Needs a core release that ships `numbering`; see core's [numbered lists](https://github.com/OpenPresentation/opf/blob/main/docs/numbered-lists.md).
@@ -631,6 +644,17 @@ const { files, findings } = await convert(editor.presentation, {
631
644
  ```
632
645
 
633
646
  - **Same drawing as the preview.** The slides are drawn by `toSvg` with the same fonts handle as the preview (its `textMeasurement`), so a PNG or SVG is the preview, and the PDF is converted from those SVGs rather than laid out again.
647
+ - **Subset faces and outlined text (opf-editor#141).** A browser fonts handle embeds whole faces in an SVG (hundreds of KB to MBs per slide) unless it was created with `subsetWasm`, and shapes outlined text with fontkit unless it has `shapeWasm`. Serve harfbuzzjs's two WASM files like the font files (the copies of the `harfbuzzjs` package the renderer depends on: `harfbuzzjs/dist/harfbuzz-subset.wasm`, 651 KB, and `harfbuzzjs/dist/harfbuzz.wasm`, 434 KB, both MIT) and create the handle you pass as `fonts` with them:
648
+
649
+ ```js
650
+ const fonts = await loadFonts({
651
+ faces, scriptBaseUrl, lazyFontsBaseUrl,
652
+ subsetWasm: "/vendor/harfbuzz-subset.wasm", // URL, bytes or a WebAssembly.Module; SVG downloads embed each face cut to the slide's glyphs
653
+ shapeWasm: "/vendor/harfbuzz.wasm", // outlined text (renderOptions.text: "paths") is shaped by HarfBuzz, as browsers shape text
654
+ });
655
+ ```
656
+
657
+ `convert` reads `fonts.subsets` and `fonts.outlines` from that handle, so nothing else changes: the same call writes SVG files with the faces cut to what each slide draws (the licence rule above still decides which faces may be embedded, and the renderer cuts only faces whose licence allows a modified version; the others stay whole) and, with `renderOptions: { text: "paths" }`, SVG and PNG files whose text is glyph outlines (no font needed). A PDF is unaffected: it embeds the registry's faces and cuts its own subsets, and it always draws selectable text. `renderOptions.subsetFonts: false` embeds whole faces. The canvas draws with `text: "system"`, which embeds nothing, so it never reads either. The playground does this: `npm run build:playground` copies both files next to the page.
634
658
  - **Fonts.** The fonts handle loads the faces the deck needs before anything is drawn (a failure rejects with `fonts-unavailable`). Only faces the handle's registry holds are embedded (bundled or hash-pinned, never a system font), only where a slide draws them, as `@font-face` data in each SVG and as subsets in the PDF. A face whose own license text is not OFL, Apache, MIT or UFL is left out and reported (`fonts/export-font-license`).
635
659
  - **PDF** is the renderer's vector PDF (selectable text, vector shapes, embedded subsets, tagged structure), `raster: true` is the image-only form. **PNG** is drawn on a canvas from the same SVG (within anti-aliasing of the renderer's resvg PNG) and is limited to 40 megapixels. **SVG** files are standalone (XML header, fonts embedded, no external references).
636
660
  - **Files.** A PDF is one file (`pages`, and the one-based `slides` it shows). PNG and SVG give one file per slide, in slide order, each with its one-based `slide`, its `id` and its `width` and `height` in pixels, as core's `convert` does; with `zip: true` they are one archive (`createZip`, no dependency; `entries` names the files in it), even for one slide. `zip` with a PDF, or `raster` with PNG or SVG, rejects with `invalid-option`.
package/dist/canvas.d.ts CHANGED
@@ -6,8 +6,10 @@ import type { Catalog } from "@openpresentation/opf";
6
6
  /**
7
7
  * The fonts the canvas, the export and `whenFontsReady` take: the renderer's handle from `loadFonts()` (`@openpresentation/opf-render/fonts-browser`).
8
8
  * Only `textMeasurement` is required to measure; `pending` and `ensure` make the canvas load script and vendored faces before it draws.
9
+ * `subsets` and `outlines` are there when the handle was created with `loadFonts({ subsetWasm, shapeWasm })` (opf-editor#141): `convert` then
10
+ * writes SVG downloads with each face cut to the slide's glyphs and can draw `renderOptions.text: "paths"` with HarfBuzz-shaped outlines.
9
11
  */
10
- export type EditorFonts = Pick<BrowserFontsHandle, "textMeasurement"> & Partial<Pick<BrowserFontsHandle, "embeddedFonts" | "pending" | "ensure" | "registry">>;
12
+ export type EditorFonts = Pick<BrowserFontsHandle, "textMeasurement"> & Partial<Pick<BrowserFontsHandle, "embeddedFonts" | "pending" | "ensure" | "registry" | "subsets" | "outlines">>;
11
13
  export interface FontsReadyHandlers {
12
14
  /** Return false once the document was superseded; the late result is then dropped. */
13
15
  isCurrent?: () => boolean;
package/dist/canvas.js CHANGED
@@ -247,6 +247,15 @@ export function createCanvasEditor(container, options = {}) {
247
247
  if (fonts?.pending(presentation ?? editor.presentation, resolveOptions()).length) show(presentation);
248
248
  else render(presentation);
249
249
  }
250
+ // The text an editing target draws: its outermost <text> elements, whitespace collapsed (the renderer's <title> and <desc> are not drawn).
251
+ function targetText(node) {
252
+ return [...node.querySelectorAll("text")]
253
+ .filter((text) => !text.parentNode.closest("text"))
254
+ .map((text) => text.textContent)
255
+ .join(" ")
256
+ .replace(/\s+/g, " ")
257
+ .trim();
258
+ }
250
259
  function render(presentation = editor.presentation) {
251
260
  if (disposed) return;
252
261
  requireFonts(presentation);
@@ -300,9 +309,12 @@ export function createCanvasEditor(container, options = {}) {
300
309
  }
301
310
  node.setAttribute("role", "button");
302
311
  node.setAttribute("tabindex", "0");
312
+ // WCAG 2.5.3 (label in name): the name carries every word the target draws, in drawing order, after "Edit <field>:", so the visible
313
+ // text is always part of the name. Content with no drawn text (a picture) falls back to the source text or "content properties".
314
+ const drawn = targetText(node);
303
315
  node.setAttribute(
304
316
  "aria-label",
305
- `Edit ${path.split(".").at(-1)}: ${["string", "number"].includes(typeof value) ? String(value).slice(0, 80) : "content properties"}`,
317
+ `Edit ${path.split(".").at(-1)}: ${drawn || (["string", "number"].includes(typeof value) ? String(value) : "content properties")}`,
306
318
  );
307
319
  if (node.matches("g")) {
308
320
  const bounds = allocatedSelectionBox(node, item);
package/dist/data-grid.js CHANGED
@@ -446,8 +446,9 @@ export function createDataGrid(container, options = {}) {
446
446
  updateFormatRow();
447
447
  const nameRows = count > 1 ? `${count} rows` : "row";
448
448
  buttons.get("delete-rows").textContent = count > 1 ? `Delete ${count} rows` : "Delete rows";
449
- buttons.get("insert-row-above").setAttribute("aria-label", `Insert ${nameRows === "row" ? "row" : nameRows} above`);
450
- buttons.get("insert-row-below").setAttribute("aria-label", `Insert ${nameRows === "row" ? "row" : nameRows} below`);
449
+ // WCAG 2.5.3 (label in name): the button says what it does in its text, so there is no aria-label to differ from it.
450
+ buttons.get("insert-row-above").textContent = `Insert ${nameRows} above`;
451
+ buttons.get("insert-row-below").textContent = `Insert ${nameRows} below`;
451
452
  buttons.get("delete-columns").textContent = columns > 1 ? `Delete ${columns} columns` : "Delete columns";
452
453
  }
453
454
 
package/dist/export.d.ts CHANGED
@@ -53,7 +53,13 @@ export interface ConvertOptions {
53
53
  renderOptions?: ToSvgOptions;
54
54
  /** Host catalogs (merged with `renderOptions.catalogs`). The deck is embedded first (core `embed`), like a saved file. */
55
55
  catalogs?: readonly Catalog[];
56
- /** The renderer's fonts handle (`loadFonts()`): it measures the text, loads the faces the deck needs before anything is drawn, and its registry's faces are what gets embedded. */
56
+ /**
57
+ * The renderer's fonts handle (`loadFonts()`): it measures the text, loads the faces the deck needs before anything is drawn, and its registry's faces are what gets embedded.
58
+ * Create a browser handle with `loadFonts({ subsetWasm, shapeWasm })` (the URLs, bytes or compiled modules of harfbuzzjs's `harfbuzz-subset.wasm` and `harfbuzz.wasm`, which the host
59
+ * serves) and `convert` uses them: an SVG embeds each face cut to the glyphs its slide draws (a few KB to tens of KB instead of whole faces of hundreds of KB to MBs), and
60
+ * `renderOptions.text: "paths"` draws outlines shaped by HarfBuzz instead of fontkit. A Node handle (`/fonts-node`) always has both. Only SVG files are subset (a PNG is a picture, and a PDF cuts its own
61
+ * subsets); a PNG takes the outlines, and a PDF is always drawn as selectable text (`text: "paths"` is ignored for it).
62
+ */
57
63
  fonts?: EditorFonts;
58
64
  /** Faces to embed instead of the registry's. */
59
65
  embeddedFonts?: Array<Record<string, unknown>>;
package/dist/export.js CHANGED
@@ -3,7 +3,9 @@
3
3
  // one-based `slide`). The slides are drawn by the same renderer the preview uses (`toSvg`, with the fonts handle's text measurement),
4
4
  // so a file is the preview, not a second layout. Fonts are the renderer's fonts handle's: the faces the document needs load through
5
5
  // the handle first, and only faces its registry holds (bundled or hash-pinned, permissively licensed) are embedded, as @font-face
6
- // data in each SVG and as subsets in the PDF. No system font is read and nothing is fetched at export time.
6
+ // data in each SVG and as subsets in the PDF. No system font is read and nothing is fetched at export time. A browser handle made with
7
+ // `loadFonts({ subsetWasm, shapeWasm })` (opf-editor#141) also cuts each SVG's faces to the slide's glyphs and draws
8
+ // `renderOptions.text: "paths"` with HarfBuzz-shaped outlines.
7
9
  //
8
10
  // const { files, findings } = await convert(editor.presentation, { format: "pdf", fonts, renderOptions, signal, onProgress });
9
11
  // // files: one PDF; one PNG or SVG per slide; or, with `zip: true`, one archive of them.
@@ -241,11 +243,25 @@ export async function convert(input, options = {}) {
241
243
  });
242
244
  progress("render", 0, numbers.length, "Drawing slides…");
243
245
  await yieldToHost();
246
+ // RR-65, RR-64: the handle's subset engine (a browser handle created with `subsetWasm`) cuts each face a standalone SVG embeds to the
247
+ // slide's glyphs, and its outline engine (`shapeWasm` shapes them with HarfBuzz) draws `renderOptions.text: "paths"`. Both are carried
248
+ // from the handle by hand because the face list above is the editor's licensed one, not the handle's. A PDF reads whole faces and
249
+ // the SVG's own @font-face data (its converter cuts its own subsets), and selectable text is its point, so it gets neither: a
250
+ // `text: "paths"` render option draws a PDF's text as text. A PNG is a picture of the SVG: it takes the outlines, not the subsets.
251
+ const toPdf = format.extension === "pdf";
252
+ const handle = options.fonts;
253
+ const drawFonts = {
254
+ textMeasurement: handle?.textMeasurement,
255
+ embeddedFonts,
256
+ ...(format.extension === "svg" && handle?.subsets ? { subsets: handle.subsets } : {}),
257
+ ...(!toPdf && handle?.outlines ? { outlines: handle.outlines } : {}),
258
+ };
244
259
  const svgs = toSvg(deck, numbers, {
245
260
  ...renderOptions,
246
- fonts: { textMeasurement: options.fonts?.textMeasurement, embeddedFonts },
261
+ ...(toPdf && renderOptions.text === "paths" ? { text: "fonts" } : {}),
262
+ fonts: drawFonts,
247
263
  // The PDF reports problems by element path; the SVG and PNG are the plain drawing.
248
- trace: format.extension === "pdf",
264
+ trace: toPdf,
249
265
  onDiagnostic: (diagnostic) => {
250
266
  record("render", diagnostic);
251
267
  renderOptions.onDiagnostic?.(diagnostic);
@@ -147,8 +147,9 @@ export function createFindPanel(container, options) {
147
147
  matches.slice(0, MAX_LISTED).forEach((match, index) => {
148
148
  const mark = h(doc, "mark", { text: match.context.match });
149
149
  const text = h(doc, "span", { class: "opf-find-text" }, doc.createTextNode(match.context.before), mark, doc.createTextNode(match.context.after));
150
- const button = h(doc, "button", { type: "button", class: "opf-find-result", "aria-current": index === current ? "true" : "false", "data-index": String(index), "aria-label": `${whereLabel(match)}: ${match.context.before}${match.context.match}${match.context.after}` },
151
- h(doc, "span", { class: "opf-find-where", text: whereLabel(match) }), text);
150
+ const button = h(doc, "button", { type: "button", class: "opf-find-result", "aria-current": index === current ? "true" : "false", "data-index": String(index) },
151
+ // WCAG 2.5.3 (label in name): no aria-label. The name is the visible text, the place and then the matching line, kept apart by a space.
152
+ h(doc, "span", { class: "opf-find-where", text: whereLabel(match) }), " ", text);
152
153
  list.append(h(doc, "li", {}, button));
153
154
  });
154
155
  if (matches.length > MAX_LISTED) list.append(h(doc, "li", { class: "opf-find-more", text: `Showing the first ${MAX_LISTED} of ${matches.length}${result.truncated ? "+" : ""} matches.` }));
@@ -159,7 +159,8 @@ export function createReviewPanel(container, options) {
159
159
  const list = h(doc, "div", { class: "opf-review-list", role: "group", "aria-label": "Review findings" });
160
160
  const empty = h(doc, "p", { class: "opf-review-empty" });
161
161
  const ignoredBox = h(doc, "details", { class: "opf-review-ignored", hidden: true });
162
- root.append(head, ...(hookStatus ? [hookStatus] : []), filters, list, empty, ignoredBox);
162
+ const gotoHint = h(doc, "span", { id: `${id}-goto-hint`, hidden: true, text: "Go to it." });
163
+ root.append(head, ...(hookStatus ? [hookStatus] : []), filters, list, empty, ignoredBox, gotoHint);
163
164
  container.append(root);
164
165
 
165
166
  const say = (message, error = false) => {
@@ -310,11 +311,13 @@ export function createReviewPanel(container, options) {
310
311
  type: "button",
311
312
  class: "opf-review-goto",
312
313
  "data-finding-id": finding.id,
313
- "aria-label": `${SEVERITY_LABELS[finding.severity]}, ${where}: ${finding.message} Go to it.`,
314
+ // WCAG 2.5.3 (label in name): no aria-label. The name is the visible text (severity, message, where and rule), kept apart by spaces;
315
+ // what the button does is its description.
316
+ "aria-describedby": `${id}-goto-hint`,
314
317
  onclick: () => go(finding),
315
318
  },
316
- h(doc, "span", { class: "opf-review-sev", text: SEVERITY_LABELS[finding.severity] }),
317
- h(doc, "span", { class: "opf-review-msg", text: finding.message }),
319
+ h(doc, "span", { class: "opf-review-sev", text: SEVERITY_LABELS[finding.severity] }), " ",
320
+ h(doc, "span", { class: "opf-review-msg", text: finding.message }), " ",
318
321
  h(doc, "span", { class: "opf-review-meta", text: `${where} · ${finding.ruleId}${finding.source && finding.source !== CORE_SOURCE ? ` · ${finding.source}` : ""}` }));
319
322
  const li = h(doc, "li", { class: "opf-review-item", "data-finding-id": finding.id, "data-rule": finding.ruleId, "data-source": finding.source ?? CORE_SOURCE, "data-severity": finding.severity, "aria-current": String(finding.id === currentId) }, go_,
320
323
  finding.help ? h(doc, "p", { class: "opf-review-help", text: finding.help }) : undefined);
@@ -359,7 +362,7 @@ export function createReviewPanel(container, options) {
359
362
  ignoredBox.replaceChildren(
360
363
  h(doc, "summary", { text: `Hidden checks (${ignored.size})` }),
361
364
  h(doc, "ul", { class: "opf-review-ignored-list", style: "list-style:none;padding:0" }, ...[...ignored].map((ruleId) => h(doc, "li", {}, h(doc, "span", { text: ruleId }),
362
- h(doc, "button", { type: "button", class: "opf-review-fix quiet", "data-restore": ruleId, text: "Show again", "aria-label": `Show ${ruleId} findings again`, onclick: () => setIgnored(ruleId, false) })))),
365
+ h(doc, "button", { type: "button", class: "opf-review-fix quiet", "data-restore": ruleId, text: "Show again", "aria-label": `Show again, ${ruleId} findings`, onclick: () => setIgnored(ruleId, false) })))),
363
366
  );
364
367
  }
365
368
 
@@ -139,10 +139,12 @@ export function createSlideManager(container, options) {
139
139
  const selected = selection.has(index);
140
140
  const title = slideTitle(slide) || "Untitled slide";
141
141
  const hidden = slide.hidden === true;
142
+ // WCAG 2.5.3 (label in name): the card has no aria-label. Its name is its own content (the number, the title and the "Hidden" badge;
143
+ // the thumbnail is aria-hidden), so the name always contains the visible text. The position and the selection are the description.
144
+ const position = `Slide ${index + 1} of ${slideCount}${selected && selection.size > 1 ? ", selected" : ""}`;
142
145
  const button = el("button", {
143
- type: "button", class: "slide-card", draggable: "true", "data-index": index, "aria-current": String(index === current()),
144
- "aria-label": `Slide ${index + 1} of ${slideCount}: ${title}${hidden ? ", hidden" : ""}${selected && selection.size > 1 ? ", selected" : ""}`,
145
- "aria-describedby": `${id}-hint`, "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown Delete", tabindex: index === focusIndex ? "0" : "-1",
146
+ type: "button", class: "slide-card", draggable: "true", "data-index": index, "data-menu-label": `Slide ${index + 1} of ${slideCount}: ${title}`, "aria-current": String(index === current()),
147
+ "aria-describedby": `${id}-${index}-position ${id}-hint`, "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown Delete", tabindex: index === focusIndex ? "0" : "-1",
146
148
  });
147
149
  button.dataset.selected = String(selected);
148
150
  button.dataset.hidden = String(hidden);
@@ -156,8 +158,9 @@ export function createSlideManager(container, options) {
156
158
  thumbnail.querySelectorAll("[tabindex]").forEach((node) => node.removeAttribute("tabindex"));
157
159
  const label = el("span", { class: "thumbnail-title", text: slideTitle(slide) || "Untitled slide" });
158
160
  const content = el("span", { class: "thumbnail-content", draggable: "true" }, thumbnail, label);
159
- if (hidden) content.append(el("span", { class: "slide-badge", text: "Hidden" }));
160
- button.append(number, content);
161
+ if (hidden) content.append(" ", el("span", { class: "slide-badge", text: "Hidden" }));
162
+ // The explicit spaces keep the number, title and badge apart in the computed name; hidden elements still describe the button.
163
+ button.append(number, " ", content, el("span", { id: `${id}-${index}-position`, hidden: true, text: position }));
161
164
  button.addEventListener("click", (event) => choose(index, event));
162
165
  button.addEventListener("keydown", (event) => keydown(event, index));
163
166
  button.addEventListener("focus", () => { focusIndex = index; });
@@ -172,9 +175,11 @@ export function createSlideManager(container, options) {
172
175
  const name = sectionLabel(section.name);
173
176
  const header = el("div", { class: "slide-section", "data-section": section.index, "data-unnamed": String(section.unnamed) });
174
177
  const toggle = el("button", {
175
- type: "button", class: "section-toggle", "aria-expanded": String(!isCollapsed), "aria-label": `${name}, ${plural(section.count, "slide")}`,
178
+ type: "button", class: "section-toggle", "aria-expanded": String(!isCollapsed), "aria-describedby": `${id}-section-${section.index}-count`,
176
179
  onclick: () => { if (isCollapsed) collapsed.delete(key); else collapsed.add(key); render(); },
177
- }, el("span", { class: "section-caret", "aria-hidden": "true", text: isCollapsed ? "▸" : "▾" }), el("span", { class: "section-name", text: name }), el("span", { class: "section-count", "aria-hidden": "true", text: String(section.count) }));
180
+ }, el("span", { class: "section-caret", "aria-hidden": "true", text: isCollapsed ? "▸" : "▾" }), " ", el("span", { class: "section-name", text: name }), " ", el("span", { class: "section-count", "aria-hidden": "true", text: String(section.count) }),
181
+ // WCAG 2.5.3: no aria-label. The name is the visible section name; the slide count is the description.
182
+ el("span", { id: `${id}-section-${section.index}-count`, hidden: true, text: plural(section.count, "slide") }));
178
183
  const more = el("button", { type: "button", class: "section-more", "aria-label": `Section actions: ${name}`, "aria-haspopup": "menu", title: "Section actions" });
179
184
  more.append(icon("more"));
180
185
  more.addEventListener("click", () => { const box = more.getBoundingClientRect(); openMenu(box.left, box.bottom, more, sectionMenu(section)); });
@@ -424,7 +429,7 @@ export function createSlideManager(container, options) {
424
429
  }
425
430
  function openMenu(x, y, opener, items = slideMenu()) {
426
431
  closeMenu(false);
427
- const root = el("div", { class: "opf-menu", role: "menu", "aria-label": opener.getAttribute?.("aria-label") ?? "Slide actions", tabindex: "-1" });
432
+ const root = el("div", { class: "opf-menu", role: "menu", "aria-label": opener.getAttribute?.("aria-label") ?? opener.dataset?.menuLabel ?? "Slide actions", tabindex: "-1" });
428
433
  const buttons = [];
429
434
  for (const item of items) {
430
435
  if (item.separator) { root.append(el("div", { class: "opf-menu-separator", role: "separator" })); continue; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -257,12 +257,12 @@
257
257
  "@codemirror/search": "6.7.2",
258
258
  "@codemirror/state": "6.7.6",
259
259
  "@codemirror/view": "6.43.13",
260
- "@lezer/highlight": "1.2.4",
261
- "@openpresentation/opf": "^0.18.1",
260
+ "@lezer/highlight": "1.2.5",
261
+ "@openpresentation/opf": "^0.18.4",
262
262
  "jsonc-parser": "3.3.1"
263
263
  },
264
264
  "peerDependencies": {
265
- "@openpresentation/opf-render": "^0.18.0"
265
+ "@openpresentation/opf-render": "^0.18.2"
266
266
  },
267
267
  "peerDependenciesMeta": {
268
268
  "@openpresentation/opf-render": {
@@ -274,8 +274,8 @@
274
274
  "@expo-google-fonts/caladea": "0.4.2",
275
275
  "@expo-google-fonts/cousine": "0.4.3",
276
276
  "@expo-google-fonts/gelasio": "0.4.1",
277
- "@expo-google-fonts/noto-color-emoji": "0.4.6",
278
- "@expo-google-fonts/noto-emoji": "0.4.7",
277
+ "@expo-google-fonts/noto-color-emoji": "0.4.7",
278
+ "@expo-google-fonts/noto-emoji": "0.4.8",
279
279
  "@expo-google-fonts/noto-naskh-arabic": "0.4.5",
280
280
  "@expo-google-fonts/noto-nastaliq-urdu": "0.4.2",
281
281
  "@expo-google-fonts/noto-sans": "0.4.2",
@@ -288,23 +288,23 @@
288
288
  "@expo-google-fonts/noto-sans-gujarati": "0.4.2",
289
289
  "@expo-google-fonts/noto-sans-gurmukhi": "0.4.1",
290
290
  "@expo-google-fonts/noto-sans-hebrew": "0.4.1",
291
- "@expo-google-fonts/noto-sans-jp": "0.4.3",
291
+ "@expo-google-fonts/noto-sans-jp": "0.4.4",
292
292
  "@expo-google-fonts/noto-sans-kannada": "0.4.3",
293
293
  "@expo-google-fonts/noto-sans-khmer": "0.4.3",
294
- "@expo-google-fonts/noto-sans-kr": "0.4.3",
294
+ "@expo-google-fonts/noto-sans-kr": "0.4.4",
295
295
  "@expo-google-fonts/noto-sans-lao": "0.4.2",
296
296
  "@expo-google-fonts/noto-sans-malayalam": "0.4.2",
297
297
  "@expo-google-fonts/noto-sans-math": "0.4.2",
298
298
  "@expo-google-fonts/noto-sans-mongolian": "0.4.1",
299
299
  "@expo-google-fonts/noto-sans-myanmar": "0.4.2",
300
300
  "@expo-google-fonts/noto-sans-oriya": "0.4.2",
301
- "@expo-google-fonts/noto-sans-sc": "0.4.3",
301
+ "@expo-google-fonts/noto-sans-sc": "0.4.4",
302
302
  "@expo-google-fonts/noto-sans-sinhala": "0.4.2",
303
303
  "@expo-google-fonts/noto-sans-symbols": "0.4.1",
304
304
  "@expo-google-fonts/noto-sans-symbols-2": "0.4.1",
305
305
  "@expo-google-fonts/noto-sans-syriac": "0.4.2",
306
306
  "@expo-google-fonts/noto-sans-tamil": "0.4.3",
307
- "@expo-google-fonts/noto-sans-tc": "0.4.3",
307
+ "@expo-google-fonts/noto-sans-tc": "0.4.4",
308
308
  "@expo-google-fonts/noto-sans-telugu": "0.4.2",
309
309
  "@expo-google-fonts/noto-sans-thaana": "0.4.2",
310
310
  "@expo-google-fonts/noto-sans-thai": "0.4.2",
@@ -314,14 +314,14 @@
314
314
  "@expo-google-fonts/roboto-mono": "0.4.2",
315
315
  "@expo-google-fonts/stix-two-math": "0.4.0",
316
316
  "@expo-google-fonts/tinos": "0.4.2",
317
- "@openpresentation/opf-pptx": "^0.18.0",
318
- "@openpresentation/opf-render": "^0.18.0",
317
+ "@openpresentation/opf-pptx": "^0.18.2",
318
+ "@openpresentation/opf-render": "^0.18.2",
319
319
  "@resvg/resvg-js": "2.6.2",
320
- "axe-core": "4.13.0",
320
+ "axe-core": "4.14.0",
321
321
  "esbuild": "0.28.2",
322
322
  "jszip": "3.10.2",
323
323
  "pdf-lib": "1.17.1",
324
- "pdfjs-dist": "6.3.289",
324
+ "pdfjs-dist": "6.4.299",
325
325
  "playwright": "1.63.0"
326
326
  }
327
327
  }