visio-solid-viewer 0.4.0 → 0.5.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,6 +1,7 @@
1
1
  import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, loadVisio, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
2
- import { attachKeyTips, registerControls } from "ooxml-ui/controls";
2
+ import { attachKeyTips, readOfficeProfile, registerControls, sanitizeOfficeProfile, writeOfficeProfile } from "ooxml-ui/controls";
3
3
  import { getIcon, registerIcon } from "ooxml-ui/icons";
4
+ import { definePresence } from "ooxml-ui/presence";
4
5
  import { className, effect, style, template, use } from "solid-js/web";
5
6
  import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
6
7
  export * from "visio-core";
@@ -1073,6 +1074,13 @@ var ViewerController = class {
1073
1074
  async undo() {
1074
1075
  return this.#mutate("undo");
1075
1076
  }
1077
+ /**
1078
+ * Adopt a package from a shared session as one undoable history step. Emits `document-change`
1079
+ * with kind `remote`, which a session must not publish back.
1080
+ */
1081
+ async applyRemoteSource(bytes) {
1082
+ return this.#mutate("remote", void 0, bytes);
1083
+ }
1076
1084
  async redo() {
1077
1085
  return this.#mutate("redo");
1078
1086
  }
@@ -1096,13 +1104,13 @@ var ViewerController = class {
1096
1104
  #sourceEditState() {
1097
1105
  return this.#sourceFormat === "vsdx" && this.#state.document?.format === "vsdx" ? this.#history?.state ?? EMPTY_EDIT_STATE : EMPTY_EDIT_STATE;
1098
1106
  }
1099
- async #mutate(kind, commands) {
1107
+ async #mutate(kind, commands, remote) {
1100
1108
  this.#assertAlive();
1101
1109
  const history = this.#history;
1102
1110
  if (!history || this.#sourceFormat !== "vsdx" || this.#state.document?.format !== "vsdx") throw new Error("Load a VSDX file before editing. Model-only documents are read only.");
1103
1111
  if (this.#state.loading || this.#state.edit.busy) throw new Error("Another document operation is in progress.");
1104
1112
  const target = kind === "undo" ? history.undoTarget : kind === "redo" ? history.redoTarget : void 0;
1105
- if (kind !== "edit" && !target) return;
1113
+ if ((kind === "undo" || kind === "redo") && !target) return;
1106
1114
  const id = ++this.#editId;
1107
1115
  const loadId = this.#loadId;
1108
1116
  const current = () => !this.#destroyed && id === this.#editId && loadId === this.#loadId && history === this.#history;
@@ -1122,6 +1130,9 @@ var ViewerController = class {
1122
1130
  assertCurrent();
1123
1131
  if (edited.bytes.byteLength > 33554432) throw new Error("The modified VSDX exceeds 32 MiB.");
1124
1132
  document = edited.document;
1133
+ } else if (kind === "remote") {
1134
+ if (remote.byteLength > 33554432) throw new Error("The shared VSDX exceeds 32 MiB.");
1135
+ document = await this.parser(Uint8Array.from(remote));
1125
1136
  } else document = await this.parser(Uint8Array.from(target.bytes));
1126
1137
  assertCurrent();
1127
1138
  assertViewableDocument(document);
@@ -1136,6 +1147,7 @@ var ViewerController = class {
1136
1147
  const selection = this.#state.selectedShape;
1137
1148
  const selectedShape = selection && visibleSelection(document, this.#state.pageIndex, selection, visible) ? selection : null;
1138
1149
  if (edited) history.append(edited.bytes, edited.diagnostics);
1150
+ else if (kind === "remote") history.append(Uint8Array.from(remote), []);
1139
1151
  else history.move(target);
1140
1152
  this.#visible = visible;
1141
1153
  this.#searchIndex = searchIndex;
@@ -1374,28 +1386,10 @@ function selection(target) {
1374
1386
  }
1375
1387
  /** Own every DOM listener and abort them together when the surface is disposed. */
1376
1388
  function wireViewerInputs(controls, controller, fit) {
1377
- const { viewport, commandRoot, zoomSlider, searchInput } = controls;
1389
+ const { viewport, zoomSlider } = controls;
1378
1390
  const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
1379
1391
  const options = { signal: events.signal };
1380
- searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
1381
- searchInput.addEventListener("keydown", (event) => {
1382
- if (event.isComposing) return;
1383
- if (event.key === "Enter") {
1384
- event.preventDefault();
1385
- if (event.shiftKey) controller.previousSearchResult();
1386
- else controller.nextSearchResult();
1387
- }
1388
- if (event.key === "Escape") {
1389
- event.preventDefault();
1390
- controller.setSearchQuery("");
1391
- }
1392
- }, options);
1393
1392
  zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
1394
- commandRoot.addEventListener("click", (event) => {
1395
- const action = event.target?.closest?.("button")?.dataset.action;
1396
- if (action === "search-next") controller.nextSearchResult();
1397
- if (action === "search-previous") controller.previousSearchResult();
1398
- }, options);
1399
1393
  viewport.addEventListener("click", (event) => controller.selectShape(selection(targetShape$1(event))), options);
1400
1394
  viewport.addEventListener("keydown", (event) => {
1401
1395
  const target = targetShape$1(event);
@@ -1536,22 +1530,12 @@ button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-vis
1536
1530
  button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
1537
1531
  .toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
1538
1532
  .spacer { flex:1; }
1539
- .ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
1540
- .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; }
1541
- .ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
1542
- .ribbon-tabs button[aria-selected="true"]::after { content:""; position:absolute; inset:auto 0 -1px; height:2.5px; background:var(--_vv-accent); }
1543
1533
  .ribbon-content { min-height:82px; display:flex; align-items:stretch; gap:0; padding:2px 4px; overflow-x:auto; }
1544
1534
  .ribbon-group { display:flex; flex-direction:column; justify-content:space-between; flex:none; gap:2px; padding:3px 10px 4px; border-right:1px solid var(--_vv-border); }
1545
1535
  .group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
1546
1536
  .ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
1547
1537
  .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; }
1548
1538
  .ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
1549
- .search-group { width:330px; }
1550
- .search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
1551
- .search-controls label { display:flex; align-items:center; min-width:0; gap:6px; flex:1; font-size:11px; }
1552
- .search-controls input { width:100%; min-width:0; padding:3px 6px; font-size:11px; }
1553
- .search-controls button { padding:3px 6px; font-size:10px; background:var(--_vv-secondary); }
1554
- .search-controls [role="status"] { display:block; width:100%; min-height:15px; font-size:10px; line-height:15px; color:var(--_vv-muted); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
1555
1539
  .workspace { position:relative; display:flex; flex:1; min-width:0; min-height:0; overflow:hidden; }
1556
1540
  .pane-heading { height:37px; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; border-bottom:1px solid var(--_vv-border); font-size:11px; font-weight:600; }
1557
1541
  .inspector-kind { color:var(--_vv-muted); font-size:10px; font-weight:400; }
@@ -1610,17 +1594,8 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
1610
1594
  button,select,input,textarea { min-height:44px; }
1611
1595
  button { min-width:44px; }
1612
1596
  summary { min-height:44px; line-height:44px; }
1613
- .ribbon-tabs { height:44px; }
1614
- .ribbon-tabs button { min-height:44px; padding:8px 16px; }
1615
1597
  .ribbon-content { min-height:86px; }
1616
1598
  .ribbon-group { padding:4px 8px; }
1617
- .search-group { width:100%; border-right:0; }
1618
- .search-controls { gap:4px; }
1619
- .search-controls label { font-size:12px; }
1620
- .search-controls input { font-size:12px; }
1621
- .search-controls button { min-width:44px; font-size:0; padding:3px; }
1622
- .search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
1623
- .search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
1624
1599
  #home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
1625
1600
  .ribbon-command { min-width:90px; }
1626
1601
  .workspace { display:block; overflow:auto; }
@@ -1639,26 +1614,17 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
1639
1614
  button,select,input,textarea { min-height:44px; }
1640
1615
  button { min-width:44px; }
1641
1616
  summary,.layer-controls label { min-height:44px; line-height:44px; }
1642
- .ribbon-tabs { min-height:44px; height:44px; }
1643
- .ribbon-tabs button { min-height:44px; }
1644
1617
  .ribbon-content { min-height:96px; }
1645
- .search-group { width:380px; max-width:100%; }
1646
1618
  .notes-strip button { min-height:44px; height:44px; }
1647
1619
  .layer-controls input { min-height:20px; }
1648
1620
  }
1649
1621
  /* Suite ribbon: tabs lead directly into Visio command groups. */
1650
- .ribbon-tabs { height:40px; }
1651
- .ribbon-tabs button { min-height:39px; padding:8px 16px; }
1652
1622
  .ribbon-content { min-height:104px; padding:4px 4px; }
1653
- .search-group { order:2; width:320px; }
1654
1623
  .ribbon-hint { order:3; padding:10px 18px; }
1655
1624
  .ribbon-command { min-width:82px; }
1656
1625
  .pane-close { display:none; }
1657
1626
  @media(max-width:760px) {
1658
- .ribbon-tabs { height:44px; }
1659
- .ribbon-tabs button { min-height:44px; }
1660
1627
  .ribbon-content { min-height:54px; padding:4px 0; }
1661
- .search-group { flex:0 0 100%; width:100%; min-height:54px; }
1662
1628
  .search-feedback,.group-label { display:none; }
1663
1629
  .ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
1664
1630
  .workspace { display:flex; position:relative; overflow:hidden; }
@@ -1694,12 +1660,10 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
1694
1660
  .ribbon-tools .ribbon-group { display:flex; width:100%; min-height:64px; border:0; padding:0; }
1695
1661
  .ribbon-tools .ribbon-actions { justify-content:space-between; }
1696
1662
  .ribbon-tools .ribbon-command { min-width:88px; font-size:12px; }
1697
- .ribbon-tools .search-group { order:1; flex:auto; }
1698
1663
  .ribbon-tools .search-feedback { display:block; }
1699
1664
  .status { flex-wrap:nowrap; gap:4px; min-height:48px; }
1700
1665
  .status .notes-strip { display:none; }
1701
1666
  }
1702
- @media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
1703
1667
  @media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
1704
1668
  `;
1705
1669
  //#endregion
@@ -1709,9 +1673,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
1709
1673
  * `styles.ts` string moves into this directory when it is next changed.
1710
1674
  */
1711
1675
  var canvasAndRibbonStyles = [
1712
- "/*\n * Microsoft Visio ribbon layout on the shared ooxml-ui controls. Viewer theme tokens feed the\n * shared --office-* tokens, so host themes still apply. Loaded after the legacy styles.ts.\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\n/* Tab row: Quick Access Toolbar, then Visio's tabs. */\n.ribbon-head {\n display: flex;\n align-items: stretch;\n min-width: 0;\n border-bottom: 1px solid var(--_vv-border);\n}\n.qat {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 6px;\n border-right: 1px solid var(--_vv-border);\n}\n.qat office-ui-button {\n --office-target-size: 30px;\n}\n.ribbon-head .ribbon-tabs {\n flex: 0 1 auto;\n min-width: 0;\n height: 36px;\n border-bottom: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.ribbon-head .ribbon-tabs button {\n flex: none;\n min-height: 35px;\n padding: 6px 12px;\n font-size: 13px;\n}\n\n/* Panels: groups of large commands and three-row stacks, as dense as Visio's. */\n.ribbon-content {\n min-height: 96px;\n padding: 2px 2px 0;\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.tools-content office-ui-ribbon-group {\n min-width: 0;\n}\noffice-ui-ribbon-group office-ui-button,\noffice-ui-ribbon-group office-ui-menu-button {\n --office-target-size: 24px;\n}\noffice-ui-ribbon-group [data-size='large'] {\n --office-target-size: 30px;\n}\noffice-ui-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 66px;\n padding: 2px 6px;\n font-size: 12px;\n}\noffice-ui-menu-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 50px;\n font-size: 12px;\n}\noffice-ui-ribbon-group [data-size='small']::part(button) {\n justify-content: flex-start;\n font-size: 12px;\n}\noffice-ui-ribbon-stack:not([orientation]) {\n justify-content: flex-start;\n padding-top: 2px;\n}\noffice-ui-ribbon-stack:not([orientation]) > * {\n min-height: 24px;\n}\noffice-ui-ribbon-group > office-ui-ribbon-stack > office-ui-ribbon-stack[orientation] {\n min-height: 32px;\n}\noffice-ui-select[data-combo] {\n --office-target-size: 24px;\n}\noffice-ui-menu-button[data-active]::part(button) {\n background: var(--_vv-accent-soft);\n box-shadow: inset 0 0 0 1px var(--_vv-accent);\n}\n[data-unsupported] {\n cursor: not-allowed;\n}\n.ribbon-check {\n display: flex;\n align-items: center;\n gap: 6px;\n min-height: 24px;\n padding: 0 6px;\n font-size: 12px;\n white-space: nowrap;\n cursor: pointer;\n}\n.ribbon-check[data-unsupported] {\n color: var(--_vv-muted);\n opacity: 0.6;\n}\n.ribbon-hint {\n display: none;\n}\n\n/* Find bar: Visio opens Find on demand instead of keeping a box in the ribbon. */\n.find-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: 40px;\n padding: 4px 8px 4px 12px;\n border-bottom: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.find-bar .search-group {\n flex: 1;\n width: auto;\n max-width: 640px;\n padding: 0;\n border: 0;\n}\n.find-bar .search-group .group-label {\n display: none;\n}\n.find-bar .search-controls {\n flex-wrap: nowrap;\n}\n.find-bar [role='status'] {\n width: auto;\n flex: 1;\n}\n.find-close {\n margin-left: auto;\n width: 28px;\n padding: 0;\n border-color: transparent;\n background: transparent;\n font-size: 18px;\n}\n\n/* Drawing window with optional rulers. */\n.canvas-area {\n display: grid;\n flex: 1;\n min-width: 0;\n min-height: 0;\n grid-template-columns: 0 1fr;\n grid-template-rows: 0 1fr;\n}\n.canvas-area[data-ruler='true'] {\n grid-template-columns: 18px 1fr;\n grid-template-rows: 18px 1fr;\n}\n.canvas-area > .viewport {\n grid-column: 2;\n grid-row: 2;\n}\n.ruler,\n.ruler-corner {\n display: none;\n background: var(--_vv-surface);\n}\n.canvas-area[data-ruler='true'] > .ruler,\n.canvas-area[data-ruler='true'] > .ruler-corner {\n display: block;\n}\n.ruler-corner {\n grid-column: 1;\n grid-row: 1;\n border-right: 1px solid var(--_vv-border);\n border-bottom: 1px solid var(--_vv-border);\n}\n.ruler-h {\n grid-column: 2;\n grid-row: 1;\n width: 100%;\n height: 18px;\n border-bottom: 1px solid var(--_vv-border);\n}\n.ruler-v {\n grid-column: 1;\n grid-row: 2;\n width: 18px;\n height: 100%;\n border-right: 1px solid var(--_vv-border);\n}\n\n.page-bar {\n display: flex;\n flex: none;\n align-items: stretch;\n min-width: 0;\n border-top: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.page-bar office-ui-menu-button {\n --office-target-size: 28px;\n flex: none;\n padding: 0 2px;\n}\n.page-tabs {\n flex: 1;\n min-width: 0;\n border-top: 0;\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\n@media (max-width: 760px) {\n .ribbon-row {\n flex: 1;\n min-width: 0;\n }\n .qat office-ui-button {\n --office-target-size: 44px;\n }\n .ribbon-tools .tools-content office-ui-ribbon-group {\n width: 100%;\n }\n .find-bar {\n flex-wrap: wrap;\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/* Visio's Tell me box sits right after the tabs. */\n.tell-me {\n flex: 0 1 280px;\n min-width: 140px;\n margin: 0 8px;\n --office-target-size: 28px;\n}\n.presentation-mode {\n --office-target-size: 24px;\n}\n@media (max-width: 760px) {\n .tell-me {\n display: none;\n }\n}\n",
1676
+ "/*\n * Microsoft Visio ribbon layout on the shared ooxml-ui controls. Viewer theme tokens feed the\n * shared --office-* tokens, so host themes still apply. Loaded after the legacy styles.ts.\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\n/* Tab row: Quick Access Toolbar, then Visio's tabs. */\n.ribbon-head {\n display: flex;\n align-items: stretch;\n min-width: 0;\n border-bottom: 1px solid var(--_vv-border);\n}\n.qat {\n display: flex;\n align-items: center;\n gap: 0;\n padding: 0 6px;\n border-right: 1px solid var(--_vv-border);\n}\n.qat office-ui-button {\n --office-target-size: 30px;\n}\n.ribbon-head .ribbon-tabs {\n flex: 0 1 auto;\n min-width: 0;\n height: 36px;\n border-bottom: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.ribbon-head .ribbon-tabs button {\n flex: none;\n min-height: 35px;\n padding: 6px 12px;\n font-size: 13px;\n}\n\n/* Panels: groups of large commands and three-row stacks, as dense as Visio's. */\n.ribbon-content {\n min-height: 96px;\n padding: 2px 2px 0;\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.tools-content office-ui-ribbon-group {\n min-width: 0;\n}\noffice-ui-ribbon-group office-ui-button,\noffice-ui-ribbon-group office-ui-menu-button {\n --office-target-size: 24px;\n}\noffice-ui-ribbon-group [data-size='large'] {\n --office-target-size: 30px;\n}\noffice-ui-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 66px;\n padding: 2px 6px;\n font-size: 12px;\n}\noffice-ui-menu-button[variant='stacked']::part(button) {\n min-width: 48px;\n min-height: 50px;\n font-size: 12px;\n}\noffice-ui-ribbon-group [data-size='small']::part(button) {\n justify-content: flex-start;\n font-size: 12px;\n}\noffice-ui-ribbon-stack:not([orientation]) {\n justify-content: flex-start;\n padding-top: 2px;\n}\noffice-ui-ribbon-stack:not([orientation]) > * {\n min-height: 24px;\n}\noffice-ui-ribbon-group > office-ui-ribbon-stack > office-ui-ribbon-stack[orientation] {\n min-height: 32px;\n}\noffice-ui-select[data-combo] {\n --office-target-size: 24px;\n}\noffice-ui-menu-button[data-active]::part(button) {\n background: var(--_vv-accent-soft);\n box-shadow: inset 0 0 0 1px var(--_vv-accent);\n}\n[data-unsupported] {\n cursor: not-allowed;\n}\n.ribbon-check {\n display: flex;\n align-items: center;\n gap: 6px;\n min-height: 24px;\n padding: 0 6px;\n font-size: 12px;\n white-space: nowrap;\n cursor: pointer;\n}\n.ribbon-check[data-unsupported] {\n color: var(--_vv-muted);\n opacity: 0.6;\n}\n.ribbon-hint {\n display: none;\n}\n\n/* Drawing window with optional rulers. */\n.canvas-area {\n display: grid;\n flex: 1;\n min-width: 0;\n min-height: 0;\n grid-template-columns: 0 1fr;\n grid-template-rows: 0 1fr;\n}\n.canvas-area[data-ruler='true'] {\n grid-template-columns: 18px 1fr;\n grid-template-rows: 18px 1fr;\n}\n.canvas-area > .viewport {\n grid-column: 2;\n grid-row: 2;\n}\n.ruler,\n.ruler-corner {\n display: none;\n background: var(--_vv-surface);\n}\n.canvas-area[data-ruler='true'] > .ruler,\n.canvas-area[data-ruler='true'] > .ruler-corner {\n display: block;\n}\n.ruler-corner {\n grid-column: 1;\n grid-row: 1;\n border-right: 1px solid var(--_vv-border);\n border-bottom: 1px solid var(--_vv-border);\n}\n/* The shared office-ui-ruler draws ticks, labels and its own edge. */\n.ruler-h {\n grid-column: 2;\n grid-row: 1;\n}\n.ruler-v {\n grid-column: 1;\n grid-row: 2;\n}\n\n.page-bar {\n display: flex;\n flex: none;\n align-items: stretch;\n min-width: 0;\n border-top: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.page-bar office-ui-menu-button {\n --office-target-size: 28px;\n flex: none;\n padding: 0 2px;\n}\n.page-tabs {\n flex: 1;\n min-width: 0;\n border-top: 0;\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\n@media (max-width: 760px) {\n .ribbon-row {\n flex: 1;\n min-width: 0;\n }\n .qat office-ui-button {\n --office-target-size: 44px;\n }\n .ribbon-tools .tools-content office-ui-ribbon-group {\n width: 100%;\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/* Visio's Tell me box sits right after the tabs. */\n.tell-me {\n flex: 0 1 280px;\n min-width: 140px;\n margin: 0 8px;\n --office-target-size: 28px;\n}\n.presentation-mode {\n --office-target-size: 24px;\n}\n@media (max-width: 760px) {\n .tell-me {\n display: none;\n }\n}\n",
1713
1677
  "/* Visio's Shapes window: stencil views, More/Quick Shapes rows and a two-column master grid. */\n.shapes-pane {\n display: flex;\n flex: 0 0 232px;\n flex-direction: column;\n width: 232px;\n min-width: 0;\n overflow: hidden;\n border-right: 1px solid var(--_vv-border);\n background: var(--_vv-secondary);\n}\n.shapes-pane .pane-heading {\n height: 42px;\n font-size: 18px;\n font-weight: 400;\n color: var(--_vv-accent);\n}\n.pane-collapse {\n min-height: 28px;\n width: 28px;\n padding: 0;\n border-color: transparent;\n background: transparent;\n font-size: 18px;\n line-height: 1;\n}\n.shapes-views {\n display: flex;\n gap: 14px;\n padding: 4px 12px 0;\n border-bottom: 1px solid var(--_vv-border);\n}\n.shapes-views button {\n position: relative;\n min-height: 28px;\n padding: 2px 0 6px;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--_vv-muted);\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.shapes-views button[aria-selected='true'] {\n color: var(--_vv-ink);\n}\n.shapes-views button[aria-selected='true']::after {\n content: '';\n position: absolute;\n inset: auto 0 -1px;\n height: 2px;\n background: var(--_vv-accent);\n}\n#shapes-stencils,\n#shapes-search {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 4px 0 8px;\n}\n.shapes-row {\n display: flex;\n align-items: center;\n width: 100%;\n min-height: 30px;\n padding: 0 12px;\n border: 0;\n border-radius: 0;\n background: transparent;\n font-size: 12px;\n text-align: left;\n}\n.shapes-row::after {\n content: '›';\n margin-left: auto;\n font-size: 15px;\n}\n.stencil-title {\n margin: 2px 0;\n padding: 6px 12px;\n border-left: 3px solid var(--_vv-accent);\n background: var(--_vv-accent-soft);\n font-size: 12px;\n font-weight: 600;\n}\n.masters {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2px;\n margin: 0;\n padding: 4px 6px;\n list-style: none;\n}\n.masters li[hidden] {\n display: none;\n}\n.master {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n min-height: 40px;\n padding: 4px 6px;\n border-color: transparent;\n background: transparent;\n font-size: 11px;\n line-height: 1.2;\n text-align: left;\n cursor: grab;\n}\n.master svg {\n flex: none;\n width: 28px;\n height: 28px;\n fill: var(--_vv-surface);\n stroke: var(--_vv-accent);\n stroke-width: 1.2;\n stroke-linejoin: round;\n}\n.master[data-unsupported] {\n cursor: not-allowed;\n opacity: 0.45;\n}\n.master[data-unsupported]:hover {\n border-color: transparent;\n background: transparent;\n}\n.shapes-search-field {\n display: block;\n width: calc(100% - 24px);\n margin: 6px 12px;\n}\n.shapes-empty {\n margin: 8px 12px;\n color: var(--_vv-muted);\n font-size: 12px;\n}\n@media (max-width: 760px) {\n .shapes-pane {\n position: absolute;\n inset: 0 auto 0 0;\n z-index: 3;\n width: min(260px, 100%);\n box-shadow: 8px 0 24px #0002;\n }\n .master {\n min-height: 44px;\n }\n}\n.shapes-strip {\n display: flex;\n flex: 0 0 28px;\n flex-direction: column;\n align-items: center;\n gap: 10px;\n width: 28px;\n min-width: 28px;\n padding: 8px 0;\n border: 0;\n border-right: 1px solid var(--_vv-border);\n border-radius: 0;\n background: var(--_vv-secondary);\n color: var(--_vv-accent);\n font-size: 16px;\n}\n.shapes-strip[hidden] {\n display: none;\n}\n.shapes-strip-label {\n writing-mode: vertical-rl;\n color: var(--_vv-ink);\n font-size: 12px;\n}\n/* Visio's floating Pan & Zoom window. */\n.pan-zoom {\n position: absolute;\n right: 16px;\n bottom: 16px;\n z-index: 5;\n width: 240px;\n border: 1px solid var(--_vv-border);\n background: var(--_vv-surface);\n box-shadow: 0 6px 20px rgb(0 0 0 / 18%);\n}\n.pan-zoom[hidden] {\n display: none;\n}\n.pan-zoom-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 2px 4px 2px 10px;\n border-bottom: 1px solid var(--_vv-border);\n font-size: 12px;\n font-weight: 600;\n}\n.pan-zoom-close {\n min-height: 24px;\n width: 24px;\n padding: 0;\n border-color: transparent;\n background: transparent;\n}\n.pan-zoom-view {\n display: grid;\n place-items: center;\n height: 180px;\n padding: 8px;\n background: var(--_vv-bg);\n cursor: crosshair;\n touch-action: none;\n}\n.pan-zoom-view svg {\n max-width: 100%;\n max-height: 100%;\n background: #fff;\n box-shadow: 0 1px 3px rgb(0 0 0 / 20%);\n}\n.pan-zoom-frame {\n fill: rgb(220 38 38 / 8%);\n stroke: #dc2626;\n stroke-width: 2px;\n}\n",
1714
- "/* Visio's File tab and backstage. */\n:host {\n position: relative;\n}\n.file-tab {\n flex: none;\n min-height: 35px;\n margin: 0 4px 0 0;\n padding: 6px 16px;\n border: 0;\n border-radius: 0;\n background: var(--_vv-accent);\n color: var(--vv-accent-ink, #fff);\n font-size: 13px;\n}\n.file-tab:hover:enabled {\n background: color-mix(in srgb, var(--_vv-accent) 85%, #000);\n color: var(--vv-accent-ink, #fff);\n}\n.backstage {\n position: absolute;\n inset: 0;\n z-index: 30;\n display: flex;\n min-width: 0;\n background: var(--_vv-surface);\n}\n.backstage[hidden] {\n display: none;\n}\n.backstage-nav {\n display: flex;\n flex: 0 0 200px;\n flex-direction: column;\n padding: 12px 0;\n overflow: auto;\n background: var(--_vv-accent);\n color: var(--vv-accent-ink, #fff);\n}\n.backstage-nav button {\n border: 0;\n border-radius: 0;\n background: transparent;\n color: inherit;\n text-align: left;\n}\n.backstage-back {\n width: 36px;\n min-height: 36px;\n margin: 0 0 12px 16px;\n padding: 0;\n border: 1.5px solid currentColor !important;\n border-radius: 50% !important;\n font-size: 18px;\n text-align: center !important;\n}\n.backstage-item {\n min-height: 38px;\n padding: 6px 24px;\n font-size: 14px;\n}\n.backstage-item:hover:enabled,\n.backstage-item[aria-current] {\n background: color-mix(in srgb, #000 22%, transparent) !important;\n color: inherit;\n}\n.backstage-item:disabled {\n opacity: 0.55;\n}\n.backstage-footer {\n display: flex;\n flex-direction: column;\n margin-top: auto;\n padding-top: 16px;\n}\n.backstage-body {\n flex: 1;\n min-width: 0;\n overflow: auto;\n padding: 28px 40px;\n}\n.backstage-page h1 {\n margin: 0 0 24px;\n font-size: 34px;\n font-weight: 300;\n}\n.backstage-page h2 {\n margin: 20px 0 8px;\n font-size: 16px;\n font-weight: 600;\n}\n.backstage-file {\n margin-top: 0 !important;\n font-size: 20px !important;\n font-weight: 400 !important;\n overflow-wrap: anywhere;\n}\n.backstage-facts {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 8px 24px;\n max-width: 520px;\n margin: 0;\n font-size: 13px;\n}\n.backstage-facts dt {\n color: var(--_vv-muted);\n}\n.backstage-facts dd {\n margin: 0;\n overflow-wrap: anywhere;\n}\n.backstage-action {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: min(420px, 100%);\n min-height: 64px;\n margin: 0 0 10px;\n padding: 10px 16px;\n text-align: left;\n background: var(--_vv-surface);\n}\n.backstage-action strong {\n font-size: 14px;\n font-weight: 600;\n}\n.backstage-action span,\n.backstage-muted,\n.backstage-note-count {\n color: var(--_vv-muted);\n font-size: 12px;\n}\n.backstage-templates {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n}\n.backstage-templates .backstage-action,\n.backstage-templates ::slotted(*) {\n width: 200px;\n min-height: 120px;\n}\n.backstage-print-preview {\n max-width: 420px;\n padding: 12px;\n border: 1px solid var(--_vv-border);\n background: #fff;\n}\n.backstage-print-preview svg {\n display: block;\n width: 100%;\n height: auto;\n}\n@media (max-width: 760px), (pointer: coarse) {\n .file-tab {\n min-height: 44px;\n min-width: 52px;\n }\n}\n@media (max-width: 760px) {\n .backstage {\n flex-direction: column;\n }\n .backstage-nav {\n flex: none;\n flex-direction: row;\n flex-wrap: wrap;\n padding: 6px;\n }\n .backstage-back {\n margin: 4px;\n }\n .backstage-item {\n min-height: 44px;\n padding: 6px 12px;\n }\n .backstage-footer {\n flex-direction: row;\n margin-top: 0;\n padding-top: 0;\n }\n .backstage-body {\n padding: 16px;\n }\n}\n",
1678
+ "/* Visio's File tab and backstage. */\n:host {\n position: relative;\n}\n/* The shared office-ui-backstage draws the File column; these style Visio's page content. */\n.backstage {\n --office-accent: var(--_vv-accent);\n --office-accent-foreground: var(--vv-accent-ink, #fff);\n}\n.backstage-page h1 {\n margin: 0 0 24px;\n font-size: 34px;\n font-weight: 300;\n}\n.backstage-page h2 {\n margin: 20px 0 8px;\n font-size: 16px;\n font-weight: 600;\n}\n.backstage-file {\n margin-top: 0 !important;\n font-size: 20px !important;\n font-weight: 400 !important;\n overflow-wrap: anywhere;\n}\n.backstage-facts {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 8px 24px;\n max-width: 520px;\n margin: 0;\n font-size: 13px;\n}\n.backstage-facts dt {\n color: var(--_vv-muted);\n}\n.backstage-facts dd {\n margin: 0;\n overflow-wrap: anywhere;\n}\n.backstage-action {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 2px;\n width: min(420px, 100%);\n min-height: 64px;\n margin: 0 0 10px;\n padding: 10px 16px;\n text-align: left;\n background: var(--_vv-surface);\n}\n.backstage-action strong {\n font-size: 14px;\n font-weight: 600;\n}\n.backstage-action span,\n.backstage-muted,\n.backstage-note-count {\n color: var(--_vv-muted);\n font-size: 12px;\n}\n.backstage-templates {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n}\n.backstage-templates .backstage-action,\n.backstage-templates ::slotted(*) {\n width: 200px;\n min-height: 120px;\n}\n/* Account: the shared profile editor, then Visio's product information. */\n.backstage-page office-ui-account {\n max-width: 560px;\n}\n.backstage-product {\n margin-top: 24px;\n}\n/* Share with people: a live session between windows of this browser. */\n.share-panel {\n max-width: 560px;\n margin-bottom: 16px;\n}\n.share-panel h2 {\n margin-top: 0 !important;\n}\n.share-field {\n display: grid;\n gap: 4px;\n max-width: 320px;\n margin: 12px 0;\n font-size: 13px;\n}\n.share-room {\n min-height: 30px;\n padding: 2px 8px;\n border: 1px solid var(--_vv-border);\n border-radius: 3px;\n background: var(--_vv-surface);\n color: inherit;\n font: inherit;\n}\n.share-buttons button {\n min-height: 32px;\n padding: 0 16px;\n}\n.share-buttons button[hidden] {\n display: none;\n}\n.share-status {\n min-height: 20px;\n font-size: 13px;\n}\n",
1715
1679
  "/*\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"
1716
1680
  ].join("\n");
1717
1681
  //#endregion
@@ -2828,8 +2792,7 @@ function ribbonCommands(root) {
2828
2792
  const commands = [];
2829
2793
  const context = (node) => {
2830
2794
  const group = node.closest("office-ui-ribbon-group")?.getAttribute("label") ?? "";
2831
- const panel = node.closest(".ribbon-content");
2832
- return [panel ? root.querySelector(`[aria-controls="${panel.id}"]`)?.textContent ?? "" : "", group].filter(Boolean).join(" › ");
2795
+ return [node.closest("[data-ribbon-tab]")?.dataset.label ?? "", group].filter(Boolean).join(" › ");
2833
2796
  };
2834
2797
  for (const node of root.querySelectorAll(".toolbar [command]")) {
2835
2798
  const id = node.getAttribute("command");
@@ -2961,50 +2924,32 @@ function quickAccess(doc) {
2961
2924
  return qat;
2962
2925
  }
2963
2926
  /**
2964
- * The Visio ribbon: Quick Access Toolbar, tab list and one panel per tab. Tab switching and
2965
- * keyboard movement belong to the chrome; commands emit `ribbon-action` events for the router.
2927
+ * The Visio ribbon on the shared `office-ui-ribbon`: Quick Access Toolbar, File, one panel per
2928
+ * tab and Tell me. The shared element owns the tab row, selection and arrow-key movement;
2929
+ * commands emit `ribbon-action` events for the router.
2966
2930
  */
2967
2931
  function createRibbon(doc) {
2968
- const toolbar = doc.createElement("div");
2969
- toolbar.className = "toolbar";
2970
- toolbar.setAttribute("role", "group");
2971
- toolbar.setAttribute("aria-label", "Diagram controls");
2972
- const head = doc.createElement("div");
2973
- head.className = "ribbon-head";
2974
- const tabs = doc.createElement("div");
2975
- tabs.className = "ribbon-tabs";
2976
- tabs.setAttribute("role", "tablist");
2977
- tabs.setAttribute("aria-label", "Ribbon");
2978
- const file = doc.createElement("button");
2979
- file.type = "button";
2980
- file.className = "file-tab";
2981
- file.textContent = "File";
2982
- file.setAttribute("aria-haspopup", "dialog");
2983
- file.setAttribute("aria-expanded", "false");
2984
- head.append(quickAccess(doc), file, tabs, createTellMe(doc));
2985
- toolbar.append(head);
2932
+ const ribbon = doc.createElement("office-ui-ribbon");
2933
+ ribbon.className = "toolbar";
2934
+ ribbon.setAttribute("role", "group");
2935
+ ribbon.setAttribute("aria-label", "Diagram controls");
2936
+ ribbon.setAttribute("label", "Ribbon");
2937
+ ribbon.setAttribute("selected", "home");
2938
+ const qat = quickAccess(doc);
2939
+ qat.slot = "quick-access";
2940
+ const tellMe = createTellMe(doc);
2941
+ tellMe.slot = "search";
2942
+ ribbon.append(qat, tellMe);
2986
2943
  for (const [key, name, build] of RIBBON_TABS) {
2987
- const selected = key === "home";
2988
- const tab = doc.createElement("button");
2989
- tab.type = "button";
2990
- tab.id = `${key}-tab`;
2991
- tab.dataset.tab = key;
2992
- tab.textContent = name;
2993
- tab.setAttribute("role", "tab");
2994
- tab.setAttribute("aria-selected", String(selected));
2995
- tab.setAttribute("aria-controls", `${key}-panel`);
2996
- if (!selected) tab.tabIndex = -1;
2997
2944
  const panel = doc.createElement("div");
2998
2945
  panel.className = "ribbon-content";
2999
2946
  panel.id = `${key}-panel`;
3000
- panel.setAttribute("role", "tabpanel");
3001
- panel.setAttribute("aria-labelledby", tab.id);
3002
- panel.hidden = !selected;
2947
+ panel.dataset.ribbonTab = key;
2948
+ panel.dataset.label = name;
3003
2949
  build(doc, panel);
3004
- tabs.append(tab);
3005
- toolbar.append(panel);
2950
+ ribbon.append(panel);
3006
2951
  }
3007
- return toolbar;
2952
+ return ribbon;
3008
2953
  }
3009
2954
  //#endregion
3010
2955
  //#region src/ribbon-keytips.ts
@@ -3110,13 +3055,13 @@ function generated(label, used, singles) {
3110
3055
  }
3111
3056
  /** Annotate the ribbon for KeyTips. Labels and ids are static application strings. */
3112
3057
  function applyKeyTips(toolbar) {
3113
- toolbar.querySelector(".file-tab").dataset.keytip = "F";
3058
+ toolbar.setAttribute("file-keytip", "F");
3114
3059
  toolbar.querySelector(".tell-me").dataset.keytip = "Q";
3115
3060
  toolbar.querySelector(".qat [command=\"undo\"]").dataset.keytip = "1";
3116
3061
  toolbar.querySelector(".qat [command=\"redo\"]").dataset.keytip = "2";
3117
- for (const tab of toolbar.querySelectorAll("[data-tab]")) {
3118
- tab.dataset.keytip = TABS[tab.dataset.tab] ?? tab.dataset.tab.slice(0, 1).toUpperCase();
3119
- tab.dataset.keytipPanel = `${tab.dataset.tab}-panel`;
3062
+ for (const panel of toolbar.querySelectorAll("[data-ribbon-tab]")) {
3063
+ const tab = panel.dataset.ribbonTab;
3064
+ panel.dataset.tabKeytip = TABS[tab] ?? tab.slice(0, 1).toUpperCase();
3120
3065
  }
3121
3066
  for (const panel of toolbar.querySelectorAll(".ribbon-content")) {
3122
3067
  panel.dataset.keytipLevel = "";
@@ -3221,98 +3166,42 @@ function createPageTabs(doc) {
3221
3166
  }
3222
3167
  //#endregion
3223
3168
  //#region src/viewer-search.ts
3224
- /** Builds the Find group: a search field, previous/next result buttons and a live status. */
3225
- function createSearchGroup(doc) {
3226
- const group = doc.createElement("div");
3227
- group.className = "ribbon-group search-group";
3228
- group.setAttribute("role", "group");
3229
- group.setAttribute("aria-label", "Find");
3230
- const controls = doc.createElement("div");
3231
- controls.className = "search-controls";
3232
- controls.setAttribute("role", "search");
3233
- controls.setAttribute("aria-label", "Diagram text search");
3234
- const label = doc.createElement("label");
3235
- label.append("Find ");
3236
- const input = doc.createElement("input");
3237
- input.type = "search";
3238
- input.setAttribute("aria-label", "Search diagram text");
3239
- input.placeholder = "Text across pages";
3240
- input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
3241
- input.autocomplete = "off";
3242
- input.spellcheck = false;
3243
- input.setAttribute("aria-describedby", "search-status");
3244
- label.append(input);
3245
- const step = (action, name, text) => {
3246
- const button = doc.createElement("button");
3247
- button.type = "button";
3248
- button.dataset.action = action;
3249
- button.setAttribute("aria-label", name);
3250
- button.textContent = text;
3251
- return button;
3252
- };
3253
- const status = doc.createElement("span");
3254
- status.id = "search-status";
3255
- status.setAttribute("role", "status");
3256
- status.setAttribute("aria-live", "polite");
3257
- status.setAttribute("aria-atomic", "true");
3258
- controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
3259
- const caption = doc.createElement("span");
3260
- caption.className = "group-label";
3261
- caption.textContent = "Find in drawing";
3262
- group.append(controls, caption);
3263
- return group;
3264
- }
3265
3169
  /**
3266
- * Visio opens Find from Home > Editing rather than keeping a box in the ribbon. The find bar
3267
- * sits under the ribbon, hidden until Find... or Ctrl+F, and closes with its button or Escape.
3170
+ * Visio opens Find from Home > Editing rather than keeping a box in the ribbon. The shared
3171
+ * `office-ui-find-bar` sits under the ribbon, hidden until Find... or Ctrl+F; Enter and
3172
+ * Shift+Enter step through matching shapes and Escape clears, then closes.
3268
3173
  */
3269
3174
  function createFindBar(doc) {
3270
- const bar = doc.createElement("div");
3175
+ const bar = doc.createElement("office-ui-find-bar");
3271
3176
  bar.className = "find-bar";
3272
- bar.hidden = true;
3273
- const close = doc.createElement("button");
3274
- close.type = "button";
3275
- close.className = "find-close";
3276
- close.dataset.action = "find-close";
3277
- close.setAttribute("aria-label", "Close Find");
3278
- close.title = "Close Find (Esc)";
3279
- close.textContent = "×";
3280
- bar.append(createSearchGroup(doc), close);
3177
+ bar.setAttribute("label", "Diagram text search");
3178
+ bar.setAttribute("input-label", "Search diagram text");
3179
+ bar.setAttribute("placeholder", "Text across pages");
3180
+ bar.setAttribute("maxlength", String(TEXT_SEARCH_LIMITS.queryCharacters));
3181
+ bar.setAttribute("previous-label", "Previous matching shape");
3182
+ bar.setAttribute("next-label", "Next matching shape");
3281
3183
  return bar;
3282
3184
  }
3283
- /** The find bar closes from its button, or Escape once the query is already empty. */
3284
- function wireFindBar(bar, input, closed) {
3185
+ /** Route the find bar's queries and steps to the controller; `closed` restores focus. */
3186
+ function wireFindBar(bar, controller, closed) {
3285
3187
  const events = new ((bar.ownerDocument.defaultView?.AbortController) ?? AbortController)();
3286
- const close = () => {
3287
- bar.hidden = true;
3288
- closed();
3289
- };
3290
- bar.addEventListener("click", (event) => {
3291
- if (event.target.closest?.("[data-action=\"find-close\"]")) close();
3292
- }, { signal: events.signal });
3293
- bar.addEventListener("keydown", (event) => {
3294
- if (event.key === "Escape" && input.value === "") close();
3295
- }, {
3296
- signal: events.signal,
3297
- capture: true
3298
- });
3188
+ const options = { signal: events.signal };
3189
+ bar.addEventListener("office-find-input", (event) => controller.setSearchQuery(event.detail.query), options);
3190
+ bar.addEventListener("office-find-step", (event) => {
3191
+ const { direction } = event.detail;
3192
+ if (direction === "next") controller.nextSearchResult();
3193
+ else controller.previousSearchResult();
3194
+ }, options);
3195
+ bar.addEventListener("office-find-close", closed, options);
3299
3196
  return () => events.abort();
3300
3197
  }
3301
- function searchControls(root) {
3302
- return {
3303
- input: root.querySelector("input[type=\"search\"]"),
3304
- status: root.querySelector("#search-status"),
3305
- previous: root.querySelector("[data-action=\"search-previous\"]"),
3306
- next: root.querySelector("[data-action=\"search-next\"]")
3307
- };
3308
- }
3309
- function renderSearchControls(controls, state) {
3310
- if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
3311
- controls.input.disabled = !state.document?.pages.length;
3312
- controls.previous.disabled = controls.next.disabled = !state.search.results.length;
3313
- controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
3198
+ function renderFindBar(bar, state) {
3199
+ bar.value = state.search.query;
3200
+ bar.toggleAttribute("disabled", !state.document?.pages.length);
3201
+ bar.toggleAttribute("navigation-disabled", !state.search.results.length);
3202
+ bar.status = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
3314
3203
  const result = state.search.results[state.search.activeIndex];
3315
- controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
3204
+ bar.statusTitle = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
3316
3205
  }
3317
3206
  //#endregion
3318
3207
  //#region src/viewer-fit.ts
@@ -3606,6 +3495,17 @@ var BACKSTAGE_FOOTER = [
3606
3495
  ["feedback", "Feedback"],
3607
3496
  ["options", "Options"]
3608
3497
  ];
3498
+ /** The navigation items, with optional disabled states (Save and Close follow the drawing). */
3499
+ function backstageItems(state = {}) {
3500
+ const item = (id, label, group) => ({
3501
+ id,
3502
+ label,
3503
+ ...group ? { group } : {},
3504
+ ...state[id]?.disabled ? { disabled: true } : {},
3505
+ ...state[id]?.title ? { title: state[id].title } : {}
3506
+ });
3507
+ return [...BACKSTAGE_ITEMS.map(([id, label]) => item(id, label)), ...BACKSTAGE_FOOTER.map(([id, label]) => item(id, label, "footer"))];
3508
+ }
3609
3509
  var LOCAL = "Documents stay in this browser; nothing is uploaded.";
3610
3510
  function el(doc, tag, className, text) {
3611
3511
  const node = doc.createElement(tag);
@@ -3629,7 +3529,6 @@ function action(doc, id, label, description, unsupported) {
3629
3529
  function page(doc, id, title, ...content) {
3630
3530
  const section = el(doc, "section", "backstage-page");
3631
3531
  section.dataset.backstagePage = id;
3632
- section.hidden = true;
3633
3532
  section.setAttribute("aria-labelledby", `backstage-${id}-title`);
3634
3533
  const heading = el(doc, "h1", "", title);
3635
3534
  heading.id = `backstage-${id}-title`;
@@ -3637,31 +3536,55 @@ function page(doc, id, title, ...content) {
3637
3536
  return section;
3638
3537
  }
3639
3538
  /**
3539
+ * Share with people: a live session (Yjs) between tabs and windows of this browser. The room name
3540
+ * is all a second window needs; nothing is uploaded.
3541
+ */
3542
+ function share(doc) {
3543
+ const panel = el(doc, "section", "share-panel");
3544
+ panel.setAttribute("aria-labelledby", "share-title");
3545
+ const title = el(doc, "h2", "", "Share with people");
3546
+ title.id = "share-title";
3547
+ const field = el(doc, "label", "share-field");
3548
+ const room = el(doc, "input", "share-room");
3549
+ room.type = "text";
3550
+ room.maxLength = 64;
3551
+ room.autocomplete = "off";
3552
+ room.spellcheck = false;
3553
+ field.append(el(doc, "span", "", "Session name"), room);
3554
+ const buttons = el(doc, "div", "share-buttons");
3555
+ const start = el(doc, "button", "share-start", "Start sharing");
3556
+ start.type = "button";
3557
+ start.dataset.share = "start";
3558
+ const stop = el(doc, "button", "share-stop", "Stop sharing");
3559
+ stop.type = "button";
3560
+ stop.dataset.share = "stop";
3561
+ stop.hidden = true;
3562
+ buttons.append(start, stop);
3563
+ const status = el(doc, "p", "share-status");
3564
+ status.setAttribute("role", "status");
3565
+ const people = doc.createElement("office-ui-presence");
3566
+ people.className = "share-people";
3567
+ people.setAttribute("label", "People in this session");
3568
+ panel.append(title, el(doc, "p", "", "Edit this drawing together with other windows of this browser. Open the same session name in another window to join."), field, buttons, status, people, el(doc, "p", "backstage-muted", "The whole drawing is shared; when two people change it at once, the last change wins. Nothing leaves this device."));
3569
+ return panel;
3570
+ }
3571
+ /** Visio's Account page: the shared Office profile editor and product information. */
3572
+ function account(doc) {
3573
+ const profile = doc.createElement("office-ui-account");
3574
+ const product = el(doc, "section", "backstage-product");
3575
+ product.append(el(doc, "h2", "", "Product Information"), el(doc, "p", "", "Visio viewer (beta). No sign-in or subscription is used."), el(doc, "p", "backstage-muted", LOCAL));
3576
+ profile.append(product);
3577
+ return profile;
3578
+ }
3579
+ /**
3640
3580
  * Visio's backstage: a full-window File view with a navigation column and one page per item.
3641
3581
  * Static application structure only; document values are filled in later as text.
3642
3582
  */
3643
3583
  function createBackstage(doc) {
3644
- const root = el(doc, "div", "backstage");
3645
- root.hidden = true;
3646
- root.setAttribute("role", "dialog");
3647
- root.setAttribute("aria-modal", "true");
3648
- root.setAttribute("aria-label", "File");
3649
- const nav = el(doc, "nav", "backstage-nav");
3650
- nav.setAttribute("aria-label", "File");
3651
- const back = el(doc, "button", "backstage-back", "←");
3652
- back.type = "button";
3653
- back.dataset.backstage = "back";
3654
- back.setAttribute("aria-label", "Back to the drawing");
3655
- back.title = "Back (Esc)";
3656
- const item = ([id, label]) => {
3657
- const button = el(doc, "button", "backstage-item", label);
3658
- button.type = "button";
3659
- button.dataset.backstageItem = id;
3660
- return button;
3661
- };
3662
- const footer = el(doc, "div", "backstage-footer");
3663
- footer.append(...BACKSTAGE_FOOTER.map(item));
3664
- nav.append(back, ...BACKSTAGE_ITEMS.map(item), footer);
3584
+ const root = doc.createElement("office-ui-backstage");
3585
+ root.className = "backstage";
3586
+ root.setAttribute("back-label", "Back to the drawing");
3587
+ root.items = backstageItems();
3665
3588
  const facts = el(doc, "dl", "backstage-facts");
3666
3589
  for (const [key, label] of [
3667
3590
  ["pages", "Pages"],
@@ -3687,67 +3610,61 @@ function createBackstage(doc) {
3687
3610
  input.accept = ".vsdx,.vsd";
3688
3611
  input.hidden = true;
3689
3612
  input.setAttribute("aria-label", "Choose a Visio drawing");
3690
- const preview = el(doc, "div", "backstage-print-preview");
3691
- preview.setAttribute("aria-label", "Print preview of the current page");
3692
- const body = el(doc, "div", "backstage-body");
3693
- body.append(page(doc, "info", "Info", file, facts, notes), page(doc, "new", "New", templates), page(doc, "open", "Open", action(doc, "open", "Browse", "Open a .vsdx drawing or a legacy .vsd preview from this device."), input, el(doc, "p", "backstage-muted", `Recent drawings are not tracked. ${LOCAL}`)), page(doc, "save-as", "Save As", action(doc, "download", "Download a copy", "Save the drawing, with your edits, as a .vsdx file."), action(doc, "export-svg", "Export the current page as SVG", "An approximate picture with compatibility notes.")), page(doc, "print", "Print", action(doc, "print", "Print", "Print the current page."), preview), page(doc, "share", "Share", action(doc, "share", "Share with people", "Invite people to the drawing.", LOCAL), action(doc, "email", "Email", "Send the drawing as an attachment.", LOCAL)), page(doc, "export", "Export", action(doc, "export-svg", "Export the current page as SVG", "An approximate picture with compatibility notes."), action(doc, "export-pdf", "Create PDF/XPS Document", "A fixed-layout document.", "Needs PDF output."), action(doc, "change-type", "Change File Type", "Save in another Visio format.", "Needs core format conversion.")), page(doc, "account", "Account", el(doc, "p", "backstage-muted", `No account is used. ${LOCAL}`)), page(doc, "feedback", "Feedback", el(doc, "p", "backstage-muted", "Feedback is not collected by this viewer.")), page(doc, "options", "Options", el(doc, "p", "backstage-muted", "Visio Options are not available in this viewer.")));
3694
- root.append(nav, body);
3613
+ const preview = doc.createElement("office-ui-print-preview");
3614
+ preview.className = "backstage-print-preview";
3615
+ preview.setAttribute("label", "Print preview of the current page");
3616
+ preview.setAttribute("empty-label", "Open a drawing to print it.");
3617
+ root.append(page(doc, "info", "Info", file, facts, notes), page(doc, "new", "New", templates), page(doc, "open", "Open", action(doc, "open", "Browse", "Open a .vsdx drawing or a legacy .vsd preview from this device."), input, el(doc, "p", "backstage-muted", `Recent drawings are not tracked. ${LOCAL}`)), page(doc, "save-as", "Save As", action(doc, "download", "Download a copy", "Save the drawing, with your edits, as a .vsdx file."), action(doc, "export-svg", "Export the current page as SVG", "An approximate picture with compatibility notes.")), page(doc, "print", "Print", action(doc, "print", "Print", "Print the current page."), preview), page(doc, "share", "Share", share(doc), action(doc, "email", "Email", "Send the drawing as an attachment.", LOCAL)), page(doc, "export", "Export", action(doc, "export-svg", "Export the current page as SVG", "An approximate picture with compatibility notes."), action(doc, "export-pdf", "Create PDF/XPS Document", "A fixed-layout document.", "Needs PDF output."), action(doc, "change-type", "Change File Type", "Save in another Visio format.", "Needs core format conversion.")), page(doc, "account", "Account", account(doc)), page(doc, "feedback", "Feedback", el(doc, "p", "backstage-muted", "Feedback is not collected by this viewer.")));
3695
3618
  return root;
3696
3619
  }
3697
3620
  //#endregion
3698
3621
  //#region src/viewer-backstage.ts
3699
- /**
3700
- * Visio's File backstage: opened from the File tab, closed with Back or Escape (focus returns to
3701
- * File). Open, Save, Save As, Export, Print and Close are real; the rest is shown disabled.
3702
- */
3703
3622
  var ViewerBackstage = class {
3704
3623
  host;
3705
3624
  #root;
3706
- #file;
3625
+ #ribbon;
3707
3626
  #input;
3708
3627
  #state;
3628
+ #items = "";
3709
3629
  #urls = /* @__PURE__ */ new Set();
3710
3630
  constructor(host) {
3711
3631
  this.host = host;
3712
- this.#root = host.root.querySelector(".backstage");
3713
- this.#file = host.root.querySelector(".file-tab");
3632
+ this.#root = host.root.querySelector("office-ui-backstage");
3633
+ this.#ribbon = host.root.querySelector("office-ui-ribbon");
3714
3634
  this.#input = this.#root.querySelector(".backstage-file-input");
3715
3635
  }
3716
3636
  get open() {
3717
- return !this.#root.hidden;
3637
+ return this.#root.open;
3718
3638
  }
3719
3639
  show(page = "info") {
3720
- this.#root.hidden = false;
3721
- this.#file.setAttribute("aria-expanded", "true");
3722
- this.#select(page);
3723
- this.#root.querySelector(`[data-backstage-item="${page}"]`)?.focus();
3640
+ this.#ribbon.setAttribute("file-expanded", "true");
3641
+ this.#root.show(page);
3642
+ if (page === "print") this.#preview();
3724
3643
  }
3725
3644
  hide() {
3726
- if (!this.open) return;
3727
- this.#root.hidden = true;
3728
- this.#file.setAttribute("aria-expanded", "false");
3729
- this.#file.focus();
3645
+ this.#root.close();
3730
3646
  }
3731
3647
  wire() {
3732
3648
  const events = new ((this.host.root.ownerDocument.defaultView?.AbortController) ?? AbortController)();
3733
3649
  const options = { signal: events.signal };
3734
- this.#file.addEventListener("click", () => this.open ? this.hide() : this.show(), options);
3650
+ this.#ribbon.addEventListener("office-ribbon-file", () => this.open ? this.hide() : this.show(), options);
3651
+ this.#root.addEventListener("office-backstage-select", (event) => {
3652
+ const id = event.detail.id;
3653
+ if (id === "save") this.#download();
3654
+ else if (id === "close") this.#close();
3655
+ else if (id === "options") {
3656
+ this.hide();
3657
+ this.host.showOptions();
3658
+ } else if (id === "print") this.#preview();
3659
+ }, options);
3660
+ this.#root.addEventListener("office-backstage-close", () => {
3661
+ this.#ribbon.setAttribute("file-expanded", "false");
3662
+ this.#ribbon.focusFile();
3663
+ }, options);
3735
3664
  this.#root.addEventListener("click", (event) => {
3736
- const target = event.target;
3737
- if (target.closest?.("[data-backstage=\"back\"]")) return this.hide();
3738
- const item = target.closest?.("[data-backstage-item]")?.dataset.backstageItem;
3739
- if (item === "save") return this.#download();
3740
- if (item === "close") return this.#close();
3741
- if (item) return this.#select(item);
3742
- const action = target.closest?.("[data-backstage-action]");
3665
+ const action = event.target.closest?.("[data-backstage-action]");
3743
3666
  if (action && !action.disabled) this.#run(action.dataset.backstageAction);
3744
3667
  }, options);
3745
- this.#root.addEventListener("keydown", (event) => {
3746
- if (event.key !== "Escape") return;
3747
- event.preventDefault();
3748
- event.stopPropagation();
3749
- this.hide();
3750
- }, options);
3751
3668
  this.#input.addEventListener("change", () => {
3752
3669
  const file = this.#input.files?.[0];
3753
3670
  this.#input.value = "";
@@ -3761,11 +3678,6 @@ var ViewerBackstage = class {
3761
3678
  this.#urls.clear();
3762
3679
  };
3763
3680
  }
3764
- #select(page) {
3765
- for (const item of this.#root.querySelectorAll("[data-backstage-item]")) item.toggleAttribute("aria-current", item.dataset.backstageItem === page);
3766
- for (const section of this.#root.querySelectorAll("[data-backstage-page]")) section.hidden = section.dataset.backstagePage !== page;
3767
- if (page === "print") this.#preview();
3768
- }
3769
3681
  #run(action) {
3770
3682
  if (action === "open") this.#input.click();
3771
3683
  else if (action === "download") this.#download();
@@ -3815,14 +3727,11 @@ var ViewerBackstage = class {
3815
3727
  this.host.announce(error instanceof Error ? error.message : String(error));
3816
3728
  }
3817
3729
  }
3818
- /** Preview is a DOM clone of the rendered page, never document markup parsed from text. */
3730
+ /** The shared print preview shows an inert clone of the rendered page, never parsed markup. */
3819
3731
  #preview() {
3820
- const target = this.#root.querySelector(".backstage-print-preview");
3732
+ const preview = this.#root.querySelector("office-ui-print-preview");
3821
3733
  const paper = this.host.viewport.querySelector("svg.paper");
3822
- target.replaceChildren(...paper ? [paper.cloneNode(true)] : []);
3823
- const clone = target.querySelector("svg");
3824
- clone?.removeAttribute("style");
3825
- clone?.querySelectorAll("[tabindex]").forEach((node) => node.removeAttribute("tabindex"));
3734
+ preview.pages = paper ? [paper] : [];
3826
3735
  }
3827
3736
  #print() {
3828
3737
  const doc = this.host.root.ownerDocument;
@@ -3866,13 +3775,24 @@ var ViewerBackstage = class {
3866
3775
  this.#root.querySelector(".backstage-note-count").textContent = notes ? `${notes} compatibility notes describe what this viewer approximates or omits.` : "No compatibility notes.";
3867
3776
  const busy = state.loading || state.edit.busy;
3868
3777
  const saveReason = state.document?.format === "vsd" ? "Legacy VSD drawings are read only here." : "Open a .vsdx file to save a copy.";
3869
- for (const node of this.#root.querySelectorAll("[data-backstage-action=\"download\"], [data-backstage-item=\"save\"]")) {
3870
- node.disabled = !state.edit.sourceAvailable || busy;
3871
- node.title = node.disabled ? saveReason : "";
3778
+ const saveDisabled = !state.edit.sourceAvailable || busy;
3779
+ const download = this.#root.querySelector("[data-backstage-action=\"download\"]");
3780
+ download.disabled = saveDisabled;
3781
+ download.title = saveDisabled ? saveReason : "";
3782
+ const items = backstageItems({
3783
+ save: {
3784
+ disabled: saveDisabled,
3785
+ ...saveDisabled ? { title: saveReason } : {}
3786
+ },
3787
+ close: { disabled: !state.document }
3788
+ });
3789
+ const key = JSON.stringify(items);
3790
+ if (key !== this.#items) {
3791
+ this.#items = key;
3792
+ this.#root.items = items;
3872
3793
  }
3873
3794
  for (const node of this.#root.querySelectorAll("[data-backstage-action=\"export-svg\"], [data-backstage-action=\"print\"]")) node.disabled = !page || busy;
3874
3795
  this.#root.querySelector("[data-backstage-action=\"notes\"]").disabled = notes === 0;
3875
- this.#root.querySelector("[data-backstage-item=\"close\"]").disabled = !state.document;
3876
3796
  }
3877
3797
  };
3878
3798
  //#endregion
@@ -4214,6 +4134,396 @@ var ViewerPanZoom = class {
4214
4134
  }
4215
4135
  };
4216
4136
  //#endregion
4137
+ //#region src/viewer-options.ts
4138
+ var disabled = (reason) => ({ disabled: `Not available yet. ${reason}` });
4139
+ /**
4140
+ * Visio's File > Options categories, in Visio's order. Only User name and Initials are honoured
4141
+ * today (they are the local profile, also used as the identity in a shared session); every other
4142
+ * setting is shown disabled with the capability it is waiting for.
4143
+ */
4144
+ var VISIO_OPTION_CATEGORIES = [
4145
+ {
4146
+ id: "general",
4147
+ label: "General",
4148
+ description: "General options for working with Visio.",
4149
+ sections: [{
4150
+ id: "ui",
4151
+ title: "User Interface options",
4152
+ controls: [
4153
+ {
4154
+ kind: "toggle",
4155
+ key: "miniToolbar",
4156
+ label: "Show Mini Toolbar on selection",
4157
+ ...disabled("Needs rich text formatting.")
4158
+ },
4159
+ {
4160
+ kind: "toggle",
4161
+ key: "livePreview",
4162
+ label: "Enable Live Preview",
4163
+ ...disabled("Needs Quick Styles and themes.")
4164
+ },
4165
+ {
4166
+ kind: "select",
4167
+ key: "screenTips",
4168
+ label: "ScreenTip style",
4169
+ choices: [{
4170
+ value: "descriptions",
4171
+ label: "Show feature descriptions in ScreenTips"
4172
+ }],
4173
+ ...disabled("ScreenTips always describe the command.")
4174
+ }
4175
+ ]
4176
+ }, {
4177
+ id: "personalize",
4178
+ title: "Personalize your copy of Microsoft Office",
4179
+ description: "Stored on this device only. Shared sessions show this name to others.",
4180
+ controls: [{
4181
+ kind: "text",
4182
+ key: "userName",
4183
+ label: "User name",
4184
+ maxLength: 64
4185
+ }, {
4186
+ kind: "text",
4187
+ key: "userInitials",
4188
+ label: "Initials",
4189
+ maxLength: 2
4190
+ }]
4191
+ }]
4192
+ },
4193
+ {
4194
+ id: "proofing",
4195
+ label: "Proofing",
4196
+ description: "Change how Visio corrects and formats your text.",
4197
+ disabled: "Not available yet. Spelling and AutoCorrect need rich text editing.",
4198
+ sections: []
4199
+ },
4200
+ {
4201
+ id: "save",
4202
+ label: "Save",
4203
+ description: "Customize how drawings are saved.",
4204
+ disabled: "Not available yet. Drawings are saved as downloaded copies; AutoRecover is not kept.",
4205
+ sections: []
4206
+ },
4207
+ {
4208
+ id: "language",
4209
+ label: "Language",
4210
+ description: "Set the Office Language Preferences.",
4211
+ disabled: "Not available yet. The viewer is English only.",
4212
+ sections: []
4213
+ },
4214
+ {
4215
+ id: "advanced",
4216
+ label: "Advanced",
4217
+ description: "Advanced options for working with Visio.",
4218
+ sections: [{
4219
+ id: "editing",
4220
+ title: "Editing options",
4221
+ controls: [{
4222
+ kind: "number",
4223
+ key: "undoLevels",
4224
+ label: "Number of undo levels",
4225
+ min: 1,
4226
+ max: 99,
4227
+ ...disabled("The history depth is fixed.")
4228
+ }, {
4229
+ kind: "toggle",
4230
+ key: "autoSizeText",
4231
+ label: "Auto-size text while editing",
4232
+ ...disabled("Needs core text layout.")
4233
+ }]
4234
+ }]
4235
+ },
4236
+ {
4237
+ id: "ribbon",
4238
+ label: "Customize Ribbon",
4239
+ description: "Customize the Ribbon and keyboard shortcuts.",
4240
+ disabled: "Not available yet. The ribbon layout is fixed.",
4241
+ sections: []
4242
+ },
4243
+ {
4244
+ id: "qat",
4245
+ label: "Quick Access Toolbar",
4246
+ description: "Customize the Quick Access Toolbar.",
4247
+ disabled: "Not available yet. The Quick Access Toolbar shows Undo and Redo.",
4248
+ sections: []
4249
+ },
4250
+ {
4251
+ id: "add-ins",
4252
+ label: "Add-ins",
4253
+ description: "View and manage Microsoft Office Add-ins.",
4254
+ disabled: "Not available. Add-ins and macros never run in this viewer.",
4255
+ sections: []
4256
+ },
4257
+ {
4258
+ id: "trust",
4259
+ label: "Trust Center",
4260
+ description: "Help keep your documents safe and your computer secure and healthy.",
4261
+ disabled: "Not available. Documents stay local and active content is never run.",
4262
+ sections: []
4263
+ }
4264
+ ];
4265
+ function profileValues(profile) {
4266
+ return {
4267
+ miniToolbar: false,
4268
+ livePreview: false,
4269
+ screenTips: "descriptions",
4270
+ undoLevels: 20,
4271
+ autoSizeText: false,
4272
+ userName: profile.displayName,
4273
+ userInitials: profile.initial ?? ""
4274
+ };
4275
+ }
4276
+ /** The Options dialog element. Built once and appended to the viewer's shadow root. */
4277
+ function createOptionsDialog(doc) {
4278
+ const dialog = doc.createElement("office-ui-options-dialog");
4279
+ dialog.setAttribute("heading", "Visio Options");
4280
+ dialog.categories = VISIO_OPTION_CATEGORIES;
4281
+ return dialog;
4282
+ }
4283
+ /**
4284
+ * File > Account and File > Options share one local profile. Edits in either are written to the
4285
+ * suite-wide profile store, mirrored into the other and reported through `onProfile`.
4286
+ */
4287
+ var ViewerProfile = class {
4288
+ onProfile;
4289
+ #profile = readOfficeProfile();
4290
+ #dialog;
4291
+ #account;
4292
+ constructor(root, onProfile = () => {}) {
4293
+ this.onProfile = onProfile;
4294
+ this.#dialog = root.querySelector("office-ui-options-dialog");
4295
+ this.#account = root.querySelector("office-ui-account");
4296
+ this.#sync();
4297
+ }
4298
+ get profile() {
4299
+ return { ...this.#profile };
4300
+ }
4301
+ /** Open Visio Options, on General as Visio does. */
4302
+ showOptions(category = "general") {
4303
+ this.#dialog.values = profileValues(this.#profile);
4304
+ this.#dialog.category = category;
4305
+ this.#dialog.show();
4306
+ }
4307
+ wire() {
4308
+ const events = new ((this.#dialog.ownerDocument.defaultView?.AbortController) ?? AbortController)();
4309
+ const options = { signal: events.signal };
4310
+ this.#dialog.addEventListener("office-options-change", (event) => {
4311
+ const { values } = event.detail;
4312
+ const initial = String(values.userInitials ?? "").trim();
4313
+ this.#update({
4314
+ displayName: String(values.userName ?? "").trim(),
4315
+ avatarColor: this.#profile.avatarColor,
4316
+ ...initial ? { initial } : {}
4317
+ });
4318
+ }, options);
4319
+ this.#account?.addEventListener("office-profile-change", (event) => this.#update(event.detail.profile), options);
4320
+ return () => events.abort();
4321
+ }
4322
+ #update(profile) {
4323
+ this.#profile = sanitizeOfficeProfile(profile);
4324
+ writeOfficeProfile(this.#profile);
4325
+ this.#sync();
4326
+ this.onProfile(this.profile);
4327
+ }
4328
+ #sync() {
4329
+ if (this.#account) this.#account.profile = this.#profile;
4330
+ this.#dialog.values = profileValues(this.#profile);
4331
+ }
4332
+ };
4333
+ //#endregion
4334
+ //#region src/viewer-share.ts
4335
+ /** Room names the core accepts: 1-64 letters, digits, hyphens or underscores. */
4336
+ var ROOM = /^[A-Za-z0-9_-]{1,64}$/;
4337
+ /**
4338
+ * File > Share: a live session over `ooxml-core/collab` (Yjs). Peers in other windows of this
4339
+ * browser share the whole VSDX package through a BroadcastChannel; remote changes arrive as one
4340
+ * undoable history step. The collab runtime loads only when sharing starts.
4341
+ */
4342
+ var ViewerShare = class {
4343
+ controller;
4344
+ profile;
4345
+ loadCollab;
4346
+ #panel;
4347
+ #room;
4348
+ #start;
4349
+ #stop;
4350
+ #status;
4351
+ #people;
4352
+ #session = null;
4353
+ #teardown = null;
4354
+ #revision = 0;
4355
+ /** The package this window last published or adopted, to skip repeats of it. */
4356
+ #last = null;
4357
+ #adopting = false;
4358
+ #pending = null;
4359
+ #starting = false;
4360
+ constructor(root, controller, profile, loadCollab = () => import("ooxml-core/collab")) {
4361
+ this.controller = controller;
4362
+ this.profile = profile;
4363
+ this.loadCollab = loadCollab;
4364
+ this.#panel = root.querySelector(".share-panel");
4365
+ this.#room = this.#panel.querySelector(".share-room");
4366
+ this.#start = this.#panel.querySelector("[data-share=\"start\"]");
4367
+ this.#stop = this.#panel.querySelector("[data-share=\"stop\"]");
4368
+ this.#status = this.#panel.querySelector(".share-status");
4369
+ this.#people = this.#panel.querySelector("office-ui-presence");
4370
+ this.#room.value = `visio-${Math.random().toString(36).slice(2, 8)}`;
4371
+ }
4372
+ get active() {
4373
+ return this.#session !== null;
4374
+ }
4375
+ wire() {
4376
+ const events = new ((this.#panel.ownerDocument.defaultView?.AbortController) ?? AbortController)();
4377
+ this.#panel.addEventListener("click", (event) => {
4378
+ const action = event.target.closest?.("[data-share]");
4379
+ if (action?.dataset.share === "start") this.start(this.#room.value.trim());
4380
+ if (action?.dataset.share === "stop") this.stop();
4381
+ }, { signal: events.signal });
4382
+ return () => {
4383
+ events.abort();
4384
+ this.stop();
4385
+ };
4386
+ }
4387
+ async start(roomId) {
4388
+ if (this.#session || this.#starting) return;
4389
+ if (!ROOM.test(roomId)) return this.#say("Use 1 to 64 letters, digits, hyphens or underscores for the session name.");
4390
+ this.#starting = true;
4391
+ this.#render();
4392
+ try {
4393
+ const collab = await this.loadCollab();
4394
+ if (!collab.canBroadcast()) return this.#say("Sharing needs BroadcastChannel, which this browser does not provide.");
4395
+ this.#join(collab, roomId);
4396
+ } catch (error) {
4397
+ this.#say(error instanceof Error ? error.message : String(error));
4398
+ } finally {
4399
+ this.#starting = false;
4400
+ this.#render();
4401
+ }
4402
+ }
4403
+ stop() {
4404
+ this.#teardown?.();
4405
+ this.#teardown = null;
4406
+ this.#session?.destroy();
4407
+ this.#session = null;
4408
+ this.#pending = null;
4409
+ this.#revision = 0;
4410
+ this.#last = null;
4411
+ this.#people.participants = [];
4412
+ this.#say("Not sharing.");
4413
+ this.#render();
4414
+ }
4415
+ #join(collab, roomId) {
4416
+ const adapter = collab.packageAdapter();
4417
+ const me = this.profile();
4418
+ const session = collab.createCollabSession({
4419
+ roomId: `visio-${roomId}`,
4420
+ provider: collab.transportProvider({ transport: collab.createBroadcastTransport({ roomId: `visio-${roomId}` }) }),
4421
+ user: {
4422
+ name: me.displayName || "Visio user",
4423
+ color: me.avatarColor
4424
+ },
4425
+ teardown: true
4426
+ });
4427
+ this.#session = session;
4428
+ const publish = () => {
4429
+ const bytes = this.#localBytes();
4430
+ if (!bytes || !session.canWrite()) return;
4431
+ adapter.write(session.doc, {
4432
+ bytes,
4433
+ revision: this.#revision
4434
+ }, collab.LOCAL_ORIGIN);
4435
+ this.#revision = adapter.read(session.doc).revision;
4436
+ this.#last = bytes;
4437
+ };
4438
+ const adopt = () => {
4439
+ if (adapter.isEmpty(session.doc)) return;
4440
+ const model = adapter.read(session.doc);
4441
+ if (model.revision < this.#revision) return;
4442
+ if (model.revision === this.#revision && same(model.bytes, this.#last)) return;
4443
+ this.#revision = model.revision;
4444
+ this.#last = model.bytes;
4445
+ this.#apply(model.bytes);
4446
+ };
4447
+ const map = session.doc.getMap("package");
4448
+ const observer = (_event, transaction) => {
4449
+ if (transaction.origin !== collab.LOCAL_ORIGIN) adopt();
4450
+ };
4451
+ map.observe(observer);
4452
+ const settle = () => adapter.isEmpty(session.doc) ? publish() : adopt();
4453
+ const stopReady = session.on("ready", settle);
4454
+ const stopPeers = session.on("peers", (peers) => this.#showPeers(peers));
4455
+ const stopEvents = this.controller.onEvent((name, detail) => {
4456
+ if (name === "document-change" && detail.kind !== "remote") publish();
4457
+ if (name === "document-load" && !this.#adopting) publish();
4458
+ });
4459
+ const stopState = this.controller.subscribe((state) => this.#flush(state));
4460
+ this.#teardown = () => {
4461
+ map.unobserve(observer);
4462
+ stopReady();
4463
+ stopPeers();
4464
+ stopEvents();
4465
+ stopState();
4466
+ };
4467
+ if (session.canWrite()) settle();
4468
+ this.#showPeers(session.peers());
4469
+ this.#say(`Sharing as ${me.displayName || "Visio user"} in session ${roomId}.`);
4470
+ }
4471
+ #localBytes() {
4472
+ const { edit, loading } = this.controller.state;
4473
+ if (!edit.sourceAvailable || edit.busy || loading) return null;
4474
+ try {
4475
+ return this.controller.exportVsdx().bytes;
4476
+ } catch {
4477
+ return null;
4478
+ }
4479
+ }
4480
+ #apply(bytes) {
4481
+ const { edit, loading, document } = this.controller.state;
4482
+ if (edit.busy || loading) {
4483
+ this.#pending = bytes;
4484
+ return;
4485
+ }
4486
+ this.#pending = null;
4487
+ const done = (error) => {
4488
+ this.#adopting = false;
4489
+ if (error) this.#say(error instanceof Error ? error.message : String(error));
4490
+ };
4491
+ this.#adopting = true;
4492
+ (document && edit.sourceAvailable ? this.controller.applyRemoteSource(bytes) : this.controller.load(bytes)).then(() => done(), done);
4493
+ }
4494
+ #flush(state) {
4495
+ if (this.#pending && !state.edit.busy && !state.loading) this.#apply(this.#pending);
4496
+ }
4497
+ #showPeers(peers) {
4498
+ const me = this.profile();
4499
+ this.#people.participants = [{
4500
+ id: "self",
4501
+ name: me.displayName || "Visio user",
4502
+ color: me.avatarColor,
4503
+ self: true
4504
+ }, ...peers.map((peer) => ({
4505
+ id: String(peer.clientId),
4506
+ name: peer.userName,
4507
+ color: peer.userColor
4508
+ }))];
4509
+ }
4510
+ #say(message) {
4511
+ this.#status.textContent = message;
4512
+ }
4513
+ #render() {
4514
+ const active = this.active;
4515
+ this.#start.hidden = active;
4516
+ this.#stop.hidden = !active;
4517
+ this.#start.disabled = this.#starting;
4518
+ this.#room.disabled = active || this.#starting;
4519
+ }
4520
+ };
4521
+ function same(a, b) {
4522
+ if (!b || a.byteLength !== b.byteLength) return false;
4523
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
4524
+ return true;
4525
+ }
4526
+ //#endregion
4217
4527
  //#region src/edit-error.ts
4218
4528
  /** Core error codes remain visible for a refused transaction; document content is plain text. */
4219
4529
  function editErrorMessage(error) {
@@ -4444,12 +4754,6 @@ function wireStencil(pane, viewport, controller, announce) {
4444
4754
  }
4445
4755
  //#endregion
4446
4756
  //#region src/viewer-ruler.ts
4447
- /**
4448
- * Visio's rulers: inch scales along the top and left of the drawing window with 0 at the page's
4449
- * left and bottom edges (vertical values grow upward, as in Visio). Viewer-only presentation:
4450
- * nothing is read from or written to the document beyond the rendered page box.
4451
- */
4452
- var SIZE = 18;
4453
4757
  /** Wraps the viewport in a grid with a corner, a top ruler and a left ruler. */
4454
4758
  function createRulers(viewport) {
4455
4759
  const doc = viewport.ownerDocument;
@@ -4457,79 +4761,35 @@ function createRulers(viewport) {
4457
4761
  area.className = "canvas-area";
4458
4762
  const corner = doc.createElement("div");
4459
4763
  corner.className = "ruler-corner";
4460
- const top = doc.createElement("canvas");
4764
+ corner.setAttribute("aria-hidden", "true");
4765
+ const top = doc.createElement("office-ui-ruler");
4461
4766
  top.className = "ruler ruler-h";
4462
- const left = doc.createElement("canvas");
4767
+ const left = doc.createElement("office-ui-ruler");
4463
4768
  left.className = "ruler ruler-v";
4464
- for (const ruler of [
4465
- corner,
4466
- top,
4467
- left
4468
- ]) ruler.setAttribute("aria-hidden", "true");
4769
+ left.setAttribute("orientation", "vertical");
4770
+ left.setAttribute("direction", "reverse");
4469
4771
  viewport.replaceWith(area);
4470
4772
  area.append(corner, top, left, viewport);
4471
4773
  let visible = false;
4472
4774
  let zoom = 1;
4473
4775
  let frame = 0;
4474
- const draw = () => {
4776
+ const place = () => {
4475
4777
  frame = 0;
4476
4778
  if (!visible) return;
4477
4779
  const paper = viewport.querySelector("svg.paper");
4478
- const ratio = doc.defaultView?.devicePixelRatio ?? 1;
4479
- const ink = (doc.defaultView?.getComputedStyle(area))?.getPropertyValue("--_vv-muted").trim() || "#605e5c";
4780
+ if (!paper) return;
4480
4781
  const base = viewport.getBoundingClientRect();
4481
- const page = paper?.getBoundingClientRect();
4482
- const inch = 96 * zoom;
4483
- const divisions = inch >= 192 ? 16 : inch >= 96 ? 8 : inch >= 48 ? 4 : 2;
4484
- const paint = (canvas, horizontal) => {
4485
- const length = horizontal ? base.width : base.height;
4486
- canvas.width = Math.max(1, Math.round(length * ratio));
4487
- canvas.height = Math.round(SIZE * ratio);
4488
- if (!horizontal) [canvas.width, canvas.height] = [canvas.height, canvas.width];
4489
- const context = canvas.getContext?.("2d");
4490
- if (!context || !page) return;
4491
- context.scale(ratio, ratio);
4492
- context.strokeStyle = ink;
4493
- context.fillStyle = ink;
4494
- context.lineWidth = 1;
4495
- context.font = "9px \"Segoe UI\", Arial, sans-serif";
4496
- const origin = horizontal ? page.left - base.left : page.bottom - base.top;
4497
- const step = inch / divisions;
4498
- const first = Math.floor((horizontal ? -origin : origin - length) / step) - 1;
4499
- const last = Math.ceil((horizontal ? length - origin : origin) / step) + 1;
4500
- context.beginPath();
4501
- for (let index = first; index <= last; index++) {
4502
- const position = horizontal ? origin + index * step : origin - index * step;
4503
- const tick = index % divisions === 0 ? SIZE : index % (divisions / 2) === 0 ? SIZE * .5 : SIZE * .3;
4504
- const at = Math.round(position) + .5;
4505
- if (horizontal) {
4506
- context.moveTo(at, SIZE);
4507
- context.lineTo(at, SIZE - tick);
4508
- } else {
4509
- context.moveTo(SIZE, at);
4510
- context.lineTo(SIZE - tick, at);
4511
- }
4512
- if (index % divisions === 0) {
4513
- const label = String(index / divisions);
4514
- if (horizontal) context.fillText(label, at + 2, 9);
4515
- else {
4516
- context.save();
4517
- context.translate(9, at - 2);
4518
- context.rotate(-Math.PI / 2);
4519
- context.fillText(label, 0, 0);
4520
- context.restore();
4521
- }
4522
- }
4523
- }
4524
- context.stroke();
4525
- };
4526
- paint(top, true);
4527
- paint(left, false);
4782
+ const page = paper.getBoundingClientRect();
4783
+ const scale = String(96 * zoom);
4784
+ top.setAttribute("scale", scale);
4785
+ left.setAttribute("scale", scale);
4786
+ top.setAttribute("origin", String(page.left - base.left));
4787
+ left.setAttribute("origin", String(page.bottom - base.top));
4528
4788
  };
4529
4789
  const schedule = () => {
4530
4790
  if (!visible || frame) return;
4531
- frame = doc.defaultView?.requestAnimationFrame?.(draw) ?? 0;
4532
- if (!frame) draw();
4791
+ frame = doc.defaultView?.requestAnimationFrame?.(place) ?? 0;
4792
+ if (!frame) place();
4533
4793
  };
4534
4794
  return {
4535
4795
  element: area,
@@ -4913,8 +5173,6 @@ var ViewerChrome = class {
4913
5173
  this.#root.addEventListener("click", (event) => {
4914
5174
  const button = event.target?.closest?.("button");
4915
5175
  if (!button || button.disabled) return;
4916
- const tab = button.dataset.tab;
4917
- if (tab) this.showTab(tab);
4918
5176
  const action = button.dataset.chrome;
4919
5177
  if (action === "inspector" || action === "shapes") this.togglePane(action);
4920
5178
  if (action === "notes") this.reveal("notes");
@@ -4930,33 +5188,13 @@ var ViewerChrome = class {
4930
5188
  event.stopPropagation();
4931
5189
  this.#controller.setPage(Number(index));
4932
5190
  }, options);
4933
- this.#root.addEventListener("keydown", (event) => {
4934
- const key = event;
4935
- const button = event.target?.closest?.("button");
4936
- if (!button?.dataset.tab || ![
4937
- "ArrowLeft",
4938
- "ArrowRight",
4939
- "Home",
4940
- "End"
4941
- ].includes(key.key)) return;
4942
- key.preventDefault();
4943
- const tabs = [...this.#root.querySelectorAll("[data-tab]")];
4944
- const index = tabs.indexOf(button);
4945
- const next = key.key === "Home" ? tabs[0] : key.key === "End" ? tabs.at(-1) : tabs[(index + (key.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length];
4946
- if (!next?.dataset.tab) return;
4947
- this.showTab(next.dataset.tab);
4948
- next.focus();
4949
- }, options);
5191
+ this.#root.addEventListener("office-ribbon-select", () => this.closeCompactTools(), options);
4950
5192
  this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
4951
5193
  return () => events.abort();
4952
5194
  }
4953
5195
  showTab(tab) {
4954
- for (const button of this.#root.querySelectorAll("[data-tab]")) {
4955
- const selected = button.dataset.tab === tab;
4956
- button.setAttribute("aria-selected", String(selected));
4957
- button.tabIndex = selected ? 0 : -1;
4958
- }
4959
- for (const panel of this.#root.querySelectorAll(".ribbon-content")) panel.hidden = panel.id !== `${tab}-panel`;
5196
+ const ribbon = this.#root.querySelector("office-ui-ribbon");
5197
+ if (ribbon) ribbon.selected = tab;
4960
5198
  }
4961
5199
  /** On phones, close the Tools sheet once a command runs. */
4962
5200
  closeCompactTools() {
@@ -6203,6 +6441,7 @@ var VISIO_ICONS = {
6203
6441
  function registerViewerControls(registry) {
6204
6442
  for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
6205
6443
  registerControls(registry);
6444
+ definePresence(registry);
6206
6445
  }
6207
6446
  //#endregion
6208
6447
  //#region src/export-svg.ts
@@ -6822,6 +7061,8 @@ var VisioViewerElement = class extends BaseElement {
6822
7061
  #rulers;
6823
7062
  #canvas;
6824
7063
  #panZoom;
7064
+ #profile;
7065
+ #share;
6825
7066
  #backstage;
6826
7067
  #fileName = "";
6827
7068
  #loadToken = 0;
@@ -6830,7 +7071,6 @@ var VisioViewerElement = class extends BaseElement {
6830
7071
  #diagnostics;
6831
7072
  #toolbar;
6832
7073
  #chrome;
6833
- #search;
6834
7074
  #edit;
6835
7075
  #layers;
6836
7076
  #notes;
@@ -6856,7 +7096,7 @@ var VisioViewerElement = class extends BaseElement {
6856
7096
  workspace.before(ribbon, this.#findBar);
6857
7097
  workspace.prepend(createShapesStrip(document), createShapesWindow(document));
6858
7098
  workspace.append(createPanZoom(document));
6859
- this.#root.append(createBackstage(document), ...createContextMenus(document));
7099
+ this.#root.append(createBackstage(document), createOptionsDialog(document), ...createContextMenus(document));
6860
7100
  workspace.after(createPageTabs(document), createStatusBar(document));
6861
7101
  this.#viewport = this.#root.querySelector(".viewport");
6862
7102
  this.#rulers = createRulers(this.#viewport);
@@ -6865,7 +7105,6 @@ var VisioViewerElement = class extends BaseElement {
6865
7105
  this.#status = this.#root.querySelector("[data-status]");
6866
7106
  this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
6867
7107
  this.#toolbar = this.#root.querySelector(".toolbar");
6868
- this.#search = searchControls(this.#root);
6869
7108
  this.#edit = new ViewerEditControls(this.#root, this.controller);
6870
7109
  this.#layers = this.#root.querySelector(".layer-controls");
6871
7110
  this.#notes = this.#root.querySelector(".notes ul");
@@ -6883,15 +7122,15 @@ var VisioViewerElement = class extends BaseElement {
6883
7122
  togglePanZoom: () => this.#panZoom.toggle(),
6884
7123
  focusSearch: () => {
6885
7124
  this.#chrome.closeCompactTools();
6886
- this.#findBar.hidden = false;
6887
- this.#search.input.focus();
6888
- this.#search.input.select();
7125
+ this.#findBar.show();
6889
7126
  },
6890
7127
  announce: (message) => {
6891
7128
  this.#announcement = message;
6892
7129
  this.#status.textContent = message;
6893
7130
  }
6894
7131
  });
7132
+ this.#profile = new ViewerProfile(this.#root);
7133
+ this.#share = new ViewerShare(this.#root, this.controller, () => this.#profile.profile);
6895
7134
  this.#backstage = new ViewerBackstage({
6896
7135
  root: this.#root,
6897
7136
  viewport: this.#viewport,
@@ -6904,6 +7143,7 @@ var VisioViewerElement = class extends BaseElement {
6904
7143
  this.controller.setDocument(null);
6905
7144
  },
6906
7145
  revealNotes: () => this.#chrome.reveal("notes"),
7146
+ showOptions: () => this.#profile.showOptions(),
6907
7147
  announce: (message) => {
6908
7148
  this.#announcement = message;
6909
7149
  this.#status.textContent = message;
@@ -7081,19 +7321,19 @@ var VisioViewerElement = class extends BaseElement {
7081
7321
  const disposeTellMe = wireTellMe(this.#root);
7082
7322
  const keyTips = attachKeyTips(this.#root);
7083
7323
  const disposePanZoom = this.#panZoom.wire();
7324
+ const disposeProfile = this.#profile.wire();
7325
+ const disposeShare = this.#share.wire();
7084
7326
  const disposeRulers = this.#rulers.wire();
7085
7327
  const disposeStencil = wireStencil(this.#root.querySelector(".shapes-pane"), this.#viewport, this.controller, (message) => {
7086
7328
  this.#announcement = message;
7087
7329
  this.#status.textContent = message;
7088
7330
  });
7089
- const disposeFind = wireFindBar(this.#findBar, this.#search.input, () => this.#viewport.focus({ preventScroll: true }));
7331
+ const disposeFind = wireFindBar(this.#findBar, this.controller, () => this.#viewport.focus({ preventScroll: true }));
7090
7332
  const disposeEdit = this.#edit.wire();
7091
7333
  const disposeLayers = wireLayerControls(this.#layers, this.controller);
7092
7334
  const disposeInputs = wireViewerInputs({
7093
7335
  viewport: this.#viewport,
7094
- commandRoot: this.#root,
7095
- zoomSlider: this.#zoomSlider,
7096
- searchInput: this.#search.input
7336
+ zoomSlider: this.#zoomSlider
7097
7337
  }, this.controller, (mode) => this.#fit(mode));
7098
7338
  return () => {
7099
7339
  disposeChrome();
@@ -7103,6 +7343,8 @@ var VisioViewerElement = class extends BaseElement {
7103
7343
  disposeTellMe();
7104
7344
  keyTips.dispose();
7105
7345
  disposePanZoom();
7346
+ disposeProfile();
7347
+ disposeShare();
7106
7348
  disposeRulers();
7107
7349
  disposeStencil();
7108
7350
  disposeFind();
@@ -7125,7 +7367,7 @@ var VisioViewerElement = class extends BaseElement {
7125
7367
  const inches = (value) => `${+value.toFixed(3)} in`;
7126
7368
  this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
7127
7369
  for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
7128
- renderSearchControls(this.#search, state);
7370
+ renderFindBar(this.#findBar, state);
7129
7371
  this.#edit.render(state);
7130
7372
  if (changed) renderLayerControls(this.#layers, state);
7131
7373
  else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;