Changeset 5218863 in Klonkt for src/assets
- Timestamp:
- 08/12/2026 08:25:10 PM (3 months ago)
- Children:
- 4bb93aa
- Parents:
- 084b046
- Location:
- src/assets
- Files:
-
- 3 edited
-
css/ward.css (modified) (1 diff)
-
js/ward-sky.js (modified) (1 diff)
-
js/ward.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/ward.css
r084b046 r5218863 100 100 /* De kaarten vallen weg, de hemel blijft: dezelfde beweging als in de app. */ 101 101 body.messages-mode .help, body.messages-mode .cards { display: none; } 102 103 /* ── De zwaai-hand ────────────────────────────────────────────────────── */ 104 /* Zelfde hoek als de boei; ze wisselen elkaar af met de stand. */ 105 #hand { 106 position: fixed; right: 18px; bottom: calc(18px + env(safe-area-inset-bottom)); 107 width: 76px; height: 76px; border: 0; border-radius: 50%; 108 background: transparent; font-size: 52px; line-height: 1; 109 touch-action: none; cursor: grab; z-index: 50; 110 } 111 #hand.dragging { cursor: grabbing; transform: scale(1.1); } 112 113 /* ── Het gesprek over de hemel ────────────────────────────────────────── */ 114 #thread { 115 position: fixed; inset: 0; z-index: 40; 116 display: flex; flex-direction: column; 117 /* Doorschijnend: de hemel eronder blijft zichtbaar draaien, net als de 118 glazen bubbels in de app. */ 119 background: rgba(20, 26, 36, .72); backdrop-filter: blur(14px); 120 } 121 #thread header { display: flex; align-items: center; gap: 10px; padding: 12px 16px; } 122 #thread header h2 { margin: 0; font-size: 1.05rem; } 123 #thread-close { border: 0; background: #2a3342; color: var(--ink); border-radius: 10px; padding: 8px 12px; font-size: 1rem; } 124 #thread-list { flex: 1; overflow-y: auto; padding: 8px 16px; display: flex; flex-direction: column; gap: 8px; } 125 .bubble { max-width: 78%; padding: 10px 12px; border-radius: 14px; background: var(--card); } 126 .bubble.out { align-self: flex-end; background: var(--accent); } 127 .bubble.wave { font-size: 1.4rem; } 128 .bubble .when { display: block; margin-top: 4px; font-size: .7rem; color: rgba(255,255,255,.55); } 129 .bubble .body img { max-width: 100%; border-radius: 8px; } 130 #thread-form { display: flex; gap: 8px; padding: 10px 16px calc(14px + env(safe-area-inset-bottom)); } 131 #thread-form input { 132 flex: 1; border-radius: 999px; border: 1px solid #2a3342; 133 background: #131a25; color: var(--ink); padding: 10px 16px; font: inherit; 134 } 135 #thread-form button { border: 0; border-radius: 999px; padding: 10px 18px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; } 136 137 /* ── Het zwaai-paneel ─────────────────────────────────────────────────── */ 138 dialog#wave-dialog { 139 border: 0; border-radius: 16px; padding: 18px; max-width: 320px; width: calc(100% - 48px); 140 background: var(--card); color: var(--ink); text-align: center; 141 } 142 dialog#wave-dialog::backdrop { background: rgba(0,0,0,.4); } 143 dialog#wave-dialog h2 { margin: 0 0 12px; font-size: 1.05rem; } 144 .wave-answers { display: grid; gap: 8px; } 145 .wave-answers button { border: 0; border-radius: 10px; padding: 12px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; } 146 #wave-cancel { margin-top: 10px; border: 0; background: none; color: var(--dim); font: inherit; } -
src/assets/js/ward-sky.js
r084b046 r5218863 172 172 this.atlas = await buildAtlas(faces); 173 173 this._resize(); 174 } 175 176 /** Publiek mikken, voor de zwaai-hand: zelfde rekensom als een tik. */ 177 pick(px, py) { 178 return nearestFace(px, py, this.spots, { 179 cx: this.w / 2, cy: this.h / 2, cell: CELL, scale: this.scale, 180 panX: this.panX, panY: this.panY, 181 }); 174 182 } 175 183 -
src/assets/js/ward.js
r084b046 r5218863 231 231 const sky = new SkyRenderer(canvas, (i) => { 232 232 const c = convs[i]; 233 if (c) window.location.href = `/ward/thread?with=${encodeURIComponent(c.uri)}`; 233 if (c) openThread(c); 234 }); 235 236 /* ── Het gesprek: een overlay OVER de levende hemel ──────────────────── 237 Geen aparte pagina: de hemel eronder blijft dezelfde draaiende 238 instantie, precies zoals de thread in Shaer over de hemel schuift. */ 239 const thread = document.getElementById('thread'); 240 const list = document.getElementById('thread-list'); 241 let openWith = null; 242 let pollTimer = null; 243 244 async function loadThread() { 245 if (!openWith) return; 246 try { 247 const r = await fetch(`/ward/api/thread?with=${encodeURIComponent(openWith.uri)}`, { headers: { Accept: 'application/json' } }); 248 if (!r.ok) return; 249 const { items } = await r.json(); 250 list.replaceChildren(); 251 // De server geeft nieuwste eerst; een gesprek lees je oudste eerst. 252 [...items].reverse().forEach((m) => { 253 const b = el('div', `bubble ${m.dir}${m.wave ? ' wave' : ''}`); 254 const body = el('div', 'body'); 255 body.innerHTML = m.bodyHtml || ''; 256 b.append(body, el('span', 'when', m.whenText || '')); 257 list.append(b); 258 }); 259 list.scrollTop = list.scrollHeight; 260 } catch { /* volgende poll probeert opnieuw */ } 261 } 262 263 function openThread(c) { 264 openWith = c; 265 document.getElementById('thread-name').textContent = c.name; 266 thread.hidden = false; 267 loadThread(); 268 // Pollen zolang het gesprek open staat: eenvoudig en zuinig genoeg. 269 // Een socket kan later; het ritme van een gesprek is geen milliseconde. 270 clearInterval(pollTimer); 271 pollTimer = setInterval(loadThread, 8000); 272 } 273 274 function closeThread() { 275 openWith = null; 276 thread.hidden = true; 277 clearInterval(pollTimer); 278 } 279 document.getElementById('thread-close').addEventListener('click', closeThread); 280 document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && openWith) closeThread(); }); 281 282 document.getElementById('thread-form').addEventListener('submit', async (e) => { 283 e.preventDefault(); 284 const input = document.getElementById('thread-text'); 285 const text = input.value.trim(); 286 if (!text || !openWith) return; 287 input.value = ''; 288 await sendMessage(openWith.uri, text, false); 289 loadThread(); 290 }); 291 292 async function sendMessage(to, text, wave) { 293 try { 294 const r = await fetch('/ward/api/message', { 295 method: 'POST', 296 headers: { 'Content-Type': 'application/json' }, 297 body: JSON.stringify({ to, text, wave }), 298 }); 299 return r.ok; 300 } catch { return false; } 301 } 302 303 /* ── De zwaai-hand (§5) ──────────────────────────────────────────────── 304 Dezelfde hoek als de boei, en ze wisselen elkaar af met de stand: met de 305 kaarten verdwijnt de boei, op de hemel hangt de hand. Slepen op een 306 gezicht opent het paneel; de tekst die je kiest IS het bericht. */ 307 const hand = document.getElementById('hand'); 308 const waveDlg = document.getElementById('wave-dialog'); 309 let waveTarget = null; 310 311 (() => { 312 let dragging = false, dx = 0, dy = 0; 313 hand.addEventListener('pointerdown', (e) => { 314 dragging = true; 315 hand.setPointerCapture(e.pointerId); 316 const r = hand.getBoundingClientRect(); 317 dx = e.clientX - r.left; dy = e.clientY - r.top; 318 hand.classList.add('dragging'); 319 }); 320 hand.addEventListener('pointermove', (e) => { 321 if (!dragging) return; 322 hand.style.left = `${e.clientX - dx}px`; 323 hand.style.top = `${e.clientY - dy}px`; 324 hand.style.right = 'auto'; hand.style.bottom = 'auto'; 325 }); 326 const drop = (e) => { 327 if (!dragging) return; 328 dragging = false; 329 hand.classList.remove('dragging'); 330 hand.style.left = hand.style.top = 'auto'; 331 hand.style.right = ''; hand.style.bottom = ''; 332 // Zelfde rekensom als een tik op de hemel: het dichtstbijzijnde gezicht 333 // binnen bereik wint, en ernaast is bewust niets. 334 const r = canvas.getBoundingClientRect(); 335 const hit = sky.pick(e.clientX - r.left, e.clientY - r.top); 336 if (hit != null && convs[hit]) { 337 waveTarget = convs[hit]; 338 document.getElementById('wave-title').textContent = 339 state.strings.waveTo.replace('{who}', waveTarget.name); 340 waveDlg.showModal(); 341 } 342 }; 343 hand.addEventListener('pointerup', drop); 344 hand.addEventListener('pointercancel', drop); 345 })(); 346 347 waveDlg.addEventListener('click', async (e) => { 348 const btn = e.target.closest('button'); 349 if (!btn) return; 350 waveDlg.close(); 351 if (btn.id === 'wave-cancel' || !waveTarget) return; 352 const isWave = btn.dataset.wave === '1'; 353 const text = isWave ? '👋' : btn.dataset.text; 354 if (await sendMessage(waveTarget.uri, text, isWave)) openThread(waveTarget); 234 355 }); 235 356 … … 244 365 canvas.style.pointerEvents = on ? 'auto' : 'none'; 245 366 document.body.classList.toggle('messages-mode', on); 367 document.getElementById('buoy').hidden = on; 368 hand.hidden = !on; 369 if (!on) closeThread(); 246 370 btn.textContent = on ? state.strings.toCards : state.strings.toSky; 247 371 btn.setAttribute('aria-pressed', String(on));
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)