@markdstage/markdstage 2.5.0 → 2.6.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.
@@ -7,40 +7,66 @@
7
7
  <link rel="stylesheet" href="./renderer/slides.css">
8
8
  <link rel="stylesheet" href="./editor/editor.css">
9
9
  </head>
10
- <body>
10
+ <body data-elements-open="true" data-inspector-open="false">
11
11
  <header class="editor-header">
12
12
  <div class="editor-title">
13
13
  <strong>Architecture Editor</strong>
14
14
  <span id="sourceLabel"></span>
15
15
  <span id="dirtyBadge" class="dirty-badge" hidden>Unsaved</span>
16
16
  </div>
17
+ <div class="editor-panel-actions" role="group" aria-label="Designer panels">
18
+ <button id="elementsPanelButton" class="editor-panel-toggle" type="button" aria-controls="elementPanel" aria-expanded="false">Elements</button>
19
+ <button id="inspectorPanelButton" class="editor-panel-toggle" type="button" aria-controls="inspectorPanel" aria-expanded="false">Properties</button>
20
+ </div>
17
21
  <div class="editor-actions" role="toolbar" aria-label="Architecture editing controls">
18
- <button type="button" data-action="undo" title="Undo (Ctrl+Z)">↶</button>
19
- <button type="button" data-action="redo" title="Redo (Ctrl+Y)">↷</button>
20
- <span class="toolbar-separator" aria-hidden="true"></span>
21
- <button type="button" data-action="add-node">Node</button>
22
- <button type="button" data-action="add-group">Group</button>
23
- <button type="button" data-action="add-image">Image</button>
24
- <button type="button" data-action="add-connector">Connector</button>
25
- <button type="button" data-action="duplicate" title="Duplicate (Ctrl+D)">Duplicate</button>
26
- <button type="button" data-action="delete" title="Delete (Delete)">Delete</button>
27
- <button type="button" data-action="release-layout">Release layout</button>
28
- <span class="toolbar-separator" aria-hidden="true"></span>
29
- <button type="button" data-action="order-back" title="Move earlier in declaration order">Send backward</button>
30
- <button type="button" data-action="order-front" title="Move later in declaration order">Bring forward</button>
31
- <span class="toolbar-separator" aria-hidden="true"></span>
32
- <button type="button" data-action="zoom-out" aria-label="Zoom out">−</button>
33
- <button type="button" data-action="zoom-fit">Fit</button>
34
- <button type="button" data-action="zoom-in" aria-label="Zoom in">+</button>
35
- <label class="snap-toggle"><input id="snapToggle" type="checkbox" checked> Grid</label>
22
+ <button type="button" data-action="undo" title="Undo (Ctrl+Z)" aria-label="Undo">↶</button>
23
+ <button type="button" data-action="redo" title="Redo (Ctrl+Y)" aria-label="Redo">↷</button>
24
+ <div class="shape-palette-host">
25
+ <button id="shapePaletteButton" type="button" data-action="toggle-shape-palette" aria-haspopup="menu" aria-controls="shapePalette" aria-expanded="false">Shape</button>
26
+ <div id="shapePalette" class="shape-palette" role="menu" aria-label="Add a shape" hidden></div>
27
+ </div>
36
28
  <button type="button" class="save-button" data-action="save" title="Save to Markdown (Ctrl+S)">Save</button>
