@markdstage/markdstage 2.3.0 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdstage/markdstage",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
5
5
  "license": "MIT",
6
6
  "author": "runceel",
package/shared/README.md CHANGED
@@ -27,8 +27,8 @@ Canvas iframe (renderer/)
27
27
  | highlights language-tagged code fences with highlight.js
28
28
  | converts ```mermaid blocks with mermaid.run
29
29
  | converts validated ```architecture JSON DSL into a safe SVG DOM
30
- | provides ◀ ▶, ✎, 16:9 PDF preview, margin clicks, arrow keys, and the ☰ slide list
31
- | opens a synchronized external window with
30
+ | keepspage ▶, ☰, and visible in a compact control bar
31
+ | groups editing, presentation, preview, import, refresh, and export under ⋯
32
32
  v
33
33
  The themed slide is displayed and updates automatically
34
34
  ```
@@ -75,26 +75,30 @@ The themed slide is displayed and updates automatically
75
75
  - Put **speaker notes** in top-level HTML comments on each slide. Presenter view
76
76
  renders notes as Markdown and follows navigation. Notes are absent from
77
77
  regular slides, the external presenter, and PDF output.
78
- - The **canvas renderer** owns navigation controls, editing, the slide list,
79
- and current position. toggles the same placement mode as
80
- `edit_architecture`. For Markdown loaded with 📂, **Advanced editing** opens
78
+ - The **canvas renderer** owns the compact navigation controls, shape editing,
79
+ the slide list, and current position. **More controls > Shape editing**
80
+ toggles the same placement mode as `edit_architecture`. For Markdown loaded
81
+ with **More controls > Open Markdown**, **Advanced editing** opens
81
82
  the dedicated `architecture-editor` canvas. The agent only opens the deck and
82
83
  does not run an `ask_user` loop. Margin clicks are installed only in normal
83
84
  canvas and presenter modes, never print mode. `goto_slide` remains available
84
85
  for an explicit page request from chat.
85
- - **PDF Export is available from the printer icon.** When `sourceName` is passed
86
+ - **PDF Export is available from More controls > Export PDF.** When
87
+ `sourceName` is passed
86
88
  to open / `load_deck`, the printer saves `<source-name>.pdf` in the workspace.
87
89
  It does not load or watch that file. AI may call `export_pdf` with another
88
90
  `outputPath`. Hidden print mode renders every page, then headless Edge/Chrome
89
91
  produces a 16:9 PDF with backgrounds, images, highlighted code, and Mermaid.
90
- - **Hybrid editable PowerPoint export is available from the P control.** It
92
+ - **Hybrid editable PowerPoint export is available from More controls > Export
93
+ PowerPoint.** It
91
94
  preserves supported text, lists, links, tables, raster images, and Architecture
92
95
  DSL objects as native PowerPoint content. Architecture nodes, groups, and
93
96
  connector-label pills are visible AutoShapes with integrated text; icons use a
94
97
  transparent foreground picture layer. Mermaid and unsupported styling stay
95
98
  visible as background artwork and are listed in the export report. AI may call
96
99
  `export_pptx` with another workspace-confined `.pptx` path.
97
- - Use the **16:9 control** to letterbox the current slide inside the canvas with
100
+ - Use **More controls > Output preview** to letterbox the current slide inside
101
+ the canvas with
98
102
  the same fixed 1280×720 typography, spacing, diagram limits, and clipping used
99
103
  by PDF output. This preview is local to the canvas and does not change deck
100
104
  state. A visible and accessible warning identifies content that would be
@@ -109,7 +113,8 @@ The themed slide is displayed and updates automatically
109
113
  element hints. Call `capture_slides` only when visual inspection is needed;
110
114
  PNGs are fixed 1280×720 files and the action returns paths instead of inline
111
115
  image data.
112
- - Open the **external presenter** with or `open_presenter`. Edge / Chrome /
116
+ - Open the **external presenter** with **More controls > External window** or
117
+ `open_presenter`. Edge / Chrome /
113
118
  Chromium starts in a dedicated temporary profile as a movable, resizable
114
119
  1280×720 app-mode window. It shares `/state`, `/navigate`, and SSE with the
115
120
  canvas, so keyboard and Surface Pen position remain synchronized. Move it to
@@ -124,12 +129,13 @@ The themed slide is displayed and updates automatically
124
129
  - Add language names such as `csharp`, `json`, or `diff` to code fences for
125
130
  highlight.js syntax highlighting.
126
131
 
127
- ## Markdown import (📂)
132
+ ## Markdown import
128
133
 
129
- Users can load Markdown directly from the canvas without AI. Press 📂 or `I` to
130
- open a filtered list of workspace `*.md` / `*.markdown` files. Selecting one
131
- loads it, splits it into slides, and replaces the deck. Import also works from
132
- the initial waiting view. The control is not shown in presenter or print mode.
134
+ Users can load Markdown directly from the canvas without AI. Select **More
135
+ controls > Open Markdown** or press `I` to open a filtered list of workspace
136
+ `*.md` / `*.markdown` files. Selecting one loads it, splits it into slides, and
137
+ replaces the deck. Import also works from the initial waiting view. The control
138
+ is not shown in presenter or print mode.
133
139
  The workspace root is the Git repository root when available, otherwise the
134
140
  folder opened for the current session.
135
141
 
@@ -728,7 +734,7 @@ should determine placement without writing every coordinate.
728
734
  ### Placement editing
729
735
 
730
736
  Architecture diagrams can be moved directly over the rendered result. For decks
731
- imported through 📂, edits write back to the source `architecture` fence and
737
+ imported through **More controls > Open Markdown**, edits write back to the source `architecture` fence and
732
738
  survive re-import. Decks supplied directly through open / `load_deck` cannot be
733
739
  reversibly mapped to a source file, so they save only to canvas deck state.
734
740
  In the standalone CLI, `markdstage present slides.md --watch` enables the same
@@ -808,7 +814,7 @@ blocks.
808
814
  - Saving reloads any MarkdStage canvas or watched CLI presentation showing the
809
815
  same Markdown while preserving the current page and theme.
810
816
 
811
- Source-backed decks imported through 📂 and CLI `present --watch` enable
817
+ Source-backed decks imported through **More controls > Open Markdown** and CLI `present --watch` enable
812
818
  **Advanced editing**. Agents can open the editor canvas directly:
813
819
 
814
820
  ```json
