@memberjunction/ng-whiteboard 0.0.1 → 5.42.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,254 @@
1
+ /**
2
+ * LIVE WHITEBOARD — the HTML-widget INPUT BRIDGE (`MJWhiteboard.submit`) and the AMBIENT
3
+ * INTERACTION RECORDER.
4
+ *
5
+ * HTML widgets are tutoring-style "illustrate a concept and GET USER INPUT" chunks —
6
+ * micro-forms, HTML+SVG explainers, concept animations, one-question quizzes. They render
7
+ * inside a strictly sandboxed iframe (`sandbox="allow-scripts"` only, opaque origin), so
8
+ * the ONLY way data can flow out is `postMessage`. This module is the Angular-free half of
9
+ * that bridge:
10
+ *
11
+ * - {@link InjectWhiteboardSubmitHelper} prepends ONE helper script to every widget's
12
+ * `srcdoc` (exactly once) that defines BOTH halves of the bridge:
13
+ * 1. `window.MJWhiteboard.submit(data)` — the EXPLICIT input path; the widget calls
14
+ * it from a button / form handler and the helper posts the payload to the parent.
15
+ * 2. The AMBIENT interaction recorder — passive document-level listeners (click /
16
+ * change / debounced input / focusin / focusout) that observe form activity the
17
+ * widget author wrote NO script for, batch it, and post it to the parent as a
18
+ * `__mjWhiteboardInteraction` message. Password / hidden inputs are skipped
19
+ * entirely; every value is truncated client-side. The recorder is defensive:
20
+ * every handler is wrapped in try/catch so it can never break widget code.
21
+ * The helper targets `'*'` deliberately: the frame runs in an OPAQUE origin, so there is
22
+ * no concrete origin to name; the HOST side validates the message instead.
23
+ * - {@link EvaluateWidgetSubmitMessage} / {@link EvaluateWidgetInteractionMessage} are
24
+ * that host-side validation: the marker property must be present, the `event.source`
25
+ * must resolve to a TRACKED widget iframe (anything else is dropped), the payload is
26
+ * size-capped (oversize → warn + drop), and interaction batches are shape-checked.
27
+ * - {@link SummarizeWidgetInteractions} compresses one validated interaction batch into a
28
+ * compact human line (`clicked "Playful pink"; changed "vibe" to "pink"; typing in
29
+ * "email"`) — what the channel plugin forwards to the live agent.
30
+ *
31
+ * The board component owns the single `window` 'message' listener and emits accepted
32
+ * submissions as {@link WhiteboardWidgetSubmitEvent} and accepted ambient batches as
33
+ * {@link WhiteboardWidgetInteractionEvent}; the channel plugin forwards them to the live
34
+ * agent context as `[whiteboard]` notes (interaction notes throttled per widget).
35
+ */
36
+ /** Marker property carried on every explicit-submit bridge postMessage payload. */
37
+ export const WHITEBOARD_SUBMIT_MARKER = '__mjWhiteboardSubmit';
38
+ /** Marker property carried on every ambient-interaction bridge postMessage payload. */
39
+ export const WHITEBOARD_INTERACTION_MARKER = '__mjWhiteboardInteraction';
40
+ /** Max chars an in-widget recorded VALUE keeps (recorder-side truncation, defensive re-clip host-side). */
41
+ export const WHITEBOARD_INTERACTION_VALUE_MAX_CHARS = 120;
42
+ /** Max chars a recorded element LABEL keeps. */
43
+ export const WHITEBOARD_INTERACTION_LABEL_MAX_CHARS = 40;
44
+ /** Recorder batch flush interval (ms). */
45
+ export const WHITEBOARD_INTERACTION_FLUSH_MS = 1500;
46
+ /** Recorder batch flushes immediately at this queue depth. */
47
+ export const WHITEBOARD_INTERACTION_FLUSH_AT = 12;
48
+ /** Per-field typing debounce inside the recorder (ms). */
49
+ export const WHITEBOARD_INTERACTION_TYPE_DEBOUNCE_MS = 800;
50
+ /**
51
+ * The ONE helper script prepended to every widget `srcdoc` — defines `MJWhiteboard.submit`
52
+ * (explicit input path) AND installs the passive ambient-interaction recorder.
53
+ * `'*'` as the target origin is acceptable here: the sandboxed frame has an opaque origin
54
+ * (there is nothing else to address), and the HOST validates marker + `event.source`.
55
+ *
56
+ * The recorder, in widget-side terms:
57
+ * - `click` — buttons / links / labels / role=button / aria-labeled elements only
58
+ * (body/div noise without a label is ignored); records the tag + best label
59
+ * (aria-label || name || id || trimmed textContent, ≤40 chars).
60
+ * - `change` — select / radio / checkbox / text fields: field label + new value
61
+ * (checkbox → 'checked'/'unchecked').
62
+ * - `input` — debounced per-field (800 ms): field label + current value.
63
+ * - `focusin` / `focusout` — field label only.
64
+ * - PRIVACY: `type="password"` / `type="hidden"` fields are skipped entirely; every
65
+ * recorded value is truncated to 120 chars BEFORE leaving the frame.
66
+ * - BATCHING: events queue and flush via postMessage every 1500 ms or at 12 queued.
67
+ * - RESILIENCE: every handler body is try/catch-wrapped — recorder errors never leak
68
+ * into widget code.
69
+ *
70
+ * AUTO-SUBMIT FALLBACK: agent-generated widgets don't always wire
71
+ * `MJWhiteboard.submit` — a plain `<form>` submit would NAVIGATE the sandboxed frame
72
+ * (blanking the widget) and an inert Submit button goes nowhere. The helper therefore:
73
+ * - intercepts every `submit` event (preventDefault — the frame never navigates) and
74
+ * forwards the form's serialized fields as `{ __auto: true, fields }`;
75
+ * - watches clicks on submit-ish buttons (type=submit, or labeled submit/send/done/…)
76
+ * and, shortly after, forwards the nearest form's (or document's) fields the same way;
77
+ * - DEDUPES: an explicit `MJWhiteboard.submit` (or a prior auto-forward) within 600 ms
78
+ * suppresses the fallback, so well-built widgets never double-submit.
79
+ * - Serialization respects the recorder's privacy rules (password/hidden skipped,
80
+ * values clipped) and only checked radios/checkboxes contribute.
81
+ */
82
+ export const WHITEBOARD_SUBMIT_HELPER = `<script>window.MJWhiteboard={__lastSubmitAt:0,submit:function(data){window.MJWhiteboard.__lastSubmitAt=Date.now();parent.postMessage({__mjWhiteboardSubmit:true,data:data},'*');}};
83
+ (function(){
84
+ var VALUE_MAX=120,LABEL_MAX=40,FLUSH_MS=1500,FLUSH_AT=12,TYPE_MS=800;
85
+ var queue=[],flushTimer=null,typeTimers={};
86
+ function flush(){flushTimer=null;if(!queue.length){return;}var batch=queue;queue=[];try{parent.postMessage({__mjWhiteboardInteraction:true,events:batch},'*');}catch(e){}}
87
+ function push(ev){queue.push(ev);if(queue.length>=FLUSH_AT){if(flushTimer){clearTimeout(flushTimer);flushTimer=null;}flush();return;}if(!flushTimer){flushTimer=setTimeout(flush,FLUSH_MS);}}
88
+ function clip(v,max){v=String(v==null?'':v);return v.length>max?v.slice(0,max):v;}
89
+ function isPrivate(el){var t=(el&&el.type?String(el.type):'').toLowerCase();return t==='password'||t==='hidden';}
90
+ function labelOf(el){if(!el||!el.getAttribute){return '';}var v=el.getAttribute('aria-label')||el.getAttribute('name')||el.id;if(v){return clip(v,LABEL_MAX);}var t=(el.textContent||'').replace(/\\s+/g,' ').trim();return t?clip(t,LABEL_MAX):'';}
91
+ function fieldLabel(el){var own=labelOf(el);if(own){return own;}try{if(el.labels&&el.labels.length){var lt=(el.labels[0].textContent||'').replace(/\\s+/g,' ').trim();if(lt){return clip(lt,LABEL_MAX);}}var wrap=el.closest?el.closest('label'):null;if(wrap){var wt=(wrap.textContent||'').replace(/\\s+/g,' ').trim();if(wt){return clip(wt,LABEL_MAX);}}}catch(e){}return el.tagName?el.tagName.toLowerCase():'field';}
92
+ function isField(el){var t=el&&el.tagName?el.tagName.toUpperCase():'';return t==='INPUT'||t==='TEXTAREA'||t==='SELECT';}
93
+ function fieldValue(el){if(el.type==='checkbox'){return el.checked?'checked':'unchecked';}return clip(el.value,VALUE_MAX);}
94
+ document.addEventListener('click',function(e){try{var el=e.target&&e.target.closest?e.target.closest('button,a,label,input[type="button"],input[type="submit"],[role="button"],[aria-label]'):null;if(!el){return;}var lbl=labelOf(el);if(!lbl){return;}push({kind:'click',target:lbl,tag:el.tagName.toLowerCase()});}catch(err){}},true);
95
+ document.addEventListener('change',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}var name=fieldLabel(el);if(typeTimers[name]){clearTimeout(typeTimers[name]);delete typeTimers[name];}push({kind:'change',target:name,value:fieldValue(el)});}catch(err){}},true);
96
+ document.addEventListener('input',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}var t=(el.type||'').toLowerCase();if(t==='checkbox'||t==='radio'){return;}var name=fieldLabel(el);if(typeTimers[name]){clearTimeout(typeTimers[name]);}typeTimers[name]=setTimeout(function(){delete typeTimers[name];try{push({kind:'input',target:name,value:clip(el.value,VALUE_MAX)});}catch(e2){}},TYPE_MS);}catch(err){}},true);
97
+ document.addEventListener('focusin',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}push({kind:'focus',target:fieldLabel(el)});}catch(err){}});
98
+ document.addEventListener('focusout',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}push({kind:'blur',target:fieldLabel(el)});}catch(err){}});
99
+ var AUTO_DEDUPE_MS=600;
100
+ function collectFields(scope){var out={},els=(scope&&scope.querySelectorAll?scope:document).querySelectorAll('input,select,textarea');for(var i=0;i<els.length;i++){var el=els[i];if(isPrivate(el)){continue;}var t=(el.type||'').toLowerCase();if((t==='radio'||t==='checkbox')&&!el.checked){continue;}var name=fieldLabel(el);if(t==='checkbox'){out[name]='checked';}else{out[name]=clip(el.value,VALUE_MAX);}}return out;}
101
+ function autoSubmit(scope,trigger){try{var last=window.MJWhiteboard.__lastSubmitAt||0;if(Date.now()-last<AUTO_DEDUPE_MS){return;}var fields=collectFields(scope);var payload={__auto:true,trigger:clip(trigger||'submit',LABEL_MAX),fields:fields};window.MJWhiteboard.submit(payload);}catch(err){}}
102
+ document.addEventListener('submit',function(e){try{e.preventDefault();autoSubmit(e.target,'form');}catch(err){}},true);
103
+ document.addEventListener('click',function(e){try{var b=e.target&&e.target.closest?e.target.closest('button,input[type="submit"],input[type="button"]'):null;if(!b){return;}var label=(labelOf(b)||'')+' '+(b.type||'');if(!/submit|send|done|go\\b|answer|check/i.test(label)){return;}setTimeout(function(){autoSubmit(b.form||document,labelOf(b)||'button');},150);}catch(err){}},true);
104
+ })();</script>`;
105
+ /** Max serialized payload size (chars) accepted from one `MJWhiteboard.submit` call. */
106
+ export const WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS = 8000;
107
+ /**
108
+ * Prepend the `MJWhiteboard.submit` helper to a widget's HTML source — EXACTLY once.
109
+ * Idempotent: when the helper is already present (re-render of an injected source) the
110
+ * input is returned unchanged, so memoized srcdocs never accumulate duplicate helpers.
111
+ */
112
+ export function InjectWhiteboardSubmitHelper(html) {
113
+ if (html.includes(WHITEBOARD_SUBMIT_HELPER)) {
114
+ return html;
115
+ }
116
+ return WHITEBOARD_SUBMIT_HELPER + html;
117
+ }
118
+ /**
119
+ * Host-side validation of one window 'message' event:
120
+ *
121
+ * 1. the payload must carry the {@link WHITEBOARD_SUBMIT_MARKER} (anything else is
122
+ * unrelated postMessage traffic — dropped silently as `'not-submit'`);
123
+ * 2. `sourceWidget` is the caller's resolution of `event.source` against its tracked
124
+ * widget iframes (window → item map) — `null` means the sender is NOT one of the
125
+ * board's widgets and the message is dropped (`'unknown-source'`);
126
+ * 3. the submitted data is JSON-serialized and capped at
127
+ * {@link WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS} (`'oversize'` beyond it).
128
+ */
129
+ export function EvaluateWidgetSubmitMessage(messageData, sourceWidget) {
130
+ if (messageData === null || typeof messageData !== 'object'
131
+ || messageData[WHITEBOARD_SUBMIT_MARKER] !== true) {
132
+ return { Kind: 'dropped', Reason: 'not-submit' };
133
+ }
134
+ if (!sourceWidget) {
135
+ return { Kind: 'dropped', Reason: 'unknown-source' };
136
+ }
137
+ let dataJson;
138
+ try {
139
+ dataJson = JSON.stringify(messageData['data']);
140
+ }
141
+ catch {
142
+ dataJson = undefined; // circular payloads can't cross postMessage, but stay defensive
143
+ }
144
+ if (dataJson === undefined) {
145
+ return { Kind: 'dropped', Reason: 'unserializable' };
146
+ }
147
+ if (dataJson.length > WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS) {
148
+ return { Kind: 'dropped', Reason: 'oversize' };
149
+ }
150
+ return {
151
+ Kind: 'submit',
152
+ Event: { ItemID: sourceWidget.ItemID, Title: sourceWidget.Title ?? '', DataJson: dataJson }
153
+ };
154
+ }
155
+ // ───────────────────────────────────────────────────────────── ambient interactions
156
+ /** Max serialized size (chars) accepted for one ambient interaction BATCH. */
157
+ export const WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS = 6000;
158
+ /** Max length of one summarized interaction line (see {@link SummarizeWidgetInteractions}). */
159
+ export const WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS = 300;
160
+ const INTERACTION_KINDS = new Set(['click', 'change', 'input', 'focus', 'blur']);
161
+ /** Shape-check one entry of an interaction batch against {@link WhiteboardWidgetInteractionRecord}. */
162
+ function isInteractionRecord(value) {
163
+ if (value === null || typeof value !== 'object') {
164
+ return false;
165
+ }
166
+ const record = value;
167
+ return typeof record['kind'] === 'string' && INTERACTION_KINDS.has(record['kind'])
168
+ && typeof record['target'] === 'string'
169
+ && (record['value'] === undefined || typeof record['value'] === 'string')
170
+ && (record['tag'] === undefined || typeof record['tag'] === 'string');
171
+ }
172
+ /**
173
+ * Host-side validation of one ambient-interaction 'message' event — mirrors the submit
174
+ * path: marker required (`'not-interaction'` otherwise), `sourceWidget` must resolve to a
175
+ * tracked widget iframe (`'unknown-source'`), the `events` array must be a non-empty list
176
+ * of well-shaped {@link WhiteboardWidgetInteractionRecord}s (`'bad-shape'`), and the
177
+ * serialized batch is capped at {@link WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS}
178
+ * (`'oversize'`). Accepted batches come back pre-summarized (via
179
+ * {@link SummarizeWidgetInteractions}) as a {@link WhiteboardWidgetInteractionEvent}.
180
+ */
181
+ export function EvaluateWidgetInteractionMessage(messageData, sourceWidget) {
182
+ if (messageData === null || typeof messageData !== 'object'
183
+ || messageData[WHITEBOARD_INTERACTION_MARKER] !== true) {
184
+ return { Kind: 'dropped', Reason: 'not-interaction' };
185
+ }
186
+ if (!sourceWidget) {
187
+ return { Kind: 'dropped', Reason: 'unknown-source' };
188
+ }
189
+ const events = messageData['events'];
190
+ if (!Array.isArray(events) || events.length === 0 || !events.every(isInteractionRecord)) {
191
+ return { Kind: 'dropped', Reason: 'bad-shape' };
192
+ }
193
+ if (JSON.stringify(events).length > WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS) {
194
+ return { Kind: 'dropped', Reason: 'oversize' };
195
+ }
196
+ return {
197
+ Kind: 'interaction',
198
+ Event: {
199
+ ItemID: sourceWidget.ItemID,
200
+ Title: sourceWidget.Title ?? '',
201
+ Summary: SummarizeWidgetInteractions(events)
202
+ }
203
+ };
204
+ }
205
+ /** Defensive host-side re-clip (the recorder already truncates, but trust nothing). */
206
+ function clipValue(value) {
207
+ return value.length > WHITEBOARD_INTERACTION_VALUE_MAX_CHARS
208
+ ? value.slice(0, WHITEBOARD_INTERACTION_VALUE_MAX_CHARS)
209
+ : value;
210
+ }
211
+ /** Render one interaction record as a human fragment. */
212
+ function interactionPhrase(record) {
213
+ switch (record.kind) {
214
+ case 'click':
215
+ return record.target ? `clicked "${record.target}"` : `clicked ${record.tag ?? 'element'}`;
216
+ case 'change':
217
+ return record.value !== undefined
218
+ ? `changed "${record.target}" to "${clipValue(record.value)}"`
219
+ : `changed "${record.target}"`;
220
+ case 'input':
221
+ return `typing in "${record.target}"`;
222
+ case 'focus':
223
+ return `focused "${record.target}"`;
224
+ case 'blur':
225
+ return `left "${record.target}"`;
226
+ }
227
+ }
228
+ /**
229
+ * Compress one validated interaction batch into a compact human line, e.g.
230
+ * `clicked "Playful pink"; changed "vibe" to "pink"; typing in "email"`.
231
+ *
232
+ * Pure: consecutive same-field `input` (typing) records are deduped (the LAST wins —
233
+ * matching the recorder's latest-value semantics), values are defensively re-truncated to
234
+ * {@link WHITEBOARD_INTERACTION_VALUE_MAX_CHARS}, and the whole line is capped at
235
+ * {@link WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS} chars (ellipsized).
236
+ */
237
+ export function SummarizeWidgetInteractions(events) {
238
+ const deduped = [];
239
+ for (const event of events) {
240
+ const previous = deduped[deduped.length - 1];
241
+ if (previous && event.kind === 'input' && previous.kind === 'input' && previous.target === event.target) {
242
+ deduped[deduped.length - 1] = event; // consecutive same-field typing — latest wins
243
+ }
244
+ else {
245
+ deduped.push(event);
246
+ }
247
+ }
248
+ const line = deduped.map(interactionPhrase).join('; ');
249
+ if (line.length > WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS) {
250
+ return line.slice(0, WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS - 1) + '…';
251
+ }
252
+ return line;
253
+ }
254
+ //# sourceMappingURL=whiteboard-widget-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-widget-bridge.js","sourceRoot":"","sources":["../../src/lib/whiteboard-widget-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * LIVE WHITEBOARD — the HTML-widget INPUT BRIDGE (`MJWhiteboard.submit`) and the AMBIENT\n * INTERACTION RECORDER.\n *\n * HTML widgets are tutoring-style \"illustrate a concept and GET USER INPUT\" chunks —\n * micro-forms, HTML+SVG explainers, concept animations, one-question quizzes. They render\n * inside a strictly sandboxed iframe (`sandbox=\"allow-scripts\"` only, opaque origin), so\n * the ONLY way data can flow out is `postMessage`. This module is the Angular-free half of\n * that bridge:\n *\n * - {@link InjectWhiteboardSubmitHelper} prepends ONE helper script to every widget's\n * `srcdoc` (exactly once) that defines BOTH halves of the bridge:\n * 1. `window.MJWhiteboard.submit(data)` — the EXPLICIT input path; the widget calls\n * it from a button / form handler and the helper posts the payload to the parent.\n * 2. The AMBIENT interaction recorder — passive document-level listeners (click /\n * change / debounced input / focusin / focusout) that observe form activity the\n * widget author wrote NO script for, batch it, and post it to the parent as a\n * `__mjWhiteboardInteraction` message. Password / hidden inputs are skipped\n * entirely; every value is truncated client-side. The recorder is defensive:\n * every handler is wrapped in try/catch so it can never break widget code.\n * The helper targets `'*'` deliberately: the frame runs in an OPAQUE origin, so there is\n * no concrete origin to name; the HOST side validates the message instead.\n * - {@link EvaluateWidgetSubmitMessage} / {@link EvaluateWidgetInteractionMessage} are\n * that host-side validation: the marker property must be present, the `event.source`\n * must resolve to a TRACKED widget iframe (anything else is dropped), the payload is\n * size-capped (oversize → warn + drop), and interaction batches are shape-checked.\n * - {@link SummarizeWidgetInteractions} compresses one validated interaction batch into a\n * compact human line (`clicked \"Playful pink\"; changed \"vibe\" to \"pink\"; typing in\n * \"email\"`) — what the channel plugin forwards to the live agent.\n *\n * The board component owns the single `window` 'message' listener and emits accepted\n * submissions as {@link WhiteboardWidgetSubmitEvent} and accepted ambient batches as\n * {@link WhiteboardWidgetInteractionEvent}; the channel plugin forwards them to the live\n * agent context as `[whiteboard]` notes (interaction notes throttled per widget).\n */\n\n/** Marker property carried on every explicit-submit bridge postMessage payload. */\nexport const WHITEBOARD_SUBMIT_MARKER = '__mjWhiteboardSubmit';\n\n/** Marker property carried on every ambient-interaction bridge postMessage payload. */\nexport const WHITEBOARD_INTERACTION_MARKER = '__mjWhiteboardInteraction';\n\n/** Max chars an in-widget recorded VALUE keeps (recorder-side truncation, defensive re-clip host-side). */\nexport const WHITEBOARD_INTERACTION_VALUE_MAX_CHARS = 120;\n\n/** Max chars a recorded element LABEL keeps. */\nexport const WHITEBOARD_INTERACTION_LABEL_MAX_CHARS = 40;\n\n/** Recorder batch flush interval (ms). */\nexport const WHITEBOARD_INTERACTION_FLUSH_MS = 1500;\n\n/** Recorder batch flushes immediately at this queue depth. */\nexport const WHITEBOARD_INTERACTION_FLUSH_AT = 12;\n\n/** Per-field typing debounce inside the recorder (ms). */\nexport const WHITEBOARD_INTERACTION_TYPE_DEBOUNCE_MS = 800;\n\n/**\n * The ONE helper script prepended to every widget `srcdoc` — defines `MJWhiteboard.submit`\n * (explicit input path) AND installs the passive ambient-interaction recorder.\n * `'*'` as the target origin is acceptable here: the sandboxed frame has an opaque origin\n * (there is nothing else to address), and the HOST validates marker + `event.source`.\n *\n * The recorder, in widget-side terms:\n * - `click` — buttons / links / labels / role=button / aria-labeled elements only\n * (body/div noise without a label is ignored); records the tag + best label\n * (aria-label || name || id || trimmed textContent, ≤40 chars).\n * - `change` — select / radio / checkbox / text fields: field label + new value\n * (checkbox → 'checked'/'unchecked').\n * - `input` — debounced per-field (800 ms): field label + current value.\n * - `focusin` / `focusout` — field label only.\n * - PRIVACY: `type=\"password\"` / `type=\"hidden\"` fields are skipped entirely; every\n * recorded value is truncated to 120 chars BEFORE leaving the frame.\n * - BATCHING: events queue and flush via postMessage every 1500 ms or at 12 queued.\n * - RESILIENCE: every handler body is try/catch-wrapped — recorder errors never leak\n * into widget code.\n *\n * AUTO-SUBMIT FALLBACK: agent-generated widgets don't always wire\n * `MJWhiteboard.submit` — a plain `<form>` submit would NAVIGATE the sandboxed frame\n * (blanking the widget) and an inert Submit button goes nowhere. The helper therefore:\n * - intercepts every `submit` event (preventDefault — the frame never navigates) and\n * forwards the form's serialized fields as `{ __auto: true, fields }`;\n * - watches clicks on submit-ish buttons (type=submit, or labeled submit/send/done/…)\n * and, shortly after, forwards the nearest form's (or document's) fields the same way;\n * - DEDUPES: an explicit `MJWhiteboard.submit` (or a prior auto-forward) within 600 ms\n * suppresses the fallback, so well-built widgets never double-submit.\n * - Serialization respects the recorder's privacy rules (password/hidden skipped,\n * values clipped) and only checked radios/checkboxes contribute.\n */\nexport const WHITEBOARD_SUBMIT_HELPER =\n `<script>window.MJWhiteboard={__lastSubmitAt:0,submit:function(data){window.MJWhiteboard.__lastSubmitAt=Date.now();parent.postMessage({__mjWhiteboardSubmit:true,data:data},'*');}};\n(function(){\nvar VALUE_MAX=120,LABEL_MAX=40,FLUSH_MS=1500,FLUSH_AT=12,TYPE_MS=800;\nvar queue=[],flushTimer=null,typeTimers={};\nfunction flush(){flushTimer=null;if(!queue.length){return;}var batch=queue;queue=[];try{parent.postMessage({__mjWhiteboardInteraction:true,events:batch},'*');}catch(e){}}\nfunction push(ev){queue.push(ev);if(queue.length>=FLUSH_AT){if(flushTimer){clearTimeout(flushTimer);flushTimer=null;}flush();return;}if(!flushTimer){flushTimer=setTimeout(flush,FLUSH_MS);}}\nfunction clip(v,max){v=String(v==null?'':v);return v.length>max?v.slice(0,max):v;}\nfunction isPrivate(el){var t=(el&&el.type?String(el.type):'').toLowerCase();return t==='password'||t==='hidden';}\nfunction labelOf(el){if(!el||!el.getAttribute){return '';}var v=el.getAttribute('aria-label')||el.getAttribute('name')||el.id;if(v){return clip(v,LABEL_MAX);}var t=(el.textContent||'').replace(/\\\\s+/g,' ').trim();return t?clip(t,LABEL_MAX):'';}\nfunction fieldLabel(el){var own=labelOf(el);if(own){return own;}try{if(el.labels&&el.labels.length){var lt=(el.labels[0].textContent||'').replace(/\\\\s+/g,' ').trim();if(lt){return clip(lt,LABEL_MAX);}}var wrap=el.closest?el.closest('label'):null;if(wrap){var wt=(wrap.textContent||'').replace(/\\\\s+/g,' ').trim();if(wt){return clip(wt,LABEL_MAX);}}}catch(e){}return el.tagName?el.tagName.toLowerCase():'field';}\nfunction isField(el){var t=el&&el.tagName?el.tagName.toUpperCase():'';return t==='INPUT'||t==='TEXTAREA'||t==='SELECT';}\nfunction fieldValue(el){if(el.type==='checkbox'){return el.checked?'checked':'unchecked';}return clip(el.value,VALUE_MAX);}\ndocument.addEventListener('click',function(e){try{var el=e.target&&e.target.closest?e.target.closest('button,a,label,input[type=\"button\"],input[type=\"submit\"],[role=\"button\"],[aria-label]'):null;if(!el){return;}var lbl=labelOf(el);if(!lbl){return;}push({kind:'click',target:lbl,tag:el.tagName.toLowerCase()});}catch(err){}},true);\ndocument.addEventListener('change',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}var name=fieldLabel(el);if(typeTimers[name]){clearTimeout(typeTimers[name]);delete typeTimers[name];}push({kind:'change',target:name,value:fieldValue(el)});}catch(err){}},true);\ndocument.addEventListener('input',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}var t=(el.type||'').toLowerCase();if(t==='checkbox'||t==='radio'){return;}var name=fieldLabel(el);if(typeTimers[name]){clearTimeout(typeTimers[name]);}typeTimers[name]=setTimeout(function(){delete typeTimers[name];try{push({kind:'input',target:name,value:clip(el.value,VALUE_MAX)});}catch(e2){}},TYPE_MS);}catch(err){}},true);\ndocument.addEventListener('focusin',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}push({kind:'focus',target:fieldLabel(el)});}catch(err){}});\ndocument.addEventListener('focusout',function(e){try{var el=e.target;if(!isField(el)||isPrivate(el)){return;}push({kind:'blur',target:fieldLabel(el)});}catch(err){}});\nvar AUTO_DEDUPE_MS=600;\nfunction collectFields(scope){var out={},els=(scope&&scope.querySelectorAll?scope:document).querySelectorAll('input,select,textarea');for(var i=0;i<els.length;i++){var el=els[i];if(isPrivate(el)){continue;}var t=(el.type||'').toLowerCase();if((t==='radio'||t==='checkbox')&&!el.checked){continue;}var name=fieldLabel(el);if(t==='checkbox'){out[name]='checked';}else{out[name]=clip(el.value,VALUE_MAX);}}return out;}\nfunction autoSubmit(scope,trigger){try{var last=window.MJWhiteboard.__lastSubmitAt||0;if(Date.now()-last<AUTO_DEDUPE_MS){return;}var fields=collectFields(scope);var payload={__auto:true,trigger:clip(trigger||'submit',LABEL_MAX),fields:fields};window.MJWhiteboard.submit(payload);}catch(err){}}\ndocument.addEventListener('submit',function(e){try{e.preventDefault();autoSubmit(e.target,'form');}catch(err){}},true);\ndocument.addEventListener('click',function(e){try{var b=e.target&&e.target.closest?e.target.closest('button,input[type=\"submit\"],input[type=\"button\"]'):null;if(!b){return;}var label=(labelOf(b)||'')+' '+(b.type||'');if(!/submit|send|done|go\\\\b|answer|check/i.test(label)){return;}setTimeout(function(){autoSubmit(b.form||document,labelOf(b)||'button');},150);}catch(err){}},true);\n})();</script>`;\n\n/** Max serialized payload size (chars) accepted from one `MJWhiteboard.submit` call. */\nexport const WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS = 8000;\n\n/** One accepted widget submission, surfaced by the board/host `WidgetSubmitted` output. */\nexport interface WhiteboardWidgetSubmitEvent {\n /** The submitting widget's board item ID (e.g. `html-2`). */\n ItemID: string;\n /** The widget's header title ('' when untitled — consumers fall back to {@link ItemID}). */\n Title: string;\n /** The submitted payload, JSON-serialized (capped at {@link WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS}). */\n DataJson: string;\n}\n\n/**\n * BEFORE-event args for the board/host `WidgetSubmitting` output — raised after a\n * widget's `MJWhiteboard.submit` payload passed validation (marker + tracked source +\n * size cap) but BEFORE it is surfaced through `WidgetSubmitted`. Handlers run\n * synchronously; set {@link Cancel} to `true` to drop the submission (no `WidgetSubmitted`\n * fires and nothing reaches the agent/consumer).\n */\nexport interface WhiteboardWidgetSubmittingEventArgs {\n /** The validated submission about to be surfaced. */\n Event: WhiteboardWidgetSubmitEvent;\n /** Set to `true` (in a synchronous handler) to drop the submission. */\n Cancel: boolean;\n}\n\n/** Why an incoming 'message' event was NOT surfaced as a widget submission. */\nexport type WhiteboardWidgetSubmitDropReason =\n /** Not a bridge message at all (no marker) — other postMessage traffic, ignore silently. */\n | 'not-submit'\n /** Marker present but `event.source` is not a tracked widget iframe — spoof/stale, drop. */\n | 'unknown-source'\n /** Payload serialized beyond the cap — warn + drop. */\n | 'oversize'\n /** Payload could not be JSON-serialized (e.g. `undefined` / function) — drop. */\n | 'unserializable';\n\n/** Outcome of evaluating one window 'message' event against the bridge contract. */\nexport type WhiteboardWidgetSubmitOutcome =\n | { Kind: 'submit'; Event: WhiteboardWidgetSubmitEvent }\n | { Kind: 'dropped'; Reason: WhiteboardWidgetSubmitDropReason };\n\n/**\n * Prepend the `MJWhiteboard.submit` helper to a widget's HTML source — EXACTLY once.\n * Idempotent: when the helper is already present (re-render of an injected source) the\n * input is returned unchanged, so memoized srcdocs never accumulate duplicate helpers.\n */\nexport function InjectWhiteboardSubmitHelper(html: string): string {\n if (html.includes(WHITEBOARD_SUBMIT_HELPER)) {\n return html;\n }\n return WHITEBOARD_SUBMIT_HELPER + html;\n}\n\n/**\n * Host-side validation of one window 'message' event:\n *\n * 1. the payload must carry the {@link WHITEBOARD_SUBMIT_MARKER} (anything else is\n * unrelated postMessage traffic — dropped silently as `'not-submit'`);\n * 2. `sourceWidget` is the caller's resolution of `event.source` against its tracked\n * widget iframes (window → item map) — `null` means the sender is NOT one of the\n * board's widgets and the message is dropped (`'unknown-source'`);\n * 3. the submitted data is JSON-serialized and capped at\n * {@link WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS} (`'oversize'` beyond it).\n */\nexport function EvaluateWidgetSubmitMessage(\n messageData: unknown,\n sourceWidget: { ItemID: string; Title?: string } | null\n): WhiteboardWidgetSubmitOutcome {\n if (messageData === null || typeof messageData !== 'object'\n || (messageData as Record<string, unknown>)[WHITEBOARD_SUBMIT_MARKER] !== true) {\n return { Kind: 'dropped', Reason: 'not-submit' };\n }\n if (!sourceWidget) {\n return { Kind: 'dropped', Reason: 'unknown-source' };\n }\n let dataJson: string | undefined;\n try {\n dataJson = JSON.stringify((messageData as Record<string, unknown>)['data']);\n }\n catch {\n dataJson = undefined; // circular payloads can't cross postMessage, but stay defensive\n }\n if (dataJson === undefined) {\n return { Kind: 'dropped', Reason: 'unserializable' };\n }\n if (dataJson.length > WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS) {\n return { Kind: 'dropped', Reason: 'oversize' };\n }\n return {\n Kind: 'submit',\n Event: { ItemID: sourceWidget.ItemID, Title: sourceWidget.Title ?? '', DataJson: dataJson }\n };\n}\n\n// ───────────────────────────────────────────────────────────── ambient interactions\n\n/** Max serialized size (chars) accepted for one ambient interaction BATCH. */\nexport const WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS = 6000;\n\n/** Max length of one summarized interaction line (see {@link SummarizeWidgetInteractions}). */\nexport const WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS = 300;\n\n/** The event kinds the in-widget recorder emits. */\nexport type WhiteboardWidgetInteractionKind = 'click' | 'change' | 'input' | 'focus' | 'blur';\n\n/** One recorded in-widget interaction, as posted by the injected recorder. */\nexport interface WhiteboardWidgetInteractionRecord {\n /** What happened. */\n kind: WhiteboardWidgetInteractionKind;\n /** Best label of the interacted element (aria-label || name || id || text, ≤40 chars). */\n target: string;\n /** The field's value after the event (change/input only; checkbox → 'checked'/'unchecked'). */\n value?: string;\n /** The clicked element's tag name (click only). */\n tag?: string;\n}\n\n/**\n * One accepted ambient-interaction batch, surfaced by the board/host `WidgetInteraction`\n * output. `Summary` is the compact human line built by {@link SummarizeWidgetInteractions}\n * — what integration layers forward to their agent runtime.\n */\nexport interface WhiteboardWidgetInteractionEvent {\n /** The widget's board item ID (e.g. `html-2`). */\n ItemID: string;\n /** The widget's header title ('' when untitled — consumers fall back to {@link ItemID}). */\n Title: string;\n /** Compact human summary of the batch (≤{@link WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS} chars). */\n Summary: string;\n}\n\n/** Why an incoming 'message' event was NOT surfaced as an ambient interaction batch. */\nexport type WhiteboardWidgetInteractionDropReason =\n /** Not an interaction message at all (no marker) — other postMessage traffic, ignore silently. */\n | 'not-interaction'\n /** Marker present but `event.source` is not a tracked widget iframe — spoof/stale, drop. */\n | 'unknown-source'\n /** Serialized batch beyond {@link WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS} — warn + drop. */\n | 'oversize'\n /** `events` missing / empty / not the documented record shape — warn + drop. */\n | 'bad-shape';\n\n/** Outcome of evaluating one window 'message' event against the interaction contract. */\nexport type WhiteboardWidgetInteractionOutcome =\n | { Kind: 'interaction'; Event: WhiteboardWidgetInteractionEvent }\n | { Kind: 'dropped'; Reason: WhiteboardWidgetInteractionDropReason };\n\nconst INTERACTION_KINDS: ReadonlySet<string> = new Set(['click', 'change', 'input', 'focus', 'blur']);\n\n/** Shape-check one entry of an interaction batch against {@link WhiteboardWidgetInteractionRecord}. */\nfunction isInteractionRecord(value: unknown): value is WhiteboardWidgetInteractionRecord {\n if (value === null || typeof value !== 'object') {\n return false;\n }\n const record = value as Record<string, unknown>;\n return typeof record['kind'] === 'string' && INTERACTION_KINDS.has(record['kind'])\n && typeof record['target'] === 'string'\n && (record['value'] === undefined || typeof record['value'] === 'string')\n && (record['tag'] === undefined || typeof record['tag'] === 'string');\n}\n\n/**\n * Host-side validation of one ambient-interaction 'message' event — mirrors the submit\n * path: marker required (`'not-interaction'` otherwise), `sourceWidget` must resolve to a\n * tracked widget iframe (`'unknown-source'`), the `events` array must be a non-empty list\n * of well-shaped {@link WhiteboardWidgetInteractionRecord}s (`'bad-shape'`), and the\n * serialized batch is capped at {@link WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS}\n * (`'oversize'`). Accepted batches come back pre-summarized (via\n * {@link SummarizeWidgetInteractions}) as a {@link WhiteboardWidgetInteractionEvent}.\n */\nexport function EvaluateWidgetInteractionMessage(\n messageData: unknown,\n sourceWidget: { ItemID: string; Title?: string } | null\n): WhiteboardWidgetInteractionOutcome {\n if (messageData === null || typeof messageData !== 'object'\n || (messageData as Record<string, unknown>)[WHITEBOARD_INTERACTION_MARKER] !== true) {\n return { Kind: 'dropped', Reason: 'not-interaction' };\n }\n if (!sourceWidget) {\n return { Kind: 'dropped', Reason: 'unknown-source' };\n }\n const events = (messageData as Record<string, unknown>)['events'];\n if (!Array.isArray(events) || events.length === 0 || !events.every(isInteractionRecord)) {\n return { Kind: 'dropped', Reason: 'bad-shape' };\n }\n if (JSON.stringify(events).length > WHITEBOARD_WIDGET_INTERACTION_MAX_CHARS) {\n return { Kind: 'dropped', Reason: 'oversize' };\n }\n return {\n Kind: 'interaction',\n Event: {\n ItemID: sourceWidget.ItemID,\n Title: sourceWidget.Title ?? '',\n Summary: SummarizeWidgetInteractions(events)\n }\n };\n}\n\n/** Defensive host-side re-clip (the recorder already truncates, but trust nothing). */\nfunction clipValue(value: string): string {\n return value.length > WHITEBOARD_INTERACTION_VALUE_MAX_CHARS\n ? value.slice(0, WHITEBOARD_INTERACTION_VALUE_MAX_CHARS)\n : value;\n}\n\n/** Render one interaction record as a human fragment. */\nfunction interactionPhrase(record: WhiteboardWidgetInteractionRecord): string {\n switch (record.kind) {\n case 'click':\n return record.target ? `clicked \"${record.target}\"` : `clicked ${record.tag ?? 'element'}`;\n case 'change':\n return record.value !== undefined\n ? `changed \"${record.target}\" to \"${clipValue(record.value)}\"`\n : `changed \"${record.target}\"`;\n case 'input':\n return `typing in \"${record.target}\"`;\n case 'focus':\n return `focused \"${record.target}\"`;\n case 'blur':\n return `left \"${record.target}\"`;\n }\n}\n\n/**\n * Compress one validated interaction batch into a compact human line, e.g.\n * `clicked \"Playful pink\"; changed \"vibe\" to \"pink\"; typing in \"email\"`.\n *\n * Pure: consecutive same-field `input` (typing) records are deduped (the LAST wins —\n * matching the recorder's latest-value semantics), values are defensively re-truncated to\n * {@link WHITEBOARD_INTERACTION_VALUE_MAX_CHARS}, and the whole line is capped at\n * {@link WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS} chars (ellipsized).\n */\nexport function SummarizeWidgetInteractions(events: WhiteboardWidgetInteractionRecord[]): string {\n const deduped: WhiteboardWidgetInteractionRecord[] = [];\n for (const event of events) {\n const previous = deduped[deduped.length - 1];\n if (previous && event.kind === 'input' && previous.kind === 'input' && previous.target === event.target) {\n deduped[deduped.length - 1] = event; // consecutive same-field typing — latest wins\n }\n else {\n deduped.push(event);\n }\n }\n const line = deduped.map(interactionPhrase).join('; ');\n if (line.length > WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS) {\n return line.slice(0, WHITEBOARD_INTERACTION_SUMMARY_MAX_CHARS - 1) + '…';\n }\n return line;\n}\n"]}
@@ -0,0 +1,55 @@
1
+ import { EventEmitter, OnDestroy } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /** Which way a zoom button points. */
4
+ type ZoomDirection = 1 | -1;
5
+ /**
6
+ * The bottom-right zoom cluster of the live whiteboard: zoom out / percentage / zoom in,
7
+ * fit-to-content, and the minimap toggle. Pure presentational; the board owns the
8
+ * viewport math and the minimap rendering.
9
+ *
10
+ * HOLD-TO-ZOOM: a plain click on +/− still emits the existing single preset step
11
+ * ({@link ZoomIn} / {@link ZoomOut}); holding the button down (pointerdown held past a
12
+ * short delay) emits {@link ZoomBy} factors (~3.5%) every 50 ms for smooth continuous
13
+ * zooming, stopping on pointerup / pointerleave / pointercancel / destroy — the timers
14
+ * never leak. The board clamps to its usual 25%–200% limits.
15
+ */
16
+ export declare class RealtimeWhiteboardZoomComponent implements OnDestroy {
17
+ /** How long a press must be held before continuous zooming kicks in. */
18
+ private static readonly HoldDelayMs;
19
+ /** Continuous-zoom tick cadence while held. */
20
+ private static readonly HoldIntervalMs;
21
+ /** Multiplicative zoom factor per held tick (~3.5%). */
22
+ private static readonly HoldFactor;
23
+ /** Current zoom as a percentage (e.g. 90). */
24
+ ZoomPercent: number;
25
+ /** Whether the minimap is currently shown (highlights the map button). */
26
+ MinimapOpen: boolean;
27
+ /** Single preset step out (a plain click on −). */
28
+ ZoomOut: EventEmitter<void>;
29
+ /** Single preset step in (a plain click on +). */
30
+ ZoomIn: EventEmitter<void>;
31
+ /**
32
+ * Continuous (held-button) zoom tick: a multiplicative factor (>1 zooms in, <1 zooms
33
+ * out) emitted every 50 ms while + or − is held. Wire to the board's `ZoomByFactor`.
34
+ */
35
+ ZoomBy: EventEmitter<number>;
36
+ Fit: EventEmitter<void>;
37
+ ToggleMinimap: EventEmitter<void>;
38
+ private holdDelayTimer;
39
+ private holdIntervalTimer;
40
+ /** Set once continuous ticks started, so the trailing click doesn't ALSO step. */
41
+ private suppressNextClick;
42
+ ngOnDestroy(): void;
43
+ /** Pointer down on +/−: arm the hold — after the delay, tick {@link ZoomBy} repeatedly. */
44
+ OnZoomPressStart(event: PointerEvent, direction: ZoomDirection): void;
45
+ /** Pointer up / leave / cancel: stop any pending or running continuous zoom. */
46
+ OnZoomPressEnd(): void;
47
+ /** The click that follows the press: a single preset step unless a hold already zoomed. */
48
+ OnZoomClick(direction: ZoomDirection): void;
49
+ /** Clear both hold timers (idempotent — safe from any stop path). */
50
+ private stopHold;
51
+ static ɵfac: i0.ɵɵFactoryDeclaration<RealtimeWhiteboardZoomComponent, never>;
52
+ static ɵcmp: i0.ɵɵComponentDeclaration<RealtimeWhiteboardZoomComponent, "mj-realtime-whiteboard-zoom", never, { "ZoomPercent": { "alias": "ZoomPercent"; "required": false; }; "MinimapOpen": { "alias": "MinimapOpen"; "required": false; }; }, { "ZoomOut": "ZoomOut"; "ZoomIn": "ZoomIn"; "ZoomBy": "ZoomBy"; "Fit": "Fit"; "ToggleMinimap": "ToggleMinimap"; }, never, never, true, never>;
53
+ }
54
+ export {};
55
+ //# sourceMappingURL=whiteboard-zoom.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-zoom.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,YAAY,EAAS,SAAS,EAAU,MAAM,eAAe,CAAC;;AAGlF,sCAAsC;AACtC,KAAK,aAAa,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAE5B;;;;;;;;;;GAUG;AACH,qBAOa,+BAAgC,YAAW,SAAS;IAC/D,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAO;IAC1C,+CAA+C;IAC/C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAM;IAC5C,wDAAwD;IACxD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAS;IAE3C,8CAA8C;IACrC,WAAW,SAAO;IAC3B,0EAA0E;IACjE,WAAW,UAAS;IAE7B,mDAAmD;IACzC,OAAO,qBAA4B;IAC7C,kDAAkD;IACxC,MAAM,qBAA4B;IAC5C;;;OAGG;IACO,MAAM,uBAA8B;IACpC,GAAG,qBAA4B;IAC/B,aAAa,qBAA4B;IAEnD,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,iBAAiB,CAA+C;IACxE,kFAAkF;IAClF,OAAO,CAAC,iBAAiB,CAAS;IAElC,WAAW,IAAI,IAAI;IAInB,2FAA2F;IACpF,gBAAgB,CAAC,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,aAAa,GAAG,IAAI;IAiB5E,gFAAgF;IACzE,cAAc,IAAI,IAAI;IAI7B,2FAA2F;IACpF,WAAW,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAalD,qEAAqE;IACrE,OAAO,CAAC,QAAQ;yCAxEL,+BAA+B;2CAA/B,+BAA+B;CAkF3C"}
@@ -0,0 +1,139 @@
1
+ import { Component, EventEmitter, Input, Output } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * The bottom-right zoom cluster of the live whiteboard: zoom out / percentage / zoom in,
6
+ * fit-to-content, and the minimap toggle. Pure presentational; the board owns the
7
+ * viewport math and the minimap rendering.
8
+ *
9
+ * HOLD-TO-ZOOM: a plain click on +/− still emits the existing single preset step
10
+ * ({@link ZoomIn} / {@link ZoomOut}); holding the button down (pointerdown held past a
11
+ * short delay) emits {@link ZoomBy} factors (~3.5%) every 50 ms for smooth continuous
12
+ * zooming, stopping on pointerup / pointerleave / pointercancel / destroy — the timers
13
+ * never leak. The board clamps to its usual 25%–200% limits.
14
+ */
15
+ export class RealtimeWhiteboardZoomComponent {
16
+ constructor() {
17
+ /** Current zoom as a percentage (e.g. 90). */
18
+ this.ZoomPercent = 100;
19
+ /** Whether the minimap is currently shown (highlights the map button). */
20
+ this.MinimapOpen = false;
21
+ /** Single preset step out (a plain click on −). */
22
+ this.ZoomOut = new EventEmitter();
23
+ /** Single preset step in (a plain click on +). */
24
+ this.ZoomIn = new EventEmitter();
25
+ /**
26
+ * Continuous (held-button) zoom tick: a multiplicative factor (>1 zooms in, <1 zooms
27
+ * out) emitted every 50 ms while + or − is held. Wire to the board's `ZoomByFactor`.
28
+ */
29
+ this.ZoomBy = new EventEmitter();
30
+ this.Fit = new EventEmitter();
31
+ this.ToggleMinimap = new EventEmitter();
32
+ this.holdDelayTimer = null;
33
+ this.holdIntervalTimer = null;
34
+ /** Set once continuous ticks started, so the trailing click doesn't ALSO step. */
35
+ this.suppressNextClick = false;
36
+ }
37
+ /** How long a press must be held before continuous zooming kicks in. */
38
+ static { this.HoldDelayMs = 350; }
39
+ /** Continuous-zoom tick cadence while held. */
40
+ static { this.HoldIntervalMs = 50; }
41
+ /** Multiplicative zoom factor per held tick (~3.5%). */
42
+ static { this.HoldFactor = 1.035; }
43
+ ngOnDestroy() {
44
+ this.stopHold();
45
+ }
46
+ /** Pointer down on +/−: arm the hold — after the delay, tick {@link ZoomBy} repeatedly. */
47
+ OnZoomPressStart(event, direction) {
48
+ if (event.button !== 0) {
49
+ return;
50
+ }
51
+ this.stopHold();
52
+ this.suppressNextClick = false;
53
+ this.holdDelayTimer = setTimeout(() => {
54
+ this.holdDelayTimer = null;
55
+ this.suppressNextClick = true;
56
+ const factor = direction === 1
57
+ ? RealtimeWhiteboardZoomComponent.HoldFactor
58
+ : 1 / RealtimeWhiteboardZoomComponent.HoldFactor;
59
+ this.ZoomBy.emit(factor); // first tick fires immediately when the hold engages
60
+ this.holdIntervalTimer = setInterval(() => this.ZoomBy.emit(factor), RealtimeWhiteboardZoomComponent.HoldIntervalMs);
61
+ }, RealtimeWhiteboardZoomComponent.HoldDelayMs);
62
+ }
63
+ /** Pointer up / leave / cancel: stop any pending or running continuous zoom. */
64
+ OnZoomPressEnd() {
65
+ this.stopHold();
66
+ }
67
+ /** The click that follows the press: a single preset step unless a hold already zoomed. */
68
+ OnZoomClick(direction) {
69
+ if (this.suppressNextClick) {
70
+ this.suppressNextClick = false;
71
+ return;
72
+ }
73
+ if (direction === 1) {
74
+ this.ZoomIn.emit();
75
+ }
76
+ else {
77
+ this.ZoomOut.emit();
78
+ }
79
+ }
80
+ /** Clear both hold timers (idempotent — safe from any stop path). */
81
+ stopHold() {
82
+ if (this.holdDelayTimer !== null) {
83
+ clearTimeout(this.holdDelayTimer);
84
+ this.holdDelayTimer = null;
85
+ }
86
+ if (this.holdIntervalTimer !== null) {
87
+ clearInterval(this.holdIntervalTimer);
88
+ this.holdIntervalTimer = null;
89
+ }
90
+ }
91
+ static { this.ɵfac = function RealtimeWhiteboardZoomComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RealtimeWhiteboardZoomComponent)(); }; }
92
+ static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RealtimeWhiteboardZoomComponent, selectors: [["mj-realtime-whiteboard-zoom"]], inputs: { ZoomPercent: "ZoomPercent", MinimapOpen: "MinimapOpen" }, outputs: { ZoomOut: "ZoomOut", ZoomIn: "ZoomIn", ZoomBy: "ZoomBy", Fit: "Fit", ToggleMinimap: "ToggleMinimap" }, decls: 12, vars: 3, consts: [[1, "zoom-cluster"], ["type", "button", "title", "Zoom out (hold for continuous)", 3, "pointerdown", "pointerup", "pointerleave", "pointercancel", "click"], [1, "fa-solid", "fa-minus"], [1, "zoom-pct"], ["type", "button", "title", "Zoom in (hold for continuous)", 3, "pointerdown", "pointerup", "pointerleave", "pointercancel", "click"], [1, "fa-solid", "fa-plus"], [1, "zoom-sep"], ["type", "button", "title", "Fit to content", 3, "click"], [1, "fa-solid", "fa-compress"], ["type", "button", "title", "Mini-map", 3, "click"], [1, "fa-regular", "fa-map"]], template: function RealtimeWhiteboardZoomComponent_Template(rf, ctx) { if (rf & 1) {
93
+ i0.ɵɵdomElementStart(0, "div", 0)(1, "button", 1);
94
+ i0.ɵɵdomListener("pointerdown", function RealtimeWhiteboardZoomComponent_Template_button_pointerdown_1_listener($event) { return ctx.OnZoomPressStart($event, -1); })("pointerup", function RealtimeWhiteboardZoomComponent_Template_button_pointerup_1_listener() { return ctx.OnZoomPressEnd(); })("pointerleave", function RealtimeWhiteboardZoomComponent_Template_button_pointerleave_1_listener() { return ctx.OnZoomPressEnd(); })("pointercancel", function RealtimeWhiteboardZoomComponent_Template_button_pointercancel_1_listener() { return ctx.OnZoomPressEnd(); })("click", function RealtimeWhiteboardZoomComponent_Template_button_click_1_listener() { return ctx.OnZoomClick(-1); });
95
+ i0.ɵɵdomElement(2, "i", 2);
96
+ i0.ɵɵdomElementEnd();
97
+ i0.ɵɵdomElementStart(3, "span", 3);
98
+ i0.ɵɵtext(4);
99
+ i0.ɵɵdomElementEnd();
100
+ i0.ɵɵdomElementStart(5, "button", 4);
101
+ i0.ɵɵdomListener("pointerdown", function RealtimeWhiteboardZoomComponent_Template_button_pointerdown_5_listener($event) { return ctx.OnZoomPressStart($event, 1); })("pointerup", function RealtimeWhiteboardZoomComponent_Template_button_pointerup_5_listener() { return ctx.OnZoomPressEnd(); })("pointerleave", function RealtimeWhiteboardZoomComponent_Template_button_pointerleave_5_listener() { return ctx.OnZoomPressEnd(); })("pointercancel", function RealtimeWhiteboardZoomComponent_Template_button_pointercancel_5_listener() { return ctx.OnZoomPressEnd(); })("click", function RealtimeWhiteboardZoomComponent_Template_button_click_5_listener() { return ctx.OnZoomClick(1); });
102
+ i0.ɵɵdomElement(6, "i", 5);
103
+ i0.ɵɵdomElementEnd();
104
+ i0.ɵɵdomElement(7, "span", 6);
105
+ i0.ɵɵdomElementStart(8, "button", 7);
106
+ i0.ɵɵdomListener("click", function RealtimeWhiteboardZoomComponent_Template_button_click_8_listener() { return ctx.Fit.emit(); });
107
+ i0.ɵɵdomElement(9, "i", 8);
108
+ i0.ɵɵdomElementEnd();
109
+ i0.ɵɵdomElementStart(10, "button", 9);
110
+ i0.ɵɵdomListener("click", function RealtimeWhiteboardZoomComponent_Template_button_click_10_listener() { return ctx.ToggleMinimap.emit(); });
111
+ i0.ɵɵdomElement(11, "i", 10);
112
+ i0.ɵɵdomElementEnd()();
113
+ } if (rf & 2) {
114
+ i0.ɵɵadvance(4);
115
+ i0.ɵɵtextInterpolate1("", ctx.ZoomPercent, "%");
116
+ i0.ɵɵadvance(6);
117
+ i0.ɵɵclassProp("on", ctx.MinimapOpen);
118
+ } }, dependencies: [CommonModule], styles: ["\n\n\n\n[_nghost-%COMP%] {\n display: block;\n position: absolute;\n right: 14px;\n bottom: 14px;\n z-index: 10;\n}\n\n.zoom-cluster[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 2px;\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n box-shadow: var(--mj-shadow-xl, 0 20px 25px -5px rgba(0, 0, 0, 0.3));\n padding: 4px;\n}\n\n.zoom-cluster[_ngcontent-%COMP%] button[_ngcontent-%COMP%] {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 0;\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 12px;\n display: grid;\n place-items: center;\n font-family: inherit;\n}\n.zoom-cluster[_ngcontent-%COMP%] button[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.zoom-cluster[_ngcontent-%COMP%] button.on[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n\n.zoom-pct[_ngcontent-%COMP%] {\n font-size: 11.5px;\n font-weight: 700;\n color: var(--mj-text-secondary);\n padding: 0 7px;\n font-family: var(--mj-font-mono, ui-monospace, monospace);\n}\n\n.zoom-sep[_ngcontent-%COMP%] {\n width: 1px;\n height: 18px;\n background: var(--mj-border-default);\n margin: 0 3px;\n}"] }); }
119
+ }
120
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RealtimeWhiteboardZoomComponent, [{
121
+ type: Component,
122
+ args: [{ standalone: true, selector: 'mj-realtime-whiteboard-zoom', imports: [CommonModule], template: "<div class=\"zoom-cluster\">\n <button type=\"button\" title=\"Zoom out (hold for continuous)\"\n (pointerdown)=\"OnZoomPressStart($event, -1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(-1)\"><i class=\"fa-solid fa-minus\"></i></button>\n <span class=\"zoom-pct\">{{ ZoomPercent }}%</span>\n <button type=\"button\" title=\"Zoom in (hold for continuous)\"\n (pointerdown)=\"OnZoomPressStart($event, 1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(1)\"><i class=\"fa-solid fa-plus\"></i></button>\n <span class=\"zoom-sep\"></span>\n <button type=\"button\" title=\"Fit to content\" (click)=\"Fit.emit()\"><i class=\"fa-solid fa-compress\"></i></button>\n <button type=\"button\" title=\"Mini-map\" [class.on]=\"MinimapOpen\" (click)=\"ToggleMinimap.emit()\"><i class=\"fa-regular fa-map\"></i></button>\n</div>\n", styles: ["/* Bottom-right zoom cluster \u2014 ported pixel-faithfully from\n plans/realtime/mockups/whiteboard.html (.zoom-cluster). */\n\n:host {\n display: block;\n position: absolute;\n right: 14px;\n bottom: 14px;\n z-index: 10;\n}\n\n.zoom-cluster {\n display: flex;\n align-items: center;\n gap: 2px;\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n box-shadow: var(--mj-shadow-xl, 0 20px 25px -5px rgba(0, 0, 0, 0.3));\n padding: 4px;\n}\n\n.zoom-cluster button {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 0;\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n font-size: 12px;\n display: grid;\n place-items: center;\n font-family: inherit;\n}\n.zoom-cluster button:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.zoom-cluster button.on {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n\n.zoom-pct {\n font-size: 11.5px;\n font-weight: 700;\n color: var(--mj-text-secondary);\n padding: 0 7px;\n font-family: var(--mj-font-mono, ui-monospace, monospace);\n}\n\n.zoom-sep {\n width: 1px;\n height: 18px;\n background: var(--mj-border-default);\n margin: 0 3px;\n}\n"] }]
123
+ }], null, { ZoomPercent: [{
124
+ type: Input
125
+ }], MinimapOpen: [{
126
+ type: Input
127
+ }], ZoomOut: [{
128
+ type: Output
129
+ }], ZoomIn: [{
130
+ type: Output
131
+ }], ZoomBy: [{
132
+ type: Output
133
+ }], Fit: [{
134
+ type: Output
135
+ }], ToggleMinimap: [{
136
+ type: Output
137
+ }] }); })();
138
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RealtimeWhiteboardZoomComponent, { className: "RealtimeWhiteboardZoomComponent", filePath: "src/lib/whiteboard-zoom.component.ts", lineNumber: 25 }); })();
139
+ //# sourceMappingURL=whiteboard-zoom.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-zoom.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.ts","../../src/lib/whiteboard-zoom.component.html"],"names":[],"mappings":"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","sourcesContent":["import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n/** Which way a zoom button points. */\ntype ZoomDirection = 1 | -1;\n\n/**\n * The bottom-right zoom cluster of the live whiteboard: zoom out / percentage / zoom in,\n * fit-to-content, and the minimap toggle. Pure presentational; the board owns the\n * viewport math and the minimap rendering.\n *\n * HOLD-TO-ZOOM: a plain click on +/− still emits the existing single preset step\n * ({@link ZoomIn} / {@link ZoomOut}); holding the button down (pointerdown held past a\n * short delay) emits {@link ZoomBy} factors (~3.5%) every 50 ms for smooth continuous\n * zooming, stopping on pointerup / pointerleave / pointercancel / destroy — the timers\n * never leak. The board clamps to its usual 25%–200% limits.\n */\n@Component({\n standalone: true,\n selector: 'mj-realtime-whiteboard-zoom',\n imports: [CommonModule],\n templateUrl: './whiteboard-zoom.component.html',\n styleUrl: './whiteboard-zoom.component.css'\n})\nexport class RealtimeWhiteboardZoomComponent implements OnDestroy {\n /** How long a press must be held before continuous zooming kicks in. */\n private static readonly HoldDelayMs = 350;\n /** Continuous-zoom tick cadence while held. */\n private static readonly HoldIntervalMs = 50;\n /** Multiplicative zoom factor per held tick (~3.5%). */\n private static readonly HoldFactor = 1.035;\n\n /** Current zoom as a percentage (e.g. 90). */\n @Input() ZoomPercent = 100;\n /** Whether the minimap is currently shown (highlights the map button). */\n @Input() MinimapOpen = false;\n\n /** Single preset step out (a plain click on −). */\n @Output() ZoomOut = new EventEmitter<void>();\n /** Single preset step in (a plain click on +). */\n @Output() ZoomIn = new EventEmitter<void>();\n /**\n * Continuous (held-button) zoom tick: a multiplicative factor (>1 zooms in, <1 zooms\n * out) emitted every 50 ms while + or − is held. Wire to the board's `ZoomByFactor`.\n */\n @Output() ZoomBy = new EventEmitter<number>();\n @Output() Fit = new EventEmitter<void>();\n @Output() ToggleMinimap = new EventEmitter<void>();\n\n private holdDelayTimer: ReturnType<typeof setTimeout> | null = null;\n private holdIntervalTimer: ReturnType<typeof setInterval> | null = null;\n /** Set once continuous ticks started, so the trailing click doesn't ALSO step. */\n private suppressNextClick = false;\n\n ngOnDestroy(): void {\n this.stopHold();\n }\n\n /** Pointer down on +/−: arm the hold — after the delay, tick {@link ZoomBy} repeatedly. */\n public OnZoomPressStart(event: PointerEvent, direction: ZoomDirection): void {\n if (event.button !== 0) {\n return;\n }\n this.stopHold();\n this.suppressNextClick = false;\n this.holdDelayTimer = setTimeout(() => {\n this.holdDelayTimer = null;\n this.suppressNextClick = true;\n const factor = direction === 1\n ? RealtimeWhiteboardZoomComponent.HoldFactor\n : 1 / RealtimeWhiteboardZoomComponent.HoldFactor;\n this.ZoomBy.emit(factor); // first tick fires immediately when the hold engages\n this.holdIntervalTimer = setInterval(() => this.ZoomBy.emit(factor), RealtimeWhiteboardZoomComponent.HoldIntervalMs);\n }, RealtimeWhiteboardZoomComponent.HoldDelayMs);\n }\n\n /** Pointer up / leave / cancel: stop any pending or running continuous zoom. */\n public OnZoomPressEnd(): void {\n this.stopHold();\n }\n\n /** The click that follows the press: a single preset step unless a hold already zoomed. */\n public OnZoomClick(direction: ZoomDirection): void {\n if (this.suppressNextClick) {\n this.suppressNextClick = false;\n return;\n }\n if (direction === 1) {\n this.ZoomIn.emit();\n }\n else {\n this.ZoomOut.emit();\n }\n }\n\n /** Clear both hold timers (idempotent — safe from any stop path). */\n private stopHold(): void {\n if (this.holdDelayTimer !== null) {\n clearTimeout(this.holdDelayTimer);\n this.holdDelayTimer = null;\n }\n if (this.holdIntervalTimer !== null) {\n clearInterval(this.holdIntervalTimer);\n this.holdIntervalTimer = null;\n }\n }\n}\n","<div class=\"zoom-cluster\">\n <button type=\"button\" title=\"Zoom out (hold for continuous)\"\n (pointerdown)=\"OnZoomPressStart($event, -1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(-1)\"><i class=\"fa-solid fa-minus\"></i></button>\n <span class=\"zoom-pct\">{{ ZoomPercent }}%</span>\n <button type=\"button\" title=\"Zoom in (hold for continuous)\"\n (pointerdown)=\"OnZoomPressStart($event, 1)\"\n (pointerup)=\"OnZoomPressEnd()\"\n (pointerleave)=\"OnZoomPressEnd()\"\n (pointercancel)=\"OnZoomPressEnd()\"\n (click)=\"OnZoomClick(1)\"><i class=\"fa-solid fa-plus\"></i></button>\n <span class=\"zoom-sep\"></span>\n <button type=\"button\" title=\"Fit to content\" (click)=\"Fit.emit()\"><i class=\"fa-solid fa-compress\"></i></button>\n <button type=\"button\" title=\"Mini-map\" [class.on]=\"MinimapOpen\" (click)=\"ToggleMinimap.emit()\"><i class=\"fa-regular fa-map\"></i></button>\n</div>\n"]}
@@ -0,0 +1,29 @@
1
+ import * as i0 from "@angular/core";
2
+ import * as i1 from "./whiteboard-board.component";
3
+ import * as i2 from "./whiteboard-toolbar.component";
4
+ import * as i3 from "./whiteboard-zoom.component";
5
+ import * as i4 from "./whiteboard-pages.component";
6
+ import * as i5 from "./whiteboard-agent-sees-popover.component";
7
+ import * as i6 from "./whiteboard-host.component";
8
+ import * as i7 from "./whiteboard-snapshot.component";
9
+ import * as i8 from "./whiteboard-srcdoc.pipe";
10
+ /**
11
+ * Convenience NgModule that re-exports every standalone whiteboard component (and the
12
+ * `wbWidgetSrcdoc` pipe), for consumers organized around NgModules. All components in
13
+ * this package are STANDALONE — apps using standalone bootstrapping can import the
14
+ * component classes directly and skip this module entirely.
15
+ */
16
+ export declare class WhiteboardModule {
17
+ static ɵfac: i0.ɵɵFactoryDeclaration<WhiteboardModule, never>;
18
+ static ɵmod: i0.ɵɵNgModuleDeclaration<WhiteboardModule, never, [typeof i1.RealtimeWhiteboardBoardComponent, typeof i2.RealtimeWhiteboardToolbarComponent, typeof i3.RealtimeWhiteboardZoomComponent, typeof i4.RealtimeWhiteboardPagesComponent, typeof i5.RealtimeWhiteboardAgentSeesPopoverComponent, typeof i6.RealtimeWhiteboardHostComponent, typeof i7.WhiteboardSnapshotComponent, typeof i8.WhiteboardWidgetSrcdocPipe], [typeof i1.RealtimeWhiteboardBoardComponent, typeof i2.RealtimeWhiteboardToolbarComponent, typeof i3.RealtimeWhiteboardZoomComponent, typeof i4.RealtimeWhiteboardPagesComponent, typeof i5.RealtimeWhiteboardAgentSeesPopoverComponent, typeof i6.RealtimeWhiteboardHostComponent, typeof i7.WhiteboardSnapshotComponent, typeof i8.WhiteboardWidgetSrcdocPipe]>;
19
+ static ɵinj: i0.ɵɵInjectorDeclaration<WhiteboardModule>;
20
+ }
21
+ /**
22
+ * Tree-shaking prevention hook. The whiteboard components are usually referenced
23
+ * statically (standalone `imports` arrays), which is enough to retain them — call this
24
+ * from your bootstrap path only when every usage in your app is resolved DYNAMICALLY
25
+ * (e.g. a plugin returns a component type through the MJ ClassFactory) and the bundler
26
+ * would otherwise see no static reference to this package.
27
+ */
28
+ export declare function LoadWhiteboardComponents(): void;
29
+ //# sourceMappingURL=whiteboard.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard.module.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard.module.ts"],"names":[],"mappings":";;;;;;;;;AAUA;;;;;GAKG;AACH,qBAsBa,gBAAgB;yCAAhB,gBAAgB;0CAAhB,gBAAgB;0CAAhB,gBAAgB;CAAG;AAEhC;;;;;;GAMG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}