@markdstage/markdstage 2.5.1 → 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.
@@ -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
  }
@@ -2148,6 +2153,7 @@ let navTotal = 0;
2148
2153
  let navMode = "deck";
2149
2154
  let overviewOpen = false;
2150
2155
  let importOpen = false;
2156
+ let architecturePickerOpen = false;
2151
2157
  let importPending = false;
2152
2158
  let importFiles = [];
2153
2159
  let sourceBacked = false;
@@ -2225,7 +2231,11 @@ function updateArchitectureEditButton(enabled = architectureEditMode) {
2225
2231
  if (!button) return;
2226
2232
  button.hidden = presenterMode || !architectureEditAvailable;
2227
2233
  button.dataset.state = enabled && !presenterMode ? "active" : "";
2228
- 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";
2229
2239
  button.setAttribute("aria-label", button.title);
2230
2240
  syncMoreControls();
2231
2241
  }
@@ -2314,8 +2324,113 @@ async function requestArchitectureEditMode(enabled) {
2314
2324
  }
2315
2325
  }
2316
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
+
2317
2420
  async function toggleArchitectureEditMode() {
2318
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
+ }
2319
2434
  await requestArchitectureEditMode(!architectureEditMode);
2320
2435
  await fetchState();
2321
2436
  }
@@ -2498,6 +2613,8 @@ async function fetchState() {
2498
2613
  if (!architectureEditAvailable) {
2499
2614
  availabilityDisabledEditMode = setArchitectureEditMode(false);
2500
2615
  }
2616
+ }
2617
+ if (editAvailabilityChanged || detailedEditChanged) {
2501
2618
  updateArchitectureEditButton();
2502
2619
  }
2503
2620
  if (
@@ -2686,6 +2803,7 @@ function updateHostActionButtons() {
2686
2803
  if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
2687
2804
  if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
2688
2805
  if (!markdownImportAvailable && importOpen) closeImportPicker();
2806
+ if (!architectureDetailedEdit && architecturePickerOpen) closeArchitecturePicker();
2689
2807
  syncMoreControls();
2690
2808
  }
2691
2809
 
@@ -3217,6 +3335,7 @@ function wireControls() {
3217
3335
  bind("navSourceMode", toggleSourceMode, { closeMore: true });
3218
3336
  bind("overviewClose", closeOverview);
3219
3337
  bind("importClose", closeImportPicker);
3338
+ bind("architecturePickerClose", () => closeArchitecturePicker({ restoreFocus: true }));
3220
3339
  bind("presenterPrevButton", goPrev);
3221
3340
  bind("presenterNextButton", goNext);
3222
3341
  bind("presenterListButton", openOverview);
@@ -3242,6 +3361,13 @@ function wireControls() {
3242
3361
  });
3243
3362
  }
3244
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
+
3245
3371
  document.addEventListener("pointerdown", (e) => {
3246
3372
  if (!moreControlsOpen) return;
3247
3373
  const tools = document.getElementById("navTools");
@@ -3272,6 +3398,15 @@ function wireControls() {
3272
3398
  e.preventDefault();
3273
3399
  return;
3274
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
+ }
3275
3410
  if (e.key === "Escape" && moreControlsOpen) {
3276
3411
  setMoreControlsOpen(false, { restoreFocus: true });
3277
3412
  e.preventDefault();
@@ -3296,6 +3431,9 @@ function wireControls() {
3296
3431
  if (importOpen) {
3297
3432
  closeImportPicker();
3298
3433
  e.preventDefault();
3434
+ } else if (architecturePickerOpen) {
3435
+ closeArchitecturePicker({ restoreFocus: true });
3436
+ e.preventDefault();
3299
3437
  } else if (overviewOpen) {
3300
3438
  closeOverview();
3301
3439
  e.preventDefault();
@@ -250,12 +250,15 @@ body.mermaid-loading .mermaid{visibility:hidden;}
250
250
  .architecture-editor [data-architecture-movable="false"]{cursor:not-allowed;outline:none;}
251
251
  .architecture-editor [data-architecture-type]:focus-visible>rect,
252
252
  .architecture-editor [data-architecture-type]:focus-visible>ellipse,
253
+ .architecture-editor [data-architecture-type]:focus-visible>polygon,
253
254
  .architecture-editor .architecture-selected>rect,
254
- .architecture-editor .architecture-selected>ellipse{
255
+ .architecture-editor .architecture-selected>ellipse,
256
+ .architecture-editor .architecture-selected>polygon{
255
257
  stroke:var(--accent-strong);stroke-width:7;filter:drop-shadow(0 0 8px var(--accent-line));}
256
258
  /* Layout-managed elements cannot move. Show a dashed explanation only when selected. */
257
259
  .architecture-editor [data-architecture-movable="false"].architecture-selected>rect,
258
- .architecture-editor [data-architecture-movable="false"].architecture-selected>ellipse{
260
+ .architecture-editor [data-architecture-movable="false"].architecture-selected>ellipse,
261
+ .architecture-editor [data-architecture-movable="false"].architecture-selected>polygon{
259
262
  stroke-dasharray:10 8;}
260
263
  .architecture-error{display:flex;flex-direction:column;gap:.35em;margin:.5em 0;
261
264
  padding:.8em 1em;border:2px solid #d13438;border-radius:8px;
@@ -569,8 +569,16 @@ function nativeShapeXml(element, path, id, relationships) {
569
569
  rect: "rect",
570
570
  roundedRect: "roundRect",
571
571
  ellipse: "ellipse",
572
+ diamond: "diamond",
573
+ triangle: "triangle",
574
+ hexagon: "hexagon",
575
+ parallelogram: "parallelogram",
572
576
  }[element.shape];
573
- if (!preset) fail(`${path}.shape must be rect, roundedRect, or ellipse`);
577
+ if (!preset) {
578
+ fail(
579
+ `${path}.shape must be rect, roundedRect, ellipse, diamond, triangle, hexagon, or parallelogram`,
580
+ );
581
+ }
574
582
  const opacity = optionalUnitInterval(element.opacity, `${path}.opacity`);
575
583
  const properties = `${xfrmXml(bounds)}<a:prstGeom prst="${preset}"><a:avLst/></a:prstGeom>${colorXml(
576
584
  element.fill,
@@ -204,7 +204,17 @@
204
204
  "properties": {
205
205
  "type": { "const": "node" },
206
206
  "id": { "$ref": "#/$defs/identifier" },
207
- "shape": { "enum": ["rect", "rounded-rect", "ellipse"] },
207
+ "shape": {
208
+ "enum": [
209
+ "rect",
210
+ "rounded-rect",
211
+ "ellipse",
212
+ "diamond",
213
+ "triangle",
214
+ "hexagon",
215
+ "parallelogram"
216
+ ]
217
+ },
208
218
  "x": { "$ref": "#/$defs/coordinate" },
209
219
  "y": { "$ref": "#/$defs/coordinate" },
210
220
  "width": { "$ref": "#/$defs/extent" },