@@ -907,7 +913,8 @@ special layout and are never auto-enlarged.
907
913
  | Long press | `Win+F18` | Previous slide |
908
914
 
909
915
  `Win+F19` double press and pen connection/removal/docking do not launch
910
- MarkdStage. Start the presenter through or explicit `open_presenter`.
916
+ MarkdStage. Start the presenter through **More controls > External window** or
917
+ explicit `open_presenter`.
911
918
 
912
919
  The Windows setting that lets apps override shortcut-button behavior may remain
913
920
  enabled. This implementation listens directly for Windows pen shortcuts rather
@@ -956,14 +963,14 @@ fails with `invalid_input`; pass the complete `slides` array or call
956
963
  | `GET /export-data` | Return the token-bound deck snapshot to print, inspection, capture, or PowerPoint mode. |
957
964
  | `POST /export-status` | Let headless output modes report rendering status and fixed-layout diagnostics. |
958
965
  | `POST /navigate` | Accept absolute `{ index }` or relative `{ delta }`, update position, and notify all clients through SSE. |
959
- | `POST /present` | Start the presenter from ⛶; accepts same-origin POST only. |
960
- | `POST /export` | Start source-named PDF export from the printer icon; accepts same-origin POST only. |
961
- | `POST /export-pptx` | Start source-named hybrid PowerPoint export from the P control; accepts same-origin POST only. |
966
+ | `POST /present` | Start the presenter from **More controls > External window**; accepts same-origin POST only. |
967
+ | `POST /export` | Start source-named PDF export from **More controls > Export PDF**; accepts same-origin POST only. |
968
+ | `POST /export-pptx` | Start source-named hybrid PowerPoint export from **More controls > Export PowerPoint**; accepts same-origin POST only. |
962
969
  | `POST /edit` | Write an edited fence with `{ index, block, source }`; returns `409 edit_mode_disabled` when editing is off. |
963
970
  | `POST /edit-mode` | Set `{ enabled }`, including for `?architectureEdit=1`; same-origin POST only. |
964
971
  | `POST /architecture-editor/open` | Convert source-backed slide/block indexes to a file-wide block index and open Architecture Editor. |
965
972
  | `GET /events` | SSE nudge for low-latency `version` changes. |
966
- | `GET /markdown-files` | Return bounded workspace-relative `*.md` / `*.markdown` paths for 📂, excluding `.git`, `node_modules`, and dot-prefixed entries. |
973
+ | `GET /markdown-files` | Return bounded workspace-relative `*.md` / `*.markdown` paths for **Open Markdown**, excluding `.git`, `node_modules`, and dot-prefixed entries. |
967
974
  | `POST /import` | Load and split `{ path, sourceMode?: "snapshot" | "live" }`; reject paths outside workspace, wrong extensions, and oversized files. Same-origin POST only. |
968
975
  | `POST /source-mode` | Switch a source-backed deck with `{ mode: "snapshot" | "live" }`; entering live mode loads the latest source. |
969
976
 
@@ -978,7 +985,7 @@ fails with `invalid_input`; pass the complete `slides` array or call
978
985
  editor.css # Workspace, tree, and inspector styles
979
986
  editor.js # Diagram commands, draft, and explicit-save UI
980
987
  copilot-extension.json # Manifest for Gist sharing
981
- markdown-deck.mjs # Raw Markdown splitting for 📂 import
988
+ markdown-deck.mjs # Raw Markdown splitting for canvas import
982
989
  scripts/
983
990
  markdown-blocks.mjs # Scan and replace architecture fences
984
991
  markdown-files.mjs # Scan workspace Markdown
