visio-react-viewer 0.1.0 → 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,5 +1,7 @@
1
1
  import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, parseVsdx, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
2
- import { createElement, forwardRef, useEffect, useImperativeHandle, useRef } from "react";
2
+ import { registerControls } from "ooxml-ui/controls";
3
+ import { getIcon, registerIcon } from "ooxml-ui/icons";
4
+ import { createElement, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useSyncExternalStore } from "react";
3
5
  export * from "visio-core";
4
6
  //#region src/document-history.ts
5
7
  var EMPTY_EDIT_STATE = Object.freeze({
@@ -532,7 +534,7 @@ function createWorkerEditor(factory, timeoutMs = 2e4) {
532
534
  if (typeof Worker === "undefined") throw new Error("Editing requires browser worker support.");
533
535
  return new Worker(new URL(
534
536
  /* @vite-ignore */
535
- new URL("assets/edit-worker-DRgXgRud.js", import.meta.url).href,
537
+ new URL("assets/edit-worker-BcXrV_St.js", import.meta.url).href,
536
538
  "" + import.meta.url
537
539
  ), { type: "module" });
538
540
  });
@@ -2143,7 +2145,7 @@ function selection(target) {
2143
2145
  }
2144
2146
  /** Own every DOM listener and abort them together when the surface is disposed. */
2145
2147
  function wireViewerInputs(controls, controller, fit) {
2146
- const { viewport, commandRoot, pageSelect, searchInput } = controls;
2148
+ const { viewport, commandRoot, zoomSlider, searchInput } = controls;
2147
2149
  const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
2148
2150
  const options = { signal: events.signal };
2149
2151
  searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
@@ -2159,13 +2161,9 @@ function wireViewerInputs(controls, controller, fit) {
2159
2161
  controller.setSearchQuery("");
2160
2162
  }
2161
2163
  }, options);
2162
- pageSelect.addEventListener("change", () => controller.setPage(Number(pageSelect.value)), options);
2164
+ zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
2163
2165
  commandRoot.addEventListener("click", (event) => {
2164
2166
  const action = event.target?.closest?.("button")?.dataset.action;
2165
- if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
2166
- if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
2167
- if (action === "fit") fit();
2168
- if (action === "actual") controller.setZoom(1);
2169
2167
  if (action === "search-next") controller.nextSearchResult();
2170
2168
  if (action === "search-previous") controller.previousSearchResult();
2171
2169
  }, options);
@@ -2204,7 +2202,7 @@ function wireViewerInputs(controls, controller, fit) {
2204
2202
  }
2205
2203
  if (event.key === "0") {
2206
2204
  event.preventDefault();
2207
- fit();
2205
+ fit("page");
2208
2206
  }
2209
2207
  }, options);
2210
2208
  return () => events.abort();
@@ -2308,13 +2306,7 @@ button:disabled { opacity:.4; cursor:default; }
2308
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; }
2309
2307
  button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
2310
2308
  .toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
2311
- .ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
2312
- .page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
2313
- .page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
2314
- .page-stepper { display:flex; align-items:center; gap:2px; }
2315
- .page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
2316
2309
  .spacer { flex:1; }
2317
- .local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
2318
2310
  .ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
2319
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; }
2320
2312
  .ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
@@ -2324,15 +2316,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
2324
2316
  .group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
2325
2317
  .ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
