UI/UX run: repair the first-run path, and 20-odd fixes across the app - #52
Open
zntznt wants to merge 11 commits into
Open
UI/UX run: repair the first-run path, and 20-odd fixes across the app#52zntznt wants to merge 11 commits into
zntznt wants to merge 11 commits into
Conversation
Seven fixes found by driving the app and measuring the real layout. Palette: the tool strip is 1086px tall, so at 1366x768 five node tools sit below the fold and at 1024x768 six do, behind an overlay scrollbar that reserves no width and stays hidden until you scroll. Added edge fades driven from the scroll position, the bottom one carrying a caret, and gave the scrollbar thumb a colour that reads on the panel. Timeline scrub row: a chip reading "Live" sat directly beside a disabled button reading "Live", which looked like one control rendered twice. The chip is now purely a position readout and the button only appears while scrubbing, relabelled "Back to live". Also drops the lowercase "live" variant. Properties panel: the amount field flipped its own label between the reset baseline and the live count, but the panel is never rebuilt mid-run, so it went on reading "Starting amount" while showing a mid-run value. The label is now patched per step alongside the value, from a shared pair of constants. Reset destroyed the selected node's sparkline and nothing rebuilt it, so the chart stayed blank through every later run until the node was reselected. Reset now re-renders the panel. The sparkline also drew an empty box and a stray midline before a run had two points, leaving a broken-looking chart above every node's fields. It stays collapsed until there is a line to draw. Property labels were title-cased in CSS over strings already written in sentence case, so the guide's "Amount (live)" rendered as "Amount (Live)" and the inline condition lead read "Stop When Value" mid-sentence. Dropped the transform and matched the one Title Case string in source. Timeline y-axis: grid lines sat at raw quarters of the data max, giving labels like 23.5 / 47 / 70.5. They now snap to a 1/2/5 ladder. Tick formatting also gains precision below 0.1, where every label used to collapse to "0.0". The value mapping is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
…l cue
Negative values were invisible. The linear y domain was hardcoded to 0..max,
so a register holding -20 was mapped below the plot floor and never painted:
the legend showed a chip for a series with no line anywhere on the chart. The
domain now spans [min, max] with min floored at 0, so all-positive charts keep
their familiar zero baseline and unchanged shape.
Hover tooltip printed raw doubles ("sunFactor: 1.12202099292274") and grew a
box taller than the canvas, so on the Food Web template only the first four of
thirty rows were readable and the last was sliced through. Values now share
the compare panel's two-decimal formatter, and rows are capped to what fits
with the remainder counted on the last line.
The canvas ignored devicePixelRatio, so every line and label was drawn at half
resolution on a 2x display and upscaled. The bitmap is now sized in device
pixels with the drawing transform left in CSS pixels, which keeps the
pixel-to-step math the hover and brush hit tests rely on exactly as it was.
29 legend chips wrapped onto three rows, and since neither the head nor the
scrubber shrink, every row came out of the plot: the chart collapsed to 68px.
The legend is capped at two rows and scrolls beyond that, and the canvas has a
min-height, which puts that case back to 92px.
Guide-line count now follows the drawer height rather than always being five,
so a squeezed chart thins its labels instead of stacking them into a smear.
The "not live" amber label had a CSS rule but nothing ever added the class to
#tl-scrub, so the cue never fired while scrubbing. It does now.
The Setup rail clips Loops and Watch as soon as the timeline opens, with no
hint that it scrolls, so it reuses the palette's edge fades.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
Below 768px the Setup rail is display:none and Reset is stripped from the topbar, but the overflow menu that is meant to hold "everything the collapsed topbar hides" offered neither. That put nine features out of reach on a phone outright: time mode, parameters, custom variables, resource types, the artificial player, scenario branches, design tests, feedback loops and the live-variable watch. The menu now lists all of them plus Reset, built from the rail's own buttons so the two lists cannot drift apart as features are added. The properties sheet is pinned to the bottom above the tool strip, and since placing a node selects it, the strip was covered the moment you used it. The only way back was knowing that a tap on bare canvas dismisses the sheet, which nothing says. Added a close button to the sheet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
Clicking a dialog's own chrome (its title, a section heading, the Monte Carlo results area) drops focus to <body>. From there the window-level shortcut handlers still fired at the canvas behind the dialog: Ctrl+A selected every node, Ctrl+V pasted, Ctrl+Z undid, Delete deleted the selection and S/D/R/T swapped tools, none of it visible under the overlay. Escape had the opposite problem: _modalize binds to the overlay element, so once focus left it the dialog could no longer be dismissed by keyboard at all. App now owns the list of overlays that count as modal. Both handlers consult it, the editor through a callback seam so it stays the host's decision, and Escape closes the topmost dialog from anywhere. Space keyup still clears unconditionally, or a swallowed keydown would strand the canvas in pan mode. The confirm dialog's focus trap listed its buttons in reverse DOM order, so Tab off "Discard & continue" was never intercepted and walked out of the alertdialog into the page behind it. It also stored its return target in the field _showModal uses, so a guard raised over the Library wiped the Library's own restore. Fixed the order and gave the guard its own field, and moved its key handler to the document so Escape works wherever focus sits. Closing a dropdown hid a popup that still held focus, resetting the browser to <body> so Tab restarted at the top of the page. Focus goes back to the trigger now. Dialogs opened from a menu stored the menu item as their return target, which is hidden by the time the dialog closes, so _hideModal falls back to the owning trigger when the stored element is gone. Also: aria-labels on the parameter, resource-type and canvas-background inputs, which had no visible label and announced as anonymous blank fields, and aria-expanded on the touch overflow menu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
…ssage The first-run canvas hint was one 612px-wide SVG line, and SVG text does not wrap, so it was chopped at both ends on any canvas narrower than that: at 1024x768 it overhung both edges, and by 820px it started off-screen to the left. Split into three short lines, with the vertical centring derived from the line count instead of hardcoded for three. At 320px the touch topbar wanted 359px and, since it does not wrap and the page does not scroll, pushed the ☰ button off the right edge, leaving 15px of a 44px target. That is the only way to Library, File, Help and the Setup panels on a phone, so it made a small screen close to unusable. Recovered the width from the Run button and the step chip rather than dropping a control. The empty-chart message told a one-series diagram to click a "Show all" chip that is only rendered from two series up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
Three faults in one field, all reproduced against the Predator & Prey template.
Editing a pool's amount wiped its resource type. setCount defaults to the
untyped grey, so retyping Rabbits from 80 to 90 rewrote colorMap from
{#7cb342: 80} to {#9e9e9e: 90}: the node greyed out on the canvas and the
panel's own "Holdings by type" row switched from "Rabbits" to a raw hex. The
field now passes the node's current colour, as the Source stock field already
did, and the +/- steppers do the same.
The hero readout and the holdings rows did not follow the edit. Typing 90 left
"CURRENT 80" sitting directly above "Starting amount 90" for the same node,
with no way to tell which was real. Both readouts refresh with the field now.
Typing into the field mid-run rewrote the reset baseline. The label reads
"Amount (live)" there and the steppers beside it are genuinely transient, but
setCount also moves the baseline, so Reset came back to a number typed mid-run
instead of the run's start. Mid-run edits now restore the baseline, matching
what the label promises.
Separately, a Trader has no amount row, so the per-step refresh was writing its
resource count into the first number input it found, which is the End/goal
value: a goal of 7 dropped to 0 on the first step. The refresh now shares the
builder's exclusion list.
Also: accessible names for the condition-row operator and bounds and for colour
pickers, all of which announced as anonymous controls, and a formula hint that
began its second sentence lowercase.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
The resource-types panel mixed both inside a single sentence ("maps a name to
a color ... pick a type from the colour fields"), and two of the aria-labels
added in this pass had picked up the other one. The visible field labels all
read "color", so everything follows that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
Two faults sat on the same path, the one a new user takes first. The tour's coach card is placed beside its target, so when the target is a palette tool it lands on the canvas just right of the strip, which is where a first diagram naturally goes. The card took the pointer, so it swallowed the drag its own step was asking for. Those steps advance by watching for the edit and show no Next button, so the tour simply stopped at "drag from the Source to the Pool" with nothing but "Skip tour" left. The card is click-through now and only its buttons take the pointer. Behind that, SimEngine seeded its flow accumulator in reset() but not in the constructor. Every path that loads something (templates, autosave, share links, the Reset button) resets first, so this only showed on the one path that does not: boot on an empty canvas, draw a Source into a Pool, press Run. The first transfer threw, and since that happens before _record and onStep, the step counter stayed at 0 and the timeline, scrubber and sparklines stayed empty while the numbers on the canvas kept climbing. Six uncaught errors per run, and the first simulation anyone builds by hand looked half broken. The test helper resets before stepping, which is why no unit test covered it; the new one steps a fresh engine deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
The Resource types panel told users to "pick a type from the colour fields on
sources, converters, and filters". There is no Filter node in the palette, so
that sent a first-time user hunting for something that does not exist. It now
names the two node fields and the connection's color filter, which are the
real surfaces.
The welcome modal's three buttons were a non-wrapping row needing about 468px,
so on a phone the row overflowed leftwards and pushed the primary "Take a quick
tour" off screen entirely, with "Explore the demo" following it at 320px. That
is the first screen a new user sees. The row wraps now.
Two guide articles were both titled "Feedback loops", so searching the guide
returned two identical-looking rows. The analysis one is now "Loop detection",
matching what it documents. Its id is unchanged, so the rail's deep link still
resolves.
The rest is one voice: American spellings throughout ("summarize", "colored"),
sentence case on the rail headings and their add buttons so they agree with the
guide articles they link to, palette tooltips that name tools the way the
shortcuts dialog does, one quote style across the Library and component save
toasts, and a demo note whose second sentence began lowercase mid-canvas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
The Delete tool draws the same crosshair as every other tool, so it is easy to leave armed, and Escape, the obvious way out, cleared the selection and any half-drawn connection but left the tool exactly where it was. The next click on the canvas then removed a node. Escape now returns to Select from Delete, with a toast saying so. Only from Delete: cancelling a half-drawn connection should not also put the Resource tool away. It lives in the app's key handler rather than the editor's because clicking a palette button leaves focus on that button, and the editor ignores keys aimed at one. The tour's "change the Rate" step compared each connection against a snapshot taken when the step was entered and skipped any connection missing from it. Delete the connection there and draw a new one and no edit could ever satisfy the step, which has no Next button to leave by. New connections are adopted into the baseline now, so a later edit to them counts. The spotlight also went stale whenever its target vanished. _positionTour has a fallback for a missing target but only ran on render and resize, so deselecting during the Rate step left the cut-out sitting over whatever took those pixels. The properties panel repositions it after a rebuild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
The brush-to-compare readout had the same unbounded height as the hover tooltip, and only the tooltip got capped. On the Food Web template it wanted 416px of panel in a 92px drawer: 5 of 28 A→B rows were visible and the last was sliced through the glyphs, so the readout the whole compare gesture exists for was mostly unreadable. It now keeps the rows that fit and counts the rest, the same way the tooltip does. Small diagrams are unaffected: a three-series comparison still lists all three. Also the Barter Economy blurb in the Library still read "watch the colours mix", and the CLI's batch summary joined its two clauses with an em dash. Found by the audit's adversarial pass, then reproduced here before fixing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SH7ouoNymubArm7VEgALHg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Drove the real app in Chromium across viewports, measured layouts, and reproduced every defect before fixing it. 11 commits, 16 files. 230/230 unit tests, smoke passing, zero console errors.
The headline: the first run was broken
Two faults sat on the exact path a new user takes, and either alone made the app look broken.
The tour dead-ended on step 3. The coach card is placed beside its target, so when the target is a palette tool it lands on the canvas just right of the strip, which is where a first diagram naturally goes. The card took the pointer, so it swallowed the drag its own step was asking for. That step advances by watching for the edit and shows no Next button, so the tour simply stopped at "drag from the Source to the Pool" with nothing but "Skip tour" left. The card is click-through now; only its buttons take the pointer.
Behind it, the first simulation threw on every tick.
SimEngineseeded its flow accumulator inreset()but not in the constructor. Every path that loads something (templates, autosave, share links, the Reset button) resets first, so this only showed on the one path that does not: boot on an empty canvas, draw a Source into a Pool, press Run. The first transfer threw, and since that happens before_recordandonStep, the step counter stayed at 0 and the timeline, scrubber and sparklines stayed empty while the numbers on the canvas kept climbing. Six uncaught errors per run.The unit-test helper resets before stepping, which is why nothing covered it. The new regression test steps a fresh engine deliberately.
Data that was invisible or wrong
0..max, so a register holding -20 mapped below the plot floor: the legend showed a chip for a series with no line anywhere. The domain now spans[min, max]with min floored at 0, so all-positive charts keep their zero baseline and shape.setCountdefaults to untyped grey, so retyping Rabbits from 80 to 90 turned the node grey on canvas and switched "Holdings by type" from "Rabbits" to a raw hex.Things you could not reach
display:noneand Reset is stripped from the topbar, but the ☰ menu offered neither. It now lists all of them, built from the rail's own buttons so the lists cannot drift apart.Keyboard leaked through open dialogs
Clicking a dialog's own chrome drops focus to
<body>, and from there the window-level handlers still fired at the canvas behind it: Ctrl+A selected every node, Ctrl+V pasted, Delete deleted, S/D/R/T swapped tools, all invisibly. Escape had the opposite problem:_modalizebinds to the overlay, so once focus left it the dialog could not be dismissed by keyboard at all. Also fixed a focus trap built in reverse DOM order that let Tab walk out of the confirm dialog, a guard that clobbered the shared return-focus field, and dropdowns that reset focus to<body>on close.The rest
Palette and Setup rail scroll affordances (5 of 13 tools sit below the fold at 1366×768 behind an invisible overlay scrollbar); round chart axis ticks instead of raw quarters of the data max; hover tooltips that printed 16-digit floats and ran off the canvas; the compare panel that wanted 416px in a 92px drawer; a 29-series legend that squeezed the plot to 68px; a retina-blurry canvas; Escape now disarms the Delete tool; and a copy pass for one spelling, sentence case, and a panel that told users to use a "Filter" node that does not exist.
Testing
npm test230/230 (one new regression test),npm run smokepassing, no console or page errors. Every fix was reproduced in the browser first and re-verified after; a parallel audit independently found the engine bug with the same root cause.Deliberately left
role="application"and tabbable with no keyboard operations. Honouring that means building node-cursor navigation, a feature rather than a fix.Generated by Claude Code