import { create } from './tinyStore.js' // Layout mode. "auto" = untouched grid: streams joining/leaving re-grid everything. The first manual move, resize // or rename makes it "custom": the grid is left alone, leavers just close, joiners wait in the tray (with a ding) // until dragged in. "Arrange" re-grids everything and goes back to auto. export const layout = create({ custom: false, pending: [], // streams live but not placed yet (custom mode) markCustom() { return this.custom ? null : { custom: true } }, reset() { return { custom: false, pending: [] } }, addPending(s) { return this.pending.some(p => p.id === s.id) ? null : { pending: [...this.pending, s] } }, dropPending(id) { return this.pending.some(p => p.id === id) ? { pending: this.pending.filter(p => p.id !== id) } : null }, keepPending(ids) { const pending = this.pending.filter(p => ids.has(p.id)); return pending.length === this.pending.length ? null : { pending } }, }) // short two-note chime (WebAudio, no asset). Browsers need one prior click on the page to allow sound. let ctx export function ding() { try { ctx = ctx || new (window.AudioContext || window.webkitAudioContext)() if (ctx.state === 'suspended') ctx.resume().catch(() => {}) const t0 = ctx.currentTime for (const [i, f] of [[0, 880], [0.16, 1320]]) { const o = ctx.createOscillator(), g = ctx.createGain() o.type = 'sine'; o.frequency.value = f g.gain.setValueAtTime(0, t0 + i); g.gain.linearRampToValueAtTime(0.25, t0 + i + 0.02); g.gain.exponentialRampToValueAtTime(0.001, t0 + i + 0.35) o.connect(g).connect(ctx.destination); o.start(t0 + i); o.stop(t0 + i + 0.4) } } catch {} }