visio-solid-viewer 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, parseVsdx, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
2
+ import { registerControls } from "ooxml-ui/controls";
3
+ import { getIcon, registerIcon } from "ooxml-ui/icons";
2
4
  import { escape, ssr, ssrAttribute, ssrHydrationKey, ssrStyle } from "solid-js/web";
3
5
  import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
4
6
  export * from "visio-core";
@@ -2136,7 +2138,7 @@ function selection(target) {
2136
2138
  }
2137
2139
  /** Own every DOM listener and abort them together when the surface is disposed. */
2138
2140
  function wireViewerInputs(controls, controller, fit) {
2139
- const { viewport, commandRoot, pageSelect, searchInput } = controls;
2141
+ const { viewport, commandRoot, zoomSlider, searchInput } = controls;
2140
2142
  const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
2141
2143
  const options = { signal: events.signal };
2142
2144
  searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
@@ -2152,13 +2154,9 @@ function wireViewerInputs(controls, controller, fit) {
2152
2154
  controller.setSearchQuery("");
2153
2155
  }
2154
2156
  }, options);
2155
- pageSelect.addEventListener("change", () => controller.setPage(Number(pageSelect.value)), options);
2157
+ zoomSlider.addEventListener("input", () => controller.setZoom(zoomSlider.value / 100), options);
2156
2158
  commandRoot.addEventListener("click", (event) => {
2157
2159
  const action = event.target?.closest?.("button")?.dataset.action;
2158
- if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
2159
- if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
2160
- if (action === "fit") fit();
2161
- if (action === "actual") controller.setZoom(1);
2162
2160
  if (action === "search-next") controller.nextSearchResult();
2163
2161
  if (action === "search-previous") controller.previousSearchResult();
2164
2162
  }, options);
@@ -2197,7 +2195,7 @@ function wireViewerInputs(controls, controller, fit) {
2197
2195
  }
2198
2196
  if (event.key === "0") {
2199
2197
  event.preventDefault();
2200
- fit();
2198
+ fit("page");
2201
2199
  }
2202
2200
  }, options);
2203
2201
  return () => events.abort();
@@ -2301,13 +2299,7 @@ button:disabled { opacity:.4; cursor:default; }
2301
2299
  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; }
2302
2300
  button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
2303
2301
  .toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
2304
- .ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
2305
- .page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
2306
- .page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
2307
- .page-stepper { display:flex; align-items:center; gap:2px; }
2308
- .page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
2309
2302
  .spacer { flex:1; }
2310
- .local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
2311
2303
  .ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
2312
2304
  .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; }
2313
2305
  .ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
@@ -2317,15 +2309,6 @@ button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-acce
2317
2309
  .group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
2318
2310
  .ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
2319
2311
  .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; }
