@markdstage/markdstage 2.5.1 → 3.0.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>
@@ -110,12 +110,18 @@ export async function readGuide(topic = "overview") {
110
110
  section(readme, "## How it works"),
111
111
  "",
112
112
  "Users can load workspace Markdown directly with **More controls > Open Markdown** (deterministic splitting without AI; natural-language summarization remains the AI's responsibility). The workspace root is the Git repository root when available, otherwise the folder opened for the current session.",
113
- "Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage present slides.md --watch`; it starts in viewing mode and enables the same placement editor plus the detailed Architecture designer. CLI `present` without `--watch` is read-only. Comprehensive edits affect the source Markdown only when explicitly saved.",
113
+ "Use **More controls > Shape editing** to adjust the placement of an existing Architecture diagram. In the CLI, run `markdstage preview slides.md --watch`; it starts in viewing mode and enables the same placement editor plus the detailed Architecture designer. CLI `preview` without `--watch` is read-only. Comprehensive edits affect the source Markdown only when explicitly saved.",
114
114
  "",
115
115
  "For details, request `slide-format`, `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or `architecture-schema`.",
116
116
  ].join("\n");
117
117
  case "slide-format":
118
- return section(readme, "### Slide fragment format");
118
+ return [
119
+ section(readme, "### Markdown file syntax"),
120
+ "",
121
+ section(readme, "### Canvas API `slides` array"),
122
+ "",
123
+ section(readme, "### `sourceName` role"),
124
+ ].join("\n");
119
125
  case "themes":
120
126
  return section(readme, "### Choosing a theme");
121
127
  case "custom-themes":
@@ -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.
@@ -1292,7 +1297,7 @@ async function collectArchitectureObjects(wrapper, deck, blockIndex) {
1292
1297
  bold: Number(run.fontWeight) >= 600,
1293
1298
  })),
1294
1299
  }));
1295
- if (mapped.dash !== undefined) mapped.dash = mapped.dash ? "dash" : "solid";
1300
+ if (mapped.dash !== undefined) mapped.dash = powerPointDashStyle(mapped.dash);
1296
1301
  for (const key of ["fill", "stroke", "color"]) {
1297
1302
  if (key in mapped) mapped[key] = resolveModelColor(mapped[key], deck);
1298
1303
  }
@@ -2143,11 +2148,13 @@ let currentVersion = -1;
2143
2148
  let knownDeckVersion = -1;
2144
2149
  let deckSlides = [];
2145
2150
  let deckTitles = [];
2151
+ let deckLayouts = [];
2146
2152
  let navIndex = 0;
2147
2153
  let navTotal = 0;
2148
2154
  let navMode = "deck";
2149
2155
  let overviewOpen = false;
2150
2156
  let importOpen = false;
2157
+ let architecturePickerOpen = false;
2151
2158
  let importPending = false;
2152
2159
  let importFiles = [];
2153
2160
  let sourceBacked = false;
@@ -2183,6 +2190,11 @@ function deriveTitle(md) {
2183
2190
  return fallback ? trimTitle(fallback) : "(Untitled)";
2184
2191
  }
2185
2192
 
