@bimetal/whiteboard-angular-components 0.29.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.
@@ -0,0 +1,845 @@
1
+ import { Component, Input, ViewChild, ElementRef, ChangeDetectionStrategy, NgZone, inject, } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { screenToBoard, boardToScreen, WHITEBOARD_TOOLS, WHITEBOARD_ALIGN, WHITEBOARD_DISTRIBUTE, whiteboardConnectorPath, whiteboardDrawPath, whiteboardDashArray, whiteboardCommentAnchor, } from '@bimetal/whiteboard-headless';
4
+ import { createWhiteboardControllerBinding } from '@bimetal/angular';
5
+ import { WhiteboardShapeComponent } from './WhiteboardShape.js';
6
+ import { TransformHandlesComponent } from './TransformHandles.js';
7
+ import { StylePanelComponent } from './StylePanel.js';
8
+ import { CommentThreadComponent } from './CommentThread.js';
9
+ import { TextDialogComponent } from './TextDialog.js';
10
+ import * as i0 from "@angular/core";
11
+ import * as i1 from "@angular/common";
12
+ const _c0 = ["viewportRef"];
13
+ function WhiteboardComponent_button_2_Template(rf, ctx) { if (rf & 1) {
14
+ const _r2 = i0.ɵɵgetCurrentView();
15
+ i0.ɵɵelementStart(0, "button", 33);
16
+ i0.ɵɵlistener("click", function WhiteboardComponent_button_2_Template_button_click_0_listener() { const t_r3 = i0.ɵɵrestoreView(_r2).$implicit; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onTool(t_r3.id)); });
17
+ i0.ɵɵtext(1);
18
+ i0.ɵɵelementEnd();
19
+ } if (rf & 2) {
20
+ const t_r3 = ctx.$implicit;
21
+ const ctx_r3 = i0.ɵɵnextContext();
22
+ i0.ɵɵclassMap(ctx_r3.toolClass(t_r3.id));
23
+ i0.ɵɵattribute("title", ctx_r3.locale.tools[t_r3.id])("aria-pressed", ctx_r3.activeTool === t_r3.id)("data-tool", t_r3.id);
24
+ i0.ɵɵadvance();
25
+ i0.ɵɵtextInterpolate(t_r3.glyph);
26
+ } }
27
+ function WhiteboardComponent_button_19_Template(rf, ctx) { if (rf & 1) {
28
+ const _r5 = i0.ɵɵgetCurrentView();
29
+ i0.ɵɵelementStart(0, "button", 5);
30
+ i0.ɵɵlistener("click", function WhiteboardComponent_button_19_Template_button_click_0_listener() { const a_r6 = i0.ɵɵrestoreView(_r5).$implicit; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onAlign(a_r6.edge)); });
31
+ i0.ɵɵtext(1);
32
+ i0.ɵɵelementEnd();
33
+ } if (rf & 2) {
34
+ const a_r6 = ctx.$implicit;
35
+ const ctx_r3 = i0.ɵɵnextContext();
36
+ i0.ɵɵproperty("disabled", !ctx_r3.canAlign);
37
+ i0.ɵɵattribute("title", ctx_r3.locale.alignLabels[a_r6.edge])("data-align", a_r6.edge);
38
+ i0.ɵɵadvance();
39
+ i0.ɵɵtextInterpolate(a_r6.glyph);
40
+ } }
41
+ function WhiteboardComponent_button_20_Template(rf, ctx) { if (rf & 1) {
42
+ const _r7 = i0.ɵɵgetCurrentView();
43
+ i0.ɵɵelementStart(0, "button", 5);
44
+ i0.ɵɵlistener("click", function WhiteboardComponent_button_20_Template_button_click_0_listener() { const d_r8 = i0.ɵɵrestoreView(_r7).$implicit; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onDistribute(d_r8.axis)); });
45
+ i0.ɵɵtext(1);
46
+ i0.ɵɵelementEnd();
47
+ } if (rf & 2) {
48
+ const d_r8 = ctx.$implicit;
49
+ const ctx_r3 = i0.ɵɵnextContext();
50
+ i0.ɵɵproperty("disabled", !ctx_r3.canDistribute);
51
+ i0.ɵɵattribute("title", ctx_r3.locale.distributeLabels[d_r8.axis])("data-distribute", d_r8.axis);
52
+ i0.ɵɵadvance();
53
+ i0.ɵɵtextInterpolate(d_r8.glyph);
54
+ } }
55
+ function WhiteboardComponent__svg_path_33_Template(rf, ctx) { if (rf & 1) {
56
+ i0.ɵɵnamespaceSVG();
57
+ i0.ɵɵelement(0, "path", 34);
58
+ } if (rf & 2) {
59
+ const c_r9 = ctx.$implicit;
60
+ i0.ɵɵattribute("data-connector-id", c_r9.id)("d", c_r9.d)("stroke", c_r9.stroke)("stroke-width", c_r9.strokeWidth)("stroke-dasharray", c_r9.dash)("opacity", c_r9.opacity)("marker-end", c_r9.markerEnd)("marker-start", c_r9.markerStart);
61
+ } }
62
+ function WhiteboardComponent__svg_path_34_Template(rf, ctx) { if (rf & 1) {
63
+ i0.ɵɵnamespaceSVG();
64
+ i0.ɵɵelement(0, "path", 35);
65
+ } if (rf & 2) {
66
+ const ctx_r3 = i0.ɵɵnextContext();
67
+ i0.ɵɵattribute("d", ctx_r3.connectorPreviewD);
68
+ } }
69
+ function WhiteboardComponent__svg_path_35_Template(rf, ctx) { if (rf & 1) {
70
+ i0.ɵɵnamespaceSVG();
71
+ i0.ɵɵelement(0, "path", 36);
72
+ } if (rf & 2) {
73
+ const ctx_r3 = i0.ɵɵnextContext();
74
+ i0.ɵɵattribute("d", ctx_r3.drawPreviewPath);
75
+ } }
76
+ function WhiteboardComponent__svg_line_36_Template(rf, ctx) { if (rf & 1) {
77
+ i0.ɵɵnamespaceSVG();
78
+ i0.ɵɵelement(0, "line", 37);
79
+ } if (rf & 2) {
80
+ const g_r10 = ctx.$implicit;
81
+ i0.ɵɵattribute("data-guide-axis", g_r10.axis)("x1", g_r10.x1)("y1", g_r10.y1)("x2", g_r10.x2)("y2", g_r10.y2);
82
+ } }
83
+ function WhiteboardComponent_bm_wb_shape_37_Template(rf, ctx) { if (rf & 1) {
84
+ const _r11 = i0.ɵɵgetCurrentView();
85
+ i0.ɵɵelementStart(0, "bm-wb-shape", 38);
86
+ i0.ɵɵlistener("shapePointerDown", function WhiteboardComponent_bm_wb_shape_37_Template_bm_wb_shape_shapePointerDown_0_listener($event) { const v_r12 = i0.ɵɵrestoreView(_r11).$implicit; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onShapeDown(v_r12.shape.id, $event)); })("shapeDoubleClick", function WhiteboardComponent_bm_wb_shape_37_Template_bm_wb_shape_shapeDoubleClick_0_listener($event) { const v_r12 = i0.ɵɵrestoreView(_r11).$implicit; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onShapeDblClick(v_r12.shape.id, $event)); });
87
+ i0.ɵɵelementEnd();
88
+ } if (rf & 2) {
89
+ const v_r12 = ctx.$implicit;
90
+ const ctx_r3 = i0.ɵɵnextContext();
91
+ i0.ɵɵproperty("view", v_r12)("dropTarget", ctx_r3.snapshot.dropTargetId === v_r12.shape.id);
92
+ } }
93
+ function WhiteboardComponent_div_38_Template(rf, ctx) { if (rf & 1) {
94
+ i0.ɵɵelement(0, "div", 39);
95
+ } if (rf & 2) {
96
+ const mq_r13 = ctx.ngIf;
97
+ i0.ɵɵstyleProp("left", mq_r13.x, "px")("top", mq_r13.y, "px")("width", mq_r13.width, "px")("height", mq_r13.height, "px");
98
+ } }
99
+ function WhiteboardComponent_div_39_Template(rf, ctx) { if (rf & 1) {
100
+ i0.ɵɵelement(0, "div", 40);
101
+ } if (rf & 2) {
102
+ const cp_r14 = ctx.ngIf;
103
+ i0.ɵɵstyleProp("left", cp_r14.x, "px")("top", cp_r14.y, "px")("width", cp_r14.width, "px")("height", cp_r14.height, "px");
104
+ } }
105
+ function WhiteboardComponent_bm_wb_transform_handles_40_Template(rf, ctx) { if (rf & 1) {
106
+ const _r15 = i0.ɵɵgetCurrentView();
107
+ i0.ɵɵelementStart(0, "bm-wb-transform-handles", 41);
108
+ i0.ɵɵlistener("resize", function WhiteboardComponent_bm_wb_transform_handles_40_Template_bm_wb_transform_handles_resize_0_listener($event) { const tv_r16 = i0.ɵɵrestoreView(_r15).ngIf; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onResizeIntent(tv_r16.shape.id, $event)); })("rotate", function WhiteboardComponent_bm_wb_transform_handles_40_Template_bm_wb_transform_handles_rotate_0_listener($event) { const tv_r16 = i0.ɵɵrestoreView(_r15).ngIf; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onRotate(tv_r16.shape.id, $event)); });
109
+ i0.ɵɵelementEnd();
110
+ } if (rf & 2) {
111
+ const tv_r16 = ctx.ngIf;
112
+ i0.ɵɵproperty("view", tv_r16);
113
+ } }
114
+ function WhiteboardComponent_button_41_Template(rf, ctx) { if (rf & 1) {
115
+ const _r17 = i0.ɵɵgetCurrentView();
116
+ i0.ɵɵelementStart(0, "button", 42);
117
+ i0.ɵɵlistener("pointerdown", function WhiteboardComponent_button_41_Template_button_pointerdown_0_listener($event) { i0.ɵɵrestoreView(_r17); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.stop($event)); })("click", function WhiteboardComponent_button_41_Template_button_click_0_listener($event) { const p_r18 = i0.ɵɵrestoreView(_r17).$implicit; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onPinClick(p_r18.id, $event)); });
118
+ i0.ɵɵtext(1);
119
+ i0.ɵɵelementEnd();
120
+ } if (rf & 2) {
121
+ const p_r18 = ctx.$implicit;
122
+ const ctx_r3 = i0.ɵɵnextContext();
123
+ i0.ɵɵclassMap(ctx_r3.pinClass(p_r18));
124
+ i0.ɵɵstyleProp("left", p_r18.x, "px")("top", p_r18.y, "px");
125
+ i0.ɵɵattribute("data-comment-id", p_r18.id)("title", p_r18.text);
126
+ i0.ɵɵadvance();
127
+ i0.ɵɵtextInterpolate(p_r18.glyph);
128
+ } }
129
+ function WhiteboardComponent__svg_svg_42__svg_line_1_Template(rf, ctx) { if (rf & 1) {
130
+ i0.ɵɵnamespaceSVG();
131
+ i0.ɵɵelement(0, "line", 45);
132
+ } if (rf & 2) {
133
+ const seg_r19 = ctx.$implicit;
134
+ i0.ɵɵattribute("x1", seg_r19.x1)("y1", seg_r19.y1)("x2", seg_r19.x2)("y2", seg_r19.y2)("stroke-opacity", seg_r19.opacity);
135
+ } }
136
+ function WhiteboardComponent__svg_svg_42_Template(rf, ctx) { if (rf & 1) {
137
+ i0.ɵɵnamespaceSVG();
138
+ i0.ɵɵelementStart(0, "svg", 43);
139
+ i0.ɵɵtemplate(1, WhiteboardComponent__svg_svg_42__svg_line_1_Template, 1, 5, "line", 44);
140
+ i0.ɵɵelementEnd();
141
+ } if (rf & 2) {
142
+ const ls_r20 = ctx.ngIf;
143
+ const ctx_r3 = i0.ɵɵnextContext();
144
+ i0.ɵɵadvance();
145
+ i0.ɵɵproperty("ngForOf", ls_r20)("ngForTrackBy", ctx_r3.trackIndex);
146
+ } }
147
+ function WhiteboardComponent_bm_wb_comment_thread_43_Template(rf, ctx) { if (rf & 1) {
148
+ const _r21 = i0.ɵɵgetCurrentView();
149
+ i0.ɵɵelementStart(0, "bm-wb-comment-thread", 46);
150
+ i0.ɵɵlistener("commit", function WhiteboardComponent_bm_wb_comment_thread_43_Template_bm_wb_comment_thread_commit_0_listener($event) { i0.ɵɵrestoreView(_r21); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onCommitDraft($event)); })("close", function WhiteboardComponent_bm_wb_comment_thread_43_Template_bm_wb_comment_thread_close_0_listener() { i0.ɵɵrestoreView(_r21); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onCancelDraft()); });
151
+ i0.ɵɵelementEnd();
152
+ } if (rf & 2) {
153
+ const at_r22 = ctx.ngIf;
154
+ const ctx_r3 = i0.ɵɵnextContext();
155
+ i0.ɵɵproperty("at", at_r22)("initial", "")("existing", false)("resolved", false)("locale", ctx_r3.locale);
156
+ } }
157
+ function WhiteboardComponent_bm_wb_comment_thread_44_Template(rf, ctx) { if (rf & 1) {
158
+ const _r23 = i0.ɵɵgetCurrentView();
159
+ i0.ɵɵelementStart(0, "bm-wb-comment-thread", 47);
160
+ i0.ɵɵlistener("commit", function WhiteboardComponent_bm_wb_comment_thread_44_Template_bm_wb_comment_thread_commit_0_listener($event) { const ot_r24 = i0.ɵɵrestoreView(_r23).ngIf; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onCommitThread(ot_r24.id, $event)); })("close", function WhiteboardComponent_bm_wb_comment_thread_44_Template_bm_wb_comment_thread_close_0_listener() { i0.ɵɵrestoreView(_r23); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onCloseThread()); })("resolve", function WhiteboardComponent_bm_wb_comment_thread_44_Template_bm_wb_comment_thread_resolve_0_listener() { const ot_r24 = i0.ɵɵrestoreView(_r23).ngIf; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onResolveThread(ot_r24.id)); })("delete", function WhiteboardComponent_bm_wb_comment_thread_44_Template_bm_wb_comment_thread_delete_0_listener() { const ot_r24 = i0.ɵɵrestoreView(_r23).ngIf; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onDeleteThread(ot_r24.id)); });
161
+ i0.ɵɵelementEnd();
162
+ } if (rf & 2) {
163
+ const ot_r24 = ctx.ngIf;
164
+ const ctx_r3 = i0.ɵɵnextContext();
165
+ i0.ɵɵproperty("at", ot_r24.at)("initial", ot_r24.text)("existing", true)("resolved", ot_r24.resolved)("locale", ctx_r3.locale);
166
+ } }
167
+ function WhiteboardComponent_bm_wb_style_panel_45_Template(rf, ctx) { if (rf & 1) {
168
+ const _r25 = i0.ɵɵgetCurrentView();
169
+ i0.ɵɵelementStart(0, "bm-wb-style-panel", 48);
170
+ i0.ɵɵlistener("styleInput", function WhiteboardComponent_bm_wb_style_panel_45_Template_bm_wb_style_panel_styleInput_0_listener($event) { i0.ɵɵrestoreView(_r25); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onStyleInput($event)); });
171
+ i0.ɵɵelementEnd();
172
+ } if (rf & 2) {
173
+ const ps_r26 = ctx.ngIf;
174
+ const ctx_r3 = i0.ɵɵnextContext();
175
+ i0.ɵɵstyleMap(ps_r26);
176
+ i0.ɵɵproperty("locale", ctx_r3.locale);
177
+ } }
178
+ function WhiteboardComponent_div_46_Template(rf, ctx) { if (rf & 1) {
179
+ const _r27 = i0.ɵɵgetCurrentView();
180
+ i0.ɵɵelementStart(0, "div", 49)(1, "span");
181
+ i0.ɵɵtext(2);
182
+ i0.ɵɵelementEnd();
183
+ i0.ɵɵelementStart(3, "button", 50);
184
+ i0.ɵɵlistener("click", function WhiteboardComponent_div_46_Template_button_click_3_listener() { i0.ɵɵrestoreView(_r27); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onUndo()); });
185
+ i0.ɵɵtext(4);
186
+ i0.ɵɵelementEnd()();
187
+ } if (rf & 2) {
188
+ const u_r28 = ctx.ngIf;
189
+ const ctx_r3 = i0.ɵɵnextContext();
190
+ i0.ɵɵadvance(2);
191
+ i0.ɵɵtextInterpolate(u_r28.description);
192
+ i0.ɵɵadvance(2);
193
+ i0.ɵɵtextInterpolate(ctx_r3.locale.undo);
194
+ } }
195
+ function WhiteboardComponent_bm_wb_text_dialog_47_Template(rf, ctx) { if (rf & 1) {
196
+ const _r29 = i0.ɵɵgetCurrentView();
197
+ i0.ɵɵelementStart(0, "bm-wb-text-dialog", 51);
198
+ i0.ɵɵlistener("save", function WhiteboardComponent_bm_wb_text_dialog_47_Template_bm_wb_text_dialog_save_0_listener($event) { i0.ɵɵrestoreView(_r29); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onSaveShape($event)); })("close", function WhiteboardComponent_bm_wb_text_dialog_47_Template_bm_wb_text_dialog_close_0_listener() { i0.ɵɵrestoreView(_r29); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onCloseDialog()); })("delete", function WhiteboardComponent_bm_wb_text_dialog_47_Template_bm_wb_text_dialog_delete_0_listener() { const ds_r30 = i0.ɵɵrestoreView(_r29).ngIf; const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onDeleteShape(ds_r30.id)); });
199
+ i0.ɵɵelementEnd();
200
+ } if (rf & 2) {
201
+ const ds_r30 = ctx.ngIf;
202
+ const ctx_r3 = i0.ɵɵnextContext();
203
+ i0.ɵɵproperty("initial", ds_r30.text)("locale", ctx_r3.locale);
204
+ } }
205
+ /** Per-instance SVG marker id counter — document-global ids would collide across
206
+ * two mounted boards and `url(#…)` would resolve to the wrong marker. */
207
+ let wbMarkerSeq = 0;
208
+ const mod = (n, m) => ((n % m) + m) % m;
209
+ function isEditableTarget(t) {
210
+ const el = t;
211
+ if (!el || !el.tagName)
212
+ return false;
213
+ return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable === true;
214
+ }
215
+ /**
216
+ * The Angular whiteboard — a thin, standalone-component binding over the canonical
217
+ * `@bimetal/whiteboard-headless` controller. Owns DOM only: it converts
218
+ * `clientX/Y → boardPoint` through the controller's viewport (`screenToBoard` —
219
+ * the binding's ONE piece of geometry), wires window pointer + wheel events, and
220
+ * renders the snapshot. Every interaction decision (tool FSM, create/drag/marquee,
221
+ * pan/zoom math, input policy) lives in the controller. 1:1 with the React
222
+ * reference.
223
+ */
224
+ export class WhiteboardComponent {
225
+ store;
226
+ canvasId;
227
+ config;
228
+ darkMode = false;
229
+ /** Surfaces infrastructure errors (a domain rejection is an undo toast). */
230
+ onError;
231
+ viewportRef;
232
+ binding;
233
+ zone = inject(NgZone, { optional: true }) ?? undefined;
234
+ tools = WHITEBOARD_TOOLS;
235
+ aligns = WHITEBOARD_ALIGN;
236
+ distributes = WHITEBOARD_DISTRIBUTE;
237
+ arrowId = `bm-wb-arrow-${wbMarkerSeq++}`;
238
+ markerUrl = `url(#${this.arrowId})`;
239
+ get ctrl() { return this.binding.ctrl; }
240
+ get snapshot() { return this.binding.snapshot(); }
241
+ get viewport() { return this.snapshot.viewport; }
242
+ get activeTool() { return this.snapshot.activeTool; }
243
+ get cfg() { return this.snapshot.config; }
244
+ get locale() { return this.cfg.locale; }
245
+ get zoomPct() { return Math.round(this.viewport.scale * 100); }
246
+ get viewportClass() {
247
+ return 'bm-wb__viewport' + (this.activeTool !== 'select' ? ' bm-wb__viewport--tool' : '');
248
+ }
249
+ get sceneTransform() {
250
+ const v = this.viewport;
251
+ return `translate(${v.offsetX}px, ${v.offsetY}px) scale(${v.scale})`;
252
+ }
253
+ get viewportVars() {
254
+ const v = this.viewport;
255
+ const gridPx = this.cfg.gridSize * v.scale;
256
+ return {
257
+ '--bmw-grid-px': `${gridPx}px`,
258
+ '--bmw-grid-x': `${mod(v.offsetX, gridPx)}px`,
259
+ '--bmw-grid-y': `${mod(v.offsetY, gridPx)}px`,
260
+ };
261
+ }
262
+ // ── Selection-derived toolbar state ──
263
+ get selectedViews() { return this.snapshot.shapes.filter((v) => v.selected); }
264
+ get hasSel() { return this.snapshot.selection.length > 0; }
265
+ get canAlign() { return this.snapshot.selection.length >= 2; } // controller no-ops below 2
266
+ get canDistribute() { return this.snapshot.selection.length >= 3; } // controller no-ops below 3
267
+ get canGroup() { return this.snapshot.selection.length >= 2; } // group needs ≥2
268
+ get canUngroup() { return this.selectedViews.some((v) => v.shape.kind === 'frame' || v.shape.kind === 'group'); }
269
+ // A group's box is derived from its children — not directly resizable/rotatable.
270
+ get transformView() {
271
+ // The controller decides WHICH shape (if any) shows handles via transformHandleId.
272
+ const id = this.snapshot.transformHandleId;
273
+ return id ? this.snapshot.shapes.find(v => v.shape.id === id) ?? null : null;
274
+ }
275
+ // Guard on the resolved VIEWS, not the raw selection length: a delete/ungroup
276
+ // transiently leaves `selection` pointing at an already-removed shape.
277
+ get panelStyle() {
278
+ const sel = this.selectedViews;
279
+ return sel.length > 0 ? sel[0].shape.style : null;
280
+ }
281
+ get undo() { return this.snapshot.lastAction; }
282
+ get dialogShape() {
283
+ const d = this.snapshot.dialog;
284
+ return d ? { id: d.shape.id, text: d.shape.text } : null;
285
+ }
286
+ // ── Connectors / previews / guides (geometry ONLY from headless helpers) ──
287
+ get connectorViews() {
288
+ const out = [];
289
+ for (const c of this.snapshot.connectors) {
290
+ const d = whiteboardConnectorPath(this.snapshot, c);
291
+ if (!d)
292
+ continue;
293
+ out.push({
294
+ id: c.id, d, stroke: c.style.stroke, strokeWidth: c.style.strokeWidth,
295
+ dash: whiteboardDashArray(c.style.dash), opacity: c.style.opacity,
296
+ markerEnd: c.style.arrowEnd ? this.markerUrl : null,
297
+ markerStart: c.style.arrowStart ? this.markerUrl : null,
298
+ });
299
+ }
300
+ return out;
301
+ }
302
+ get connectorPreviewD() {
303
+ const p = this.snapshot.connectorPreview;
304
+ return p ? `M ${p.from.x} ${p.from.y} L ${p.to.x} ${p.to.y}` : null;
305
+ }
306
+ get drawPreviewPath() {
307
+ const dp = this.snapshot.drawPreview;
308
+ return dp && dp.length > 1 ? whiteboardDrawPath(dp) : null;
309
+ }
310
+ get guideLines() {
311
+ return this.snapshot.guides.map((g) => ({
312
+ axis: g.axis,
313
+ x1: g.axis === 'x' ? g.pos : g.start,
314
+ y1: g.axis === 'x' ? g.start : g.pos,
315
+ x2: g.axis === 'x' ? g.pos : g.end,
316
+ y2: g.axis === 'x' ? g.end : g.pos,
317
+ }));
318
+ }
319
+ get commentPins() {
320
+ const out = [];
321
+ for (const c of this.snapshot.comments) {
322
+ const at = whiteboardCommentAnchor(this.snapshot, c);
323
+ if (!at)
324
+ continue;
325
+ out.push({ id: c.id, x: at.x, y: at.y, resolved: c.resolved, text: c.text, glyph: c.resolved ? '✓' : '💬' });
326
+ }
327
+ return out;
328
+ }
329
+ // Laser is a presentation pointer — painted ABOVE every shape via its own top SVG
330
+ // (rendered LAST in the scene). null when there is no live trail.
331
+ get laserSegments() {
332
+ const laser = this.snapshot.laser;
333
+ if (!laser || laser.length <= 1)
334
+ return null;
335
+ const out = [];
336
+ for (let i = 1; i < laser.length; i++) {
337
+ out.push({
338
+ x1: laser[i - 1].x, y1: laser[i - 1].y, x2: laser[i].x, y2: laser[i].y,
339
+ opacity: i / laser.length, // faint tail → bright head
340
+ });
341
+ }
342
+ return out;
343
+ }
344
+ // ── Comment editors (SCREEN space via boardToScreen) ──
345
+ get draftAt() {
346
+ const d = this.snapshot.commentDraft;
347
+ return d ? boardToScreen(this.viewport, d) : null;
348
+ }
349
+ get openThread() {
350
+ const id = this.snapshot.openCommentId;
351
+ if (!id)
352
+ return null;
353
+ const c = this.snapshot.comments.find((x) => x.id === id);
354
+ if (!c)
355
+ return null;
356
+ const at = whiteboardCommentAnchor(this.snapshot, c);
357
+ if (!at)
358
+ return null;
359
+ return { id: c.id, at: boardToScreen(this.viewport, at), text: c.text, resolved: c.resolved };
360
+ }
361
+ toolClass(id) {
362
+ return 'bm-wb__tool' + (this.activeTool === id ? ' bm-wb__tool--active' : '');
363
+ }
364
+ pinClass(p) {
365
+ return 'bm-wb__comment-pin' + (p.resolved ? ' bm-wb__comment-pin--resolved' : '');
366
+ }
367
+ // ── clientX/Y → board point via the live viewport rect + camera ──
368
+ boardOf(clientX, clientY) {
369
+ const rect = this.viewportRef?.nativeElement.getBoundingClientRect();
370
+ const screen = { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };
371
+ return screenToBoard(this.ctrl.getSnapshot().viewport, screen);
372
+ }
373
+ // Window pointer move/up/cancel feed the active gesture (pointerId-guarded in the
374
+ // controller). Wheel is a native non-passive listener so zoom can preventDefault.
375
+ onMove = (e) => {
376
+ const el = document.elementFromPoint(e.clientX, e.clientY);
377
+ const over = el?.closest('.bm-wb__shape')?.dataset['shapeId'] ?? null;
378
+ this.ctrl.pointerMove(this.boardOf(e.clientX, e.clientY), e.pointerId, over);
379
+ };
380
+ onUp = (e) => this.ctrl.pointerUp(e.pointerId);
381
+ onCancel = (e) => this.ctrl.pointerCancel(e.pointerId);
382
+ onWheel = (e) => {
383
+ e.preventDefault();
384
+ const vp = this.viewportRef?.nativeElement;
385
+ if (!vp)
386
+ return;
387
+ if (e.ctrlKey || e.metaKey) {
388
+ const rect = vp.getBoundingClientRect();
389
+ this.ctrl.zoomBy(e.deltaY < 0 ? 1.1 : 0.9, { x: e.clientX - rect.left, y: e.clientY - rect.top });
390
+ }
391
+ else {
392
+ this.ctrl.panBy(-e.deltaX, -e.deltaY);
393
+ }
394
+ };
395
+ ngOnInit() {
396
+ this.binding = createWhiteboardControllerBinding({
397
+ store: this.store,
398
+ canvasId: this.canvasId,
399
+ config: this.config,
400
+ onError: (err) => this.onError?.(err),
401
+ }, this.zone);
402
+ if (typeof window !== 'undefined') {
403
+ window.addEventListener('pointermove', this.onMove);
404
+ window.addEventListener('pointerup', this.onUp);
405
+ window.addEventListener('pointercancel', this.onCancel);
406
+ }
407
+ }
408
+ ngAfterViewInit() {
409
+ this.viewportRef?.nativeElement.addEventListener('wheel', this.onWheel, { passive: false });
410
+ }
411
+ ngOnChanges(changes) {
412
+ if (!this.binding)
413
+ return; // before ngOnInit
414
+ if (changes['config'] && !changes['config'].firstChange) {
415
+ this.ctrl.setConfig(this.config);
416
+ }
417
+ }
418
+ ngOnDestroy() {
419
+ if (typeof window !== 'undefined') {
420
+ window.removeEventListener('pointermove', this.onMove);
421
+ window.removeEventListener('pointerup', this.onUp);
422
+ window.removeEventListener('pointercancel', this.onCancel);
423
+ }
424
+ this.viewportRef?.nativeElement.removeEventListener('wheel', this.onWheel);
425
+ this.binding?.destroy();
426
+ }
427
+ // ── Pointer entry points (read the live snapshot for the armed tool) ──
428
+ onViewportPointerDown(e) {
429
+ if (e.button !== 0)
430
+ return;
431
+ const board = this.boardOf(e.clientX, e.clientY);
432
+ const tool = this.activeTool;
433
+ if (tool === 'connector')
434
+ this.ctrl.beginConnector(null, board, e.pointerId);
435
+ else if (tool === 'draw')
436
+ this.ctrl.beginDraw(board, e.pointerId);
437
+ else if (tool === 'laser')
438
+ this.ctrl.beginLaser(board, e.pointerId);
439
+ else if (tool === 'comment')
440
+ this.ctrl.beginComment(board, null);
441
+ else if (tool !== 'select')
442
+ this.ctrl.beginCreate(board, e.pointerId);
443
+ else
444
+ this.ctrl.beginMarquee(board, e.shiftKey, e.pointerId); // shapes stopPropagation, so this is empty canvas
445
+ }
446
+ onShapeDown(shapeId, e) {
447
+ if (e.button !== 0)
448
+ return; // primary button only
449
+ const tool = this.activeTool;
450
+ if (tool === 'connector') {
451
+ e.stopPropagation();
452
+ this.ctrl.beginConnector(shapeId, this.boardOf(e.clientX, e.clientY), e.pointerId);
453
+ return;
454
+ }
455
+ if (tool === 'comment') {
456
+ e.stopPropagation();
457
+ this.ctrl.beginComment(this.boardOf(e.clientX, e.clientY), shapeId);
458
+ return;
459
+ }
460
+ if (tool !== 'select')
461
+ return; // a shape tool is armed → bubble to the viewport's create
462
+ e.stopPropagation();
463
+ this.ctrl.beginShapeDrag(shapeId, this.boardOf(e.clientX, e.clientY), e.shiftKey, e.pointerId);
464
+ }
465
+ onShapeDblClick(shapeId, e) {
466
+ e.stopPropagation();
467
+ this.ctrl.openEdit(shapeId, e.currentTarget.getBoundingClientRect());
468
+ }
469
+ onResizeIntent(shapeId, intent) {
470
+ if (intent.event.button !== 0)
471
+ return;
472
+ intent.event.stopPropagation();
473
+ this.ctrl.beginResize(shapeId, this.boardOf(intent.event.clientX, intent.event.clientY), intent.event.pointerId, intent.handle);
474
+ }
475
+ onRotate(shapeId, e) {
476
+ if (e.button !== 0)
477
+ return;
478
+ e.stopPropagation();
479
+ this.ctrl.beginRotate(shapeId, this.boardOf(e.clientX, e.clientY), e.pointerId);
480
+ }
481
+ onKeydown(e) {
482
+ this.ctrl.handleKeydown({
483
+ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey,
484
+ isEditable: isEditableTarget(e.target), preventDefault: () => e.preventDefault(),
485
+ });
486
+ }
487
+ // ── Thin toolbar/action methods (no arrow functions in the template) ──
488
+ onTool(id) { this.ctrl.setTool(id); }
489
+ onBringToFront() { this.ctrl.voidBringToFront(); }
490
+ onSendToBack() { this.ctrl.voidSendToBack(); }
491
+ onToggleLock() { this.ctrl.voidToggleLock(); }
492
+ onGroup() { this.ctrl.voidGroup(); }
493
+ onUngroup() { this.ctrl.voidUngroup(); }
494
+ onDuplicate() { this.ctrl.voidDuplicate(); }
495
+ onDeleteSelection() { this.ctrl.voidDeleteSelection(); }
496
+ onAlign(edge) { this.ctrl.voidAlignSelection(edge); }
497
+ onDistribute(axis) { this.ctrl.voidDistributeSelection(axis); }
498
+ onResetViewport() { this.ctrl.resetViewport(); }
499
+ onUndo() { this.ctrl.voidUndo(); }
500
+ onStyleInput(raw) { this.ctrl.voidSetSelectionStyleFromInput(raw); }
501
+ // ── Comments ──
502
+ onPinClick(id, e) { e.stopPropagation(); this.ctrl.openCommentThread(id); }
503
+ onCommitDraft(text) { this.ctrl.voidCommitComment(text); }
504
+ onCancelDraft() { this.ctrl.cancelComment(); }
505
+ onCloseThread() { this.ctrl.closeCommentThread(); }
506
+ onCommitThread(id, text) { this.ctrl.voidSetCommentText(id, text); this.ctrl.closeCommentThread(); }
507
+ onResolveThread(id) { this.ctrl.voidToggleCommentResolved(id); }
508
+ onDeleteThread(id) { this.ctrl.voidRemoveComment(id); }
509
+ // ── Text dialog ──
510
+ onSaveShape(text) { this.ctrl.voidSaveShape({ text }); }
511
+ onCloseDialog() { this.ctrl.closeDialog(); }
512
+ onDeleteShape(id) { this.ctrl.voidDeleteShape(id); }
513
+ stop(e) { e.stopPropagation(); }
514
+ trackShape(_i, v) { return v.shape.id; }
515
+ trackConnector(_i, c) { return c.id; }
516
+ trackPin(_i, p) { return p.id; }
517
+ trackIndex(i) { return i; }
518
+ static ɵfac = function WhiteboardComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || WhiteboardComponent)(); };
519
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: WhiteboardComponent, selectors: [["bm-whiteboard"]], viewQuery: function WhiteboardComponent_Query(rf, ctx) { if (rf & 1) {
520
+ i0.ɵɵviewQuery(_c0, 5);
521
+ } if (rf & 2) {
522
+ let _t;
523
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.viewportRef = _t.first);
524
+ } }, hostAttrs: [2, "display", "block", "width", "100%", "height", "100%"], inputs: { store: "store", canvasId: "canvasId", config: "config", darkMode: "darkMode", onError: "onError" }, features: [i0.ɵɵNgOnChangesFeature], decls: 48, vars: 46, consts: [["viewportRef", ""], ["role", "application", "tabindex", "0", 1, "bm-wb", 3, "keydown"], [1, "bm-wb__toolbar"], ["type", "button", 3, "class", "click", 4, "ngFor", "ngForOf"], [1, "bm-wb__toolbar-sep"], ["type", "button", 1, "bm-wb__btn", 3, "click", "disabled"], ["type", "button", 1, "bm-wb__btn", "bm-wb__lock-btn", 3, "click", "disabled"], ["type", "button", "data-action", "group", 1, "bm-wb__btn", 3, "click", "disabled"], ["type", "button", "data-action", "ungroup", 1, "bm-wb__btn", 3, "click", "disabled"], ["type", "button", "data-action", "duplicate", 1, "bm-wb__btn", 3, "click", "disabled"], ["type", "button", "class", "bm-wb__btn", 3, "disabled", "click", 4, "ngFor", "ngForOf"], ["type", "button", 1, "bm-wb__btn", 3, "click"], [1, "bm-wb__toolbar-zoom"], ["data-testid", "wb-viewport", 3, "pointerdown", "ngStyle"], [1, "bm-wb__scene"], ["data-testid", "wb-connectors", 1, "bm-wb__connectors"], ["markerWidth", "8", "markerHeight", "8", "refX", "7", "refY", "4", "orient", "auto"], ["d", "M0,0 L7,4 L0,8 Z", "fill", "context-stroke"], ["class", "bm-wb__connector", 4, "ngFor", "ngForOf", "ngForTrackBy"], ["class", "bm-wb__connector-preview", 4, "ngIf"], ["class", "bm-wb__draw-preview", "data-testid", "wb-draw-preview", 4, "ngIf"], ["class", "bm-wb__guide", 4, "ngFor", "ngForOf", "ngForTrackBy"], [3, "view", "dropTarget", "shapePointerDown", "shapeDoubleClick", 4, "ngFor", "ngForOf", "ngForTrackBy"], ["class", "bm-wb__marquee", 3, "left", "top", "width", "height", 4, "ngIf"], ["class", "bm-wb__create-preview", 3, "left", "top", "width", "height", 4, "ngIf"], [3, "view", "resize", "rotate", 4, "ngIf"], ["type", "button", 3, "class", "left", "top", "pointerdown", "click", 4, "ngFor", "ngForOf", "ngForTrackBy"], ["class", "bm-wb__laser-layer", "data-testid", "wb-laser-layer", 4, "ngIf"], [3, "at", "initial", "existing", "resolved", "locale", "commit", "close", 4, "ngIf"], [3, "at", "initial", "existing", "resolved", "locale", "commit", "close", "resolve", "delete", 4, "ngIf"], [3, "style", "locale", "styleInput", 4, "ngIf"], ["class", "bm-wb__toast", "role", "status", 4, "ngIf"], [3, "initial", "locale", "save", "close", "delete", 4, "ngIf"], ["type", "button", 3, "click"], [1, "bm-wb__connector"], [1, "bm-wb__connector-preview"], ["data-testid", "wb-draw-preview", 1, "bm-wb__draw-preview"], [1, "bm-wb__guide"], [3, "shapePointerDown", "shapeDoubleClick", "view", "dropTarget"], [1, "bm-wb__marquee"], [1, "bm-wb__create-preview"], [3, "resize", "rotate", "view"], ["type", "button", 3, "pointerdown", "click"], ["data-testid", "wb-laser-layer", 1, "bm-wb__laser-layer"], ["class", "bm-wb__laser", "data-testid", "wb-laser", 4, "ngFor", "ngForOf", "ngForTrackBy"], ["data-testid", "wb-laser", 1, "bm-wb__laser"], [3, "commit", "close", "at", "initial", "existing", "resolved", "locale"], [3, "commit", "close", "resolve", "delete", "at", "initial", "existing", "resolved", "locale"], [3, "styleInput", "locale"], ["role", "status", 1, "bm-wb__toast"], ["type", "button", 1, "bm-wb__toast-btn", 3, "click"], [3, "save", "close", "delete", "initial", "locale"]], template: function WhiteboardComponent_Template(rf, ctx) { if (rf & 1) {
525
+ const _r1 = i0.ɵɵgetCurrentView();
526
+ i0.ɵɵelementStart(0, "div", 1);
527
+ i0.ɵɵlistener("keydown", function WhiteboardComponent_Template_div_keydown_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onKeydown($event)); });
528
+ i0.ɵɵelementStart(1, "div", 2);
529
+ i0.ɵɵtemplate(2, WhiteboardComponent_button_2_Template, 2, 6, "button", 3);
530
+ i0.ɵɵelement(3, "span", 4);
531
+ i0.ɵɵelementStart(4, "button", 5);
532
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_4_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onBringToFront()); });
533
+ i0.ɵɵtext(5, "\u2B06");
534
+ i0.ɵɵelementEnd();
535
+ i0.ɵɵelementStart(6, "button", 5);
536
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_6_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onSendToBack()); });
537
+ i0.ɵɵtext(7, "\u2B07");
538
+ i0.ɵɵelementEnd();
539
+ i0.ɵɵelementStart(8, "button", 6);
540
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onToggleLock()); });
541
+ i0.ɵɵtext(9, "\uD83D\uDD12");
542
+ i0.ɵɵelementEnd();
543
+ i0.ɵɵelementStart(10, "button", 7);
544
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_10_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onGroup()); });
545
+ i0.ɵɵtext(11, "\u29C9");
546
+ i0.ɵɵelementEnd();
547
+ i0.ɵɵelementStart(12, "button", 8);
548
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_12_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onUngroup()); });
549
+ i0.ɵɵtext(13, "\u229F");
550
+ i0.ɵɵelementEnd();
551
+ i0.ɵɵelementStart(14, "button", 9);
552
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_14_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDuplicate()); });
553
+ i0.ɵɵtext(15, "\u29C9\u207A");
554
+ i0.ɵɵelementEnd();
555
+ i0.ɵɵelementStart(16, "button", 5);
556
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_16_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDeleteSelection()); });
557
+ i0.ɵɵtext(17, "\uD83D\uDDD1");
558
+ i0.ɵɵelementEnd();
559
+ i0.ɵɵelement(18, "span", 4);
560
+ i0.ɵɵtemplate(19, WhiteboardComponent_button_19_Template, 2, 4, "button", 10)(20, WhiteboardComponent_button_20_Template, 2, 4, "button", 10);
561
+ i0.ɵɵelement(21, "span", 4);
562
+ i0.ɵɵelementStart(22, "button", 11);
563
+ i0.ɵɵlistener("click", function WhiteboardComponent_Template_button_click_22_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onResetViewport()); });
564
+ i0.ɵɵtext(23, "\u2316");
565
+ i0.ɵɵelementEnd();
566
+ i0.ɵɵelementStart(24, "span", 12);
567
+ i0.ɵɵtext(25);
568
+ i0.ɵɵelementEnd()();
569
+ i0.ɵɵelementStart(26, "div", 13, 0);
570
+ i0.ɵɵlistener("pointerdown", function WhiteboardComponent_Template_div_pointerdown_26_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onViewportPointerDown($event)); });
571
+ i0.ɵɵelementStart(28, "div", 14);
572
+ i0.ɵɵnamespaceSVG();
573
+ i0.ɵɵelementStart(29, "svg", 15)(30, "defs")(31, "marker", 16);
574
+ i0.ɵɵelement(32, "path", 17);
575
+ i0.ɵɵelementEnd()();
576
+ i0.ɵɵtemplate(33, WhiteboardComponent__svg_path_33_Template, 1, 8, "path", 18)(34, WhiteboardComponent__svg_path_34_Template, 1, 1, "path", 19)(35, WhiteboardComponent__svg_path_35_Template, 1, 1, "path", 20)(36, WhiteboardComponent__svg_line_36_Template, 1, 5, "line", 21);
577
+ i0.ɵɵelementEnd();
578
+ i0.ɵɵtemplate(37, WhiteboardComponent_bm_wb_shape_37_Template, 1, 2, "bm-wb-shape", 22)(38, WhiteboardComponent_div_38_Template, 1, 8, "div", 23)(39, WhiteboardComponent_div_39_Template, 1, 8, "div", 24)(40, WhiteboardComponent_bm_wb_transform_handles_40_Template, 1, 1, "bm-wb-transform-handles", 25)(41, WhiteboardComponent_button_41_Template, 2, 9, "button", 26)(42, WhiteboardComponent__svg_svg_42_Template, 2, 2, "svg", 27);
579
+ i0.ɵɵelementEnd();
580
+ i0.ɵɵtemplate(43, WhiteboardComponent_bm_wb_comment_thread_43_Template, 1, 5, "bm-wb-comment-thread", 28)(44, WhiteboardComponent_bm_wb_comment_thread_44_Template, 1, 5, "bm-wb-comment-thread", 29);
581
+ i0.ɵɵelementEnd();
582
+ i0.ɵɵtemplate(45, WhiteboardComponent_bm_wb_style_panel_45_Template, 1, 3, "bm-wb-style-panel", 30)(46, WhiteboardComponent_div_46_Template, 5, 2, "div", 31)(47, WhiteboardComponent_bm_wb_text_dialog_47_Template, 1, 2, "bm-wb-text-dialog", 32);
583
+ i0.ɵɵelementEnd();
584
+ } if (rf & 2) {
585
+ i0.ɵɵattribute("data-theme", ctx.darkMode ? "dark" : null)("aria-label", ctx.snapshot.ariaLabel);
586
+ i0.ɵɵadvance(2);
587
+ i0.ɵɵproperty("ngForOf", ctx.tools);
588
+ i0.ɵɵadvance(2);
589
+ i0.ɵɵproperty("disabled", !ctx.hasSel);
590
+ i0.ɵɵattribute("title", ctx.locale.bringToFront);
591
+ i0.ɵɵadvance(2);
592
+ i0.ɵɵproperty("disabled", !ctx.hasSel);
593
+ i0.ɵɵattribute("title", ctx.locale.sendToBack);
594
+ i0.ɵɵadvance(2);
595
+ i0.ɵɵproperty("disabled", !ctx.hasSel);
596
+ i0.ɵɵattribute("title", ctx.locale.lock);
597
+ i0.ɵɵadvance(2);
598
+ i0.ɵɵproperty("disabled", !ctx.canGroup);
599
+ i0.ɵɵattribute("title", ctx.locale.group);
600
+ i0.ɵɵadvance(2);
601
+ i0.ɵɵproperty("disabled", !ctx.canUngroup);
602
+ i0.ɵɵattribute("title", ctx.locale.ungroup);
603
+ i0.ɵɵadvance(2);
604
+ i0.ɵɵproperty("disabled", !ctx.hasSel);
605
+ i0.ɵɵattribute("title", ctx.locale.duplicate);
606
+ i0.ɵɵadvance(2);
607
+ i0.ɵɵproperty("disabled", !ctx.hasSel);
608
+ i0.ɵɵattribute("title", ctx.locale.delete);
609
+ i0.ɵɵadvance(3);
610
+ i0.ɵɵproperty("ngForOf", ctx.aligns);
611
+ i0.ɵɵadvance();
612
+ i0.ɵɵproperty("ngForOf", ctx.distributes);
613
+ i0.ɵɵadvance(2);
614
+ i0.ɵɵattribute("title", ctx.locale.resetZoom);
615
+ i0.ɵɵadvance(3);
616
+ i0.ɵɵtextInterpolate1("", ctx.zoomPct, "%");
617
+ i0.ɵɵadvance();
618
+ i0.ɵɵclassMap(ctx.viewportClass);
619
+ i0.ɵɵproperty("ngStyle", ctx.viewportVars);
620
+ i0.ɵɵadvance(2);
621
+ i0.ɵɵstyleProp("transform", ctx.sceneTransform);
622
+ i0.ɵɵadvance(3);
623
+ i0.ɵɵattribute("id", ctx.arrowId);
624
+ i0.ɵɵadvance(2);
625
+ i0.ɵɵproperty("ngForOf", ctx.connectorViews)("ngForTrackBy", ctx.trackConnector);
626
+ i0.ɵɵadvance();
627
+ i0.ɵɵproperty("ngIf", ctx.connectorPreviewD);
628
+ i0.ɵɵadvance();
629
+ i0.ɵɵproperty("ngIf", ctx.drawPreviewPath);
630
+ i0.ɵɵadvance();
631
+ i0.ɵɵproperty("ngForOf", ctx.guideLines)("ngForTrackBy", ctx.trackIndex);
632
+ i0.ɵɵadvance();
633
+ i0.ɵɵproperty("ngForOf", ctx.snapshot.shapes)("ngForTrackBy", ctx.trackShape);
634
+ i0.ɵɵadvance();
635
+ i0.ɵɵproperty("ngIf", ctx.snapshot.marquee);
636
+ i0.ɵɵadvance();
637
+ i0.ɵɵproperty("ngIf", ctx.snapshot.createPreview);
638
+ i0.ɵɵadvance();
639
+ i0.ɵɵproperty("ngIf", ctx.transformView);
640
+ i0.ɵɵadvance();
641
+ i0.ɵɵproperty("ngForOf", ctx.commentPins)("ngForTrackBy", ctx.trackPin);
642
+ i0.ɵɵadvance();
643
+ i0.ɵɵproperty("ngIf", ctx.laserSegments);
644
+ i0.ɵɵadvance();
645
+ i0.ɵɵproperty("ngIf", ctx.draftAt);
646
+ i0.ɵɵadvance();
647
+ i0.ɵɵproperty("ngIf", ctx.openThread);
648
+ i0.ɵɵadvance();
649
+ i0.ɵɵproperty("ngIf", ctx.panelStyle);
650
+ i0.ɵɵadvance();
651
+ i0.ɵɵproperty("ngIf", ctx.undo);
652
+ i0.ɵɵadvance();
653
+ i0.ɵɵproperty("ngIf", ctx.dialogShape);
654
+ } }, dependencies: [CommonModule, i1.NgForOf, i1.NgIf, i1.NgStyle, WhiteboardShapeComponent, TransformHandlesComponent,
655
+ StylePanelComponent, CommentThreadComponent, TextDialogComponent], encapsulation: 2, changeDetection: 0 });
656
+ }
657
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(WhiteboardComponent, [{
658
+ type: Component,
659
+ args: [{
660
+ selector: 'bm-whiteboard',
661
+ standalone: true,
662
+ // The element host defaults to display:inline — `.bm-wb` (height:100%) would then
663
+ // resolve against an unsized box and the viewport wouldn't clip the scene. Fill the
664
+ // parent like `.bm-wb` itself, so a consumer sizes the host's parent (same contract
665
+ // as the React/Vue/Svelte root, where `.bm-wb` IS the root box).
666
+ host: { style: 'display: block; width: 100%; height: 100%' },
667
+ imports: [
668
+ CommonModule, WhiteboardShapeComponent, TransformHandlesComponent,
669
+ StylePanelComponent, CommentThreadComponent, TextDialogComponent,
670
+ ],
671
+ changeDetection: ChangeDetectionStrategy.OnPush,
672
+ template: `
673
+ <div class="bm-wb" [attr.data-theme]="darkMode ? 'dark' : null" role="application" [attr.aria-label]="snapshot.ariaLabel" tabindex="0" (keydown)="onKeydown($event)">
674
+ <div class="bm-wb__toolbar">
675
+ <button
676
+ *ngFor="let t of tools"
677
+ type="button"
678
+ [class]="toolClass(t.id)"
679
+ [attr.title]="locale.tools[t.id]"
680
+ [attr.aria-pressed]="activeTool === t.id"
681
+ [attr.data-tool]="t.id"
682
+ (click)="onTool(t.id)">{{ t.glyph }}</button>
683
+ <span class="bm-wb__toolbar-sep"></span>
684
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.bringToFront" [disabled]="!hasSel" (click)="onBringToFront()">⬆</button>
685
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.sendToBack" [disabled]="!hasSel" (click)="onSendToBack()">⬇</button>
686
+ <button type="button" class="bm-wb__btn bm-wb__lock-btn" [attr.title]="locale.lock" [disabled]="!hasSel" (click)="onToggleLock()">🔒</button>
687
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.group" data-action="group" [disabled]="!canGroup" (click)="onGroup()">⧉</button>
688
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.ungroup" data-action="ungroup" [disabled]="!canUngroup" (click)="onUngroup()">⊟</button>
689
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.duplicate" data-action="duplicate" [disabled]="!hasSel" (click)="onDuplicate()">⧉⁺</button>
690
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.delete" [disabled]="!hasSel" (click)="onDeleteSelection()">🗑</button>
691
+ <span class="bm-wb__toolbar-sep"></span>
692
+ <button
693
+ *ngFor="let a of aligns"
694
+ type="button"
695
+ class="bm-wb__btn"
696
+ [attr.title]="locale.alignLabels[a.edge]"
697
+ [attr.data-align]="a.edge"
698
+ [disabled]="!canAlign"
699
+ (click)="onAlign(a.edge)">{{ a.glyph }}</button>
700
+ <button
701
+ *ngFor="let d of distributes"
702
+ type="button"
703
+ class="bm-wb__btn"
704
+ [attr.title]="locale.distributeLabels[d.axis]"
705
+ [attr.data-distribute]="d.axis"
706
+ [disabled]="!canDistribute"
707
+ (click)="onDistribute(d.axis)">{{ d.glyph }}</button>
708
+ <span class="bm-wb__toolbar-sep"></span>
709
+ <button type="button" class="bm-wb__btn" [attr.title]="locale.resetZoom" (click)="onResetViewport()">⌖</button>
710
+ <span class="bm-wb__toolbar-zoom">{{ zoomPct }}%</span>
711
+ </div>
712
+
713
+ <div
714
+ #viewportRef
715
+ [class]="viewportClass"
716
+ [ngStyle]="viewportVars"
717
+ data-testid="wb-viewport"
718
+ (pointerdown)="onViewportPointerDown($event)">
719
+ <div class="bm-wb__scene" [style.transform]="sceneTransform">
720
+ <svg class="bm-wb__connectors" data-testid="wb-connectors">
721
+ <defs>
722
+ <marker [attr.id]="arrowId" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
723
+ <path d="M0,0 L7,4 L0,8 Z" fill="context-stroke"></path>
724
+ </marker>
725
+ </defs>
726
+ <path
727
+ *ngFor="let c of connectorViews; trackBy: trackConnector"
728
+ class="bm-wb__connector"
729
+ [attr.data-connector-id]="c.id"
730
+ [attr.d]="c.d"
731
+ [attr.stroke]="c.stroke"
732
+ [attr.stroke-width]="c.strokeWidth"
733
+ [attr.stroke-dasharray]="c.dash"
734
+ [attr.opacity]="c.opacity"
735
+ [attr.marker-end]="c.markerEnd"
736
+ [attr.marker-start]="c.markerStart"></path>
737
+ <path *ngIf="connectorPreviewD" class="bm-wb__connector-preview" [attr.d]="connectorPreviewD"></path>
738
+ <path *ngIf="drawPreviewPath" class="bm-wb__draw-preview" data-testid="wb-draw-preview" [attr.d]="drawPreviewPath"></path>
739
+ <line
740
+ *ngFor="let g of guideLines; let i = index; trackBy: trackIndex"
741
+ class="bm-wb__guide"
742
+ [attr.data-guide-axis]="g.axis"
743
+ [attr.x1]="g.x1" [attr.y1]="g.y1" [attr.x2]="g.x2" [attr.y2]="g.y2"></line>
744
+ </svg>
745
+
746
+ <bm-wb-shape
747
+ *ngFor="let v of snapshot.shapes; trackBy: trackShape"
748
+ [view]="v"
749
+ [dropTarget]="snapshot.dropTargetId === v.shape.id"
750
+ (shapePointerDown)="onShapeDown(v.shape.id, $event)"
751
+ (shapeDoubleClick)="onShapeDblClick(v.shape.id, $event)"></bm-wb-shape>
752
+
753
+ <div *ngIf="snapshot.marquee as mq" class="bm-wb__marquee"
754
+ [style.left.px]="mq.x" [style.top.px]="mq.y" [style.width.px]="mq.width" [style.height.px]="mq.height"></div>
755
+ <div *ngIf="snapshot.createPreview as cp" class="bm-wb__create-preview"
756
+ [style.left.px]="cp.x" [style.top.px]="cp.y" [style.width.px]="cp.width" [style.height.px]="cp.height"></div>
757
+
758
+ <bm-wb-transform-handles
759
+ *ngIf="transformView as tv"
760
+ [view]="tv"
761
+ (resize)="onResizeIntent(tv.shape.id, $event)"
762
+ (rotate)="onRotate(tv.shape.id, $event)"></bm-wb-transform-handles>
763
+
764
+ <button
765
+ *ngFor="let p of commentPins; trackBy: trackPin"
766
+ type="button"
767
+ [class]="pinClass(p)"
768
+ [attr.data-comment-id]="p.id"
769
+ [style.left.px]="p.x" [style.top.px]="p.y"
770
+ [attr.title]="p.text"
771
+ (pointerdown)="stop($event)"
772
+ (click)="onPinClick(p.id, $event)">{{ p.glyph }}</button>
773
+
774
+ <svg *ngIf="laserSegments as ls" class="bm-wb__laser-layer" data-testid="wb-laser-layer">
775
+ <line
776
+ *ngFor="let seg of ls; let i = index; trackBy: trackIndex"
777
+ class="bm-wb__laser"
778
+ data-testid="wb-laser"
779
+ [attr.x1]="seg.x1" [attr.y1]="seg.y1" [attr.x2]="seg.x2" [attr.y2]="seg.y2"
780
+ [attr.stroke-opacity]="seg.opacity"></line>
781
+ </svg>
782
+ </div>
783
+
784
+ <bm-wb-comment-thread
785
+ *ngIf="draftAt as at"
786
+ [at]="at"
787
+ [initial]="''"
788
+ [existing]="false"
789
+ [resolved]="false"
790
+ [locale]="locale"
791
+ (commit)="onCommitDraft($event)"
792
+ (close)="onCancelDraft()"></bm-wb-comment-thread>
793
+
794
+ <bm-wb-comment-thread
795
+ *ngIf="openThread as ot"
796
+ [at]="ot.at"
797
+ [initial]="ot.text"
798
+ [existing]="true"
799
+ [resolved]="ot.resolved"
800
+ [locale]="locale"
801
+ (commit)="onCommitThread(ot.id, $event)"
802
+ (close)="onCloseThread()"
803
+ (resolve)="onResolveThread(ot.id)"
804
+ (delete)="onDeleteThread(ot.id)"></bm-wb-comment-thread>
805
+ </div>
806
+
807
+ <bm-wb-style-panel
808
+ *ngIf="panelStyle as ps"
809
+ [style]="ps"
810
+ [locale]="locale"
811
+ (styleInput)="onStyleInput($event)"></bm-wb-style-panel>
812
+
813
+ <div *ngIf="undo as u" class="bm-wb__toast" role="status">
814
+ <span>{{ u.description }}</span>
815
+ <button type="button" class="bm-wb__toast-btn" (click)="onUndo()">{{ locale.undo }}</button>
816
+ </div>
817
+
818
+ <bm-wb-text-dialog
819
+ *ngIf="dialogShape as ds"
820
+ [initial]="ds.text"
821
+ [locale]="locale"
822
+ (save)="onSaveShape($event)"
823
+ (close)="onCloseDialog()"
824
+ (delete)="onDeleteShape(ds.id)"></bm-wb-text-dialog>
825
+ </div>
826
+ `,
827
+ }]
828
+ }], null, { store: [{
829
+ type: Input,
830
+ args: [{ required: true }]
831
+ }], canvasId: [{
832
+ type: Input,
833
+ args: [{ required: true }]
834
+ }], config: [{
835
+ type: Input
836
+ }], darkMode: [{
837
+ type: Input
838
+ }], onError: [{
839
+ type: Input
840
+ }], viewportRef: [{
841
+ type: ViewChild,
842
+ args: ['viewportRef']
843
+ }] }); })();
844
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(WhiteboardComponent, { className: "WhiteboardComponent", filePath: "components/whiteboard.ts", lineNumber: 229 }); })();
845
+ //# sourceMappingURL=Whiteboard.js.map