2320
- .command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
2321
- .command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
2322
- [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"); }
2323
- [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"); }
2324
- [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"); }
2325
- [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"); }
2326
- [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"); }
2327
- [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"); }
2328
- .ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
2329
2312
  .ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
2330
2313
  .search-group { width:330px; }
2331
2314
  .search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
@@ -2397,11 +2380,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2397
2380
  .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; }
2398
2381
  .status-message { display:flex; gap:16px; min-width:0; }
2399
2382
  .status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
2400
- .zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
2401
- .zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
2402
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
2403
- .zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
2404
- .zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
2405
2383
  .empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
2406
2384
  .empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
2407
2385
  @media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
@@ -2409,11 +2387,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2409
2387
  button,select,input,textarea { min-height:44px; }
2410
2388
  button { min-width:44px; }
2411
2389
  summary { min-height:44px; line-height:44px; }
2412
- .ribbon-primary { height:52px; padding:4px 8px; }
2413
- .page-picker { flex:1; }
2414
- .page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
2415
- .page-stepper button { width:44px; min-height:44px; height:44px; }
2416
- .local-label,.ribbon-primary>.spacer { display:none; }
2417
2390
  .ribbon-tabs { height:44px; }
2418
2391
  .ribbon-tabs button { min-height:44px; padding:8px 16px; }
2419
2392
  .ribbon-content { min-height:86px; }
@@ -2425,7 +2398,7 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2425
2398
  .search-controls button { min-width:44px; font-size:0; padding:3px; }
2426
2399
  .search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
2427
2400
  .search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
2428
- #home-panel>.ribbon-group:not(.search-group) { display:none; }
2401
+ #home-panel>.ribbon-group:not(.search-group):not(.undo-group) { display:none; }
2429
2402
  .ribbon-command { min-width:90px; }
2430
2403
  .workspace { display:block; overflow:auto; }
2431
2404
  .page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
@@ -2442,35 +2415,22 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2442
2415
  .notes-strip button { min-height:44px; }
2443
2416
  .status { flex-wrap:wrap; gap:0; padding:4px 8px; }
2444
2417
  .status-message { width:100%; min-height:18px; }
2445
- .zoom-controls { justify-content:flex-end; width:100%; }
2446
- .zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
2447
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
2448
- .zoom { font-size:11px; }
2449
2418
  }
2450
2419
  @media(pointer:coarse) {
2451
2420
  button,select,input,textarea { min-height:44px; }
2452
2421
  button { min-width:44px; }
2453
2422
  summary,.layer-controls label { min-height:44px; line-height:44px; }
2454
- .ribbon-primary { min-height:52px; height:52px; }
2455
- .page-picker select,.page-stepper button { min-height:44px; height:44px; }
2456
- .page-stepper button { min-width:44px; }
2457
2423
  .ribbon-tabs { min-height:44px; height:44px; }
2458
2424
  .ribbon-tabs button { min-height:44px; }
2459
2425
  .ribbon-content { min-height:96px; }
2460
2426
  .search-group { width:380px; max-width:100%; }
2461
- .zoom-controls button,.notes-strip button { min-height:44px; height:44px; }
2462
- .zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
2427
+ .notes-strip button { min-height:44px; height:44px; }
2463
2428
  .layer-controls input { min-height:20px; }
2464
2429
  }
2465
- /* Suite ribbon: tabs lead directly into command groups, with page navigation inside. */
2430
+ /* Suite ribbon: tabs lead directly into Visio command groups. */
2466
2431
  .ribbon-tabs { height:40px; }
2467
2432
  .ribbon-tabs button { min-height:39px; padding:8px 16px; }
2468
2433
  .ribbon-content { min-height:104px; padding:4px 4px; }
2469
- .ribbon-primary { height:auto; min-height:92px; width:238px; flex:none; flex-wrap:wrap; justify-content:center; gap:2px; padding:10px 12px 18px; border-right:1px solid var(--_vv-border); position:relative; order:1; }
2470
- .ribbon-primary .page-picker { flex-direction:column; align-items:start; gap:5px; }
2471
- .ribbon-primary .page-picker select { width:160px; max-width:100%; }
2472
- .ribbon-primary .spacer { display:none; }
2473
- .ribbon-primary .local-label { position:absolute; bottom:4px; left:0; width:100%; text-align:center; }
2474
2434
  .search-group { order:2; width:320px; }
2475
2435
  .ribbon-hint { order:3; padding:10px 18px; }
2476
2436
  .ribbon-command { min-width:82px; }
@@ -2479,13 +2439,9 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2479
2439
  .ribbon-tabs { height:44px; }
2480
2440
  .ribbon-tabs button { min-height:44px; }
2481
2441
  .ribbon-content { min-height:54px; padding:4px 0; }
2482
- .ribbon-primary { min-height:44px; height:44px; width:100%; padding:0 8px; border:0; flex-wrap:nowrap; gap:4px; order:0; }
2483
- .ribbon-primary .page-picker { flex-direction:row; flex:1; align-items:center; gap:8px; }
2484
- .ribbon-primary .page-picker select { flex:1; width:100%; min-width:0; }
2485
2442
  .search-group { flex:0 0 100%; width:100%; min-height:54px; }
2486
2443
  .search-feedback,.group-label { display:none; }
2487
2444
  .ribbon-content { overflow-x:auto; scroll-snap-type:x mandatory; }
2488
- .ribbon-primary,.search-group { scroll-snap-align:start; }
2489
2445
  .workspace { display:flex; position:relative; overflow:hidden; }
2490
2446
  .viewport { flex:1; width:100%; height:100%; min-height:0; padding:16px 4px; }
2491
2447
  .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; }
@@ -2514,9 +2470,6 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2514
2470
  .status-message { flex:1; }
2515
2471
  @media(max-width:760px) {
2516
2472
  #home-panel { overflow:visible; min-height:54px; padding:4px; }
2517
- .ribbon-primary { width:calc(100% - 92px); min-width:0; flex:1; padding:0 4px; }
2518
- .ribbon-primary .page-picker { font-size:0; gap:0; }
2519
- .ribbon-primary .page-picker select { font-size:12px; }
2520
2473
  .ribbon-tools { display:block; flex:0 0 84px; width:84px; order:1; }
2521
2474
  .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; }
2522
2475
  .ribbon-tools>summary::-webkit-details-marker { display:none; }
@@ -2528,14 +2481,347 @@ summary::marker { color:var(--_vv-muted); font-size:10px; }
2528
2481
  .ribbon-tools .search-group { order:1; flex:auto; }
2529
2482
  .ribbon-tools .search-feedback { display:block; }
2530
2483
  .status { flex-wrap:nowrap; gap:4px; min-height:48px; }
2531
- .zoom-controls { width:auto; flex:1; gap:0; }
2532
- .zoom-divider { margin:0; }
2533
2484
  .status .notes-strip { display:none; }
2534
2485
  }
2535
2486
  @media(pointer:coarse) { .ribbon-tabs {height:45px;min-height:45px;} .ribbon-tabs button {min-height:44px;height:44px;} }
2536
2487
  @media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
2537
2488
  `;
2538
2489
  //#endregion
2490
+ //#region src/styles/index.ts
2491
+ /**
2492
+ * Viewer stylesheets as text for the shadow root. New styles are CSS files here; the legacy
2493
+ * `styles.ts` string moves into this directory when it is next changed.
2494
+ */
2495
+ var canvasAndRibbonStyles = [":host{--office-foreground:var(--_vv-ink);--office-muted-foreground:var(--_vv-muted);--office-background:var(--_vv-surface);--office-surface:var(--_vv-accent-soft);--office-border:var(--_vv-border);--office-accent:var(--_vv-accent);--office-accent-foreground:var(--vv-accent-ink,#fff);--office-ring:var(--vv-focus,var(--_vv-accent));--office-font:\"Segoe UI\", Arial, sans-serif}.ribbon-row{background:0 0;border:0;flex-wrap:nowrap;align-items:stretch;gap:0;padding:0;display:flex}.ribbon-row office-ui-ribbon-group{min-width:0}.ribbon-row office-ui-button{--office-target-size:28px}.ribbon-row office-ui-button[variant=stacked]::part(button){min-width:64px;min-height:62px;font-size:12px}office-ui-button[disabled]::part(button){opacity:.45}.page-tabs{flex:none}office-ui-status-bar.status{background:var(--_vv-surface);min-height:29px;color:var(--_vv-muted);gap:16px;padding:0 8px 0 12px;font-size:11px}office-ui-status-item[value=\"\"]{display:none}office-ui-status-item[data-page-status]{color:var(--_vv-ink)}.status-message{flex:1;min-width:0}.viewport[data-tool=rectangle],.viewport[data-tool=rectangle] .paper [data-shape-id]{cursor:crosshair}.viewport[data-tool=rectangle]{touch-action:none}.draw-preview{fill:var(--_vv-accent-soft);stroke:var(--_vv-accent);stroke-width:1.5px;stroke-dasharray:4 3;pointer-events:none}.viewport[data-grid=true] .paper{--_vv-grid-minor:#00000012;--_vv-grid-major:#00000029;background-image:linear-gradient(to right, var(--_vv-grid-major) 1px, transparent 1px), linear-gradient(to top, var(--_vv-grid-major) 1px, transparent 1px), linear-gradient(to right, var(--_vv-grid-minor) 1px, transparent 1px), linear-gradient(to top, var(--_vv-grid-minor) 1px, transparent 1px);background-size:var(--vv-inch,96px) var(--vv-inch,96px), var(--vv-inch,96px) var(--vv-inch,96px), calc(var(--vv-inch,96px) / 4) calc(var(--vv-inch,96px) / 4), calc(var(--vv-inch,96px) / 4) calc(var(--vv-inch,96px) / 4);background-position:0 100%}:host(:fullscreen){width:100vw;height:100vh}@media (width<=760px){.ribbon-row{flex:1;min-width:0}.undo-group{flex:none}.ribbon-row office-ui-button[variant=stacked]::part(button){min-width:52px;min-height:44px}.ribbon-tools .tools-content office-ui-ribbon-group{width:100%}.search-group{scroll-snap-align:start}office-ui-status-bar.status{gap:4px;min-height:48px}office-ui-status-bar .notes-strip,office-ui-status-bar .status-message,office-ui-status-item[data-shape-status],office-ui-status-item[data-page-status]{display:none}.zoom-controls{--office-zoom-slider-width:0;flex:1;min-width:0}}", ".paper [data-shape-id]:focus,.paper [data-shape-id]:focus-visible{outline:none}.paper [data-shape-id]:focus-visible>[data-geometry]{stroke-dasharray:.08 .05;stroke:var(--vv-focus,var(--_vv-accent))!important;stroke-width:.04px!important}"].join("\n");
2496
+ //#endregion
2497
+ //#region src/ribbon-action.ts
2498
+ /** Event name shared by every ribbon control; detail is the typed action. */
2499
+ var RIBBON_ACTION_EVENT = "ribbon-action";
2500
+ /** Internal, non-composed event: it stays inside the viewer's shadow tree. */
2501
+ function emitRibbonAction(target, action) {
2502
+ target.dispatchEvent(new CustomEvent(RIBBON_ACTION_EVENT, {
2503
+ detail: action,
2504
+ bubbles: true
2505
+ }));
2506
+ }
2507
+ //#endregion
2508
+ //#region src/ribbon-parts.ts
2509
+ /**
2510
+ * A large stacked ribbon command. `id` is a stable name for state sync and tests; the typed
2511
+ * `action` is what runs. Labels, icons and shortcuts are static application strings.
2512
+ */
2513
+ function tool(doc, id, label, icon, action, options = {}) {
2514
+ const el = doc.createElement("office-ui-button");
2515
+ el.setAttribute("variant", "stacked");
2516
+ el.setAttribute("command", id);
2517
+ el.setAttribute("icon", icon);
2518
+ el.setAttribute("label", label);
2519
+ el.setAttribute("title", options.keys ? `${label} (${options.keys[1]})` : label);
2520
+ if (options.keys) el.setAttribute("keyshortcuts", options.keys[0]);
2521
+ if (options.pressed !== void 0) el.setAttribute("pressed", String(options.pressed));
2522
+ if (options.controls) el.setAttribute("aria-controls", options.controls);
2523
+ el.addEventListener("office-command", (event) => {
2524
+ event.stopPropagation();
2525
+ emitRibbonAction(el, action);
2526
+ });
2527
+ return el;
2528
+ }
2529
+ /** A labelled ribbon group (the shared `office-ui-ribbon-group`). */
2530
+ function group(doc, label, children, className = "") {
2531
+ const el = doc.createElement("office-ui-ribbon-group");
2532
+ el.setAttribute("label", label);
2533
+ if (className) el.className = className;
2534
+ el.append(...children);
2535
+ return el;
2536
+ }
2537
+ /** A keyboard-navigable row of groups (the shared `office-ui-toolbar`). */
2538
+ function commandRow(doc, label, children) {
2539
+ const el = doc.createElement("office-ui-toolbar");
2540
+ el.className = "ribbon-row";
2541
+ el.setAttribute("aria-label", label);
2542
+ el.append(...children);
2543
+ return el;
2544
+ }
2545
+ /** Muted two-line guidance at the end of a ribbon panel. */
2546
+ function hint(doc, lines) {
2547
+ const el = doc.createElement("p");
2548
+ el.className = "ribbon-hint";
2549
+ lines.forEach((line, index) => {
2550
+ if (index) el.append(doc.createElement("br"));
2551
+ el.append(line);
2552
+ });
2553
+ return el;
2554
+ }
2555
+ //#endregion
2556
+ //#region src/viewer-search.ts
2557
+ /** Builds the Find group: a search field, previous/next result buttons and a live status. */
2558
+ function createSearchGroup(doc) {
2559
+ const group = doc.createElement("div");
2560
+ group.className = "ribbon-group search-group";
2561
+ group.setAttribute("role", "group");
2562
+ group.setAttribute("aria-label", "Find");
2563
+ const controls = doc.createElement("div");
2564
+ controls.className = "search-controls";
2565
+ controls.setAttribute("role", "search");
2566
+ controls.setAttribute("aria-label", "Diagram text search");
2567
+ const label = doc.createElement("label");
2568
+ label.append("Find ");
2569
+ const input = doc.createElement("input");
2570
+ input.type = "search";
2571
+ input.setAttribute("aria-label", "Search diagram text");
2572
+ input.placeholder = "Text across pages";
2573
+ input.maxLength = TEXT_SEARCH_LIMITS.queryCharacters;
2574
+ input.autocomplete = "off";
2575
+ input.spellcheck = false;
2576
+ input.setAttribute("aria-describedby", "search-status");
2577
+ label.append(input);
2578
+ const step = (action, name, text) => {
2579
+ const button = doc.createElement("button");
2580
+ button.type = "button";
2581
+ button.dataset.action = action;
2582
+ button.setAttribute("aria-label", name);
2583
+ button.textContent = text;
2584
+ return button;
2585
+ };
2586
+ const status = doc.createElement("span");
2587
+ status.id = "search-status";
2588
+ status.setAttribute("role", "status");
2589
+ status.setAttribute("aria-live", "polite");
2590
+ status.setAttribute("aria-atomic", "true");
2591
+ controls.append(label, step("search-previous", "Previous matching shape", "Previous"), step("search-next", "Next matching shape", "Next"), status);
2592
+ const caption = doc.createElement("span");
2593
+ caption.className = "group-label";
2594
+ caption.textContent = "Find in drawing";
2595
+ group.append(controls, caption);
2596
+ return group;
2597
+ }
2598
+ function searchControls(root) {
2599
+ return {
2600
+ input: root.querySelector("input[type=\"search\"]"),
2601
+ status: root.querySelector("#search-status"),
2602
+ previous: root.querySelector("[data-action=\"search-previous\"]"),
2603
+ next: root.querySelector("[data-action=\"search-next\"]")
2604
+ };
2605
+ }
2606
+ function renderSearchControls(controls, state) {
2607
+ if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
2608
+ controls.input.disabled = !state.document?.pages.length;
2609
+ controls.previous.disabled = controls.next.disabled = !state.search.results.length;
2610
+ controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
2611
+ const result = state.search.results[state.search.activeIndex];
2612
+ controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
2613
+ }
2614
+ //#endregion
2615
+ //#region src/ribbon-home.ts
2616
+ /**
2617
+ * Visio's Home tab, limited to commands this build can honestly perform: Undo, Tools and
2618
+ * Editing. Clipboard, Font, Paragraph, Shape Styles and Arrange wait for core edit commands.
2619
+ * The compact (phone) layout folds every group but Undo into the Tools disclosure.
2620
+ */
2621
+ function buildHomePanel(doc, panel) {
2622
+ const tools = doc.createElement("details");
2623
+ tools.className = "ribbon-tools";
2624
+ tools.open = true;
2625
+ const summary = doc.createElement("summary");
2626
+ const caret = doc.createElement("span");
2627
+ caret.setAttribute("aria-hidden", "true");
2628
+ caret.textContent = "⌄";
2629
+ summary.append("Tools ", caret);
2630
+ const content = doc.createElement("div");
2631
+ content.className = "tools-content";
2632
+ content.append(group(doc, "Tools", [
2633
+ tool(doc, "pointer", "Pointer Tool", "pointer", {
2634
+ type: "tool",
2635
+ tool: "pointer"
2636
+ }, {
2637
+ keys: ["Control+1", "Ctrl+1"],
2638
+ pressed: true
2639
+ }),
2640
+ tool(doc, "rectangle", "Rectangle", "rectangle", {
2641
+ type: "tool",
2642
+ tool: "rectangle"
2643
+ }, {
2644
+ keys: ["Control+8", "Ctrl+8"],
2645
+ pressed: false
2646
+ }),
2647
+ tool(doc, "edit", "Edit text", "pencil", {
2648
+ type: "reveal",
2649
+ panel: "edit"
2650
+ }, { keys: ["F2", "F2"] })
2651
+ ]), createSearchGroup(doc), group(doc, "Editing", [
2652
+ tool(doc, "delete", "Delete", "trash", { type: "delete" }, { keys: ["Delete", "Del"] }),
2653
+ tool(doc, "layers", "Layers", "visioLayers", {
2654
+ type: "reveal",
2655
+ panel: "layers"
2656
+ }),
2657
+ tool(doc, "selection", "Shape details", "visioShapeData", {
2658
+ type: "reveal",
2659
+ panel: "selection"
2660
+ })
2661
+ ]));
2662
+ tools.append(summary, content);
2663
+ panel.append(commandRow(doc, "Home commands", [group(doc, "Undo", [tool(doc, "undo", "Undo", "undo", {
2664
+ type: "history",
2665
+ key: "undo"
2666
+ }, { keys: ["Control+Z", "Ctrl+Z"] }), tool(doc, "redo", "Redo", "redo", {
2667
+ type: "history",
2668
+ key: "redo"
2669
+ }, { keys: ["Control+Y", "Ctrl+Y"] })], "undo-group"), tools]), hint(doc, ["Rectangle: drag on the page to draw.", "Editing is experimental. Keep your original file."]));
2670
+ }
2671
+ //#endregion
2672
+ //#region src/ribbon-view.ts
2673
+ /** Visio's View tab: Views, Show (grid and task panes) and Zoom. */
2674
+ function buildViewPanel(doc, panel) {
2675
+ panel.append(commandRow(doc, "View commands", [
2676
+ group(doc, "Views", [tool(doc, "fullscreen", "Full Screen", "fullscreen", { type: "fullscreen" }, {
2677
+ keys: ["F5", "F5"],
2678
+ pressed: false
2679
+ })]),
2680
+ group(doc, "Show", [
2681
+ tool(doc, "grid", "Grid", "grid", { type: "grid" }, { pressed: false }),
2682
+ tool(doc, "pages", "Pages pane", "visioPagesPane", {
2683
+ type: "pane",
2684
+ pane: "pages"
2685
+ }, {
2686
+ pressed: true,
2687
+ controls: "page-rail"
2688
+ }),
2689
+ tool(doc, "inspector", "Inspector pane", "visioInspectorPane", {
2690
+ type: "pane",
2691
+ pane: "inspector"
2692
+ }, {
2693
+ pressed: true,
2694
+ controls: "inspector-pane"
2695
+ }),
2696
+ tool(doc, "notes", "Review notes", "message", {
2697
+ type: "reveal",
2698
+ panel: "notes"
2699
+ })
2700
+ ]),
2701
+ group(doc, "Zoom", [
2702
+ tool(doc, "zoom-fit", "Fit to Window", "fitPage", {
2703
+ type: "zoom",
2704
+ mode: "fit"
2705
+ }, { keys: ["Control+Shift+W", "Ctrl+Shift+W"] }),
2706
+ tool(doc, "page-width", "Page Width", "pageWidth", {
2707
+ type: "zoom",
2708
+ mode: "width"
2709
+ }),
2710
+ tool(doc, "actual-size", "100%", "search", {
2711
+ type: "zoom",
2712
+ mode: "actual"
2713
+ })
2714
+ ])
2715
+ ]), hint(doc, ["Ctrl+wheel or + / − to zoom.", "Ctrl+Page Up / Page Down changes page."]));
2716
+ }
2717
+ //#endregion
2718
+ //#region src/ribbon.ts
2719
+ var TABS = [[
2720
+ "home",
2721
+ "Home",
2722
+ buildHomePanel
2723
+ ], [
2724
+ "view",
2725
+ "View",
2726
+ buildViewPanel
2727
+ ]];
2728
+ /**
2729
+ * The Visio ribbon: a tab list and one panel per tab. Tab switching and keyboard movement
2730
+ * belong to the chrome; commands emit `ribbon-action` events for the router.
2731
+ */
2732
+ function createRibbon(doc) {
2733
+ const toolbar = doc.createElement("div");
2734
+ toolbar.className = "toolbar";
2735
+ toolbar.setAttribute("role", "group");
2736
+ toolbar.setAttribute("aria-label", "Diagram controls");
2737
+ const tabs = doc.createElement("div");
2738
+ tabs.className = "ribbon-tabs";
2739
+ tabs.setAttribute("role", "tablist");
2740
+ tabs.setAttribute("aria-label", "Ribbon");
2741
+ toolbar.append(tabs);
2742
+ for (const [key, name, build] of TABS) {
2743
+ const selected = key === "home";
2744
+ const tab = doc.createElement("button");
2745
+ tab.type = "button";
2746
+ tab.id = `${key}-tab`;
2747
+ tab.dataset.tab = key;
2748
+ tab.textContent = name;
2749
+ tab.setAttribute("role", "tab");
2750
+ tab.setAttribute("aria-selected", String(selected));
2751
+ tab.setAttribute("aria-controls", `${key}-panel`);
2752
+ if (!selected) tab.tabIndex = -1;
2753
+ const panel = doc.createElement("div");
2754
+ panel.className = "ribbon-content";
2755
+ panel.id = `${key}-panel`;
2756
+ panel.setAttribute("role", "tabpanel");
2757
+ panel.setAttribute("aria-labelledby", tab.id);
2758
+ panel.hidden = !selected;
2759
+ build(doc, panel);
2760
+ tabs.append(tab);
2761
+ toolbar.append(panel);
2762
+ }
2763
+ return toolbar;
2764
+ }
2765
+ //#endregion
2766
+ //#region src/status-bar.ts
2767
+ /**
2768
+ * Visio's status bar on the shared `office-ui-status-bar`: page and shape metrics, a live
2769
+ * message region, compatibility notes, the host footer slot and the shared zoom slider.
2770
+ */
2771
+ function createStatusBar(doc) {
2772
+ const bar = doc.createElement("office-ui-status-bar");
2773
+ bar.className = "status";
2774
+ bar.setAttribute("label", "Status bar");
2775
+ const item = (name) => {
2776
+ const el = doc.createElement("office-ui-status-item");
2777
+ el.setAttribute(name, "");
2778
+ el.setAttribute("label", "");
2779
+ el.setAttribute("value", "");
2780
+ return el;
2781
+ };
2782
+ const message = doc.createElement("div");
2783
+ message.className = "status-message";
2784
+ message.setAttribute("role", "status");
2785
+ const text = doc.createElement("span");
2786
+ text.dataset.status = "";
2787
+ const diagnostics = doc.createElement("span");
2788
+ diagnostics.dataset.diagnostics = "";
2789
+ message.append(text, diagnostics);
2790
+ const notes = doc.createElement("div");
2791
+ notes.className = "notes-strip";
2792
+ const notesButton = doc.createElement("button");
2793
+ notesButton.type = "button";
2794
+ notesButton.dataset.chrome = "notes";
2795
+ notesButton.setAttribute("aria-controls", "inspector-pane");
2796
+ notesButton.setAttribute("aria-expanded", "false");
2797
+ const count = doc.createElement("span");
2798
+ count.className = "notes-count";
2799
+ count.dataset.noteCount = "";
2800
+ notesButton.append("Notes", count);
2801
+ const local = doc.createElement("span");
2802
+ local.textContent = "Files stay in your browser";
2803
+ notes.append(notesButton, local);
2804
+ const footer = doc.createElement("slot");
2805
+ footer.name = "workspace-footer";
2806
+ const zoom = doc.createElement("office-ui-zoom-slider");
2807
+ zoom.slot = "end";
2808
+ zoom.className = "zoom-controls";
2809
+ zoom.setAttribute("label", "Canvas zoom");
2810
+ zoom.setAttribute("fit", "Fit page to current window");
2811
+ zoom.setAttribute("value", "100");
2812
+ bar.append(item("data-page-status"), item("data-shape-status"), message, notes, footer, zoom);
2813
+ return bar;
2814
+ }
2815
+ /** Visio's page tabs under the drawing, on the shared document tab strip. */
2816
+ function createPageTabs(doc) {
2817
+ const strip = doc.createElement("office-ui-tab-strip");
2818
+ strip.className = "page-tabs";
2819
+ strip.setAttribute("label", "Pages");
2820
+ strip.setAttribute("previous-label", "Previous page");
2821
+ strip.setAttribute("next-label", "Next page");
2822
+ return strip;
2823
+ }
2824
+ //#endregion
2539
2825
  //#region src/edit-error.ts
2540
2826
  /** Core error codes remain visible for a refused transaction; document content is plain text. */
2541
2827
  function editErrorMessage(error) {
@@ -2823,59 +3109,13 @@ var ViewerEditControls = class {
2823
3109
  }
2824
3110
  };
2825
3111
  //#endregion
2826
- //#region src/viewer-search.ts
2827
- 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>`;
2828
- function searchControls(root) {
2829
- return {
2830
- input: root.querySelector("input[type=\"search\"]"),
2831
- status: root.querySelector("#search-status"),
2832
- previous: root.querySelector("[data-action=\"search-previous\"]"),
2833
- next: root.querySelector("[data-action=\"search-next\"]")
2834
- };
2835
- }
2836
- function renderSearchControls(controls, state) {
2837
- if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
2838
- controls.input.disabled = !state.document?.pages.length;
2839
- controls.previous.disabled = controls.next.disabled = !state.search.results.length;
2840
- controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
2841
- const result = state.search.results[state.search.activeIndex];
2842
- controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
2843
- }
2844
- //#endregion
2845
3112
  //#region src/viewer-chrome.ts
2846
- /** Only static application markup belongs here. All document labels are inserted as text. */
2847
- var viewerChromeTemplate = `
2848
- <div class="toolbar" role="group" aria-label="Diagram controls">
2849
-
2850
- <div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
2851
- <button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
2852
- <button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
2853
- </div>
2854
- <div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
2855
- <details class="ribbon-tools" open><summary>Tools <span aria-hidden="true">⌄</span></summary><div class="tools-content"><div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
2856
- <div class="ribbon-group"><div class="ribbon-actions">
2857
- <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>
2858
- <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>
2859
- <button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
2860
- </div><span class="group-label">Inspect and edit</span></div>
2861
- </div></details>
2862
- <div class="ribbon-primary">
2863
- <label class="page-picker">Page <select aria-label="Page"></select></label>
2864
- <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>
2865
- <span class="spacer"></span><span class="local-label">Local workspace</span>
2866
- </div>
2867
- <p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
2868
- </div>
2869
- <div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
2870
- <div class="ribbon-group"><div class="ribbon-actions">
2871
- <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>
2872
- <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>
2873
- <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>
2874
- </div><span class="group-label">Workspace panes</span></div>
2875
- <p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
2876
- </div>
2877
- </div>
2878
- <div class="workspace">
3113
+ /**
3114
+ * Static workspace markup (legacy; migrate to builders when next changed). The ribbon, page
3115
+ * tabs and status bar are built by `createRibbon`, `createPageTabs` and `createStatusBar`.
3116
+ * All document labels are inserted as text.
3117
+ */
3118
+ var viewerChromeTemplate = `<div class="workspace">
2879
3119
  <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>
2880
3120
  <div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
2881
3121
  <aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
@@ -2888,14 +3128,15 @@ var viewerChromeTemplate = `
2888
3128
  <details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
2889
3129
  </div>
2890
3130
  </aside>
2891
- </div>
2892
- <div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><div class="notes-strip"><button type="button" data-chrome="notes" aria-controls="inspector-pane" aria-expanded="false">Notes<span class="notes-count" data-note-count></span></button><span>Files stay in your browser</span></div><slot name="workspace-footer"></slot><div class="zoom-controls" role="group" aria-label="Canvas zoom"><button type="button" data-action="fit">Fit page</button><button type="button" data-action="actual">100%</button><span class="zoom-divider"></span><button type="button" data-action="out" aria-label="Zoom out">−</button><output class="zoom" aria-label="Zoom level">100%</output><button type="button" data-action="in" aria-label="Zoom in">+</button></div></div>`;
3131
+ </div>`;
2893
3132
  /** Presentation state stays local; page navigation uses the same controller as every binding. */
2894
3133
  var ViewerChrome = class {
2895
3134
  #root;
2896
3135
  #controller;
2897
3136
  #document;
2898
3137
  #pageList;
3138
+ #pageTabs;
3139
+ #tools;
2899
3140
  #rail;
2900
3141
  #inspector;
2901
3142
  #notes;
@@ -2907,6 +3148,8 @@ var ViewerChrome = class {
2907
3148
  this.#root = root;
2908
3149
  this.#controller = controller;
2909
3150
  this.#pageList = root.querySelector(".page-list");
3151
+ this.#pageTabs = root.querySelector("office-ui-tab-strip");
3152
+ this.#tools = root.querySelector(".ribbon-tools");
2910
3153
  this.#rail = root.querySelector(".page-rail");
2911
3154
  this.#inspector = root.querySelector(".inspector-pane");
2912
3155
  this.#notes = root.querySelector(".notes");
@@ -2917,7 +3160,7 @@ var ViewerChrome = class {
2917
3160
  this.#responsive = view?.matchMedia?.("(max-width: 980px)");
2918
3161
  const compact = view?.matchMedia?.("(max-width: 760px)");
2919
3162
  this.#compact = compact;
2920
- const tools = this.#root.querySelector(".ribbon-tools");
3163
+ const tools = this.#tools;
2921
3164
  const toolLayout = () => {
2922
3165
  tools.open = !compact?.matches;
2923
3166
  };
@@ -2944,22 +3187,16 @@ var ViewerChrome = class {
2944
3187
  this.#root.addEventListener("click", (event) => {
2945
3188
  const button = event.target?.closest?.("button");
2946
3189
  if (!button || button.disabled) return;
2947
- if (compact?.matches && button.dataset.chrome) tools.open = false;
2948
3190
  const tab = button.dataset.tab;
2949
- if (tab === "home" || tab === "view") this.#showTab(tab);
3191
+ if (tab === "home" || tab === "view") this.showTab(tab);
2950
3192
  if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
2951
3193
  const action = button.dataset.chrome;
2952
- if (action === "previous-page") this.#controller.setPage(this.#controller.state.pageIndex - 1);
2953
- if (action === "next-page") this.#controller.setPage(this.#controller.state.pageIndex + 1);
2954
- if (action === "pages") {
2955
- this.#pagesManuallyToggled = true;
2956
- this.#setPane("pages", this.#rail.hidden);
2957
- }
2958
- if (action === "inspector") {
2959
- this.#inspectorManuallyToggled = true;
2960
- this.#setPane("inspector", this.#inspector.hidden);
2961
- }
2962
- if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
3194
+ if (action === "inspector") this.togglePane("inspector");
3195
+ if (action === "notes") this.reveal("notes");
3196
+ }, options);
3197
+ this.#root.addEventListener("office-tab-select", (event) => {
3198
+ const id = Number(event.detail.id);
3199
+ if (Number.isSafeInteger(id)) this.#controller.setPage(id);
2963
3200
  }, options);
2964
3201
  this.#root.addEventListener("keydown", (event) => {
2965
3202
  const key = event;
@@ -2976,11 +3213,11 @@ var ViewerChrome = class {
2976
3213
  if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
2977
3214
  key.preventDefault();
2978
3215
  const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
2979
- this.#showTab(tab);
3216
+ this.showTab(tab);
2980
3217
  this.#root.querySelector(`[data-tab="${tab}"]`).focus();
2981
3218
  } else if (button.dataset.pageIndex !== void 0) {
2982
3219
  key.preventDefault();
2983
- const pages = [...this.#pageList.querySelectorAll("button")];
3220
+ const pages = [...this.#pageList.querySelectorAll("[data-page-index]")];
2984
3221
  const current = Number(button.dataset.pageIndex);
2985
3222
  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)));
2986
3223
  this.#controller.setPage(next);
@@ -2990,7 +3227,7 @@ var ViewerChrome = class {
2990
3227
  this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
2991
3228
  return () => events.abort();
2992
3229
  }
2993
- #showTab(tab) {
3230
+ showTab(tab) {
2994
3231
  for (const button of this.#root.querySelectorAll("[data-tab]")) {
2995
3232
  const selected = button.dataset.tab === tab;
2996
3233
  button.setAttribute("aria-selected", String(selected));
@@ -2998,27 +3235,41 @@ var ViewerChrome = class {
2998
3235
  }
2999
3236
  for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
3000
3237
  }
3238
+ /** Show or hide a task pane; compact layouts close the Tools menu afterwards. */
3239
+ togglePane(pane) {
3240
+ if (this.#compact?.matches) this.#tools.open = false;
3241
+ if (pane === "pages") this.#pagesManuallyToggled = true;
3242
+ else this.#inspectorManuallyToggled = true;
3243
+ this.#setPane(pane, (pane === "pages" ? this.#rail : this.#inspector).hidden);
3244
+ }
3245
+ #command(name) {
3246
+ return this.#root.querySelector(`office-ui-button[command="${name}"]`);
3247
+ }
3001
3248
  #setPane(pane, visible) {
3002
3249
  if (visible && this.#compact?.matches) {
3003
3250
  const other = pane === "pages" ? "inspector" : "pages";
3004
3251
  (other === "pages" ? this.#rail : this.#inspector).hidden = true;
3005
- this.#root.querySelector(`[data-chrome="${other}"]`).setAttribute("aria-pressed", "false");
3252
+ this.#command(other).setAttribute("pressed", "false");
3006
3253
  }
3007
3254
  (pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
3008
- this.#root.querySelector(`[data-chrome="${pane}"]`).setAttribute("aria-pressed", String(visible));
3255
+ this.#command(pane).setAttribute("pressed", String(visible));
3009
3256
  this.#syncNotes();
3010
3257
  }
3011
3258
  #syncNotes() {
3012
3259
  this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
3013
3260
  }
3014
- #reveal(kind) {
3261
+ /** Open an inspector disclosure; text editing may focus the text field directly (F2). */
3262
+ reveal(kind, focusText = false) {
3263
+ if (this.#compact?.matches) this.#tools.open = false;
3015
3264
  this.#inspectorManuallyToggled = true;
3016
3265
  this.#setPane("inspector", true);
3017
3266
  const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
3018
3267
  const panel = this.#root.querySelector(selector);
3019
3268
  if (panel.hidden) return;
3020
3269
  panel.open = true;
3021
- panel.querySelector("summary").focus();
3270
+ const text = panel.querySelector("textarea");
3271
+ if (focusText && text && !text.disabled) text.focus();
3272
+ else panel.querySelector("summary").focus();
3022
3273
  panel.scrollIntoView?.({
3023
3274
  block: "nearest",
3024
3275
  inline: "nearest"
@@ -3059,14 +3310,21 @@ var ViewerChrome = class {
3059
3310
  fragment.append(item);
3060
3311
  }
3061
3312
  this.#pageList.replaceChildren(fragment);
3313
+ this.#pageTabs.tabs = (state.document?.pages ?? []).map((candidate, index) => ({
3314
+ id: String(index),
3315
+ label: candidate.name,
3316
+ title: candidate.isBackground ? `${candidate.name} (background page)` : candidate.name
3317
+ }));
3062
3318
  if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
3063
3319
  }
3320
+ this.#pageTabs.selected = page ? String(state.pageIndex) : "";
3064
3321
  for (const button of this.#pageList.querySelectorAll("button")) {
3065
3322
  const selected = Number(button.dataset.pageIndex) === state.pageIndex;
3066
3323
  if (selected) button.setAttribute("aria-current", "page");
3067
3324
  else button.removeAttribute("aria-current");
3068
3325
  button.tabIndex = selected ? 0 : -1;
3069
3326
  }
3327
+ this.#root.querySelector("[data-page-status]").setAttribute("value", page ? `Page ${state.pageIndex + 1} of ${state.document.pages.length}` : "");
3070
3328
  const count = state.document?.pages.length ?? 0;
3071
3329
  this.#root.querySelector("[data-page-count]").textContent = String(count);
3072
3330
  this.#root.querySelector(".page-empty").hidden = count > 0;
@@ -3074,17 +3332,405 @@ var ViewerChrome = class {
3074
3332
  this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
3075
3333
  this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
3076
3334
  this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
3077
- this.#root.querySelector("[data-chrome=\"previous-page\"]").disabled = !page || state.pageIndex === 0;
3078
- this.#root.querySelector("[data-chrome=\"next-page\"]").disabled = !page || state.pageIndex >= count - 1;
3079
- this.#root.querySelector("[data-chrome=\"selection\"]").disabled = !state.selectedShape;
3080
- this.#root.querySelector("[data-chrome=\"edit\"]").disabled = !page;
3081
- this.#root.querySelector("[data-chrome=\"layers\"]").disabled = this.#root.querySelector(".layer-controls").hidden;
3082
- for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
3335
+ this.#command("selection").disabled = !state.selectedShape;
3336
+ this.#command("edit").disabled = !page;
3337
+ this.#command("layers").disabled = this.#root.querySelector(".layer-controls").hidden;
3338
+ this.#command("notes").disabled = noteCount === 0;
3339
+ this.#root.querySelector(".notes-strip button").disabled = noteCount === 0;
3083
3340
  this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
3084
3341
  this.#syncNotes();
3085
3342
  }
3086
3343
  };
3087
3344
  //#endregion
3345
+ //#region src/ribbon-router.ts
3346
+ /** Routes a ribbon, status-bar or shortcut action to the controller that owns it. */
3347
+ function routeRibbonAction(targets, action) {
3348
+ const { controller } = targets;
3349
+ const page = controller.state.document?.pages[controller.state.pageIndex];
3350
+ switch (action.type) {
3351
+ case "history": return targets.history(action.key);
3352
+ case "delete": return targets.deleteSelection();
3353
+ case "tool": return targets.setTool(action.tool);
3354
+ case "grid": return targets.toggleGrid();
3355
+ case "fullscreen": return targets.toggleFullscreen();
3356
+ case "pane": return targets.togglePane(action.pane);
3357
+ case "reveal": return targets.reveal(action.panel, action.focusText ?? false);
3358
+ case "search": return targets.focusSearch();
3359
+ case "page": return controller.setPage(controller.state.pageIndex + action.step);
3360
+ case "zoom":
3361
+ if (!page) return;
3362
+ if (action.mode === "actual") return controller.setZoom(1);
3363
+ return targets.fit(action.mode === "fit" ? "page" : "width");
3364
+ }
3365
+ }
3366
+ //#endregion
3367
+ //#region src/viewer-draw-tool.ts
3368
+ /** Visio snaps new geometry to ruler subdivisions; 1/16 inch matches its default fine grid. */
3369
+ var SNAP = 1 / 16;
3370
+ var MIN_SIZE = SNAP;
3371
+ var snap = (value) => Math.round(value / SNAP) * SNAP;
3372
+ /** Next free numeric shape ID on the page tree. Core still rejects any collision. */
3373
+ function nextShapeId(page) {
3374
+ let max = 0;
3375
+ const pending = [...page.shapes];
3376
+ while (pending.length) {
3377
+ const shape = pending.pop();
3378
+ if (/^\d{1,9}$/.test(shape.id)) max = Math.max(max, Number(shape.id));
3379
+ pending.push(...shape.children);
3380
+ }
3381
+ return String(max + 1);
3382
+ }
3383
+ /** Page inches with a top-left origin (SVG user space) from a client point. */
3384
+ function pagePoint(svg, page, event) {
3385
+ const matrix = svg.getScreenCTM?.();
3386
+ if (!matrix) return void 0;
3387
+ const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse());
3388
+ return {
3389
+ x: snap(Math.max(0, Math.min(page.width, point.x))),
3390
+ y: snap(Math.max(0, Math.min(page.height, point.y)))
3391
+ };
3392
+ }
3393
+ /** Drag-to-draw rectangle tool. The preview is viewer-only; the shape is created by core. */
3394
+ var RectangleDrawTool = class {
3395
+ viewport;
3396
+ controller;
3397
+ options;
3398
+ #drag;
3399
+ #request = 0;
3400
+ constructor(viewport, controller, options) {
3401
+ this.viewport = viewport;
3402
+ this.controller = controller;
3403
+ this.options = options;
3404
+ }
3405
+ get drawing() {
3406
+ return !!this.#drag;
3407
+ }
3408
+ wire() {
3409
+ const events = new ((this.viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
3410
+ const options = { signal: events.signal };
3411
+ const viewport = this.viewport;
3412
+ viewport.addEventListener("click", (event) => {
3413
+ if (this.options.active()) event.stopImmediatePropagation();
3414
+ }, {
3415
+ ...options,
3416
+ capture: true
3417
+ });
3418
+ viewport.addEventListener("pointerdown", (event) => this.#start(event), options);
3419
+ viewport.addEventListener("pointermove", (event) => this.#move(event), options);
3420
+ viewport.addEventListener("pointerup", (event) => void this.#finish(event), options);
3421
+ viewport.addEventListener("pointercancel", () => this.#cancel(), options);
3422
+ viewport.addEventListener("keydown", (event) => {
3423
+ if (event.key === "Escape" && this.#drag) {
3424
+ event.preventDefault();
3425
+ this.#cancel();
3426
+ }
3427
+ }, options);
3428
+ return () => {
3429
+ ++this.#request;
3430
+ this.#cancel();
3431
+ events.abort();
3432
+ };
3433
+ }
3434
+ #start(event) {
3435
+ if (!this.options.active() || event.button !== 0 || this.#drag) return;
3436
+ const state = this.controller.state;
3437
+ const page = state.document?.pages[state.pageIndex];
3438
+ const svg = this.viewport.querySelector("svg.paper");
3439
+ if (!page || !svg || !state.edit.sourceAvailable || state.edit.busy) return;
3440
+ const start = pagePoint(svg, page, event);
3441
+ if (!start) return;
3442
+ event.preventDefault();
3443
+ const rect = this.viewport.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "rect");
3444
+ rect.classList.add("draw-preview");
3445
+ rect.setAttribute("vector-effect", "non-scaling-stroke");
3446
+ svg.append(rect);
3447
+ this.#drag = {
3448
+ pointer: event.pointerId,
3449
+ svg,
3450
+ page,
3451
+ ...start,
3452
+ rect
3453
+ };
3454
+ this.viewport.setPointerCapture?.(event.pointerId);
3455
+ this.#update(start);
3456
+ }
3457
+ #move(event) {
3458
+ if (!this.#drag || event.pointerId !== this.#drag.pointer) return;
3459
+ const point = pagePoint(this.#drag.svg, this.#drag.page, event);
3460
+ if (point) this.#update(point);
3461
+ }
3462
+ #update(point) {
3463
+ const { x, y, rect } = this.#drag;
3464
+ rect.setAttribute("x", String(Math.min(x, point.x)));
3465
+ rect.setAttribute("y", String(Math.min(y, point.y)));
3466
+ rect.setAttribute("width", String(Math.abs(point.x - x)));
3467
+ rect.setAttribute("height", String(Math.abs(point.y - y)));
3468
+ }
3469
+ #cancel() {
3470
+ this.#drag?.rect.remove();
3471
+ this.#drag = void 0;
3472
+ }
3473
+ async #finish(event) {
3474
+ const drag = this.#drag;
3475
+ if (!drag || event.pointerId !== drag.pointer) return;
3476
+ const end = pagePoint(drag.svg, drag.page, event) ?? drag;
3477
+ this.#cancel();
3478
+ const width = Math.abs(end.x - drag.x), height = Math.abs(end.y - drag.y);
3479
+ if (width < MIN_SIZE || height < MIN_SIZE) {
3480
+ this.options.announce("Drag on the page to draw a rectangle.");
3481
+ return;
3482
+ }
3483
+ const state = this.controller.state;
3484
+ if (state.document?.pages[state.pageIndex] !== drag.page) return;
3485
+ const shapeId = nextShapeId(drag.page);
3486
+ const pageId = drag.page.id;
3487
+ const request = ++this.#request;
3488
+ try {
3489
+ await this.controller.applyEdits([{
3490
+ type: "create-rectangle",
3491
+ pageId,
3492
+ shapeId,
3493
+ x: (drag.x + end.x) / 2,
3494
+ y: drag.page.height - (drag.y + end.y) / 2,
3495
+ width,
3496
+ height
3497
+ }]);
3498
+ if (request !== this.#request) return;
3499
+ const created = this.controller.state.document?.pages.find((page) => page.id === pageId)?.shapes.find((shape) => shape.id === shapeId);
3500
+ if (created) this.controller.selectShape({
3501
+ id: shapeId,
3502
+ name: created.name,
3503
+ pageId
3504
+ });
3505
+ this.options.announce(`Rectangle ${shapeId} added (${+width.toFixed(4)} × ${+height.toFixed(4)} in).`);
3506
+ } catch (error) {
3507
+ if (request === this.#request && !isEditCancellation(error)) this.options.announce(editErrorMessage(error));
3508
+ }
3509
+ }
3510
+ };
3511
+ //#endregion
3512
+ //#region src/viewer-commands.ts
3513
+ var editable = (target) => target instanceof Element && !!target.closest("input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])");
3514
+ /**
3515
+ * Visio command state (tool, grid, pending edits) and keyboard shortcuts. Ribbon controls emit
3516
+ * typed `ribbon-action` events and shortcuts build the same actions; both go through
3517
+ * `routeRibbonAction`. Document mutation stays in the controller and core.
3518
+ */
3519
+ var ViewerCommands = class {
3520
+ host;
3521
+ #tool = "pointer";
3522
+ #grid = false;
3523
+ #pending = 0;
3524
+ #draw;
3525
+ #targets;
3526
+ constructor(host) {
3527
+ this.host = host;
3528
+ this.#draw = new RectangleDrawTool(host.viewport, host.controller, {
3529
+ active: () => this.#tool === "rectangle",
3530
+ announce: host.announce
3531
+ });
3532
+ this.#targets = {
3533
+ controller: host.controller,
3534
+ history: (key) => this.#history(key),
3535
+ deleteSelection: () => this.#delete(),
3536
+ setTool: (tool) => this.setTool(tool),
3537
+ toggleGrid: () => {
3538
+ this.#grid = !this.#grid;
3539
+ this.render(host.controller.state);
3540
+ },
3541
+ toggleFullscreen: () => this.#toggleFullscreen(),
3542
+ togglePane: host.togglePane,
3543
+ reveal: host.reveal,
3544
+ fit: host.fit,
3545
+ focusSearch: host.focusSearch
3546
+ };
3547
+ }
3548
+ get tool() {
3549
+ return this.#tool;
3550
+ }
3551
+ /** Run one typed action, exactly as a ribbon control or shortcut would. */
3552
+ run(action) {
3553
+ routeRibbonAction(this.#targets, action);
3554
+ }
3555
+ wire() {
3556
+ const { root, viewport } = this.host;
3557
+ const doc = root.ownerDocument;
3558
+ const events = new ((doc.defaultView?.AbortController) ?? AbortController)();
3559
+ const options = { signal: events.signal };
3560
+ root.addEventListener(RIBBON_ACTION_EVENT, (event) => this.run(event.detail), options);
3561
+ root.addEventListener("office-command", (event) => {
3562
+ if (event.detail?.command === "zoom-fit") this.run({
3563
+ type: "zoom",
3564
+ mode: "fit"
3565
+ });
3566
+ }, options);
3567
+ root.addEventListener("keydown", (event) => this.#shortcut(event), options);
3568
+ viewport.addEventListener("wheel", (event) => {
3569
+ if (!event.ctrlKey && !event.metaKey) return;
3570
+ event.preventDefault();
3571
+ const zoom = this.host.controller.state.zoom;
3572
+ this.host.controller.setZoom(event.deltaY < 0 ? zoom * 1.1 : zoom / 1.1);
3573
+ }, {
3574
+ ...options,
3575
+ passive: false
3576
+ });
3577
+ viewport.addEventListener("pointerdown", () => {
3578
+ if (!viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
3579
+ }, options);
3580
+ viewport.addEventListener("dblclick", (event) => {
3581
+ if (this.#tool !== "pointer") return;
3582
+ if (event.target?.closest?.("[data-shape-id]")) this.run({
3583
+ type: "reveal",
3584
+ panel: "edit",
3585
+ focusText: true
3586
+ });
3587
+ }, options);
3588
+ doc.addEventListener("fullscreenchange", () => this.render(this.host.controller.state), options);
3589
+ const disposeDraw = this.#draw.wire();
3590
+ return () => {
3591
+ ++this.#pending;
3592
+ events.abort();
3593
+ disposeDraw();
3594
+ };
3595
+ }
3596
+ setTool(tool) {
3597
+ if (tool === "rectangle" && !this.#canEdit(this.host.controller.state)) return;
3598
+ this.#tool = tool;
3599
+ this.render(this.host.controller.state);
3600
+ }
3601
+ #canEdit(state) {
3602
+ return state.edit.sourceAvailable && !state.loading && !state.edit.busy;
3603
+ }
3604
+ #history(key) {
3605
+ const { controller } = this.host;
3606
+ const { edit, loading } = controller.state;
3607
+ if ((key === "undo" ? edit.canUndo : edit.canRedo) && !edit.busy && !loading) this.#edit(() => controller[key]());
3608
+ }
3609
+ #delete() {
3610
+ const state = this.host.controller.state;
3611
+ const shape = state.selectedShape;
3612
+ const pageId = shape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
3613
+ if (!shape || pageId === void 0 || !this.#canEdit(state)) return;
3614
+ this.#edit(() => this.host.controller.applyEdits([{
3615
+ type: "delete-shape",
3616
+ pageId,
3617
+ shapeId: shape.id
3618
+ }]), `Deleted ${shape.name || `shape ${shape.id}`}.`);
3619
+ }
3620
+ async #edit(action, success) {
3621
+ const request = ++this.#pending;
3622
+ const { root, viewport } = this.host;
3623
+ const canvasFocused = viewport.contains(root.activeElement);
3624
+ try {
3625
+ await action();
3626
+ if (canvasFocused && !viewport.contains(root.activeElement)) viewport.focus({ preventScroll: true });
3627
+ if (success && request === this.#pending) this.host.announce(success);
3628
+ } catch (error) {
3629
+ if (request === this.#pending && !isEditCancellation(error) && !this.host.controller.state.edit.error) this.host.announce(editErrorMessage(error));
3630
+ }
3631
+ }
3632
+ #toggleFullscreen() {
3633
+ const host = this.host.root.host;
3634
+ const doc = this.host.root.ownerDocument;
3635
+ (doc.fullscreenElement === host ? doc.exitFullscreen?.() : host.requestFullscreen?.())?.catch?.(() => this.host.announce("Full screen is not available in this browser frame."));
3636
+ }
3637
+ /** Visio shortcuts as typed actions. Text fields keep native undo, deletion and editing keys. */
3638
+ #shortcutAction(event) {
3639
+ const state = this.host.controller.state;
3640
+ const control = event.ctrlKey || event.metaKey;
3641
+ const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
3642
+ if (control && key === "f") return { type: "search" };
3643
+ if (control && event.shiftKey && key === "w") return {
3644
+ type: "zoom",
3645
+ mode: "fit"
3646
+ };
3647
+ if (control && (key === "PageDown" || key === "PageUp")) return {
3648
+ type: "page",
3649
+ step: key === "PageDown" ? 1 : -1
3650
+ };
3651
+ if (key === "F5" && !control) return { type: "fullscreen" };
3652
+ if (editable(event.target)) return void 0;
3653
+ if (control && !event.shiftKey && key === "z") return {
3654
+ type: "history",
3655
+ key: "undo"
3656
+ };
3657
+ if (control && (key === "y" || event.shiftKey && key === "z")) return {
3658
+ type: "history",
3659
+ key: "redo"
3660
+ };
3661
+ if (control && key === "1") return {
3662
+ type: "tool",
3663
+ tool: "pointer"
3664
+ };
3665
+ if (control && key === "8") return {
3666
+ type: "tool",
3667
+ tool: "rectangle"
3668
+ };
3669
+ if (!control && key === "Delete" && state.selectedShape) return { type: "delete" };
3670
+ if (!control && key === "F2" && state.document) return {
3671
+ type: "reveal",
3672
+ panel: "edit",
3673
+ focusText: true
3674
+ };
3675
+ if (key === "Escape" && this.#tool !== "pointer" && !this.#draw.drawing) return {
3676
+ type: "tool",
3677
+ tool: "pointer"
3678
+ };
3679
+ }
3680
+ #shortcut(event) {
3681
+ if (event.defaultPrevented || event.isComposing || event.altKey) return;
3682
+ const action = this.#shortcutAction(event);
3683
+ if (!action) return;
3684
+ event.preventDefault();
3685
+ this.run(action);
3686
+ }
3687
+ render(state) {
3688
+ const { root, viewport } = this.host;
3689
+ const button = (name) => root.querySelector(`office-ui-button[command="${name}"]`);
3690
+ const editing = this.#canEdit(state);
3691
+ if (this.#tool === "rectangle" && !state.edit.sourceAvailable) this.#tool = "pointer";
3692
+ const page = state.document?.pages[state.pageIndex];
3693
+ button("undo").disabled = !state.edit.canUndo || state.edit.busy || state.loading;
3694
+ button("redo").disabled = !state.edit.canRedo || state.edit.busy || state.loading;
3695
+ button("delete").disabled = !editing || !state.selectedShape;
3696
+ button("pointer").setAttribute("pressed", String(this.#tool === "pointer"));
3697
+ const rectangle = button("rectangle");
3698
+ rectangle.setAttribute("pressed", String(this.#tool === "rectangle"));
3699
+ rectangle.disabled = !editing || !page;
3700
+ rectangle.title = state.edit.sourceAvailable ? "Rectangle (Ctrl+8)" : "Rectangle (Ctrl+8): open a .vsdx file to draw. Model-only documents are read only.";
3701
+ button("grid").setAttribute("pressed", String(this.#grid));
3702
+ button("grid").disabled = !page;
3703
+ for (const name of [
3704
+ "zoom-fit",
3705
+ "page-width",
3706
+ "actual-size"
3707
+ ]) button(name).disabled = !page;
3708
+ const fullscreen = button("fullscreen");
3709
+ const host = root.host;
3710
+ fullscreen.disabled = typeof host.requestFullscreen !== "function";
3711
+ fullscreen.setAttribute("pressed", String(root.ownerDocument.fullscreenElement === host));
3712
+ viewport.dataset.tool = this.#tool;
3713
+ viewport.dataset.grid = String(this.#grid);
3714
+ }
3715
+ };
3716
+ //#endregion
3717
+ //#region src/office-ui.ts
3718
+ /**
3719
+ * Visio-only glyphs (20x20 path data). Neutral Office glyphs come from ooxml-ui; add a glyph
3720
+ * there instead when another Office product could use it.
3721
+ */
3722
+ var VISIO_ICONS = {
3723
+ 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",
3724
+ visioShapeData: "M3 3.5h14v13H3ZM3 7.5h14M8 7.5v9M10.5 11h4M10.5 14h4",
3725
+ visioPagesPane: "M2.5 3h15v14h-15ZM7 3v14M3.8 6h1.9M3.8 9h1.9M3.8 12h1.9",
3726
+ visioInspectorPane: "M2.5 3h15v14h-15ZM13 3v14M14.5 6h1.5M14.5 9h1.5M14.5 12h1.5"
3727
+ };
3728
+ /** Define the shared Office controls the viewer chrome uses. Idempotent; browser only. */
3729
+ function registerViewerControls(registry) {
3730
+ for (const [name, path] of Object.entries(VISIO_ICONS)) if (!getIcon(name)) registerIcon(name, path);
3731
+ registerControls(registry);
3732
+ }
3733
+ //#endregion
3088
3734
  //#region src/export-svg.ts
3089
3735
  var MAX_SVG_EXPORT_BYTES = 16777216;
3090
3736
  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.";
@@ -3695,8 +4341,10 @@ var VisioViewerElement = class extends BaseElement {
3695
4341
  #root;
3696
4342
  #disposeInputs;
3697
4343
  #viewport;
3698
- #pageSelect;
3699
- #zoomLabel;
4344
+ #zoomSlider;
4345
+ #shapeStatus;
4346
+ #announcement;
4347
+ #commands;
3700
4348
  #status;
3701
4349
  #diagnostics;
3702
4350
  #toolbar;
@@ -3729,10 +4377,14 @@ var VisioViewerElement = class extends BaseElement {
3729
4377
  constructor() {
3730
4378
  super();
3731
4379
  this.#root = this.attachShadow({ mode: "open" });
3732
- this.#root.innerHTML = `<style>${viewerStyles}</style>${viewerChromeTemplate}`;
4380
+ registerViewerControls();
4381
+ this.#root.innerHTML = `<style>${viewerStyles}${canvasAndRibbonStyles}</style>${viewerChromeTemplate}`;
4382
+ const workspace = this.#root.querySelector(".workspace");
4383
+ workspace.before(createRibbon(document));
4384
+ workspace.after(createPageTabs(document), createStatusBar(document));
3733
4385
  this.#viewport = this.#root.querySelector(".viewport");
3734
- this.#pageSelect = this.#root.querySelector("select");
3735
- this.#zoomLabel = this.#root.querySelector("output");
4386
+ this.#zoomSlider = this.#root.querySelector("office-ui-zoom-slider");
4387
+ this.#shapeStatus = this.#root.querySelector("[data-shape-status]");
3736
4388
  this.#status = this.#root.querySelector("[data-status]");
3737
4389
  this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
3738
4390
  this.#toolbar = this.#root.querySelector(".toolbar");
@@ -3743,6 +4395,23 @@ var VisioViewerElement = class extends BaseElement {
3743
4395
  this.#notesPanel = this.#root.querySelector(".notes");
3744
4396
  this.#inspector = this.#root.querySelector(".shape-inspector");
3745
4397
  this.#chrome = new ViewerChrome(this.#root, this.controller);
4398
+ this.#commands = new ViewerCommands({
4399
+ root: this.#root,
4400
+ viewport: this.#viewport,
4401
+ controller: this.controller,
4402
+ fit: (mode) => this.#fit(mode),
4403
+ togglePane: (pane) => this.#chrome.togglePane(pane),
4404
+ reveal: (panel, focusText) => this.#chrome.reveal(panel, focusText),
4405
+ focusSearch: () => {
4406
+ this.#chrome.showTab("home");
4407
+ this.#search.input.focus();
4408
+ this.#search.input.select();
4409
+ },
4410
+ announce: (message) => {
4411
+ this.#announcement = message;
4412
+ this.#status.textContent = message;
4413
+ }
4414
+ });
3746
4415
  this.#disposeInputs = this.#wireInputs();
3747
4416
  this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
3748
4417
  this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
@@ -3816,13 +4485,24 @@ var VisioViewerElement = class extends BaseElement {
3816
4485
  }
3817
4486
  fit() {
3818
4487
  this.#assertAlive();
4488
+ this.#fit("page");
4489
+ }
4490
+ /** Visio Fit to Window shows the whole page; Page Width fills the canvas width. */
4491
+ #fit(mode) {
3819
4492
  const page = this.document?.pages[this.pageIndex];
3820
4493
  if (!page) return;
3821
4494
  const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
3822
4495
  const padding = (value) => Number.parseFloat(value ?? "") || 0;
3823
4496
  const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
3824
4497
  const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
3825
- this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
4498
+ if (mode === "page") {
4499
+ this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
4500
+ return;
4501
+ }
4502
+ const scrollbar = this.#viewport.offsetWidth - this.#viewport.clientWidth;
4503
+ const fitted = width / (page.width * 96);
4504
+ const reserve = page.height * 96 * fitted > height && scrollbar <= 0 ? 17 : 0;
4505
+ this.zoom = Math.max(1, width - reserve - 1) / (page.width * 96);
3826
4506
  }
3827
4507
  setLayerVisibility(pageId, layerId, visible) {
3828
4508
  this.#assertAlive();
@@ -3892,16 +4572,18 @@ var VisioViewerElement = class extends BaseElement {
3892
4572
  }
3893
4573
  #wireInputs() {
3894
4574
  const disposeChrome = this.#chrome.wire();
4575
+ const disposeCommands = this.#commands.wire();
3895
4576
  const disposeEdit = this.#edit.wire();
3896
4577
  const disposeLayers = wireLayerControls(this.#layers, this.controller);
3897
4578
  const disposeInputs = wireViewerInputs({
3898
4579
  viewport: this.#viewport,
3899
4580
  commandRoot: this.#root,
3900
- pageSelect: this.#pageSelect,
4581
+ zoomSlider: this.#zoomSlider,
3901
4582
  searchInput: this.#search.input
3902
- }, this.controller, () => this.fit());
4583
+ }, this.controller, (mode) => this.#fit(mode));
3903
4584
  return () => {
3904
4585
  disposeChrome();
4586
+ disposeCommands();
3905
4587
  disposeInputs();
3906
4588
  disposeLayers();
3907
4589
  disposeEdit();
@@ -3920,14 +4602,7 @@ var VisioViewerElement = class extends BaseElement {
3920
4602
  this.#document = state.document;
3921
4603
  this.#renderedPage = state.pageIndex;
3922
4604
  this.#renderedLayerOverrides = state.layerVisibilityOverrides;
3923
- this.#pageSelect.replaceChildren();
3924
- for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
3925
- const option = document.createElement("option");
3926
- option.value = String(index);
3927
- option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
3928
- this.#pageSelect.append(option);
3929
- }
3930
- this.#pageSelect.value = String(state.pageIndex);
4605
+ this.#announcement = void 0;
3931
4606
  this.#renderWarnings = [];
3932
4607
  if (state.document && page) {
3933
4608
  const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
@@ -3954,6 +4629,7 @@ var VisioViewerElement = class extends BaseElement {
3954
4629
  if (svg && page) {
3955
4630
  svg.style.width = `${page.width * 96 * state.zoom}px`;
3956
4631
  svg.style.height = `${page.height * 96 * state.zoom}px`;
4632
+ svg.style.setProperty("--vv-inch", `${96 * state.zoom}px`);
3957
4633
  }
3958
4634
  const searchResult = state.search.results[state.search.activeIndex];
3959
4635
  const selection = state.selectedShape;
@@ -3981,16 +4657,20 @@ var VisioViewerElement = class extends BaseElement {
3981
4657
  this.#renderedSelection = selection ? { ...selection } : null;
3982
4658
  }
3983
4659
  if (!searchResult) this.#revealedSearchResult = void 0;
3984
- this.#zoomLabel.value = `${Math.round(state.zoom * 100)}%`;
3985
- this.#pageSelect.disabled = !page;
4660
+ this.#zoomSlider.value = Math.round(state.zoom * 100);
4661
+ this.#zoomSlider.disabled = !page;
4662
+ const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
4663
+ const inches = (value) => `${+value.toFixed(3)} in`;
4664
+ this.#shapeStatus.setAttribute("value", inspected ? `Width: ${inches(inspected.width)} Height: ${inches(inspected.height)}` : "");
3986
4665
  for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
3987
4666
  renderSearchControls(this.#search, state);
3988
4667
  this.#edit.render(state);
3989
4668
  if (changed) renderLayerControls(this.#layers, state);
3990
4669
  else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
3991
4670
  this.#chrome.render(state, this.#notes.children.length);
4671
+ this.#commands.render(state);
3992
4672
  this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
3993
- this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
4673
+ 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");
3994
4674
  const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
3995
4675
  this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
3996
4676
  }
@@ -4200,6 +4880,23 @@ function viewerHandle(current) {
4200
4880
  }
4201
4881
  };
4202
4882
  }
4883
+ function viewerStateSource(current) {
4884
+ const controller = () => {
4885
+ try {
4886
+ return current()?.controller;
4887
+ } catch {
4888
+ return;
4889
+ }
4890
+ };
4891
+ return {
4892
+ subscribe(onChange) {
4893
+ return controller()?.subscribe(() => onChange()) ?? (() => {});
4894
+ },
4895
+ getSnapshot() {
4896
+ return controller()?.state ?? null;
4897
+ }
4898
+ };
4899
+ }
4203
4900
  //#endregion
4204
4901
  //#region packages/bindings/src/solid.tsx
4205
4902
  var _tmpl$ = [
@@ -4243,5 +4940,19 @@ function VisioViewer(props) {
4243
4940
  });
4244
4941
  return ssr(_tmpl$, ssrHydrationKey() + ssrAttribute("class", escape(props.class, true), false), ssrStyle(props.style));
4245
4942
  }
4943
+ /**
4944
+ * Reactive viewer state as a Solid accessor for a handle accessor (for example a signal set by
4945
+ * `viewerRef`). Follows handle changes and cleans up with the owning scope.
4946
+ */
4947
+ function createVisioViewerState(handle) {
4948
+ const [state, setState] = createSignal(null);
4949
+ createEffect(() => {
4950
+ const current = handle();
4951
+ const source = viewerStateSource(() => current);
4952
+ setState(() => source.getSnapshot());
4953
+ onCleanup(source.subscribe(() => setState(() => source.getSnapshot())));
4954
+ });
4955
+ return state;
4956
+ }
4246
4957
  //#endregion
4247
- 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 };
4958
+ export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createVisioViewerState, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };