@memberjunction/ng-whiteboard 5.51.2 → 5.51.3

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 (32) hide show
  1. package/README.md +5 -0
  2. package/dist/lib/whiteboard-board.component.d.ts +11 -3
  3. package/dist/lib/whiteboard-board.component.d.ts.map +1 -1
  4. package/dist/lib/whiteboard-board.component.js +12 -3
  5. package/dist/lib/whiteboard-board.component.js.map +1 -1
  6. package/dist/lib/whiteboard-context-menu.d.ts +2 -1
  7. package/dist/lib/whiteboard-context-menu.d.ts.map +1 -1
  8. package/dist/lib/whiteboard-context-menu.js +11 -7
  9. package/dist/lib/whiteboard-context-menu.js.map +1 -1
  10. package/dist/lib/whiteboard-host.component.d.ts +72 -3
  11. package/dist/lib/whiteboard-host.component.d.ts.map +1 -1
  12. package/dist/lib/whiteboard-host.component.dom.test.d.ts +2 -0
  13. package/dist/lib/whiteboard-host.component.dom.test.d.ts.map +1 -0
  14. package/dist/lib/whiteboard-host.component.dom.test.js +187 -0
  15. package/dist/lib/whiteboard-host.component.dom.test.js.map +1 -0
  16. package/dist/lib/whiteboard-host.component.js +132 -12
  17. package/dist/lib/whiteboard-host.component.js.map +1 -1
  18. package/dist/lib/whiteboard-tool-roster.d.ts +55 -0
  19. package/dist/lib/whiteboard-tool-roster.d.ts.map +1 -0
  20. package/dist/lib/whiteboard-tool-roster.js +58 -0
  21. package/dist/lib/whiteboard-tool-roster.js.map +1 -0
  22. package/dist/lib/whiteboard-toolbar.component.d.ts +11 -4
  23. package/dist/lib/whiteboard-toolbar.component.d.ts.map +1 -1
  24. package/dist/lib/whiteboard-toolbar.component.dom.test.js +13 -0
  25. package/dist/lib/whiteboard-toolbar.component.dom.test.js.map +1 -1
  26. package/dist/lib/whiteboard-toolbar.component.js +18 -3
  27. package/dist/lib/whiteboard-toolbar.component.js.map +1 -1
  28. package/dist/public-api.d.ts +1 -0
  29. package/dist/public-api.d.ts.map +1 -1
  30. package/dist/public-api.js +2 -0
  31. package/dist/public-api.js.map +1 -1
  32. package/package.json +6 -6
@@ -1,6 +1,7 @@
1
- import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, Output, ViewChild, inject } from '@angular/core';
1
+ import { ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, HostListener, Input, Output, ViewChild, inject } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import { WhiteboardState } from './whiteboard-state';
4
+ import { ClampToolToRoster, IsToolAllowed } from './whiteboard-tool-roster';
4
5
  import { BuildWhiteboardExportHtml, BuildWhiteboardExportHtmlAllPages, BuildWhiteboardExportSvg, BuildWhiteboardExportSvgPages } from './whiteboard-export';
5
6
  import { ApplyWhiteboardAgentTool, WHITEBOARD_TOOL_NAMES } from './whiteboard-tools';
6
7
  import { RealtimeWhiteboardBoardComponent } from './whiteboard-board.component';