2193
+ function deriveLayout(md) {
2194
+ const { meta } = splitFrontMatter(typeof md === "string" ? md : "");
2195
+ return typeof meta.layout === "string" ? meta.layout.trim().toLowerCase() : "";
2196
+ }
2197
+
2186
2198
  function trimTitle(text) {
2187
2199
  const stripped = text
2188
2200
  .replace(/[*_`>#~]/g, "")
@@ -2199,6 +2211,7 @@ async function fetchDeck() {
2199
2211
  if (Array.isArray(data.slides)) {
2200
2212
  deckSlides = data.slides;
2201
2213
  deckTitles = deckSlides.map(deriveTitle);
2214
+ deckLayouts = deckSlides.map(deriveLayout);
2202
2215
  }
2203
2216
  if (typeof data.deckVersion === "number") knownDeckVersion = data.deckVersion;
2204
2217
  buildOverview();
@@ -2225,7 +2238,11 @@ function updateArchitectureEditButton(enabled = architectureEditMode) {
2225
2238
  if (!button) return;
2226
2239
  button.hidden = presenterMode || !architectureEditAvailable;
2227
2240
  button.dataset.state = enabled && !presenterMode ? "active" : "";
2228
- button.title = enabled ? "Exit shape editing mode" : "Shape editing mode";
2241
+ button.title = architectureDetailedEdit
2242
+ ? "Open Architecture Designer"
2243
+ : enabled
2244
+ ? "Exit shape editing mode"
2245
+ : "Shape editing mode";
2229
2246
  button.setAttribute("aria-label", button.title);
2230
2247
  syncMoreControls();
2231
2248
  }
@@ -2314,8 +2331,113 @@ async function requestArchitectureEditMode(enabled) {
2314
2331
  }
2315
2332
  }
2316
2333
 
2334
+ function architectureDiagramOptions() {
2335
+ return [...document.querySelectorAll(".architecture-diagram[data-architecture-block]")].map(
2336
+ (wrapper, index) => ({
2337
+ block: Number(wrapper.dataset.architectureBlock),
2338
+ title: wrapper.dataset.architectureTitle || `Diagram ${index + 1}`,
2339
+ }),
2340
+ );
2341
+ }
2342
+
2343
+ function closeArchitecturePicker({ restoreFocus = false } = {}) {
2344
+ architecturePickerOpen = false;
2345
+ const picker = document.getElementById("architecturePicker");
2346
+ if (picker) picker.hidden = true;
2347
+ if (restoreFocus) document.getElementById("navEdit")?.focus();
2348
+ }
2349
+
2350
+ async function openArchitectureDesigner(block) {
2351
+ const status = document.getElementById("sourceStatus");
2352
+ if (status) status.textContent = "Opening the Architecture Designer.";
2353
+ const result = await openDetailedArchitectureEditor(navIndex, block);
2354
+ if (!result?.ok && status) {
2355
+ status.textContent = result?.message || "Could not open the Architecture Designer.";
2356
+ }
2357
+ return result;
2358
+ }
2359
+
2360
+ function openArchitecturePicker(options) {
2361
+ const picker = document.getElementById("architecturePicker");
2362
+ const list = document.getElementById("architecturePickerList");
2363
+ if (!picker || !list) return;
2364
+ list.replaceChildren();
2365
+ for (const [index, option] of options.entries()) {
2366
+ const item = document.createElement("li");
2367
+ item.className = "overview-item";
2368
+ const button = document.createElement("button");
2369
+ button.type = "button";
2370
+ button.className = "overview-link";
2371
+ const number = document.createElement("span");
2372
+ number.className = "overview-num";
2373
+ number.textContent = String(index + 1);
2374
+ const label = document.createElement("span");
2375
+ label.className = "overview-label";
2376
+ label.textContent = option.title;
2377
+ button.append(number, label);
2378
+ button.addEventListener("click", () => {
2379
+ closeArchitecturePicker();
2380
+ void openArchitectureDesigner(option.block);
2381
+ });
2382
+ item.appendChild(button);
2383
+ list.appendChild(item);
2384
+ }
2385
+ architecturePickerOpen = true;
2386
+ picker.hidden = false;
2387
+ list.querySelector("button")?.focus();
2388
+ }
2389
+
2390
+ function handleArchitecturePickerKey(event) {
2391
+ const picker = document.getElementById("architecturePicker");
2392
+ const list = document.getElementById("architecturePickerList");
2393
+ if (!picker || !list) return;
2394
+ const focusable = [...picker.querySelectorAll("button:not([disabled])")];
2395
+ const options = [...list.querySelectorAll("button:not([disabled])")];
2396
+ if (event.key === "Tab") {
2397
+ const current = focusable.indexOf(document.activeElement);
2398
+ if (current < 0 || (!event.shiftKey && current === focusable.length - 1)) {
2399
+ event.preventDefault();
2400
+ focusable[0]?.focus();
2401
+ } else if (event.shiftKey && current === 0) {
2402
+ event.preventDefault();
2403
+ focusable.at(-1)?.focus();
2404
+ }
2405
+ return;
2406
+ }
2407
+ const current = options.indexOf(document.activeElement);
2408
+ let next = null;
2409
+ if (event.key === "ArrowDown" || event.key === "ArrowRight") {
2410
+ next = options[(Math.max(current, -1) + 1) % options.length];
2411
+ } else if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
2412
+ next = options[(current <= 0 ? options.length : current) - 1];
2413
+ } else if (event.key === "Home") {
2414
+ next = options[0];
2415
+ } else if (event.key === "End") {
2416
+ next = options.at(-1);
2417
+ } else if (event.key === "PageDown" || event.key === "PageUp") {
2418
+ event.preventDefault();
2419
+ return;
2420
+ }
2421
+ if (next) {
2422
+ event.preventDefault();
2423
+ next.focus();
2424
+ }
2425
+ }
2426
+
2317
2427
  async function toggleArchitectureEditMode() {
2318
2428
  if (presenterMode || !architectureEditAvailable) return;
2429
+ if (architectureDetailedEdit) {
2430
+ const options = architectureDiagramOptions();
2431
+ if (options.length === 1) {
2432
+ await openArchitectureDesigner(options[0].block);
2433
+ } else if (options.length > 1) {
2434
+ openArchitecturePicker(options);
2435
+ } else {
2436
+ const status = document.getElementById("sourceStatus");
2437
+ if (status) status.textContent = "The current slide has no Architecture diagram to edit.";
2438
+ }
2439
+ return;
2440
+ }
2319
2441
  await requestArchitectureEditMode(!architectureEditMode);
2320
2442
  await fetchState();
2321
2443
  }
@@ -2498,6 +2620,8 @@ async function fetchState() {
2498
2620
  if (!architectureEditAvailable) {
2499
2621
  availabilityDisabledEditMode = setArchitectureEditMode(false);
2500
2622
  }
2623
+ }
2624
+ if (editAvailabilityChanged || detailedEditChanged) {
2501
2625
  updateArchitectureEditButton();
2502
2626
  }
2503
2627
  if (
@@ -2686,6 +2810,7 @@ function updateHostActionButtons() {
2686
2810
  if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
2687
2811
  if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
2688
2812
  if (!markdownImportAvailable && importOpen) closeImportPicker();
2813
+ if (!architectureDetailedEdit && architecturePickerOpen) closeArchitecturePicker();
2689
2814
  syncMoreControls();
2690
2815
  }
2691
2816
 
@@ -2914,24 +3039,51 @@ function buildOverview() {
2914
3039
  const list = document.getElementById("overviewList");
2915
3040
  if (!list) return;
2916
3041
  list.replaceChildren();
3042
+ let insideSection = false;
2917
3043
  deckTitles.forEach((title, i) => {
3044
+ const layout = deckLayouts[i] || "";
3045
+ if (layout === "title" || layout === "backcover") insideSection = false;
3046
+ const sectionChild =
3047
+ insideSection && layout !== "title" && layout !== "section" && layout !== "backcover";
3048
+
2918
3049
  const li = document.createElement("li");
2919
3050
  li.className = "overview-item";
3051
+ if (layout === "title" || layout === "section") {
3052
+ li.classList.add(`overview-item-${layout}`);
3053
+ }
3054
+ if (sectionChild) li.classList.add("overview-item-section-child");
2920
3055
  li.dataset.index = String(i);
3056
+ if (layout) li.dataset.layout = layout;
3057
+
2921
3058
  const btn = document.createElement("button");
2922
3059
  btn.type = "button";
2923
3060
  btn.className = "overview-link";
3061
+ btn.setAttribute(
3062
+ "aria-label",
3063
+ `${i + 1} ${title}${layout === "title" || layout === "section" ? `, ${layout} slide` : ""}`,
3064
+ );
3065
+
2924
3066
  const num = document.createElement("span");
2925
3067
  num.className = "overview-num";
2926
3068
  num.textContent = String(i + 1);
2927
3069
  const label = document.createElement("span");
2928
3070
  label.className = "overview-label";
2929
3071
  label.textContent = title;
3072
+
2930
3073
  btn.appendChild(num);
3074
+ if (layout === "title" || layout === "section") {
3075
+ const kind = document.createElement("span");
3076
+ kind.className = "overview-kind";
3077
+ kind.textContent = layout === "title" ? "Title" : "Section";
3078
+ kind.setAttribute("aria-hidden", "true");
3079
+ btn.appendChild(kind);
3080
+ }
2931
3081
  btn.appendChild(label);
2932
3082
  btn.addEventListener("click", () => goToIndex(i));
2933
3083
  li.appendChild(btn);
2934
3084
  list.appendChild(li);
3085
+
3086
+ if (layout === "section") insideSection = true;
2935
3087
  });
2936
3088
  highlightOverview();
2937
3089
  }
@@ -3217,6 +3369,7 @@ function wireControls() {
3217
3369
  bind("navSourceMode", toggleSourceMode, { closeMore: true });
3218
3370
  bind("overviewClose", closeOverview);
3219
3371
  bind("importClose", closeImportPicker);
3372
+ bind("architecturePickerClose", () => closeArchitecturePicker({ restoreFocus: true }));
3220
3373
  bind("presenterPrevButton", goPrev);
3221
3374
  bind("presenterNextButton", goNext);
3222
3375
  bind("presenterListButton", openOverview);
@@ -3242,6 +3395,13 @@ function wireControls() {
3242
3395
  });
3243
3396
  }
3244
3397
 
3398
+ const architecturePicker = document.getElementById("architecturePicker");
3399
+ if (architecturePicker) {
3400
+ architecturePicker.addEventListener("click", (e) => {
3401
+ if (e.target === architecturePicker) closeArchitecturePicker({ restoreFocus: true });
3402
+ });
3403
+ }
3404
+
3245
3405
  document.addEventListener("pointerdown", (e) => {
3246
3406
  if (!moreControlsOpen) return;
3247
3407
  const tools = document.getElementById("navTools");
@@ -3272,6 +3432,15 @@ function wireControls() {
3272
3432
  e.preventDefault();
3273
3433
  return;
3274
3434
  }
3435
+ if (e.key === "Escape" && architecturePickerOpen) {
3436
+ closeArchitecturePicker({ restoreFocus: true });
3437
+ e.preventDefault();
3438
+ return;
3439
+ }
3440
+ if (architecturePickerOpen) {
3441
+ handleArchitecturePickerKey(e);
3442
+ return;
3443
+ }
3275
3444
  if (e.key === "Escape" && moreControlsOpen) {
3276
3445
  setMoreControlsOpen(false, { restoreFocus: true });
3277
3446
  e.preventDefault();
@@ -3296,6 +3465,9 @@ function wireControls() {
3296
3465
  if (importOpen) {
3297
3466
  closeImportPicker();
3298
3467
  e.preventDefault();
3468
+ } else if (architecturePickerOpen) {
3469
+ closeArchitecturePicker({ restoreFocus: true });
3470
+ e.preventDefault();
3299
3471
  } else if (overviewOpen) {
3300
3472
  closeOverview();
3301
3473
  e.preventDefault();
@@ -3340,13 +3512,9 @@ function init() {
3340
3512
  return;
3341
3513
  }
3342
3514
  if (params.get("print") === "1") {
3343
- // Print mode never reaches editing-mode branches. Removing this return would
3344
- // bake the editing UI into PDFs, so a regression test protects it.
3345
- //
3346
- // This early return is also **the primary fix for the #12 hang**. Only print
3347
- // mode avoids connectEvents() (an unclosed SSE) and the two-second setInterval,
3348
- // allowing the page to become idle and --print-to-pdf to complete.
3349
- // Removing the return makes printing hang forever.
3515
+ // Print mode renders one immutable output snapshot and never reaches editing,
3516
+ // SSE, or polling branches. The output runtime waits for its ready report
3517
+ // before asking Chromium to create the PDF.
3350
3518
  initPrint(params).catch(reportPrintBootstrapFailure);
3351
3519
  return;
3352
3520
  }