PURPOSE

Full-screen viewer for browsing all nebula archetypes defined in @starship-survivors/data/nebula-archetypes (exported as DISPLAY_ARCHETYPES). Renders a live NebulaBackground of the currently selected archetype, exposes the same post-fx pipeline used by the playground FX tab, and provides a bake-to-PNG flipbook export. Mounted at route /nebula-viewer and deep-linkable via ?n=<index>.

OWNS

  • Local React state: idx (current archetype index, initialized from URL), showInfo, showFx, baking, bakeStatus, layerFxOpen.
  • stripRef (HTMLDivElement) — ref to the bottom selector strip used for auto-centering the active button.
  • Index navigation helpers: setIdxClamped (wrap-around modulo ARCHETYPES.length), prev, next, jump(delta), random (re-rolls until different from current).
  • bakeCurrent callback — invokes nebulaBakeFrames with a fixed config (tileSize: 1024, frameCount: 64, fps: 12, loopSeconds: 5.0) plus the current PostFxValues, then triggers a browser download of the returned PNG bytes.
  • Two module-level constant arrays: ART_STYLES (11 entries: borderlands, synthwave, vhs, pixelArt, oilPaint, watercolor, comic, blueprint, thermal, stainedGlass, cyberglitch) and POLISH_KNOBS (5 entries: polishBlur, polishHue, polishSat, polishLight, polishAnimSpeed).
  • Module-level helpers: readUrlIndex(), writeUrlIndex(idx), downloadPngBytes(bytes, filename).
  • Internal sub-components: FxPanel, FxSlider, FxToggle — all defined inside this file.
  • Inline style objects: navBtnStyle, iconBtnStyle, fxRowStyle, fxLabelStyle, fxValueStyle, fxSectionStyle.

READS FROM

  • DISPLAY_ARCHETYPES (re-exported as ARCHETYPES) from @starship-survivors/data/nebula-archetypes — the archetype list and its length.
  • usePostFxState from @starship-survivors/engine/rendering/post-fx-store — subscribes the component (and FxSlider/FxToggle) to live FX state.
  • getPostFxState, getPostFxIdentity from the same store — snapshot reads for bake input and per-knob identity values.
  • window.location.search and window.location.href on mount and on every index change.
  • PostFxState and PostFxValues types (both from the rendering layer).

PUSHES TO

  • setPostFxValue(key, value) — called from FxSlider (range input + reset button), FxToggle, and the RESET ALL button (zeros every art style, restores polish knobs to identity, turns off the three layer FX toggles).
  • nebulaBakeFrames(arch, opts) — async call that produces a PNG flipbook; result is downloaded via downloadPngBytes.
  • window.history.replaceState via writeUrlIndex — keeps ?n=<idx> in sync on every index change.
  • DOM mutations in downloadPngBytes: creates a Blob, an object URL, an <a> element appended to document.body, dispatches .click(), removes the element, and revokes the object URL after 1 s.
  • strip.scrollBy({ left, behavior: 'smooth' }) on the selector strip to keep the active button centered.

DOES NOT

  • Does not mutate archetype data — ARCHETYPES[idx] is read-only.
  • Does not persist anything outside the URL — no localStorage, no Supabase, no telemetry call.
  • Does not register a popstate listener; navigating the browser back button does not feed back into local state (URL is push-only via replaceState).
  • Does not block key handlers when an info or FX panel is open — the same keys remain bound; only an HTMLInputElement target short-circuits the handler.
  • Does not call any router; the file is referenced by whichever route table maps /nebula-viewer to this default export.
  • Does not import from @starship-survivors/engine beyond rendering/post-fx-store and rendering/nebula-engine.
  • Does not handle archetype-list mutation at runtime; setIdxClamped assumes ARCHETYPES.length is constant for the lifetime of the component.

Signals

  • useEffect([idx]) × 2: one writes the URL, one auto-scrolls the selector strip to center the active button (queries by data-arch-idx attribute).
  • useEffect([prev, next, jump, random, setIdxClamped, showInfo, showFx]) registers a single keydown listener on window and cleans it up on unmount.
  • useMemo([arch]) builds infoRows — a 15-row array of [label, value] tuples covering name, palette, curve, warp, density, speed, thresh, bg, shape, noiseFreq, noiseType, emit, sat, lum, fog.
  • Bake state machine: baking toggles button enabled/cursor; bakeStatus is a free-form status string set on start, success (frame count, tile size, grid dims, KB), or failure (error message). It is never cleared automatically.

Entry points

  • Default export: NebulaViewerScreen — the route component for /nebula-viewer.
  • URL contract: ?n=N where N is a finite integer in [0, ARCHETYPES.length). Anything else falls back to 0.
  • Keyboard contract: ArrowLeft / ArrowRight step ±1; PageUp / PageDown step ±10; Home / End jump to first/last; R randomizes; I toggles the info panel; F toggles the FX panel; Escape closes whichever panel is open (info first, then FX).
  • Top-bar UI: numeric index, archetype name, total count, plus four action buttons — FX, i, (random), BAKE.
  • Bottom UI: horizontally-scrolling button strip (one button per archetype, labeled i: name) and a PREV / NEXT row.

Pattern notes

  • All FX surface state lives in the shared post-fx-store, so changes made here also affect the ship playground and any other consumer — the file’s own state holds only viewer-local concerns (which panels are open, bake progress, current index).
  • The FX panel is laid out as a three-layer pipeline mirroring the playground’s FxTab: art-style sliders (mutually-styled presets) → optional layer-FX foldout (BG stars, sparkles, shooting stars, each with conditional sub-sliders gated on a >= 0.5 threshold) → polish knobs (blur, hue, sat, lightness, anim speed). The RESET ALL button zeros art styles and layer toggles but resets polish to per-knob identity rather than zero.
  • FxSlider highlights its value in amber when the current value differs from identity, providing a visual diff against the default.
  • FxToggle treats the underlying numeric FX value as a boolean by comparing >= 0.5; clicks write 0 or 1.
  • The selector-strip auto-scroll uses smooth scrolling and recomputes from the strip’s measured center vs. the button’s measured center — works correctly even after resize or when the strip has been manually scrolled.
  • Bake output filename pattern: nebula-<idx>-<slugified-name>-<tileSize>-<frameCount>f.png; the slugifier lowercases and replaces runs of non-alphanumeric characters with a single hyphen.
  • Type assertions: setPostFxValue(key, value as never) is used to bypass the generic key-typed signature, because the value type varies per key.
  • The keydown handler bails early when e.target instanceof HTMLInputElement — leaves room for a future search input without changing the dispatcher.
  • The BlobPart cast (bytes as BlobPart) is needed because Uint8Array is accepted by the Blob constructor at runtime but the TS lib types are stricter.