@@ -109,8 +109,8 @@ export async function readGuide(topic = "overview") {
109
109
  return [
110
110
  section(readme, "## How it works"),
111
111
  "",
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. 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.",
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.",
114
114
  "",
115
115
  "For details, request `slide-format`, `themes`, `custom-themes`, `theme-schema`, `architecture-dsl`, or `architecture-schema`.",
116
116
  ].join("\n");
@@ -19,18 +19,58 @@
19
19
 
20
20
  <!-- In-canvas navigation controls (persist across slide re-renders). -->
21
21
  <nav id="nav" class="nav" hidden aria-label="Slide controls">
22
- <button id="navPrev" class="nav-btn" type="button" title="Previous (←)" aria-label="Previous slide">&#9664;</button>
23
- <span id="navCounter" class="nav-counter" aria-live="polite"></span>
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" 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
- <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" 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>
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>
33
- <button id="navList" class="nav-btn" type="button" title="Slide list (O)" aria-label="Slide list">&#9776;</button>
22
+ <div class="nav-main" role="group" aria-label="Slide navigation">
23
+ <button id="navPrev" class="nav-btn" type="button" title="Previous (←)" aria-label="Previous slide">&#9664;</button>
24
+ <span id="navCounter" class="nav-counter" aria-live="polite"></span>
25
+ <button id="navNext" class="nav-btn" type="button" title="Next (→)" aria-label="Next slide">&#9654;</button>
26
+ <button id="navList" class="nav-btn" type="button" title="Slide list (O)" aria-label="Slide list">&#9776;</button>
27
+ </div>
28
+ <div id="navTools" class="nav-tools">
29
+ <button id="navMore" class="nav-btn nav-more-toggle" type="button" title="More controls" aria-label="More controls" aria-expanded="false" aria-controls="navMorePanel">&#8943;</button>
30
+ </div>
31
+ <div id="navMorePanel" class="nav-more-panel" role="group" aria-label="More slide controls" hidden>
32
+ <section class="nav-more-group" data-nav-group="present" aria-labelledby="navMorePresentLabel">
33
+ <span id="navMorePresentLabel" class="nav-more-title">Present</span>
34
+ <button id="navPresent" class="nav-more-item" type="button" title="Open in external window (F11 for full screen)" aria-label="Open in external window" hidden>
35
+ <span class="nav-more-icon" aria-hidden="true">&#x26F6;</span>
36
+ <span class="nav-more-label">External window</span>
37
+ </button>
38
+ <button id="navPresenterView" class="nav-more-item" type="button" title="Presenter view" aria-label="Presenter view" hidden>
39
+ <span class="nav-more-icon" aria-hidden="true">&#128100;</span>
40
+ <span class="nav-more-label">Presenter view</span>
41
+ </button>
42
+ </section>
43
+ <section class="nav-more-group" data-nav-group="view" aria-labelledby="navMoreViewLabel">
44
+ <span id="navMoreViewLabel" class="nav-more-title">View &amp; edit</span>
45
+ <button id="navEdit" class="nav-more-item" type="button" title="Shape editing mode" aria-label="Shape editing mode" hidden>
46
+ <span class="nav-more-icon" aria-hidden="true">&#x270E;</span>
47
+ <span class="nav-more-label">Shape editing</span>
48
+ </button>
49
+ <button id="navFixedPreview" class="nav-more-item" type="button" title="Preview PDF layout at 16:9" aria-label="Preview PDF layout at 16:9" aria-pressed="false">
50
+ <span class="nav-more-icon nav-fixed-preview" aria-hidden="true">16:9</span>
51
+ <span class="nav-more-label">Output preview</span>
52
+ </button>
53
+ </section>
54
+ <section class="nav-more-group" data-nav-group="file" aria-labelledby="navMoreFileLabel">
55
+ <span id="navMoreFileLabel" class="nav-more-title">File</span>
56
+ <button id="navImport" class="nav-more-item" type="button" title="Open Markdown (I)" aria-label="Open Markdown" hidden>
57
+ <span class="nav-more-icon" aria-hidden="true">&#128193;</span>
58
+ <span class="nav-more-label">Open Markdown</span>
59
+ </button>
60
+ <button id="navSourceMode" class="nav-more-item" type="button" title="Automatically refresh when Markdown is saved" aria-label="Automatically refresh when Markdown is saved" hidden>
61
+ <span class="nav-more-icon" aria-hidden="true">&#8635;</span>
62
+ <span class="nav-more-label">Automatic refresh</span>
63
+ </button>
64
+ <button id="navExport" class="nav-more-item" type="button" title="Save as PDF" aria-label="Save as PDF" hidden>
65
+ <span class="nav-more-icon" aria-hidden="true">&#128438;</span>
66
+ <span class="nav-more-label">Export PDF</span>
67
+ </button>
68
+ <button id="navExportPptx" class="nav-more-item" type="button" title="Save as editable PowerPoint" aria-label="Save as editable PowerPoint" hidden>
69
+ <span class="nav-more-icon" aria-hidden="true">P</span>
70
+ <span class="nav-more-label">Export PowerPoint</span>
71
+ </button>
72
+ </section>
73
+ </div>
34
74
  <span id="presentStatus" class="sr-only" aria-live="polite"></span>
35
75
  <span id="exportStatus" class="sr-only" aria-live="polite"></span>
36
76
  <span id="sourceStatus" class="sr-only" aria-live="polite"></span>
@@ -98,6 +98,7 @@ let previewMode = false;
98
98
  let previewOffset = 0;
99
99
  let navigationEnabled = true;
100
100
  let fixedPreviewMode = false;
101
+ let moreControlsOpen = false;
101
102
  // Markdown for the most recently rendered slide, retained for editing-mode rerenders.
102
103
  let lastMarkdown = "";
103
104
  // Editing UI attached to the rendered slide; destroyed on every rerender.
@@ -427,12 +428,14 @@ function updateFixedPreviewWarning() {
427
428
  warning.textContent = "";
428
429
  }
429
430
  if (button) button.dataset.state = fixedPreviewMode ? "active" : "";
431
+ syncMoreControls();
430
432
  return;
431
433
  }