37
- <button type="button" data-action="reload" title="Reload from Markdown">Reload</button>
29
+ <div class="toolbar-more-host">
30
+ <button id="toolbarMoreButton" type="button" aria-haspopup="menu" aria-controls="toolbarMoreMenu" aria-expanded="false">More</button>
31
+ <div id="toolbarMoreMenu" class="toolbar-more-menu" role="menu" aria-label="More editing controls" hidden>
32
+ <div class="toolbar-more-group" role="presentation">
33
+ <span class="toolbar-more-label">Add</span>
34
+ <button type="button" role="menuitem" data-action="add-group">Group</button>
35
+ <button type="button" role="menuitem" data-action="add-image">Image</button>
36
+ <button type="button" role="menuitem" data-action="add-connector">Connector</button>
37
+ </div>
38
+ <div class="toolbar-more-group" role="presentation">
39
+ <span class="toolbar-more-label">Edit</span>
40
+ <button type="button" role="menuitem" data-action="duplicate" title="Duplicate (Ctrl+D)">Duplicate</button>
41
+ <button type="button" role="menuitem" data-action="delete" title="Delete (Delete)">Delete</button>
42
+ <button type="button" role="menuitem" data-action="release-layout">Release layout</button>
43
+ </div>
44
+ <div class="toolbar-more-group" role="presentation">
45
+ <span class="toolbar-more-label">Arrange</span>
46
+ <button type="button" role="menuitem" data-action="order-back" title="Move earlier in declaration order">Send backward</button>
47
+ <button type="button" role="menuitem" data-action="order-front" title="Move later in declaration order">Bring forward</button>
48
+ </div>
49
+ <div class="toolbar-more-group" role="presentation">
50
+ <span class="toolbar-more-label">View</span>
51
+ <div class="toolbar-more-row">
52
+ <button type="button" role="menuitem" data-action="zoom-out" aria-label="Zoom out">−</button>
53
+ <button type="button" role="menuitem" data-action="zoom-fit">Fit</button>
54
+ <button type="button" role="menuitem" data-action="zoom-in" aria-label="Zoom in">+</button>
55
+ </div>
56
+ <label class="snap-toggle"><input id="snapToggle" type="checkbox" checked> Snap to grid</label>
57
+ </div>
58
+ <button type="button" role="menuitem" data-action="reload" title="Reload from Markdown">Reload from Markdown</button>
59
+ </div>
60
+ </div>
38
61
  </div>
39
62
  </header>
40
63
 
41
64
  <main class="editor-layout">
42
- <aside class="editor-sidebar element-panel" aria-label="Elements">
43
- <h2>Elements</h2>
65
+ <aside id="elementPanel" class="editor-sidebar element-panel" aria-label="Elements">
66
+ <div class="editor-sidebar-head">
67
+ <h2>Elements</h2>
68
+ <button class="editor-panel-close" type="button" data-panel-close="elements" aria-label="Close Elements">×</button>
69
+ </div>
44
70
  <div id="elementTree" class="element-tree" role="tree"></div>
45
71
  </aside>
46
72
 
@@ -48,8 +74,11 @@
48
74
  <div id="canvasSurface" class="canvas-surface"></div>
49
75
  </section>
50
76
 
51
- <aside class="editor-sidebar inspector-panel" aria-label="Properties">
52
- <h2>Properties</h2>
77
+ <aside id="inspectorPanel" class="editor-sidebar inspector-panel" aria-label="Properties">
78
+ <div class="editor-sidebar-head">
79
+ <h2>Properties</h2>
80
+ <button class="editor-panel-close" type="button" data-panel-close="inspector" aria-label="Close Properties">×</button>
81
+ </div>
53
82
  <form id="inspector" class="inspector" autocomplete="off"></form>
54
83
  </aside>
55
84
  </main>
@@ -91,7 +91,15 @@ const ROUTE_FALLBACK_REMEDIES = Object.freeze({
91
91
  });
92
92
 
93
93
  const ID_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
94
- const SHAPES = new Set(["rect", "rounded-rect", "ellipse"]);
94
+ const SHAPES = new Set([
95
+ "rect",
96
+ "rounded-rect",
97
+ "ellipse",
98
+ "diamond",
99
+ "triangle",
100
+ "hexagon",
101
+ "parallelogram",
102
+ ]);
95
103
  const ROUTINGS = new Set(["straight", "orthogonal", "polyline"]);
96
104
  const PORTS = new Set(["auto", "top", "right", "bottom", "left"]);
97
105
  const LABEL_LAYERS = new Set(["front", "behind"]);
@@ -789,6 +797,19 @@ function layoutPlacements(children, group, layout, path, graphEdges = []) {
789
797
  return placements;
790
798
  }
791
799
 
