@markdstage/markdstage 0.1.3 → 2.3.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.
@@ -0,0 +1,99 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Architecture Editor</title>
7
+ <link rel="stylesheet" href="./renderer/slides.css">
8
+ <link rel="stylesheet" href="./editor/editor.css">
9
+ </head>
10
+ <body>
11
+ <header class="editor-header">
12
+ <div class="editor-title">
13
+ <strong>Architecture Editor</strong>
14
+ <span id="sourceLabel"></span>
15
+ <span id="dirtyBadge" class="dirty-badge" hidden>Unsaved</span>
16
+ </div>
17
+ <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>
36
+ <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>
38
+ </div>
39
+ </header>
40
+
41
+ <main class="editor-layout">
42
+ <aside class="editor-sidebar element-panel" aria-label="Elements">
43
+ <h2>Elements</h2>
44
+ <div id="elementTree" class="element-tree" role="tree"></div>
45
+ </aside>
46
+
47
+ <section id="viewport" class="editor-viewport" aria-label="Drawing area">
48
+ <div id="canvasSurface" class="canvas-surface"></div>
49
+ </section>
50
+
51
+ <aside class="editor-sidebar inspector-panel" aria-label="Properties">
52
+ <h2>Properties</h2>
53
+ <form id="inspector" class="inspector" autocomplete="off"></form>
54
+ </aside>
55
+ </main>
56
+
57
+ <footer class="editor-footer">
58
+ <span id="status" role="status" aria-live="polite">Loading…</span>
59
+ <span id="zoomStatus">100%</span>
60
+ </footer>
61
+
62
+ <div id="contextMenu" class="context-menu" role="menu" aria-label="Editing actions" tabindex="-1" hidden></div>
63
+
64
+ <dialog id="assetDialog" class="asset-dialog" aria-labelledby="assetDialogTitle">
65
+ <div class="asset-dialog-header">
66
+ <div>
67
+ <h2 id="assetDialogTitle">Select an image</h2>
68
+ <p>Select an image from the workspace assets/ directory or import one from your PC.</p>
69
+ </div>
70
+ <button id="assetDialogClose" type="button" aria-label="Close image selection">×</button>
71
+ </div>
72
+ <div class="asset-dialog-tools">
73
+ <label for="assetSearch">Search images</label>
74
+ <input id="assetSearch" type="search" placeholder="File name or path">
75
+ <button id="assetImportButton" type="button">Import from PC</button>
76
+ <input id="assetFileInput" type="file" accept=".svg,.png,.webp,.jpg,.jpeg,image/svg+xml,image/png,image/webp,image/jpeg" hidden>
77
+ </div>
78
+ <div class="asset-dialog-body">
79
+ <div id="assetList" class="asset-list" role="listbox" aria-label="Available images"></div>
80
+ <div class="asset-preview">
81
+ <div class="asset-preview-frame">
82
+ <div id="assetPreviewImageHost"></div>
83
+ <span id="assetPreviewEmpty">Select an image</span>
84
+ </div>
85
+ <p id="assetPreviewPath"></p>
86
+ </div>
87
+ </div>
88
+ <div class="asset-dialog-footer">
89
+ <span id="assetDialogStatus" role="status" aria-live="polite"></span>
90
+ <div>
91
+ <button id="assetCancelButton" type="button">Cancel</button>
92
+ <button id="assetChooseButton" class="save-button" type="button" disabled>Select</button>
93
+ </div>
94
+ </div>
95
+ </dialog>
96
+
97
+ <script type="module" src="./editor/editor.js"></script>
98
+ </body>
99
+ </html>
@@ -110,7 +110,7 @@ export async function readGuide(topic = "overview") {
110
110
  section(readme, "## How it works"),
111
111
  "",
112
112
  "Users can load workspace Markdown directly with the canvas 📂 button (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 MarkdStage's ✎ control to adjust the placement of an existing Architecture diagram. For comprehensive editing, including adding or deleting elements, open the architecture-editor canvas with sourcePath and blockIndex. Comprehensive edits affect the source Markdown only when explicitly saved.",
113
+ "Use MarkdStage's ✎ control 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.",
114
114
  "",
115
115
  "For details, request `slide-format`, `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or `architecture-schema`.",
116
116
  ].join("\n");
