ka-agent v3 : bulle déplaçable par l'usager (drag pointer events + position mémorisée par site) — sync ka-ui 4e63c53
1 changed file +78 −2
modified
frontend/public/ka-agent.js
+78 −2
@@ -10,6 +10,10 @@ | |||||
| 10 | * streaming (titres, listes, tableaux, code, liens nommés, citations). | 10 | * streaming (titres, listes, tableaux, code, liens nommés, citations). | ||
| 11 | * Flux SSE en direct depuis le backend central (texte token par token + | 11 | * Flux SSE en direct depuis le backend central (texte token par token + | ||
| 12 | * indicateur d'outil). Historique conservé en localStorage (par site). | 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 | (function () { | 18 | (function () { | ||
| 15 | "use strict"; | 19 | "use strict"; | ||
@@ -25,8 +29,10 @@ | |||||
| 25 | border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); | 29 | border-radius:50%;border:2px solid var(--ink,#141814);background:var(--accent,#d9f26b); | ||
| 26 | color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif; | 30 | color:var(--on-accent,#141814);font:700 17px/1 "Space Grotesk",system-ui,sans-serif; | ||
| 27 | cursor:pointer;box-shadow:4px 4px 0 rgba(20,24,20,.85);display:flex;align-items:center; | 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 | .kaa-btn:hover{transform:translate(-2px,-2px)} | 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 | .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee); | 36 | .kaa-panel{position:fixed;z-index:2147483001;display:none;flex-direction:column;background:var(--paper,#f5f3ee); | ||
| 31 | overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation} | 37 | overflow:hidden;font-family:Inter,system-ui,sans-serif;touch-action:manipulation} | ||
| 32 | .kaa-panel.kaa-open{display:flex} | 38 | .kaa-panel.kaa-open{display:flex} | ||
@@ -34,6 +40,7 @@ | |||||
| 34 | .kaa-panel:not(.kaa-full){right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));width:min(392px,calc(100vw - 24px)); | 40 | .kaa-panel:not(.kaa-full){right:16px;bottom:calc(86px + env(safe-area-inset-bottom,0px));width:min(392px,calc(100vw - 24px)); | ||
| 35 | height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px)); | 41 | height:min(580px,calc(100vh - 110px));height:min(580px,calc(100dvh - 110px)); | ||
| 36 | border:2px solid var(--ink,#141814);border-radius:14px;box-shadow:8px 8px 0 rgba(20,24,20,.85)} | 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 | html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none;touch-action:none} | 44 | html.kaa-lock,html.kaa-lock body{overflow:hidden!important;overscroll-behavior:none;touch-action:none} | ||
| 38 | .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); | 45 | .kaa-head{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--ink,#141814); | ||
| 39 | color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))} | 46 | color:var(--paper,#f5f3ee);flex:none;padding-top:max(11px,env(safe-area-inset-top,0px))} | ||
@@ -279,7 +286,76 @@ | |||||
| 279 | fitVV(); | 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 | panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office | 359 | panel.classList.add("kaa-open", "kaa-full"); // plein écran d'office | ||
| 284 | render(); | 360 | render(); | ||
| 285 | if (fineInput.matches) ta.focus(); | 361 | if (fineInput.matches) ta.focus(); | ||
| 286 | 362 | ||||