@memberjunction/ng-whiteboard 0.0.1 → 5.41.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.
Files changed (62) hide show
  1. package/README.md +302 -28
  2. package/dist/lib/whiteboard-agent-sees-popover.component.d.ts +31 -0
  3. package/dist/lib/whiteboard-agent-sees-popover.component.d.ts.map +1 -0
  4. package/dist/lib/whiteboard-agent-sees-popover.component.js +132 -0
  5. package/dist/lib/whiteboard-agent-sees-popover.component.js.map +1 -0
  6. package/dist/lib/whiteboard-board.component.d.ts +458 -0
  7. package/dist/lib/whiteboard-board.component.d.ts.map +1 -0
  8. package/dist/lib/whiteboard-board.component.js +2357 -0
  9. package/dist/lib/whiteboard-board.component.js.map +1 -0
  10. package/dist/lib/whiteboard-context-menu.d.ts +57 -0
  11. package/dist/lib/whiteboard-context-menu.d.ts.map +1 -0
  12. package/dist/lib/whiteboard-context-menu.js +69 -0
  13. package/dist/lib/whiteboard-context-menu.js.map +1 -0
  14. package/dist/lib/whiteboard-export.d.ts +94 -0
  15. package/dist/lib/whiteboard-export.d.ts.map +1 -0
  16. package/dist/lib/whiteboard-export.js +592 -0
  17. package/dist/lib/whiteboard-export.js.map +1 -0
  18. package/dist/lib/whiteboard-host.component.d.ts +174 -0
  19. package/dist/lib/whiteboard-host.component.d.ts.map +1 -0
  20. package/dist/lib/whiteboard-host.component.js +758 -0
  21. package/dist/lib/whiteboard-host.component.js.map +1 -0
  22. package/dist/lib/whiteboard-pages.component.d.ts +83 -0
  23. package/dist/lib/whiteboard-pages.component.d.ts.map +1 -0
  24. package/dist/lib/whiteboard-pages.component.js +211 -0
  25. package/dist/lib/whiteboard-pages.component.js.map +1 -0
  26. package/dist/lib/whiteboard-snapshot.component.d.ts +30 -0
  27. package/dist/lib/whiteboard-snapshot.component.d.ts.map +1 -0
  28. package/dist/lib/whiteboard-snapshot.component.js +91 -0
  29. package/dist/lib/whiteboard-snapshot.component.js.map +1 -0
  30. package/dist/lib/whiteboard-srcdoc.pipe.d.ts +60 -0
  31. package/dist/lib/whiteboard-srcdoc.pipe.d.ts.map +1 -0
  32. package/dist/lib/whiteboard-srcdoc.pipe.js +75 -0
  33. package/dist/lib/whiteboard-srcdoc.pipe.js.map +1 -0
  34. package/dist/lib/whiteboard-state.d.ts +1113 -0
  35. package/dist/lib/whiteboard-state.d.ts.map +1 -0
  36. package/dist/lib/whiteboard-state.js +1396 -0
  37. package/dist/lib/whiteboard-state.js.map +1 -0
  38. package/dist/lib/whiteboard-toolbar.component.d.ts +95 -0
  39. package/dist/lib/whiteboard-toolbar.component.d.ts.map +1 -0
  40. package/dist/lib/whiteboard-toolbar.component.js +344 -0
  41. package/dist/lib/whiteboard-toolbar.component.js.map +1 -0
  42. package/dist/lib/whiteboard-tools.d.ts +103 -0
  43. package/dist/lib/whiteboard-tools.d.ts.map +1 -0
  44. package/dist/lib/whiteboard-tools.js +748 -0
  45. package/dist/lib/whiteboard-tools.js.map +1 -0
  46. package/dist/lib/whiteboard-widget-bridge.d.ts +217 -0
  47. package/dist/lib/whiteboard-widget-bridge.d.ts.map +1 -0
  48. package/dist/lib/whiteboard-widget-bridge.js +254 -0
  49. package/dist/lib/whiteboard-widget-bridge.js.map +1 -0
  50. package/dist/lib/whiteboard-zoom.component.d.ts +55 -0
  51. package/dist/lib/whiteboard-zoom.component.d.ts.map +1 -0
  52. package/dist/lib/whiteboard-zoom.component.js +139 -0
  53. package/dist/lib/whiteboard-zoom.component.js.map +1 -0
  54. package/dist/lib/whiteboard.module.d.ts +29 -0
  55. package/dist/lib/whiteboard.module.d.ts.map +1 -0
  56. package/dist/lib/whiteboard.module.js +78 -0
  57. package/dist/lib/whiteboard.module.js.map +1 -0
  58. package/dist/public-api.d.ts +49 -0
  59. package/dist/public-api.d.ts.map +1 -0
  60. package/dist/public-api.js +59 -0
  61. package/dist/public-api.js.map +1 -0
  62. package/package.json +43 -6