@@ -3826,6 +3826,233 @@ export function architectureSemanticSnapshot(model) {
3826
3826
  };
3827
3827
  }
3828
3828
 
3829
+ /**
3830
+ * Preserve the normalized DSL and final routed geometry for hybrid PowerPoint
3831
+ * export. Coordinates remain in Architecture canvas units here; renderer.js
3832
+ * maps them through the rendered SVG viewBox into slide pixels.
3833
+ */
3834
+ export function architecturePowerPointSnapshot(model, documentRef = globalThis.document) {
3835
+ const lookup = new Map(
3836
+ model.elements
3837
+ .filter((element) => element.type !== "connector")
3838
+ .map((element) => [element.id, element]),
3839
+ );
3840
+ const { routes, diagnostics } = planConnectorRoutes(model, lookup);
3841
+ const objects = [];
3842
+ const fallbacks = [];
3843
+ const icons = [];
3844
+ const frontLabels = [];
3845
+ const source = (element, kind = element.type) => ({
3846
+ kind,
3847
+ id: element.id || "",
3848
+ sourcePath: element.sourcePath,
3849
+ order: element.order,
3850
+ z: element.z,
3851
+ });
3852
+ const style = (element) => ({
3853
+ fill: element.style.fill,
3854
+ stroke: element.style.stroke,
3855
+ strokeWidth: element.style.strokeWidth,
3856
+ dash: element.style.dash,
3857
+ opacity: element.style.opacity,
3858
+ cornerRadius: element.style.cornerRadius,
3859
+ });
3860
+ const shapeText = (element, text, options = {}) =>
3861
+ text
3862
+ ? {
3863
+ paragraphs: [
3864
+ {
3865
+ alignment: options.alignment || "center",
3866
+ runs: [
3867
+ {
3868
+ text,
3869
+ fontSize: options.fontSize || element.style.fontSize,
3870
+ fontWeight: options.fontWeight || 600,
3871
+ bold: (options.fontWeight || 600) >= 600,
3872
+ color: element.style.textColor,
3873
+ },
3874
+ ],
3875
+ },
3876
+ ],
3877
+ }
3878
+ : undefined;
3879
+
3880
+ for (const element of model.elements) {
3881
+ if (element.type === "group") {
3882
+ objects.push({
3883
+ type: "shape",
3884
+ shape: "roundedRect",
3885
+ x: element.x,
3886
+ y: element.y,
3887
+ width: element.width,
3888
+ height: element.height,
3889
+ ...style(element),
3890
+ text: shapeText(element, element.title, {
3891
+ alignment: "left",
3892
+ fontWeight: 700,
3893
+ }),
3894
+ verticalAlignment: "top",
3895
+ textWrap: "none",
3896
+ textInsets: {
3897
+ left: 24,
3898
+ top: Math.max(0, element.style.fontSize * 0.2),
3899
+ right: 24,
3900
+ bottom: 0,
3901
+ },
3902
+ architecture: source(element),
3903
+ });
3904
+ continue;
3905
+ }
3906
+
3907
+ if (element.type === "node") {
3908
+ const iconSize = element.icon
3909
+ ? Math.min(58, element.height * 0.36, element.width * 0.2)
3910
+ : 0;
3911
+ const iconX = element.x + Math.max(20, element.width * 0.08);
3912
+ const iconY = element.y + element.height / 2 - iconSize / 2;
3913
+ 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);
3917
+ const longestLine = Math.max(
3918
+ ...element.text.split(/\r?\n/).slice(0, 8).map(textWidthUnits),
3919
+ 1,
3920
+ );
3921
+ const fittedFontSize = Math.min(
3922
+ element.style.fontSize,
3923
+ Math.max(MIN_FONT_SIZE, availableTextWidth / longestLine),
3924
+ );
3925
+ objects.push({
3926
+ type: "shape",
3927
+ shape:
3928
+ element.shape === "ellipse"
3929
+ ? "ellipse"
3930
+ : element.shape === "rounded-rect"
3931
+ ? "roundedRect"
3932
+ : "rect",
3933
+ x: element.x,
3934
+ y: element.y,
3935
+ width: element.width,
3936
+ height: element.height,
3937
+ ...style(element),
3938
+ text: shapeText(element, element.text, {
3939
+ fontSize: fittedFontSize,
3940
+ }),
3941
+ verticalAlignment: "middle",
3942
+ textWrap: element.text.includes("\n") ? "square" : "none",
3943
+ textInsets: {
3944
+ left: textInset,
3945
+ top: 12,
3946
+ right: 16,
3947
+ bottom: 12,
3948
+ },
3949
+ icon: element.icon || "",
3950
+ architecture: source(element),
3951
+ });
3952
+ if (element.icon) {
3953
+ icons.push({
3954
+ id: element.id,
3955
+ icon: element.icon,
3956
+ sourcePath: element.sourcePath,
3957
+ x: iconX,
3958
+ y: iconY,
3959
+ width: iconSize,
3960
+ height: iconSize,
3961
+ opacity: element.style.opacity,
3962
+ order: element.order,
3963
+ z: element.z,
3964
+ });
3965
+ }
3966
+ continue;
3967
+ }
3968
+
3969
+ if (element.type === "image") {
3970
+ objects.push({
3971
+ type: "image",
3972
+ x: element.x,
3973
+ y: element.y,
3974
+ width: element.width,
3975
+ height: element.height,
3976
+ src: localAssetUrl(documentRef || {}, element.src),
3977
+ source: element.src,
3978
+ fit: element.fit,
3979
+ opacity: element.style.opacity,
3980
+ architecture: source(element),
3981
+ });
3982
+ continue;
3983
+ }
3984
+
3985
+ const points = routes.get(element);
3986
+ objects.push({
3987
+ type: "connector",
3988
+ points,
3989
+ x: Math.min(...points.map((point) => point.x)),
3990
+ y: Math.min(...points.map((point) => point.y)),
3991
+ width:
3992
+ Math.max(...points.map((point) => point.x)) -
3993
+ Math.min(...points.map((point) => point.x)),
3994
+ height:
3995
+ Math.max(...points.map((point) => point.y)) -
3996
+ Math.min(...points.map((point) => point.y)),
3997
+ stroke: element.style.stroke,
3998
+ strokeWidth: element.style.strokeWidth,
3999
+ dash: element.style.dash,
4000
+ opacity: element.style.opacity,
4001
+ arrowEnd: element.arrow,
4002
+ from: element.from,
4003
+ to: element.to,
4004
+ architecture: source(element),
4005
+ });
4006
+ if (element.label) {
4007
+ const metrics = connectorLabelMetrics(element, model.canvas);
4008
+ const anchor = connectorLabelAnchor(element, points, metrics, model.canvas);
4009
+ const labelShape = {
4010
+ type: "shape",
4011
+ shape: "roundedRect",
4012
+ x: anchor.x - metrics.width / 2,
4013
+ y: anchor.y - metrics.height / 2,
4014
+ width: metrics.width,
4015
+ height: metrics.height,
4016
+ fill: "var(--surface)",
4017
+ stroke: "var(--border)",
4018
+ strokeWidth: CONNECTOR_LABEL_STROKE_WIDTH,
4019
+ dash: "",
4020
+ opacity: element.style.opacity,
4021
+ cornerRadius: metrics.height / 2,
4022
+ text: shapeText(element, metrics.text, {
4023
+ fontSize: metrics.fontSize,
4024
+ }),
4025
+ verticalAlignment: "middle",
4026
+ textWrap: "none",
4027
+ textInsets: {
4028
+ left: 8,
4029
+ top: 2,
4030
+ right: 8,
4031
+ bottom: 2,
4032
+ },
4033
+ architecture: source(element, "connector-label"),
4034
+ };
4035
+ if (element.labelLayer === "front") frontLabels.push(labelShape);
4036
+ else objects.push(labelShape);
4037
+ }
4038
+ }
4039
+ objects.push(...frontLabels);
4040
+
4041
+ return {
4042
+ version: model.version,
4043
+ title: model.title,
4044
+ description: model.description,
4045
+ canvas: { ...model.canvas },
4046
+ routing: {
4047
+ degraded: diagnostics.length > 0,
4048
+ diagnostics,
4049
+ },
4050
+ objects,
4051
+ icons,
4052
+ fallbacks,
4053
+ };
4054
+ }
4055
+
3829
4056
  export function renderArchitectureDiagram(
3830
4057
  model,
3831
4058
  documentRef = globalThis.document,
@@ -3931,6 +4158,12 @@ export function renderArchitectureDiagram(
3931
4158
  });
