Changeset 39dab6b in Klonkt for src/assets


Ignore:
Timestamp:
08/12/2026 09:43:20 PM (3 months ago)
Author:
Robin <roboburr@…>
Children:
5e0fea7
Parents:
2086ea2
Message:

Ward-PWA: de mini-speler, de themakleur en een breder kaartraster

Drie dingen in een ronde.

DE THEMAKLEUR (SkyPalette). De acht presets en de luminantie-regel komen
LETTERLIJK uit Shaer: dezelfde kleuren, en dezelfde beslissing welke
tekst erop leesbaar is (donker op zon, wit op nacht). Een handgehouden
lijstje zou uit de pas lopen; luminantie beslist. De keuze kleurt het
accent (knoppen, bubbels, de (+)), tint de achtergrond zacht, en laat de
hemel zijn placeholder-schijfjes bijkleuren. Opgeslagen per apparaat.

DE MINI-SPELER. Een feed-kaart rendert een gewone audio, maar de feed
wordt bij elke verversing opnieuw getekend en dan sterft dat element:
de muziek stopt midden in een nummer. Daarom is er EEN blijvende speler
buiten de feed; een audiotegel geeft alleen zijn bron door (gevangen in
de capture-fase) en pauzeert zichzelf. Zo overleeft het geluid een
herteken en een wissel naar de hemel, precies als in de app.

HET KAARTRASTER (Robins wens, 12-8). Op desktop net als op mobiel en
tablet MEER kolommen: auto-fill met een minimum van 280px vult zelf op,
een op een smalle telefoon, twee op een tablet, drie of meer breed.

New file:
src/assets/js/ward-theme.js

  • presets, onSky (luminantie), load/save/apply

Changed files:
src/assets/js/ward.js

  • wireTheme (swatches), wireMiniPlayer (gedeelde audio)
  • de hemel skint bij op een themawissel

src/assets/js/ward-sky.js

  • buildAtlas neemt het accent; reskin() bij een kleurwissel

src/views/pages/ward.ejs

  • themaknop, themadialoog, mini-speler-balk

src/assets/css/ward.css

  • accent/achtergrond als variabelen, mini-speler, swatches, responsief raster (auto-fill minmax 280px)

src/services/i18n.js

  • kleur + play/pause in nl, en, de

test/ward-theme.test.js

  • acht presets, en de tekstkleur volgt luminantie

