SPB Git

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

ka-agent v3 : bulle déplaçable par l'usager (drag pointer events + position mémorisée par site) — sync ka-ui 4e63c53

Simon-Pierre Boucher committed (Aug 22, 2026) parent e51e286 Browse files

2 changed files +156 −4

modified frontend/dist/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();
modified frontend/src/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
Menu

Theme