mdv-live 0.5.4 → 0.5.8

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.
@@ -95,6 +95,13 @@
95
95
  PDF
96
96
  </button>
97
97
 
98
+ <button class="toolbar-btn" id="pdfStyleToggle" title="PDF style settings" aria-label="PDF style settings">
99
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
100
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
101
+ </svg>
102
+ Style
103
+ </button>
104
+
98
105
  <div class="toolbar-spacer"></div>
99
106
 
100
107
  <span class="editor-status" id="editorStatus" style="display: none;">Ready</span>
@@ -110,6 +117,19 @@
110
117
  </div>
111
118
  </div>
112
119
 
120
+ <div class="pdf-style-panel hidden" id="pdfStylePanel">
121
+ <label>
122
+ <span>CSS</span>
123
+ <input type="text" id="pdfStylePath" placeholder="src/styles/report.example.css">
124
+ </label>
125
+ <label>
126
+ <span>PDF options</span>
127
+ <input type="text" id="pdfOptionsPath" placeholder="src/styles/report.pdf-options.example.json">
128
+ </label>
129
+ <button class="toolbar-btn" id="pdfStyleApply">Apply</button>
130
+ <button class="toolbar-btn" id="pdfStyleClear">Clear</button>
131
+ </div>
132
+
113
133
  <!-- Tab bar -->
114
134
  <div class="tab-bar" id="tabBar" role="tablist" aria-label="Open files"></div>
115
135
 
@@ -157,6 +177,10 @@
157
177
  <!-- Hidden File Input -->
158
178
  <input type="file" id="fileInput" multiple hidden>
159
179
 
180
+ <script src="/static/lib/presenterChannel.js"></script>
181
+ <script src="/static/lib/apiClient.js"></script>
182
+ <script src="/static/lib/saveQueue.js"></script>
183
+ <script src="/static/lib/tabRegistry.js"></script>
160
184
  <script src="/static/app.js"></script>
161
185
  </body>