3932
4159
  frontLabels.forEach((label) => svg.appendChild(label));
3933
4160
  wrapper.appendChild(svg);
4161
+ const powerPointSnapshot = architecturePowerPointSnapshot(model, documentRef);
4162
+ Object.defineProperty(wrapper, "__presentationPptxSnapshot", {
4163
+ value: powerPointSnapshot,
4164
+ enumerable: true,
4165
+ });
4166
+ wrapper.setAttribute("data-architecture-pptx", "ready");
3934
4167
  appendRoutingWarning(documentRef, wrapper, routingDiagnostics);
3935
4168
  return wrapper;
3936
4169
  }
@@ -22,12 +22,13 @@
22
22
  <button id="navPrev" class="nav-btn" type="button" title="Previous (←)" aria-label="Previous slide">&#9664;</button>
23
23
  <span id="navCounter" class="nav-counter" aria-live="polite"></span>
24
24
  <button id="navNext" class="nav-btn" type="button" title="Next (→)" aria-label="Next slide">&#9654;</button>
25
- <button id="navEdit" class="nav-btn" type="button" title="Shape editing mode" aria-label="Shape editing mode">&#x270E;</button>
26
- <button id="navPresent" class="nav-btn" type="button" title="Open in external window (F11 for full screen)" aria-label="Open in external window">&#x26F6;</button>
27
- <button id="navPresenterView" class="nav-btn" type="button" title="Presenter view" aria-label="Presenter view">&#128100;</button>
25
+ <button id="navEdit" class="nav-btn" type="button" title="Shape editing mode" aria-label="Shape editing mode" hidden>&#x270E;</button>
26
+ <button id="navPresent" class="nav-btn" type="button" title="Open in external window (F11 for full screen)" aria-label="Open in external window" hidden>&#x26F6;</button>
27
+ <button id="navPresenterView" class="nav-btn" type="button" title="Presenter view" aria-label="Presenter view" hidden>&#128100;</button>
28
28
  <button id="navFixedPreview" class="nav-btn nav-fixed-preview" type="button" title="Preview PDF layout at 16:9" aria-label="Preview PDF layout at 16:9" aria-pressed="false">16:9</button>