@@ -115,6 +116,24 @@ export class RealtimeWhiteboardHostComponent {
115
116
  this.BoardTitle = 'Whiteboard';
116
117
  /** Persistence chip text (e.g. "Saved to session · v14"). */
117
118
  this.SavedLabel = 'Saved to session';
119
+ /**
120
+ * Opt back in to DOCUMENT-WIDE keyboard shortcuts.
121
+ *
122
+ * The host binds eleven bare single-character tool shortcuts (v h p r s t m w i c e). While
123
+ * those listened on `document` unconditionally they fired wherever the user was on the page,
124
+ * which fails WCAG 2.1.4 (Character Key Shortcuts): a speech-input user saying an ordinary
125
+ * word, or anyone typing in a non-input control elsewhere, silently switched the board tool.
126
+ * They are now scoped to focus being inside this host, which satisfies the criterion's
127
+ * "active only on focus" branch.
128
+ *
129
+ * Scoping covers the host's ENTIRE keydown handler, not only the letters: undo/redo
130
+ * (Cmd/Ctrl+Z, +Y), Escape and Delete/Backspace are focus-gated as well.
131
+ *
132
+ * Set `true` only for a surface where the whiteboard is the entire page AND the 2.1.4
133
+ * exposure has been accepted — it is an accessibility regression, not a convenience flag.
134
+ */
135
+ this.EnableGlobalShortcuts = false;
136
+ this._toolRoster = null;
118
137
  /** Debounced (750 ms), coalesced scene-delta JSON — the live perception feed. */
119
138
  this.SceneDelta = new EventEmitter();
120
139
  /** The user clicked Undo on the agent-action toast (the undo itself already applied). */
@@ -167,8 +186,7 @@ export class RealtimeWhiteboardHostComponent {
167
186
  this.ItemRemoving = new EventEmitter();
168
187
  /** AFTER mirror of {@link WhiteboardState.ItemRemoved$}. */
169
188
  this.ItemRemoved = new EventEmitter();
170
- // tool state (host-owned; toolbar + keyboard drive it, board consumes it)
171
- this.Tool = 'select';
189
+ this._tool = 'select';
172
190
  this.PenColor = WHITEBOARD_PEN_COLORS[0];
173
191
  this.PenWidth = 4;
174
192
  this.ShapeKind = 'rect';
@@ -196,6 +214,68 @@ export class RealtimeWhiteboardHostComponent {
196
214
  this.ticker = null;
197
215
  this.mirrorSubs = [];
198
216
  this.cdr = inject(ChangeDetectorRef);
217
+ // ────────────────────────────────────────────── keyboard shortcuts
218
+ /**
219
+ * The board is made focusable so its shortcuts have a focus scope to be bound to. `-1`, not
220
+ * `0`: the canvas has no keyboard interaction model yet (see the whiteboard keyboard-
221
+ * operability work), so adding a real Tab stop would put a stop in the page that a keyboard
222
+ * user can reach and do nothing with. Click focuses it; that is the whole contract today.
223
+ */
224
+ this.HostTabIndex = '-1';
225
+ this.host = inject(ElementRef);
226
+ }
227
+ /**
228
+ * Which tools this surface offers. `null` (default) is all eleven: today's rendering.
229
+ *
230
+ * The roster governs which tools are AVAILABLE, closing every door to a tool it leaves out:
231
+ * the toolbar button, the single-letter shortcut, and the canvas "add … here" action. It is
232
+ * deliberately not a content policy — it does not restrict what already exists on the board,
233
+ * what the agent places, or authoring on existing items (Restyle, Duplicate, paste). An empty
234
+ * roster narrows the palette to nothing; it does NOT make the board read-only, which is a
235
+ * separate input.
236
+ *
237
+ * A setter rather than `ngOnChanges` (packages/Angular/CLAUDE.md): a realtime channel's
238
+ * `BindSurface` assigns this property directly on a dynamically-created component, and
239
+ * Angular fires no `ngOnChanges` for a plain assignment, so a clamp implemented there would
240
+ * silently never run on the path real consumers use.
241
+ */
242
+ set ToolRoster(value) {
243
+ const next = value ?? null;
244
+ // Identity compare, per the Angular rule: a consumer binding a freshly-built array literal
245
+ // re-fires this setter every change-detection pass, and re-running the clamp on every pass
246
+ // would fight the user for the active tool.
247
+ if (next === this._toolRoster) {
248
+ return;
249
+ }
250
+ this._toolRoster = next;
251
+ // Re-validate the CURRENT tool through the same chokepoint every other write uses.
252
+ this.Tool = this._tool;
253
+ }
254
+ get ToolRoster() {
255
+ return this._toolRoster;
256
+ }
257
+ // tool state (host-owned; toolbar + keyboard drive it, board consumes it)
258
+ /**
259
+ * The active tool, and the ONE place the roster invariant is enforced on a write.
260
+ *
261
+ * "The active tool is always one the roster allows" is a property of every write, not of the
262
+ * handful of call sites that exist today (the roster setter, Escape, the key map, and the two
263
+ * template `Tool = $event` bindings). Guarding each of those would leave the next writer —
264
+ * a new shortcut, a gesture, a host API — to remember the rule. Guarding here closes them all
265
+ * by construction, and makes assignment order irrelevant: setting `ToolRoster` and `Tool` in
266
+ * either order still lands on an allowed tool.
267
+ *
268
+ * A DISALLOWED request is ignored rather than clamped: pressing Escape under `['pen','eraser']`
269
+ * should leave you on pen, not bounce you to the roster's fallback. The clamp is only for the
270
+ * case where the tool you are already holding stopped being allowed.
271
+ */
272
+ get Tool() {
273
+ return this._tool;
274
+ }
275
+ set Tool(value) {
276
+ this._tool = IsToolAllowed(this._toolRoster, value)
277
+ ? value
278
+ : ClampToolToRoster(this._tool, this._toolRoster);
199
279
  }
200
280
  ngOnInit() {
201
281
  this.changedSub = this.State.Changed$.subscribe((change) => {
@@ -370,8 +450,34 @@ export class RealtimeWhiteboardHostComponent {
370
450
  this.hideToast();
371
451
  this.AgentUndo.emit();
372
452
  }
373
- // ────────────────────────────────────────────── keyboard shortcuts
453
+ /**
454
+ * Clicking the board takes focus, so the shortcuts below apply. The browser would usually do
455
+ * this on its own by focusing the nearest focusable ancestor, but the board's pointer
456
+ * pipeline is elaborate enough (drag, marquee, handles, transient shapes) that relying on
457
+ * that default is fragile — one `preventDefault()` added later would silently kill every
458
+ * shortcut. This makes it explicit.
459
+ */
460
+ OnHostPointerDown() {
461
+ if (!this.hasFocusWithin()) {
462
+ this.host.nativeElement.focus({ preventScroll: true });
463
+ }
464
+ }
465
+ /** Whether focus currently sits on, or inside, this whiteboard host. */
466
+ hasFocusWithin() {
467
+ const el = this.host.nativeElement;
468
+ const active = document.activeElement;
469
+ return !!active && (active === el || el.contains(active));
470
+ }
374
471
  OnKeydown(event) {
472
+ // WCAG 2.1.4: only live while focus is on the board. NOTE this gates the WHOLE handler,
473
+ // not just the single-character tool keys that 2.1.4 is about — undo/redo (Cmd/Ctrl+Z,
474
+ // +Y), Escape and Delete/Backspace are scoped too. That is deliberate: a board that
475
+ // swallows the document's Cmd+Z from anywhere on the page is its own bug, and "the board
476
+ // responds to keys when you are on the board" is the only model that stays predictable.
477
+ // See {@link EnableGlobalShortcuts} for the (discouraged) opt-out.
478
+ if (!this.EnableGlobalShortcuts && !this.hasFocusWithin()) {
479
+ return;
480
+ }
375
481
  const target = event.target;
376
482
  if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
377
483
  return;
@@ -410,6 +516,8 @@ export class RealtimeWhiteboardHostComponent {
410
516
  break;
411
517
  default: {
412
518
  const tool = RealtimeWhiteboardHostComponent.toolForKey(event.key);
519
+ // No roster check here: the Tool setter ignores a disallowed write, so a key for a
520
+ // hidden tool is a no-op by construction rather than by a remembered guard.
413
521
  if (tool) {
414
522
  this.Tool = tool;
415
523
  }
@@ -546,9 +654,11 @@ export class RealtimeWhiteboardHostComponent {
546
654
  } if (rf & 2) {
547
655
  let _t;
548
656
  i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.Board = _t.first);
549
- } }, hostBindings: function RealtimeWhiteboardHostComponent_HostBindings(rf, ctx) { if (rf & 1) {
550
- i0.ɵɵlistener("click", function RealtimeWhiteboardHostComponent_click_HostBindingHandler() { return ctx.CloseExport(); }, i0.ɵɵresolveDocument)("keydown", function RealtimeWhiteboardHostComponent_keydown_HostBindingHandler($event) { return ctx.OnKeydown($event); }, i0.ɵɵresolveDocument);
551
- } }, inputs: { State: "State", AgentName: "AgentName", BoardTitle: "BoardTitle", SavedLabel: "SavedLabel" }, outputs: { SceneDelta: "SceneDelta", AgentUndo: "AgentUndo", FocusModeChange: "FocusModeChange", SaveToArtifactsRequested: "SaveToArtifactsRequested", WidgetSubmitting: "WidgetSubmitting", WidgetSubmitted: "WidgetSubmitted", WidgetInteraction: "WidgetInteraction", ContentApplying: "ContentApplying", ContentApplied: "ContentApplied", ItemAdding: "ItemAdding", ItemAdded: "ItemAdded", ItemUpdating: "ItemUpdating", ItemUpdated: "ItemUpdated", ItemRemoving: "ItemRemoving", ItemRemoved: "ItemRemoved" }, decls: 70, vars: 48, consts: [["board", ""], [1, "board-head"], [1, "bt"], [1, "fa-solid", "fa-chalkboard"], [1, "board-save"], [1, "fa-solid", "fa-circle-check"], [1, "board-legend"], [1, "sw", "sw--you"], [1, "sw", "sw--sage"], [1, "fa-solid", "fa-feather", "legend-feather"], [1, "head-end"], ["type", "button", 1, "head-btn", "head-btn--sees", 3, "click"], [1, "eye-pulse"], [1, "fa-regular", "fa-eye"], [3, "AgentName", "DeltaJson", "DebounceMs", "ElementCount", "LastDeltaLabel"], [1, "export-wrap"], ["type", "button", 1, "head-btn", 3, "click"], [1, "fa-solid", "fa-file-export"], [1, "export-menu"], ["type", "button", "title", "More", 1, "head-btn", "head-btn--more"], [1, "fa-solid", "fa-ellipsis"], [1, "board-wrap"], [3, "ToolChangeRequest", "WidgetSubmitting", "WidgetSubmitted", "WidgetInteraction", "ContentApplying", "ContentApplied", "State", "AgentName", "Tool", "PenColor", "PenWidth", "ShapeKind", "TextSize", "TextFamily", "TextBold", "TextColor", "AgentPresence"], [3, "ToolChange", "PenColorChange", "PenWidthChange", "ShapeKindChange", "TextSizeChange", "TextFamilyChange", "TextBoldChange", "TextColorChange", "StyleSelection", "Undo", "Redo", "ActiveTool", "PenColor", "PenWidth", "ShapeKind", "TextSize", "TextFamily", "TextBold", "TextColor", "CanUndo", "CanRedo", "AgentName"], [3, "ZoomOut", "ZoomIn", "ZoomBy", "Fit", "ToggleMinimap", "ZoomPercent", "MinimapOpen"], [1, "board-toast"], [1, "board-status"], [1, "stat"], [1, "seg-r"], [1, "stat", "ctx-live", 3, "title"], [1, "d"], [1, "ctx-live__text"], ["title", "Persists as a session artifact when the call ends", 1, "stat", "persist"], ["aria-hidden", "true", 1, "fa-solid", "fa-box-archive"], [1, "persist__long"], [1, "persist__short"], ["type", "button", 1, "export-item", 3, "click"], [1, "fa-solid", "fa-file-code"], ["type", "button", 1, "export-item"], [1, "fa-solid", "fa-bezier-curve"], ["type", "button", "title", "One SVG file per page", 1, "export-item"], [1, "fa-solid", "fa-print"], [1, "fa-solid", "fa-box-archive"], ["type", "button", "title", "One SVG file per page", 1, "export-item", 3, "click"], [1, "fa-solid", "fa-feather"], [1, "board-toast__text"], [1, "undo", 3, "click"], [1, "fa-solid", "fa-rotate-left"]], template: function RealtimeWhiteboardHostComponent_Template(rf, ctx) { if (rf & 1) {
657
+ } }, hostVars: 1, hostBindings: function RealtimeWhiteboardHostComponent_HostBindings(rf, ctx) { if (rf & 1) {
658
+ i0.ɵɵlistener("click", function RealtimeWhiteboardHostComponent_click_HostBindingHandler() { return ctx.CloseExport(); }, i0.ɵɵresolveDocument)("pointerdown", function RealtimeWhiteboardHostComponent_pointerdown_HostBindingHandler() { return ctx.OnHostPointerDown(); })("keydown", function RealtimeWhiteboardHostComponent_keydown_HostBindingHandler($event) { return ctx.OnKeydown($event); }, i0.ɵɵresolveDocument);
659
+ } if (rf & 2) {
660
+ i0.ɵɵattribute("tabindex", ctx.HostTabIndex);
661
+ } }, inputs: { State: "State", AgentName: "AgentName", BoardTitle: "BoardTitle", SavedLabel: "SavedLabel", EnableGlobalShortcuts: "EnableGlobalShortcuts", ToolRoster: "ToolRoster" }, outputs: { SceneDelta: "SceneDelta", AgentUndo: "AgentUndo", FocusModeChange: "FocusModeChange", SaveToArtifactsRequested: "SaveToArtifactsRequested", WidgetSubmitting: "WidgetSubmitting", WidgetSubmitted: "WidgetSubmitted", WidgetInteraction: "WidgetInteraction", ContentApplying: "ContentApplying", ContentApplied: "ContentApplied", ItemAdding: "ItemAdding", ItemAdded: "ItemAdded", ItemUpdating: "ItemUpdating", ItemUpdated: "ItemUpdated", ItemRemoving: "ItemRemoving", ItemRemoved: "ItemRemoved" }, decls: 70, vars: 50, consts: [["board", ""], [1, "board-head"], [1, "bt"], [1, "fa-solid", "fa-chalkboard"], [1, "board-save"], [1, "fa-solid", "fa-circle-check"], [1, "board-legend"], [1, "sw", "sw--you"], [1, "sw", "sw--sage"], [1, "fa-solid", "fa-feather", "legend-feather"], [1, "head-end"], ["type", "button", 1, "head-btn", "head-btn--sees", 3, "click"], [1, "eye-pulse"], [1, "fa-regular", "fa-eye"], [3, "AgentName", "DeltaJson", "DebounceMs", "ElementCount", "LastDeltaLabel"], [1, "export-wrap"], ["type", "button", 1, "head-btn", 3, "click"], [1, "fa-solid", "fa-file-export"], [1, "export-menu"], ["type", "button", "title", "More", 1, "head-btn", "head-btn--more"], [1, "fa-solid", "fa-ellipsis"], [1, "board-wrap"], [3, "ToolChangeRequest", "WidgetSubmitting", "WidgetSubmitted", "WidgetInteraction", "ContentApplying", "ContentApplied", "State", "AgentName", "Tool", "ToolRoster", "PenColor", "PenWidth", "ShapeKind", "TextSize", "TextFamily", "TextBold", "TextColor", "AgentPresence"], [3, "ToolChange", "PenColorChange", "PenWidthChange", "ShapeKindChange", "TextSizeChange", "TextFamilyChange", "TextBoldChange", "TextColorChange", "StyleSelection", "Undo", "Redo", "ActiveTool", "ToolRoster", "PenColor", "PenWidth", "ShapeKind", "TextSize", "TextFamily", "TextBold", "TextColor", "CanUndo", "CanRedo", "AgentName"], [3, "ZoomOut", "ZoomIn", "ZoomBy", "Fit", "ToggleMinimap", "ZoomPercent", "MinimapOpen"], [1, "board-toast"], [1, "board-status"], [1, "stat"], [1, "seg-r"], [1, "stat", "ctx-live", 3, "title"], [1, "d"], [1, "ctx-live__text"], ["title", "Persists as a session artifact when the call ends", 1, "stat", "persist"], ["aria-hidden", "true", 1, "fa-solid", "fa-box-archive"], [1, "persist__long"], [1, "persist__short"], ["type", "button", 1, "export-item", 3, "click"], [1, "fa-solid", "fa-file-code"], ["type", "button", 1, "export-item"], [1, "fa-solid", "fa-bezier-curve"], ["type", "button", "title", "One SVG file per page", 1, "export-item"], [1, "fa-solid", "fa-print"], [1, "fa-solid", "fa-box-archive"], ["type", "button", "title", "One SVG file per page", 1, "export-item", 3, "click"], [1, "fa-solid", "fa-feather"], [1, "board-toast__text"], [1, "undo", 3, "click"], [1, "fa-solid", "fa-rotate-left"]], template: function RealtimeWhiteboardHostComponent_Template(rf, ctx) { if (rf & 1) {
552
662
  const _r1 = i0.ɵɵgetCurrentView();
553
663
  i0.ɵɵelementStart(0, "div", 1)(1, "span", 2);
554
664
  i0.ɵɵelement(2, "i", 3);
@@ -664,9 +774,9 @@ export class RealtimeWhiteboardHostComponent {
664
774
  i0.ɵɵadvance();
665
775
  i0.ɵɵtextInterpolate1(" ", ctx.FocusMode ? "Show thread" : "Focus board", " ");
666
776
  i0.ɵɵadvance(4);
667
- i0.ɵɵproperty("State", ctx.State)("AgentName", ctx.AgentName)("Tool", ctx.Tool)("PenColor", ctx.PenColor)("PenWidth", ctx.PenWidth)("ShapeKind", ctx.ShapeKind)("TextSize", ctx.TextSize)("TextFamily", ctx.TextFamily)("TextBold", ctx.TextBold)("TextColor", ctx.TextColor)("AgentPresence", ctx.AgentPresence);
777
+ i0.ɵɵproperty("State", ctx.State)("AgentName", ctx.AgentName)("Tool", ctx.Tool)("ToolRoster", ctx.ToolRoster)("PenColor", ctx.PenColor)("PenWidth", ctx.PenWidth)("ShapeKind", ctx.ShapeKind)("TextSize", ctx.TextSize)("TextFamily", ctx.TextFamily)("TextBold", ctx.TextBold)("TextColor", ctx.TextColor)("AgentPresence", ctx.AgentPresence);
668
778
  i0.ɵɵadvance(2);
669
- i0.ɵɵproperty("ActiveTool", ctx.Tool)("PenColor", ctx.PenColor)("PenWidth", ctx.PenWidth)("ShapeKind", ctx.ShapeKind)("TextSize", ctx.TextSize)("TextFamily", ctx.TextFamily)("TextBold", ctx.TextBold)("TextColor", ctx.TextColor)("CanUndo", ctx.State.CanUndo)("CanRedo", ctx.State.CanRedo)("AgentName", ctx.AgentName);
779
+ i0.ɵɵproperty("ActiveTool", ctx.Tool)("ToolRoster", ctx.ToolRoster)("PenColor", ctx.PenColor)("PenWidth", ctx.PenWidth)("ShapeKind", ctx.ShapeKind)("TextSize", ctx.TextSize)("TextFamily", ctx.TextFamily)("TextBold", ctx.TextBold)("TextColor", ctx.TextColor)("CanUndo", ctx.State.CanUndo)("CanRedo", ctx.State.CanRedo)("AgentName", ctx.AgentName);
670
780
  i0.ɵɵadvance();
671
781
  i0.ɵɵproperty("ZoomPercent", board_r6.ZoomPercent)("MinimapOpen", board_r6.MinimapOpen);
672
782
  i0.ɵɵadvance();
@@ -691,7 +801,7 @@ export class RealtimeWhiteboardHostComponent {
691
801
  RealtimeWhiteboardBoardComponent,
692
802
  RealtimeWhiteboardToolbarComponent,
693
803
  RealtimeWhiteboardZoomComponent,
694
- RealtimeWhiteboardAgentSeesPopoverComponent], styles: ["\n\n\n\n\n[_nghost-%COMP%] {\n \n\n --sage: var(--mj-color-violet-400, #a78bfa);\n --sage-bg: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 14%, transparent);\n --sage-border: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 45%, transparent);\n --sage-chip-bg: color-mix(in srgb, var(--mj-color-violet-500, #8b5cf6) 32%, var(--mj-bg-surface-sunken));\n\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n height: 100%;\n \n\n container-type: inline-size;\n container-name: wb-host;\n}\n\n\n\n.board-head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n flex-wrap: wrap;\n}\n.board-head[_ngcontent-%COMP%] .bt[_ngcontent-%COMP%] {\n font-weight: 700;\n font-size: 13.5px;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n}\n.board-head[_ngcontent-%COMP%] .bt[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n font-size: 13px;\n}\n.board-save[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-muted);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n.board-save[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n font-size: 10px;\n}\n\n.board-legend[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n color: var(--mj-text-muted);\n margin-left: 6px;\n padding: 4px 10px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--mj-bg-surface-card);\n}\n.board-legend[_ngcontent-%COMP%] .sw[_ngcontent-%COMP%] {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n display: inline-block;\n vertical-align: -1px;\n margin-right: 5px;\n}\n.board-legend[_ngcontent-%COMP%] .sw--you[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n}\n.board-legend[_ngcontent-%COMP%] .sw--sage[_ngcontent-%COMP%] {\n background: var(--sage-bg);\n border: 1px solid var(--sage);\n}\n.legend-feather[_ngcontent-%COMP%] {\n font-size: 9px;\n color: var(--sage);\n}\n\n.head-end[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n position: relative;\n}\n\n.head-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n padding: 5px 11px;\n font-size: 11.5px;\n font-weight: 600;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n cursor: pointer;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n transition: all 140ms ease;\n font-family: inherit;\n}\n.head-btn[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n border-color: var(--mj-border-strong);\n}\n.head-btn.on[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n.head-btn--sees[_ngcontent-%COMP%] {\n position: relative;\n}\n.head-btn--sees[_ngcontent-%COMP%] .eye-pulse[_ngcontent-%COMP%] {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent);\n animation: _ngcontent-%COMP%_wb-pulse-ring 2s infinite;\n}\n.head-btn--more[_ngcontent-%COMP%] {\n padding: 5px 9px;\n}\n\n\n\n.export-wrap[_ngcontent-%COMP%] {\n position: relative;\n display: inline-flex;\n}\n.export-menu[_ngcontent-%COMP%] {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 30;\n min-width: 168px;\n display: flex;\n flex-direction: column;\n padding: 5px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-elevated);\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n}\n.export-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 11.5px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n border: none;\n border-radius: var(--mj-radius-sm, 5px);\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n white-space: nowrap;\n}\n.export-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.export-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n width: 14px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n.export-item[_ngcontent-%COMP%]:hover i[_ngcontent-%COMP%] {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n@keyframes _ngcontent-%COMP%_wb-pulse-ring {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent); }\n 70% { box-shadow: 0 0 0 6px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n\n\n.board-wrap[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n\n\n\n\n\n\n.board-toast[_ngcontent-%COMP%] {\n position: absolute;\n left: 50%;\n bottom: 64px;\n transform: translateX(-50%);\n z-index: 9;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 8px 16px;\n max-width: calc(100% - 32px);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--sage-chip-bg);\n border: 1px solid var(--sage-border);\n color: var(--mj-color-violet-300, #c4b5fd);\n font-size: 12px;\n font-weight: 600;\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n animation: _ngcontent-%COMP%_wb-toast-fade 300ms ease both;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast[_ngcontent-%COMP%] > i[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n.board-toast__text[_ngcontent-%COMP%] {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast[_ngcontent-%COMP%] .undo[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n@keyframes _ngcontent-%COMP%_wb-toast-fade {\n from { opacity: 0; transform: translate(-50%, 4px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n.board-toast[_ngcontent-%COMP%] .undo[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-weight: 600;\n cursor: pointer;\n margin-left: 4px;\n}\n.board-toast[_ngcontent-%COMP%] .undo[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n}\n\n\n\n.board-status[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 18px;\n padding: 6px 16px;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n font-size: 10.5px;\n color: var(--mj-text-muted);\n font-weight: 500;\n}\n.board-status[_ngcontent-%COMP%] b[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n font-weight: 600;\n}\n.board-status[_ngcontent-%COMP%] .seg-r[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n}\n.board-status[_ngcontent-%COMP%] .ctx-live[_ngcontent-%COMP%] {\n color: var(--mj-color-violet-300, var(--mj-text-secondary));\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-weight: 600;\n}\n.board-status[_ngcontent-%COMP%] .ctx-live[_ngcontent-%COMP%] .d[_ngcontent-%COMP%] {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n animation: _ngcontent-%COMP%_wb-pulse-ring 2s infinite;\n}"] }); }
804
+ RealtimeWhiteboardAgentSeesPopoverComponent], styles: ["\n\n\n\n[_nghost-%COMP%]:focus:not(:focus-visible) {\n outline: none;\n}\n\n\n\n\n\n\n[_nghost-%COMP%] {\n \n\n --sage: var(--mj-color-violet-400, #a78bfa);\n --sage-bg: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 14%, transparent);\n --sage-border: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 45%, transparent);\n --sage-chip-bg: color-mix(in srgb, var(--mj-color-violet-500, #8b5cf6) 32%, var(--mj-bg-surface-sunken));\n\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n height: 100%;\n \n\n container-type: inline-size;\n container-name: wb-host;\n}\n\n\n\n.board-head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n flex-wrap: wrap;\n}\n.board-head[_ngcontent-%COMP%] .bt[_ngcontent-%COMP%] {\n font-weight: 700;\n font-size: 13.5px;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n}\n.board-head[_ngcontent-%COMP%] .bt[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n font-size: 13px;\n}\n.board-save[_ngcontent-%COMP%] {\n font-size: 11px;\n color: var(--mj-text-muted);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n.board-save[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n font-size: 10px;\n}\n\n.board-legend[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n color: var(--mj-text-muted);\n margin-left: 6px;\n padding: 4px 10px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--mj-bg-surface-card);\n}\n.board-legend[_ngcontent-%COMP%] .sw[_ngcontent-%COMP%] {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n display: inline-block;\n vertical-align: -1px;\n margin-right: 5px;\n}\n.board-legend[_ngcontent-%COMP%] .sw--you[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n}\n.board-legend[_ngcontent-%COMP%] .sw--sage[_ngcontent-%COMP%] {\n background: var(--sage-bg);\n border: 1px solid var(--sage);\n}\n.legend-feather[_ngcontent-%COMP%] {\n font-size: 9px;\n color: var(--sage);\n}\n\n.head-end[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n position: relative;\n}\n\n.head-btn[_ngcontent-%COMP%] {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n padding: 5px 11px;\n font-size: 11.5px;\n font-weight: 600;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n cursor: pointer;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n transition: all 140ms ease;\n font-family: inherit;\n}\n.head-btn[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n border-color: var(--mj-border-strong);\n}\n.head-btn.on[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n.head-btn--sees[_ngcontent-%COMP%] {\n position: relative;\n}\n.head-btn--sees[_ngcontent-%COMP%] .eye-pulse[_ngcontent-%COMP%] {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent);\n animation: _ngcontent-%COMP%_wb-pulse-ring 2s infinite;\n}\n.head-btn--more[_ngcontent-%COMP%] {\n padding: 5px 9px;\n}\n\n\n\n.export-wrap[_ngcontent-%COMP%] {\n position: relative;\n display: inline-flex;\n}\n.export-menu[_ngcontent-%COMP%] {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 30;\n min-width: 168px;\n display: flex;\n flex-direction: column;\n padding: 5px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-elevated);\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n}\n.export-item[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 11.5px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n border: none;\n border-radius: var(--mj-radius-sm, 5px);\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n white-space: nowrap;\n}\n.export-item[_ngcontent-%COMP%]:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.export-item[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n width: 14px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n.export-item[_ngcontent-%COMP%]:hover i[_ngcontent-%COMP%] {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n@keyframes _ngcontent-%COMP%_wb-pulse-ring {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent); }\n 70% { box-shadow: 0 0 0 6px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n\n\n.board-wrap[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n\n\n\n\n\n\n.board-toast[_ngcontent-%COMP%] {\n position: absolute;\n left: 50%;\n bottom: 64px;\n transform: translateX(-50%);\n z-index: 9;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 8px 16px;\n max-width: calc(100% - 32px);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--sage-chip-bg);\n border: 1px solid var(--sage-border);\n color: var(--mj-color-violet-300, #c4b5fd);\n font-size: 12px;\n font-weight: 600;\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n animation: _ngcontent-%COMP%_wb-toast-fade 300ms ease both;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast[_ngcontent-%COMP%] > i[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n.board-toast__text[_ngcontent-%COMP%] {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast[_ngcontent-%COMP%] .undo[_ngcontent-%COMP%] {\n flex-shrink: 0;\n}\n@keyframes _ngcontent-%COMP%_wb-toast-fade {\n from { opacity: 0; transform: translate(-50%, 4px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n.board-toast[_ngcontent-%COMP%] .undo[_ngcontent-%COMP%] {\n color: var(--mj-text-muted);\n font-weight: 600;\n cursor: pointer;\n margin-left: 4px;\n}\n.board-toast[_ngcontent-%COMP%] .undo[_ngcontent-%COMP%]:hover {\n color: var(--mj-text-primary);\n}\n\n\n\n.board-status[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 18px;\n padding: 6px 16px;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n font-size: 10.5px;\n color: var(--mj-text-muted);\n font-weight: 500;\n}\n.board-status[_ngcontent-%COMP%] b[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n font-weight: 600;\n}\n.board-status[_ngcontent-%COMP%] .seg-r[_ngcontent-%COMP%] {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n}\n.board-status[_ngcontent-%COMP%] .ctx-live[_ngcontent-%COMP%] {\n color: var(--mj-color-violet-300, var(--mj-text-secondary));\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-weight: 600;\n}\n.board-status[_ngcontent-%COMP%] .ctx-live[_ngcontent-%COMP%] .d[_ngcontent-%COMP%] {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n animation: _ngcontent-%COMP%_wb-pulse-ring 2s infinite;\n}"] }); }
695
805
  }
696
806
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RealtimeWhiteboardHostComponent, [{
697
807
  type: Component,
@@ -701,7 +811,7 @@ export class RealtimeWhiteboardHostComponent {
701
811
  RealtimeWhiteboardToolbarComponent,
702
812
  RealtimeWhiteboardZoomComponent,
703
813
  RealtimeWhiteboardAgentSeesPopoverComponent
704
- ], template: "<!-- board header -->\n<div class=\"board-head\">\n <span class=\"bt\"><i class=\"fa-solid fa-chalkboard\"></i> {{ BoardTitle }}</span>\n <span class=\"board-save\"><i class=\"fa-solid fa-circle-check\"></i> {{ SavedLabel }}</span>\n <span class=\"board-legend\">\n <span><span class=\"sw sw--you\"></span>You</span>\n <span><span class=\"sw sw--sage\"></span>{{ AgentName }} <i class=\"fa-solid fa-feather legend-feather\"></i></span>\n </span>\n <div class=\"head-end\">\n <button type=\"button\" class=\"head-btn head-btn--sees\" [class.on]=\"SeesOpen\" (click)=\"ToggleSees($event)\">\n <span class=\"eye-pulse\"></span><i class=\"fa-regular fa-eye\"></i> What {{ AgentName }} sees\n @if (SeesOpen) {\n <mj-realtime-whiteboard-agent-sees-popover\n [AgentName]=\"AgentName\"\n [DeltaJson]=\"LastDeltaJson\"\n [DebounceMs]=\"SceneDeltaDebounceMs\"\n [ElementCount]=\"ElementCount\"\n [LastDeltaLabel]=\"LastDeltaLabel\"/>\n }\n </button>\n <span class=\"export-wrap\">\n <button type=\"button\" class=\"head-btn\" [class.on]=\"ExportOpen\" (click)=\"ToggleExport($event)\">\n <i class=\"fa-solid fa-file-export\"></i> Export\n </button>\n @if (ExportOpen) {\n <div class=\"export-menu\">\n <button type=\"button\" class=\"export-item\" (click)=\"ExportDownloadHtml()\">\n <i class=\"fa-solid fa-file-code\"></i> Download HTML\n </button>\n @if (HasMultiplePages) {\n <button type=\"button\" class=\"export-item\" (click)=\"ExportDownloadHtmlAllPages()\">\n <i class=\"fa-solid fa-file-code\"></i> Download HTML \u2014 all pages\n </button>\n }\n <button type=\"button\" class=\"export-item\" (click)=\"ExportDownloadSvg()\">\n <i class=\"fa-solid fa-bezier-curve\"></i> Download SVG\n </button>\n @if (HasMultiplePages) {\n <button type=\"button\" class=\"export-item\" title=\"One SVG file per page\"\n (click)=\"ExportDownloadSvgAllPages()\">\n <i class=\"fa-solid fa-bezier-curve\"></i> Download SVG \u2014 all pages\n </button>\n }\n <button type=\"button\" class=\"export-item\" (click)=\"ExportPrint()\">\n <i class=\"fa-solid fa-print\"></i> Print\n </button>\n <button type=\"button\" class=\"export-item\" (click)=\"ExportSaveToArtifacts()\">\n <i class=\"fa-solid fa-box-archive\"></i> Save to artifacts\n </button>\n </div>\n }\n </span>\n <button type=\"button\" class=\"head-btn\" [class.on]=\"FocusMode\" (click)=\"ToggleFocus()\">\n <i [class]=\"FocusMode ? 'fa-solid fa-compress' : 'fa-solid fa-expand'\"></i>\n {{ FocusMode ? 'Show thread' : 'Focus board' }}\n </button>\n <button type=\"button\" class=\"head-btn head-btn--more\" title=\"More\"><i class=\"fa-solid fa-ellipsis\"></i></button>\n </div>\n</div>\n\n<!-- the board -->\n<div class=\"board-wrap\">\n <mj-realtime-whiteboard #board\n [State]=\"State\"\n [AgentName]=\"AgentName\"\n [Tool]=\"Tool\"\n [PenColor]=\"PenColor\"\n [PenWidth]=\"PenWidth\"\n [ShapeKind]=\"ShapeKind\"\n [TextSize]=\"TextSize\"\n [TextFamily]=\"TextFamily\"\n [TextBold]=\"TextBold\"\n [TextColor]=\"TextColor\"\n [AgentPresence]=\"AgentPresence\"\n (ToolChangeRequest)=\"Tool = $event\"\n (WidgetSubmitting)=\"WidgetSubmitting.emit($event)\"\n (WidgetSubmitted)=\"WidgetSubmitted.emit($event)\"\n (WidgetInteraction)=\"WidgetInteraction.emit($event)\"\n (ContentApplying)=\"ContentApplying.emit($event)\"\n (ContentApplied)=\"ContentApplied.emit($event)\"/>\n\n <mj-realtime-whiteboard-toolbar\n [ActiveTool]=\"Tool\"\n [PenColor]=\"PenColor\"\n [PenWidth]=\"PenWidth\"\n [ShapeKind]=\"ShapeKind\"\n [TextSize]=\"TextSize\"\n [TextFamily]=\"TextFamily\"\n [TextBold]=\"TextBold\"\n [TextColor]=\"TextColor\"\n [CanUndo]=\"State.CanUndo\"\n [CanRedo]=\"State.CanRedo\"\n [AgentName]=\"AgentName\"\n (ToolChange)=\"Tool = $event\"\n (PenColorChange)=\"PenColor = $event\"\n (PenWidthChange)=\"PenWidth = $event\"\n (ShapeKindChange)=\"ShapeKind = $event\"\n (TextSizeChange)=\"TextSize = $event\"\n (TextFamilyChange)=\"TextFamily = $event\"\n (TextBoldChange)=\"TextBold = $event\"\n (TextColorChange)=\"TextColor = $event\"\n (StyleSelection)=\"board.ApplyTextStyleToSelection($event)\"\n (Undo)=\"State.Undo()\"\n (Redo)=\"State.Redo()\"/>\n\n <mj-realtime-whiteboard-zoom\n [ZoomPercent]=\"board.ZoomPercent\"\n [MinimapOpen]=\"board.MinimapOpen\"\n (ZoomOut)=\"board.ZoomOut()\"\n (ZoomIn)=\"board.ZoomIn()\"\n (ZoomBy)=\"board.ZoomByFactor($event)\"\n (Fit)=\"board.FitToContent()\"\n (ToggleMinimap)=\"board.ToggleMinimap()\"/>\n\n <!-- agent action toast -->\n @if (ToastText) {\n <div class=\"board-toast\">\n <i class=\"fa-solid fa-feather\"></i>\n <span class=\"board-toast__text\">{{ ToastText }}</span>\n <span class=\"undo\" (click)=\"OnToastUndo($event)\"><i class=\"fa-solid fa-rotate-left\"></i> Undo</span>\n </div>\n }\n</div>\n\n<!-- board status footer: ONE row, never wraps \u2014 items ellipsize, and the long\n persistence prose collapses to an icon chip (full text on its tooltip) when\n the host is narrow (container query on the host width). -->\n<div class=\"board-status\">\n <span class=\"stat\"><b>{{ ElementCount }}</b> elements</span>\n <span class=\"stat\">you <b>{{ UserCount }}</b> \u00B7 {{ AgentName }} <b>{{ AgentCount }}</b></span>\n <span class=\"stat\">synced <b>{{ LastDeltaLabel }}</b> \u00B7 seq <b>{{ CurrentSeq }}</b></span>\n <span class=\"seg-r\">\n <span class=\"stat ctx-live\" [title]=\"AgentName + '\u2019s live context: current'\">\n <span class=\"d\"></span><span class=\"ctx-live__text\"> {{ AgentName }}'s live context: current</span>\n </span>\n <span class=\"stat persist\" title=\"Persists as a session artifact when the call ends\">\n <i class=\"fa-solid fa-box-archive\" aria-hidden=\"true\"></i>\n <span class=\"persist__long\">persists as <b>session artifact</b> when the call ends</span>\n <span class=\"persist__short\">session artifact</span>\n </span>\n </span>\n</div>\n", styles: ["/* LIVE WHITEBOARD host chrome \u2014 board header, board wrap, agent toast, status footer.\n Ported pixel-faithfully from plans/realtime/mockups/whiteboard.html\n (.board-head, .head-btn, .board-legend, .board-toast, .board-status). */\n\n:host {\n /* agent (violet) accent \u2014 same derivation as the sibling realtime components */\n --sage: var(--mj-color-violet-400, #a78bfa);\n --sage-bg: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 14%, transparent);\n --sage-border: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 45%, transparent);\n --sage-chip-bg: color-mix(in srgb, var(--mj-color-violet-500, #8b5cf6) 32%, var(--mj-bg-surface-sunken));\n\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n height: 100%;\n /* width container \u2014 drives the status footer's narrow-width collapse rules */\n container-type: inline-size;\n container-name: wb-host;\n}\n\n/* ---------- board header row ---------- */\n.board-head {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n flex-wrap: wrap;\n}\n.board-head .bt {\n font-weight: 700;\n font-size: 13.5px;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n}\n.board-head .bt i {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n font-size: 13px;\n}\n.board-save {\n font-size: 11px;\n color: var(--mj-text-muted);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n.board-save i {\n color: var(--mj-status-success);\n font-size: 10px;\n}\n\n.board-legend {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n color: var(--mj-text-muted);\n margin-left: 6px;\n padding: 4px 10px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--mj-bg-surface-card);\n}\n.board-legend .sw {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n display: inline-block;\n vertical-align: -1px;\n margin-right: 5px;\n}\n.board-legend .sw--you {\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n}\n.board-legend .sw--sage {\n background: var(--sage-bg);\n border: 1px solid var(--sage);\n}\n.legend-feather {\n font-size: 9px;\n color: var(--sage);\n}\n\n.head-end {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n position: relative;\n}\n\n.head-btn {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n padding: 5px 11px;\n font-size: 11.5px;\n font-weight: 600;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n cursor: pointer;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n transition: all 140ms ease;\n font-family: inherit;\n}\n.head-btn:hover {\n color: var(--mj-text-primary);\n border-color: var(--mj-border-strong);\n}\n.head-btn.on {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n.head-btn--sees {\n position: relative;\n}\n.head-btn--sees .eye-pulse {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent);\n animation: wb-pulse-ring 2s infinite;\n}\n.head-btn--more {\n padding: 5px 9px;\n}\n\n/* ---------- export menu (Download HTML / Download SVG / Print) ---------- */\n.export-wrap {\n position: relative;\n display: inline-flex;\n}\n.export-menu {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 30;\n min-width: 168px;\n display: flex;\n flex-direction: column;\n padding: 5px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-elevated);\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n}\n.export-item {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 11.5px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n border: none;\n border-radius: var(--mj-radius-sm, 5px);\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n white-space: nowrap;\n}\n.export-item:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.export-item i {\n width: 14px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n.export-item:hover i {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n@keyframes wb-pulse-ring {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent); }\n 70% { box-shadow: 0 0 0 6px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* ---------- the board ---------- */\n.board-wrap {\n position: relative;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n\n/* ---------- agent action toast ----------\n Bottom-center, raised ABOVE the zoom cluster's row (cluster: bottom 14px + ~38px\n tall) so the two can never collide, and width-capped clear of the cluster at the\n board's right edge. Sits just above the status footer per the mockup. */\n.board-toast {\n position: absolute;\n left: 50%;\n bottom: 64px;\n transform: translateX(-50%);\n z-index: 9;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 8px 16px;\n max-width: calc(100% - 32px);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--sage-chip-bg);\n border: 1px solid var(--sage-border);\n color: var(--mj-color-violet-300, #c4b5fd);\n font-size: 12px;\n font-weight: 600;\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n animation: wb-toast-fade 300ms ease both;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast > i {\n flex-shrink: 0;\n}\n.board-toast__text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast .undo {\n flex-shrink: 0;\n}\n@keyframes wb-toast-fade {\n from { opacity: 0; transform: translate(-50%, 4px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n.board-toast .undo {\n color: var(--mj-text-muted);\n font-weight: 600;\n cursor: pointer;\n margin-left: 4px;\n}\n.board-toast .undo:hover {\n color: var(--mj-text-primary);\n}\n\n/* ---------- board status footer ---------- */\n.board-status {\n display: flex;\n align-items: center;\n gap: 18px;\n padding: 6px 16px;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n font-size: 10.5px;\n color: var(--mj-text-muted);\n font-weight: 500;\n}\n.board-status b {\n color: var(--mj-text-secondary);\n font-weight: 600;\n}\n.board-status .seg-r {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n}\n.board-status .ctx-live {\n color: var(--mj-color-violet-300, var(--mj-text-secondary));\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-weight: 600;\n}\n.board-status .ctx-live .d {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n animation: wb-pulse-ring 2s infinite;\n}\n"] }]
814
+ ], template: "<!-- board header -->\n<div class=\"board-head\">\n <span class=\"bt\"><i class=\"fa-solid fa-chalkboard\"></i> {{ BoardTitle }}</span>\n <span class=\"board-save\"><i class=\"fa-solid fa-circle-check\"></i> {{ SavedLabel }}</span>\n <span class=\"board-legend\">\n <span><span class=\"sw sw--you\"></span>You</span>\n <span><span class=\"sw sw--sage\"></span>{{ AgentName }} <i class=\"fa-solid fa-feather legend-feather\"></i></span>\n </span>\n <div class=\"head-end\">\n <button type=\"button\" class=\"head-btn head-btn--sees\" [class.on]=\"SeesOpen\" (click)=\"ToggleSees($event)\">\n <span class=\"eye-pulse\"></span><i class=\"fa-regular fa-eye\"></i> What {{ AgentName }} sees\n @if (SeesOpen) {\n <mj-realtime-whiteboard-agent-sees-popover\n [AgentName]=\"AgentName\"\n [DeltaJson]=\"LastDeltaJson\"\n [DebounceMs]=\"SceneDeltaDebounceMs\"\n [ElementCount]=\"ElementCount\"\n [LastDeltaLabel]=\"LastDeltaLabel\"/>\n }\n </button>\n <span class=\"export-wrap\">\n <button type=\"button\" class=\"head-btn\" [class.on]=\"ExportOpen\" (click)=\"ToggleExport($event)\">\n <i class=\"fa-solid fa-file-export\"></i> Export\n </button>\n @if (ExportOpen) {\n <div class=\"export-menu\">\n <button type=\"button\" class=\"export-item\" (click)=\"ExportDownloadHtml()\">\n <i class=\"fa-solid fa-file-code\"></i> Download HTML\n </button>\n @if (HasMultiplePages) {\n <button type=\"button\" class=\"export-item\" (click)=\"ExportDownloadHtmlAllPages()\">\n <i class=\"fa-solid fa-file-code\"></i> Download HTML \u2014 all pages\n </button>\n }\n <button type=\"button\" class=\"export-item\" (click)=\"ExportDownloadSvg()\">\n <i class=\"fa-solid fa-bezier-curve\"></i> Download SVG\n </button>\n @if (HasMultiplePages) {\n <button type=\"button\" class=\"export-item\" title=\"One SVG file per page\"\n (click)=\"ExportDownloadSvgAllPages()\">\n <i class=\"fa-solid fa-bezier-curve\"></i> Download SVG \u2014 all pages\n </button>\n }\n <button type=\"button\" class=\"export-item\" (click)=\"ExportPrint()\">\n <i class=\"fa-solid fa-print\"></i> Print\n </button>\n <button type=\"button\" class=\"export-item\" (click)=\"ExportSaveToArtifacts()\">\n <i class=\"fa-solid fa-box-archive\"></i> Save to artifacts\n </button>\n </div>\n }\n </span>\n <button type=\"button\" class=\"head-btn\" [class.on]=\"FocusMode\" (click)=\"ToggleFocus()\">\n <i [class]=\"FocusMode ? 'fa-solid fa-compress' : 'fa-solid fa-expand'\"></i>\n {{ FocusMode ? 'Show thread' : 'Focus board' }}\n </button>\n <button type=\"button\" class=\"head-btn head-btn--more\" title=\"More\"><i class=\"fa-solid fa-ellipsis\"></i></button>\n </div>\n</div>\n\n<!-- the board -->\n<div class=\"board-wrap\">\n <mj-realtime-whiteboard #board\n [State]=\"State\"\n [AgentName]=\"AgentName\"\n [Tool]=\"Tool\"\n [ToolRoster]=\"ToolRoster\"\n [PenColor]=\"PenColor\"\n [PenWidth]=\"PenWidth\"\n [ShapeKind]=\"ShapeKind\"\n [TextSize]=\"TextSize\"\n [TextFamily]=\"TextFamily\"\n [TextBold]=\"TextBold\"\n [TextColor]=\"TextColor\"\n [AgentPresence]=\"AgentPresence\"\n (ToolChangeRequest)=\"Tool = $event\"\n (WidgetSubmitting)=\"WidgetSubmitting.emit($event)\"\n (WidgetSubmitted)=\"WidgetSubmitted.emit($event)\"\n (WidgetInteraction)=\"WidgetInteraction.emit($event)\"\n (ContentApplying)=\"ContentApplying.emit($event)\"\n (ContentApplied)=\"ContentApplied.emit($event)\"/>\n\n <mj-realtime-whiteboard-toolbar\n [ActiveTool]=\"Tool\"\n [ToolRoster]=\"ToolRoster\"\n [PenColor]=\"PenColor\"\n [PenWidth]=\"PenWidth\"\n [ShapeKind]=\"ShapeKind\"\n [TextSize]=\"TextSize\"\n [TextFamily]=\"TextFamily\"\n [TextBold]=\"TextBold\"\n [TextColor]=\"TextColor\"\n [CanUndo]=\"State.CanUndo\"\n [CanRedo]=\"State.CanRedo\"\n [AgentName]=\"AgentName\"\n (ToolChange)=\"Tool = $event\"\n (PenColorChange)=\"PenColor = $event\"\n (PenWidthChange)=\"PenWidth = $event\"\n (ShapeKindChange)=\"ShapeKind = $event\"\n (TextSizeChange)=\"TextSize = $event\"\n (TextFamilyChange)=\"TextFamily = $event\"\n (TextBoldChange)=\"TextBold = $event\"\n (TextColorChange)=\"TextColor = $event\"\n (StyleSelection)=\"board.ApplyTextStyleToSelection($event)\"\n (Undo)=\"State.Undo()\"\n (Redo)=\"State.Redo()\"/>\n\n <mj-realtime-whiteboard-zoom\n [ZoomPercent]=\"board.ZoomPercent\"\n [MinimapOpen]=\"board.MinimapOpen\"\n (ZoomOut)=\"board.ZoomOut()\"\n (ZoomIn)=\"board.ZoomIn()\"\n (ZoomBy)=\"board.ZoomByFactor($event)\"\n (Fit)=\"board.FitToContent()\"\n (ToggleMinimap)=\"board.ToggleMinimap()\"/>\n\n <!-- agent action toast -->\n @if (ToastText) {\n <div class=\"board-toast\">\n <i class=\"fa-solid fa-feather\"></i>\n <span class=\"board-toast__text\">{{ ToastText }}</span>\n <span class=\"undo\" (click)=\"OnToastUndo($event)\"><i class=\"fa-solid fa-rotate-left\"></i> Undo</span>\n </div>\n }\n</div>\n\n<!-- board status footer: ONE row, never wraps \u2014 items ellipsize, and the long\n persistence prose collapses to an icon chip (full text on its tooltip) when\n the host is narrow (container query on the host width). -->\n<div class=\"board-status\">\n <span class=\"stat\"><b>{{ ElementCount }}</b> elements</span>\n <span class=\"stat\">you <b>{{ UserCount }}</b> \u00B7 {{ AgentName }} <b>{{ AgentCount }}</b></span>\n <span class=\"stat\">synced <b>{{ LastDeltaLabel }}</b> \u00B7 seq <b>{{ CurrentSeq }}</b></span>\n <span class=\"seg-r\">\n <span class=\"stat ctx-live\" [title]=\"AgentName + '\u2019s live context: current'\">\n <span class=\"d\"></span><span class=\"ctx-live__text\"> {{ AgentName }}'s live context: current</span>\n </span>\n <span class=\"stat persist\" title=\"Persists as a session artifact when the call ends\">\n <i class=\"fa-solid fa-box-archive\" aria-hidden=\"true\"></i>\n <span class=\"persist__long\">persists as <b>session artifact</b> when the call ends</span>\n <span class=\"persist__short\">session artifact</span>\n </span>\n </span>\n</div>\n", styles: ["/* The host carries tabindex=\"-1\" so the board has a focus scope for its keyboard\n shortcuts (WCAG 2.1.4). Suppress the ring for the pointer path only \u2014 a keyboard\n focus, if the board ever becomes tab-reachable, must still show one (2.4.7). */\n:host:focus:not(:focus-visible) {\n outline: none;\n}\n\n/* LIVE WHITEBOARD host chrome \u2014 board header, board wrap, agent toast, status footer.\n Ported pixel-faithfully from plans/realtime/mockups/whiteboard.html\n (.board-head, .head-btn, .board-legend, .board-toast, .board-status). */\n\n:host {\n /* agent (violet) accent \u2014 same derivation as the sibling realtime components */\n --sage: var(--mj-color-violet-400, #a78bfa);\n --sage-bg: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 14%, transparent);\n --sage-border: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 45%, transparent);\n --sage-chip-bg: color-mix(in srgb, var(--mj-color-violet-500, #8b5cf6) 32%, var(--mj-bg-surface-sunken));\n\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n height: 100%;\n /* width container \u2014 drives the status footer's narrow-width collapse rules */\n container-type: inline-size;\n container-name: wb-host;\n}\n\n/* ---------- board header row ---------- */\n.board-head {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n flex-shrink: 0;\n flex-wrap: wrap;\n}\n.board-head .bt {\n font-weight: 700;\n font-size: 13.5px;\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-text-primary);\n}\n.board-head .bt i {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n font-size: 13px;\n}\n.board-save {\n font-size: 11px;\n color: var(--mj-text-muted);\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n.board-save i {\n color: var(--mj-status-success);\n font-size: 10px;\n}\n\n.board-legend {\n display: inline-flex;\n align-items: center;\n gap: 12px;\n font-size: 11px;\n color: var(--mj-text-muted);\n margin-left: 6px;\n padding: 4px 10px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--mj-bg-surface-card);\n}\n.board-legend .sw {\n width: 10px;\n height: 10px;\n border-radius: 3px;\n display: inline-block;\n vertical-align: -1px;\n margin-right: 5px;\n}\n.board-legend .sw--you {\n background: var(--mj-bg-surface-elevated);\n border: 1px solid var(--mj-border-strong);\n}\n.board-legend .sw--sage {\n background: var(--sage-bg);\n border: 1px solid var(--sage);\n}\n.legend-feather {\n font-size: 9px;\n color: var(--sage);\n}\n\n.head-end {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n position: relative;\n}\n\n.head-btn {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n padding: 5px 11px;\n font-size: 11.5px;\n font-weight: 600;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n cursor: pointer;\n background: var(--mj-bg-surface-card);\n color: var(--mj-text-secondary);\n transition: all 140ms ease;\n font-family: inherit;\n}\n.head-btn:hover {\n color: var(--mj-text-primary);\n border-color: var(--mj-border-strong);\n}\n.head-btn.on {\n background: color-mix(in srgb, var(--mj-brand-primary) 14%, transparent);\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n.head-btn--sees {\n position: relative;\n}\n.head-btn--sees .eye-pulse {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent);\n animation: wb-pulse-ring 2s infinite;\n}\n.head-btn--more {\n padding: 5px 9px;\n}\n\n/* ---------- export menu (Download HTML / Download SVG / Print) ---------- */\n.export-wrap {\n position: relative;\n display: inline-flex;\n}\n.export-menu {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 30;\n min-width: 168px;\n display: flex;\n flex-direction: column;\n padding: 5px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-elevated);\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n}\n.export-item {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 7px 10px;\n font-size: 11.5px;\n font-weight: 600;\n font-family: inherit;\n text-align: left;\n border: none;\n border-radius: var(--mj-radius-sm, 5px);\n background: transparent;\n color: var(--mj-text-secondary);\n cursor: pointer;\n white-space: nowrap;\n}\n.export-item:hover {\n background: var(--mj-bg-surface-hover);\n color: var(--mj-text-primary);\n}\n.export-item i {\n width: 14px;\n text-align: center;\n color: var(--mj-text-muted);\n}\n.export-item:hover i {\n color: var(--mj-brand-accent, var(--mj-brand-primary));\n}\n@keyframes wb-pulse-ring {\n 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 50%, transparent); }\n 70% { box-shadow: 0 0 0 6px transparent; }\n 100% { box-shadow: 0 0 0 0 transparent; }\n}\n\n/* ---------- the board ---------- */\n.board-wrap {\n position: relative;\n flex: 1;\n min-height: 0;\n overflow: hidden;\n}\n\n/* ---------- agent action toast ----------\n Bottom-center, raised ABOVE the zoom cluster's row (cluster: bottom 14px + ~38px\n tall) so the two can never collide, and width-capped clear of the cluster at the\n board's right edge. Sits just above the status footer per the mockup. */\n.board-toast {\n position: absolute;\n left: 50%;\n bottom: 64px;\n transform: translateX(-50%);\n z-index: 9;\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 8px 16px;\n max-width: calc(100% - 32px);\n border-radius: var(--mj-radius-full, 9999px);\n background: var(--sage-chip-bg);\n border: 1px solid var(--sage-border);\n color: var(--mj-color-violet-300, #c4b5fd);\n font-size: 12px;\n font-weight: 600;\n box-shadow: var(--mj-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.3));\n animation: wb-toast-fade 300ms ease both;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast > i {\n flex-shrink: 0;\n}\n.board-toast__text {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.board-toast .undo {\n flex-shrink: 0;\n}\n@keyframes wb-toast-fade {\n from { opacity: 0; transform: translate(-50%, 4px); }\n to { opacity: 1; transform: translate(-50%, 0); }\n}\n.board-toast .undo {\n color: var(--mj-text-muted);\n font-weight: 600;\n cursor: pointer;\n margin-left: 4px;\n}\n.board-toast .undo:hover {\n color: var(--mj-text-primary);\n}\n\n/* ---------- board status footer ---------- */\n.board-status {\n display: flex;\n align-items: center;\n gap: 18px;\n padding: 6px 16px;\n flex-shrink: 0;\n border-top: 1px solid var(--mj-border-default);\n background: var(--mj-bg-surface);\n font-size: 10.5px;\n color: var(--mj-text-muted);\n font-weight: 500;\n}\n.board-status b {\n color: var(--mj-text-secondary);\n font-weight: 600;\n}\n.board-status .seg-r {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 18px;\n}\n.board-status .ctx-live {\n color: var(--mj-color-violet-300, var(--mj-text-secondary));\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-weight: 600;\n}\n.board-status .ctx-live .d {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--sage);\n animation: wb-pulse-ring 2s infinite;\n}\n"] }]
705
815
  }], null, { State: [{
706
816
  type: Input,
707
817
  args: [{ required: true }]
@@ -711,6 +821,10 @@ export class RealtimeWhiteboardHostComponent {
711
821
  type: Input
712
822
  }], SavedLabel: [{
713
823
  type: Input
824
+ }], EnableGlobalShortcuts: [{
825
+ type: Input
826
+ }], ToolRoster: [{
827
+ type: Input
714
828
  }], SceneDelta: [{
715
829
  type: Output
716
830
  }], AgentUndo: [{
@@ -750,9 +864,15 @@ export class RealtimeWhiteboardHostComponent {
750
864
  }], CloseExport: [{
751
865
  type: HostListener,
752
866
  args: ['document:click']
867
+ }], HostTabIndex: [{
868
+ type: HostBinding,
869
+ args: ['attr.tabindex']
870
+ }], OnHostPointerDown: [{
871
+ type: HostListener,
872
+ args: ['pointerdown']
753
873
  }], OnKeydown: [{
754
874
  type: HostListener,
755
875
  args: ['document:keydown', ['$event']]
756
876
  }] }); })();
757
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RealtimeWhiteboardHostComponent, { className: "RealtimeWhiteboardHostComponent", filePath: "src/lib/whiteboard-host.component.ts", lineNumber: 71 }); })();
877
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RealtimeWhiteboardHostComponent, { className: "RealtimeWhiteboardHostComponent", filePath: "src/lib/whiteboard-host.component.ts", lineNumber: 72 }); })();
758
878
  //# sourceMappingURL=whiteboard-host.component.js.map