visio-angular-viewer 0.1.1 → 0.2.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/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, parseVsdx, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
2
- import { Component, ElementRef, EventEmitter, Input, Output, inject } from "@angular/core";
2
+ import { registerControls } from "ooxml-ui/controls";
3
+ import { getIcon, registerIcon } from "ooxml-ui/icons";
4
+ import { Component, ElementRef, EventEmitter, Input, Output, inject, signal } from "@angular/core";
3
5
  export * from "visio-core";
4
- //#region .package-build/output/src/document-history.js
6
+ //#region src/document-history.ts
5
7
  var EMPTY_EDIT_STATE = Object.freeze({
6
8
  sourceAvailable: false,
7
9
  busy: false,
@@ -18,6 +20,7 @@ var EDIT_HISTORY_LIMITS = Object.freeze({
18
20
  });
19
21
  /** Private controller-owned bytes; transient worker copies and models are separate from this budget. */
20
22
  var DocumentHistory = class {
23
+ original;
21
24
  #entries;
22
25
  #index = 0;
23
26
  #truncated = false;
@@ -81,7 +84,7 @@ var DocumentHistory = class {
81
84
  }
82
85
  };
83
86
  //#endregion
84
- //#region .package-build/output/src/edit-commands.js
87
+ //#region src/edit-commands.ts
85
88
  /** Bound cloning and strip arbitrary host properties. Semantic validation belongs to core. */
86
89
  function snapshotEdits(edits) {
87
90
  if (!Array.isArray(edits) || edits.length > 1e3) throw new Error("At most 1000 edits are accepted per operation.");
@@ -143,7 +146,7 @@ function snapshotEdits(edits) {
143
146
  });
144
147
  }
145
148
  //#endregion
146
- //#region .package-build/output/src/scene-details.js
149
+ //#region src/scene-details.ts
147
150
  /** Runtime model checks prevent implicit DOM stringification of host-provided objects. */
148
151
  function assertShapeDetails(shape, budget) {
149
152
  const data = shape.shapeData ?? [], links = shape.hyperlinks ?? [];
@@ -210,7 +213,7 @@ function assertShapeDetails(shape, budget) {
210
213
  }
211
214
  }
212
215
  //#endregion
213
- //#region .package-build/output/src/foreign-vector-budget.js
216
+ //#region src/foreign-vector-budget.ts
214
217
  /** UI allocation limits, charged for every instance, including repeated shared resources. */
215
218
  var FOREIGN_VECTOR_SCENE_LIMITS = Object.freeze({
216
219
  maxNodes: 1e5,
@@ -247,11 +250,9 @@ function inspectForeignVectorResource(source) {
247
250
  }
248
251
  /** Invocation-local budget. Never trusts frozen host objects or caches across calls. */
249
252
  var ForeignVectorBudget = class {
250
- constructor() {
251
- this.nodes = 0;
252
- this.commands = 0;
253
- this.operands = 0;
254
- }
253
+ nodes = 0;
254
+ commands = 0;
255
+ operands = 0;
255
256
  take(source) {
256
257
  const resource = inspectForeignVectorResource(source);
257
258
  this.nodes += resource.nodes;
@@ -527,13 +528,13 @@ function assertViewableDocument(model) {
527
528
  }
528
529
  }
529
530
  //#endregion
530
- //#region .package-build/output/src/worker-editor.js
531
+ //#region src/worker-editor.ts
531
532
  function createWorkerEditor(factory, timeoutMs = 2e4) {
532
533
  const create = factory ?? (() => {
533
534
  if (typeof Worker === "undefined") throw new Error("Editing requires browser worker support.");
534
535
  return new Worker(new URL(
535
536
  /* @vite-ignore */
536
- new URL("assets/edit-worker-DnTSJ65K.js", import.meta.url).href,
537
+ new URL("assets/edit-worker-BcXrV_St.js", import.meta.url).href,
537
538
  "" + import.meta.url
538
539
  ), { type: "module" });
539
540
  });
@@ -602,7 +603,7 @@ function createWorkerEditor(factory, timeoutMs = 2e4) {
602
603
  return edit;
603
604
  }
604
605
  //#endregion
605
- //#region .package-build/output/src/viewer-layers.js
606
+ //#region src/viewer-layers.ts
606
607
  /** UI limits. Visibility semantics and shape traversal remain in ooxml-core. */
607
608
  var VIEWER_LAYER_LIMITS = Object.freeze({
608
609
  overrides: VISIO_VISIBILITY_LIMITS.maxLayers,
@@ -659,7 +660,7 @@ function hasVisibleShapeContent(shape, visible, cache = /* @__PURE__ */ new Weak
659
660
  return renderable;
660
661
  }
661
662
  //#endregion
662
- //#region .package-build/output/src/document-text-search.js
663
+ //#region src/document-text-search.ts
663
664
  /** UTF-16 character bounds, applied before copying or case conversion. */
664
665
  var TEXT_SEARCH_LIMITS = Object.freeze({
665
666
  queryCharacters: 256,
@@ -764,8 +765,11 @@ function textSearchStatus(search) {
764
765
  return (search.activeIndex < 0 ? `${count}${search.resultsTruncated ? "+" : ""} matching shapes` : `${search.activeIndex + 1} of ${count}${search.resultsTruncated ? "+" : ""} matching shapes`) + (search.indexTruncated ? " · Partial text index" : "");
765
766
  }
766
767
  //#endregion
767
- //#region .package-build/output/src/controller.js
768
+ //#region src/controller.ts
768
769
  var ViewerController = class {
770
+ parser;
771
+ listenerError;
772
+ editor;
769
773
  #state = Object.freeze({
770
774
  document: null,
771
775
  edit: EMPTY_EDIT_STATE,
@@ -1183,7 +1187,7 @@ var ViewerController = class {
1183
1187
  }
1184
1188
  };
1185
1189
  //#endregion
1186
- //#region .package-build/output/src/contract.js
1190
+ //#region src/contract.ts
1187
1191
  var propertyKeys = [
1188
1192
  "document",
1189
1193
  "pageIndex",
@@ -1199,13 +1203,13 @@ var eventKeys = [
1199
1203
  "shape-select"
1200
1204
  ];
1201
1205
  //#endregion
1202
- //#region .package-build/output/src/worker-parser.js
1206
+ //#region src/worker-parser.ts
1203
1207
  /** One disposable worker per document prevents old parses and long CPU tasks from occupying the UI. */
1204
1208
  function createWorkerParser(factory, timeoutMs = 15e3) {
1205
1209
  if (!factory && typeof Worker === "undefined") return parseVsdx;
1206
1210
  const create = factory ?? (() => new Worker(new URL(
1207
1211
  /* @vite-ignore */
1208
- new URL("assets/parse-worker-CvtlZkVJ.js", import.meta.url).href,
1212
+ new URL("assets/parse-worker-B8EBQQpD.js", import.meta.url).href,
1209
1213
  "" + import.meta.url
1210
1214
  ), { type: "module" }));
1211
1215
  let cancel;
@@ -1252,7 +1256,7 @@ function createWorkerParser(factory, timeoutMs = 15e3) {
1252
1256
  return parse;
1253
1257
  }
1254
1258
  //#endregion
1255
- //#region .package-build/output/src/render-fill.js
1259
+ //#region src/render-fill.ts
1256
1260
  var gradientId = 0;
1257
1261
  function fillPaint(style, defs) {
1258
1262
  if (!style.fillGradient) return safeColor(style.fill, "#fff");
@@ -1274,7 +1278,7 @@ function fillPaint(style, defs) {
1274
1278
  return `url(#${gradient.id})`;
1275
1279
  }
1276
1280
  //#endregion
1277
- //#region .package-build/output/src/render-image.js
1281
+ //#region src/render-image.ts
1278
1282
  var clipId$1 = 0;
1279
1283
  /** URLs are created only from bounded embedded raster bytes, never from document strings. */
1280
1284
  function renderImage(shape, resources) {
@@ -1307,7 +1311,7 @@ function renderImage(shape, resources) {
1307
1311
  return group;
1308
1312
  }
1309
1313
  //#endregion
1310
- //#region .package-build/output/src/render-foreign-vector.js
1314
+ //#region src/render-foreign-vector.ts
1311
1315
  var NS$1 = "http://www.w3.org/2000/svg";
1312
1316
  var nextResource = 0;
1313
1317
  var pathData = (commands) => commands.map(({ command, values }) => `${command}${values.length ? " " + values.join(" ") : ""}`).join(" ");
@@ -1373,7 +1377,7 @@ function renderForeignVector(source, owner = document, budget = new ForeignVecto
1373
1377
  return root;
1374
1378
  }
1375
1379
  //#endregion
1376
- //#region .package-build/output/src/render-foreign-vector-shape.js
1380
+ //#region src/render-foreign-vector-shape.ts
1377
1381
  var clipId = 0;
1378
1382
  /** Core supplies shape-local placement; converter coordinates have a downward Y axis. */
1379
1383
  function renderForeignVectorShape(shape, budget) {
@@ -1397,10 +1401,11 @@ function renderForeignVectorShape(shape, budget) {
1397
1401
  return group;
1398
1402
  }
1399
1403
  //#endregion
1400
- //#region .package-build/output/src/render-resources.js
1404
+ //#region src/render-resources.ts
1401
1405
  var MAX_EMBEDDED_BYTES = 16777216;
1402
1406
  /** One reference per shared core image resource, with explicit per-render disposal. */
1403
1407
  var RenderResources = class {
1408
+ definitions;
1404
1409
  #images = /* @__PURE__ */ new Map();
1405
1410
  #disposed = false;
1406
1411
  #embeddedBytes = 0;
@@ -1454,7 +1459,7 @@ function base64(bytes) {
1454
1459
  return chunks.join("");
1455
1460
  }
1456
1461
  //#endregion
1457
- //#region .package-build/output/src/arrowheads.js
1462
+ //#region src/arrowheads.ts
1458
1463
  var markerId = 0;
1459
1464
  /** Common Visio line/triangle and concave arrow styles. Sizes remain a documented visual approximation. */
1460
1465
  function applyArrowheads(path, style, defs, warnings) {
@@ -1498,7 +1503,7 @@ function applyArrowheads(path, style, defs, warnings) {
1498
1503
  }
1499
1504
  }
1500
1505
  //#endregion
1501
- //#region .package-build/output/src/text-layout.js
1506
+ //#region src/text-layout.ts
1502
1507
  var DEFAULT_LIMITS = {
1503
1508
  maxCharacters: 25e4,
1504
1509
  maxMeasurements: 5e4,
@@ -1518,6 +1523,9 @@ var TextLayoutBudgetError = class extends Error {
1518
1523
  };
1519
1524
  /** One bounded measurement cache and work budget per render; shape scopes share both. */
1520
1525
  var TextLayoutBudget = class TextLayoutBudget {
1526
+ limits;
1527
+ cache;
1528
+ parent;
1521
1529
  #characters = 0;
1522
1530
  #measurements = 0;
1523
1531
  #lines = 0;
@@ -1687,7 +1695,7 @@ function wrapText(runs, width, measure = measureBrowserText, firstLineIndent = 0
1687
1695
  return lines;
1688
1696
  }
1689
1697
  //#endregion
1690
- //#region .package-build/output/src/paragraph-layout.js
1698
+ //#region src/paragraph-layout.ts
1691
1699
  function layoutParagraphs(text, measure = measureBrowserText, budget = createTextLayoutBudget()) {
1692
1700
  const scope = budget.forShape();
1693
1701
  const runs = text.runs.length ? text.runs : [{
@@ -1785,7 +1793,7 @@ function sliceRuns(runs, offsets, start, end) {
1785
1793
  return result;
1786
1794
  }
1787
1795
  //#endregion
1788
- //#region .package-build/output/src/render-text.js
1796
+ //#region src/render-text.ts
1789
1797
  /** Browser-font layout is approximate until compared with the licensed Visio reference corpus. */
1790
1798
  function renderText(text, warnings, budget) {
1791
1799
  const group = svgElement("g");
@@ -1863,7 +1871,7 @@ function renderText(text, warnings, budget) {
1863
1871
  return group;
1864
1872
  }
1865
1873
  //#endregion
1866
- //#region .package-build/output/src/render-svg.js
1874
+ //#region src/render-svg.ts
1867
1875
  var NS = "http://www.w3.org/2000/svg";
1868
1876
  function svgElement(name) {
1869
1877
  return document.createElementNS(NS, name);
@@ -1996,7 +2004,7 @@ function drawOwn(shape, group, context) {
1996
2004
  if (shape.kind === "foreign" && !shape.image && !shape.foreignVector) warnings.add("Embedded or foreign objects are not rendered in this build.");
1997
2005
  }
1998
2006
  //#endregion
1999
- //#region .package-build/output/src/shape-inspector.js
2007
+ //#region src/shape-inspector.ts
2000
2008
  function selectedShape(document, selection, pageIndex) {
2001
2009
  if (!document || !selection) return void 0;
2002
2010
  const stack = [...(selection.pageId ? document.pages.find((page) => page.id === selection.pageId) : document.pages[pageIndex])?.shapes ?? []];
@@ -2094,7 +2102,7 @@ function shapeDetails(shape) {
2094
2102
  return fragment;
2095
2103
  }
2096
2104
  //#endregion
2097
- //#region .package-build/output/src/diagnostics.js
2105
+ //#region src/diagnostics.ts
2098
2106
  /** Presentation-only grouping keeps repeated per-shape diagnostics readable without losing counts. */
2099
2107
  function compatibilityNotes(diagnostics, renderWarnings = []) {
2100
2108
  const notes = /* @__PURE__ */ new Map();
@@ -2126,7 +2134,7 @@ function compatibilityText(note) {
2126
2134
  return note.message + (note.count > 1 ? ` (${note.count} occurrences)` : "");
2127
2135
  }
2128
2136
  //#endregion
2129
- //#region .package-build/output/src/viewer-input.js
2137
+ //#region src/viewer-input.ts
2130
2138
  var targetShape = (event) => event.target?.closest?.("[data-shape-id]");
2131
2139
  function selection(target) {
2132
2140
  return target ? {
@@ -2137,7 +2145,7 @@ function selection(target) {
2137
2145
  }
2138
2146
  /** Own every DOM listener and abort them together when the surface is disposed. */
2139
2147
  function wireViewerInputs(controls, controller, fit) {
2140
- const { viewport, commandRoot, pageSelect, searchInput } = controls;
2148
+ const { viewport, commandRoot, zoomSlider, searchInput } = controls;
2141
2149
  const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
2142
2150
  const options = { signal: events.signal };
2143
2151
  searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
@@ -2153,13 +2161,9 @@ function wireViewerInputs(controls, controller, fit) {
2153
2161
  controller.setSearchQuery("");
2154
2162
  }
2155
2163
  }, options);
2156
- pageSelect.addEventListener("change", () => controller.setPage(Number(pageSelect.value)), options);
2164
+ zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
2157
2165
  commandRoot.addEventListener("click", (event) => {
2158
2166
  const action = event.target?.closest?.("button")?.dataset.action;
2159
- if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
2160
- if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
2161
- if (action === "fit") fit();
2162
- if (action === "actual") controller.setZoom(1);
2163
2167
  if (action === "search-next") controller.nextSearchResult();
2164
2168
  if (action === "search-previous") controller.previousSearchResult();
2165
2169
  }, options);
@@ -2198,13 +2202,13 @@ function wireViewerInputs(controls, controller, fit) {
2198
2202
  }
2199
2203
  if (event.key === "0") {
2200
2204
  event.preventDefault();
2201
- fit();
2205
+ fit("page");
2202
2206
  }
2203
2207
  }, options);
2204
2208
  return () => events.abort();
2205
2209
  }
2206
2210
  //#endregion
2207
- //#region .package-build/output/src/viewer-layer-controls.js
2211
+ //#region src/viewer-layer-controls.ts
2208
2212
  var layerControlsTemplate = `<details class="layer-controls" hidden><summary>Layers</summary><p>Display changes only. SVG and print snapshots use saved visibility. Guides, NoShow and unknown hidden reasons remain hidden.</p><div data-layer-list></div><p data-layer-status role="status"></p><button type="button" data-layer-reset="all">Reset all layers</button></details>`;
2209
2213
  /** Static controls and text-only labels. Document strings never become selectors or markup. */
2210
2214
  function renderLayerControls(panel, state) {
@@ -2282,7 +2286,7 @@ function wireLayerControls(panel, controller) {
2282
2286
  return () => events.abort();
2283
2287
  }
2284
2288
  //#endregion
2285
- //#region .package-build/output/src/styles.js
2289
+ //#region src/styles.ts
2286
2290
  /** Inherited public tokens use local fallback aliases so host themes are never shadowed. */
2287
2291
  var viewerStyles = `
2288
2292
  :host {
@@ -2302,13 +2306,7 @@ button:disabled { opacity:.4; cursor:default; }
2302
2306
  button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible { outline:2px solid var(--vv-focus,var(--_vv-accent)); outline-offset:2px; }
2303
2307
  button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
2304
2308
  .toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
2305
- .ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
2306
- .page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
2307
- .page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
2308
- .page-stepper { display:flex; align-items:center; gap:2px; }
2309
- .page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
2310
2309
  .spacer { flex:1; }
2311
- .local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
2312
2310
  .ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
2313
2311
  .ribbon-tabs button { position:relative; min-height:34px; padding:7px 14px; border:0; border-radius:0; background:transparent; color:var(--_vv-muted); font-size:12px; font-weight:500; }
2314
2312
  .ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
@@ -2318,15 +2316,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
2318
2316
  .group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
2319
2317
  .ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
2320
2318
  .ribbon-command { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:2px; min-width:70px; padding:2px 6px; border-color:transparent; background:transparent; font-size:11px; white-space:nowrap; }
2321
- .command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
2322
- .command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
2323
- [data-icon="selection"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m5%203%2013%209-6%201-3%206z%22%2F%3E%3C%2Fsvg%3E"); }
2324
- [data-icon="edit"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%205h16M12%205v14M8%2019h8%22%2F%3E%3C%2Fsvg%3E"); }
2325
- [data-icon="layers"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m3%208%209-5%209%205-9%205zM3%2012l9%205%209-5M3%2016l9%205%209-5%22%2F%3E%3C%2Fsvg%3E"); }
2326
- [data-icon="pages"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%225%22%20y%3D%223%22%20width%3D%2214%22%20height%3D%2218%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h8M8%2016h5%22%2F%3E%3C%2Fsvg%3E"); }
2327
- [data-icon="inspector"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%223%22%20y%3D%224%22%20width%3D%2218%22%20height%3D%2216%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M14%204v16M17%208h1M17%2012h1M17%2016h1%22%2F%3E%3C%2Fsvg%3E"); }
2328
- [data-icon="notes"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%204h16v12H9l-5%204z%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h5%22%2F%3E%3C%2Fsvg%3E"); }
2329
- .ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
2330
2319
  .ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
2331
2320
  .search-group { width:330px; }
2332
2321
  .search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
@@ -2398,11 +2387,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2398
2387
  .status { flex:none; display:flex; align-items:center; justify-content:space-between; gap:12px; min-width:0; min-height:29px; padding:0 8px 0 12px; border-top:1px solid var(--_vv-border); color:var(--_vv-muted); background:var(--_vv-surface); font-size:10px; }
2399
2388
  .status-message { display:flex; gap:16px; min-width:0; }
2400
2389
  .status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
2401
- .zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
2402
- .zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
2403
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
2404
- .zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
2405
- .zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
2406
2390
  .empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
2407
2391
  .empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
2408
2392
  @media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
@@ -2410,11 +2394,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2410
2394
  button,select,input,textarea { min-height:44px; }
2411
2395
  button { min-width:44px; }
2412
2396
  summary { min-height:44px; line-height:44px; }
2413
- .ribbon-primary { height:52px; padding:4px 8px; }
2414
- .page-picker { flex:1; }
2415
- .page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
2416
- .page-stepper button { width:44px; min-height:44px; height:44px; }
2417
- .local-label,.ribbon-primary>.spacer { display:none; }
2418
2397
  .ribbon-tabs { height:44px; }
2419
2398
  .ribbon-tabs button { min-height:44px; padding:8px 16px; }
2420
2399
  .ribbon-content { min-height:86px; }
@@ -2426,7 +2405,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2426
2405
  .search-controls button { min-width:44px; font-size:0; padding:3px; }
2427
2406
  .search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
2428
2407
  .search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
2429
- #home-panel>.ribbon-group:not(.search-group) { display:none; }
2408
+ #home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
2430
2409
  .ribbon-command { min-width:90px; }
2431
2410
  .workspace { display:block; overflow:auto; }
2432
2411
  .page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
@@ -2443,35 +2422,22 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2443
2422
  .notes-strip button { min-height:44px; }
2444
2423
  .status { flex-wrap:wrap; gap:0; padding:4px 8px; }
2445
2424
  .status-message { width:100%; min-height:18px; }
2446
- .zoom-controls { justify-content:flex-end; width:100%; }
2447
- .zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
2448
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
2449
- .zoom { font-size:11px; }
2450
2425
  }
2451
2426
  @media(pointer:coarse) {
2452
2427
  button,select,input,textarea { min-height:44px; }
2453
2428
  button { min-width:44px; }
2454
2429
  summary,.layer-controls label { min-height:44px; line-height:44px; }
2455
- .ribbon-primary { min-height:52px; height:52px; }
2456
- .page-picker select,.page-stepper button { min-height:44px; height:44px; }
2457
- .page-stepper button { min-width:44px; }
2458
2430
  .ribbon-tabs { min-height:44px; height:44px; }
2459
2431
  .ribbon-tabs button { min-height:44px; }
2460
2432
  .ribbon-content { min-height:96px; }
2461
2433
  .search-group { width:380px; max-width:100%; }
2462
- .zoom-controls button,.notes-strip button { min-height:44px; height:44px; }
2463
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
2434
+ .notes-strip button { min-height:44px; height:44px; }
2464
2435
  .layer-controls input { min-height:20px; }
2465
2436
  }
2466
- /* Suite ribbon: tabs lead directly into command groups, with page navigation inside. */
2437
+ /* Suite ribbon: tabs lead directly into Visio command groups. */
2467
2438
  .ribbon-tabs { height:40px; }
2468
2439
  .ribbon-tabs button { min-height:39px; padding:8px 16px; }
2469
2440
  .ribbon-content { min-height:104px; padding:4px 4px; }
2470
- .ribbon-primary { height:auto; min-height:92px; width:238px; flex:none; flex-wrap:wrap; justify-content:center; gap:2px; padding:10px 12px 18px; border-right:1px solid var(--_vv-border); position:relative; order:1; }
2471
- .ribbon-primary .page-picker { flex-direction:column; align-items:start; gap:5px; }
2472
- .ribbon-primary .page-picker select { width:160px; max-width:100%; }
2473
- .ribbon-primary .spacer { display:none; }
2474
- .ribbon-primary .local-label { position:absolute; bottom:4px; left:0; width:100%; text-align:center; }
2475
2441
  .search-group { order:2; width:320px; }
2476
2442
  .ribbon-hint { order:3; padding:10px 18px; }
2477
2443
  .ribbon-command { min-width:82px; }
@@ -2480,13 +2446,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2480
2446
  .ribbon-tabs { height:44px; }
2481
2447
  .ribbon-tabs button { min-height:44px; }
2482
2448
  .ribbon-content { min-height:54px; padding:4px 0; }
2483
- .ribbon-primary { min-height:44px; height:44px; width:100%; padding:0 8px; border:0; flex-wrap:nowrap; gap:4px; order:0; }
2484
- .ribbon-primary .page-picker { flex-direction:row; flex:1; align-items:center; gap:8px; }
2485
- .ribbon-primary .page-picker select { flex:1; width:100%; min-width:0; }
2486
2449
  .search-group { flex:0 0 100%; width:100%; min-height:54px; }
2487
2450
  .search-feedback,.group-label { display:none; }
2488
2451
  .ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
2489
- .ribbon-primary,.search-group { scroll-snap-align:start; }
2490
2452
  .workspace { display:flex; position:relative; overflow:hidden; }
2491
2453
  .viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
2492
2454
  .inspector-pane { position:absolute; inset:0 0 0 auto; width:min(320px,100%); z-index:4; overflow:auto; border-left:1px solid var(--_vv-border); box-shadow:-8px 0 24px #0002; }
@@ -2515,9 +2477,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2515
2477
  .status-message { flex:1; }
2516
2478
  @media(max-width:760px) {
2517
2479
  #home-panel { overflow:visible; min-height:54px; padding:4px; }
2518
- .ribbon-primary { width:calc(100% - 92px); min-width:0; flex:1; padding:0 4px; }
2519
- .ribbon-primary .page-picker { font-size:0; gap:0; }
2520
- .ribbon-primary .page-picker select { font-size:12px; }
2521
2480
  .ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
2522
2481
  .ribbon-tools>summary { display:flex; min-height:44px; align-items:center; justify-content:center; gap:6px; padding:0 10px; border:1px solid var(--_vv-border); background:var(--_vv-surface); border-radius:4px; font-size:12px; cursor:pointer; list-style:none; }
2523
2482
  .ribbon-tools>summary::-webkit-details-marker { display:none; }
@@ -2529,15 +2488,348 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2529
2488
  .ribbon-tools .search-group { order:1; flex:auto; }
2530
2489
  .ribbon-tools .search-feedback { display:block; }
2531
2490
  .status { flex-wrap:nowrap; gap:4px; min-height:48px; }
2532
- .zoom-controls { width:auto; flex:1; gap:0; }
2533
- .zoom-divider { margin:0; }
2534
2491
  .status .notes-strip { display:none; }
2535
2492
  }
2536
2493
  @media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
2537
2494
  @media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
2538
2495
  `;
2539
2496
  //#endregion
2540
- //#region .package-build/output/src/edit-error.js
2497
+ //#region src/styles/index.ts
2498
+ /**
2499
+ * Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
2500
+ * `styles.ts` string moves into this directory when it is next changed.
2501
+ */
2502
+ var canvasAndRibbonStyles = ["/*\n * Layout for the shared ooxml-ui ribbon, tab strip and status controls inside the viewer.\n * Viewer theme tokens feed the shared --office-* tokens, so host themes still apply.\n */\n:host {\n --office-foreground: var(--_vv-ink);\n --office-muted-foreground: var(--_vv-muted);\n --office-background: var(--_vv-surface);\n --office-surface: var(--_vv-accent-soft);\n --office-border: var(--_vv-border);\n --office-accent: var(--_vv-accent);\n --office-accent-foreground: var(--vv-accent-ink, #fff);\n --office-ring: var(--vv-focus, var(--_vv-accent));\n --office-font: 'Segoe UI', Arial, sans-serif;\n}\n.ribbon-row {\n display: flex;\n flex-wrap: nowrap;\n align-items: stretch;\n gap: 0;\n padding: 0;\n border: 0;\n background: transparent;\n}\n.ribbon-row office-ui-ribbon-group {\n min-width: 0;\n}\n.ribbon-row office-ui-button {\n --office-target-size: 28px;\n}\n.ribbon-row office-ui-button[variant='stacked']::part(button) {\n min-width: 64px;\n min-height: 62px;\n font-size: 12px;\n}\noffice-ui-button[disabled]::part(button) {\n opacity: 0.45;\n}\n.page-tabs {\n flex: none;\n}\noffice-ui-status-bar.status {\n gap: 16px;\n min-height: 29px;\n padding: 0 8px 0 12px;\n font-size: 11px;\n background: var(--_vv-surface);\n color: var(--_vv-muted);\n}\noffice-ui-status-item[value=''] {\n display: none;\n}\noffice-ui-status-item[data-page-status] {\n color: var(--_vv-ink);\n}\n.status-message {\n flex: 1;\n min-width: 0;\n}\n.viewport[data-tool='rectangle'],\n.viewport[data-tool='rectangle'] .paper [data-shape-id] {\n cursor: crosshair;\n}\n.viewport[data-tool='rectangle'] {\n touch-action: none;\n}\n.draw-preview {\n fill: var(--_vv-accent-soft);\n stroke: var(--_vv-accent);\n stroke-width: 1.5px;\n stroke-dasharray: 4 3;\n pointer-events: none;\n}\n.viewport[data-grid='true'] .paper {\n --_vv-grid-minor: rgb(0 0 0 / 7%);\n --_vv-grid-major: rgb(0 0 0 / 16%);\n background-image:\n linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px),\n linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px),\n linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);\n background-size:\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n var(--vv-inch, 96px) var(--vv-inch, 96px),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4),\n calc(var(--vv-inch, 96px) / 4) calc(var(--vv-inch, 96px) / 4);\n background-position: left bottom;\n}\n:host(:fullscreen) {\n width: 100vw;\n height: 100vh;\n}\n@media (max-width: 760px) {\n .ribbon-row {\n flex: 1;\n min-width: 0;\n }\n .undo-group {\n flex: none;\n }\n .ribbon-row office-ui-button[variant='stacked']::part(button) {\n min-width: 52px;\n min-height: 44px;\n }\n .ribbon-tools .tools-content office-ui-ribbon-group {\n width: 100%;\n }\n .search-group {\n scroll-snap-align: start;\n }\n office-ui-status-bar.status {\n gap: 4px;\n min-height: 48px;\n }\n office-ui-status-bar .notes-strip,\n office-ui-status-bar .status-message,\n office-ui-status-item[data-shape-status],\n office-ui-status-item[data-page-status] {\n display: none;\n }\n .zoom-controls {\n flex: 1;\n min-width: 0;\n --office-zoom-slider-width: 0;\n }\n}\n", "/*\n * Drawing canvas focus. SVG outlines are drawn in drawing units (inches here), so a browser\n * focus ring on a shape becomes a page-sized blob. Shapes show focus with a dashed accent\n * stroke instead, only for keyboard focus; selection keeps its solid accent stroke.\n */\n.paper [data-shape-id]:focus,\n.paper [data-shape-id]:focus-visible {\n outline: none;\n}\n.paper [data-shape-id]:focus-visible > [data-geometry] {\n stroke: var(--vv-focus, var(--_vv-accent)) !important;\n stroke-width: 0.04 !important;\n stroke-dasharray: 0.08 0.05;\n}\n"].join("\n");
2503
+ //#endregion
2504
+ //#region src/ribbon-action.ts
2505
+ /** Event name shared by every ribbon control; detail is the typed action. */
2506
+ var RIBBON_ACTION_EVENT = "ribbon-action";
2507
+ /** Internal, non-composed event: it stays inside the viewer's shadow tree. */
2508
+ function emitRibbonAction(target, action) {
2509
+ target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
2510
+ detail: action,
2511
+ bubbles: true
2512
+ }));
2513
+ }
2514
+ //#endregion
2515
+ //#region src/ribbon-parts.ts
2516
+ /**
2517
+ * A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
2518
+ * `action` is what runs. Labels, icons and shortcuts are static application strings.
2519
+ */
2520
+ function tool(doc, id, label, icon, action, options = {}) {
2521
+ const el = doc.createElement("office-ui-button");
2522
+ el.setAttribute("variant", "stacked");
2523
+ el.setAttribute("command", id);
2524
+ el.setAttribute("icon", icon);
2525
+ el.setAttribute("label", label);
2526
+ el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
2527
+ if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
2528
+ if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
2529
+ if (options.controls) el.setAttribute("aria-controls", options.controls);
2530
+ el.addEventListener("office-command", (event) => {
2531
+ event.stopPropagation();
2532
+ emitRibbonAction(el, action);
2533
+ });
2534
+ return el;
2535
+ }
2536
+ /** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
2537
+ function group(doc, label, children, className = "") {
2538
+ const el = doc.createElement("office-ui-ribbon-group");
2539
+ el.setAttribute("label", label);
2540
+ if (className) el.className = className;
2541
+ el.append(...children);
2542
+ return el;
2543
+ }
2544
+ /** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
2545
+ function commandRow(doc, label, children) {
2546
+ const el = doc.createElement("office-ui-toolbar");
2547
+ el.className = "ribbon-row";
2548
+ el.setAttribute("aria-label", label);
2549
+ el.append(...children);
2550
+ return el;
2551
+ }
2552
+ /** Muted two-line guidance at the end of a ribbon panel. */
2553
+ function hint(doc, lines) {
2554
+ const el = doc.createElement("p");
2555
+ el.className = "ribbon-hint";
2556
+ lines.forEach((line, index) => {
2557
+ if (index) el.append(doc.createElement("br"));
2558
+ el.append(line);
2559
+ });
2560
+ return el;
2561
+ }
2562
+ //#endregion
2563
+ //#region src/viewer-search.ts
2564
+ /** Builds the Find group: a search field, previous/next result buttons and a live status. */
2565
+ function createSearchGroup(doc) {
2566
+ const group = doc.createElement("div");
2567
+ group.className = "ribbon-group search-group";
2568
+ group.setAttribute("role", "group");
2569
+ group.setAttribute("aria-label", "Find");
2570
+ const controls = doc.createElement("div");
2571
+ controls.className = "search-controls";
2572
+ controls.setAttribute("role", "search");
2573
+ controls.setAttribute("aria-label", "Diagram text search");
2574
+ const label = doc.createElement("label");
2575
+ label.append("Find ");
2576
+ const input = doc.createElement("input");
2577
+ input.type = "search";
2578
+ input.setAttribute("aria-label", "Search diagram text");
2579
+ input.placeholder = "Text across pages";
2580
+ input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
2581
+ input.autocomplete = "off";
2582
+ input.spellcheck = false;
2583
+ input.setAttribute("aria-describedby", "search-status");
2584
+ label.append(input);
2585
+ const step = (action, name, text) => {
2586
+ const button = doc.createElement("button");
2587
+ button.type = "button";
2588
+ button.dataset.action = action;
2589
+ button.setAttribute("aria-label", name);
2590
+ button.textContent = text;
2591
+ return button;
2592
+ };
2593
+ const status = doc.createElement("span");
2594
+ status.id = "search-status";
2595
+ status.setAttribute("role", "status");
2596
+ status.setAttribute("aria-live", "polite");
2597
+ status.setAttribute("aria-atomic", "true");
2598
+ controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
2599
+ const caption = doc.createElement("span");
2600
+ caption.className = "group-label";
2601
+ caption.textContent = "Find in drawing";
2602
+ group.append(controls, caption);
2603
+ return group;
2604
+ }
2605
+ function searchControls(root) {
2606
+ return {
2607
+ input: root.querySelector("input[type=\"search\"]"),
2608
+ status: root.querySelector("#search-status"),
2609
+ previous: root.querySelector("[data-action=\"search-previous\"]"),
2610
+ next: root.querySelector("[data-action=\"search-next\"]")
2611
+ };
2612
+ }
2613
+ function renderSearchControls(controls, state) {
2614
+ if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
2615
+ controls.input.disabled = !state.document?.pages.length;
2616
+ controls.previous.disabled = controls.next.disabled = !state.search.results.length;
2617
+ controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
2618
+ const result = state.search.results[state.search.activeIndex];
2619
+ controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
2620
+ }
2621
+ //#endregion
2622
+ //#region src/ribbon-home.ts
2623
+ /**
2624
+ * Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
2625
+ * Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
2626
+ * The compact (phone) layout folds every group but Undo into the Tools disclosure.
2627
+ */
2628
+ function buildHomePanel(doc, panel) {
2629
+ const tools = doc.createElement("details");
2630
+ tools.className = "ribbon-tools";
2631
+ tools.open = true;
2632
+ const summary = doc.createElement("summary");
2633
+ const caret = doc.createElement("span");
2634
+ caret.setAttribute("aria-hidden", "true");
2635
+ caret.textContent = "⌄";
2636
+ summary.append("Tools ", caret);
2637
+ const content = doc.createElement("div");
2638
+ content.className = "tools-content";
2639
+ content.append(group(doc, "Tools", [
2640
+ tool(doc, "pointer", "Pointer Tool", "pointer", {
2641
+ type: "tool",
2642
+ tool: "pointer"
2643
+ }, {
2644
+ keys: ["Control+1", "Ctrl+1"],
2645
+ pressed: true
2646
+ }),
2647
+ tool(doc, "rectangle", "Rectangle", "rectangle", {
2648
+ type: "tool",
2649
+ tool: "rectangle"
2650
+ }, {
2651
+ keys: ["Control+8", "Ctrl+8"],
2652
+ pressed: false
2653
+ }),
2654
+ tool(doc, "edit", "Edit text", "pencil", {
2655
+ type: "reveal",
2656
+ panel: "edit"
2657
+ }, { keys: ["F2", "F2"] })
2658
+ ]), createSearchGroup(doc), group(doc, "Editing", [
2659
+ tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
2660
+ tool(doc, "layers", "Layers", "visioLayers", {
2661
+ type: "reveal",
2662
+ panel: "layers"
2663
+ }),
2664
+ tool(doc, "selection", "Shape details", "visioShapeData", {
2665
+ type: "reveal",
2666
+ panel: "selection"
2667
+ })
2668
+ ]));
2669
+ tools.append(summary, content);
2670
+ panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
2671
+ type: "history",
2672
+ key: "undo"
2673
+ }, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
2674
+ type: "history",
2675
+ key: "redo"
2676
+ }, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
2677
+ }
2678
+ //#endregion
2679
+ //#region src/ribbon-view.ts
2680
+ /** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
2681
+ function buildViewPanel(doc, panel) {
2682
+ panel.append(commandRow(doc, "View commands", [
2683
+ group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
2684
+ keys: ["F5", "F5"],
2685
+ pressed: false
2686
+ })]),
2687
+ group(doc, "Show", [
2688
+ tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
2689
+ tool(doc, "pages", "Pages pane", "visioPagesPane", {
2690
+ type: "pane",
2691
+ pane: "pages"
2692
+ }, {
2693
+ pressed: true,
2694
+ controls: "page-rail"
2695
+ }),
2696
+ tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
2697
+ type: "pane",
2698
+ pane: "inspector"
2699
+ }, {
2700
+ pressed: true,
2701
+ controls: "inspector-pane"
2702
+ }),
2703
+ tool(doc, "notes", "Review notes", "message", {
2704
+ type: "reveal",
2705
+ panel: "notes"
2706
+ })
2707
+ ]),
2708
+ group(doc, "Zoom", [
2709
+ tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
2710
+ type: "zoom",
2711
+ mode: "fit"
2712
+ }, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
2713
+ tool(doc, "page-width", "Page Width", "pageWidth", {
2714
+ type: "zoom",
2715
+ mode: "width"
2716
+ }),
2717
+ tool(doc, "actual-size", "100%", "search", {
2718
+ type: "zoom",
2719
+ mode: "actual"
2720
+ })
2721
+ ])
2722
+ ]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
2723
+ }
2724
+ //#endregion
2725
+ //#region src/ribbon.ts
2726
+ var TABS = [[
2727
+ "home",
2728
+ "Home",
2729
+ buildHomePanel
2730
+ ], [
2731
+ "view",
2732
+ "View",
2733
+ buildViewPanel
2734
+ ]];
2735
+ /**
2736
+ * The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
2737
+ * belong to the chrome; commands emit `ribbon-action` events for the router.
2738
+ */
2739
+ function createRibbon(doc) {
2740
+ const toolbar = doc.createElement("div");
2741
+ toolbar.className = "toolbar";
2742
+ toolbar.setAttribute("role", "group");
2743
+ toolbar.setAttribute("aria-label", "Diagram controls");
2744
+ const tabs = doc.createElement("div");
2745
+ tabs.className = "ribbon-tabs";
2746
+ tabs.setAttribute("role", "tablist");
2747
+ tabs.setAttribute("aria-label", "Ribbon");
2748
+ toolbar.append(tabs);
2749
+ for (const [key, name, build] of TABS) {
2750
+ const selected = key === "home";
2751
+ const tab = doc.createElement("button");
2752
+ tab.type = "button";
2753
+ tab.id = `${key}-tab`;
2754
+ tab.dataset.tab = key;
2755
+ tab.textContent = name;
2756
+ tab.setAttribute("role", "tab");
2757
+ tab.setAttribute("aria-selected", String(selected));
2758
+ tab.setAttribute("aria-controls", `${key}-panel`);
2759
+ if (!selected) tab.tabIndex = -1;
2760
+ const panel = doc.createElement("div");
2761
+ panel.className = "ribbon-content";
2762
+ panel.id = `${key}-panel`;
2763
+ panel.setAttribute("role", "tabpanel");
2764
+ panel.setAttribute("aria-labelledby", tab.id);
2765
+ panel.hidden = !selected;
2766
+ build(doc, panel);
2767
+ tabs.append(tab);
2768
+ toolbar.append(panel);
2769
+ }
2770
+ return toolbar;
2771
+ }
2772
+ //#endregion
2773
+ //#region src/status-bar.ts
2774
+ /**
2775
+ * Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
2776
+ * message region, compatibility notes, the host footer slot and the shared zoom slider.
2777
+ */
2778
+ function createStatusBar(doc) {
2779
+ const bar = doc.createElement("office-ui-status-bar");
2780
+ bar.className = "status";
2781
+ bar.setAttribute("label", "Status bar");
2782
+ const item = (name) => {
2783
+ const el = doc.createElement("office-ui-status-item");
2784
+ el.setAttribute(name, "");
2785
+ el.setAttribute("label", "");
2786
+ el.setAttribute("value", "");
2787
+ return el;
2788
+ };
2789
+ const message = doc.createElement("div");
2790
+ message.className = "status-message";
2791
+ message.setAttribute("role", "status");
2792
+ const text = doc.createElement("span");
2793
+ text.dataset.status = "";
2794
+ const diagnostics = doc.createElement("span");
2795
+ diagnostics.dataset.diagnostics = "";
2796
+ message.append(text, diagnostics);
2797
+ const notes = doc.createElement("div");
2798
+ notes.className = "notes-strip";
2799
+ const notesButton = doc.createElement("button");
2800
+ notesButton.type = "button";
2801
+ notesButton.dataset.chrome = "notes";
2802
+ notesButton.setAttribute("aria-controls", "inspector-pane");
2803
+ notesButton.setAttribute("aria-expanded", "false");
2804
+ const count = doc.createElement("span");
2805
+ count.className = "notes-count";
2806
+ count.dataset.noteCount = "";
2807
+ notesButton.append("Notes", count);
2808
+ const local = doc.createElement("span");
2809
+ local.textContent = "Files stay in your browser";
2810
+ notes.append(notesButton, local);
2811
+ const footer = doc.createElement("slot");
2812
+ footer.name = "workspace-footer";
2813
+ const zoom = doc.createElement("office-ui-zoom-slider");
2814
+ zoom.slot = "end";
2815
+ zoom.className = "zoom-controls";
2816
+ zoom.setAttribute("label", "Canvas zoom");
2817
+ zoom.setAttribute("fit", "Fit page to current window");
2818
+ zoom.setAttribute("value", "100");
2819
+ bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
2820
+ return bar;
2821
+ }
2822
+ /** Visio's page tabs under the drawing, on the shared document tab strip. */
2823
+ function createPageTabs(doc) {
2824
+ const strip = doc.createElement("office-ui-tab-strip");
2825
+ strip.className = "page-tabs";
2826
+ strip.setAttribute("label", "Pages");
2827
+ strip.setAttribute("previous-label", "Previous page");
2828
+ strip.setAttribute("next-label", "Next page");
2829
+ return strip;
2830
+ }
2831
+ //#endregion
2832
+ //#region src/edit-error.ts
2541
2833
  /** Core error codes remain visible for a refused transaction; document content is plain text. */
2542
2834
  function editErrorMessage(error) {
2543
2835
  const message = error instanceof Error ? error.message : String(error);
@@ -2549,10 +2841,13 @@ function isEditCancellation(error) {
2549
2841
  return !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
2550
2842
  }
2551
2843
  //#endregion
2552
- //#region .package-build/output/src/viewer-geometry-controls.js
2844
+ //#region src/viewer-geometry-controls.ts
2553
2845
  var geometryControlsTemplate = `<fieldset data-geometry><legend>Geometry (experimental)</legend><p>Pin X/Y use drawing inches, bottom-left origin, Y up. Resize holds the rotation pin fixed. Core rejects unsupported formulas, protected cells, unsafe group/master dependencies and referenced deletion. Native Visio reopen fidelity is unverified.</p><label>New rectangle ID <input data-geometry-field="id" type="text" maxlength="256"></label><label>Rectangle text (optional) <input data-geometry-field="text" type="text" maxlength="1000000"></label><label>Pin X (inches) <input data-geometry-field="x" type="number" step="any"></label><label>Pin Y (inches) <input data-geometry-field="y" type="number" step="any"></label><label>Width (inches) <input data-geometry-field="width" type="number" min="0" step="any"></label><label>Height (inches) <input data-geometry-field="height" type="number" min="0" step="any"></label><div class="edit-actions"><button type="button" data-geometry-action="create-rectangle">Create rectangle</button><button type="button" data-geometry-action="move-shape">Move selected</button><button type="button" data-geometry-action="resize-shape">Resize selected</button><button type="button" data-geometry-action="delete-shape">Delete selected</button></div><p data-geometry-error role="alert" hidden></p></fieldset>`;
2554
2846
  /** Collect explicit coordinates; never infer a Visio pin from rendered SVG transforms. */
2555
2847
  var ViewerGeometryControls = class {
2848
+ panel;
2849
+ controller;
2850
+ draftChanged;
2556
2851
  #fields;
2557
2852
  #buttons;
2558
2853
  #error;
@@ -2673,10 +2968,12 @@ var ViewerGeometryControls = class {
2673
2968
  }
2674
2969
  };
2675
2970
  //#endregion
2676
- //#region .package-build/output/src/viewer-edit-controls.js
2971
+ //#region src/viewer-edit-controls.ts
2677
2972
  /** Static markup only. Document text is assigned exclusively through value/textContent. */
2678
2973
  var editControlsTemplate = `<details class="edit-controls"><summary>Edit diagram (experimental)</summary><p id="edit-warning">Experimental source-backed editing. Unsupported targets are rejected. Native Visio compatibility is not verified. Keep your original file.</p><p data-edit-target></p><label for="edit-text">Selected shape text</label><textarea id="edit-text" rows="3" aria-describedby="edit-warning edit-target edit-status"></textarea><div class="edit-actions"><button type="button" data-edit="apply">Apply text</button><button type="button" data-edit="cancel">Cancel</button><button type="button" data-edit="undo">Undo</button><button type="button" data-edit="redo">Redo</button></div><p id="edit-status" role="status" aria-live="polite"></p><p data-edit-error role="alert" hidden></p><ul data-edit-diagnostics></ul>${geometryControlsTemplate}</details>`;
2679
2974
  var ViewerEditControls = class {
2975
+ panel;
2976
+ input;
2680
2977
  #target;
2681
2978
  #status;
2682
2979
  #error;
@@ -2819,59 +3116,13 @@ var ViewerEditControls = class {
2819
3116
  }
2820
3117
  };
2821
3118
  //#endregion
2822
- //#region .package-build/output/src/viewer-search.js
2823
- var searchTemplate = `<div class="search-controls" role="search" aria-label="Diagram text search"><label>Find <input type="search" aria-label="Search diagram text" placeholder="Text across pages" maxlength="${TEXT_SEARCH_LIMITS.queryCharacters}" autocomplete="off" spellcheck="false" aria-describedby="search-status"></label><button type="button" data-action="search-previous" aria-label="Previous matching shape">Previous</button><button type="button" data-action="search-next" aria-label="Next matching shape">Next</button><span id="search-status" role="status" aria-live="polite" aria-atomic="true"></span></div>`;
2824
- function searchControls(root) {
2825
- return {
2826
- input: root.querySelector("input[type=\"search\"]"),
2827
- status: root.querySelector("#search-status"),
2828
- previous: root.querySelector("[data-action=\"search-previous\"]"),
2829
- next: root.querySelector("[data-action=\"search-next\"]")
2830
- };
2831
- }
2832
- function renderSearchControls(controls, state) {
2833
- if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
2834
- controls.input.disabled = !state.document?.pages.length;
2835
- controls.previous.disabled = controls.next.disabled = !state.search.results.length;
2836
- controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
2837
- const result = state.search.results[state.search.activeIndex];
2838
- controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
2839
- }
2840
- //#endregion
2841
- //#region .package-build/output/src/viewer-chrome.js
2842
- /** Only static application markup belongs here. All document labels are inserted as text. */
2843
- var viewerChromeTemplate = `
2844
- <div class="toolbar" role="group" aria-label="Diagram controls">
2845
-
2846
- <div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
2847
- <button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
2848
- <button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
2849
- </div>
2850
- <div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
2851
- <details class="ribbon-tools" open><summary>Tools <span aria-hidden="true">⌄</span></summary><div class="tools-content"><div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
2852
- <div class="ribbon-group"><div class="ribbon-actions">
2853
- <button class="ribbon-command" type="button" data-chrome="selection"><span class="command-icon" data-icon="selection" aria-hidden="true"></span><span>Shape details</span></button>
2854
- <button class="ribbon-command" type="button" data-chrome="edit"><span class="command-icon" data-icon="edit" aria-hidden="true"></span><span>Edit text</span></button>
2855
- <button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
2856
- </div><span class="group-label">Inspect and edit</span></div>
2857
- </div></details>
2858
- <div class="ribbon-primary">
2859
- <label class="page-picker">Page <select aria-label="Page"></select></label>
2860
- <div class="page-stepper"><button type="button" data-chrome="previous-page" aria-label="Previous page">‹</button><button type="button" data-chrome="next-page" aria-label="Next page">›</button></div>
2861
- <span class="spacer"></span><span class="local-label">Local workspace</span>
2862
- </div>
2863
- <p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
2864
- </div>
2865
- <div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
2866
- <div class="ribbon-group"><div class="ribbon-actions">
2867
- <button class="ribbon-command" type="button" data-chrome="pages" aria-pressed="true" aria-controls="page-rail"><span class="command-icon" data-icon="pages" aria-hidden="true"></span><span>Pages pane</span></button>
2868
- <button class="ribbon-command" type="button" data-chrome="inspector" aria-pressed="true" aria-controls="inspector-pane"><span class="command-icon" data-icon="inspector" aria-hidden="true"></span><span>Inspector pane</span></button>
2869
- <button class="ribbon-command" type="button" data-chrome="notes"><span class="command-icon" data-icon="notes" aria-hidden="true"></span><span>Review notes</span></button>
2870
- </div><span class="group-label">Workspace panes</span></div>
2871
- <p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
2872
- </div>
2873
- </div>
2874
- <div class="workspace">
3119
+ //#region src/viewer-chrome.ts
3120
+ /**
3121
+ * Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
3122
+ * tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
3123
+ * All document labels are inserted as text.
3124
+ */
3125
+ var viewerChromeTemplate = `<div class="workspace">
2875
3126
  <nav id="page-rail" class="page-rail" aria-label="Diagram pages"><div class="pane-heading"><span>Pages</span><span data-page-count>0</span></div><ol class="page-list"></ol><p class="page-empty">Open a drawing to see its pages.</p></nav>
2876
3127
  <div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
2877
3128
  <aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
@@ -2884,14 +3135,15 @@ var viewerChromeTemplate = `
2884
3135
  <details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
2885
3136
  </div>
2886
3137
  </aside>
2887
- </div>
2888
- <div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><div class="notes-strip"><button type="button" data-chrome="notes" aria-controls="inspector-pane" aria-expanded="false">Notes<span class="notes-count" data-note-count></span></button><span>Files stay in your browser</span></div><slot name="workspace-footer"></slot><div class="zoom-controls" role="group" aria-label="Canvas zoom"><button type="button" data-action="fit">Fit page</button><button type="button" data-action="actual">100%</button><span class="zoom-divider"></span><button type="button" data-action="out" aria-label="Zoom out">−</button><output class="zoom" aria-label="Zoom level">100%</output><button type="button" data-action="in" aria-label="Zoom in">+</button></div></div>`;
3138
+ </div>`;
2889
3139
  /** Presentation state stays local; page navigation uses the same controller as every binding. */
2890
3140
  var ViewerChrome = class {
2891
3141
  #root;
2892
3142
  #controller;
2893
3143
  #document;
2894
3144
  #pageList;
3145
+ #pageTabs;
3146
+ #tools;
2895
3147
  #rail;
2896
3148
  #inspector;
2897
3149
  #notes;
@@ -2903,6 +3155,8 @@ var ViewerChrome = class {
2903
3155
  this.#root = root;
2904
3156
  this.#controller = controller;
2905
3157
  this.#pageList = root.querySelector(".page-list");
3158
+ this.#pageTabs = root.querySelector("office-ui-tab-strip");
3159
+ this.#tools = root.querySelector(".ribbon-tools");
2906
3160
  this.#rail = root.querySelector(".page-rail");
2907
3161
  this.#inspector = root.querySelector(".inspector-pane");
2908
3162
  this.#notes = root.querySelector(".notes");
@@ -2913,7 +3167,7 @@ var ViewerChrome = class {
2913
3167
  this.#responsive = view?.matchMedia?.("(max-width: 980px)");
2914
3168
  const compact = view?.matchMedia?.("(max-width: 760px)");
2915
3169
  this.#compact = compact;
2916
- const tools = this.#root.querySelector(".ribbon-tools");
3170
+ const tools = this.#tools;
2917
3171
  const toolLayout = () => {
2918
3172
  tools.open = !compact?.matches;
2919
3173
  };
@@ -2940,22 +3194,16 @@ var ViewerChrome = class {
2940
3194
  this.#root.addEventListener("click", (event) => {
2941
3195
  const button = event.target?.closest?.("button");
2942
3196
  if (!button || button.disabled) return;
2943
- if (compact?.matches && button.dataset.chrome) tools.open = false;
2944
3197
  const tab = button.dataset.tab;
2945
- if (tab === "home" || tab === "view") this.#showTab(tab);
3198
+ if (tab === "home" || tab === "view") this.showTab(tab);
2946
3199
  if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
2947
3200
  const action = button.dataset.chrome;
2948
- if (action === "previous-page") this.#controller.setPage(this.#controller.state.pageIndex - 1);
2949
- if (action === "next-page") this.#controller.setPage(this.#controller.state.pageIndex + 1);
2950
- if (action === "pages") {
2951
- this.#pagesManuallyToggled = true;
2952
- this.#setPane("pages", this.#rail.hidden);
2953
- }
2954
- if (action === "inspector") {
2955
- this.#inspectorManuallyToggled = true;
2956
- this.#setPane("inspector", this.#inspector.hidden);
2957
- }
2958
- if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
3201
+ if (action === "inspector") this.togglePane("inspector");
3202
+ if (action === "notes") this.reveal("notes");
3203
+ }, options);
3204
+ this.#root.addEventListener("office-tab-select", (event) => {
3205
+ const id = Number(event.detail.id);
3206
+ if (Number.isSafeInteger(id)) this.#controller.setPage(id);
2959
3207
  }, options);
2960
3208
  this.#root.addEventListener("keydown", (event) => {
2961
3209
  const key = event;
@@ -2972,11 +3220,11 @@ var ViewerChrome = class {
2972
3220
  if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
2973
3221
  key.preventDefault();
2974
3222
  const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
2975
- this.#showTab(tab);
3223
+ this.showTab(tab);
2976
3224
  this.#root.querySelector(`[data-tab="${tab}"]`).focus();
2977
3225
  } else if (button.dataset.pageIndex !== void 0) {
2978
3226
  key.preventDefault();
2979
- const pages = [...this.#pageList.querySelectorAll("button")];
3227
+ const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
2980
3228
  const current = Number(button.dataset.pageIndex);
2981
3229
  const next = key.key === "Home" ? 0 : key.key === "End" ? pages.length - 1 : Math.max(0, Math.min(pages.length - 1, current + (["ArrowRight", "ArrowDown"].includes(key.key) ? 1 : -1)));
2982
3230
  this.#controller.setPage(next);
@@ -2986,7 +3234,7 @@ var ViewerChrome = class {
2986
3234
  this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
2987
3235
  return () => events.abort();
2988
3236
  }
2989
- #showTab(tab) {
3237
+ showTab(tab) {
2990
3238
  for (const button of this.#root.querySelectorAll("[data-tab]")) {
2991
3239
  const selected = button.dataset.tab === tab;
2992
3240
  button.setAttribute("aria-selected", String(selected));
@@ -2994,27 +3242,41 @@ var ViewerChrome = class {
2994
3242
  }
2995
3243
  for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
2996
3244
  }
3245
+ /** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
3246
+ togglePane(pane) {
3247
+ if (this.#compact?.matches) this.#tools.open = false;
3248
+ if (pane === "pages") this.#pagesManuallyToggled = true;
3249
+ else this.#inspectorManuallyToggled = true;
3250
+ this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
3251
+ }
3252
+ #command(name) {
3253
+ return this.#root.querySelector(`office-ui-button[command="${name}"]`);
3254
+ }
2997
3255
  #setPane(pane, visible) {
2998
3256
  if (visible && this.#compact?.matches) {
2999
3257
  const other = pane === "pages" ? "inspector" : "pages";
3000
3258
  (other === "pages" ? this.#rail : this.#inspector).hidden = true;
3001
- this.#root.querySelector(`[data-chrome="${other}"]`).setAttribute("aria-pressed", "false");
3259
+ this.#command(other).setAttribute("pressed", "false");
3002
3260
  }
3003
3261
  (pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
3004
- this.#root.querySelector(`[data-chrome="${pane}"]`).setAttribute("aria-pressed", String(visible));
3262
+ this.#command(pane).setAttribute("pressed", String(visible));
3005
3263
  this.#syncNotes();
3006
3264
  }
3007
3265
  #syncNotes() {
3008
3266
  this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
3009
3267
  }
3010
- #reveal(kind) {
3268
+ /** Open an inspector disclosure; text editing may focus the text field directly (F2). */
3269
+ reveal(kind, focusText = false) {
3270
+ if (this.#compact?.matches) this.#tools.open = false;
3011
3271
  this.#inspectorManuallyToggled = true;
3012
3272
  this.#setPane("inspector", true);
3013
3273
  const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
3014
3274
  const panel = this.#root.querySelector(selector);
3015
3275
  if (panel.hidden) return;
3016
3276
  panel.open = true;
3017
- panel.querySelector("summary").focus();
3277
+ const text = panel.querySelector("textarea");
3278
+ if (focusText && text && !text.disabled) text.focus();
3279
+ else panel.querySelector("summary").focus();
3018
3280
  panel.scrollIntoView?.({
3019
3281
  block: "nearest",
3020
3282
  inline: "nearest"
@@ -3055,14 +3317,21 @@ var ViewerChrome = class {
3055
3317
  fragment.append(item);
3056
3318
  }
3057
3319
  this.#pageList.replaceChildren(fragment);
3320
+ this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
3321
+ id: String(index),
3322
+ label: candidate.name,
3323
+ title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
3324
+ }));
3058
3325
  if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
3059
3326
  }
3327
+ this.#pageTabs.selected = page ? String(state.pageIndex) : "";
3060
3328
  for (const button of this.#pageList.querySelectorAll("button")) {
3061
3329
  const selected = Number(button.dataset.pageIndex) === state.pageIndex;
3062
3330
  if (selected) button.setAttribute("aria-current", "page");
3063
3331
  else button.removeAttribute("aria-current");
3064
3332
  button.tabIndex = selected ? 0 : -1;
3065
3333
  }
3334
+ this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
3066
3335
  const count = state.document?.pages.length ?? 0;
3067
3336
  this.#root.querySelector("[data-page-count]").textContent = String(count);
3068
3337
  this.#root.querySelector(".page-empty").hidden = count > 0;
@@ -3070,18 +3339,406 @@ var ViewerChrome = class {
3070
3339
  this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
3071
3340
  this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
3072
3341
  this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
3073
- this.#root.querySelector("[data-chrome=\"previous-page\"]").disabled = !page || state.pageIndex === 0;
3074
- this.#root.querySelector("[data-chrome=\"next-page\"]").disabled = !page || state.pageIndex >= count - 1;
3075
- this.#root.querySelector("[data-chrome=\"selection\"]").disabled = !state.selectedShape;
3076
- this.#root.querySelector("[data-chrome=\"edit\"]").disabled = !page;
3077
- this.#root.querySelector("[data-chrome=\"layers\"]").disabled = this.#root.querySelector(".layer-controls").hidden;
3078
- for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
3342
+ this.#command("selection").disabled = !state.selectedShape;
3343
+ this.#command("edit").disabled = !page;
3344
+ this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
3345
+ this.#command("notes").disabled = noteCount === 0;
3346
+ this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
3079
3347
  this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
3080
3348
  this.#syncNotes();
3081
3349
  }
3082
3350
  };
3083
3351
  //#endregion
3084
- //#region .package-build/output/src/export-svg.js
3352
+ //#region src/ribbon-router.ts
3353
+ /** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
3354
+ function routeRibbonAction(targets, action) {
3355
+ const { controller } = targets;
3356
+ const page = controller.state.document?.pages[controller.state.pageIndex];
3357
+ switch (action.type) {
3358
+ case "history": return targets.history(action.key);
3359
+ case "delete": return targets.deleteSelection();
3360
+ case "tool": return targets.setTool(action.tool);
3361
+ case "grid": return targets.toggleGrid();
3362
+ case "fullscreen": return targets.toggleFullscreen();
3363
+ case "pane": return targets.togglePane(action.pane);
3364
+ case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
3365
+ case "search": return targets.focusSearch();
3366
+ case "page": return controller.setPage(controller.state.pageIndex + action.step);
3367
+ case "zoom":
3368
+ if (!page) return;
3369
+ if (action.mode === "actual") return controller.setZoom(1);
3370
+ return targets.fit(action.mode === "fit" ? "page" : "width");
3371
+ }
3372
+ }
3373
+ //#endregion
3374
+ //#region src/viewer-draw-tool.ts
3375
+ /** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
3376
+ var SNAP = 1 / 16;
3377
+ var MIN_SIZE = SNAP;
3378
+ var snap = (value) => Math.round(value / SNAP) * SNAP;
3379
+ /** Next free numeric shape ID on the page tree. Core still rejects any collision. */
3380
+ function nextShapeId(page) {
3381
+ let max = 0;
3382
+ const pending = [...page.shapes];
3383
+ while (pending.length) {
3384
+ const shape = pending.pop();
3385
+ if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
3386
+ pending.push(...shape.children);
3387
+ }
3388
+ return String(max + 1);
3389
+ }
3390
+ /** Page inches with a top-left origin (SVG user space) from a client point. */
3391
+ function pagePoint(svg, page, event) {
3392
+ const matrix = svg.getScreenCTM?.();
3393
+ if (!matrix) return void 0;
3394
+ const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
3395
+ return {
3396
+ x: snap(Math.max(0, Math.min(page.width, point.x))),
3397
+ y: snap(Math.max(0, Math.min(page.height, point.y)))
3398
+ };
3399
+ }
3400
+ /** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
3401
+ var RectangleDrawTool = class {
3402
+ viewport;
3403
+ controller;
3404
+ options;
3405
+ #drag;
3406
+ #request = 0;
3407
+ constructor(viewport, controller, options) {
3408
+ this.viewport = viewport;
3409
+ this.controller = controller;
3410
+ this.options = options;
3411
+ }
3412
+ get drawing() {
3413
+ return !!this.#drag;
3414
+ }
3415
+ wire() {
3416
+ const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
3417
+ const options = { signal: events.signal };
3418
+ const viewport = this.viewport;
3419
+ viewport.addEventListener("click", (event) => {
3420
+ if (this.options.active()) event.stopImmediatePropagation();
3421
+ }, {
3422
+ ...options,
3423
+ capture: true
3424
+ });
3425
+ viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
3426
+ viewport.addEventListener("pointermove", (event) => this.#move(event), options);
3427
+ viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
3428
+ viewport.addEventListener("pointercancel", () => this.#cancel(), options);
3429
+ viewport.addEventListener("keydown", (event) => {
3430
+ if (event.key === "Escape" && this.#drag) {
3431
+ event.preventDefault();
3432
+ this.#cancel();
3433
+ }
3434
+ }, options);
3435
+ return () => {
3436
+ ++this.#request;
3437
+ this.#cancel();
3438
+ events.abort();
3439
+ };
3440
+ }
3441
+ #start(event) {
3442
+ if (!this.options.active() || event.button !== 0 || this.#drag) return;
3443
+ const state = this.controller.state;
3444
+ const page = state.document?.pages[state.pageIndex];
3445
+ const svg = this.viewport.querySelector("svg.paper");
3446
+ if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
3447
+ const start = pagePoint(svg, page, event);
3448
+ if (!start) return;
3449
+ event.preventDefault();
3450
+ const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
3451
+ rect.classList.add("draw-preview");
3452
+ rect.setAttribute("vector-effect", "non-scaling-stroke");
3453
+ svg.append(rect);
3454
+ this.#drag = {
3455
+ pointer: event.pointerId,
3456
+ svg,
3457
+ page,
3458
+ ...start,
3459
+ rect
3460
+ };
3461
+ this.viewport.setPointerCapture?.(event.pointerId);
3462
+ this.#update(start);
3463
+ }
3464
+ #move(event) {
3465
+ if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
3466
+ const point = pagePoint(this.#drag.svg, this.#drag.page, event);
3467
+ if (point) this.#update(point);
3468
+ }
3469
+ #update(point) {
3470
+ const { x, y, rect } = this.#drag;
3471
+ rect.setAttribute("x", String(Math.min(x, point.x)));
3472
+ rect.setAttribute("y", String(Math.min(y, point.y)));
3473
+ rect.setAttribute("width", String(Math.abs(point.x - x)));
3474
+ rect.setAttribute("height", String(Math.abs(point.y - y)));
3475
+ }
3476
+ #cancel() {
3477
+ this.#drag?.rect.remove();
3478
+ this.#drag = void 0;
3479
+ }
3480
+ async #finish(event) {
3481
+ const drag = this.#drag;
3482
+ if (!drag || event.pointerId !== drag.pointer) return;
3483
+ const end = pagePoint(drag.svg, drag.page, event) ?? drag;
3484
+ this.#cancel();
3485
+ const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
3486
+ if (width < MIN_SIZE || height < MIN_SIZE) {
3487
+ this.options.announce("Drag on the page to draw a rectangle.");
3488
+ return;
3489
+ }
3490
+ const state = this.controller.state;
3491
+ if (state.document?.pages[state.pageIndex] !== drag.page) return;
3492
+ const shapeId = nextShapeId(drag.page);
3493
+ const pageId = drag.page.id;
3494
+ const request = ++this.#request;
3495
+ try {
3496
+ await this.controller.applyEdits([{
3497
+ type: "create-rectangle",
3498
+ pageId,
3499
+ shapeId,
3500
+ x: (drag.x + end.x) / 2,
3501
+ y: drag.page.height - (drag.y + end.y) / 2,
3502
+ width,
3503
+ height
3504
+ }]);
3505
+ if (request !== this.#request) return;
3506
+ const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
3507
+ if (created) this.controller.selectShape({
3508
+ id: shapeId,
3509
+ name: created.name,
3510
+ pageId
3511
+ });
3512
+ this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
3513
+ } catch (error) {
3514
+ if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
3515
+ }
3516
+ }
3517
+ };
3518
+ //#endregion
3519
+ //#region src/viewer-commands.ts
3520
+ var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
3521
+ /**
3522
+ * Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
3523
+ * typed `ribbon-action` events and shortcuts build the same actions; both go through
3524
+ * `routeRibbonAction`. Document mutation stays in the controller and core.
3525
+ */
3526
+ var ViewerCommands = class {
3527
+ host;
3528
+ #tool = "pointer";
3529
+ #grid = false;
3530
+ #pending = 0;
3531
+ #draw;
3532
+ #targets;
3533
+ constructor(host) {
3534
+ this.host = host;
3535
+ this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
3536
+ active: () => this.#tool === "rectangle",
3537
+ announce: host.announce
3538
+ });
3539
+ this.#targets = {
3540
+ controller: host.controller,
3541
+ history: (key) => this.#history(key),
3542
+ deleteSelection: () => this.#delete(),
3543
+ setTool: (tool) => this.setTool(tool),
3544
+ toggleGrid: () => {
3545
+ this.#grid = !this.#grid;
3546
+ this.render(host.controller.state);
3547
+ },
3548
+ toggleFullscreen: () => this.#toggleFullscreen(),
3549
+ togglePane: host.togglePane,
3550
+ reveal: host.reveal,
3551
+ fit: host.fit,
3552
+ focusSearch: host.focusSearch
3553
+ };
3554
+ }
3555
+ get tool() {
3556
+ return this.#tool;
3557
+ }
3558
+ /** Run one typed action, exactly as a ribbon control or shortcut would. */
3559
+ run(action) {
3560
+ routeRibbonAction(this.#targets, action);
3561
+ }
3562
+ wire() {
3563
+ const { root, viewport } = this.host;
3564
+ const doc = root.ownerDocument;
3565
+ const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
3566
+ const options = { signal: events.signal };
3567
+ root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
3568
+ root.addEventListener("office-command", (event) => {
3569
+ if (event.detail?.command === "zoom-fit") this.run({
3570
+ type: "zoom",
3571
+ mode: "fit"
3572
+ });
3573
+ }, options);
3574
+ root.addEventListener("keydown", (event) => this.#shortcut(event), options);
3575
+ viewport.addEventListener("wheel", (event) => {
3576
+ if (!event.ctrlKey && !event.metaKey) return;
3577
+ event.preventDefault();
3578
+ const zoom = this.host.controller.state.zoom;
3579
+ this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
3580
+ }, {
3581
+ ...options,
3582
+ passive: false
3583
+ });
3584
+ viewport.addEventListener("pointerdown", () => {
3585
+ if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
3586
+ }, options);
3587
+ viewport.addEventListener("dblclick", (event) => {
3588
+ if (this.#tool !== "pointer") return;
3589
+ if (event.target?.closest?.("[data-shape-id]")) this.run({
3590
+ type: "reveal",
3591
+ panel: "edit",
3592
+ focusText: true
3593
+ });
3594
+ }, options);
3595
+ doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
3596
+ const disposeDraw = this.#draw.wire();
3597
+ return () => {
3598
+ ++this.#pending;
3599
+ events.abort();
3600
+ disposeDraw();
3601
+ };
3602
+ }
3603
+ setTool(tool) {
3604
+ if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
3605
+ this.#tool = tool;
3606
+ this.render(this.host.controller.state);
3607
+ }
3608
+ #canEdit(state) {
3609
+ return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
3610
+ }
3611
+ #history(key) {
3612
+ const { controller } = this.host;
3613
+ const { edit, loading } = controller.state;
3614
+ if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
3615
+ }
3616
+ #delete() {
3617
+ const state = this.host.controller.state;
3618
+ const shape = state.selectedShape;
3619
+ const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
3620
+ if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
3621
+ this.#edit(() => this.host.controller.applyEdits([{
3622
+ type: "delete-shape",
3623
+ pageId,
3624
+ shapeId: shape.id
3625
+ }]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
3626
+ }
3627
+ async #edit(action, success) {
3628
+ const request = ++this.#pending;
3629
+ const { root, viewport } = this.host;
3630
+ const canvasFocused = viewport.contains(root.activeElement);
3631
+ try {
3632
+ await action();
3633
+ if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
3634
+ if (success && request === this.#pending) this.host.announce(success);
3635
+ } catch (error) {
3636
+ if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
3637
+ }
3638
+ }
3639
+ #toggleFullscreen() {
3640
+ const host = this.host.root.host;
3641
+ const doc = this.host.root.ownerDocument;
3642
+ (doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
3643
+ }
3644
+ /** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
3645
+ #shortcutAction(event) {
3646
+ const state = this.host.controller.state;
3647
+ const control = event.ctrlKey || event.metaKey;
3648
+ const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
3649
+ if (control && key === "f") return { type: "search" };
3650
+ if (control && event.shiftKey && key === "w") return {
3651
+ type: "zoom",
3652
+ mode: "fit"
3653
+ };
3654
+ if (control && (key === "PageDown" || key === "PageUp")) return {
3655
+ type: "page",
3656
+ step: key === "PageDown" ? 1 : -1
3657
+ };
3658
+ if (key === "F5" && !control) return { type: "fullscreen" };
3659
+ if (editable(event.target)) return void 0;
3660
+ if (control && !event.shiftKey && key === "z") return {
3661
+ type: "history",
3662
+ key: "undo"
3663
+ };
3664
+ if (control && (key === "y" || event.shiftKey && key === "z")) return {
3665
+ type: "history",
3666
+ key: "redo"
3667
+ };
3668
+ if (control && key === "1") return {
3669
+ type: "tool",
3670
+ tool: "pointer"
3671
+ };
3672
+ if (control && key === "8") return {
3673
+ type: "tool",
3674
+ tool: "rectangle"
3675
+ };
3676
+ if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
3677
+ if (!control && key === "F2" && state.document) return {
3678
+ type: "reveal",
3679
+ panel: "edit",
3680
+ focusText: true
3681
+ };
3682
+ if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
3683
+ type: "tool",
3684
+ tool: "pointer"
3685
+ };
3686
+ }
3687
+ #shortcut(event) {
3688
+ if (event.defaultPrevented || event.isComposing || event.altKey) return;
3689
+ const action = this.#shortcutAction(event);
3690
+ if (!action) return;
3691
+ event.preventDefault();
3692
+ this.run(action);
3693
+ }
3694
+ render(state) {
3695
+ const { root, viewport } = this.host;
3696
+ const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
3697
+ const editing = this.#canEdit(state);
3698
+ if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
3699
+ const page = state.document?.pages[state.pageIndex];
3700
+ button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
3701
+ button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
3702
+ button("delete").disabled = !editing || !state.selectedShape;
3703
+ button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
3704
+ const rectangle = button("rectangle");
3705
+ rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
3706
+ rectangle.disabled = !editing || !page;
3707
+ rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
3708
+ button("grid").setAttribute("pressed", String(this.#grid));
3709
+ button("grid").disabled = !page;
3710
+ for (const name of [
3711
+ "zoom-fit",
3712
+ "page-width",
3713
+ "actual-size"
3714
+ ]) button(name).disabled = !page;
3715
+ const fullscreen = button("fullscreen");
3716
+ const host = root.host;
3717
+ fullscreen.disabled = typeof host.requestFullscreen !== "function";
3718
+ fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
3719
+ viewport.dataset.tool = this.#tool;
3720
+ viewport.dataset.grid = String(this.#grid);
3721
+ }
3722
+ };
3723
+ //#endregion
3724
+ //#region src/office-ui.ts
3725
+ /**
3726
+ * Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
3727
+ * there instead when another Office product could use it.
3728
+ */
3729
+ var VISIO_ICONS = {
3730
+ visioLayers: "m2.5 7 7.5-4 7.5 4-7.5 4ZM2.5 10.5l7.5 4 7.5-4M2.5 14l7.5 4 7.5-4",
3731
+ visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
3732
+ visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
3733
+ visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
3734
+ };
3735
+ /** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
3736
+ function registerViewerControls(registry) {
3737
+ for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
3738
+ registerControls(registry);
3739
+ }
3740
+ //#endregion
3741
+ //#region src/export-svg.ts
3085
3742
  var MAX_SVG_EXPORT_BYTES = 16777216;
3086
3743
  var APPROXIMATION = "Static SVG export is a rendered approximation, not an editable VSDX round-trip or a guarantee of Microsoft Visio fidelity. Fonts are not embedded.";
3087
3744
  var INVALID_XML = "Invalid XML characters were replaced with the Unicode replacement character in the SVG export.";
@@ -3251,7 +3908,7 @@ function assertSerializedBudget(root, maxBytes) {
3251
3908
  }
3252
3909
  }
3253
3910
  //#endregion
3254
- //#region .package-build/output/src/snapshot-scene.js
3911
+ //#region src/snapshot-scene.ts
3255
3912
  /** Copy only named scalar fields, never arbitrary host properties or their getters. */
3256
3913
  function fields(source, keys) {
3257
3914
  const result = {};
@@ -3467,7 +4124,7 @@ function copySnapshotScene(model) {
3467
4124
  };
3468
4125
  }
3469
4126
  //#endregion
3470
- //#region .package-build/output/src/print-snapshot.js
4127
+ //#region src/print-snapshot.ts
3471
4128
  /** Application guardrails, not printer capabilities or Microsoft Visio limits. */
3472
4129
  var PRINT_SNAPSHOT_LIMITS = Object.freeze({
3473
4130
  maxPages: 32,
@@ -3684,14 +4341,17 @@ function validationCost(model) {
3684
4341
  };
3685
4342
  }
3686
4343
  //#endregion
3687
- //#region .package-build/output/src/viewer-element.js
4344
+ //#region src/viewer-element.ts
3688
4345
  var BaseElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
3689
4346
  var VisioViewerElement = class extends BaseElement {
4347
+ controller = new ViewerController(createWorkerParser());
3690
4348
  #root;
3691
4349
  #disposeInputs;
3692
4350
  #viewport;
3693
- #pageSelect;
3694
- #zoomLabel;
4351
+ #zoomSlider;
4352
+ #shapeStatus;
4353
+ #announcement;
4354
+ #commands;
3695
4355
  #status;
3696
4356
  #diagnostics;
3697
4357
  #toolbar;
@@ -3705,38 +4365,33 @@ var VisioViewerElement = class extends BaseElement {
3705
4365
  #notesPanel;
3706
4366
  #inspector;
3707
4367
  #inspectedShape;
3708
- #renderedSelection;
4368
+ #renderedSelection = null;
3709
4369
  #unsubscribe;
3710
4370
  #eventUnsubscribe;
3711
- #document;
3712
- #renderedPage;
3713
- #disposed;
3714
- #suspended;
3715
- #renderWarnings;
3716
- #disposeRenderer;
4371
+ #document = null;
4372
+ #renderedPage = -1;
4373
+ #disposed = false;
4374
+ #suspended = false;
4375
+ #renderWarnings = [];
4376
+ #disposeRenderer = () => {};
3717
4377
  #fontEvents;
3718
- #fontsChanged;
3719
- constructor() {
3720
- super();
3721
- this.controller = new ViewerController(createWorkerParser());
3722
- this.#renderedSelection = null;
4378
+ #fontsChanged = () => {
4379
+ if (this.#disposed) return;
3723
4380
  this.#document = null;
3724
4381
  this.#renderedPage = -1;
3725
- this.#disposed = false;
3726
- this.#suspended = false;
3727
- this.#renderWarnings = [];
3728
- this.#disposeRenderer = () => {};
3729
- this.#fontsChanged = () => {
3730
- if (this.#disposed) return;
3731
- this.#document = null;
3732
- this.#renderedPage = -1;
3733
- this.#render(this.controller.state);
3734
- };
4382
+ this.#render(this.controller.state);
4383
+ };
4384
+ constructor() {
4385
+ super();
3735
4386
  this.#root = this.attachShadow({ mode: "open" });
3736
- this.#root.innerHTML = `<style>${viewerStyles}</style>${viewerChromeTemplate}`;
4387
+ registerViewerControls();
4388
+ this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
4389
+ const workspace = this.#root.querySelector(".workspace");
4390
+ workspace.before(createRibbon(document));
4391
+ workspace.after(createPageTabs(document), createStatusBar(document));
3737
4392
  this.#viewport = this.#root.querySelector(".viewport");
3738
- this.#pageSelect = this.#root.querySelector("select");
3739
- this.#zoomLabel = this.#root.querySelector("output");
4393
+ this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
4394
+ this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
3740
4395
  this.#status = this.#root.querySelector("[data-status]");
3741
4396
  this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
3742
4397
  this.#toolbar = this.#root.querySelector(".toolbar");
@@ -3747,6 +4402,23 @@ var VisioViewerElement = class extends BaseElement {
3747
4402
  this.#notesPanel = this.#root.querySelector(".notes");
3748
4403
  this.#inspector = this.#root.querySelector(".shape-inspector");
3749
4404
  this.#chrome = new ViewerChrome(this.#root, this.controller);
4405
+ this.#commands = new ViewerCommands({
4406
+ root: this.#root,
4407
+ viewport: this.#viewport,
4408
+ controller: this.controller,
4409
+ fit: (mode) => this.#fit(mode),
4410
+ togglePane: (pane) => this.#chrome.togglePane(pane),
4411
+ reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
4412
+ focusSearch: () => {
4413
+ this.#chrome.showTab("home");
4414
+ this.#search.input.focus();
4415
+ this.#search.input.select();
4416
+ },
4417
+ announce: (message) => {
4418
+ this.#announcement = message;
4419
+ this.#status.textContent = message;
4420
+ }
4421
+ });
3750
4422
  this.#disposeInputs = this.#wireInputs();
3751
4423
  this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
3752
4424
  this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
@@ -3820,13 +4492,24 @@ var VisioViewerElement = class extends BaseElement {
3820
4492
  }
3821
4493
  fit() {
3822
4494
  this.#assertAlive();
4495
+ this.#fit("page");
4496
+ }
4497
+ /** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
4498
+ #fit(mode) {
3823
4499
  const page = this.document?.pages[this.pageIndex];
3824
4500
  if (!page) return;
3825
4501
  const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
3826
4502
  const padding = (value) => Number.parseFloat(value ?? "") || 0;
3827
4503
  const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
3828
4504
  const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
3829
- this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
4505
+ if (mode === "page") {
4506
+ this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
4507
+ return;
4508
+ }
4509
+ const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
4510
+ const fitted = width / (page.width * 96);
4511
+ const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
4512
+ this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
3830
4513
  }
3831
4514
  setLayerVisibility(pageId, layerId, visible) {
3832
4515
  this.#assertAlive();
@@ -3896,16 +4579,18 @@ var VisioViewerElement = class extends BaseElement {
3896
4579
  }
3897
4580
  #wireInputs() {
3898
4581
  const disposeChrome = this.#chrome.wire();
4582
+ const disposeCommands = this.#commands.wire();
3899
4583
  const disposeEdit = this.#edit.wire();
3900
4584
  const disposeLayers = wireLayerControls(this.#layers, this.controller);
3901
4585
  const disposeInputs = wireViewerInputs({
3902
4586
  viewport: this.#viewport,
3903
4587
  commandRoot: this.#root,
3904
- pageSelect: this.#pageSelect,
4588
+ zoomSlider: this.#zoomSlider,
3905
4589
  searchInput: this.#search.input
3906
- }, this.controller, () => this.fit());
4590
+ }, this.controller, (mode) => this.#fit(mode));
3907
4591
  return () => {
3908
4592
  disposeChrome();
4593
+ disposeCommands();
3909
4594
  disposeInputs();
3910
4595
  disposeLayers();
3911
4596
  disposeEdit();
@@ -3924,14 +4609,7 @@ var VisioViewerElement = class extends BaseElement {
3924
4609
  this.#document = state.document;
3925
4610
  this.#renderedPage = state.pageIndex;
3926
4611
  this.#renderedLayerOverrides = state.layerVisibilityOverrides;
3927
- this.#pageSelect.replaceChildren();
3928
- for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
3929
- const option = document.createElement("option");
3930
- option.value = String(index);
3931
- option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
3932
- this.#pageSelect.append(option);
3933
- }
3934
- this.#pageSelect.value = String(state.pageIndex);
4612
+ this.#announcement = void 0;
3935
4613
  this.#renderWarnings = [];
3936
4614
  if (state.document && page) {
3937
4615
  const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
@@ -3958,6 +4636,7 @@ var VisioViewerElement = class extends BaseElement {
3958
4636
  if (svg && page) {
3959
4637
  svg.style.width = `${page.width * 96 * state.zoom}px`;
3960
4638
  svg.style.height = `${page.height * 96 * state.zoom}px`;
4639
+ svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
3961
4640
  }
3962
4641
  const searchResult = state.search.results[state.search.activeIndex];
3963
4642
  const selection = state.selectedShape;
@@ -3985,16 +4664,20 @@ var VisioViewerElement = class extends BaseElement {
3985
4664
  this.#renderedSelection = selection ? { ...selection } : null;
3986
4665
  }
3987
4666
  if (!searchResult) this.#revealedSearchResult = void 0;
3988
- this.#zoomLabel.value = `${Math.round(state.zoom * 100)}%`;
3989
- this.#pageSelect.disabled = !page;
4667
+ this.#zoomSlider.value = Math.round(state.zoom * 100);
4668
+ this.#zoomSlider.disabled = !page;
4669
+ const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
4670
+ const inches = (value) => `${+value.toFixed(3)} in`;
4671
+ this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
3990
4672
  for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
3991
4673
  renderSearchControls(this.#search, state);
3992
4674
  this.#edit.render(state);
3993
4675
  if (changed) renderLayerControls(this.#layers, state);
3994
4676
  else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
3995
4677
  this.#chrome.render(state, this.#notes.children.length);
4678
+ this.#commands.render(state);
3996
4679
  this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
3997
- this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
4680
+ this.#status.textContent = state.loading ? "Opening diagram…" : state.edit.busy ? "Updating diagram…" : state.error?.message ?? (state.edit.error ? `Edit rejected: ${editErrorMessage(state.edit.error)}` : void 0) ?? this.#announcement ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
3998
4681
  const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
3999
4682
  this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
4000
4683
  }
@@ -4004,7 +4687,7 @@ function registerVisioViewer() {
4004
4687
  if (!customElements.get("visio-viewer")) customElements.define("visio-viewer", VisioViewerElement);
4005
4688
  }
4006
4689
  //#endregion
4007
- //#region .package-build/output/src/binding.js
4690
+ //#region src/binding.ts
4008
4691
  /** One client-only lifecycle adapter; framework wrappers forward to this implementation. */
4009
4692
  function mountViewer(container, initial = {}) {
4010
4693
  registerVisioViewer();
@@ -4105,7 +4788,7 @@ function mountViewer(container, initial = {}) {
4105
4788
  };
4106
4789
  }
4107
4790
  //#endregion
4108
- //#region .package-build/output/packages/bindings/src/common.js
4791
+ //#region packages/bindings/src/common.ts
4109
4792
  /** One mapping for every property and event, shared by every framework adapter. */
4110
4793
  function viewerOptions(props) {
4111
4794
  const result = { events: props.events ?? {} };
@@ -4204,13 +4887,30 @@ function viewerHandle(current) {
4204
4887
  }
4205
4888
  };
4206
4889
  }
4890
+ function viewerStateSource(current) {
4891
+ const controller = () => {
4892
+ try {
4893
+ return current()?.controller;
4894
+ } catch {
4895
+ return;
4896
+ }
4897
+ };
4898
+ return {
4899
+ subscribe(onChange) {
4900
+ return controller()?.subscribe(() => onChange()) ?? (() => {});
4901
+ },
4902
+ getSnapshot() {
4903
+ return controller()?.state ?? null;
4904
+ }
4905
+ };
4906
+ }
4207
4907
  /** Native framework emits and callback-map props share one complete event inventory. */
4208
4908
  function withEventEmitter(props, emit) {
4209
4909
  const options = viewerOptions(props);
4210
4910
  const callbacks = {};
4211
4911
  for (const name of eventKeys) Object.assign(callbacks, { [name]: (value) => {
4212
4912
  try {
4213
- props.events?.[name]?.(value);
4913
+ (props.events?.[name])?.(value);
4214
4914
  } finally {
4215
4915
  emit(name, value);
4216
4916
  }
@@ -4221,14 +4921,15 @@ function withEventEmitter(props, emit) {
4221
4921
  };
4222
4922
  }
4223
4923
  //#endregion
4224
- //#region .package-build/output/packages/bindings/src/angular.js
4225
- var __decorate = function(decorators, target, key, desc) {
4924
+ //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
4925
+ function __decorate(decorators, target, key, desc) {
4226
4926
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4227
4927
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4228
4928
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
4229
4929
  return c > 3 && r && Object.defineProperty(target, key, r), r;
4230
- };
4231
- /** Angular input/output and lifecycle wiring over the one shared browser binding. */
4930
+ }
4931
+ //#endregion
4932
+ //#region packages/bindings/src/angular.ts
4232
4933
  var VisioViewerComponent = class VisioViewerComponent {
4233
4934
  constructor() {
4234
4935
  this.documentLoad = new EventEmitter();
@@ -4239,6 +4940,9 @@ var VisioViewerComponent = class VisioViewerComponent {
4239
4940
  this.shapeSelect = new EventEmitter();
4240
4941
  this.host = inject(ElementRef);
4241
4942
  this.handle = viewerHandle(() => this.binding);
4943
+ this.stopState = () => {};
4944
+ this.viewerState = signal(null);
4945
+ this.state = this.viewerState.asReadonly();
4242
4946
  this.outputs = {
4243
4947
  "document-load": this.documentLoad,
4244
4948
  "document-change": this.documentChange,
@@ -4255,11 +4959,17 @@ var VisioViewerComponent = class VisioViewerComponent {
4255
4959
  }
4256
4960
  ngAfterViewInit() {
4257
4961
  this.binding = mountFrameworkViewer(this.host.nativeElement, this.options());
4962
+ const source = viewerStateSource(() => this.handle);
4963
+ this.viewerState.set(source.getSnapshot());
4964
+ this.stopState = source.subscribe(() => this.viewerState.set(source.getSnapshot()));
4258
4965
  }
4259
4966
  ngOnChanges() {
4260
4967
  this.binding?.update(this.options());
4261
4968
  }
4262
4969
  ngOnDestroy() {
4970
+ this.stopState();
4971
+ this.stopState = () => {};
4972
+ this.viewerState.set(null);
4263
4973
  const mounted = this.binding;
4264
4974
  this.binding = void 0;
4265
4975
  mounted?.destroy();