29
- <button id="navExport" class="nav-btn" type="button" title="Save as PDF" aria-label="Save as PDF">&#128438;</button>
30
- <button id="navImport" class="nav-btn" type="button" title="Load Markdown (I)" aria-label="Load Markdown">&#128193;</button>
29
+ <button id="navExport" class="nav-btn" type="button" title="Save as PDF" aria-label="Save as PDF" hidden>&#128438;</button>
30
+ <button id="navExportPptx" class="nav-btn" type="button" title="Save as editable PowerPoint" aria-label="Save as editable PowerPoint" hidden>P</button>
31
+ <button id="navImport" class="nav-btn" type="button" title="Load Markdown (I)" aria-label="Load Markdown" hidden>&#128193;</button>
31
32
  <button id="navSourceMode" class="nav-btn" type="button" title="Automatically refresh when Markdown is saved" aria-label="Automatically refresh when Markdown is saved" hidden>&#8635;</button>
32
33
  <button id="navList" class="nav-btn" type="button" title="Slide list (O)" aria-label="Slide list">&#9776;</button>
33
34
  <span id="presentStatus" class="sr-only" aria-live="polite"></span>
@@ -64,7 +65,7 @@
64
65
  <span id="presenterCounter" class="presenter-counter" aria-live="polite"></span>
65
66
  <button id="presenterNextButton" class="presenter-button" type="button">Next</button>
66
67
  <button id="presenterListButton" class="presenter-button" type="button">Slide list</button>
67
- <button id="presenterToggleButton" class="presenter-button presenter-toggle" type="button">Start presentation</button>
68
+ <button id="presenterToggleButton" class="presenter-button presenter-toggle" type="button" hidden>Start presentation</button>
68
69
  </footer>
69
70
  </section>
70
71