Merge pull request #12 from knonode/main
much update many wow
97d51dd5
main
106/14,191 ++ 25,285 --
Commits and releases over time
No repositories match that filter.
much update many wow
It inherited the cyan the old viz buttons used, which is harder to read at eleven pixels than the white on the buttons beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
Eight buttons across the left of the second row, only ever one of them in use. They were the widest thing in the bar and pushed everything else onto a second line at any ordinary window size. The bar is one row again at 1100 pixels. The three unbuilt ones keep their slots and their order in the menu, greyed out, because the order is the roadmap and not an accident of when each was written. The menu's own name is the "none" entry, so picking Visualize again closes what is open, alongside Escape and a click on the overlay itself. Escape and the click reset the menu, or it would go on naming a visualization that had already gone. The guide loses its screenshot for this section: the set is in a dropdown now, and a closed dropdown shows none of it. The text names the menu instead, and the capture run drops the stale PNG on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
Three lines of height off the card, and a label that stops throwing my prose at you. "round trip" was a phrase, not a reading. It is lat. now, next to bal., and the two share one line: at a card's width they fit even at the worst case of a four-digit latency, measured down to a 241px card with four to a row. The tooltip spells both out, along with the escrow address. The resolved address under Play as answered "which account did that name mean" once and then charged for the answer forever. It clears itself after four seconds, and the row collapses rather than sitting empty. A failed lookup stays, because it is still true. Play as had a sixty-pixel label column to line up with rows that no longer exist — device and channel left for the header. The label takes the width of its own text now, so the input keeps what it gains as the card narrows. Card height: 893 to 865. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
Two problems, one of which had no answer at all. Nothing anywhere told you where to send money. The card showed a balance draining and no way to fill it — you would have had to read the TEAL to find the address. The balance line now carries it in its tooltip and copies it on a click, which costs no height, because a line that says "escrow: 0.141 ALGO" is already the line about the escrow. Device and channel were two rows in the parameter area. They are now two dropdowns in the header, where Type and Sub sit on every other card and where the room already existed. The MIDI card is the same height as an ordinary synth card again: 892 pixels against 892, measured rather than assumed. The device picker reads MIDI until a device is chosen, so the header still names the card. Device names are longer than the select is wide; the tooltip carries the full one. The guide follows both: where the pickers are, and that clicking the balance copies the address to top it up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
Nothing in this takes a keyboard. It takes note-on with velocity, which is what pads, MPCs, wind controllers, guitar pickups, sequencers and anything with a MIDI out send. The copy said keyboard throughout because the first device to play it was one. It read worst in the 0.11.0 note — "one keyboard can send up to eight parts" excludes exactly the multi-track devices that feature exists for. Two uses of the word are left. One is the computer keyboard that reorders cards, which is what it says. The other is the list of examples in the guide, where a keyboard is one of the things named. Copy only, so no version bump: a card that read the release notes once is not worth interrupting again over a wording fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
The guide gains a Channel paragraph in the MIDI section: leave it on all for one keyboard, add a card per track and set a channel each for a device that sends several at once. The release notes lead with the part that needs saying loudest, which is that everyone has to update. Notes on channels 2 to 8 are dropped by 0.10.0 rather than sounding wrong, so a friend still on the old build hears a quarter of a four-track performance and no error. MINOR: a 0.10.0 preset still loads, and channel is a new key with a default. The break is in what goes over the network, not in what a preset means, and the compatibility surface here is presets. The MIDI card screenshot is recaptured for the new row. The other twenty-one are left alone — a full run rewrites them all, but only this one changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
Rewritten in your voice, plus four typos. A tab had got into "Plug in your MIDI keyboard", and "plug in ... into" doubled the preposition. "Through relay node" wanted an article. In the guide, the Play as paragraph had lost the clause between "an NFD name.algo" and "Leave it empty", which left a semicolon in front of a capital letter. The paragraph after it kept a dash that no longer had anything on the other side of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
A groovebox is not a keyboard. Its tracks are separate instruments, and arriving as one merged stream in one voice is the wrong answer to what the device is for. The separation could have come from addresses — a different "Play as" per track, four tracks arriving as four strangers. That works today with no change at all, and it is wrong. Identity is load-bearing here: it carries your NFD, and it is what "hear only this player" asks about. Four tracks of one performance are not four people, and answering a filter for one of them with a quarter of the music is a bug wearing a feature's clothes. So the part rides in the asset ID, next to the pitch. Each note has always owned a thousand IDs and a keypress only ever needed a velocity, so nine tenths of every slot was empty: it now holds eight parts of a hundred steps each. The top two hundred stay unreachable from a keyboard, which is where the voice announcement already lives. Nothing about the escrow changes — the range check still holds, with two hundred to spare at the top — so the instrument keeps its address. Velocity drops from 128 levels to 100. It was only ever read as a gain between 0.05 and 1, so the loss is arithmetic rather than audible. This is a change to what goes on the wire. A listener on 0.10.0 will hear part 0 and silently drop the rest, since their decoder rejects a velocity above 127 — which is the correct behaviour for them and a reason for this to go out as a release rather than sit on a branch. The rest follows from the encoding. Voices are filed under player and part rather than player alone, so one performer's tracks stop overwriting each other's sound. A card knows which part it plays, so the card that sent a note is the one that sounds it, instead of all four playing every line. And a device now feeds as many cards as want it, each on its own channel — binding was one-at-a-time before, on the reasoning that two cards on one keyboard would pay twice for every keypress. They would not: they listen on different channels, and a keypress only lands on one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
The escrow readout says what the hat holds, which is everyone's. It does not say what you have spent, and those are different questions the moment more than one person is playing. They are different for a second reason, which is the one that prompted this. An arpeggiator bills you a note at a time without a finger moving, and a sequencer left running does it indefinitely — the first you would otherwise know of either is an empty hat. The count is worth its line on the card while it is happening rather than afterwards. Notes dropped by the rate cap get a line too. They cost nothing, but you pressed a key and heard silence, and that is worth saying rather than leaving you to wonder whether it went out. The readout is four short lines now instead of two long ones. A card is about twenty-eight characters wide at this size, so a line carrying two facts wraps mid-phrase and reads as neither. window.SOA_MIDI.spend() gives the same numbers from the console, next to voices() and playing(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
The escrow balance was read once when the card was built and never again, so the one number that tells you how much playing is left sat still while you spent it. A reload was the only way to find out. It is now driven by what is heard rather than by asking. Every note is a transaction that passes this browser on its way back from the relay, and every one of them costs the same minimum fee, so a keypress anywhere in the world moves the number here at the moment you hear the note that spent it. That covers other people's playing too, which is the point: the hat is shared, and watching it drain is part of knowing whether to top it up. Counting what you hear drifts — the stream can be stopped, a relay can miss a transaction, and someone who is not playing can top the hat up — so the count is corrected against the chain every forty-five seconds, and not at all while the tab is hidden, since nobody is watching a number they cannot see. A failed read is left alone rather than treated as a balance of nothing. The probe's floor drops from 0.2 ALGO to 0.05. The old figure was picked out of the air and refused to play a hat that could still afford a hundred and sixty notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
The guide gains a last section, after presets, because the keyboard is the one part of this app that does not listen. It says what a note actually is, that you will not hear a key when you press it, that the instrument belongs to nobody, and what Play as buys you — which is not identity for its own sake but the thing that lets your sound travel with your notes. The second row's section gains the two controls beside + MIDI. Others is described where the reader meets it rather than in the MIDI section, since it governs what you hear whether or not you ever plug a keyboard in. MINOR by the rule in the README: settings keys were added with sensible defaults and nothing was renamed or repurposed, so a preset written by 0.9.0 sounds the same here. Only two screenshots are committed. A full capture run rewrites all twenty-two, but the other twenty differ only in font rendering — this machine's Chromium against whoever captured them last — and the presets modal cannot have been changed by any of this work. Committing them would be twenty binary files of noise around the two that mean something. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
A note is a transaction: a zero-amount transfer of an asset that does not exist. The AVM allows this because a zero transfer returns before it looks up any holding, so no opt-in is needed, no supply is needed, and the asset ID is free to mean something other than an asset. It means the note — 2^40 + midiNote*1000 + velocity, three orders of magnitude above the highest real asset ID so it can never be mistaken for a transfer of something that exists. The amount cannot carry anything, which is the one thing that had to be designed around rather than chosen. A non-zero transfer needs a holding the sender does not have, so every note is aamt 0 and the pitch lives somewhere else. Sending to yourself is out for the same family of reasons: aamt 0 to your own address is the opt-in branch, which does look the asset up and fails. The instrument is an escrow with no private key. tools/midi/keyboard.teal authorises exactly one shape of transaction — axfer, amount zero, fee at most the minimum, no clawback, no close-out, no rekey, asset ID inside the note range — so the only thing that can ever leave the account is fees. Anyone holding the program can sign for it, which is the point: several people play the same instrument at once with no server, no session and nothing to coordinate, because Algorand has no nonces and simultaneous senders never collide. The receiver field carries who played, since the sender is the escrow for everybody. It costs nothing, a zero transfer never touching the receiving account, and it can be typed as an NFD. Nothing is heard when it is sent. A note is heard when it comes back through the relay, most of a second later, in whatever order gossip chose. That is not a defect to be papered over: the mempool is the delay line, and the instrument is one whose latency is a live measurement of the network. Slow, sparse, chordal playing sounds deliberate. Fast runs sound like a fax machine, and should. A player's sound travels with them, in the note field of the notes they are already sending. A fully designed voice serialises to about 460 bytes against the 1024 a note field allows, and mainnet's fee-per-byte is zero, so those bytes are free space rather than a cost — every note carries the whole sound, a change is heard on the very next keypress, and someone who joins mid-jam has your voice on the first note they hear. No transaction is ever sent for a voice alone. That voice is a stranger's data applied to your audio graph, so services/midi/voice.ts forces every value into a range Tone can take before it reaches a node, refuses a payload from a format version it does not know rather than voicing it with today's reader, and drops the fields that are a card's state rather than a sound. It cannot mute your synth or move your sequencer. It also cannot move the shared reverb, which belongs to everybody's hall rather than one player's patch. Applying a voice is cheap in the case that matters. Only the engine is a different class of object; waveform, envelope, detune, cutoff, delay, pan and LFO are parameters on nodes that already exist, so applyVoice sets them on a running graph and rebuilds only on an engine change. Since every note carries its player's whole sound, a fingerprint comparison on the way in means Tone is touched only when the sound actually differs — on a knob turn, never on a keypress. Nothing needs rate limiting. Four voices, evicted least-recently-heard, kept out of activeSynths on purpose: they are not cards, and must never be saved into a preset, reordered, or matched against ordinary transaction rules. Your own notes go to your own card, where the sound you are designing lives and where the LED tells you the mempool heard you. Everyone else goes to the pool. A card never plays another player's notes — hearing four people through one card's voice was only ever a stand-in for not having their voices. Anonymous players, having no address to file a sound under, share one slot and the house default rather than being silent. Two things had to be learned by encoding a transaction and reading the bytes back, because neither is visible in this code. Canonical msgpack omits zero-valued fields. So aamt is absent rather than 0 on every note, and arcv is absent entirely when the receiver is the zero address. Both guards are written around that. And gossip dedup was dropping notes. A logic-signed transaction has no sig, so the fallback key was a composite of type, sender, fee and validity window — every one of which is identical for every note this escrow plays in a given minute. The first note of each window would have sounded and the rest would have been swallowed in the ingest layer, before any synth saw them. The key is now built at normalize time, while the raw bytes are still in hand, from what actually differs: the asset ID that carries the note, the receiver that says who played it, and a hash of the bytes the sender added to keep two identical notes from being one transaction. This was never MIDI-specific; all lsig and msig traffic was under-deduplicated the same way, and MIDI only made it total. One bug is left alone deliberately. axfer-opt-in tests (aamt ?? -1) === 0 against a field msgpack drops, so that subtype has never matched anything. Fixing it changes what an existing preset hears, which is a release decision rather than a drive-by. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3U3qYRnw6NPt5BRj81S9X
Tabbing through a synth card showed nothing at all on the sliders. The app had no focus style of its own, so every control was falling back on the browser's, which is drawn in a colour chosen for a white page and hugs the thumb of a five-pixel track on a dark teal card. One mark for everything reachable by Tab, and :focus-visible rather than :focus so a mouse click on a slider does not leave a ring sitting there. The whole control is ringed rather than the knob: outlining the knob means taking over the slider's appearance wholesale, and then owning the look of every slider in the app forever. Two things had to be found by tabbing the card and reading back what each control actually computed, because neither is visible in this file. Most of the value boxes carry no type attribute — they are text inputs by default — and an attribute selector matches the attribute rather than the behaviour, so a list of input[type=...] silently skips exactly the controls you spend the most time in. Range then has to be named on top of that: a bare input:focus-visible reaches every other control and is beaten on the sliders alone, since the Tailwind and daisyUI form layer claims [type=range] by attribute and outranks a plain element selector. The value boxes also had hover and focus sharing one rule that cleared the outline, so a box you had tabbed to looked identical to one the mouse merely happened to be over. The border stays as the hover tell; focus keeps the ring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The grab strip has been sitting between the header and the parameter area promising a gesture that was never written. This is the gesture. A layout is read left to right and top to bottom, so the order of the cards is the order of the piece: which voice you reach for first, which three belong together. Until now that was the order you happened to add them in, and the only way to change it was to delete a card and build it again. The order of activeSynths follows the cards, because that array is what a preset is written from and rebuilt into — if the two disagree, a layout saved after a drag comes back in the order it had before. The card moves in the DOM as you drag rather than floating under the cursor while a placeholder holds its slot. The grid is auto-fit, so a card lifted out of flow changes the column count for everything else and the layout you were dragging over would not be the layout you dropped into. Moving the real node means the preview cannot lie. Two rules keep that from thrashing: a press does not become a drag until the pointer has travelled a few pixels, and a card only gives up its place once the pointer has properly arrived in it. Across a row that is the midpoint. Between rows it cannot be — a synth card is routinely taller than the window it is being dragged in, so the middle of the row below is off the bottom of the screen, and dragging down was asking for a position the pointer could not reach while dragging up worked fine. It is measured against the part of the target actually on screen instead: whatever is visible, cross half of it. The drag is followed on the window rather than by capturing the pointer. Moving a node is a removal and an insertion, and a capture target that leaves the document loses the capture — so capturing delivers events right up until the first card is displaced and then silently stops, which looks exactly like a drag that moves one slot and never lets go. Marked while dragging by an outline and nothing else. Cards are colour coded by transaction type and every one of those colours is a translucent wash over the page, so dimming a card takes its own colour with it and for a moment you are dragging a card of a type you did not pick. The strip is no longer aria-hidden, because it is a control now. Arrow keys move the focused card one place, Home and End take it to the ends, and every move is announced: what moved, the position it now holds, and which synth it is now behind and which it is now in front of. Neighbours are what a layout is made of — "third of seven" is a fact you have to hold, "after the pay drone, before the block bell" is a place — and reordering by keyboard is worthless if the only way to know where a card landed is to have counted. Enter repeats it without moving anything. Plain arrows rather than Ctrl-arrows: Ctrl with Left or Right is how macOS switches desktops and the browser is never told it happened. No pick-up-and-drop mode either — it is a state you can be stranded in, it needs a cancel key, and Space here is already play/stop everywhere. A keyboard move shifts the NEIGHBOUR past the card rather than the card past the neighbour. Both produce the same order and only one survives: a node holding the focus is blurred the moment it leaves the document, and reinserting it does not give the focus back, so moving the card itself fired once and then swallowed every arrow key after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for drag-to-reorder: the cards need somewhere to be grabbed that isn't already occupied by a control, and the header is crowded once a preset has more than five synths in it. The space was already there and doing nothing. .mini-synth is a flex column, so the header's 5px margin-bottom and the parameter area's 8px margin-top never collapsed into each other the way they would in a block container — they added up to 13px of dead band. The strip takes over exactly that 13px and both margins are zeroed, so card height is unchanged (verified: header-to-parameter distance is 13px before and after). Three flat 1px rules, no light-under-dark highlight pass — this reads as a groove without pulling a 3D bevel into a UI that has none. The 9px band centres on whole pixels; a 10px one lands at 1.5px and blurs, and crowds the first rule against the header border until the two read as one doubled line. Master keeps its original margins and gets no strip: it isn't going to be draggable, so the affordance would be a lie. No drag behaviour yet, and deliberately no cursor:grab — the strip shouldn't promise a gesture that isn't wired up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The local-run instructions were wrong in a way that cost a new contributor their first command: they said to run `npx tsx api-dev-server.ts`, which exists nowhere in this repo, and nothing in src/ makes a single /api call. There is no backend — the gossip endpoint is a public relay — so `npm install && npm run dev` is the whole setup, with .env needed only for the wallet and NFPreset features. Counts are out, in the README and the guide both. "Two visualizations" had been wrong since Kintsugi landed, and naming five would have been wrong again the next time one is added or dropped. The guide now points at the viz bar in its own screenshot for the current set — which the capture script keeps accurate for free — and names Score and Territory as examples rather than reciting a roster. Same treatment for the engine list, since a metal engine has been on the cards. Added a Versioning section covering the release steps and, more usefully, the two judgements behind them: that the compatibility surface is presets rather than code, and that a bump interrupts every returning user with a card, so docs changes ride along with the next real release instead of earning one. No version bump: this changes nothing anyone runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unstamped presets now resolve to a baseline version rather than null, so callers always have something to compare and no null case to handle. 0.9.0 is the right baseline as a matter of fact rather than convenience: a preset without a stamp necessarily predates stamping, stamping landed in 0.9.0, and everything in the wild was therefore authored against 0.9.0-era synths and sounds correct on them today. `inferred: true` keeps the distinction for diagnostics. Nothing on disk is rewritten. Migrating on load would mean a read silently mutating stored data, and it buys nothing the read-side default doesn't already give; presets pick up a real stamp the next time they are saved. stpf.json deleted: a block payload captured during development that happened to live in presets/. Nothing referenced it, and state proof overlays run from live transactions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A preset was just { activeSynths }. Nothing recorded which build produced it, so
the usual way of absorbing a breaking change — keep the old code path and route
old presets to it — was not available: there was no way to tell an old preset
from a new one.
Two fields, because they answer different questions:
formatVersion the shape of the object. Bump only when a reader has to behave
differently. Adding a settings key with a default is not a bump.
appVersion which build wrote it. This is the one that matters for sound.
Swapping a synth engine or the reverb changes how an existing
preset renders without touching the schema at all, so
formatVersion would not move.
An absent formatVersion means the preset predates stamping — which is exactly the
set written before any future behaviour change. The absence is the signal, so
nothing already saved needs migrating. getLoadedPresetProvenance() is the hook
for branching on it later.
Urgent only because NFPresets are immutable on-chain: anything minted before this
can never be stamped, so every day without it adds permanently ambiguous assets.
The five shipped presets are stamped 0.9.0, which is what they were voiced
against. stpf.json is left alone — despite living in presets/ it is a captured
block payload, not a preset.
app-version.js now warns when it falls back to 0.0.0-dev, since that string would
otherwise be stamped into presets saved against a stale dev server without
anything saying so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card branched on whether localStorage had ever seen a version: brand-new visitors got the description, returning ones got the changelog. That split meant anyone upgrading never saw what the app was, and anyone new never learned a release had just landed. Both now appear together on one card, for anyone who isn't already on the current version. Two buttons instead of one relabelled button, so either destination is one click away. Still capped at the newest three highlights — the full list is behind Full changelog. Verified the card fits without scrolling at 1280x900, 1366x768 and 390x780. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three presentation fixes. The screenshots were far too wide. .synth-grid is auto-fit, so in a 1280px window two cards stretch to half the screen each and the crop came out a wide sparse band that shrank to an illegible sliver in the modal. Captured at 700px a card gets its natural ~337px, which is both readable and narrow enough to sit beside its explanation rather than above it — so .guide-step is now a flex row. It wraps rather than using a media query, so on a phone the text drops below the image on its own. The full-width bars opt out via `viewport` and keep spanning the row. Lists rendered with no markers at all: @tailwind base resets list-style on every ul, and the legacy rules set padding without restoring it, so the release notes and every list in the guide ran together as unmarked paragraphs. Dashes rather than bullets, with a hanging indent so wrapped lines align under the text. Also removed the slashes from the prose. Berkeley Mono Trial draws "/" almost exactly like "\", which turned "Min / Max" into "Min \ Max" (the same quirk makes the existing rate hint read "1-100\s"). Not worth a font change; just not worth writing slashes either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was an inline strip above the actions bar. A centred overlay card, the same shape as the preset modals, is what actually reads as "read me first" — and it inherits the close X, the backdrop click and the dark surround for free. Reworded, because the old copy was wrong about the first step: a bare page load brings up only the master bus, so there is nothing to hear from pressing Start on its own. It now says to load a preset first. Escape dismisses it, and Space is ignored while it's up, for the same reason the info modal ignores it: the shortcut deliberately doesn't exempt focused buttons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One entry point beside the title opens both the guide and the changelog. The banner appears once per version and then never again — on a first visit it says what the app is and points at the guide, since a changelog means nothing without a baseline; on an upgrade it lists the top three highlights. Dismissing it or opening the modal records the version in localStorage under txSynthLastSeenVersion. Modal and banner ship together because neither works alone: the banner's only action is opening the modal, and opening the modal is what dismisses the banner. Details worth knowing: - The title and its (i) sit in a .title-group wrapper. .top-bar is a space-between flex expecting exactly two children; a third would float the button into the middle. - Panes are built on first open, so the 21 guide screenshots cost nothing to visitors who never open it. - Escape now checks the info modal before falling through to closeViz(), which it previously called unconditionally — otherwise reading the guide would blow away a fullscreen viz the user can't see. - Space is ignored while the modal is open. The play/stop shortcut deliberately does not exempt focused buttons, and the modal is full of them. - New class names throughout (.info-tab-btn, .info-pane — not .tab-btn or .tab-content) to stay clear of the synth card's own tab strip and DaisyUI. - .info-modal widens the shared 450px .modal-content cap to 760px; the guide screenshots are unreadable at the narrower width. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The app had no in-app documentation at all. A newcomer landed on a grid of unlabelled sliders with no way to learn that a synth is built as a ladder: transaction type, then subtype, then filter, then engine, on down to the LFO. guide-sections.js is that ladder written out, and is deliberately the single source of truth for both halves of the guide: the modal renders `title` + `body` and points an <img> at /guide/<id>.png, while capture-guide-shots.mjs reads `selector` + `state` to produce exactly that file. There's no second list to keep in sync, the capture run deletes orphaned PNGs, and adding a section is enough to get it captured. Screenshots are generated rather than hand-taken so they can be regenerated after any UI change (npm run guide:shots) instead of quietly going stale. Full-width bars declare a narrower `viewport` — a 1280px crop scaled into a 760px modal is illegible. Prose seeds from the existing title= tooltips where they were already good. It also documents what has never had a tooltip: the waveform and LFO destination selects, the tab strip, and every Master control. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing carried a version: package.json said 0.1.0, nothing read it, and there were no tags. Testers had no way to say which build they were on. package.json is now the single source of truth, injected as __APP_VERSION__ by a define in vite.config.ts and shown in the footer. Read with readFileSync rather than imported, because tsconfig.node.json (which covers vite.config.ts) doesn't enable resolveJsonModule and doesn't extend the root config. Starting at 0.9.0 — feature-complete enough to hand to testers, not 1.0 yet. To ship from here: bump package.json, add a changelog entry, commit, tag. Note that vite reads package.json once at config load, so a version bump needs a dev server restart to show up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>