agent v3 : bulle déplaçable (pointer events, seuil clic/glisser 6 px, capture au pointerdown, position bornée + mémorisée par site, panneau fenêtré ancré du côté de la bulle)
1 changed file +78 −2
modified
agent/ka-agent.js
+78 −2
@@ -10,6 +10,10 @@ | ||
| 10 | 10 | * streaming (titres, listes, tableaux, code, liens nommés, citations). |
| 11 | 11 | * Flux SSE en direct depuis le backend central (texte token par token + |
| 12 | 12 | * indicateur d'outil). Historique conservé en localStorage (par site). |
| 13 | + * v3 (2026-08-22) : bulle DÉPLAÇABLE au doigt/à la souris (pointer events, | |
| 14 | + * seuil 6 px pour distinguer clic et glisser, bornée aux marges de l'écran, | |
| 15 | + * position mémorisée par site en localStorage, re-bornée au resize) ; en mode | |
| 16 | + * fenêtré le panneau s'ancre du côté où se trouve la bulle. | |
| 13 | 17 | */ |
| 14 | 18 | (function () { |
| 15 | 19 | "use strict"; |
@@ -25,8 +29,10 @@ | ||
| 25 | 29 | border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); |
| 26 | 30 | color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif; |
| 27 | 31 | cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center; |
| 28 | − justify-content:center;transition:transform .15s;touch-action:manipulation} | |
| 32 | + justify-content:center;transition:transform .15s;touch-action:none} | |
| 29 | 33 | .kaa-btn:hover{transform:translate(-2px,-2px)} |
| 34 | + .kaa-btn.kaa-drag{transition:none;cursor:grabbing} | |
| 35 | + .kaa-btn.kaa-drag:hover{transform:none} | |
| 30 | 36 | .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee); |
| 31 | 37 | overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation} |
| 32 | 38 | .kaa-panel.kaa-open{display:flex} |
@@ -34,6 +40,7 @@ | ||
| 34 | 40 | .kaa-panel:not(.kaa-full){right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));width:min(392px,calc(100vw - 24px)); |
| 35 | 41 | height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px)); |
| 36 | 42 | border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85)} |
| 43 | + .kaa-panel.kaa-left:not(.kaa-full){right:auto;left:16px} | |
| 37 | 44 | html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none;touch-action:none} |
| 38 | 45 | .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); |
| 39 | 46 | color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))} |
@@ -279,7 +286,76 @@ | ||
| 279 | 286 | fitVV(); |
| 280 | 287 | } |
| 281 | 288 | |
| 282 | − btn.addEventListener("click", function () { | |
| 289 | + /* ---------- bulle déplaçable (v3) ---------- | |
| 290 | + pointerdown sur la bulle → si le doigt/curseur bouge de plus de 6 px c'est | |
| 291 | + un GLISSER (la bulle suit, bornée à 8 px des bords) ; sinon c'est un CLIC | |
| 292 | + (ouverture normale). Position mémorisée par site (localStorage) et | |
| 293 | + re-bornée quand le viewport change. En mode fenêtré, le panneau s'ancre à | |
| 294 | + gauche si la bulle est dans la moitié gauche de l'écran. */ | |
| 295 | + var POSKEY = "ka-agent-pos-" + SITE; | |
| 296 | + var drag = { on: false, moved: false, id: null, sx: 0, sy: 0, bx: 0, by: 0 }; | |
| 297 | + | |
| 298 | + function clampPos(l, t) { | |
| 299 | + var m = 8, w = btn.offsetWidth || 58, h = btn.offsetHeight || 58; | |
| 300 | + return { | |
| 301 | + l: Math.min(Math.max(l, m), Math.max(m, window.innerWidth - w - m)), | |
| 302 | + t: Math.min(Math.max(t, m), Math.max(m, window.innerHeight - h - m)), | |
| 303 | + }; | |
| 304 | + } | |
| 305 | + function applyPos(l, t) { | |
| 306 | + var p = clampPos(l, t); | |
| 307 | + btn.style.left = p.l + "px"; | |
| 308 | + btn.style.top = p.t + "px"; | |
| 309 | + btn.style.right = "auto"; | |
| 310 | + btn.style.bottom = "auto"; | |
| 311 | + panel.classList.toggle("kaa-left", p.l + (btn.offsetWidth || 58) / 2 < window.innerWidth / 2); | |
| 312 | + } | |
| 313 | + function savePos() { | |
| 314 | + var r = btn.getBoundingClientRect(); | |
| 315 | + try { localStorage.setItem(POSKEY, JSON.stringify({ l: r.left, t: r.top })); } catch (e) {} | |
| 316 | + } | |
| 317 | + btn.addEventListener("pointerdown", function (e) { | |
| 318 | + if (!e.isPrimary) return; | |
| 319 | + drag.on = true; drag.moved = false; drag.id = e.pointerId; | |
| 320 | + drag.sx = e.clientX; drag.sy = e.clientY; | |
| 321 | + var r = btn.getBoundingClientRect(); | |
| 322 | + drag.bx = r.left; drag.by = r.top; | |
| 323 | + try { btn.setPointerCapture(e.pointerId); } catch (err) {} // capture immédiate : un glisser rapide sort de la bulle avant le 1er move | |
| 324 | + }); | |
| 325 | + btn.addEventListener("pointermove", function (e) { | |
| 326 | + if (!drag.on || e.pointerId !== drag.id) return; | |
| 327 | + var dx = e.clientX - drag.sx, dy = e.clientY - drag.sy; | |
| 328 | + if (!drag.moved && dx * dx + dy * dy < 36) return; // < 6 px : encore un clic | |
| 329 | + if (!drag.moved) { | |
| 330 | + drag.moved = true; | |
| 331 | + btn.classList.add("kaa-drag"); | |
| 332 | + } | |
| 333 | + applyPos(drag.bx + dx, drag.by + dy); | |
| 334 | + }); | |
| 335 | + function dragEnd(e) { | |
| 336 | + if (!drag.on || e.pointerId !== drag.id) return; | |
| 337 | + drag.on = false; | |
| 338 | + if (drag.moved) { | |
| 339 | + btn.classList.remove("kaa-drag"); | |
| 340 | + savePos(); | |
| 341 | + setTimeout(function () { drag.moved = false; }, 0); // filet si le click ne suit pas | |
| 342 | + } | |
| 343 | + } | |
| 344 | + btn.addEventListener("pointerup", dragEnd); | |
| 345 | + btn.addEventListener("pointercancel", dragEnd); | |
| 346 | + window.addEventListener("resize", function () { | |
| 347 | + if (!btn.style.left) return; | |
| 348 | + var r = btn.getBoundingClientRect(); | |
| 349 | + applyPos(r.left, r.top); | |
| 350 | + }); | |
| 351 | + (function restorePos() { | |
| 352 | + var p = null; | |
| 353 | + try { p = JSON.parse(localStorage.getItem(POSKEY) || "null"); } catch (e) {} | |
| 354 | + if (p && typeof p.l === "number" && typeof p.t === "number") applyPos(p.l, p.t); | |
| 355 | + })(); | |
| 356 | + | |
| 357 | + btn.addEventListener("click", function (e) { | |
| 358 | + if (drag.moved) { drag.moved = false; e.preventDefault(); return; } // fin de glisser, pas un clic | |
| 283 | 359 | panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office |
| 284 | 360 | render(); |
| 285 | 361 | if (fineInput.matches) ta.focus(); |
| 286 | 362 | |