2326
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; }
2327
- .command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
2328
- .command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
2329
- [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"); }
2330
- [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"); }
2331
- [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"); }
2332
- [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"); }
2333
- [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"); }
2334
- [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"); }
2335
- .ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
2336
2319
  .ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
2337
2320
  .search-group { width:330px; }
2338
2321
  .search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
@@ -2404,11 +2387,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2404
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; }
2405
2388
  .status-message { display:flex; gap:16px; min-width:0; }
2406
2389
  .status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
2407
- .zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
2408
- .zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
2409
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
2410
- .zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
2411
- .zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
2412
2390
  .empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
2413
2391
  .empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
2414
2392
  @media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
@@ -2416,11 +2394,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2416
2394
  button,select,input,textarea { min-height:44px; }
2417
2395
  button { min-width:44px; }
2418
2396
  summary { min-height:44px; line-height:44px; }
2419
- .ribbon-primary { height:52px; padding:4px 8px; }
2420
- .page-picker { flex:1; }
2421
- .page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
2422
- .page-stepper button { width:44px; min-height:44px; height:44px; }
2423
- .local-label,.ribbon-primary>.spacer { display:none; }
2424
2397
  .ribbon-tabs { height:44px; }
2425
2398
  .ribbon-tabs button { min-height:44px; padding:8px 16px; }
2426
2399
  .ribbon-content { min-height:86px; }
@@ -2432,7 +2405,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2432
2405
  .search-controls button { min-width:44px; font-size:0; padding:3px; }
2433
2406
  .search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
2434
2407
  .search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
2435
- #home-panel>.ribbon-group:not(.search-group) { display:none; }
2408
+ #home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
2436
2409
  .ribbon-command { min-width:90px; }
2437
2410
  .workspace { display:block; overflow:auto; }
2438
2411
  .page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
@@ -2449,29 +2422,413 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2449
2422
  .notes-strip button { min-height:44px; }
2450
2423
  .status { flex-wrap:wrap; gap:0; padding:4px 8px; }
2451
2424
  .status-message { width:100%; min-height:18px; }
2452
- .zoom-controls { justify-content:flex-end; width:100%; }
2453
- .zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
2454
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
2455
- .zoom { font-size:11px; }
2456
2425
  }
2457
2426
  @media(pointer:coarse) {
2458
2427
  button,select,input,textarea { min-height:44px; }
2459
2428
  button { min-width:44px; }
2460
2429
  summary,.layer-controls label { min-height:44px; line-height:44px; }
2461
- .ribbon-primary { min-height:52px; height:52px; }
2462
- .page-picker select,.page-stepper button { min-height:44px; height:44px; }
2463
- .page-stepper button { min-width:44px; }
2464
2430
  .ribbon-tabs { min-height:44px; height:44px; }
2465
2431
  .ribbon-tabs button { min-height:44px; }
2466
2432
  .ribbon-content { min-height:96px; }
2467
2433
  .search-group { width:380px; max-width:100%; }
2468
- .zoom-controls button,.notes-strip button { min-height:44px; height:44px; }
2469
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
2434
+ .notes-strip button { min-height:44px; height:44px; }
2470
2435
  .layer-controls input { min-height:20px; }
2471
2436
  }
2437
+ /* Suite ribbon: tabs lead directly into Visio command groups. */
2438
+ .ribbon-tabs { height:40px; }
2439
+ .ribbon-tabs button { min-height:39px; padding:8px 16px; }
2440
+ .ribbon-content { min-height:104px; padding:4px 4px; }
2441
+ .search-group { order:2; width:320px; }
2442
+ .ribbon-hint { order:3; padding:10px 18px; }
2443
+ .ribbon-command { min-width:82px; }
2444
+ .pane-close { display:none; }
2445
+ @media(max-width:760px) {
2446
+ .ribbon-tabs { height:44px; }
2447
+ .ribbon-tabs button { min-height:44px; }
2448
+ .ribbon-content { min-height:54px; padding:4px 0; }
2449
+ .search-group { flex:0 0 100%; width:100%; min-height:54px; }
2450
+ .search-feedback,.group-label { display:none; }
2451
+ .ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
2452
+ .workspace { display:flex; position:relative; overflow:hidden; }
2453
+ .viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
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; }
2455
+ .pane-close { display:block; width:44px; height:44px; padding:0; font-size:22px; background:transparent; border:0; }
2456
+ .inspector-kind { margin-left:auto; }
2457
+ .page-rail { position:absolute; inset:0 auto 0 0; width:180px; max-height:none; z-index:3; border-right:1px solid var(--_vv-border); background:var(--_vv-secondary); }
2458
+ .page-list { flex-direction:column; }
2459
+ .page-list li { flex:none; }
2460
+ .notes-strip { display:none; }
2461
+ .status { min-height:48px; padding:2px 8px; }
2462
+ .status-message { display:none; }
2463
+ }
2464
+ .toolbar { position:relative; }
2465
+ .ribbon-tools { display:flex; flex:none; order:0; }
2466
+ .tools-content { display:flex; align-items:stretch; }
2467
+ .ribbon-tools>summary { display:none; }
2468
+ .ribbon-command { font-size:12px; min-width:88px; }
2469
+ .ribbon-hint { font-size:12px; line-height:20px; flex:1; }
2470
+ .group-label { font-size:11px; line-height:14px; }
2471
+ .inspector-card,.edit-controls,.layer-controls { font-size:12px; line-height:18px; }
2472
+ .selection-hint,.document-card dl { font-size:12px; }
2473
+ .pane-heading { font-size:12px; }
2474
+ .inspector-pane .pane-heading { position:sticky; top:0; z-index:1; background:var(--_vv-secondary); flex:none; }
2475
+ .status .notes-strip { border:0; padding:0 8px; background:transparent; min-height:28px; }
2476
+ .status .notes-strip>span { display:none; }
2477
+ .status-message { flex:1; }
2478
+ @media(max-width:760px) {
2479
+ #home-panel { overflow:visible; min-height:54px; padding:4px; }
2480
+ .ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
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; }
2482
+ .ribbon-tools>summary::-webkit-details-marker { display:none; }
2483
+ .ribbon-tools .tools-content { display:flex; position:absolute; top:100%; left:4px; right:4px; z-index:6; flex-direction:column; padding:12px; gap:12px; border:1px solid var(--_vv-border); border-radius:4px; background:var(--_vv-surface); box-shadow:0 12px 28px #0003; max-height:60vh; overflow:auto; }
2484
+ .ribbon-tools:not([open])>.tools-content { display:none; }
2485
+ .ribbon-tools .ribbon-group { display:flex; width:100%; min-height:64px; border:0; padding:0; }
2486
+ .ribbon-tools .ribbon-actions { justify-content:space-between; }
2487
+ .ribbon-tools .ribbon-command { min-width:88px; font-size:12px; }
2488
+ .ribbon-tools .search-group { order:1; flex:auto; }
2489
+ .ribbon-tools .search-feedback { display:block; }
2490
+ .status { flex-wrap:nowrap; gap:4px; min-height:48px; }
2491
+ .status .notes-strip { display:none; }
2492
+ }
2493
+ @media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
2472
2494
  @media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
2473
2495
  `;
2474
2496
  //#endregion
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
2475
2832
  //#region src/edit-error.ts
2476
2833
  /** Core error codes remain visible for a refused transaction; document content is plain text. */
2477
2834
  function editErrorMessage(error) {
@@ -2759,61 +3116,17 @@ var ViewerEditControls = class {
2759
3116
  }
2760
3117
  };
2761
3118
  //#endregion
2762
- //#region src/viewer-search.ts
2763
- 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>`;
2764
- function searchControls(root) {
2765
- return {
2766
- input: root.querySelector("input[type=\"search\"]"),
2767
- status: root.querySelector("#search-status"),
2768
- previous: root.querySelector("[data-action=\"search-previous\"]"),
2769
- next: root.querySelector("[data-action=\"search-next\"]")
2770
- };
2771
- }
2772
- function renderSearchControls(controls, state) {
2773
- if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
2774
- controls.input.disabled = !state.document?.pages.length;
2775
- controls.previous.disabled = controls.next.disabled = !state.search.results.length;
2776
- controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
2777
- const result = state.search.results[state.search.activeIndex];
2778
- controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
2779
- }
2780
- //#endregion
2781
3119
  //#region src/viewer-chrome.ts
2782
- /** Only static application markup belongs here. All document labels are inserted as text. */
2783
- var viewerChromeTemplate = `
2784
- <div class="toolbar" role="group" aria-label="Diagram controls">
2785
- <div class="ribbon-primary">
2786
- <label class="page-picker">Page <select aria-label="Page"></select></label>
2787
- <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>
2788
- <span class="spacer"></span><span class="local-label">Local workspace</span>
2789
- </div>
2790
- <div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
2791
- <button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
2792
- <button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
2793
- </div>
2794
- <div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
2795
- <div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
2796
- <div class="ribbon-group"><div class="ribbon-actions">
2797
- <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>
2798
- <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>
2799
- <button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
2800
- </div><span class="group-label">Inspect and edit</span></div>
2801
- <p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
2802
- </div>
2803
- <div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
2804
- <div class="ribbon-group"><div class="ribbon-actions">
2805
- <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>
2806
- <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>
2807
- <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>
2808
- </div><span class="group-label">Workspace panes</span></div>
2809
- <p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
2810
- </div>
2811
- </div>
2812
- <div class="workspace">
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">
2813
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>
2814
3127
  <div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
2815
3128
  <aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
2816
- <div class="pane-heading"><span>Inspector</span><span class="inspector-kind">Drawing</span></div>
3129
+ <div class="pane-heading"><span>Inspector</span><span class="inspector-kind">Drawing</span><button class="pane-close" type="button" data-chrome="inspector" aria-label="Close inspector">×</button></div>
2817
3130
  <div class="inspector-body">
2818
3131
  <section class="inspector-card document-card" aria-label="Current page"><h2>Current page</h2><p class="current-page-name" data-page-name>No diagram open</p><dl><dt>Size</dt><dd data-page-size>No page</dd><dt>Shapes</dt><dd data-page-shapes>No shapes</dd></dl></section>
2819
3132
  <p class="selection-hint">Select a shape on the canvas to see its data and links.</p>
@@ -2822,24 +3135,28 @@ var viewerChromeTemplate = `
2822
3135
  <details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
2823
3136
  </div>
2824
3137
  </aside>
2825
- </div>
2826
- <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>
2827
- <div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><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>`;
2828
3139
  /** Presentation state stays local; page navigation uses the same controller as every binding. */
2829
3140
  var ViewerChrome = class {
2830
3141
  #root;
2831
3142
  #controller;
2832
3143
  #document;
2833
3144
  #pageList;
3145
+ #pageTabs;
3146
+ #tools;
2834
3147
  #rail;
2835
3148
  #inspector;
2836
3149
  #notes;
2837
3150
  #responsive;
3151
+ #compact;
2838
3152
  #pagesManuallyToggled = false;
3153
+ #inspectorManuallyToggled = false;
2839
3154
  constructor(root, controller) {
2840
3155
  this.#root = root;
2841
3156
  this.#controller = controller;
2842
3157
  this.#pageList = root.querySelector(".page-list");
3158
+ this.#pageTabs = root.querySelector("office-ui-tab-strip");
3159
+ this.#tools = root.querySelector(".ribbon-tools");
2843
3160
  this.#rail = root.querySelector(".page-rail");
2844
3161
  this.#inspector = root.querySelector(".inspector-pane");
2845
3162
  this.#notes = root.querySelector(".notes");
@@ -2848,26 +3165,45 @@ var ViewerChrome = class {
2848
3165
  const view = this.#root.ownerDocument.defaultView;
2849
3166
  const events = new ((view?.AbortController) ?? AbortController)(), options = { signal: events.signal };
2850
3167
  this.#responsive = view?.matchMedia?.("(max-width: 980px)");
3168
+ const compact = view?.matchMedia?.("(max-width: 760px)");
3169
+ this.#compact = compact;
3170
+ const tools = this.#tools;
3171
+ const toolLayout = () => {
3172
+ tools.open = !compact?.matches;
3173
+ };
3174
+ toolLayout();
3175
+ compact?.addEventListener("change", toolLayout, options);
3176
+ this.#root.addEventListener("keydown", (event) => {
3177
+ if (event.key === "Escape" && compact?.matches && tools.open) {
3178
+ event.preventDefault();
3179
+ event.stopPropagation();
3180
+ tools.open = false;
3181
+ tools.querySelector("summary").focus();
3182
+ }
3183
+ }, {
3184
+ ...options,
3185
+ capture: true
3186
+ });
2851
3187
  const responsive = () => {
2852
3188
  if (!this.#pagesManuallyToggled) this.#setPane("pages", !this.#responsive?.matches);
3189
+ if (!this.#inspectorManuallyToggled) this.#setPane("inspector", !compact?.matches);
2853
3190
  };
2854
3191
  responsive();
2855
3192
  this.#responsive?.addEventListener("change", responsive, options);
3193
+ compact?.addEventListener("change", responsive, options);
2856
3194
  this.#root.addEventListener("click", (event) => {
2857
3195
  const button = event.target?.closest?.("button");
2858
3196
  if (!button || button.disabled) return;
2859
3197
  const tab = button.dataset.tab;
2860
- if (tab === "home" || tab === "view") this.#showTab(tab);
3198
+ if (tab === "home" || tab === "view") this.showTab(tab);
2861
3199
  if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
2862
3200
  const action = button.dataset.chrome;
2863
- if (action === "previous-page") this.#controller.setPage(this.#controller.state.pageIndex - 1);
2864
- if (action === "next-page") this.#controller.setPage(this.#controller.state.pageIndex + 1);
2865
- if (action === "pages") {
2866
- this.#pagesManuallyToggled = true;
2867
- this.#setPane("pages", this.#rail.hidden);
2868
- }
2869
- if (action === "inspector") this.#setPane("inspector", this.#inspector.hidden);
2870
- 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);
2871
3207
  }, options);
2872
3208
  this.#root.addEventListener("keydown", (event) => {
2873
3209
  const key = event;
@@ -2884,11 +3220,11 @@ var ViewerChrome = class {
2884
3220
  if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
2885
3221
  key.preventDefault();
2886
3222
  const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
2887
- this.#showTab(tab);
3223
+ this.showTab(tab);
2888
3224
  this.#root.querySelector(`[data-tab="${tab}"]`).focus();
2889
3225
  } else if (button.dataset.pageIndex !== void 0) {
2890
3226
  key.preventDefault();
2891
- const pages = [...this.#pageList.querySelectorAll("button")];
3227
+ const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
2892
3228
  const current = Number(button.dataset.pageIndex);
2893
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)));
2894
3230
  this.#controller.setPage(next);
@@ -2898,7 +3234,7 @@ var ViewerChrome = class {
2898
3234
  this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
2899
3235
  return () => events.abort();
2900
3236
  }
2901
- #showTab(tab) {
3237
+ showTab(tab) {
2902
3238
  for (const button of this.#root.querySelectorAll("[data-tab]")) {
2903
3239
  const selected = button.dataset.tab === tab;
2904
3240
  button.setAttribute("aria-selected", String(selected));
@@ -2906,21 +3242,41 @@ var ViewerChrome = class {
2906
3242
  }
2907
3243
  for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
2908
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
+ }
2909
3255
  #setPane(pane, visible) {
3256
+ if (visible && this.#compact?.matches) {
3257
+ const other = pane === "pages" ? "inspector" : "pages";
3258
+ (other === "pages" ? this.#rail : this.#inspector).hidden = true;
3259
+ this.#command(other).setAttribute("pressed", "false");
3260
+ }
2910
3261
  (pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
2911
- this.#root.querySelector(`[data-chrome="${pane}"]`).setAttribute("aria-pressed", String(visible));
3262
+ this.#command(pane).setAttribute("pressed", String(visible));
2912
3263
  this.#syncNotes();
2913
3264
  }
2914
3265
  #syncNotes() {
2915
3266
  this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
2916
3267
  }
2917
- #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;
3271
+ this.#inspectorManuallyToggled = true;
2918
3272
  this.#setPane("inspector", true);
2919
3273
  const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
2920
3274
  const panel = this.#root.querySelector(selector);
2921
3275
  if (panel.hidden) return;
2922
3276
  panel.open = true;
2923
- panel.querySelector("summary").focus();
3277
+ const text = panel.querySelector("textarea");
3278
+ if (focusText && text && !text.disabled) text.focus();
3279
+ else panel.querySelector("summary").focus();
2924
3280
  panel.scrollIntoView?.({
2925
3281
  block: "nearest",
2926
3282
  inline: "nearest"
@@ -2961,14 +3317,21 @@ var ViewerChrome = class {
2961
3317
  fragment.append(item);
2962
3318
  }
2963
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
+ }));
2964
3325
  if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
2965
3326
  }
3327
+ this.#pageTabs.selected = page ? String(state.pageIndex) : "";
2966
3328
  for (const button of this.#pageList.querySelectorAll("button")) {
2967
3329
  const selected = Number(button.dataset.pageIndex) === state.pageIndex;
2968
3330
  if (selected) button.setAttribute("aria-current", "page");
2969
3331
  else button.removeAttribute("aria-current");
2970
3332
  button.tabIndex = selected ? 0 : -1;
2971
3333
  }
3334
+ this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
2972
3335
  const count = state.document?.pages.length ?? 0;
2973
3336
  this.#root.querySelector("[data-page-count]").textContent = String(count);
2974
3337
  this.#root.querySelector(".page-empty").hidden = count > 0;
@@ -2976,17 +3339,405 @@ var ViewerChrome = class {
2976
3339
  this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
2977
3340
  this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
2978
3341
  this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
2979
- this.#root.querySelector("[data-chrome=\"previous-page\"]").disabled = !page || state.pageIndex === 0;
2980
- this.#root.querySelector("[data-chrome=\"next-page\"]").disabled = !page || state.pageIndex >= count - 1;
2981
- this.#root.querySelector("[data-chrome=\"selection\"]").disabled = !state.selectedShape;
2982
- this.#root.querySelector("[data-chrome=\"edit\"]").disabled = !page;
2983
- this.#root.querySelector("[data-chrome=\"layers\"]").disabled = this.#root.querySelector(".layer-controls").hidden;
2984
- 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;
2985
3347
  this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
2986
3348
  this.#syncNotes();
2987
3349
  }
2988
3350
  };
2989
3351
  //#endregion
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
2990
3741
  //#region src/export-svg.ts
2991
3742
  var MAX_SVG_EXPORT_BYTES = 16777216;
2992
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.";
@@ -3597,8 +4348,10 @@ var VisioViewerElement = class extends BaseElement {
3597
4348
  #root;
3598
4349
  #disposeInputs;
3599
4350
  #viewport;
3600
- #pageSelect;
3601
- #zoomLabel;
4351
+ #zoomSlider;
4352
+ #shapeStatus;
4353
+ #announcement;
4354
+ #commands;
3602
4355
  #status;
3603
4356
  #diagnostics;
3604
4357
  #toolbar;
@@ -3631,10 +4384,14 @@ var VisioViewerElement = class extends BaseElement {
3631
4384
  constructor() {
3632
4385
  super();
3633
4386
  this.#root = this.attachShadow({ mode: "open" });
3634
- 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));
3635
4392
  this.#viewport = this.#root.querySelector(".viewport");
3636
- this.#pageSelect = this.#root.querySelector("select");
3637
- 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]");
3638
4395
  this.#status = this.#root.querySelector("[data-status]");
3639
4396
  this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
3640
4397
  this.#toolbar = this.#root.querySelector(".toolbar");
@@ -3645,6 +4402,23 @@ var VisioViewerElement = class extends BaseElement {
3645
4402
  this.#notesPanel = this.#root.querySelector(".notes");
3646
4403
  this.#inspector = this.#root.querySelector(".shape-inspector");
3647
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
+ });
3648
4422
  this.#disposeInputs = this.#wireInputs();
3649
4423
  this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
3650
4424
  this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
@@ -3718,13 +4492,24 @@ var VisioViewerElement = class extends BaseElement {
3718
4492
  }
3719
4493
  fit() {
3720
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) {
3721
4499
  const page = this.document?.pages[this.pageIndex];
3722
4500
  if (!page) return;
3723
4501
  const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
3724
4502
  const padding = (value) => Number.parseFloat(value ?? "") || 0;
3725
4503
  const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
3726
4504
  const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
3727
- 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);
3728
4513
  }
3729
4514
  setLayerVisibility(pageId, layerId, visible) {
3730
4515
  this.#assertAlive();
@@ -3794,16 +4579,18 @@ var VisioViewerElement = class extends BaseElement {
3794
4579
  }
3795
4580
  #wireInputs() {
3796
4581
  const disposeChrome = this.#chrome.wire();
4582
+ const disposeCommands = this.#commands.wire();
3797
4583
  const disposeEdit = this.#edit.wire();
3798
4584
  const disposeLayers = wireLayerControls(this.#layers, this.controller);
3799
4585
  const disposeInputs = wireViewerInputs({
3800
4586
  viewport: this.#viewport,
3801
4587
  commandRoot: this.#root,
3802
- pageSelect: this.#pageSelect,
4588
+ zoomSlider: this.#zoomSlider,
3803
4589
  searchInput: this.#search.input
3804
- }, this.controller, () => this.fit());
4590
+ }, this.controller, (mode) => this.#fit(mode));
3805
4591
  return () => {
3806
4592
  disposeChrome();
4593
+ disposeCommands();
3807
4594
  disposeInputs();
3808
4595
  disposeLayers();
3809
4596
  disposeEdit();
@@ -3822,14 +4609,7 @@ var VisioViewerElement = class extends BaseElement {
3822
4609
  this.#document = state.document;
3823
4610
  this.#renderedPage = state.pageIndex;
3824
4611
  this.#renderedLayerOverrides = state.layerVisibilityOverrides;
3825
- this.#pageSelect.replaceChildren();
3826
- for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
3827
- const option = document.createElement("option");
3828
- option.value = String(index);
3829
- option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
3830
- this.#pageSelect.append(option);
3831
- }
3832
- this.#pageSelect.value = String(state.pageIndex);
4612
+ this.#announcement = void 0;
3833
4613
  this.#renderWarnings = [];
3834
4614
  if (state.document && page) {
3835
4615
  const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
@@ -3856,6 +4636,7 @@ var VisioViewerElement = class extends BaseElement {
3856
4636
  if (svg && page) {
3857
4637
  svg.style.width = `${page.width * 96 * state.zoom}px`;
3858
4638
  svg.style.height = `${page.height * 96 * state.zoom}px`;
4639
+ svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
3859
4640
  }
3860
4641
  const searchResult = state.search.results[state.search.activeIndex];
3861
4642
  const selection = state.selectedShape;
@@ -3883,16 +4664,20 @@ var VisioViewerElement = class extends BaseElement {
3883
4664
  this.#renderedSelection = selection ? { ...selection } : null;
3884
4665
  }
3885
4666
  if (!searchResult) this.#revealedSearchResult = void 0;
3886
- this.#zoomLabel.value = `${Math.round(state.zoom * 100)}%`;
3887
- 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)}` : "");
3888
4672
  for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
3889
4673
  renderSearchControls(this.#search, state);
3890
4674
  this.#edit.render(state);
3891
4675
  if (changed) renderLayerControls(this.#layers, state);
3892
4676
  else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
3893
4677
  this.#chrome.render(state, this.#notes.children.length);
4678
+ this.#commands.render(state);
3894
4679
  this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
3895
- 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");
3896
4681
  const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
3897
4682
  this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
3898
4683
  }
@@ -4102,6 +4887,23 @@ function viewerHandle(current) {
4102
4887
  }
4103
4888
  };
4104
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
+ }
4105
4907
  //#endregion
4106
4908
  //#region packages/bindings/src/react.ts
4107
4909
  /** React owns only the host and lifecycle; the shared element owns all viewer behavior. */
@@ -4127,5 +4929,13 @@ var VisioViewer = forwardRef(function VisioViewer(props, ref) {
4127
4929
  "aria-label": props["aria-label"]
4128
4930
  });
4129
4931
  });
4932
+ /**
4933
+ * Reactive viewer state (page, zoom, selection, search, edit and undo state) for the `ref`
4934
+ * given to `<VisioViewer>`, through React's external-store hook. `null` until mounted.
4935
+ */
4936
+ function useVisioViewerState(ref) {
4937
+ const source = useMemo(() => viewerStateSource(() => ref.current), [ref]);
4938
+ return useSyncExternalStore(source.subscribe, source.getSnapshot, () => null);
4939
+ }
4130
4940
  //#endregion
4131
- export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };
4941
+ export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage, useVisioViewerState };