800
+ export function powerPointDashStyle(value) {
801
+ const pattern = String(value || "").trim();
802
+ if (!pattern) return "solid";
803
+ const values = pattern
804
+ .split(/[ ,]+/)
805
+ .map(Number)
806
+ .filter(Number.isFinite);
807
+ if (values.length >= 2 && values[0] <= 2 && values[1] >= values[0] * 2) {
808
+ return "dotted";
809
+ }
810
+ return "dash";
811
+ }
812
+
792
813
  function normalizeBox(element, origin, path, placement) {
793
814
  return {
794
815
  x:
@@ -1392,6 +1413,20 @@ function appendText(documentRef, parent, element, text, options = {}) {
1392
1413
  parent.appendChild(textElement);
1393
1414
  }
1394
1415
 
1416
+ function nodeContentInset(element) {
1417
+ const ratio = {
1418
+ diamond: 0.24,
1419
+ triangle: 0.24,
1420
+ hexagon: 0.14,
1421
+ parallelogram: 0.16,
1422
+ }[element.shape];
1423
+ return ratio ? Math.max(16, element.width * ratio) : 16;
1424
+ }
1425
+
1426
+ function nodeIconInset(element) {
1427
+ return Math.max(20, nodeContentInset(element), element.width * 0.08);
1428
+ }
1429
+
1395
1430
  function elementCenter(element) {
1396
1431
  return { x: element.x + element.width / 2, y: element.y + element.height / 2 };
1397
1432
  }
@@ -3451,11 +3486,48 @@ function iconShapeAttributes(shape, textColor) {
3451
3486
  return shape.solid ? { ...shape.attributes, fill: textColor } : shape.attributes;
3452
3487
  }
3453
3488
 
3489
+ function polygonPoints(element) {
3490
+ const { x, y, width, height } = element;
3491
+ const right = x + width;
3492
+ const bottom = y + height;
3493
+ const centerX = x + width / 2;
3494
+ const centerY = y + height / 2;
3495
+ if (element.shape === "diamond") {
3496
+ return `${centerX},${y} ${right},${centerY} ${centerX},${bottom} ${x},${centerY}`;
3497
+ }
3498
+ if (element.shape === "triangle") {
3499
+ return `${centerX},${y} ${right},${bottom} ${x},${bottom}`;
3500
+ }
3501
+ if (element.shape === "hexagon") {
3502
+ const inset = width * 0.25;
3503
+ return `${x + inset},${y} ${right - inset},${y} ${right},${centerY} ${
3504
+ right - inset
3505
+ },${bottom} ${x + inset},${bottom} ${x},${centerY}`;
3506
+ }
3507
+ if (element.shape === "parallelogram") {
3508
+ const inset = Math.min(width * 0.2, height * 0.75);
3509
+ return `${x + inset},${y} ${right},${y} ${right - inset},${bottom} ${x},${bottom}`;
3510
+ }
3511
+ return "";
3512
+ }
3513
+
3514
+ function nodePowerPointShape(shape) {
3515
+ return {
3516
+ rect: "rect",
3517
+ "rounded-rect": "roundedRect",
3518
+ ellipse: "ellipse",
3519
+ diamond: "diamond",
3520
+ triangle: "triangle",
3521
+ hexagon: "hexagon",
3522
+ parallelogram: "parallelogram",
3523
+ }[shape];
3524
+ }
3525
+
3454
3526
  function renderIcon(documentRef, element) {
3455
3527
  if (!element.icon) return null;
3456
3528
  const size = Math.min(58, element.height * 0.36, element.width * 0.2);
3457
3529
  const x = element.text
3458
- ? element.x + Math.max(20, element.width * 0.08)
3530
+ ? element.x + nodeIconInset(element)
3459
3531
  : element.x + element.width / 2 - size / 2;
3460
3532
  const y = element.y + element.height / 2 - size / 2;
3461
3533
  const shapes = ICON_SHAPES[element.icon];
@@ -3481,6 +3553,7 @@ function renderIcon(documentRef, element) {
3481
3553
  );
3482
3554
  return { group, size, x };
3483
3555
  }
3556
+
3484
3557
  const group = svgElement(documentRef, "g", {
3485
3558
  "data-architecture-icon": element.icon,
3486
3559
  "data-architecture-icon-source": "builtin",
@@ -3578,6 +3651,13 @@ function renderNode(documentRef, element) {
3578
3651
  ...common,
3579
3652
  }),
3580
3653
  );
3654
+ } else if (["diamond", "triangle", "hexagon", "parallelogram"].includes(element.shape)) {
3655
+ group.appendChild(
3656
+ svgElement(documentRef, "polygon", {
3657
+ points: polygonPoints(element),
3658
+ ...common,
3659
+ }),
3660
+ );
3581
3661
  } else {
3582
3662
  group.appendChild(
3583
3663
  svgElement(documentRef, "rect", {
@@ -3594,13 +3674,16 @@ function renderNode(documentRef, element) {
3594
3674
  if (icon) group.appendChild(icon.group);
3595
3675
  if (icon && element.text) {
3596
3676
  const textLeft = icon.x + icon.size + 16;
3597
- const textRight = element.x + element.width - 16;
3677
+ const textRight = element.x + element.width - nodeContentInset(element);
3598
3678
  appendText(documentRef, group, element, element.text, {
3599
3679
  centerX: (textLeft + textRight) / 2,
3600
3680
  availableWidth: Math.max(32, textRight - textLeft),
3601
3681
  });
3602
3682
  } else {
3603
- appendText(documentRef, group, element, element.text);
3683
+ const inset = nodeContentInset(element);
3684
+ appendText(documentRef, group, element, element.text, {
3685
+ availableWidth: Math.max(32, element.width - inset * 2),
3686
+ });
3604
3687
  }
3605
3688
  return group;
3606
3689
  }
@@ -3908,12 +3991,15 @@ export function architecturePowerPointSnapshot(model, documentRef = globalThis.d
3908
3991
  const iconSize = element.icon
3909
3992
  ? Math.min(58, element.height * 0.36, element.width * 0.2)
3910
3993
  : 0;
3911
- const iconX = element.x + Math.max(20, element.width * 0.08);
3994
+ const contentInset = nodeContentInset(element);
3995
+ const iconX = element.text
3996
+ ? element.x + nodeIconInset(element)
3997
+ : element.x + element.width / 2 - iconSize / 2;
3912
3998
  const iconY = element.y + element.height / 2 - iconSize / 2;
3913
3999
  const textInset = element.icon
3914
- ? Math.max(20, element.width * 0.08) + iconSize + 16
3915
- : 16;
3916
- const availableTextWidth = Math.max(32, element.width - textInset - 16);
4000
+ ? nodeIconInset(element) + iconSize + 16
4001
+ : contentInset;
4002
+ const availableTextWidth = Math.max(32, element.width - textInset - contentInset);
3917
4003
  const longestLine = Math.max(
3918
4004
  ...element.text.split(/\r?\n/).slice(0, 8).map(textWidthUnits),
3919
4005
  1,
@@ -3924,12 +4010,7 @@ export function architecturePowerPointSnapshot(model, documentRef = globalThis.d
3924
4010
  );
3925
4011
  objects.push({
3926
4012
  type: "shape",
3927
- shape:
3928
- element.shape === "ellipse"
3929
- ? "ellipse"
3930
- : element.shape === "rounded-rect"
3931
- ? "roundedRect"
3932
- : "rect",
4013
+ shape: nodePowerPointShape(element.shape),
3933
4014
  x: element.x,
3934
4015
  y: element.y,
3935
4016
  width: element.width,
@@ -3943,7 +4024,7 @@ export function architecturePowerPointSnapshot(model, documentRef = globalThis.d
3943
4024
  textInsets: {
3944
4025
  left: textInset,
3945
4026
  top: 12,
3946
- right: 16,
4027
+ right: contentInset,
3947
4028
  bottom: 12,
3948
4029
  },
3949
4030
  icon: element.icon || "",
@@ -138,6 +138,17 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
+ <div id="architecturePicker" class="overview" hidden>
142
+ <div class="overview-panel" role="dialog" aria-modal="true" aria-label="Choose an Architecture diagram">
143
+ <div class="overview-head">
144
+ <span>Choose a diagram</span>
145
+ <button id="architecturePickerClose" class="nav-btn" type="button" title="Close (Esc)" aria-label="Close">&#10005;</button>
146
+ </div>
147
+ <p class="import-message">This slide contains multiple Architecture diagrams.</p>
148
+ <ol id="architecturePickerList" class="overview-list"></ol>
149
+ </div>
150
+ </div>
151
+
141
152
  <script src="./vendor/marked.min.js"></script>
142
153
  <script src="./vendor/purify.min.js"></script>
143
154
  <script src="./vendor/highlight.min.js"></script>
@@ -1,4 +1,4 @@
1
- import { renderArchitectureBlock } from "./architecture.mjs";
1
+ import { powerPointDashStyle, renderArchitectureBlock } from "./architecture.mjs";
2
2
  import { attachArchitectureEditor } from "./architecture-editor.mjs";
3
3
  import {
4
4
  DEFAULT_THEME,
@@ -703,7 +703,11 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
703
703
  const source = code.textContent;
704
704
  const slideIndex = navIndex;
705
705
  if (!architectureEditMode) {
706
- target.replaceWith(renderArchitectureBlock(source, document));
706
+ const wrapper = renderArchitectureBlock(source, document);
707
+ wrapper.dataset.architectureBlock = String(blockIndex);
708
+ const title = wrapper.__presentationPptxSnapshot?.title;
709
+ if (title) wrapper.dataset.architectureTitle = title;
710
+ target.replaceWith(wrapper);
707
711
  return;
708
712
  }
709
713
  // Insert the editing UI only in editing mode. Normal view never takes this path,
@@ -780,6 +784,7 @@ function createSlide(markdown, fallbackTheme, themeLocked = deckThemeLocked) {
780
784
  }
781
785
 
782
786
  function renderSlide(markdown) {
787
+ closeArchitecturePicker();
783
788
  lastMarkdown = typeof markdown === "string" ? markdown : "";
784
789
  document.body.classList.toggle("markdstage-empty", !nonEmpty(markdown));
785
790
  // Always detach the previous slide's editing UI because it owns document listeners.
@@ -940,6 +945,60 @@ function textContentBounds(element, deck) {
940
945
  };
941
946
  }
942
947
 
948
+ function textInsetsFor(element) {
949
+ const style = getComputedStyle(element);
950
+ const metric = (padding, border) => {
951
+ const paddingValue = Number.parseFloat(style[padding]);
952
+ const borderValue = Number.parseFloat(style[border]);
953
+ return roundedMetric(
954
+ Math.max(0, Number.isFinite(paddingValue) ? paddingValue : 0) +
955
+ Math.max(0, Number.isFinite(borderValue) ? borderValue : 0),
956
+ );
957
+ };
958
+ const insets = {
959
+ left: metric("paddingLeft", "borderLeftWidth"),
960
+ top: metric("paddingTop", "borderTopWidth"),
961
+ right: metric("paddingRight", "borderRightWidth"),
962
+ bottom: metric("paddingBottom", "borderBottomWidth"),
963
+ };
964
+ return Object.values(insets).some((value) => value > 0) ? insets : null;
965
+ }
966
+
967
+ function singleLineTextLayout(element, deck, bounds, textInsets, alignment) {
968
+ const content = textContentBounds(element, deck);
969
+ const fontSize = Number.parseFloat(getComputedStyle(element).fontSize);
970
+ const widthBuffer = Math.max(2, (Number.isFinite(fontSize) ? fontSize : 0) * 0.5);
971
+ const fittedInsets = textInsets ? { ...textInsets } : null;
972
+ const requiredWidth = () =>
973
+ content.width +
974
+ (fittedInsets?.left || 0) +
975
+ (fittedInsets?.right || 0) +
976
+ widthBuffer;
977
+ const resolvedAlignment =
978
+ alignment || pptxAlignment(getComputedStyle(element).textAlign);
979
+ // Keep the leading CSS inset, but release a trailing inset when browser and
980
+ // PowerPoint font metrics would otherwise force a visually single line to wrap.
981
+ if (fittedInsets && requiredWidth() > bounds.width) {
982
+ if (resolvedAlignment === "right") fittedInsets.left = 0;
983
+ if (resolvedAlignment === "left") fittedInsets.right = 0;
984
+ }
985
+ const extraWidth = Math.max(0, requiredWidth() - bounds.width);
986
+ const offset =
987
+ resolvedAlignment === "right"
988
+ ? extraWidth
989
+ : resolvedAlignment === "center"
990
+ ? extraWidth / 2
991
+ : 0;
992
+ return {
993
+ bounds: {
994
+ ...bounds,
995
+ x: roundedMetric(bounds.x - offset),
996
+ width: roundedMetric(bounds.width + extraWidth),
997
+ },
998
+ textInsets: fittedInsets,
999
+ };
1000
+ }
1001
+
943
1002
  function rasterImageSupported(image) {
944
1003
  const source = image.currentSrc || image.getAttribute("src") || "";
945
1004
  if (["cover", "none"].includes(getComputedStyle(image).objectFit)) return false;
@@ -1028,12 +1087,22 @@ function paragraphFor(element, options = {}) {
1028
1087
  function renderedTextLineCount(element) {
1029
1088
  const range = document.createRange();
1030
1089
  range.selectNodeContents(element);
1031
- const tops = [];
1090
+ const lines = [];
1032
1091
  for (const rect of range.getClientRects()) {
1033
1092
  if (rect.width <= 0 || rect.height <= 0) continue;
1034
- if (!tops.some((top) => Math.abs(top - rect.top) < 1)) tops.push(rect.top);
1093
+ const line = lines.find(
1094
+ (candidate) =>
1095
+ rect.top < candidate.bottom - 1 &&
1096
+ rect.bottom > candidate.top + 1,
1097
+ );
1098
+ if (line) {
1099
+ line.top = Math.min(line.top, rect.top);
1100
+ line.bottom = Math.max(line.bottom, rect.bottom);
1101
+ } else {
1102
+ lines.push({ top: rect.top, bottom: rect.bottom });
1103
+ }
1035
1104
  }
1036
- return Math.max(1, tops.length);
1105
+ return Math.max(1, lines.length);
1037
1106
  }
1038
1107
 
1039
1108
  function unsupportedEffects(element) {
@@ -1228,7 +1297,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
1228
1297
  bold: Number(run.fontWeight) >= 600,
1229
1298
  })),
1230
1299
  }));
1231
- if (mapped.dash !== undefined) mapped.dash = mapped.dash ? "dash" : "solid";
1300
+ if (mapped.dash !== undefined) mapped.dash = powerPointDashStyle(mapped.dash);
1232
1301
  for (const key of ["fill", "stroke", "color"]) {
1233
1302
  if (key in mapped) mapped[key] = resolveModelColor(mapped[key], deck);
1234
1303
  }
@@ -1587,17 +1656,29 @@ async function collectPptxSlide(slide, index) {
1587
1656
  : undefined,
1588
1657
  });
1589
1658
  if (!paragraph.runs.some((run) => run.text.trim())) continue;
1590
- const disableTextWrap =
1591
- element.classList.contains("kicker") ||
1592
- (element.matches("h1, h2, .slide-title") && renderedTextLineCount(element) === 1);
1659
+ const disableTextWrap = renderedTextLineCount(element) === 1;
1660
+ const textInsets = textInsetsFor(element);
1661
+ const baseBounds = element.classList.contains("kicker")
1662
+ ? textContentBounds(element, deck)
1663
+ : relativeBounds(element, deck);
1664
+ const singleLineLayout = disableTextWrap
1665
+ ? singleLineTextLayout(
1666
+ element,
1667
+ deck,
1668
+ baseBounds,
1669
+ textInsets,
1670
+ element.classList.contains("kicker") ? "left" : undefined,
1671
+ )
1672
+ : null;
1673
+ const bounds = singleLineLayout?.bounds || baseBounds;
1674
+ const fittedTextInsets = singleLineLayout?.textInsets || textInsets;
1593
1675
  elements.push({
1594
1676
  type: "text",
1595
1677
  path: elementPath(element, deck),
1596
- ...(element.classList.contains("kicker")
1597
- ? textContentBounds(element, deck)
1598
- : relativeBounds(element, deck)),
1678
+ ...bounds,
1599
1679
  paragraphs: [paragraph],
1600
1680
  opacity: Number(getComputedStyle(element).opacity) || 1,
1681
+ ...(fittedTextInsets ? { textInsets: fittedTextInsets } : {}),
1601
1682
  ...(disableTextWrap ? { textWrap: "none" } : {}),
1602
1683
  });
1603
1684
  element.setAttribute("data-pptx-native", "text");
@@ -2072,6 +2153,7 @@ let navTotal = 0;
2072
2153
  let navMode = "deck";
2073
2154
  let overviewOpen = false;
2074
2155
  let importOpen = false;
2156
+ let architecturePickerOpen = false;
2075
2157
  let importPending = false;
2076
2158
  let importFiles = [];
2077
2159
  let sourceBacked = false;
@@ -2149,7 +2231,11 @@ function updateArchitectureEditButton(enabled = architectureEditMode) {
2149
2231
  if (!button) return;
2150
2232
  button.hidden = presenterMode || !architectureEditAvailable;
2151
2233
  button.dataset.state = enabled && !presenterMode ? "active" : "";
2152
- button.title = enabled ? "Exit shape editing mode" : "Shape editing mode";
2234
+ button.title = architectureDetailedEdit
2235
+ ? "Open Architecture Designer"
2236
+ : enabled
2237
+ ? "Exit shape editing mode"
2238
+ : "Shape editing mode";
2153
2239
  button.setAttribute("aria-label", button.title);
2154
2240
  syncMoreControls();
2155
2241
  }
@@ -2238,8 +2324,113 @@ async function requestArchitectureEditMode(enabled) {
2238
2324
  }
2239
2325
  }
2240
2326
 
2327
+ function architectureDiagramOptions() {
2328
+ return [...document.querySelectorAll(".architecture-diagram[data-architecture-block]")].map(
2329
+ (wrapper, index) => ({
2330
+ block: Number(wrapper.dataset.architectureBlock),
2331
+ title: wrapper.dataset.architectureTitle || `Diagram ${index + 1}`,
2332
+ }),
2333
+ );
2334
+ }
2335
+
2336
+ function closeArchitecturePicker({ restoreFocus = false } = {}) {
2337
+ architecturePickerOpen = false;
2338
+ const picker = document.getElementById("architecturePicker");
2339
+ if (picker) picker.hidden = true;
2340
+ if (restoreFocus) document.getElementById("navEdit")?.focus();
2341
+ }
2342
+
2343
+ async function openArchitectureDesigner(block) {
2344
+ const status = document.getElementById("sourceStatus");
2345
+ if (status) status.textContent = "Opening the Architecture Designer.";
2346
+ const result = await openDetailedArchitectureEditor(navIndex, block);
2347
+ if (!result?.ok && status) {
2348
+ status.textContent = result?.message || "Could not open the Architecture Designer.";
2349
+ }
2350
+ return result;
2351
+ }
2352
+
2353
+ function openArchitecturePicker(options) {
2354
+ const picker = document.getElementById("architecturePicker");
2355
+ const list = document.getElementById("architecturePickerList");
2356
+ if (!picker || !list) return;
2357
+ list.replaceChildren();
2358
+ for (const [index, option] of options.entries()) {
2359
+ const item = document.createElement("li");
2360
+ item.className = "overview-item";
2361
+ const button = document.createElement("button");
2362
+ button.type = "button";
2363
+ button.className = "overview-link";
2364
+ const number = document.createElement("span");
2365
+ number.className = "overview-num";
2366
+ number.textContent = String(index + 1);
2367
+ const label = document.createElement("span");
2368
+ label.className = "overview-label";
2369
+ label.textContent = option.title;
2370
+ button.append(number, label);
2371
+ button.addEventListener("click", () => {
2372
+ closeArchitecturePicker();
2373
+ void openArchitectureDesigner(option.block);
2374
+ });
2375
+ item.appendChild(button);
2376
+ list.appendChild(item);
2377
+ }
2378
+ architecturePickerOpen = true;
2379
+ picker.hidden = false;
2380
+ list.querySelector("button")?.focus();
2381
+ }
2382
+
2383
+ function handleArchitecturePickerKey(event) {
2384
+ const picker = document.getElementById("architecturePicker");
2385
+ const list = document.getElementById("architecturePickerList");
2386
+ if (!picker || !list) return;
2387
+ const focusable = [...picker.querySelectorAll("button:not([disabled])")];
2388
+ const options = [...list.querySelectorAll("button:not([disabled])")];
2389
+ if (event.key === "Tab") {
2390
+ const current = focusable.indexOf(document.activeElement);
2391
+ if (current < 0 || (!event.shiftKey && current === focusable.length - 1)) {
2392
+ event.preventDefault();
2393
+ focusable[0]?.focus();
2394
+ } else if (event.shiftKey && current === 0) {
2395
+ event.preventDefault();
2396
+ focusable.at(-1)?.focus();
2397
+ }
2398
+ return;
2399
+ }
2400
+ const current = options.indexOf(document.activeElement);
2401
+ let next = null;
2402
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
2403
+ next = options[(Math.max(current, -1) + 1) % options.length];
2404
+ } else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
2405
+ next = options[(current <= 0 ? options.length : current) - 1];
2406
+ } else if (event.key === "Home") {
2407
+ next = options[0];
2408
+ } else if (event.key === "End") {
2409
+ next = options.at(-1);
2410
+ } else if (event.key === "PageDown" || event.key === "PageUp") {
2411
+ event.preventDefault();
2412
+ return;
2413
+ }
2414
+ if (next) {
2415
+ event.preventDefault();
2416
+ next.focus();
2417
+ }
2418
+ }
2419
+
2241
2420
  async function toggleArchitectureEditMode() {
2242
2421
  if (presenterMode || !architectureEditAvailable) return;
2422
+ if (architectureDetailedEdit) {
2423
+ const options = architectureDiagramOptions();
2424
+ if (options.length === 1) {
2425
+ await openArchitectureDesigner(options[0].block);
2426
+ } else if (options.length > 1) {
2427
+ openArchitecturePicker(options);
2428
+ } else {
2429
+ const status = document.getElementById("sourceStatus");
2430
+ if (status) status.textContent = "The current slide has no Architecture diagram to edit.";
2431
+ }
2432
+ return;
2433
+ }
2243
2434
  await requestArchitectureEditMode(!architectureEditMode);
2244
2435
  await fetchState();
2245
2436
  }
@@ -2422,6 +2613,8 @@ async function fetchState() {
2422
2613
  if (!architectureEditAvailable) {
2423
2614
  availabilityDisabledEditMode = setArchitectureEditMode(false);
2424
2615
  }
2616
+ }
2617
+ if (editAvailabilityChanged || detailedEditChanged) {
2425
2618
  updateArchitectureEditButton();
2426
2619
  }
2427
2620
  if (
@@ -2610,6 +2803,7 @@ function updateHostActionButtons() {
2610
2803
  if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
2611
2804
  if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
2612
2805
  if (!markdownImportAvailable && importOpen) closeImportPicker();
2806
+ if (!architectureDetailedEdit && architecturePickerOpen) closeArchitecturePicker();
2613
2807
  syncMoreControls();
2614
2808
  }
2615
2809
 
@@ -3141,6 +3335,7 @@ function wireControls() {
3141
3335
  bind("navSourceMode", toggleSourceMode, { closeMore: true });
3142
3336
  bind("overviewClose", closeOverview);
3143
3337
  bind("importClose", closeImportPicker);
3338
+ bind("architecturePickerClose", () => closeArchitecturePicker({ restoreFocus: true }));
3144
3339
  bind("presenterPrevButton", goPrev);
3145
3340
  bind("presenterNextButton", goNext);
3146
3341
  bind("presenterListButton", openOverview);
@@ -3166,6 +3361,13 @@ function wireControls() {
3166
3361
  });
3167
3362
  }
3168
3363
 
3364
+ const architecturePicker = document.getElementById("architecturePicker");
3365
+ if (architecturePicker) {
3366
+ architecturePicker.addEventListener("click", (e) => {
3367
+ if (e.target === architecturePicker) closeArchitecturePicker({ restoreFocus: true });
3368
+ });
3369
+ }
3370
+
3169
3371
  document.addEventListener("pointerdown", (e) => {
3170
3372
  if (!moreControlsOpen) return;
3171
3373
  const tools = document.getElementById("navTools");
@@ -3196,6 +3398,15 @@ function wireControls() {
3196
3398
  e.preventDefault();
3197
3399
  return;
3198
3400
  }
3401
+ if (e.key === "Escape" && architecturePickerOpen) {
3402
+ closeArchitecturePicker({ restoreFocus: true });
3403
+ e.preventDefault();
3404
+ return;
3405
+ }
3406
+ if (architecturePickerOpen) {
3407
+ handleArchitecturePickerKey(e);
3408
+ return;
3409
+ }
3199
3410
  if (e.key === "Escape" && moreControlsOpen) {
3200
3411
  setMoreControlsOpen(false, { restoreFocus: true });
3201
3412
  e.preventDefault();
@@ -3220,6 +3431,9 @@ function wireControls() {
3220
3431
  if (importOpen) {
3221
3432
  closeImportPicker();
3222
3433
  e.preventDefault();
3434
+ } else if (architecturePickerOpen) {
3435
+ closeArchitecturePicker({ restoreFocus: true });
3436
+ e.preventDefault();
3223
3437
  } else if (overviewOpen) {
3224
3438
  closeOverview();
3225
3439
  e.preventDefault();