@kibee/renderer-three 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1913 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ThreeBeeRenderer: () => ThreeBeeRenderer,
34
+ applyLegacyBeeMaterialCompat: () => applyLegacyBeeMaterialCompat
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/ThreeBeeRenderer.ts
39
+ var THREE2 = __toESM(require("three"));
40
+ var import_gsap = require("gsap");
41
+ var import_GLTFLoader = require("three/examples/jsm/loaders/GLTFLoader.js");
42
+
43
+ // src/gltf/registerPbrSpecularGlossinessGLTFPlugin.ts
44
+ var import_three = require("three");
45
+ var EXT = "KHR_materials_pbrSpecularGlossiness";
46
+ function registerPbrSpecularGlossinessGLTFPlugin(loader) {
47
+ loader.register((parser) => ({
48
+ name: EXT,
49
+ extendMaterialParams(materialIndex, materialParams) {
50
+ const materialDef = parser.json.materials?.[materialIndex];
51
+ const ext = materialDef?.extensions?.[EXT];
52
+ if (!ext) return Promise.resolve();
53
+ const pending = [];
54
+ if (Array.isArray(ext.diffuseFactor) && ext.diffuseFactor.length >= 3) {
55
+ const c = new import_three.Color().setRGB(
56
+ ext.diffuseFactor[0],
57
+ ext.diffuseFactor[1],
58
+ ext.diffuseFactor[2],
59
+ import_three.LinearSRGBColorSpace
60
+ );
61
+ materialParams.color = c;
62
+ if (ext.diffuseFactor[3] !== void 0) {
63
+ materialParams.opacity = ext.diffuseFactor[3];
64
+ materialParams.transparent = ext.diffuseFactor[3] < 1;
65
+ }
66
+ }
67
+ if (ext.diffuseTexture !== void 0) {
68
+ pending.push(
69
+ parser.assignTexture(
70
+ materialParams,
71
+ "map",
72
+ ext.diffuseTexture,
73
+ import_three.SRGBColorSpace
74
+ )
75
+ );
76
+ }
77
+ const gloss = ext.glossinessFactor ?? 1;
78
+ materialParams.roughness = Math.min(1, Math.max(0.04, 1 - gloss));
79
+ materialParams.metalness = 0;
80
+ return Promise.all(pending);
81
+ }
82
+ }));
83
+ }
84
+
85
+ // src/legacyBeeMaterial.ts
86
+ var THREE = __toESM(require("three"));
87
+ function toMeshArray(node) {
88
+ const meshes = [];
89
+ node.traverse((child) => {
90
+ if (child instanceof THREE.Mesh) {
91
+ meshes.push(child);
92
+ }
93
+ });
94
+ return meshes;
95
+ }
96
+ async function applyLegacyBeeMaterialCompat(gltf) {
97
+ const parser = gltf.parser;
98
+ const materialDef = parser?.json?.materials?.[0];
99
+ const legacyDef = materialDef?.extensions?.KHR_materials_pbrSpecularGlossiness;
100
+ if (!parser?.loadTexture || !legacyDef?.diffuseTexture) {
101
+ return;
102
+ }
103
+ const diffuseMap = await parser.loadTexture(legacyDef.diffuseTexture.index);
104
+ const normalMap = materialDef?.normalTexture ? await parser.loadTexture(materialDef.normalTexture.index) : null;
105
+ const aoMap = materialDef?.occlusionTexture ? await parser.loadTexture(materialDef.occlusionTexture.index) : null;
106
+ diffuseMap.colorSpace = THREE.SRGBColorSpace;
107
+ const meshes = toMeshArray(gltf.scene);
108
+ for (const mesh of meshes) {
109
+ const previousMaterial = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
110
+ const replacement = new THREE.MeshStandardMaterial({
111
+ map: diffuseMap,
112
+ normalMap,
113
+ aoMap,
114
+ roughness: 0.76,
115
+ metalness: 0.02,
116
+ transparent: true,
117
+ alphaTest: 0.08,
118
+ side: materialDef?.doubleSided ? THREE.DoubleSide : THREE.FrontSide
119
+ });
120
+ if (mesh.geometry.getAttribute("uv") && !mesh.geometry.getAttribute("uv2")) {
121
+ mesh.geometry.setAttribute("uv2", mesh.geometry.getAttribute("uv"));
122
+ }
123
+ mesh.material = replacement;
124
+ if (previousMaterial && "dispose" in previousMaterial) {
125
+ previousMaterial.dispose();
126
+ }
127
+ }
128
+ }
129
+
130
+ // src/ThreeBeeRenderer.ts
131
+ var CLIP_BY_STATE = {
132
+ idle: "idle",
133
+ guiding: "hover",
134
+ thinking: "hover",
135
+ celebrating: "take_off_and_land"
136
+ };
137
+ var ThreeBeeRenderer = class {
138
+ constructor(options = {}) {
139
+ this.options = options;
140
+ this.dockClickBehavior = options.dockClickBehavior ?? "undock";
141
+ this.restorePersistedDockPosition = options.restorePersistedDockPosition !== false;
142
+ if (this.restorePersistedDockPosition) {
143
+ try {
144
+ const saved = localStorage.getItem("kibee:dock_pos");
145
+ if (saved) {
146
+ const pos = JSON.parse(saved);
147
+ if (typeof pos.x === "number" && typeof pos.y === "number") {
148
+ this.customDockPos = pos;
149
+ }
150
+ }
151
+ } catch {
152
+ }
153
+ }
154
+ }
155
+ options;
156
+ root;
157
+ canvasWrap;
158
+ tooltip;
159
+ hoverZone;
160
+ dockButton;
161
+ scene;
162
+ camera;
163
+ renderer;
164
+ beeGroup = new THREE2.Group();
165
+ visualRig = new THREE2.Group();
166
+ mixer = null;
167
+ actions = /* @__PURE__ */ new Map();
168
+ activeActionName = null;
169
+ runtimeWings = null;
170
+ mounted = false;
171
+ /** Deduplicates concurrent mount() calls (React Strict Mode, double init, etc.) */
172
+ mountPromise = null;
173
+ disposed = false;
174
+ animationFrameId = null;
175
+ clock = new THREE2.Clock();
176
+ state = "idle";
177
+ currentX = 120;
178
+ currentY = 120;
179
+ currentFacing = "right";
180
+ docked = true;
181
+ dockVisible = true;
182
+ hoveringHome = false;
183
+ reducedMotion = false;
184
+ renderingAvailable = false;
185
+ idleTween = null;
186
+ celebrateTween = null;
187
+ tooltipPulseTimer = null;
188
+ tooltipMessage = "";
189
+ actionHandler = null;
190
+ dockClickBehavior;
191
+ autoDockTimer = null;
192
+ customDockPos = null;
193
+ // Radial action buttons (individually positioned, not wrapped)
194
+ radialButtons = [];
195
+ // Drag state
196
+ isDragging = false;
197
+ dragStartX = 0;
198
+ dragStartY = 0;
199
+ dragMoved = false;
200
+ /** Pointer went down while docked — used to distinguish dock tap vs drag-out */
201
+ dragArmedFromDock = false;
202
+ // Debounce timer — prevents hiding buttons while cursor travels to them
203
+ hideButtonsTimer = null;
204
+ // Whether the bee is currently parked at the chat panel header
205
+ panelOpen = false;
206
+ /** When false, constructor skips LS restore and setCustomDockPosition skips LS writes by default. */
207
+ restorePersistedDockPosition;
208
+ // Tooltip nav elements (created in initDOM)
209
+ tooltipText;
210
+ tooltipNav;
211
+ navPrevBtn;
212
+ navLabel;
213
+ navNextBtn;
214
+ tooltipHasNav = false;
215
+ // Inline prompt (used when capturing a 1-2 line note for honey/sting)
216
+ tooltipPrompt;
217
+ tooltipPromptInput;
218
+ tooltipPromptSaveBtn;
219
+ tooltipPromptSkipBtn;
220
+ tooltipPromptHint;
221
+ tooltipPromptActive = false;
222
+ tooltipPromptMaxLen = 240;
223
+ async mount(container = document.body) {
224
+ if (this.mounted) return;
225
+ if (this.mountPromise) {
226
+ await this.mountPromise;
227
+ return;
228
+ }
229
+ this.mountPromise = this.performMount(container);
230
+ try {
231
+ await this.mountPromise;
232
+ } finally {
233
+ this.mountPromise = null;
234
+ }
235
+ }
236
+ async performMount(container) {
237
+ this.reducedMotion = this.shouldReduceMotion();
238
+ this.disposed = false;
239
+ this.createRoot(container);
240
+ this.createScene();
241
+ this.createCamera();
242
+ this.createRenderer();
243
+ this.scene.add(this.beeGroup);
244
+ this.beeGroup.add(this.visualRig);
245
+ if (this.renderingAvailable) {
246
+ this.createLights();
247
+ const loaded = await this.loadBeeModel();
248
+ if (!loaded) {
249
+ this.createFallbackBee();
250
+ }
251
+ }
252
+ this.resize();
253
+ this.initializeDockedHome();
254
+ if (this.renderingAvailable) {
255
+ this.startRenderLoop();
256
+ }
257
+ this.mounted = true;
258
+ document.body.dataset.kibeeRendererActive = "true";
259
+ this.state = "guiding";
260
+ this.setState("idle");
261
+ window.addEventListener("resize", this.handleResize, { passive: true });
262
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
263
+ window.addEventListener("pointermove", this.handlePointerMove, {
264
+ passive: true
265
+ });
266
+ window.addEventListener("pointerup", this.handlePointerUp);
267
+ window.addEventListener("kibee-panel-open", this.handlePanelOpen);
268
+ window.addEventListener("kibee-panel-close", this.handlePanelClose);
269
+ }
270
+ async moveTo(x, y, options = {}) {
271
+ const duration = options.duration ?? 0.9;
272
+ const offsetX = options.offsetX ?? 16;
273
+ const offsetY = options.offsetY ?? -18;
274
+ const targetX = x + offsetX;
275
+ const targetY = y + offsetY;
276
+ await this.undock();
277
+ this.hoveringHome = false;
278
+ this.applyFacing(targetX >= this.currentX ? "right" : "left");
279
+ this.setState("guiding");
280
+ return new Promise((resolve) => {
281
+ import_gsap.gsap.to(this.beeGroup.scale, {
282
+ x: this.getActiveScale(),
283
+ y: this.getActiveScale(),
284
+ z: this.getActiveScale(),
285
+ duration: 0.24,
286
+ ease: "power2.out",
287
+ overwrite: "auto"
288
+ });
289
+ import_gsap.gsap.to(this, {
290
+ currentX: targetX,
291
+ currentY: targetY,
292
+ duration,
293
+ ease: "power2.out",
294
+ overwrite: "auto",
295
+ onUpdate: () => {
296
+ this.updateBeeScreenPosition();
297
+ this.positionTooltipNearBee();
298
+ },
299
+ onComplete: () => {
300
+ this.setState("idle");
301
+ resolve();
302
+ }
303
+ });
304
+ });
305
+ }
306
+ async dock() {
307
+ if (!this.isDockEnabled()) return;
308
+ if (this.docked) return;
309
+ this.clearAutoDockTimer();
310
+ this.hideMessage();
311
+ this.clearHideButtonsTimer();
312
+ this.hideRadialButtons();
313
+ const dock = this.getDockCenter();
314
+ this.dockVisible = true;
315
+ this.hoveringHome = false;
316
+ this.stopStateTweens();
317
+ this.applyHomeFacing();
318
+ this.syncDockState();
319
+ if (this.reducedMotion) {
320
+ this.currentX = dock.x;
321
+ this.currentY = dock.y;
322
+ this.docked = true;
323
+ this.beeGroup.scale.setScalar(this.getDockedScale());
324
+ this.updateBeeScreenPosition();
325
+ this.syncDockState();
326
+ if (this.state === "idle") {
327
+ this.startIdleMotion();
328
+ }
329
+ return;
330
+ }
331
+ return new Promise((resolve) => {
332
+ import_gsap.gsap.timeline({
333
+ onComplete: () => {
334
+ this.currentX = dock.x;
335
+ this.currentY = dock.y;
336
+ this.docked = true;
337
+ this.syncDockState();
338
+ if (this.state === "idle") {
339
+ this.startIdleMotion();
340
+ }
341
+ resolve();
342
+ }
343
+ }).to(
344
+ this,
345
+ {
346
+ currentX: dock.x,
347
+ currentY: dock.y,
348
+ duration: 0.58,
349
+ ease: "power2.inOut",
350
+ overwrite: "auto",
351
+ onUpdate: () => {
352
+ this.updateBeeScreenPosition();
353
+ this.positionTooltipNearBee();
354
+ }
355
+ },
356
+ 0
357
+ ).to(
358
+ this.beeGroup.scale,
359
+ {
360
+ x: this.getDockedScale(),
361
+ y: this.getDockedScale(),
362
+ z: this.getDockedScale(),
363
+ duration: 0.58,
364
+ ease: "power2.inOut",
365
+ overwrite: "auto"
366
+ },
367
+ 0
368
+ ).to(
369
+ this.beeGroup.rotation,
370
+ {
371
+ z: 0.02,
372
+ duration: 0.2,
373
+ ease: "sine.out",
374
+ overwrite: "auto"
375
+ },
376
+ 0
377
+ );
378
+ });
379
+ }
380
+ resetAutoDockTimer() {
381
+ if (this.autoDockTimer) clearTimeout(this.autoDockTimer);
382
+ this.autoDockTimer = setTimeout(() => {
383
+ if (!this.docked && this.state === "idle") {
384
+ void this.dock();
385
+ }
386
+ }, 1e4);
387
+ }
388
+ clearAutoDockTimer() {
389
+ if (this.autoDockTimer) {
390
+ clearTimeout(this.autoDockTimer);
391
+ this.autoDockTimer = null;
392
+ }
393
+ }
394
+ async undock(options = {}) {
395
+ if (!this.isDockEnabled()) return;
396
+ this.resetAutoDockTimer();
397
+ const hideDock = options.hideDock ?? true;
398
+ this.docked = false;
399
+ this.dockVisible = !hideDock;
400
+ this.hoveringHome = !hideDock;
401
+ this.syncDockState();
402
+ if (hideDock) {
403
+ if (this.reducedMotion) {
404
+ this.beeGroup.scale.setScalar(this.getActiveScale());
405
+ return;
406
+ }
407
+ return new Promise((resolve) => {
408
+ import_gsap.gsap.to(this.beeGroup.scale, {
409
+ x: this.getActiveScale(),
410
+ y: this.getActiveScale(),
411
+ z: this.getActiveScale(),
412
+ duration: 0.24,
413
+ ease: "power2.out",
414
+ overwrite: "auto",
415
+ onComplete: resolve
416
+ });
417
+ });
418
+ }
419
+ const wake = this.getWakeHoverPosition();
420
+ this.stopStateTweens();
421
+ this.applyHomeFacing();
422
+ if (this.reducedMotion) {
423
+ this.currentX = wake.x;
424
+ this.currentY = wake.y;
425
+ this.updateBeeScreenPosition();
426
+ this.beeGroup.scale.setScalar(this.getHomeHoverScale());
427
+ if (this.state === "idle") {
428
+ this.startIdleMotion();
429
+ }
430
+ return;
431
+ }
432
+ return new Promise((resolve) => {
433
+ import_gsap.gsap.timeline({
434
+ onComplete: () => {
435
+ this.currentX = wake.x;
436
+ this.currentY = wake.y;
437
+ if (this.state === "idle") {
438
+ this.startIdleMotion();
439
+ }
440
+ resolve();
441
+ }
442
+ }).to(
443
+ this,
444
+ {
445
+ currentX: wake.x,
446
+ currentY: wake.y,
447
+ duration: 0.36,
448
+ ease: "power2.out",
449
+ overwrite: "auto",
450
+ onUpdate: () => {
451
+ this.updateBeeScreenPosition();
452
+ this.positionTooltipNearBee();
453
+ }
454
+ },
455
+ 0
456
+ ).to(
457
+ this.beeGroup.scale,
458
+ {
459
+ x: this.getHomeHoverScale(),
460
+ y: this.getHomeHoverScale(),
461
+ z: this.getHomeHoverScale(),
462
+ duration: 0.36,
463
+ ease: "power2.out",
464
+ overwrite: "auto"
465
+ },
466
+ 0
467
+ );
468
+ });
469
+ }
470
+ isDocked() {
471
+ return this.docked;
472
+ }
473
+ setState(state) {
474
+ if (!this.mounted) return;
475
+ if (this.state === state) return;
476
+ this.state = state;
477
+ this.syncDockState();
478
+ this.stopStateTweens();
479
+ if (state !== "idle") {
480
+ this.clearHideButtonsTimer();
481
+ this.hideRadialButtons();
482
+ if (this.panelOpen) {
483
+ window.dispatchEvent(new CustomEvent("kibee-panel-close"));
484
+ this.panelOpen = false;
485
+ }
486
+ }
487
+ switch (state) {
488
+ case "idle":
489
+ this.startIdleMotion();
490
+ break;
491
+ case "guiding":
492
+ this.playClipForState("guiding");
493
+ import_gsap.gsap.to(this.beeGroup.rotation, {
494
+ z: this.reducedMotion ? 0.04 : 0.08,
495
+ duration: 0.22,
496
+ ease: "power1.out",
497
+ overwrite: "auto"
498
+ });
499
+ break;
500
+ case "thinking":
501
+ this.startThinkingMotion();
502
+ break;
503
+ case "celebrating":
504
+ this.startCelebrateMotion();
505
+ break;
506
+ }
507
+ }
508
+ speak(message, options = {}) {
509
+ if (!this.mounted) return;
510
+ if (this.docked) return;
511
+ if (this.panelOpen) {
512
+ this.tooltipMessage = message;
513
+ return;
514
+ }
515
+ this.clearTooltipPulse();
516
+ this.tooltipMessage = message;
517
+ this.tooltipText.textContent = message;
518
+ this.tooltip.style.display = "block";
519
+ this.tooltip.classList.remove("kibee-tooltip--hidden");
520
+ this.tooltip.classList.add("kibee-tooltip--visible");
521
+ const hasNav = options.onPrev !== void 0 || options.onNext !== void 0;
522
+ this.tooltipHasNav = hasNav;
523
+ if (hasNav) {
524
+ this.navPrevBtn.onclick = options.onPrev ?? null;
525
+ this.navPrevBtn.style.visibility = options.onPrev ? "visible" : "hidden";
526
+ this.navNextBtn.onclick = options.onNext ?? null;
527
+ this.navNextBtn.style.visibility = options.onNext ? "visible" : "hidden";
528
+ this.navLabel.textContent = options.stepLabel ?? "";
529
+ this.tooltipNav.style.display = "flex";
530
+ this.tooltip.style.pointerEvents = "auto";
531
+ } else {
532
+ this.tooltipHasNav = false;
533
+ this.tooltipNav.style.display = "none";
534
+ this.tooltip.style.pointerEvents = "";
535
+ }
536
+ if (options.maxWidth) {
537
+ this.tooltip.style.maxWidth = `${options.maxWidth}px`;
538
+ }
539
+ if (typeof options.x === "number" && typeof options.y === "number") {
540
+ this.tooltip.style.left = `${options.x}px`;
541
+ this.tooltip.style.top = `${options.y}px`;
542
+ if (options.arrowEdge) {
543
+ this.tooltip.dataset.arrowEdge = options.arrowEdge;
544
+ } else {
545
+ this.inferTooltipArrowEdgeForPlacedTooltip(options.x);
546
+ }
547
+ } else {
548
+ this.positionTooltipNearBee();
549
+ }
550
+ this.startTooltipPulse();
551
+ }
552
+ hideMessage() {
553
+ if (!this.mounted) return;
554
+ this.clearTooltipPulse();
555
+ this.tooltipMessage = "";
556
+ this.tooltip.classList.remove("kibee-tooltip--visible");
557
+ this.tooltip.classList.add("kibee-tooltip--hidden");
558
+ this.tooltip.style.display = "none";
559
+ this.tooltip.style.pointerEvents = "";
560
+ this.tooltipHasNav = false;
561
+ this.tooltipNav.style.display = "none";
562
+ this.navPrevBtn.onclick = null;
563
+ this.navNextBtn.onclick = null;
564
+ delete this.tooltip.dataset.arrowEdge;
565
+ this.tooltipPrompt.style.display = "none";
566
+ this.tooltipPromptActive = false;
567
+ this.tooltipPromptSaveBtn.onclick = null;
568
+ this.tooltipPromptSkipBtn.onclick = null;
569
+ this.tooltipPromptInput.value = "";
570
+ this.tooltip.classList.remove("kibee-tooltip--prompt");
571
+ }
572
+ /**
573
+ * Show an inline 1-2 line prompt inside the bee's speech bubble. Used by
574
+ * the bookmark + issue chips to capture a short note before saving.
575
+ * Resolves to the user's note (trimmed) or null if they skipped / dismissed.
576
+ */
577
+ prompt(message, options = {}) {
578
+ if (!this.mounted) return Promise.resolve(null);
579
+ return new Promise((resolve) => {
580
+ this.speak(message, {});
581
+ this.clearTooltipPulse();
582
+ this.tooltipHasNav = true;
583
+ this.tooltip.style.pointerEvents = "auto";
584
+ this.tooltip.classList.add("kibee-tooltip--prompt");
585
+ this.tooltipPrompt.style.display = "flex";
586
+ this.tooltipPromptActive = true;
587
+ this.tooltipPromptInput.value = "";
588
+ if (options.placeholder) {
589
+ this.tooltipPromptInput.placeholder = options.placeholder;
590
+ }
591
+ this.updatePromptHint();
592
+ this.positionTooltipNearBee();
593
+ const finish = (note) => {
594
+ if (!this.tooltipPromptActive) return;
595
+ this.tooltipPromptActive = false;
596
+ this.tooltipPromptSaveBtn.onclick = null;
597
+ this.tooltipPromptSkipBtn.onclick = null;
598
+ this.tooltipPrompt.style.display = "none";
599
+ this.tooltipPromptInput.value = "";
600
+ this.tooltip.classList.remove("kibee-tooltip--prompt");
601
+ resolve(note);
602
+ };
603
+ this.tooltipPromptSaveBtn.onclick = () => {
604
+ const value = this.tooltipPromptInput.value.trim();
605
+ finish(value || "");
606
+ };
607
+ this.tooltipPromptSkipBtn.onclick = () => finish(null);
608
+ requestAnimationFrame(() => this.tooltipPromptInput.focus());
609
+ });
610
+ }
611
+ /** Swap the prompt's bottom-left hint between the keyboard hint (when empty)
612
+ * and a tabular char counter (once the user starts typing). */
613
+ updatePromptHint() {
614
+ const len = this.tooltipPromptInput.value.length;
615
+ if (len === 0) {
616
+ this.tooltipPromptHint.innerHTML = `<kbd>\u23CE</kbd> save \xB7 <kbd>esc</kbd> skip`;
617
+ return;
618
+ }
619
+ const max = this.tooltipPromptMaxLen;
620
+ this.tooltipPromptHint.textContent = `${len} / ${max}`;
621
+ }
622
+ inferTooltipArrowEdgeForPlacedTooltip(tooltipLeft) {
623
+ requestAnimationFrame(() => {
624
+ if (!this.tooltip || this.tooltip.style.display === "none") return;
625
+ const tw = this.tooltip.offsetWidth || 220;
626
+ const midX = tooltipLeft + tw / 2;
627
+ this.tooltip.dataset.arrowEdge = midX >= this.currentX ? "left" : "right";
628
+ });
629
+ }
630
+ showActions(handler) {
631
+ if (!this.mounted) return;
632
+ this.actionHandler = handler;
633
+ this.positionHoverZone();
634
+ }
635
+ hideActions() {
636
+ if (!this.mounted) return;
637
+ this.clearHideButtonsTimer();
638
+ this.actionHandler = null;
639
+ this.hideRadialButtons();
640
+ this.positionHoverZone();
641
+ }
642
+ resize() {
643
+ if (!this.root) return;
644
+ const width = window.innerWidth;
645
+ const height = window.innerHeight;
646
+ if (this.renderingAvailable && this.renderer && this.camera) {
647
+ this.renderer.setSize(width, height, false);
648
+ this.camera.left = 0;
649
+ this.camera.right = width;
650
+ this.camera.top = 0;
651
+ this.camera.bottom = height;
652
+ this.camera.near = -1e3;
653
+ this.camera.far = 1e3;
654
+ this.camera.position.set(0, 0, 10);
655
+ this.camera.lookAt(0, 0, 0);
656
+ this.camera.updateProjectionMatrix();
657
+ }
658
+ if (this.isDockEnabled()) {
659
+ if (this.docked) {
660
+ const dock = this.getDockCenter();
661
+ this.currentX = dock.x;
662
+ this.currentY = dock.y;
663
+ } else if (this.hoveringHome) {
664
+ const wake = this.getWakeHoverPosition();
665
+ this.currentX = wake.x;
666
+ this.currentY = wake.y;
667
+ }
668
+ this.syncDockState();
669
+ }
670
+ this.updateBeeScreenPosition();
671
+ this.positionTooltipNearBee();
672
+ }
673
+ setDockClickBehavior(mode) {
674
+ this.dockClickBehavior = mode;
675
+ this.updateDockAria();
676
+ this.syncDockState();
677
+ }
678
+ destroy() {
679
+ this.mountPromise = null;
680
+ window.removeEventListener("resize", this.handleResize);
681
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
682
+ window.removeEventListener("pointermove", this.handlePointerMove);
683
+ window.removeEventListener("pointerup", this.handlePointerUp);
684
+ window.removeEventListener("kibee-panel-open", this.handlePanelOpen);
685
+ window.removeEventListener("kibee-panel-close", this.handlePanelClose);
686
+ delete document.body.dataset.kibeeRendererActive;
687
+ this.clearHideButtonsTimer();
688
+ this.clearAutoDockTimer();
689
+ this.disposed = true;
690
+ if (this.animationFrameId !== null) {
691
+ cancelAnimationFrame(this.animationFrameId);
692
+ this.animationFrameId = null;
693
+ }
694
+ this.stopStateTweens();
695
+ import_gsap.gsap.killTweensOf(this);
696
+ this.dockButton?.removeEventListener("click", this.handleDockClick);
697
+ if (this.renderer) {
698
+ this.renderer.dispose();
699
+ }
700
+ this.disposeSceneObjects();
701
+ if (this.root?.parentNode) {
702
+ this.root.parentNode.removeChild(this.root);
703
+ }
704
+ this.mounted = false;
705
+ }
706
+ createRoot(container) {
707
+ document.querySelectorAll(".kibee-root").forEach((el) => {
708
+ el.remove();
709
+ });
710
+ this.root = document.createElement("div");
711
+ this.root.className = "kibee-root";
712
+ const pos = this.options.dockPosition ?? "bottom-right";
713
+ if (typeof pos === "object" && "inline" in pos) {
714
+ this.root.classList.add("kibee-root--inline");
715
+ }
716
+ this.root.style.setProperty("--kibee-dock-size", `${this.getDockSize()}px`);
717
+ const cssVars = this.resolveDockCSSVars();
718
+ this.root.style.setProperty("--kibee-dock-right", cssVars.right);
719
+ this.root.style.setProperty("--kibee-dock-bottom", cssVars.bottom);
720
+ this.root.style.setProperty("--kibee-dock-left", cssVars.left);
721
+ this.root.style.setProperty("--kibee-dock-top", cssVars.top);
722
+ this.canvasWrap = document.createElement("div");
723
+ this.canvasWrap.className = "kibee-canvas-wrap";
724
+ this.tooltip = document.createElement("div");
725
+ this.tooltip.className = "kibee-tooltip";
726
+ this.tooltip.style.display = "none";
727
+ const closeBtn = document.createElement("button");
728
+ closeBtn.type = "button";
729
+ closeBtn.className = "kibee-tooltip-close";
730
+ closeBtn.setAttribute("aria-label", "Dismiss");
731
+ closeBtn.innerHTML = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true"><path d="M9 1 1 9M1 1l8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`;
732
+ closeBtn.addEventListener("click", () => {
733
+ this.hideMessage();
734
+ void this.dock();
735
+ });
736
+ this.tooltip.appendChild(closeBtn);
737
+ this.tooltipText = document.createElement("div");
738
+ this.tooltipText.className = "kibee-tooltip-body";
739
+ this.tooltip.appendChild(this.tooltipText);
740
+ this.tooltipNav = document.createElement("div");
741
+ this.tooltipNav.className = "kibee-tooltip-nav";
742
+ this.tooltipNav.style.display = "none";
743
+ this.navPrevBtn = document.createElement("button");
744
+ this.navPrevBtn.type = "button";
745
+ this.navPrevBtn.className = "kibee-nav-btn";
746
+ this.navPrevBtn.setAttribute("aria-label", "Previous step");
747
+ this.navPrevBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M8 2 4 6l4 4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
748
+ this.navLabel = document.createElement("span");
749
+ this.navLabel.className = "kibee-nav-label";
750
+ this.navNextBtn = document.createElement("button");
751
+ this.navNextBtn.type = "button";
752
+ this.navNextBtn.className = "kibee-nav-btn";
753
+ this.navNextBtn.setAttribute("aria-label", "Next step");
754
+ this.navNextBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
755
+ this.tooltipNav.appendChild(this.navPrevBtn);
756
+ this.tooltipNav.appendChild(this.navLabel);
757
+ this.tooltipNav.appendChild(this.navNextBtn);
758
+ this.tooltip.appendChild(this.tooltipNav);
759
+ this.tooltipPrompt = document.createElement("div");
760
+ this.tooltipPrompt.className = "kibee-tooltip-prompt";
761
+ this.tooltipPrompt.style.display = "none";
762
+ this.tooltipPromptInput = document.createElement("textarea");
763
+ this.tooltipPromptInput.className = "kibee-tooltip-prompt-input";
764
+ this.tooltipPromptInput.rows = 2;
765
+ this.tooltipPromptInput.maxLength = this.tooltipPromptMaxLen;
766
+ this.tooltipPromptInput.placeholder = "One line or two\u2026";
767
+ this.tooltipPromptInput.spellcheck = true;
768
+ this.tooltipPromptInput.addEventListener("pointerdown", (e) => e.stopPropagation());
769
+ this.tooltipPromptInput.addEventListener("input", () => this.updatePromptHint());
770
+ this.tooltipPromptInput.addEventListener("keydown", (e) => {
771
+ e.stopPropagation();
772
+ if (e.key === "Enter" && !e.shiftKey) {
773
+ e.preventDefault();
774
+ this.tooltipPromptSaveBtn.click();
775
+ } else if (e.key === "Escape") {
776
+ e.preventDefault();
777
+ this.tooltipPromptSkipBtn.click();
778
+ }
779
+ });
780
+ this.tooltipPrompt.appendChild(this.tooltipPromptInput);
781
+ const promptRow = document.createElement("div");
782
+ promptRow.className = "kibee-tooltip-prompt-row";
783
+ this.tooltipPromptHint = document.createElement("span");
784
+ this.tooltipPromptHint.className = "kibee-tooltip-prompt-hint";
785
+ this.tooltipPromptHint.innerHTML = `<kbd>\u23CE</kbd> save \xB7 <kbd>esc</kbd> skip`;
786
+ promptRow.appendChild(this.tooltipPromptHint);
787
+ const promptActions = document.createElement("div");
788
+ promptActions.className = "kibee-tooltip-prompt-actions";
789
+ this.tooltipPromptSkipBtn = document.createElement("button");
790
+ this.tooltipPromptSkipBtn.type = "button";
791
+ this.tooltipPromptSkipBtn.className = "kibee-tooltip-prompt-skip";
792
+ this.tooltipPromptSkipBtn.textContent = "Skip";
793
+ this.tooltipPromptSkipBtn.addEventListener("click", (e) => e.stopPropagation());
794
+ this.tooltipPromptSkipBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
795
+ this.tooltipPromptSaveBtn = document.createElement("button");
796
+ this.tooltipPromptSaveBtn.type = "button";
797
+ this.tooltipPromptSaveBtn.className = "kibee-tooltip-prompt-save";
798
+ this.tooltipPromptSaveBtn.textContent = "Save";
799
+ this.tooltipPromptSaveBtn.addEventListener("click", (e) => e.stopPropagation());
800
+ this.tooltipPromptSaveBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
801
+ promptActions.appendChild(this.tooltipPromptSkipBtn);
802
+ promptActions.appendChild(this.tooltipPromptSaveBtn);
803
+ promptRow.appendChild(promptActions);
804
+ this.tooltipPrompt.appendChild(promptRow);
805
+ this.tooltip.appendChild(this.tooltipPrompt);
806
+ const radialDefs = [
807
+ {
808
+ cls: "kibee-action--honey",
809
+ label: "Save bookmark",
810
+ name: "Bookmark",
811
+ icon: `<svg width="17" height="17" viewBox="0 0 13 13" fill="none" aria-hidden="true">
812
+ <path d="M2.5 2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v9.5l-4-2.5-4 2.5V2Z"
813
+ stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
814
+ </svg>`,
815
+ action: () => this.actionHandler?.("honey")
816
+ },
817
+ {
818
+ cls: "kibee-action--sting",
819
+ label: "Flag issue",
820
+ name: "Issue",
821
+ icon: `<svg width="17" height="17" viewBox="0 0 13 13" fill="none" aria-hidden="true">
822
+ <circle cx="6.5" cy="6.5" r="5.25" stroke="currentColor" stroke-width="1.4"/>
823
+ <path d="M6.5 4v3M6.5 9h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
824
+ </svg>`,
825
+ action: () => this.actionHandler?.("sting")
826
+ },
827
+ {
828
+ cls: "kibee-action--chat",
829
+ label: "Open assistant",
830
+ name: "Chat",
831
+ icon: `<svg width="17" height="17" viewBox="0 0 17 17" fill="none" aria-hidden="true">
832
+ <path d="M3 2.5 H14 C14.6 2.5 15 2.9 15 3.5 V10.5 C15 11.1 14.6 11.5 14 11.5
833
+ H6.5 L3 14.5 V11.5 H3 C2.4 11.5 2 11.1 2 10.5 V3.5 C2 2.9 2.4 2.5 3 2.5 Z"
834
+ stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
835
+ </svg>`,
836
+ action: () => {
837
+ if (this.state !== "idle") return;
838
+ window.dispatchEvent(
839
+ new CustomEvent("kibee-assist-toggle", {
840
+ detail: { x: this.currentX, y: this.currentY }
841
+ })
842
+ );
843
+ }
844
+ },
845
+ {
846
+ cls: "kibee-action--dock",
847
+ label: "Set dock point here",
848
+ name: "Dock here",
849
+ icon: `<svg width="17" height="17" viewBox="0 0 17 17" fill="none" aria-hidden="true">
850
+ <circle cx="8.5" cy="8.5" r="3" stroke="currentColor" stroke-width="1.4"/>
851
+ <path d="M8.5 2v3M8.5 12v3M2 8.5h3M12 8.5h3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
852
+ </svg>`,
853
+ action: () => {
854
+ this.setCustomDockPosition(this.currentX, this.currentY, { persist: true });
855
+ void this.dock();
856
+ }
857
+ }
858
+ ];
859
+ this.radialButtons = radialDefs.map((def, i) => {
860
+ const btn = document.createElement("button");
861
+ btn.type = "button";
862
+ btn.className = `kibee-action ${def.cls}`;
863
+ btn.setAttribute("aria-label", def.label);
864
+ btn.dataset.label = def.name;
865
+ btn.innerHTML = def.icon;
866
+ btn.style.setProperty("--i", String(i));
867
+ btn.style.display = "none";
868
+ btn.addEventListener("pointerdown", (e) => e.stopPropagation());
869
+ btn.addEventListener("mousedown", (e) => e.stopPropagation());
870
+ btn.addEventListener("click", (e) => {
871
+ e.stopPropagation();
872
+ e.preventDefault();
873
+ def.action();
874
+ });
875
+ btn.addEventListener("mouseenter", () => this.clearHideButtonsTimer());
876
+ btn.addEventListener("mouseleave", () => this.scheduleHideButtons());
877
+ return btn;
878
+ });
879
+ this.hoverZone = document.createElement("div");
880
+ this.hoverZone.className = "kibee-hover-zone";
881
+ this.hoverZone.addEventListener("mouseenter", this.handleBeeHoverEnter);
882
+ this.hoverZone.addEventListener("mouseleave", this.handleBeeHoverLeave);
883
+ this.hoverZone.addEventListener("pointerdown", this.handlePointerDown);
884
+ this.tooltip.addEventListener("mouseenter", this.handleBeeHoverEnter);
885
+ this.tooltip.addEventListener("mouseleave", this.handleBeeHoverLeave);
886
+ this.dockButton = document.createElement("button");
887
+ this.dockButton.type = "button";
888
+ this.dockButton.className = "kibee-dock";
889
+ this.dockButton.setAttribute("aria-label", "Wake KiBee");
890
+ this.dockButton.innerHTML = '<span class="kibee-dock__core" aria-hidden="true"></span><span class="kibee-dock__spark" aria-hidden="true"></span>';
891
+ this.dockButton.addEventListener("click", this.handleDockClick);
892
+ this.updateDockAria();
893
+ this.root.appendChild(this.canvasWrap);
894
+ this.root.appendChild(this.hoverZone);
895
+ this.root.appendChild(this.tooltip);
896
+ for (const btn of this.radialButtons) {
897
+ this.root.appendChild(btn);
898
+ }
899
+ this.root.appendChild(this.dockButton);
900
+ container.appendChild(this.root);
901
+ this.root.dataset.rendererReady = "false";
902
+ this.syncDockState();
903
+ }
904
+ createScene() {
905
+ this.scene = new THREE2.Scene();
906
+ this.scene.fog = new THREE2.FogExp2(16117212, 16e-4);
907
+ }
908
+ createCamera() {
909
+ this.camera = new THREE2.OrthographicCamera(
910
+ 0,
911
+ window.innerWidth,
912
+ 0,
913
+ window.innerHeight,
914
+ -1e3,
915
+ 1e3
916
+ );
917
+ this.camera.position.z = 10;
918
+ }
919
+ createRenderer() {
920
+ try {
921
+ this.renderer = new THREE2.WebGLRenderer({
922
+ alpha: true,
923
+ antialias: true,
924
+ powerPreference: "high-performance"
925
+ });
926
+ } catch {
927
+ this.renderingAvailable = false;
928
+ return;
929
+ }
930
+ const performanceCap = this.resolvePixelRatioCap();
931
+ this.renderer.setPixelRatio(
932
+ Math.min(window.devicePixelRatio, performanceCap)
933
+ );
934
+ this.renderer.outputColorSpace = THREE2.SRGBColorSpace;
935
+ this.renderer.toneMapping = THREE2.ACESFilmicToneMapping;
936
+ this.renderer.toneMappingExposure = 2.1;
937
+ this.renderer.shadowMap.enabled = this.options.enableShadows ?? false;
938
+ this.renderer.shadowMap.type = THREE2.PCFSoftShadowMap;
939
+ this.renderer.domElement.className = "kibee-canvas";
940
+ this.canvasWrap.appendChild(this.renderer.domElement);
941
+ this.renderingAvailable = true;
942
+ this.root.dataset.rendererReady = "true";
943
+ }
944
+ createLights() {
945
+ const ambient = new THREE2.AmbientLight(4210752, 1.7);
946
+ const hemisphere = new THREE2.HemisphereLight(16249575, 11187132, 1.35);
947
+ const key = new THREE2.DirectionalLight(16777215, 1.8);
948
+ const fill = new THREE2.DirectionalLight(16777215, 1);
949
+ const rim = new THREE2.DirectionalLight(16773327, 0.85);
950
+ key.position.set(140, -180, 260);
951
+ fill.position.set(-140, -18, 140);
952
+ rim.position.set(-80, -120, 220);
953
+ if (this.renderer.shadowMap.enabled) {
954
+ [key, fill, rim].forEach((light) => {
955
+ light.castShadow = true;
956
+ light.shadow.mapSize.set(1024, 1024);
957
+ });
958
+ }
959
+ this.scene.add(ambient, hemisphere, key, fill, rim);
960
+ }
961
+ async loadBeeModel() {
962
+ try {
963
+ const loader = new import_GLTFLoader.GLTFLoader();
964
+ registerPbrSpecularGlossinessGLTFPlugin(loader);
965
+ const gltf = await loader.loadAsync(
966
+ this.options.assetUrl ?? "/models/bee-runtime.glb"
967
+ );
968
+ if (this.disposed) {
969
+ return false;
970
+ }
971
+ await applyLegacyBeeMaterialCompat(gltf);
972
+ const model = gltf.scene;
973
+ const anchor = new THREE2.Group();
974
+ const box = new THREE2.Box3().setFromObject(model);
975
+ const center = box.getCenter(new THREE2.Vector3());
976
+ const size = box.getSize(new THREE2.Vector3());
977
+ const targetHeight = this.options.assetVariant === "hero" ? 220 : 68;
978
+ const scale = targetHeight / Math.max(size.y, 1e-3);
979
+ model.position.copy(center.multiplyScalar(-1));
980
+ model.traverse((node) => {
981
+ if (!(node instanceof THREE2.Mesh)) {
982
+ return;
983
+ }
984
+ node.castShadow = Boolean(this.options.enableShadows);
985
+ node.receiveShadow = Boolean(this.options.enableShadows);
986
+ const materials = Array.isArray(node.material) ? node.material : [node.material];
987
+ for (const material of materials) {
988
+ if ("roughness" in material && typeof material.roughness === "number") {
989
+ material.roughness = Math.min(
990
+ 1,
991
+ Math.max(0.2, material.roughness * 1.02)
992
+ );
993
+ }
994
+ if ("metalness" in material && typeof material.metalness === "number") {
995
+ material.metalness = Math.min(material.metalness, 0.08);
996
+ }
997
+ if ("envMapIntensity" in material) {
998
+ material.envMapIntensity = 1;
999
+ }
1000
+ if ("map" in material && material.map instanceof THREE2.Texture) {
1001
+ material.map.colorSpace = THREE2.SRGBColorSpace;
1002
+ }
1003
+ material.needsUpdate = true;
1004
+ }
1005
+ });
1006
+ anchor.scale.setScalar(scale);
1007
+ anchor.rotation.x = Math.PI;
1008
+ anchor.add(model);
1009
+ this.visualRig.add(anchor);
1010
+ this.visualRig.position.set(0, -10, 0);
1011
+ this.applyFacing("right", true);
1012
+ if (gltf.animations.length > 0) {
1013
+ this.mixer = new THREE2.AnimationMixer(model);
1014
+ for (const clip of gltf.animations) {
1015
+ this.actions.set(clip.name, this.mixer.clipAction(clip));
1016
+ }
1017
+ }
1018
+ this.playClipForState("idle");
1019
+ this.updateBeeScreenPosition();
1020
+ return true;
1021
+ } catch {
1022
+ return false;
1023
+ }
1024
+ }
1025
+ createFallbackBee() {
1026
+ this.runtimeWings = null;
1027
+ const bodyMaterial = new THREE2.MeshStandardMaterial({ color: 13867825 });
1028
+ const stripeMaterial = new THREE2.MeshStandardMaterial({ color: 2760726 });
1029
+ const wingMaterial = new THREE2.MeshStandardMaterial({
1030
+ color: 13483943,
1031
+ transparent: true,
1032
+ opacity: 0.52
1033
+ });
1034
+ const body = new THREE2.Mesh(
1035
+ new THREE2.SphereGeometry(14, 24, 24),
1036
+ bodyMaterial
1037
+ );
1038
+ body.scale.set(1.2, 0.8, 0.8);
1039
+ const stripe1 = new THREE2.Mesh(
1040
+ new THREE2.BoxGeometry(6, 24, 16),
1041
+ stripeMaterial
1042
+ );
1043
+ stripe1.position.x = -6;
1044
+ const stripe2 = new THREE2.Mesh(
1045
+ new THREE2.BoxGeometry(6, 24, 16),
1046
+ stripeMaterial
1047
+ );
1048
+ stripe2.position.x = 6;
1049
+ const wingL = new THREE2.Mesh(
1050
+ new THREE2.SphereGeometry(8, 16, 16),
1051
+ wingMaterial
1052
+ );
1053
+ wingL.scale.set(1.2, 0.5, 0.2);
1054
+ wingL.position.set(-4, -12, 2);
1055
+ wingL.rotation.z = -0.4;
1056
+ const wingR = wingL.clone();
1057
+ wingR.position.set(-4, 12, 2);
1058
+ wingR.rotation.z = 0.4;
1059
+ const head = new THREE2.Mesh(
1060
+ new THREE2.SphereGeometry(8, 20, 20),
1061
+ stripeMaterial
1062
+ );
1063
+ head.position.set(-18, 0, 0);
1064
+ this.visualRig.add(body, stripe1, stripe2, wingL, wingR, head);
1065
+ this.runtimeWings = {
1066
+ left: wingL,
1067
+ right: wingR
1068
+ };
1069
+ this.applyHomeFacing(true);
1070
+ this.updateBeeScreenPosition();
1071
+ }
1072
+ initializeDockedHome() {
1073
+ if (!this.isDockEnabled()) {
1074
+ return;
1075
+ }
1076
+ const dock = this.getDockCenter();
1077
+ this.currentX = dock.x;
1078
+ this.currentY = dock.y;
1079
+ this.docked = true;
1080
+ this.dockVisible = true;
1081
+ this.hoveringHome = false;
1082
+ this.applyHomeFacing(true);
1083
+ this.beeGroup.scale.setScalar(this.getDockedScale());
1084
+ this.updateBeeScreenPosition();
1085
+ this.syncDockState();
1086
+ }
1087
+ startRenderLoop() {
1088
+ if (!this.renderingAvailable) {
1089
+ return;
1090
+ }
1091
+ const render = () => {
1092
+ this.animationFrameId = requestAnimationFrame(render);
1093
+ const delta = this.clock.getDelta();
1094
+ const t = this.clock.getElapsedTime();
1095
+ if (this.mixer) {
1096
+ this.mixer.update(this.reducedMotion ? delta * 0.6 : delta);
1097
+ }
1098
+ if (this.runtimeWings?.left && this.runtimeWings.right) {
1099
+ const wingSpeed = this.reducedMotion ? 12 : 24;
1100
+ const wingArc = this.reducedMotion ? 0.12 : 0.25;
1101
+ this.runtimeWings.left.rotation.y = Math.sin(t * wingSpeed) * wingArc;
1102
+ this.runtimeWings.right.rotation.y = -Math.sin(t * wingSpeed) * wingArc;
1103
+ }
1104
+ this.renderer.render(this.scene, this.camera);
1105
+ };
1106
+ render();
1107
+ }
1108
+ updateBeeScreenPosition() {
1109
+ this.beeGroup.position.set(this.currentX, this.currentY, 0);
1110
+ this.positionHoverZone();
1111
+ this.publishBeePosition();
1112
+ }
1113
+ /**
1114
+ * Notify any listeners (the assist panel, debugging overlays) where the
1115
+ * bee currently sits in viewport coords. Throttled to ~60fps via the
1116
+ * existing render loop — every callsite that moves the bee already calls
1117
+ * updateBeeScreenPosition, so this rides along with no extra timer.
1118
+ */
1119
+ publishBeePosition() {
1120
+ if (typeof window === "undefined") return;
1121
+ window.dispatchEvent(
1122
+ new CustomEvent("kibee-bee-move", {
1123
+ detail: { x: this.currentX, y: this.currentY, docked: this.docked }
1124
+ })
1125
+ );
1126
+ }
1127
+ /** Public position read for callers that prefer pull over event subscription. */
1128
+ getBeePosition() {
1129
+ return { x: this.currentX, y: this.currentY, docked: this.docked };
1130
+ }
1131
+ startIdleMotion() {
1132
+ this.playClipForState("idle");
1133
+ const amplitude = this.docked ? 1 : this.hoveringHome ? 1.5 : 2;
1134
+ const tilt = this.docked ? 0.01 : 0.02;
1135
+ if (this.reducedMotion) {
1136
+ import_gsap.gsap.set(this.beeGroup.position, { y: this.currentY });
1137
+ import_gsap.gsap.to(this.beeGroup.rotation, {
1138
+ z: tilt,
1139
+ duration: 0.2,
1140
+ ease: "sine.out",
1141
+ overwrite: "auto"
1142
+ });
1143
+ return;
1144
+ }
1145
+ this.idleTween = import_gsap.gsap.to(this.beeGroup.position, {
1146
+ y: this.currentY + amplitude,
1147
+ duration: this.docked ? 2 : 1.8,
1148
+ yoyo: true,
1149
+ repeat: -1,
1150
+ ease: "sine.inOut"
1151
+ });
1152
+ import_gsap.gsap.to(this.beeGroup.rotation, {
1153
+ z: tilt,
1154
+ duration: 0.6,
1155
+ ease: "sine.inOut",
1156
+ overwrite: "auto"
1157
+ });
1158
+ }
1159
+ startThinkingMotion() {
1160
+ this.playClipForState("thinking");
1161
+ if (this.reducedMotion) {
1162
+ import_gsap.gsap.to(this.beeGroup.rotation, {
1163
+ z: 0.1,
1164
+ duration: 0.2,
1165
+ ease: "sine.out",
1166
+ overwrite: "auto"
1167
+ });
1168
+ return;
1169
+ }
1170
+ this.idleTween = import_gsap.gsap.to(this.beeGroup.rotation, {
1171
+ z: 0.18,
1172
+ duration: 0.5,
1173
+ yoyo: true,
1174
+ repeat: -1,
1175
+ ease: "sine.inOut"
1176
+ });
1177
+ import_gsap.gsap.to(this.beeGroup.position, {
1178
+ x: this.currentX + 8,
1179
+ duration: 0.5,
1180
+ yoyo: true,
1181
+ repeat: -1,
1182
+ ease: "sine.inOut"
1183
+ });
1184
+ }
1185
+ startCelebrateMotion() {
1186
+ this.playClipForState("celebrating");
1187
+ if (this.reducedMotion) {
1188
+ import_gsap.gsap.to(this.beeGroup.rotation, {
1189
+ z: 0.12,
1190
+ duration: 0.18,
1191
+ yoyo: true,
1192
+ repeat: 1,
1193
+ ease: "power1.out",
1194
+ overwrite: "auto"
1195
+ });
1196
+ return;
1197
+ }
1198
+ this.celebrateTween = import_gsap.gsap.timeline();
1199
+ this.celebrateTween.to(this.beeGroup.position, {
1200
+ y: this.currentY - 18,
1201
+ duration: 0.22,
1202
+ ease: "power2.out"
1203
+ }).to(this.beeGroup.rotation, {
1204
+ z: 0.42,
1205
+ duration: 0.18,
1206
+ ease: "power1.out"
1207
+ }).to(this.beeGroup.position, {
1208
+ y: this.currentY,
1209
+ duration: 0.28,
1210
+ ease: "bounce.out"
1211
+ }).to(
1212
+ this.beeGroup.rotation,
1213
+ {
1214
+ z: 0,
1215
+ duration: 0.25,
1216
+ ease: "power1.out"
1217
+ },
1218
+ "<"
1219
+ );
1220
+ }
1221
+ stopStateTweens() {
1222
+ if (this.idleTween) {
1223
+ this.idleTween.kill();
1224
+ this.idleTween = null;
1225
+ }
1226
+ if (this.celebrateTween) {
1227
+ this.celebrateTween.kill();
1228
+ this.celebrateTween = null;
1229
+ }
1230
+ import_gsap.gsap.killTweensOf(this.beeGroup.position);
1231
+ import_gsap.gsap.killTweensOf(this.beeGroup.rotation);
1232
+ import_gsap.gsap.killTweensOf(this.beeGroup.scale);
1233
+ }
1234
+ positionTooltipNearBee() {
1235
+ if (!this.tooltip || this.tooltip.style.display === "none") return;
1236
+ const tw = this.tooltip.offsetWidth || 220;
1237
+ const th = this.tooltip.offsetHeight || 70;
1238
+ const layout = this.computeRadialLayout(tw, th);
1239
+ this.tooltip.style.left = `${layout.tooltip.x}px`;
1240
+ this.tooltip.style.top = `${layout.tooltip.y}px`;
1241
+ this.tooltip.dataset.arrowEdge = layout.tooltip.arrowEdge;
1242
+ }
1243
+ /**
1244
+ * Compute viewport-aware radial positions for radial action buttons and the
1245
+ * tooltip. All returned coordinates are CSS `left`/`top` pixel values.
1246
+ * The tooltip's `top` value is the *rendered center Y* (because CSS applies
1247
+ * `transform: translate(0, -50%)` to the tooltip element).
1248
+ */
1249
+ computeRadialLayout(tooltipW, tooltipH) {
1250
+ const bx = this.currentX;
1251
+ const by = this.currentY;
1252
+ const vw = window.innerWidth;
1253
+ const vh = window.innerHeight;
1254
+ const RADIUS = 54;
1255
+ const BTN = 30;
1256
+ const MARGIN = 8;
1257
+ const TOOLTIP_GAP = 70;
1258
+ const ARC = 2 * Math.PI / 3;
1259
+ const midAngle = Math.atan2(vh / 2 - by, vw / 2 - bx);
1260
+ const n = Math.max(1, this.radialButtons.length);
1261
+ const step = n <= 1 ? 0 : ARC / (n - 1);
1262
+ const startAngle = midAngle - ARC / 2;
1263
+ const angles = Array.from({ length: n }, (_, i) => startAngle + i * step);
1264
+ const buttons = angles.map((a) => ({
1265
+ x: Math.max(
1266
+ MARGIN,
1267
+ Math.min(vw - BTN - MARGIN, bx + RADIUS * Math.cos(a) - BTN / 2)
1268
+ ),
1269
+ y: Math.max(
1270
+ MARGIN,
1271
+ Math.min(vh - BTN - MARGIN, by + RADIUS * Math.sin(a) - BTN / 2)
1272
+ )
1273
+ }));
1274
+ const oppAngle = midAngle + Math.PI;
1275
+ const cos = Math.cos(oppAngle);
1276
+ const sin = Math.sin(oppAngle);
1277
+ let tooltipLeft;
1278
+ let tooltipCssTop;
1279
+ if (Math.abs(cos) >= Math.abs(sin)) {
1280
+ tooltipLeft = cos > 0 ? bx + TOOLTIP_GAP : bx - TOOLTIP_GAP - tooltipW;
1281
+ tooltipCssTop = by;
1282
+ } else {
1283
+ tooltipLeft = bx - tooltipW / 2;
1284
+ tooltipCssTop = sin > 0 ? by + TOOLTIP_GAP + tooltipH / 2 : by - TOOLTIP_GAP - tooltipH / 2;
1285
+ }
1286
+ tooltipLeft = Math.max(MARGIN, Math.min(vw - tooltipW - MARGIN, tooltipLeft));
1287
+ tooltipCssTop = Math.max(
1288
+ MARGIN + tooltipH / 2,
1289
+ Math.min(vh - MARGIN - tooltipH / 2, tooltipCssTop)
1290
+ );
1291
+ const tipTop = tooltipCssTop - tooltipH / 2;
1292
+ const tipBottom = tooltipCssTop + tooltipH / 2;
1293
+ const tipRight = tooltipLeft + tooltipW;
1294
+ for (const btn of buttons) {
1295
+ const overlapX = tooltipLeft < btn.x + BTN && tipRight > btn.x;
1296
+ const overlapY = tipTop < btn.y + BTN && tipBottom > btn.y;
1297
+ if (overlapX && overlapY) {
1298
+ const nudge = BTN + MARGIN;
1299
+ tooltipLeft = Math.max(
1300
+ MARGIN,
1301
+ Math.min(vw - tooltipW - MARGIN, tooltipLeft + cos * nudge)
1302
+ );
1303
+ tooltipCssTop = Math.max(
1304
+ MARGIN + tooltipH / 2,
1305
+ Math.min(
1306
+ vh - MARGIN - tooltipH / 2,
1307
+ tooltipCssTop + sin * nudge
1308
+ )
1309
+ );
1310
+ }
1311
+ }
1312
+ const BEE_HW = 62;
1313
+ const BEE_HH = 34;
1314
+ const tipFinalTop = tooltipCssTop - tooltipH / 2;
1315
+ const tipFinalBottom = tooltipCssTop + tooltipH / 2;
1316
+ const tipFinalRight = tooltipLeft + tooltipW;
1317
+ const beeLeft = bx - BEE_HW;
1318
+ const beeRight = bx + BEE_HW;
1319
+ const beeTop = by - BEE_HH;
1320
+ const beeBottom = by + BEE_HH;
1321
+ const overlapBeeX = tooltipLeft < beeRight && tipFinalRight > beeLeft;
1322
+ const overlapBeeY = tipFinalTop < beeBottom && tipFinalBottom > beeTop;
1323
+ if (overlapBeeX && overlapBeeY) {
1324
+ const spaceLeft = tooltipLeft - beeLeft;
1325
+ const spaceRight = beeRight - tipFinalRight;
1326
+ if (cos <= 0) {
1327
+ tooltipLeft = Math.max(MARGIN, beeLeft - tooltipW - MARGIN);
1328
+ } else {
1329
+ tooltipLeft = Math.min(vw - tooltipW - MARGIN, beeRight + MARGIN);
1330
+ }
1331
+ void spaceLeft;
1332
+ void spaceRight;
1333
+ }
1334
+ const relX = tooltipLeft + tooltipW / 2 - bx;
1335
+ const relY = tooltipCssTop - by;
1336
+ let arrowEdge;
1337
+ if (Math.abs(relX) >= Math.abs(relY)) {
1338
+ arrowEdge = relX > 0 ? "left" : "right";
1339
+ } else {
1340
+ arrowEdge = relY > 0 ? "top" : "bottom";
1341
+ }
1342
+ return {
1343
+ buttons,
1344
+ tooltip: { x: tooltipLeft, y: tooltipCssTop, arrowEdge }
1345
+ };
1346
+ }
1347
+ showRadialButtons() {
1348
+ if (!this.radialButtons.length || this.isDragging) return;
1349
+ const tw = this.tooltip.offsetWidth || 220;
1350
+ const th = this.tooltip.offsetHeight || 70;
1351
+ const layout = this.computeRadialLayout(tw, th);
1352
+ this.radialButtons.forEach((btn, i) => {
1353
+ const pos = layout.buttons[i];
1354
+ if (!pos) return;
1355
+ btn.style.left = `${pos.x}px`;
1356
+ btn.style.top = `${pos.y}px`;
1357
+ btn.style.display = "grid";
1358
+ void btn.offsetWidth;
1359
+ btn.classList.remove("kibee-action--hidden");
1360
+ btn.classList.add("kibee-action--visible");
1361
+ });
1362
+ if (this.tooltip.style.display !== "none") {
1363
+ this.tooltip.style.left = `${layout.tooltip.x}px`;
1364
+ this.tooltip.style.top = `${layout.tooltip.y}px`;
1365
+ this.tooltip.dataset.arrowEdge = layout.tooltip.arrowEdge;
1366
+ }
1367
+ }
1368
+ hideRadialButtons() {
1369
+ for (const btn of this.radialButtons) {
1370
+ btn.classList.remove("kibee-action--visible");
1371
+ btn.classList.add("kibee-action--hidden");
1372
+ }
1373
+ setTimeout(() => {
1374
+ for (const btn of this.radialButtons) {
1375
+ if (!btn.classList.contains("kibee-action--visible")) {
1376
+ btn.style.display = "none";
1377
+ }
1378
+ }
1379
+ }, 260);
1380
+ }
1381
+ positionHoverZone() {
1382
+ if (!this.hoverZone) return;
1383
+ const size = this.actionHandler ? 100 : 72;
1384
+ this.hoverZone.style.left = `${this.currentX - size / 2}px`;
1385
+ this.hoverZone.style.top = `${this.currentY - size / 2}px`;
1386
+ this.hoverZone.style.width = `${size}px`;
1387
+ this.hoverZone.style.height = `${size}px`;
1388
+ }
1389
+ startTooltipPulse() {
1390
+ if (this.tooltipHasNav) return;
1391
+ this.clearTooltipPulse();
1392
+ this.tooltipPulseTimer = window.setTimeout(() => {
1393
+ if (!this.tooltipMessage) return;
1394
+ this.tooltip.classList.remove("kibee-tooltip--visible");
1395
+ this.tooltip.classList.add("kibee-tooltip--hidden");
1396
+ }, 3400);
1397
+ }
1398
+ clearTooltipPulse() {
1399
+ if (this.tooltipPulseTimer !== null) {
1400
+ clearTimeout(this.tooltipPulseTimer);
1401
+ this.tooltipPulseTimer = null;
1402
+ }
1403
+ }
1404
+ handleBeeHoverEnter = () => {
1405
+ if (this.isDragging) return;
1406
+ this.clearHideButtonsTimer();
1407
+ if (this.tooltipMessage && !this.docked) {
1408
+ this.tooltip.style.display = "block";
1409
+ this.tooltip.classList.remove("kibee-tooltip--hidden");
1410
+ this.tooltip.classList.add("kibee-tooltip--visible");
1411
+ this.clearTooltipPulse();
1412
+ }
1413
+ if (this.actionHandler && this.state === "idle") {
1414
+ this.showRadialButtons();
1415
+ }
1416
+ };
1417
+ handleBeeHoverLeave = () => {
1418
+ if (this.tooltipMessage && !this.docked) {
1419
+ this.startTooltipPulse();
1420
+ }
1421
+ if (this.actionHandler) {
1422
+ this.scheduleHideButtons();
1423
+ }
1424
+ };
1425
+ scheduleHideButtons() {
1426
+ this.clearHideButtonsTimer();
1427
+ this.hideButtonsTimer = window.setTimeout(() => {
1428
+ this.hideButtonsTimer = null;
1429
+ this.hideRadialButtons();
1430
+ }, 320);
1431
+ }
1432
+ clearHideButtonsTimer() {
1433
+ if (this.hideButtonsTimer !== null) {
1434
+ clearTimeout(this.hideButtonsTimer);
1435
+ this.hideButtonsTimer = null;
1436
+ }
1437
+ }
1438
+ // ── Drag handlers ───────────────────────────────────────────────────────
1439
+ /** Instant undock used when the user drags the bee off the dock / launcher */
1440
+ fastUndockForDrag() {
1441
+ if (!this.isDockEnabled() || !this.docked) return;
1442
+ this.docked = false;
1443
+ this.dockVisible = false;
1444
+ this.hoveringHome = false;
1445
+ this.stopStateTweens();
1446
+ import_gsap.gsap.killTweensOf(this);
1447
+ import_gsap.gsap.killTweensOf(this.beeGroup.position);
1448
+ import_gsap.gsap.killTweensOf(this.beeGroup.rotation);
1449
+ import_gsap.gsap.killTweensOf(this.beeGroup.scale);
1450
+ this.beeGroup.scale.setScalar(this.getActiveScale());
1451
+ this.syncDockState();
1452
+ }
1453
+ handlePointerDown = (event) => {
1454
+ if (event.button !== 0) return;
1455
+ event.preventDefault();
1456
+ this.isDragging = true;
1457
+ this.dragMoved = false;
1458
+ this.dragArmedFromDock = this.docked;
1459
+ this.dragStartX = this.currentX - event.clientX;
1460
+ this.dragStartY = this.currentY - event.clientY;
1461
+ if (this.idleTween) {
1462
+ this.idleTween.kill();
1463
+ this.idleTween = null;
1464
+ }
1465
+ if (!this.docked) {
1466
+ import_gsap.gsap.killTweensOf(this);
1467
+ import_gsap.gsap.killTweensOf(this.beeGroup.position);
1468
+ import_gsap.gsap.killTweensOf(this.beeGroup.rotation);
1469
+ }
1470
+ this.hoverZone.setPointerCapture(event.pointerId);
1471
+ this.root.classList.add("kibee-root--dragging");
1472
+ this.hideRadialButtons();
1473
+ };
1474
+ handlePointerMove = (event) => {
1475
+ if (!this.isDragging) return;
1476
+ const newX = event.clientX + this.dragStartX;
1477
+ const newY = event.clientY + this.dragStartY;
1478
+ if (this.docked) {
1479
+ if (!this.dragMoved && (Math.abs(newX - this.currentX) > 4 || Math.abs(newY - this.currentY) > 4)) {
1480
+ this.dragMoved = true;
1481
+ this.fastUndockForDrag();
1482
+ }
1483
+ if (this.docked) return;
1484
+ }
1485
+ if (!this.dragMoved && (Math.abs(newX - this.currentX) > 4 || Math.abs(newY - this.currentY) > 4)) {
1486
+ this.dragMoved = true;
1487
+ }
1488
+ const pad = 36;
1489
+ this.currentX = Math.max(pad, Math.min(window.innerWidth - pad, newX));
1490
+ this.currentY = Math.max(pad, Math.min(window.innerHeight - pad, newY));
1491
+ this.updateBeeScreenPosition();
1492
+ if (this.tooltipMessage && this.tooltip.style.display !== "none") {
1493
+ this.positionTooltipNearBee();
1494
+ }
1495
+ };
1496
+ handlePointerUp = (event) => {
1497
+ if (!this.isDragging) return;
1498
+ const armedFromDock = this.dragArmedFromDock;
1499
+ const moved = this.dragMoved;
1500
+ if (armedFromDock && !moved && this.docked) {
1501
+ this.handleDockClick();
1502
+ }
1503
+ if (!moved && !this.docked && this.dockClickBehavior === "assist-panel" && this.state === "idle") {
1504
+ window.dispatchEvent(
1505
+ new CustomEvent("kibee-assist-toggle", {
1506
+ detail: { x: this.currentX, y: this.currentY }
1507
+ })
1508
+ );
1509
+ }
1510
+ this.isDragging = false;
1511
+ this.dragArmedFromDock = false;
1512
+ this.hoverZone.releasePointerCapture(event.pointerId);
1513
+ this.root.classList.remove("kibee-root--dragging");
1514
+ if (this.state === "idle") {
1515
+ this.startIdleMotion();
1516
+ }
1517
+ if (!this.docked && this.actionHandler && this.state === "idle") {
1518
+ if (moved || !armedFromDock) {
1519
+ this.clearHideButtonsTimer();
1520
+ this.showRadialButtons();
1521
+ }
1522
+ }
1523
+ if (!this.docked && this.tooltipMessage && this.state === "idle" && (moved || !armedFromDock)) {
1524
+ this.tooltip.style.display = "block";
1525
+ this.tooltip.classList.remove("kibee-tooltip--hidden");
1526
+ this.tooltip.classList.add("kibee-tooltip--visible");
1527
+ this.positionTooltipNearBee();
1528
+ this.clearTooltipPulse();
1529
+ }
1530
+ };
1531
+ handleResize = () => {
1532
+ this.resize();
1533
+ };
1534
+ handleVisibilityChange = () => {
1535
+ if (document.hidden) {
1536
+ if (this.animationFrameId !== null) {
1537
+ cancelAnimationFrame(this.animationFrameId);
1538
+ this.animationFrameId = null;
1539
+ }
1540
+ } else if (this.animationFrameId === null && this.renderingAvailable) {
1541
+ this.clock.getDelta();
1542
+ this.startRenderLoop();
1543
+ }
1544
+ };
1545
+ handlePanelOpen = (_event) => {
1546
+ this.panelOpen = true;
1547
+ if (this.tooltip.style.display !== "none") {
1548
+ this.tooltip.classList.remove("kibee-tooltip--visible");
1549
+ this.tooltip.classList.add("kibee-tooltip--hidden");
1550
+ this.tooltip.style.display = "none";
1551
+ this.clearTooltipPulse();
1552
+ }
1553
+ if (!this.docked) return;
1554
+ requestAnimationFrame(() => {
1555
+ if (!this.panelOpen) return;
1556
+ const panel = document.querySelector(".kbp-panel");
1557
+ if (!panel) return;
1558
+ const rect = panel.getBoundingClientRect();
1559
+ if (!rect.width) return;
1560
+ void this.moveTo(rect.left + rect.width / 2, rect.top, {
1561
+ offsetX: 0,
1562
+ offsetY: 0,
1563
+ duration: 0.55
1564
+ });
1565
+ });
1566
+ };
1567
+ handlePanelClose = () => {
1568
+ this.panelOpen = false;
1569
+ if (this.dockClickBehavior !== "assist-panel") {
1570
+ void this.dock();
1571
+ }
1572
+ };
1573
+ handleDockClick = () => {
1574
+ if (document.body.dataset.kibeeEmptyDock === "true") {
1575
+ return;
1576
+ }
1577
+ if (this.dockClickBehavior === "assist-panel") {
1578
+ if (this.state !== "idle") return;
1579
+ window.dispatchEvent(
1580
+ new CustomEvent("kibee-assist-toggle", {
1581
+ detail: { x: this.currentX, y: this.currentY }
1582
+ })
1583
+ );
1584
+ return;
1585
+ }
1586
+ if (!this.docked) {
1587
+ return;
1588
+ }
1589
+ this.hideMessage();
1590
+ void this.undock({ hideDock: false }).then(() => {
1591
+ this.speak("How can I help? Hover to bookmark or flag an issue.");
1592
+ });
1593
+ };
1594
+ updateDockAria() {
1595
+ if (!this.dockButton) {
1596
+ return;
1597
+ }
1598
+ if (this.dockClickBehavior === "assist-panel") {
1599
+ this.dockButton.setAttribute(
1600
+ "aria-label",
1601
+ "Open or close KiBee assistant"
1602
+ );
1603
+ } else {
1604
+ this.dockButton.setAttribute("aria-label", "Wake KiBee");
1605
+ }
1606
+ }
1607
+ shouldReduceMotion() {
1608
+ if (this.options.motionMode === "reduced") {
1609
+ return true;
1610
+ }
1611
+ if (this.options.motionMode === "full") {
1612
+ return false;
1613
+ }
1614
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
1615
+ }
1616
+ resolvePixelRatioCap() {
1617
+ const memoryAwareNavigator = navigator;
1618
+ if (this.options.qualityTier === "high") {
1619
+ return 2;
1620
+ }
1621
+ if (this.options.qualityTier === "low") {
1622
+ return 1.05;
1623
+ }
1624
+ if (this.options.qualityTier === "balanced") {
1625
+ return 1.45;
1626
+ }
1627
+ const lowCpu = navigator.hardwareConcurrency !== void 0 && navigator.hardwareConcurrency <= 4;
1628
+ const lowMemory = memoryAwareNavigator.deviceMemory !== void 0 && memoryAwareNavigator.deviceMemory <= 4;
1629
+ return lowCpu || lowMemory ? 1.05 : 1.45;
1630
+ }
1631
+ playClipForState(state) {
1632
+ if (!this.mixer || this.actions.size === 0) {
1633
+ return;
1634
+ }
1635
+ const preferredName = CLIP_BY_STATE[state];
1636
+ const nextAction = this.actions.get(preferredName) ?? this.actions.values().next().value;
1637
+ if (!nextAction) {
1638
+ return;
1639
+ }
1640
+ const previousAction = this.activeActionName ? this.actions.get(this.activeActionName) ?? null : null;
1641
+ if (previousAction === nextAction) {
1642
+ nextAction.timeScale = this.getClipTimeScale(state);
1643
+ return;
1644
+ }
1645
+ if (previousAction) {
1646
+ previousAction.fadeOut(0.24);
1647
+ }
1648
+ nextAction.reset();
1649
+ nextAction.enabled = true;
1650
+ nextAction.timeScale = this.getClipTimeScale(state);
1651
+ nextAction.clampWhenFinished = preferredName === "take_off_and_land";
1652
+ nextAction.setLoop(
1653
+ preferredName === "take_off_and_land" ? THREE2.LoopOnce : THREE2.LoopRepeat,
1654
+ preferredName === "take_off_and_land" ? 1 : Infinity
1655
+ );
1656
+ nextAction.fadeIn(0.28).play();
1657
+ this.activeActionName = nextAction.getClip().name;
1658
+ }
1659
+ getClipTimeScale(state) {
1660
+ if (state === "guiding") {
1661
+ return this.reducedMotion ? 0.7 : 1.12;
1662
+ }
1663
+ if (state === "thinking") {
1664
+ return this.reducedMotion ? 0.5 : 0.82;
1665
+ }
1666
+ return this.reducedMotion ? 0.65 : 1;
1667
+ }
1668
+ applyFacing(facing, immediate = false) {
1669
+ this.currentFacing = facing;
1670
+ const targetY = facing === "right" ? -Math.PI / 1.56 : Math.PI / 1.56;
1671
+ const tiltZ = facing === "right" ? -0.02 : 0.02;
1672
+ if (immediate) {
1673
+ this.visualRig.rotation.set(0.12, targetY, tiltZ);
1674
+ return;
1675
+ }
1676
+ import_gsap.gsap.to(this.visualRig.rotation, {
1677
+ x: 0.12,
1678
+ y: targetY,
1679
+ z: tiltZ,
1680
+ duration: 0.28,
1681
+ ease: "power2.out",
1682
+ overwrite: "auto"
1683
+ });
1684
+ }
1685
+ applyHomeFacing(immediate = false) {
1686
+ const facesLeft = this.isDockOnRight();
1687
+ this.currentFacing = facesLeft ? "left" : "right";
1688
+ const yRot = facesLeft ? Math.PI / 1.6 : -Math.PI / 1.6;
1689
+ if (immediate) {
1690
+ this.visualRig.rotation.set(0.16, yRot, 0.12);
1691
+ return;
1692
+ }
1693
+ import_gsap.gsap.to(this.visualRig.rotation, {
1694
+ x: 0.16,
1695
+ y: yRot,
1696
+ z: 0.12,
1697
+ duration: 0.28,
1698
+ ease: "power2.out",
1699
+ overwrite: "auto"
1700
+ });
1701
+ }
1702
+ syncDockState() {
1703
+ if (!this.root) {
1704
+ return;
1705
+ }
1706
+ this.root.dataset.docked = String(this.docked);
1707
+ this.root.dataset.idle = String(this.state === "idle");
1708
+ this.root.dataset.dockVisible = String(this.dockVisible);
1709
+ this.root.style.setProperty("--kibee-dock-size", `${this.getDockSize()}px`);
1710
+ const cssVars = this.resolveDockCSSVars();
1711
+ this.root.style.setProperty("--kibee-dock-right", cssVars.right);
1712
+ this.root.style.setProperty("--kibee-dock-bottom", cssVars.bottom);
1713
+ this.root.style.setProperty("--kibee-dock-left", cssVars.left);
1714
+ this.root.style.setProperty("--kibee-dock-top", cssVars.top);
1715
+ if (this.hoverZone) {
1716
+ this.hoverZone.style.pointerEvents = "";
1717
+ }
1718
+ if (this.dockButton) {
1719
+ const passThrough = this.docked && this.dockClickBehavior === "assist-panel";
1720
+ this.dockButton.style.pointerEvents = passThrough ? "none" : "";
1721
+ }
1722
+ }
1723
+ isDockEnabled() {
1724
+ return this.options.dockEnabled ?? true;
1725
+ }
1726
+ setCustomDockPosition(x, y, opts = {}) {
1727
+ const persist = opts.persist !== false && this.restorePersistedDockPosition;
1728
+ this.customDockPos = { x, y };
1729
+ if (persist) {
1730
+ try {
1731
+ localStorage.setItem("kibee:dock_pos", JSON.stringify({ x, y }));
1732
+ } catch {
1733
+ }
1734
+ }
1735
+ this.syncDockState();
1736
+ }
1737
+ /**
1738
+ * Snap the docked bee and dock chrome to the current `getDockCenter()` without a full resize.
1739
+ * Use after `setCustomDockPosition` when the dock target moves (e.g. scroll-linked layout).
1740
+ */
1741
+ refreshDockedBeeAfterDockMove() {
1742
+ if (!this.isDockEnabled() || !this.docked || this.isDragging) return;
1743
+ const dock = this.getDockCenter();
1744
+ this.currentX = dock.x;
1745
+ this.currentY = dock.y;
1746
+ this.updateBeeScreenPosition();
1747
+ this.positionTooltipNearBee();
1748
+ this.syncDockState();
1749
+ }
1750
+ getDockCenter() {
1751
+ if (this.customDockPos) {
1752
+ return { ...this.customDockPos };
1753
+ }
1754
+ const size = this.getDockSize();
1755
+ const pos = this.options.dockPosition ?? "bottom-right";
1756
+ if (typeof pos === "string") {
1757
+ return this.presetToCenter(pos, size);
1758
+ }
1759
+ if ("preset" in pos) {
1760
+ const base = this.presetToCenter(pos.preset, size);
1761
+ return { x: base.x + (pos.offsetX ?? 0), y: base.y + (pos.offsetY ?? 0) };
1762
+ }
1763
+ if ("custom" in pos) {
1764
+ return { x: pos.custom.x, y: pos.custom.y };
1765
+ }
1766
+ if ("inline" in pos) {
1767
+ return this.resolveInlineCenter(pos.inline, size);
1768
+ }
1769
+ return this.presetToCenter("bottom-right", size);
1770
+ }
1771
+ presetToCenter(preset, size) {
1772
+ const margin = 22;
1773
+ const padding = 24;
1774
+ switch (preset) {
1775
+ case "bottom-right":
1776
+ return {
1777
+ x: window.innerWidth - margin - size / 2,
1778
+ y: window.innerHeight - padding - size / 2
1779
+ };
1780
+ case "bottom-left":
1781
+ return {
1782
+ x: margin + size / 2,
1783
+ y: window.innerHeight - padding - size / 2
1784
+ };
1785
+ case "top-right":
1786
+ return {
1787
+ x: window.innerWidth - margin - size / 2,
1788
+ y: padding + size / 2
1789
+ };
1790
+ case "top-left":
1791
+ return {
1792
+ x: margin + size / 2,
1793
+ y: padding + size / 2
1794
+ };
1795
+ }
1796
+ }
1797
+ resolveInlineCenter(anchor, size) {
1798
+ const el = typeof anchor.container === "string" ? document.querySelector(anchor.container) : anchor.container;
1799
+ if (!el) return this.presetToCenter("bottom-right", size);
1800
+ const rect = el.getBoundingClientRect();
1801
+ const align = anchor.align ?? "center";
1802
+ const x = align === "left" ? rect.left + size / 2 : align === "right" ? rect.right - size / 2 : rect.left + rect.width / 2;
1803
+ const y = rect.top + rect.height / 2;
1804
+ return { x, y };
1805
+ }
1806
+ /** Returns true when the dock sits on the right half of the viewport */
1807
+ isDockOnRight() {
1808
+ const pos = this.options.dockPosition ?? "bottom-right";
1809
+ if (typeof pos === "string") {
1810
+ return pos === "bottom-right" || pos === "top-right";
1811
+ }
1812
+ if ("preset" in pos) {
1813
+ return pos.preset === "bottom-right" || pos.preset === "top-right";
1814
+ }
1815
+ return this.getDockCenter().x > window.innerWidth / 2;
1816
+ }
1817
+ /** Returns true when the dock sits on the bottom half of the viewport */
1818
+ isDockOnBottom() {
1819
+ const pos = this.options.dockPosition ?? "bottom-right";
1820
+ if (typeof pos === "string") {
1821
+ return pos === "bottom-right" || pos === "bottom-left";
1822
+ }
1823
+ if ("preset" in pos) {
1824
+ return pos.preset === "bottom-right" || pos.preset === "bottom-left";
1825
+ }
1826
+ return this.getDockCenter().y > window.innerHeight / 2;
1827
+ }
1828
+ resolveDockCSSVars() {
1829
+ const defaults = { right: "auto", bottom: "auto", left: "auto", top: "auto" };
1830
+ if (this.customDockPos) {
1831
+ const size = this.getDockSize();
1832
+ return {
1833
+ ...defaults,
1834
+ left: `${this.customDockPos.x - size / 2}px`,
1835
+ top: `${this.customDockPos.y - size / 2}px`
1836
+ };
1837
+ }
1838
+ const pos = this.options.dockPosition ?? "bottom-right";
1839
+ if (typeof pos === "string" || "preset" in pos) {
1840
+ const preset = typeof pos === "string" ? pos : pos.preset;
1841
+ switch (preset) {
1842
+ case "bottom-right":
1843
+ return { ...defaults, right: "22px", bottom: "24px" };
1844
+ case "bottom-left":
1845
+ return { ...defaults, left: "22px", bottom: "24px" };
1846
+ case "top-right":
1847
+ return { ...defaults, right: "22px", top: "24px" };
1848
+ case "top-left":
1849
+ return { ...defaults, left: "22px", top: "24px" };
1850
+ }
1851
+ }
1852
+ if ("custom" in pos) {
1853
+ return { ...defaults, left: `${pos.custom.x}px`, top: `${pos.custom.y}px` };
1854
+ }
1855
+ return defaults;
1856
+ }
1857
+ getWakeHoverPosition() {
1858
+ const dock = this.getDockCenter();
1859
+ const xDir = this.isDockOnRight() ? -1 : 1;
1860
+ const yDir = this.isDockOnBottom() ? -1 : 1;
1861
+ return {
1862
+ x: dock.x + xDir * 34,
1863
+ y: dock.y + yDir * 66
1864
+ };
1865
+ }
1866
+ getDockScreenPosition() {
1867
+ return this.getDockCenter();
1868
+ }
1869
+ getDockSize() {
1870
+ return this.options.dockSize ?? 60;
1871
+ }
1872
+ getActiveScale() {
1873
+ return 0.86;
1874
+ }
1875
+ getDockedScale() {
1876
+ return 0.48;
1877
+ }
1878
+ getHomeHoverScale() {
1879
+ return 0.68;
1880
+ }
1881
+ disposeSceneObjects() {
1882
+ this.scene?.traverse((node) => {
1883
+ if (!(node instanceof THREE2.Mesh)) {
1884
+ return;
1885
+ }
1886
+ node.geometry.dispose();
1887
+ const materials = Array.isArray(node.material) ? node.material : [node.material];
1888
+ for (const material of materials) {
1889
+ const textureKeys = [
1890
+ "map",
1891
+ "normalMap",
1892
+ "aoMap",
1893
+ "roughnessMap",
1894
+ "metalnessMap",
1895
+ "specularMap",
1896
+ "emissiveMap"
1897
+ ];
1898
+ for (const key of textureKeys) {
1899
+ const texture = material[key];
1900
+ if (texture instanceof THREE2.Texture) {
1901
+ texture.dispose();
1902
+ }
1903
+ }
1904
+ material.dispose();
1905
+ }
1906
+ });
1907
+ }
1908
+ };
1909
+ // Annotate the CommonJS export names for ESM import in node:
1910
+ 0 && (module.exports = {
1911
+ ThreeBeeRenderer,
1912
+ applyLegacyBeeMaterialCompat
1913
+ });