Hero becomes a content-owned module; site asset proxy; gesture growth fix
Test / test (push) Successful in 24s

site.yaml's hero is now plain | module, where module names a
JavaScript file the content repo ships (mount(container) -> handle
with stop()). Portal serves content assets same-origin at
/site/<path> (Gitea raw sends no CORS headers), starts the module at
HTML parse time, adopts it on hydration, mounts fresh via the inline
script's __mountHero on client-side navigation, and stops it on
leave. The YES canvas (yes.js) and the gesture hero mode leave the
engine - uhhm/questions ships YES as its hero.js, redoal/questions
ships a sine-swings band. Gesture form canvas no longer balloons after
a stroke: the wrap's aspect-ratio reservation is scoped to :empty
(pre-mount) so it can't turn echo-strip height into width inside the
flex field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-25 17:26:29 +02:00
co-authored by Claude Fable 5
parent 307fd7e753
commit 1afd34c22e
8 changed files with 230 additions and 884 deletions
+6 -28
View File
@@ -42,14 +42,12 @@ const RECONNECT_BASE_MS = 1000;
const RECONNECT_MAX_MS = 30000;
class GestureWidget {
constructor(container, hidden, relayUrl, hero) {
constructor(container, hidden, relayUrl) {
this.container = container;
this.hidden = hidden || null;
this.relayUrl = relayUrl || '';
this.hero = !!hero;
this.points = [];
this.ghost = null;
this.ambient = []; // hero mode: echoed strokes drawn in place
this.drawing = false;
this.stopped = false;
this.ws = null;
@@ -60,11 +58,9 @@ class GestureWidget {
this.canvas = document.createElement('canvas');
this.canvas.className = 'gesture-canvas';
container.appendChild(this.canvas);
if (!this.hero) {
this.echoes = document.createElement('div');
this.echoes.className = 'gesture-echoes';
container.appendChild(this.echoes);
}
this.echoes = document.createElement('div');
this.echoes.className = 'gesture-echoes';
container.appendChild(this.echoes);
if (this.relayUrl) {
this.status = document.createElement('span');
this.status.className = 'gesture-status offline';
@@ -119,7 +115,6 @@ class GestureWidget {
this.drawing = true;
this.points = [this.pos(e)];
this.ghost = null;
this.ambient = [];
this.render();
}
@@ -223,13 +218,7 @@ class GestureWidget {
this.ghost = msg.path;
this.render();
} else if (msg.type === 'echo') {
if (this.hero) {
this.ambient.push(msg.path);
if (this.ambient.length > MAX_ECHOES) this.ambient.shift();
this.render();
} else {
this.addEchoThumbnail(msg);
}
this.addEchoThumbnail(msg);
}
// 'error' frames are intentionally silent: the widget is a
// form field first, and a rate-limited announce shouldn't
@@ -284,11 +273,6 @@ class GestureWidget {
if (!this.ctx) return;
const t = this.theme();
this.ctx.clearRect(0, 0, this.cssWidth, this.cssHeight);
for (const path of this.ambient) {
this.ctx.globalAlpha = 0.25;
this.drawPath(this.ctx, path, this.cssWidth, this.cssHeight, t.echo, 1.5, 0.2);
}
this.ctx.globalAlpha = 1;
if (this.ghost) {
this.drawPath(this.ctx, this.ghost, this.cssWidth, this.cssHeight, t.ghost, 2, 0.2);
}
@@ -325,11 +309,5 @@ class GestureWidget {
}
export function mountGesture(container, hidden, relayUrl) {
return new GestureWidget(container, hidden, relayUrl, false);
}
// Hero variant: no form field to mirror into, and echoes render as
// ambient strokes on the drawing canvas itself instead of thumbnails.
export function mountGestureHero(container, relayUrl) {
return new GestureWidget(container, null, relayUrl, true);
return new GestureWidget(container, hidden, relayUrl);
}