@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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/shared/README.md +75 -28
- package/shared/architecture-editor/editor.css +116 -20
- package/shared/architecture-editor/editor.js +577 -42
- package/shared/architecture-editor/index.html +53 -24
- package/shared/markdstage-guide.mjs +8 -2
- package/shared/renderer/architecture.mjs +96 -15
- package/shared/renderer/index.html +11 -0
- package/shared/renderer/renderer.js +179 -11
- package/shared/renderer/slides.css +19 -2
- package/shared/runtime/browser.mjs +29 -104
- package/shared/runtime/output.mjs +5 -5
- package/shared/runtime/pptx-package.mjs +9 -1
- package/shared/schema/architecture-v1.schema.json +11 -1
- package/src/cli.mjs +13 -13
- package/src/commands/present.mjs +4 -2
- package/src/skills.mjs +6 -6
|
@@ -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
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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([
|
|
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 +
|
|
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 -
|
|
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
|
-
|
|
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
|
|
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
|
-
?
|
|
3915
|
-
:
|
|
3916
|
-
const availableTextWidth = Math.max(32, element.width - textInset -
|
|
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:
|
|
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">✕</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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
3344
|
-
//
|
|
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
|
}
|