@@ -0,0 +1,592 @@
1
+ import { WHITEBOARD_DEFAULTS, WhiteboardState } from './whiteboard-state';
2
+ /** Padding (px) added around the content bounds so nothing renders flush to the edge. */
3
+ const EXPORT_PAD = 36;
4
+ /** Fallback board size when the board is empty. */
5
+ const EMPTY_BOARD_W = 640;
6
+ const EMPTY_BOARD_H = 240;
7
+ /** Paper-friendly literal palette (self-contained exports cannot reference app tokens). */
8
+ const PALETTE = {
9
+ PageBg: '#f6f5f1',
10
+ PaperBg: '#ffffff',
11
+ PaperBorder: '#d9d4c8',
12
+ TextPrimary: '#1f2937',
13
+ TextMuted: '#6b7280',
14
+ UserChipBg: '#eef2f7',
15
+ UserChipBorder: '#cbd5e1',
16
+ UserChipText: '#475569',
17
+ AgentChipBg: '#ede9fe',
18
+ AgentChipBorder: '#c4b5fd',
19
+ AgentChipText: '#6d28d9',
20
+ StickyAmberBg: '#fef3c7',
21
+ StickyAmberBorder: '#f0d48a',
22
+ StickyAmberLightBg: '#fffbeb',
23
+ StickyAmberLightBorder: '#f3e3b3',
24
+ StickyAgentBg: '#f1ecfe',
25
+ StickyAgentBorder: '#c4b5fd',
26
+ ShapeUserBg: '#fdfdfb',
27
+ ShapeUserBorder: '#94a3b8',
28
+ ShapeAgentBg: '#f5f3ff',
29
+ ShapeAgentBorder: '#a78bfa',
30
+ ImageBg: '#f1f5f9',
31
+ ImageBorder: '#cbd5e1',
32
+ ConnectorUser: '#64748b',
33
+ ConnectorAgent: '#8b5cf6',
34
+ HighlightUser: '#d97706',
35
+ HighlightAgent: '#8b5cf6'
36
+ };
37
+ /** Map a {@link WhiteboardFontFamily} key to a literal font stack. */
38
+ const FONT_STACKS = {
39
+ sans: "ui-sans-serif, system-ui, 'Segoe UI', Helvetica, Arial, sans-serif",
40
+ serif: "Georgia, 'Times New Roman', serif",
41
+ mono: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
42
+ };
43
+ /** HTML/SVG-escape every user-authored string: & < > " ' (also valid in XML text/attrs). */
44
+ function escapeHtml(text) {
45
+ return (text ?? '')
46
+ .replace(/&/g, '&amp;')
47
+ .replace(/</g, '&lt;')
48
+ .replace(/>/g, '&gt;')
49
+ .replace(/"/g, '&quot;')
50
+ .replace(/'/g, '&#39;');
51
+ }
52
+ // ────────────────────────────────────────────── minimal inert markdown renderer
53
+ /**
54
+ * Format ONE already-escaped line's inline markdown: code spans, links (http/https/mailto
55
+ * only — anything else stays literal text), bold, italic. Code spans are protected from
56
+ * the other inline formats via placeholders.
57
+ */
58
+ function markdownInline(escaped) {
59
+ const codeSpans = [];
60
+ let text = escaped.replace(/`([^`]+)`/g, (_m, c) => {
61
+ codeSpans.push(c);
62
+ return `\u0000${codeSpans.length - 1}\u0000`;
63
+ });
64
+ // href is escaped text in a whitelisted scheme — quotes are entities, so it can't break the attribute
65
+ text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^()\s]+|mailto:[^()\s]+)\)/g, '<a href="$2" rel="noopener noreferrer">$1</a>');
66
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
67
+ text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
68
+ return text.replace(/\u0000(\d+)\u0000/g, (_m, i) => `<code>${codeSpans[Number(i)]}</code>`);
69
+ }
70
+ /**
71
+ * Minimal, SAFE markdown → HTML renderer for the self-contained exports (the live board
72
+ * renders through the shared `mj-markdown` Angular component instead — this exists only
73
+ * because export builders are pure functions with no Angular available).
74
+ *
75
+ * SECURITY: ESCAPE-FIRST — the whole source is HTML-escaped before any formatting, so raw
76
+ * HTML in the markdown can never become live markup (NO passthrough, ever). Supported:
77
+ * headings, bold/italic, inline code, fenced code blocks, un/ordered lists, paragraphs,
78
+ * and links restricted to http(s)/mailto.
79
+ */
80
+ export function RenderMarkdownInert(markdown) {
81
+ const lines = escapeHtml(markdown ?? '').split('\n');
82
+ const out = [];
83
+ let para = [];
84
+ let list = null;
85
+ let code = null;
86
+ const flushPara = () => {
87
+ if (para.length > 0) {
88
+ out.push(`<p>${para.join('<br>')}</p>`);
89
+ para = [];
90
+ }
91
+ };
92
+ const flushList = () => {
93
+ if (list) {
94
+ out.push(`<${list.tag}>${list.items.map((i) => `<li>${i}</li>`).join('')}</${list.tag}>`);
95
+ list = null;
96
+ }
97
+ };
98
+ for (const line of lines) {
99
+ if (code) {
100
+ if (/^```/.test(line)) {
101
+ out.push(`<pre><code>${code.join('\n')}</code></pre>`);
102
+ code = null;
103
+ }
104
+ else {
105
+ code.push(line);
106
+ }
107
+ continue;
108
+ }
109
+ if (/^```/.test(line)) {
110
+ flushPara();
111
+ flushList();
112
+ code = [];
113
+ continue;
114
+ }
115
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
116
+ if (heading) {
117
+ flushPara();
118
+ flushList();
119
+ out.push(`<h${heading[1].length}>${markdownInline(heading[2])}</h${heading[1].length}>`);
120
+ continue;
121
+ }
122
+ const ul = /^\s*[-*]\s+(.*)$/.exec(line);
123
+ const ol = /^\s*\d+\.\s+(.*)$/.exec(line);
124
+ if (ul || ol) {
125
+ flushPara();
126
+ const tag = ul ? 'ul' : 'ol';
127
+ if (!list || list.tag !== tag) {
128
+ flushList();
129
+ list = { tag, items: [] };
130
+ }
131
+ list.items.push(markdownInline((ul ?? ol)[1]));
132
+ continue;
133
+ }
134
+ if (line.trim().length === 0) {
135
+ flushPara();
136
+ flushList();
137
+ continue;
138
+ }
139
+ flushList();
140
+ para.push(markdownInline(line));
141
+ }
142
+ if (code) {
143
+ out.push(`<pre><code>${code.join('\n')}</code></pre>`); // unterminated fence
144
+ }
145
+ flushPara();
146
+ flushList();
147
+ return out.join('');
148
+ }
149
+ /** Round to 2 decimals for compact, deterministic coordinate output. */
150
+ function n(value) {
151
+ return Math.round(value * 100) / 100;
152
+ }
153
+ /** Board bounds padded for export (or a default empty stage). */
154
+ function exportBounds(state) {
155
+ const content = state.ContentBounds();
156
+ if (!content) {
157
+ return { X: 0, Y: 0, W: EMPTY_BOARD_W, H: EMPTY_BOARD_H };
158
+ }
159
+ return {
160
+ X: content.X - EXPORT_PAD,
161
+ Y: content.Y - EXPORT_PAD,
162
+ W: content.W + EXPORT_PAD * 2,
163
+ H: content.H + EXPORT_PAD * 2
164
+ };
165
+ }
166
+ /** Inline font-style fragment from the optional text-style fields. */
167
+ function textStyleCss(item) {
168
+ const parts = [];
169
+ if (item.FontSize != null) {
170
+ parts.push(`font-size:${n(item.FontSize)}px`);
171
+ }
172
+ if (item.FontFamily) {
173
+ parts.push(`font-family:${FONT_STACKS[item.FontFamily]}`);
174
+ }
175
+ if (item.FontWeight != null) {
176
+ parts.push(`font-weight:${item.FontWeight}`);
177
+ }
178
+ return parts.length > 0 ? parts.join(';') + ';' : '';
179
+ }
180
+ /** The small ownership chip rendered on each box/ink item. */
181
+ function chipHtml(item, agentName) {
182
+ const label = item.Author === 'agent' ? escapeHtml(agentName) : 'You';
183
+ return `<span class="chip chip--${item.Author}">${label}</span>`;
184
+ }
185
+ // ────────────────────────────────────────────── box items (positioned divs)
186
+ function stickyHtml(item, ox, oy, agentName) {
187
+ const w = item.W ?? WHITEBOARD_DEFAULTS.StickyW;
188
+ const isAgent = item.Author === 'agent';
189
+ const bg = isAgent ? PALETTE.StickyAgentBg : item.Tint === 'amber-light' ? PALETTE.StickyAmberLightBg : PALETTE.StickyAmberBg;
190
+ const border = isAgent ? PALETTE.StickyAgentBorder : item.Tint === 'amber-light' ? PALETTE.StickyAmberLightBorder : PALETTE.StickyAmberBorder;
191
+ const rotate = item.Rotation ? `transform:rotate(${n(item.Rotation)}deg);` : '';
192
+ const style = `left:${n(item.X - ox)}px;top:${n(item.Y - oy)}px;width:${n(w)}px;min-height:${WHITEBOARD_DEFAULTS.StickyH}px;` +
193
+ `background:${bg};border-color:${border};${rotate}${textStyleCss(item)}`;
194
+ return `<div class="it sticky" style="${style}">${chipHtml(item, agentName)}<div class="it-text">${escapeHtml(item.Text)}</div></div>`;
195
+ }
196
+ function shapeHtml(item, ox, oy, agentName) {
197
+ const isAgent = item.Author === 'agent';
198
+ const bg = isAgent ? PALETTE.ShapeAgentBg : PALETTE.ShapeUserBg;
199
+ const border = isAgent ? PALETTE.ShapeAgentBorder : PALETTE.ShapeUserBorder;
200
+ const shapeCss = item.Shape === 'ellipse' ? 'border-radius:50%;'
201
+ : item.Shape === 'diamond' ? 'clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);border:none;'
202
+ : '';
203
+ const style = `left:${n(item.X - ox)}px;top:${n(item.Y - oy)}px;width:${n(item.W)}px;height:${n(item.H)}px;` +
204
+ `background:${bg};border-color:${border};${shapeCss}`;
205
+ const sub = item.Sub ? `<div class="it-sub">${escapeHtml(item.Sub)}</div>` : '';
206
+ return `<div class="it shape" style="${style}">${chipHtml(item, agentName)}<div class="it-label">${escapeHtml(item.Label)}</div>${sub}</div>`;
207
+ }
208
+ function textHtml(item, ox, oy, agentName) {
209
+ const color = item.Color ? `color:${escapeHtml(item.Color)};` : '';
210
+ // explicit wrap width wins; otherwise wrap at the on-board default max width
211
+ const width = item.W ? `width:${n(item.W)}px;` : 'max-width:380px;';
212
+ const style = `left:${n(item.X - ox)}px;top:${n(item.Y - oy)}px;${width}${color}${textStyleCss(item)}`;
213
+ return `<div class="it label" style="${style}">${chipHtml(item, agentName)}<div class="it-text">${escapeHtml(item.Text)}</div></div>`;
214
+ }
215
+ function imageHtml(item, ox, oy, agentName) {
216
+ const w = item.W ?? WHITEBOARD_DEFAULTS.ImageW;
217
+ const style = `left:${n(item.X - ox)}px;top:${n(item.Y - oy)}px;width:${n(w)}px;height:${WHITEBOARD_DEFAULTS.ImageH}px;`;
218
+ // Url is a runtime object URL (not persisted as pixels) — always export a named placeholder.
219
+ return `<div class="it image" style="${style}">${chipHtml(item, agentName)}` +
220
+ `<div class="image-glyph">▦</div><div class="image-name">${escapeHtml(item.Name)}</div></div>`;
221
+ }
222
+ /** Markdown panel: rendered through the inert escape-first renderer (never raw HTML). */
223
+ function markdownItemHtml(item, ox, oy, agentName) {
224
+ const maxH = item.H ? `max-height:${n(item.H)}px;overflow:hidden;` : '';
225
+ const style = `left:${n(item.X - ox)}px;top:${n(item.Y - oy)}px;width:${n(item.W)}px;${maxH}`;
226
+ return `<div class="it md" style="${style}">${chipHtml(item, agentName)}` +
227
+ `<div class="md-body">${RenderMarkdownInert(item.Markdown)}</div></div>`;
228
+ }
229
+ /**
230
+ * HTML widget: exported as a PLACEHOLDER card — title, an "interactive widget" note, and
231
+ * the ESCAPED source inside a collapsed <details>. The live HTML is deliberately NOT
232
+ * inlined into the export document: the board's iframe sandbox doesn't exist here, so
233
+ * embedding it would hand arbitrary script a same-origin document (XSS).
234
+ */
235
+ function htmlWidgetItemHtml(item, ox, oy, agentName) {
236
+ const title = escapeHtml(item.Title || 'HTML widget');
237
+ const style = `left:${n(item.X - ox)}px;top:${n(item.Y - oy)}px;width:${n(item.W)}px;min-height:${n(item.H)}px;`;
238
+ return `<div class="it htmlw" style="${style}">${chipHtml(item, agentName)}` +
239
+ `<div class="htmlw-title">${title}</div>` +
240
+ `<div class="htmlw-note">Interactive HTML widget — renders sandboxed on the live board; not embedded in this export.</div>` +
241
+ `<details class="htmlw-src"><summary>View source</summary><pre>${escapeHtml(item.Html)}</pre></details></div>`;
242
+ }
243
+ /** Ink strokes draw in the SVG layer, but their ownership chip is an HTML div at the stroke's bounds. */
244
+ function inkChipHtml(item, state, ox, oy, agentName) {
245
+ const b = state.ItemBounds(item);
246
+ return `<div class="ink-chip" style="left:${n(b.X - ox)}px;top:${n(b.Y - oy - 18)}px;">${chipHtml(item, agentName)}</div>`;
247
+ }
248
+ // ────────────────────────────────────────────── vector layer (one inline SVG)
249
+ function inkSvg(item, ox, oy) {
250
+ const points = item.Points.map((p) => `${n(p.X - ox)},${n(p.Y - oy)}`).join(' ');
251
+ return `<polyline points="${points}" fill="none" stroke="${escapeHtml(item.Color)}" stroke-width="${n(item.StrokeWidth)}" stroke-linecap="round" stroke-linejoin="round"/>`;
252
+ }
253
+ function connectorSvg(item, state, ox, oy) {
254
+ // Reuse the engine's own bounds math: endpoints anchor to the referenced item's center.
255
+ const from = state.ResolveEndpoint(item, 'from');
256
+ const to = state.ResolveEndpoint(item, 'to');
257
+ const stroke = item.Author === 'agent' ? PALETTE.ConnectorAgent : PALETTE.ConnectorUser;
258
+ return `<line x1="${n(from.X - ox)}" y1="${n(from.Y - oy)}" x2="${n(to.X - ox)}" y2="${n(to.Y - oy)}" ` +
259
+ `stroke="${stroke}" stroke-width="2" marker-end="url(#wb-arrow)"/>`;
260
+ }
261
+ function highlightSvg(item, ox, oy) {
262
+ const stroke = item.Author === 'agent' ? PALETTE.HighlightAgent : PALETTE.HighlightUser;
263
+ const rect = `<rect x="${n(item.X - ox)}" y="${n(item.Y - oy)}" width="${n(item.W)}" height="${n(item.H)}" rx="10" ` +
264
+ `fill="none" stroke="${stroke}" stroke-width="2" stroke-dasharray="7 5"/>`;
265
+ const label = item.Label
266
+ ? `<text x="${n(item.X - ox + 6)}" y="${n(item.Y - oy - 7)}" font-size="11" fill="${stroke}">${escapeHtml(item.Label)}</text>`
267
+ : '';
268
+ return rect + label;
269
+ }
270
+ /** The single vector layer: ink + connectors + highlight outlines, in render (Z) order. */
271
+ function vectorLayerSvg(state, b) {
272
+ const parts = [];
273
+ for (const item of state.Items) {
274
+ if (item.Kind === 'ink') {
275
+ parts.push(inkSvg(item, b.X, b.Y));
276
+ }
277
+ else if (item.Kind === 'connector') {
278
+ parts.push(connectorSvg(item, state, b.X, b.Y));
279
+ }
280
+ else if (item.Kind === 'highlight') {
281
+ parts.push(highlightSvg(item, b.X, b.Y));
282
+ }
283
+ }
284
+ return `<svg class="vector" width="${n(b.W)}" height="${n(b.H)}" viewBox="0 0 ${n(b.W)} ${n(b.H)}" xmlns="http://www.w3.org/2000/svg">` +
285
+ `<defs><marker id="wb-arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">` +
286
+ `<path d="M0,0 L7,3 L0,6 Z" fill="${PALETTE.ConnectorUser}"/></marker></defs>${parts.join('')}</svg>`;
287
+ }
288
+ /** All positioned box-item divs (+ ink ownership chips), in render (Z) order. */
289
+ function boxLayerHtml(state, b, agentName) {
290
+ const parts = [];
291
+ for (const item of state.Items) {
292
+ switch (item.Kind) {
293
+ case 'sticky':
294
+ parts.push(stickyHtml(item, b.X, b.Y, agentName));
295
+ break;
296
+ case 'shape':
297
+ parts.push(shapeHtml(item, b.X, b.Y, agentName));
298
+ break;
299
+ case 'text':
300
+ parts.push(textHtml(item, b.X, b.Y, agentName));
301
+ break;
302
+ case 'image':
303
+ parts.push(imageHtml(item, b.X, b.Y, agentName));
304
+ break;
305
+ case 'markdown':
306
+ parts.push(markdownItemHtml(item, b.X, b.Y, agentName));
307
+ break;
308
+ case 'html':
309
+ parts.push(htmlWidgetItemHtml(item, b.X, b.Y, agentName));
310
+ break;
311
+ case 'ink':
312
+ parts.push(inkChipHtml(item, state, b.X, b.Y, agentName));
313
+ break;
314
+ case 'connector':
315
+ case 'highlight':
316
+ break; // vector layer only
317
+ }
318
+ }
319
+ return parts.join('');
320
+ }
321
+ // ────────────────────────────────────────────── document chrome
322
+ function documentCss() {
323
+ return `
324
+ * { box-sizing: border-box; }
325
+ body { margin: 0; padding: 24px; background: ${PALETTE.PageBg}; color: ${PALETTE.TextPrimary};
326
+ font-family: ${FONT_STACKS.sans}; }
327
+ .exp-head { margin-bottom: 14px; }
328
+ .exp-head h1 { margin: 0 0 4px; font-size: 19px; }
329
+ .exp-meta { font-size: 12px; color: ${PALETTE.TextMuted}; }
330
+ .board-wrap { overflow: auto; }
331
+ .board { position: relative; background: ${PALETTE.PaperBg}; border: 1px solid ${PALETTE.PaperBorder};
332
+ border-radius: 8px; box-shadow: 0 2px 10px rgba(15, 23, 42, 0.08); overflow: hidden; }
333
+ .vector { position: absolute; left: 0; top: 0; }
334
+ .it { position: absolute; border: 1px solid transparent; border-radius: 6px; padding: 18px 10px 10px;
335
+ font-size: 12px; line-height: 1.35; box-shadow: 0 1px 4px rgba(15, 23, 42, 0.10); }
336
+ .it-text { white-space: pre-wrap; word-break: break-word; }
337
+ .it.shape { display: flex; flex-direction: column; align-items: center; justify-content: center;
338
+ text-align: center; padding: 10px; }
339
+ .it-label { font-weight: 600; }
340
+ .it-sub { font-size: 10.5px; color: ${PALETTE.TextMuted}; margin-top: 2px; }
341
+ .it.label { background: transparent; border: none; box-shadow: none; padding: 14px 2px 2px; }
342
+ .it.image { background: ${PALETTE.ImageBg}; border-color: ${PALETTE.ImageBorder}; display: flex;
343
+ flex-direction: column; align-items: center; justify-content: center; gap: 6px; }
344
+ .image-glyph { font-size: 22px; color: ${PALETTE.TextMuted}; }
345
+ .image-name { font-size: 10.5px; color: ${PALETTE.TextMuted}; max-width: 100%; overflow: hidden;
346
+ text-overflow: ellipsis; white-space: nowrap; }
347
+ .ink-chip { position: absolute; }
348
+ .it.md { background: ${PALETTE.PaperBg}; border-color: ${PALETTE.PaperBorder}; }
349
+ .md-body h1 { font-size: 16px; margin: 2px 0 6px; }
350
+ .md-body h2 { font-size: 14px; margin: 2px 0 6px; }
351
+ .md-body h3, .md-body h4, .md-body h5, .md-body h6 { font-size: 12.5px; margin: 2px 0 4px; }
352
+ .md-body p { margin: 0 0 6px; }
353
+ .md-body ul, .md-body ol { margin: 0 0 6px; padding-left: 18px; }
354
+ .md-body pre { background: ${PALETTE.ImageBg}; border: 1px solid ${PALETTE.ImageBorder}; border-radius: 4px;
355
+ padding: 6px 8px; margin: 0 0 6px; font-size: 10.5px; overflow: auto; white-space: pre-wrap; }
356
+ .md-body code { font-family: ${FONT_STACKS.mono}; font-size: 10.5px; }
357
+ .md-body a { color: ${PALETTE.AgentChipText}; }
358
+ .it.htmlw { background: ${PALETTE.ImageBg}; border-color: ${PALETTE.ImageBorder}; border-style: dashed; }
359
+ .htmlw-title { font-weight: 700; margin-bottom: 4px; }
360
+ .htmlw-note { font-size: 10.5px; color: ${PALETTE.TextMuted}; margin-bottom: 6px; }
361
+ .htmlw-src summary { font-size: 10.5px; color: ${PALETTE.TextMuted}; cursor: pointer; }
362
+ .htmlw-src pre { background: ${PALETTE.PaperBg}; border: 1px solid ${PALETTE.ImageBorder}; border-radius: 4px;
363
+ padding: 6px 8px; margin: 4px 0 0; font-size: 9.5px; font-family: ${FONT_STACKS.mono};
364
+ overflow: auto; white-space: pre-wrap; word-break: break-word; max-height: 220px; }
365
+ .chip { position: absolute; top: -9px; left: 8px; font-size: 9px; font-weight: 700; letter-spacing: 0.02em;
366
+ padding: 1px 7px; border-radius: 999px; border: 1px solid ${PALETTE.UserChipBorder};
367
+ background: ${PALETTE.UserChipBg}; color: ${PALETTE.UserChipText}; white-space: nowrap; }
368
+ .chip--agent { border-color: ${PALETTE.AgentChipBorder}; background: ${PALETTE.AgentChipBg};
369
+ color: ${PALETTE.AgentChipText}; }
370
+ .ink-chip .chip { position: static; }
371
+ .exp-empty { padding: 48px 0; text-align: center; color: ${PALETTE.TextMuted}; font-size: 13px; }
372
+ .exp-foot { margin-top: 12px; font-size: 11px; color: ${PALETTE.TextMuted}; }
373
+ .exp-page { margin-bottom: 22px; }
374
+ .exp-page__name { margin: 0 0 8px; font-size: 15px; }
375
+ .exp-page__idx { color: ${PALETTE.TextMuted}; font-weight: 400; font-size: 12px; }
376
+ @media print {
377
+ body { background: #ffffff; padding: 0; }
378
+ .board, .it { box-shadow: none !important; }
379
+ .board-wrap { overflow: visible; }
380
+ .exp-page { break-inside: avoid; }
381
+ .exp-page + .exp-page { break-before: page; }
382
+ }
383
+ @page { margin: 12mm; }`;
384
+ }
385
+ /**
386
+ * Build a fully self-contained HTML document for the given board snapshot — inline CSS
387
+ * only, light paper-friendly palette, header (title / generated-at / agent byline), the
388
+ * positioned board, ownership chips, item-count footer and print-friendly `@media print`
389
+ * rules. Deterministic for a given state + options; all text is HTML-escaped.
390
+ *
391
+ * Renders the ACTIVE page only — see {@link BuildWhiteboardExportHtmlAllPages} for the
392
+ * every-page document.
393
+ */
394
+ export function BuildWhiteboardExportHtml(state, opts) {
395
+ const b = exportBounds(state);
396
+ const title = escapeHtml(opts.Title);
397
+ const agent = escapeHtml(opts.AgentName);
398
+ const empty = state.Items.length === 0;
399
+ const boardInner = empty
400
+ ? `<div class="exp-empty">This whiteboard is empty.</div>`
401
+ : vectorLayerSvg(state, b) + boxLayerHtml(state, b, opts.AgentName);
402
+ return `<!DOCTYPE html>
403
+ <html lang="en">
404
+ <head>
405
+ <meta charset="utf-8">
406
+ <title>${title}</title>
407
+ <style>${documentCss()}</style>
408
+ </head>
409
+ <body>
410
+ <header class="exp-head">
411
+ <h1>${title}</h1>
412
+ <div class="exp-meta">Generated ${escapeHtml(opts.GeneratedAt)} &middot; Live session with ${agent}</div>
413
+ </header>
414
+ <main class="board-wrap">
415
+ <div class="board" style="width:${n(b.W)}px;height:${n(b.H)}px;">${boardInner}</div>
416
+ </main>
417
+ <footer class="exp-foot">${state.ElementCount} elements &middot; you ${state.CountByAuthor('user')} &middot; ${agent} ${state.CountByAuthor('agent')}</footer>
418
+ </body>
419
+ </html>`;
420
+ }
421
+ // ────────────────────────────────────────────── standalone SVG export
422
+ /** First line of a (possibly multi-line) text, clipped for single-line SVG rendering. */
423
+ function svgLine(text, max = 60) {
424
+ const line = (text ?? '').split('\n')[0].trim();
425
+ return escapeHtml(line.length > max ? `${line.slice(0, max).trimEnd()}…` : line);
426
+ }
427
+ function boxItemSvg(item, state, ox, oy) {
428
+ switch (item.Kind) {
429
+ case 'sticky': {
430
+ const b = state.ItemBounds(item);
431
+ const isAgent = item.Author === 'agent';
432
+ const fill = isAgent ? PALETTE.StickyAgentBg : PALETTE.StickyAmberBg;
433
+ const stroke = isAgent ? PALETTE.StickyAgentBorder : PALETTE.StickyAmberBorder;
434
+ const fs = item.FontSize ?? 12;
435
+ const font = FONT_STACKS[item.FontFamily ?? 'sans'];
436
+ const weight = item.FontWeight ?? 400;
437
+ return `<rect x="${n(b.X - ox)}" y="${n(b.Y - oy)}" width="${n(b.W)}" height="${n(b.H)}" rx="6" fill="${fill}" stroke="${stroke}"/>` +
438
+ `<text x="${n(b.X - ox + 10)}" y="${n(b.Y - oy + 10 + fs)}" font-size="${n(fs)}" font-family="${font}" font-weight="${weight}" fill="${PALETTE.TextPrimary}">${svgLine(item.Text)}</text>`;
439
+ }
440
+ case 'shape': {
441
+ const isAgent = item.Author === 'agent';
442
+ const fill = isAgent ? PALETTE.ShapeAgentBg : PALETTE.ShapeUserBg;
443
+ const stroke = isAgent ? PALETTE.ShapeAgentBorder : PALETTE.ShapeUserBorder;
444
+ const x = item.X - ox, y = item.Y - oy;
445
+ const cx = x + item.W / 2, cy = y + item.H / 2;
446
+ const geom = item.Shape === 'ellipse'
447
+ ? `<ellipse cx="${n(cx)}" cy="${n(cy)}" rx="${n(item.W / 2)}" ry="${n(item.H / 2)}" fill="${fill}" stroke="${stroke}"/>`
448
+ : item.Shape === 'diamond'
449
+ ? `<polygon points="${n(cx)},${n(y)} ${n(x + item.W)},${n(cy)} ${n(cx)},${n(y + item.H)} ${n(x)},${n(cy)}" fill="${fill}" stroke="${stroke}"/>`
450
+ : `<rect x="${n(x)}" y="${n(y)}" width="${n(item.W)}" height="${n(item.H)}" rx="6" fill="${fill}" stroke="${stroke}"/>`;
451
+ const sub = item.Sub
452
+ ? `<text x="${n(cx)}" y="${n(cy + 16)}" font-size="10.5" text-anchor="middle" fill="${PALETTE.TextMuted}">${svgLine(item.Sub)}</text>`
453
+ : '';
454
+ return geom +
455
+ `<text x="${n(cx)}" y="${n(cy + 4)}" font-size="12" font-weight="600" text-anchor="middle" fill="${PALETTE.TextPrimary}">${svgLine(item.Label)}</text>` + sub;
456
+ }
457
+ case 'text': {
458
+ const fs = item.FontSize ?? 12;
459
+ const font = FONT_STACKS[item.FontFamily ?? 'sans'];
460
+ const weight = item.FontWeight ?? 400;
461
+ const color = item.Color ? escapeHtml(item.Color) : PALETTE.TextPrimary;
462
+ return `<text x="${n(item.X - ox)}" y="${n(item.Y - oy + fs)}" font-size="${n(fs)}" font-family="${font}" font-weight="${weight}" fill="${color}">${svgLine(item.Text)}</text>`;
463
+ }
464
+ case 'image': {
465
+ const b = state.ItemBounds(item);
466
+ return `<rect x="${n(b.X - ox)}" y="${n(b.Y - oy)}" width="${n(b.W)}" height="${n(b.H)}" rx="6" fill="${PALETTE.ImageBg}" stroke="${PALETTE.ImageBorder}"/>` +
467
+ `<text x="${n(b.X - ox + b.W / 2)}" y="${n(b.Y - oy + b.H / 2 + 4)}" font-size="10.5" text-anchor="middle" fill="${PALETTE.TextMuted}">${svgLine(item.Name)}</text>`;
468
+ }
469
+ case 'markdown': {
470
+ // simple labeled rect — the first markdown line as the label
471
+ const b = state.ItemBounds(item);
472
+ return `<rect x="${n(b.X - ox)}" y="${n(b.Y - oy)}" width="${n(b.W)}" height="${n(b.H)}" rx="6" fill="${PALETTE.PaperBg}" stroke="${PALETTE.PaperBorder}"/>` +
473
+ `<text x="${n(b.X - ox + 10)}" y="${n(b.Y - oy + 20)}" font-size="11" fill="${PALETTE.TextPrimary}">${svgLine(item.Markdown)}</text>` +
474
+ `<text x="${n(b.X - ox + 10)}" y="${n(b.Y - oy + 36)}" font-size="9.5" fill="${PALETTE.TextMuted}">markdown panel</text>`;
475
+ }
476
+ case 'html': {
477
+ // simple labeled rect — title + an "interactive widget" note (never live HTML)
478
+ const x = item.X - ox, y = item.Y - oy;
479
+ return `<rect x="${n(x)}" y="${n(y)}" width="${n(item.W)}" height="${n(item.H)}" rx="6" fill="${PALETTE.ImageBg}" stroke="${PALETTE.ImageBorder}" stroke-dasharray="5 4"/>` +
480
+ `<text x="${n(x + item.W / 2)}" y="${n(y + item.H / 2 - 4)}" font-size="11" font-weight="600" text-anchor="middle" fill="${PALETTE.TextPrimary}">${svgLine(item.Title || 'HTML widget')}</text>` +
481
+ `<text x="${n(x + item.W / 2)}" y="${n(y + item.H / 2 + 12)}" font-size="9.5" text-anchor="middle" fill="${PALETTE.TextMuted}">interactive HTML widget</text>`;
482
+ }
483
+ default:
484
+ return '';
485
+ }
486
+ }
487
+ /**
488
+ * Build a standalone SVG document of the board snapshot: box items as SVG rects / shapes /
489
+ * single-line text, plus the same vector layer (ink, connectors, highlight outlines).
490
+ * All text is escaped; output is deterministic for a given state.
491
+ *
492
+ * Renders the ACTIVE page only — see {@link BuildWhiteboardExportSvgPages} for one SVG
493
+ * document per page.
494
+ */
495
+ export function BuildWhiteboardExportSvg(state) {
496
+ const b = exportBounds(state);
497
+ const parts = [];
498
+ for (const item of state.Items) {
499
+ if (item.Kind === 'ink') {
500
+ parts.push(inkSvg(item, b.X, b.Y));
501
+ }
502
+ else if (item.Kind === 'connector') {
503
+ parts.push(connectorSvg(item, state, b.X, b.Y));
504
+ }
505
+ else if (item.Kind === 'highlight') {
506
+ parts.push(highlightSvg(item, b.X, b.Y));
507
+ }
508
+ else {
509
+ parts.push(boxItemSvg(item, state, b.X, b.Y));
510
+ }
511
+ }
512
+ return `<?xml version="1.0" encoding="UTF-8"?>\n` +
513
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${n(b.W)}" height="${n(b.H)}" viewBox="0 0 ${n(b.W)} ${n(b.H)}" font-family="${FONT_STACKS.sans}">` +
514
+ `<defs><marker id="wb-arrow" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto">` +
515
+ `<path d="M0,0 L7,3 L0,6 Z" fill="${PALETTE.ConnectorUser}"/></marker></defs>` +
516
+ `<rect x="0" y="0" width="${n(b.W)}" height="${n(b.H)}" fill="${PALETTE.PaperBg}"/>` +
517
+ `${parts.join('')}</svg>`;
518
+ }
519
+ /**
520
+ * Enumerate per-page snapshot states WITHOUT mutating the live engine: the board's
521
+ * persisted JSON is rehydrated once per page with that page forced active, so the pure
522
+ * single-page builders (which always render the ACTIVE page) can render ANY page.
523
+ * Pure: the live `state` is only read (`ToJSON` / `Pages`), never switched.
524
+ */
525
+ function buildPageStates(state) {
526
+ const raw = JSON.parse(state.ToJSON());
527
+ return state.Pages.map((page) => {
528
+ raw['activePageId'] = page.ID;
529
+ return { Page: { ID: page.ID, Name: page.Name }, BoardState: WhiteboardState.FromJSON(JSON.stringify(raw)) };
530
+ });
531
+ }
532
+ /**
533
+ * Build one standalone SVG document PER PAGE (SVG is a single-image format, so "all
534
+ * pages" means one file per page — the host downloads them sequentially, suffixing each
535
+ * filename with the page name). Page order matches the board's page strip; each entry's
536
+ * content is exactly what {@link BuildWhiteboardExportSvg} produces for that page.
537
+ */
538
+ export function BuildWhiteboardExportSvgPages(state) {
539
+ return buildPageStates(state).map(({ Page, BoardState }) => ({
540
+ PageID: Page.ID,
541
+ PageName: Page.Name,
542
+ Content: BuildWhiteboardExportSvg(BoardState)
543
+ }));
544
+ }
545
+ /**
546
+ * Build ONE fully self-contained HTML document containing EVERY page of the board —
547
+ * each page renders as its own titled section ("name (n of m)") with the same
548
+ * board/chip/vector rendering as the single-page export, and the print CSS breaks
549
+ * between pages. The footer totals span all pages. Deterministic and escape-safe like
550
+ * the single-page builder. (The board's JSON/state persistence already carries all
551
+ * pages inherently — this is the human-readable counterpart.)
552
+ */
553
+ export function BuildWhiteboardExportHtmlAllPages(state, opts) {
554
+ const pages = buildPageStates(state);
555
+ const title = escapeHtml(opts.Title);
556
+ const agent = escapeHtml(opts.AgentName);
557
+ let totalElements = 0;
558
+ let totalUser = 0;
559
+ let totalAgent = 0;
560
+ const sections = pages.map(({ Page, BoardState }, index) => {
561
+ totalElements += BoardState.ElementCount;
562
+ totalUser += BoardState.CountByAuthor('user');
563
+ totalAgent += BoardState.CountByAuthor('agent');
564
+ const b = exportBounds(BoardState);
565
+ const inner = BoardState.Items.length === 0
566
+ ? `<div class="exp-empty">This page is empty.</div>`
567
+ : vectorLayerSvg(BoardState, b) + boxLayerHtml(BoardState, b, opts.AgentName);
568
+ return `<section class="exp-page">
569
+ <h2 class="exp-page__name">${escapeHtml(Page.Name)} <span class="exp-page__idx">(${index + 1} of ${pages.length})</span></h2>
570
+ <div class="board-wrap"><div class="board" style="width:${n(b.W)}px;height:${n(b.H)}px;">${inner}</div></div>
571
+ </section>`;
572
+ }).join('\n');
573
+ return `<!DOCTYPE html>
574
+ <html lang="en">
575
+ <head>
576
+ <meta charset="utf-8">
577
+ <title>${title}</title>
578
+ <style>${documentCss()}</style>
579
+ </head>
580
+ <body>
581
+ <header class="exp-head">
582
+ <h1>${title}</h1>
583
+ <div class="exp-meta">Generated ${escapeHtml(opts.GeneratedAt)} &middot; Live session with ${agent} &middot; ${pages.length} ${pages.length === 1 ? 'page' : 'pages'}</div>
584
+ </header>
585
+ <main>
586
+ ${sections}
587
+ </main>
588
+ <footer class="exp-foot">${totalElements} elements across ${pages.length} ${pages.length === 1 ? 'page' : 'pages'} &middot; you ${totalUser} &middot; ${agent} ${totalAgent}</footer>
589
+ </body>
590
+ </html>`;
591
+ }
592
+ //# sourceMappingURL=whiteboard-export.js.map