432
434
 
433
435
  const diagnostic = collectSlideLayout(layoutTarget, navIndex);
434
436
  document.body.classList.toggle("fixed-preview-overflow", diagnostic.pdfClipped);
435
437
  if (button) button.dataset.state = diagnostic.pdfClipped ? "error" : "active";
438
+ syncMoreControls();
436
439
  if (!warning) return;
437
440
  if (!diagnostic.pdfClipped) {
438
441
  warning.hidden = true;
@@ -2141,6 +2144,7 @@ function updateArchitectureEditButton(enabled = architectureEditMode) {
2141
2144
  button.dataset.state = enabled && !presenterMode ? "active" : "";
2142
2145
  button.title = enabled ? "Exit shape editing mode" : "Shape editing mode";
2143
2146
  button.setAttribute("aria-label", button.title);
2147
+ syncMoreControls();
2144
2148
  }
2145
2149
 
2146
2150
  function sourceWatchErrorMessage(code) {
@@ -2160,6 +2164,7 @@ function updateSourceModeButton() {
2160
2164
  if (!sourceBacked) {
2161
2165
  button.dataset.state = "";
2162
2166
  if (status) status.textContent = "";
2167
+ syncMoreControls();
2163
2168
  return;
2164
2169
  }
2165
2170
  if (sourceMode === "live" && sourceWatchStatus === "error") {
@@ -2168,6 +2173,7 @@ function updateSourceModeButton() {
2168
2173
  button.title = `${message}. Click to pin the display to the loaded snapshot`;
2169
2174
  button.setAttribute("aria-label", button.title);
2170
2175
  if (status) status.textContent = message;
2176
+ syncMoreControls();
2171
2177
  return;
2172
2178
  }
2173
2179
  const live = sourceMode === "live";
@@ -2181,6 +2187,7 @@ function updateSourceModeButton() {
2181
2187
  ? "Slides refresh automatically when Markdown is saved"
2182
2188
  : "Markdown retains the display from the loaded snapshot";
2183
2189
  }
2190
+ syncMoreControls();
2184
2191
  }
2185
2192
 
2186
2193
  async function requestSourceMode(mode) {
@@ -2508,6 +2515,7 @@ async function setPresenterRunning(running) {
2508
2515
  button.dataset.state = "error";
2509
2516
  button.title = message;
2510
2517
  }
2518
+ syncMoreControls();
2511
2519
  } finally {
2512
2520
  presenterRequestPending = false;
2513
2521
  if (button) button.disabled = false;
@@ -2522,6 +2530,64 @@ function togglePresenterWindow() {
2522
2530
  return setPresenterRunning(!presenterRunning);
2523
2531
  }
2524
2532
 
2533
+ function visibleMoreControlButtons() {
2534
+ const panel = document.getElementById("navMorePanel");
2535
+ if (!panel) return [];
2536
+ return [...panel.querySelectorAll(".nav-more-item")].filter((button) => !button.hidden);
2537
+ }
2538
+
2539
+ function setMoreControlsOpen(enabled, { focusFirst = false, restoreFocus = false } = {}) {
2540
+ const nav = document.getElementById("nav");
2541
+ const trigger = document.getElementById("navMore");
2542
+ const panel = document.getElementById("navMorePanel");
2543
+ if (!nav || !trigger || !panel) return;
2544
+
2545
+ const next = Boolean(enabled) && !nav.classList.contains("nav-empty") && !trigger.hidden;
2546
+ moreControlsOpen = next;
2547
+ trigger.setAttribute("aria-expanded", next ? "true" : "false");
2548
+ panel.hidden = nav.classList.contains("nav-empty") ? !markdownImportAvailable : !next;
2549
+
2550
+ if (next && focusFirst) {
2551
+ requestAnimationFrame(() => visibleMoreControlButtons()[0]?.focus());
2552
+ } else if (!next && restoreFocus) {
2553
+ trigger.focus();
2554
+ }
2555
+ }
2556
+
2557
+ function toggleMoreControls() {
2558
+ setMoreControlsOpen(!moreControlsOpen, { focusFirst: !moreControlsOpen });
2559
+ }
2560
+
2561
+ function syncMoreControls() {
2562
+ const nav = document.getElementById("nav");
2563
+ const trigger = document.getElementById("navMore");
2564
+ const panel = document.getElementById("navMorePanel");
2565
+ if (!nav || !trigger || !panel) return;
2566
+
2567
+ panel.querySelectorAll(".nav-more-group").forEach((group) => {
2568
+ group.hidden = ![...group.querySelectorAll(".nav-more-item")].some(
2569
+ (button) => !button.hidden,
2570
+ );
2571
+ });
2572
+
2573
+ const buttons = visibleMoreControlButtons();
2574
+ const hasError = buttons.some((button) => button.dataset.state === "error");
2575
+ const hasActive = buttons.some((button) => button.dataset.state === "active");
2576
+ trigger.dataset.state = hasError ? "error" : hasActive ? "active" : "";
2577
+ trigger.title = hasError
2578
+ ? "More controls (attention required)"
2579
+ : hasActive
2580
+ ? "More controls (an option is active)"
2581
+ : "More controls";
2582
+ trigger.setAttribute("aria-label", trigger.title);
2583
+
2584
+ const empty = nav.classList.contains("nav-empty");
2585
+ trigger.hidden = empty || buttons.length === 0;
2586
+ if (trigger.hidden) moreControlsOpen = false;
2587
+ trigger.setAttribute("aria-expanded", moreControlsOpen ? "true" : "false");
2588
+ panel.hidden = empty ? !markdownImportAvailable : !moreControlsOpen;
2589
+ }
2590
+
2525
2591
  function updateHostActionButtons() {
2526
2592
  const present = document.getElementById("navPresent");
2527
2593
  if (present) present.hidden = presenterMode || !presenterWindowAvailable;
@@ -2537,6 +2603,7 @@ function updateHostActionButtons() {
2537
2603
  if (importButton) importButton.hidden = presenterMode || !markdownImportAvailable;
2538
2604
  if (!presenterViewAvailable && presenterViewOpen) closePresenterView();
2539
2605
  if (!markdownImportAvailable && importOpen) closeImportPicker();
2606
+ syncMoreControls();
2540
2607
  }
2541
2608
 
2542
2609
  function updatePresenterButton(running, message = "") {
@@ -2553,6 +2620,7 @@ function updatePresenterButton(running, message = "") {
2553
2620
  toggle.textContent = running ? "End presentation" : "Start presentation";
2554
2621
  toggle.dataset.state = running ? "active" : "";
2555
2622
  }
2623
+ syncMoreControls();
2556
2624
  }
2557
2625
 
2558
2626
  async function exportPdfFromCanvas() {
@@ -2582,6 +2650,7 @@ async function exportPdfFromCanvas() {
2582
2650
  button.dataset.state = "active";
2583
2651
  button.title = message;
2584
2652
  }
2653
+ syncMoreControls();
2585
2654
  } catch (error) {
2586
2655
  const message = error?.message || "Could not save the PDF.";
2587
2656
  console.error("PDF export failed", error);
@@ -2590,6 +2659,7 @@ async function exportPdfFromCanvas() {
2590
2659
  button.dataset.state = "error";
2591
2660
  button.title = message;
2592
2661
  }
2662
+ syncMoreControls();
2593
2663
  } finally {
2594
2664
  pdfExportPending = false;
2595
2665
  if (button) button.disabled = false;
@@ -2626,6 +2696,7 @@ async function exportPptxFromCanvas() {
2626
2696
  button.dataset.state = "active";
2627
2697
  button.title = message;
2628
2698
  }
2699
+ syncMoreControls();
2629
2700
  } catch (error) {
2630
2701
  const message = error?.message || "Could not save the PowerPoint presentation.";
2631
2702
  console.error("PowerPoint export failed", error);
@@ -2634,6 +2705,7 @@ async function exportPptxFromCanvas() {
2634
2705
  button.dataset.state = "error";
2635
2706
  button.title = message;
2636
2707
  }
2708
+ syncMoreControls();
2637
2709
  } finally {
2638
2710
  pptxExportPending = false;
2639
2711
  if (button) button.disabled = false;
@@ -2653,6 +2725,7 @@ function setFixedPreviewMode(enabled) {
2653
2725
  ? "Return to responsive canvas layout"
2654
2726
  : "Preview PDF layout at 16:9";
2655
2727
  }
2728
+ syncMoreControls();
2656
2729
  if (fixedPreviewMode) {
2657
2730
  updateFixedPreviewScale();
2658
2731
  } else {
@@ -2687,6 +2760,7 @@ function updateNav() {
2687
2760
  if (next) next.disabled = navMode === "deck" && navIndex >= navTotal - 1;
2688
2761
  highlightOverview();
2689
2762
  updatePresenterView();
2763
+ syncMoreControls();
2690
2764
  }
2691
2765
 
2692
2766
  function openPresenterView() {
@@ -2711,7 +2785,7 @@ function closePresenterView() {
2711
2785
  document.body.classList.remove("presenter-view-mode");
2712
2786
  const view = document.getElementById("presenterView");
2713
2787
  if (view) view.hidden = true;
2714
- document.getElementById("navPresenterView")?.focus();
2788
+ document.getElementById("navMore")?.focus();
2715
2789
  }
2716
2790
 
2717
2791
  function updatePresenterView() {
@@ -3035,10 +3109,11 @@ function wirePreviewKeyboardNavigation() {
3035
3109
  }
3036
3110
 
3037
3111
  function wireControls() {
3038
- const bind = (id, fn) => {
3112
+ const bind = (id, fn, { closeMore = false } = {}) => {
3039
3113
  const el = document.getElementById(id);
3040
3114
  if (!el) return;
3041
3115
  el.addEventListener("click", () => {
3116
+ if (closeMore) setMoreControlsOpen(false);
3042
3117
  fn();
3043
3118
  // Drop focus so a follow-up Space/Enter doesn't re-trigger the button on
3044
3119
  // top of the global keyboard handler.
@@ -3047,15 +3122,16 @@ function wireControls() {
3047
3122
  };
3048
3123
  bind("navPrev", goPrev);
3049
3124
  bind("navNext", goNext);
3050
- bind("navEdit", toggleArchitectureEditMode);
3051
- bind("navPresent", openPresenterWindow);
3052
- bind("navPresenterView", openPresenterView);
3053
- bind("navFixedPreview", toggleFixedPreviewMode);
3054
- bind("navExport", exportPdfFromCanvas);
3055
- bind("navExportPptx", exportPptxFromCanvas);
3056
- bind("navImport", toggleImportPicker);
3057
- bind("navSourceMode", toggleSourceMode);
3058
3125
  bind("navList", toggleOverview);
3126
+ bind("navMore", toggleMoreControls);
3127
+ bind("navEdit", toggleArchitectureEditMode, { closeMore: true });
3128
+ bind("navPresent", openPresenterWindow, { closeMore: true });
3129
+ bind("navPresenterView", openPresenterView, { closeMore: true });
3130
+ bind("navFixedPreview", toggleFixedPreviewMode, { closeMore: true });
3131
+ bind("navExport", exportPdfFromCanvas, { closeMore: true });
3132
+ bind("navExportPptx", exportPptxFromCanvas, { closeMore: true });
3133
+ bind("navImport", toggleImportPicker, { closeMore: true });
3134
+ bind("navSourceMode", toggleSourceMode, { closeMore: true });
3059
3135
  bind("overviewClose", closeOverview);
3060
3136
  bind("importClose", closeImportPicker);
3061
3137
  bind("presenterPrevButton", goPrev);
@@ -3083,6 +3159,14 @@ function wireControls() {
3083
3159
  });
3084
3160
  }
3085
3161
 
3162
+ document.addEventListener("pointerdown", (e) => {
3163
+ if (!moreControlsOpen) return;
3164
+ const tools = document.getElementById("navTools");
3165
+ const panel = document.getElementById("navMorePanel");
3166
+ if (tools?.contains(e.target) || panel?.contains(e.target)) return;
3167
+ setMoreControlsOpen(false);
3168
+ });
3169
+
3086
3170
  wirePointerNavigation();
3087
3171
 
3088
3172
  // The iframe must be focused to receive key events; grab focus up front and
@@ -3105,16 +3189,23 @@ function wireControls() {
3105
3189
  e.preventDefault();
3106
3190
  return;
3107
3191
  }
3192
+ if (e.key === "Escape" && moreControlsOpen) {
3193
+ setMoreControlsOpen(false, { restoreFocus: true });
3194
+ e.preventDefault();
3195
+ return;
3196
+ }
3108
3197
  if (handleSlideNavigationKey(e)) return;
3109
3198
  if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
3110
3199
  switch (e.key) {
3111
3200
  case "o":
3112
3201
  case "O":
3202
+ setMoreControlsOpen(false);
3113
3203
  toggleOverview();
3114
3204
  e.preventDefault();
3115
3205
  break;
3116
3206
  case "i":
3117
3207
  case "I":
3208
+ setMoreControlsOpen(false);
3118
3209
  toggleImportPicker();
3119
3210
  e.preventDefault();
3120
3211
  break;
@@ -389,6 +389,8 @@ footer .page{color:var(--accent-strong);font-weight:600;background:var(--accent-
389
389
  opacity:.45;transition:opacity .2s ease;}
390
390
  .nav:hover,.nav:focus-within{opacity:1;}
391
391
  @media (prefers-reduced-motion:reduce){.nav{transition:none;}}
392
+ .nav-main{display:flex;align-items:center;gap:.4em;}
393
+ .nav-tools{position:relative;display:flex;align-items:center;}
392
394
  .nav-btn{display:inline-flex;align-items:center;justify-content:center;
393
395
  min-width:2em;height:2em;padding:0 .55em;
394
396
  font-size:clamp(13px,1.9vh,17px);line-height:1;cursor:pointer;
@@ -400,10 +402,37 @@ footer .page{color:var(--accent-strong);font-weight:600;background:var(--accent-
400
402
  .nav-btn:disabled{opacity:.32;cursor:default;}
401
403
  .nav-btn[data-state="active"]{color:var(--accent-strong);background:var(--accent-soft);}
402
404
  .nav-btn[data-state="error"]{color:#d13438;}
403
- .nav-fixed-preview{min-width:3.25em;font-size:clamp(11px,1.55vh,14px);font-weight:700;}
405
+ .nav-more-toggle{position:relative;}
406
+ .nav-more-toggle[data-state]::after{content:"";position:absolute;right:.22em;top:.22em;
407
+ width:.38em;height:.38em;border-radius:50%;background:transparent;}
408
+ .nav-more-toggle[data-state="active"]::after{background:var(--accent-strong);}
409
+ .nav-more-toggle[data-state="error"]::after{background:#d13438;}
404
410
  .nav-counter{min-width:4.2em;text-align:center;font-variant-numeric:tabular-nums;
405
411
  font-weight:600;color:var(--accent-strong);font-size:clamp(12px,1.7vh,16px);
406
412
  padding:0 .3em;user-select:none;}
413
+ .nav-more-panel[hidden],.nav-more-group[hidden]{display:none;}
414
+ .nav-more-panel{position:absolute;left:50%;bottom:calc(100% + .65em);transform:translateX(-50%);
415
+ width:min(20em,calc(100vw - 24px));max-height:min(70vh,32em);overflow:auto;
416
+ display:grid;gap:.7em;padding:.7em;border:1px solid var(--border);border-radius:12px;
417
+ color:var(--fg);background:color-mix(in srgb,var(--surface) 96%,transparent);
418
+ box-shadow:0 12px 34px rgba(0,0,0,.3);}
419
+ .nav-more-group{display:grid;gap:.18em;}
420
+ .nav-more-title{padding:.2em .65em;color:var(--muted);font-size:.72em;font-weight:750;
421
+ letter-spacing:.06em;text-transform:uppercase;}
422
+ .nav-more-item{display:grid;grid-template-columns:2.8em minmax(0,1fr);align-items:center;
423
+ width:100%;min-height:2.55em;padding:.42em .65em;border:1px solid transparent;
424
+ border-radius:8px;color:var(--fg);background:transparent;font:inherit;text-align:left;
425
+ cursor:pointer;}
426
+ .nav-more-item[hidden]{display:none;}
427
+ .nav-more-item:hover:not(:disabled){background:var(--accent-soft);color:var(--accent-strong);}
428
+ .nav-more-item:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
429
+ .nav-more-item:disabled{opacity:.4;cursor:default;}
430
+ .nav-more-item[data-state="active"]{color:var(--accent-strong);background:var(--accent-soft);}
431
+ .nav-more-item[data-state="error"]{color:#d13438;}
432
+ .nav-more-icon{display:inline-flex;align-items:center;justify-content:center;min-width:2.2em;
433
+ font-size:.95em;font-weight:700;}
434
+ .nav-more-icon.nav-fixed-preview{font-size:.72em;}
435
+ .nav-more-label{min-width:0;font-size:.88em;font-weight:600;line-height:1.25;}
407
436
  .layout-warning{position:fixed;top:14px;right:14px;z-index:55;
408
437
  max-width:min(420px,calc(100vw - 28px));padding:.55em .8em;border-radius:8px;
409
438
  color:#fff;background:#a4262c;border:1px solid #f1aeb5;
@@ -418,14 +447,20 @@ body.presenter-mode #navImport{display:none;}
418
447
  body.presenter-mode #navSourceMode{display:none;}
419
448
  body.preview-mode .nav,body.preview-mode .overview{display:none!important;}
420
449
  /* When no deck is loaded, leave only the load button as the available action. */
421
- .nav.nav-empty > :not(#navImport){display:none;}
450
+ .nav.nav-empty .nav-main,.nav.nav-empty #navMore{display:none;}
451
+ .nav.nav-empty .nav-more-panel{position:static;display:block;width:auto;max-height:none;
452
+ overflow:visible;padding:0;border:0;border-radius:0;transform:none;background:transparent;
453
+ box-shadow:none;}
454
+ .nav.nav-empty .nav-more-group,.nav.nav-empty .nav-more-title,
455
+ .nav.nav-empty .nav-more-item{display:none;}
456
+ .nav.nav-empty .nav-more-group[data-nav-group="file"]{display:block;}
457
+ .nav.nav-empty #navImport{display:inline-flex;gap:.55em;width:auto;min-width:2.5em;height:2.5em;
458
+ padding:0 1em;}
422
459
  body.markdstage-empty .nav{
423
460
  opacity:1;padding:.42em;border-color:#3b4562;background:#151b2f;
424
461
  box-shadow:0 10px 30px rgba(0,0,0,.36);}
425
462
  body.markdstage-empty #navImport{
426
- width:auto;min-width:2.5em;height:2.5em;padding:0 1em;
427
463
  border-color:#ffb547;background:#ffb547;color:#0b1020;font-weight:750;}
428
- body.markdstage-empty #navImport::after{content:" Open Markdown";}
429
464
  body.markdstage-empty #navImport:hover{background:#ffd77a;color:#0b1020;}
430
465
 
431
466
  /* ===== Presenter view ===== */
package/src/skills.mjs CHANGED
@@ -24,11 +24,12 @@ export const SKILL_TARGETS = {
24
24
  };
25
25
 
26
26
  const DESCRIPTION =
27
- "Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user " +
28
- 'asks to present, preview, validate, screenshot, or export a Markdown deck ("present slides.md", ' +
29
- '"turn this file into slides", "export the deck to PDF", "check whether my slides fit"). ' +
30
- "Provides deterministic commands for presenting and visually editing Architecture DSL in a " +
31
- "browser, validating themes, inspecting 1280x720 clipping, capturing PNGs, and exporting PDF.";
27
+ "Turn Markdown into 16:9 slides with the MarkdStage CLI. Use when the user asks to create, " +
28
+ "refine, present, preview, validate, inspect, screenshot, or export a Markdown deck " +
29
+ '("present slides.md", "turn this file into slides", "export the deck to PDF or PowerPoint", ' +
30
+ '"check whether my slides fit"). Provides a deterministic create-review-deliver workflow, ' +
31
+ "browser-based Architecture DSL editing, theme validation, 1280x720 clipping diagnostics, " +
32
+ "targeted PNG capture, and PDF/PowerPoint export.";
32
33
 
33
34
  function frontMatter(fields) {
34
35
  const lines = ["---"];
@@ -66,32 +67,51 @@ what the MarkdStage canvas and MarkdStage Desktop render.
66
67
  - An installed Microsoft Edge, Google Chrome, or Chromium (never downloaded automatically).
67
68
  - The CLI: \`npx @markdstage/markdstage <command>\` or \`npm install --global @markdstage/markdstage\`.
68
69
 
69
- ## Workflow
70
-
71
- 1. Write or edit the deck as one Markdown file (see \`references/slide-format.md\`).
72
- 2. Validate it: \`markdstage validate slides.md --json\`.
73
- 3. Check the fixed 16:9 layout: \`markdstage inspect slides.md --json\`.
74
- 4. Capture clipped slides for review: \`markdstage capture slides.md\`.
75
- 5. Present or export: \`markdstage present slides.md --watch\` / \`markdstage export slides.md\`.
76
-
77
- Use \`present --watch\` for live authoring. It starts in viewing mode; the user can
78
- activate the pencil control to move Architecture elements, then choose **Advanced
79
- edit** for the detailed designer. Placement changes save immediately, while the
70
+ ## Recommended authoring workflow
71
+
72
+ 1. Establish the source material, audience, objective, approximate length, theme,
73
+ required diagrams, and output format.
74
+ 2. Read only the relevant guidance. Start with
75
+ \`markdstage guide slide-format\`, then retrieve \`themes\`,
76
+ \`custom-themes\`, or \`architecture-dsl\` when needed.
77
+ 3. Create the complete deck as one Markdown source file (see
78
+ \`references/slide-format.md\`).
79
+ 4. Validate structure, themes, and Architecture DSL before visual review:
80
+ \`markdstage validate slides.md --json\`.
81
+ 5. Use \`markdstage present slides.md --watch\` for live source-backed authoring.
82
+ It reloads on save without losing the current slide and keeps the last valid
83
+ deck while a save is incomplete.
84
+ 6. Check fixed 16:9 output with \`markdstage inspect slides.md --json\`. Use
85
+ \`--slide <n>\` after localized changes and \`--fail-on-issues\` in CI.
86
+ 7. Run \`markdstage capture slides.md\` only after inspection. Without
87
+ \`--pages\`, it captures only clipped slides; use \`--pages 2,4\` for pages
88
+ whose balance, spacing, or diagrams need visual judgment.
89
+ 8. Revise Markdown and repeat validation plus targeted inspection until the deck
90
+ is valid, unclipped, concise, and visually balanced.
91
+ 9. Deliver from the same source with \`markdstage presentation slides.md\`,
92
+ \`markdstage export slides.md --output slides.pdf\`, or
93
+ \`markdstage export slides.md --output slides.pptx\`.
94
+
95
+ The browser in \`present --watch\` starts in viewing mode. The user can activate
96
+ the pencil control to move Architecture elements, then choose **Advanced edit**
97
+ for the detailed designer. Placement changes save immediately, while the
80
98
  detailed designer saves only when the user selects **Save**. \`present\` without
81
99
  \`--watch\` is read-only.
82
100
 
83
101
  Never hand-write HTML or CSS for a slide. Fix layout problems by shortening the
84
- content or by changing the layout in front matter.
102
+ content or by changing the layout in front matter. Prefer structured validation
103
+ and layout diagnostics over capturing every slide.
85
104
 
86
105
  ## Commands
87
106
 
88
107
  | Command | Purpose |
89
108
  | --- | --- |
109
+ | \`markdstage presentation <file> [--watch]\` | Open presenter view with the current slide, next-slide preview, speaker notes, and controls for a synchronized audience window. |
90
110
  | \`markdstage present <file> [--watch]\` | Serve the deck on loopback and open it in a browser window. \`--watch\` reloads on save, keeps the current slide, and enables Architecture placement and detailed editing. Without it, the source is read-only. |
91
111
  | \`markdstage validate <file> [--json]\` | Check deck structure, Architecture DSL blocks, and themes. |
92
- | \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide. |
112
+ | \`markdstage inspect <file> [--json]\` | Report 1280x720 clipping diagnostics for the deck or one slide; use \`--fail-on-issues\` for quality gates. |
93
113
  | \`markdstage capture <file> [--pages 2,4]\` | Write 1280x720 PNG files; without \`--pages\` only clipped slides are captured. |
94
- | \`markdstage export <file> [--output slides.pdf]\` | Produce the 16:9 PDF. |
114
+ | \`markdstage export <file> [--output slides.pdf|slides.pptx]\` | Produce a 16:9 PDF or hybrid editable PowerPoint. |
95
115
  | \`markdstage guide <topic>\` | Print the canonical MarkdStage authoring guide. |
96
116
 
97
117
  Exit codes: \`0\` success, \`1\` usage error, \`2\` deck or input error, \`3\` no