remarks: 905 tests groen. Slepen en het echt doorspelen van audio zijn
in code getest, niet met oor en vinger; dat is aan Robin op dev.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Location:
src/assets
Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • src/assets/css/ward.css

    r2086ea2 r39dab6b  
    66  --dim: #9aa7bb;
    77  --accent: #5a32e6;
     8  --on-accent: #ffffff;
     9  --bg-top: #1a2233;
     10  --bg-bottom: #0d1017;
    811  --alarm: #ef9f27;
    912  --ok: #46c26a;
    … …  
    1720body {
    1821  margin: 0; padding: 16px calc(16px + env(safe-area-inset-right)) 32px calc(16px + env(safe-area-inset-left));
    19   background: var(--bg); color: var(--ink);
     22  background: linear-gradient(var(--bg-top), var(--bg-bottom)) fixed, var(--bg);
     23  color: var(--ink);
    2024  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    2125}
    … …  
    2731.help button {
    2832  width: 100%; padding: 18px; border: 0; border-radius: 14px;
    29   background: var(--accent); color: #fff; font-size: 1.1rem; font-weight: 600;
     33  background: var(--accent); color: var(--on-accent); font-size: 1.1rem; font-weight: 600;
    3034}
    3135.help button:disabled { background: #3a3f4d; color: var(--dim); }
    … …  
    5054
    5155/* ── De feed ──────────────────────────────────────────────────────────── */
    52 .cards { display: grid; gap: 12px; }
    5356.post { background: var(--card); border-radius: 12px; padding: 12px 14px; border: 2px solid transparent; }
    5457.post .avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; }
    … …  
    8487dialog#help-dialog menu { display: flex; gap: 8px; justify-content: flex-end; margin: 14px 0 0; padding: 0; }
    8588dialog#help-dialog button { border: 0; border-radius: 10px; padding: 10px 14px; background: #2a3342; color: var(--ink); font: inherit; }
    86 dialog#help-dialog button.primary { background: var(--accent); color: #fff; font-weight: 600; }
     89dialog#help-dialog button.primary { background: var(--accent); color: var(--on-accent); font-weight: 600; }
    8790
    8891/* Voor schermlezers, niet voor het oog. */
    … …  
    138141  background: #131a25; color: var(--ink); padding: 10px 16px; font: inherit;
    139142}
    140 #thread-form button { border: 0; border-radius: 999px; padding: 10px 18px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
     143#thread-form button { border: 0; border-radius: 999px; padding: 10px 18px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
    141144
    142145/* ── Het zwaai-paneel ─────────────────────────────────────────────────── */
    … …  
    148151dialog#wave-dialog h2 { margin: 0 0 12px; font-size: 1.05rem; }
    149152.wave-answers { display: grid; gap: 8px; }
    150 .wave-answers button { border: 0; border-radius: 10px; padding: 12px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
     153.wave-answers button { border: 0; border-radius: 10px; padding: 12px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
    151154#wave-cancel { margin-top: 10px; border: 0; background: none; color: var(--dim); font: inherit; }
    152155
    … …  
    170173.feed-head { display: flex; align-items: center; gap: 12px; }
    171174.feed-head h2 { margin: 24px 0 10px; }
    172 #compose-open { margin-left: auto; border: 0; border-radius: 999px; padding: 8px 16px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
     175#compose-open { margin-left: auto; border: 0; border-radius: 999px; padding: 8px 16px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
    173176
    174177dialog#compose-dialog {
    … …  
    180183.compose-head #compose-cancel { border: 0; background: #2a3342; color: var(--ink); border-radius: 10px; padding: 8px 12px; font-size: 1rem; }
    181184.compose-head #compose-vis { flex: 1; border-radius: 10px; border: 1px solid #2a3342; background: #131a25; color: var(--ink); padding: 8px; font: inherit; }
    182 .compose-head .primary { border: 0; border-radius: 10px; padding: 8px 16px; background: var(--accent); color: #fff; font: inherit; font-weight: 600; }
     185.compose-head .primary { border: 0; border-radius: 10px; padding: 8px 16px; background: var(--accent); color: var(--on-accent); font: inherit; font-weight: 600; }
    183186.compose-head .primary:disabled { background: #3a3f4d; color: var(--dim); }
    184187#compose-text { width: 100%; border-radius: 12px; border: 1px solid #2a3342; background: #131a25; color: var(--ink); padding: 10px; font: inherit; resize: vertical; }
    … …  
    192195.compose-tools .tool { font-size: 1.4rem; cursor: pointer; padding: 4px 8px; border-radius: 10px; background: #131a25; }
    193196.compose-tools .hint { margin-left: auto; }
     197
     198/* ── Het kaartraster: meer kolommen naarmate er ruimte is ─────────────── */
     199/* Robins wens (12-8): op desktop net als op mobiel en tablet MEERDERE
     200   kolommen. auto-fill met een minimum vult zelf op: één kolom op een smalle
     201   telefoon, twee op een tablet, drie of meer op een breed scherm. */
     202.cards {
     203  display: grid;
     204  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
     205  gap: 12px;
     206  align-items: start;
     207}
     208
     209/* ── De mini-speler ───────────────────────────────────────────────────── */
     210#mini-player {
     211  position: fixed; left: 12px; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom));
     212  z-index: 45; display: flex; align-items: center; gap: 10px;
     213  padding: 8px 12px; border-radius: 14px;
     214  background: var(--card); box-shadow: 0 6px 24px rgba(0,0,0,.4);
     215  max-width: 520px; margin: 0 auto;
     216}
     217#mini-player #mini-toggle { border: 0; border-radius: 50%; width: 40px; height: 40px; background: var(--accent); color: var(--on-accent); font-size: 1rem; }
     218#mini-player #mini-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     219#mini-player #mini-close { border: 0; background: none; color: var(--dim); font-size: 1rem; }
     220
     221/* ── De themakiezer ───────────────────────────────────────────────────── */
     222.icon-btn { border: 0; background: #2a3342; border-radius: 999px; width: 38px; height: 38px; font-size: 1.1rem; }
     223dialog#theme-dialog {
     224  border: 0; border-radius: 16px; padding: 18px; max-width: 320px; width: calc(100% - 48px);
     225  background: var(--card); color: var(--ink); text-align: center;
     226}
     227dialog#theme-dialog::backdrop { background: rgba(0,0,0,.4); }
     228dialog#theme-dialog h2 { margin: 0 0 14px; font-size: 1.05rem; }
     229.swatches { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
     230.swatch { width: 100%; aspect-ratio: 1; border: 3px solid transparent; border-radius: 50%; cursor: pointer; }
     231.swatch.chosen { border-color: var(--ink); }
     232#theme-close { margin-top: 14px; border: 0; background: none; color: var(--dim); font: inherit; }
  • src/assets/js/ward-sky.js

    r2086ea2 r39dab6b  
    9999
    100100/** Eén canvas met alle avatars naast elkaar, zodat tekenen één drawImage is. */
    101 async function buildAtlas(faces) {
     101async function buildAtlas(faces, accent) {
    102102  const size = FACE * 2;                       // retina-scherp
    103103  const cols = Math.max(1, Math.ceil(Math.sqrt(faces.length || 1)));
    … …  
    120120      g.save();
    121121      g.beginPath(); g.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2); g.clip();
    122       g.fillStyle = '#5a32e6'; g.fillRect(x, y, size, size);
     122      g.fillStyle = accent || '#5a32e6'; g.fillRect(x, y, size, size);
    123123      g.fillStyle = '#fff';
    124124      g.font = `600 ${size * 0.45}px system-ui, sans-serif`;
    … …  
    176176    this.faces = faces;
    177177    this.spots = orbitPlacement(faces.map((f) => f.ageMs));
    178     this.atlas = await buildAtlas(faces);
     178    this.atlas = await buildAtlas(faces, this._accent());
    179179    this._resize();
     180  }
     181
     182  /** De themakleur uit de CSS-variabele, voor de placeholder-schijfjes. */
     183  _accent() {
     184    return getComputedStyle(document.documentElement).getPropertyValue('--accent').trim() || '#5a32e6';
     185  }
     186
     187  /** Het thema veranderde: de letters-op-schijf opnieuw in het nieuwe accent. */
     188  async reskin() {
     189    if (this.faces.length) this.atlas = await buildAtlas(this.faces, this._accent());
    180190  }
    181191
  • src/assets/js/ward.js

    r2086ea2 r39dab6b  
    88 */
    99import { SkyRenderer } from '/ward/sky.js';
     10import { PRESETS, loadColor, saveColor, applyColor } from '/ward/theme.js';
    1011
    1112const state = JSON.parse(document.getElementById('ward-state').textContent);
     13
     14// De opgeslagen themakleur meteen toepassen, voor het eerste frame.
     15applyColor(loadColor());
    1216const $ = (id) => document.getElementById(id);
    1317
    … …  
    421425    } catch { /* zonder hemel blijft het scherm gewoon werken */ }
    422426  })();
     427
     428  // De hemel skint zijn placeholder-schijfjes bij als de themakleur wisselt.
     429  window.addEventListener('ward-accent', () => sky.reskin());
    423430}
    424431
    … …  
    515522}
    516523
    517 /** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
     524/**
     525 * De themakiezer (SkyPalette): acht presets, gekozen kleur kleurt alles.
     526 * Een rij swatches in een dialoog; de keuze staat meteen en blijft bewaard.
     527 */
     528function wireTheme() {
     529  const dlg = document.getElementById('theme-dialog');
     530  const row = document.getElementById('theme-swatches');
     531  const current = loadColor();
     532  row.replaceChildren();
     533  PRESETS.forEach((rgb) => {
     534    const b = el('button', 'swatch');
     535    b.type = 'button';
     536    b.style.background = '#' + rgb.toString(16).padStart(6, '0');
     537    b.setAttribute('aria-label', 'kleur ' + rgb.toString(16));
     538    if (rgb === current) b.classList.add('chosen');
     539    b.addEventListener('click', () => {
     540      saveColor(rgb);
     541      row.querySelectorAll('.swatch').forEach((s) => s.classList.remove('chosen'));
     542      b.classList.add('chosen');
     543      // De hemel tekent de placeholder-schijfjes in het accent; even opnieuw.
     544      window.dispatchEvent(new CustomEvent('ward-accent'));
     545    });
     546    row.append(b);
     547  });
     548  document.getElementById('theme-open').addEventListener('click', () => dlg.showModal());
     549  document.getElementById('theme-close').addEventListener('click', () => dlg.close());
     550}
     551
     552/**
     553 * De mini-speler: audio blijft doorspelen terwijl je scrolt of van stand
     554 * wisselt.
     555 *
     556 * Een feed-kaart rendert een gewone <audio>, maar de feed wordt bij elke
     557 * verversing opnieuw getekend en dan sterft dat element -- de muziek stopt
     558 * midden in een nummer. Daarom is er ÉÉN blijvende speler buiten de feed; een
     559 * audiotegel geeft alleen zijn bron door en pauzeert zichzelf. Zo overleeft
     560 * het geluid een herteken en een wissel naar de hemel, precies als in de app.
     561 */
     562function wireMiniPlayer() {
     563  const bar = document.getElementById('mini-player');
     564  const audio = document.getElementById('mini-audio');
     565  const title = document.getElementById('mini-title');
     566  const toggle = document.getElementById('mini-toggle');
     567
     568  // Vangen in de CAPTURE-fase: zodra een feed-audio wil spelen, neemt de
     569  // gedeelde speler het over. Delegatie op document, want de kaarten komen en
     570  // gaan.
     571  document.addEventListener('play', (e) => {
     572    const t = e.target;
     573    if (!(t instanceof HTMLAudioElement) || t === audio) return;
     574    e.preventDefault();
     575    t.pause();
     576    const card = t.closest('.post');
     577    title.textContent = card?.querySelector('.name')?.textContent || 'Audio';
     578    if (audio.src !== new URL(t.currentSrc || t.src, location.href).href) {
     579      audio.src = t.currentSrc || t.src;
     580    }
     581    audio.play().catch(() => {});
     582    bar.hidden = false;
     583  }, true);
     584
     585  toggle.addEventListener('click', () => { audio.paused ? audio.play() : audio.pause(); });
     586  audio.addEventListener('play', () => { toggle.textContent = '⏸'; });
     587  audio.addEventListener('pause', () => { toggle.textContent = '▶'; });
     588  audio.addEventListener('ended', () => { bar.hidden = true; });
     589  document.getElementById('mini-close').addEventListener('click', () => { audio.pause(); bar.hidden = true; });
     590}
     591
     592/** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. *//** De controle kost per guardian een remote fetch, dus hij komt ná het tekenen. */
    518593async function loadChecks() {
    519594  if (!state.guardians.length) return;
    … …  
    542617wireSky();
    543618wireComposer();
     619wireTheme();
     620wireMiniPlayer();
    544621loadFeed();
    545622renderOffers();
Note: See TracChangeset for help on using the changeset viewer.