Gesture input type, redoal-relay client, gitea_releases, content-driven branding

Four coupled additions that let one portal build serve a second face
(redoal.com) next to uhhm.no:

- type: gesture requirement - gesture.js draws a single stroke on a
  DPR-aware canvas (pointer events, touch-action: none), mirrors
  {points, key} into the paired hidden input prosekit-style, and -
  when content declares relay: wss://... - speaks the redoal-relay
  protocol: announce on stroke end, ghost the ack's decoded key path,
  show echoes of similar strokes as thumbnails. Offline/broken relay
  degrades to a plain drawing input; the widget handle's stop()
  closes the socket on SPA navigation (yes.js lifecycle, not
  prosekit's fire-and-forget). Submit re-parses gesture values so the
  bucket stores a real object, not double-encoded JSON.
- gitea_releases resource source - token-authenticated
  /repos/{owner}/{repo}/releases, for advertising a private repo's
  releases (content pins url: null - private html_urls 404 publicly).
- site.yaml branding - optional, at the content repo root: title,
  wordmark, hero {kind: yes|gesture|plain, relay}. Absent file means
  the historical uhhm look, so uhhm changes nothing without a content
  edit. Hot-swapped with questions/aggregates on content reload;
  question_lint validates it in both --path and --repo modes.
- deploy.yml ships the same build twice: uhhm-portal (3010) as
  before, redoal-portal (3020, CONTENT_REPO=redoal/questions,
  redoal.com vhost). Needs host prep + REDOAL_OAUTH2_* repo
  secrets/vars before the new steps succeed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-23 13:03:29 +02:00
co-authored by Claude Fable 5
parent 6cd8cc6ff5
commit 0221253cba
9 changed files with 951 additions and 17 deletions
+87
View File
@@ -262,6 +262,31 @@ main.not-found {
text-shadow: 0 0.12em 1.4em var(--hero-glow);
}
/* The gesture hero (site.yaml hero.kind: gesture - redoal.com's
landing): a tall drawing surface where the visitor's stroke and
ambient echoes from the relay share the stage. Not sticky like the
YES piece - drawing and scrolling fight over the same finger. */
.hero-gesture .hero-draw {
position: relative;
width: 100%;
max-width: 46rem;
}
.hero-gesture .gesture-canvas {
aspect-ratio: auto;
/* Plain-vh fallback first, same reasoning as .hero-yes's height. */
height: 55vh;
height: 55svh;
border: none;
background: transparent;
}
.hero-gesture .gesture-status {
top: auto;
bottom: 0.55rem;
right: 0.55rem;
}
/* alternatives */
/* position + z-index on these three: everything that scrolls over the
@@ -529,6 +554,68 @@ textarea:focus {
overflow: hidden;
}
/* The gesture drawing field (gesture.js). Stroke/ghost/echo colors
live in the JS module's own palette table - canvas can't read these
custom properties - so palette changes go there AND here. */
.gesture-wrap {
position: relative;
}
.gesture-canvas {
display: block;
width: 100%;
aspect-ratio: 3 / 2;
background: var(--paper);
border: 0.06rem solid var(--line);
border-radius: 0.6rem;
cursor: crosshair;
/* Load-bearing on touch: without it the browser pans instead of
letting the stroke happen. */
touch-action: none;
}
.gesture-echoes {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
min-height: 3rem;
}
.gesture-echoes:empty {
display: none;
}
.gesture-echo {
width: 3rem;
height: 3rem;
background: var(--paper);
border: 0.06rem solid var(--line);
border-radius: 0.4rem;
opacity: 0;
transition: opacity 0.5s ease;
}
.gesture-echo.shown {
opacity: var(--echo-strength, 1);
}
.gesture-status {
position: absolute;
top: 0.55rem;
right: 0.55rem;
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
background: var(--ink-dim);
opacity: 0.35;
transition: opacity 0.3s ease, background 0.3s ease;
}
.gesture-status.live {
background: var(--accent);
opacity: 0.9;
}
.prosekit-toolbar {
display: flex;
gap: 0.2rem;