glytch/fktry/src/ui/toasts.ts
type-two c18309f464 [ui] build bar, top strip, inspector, commission fax, toasts
The five M1 surfaces, replacing the stub:

- buildbar.ts: one button per machine, hotkeys 1-9, R rotates, Esc or
  right-click clears. LANE-DATA shipped 21 machines, so the bar wraps
  rather than running off both edges of the viewport; slots 10-21 are
  click-only for now (paging proposed in NOTES).
- topstrip.ts: bandwidth meter going amber at 85% and red + flashing
  BROWNOUT over 100%, buffer, shipped ticker + per-item chips, tick or
  HALTED. The brownout is legible from this panel alone, which is the
  one failure the player has to read across the room.
- inspector.ts: name, codex flavor, intake/output chips with live
  buffers, process bar, heat, jam reason. Holds an entity id and
  re-looks-up each frame, so a removed unit closes itself.
- fax.ts: the commission card with wants-as-chips and a FAX SENT stamp
  on commissionDone.
- toasts.ts: max 4, 6s TTL, colored per event kind.

Click-to-inspect is not wired: the UI cannot reach renderer.pickTile
(UIBus exposes only dispatch/selectedBuild, and ui.init never sees the
Renderer). TAB cycles the unit manifest as a stopgap so the inspector is
reachable without a console. Contract request filed in NOTES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:15:50 +10:00

50 lines
1.4 KiB
TypeScript

/**
* LANE-UI — event toasts, bottom right. Max 4 on screen, 6s each, oldest evicted.
*
* Wall-clock timing is fine here: toasts are chrome, not sim. The determinism rule in
* MASTERPLAN binds `src/sim/**`, and nothing in this file feeds back into it.
*/
import type { SimEvent } from '../contracts';
import { cls, el } from './dom';
import { eventToast } from './voice';
export interface Toasts {
el: HTMLElement;
push(ev: SimEvent, machineName: (entityId: number) => string): void;
/** Called once per frame to expire old lines. */
update(nowMs: number): void;
}
const TTL_MS = 6000;
const FADE_MS = 400; // must match the .fk-toast.is-out transition
const MAX = 4;
export function createToasts(): Toasts {
const root = el('div');
root.id = 'fk-toasts';
const live: Array<{ node: HTMLElement; dieAt: number }> = [];
function retire(entry: { node: HTMLElement; dieAt: number }) {
cls(entry.node, 'is-out', true);
setTimeout(() => entry.node.remove(), FADE_MS);
}
return {
el: root,
push(ev, machineName) {
const msg = eventToast(ev, machineName);
if (!msg) return;
const node = el('div', `fk-toast k-${ev.kind}`, [msg]);
root.append(node);
live.push({ node, dieAt: performance.now() + TTL_MS });
while (live.length > MAX) retire(live.shift()!);
},
update(nowMs) {
while (live.length && live[0].dieAt <= nowMs) retire(live.shift()!);
},
};
}