162
186
  </html>
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Thin HTTP client used by the main MDV window.
3
+ *
4
+ * Centralizes URL construction, common headers (If-Match), JSON parsing, and
5
+ * error normalization. Loaded as a classic `<script>` so the API is exposed
6
+ * on `window.MDVApi`.
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+
11
+ async function jsonOrEmpty(res) {
12
+ try { return await res.json(); } catch { return {}; }
13
+ }
14
+
15
+ /** GET /api/marp/decks/:path */
16
+ async function getDeck(path) {
17
+ const res = await fetch('/api/marp/decks/' + encodeURIComponent(path));
18
+ return { res, data: await jsonOrEmpty(res) };
19
+ }
20
+
21
+ /** PUT /api/marp/decks/:path/slides/:N/note (If-Match required) */
22
+ async function saveMarpNote(path, slideIndex, note, ifMatch) {
23
+ const url = '/api/marp/decks/' + encodeURIComponent(path)
24
+ + '/slides/' + slideIndex + '/note';
25
+ const res = await fetch(url, {
26
+ method: 'PUT',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ 'If-Match': ifMatch
30
+ },
31
+ body: JSON.stringify({ note })
32
+ });
33
+ return { res, data: await jsonOrEmpty(res) };
34
+ }
35
+
36
+ // --- Other MDV endpoints used throughout the app -------------------------
37
+
38
+ function fetchTree() { return fetch('/api/tree'); }
39
+ function expandTree(path) {
40
+ return fetch('/api/tree/expand?path=' + encodeURIComponent(path));
41
+ }
42
+ function fetchFile(path) {
43
+ return fetch('/api/file?path=' + encodeURIComponent(path));
44
+ }
45
+ function saveFile(path, content) {
46
+ return fetch('/api/file', {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({ path, content })
50
+ });
51
+ }
52
+ function fetchInfo() { return fetch('/api/info'); }
53
+ function exportPdf(payload) {
54
+ return fetch('/api/pdf/export', {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify(payload)
58
+ });
59
+ }
60
+
61
+ if (typeof globalThis !== 'undefined') {
62
+ globalThis.MDVApi = {
63
+ getDeck,
64
+ saveMarpNote,
65
+ fetchTree,
66
+ expandTree,
67
+ fetchFile,
68
+ saveFile,
69
+ fetchInfo,
70
+ exportPdf
71
+ };
72
+ }
73
+ })();
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Single source of truth for the BroadcastChannel name and message schemas
3
+ * used between the main MDV window and the Presenter window.
4
+ *
5
+ * Imported from a `<script>` tag (no module loader) so we expose globals on
6
+ * `window.MDVPresenterChannel`.
7
+ *
8
+ * Message types (discriminated by `type`):
9
+ *
10
+ * main → presenter
11
+ * { type: 'slides', path, html, css, etag, notes, notesMultiplicity, current }
12
+ * { type: 'slides', empty: true, reason } ← clear / no-deck
13
+ * { type: 'index', index }
14
+ * { type: 'note-saved', slideIndex, ok, etag?, normalizedNote?, code?, reason? }
15
+ *
16
+ * presenter → main
17
+ * { type: 'request-slides' }
18
+ * { type: 'goto', index }
19
+ * { type: 'edit-note', path, etag, slideIndex, note }
20
+ */
21
+ (function () {
22
+ 'use strict';
23
+ const CHANNEL_NAME = 'mdv-marp-presenter';
24
+
25
+ function create() {
26
+ if (typeof BroadcastChannel === 'undefined') return null;
27
+ return new BroadcastChannel(CHANNEL_NAME);
28
+ }
29
+
30
+ if (typeof globalThis !== 'undefined') {
31
+ globalThis.MDVPresenterChannel = { CHANNEL_NAME, create };
32
+ }
33
+ })();
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Per-deck save queue with per-slide coalescing.
3
+ *
4
+ * - Saves to the same deck are processed strictly serially (the server has
5
+ * a per-path mutex, but client-side serialization keeps user-visible
6
+ * ordering intuitive).
7
+ * - New edits for the same slideIndex overwrite any pending value (coalesce).
8
+ * - Other slides' pending edits keep their place in insertion order.
9
+ * - `saveFn(path, slideIndex, note, etag)` is supplied by the caller.
10
+ *
11
+ * Loaded as a classic <script>; exposes window.MDVSaveQueue.
12
+ */
13
+ (function () {
14
+ 'use strict';
15
+
16
+ function createSaveQueue({ saveFn }) {
17
+ /** @type {Map<string, { pendingBySlide: Map<number, {note:string, etag:string|null}>, isDraining: boolean }>} */
18
+ const queue = new Map();
19
+
20
+ function enqueue(path, slideIndex, note, etag) {
21
+ let entry = queue.get(path);
22
+ if (!entry) {
23
+ entry = { pendingBySlide: new Map(), isDraining: false };
24
+ queue.set(path, entry);
25
+ }
26
+ entry.pendingBySlide.set(slideIndex, { note, etag });
27
+ if (!entry.isDraining) drain(path);
28
+ }
29
+
30
+ async function drain(path) {
31
+ const entry = queue.get(path);
32
+ if (!entry || entry.isDraining) return;
33
+ entry.isDraining = true;
34
+ try {
35
+ while (entry.pendingBySlide.size > 0) {
36
+ const it = entry.pendingBySlide.entries().next();
37
+ if (it.done) break;
38
+ const [slideIndex, payload] = it.value;
39
+ entry.pendingBySlide.delete(slideIndex);
40
+ try {
41
+ await saveFn(path, slideIndex, payload.note, payload.etag);
42
+ } catch (err) {
43
+ // Caller logs; never let drain break.
44
+ console.error('saveQueue saveFn error', err);
45
+ }
46
+ }
47
+ } finally {
48
+ entry.isDraining = false;
49
+ if (entry.pendingBySlide.size === 0) queue.delete(path);
50
+ }
51
+ }
52
+
53
+ /** Drop all pending edits for a path (e.g. when its tab is closed). */
54
+ function dropPath(path) {
55
+ const entry = queue.get(path);
56
+ if (!entry) return;
57
+ entry.pendingBySlide.clear();
58
+ // If a drain is isDraining, it will exit cleanly when the map is empty.
59
+ if (!entry.isDraining) queue.delete(path);
60
+ }
61
+
62
+ /** Test/observation helper. */
63
+ function _size() { return queue.size; }
64
+
65
+ return { enqueue, drain, dropPath, _size };
66
+ }
67
+
68
+ if (typeof globalThis !== 'undefined') {
69
+ globalThis.MDVSaveQueue = { createSaveQueue };
70
+ }
71
+ })();
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Lightweight tab life-cycle hub.
3
+ *
4
+ * Subscribers (e.g. PresenterView) register cleanup callbacks once at init
5
+ * time, and TabManager fires `notifyClosed(path)` when a tab is closed.
6
+ * Solves the memory leak the audit found in `lastSavedEtag` and the save
7
+ * queue when users open and close many decks.
8
+ */
9
+ (function () {
10
+ 'use strict';
11
+
12
+ const closeListeners = [];
13
+ const switchListeners = [];
14
+
15
+ function onTabClosed(fn) { if (typeof fn === 'function') closeListeners.push(fn); }
16
+ function onTabSwitched(fn) { if (typeof fn === 'function') switchListeners.push(fn); }
17
+
18
+ function notifyClosed(path) {
19
+ for (const fn of closeListeners) {
20
+ try { fn(path); } catch (err) { console.error('tabRegistry close listener error', err); }
21
+ }
22
+ }
23
+ function notifySwitched(activePath) {
24
+ for (const fn of switchListeners) {
25
+ try { fn(activePath); } catch (err) { console.error('tabRegistry switch listener error', err); }
26
+ }
27
+ }
28
+
29
+ if (typeof globalThis !== 'undefined') {
30
+ globalThis.MDVTabRegistry = { onTabClosed, onTabSwitched, notifyClosed, notifySwitched };
31
+ }
32
+ })();