Repository navigation
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…6-10-01 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Bundle delta: 0 KB measured (dist totals 231680 KB before and after; no src file imports the new packages yet, so webpack tree-shakes them out entirely). Installed-package disk footprint (not the shipped bundle size) is ~9.9 MB for react-aria-components and ~1.5 MB for @internationalized/date; actual gzip/minified contribution will only be measurable once the DateField component is built and imported. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lectable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
React Aria disables adjacent-month days regardless of minValue or maxValue, so the previous assertion on a trailing October day passed whether or not the bound was wired. Both bounds are now asserted inside the focused month, and each fails if its prop is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CustomSelect resolves its header as `title || selectedItem.text`, so the static title the dropdowns were given would have left the closed control reading "Choose a station" however many times a student changed it, with only the tick in the open list to say otherwise. The placeholder is now supplied only while nothing is selected. The list assertions move inside the list, since a selected label also appears in the header. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CustomSelect falls back to treating its first item as selected when none is marked, which ticked and emboldened a station the student had not picked - visible in exactly the state the design calls Default, since no unit configures defaultStation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both values the designer gave already exist as tokens: #d8eff5 is $workspace-teal-light-6 and #b7e2ec is $workspace-teal-light-4, so no new colour was added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The white resting fill added for the design matched the house open-state rule on specificity, so load order decided which won and the header reverted to white as soon as the list opened. The open state is now restated in the tile's own override, where it outranks the resting fill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pad month and day to two digits so the field stops changing width as the date changes. Move the calendar glyph to the front of the field and render it in dark teal. The icon copied from the Data Card carried a 24x24 spacer rect and a card-background rect; with a fill applied either would have painted a solid block over the glyph, so both are removed and only the paths take the fill. Widen the calendar columns to 41px and make the selected day an outlined cell - white, 2px dark teal, 8px radius - rather than a filled one. Every cell reserves that border width so selecting one does not shift the grid. Floor the Data Setup panel at 426px. The sections share space with flex-basis 0, so each is floored by its own min-content and the panel narrowed whenever a short station name was chosen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A selected day is an outlined cell around a filled swatch, so the number carries its own element. Today takes the same swatch with a charcoal border and light charcoal fill. Both of today's colours were already tokens: #545454 is $charcoal-dark-1 and the charcoal-light-4 variable is $charcoal-light-4. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
min-width alone left the panel floored by its own min-content while the sections shared space from a zero flex-basis. Giving it a real basis and forbidding shrink states the intent directly. Scoped to the horizontal layout, since flex-basis follows the main axis and would otherwise set a height in the stacked one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 12px moved outside the border: as padding it let the section's teal show through as a band between the outline and the graph. The status line collapses when it has nothing to report, rather than standing as a white bar under the graph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A flex item is floored by its own min-content, so a long station name widened one field and both controls resized. The fields now split the row evenly and the label ellipsises. The running/loading messages never carried text-align of their own - only the estimated-time line below them did - so they sat left once the shared padding was applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tile's useEffect called onRequestRowHeight whenever its layout flips between stacked and side-by-side, but that callback mutates the shared row model regardless of whether this instance is editable. The same document can render editable and read-only at once at different widths (four-up, a published document view), so a read-only instance could overwrite the height the editable one had just set. Only request the height when not read-only. Test asserts a read-only render never calls onRequestRowHeight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks — all six addressed. Three were bugs this branch introduced, and one was a test I knew was hollow when it was written. Dropdown labels (1 & 2). Right, and worse than it reads: Clear bypassing the bounds (3). Confirmed, including your exact scenario. Stale error masking the status line (4). This one is ours, and it only became reachable in this branch: the panel used to have separate rows per message, and consolidating them to a single line with errors at the top of the priority chain is what let a dead Read-only height requests (5). Correct — The single-day test (6). You're right that it proved nothing, and that was known when it was written rather than discovered now — the comparison lives only inside Mutation-checked independently: reverting Full suite 5351 passing, tsc clean, lint 0 errors. |
kswenson
left a comment
There was a problem hiding this comment.
PR Review Summary
Changes: 25 files, +3442 / -131 lines (of which ~1,674 are the committed plan/spec docs and ~257 package-lock)
What it does
Restyles the WaveRunner tile's Data Setup panel to the design. Because neither a native <select>'s option list nor a datetime-local calendar can be styled, the Station/Model dropdowns move to CLUE's CustomSelect and the date fields move to a new DateField built on React Aria's DatePicker (new deps react-aria-components and @internationalized/date, lazily loaded with the WaveRunner chunk). It also bounds the dates (end ≥ start, nothing past today UTC), accepts a single-day run, consolidates the status area into one line, has runModel/loadEnvelopeData clear each other's errors, and has the tile request a fixed row height.
The approach
DateField converts at its boundary between the model's "YYYY-MM-DD" strings and CalendarDate, so the model's storage is unchanged. It keeps three pieces of local state: pending (a calendar pick staged behind Clear/Cancel/OK), draft (what the segments show while typing), and focused (the calendar's month). A change with the popover closed is treated as typing and committed straight to the model. CustomSelect gains an optional ariaLabel prop. The tile requests 164 + 78 px side by side and 2×164 + 78 stacked, skipping the request when read-only.
Assessment
The calendar path is well built and thoroughly tested. The Clear/Cancel/OK buffering, the bounds in the calendar, and the lazy-load and bundle reasoning all hold up. The 105 plugin tests pass, tsc is clean, the SCSS compiles, and lint is clean. No other CustomSelect consumer is affected.
The typed path is broken, though. I confirmed this with a throwaway jest probe using a controlled wrapper, so onChange fed back into value as the model does. The existing tests pass a fixed value and never re-render from onChange, so they cannot see it. A student who types a year loses the month and day, and the model briefly holds a malformed date. A typed date also bypasses the min/max bounds the PR says make out-of-order dates unselectable. These need fixing before merge.
There is also a real accessibility regression on Station/Model. The aria-label hides the chosen value from screen readers, which a native <select> did not do.
Issues
Requested change for issues 1–3: buffer typed edits in draft, commit them on blur or Enter after checking them against min/max, and revert to the model value if the typed date is invalid or incomplete. Please also add a test that round-trips onChange back into value, as the model does.
-
[major, requested change] Typing a year commits the first digit as a malformed date and blanks the month and day.
date-field.tsx:107-114,date-utils.ts:18-22.- Confirmed by a jest probe and in the branch build: start with value
2026-09-15, click the year segment and type2024. The first keystroke sendsonChange("2-09-15"), the model stores it, and the segments rendermm|dd|2024. The student then has to retype the month and day. - Cause: React Aria fires
onChangeon each keystroke once the date is complete.handlePickerChangecommits it immediately, andtoDateStringdoesn't pad the year. fromDateString("2-09-15")fails, so the[value]effect nullsdraft, which blanks the other segments.- Until the student retypes them, the model holds
"2-09-15". The intermediate commit also callssetStartDate/setEndDate, which runsloadData()andclearEventsDataSet(). - Suggested fix: commit typed dates on blur (or Enter) rather than per keystroke, validate against min/max, and pad the year.
- Confirmed by a jest probe and in the branch build: start with value
-
[major, requested change] Typed dates bypass
minValue/maxValue, silently.date-field.tsx:107-114.- Confirmed by probe and in the branch build. With min
2026-09-10and max2026-10-08, typing12into the month commits2026-01-15(below min) and then2026-12-15(past today). - React Aria's bounds are validation only and do not block
onChange. The model setters don't validate either. - So "an end date before the start date is unselectable" and "neither field reaches past today" hold only for the calendar.
- The invalid state shows no feedback: no field styling and no status message. Date changes go through
loadData(), which sets no error. "Invalid date range…" appears only once the student presses Load Data or Run. - The invalid date also inverts the other field's bounds, so start's max ends up below its value and end's min above its own.
- Confirmed by probe and in the branch build. With min
-
[major, requested change] Clearing a segment leaves the field and model disagreeing.
date-field.tsx:107-114,:87-91.- Confirmed by probe: backspacing the day twice and tabbing away leaves the field showing
09|dd|2026while the model holds2026-09-01. The first backspace committed day 1. - Confirmed in the branch build: opening the calendar then snaps the field back to the model's date, because
handleOpenChangereseeds fromvalue. onChange(null)setsdraftto null without committing, and nothing restoresdraftfromvalueon blur. Run then uses a date the student can't see.
- Confirmed by probe: backspacing the day twice and tabbing away leaves the field showing
-
[major, requested change] Station/Model announce only "Station, button" / "Model, button" and never the chosen value.
custom-select.tsx:109,data-setup.tsx:121,132.- On a
role="button",aria-labelreplaces the text content as the accessible name, andtriggerPropsdoesn't override it. The test atdata-setup.test.tsx:~136asserts exactly this name. - This regresses from the native
<select>, which exposed its value. It fails WCAG 4.1.2 and 2.5.3 (label in name). - The new prop's doc comment (
custom-select.tsx:33-35) has it backwards:ariaLabelis what stops the value from being announced. - This is the other half of Copilot's comments 1 & 2. The
ariaLabelfix made the field's purpose announced, but in doing so it replaced the value. The new tests assert that the name is exactly "Station", so they pin the regression rather than catch it. - Suggested fix:
aria-labelledbypointing at the visible<label>(give it an id) plus the header text. That also fixes the orphaned labels (issue 9).
- On a
-
[major, not blocking: follow-up to fix
useDropdownin accessibility-tools] Keyboard: Enter, Enter on the month dropdown jumps the calendar back a year.date-field.tsx:176-181, shareduseDropdown.- Confirmed by a verifier's probe. Opening the list focuses item 0, which is 12 months back, because
useDropdownsetsaria-selectedfromactiveIndexrather than the chosen item, and its open effect falls back tofocusItem(0). - So Enter then Enter selects September 2025.
- Station/Model share the same hook behavior: focus always opens on the first item, and arrowing announces each option as "selected".
- The root cause is in the shared hook, but this PR introduces the month dropdown where it bites hardest.
- Confirmed by a verifier's probe. Opening the list focuses item 0, which is 12 months back, because
-
[minor, not blocking: same follow-up as 5] Escape in the open month list closes the whole date picker and drops the pending pick.
- Confirmed by probe.
useDropdown's list keydown callspreventDefaultbut notstopPropagation, so Escape reaches React Aria's Popover dismiss handler.
- Confirmed by probe.
-
[minor, requested change] A wide WaveRunner sharing a row can leave the row stuck at the stacked height.
wave-runner-tile.tsx:15-16,24-27.- On first render
useResizeDetector's width isundefined, soverticalis true and the tile requests 406. Once the width is measured it requests 242. tile-row.tsx:157refuses to shrink a multi-tile row, so it stays at 406, with about 164px of extra space and no clipping.- Separately, the first editable open of an existing document rewrites its stored row height (320 → 242/406) without undo, which dirties the document once.
- Suggested fix: skip the request until
containerWidthis known.
- On first render
-
[minor, requested change]
runModel's early returns don't clear a staleloadDataError.wave-runner-content.ts:205-220.- The cross-clear runs only after the "No model selected", metadata and "No station selected" guards.
- A failed load followed by Run with no station keeps showing the old load error, because the status line shows
loadDataError || runError. The comment at:225-227promises more than the code does. This is what remains of Copilot's comment 4: the fix covers a run that gets started, but not one that bails out early.
-
[minor, requested change (covered by 4)] Visible Station/Model
<label>s are orphaned.data-setup.tsx:114,127. They have nohtmlFor/idlink, whereas master's<select>hadhtmlFor="wave-runner-station". Clicking the label does nothing, and the label isn't what names the control. The fix for issue 4 covers this. -
[minor, author's discretion] Contrast.
- Placeholder date segments use
$charcoal-light-1on white: 3.03:1, against the 4.5:1 needed for text. - The date-field border is the same color against the
teal-light-7panel: 2.68:1, against the 3:1 needed for a non-text boundary (WCAG 1.4.11). - Something like
#767676passes both.
- Placeholder date segments use
-
[minor, author's discretion] Gaps in the test suite (the "mutation-checked" claim doesn't extend to these).
- The typed path never round-trips
onChange→value(that is how issues 1-3 escaped). - The status-line priority chain (error > loading > running > complete > configured > setup) is untested, apart from the setup message and the
configuredclass. date-field.test.tsx:91-95asserts the absence ofdate-field-time-column, a test id that exists nowhere, so the test can never fail.date-field.test.tsx:236-246claims "pending selection intact" but never picks a day.
- The typed path never round-trips
-
[minor, not blocking: follow-up story] Single-day ranges give the seismogram viewport and "Timeline It!" a zero-length range.
wave-runner-content.ts:82-83.endTimeISOis the start of the end day.- This was already reachable through
loadDataon master. The PR makes single-day ranges an intended feature, so it is worth fixing here or in a follow-up: end-of-day, or+ SECONDS_PER_DAY.
-
[nit, author's discretion] Committed plan and spec docs.
docs/superpowers/plans/…-clue-669-…mdis 1,466 lines anddocs/superpowers/specs/…is 208, and both carry Jira keys.- There is precedent on master (CLUE-260, among others), so this is a preference rather than a rule.
- The plan in particular is a task checklist that is stale the moment it ships. I'd drop the plan and keep the spec only if it says something the PR description doesn't.
-
[nit, author's discretion] Smaller items.
monthOptionsclips only atmaxValue, so the end field offers months before the start date, which the calendar then refuses.clear()'s clamp lands below min when min > max (only reachable after issue 2).- The
new CalendarDate(2026, 9, 1)fallback duplicateskDefaultStartDate. z-index: 11on.date-field-popoveris dead, because React Aria setsz-index: 100000inline.- The
78chrome constant isn't derived from anything. Its comment says the title adds height, but.title-areais absolutely positioned. - The default status line reads "Estimated time to complete run:" with nothing after it.
- The status line isn't a live region (pre-existing), and errors aren't visually distinct from status text.
- Disabled
CustomSelecthas noaria-disabled(pre-existing in the shared component, newly exposed on Station/Model).
Comments and docs (author's discretion)
All items were found by the comment-lens agent; I personally verified the two marked ✔︎. I left out pure wording nits.
Spelling and usage. The repo uses US spellings: gray 29 : grey 3, centered 25 : centred 0.
- "greyed" at
_dropdown-appearance.scss:45, "Centred" atstatus-and-output.scss:14, and "grey" atstatus-and-output.tsx:15and in the test titles atstatus-and-output.test.tsx:33,37. - "ellipsises" at
data-setup.scss:19. - "tick" for the checkmark at
data-setup.scss:39anddata-setup.test.tsx:115. - "house dropdown/component" is used as a second name for
CustomSelectat_dropdown-appearance.scss:15,28,30,44.
Inaccurate or stale
- ✔︎
wave-runner-content.ts:225and:241say "loadData" where they meanloadEnvelopeData.loadData()never touchesloadDataError. custom-select.tsx:33-35: inCustomSelect,titlealways overrides the selected text. This comment describes WaveRunner's convention, not the component's, and it inverts the a11y effect (see issue 4).data-setup.test.tsx:154-156says the date picker's month select "is a native select". It is aCustomSelect.date-field.test.tsx:209-211: the change-event comment dates from the native month select, and it sits on the wrong test._tile-metrics.scss:14says "the section carries no vertical padding of its own". It has an 8px top padding (wave-runner-tile.scss:58).wave-runner-types.ts:3-5: the 78px chrome doesn't add up as described, because the title is absolutely positioned.
Backward-looking. The ones that narrate the code's history:
data-setup.test.tsx:64: "no longer renders native datetime inputs"wave-runner-content.test.ts:355,375: "vs. the old<="date-field.scss:144: "no longer unstylable"date-field.tsx:84-86: "Handing the picker only the committed date meant…"status-and-output.tsx:19-20: "Reserving a row … left an empty one"wave-runner-tile.scss:27-28,47-50date-utils.ts:5-6date-field.test.tsx:138: "Reproduces the reported bug exactly"
Repeated explanations. Collapse each to its canonical site and point to it from the others:
- Title masks the selection: canonical
data-setup.tsx:82. Repeated indate-field.test.tsx:201,data-setup.test.tsx:129-132andcustom-select.tsx:33. - Cross-clearing of errors: canonical
wave-runner-content.ts:225. Repeated inwave-runner-content.test.ts:562. - One status line: canonical
status-and-output.tsx:19. Repeated instatus-and-output.scss:37and_tile-metrics.scss:9. - Known-not-measured height: canonical
wave-runner-types.ts. Repeated in_tile-metrics.scss:1-3andwave-runner-tile.test.tsx:125. - Clear clamps to bounds: canonical
date-field.tsx:123. Repeated indate-field.test.tsx:138.
Adjacent and pre-existing. These test titles sit next to the changed tests and are cheap to fix:
- ✔︎
wave-runner-tile.test.tsx"…less than 450" / "…450 or greater". The threshold is 700. wave-runner-content.test.ts"…covering the mock data range".
…ds dates Typing into a date segment committed to the model on every keystroke once react-aria considered the in-progress value "complete": typing "2024" into a year committed year "2" on the first keystroke, and toDateString did not pad the year, so that commit produced the unparsable string "2-09-15". fromDateString then failed to parse it back, nulling draft and blanking month/day to their placeholders. Because minValue/maxValue are validation-only for react-aria (they disable calendar days but never block onChange), the same per-keystroke commit let a typed date silently bypass the picker's own bounds, with no field styling or status message - contradicting the claim that out-of-order/future dates are unselectable. Clearing a segment to empty also left the field and the model disagreeing: react-aria only notifies onChange for an edit that is complete and valid, so a cleared segment just shows its own placeholder locally while the component's `draft` keeps whatever complete value it last heard about - nothing synced them back up on blur. Fix: typed edits now stay in `draft` only. commitTyped runs on blur (deferred to a microtask so react-aria's internal segment remounts aren't mistaken for the student leaving the field) or Enter, validates against minValue/maxValue and checks for a placeholder segment (via groupRef) to catch the "complete-looking but actually incomplete" case, and either commits once or reverts draft back to the model's value. toDateString now pads the year to 4 digits. Added a controlled-wrapper test helper (onChange feeds back into value, exactly as the model does) since the existing fixed-value tests pass a static `value` that never re-renders from a commit, which is exactly what let these bugs escape. Mutation-check: reintroducing the per-keystroke commit makes all three new controlled-wrapper tests fail, confirming they catch the regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CustomSelect's trigger is a role="button" div. On that role, aria-label REPLACES the element's text content as the accessible name rather than supplementing it - so passing aria-label="Station" made the control announce "Station, button" and never the chosen value once one was picked, a regression from the native <select> it replaced (which always exposed its value). The ariaLabel prop's own doc comment had this backwards, describing it as what keeps the purpose announced "regardless of selection state" when it was actually what suppressed the value. The data-setup.test.tsx assertion `name: "Station"` asserted exactly the regressed behavior, so it pinned the bug instead of catching it. Separately, the visible Station/Model <label>s had no id and no htmlFor, so they were not programmatically associated with their control at all - orphaned labels that an assistive-tech user could not use to find the control's purpose. Fix: replaced the `ariaLabel` prop with `ariaLabelledBy` (an id, not text). CustomSelect now gives its header its own id and sets aria-labelledby="<the given id> <its own id>", so the accessible name concatenates the field's purpose (the caller's label) with its current value (the header's own text). data-setup.tsx gives the visible labels ids and passes them through. (A plain htmlFor could not do this job here - CustomSelect's header is a div, not a native labelable control, so a browser would not forward a label click to it the way it does for <select>.) Updated the two tests to assert the accessible name CONTAINS both the field's purpose and the chosen value, so either one disappearing fails them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
useResizeDetector's width is undefined on first render, so `vertical` defaulted true and the tile asked for the stacked row height before it had ever measured its own container. Once the real width came in it asked again for the smaller single-panel height, but tile-row.tsx refuses to shrink a multi-tile row back down - so a WaveRunner sharing a row with another tile got stuck at the stacked height, with ~164px of dead space below it, even though it was wide enough to lay out side by side. Fix: skip the height request entirely until containerWidth has a value. Added a test asserting onRequestRowHeight is not called while width is undefined. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cross-clear of the other operation's error ran after the "no model selected", metadata-load, and "no station selected" guards in runModel. Any of those early returns left loadDataError untouched, so a failed Load Data followed by a Run with no station selected kept showing the old load error - status-and-output.tsx shows loadDataError || runError, so the stale message masked the fact that this run attempt had its own (different) problem. Also corrected two comments that said "loadData" where they meant the loadEnvelopeData action - loadData() is the separate, always-successful helper that just syncs the shared seismogram and never touches loadDataError, so attributing the cross-clear or the inclusive-end-date convention to it was misleading. Fix: moved `self.loadDataError = null` to the top of runModel, before any of the early-return guards. Added a test reproducing the exact scenario (loadEnvelopeData fails, then runModel is called with no model selected) and verified it fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Placeholder date segments and the date-field border both used $charcoal-light-1, which measured 3.03:1 on white (text needs 4.5:1) and 2.68:1 against the data-setup panel's teal-light-7 background (a non-text boundary needs 3:1, WCAG 1.4.11). $charcoal, an existing token already in vars.scss, measures 4.95:1 on white and 4.37:1 on teal-light-7, so it was reused rather than adding a new token. Also removed the dead z-index: 11 on .date-field-popover: React Aria sets z-index: 100000 inline on the same element, so the rule never applied. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three assertions on this branch proved nothing: - date-field.test.tsx asserted the absence of a "date-field-time-column" test id that is never produced anywhere in the codebase, so it could never fail. Replaced it with a real assertion: the popover contains no spinbutton (time-editing) controls at all. - The "...pending selection intact" test changed the calendar's month without ever picking a day first, so there was no pending selection for it to lose. It now picks a day, changes the month, and confirms OK still commits the originally picked day. - The status line's priority chain (error > loading > running > complete > configured > setup) had no tests besides the lowest-priority setup message. Added coverage for every branch, including an error correctly outranking in-progress and completed-run state - the chain this branch's cross-clearing fix (wave-runner-content.ts) depends on. Also added tests for the two small logic fixes going into the next commit: offering no month before the first selectable date, and clampDate's own unit tests (including the contradictory-bounds case, which can only be exercised at that level - the rendered Calendar hangs if minValue is ever actually past maxValue). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- monthOptions() only clipped the month dropdown at maxValue, so the end field's month list offered months before the start date - which the calendar then refused to let you pick a day in. Now clipped at both ends. - clear()'s clamp applied min then max, so if the two bounds were themselves out of order (minValue tracks the other field and can transiently land past maxValue - see data-setup.tsx) the result could land below min. Extracted the clamp into date-utils.ts's new clampDate() and reordered it (max first, then min) so a contradictory pair always resolves to at least min. - The hardcoded `new CalendarDate(2026, 9, 1)` fallback for the focused month duplicated kDefaultStartDate; now derived from that constant. - The default "configured but nothing run yet" status line read "Estimated time to complete run:" with nothing ever filled in after the colon. Reads as a complete sentence now: "Ready to run the model." Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
US spelling: greyed/grey -> grayed/gray (_dropdown-appearance.scss, status-and-output.tsx, status-and-output.test.tsx titles); "ellipsises" reworded to "truncates with an ellipsis" (data-setup.scss); "tick" -> "checkmark" for the selection check icon (data-setup.scss, data-setup.test.tsx); "house dropdown/component" renamed to CustomSelect throughout (_dropdown-appearance.scss, data-setup.test.tsx). Inaccurate or stale: - custom-select.tsx's ariaLabelledBy doc comment used "Station" as its example, baking WaveRunner's own field name into a shared component's doc; reworded to describe the behavior generically. - data-setup.test.tsx called the month-and-year chooser "a native select" - it is a CustomSelect, same as everything else in this tile. - date-field.test.tsx had a comment about a real click sequence exposing CustomSelect's outside-click handling sitting on the wrong test (one that never clicks an option); moved it to the test that actually does. - _tile-metrics.scss claimed the section "carries no vertical padding of its own," but its 8px top value is exactly the section's padding-top (wave-runner-tile.scss); reworded to say so. - wave-runner-types.ts's chromeHeight comment summed "title bar + teal background + padding," but the tile's title bar is absolutely positioned (tile-title-area.scss) and contributes nothing to that sum; reworded to say plainly that the number is empirical. Backward-looking comments (data-setup.test.tsx, wave-runner-content.test.ts, date-field.tsx, wave-runner-tile.scss, date-utils.ts) rewritten to state the current constraint instead of narrating the bug or "previous version" they used to guard against. Repeated explanations collapsed to point at a canonical site instead of restating it: title-masking (data-setup.tsx), cross-operation error clearing (wave-runner-content.ts), the single status line (status-and-output.tsx), the known-not-measured tile height (wave-runner-types.ts), and Clear's clamp (date-utils.ts's clampDate). Stale test titles: the 450px stacking threshold is actually 700 (wave-runner-tile.test.tsx); "covering the mock data range" renamed to describe the actual assertion (wave-runner-content.test.ts). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thank you — this was the review this branch needed. All four requested changes are in, plus the discretionary items. Eight commits, You were right that the typed path was broken, and right about why it escaped: the tests pass a fixed 1–3, typed dates. The root cause was in Two things we found while implementing that your probes wouldn't have surfaced, and that shaped the fix:
4 and 9, the a11y regression. Correct, and worse than a miss: the test I added asserted the name was exactly "Station", so it pinned the regression. 7. The request now waits until 10, contrast. 11, the hollow tests. All three repaired. 12, 13, 14 and the comment pass. The small fixes are in — One finding of our own while testing Left for a decision rather than actioned:
119 plugin tests, 5369 across the repo, |
useDropdown (from @concord-consortium/accessibility-tools) had two accessibility bugs that CustomSelect's 7 call sites all inherited: 1. getItemProps stamped aria-selected onto whichever item has the keyboard cursor (activeIndex), not onto the item actually chosen. Every option a screen reader user arrowed past was announced as "selected", and - since the hook's open effect looks for aria-selected="true" in the DOM to decide where to focus, falling back to item 0 - opening a list always focused the first item instead of the chosen one. In the WaveRunner date picker this meant opening the month list focused the option 12 months back, so Enter then Enter silently jumped the calendar back a year. 2. handleListKeyDown's Escape case called close() but never stopPropagation(), so Escape kept bubbling past the list. In the date picker the month/station/model lists sit inside a React Aria Popover, whose own Escape dismiss handler then closed the entire picker, discarding the pending pick. Both are fixed inside custom-select.tsx (not a dependency patch) so all 7 call sites benefit: aria-selected is now set after the hook's itemProps spread, reflecting the real selection (which also gives the hook's own open effect a real aria-selected="true" to find); the list's onKeyDown now runs the hook's handler first and then stops Escape specifically from propagating further. Added src/clue/components/custom-select.test.tsx (none existed) covering both fixes plus the Escape propagation fix, each verified by reverting its fix and confirming the corresponding test fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
endDateISO returned the START of the end day (T00:00:00Z), so a single-day range - valid since this branch now lets a student pick the same day for both start and end - made start and end the same instant. status-and-output.tsx feeds startDateISO/endDateISO straight into the seismogram's startTime/endTime, so the viewport collapsed to zero width and nothing rendered. The data loaders (loadData's run and loadEnvelopeData flows) already treat the end date as inclusive, adding SECONDS_PER_DAY so the end day is fully covered; only these display getters didn't. endDateISO now adds SECONDS_PER_DAY too, so it returns the END of the end day, consistent with what the loaders already assume. Added tests asserting a single-day range yields a non-zero (exactly one day) span, and a multi-day range still spans correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deletes docs/superpowers/plans/2026-10-05-clue-669-waverunner-date-setup-styling.md, a 1,466-line task checklist that went stale the moment the work shipped. Confirmed nothing else references this plan file; the spec it pointed to (docs/superpowers/specs/2026-10-05-clue-669-waverunner-date-setup-styling-design.md) is kept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Items 5, 6, 12 and 13 are now done too, so nothing from the review is outstanding. 5 and 6 — fixed here, in CLUE, with no dependency patch. I'd assumed these had to wait on That means all seven
12 — fixed. 13 — the 1,466-line plan is deleted, the spec kept. Nothing referenced the plan; the only link pointed the other way. Full suite 5374 passing with no regressions at any of the other One note on 5 for whenever |
There was a problem hiding this comment.
🟡 Changes recommended
Legacy date bounds, shared single-day ranges, status styling, and dropdown state contain unresolved functional issues.
6 open findings
Prevent contradictory date picker bounds · New Respect explicit unselected state in CustomSelect · New Handle persisted model URLs missing from current config · New Disable Model control while data is loading · New Remove padding from fixed-height waveform error row · New Store inclusive end date in SharedSeismogram · New
6 resolved since last review
🧠 Review effort: Balanced
endDateISO already covered the whole end day, but loadData still handed SharedSeismogram the start of it. Timeline It! copies that range, and the Timeline rejects a view whose start is not before its end, so a single-day range produced a blank timeline (and a multi-day one lost its last day). loadData now uses endDateISO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A future start date from a saved or authored document became the end field's minimum, above its maximum of today, and opening that calendar threw React's "Too many re-renders". The minimum is now capped at today. The Model dropdown now also disables while data loads, as Station does and as the design spec calls for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the status-and-output.tsx conflict with the Run/Pause work: the pause states (Pausing..., Model paused at day N of M) now take their place in the single status line, after loading and processing and before Run complete. The screen-reader status region is kept. The Run/Pause tests now read the status line rather than the removed .estimated-time element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kswenson
left a comment
There was a problem hiding this comment.
Approving. Thanks for the thorough follow-up. I checked every item from my earlier review against the code, and all of them are addressed. Fixing the useDropdown problems in CustomSelect was the better call: it fixes every place CustomSelect is used, not just WaveRunner.
I've pushed a few changes to the branch so this can merge without another round. They're described below, along with some smaller items for you to decide on.
Changes I made on the branch
941eb3606: the shared seismogram now gets the inclusive end day. This finishes item 12. endDateISO covered the whole end day, but loadData still stored the start of the end day in SharedSeismogram. Timeline It! copies that range, and TimelineContentModel.setViewRange refuses a view whose start isn't before its end. So a single-day range produced a blank timeline, and a multi-day range lost its last day. loadData now uses endDateISO, and there is a test for the single-day span. (Copilot flagged this.)
5268ed166: two fixes in data-setup.tsx.
- The end picker's bounds and saved future dates. Master's
datetime-localhad no maximum, so a saved or authored document can already have a start date after today. That start date became the end field'sminValue, above itsmaxValueof today. Opening the end calendar then threw React's "Too many re-renders", which is the contradictory-bounds problem you ran into in the library. The minimum is now capped at today. The new test failed before the fix. (Copilot flagged this.) - Model is disabled while data loads, as Station already was and as the spec (line 191) calls for. (Copilot flagged this.)
423f0f6c8: merged master and resolved the conflict with the Run/Pause work. The single status line now includes the pause states, in priority order:
- error
- loading
- "Processing day N of M..."
- "Pausing after day N..." / "Pausing..." / "Model paused at day N of M. Run to continue."
- "Run complete."
- "Ready to run the model." / "Set up data then run the model."
Master's screen-reader role="status" region is kept. The Run/Pause tests read .status-line now that .estimated-time is gone. I also combined a duplicate mobx-state-tree import the merge left behind.
The PR description. I added a section on the CustomSelect changes, since they affect the app header, the student/problem/class menus and sort-work, and QA should know. I also added the three new behavior changes (typed-date commit, inclusive end day, end-field bound), updated the test counts and switched a few spellings to US English.
For your consideration
These are non-blocking, so fix them here, in a follow-up or not at all.
- Before anything is chosen, the first option is announced as "selected". This affects both Station and Model. When no item is marked
selected,CustomSelect'sselectedstate falls back toitems[0].text. The newaria-selectedoverride then marks the first option as selected before the student has picked one, which is the same statedata-setup.scssalready hides visually. One fix would be to honor an explicitselected: falseand fall back to the first item only when no item sets the property. (Also flagged by Copilot.) - A saved model URL that's missing from the unit's config shows as the first model. The placeholder is hidden because
selectedModelUrlis set, soCustomSelectdisplays its first item while runs still use the orphaned URL. Master's native<select>did much the same, but stations already handle the orphan case and models don't. (Flagged by Copilot.) - Opening the calendar after typing, without leaving the field, ignores the typed date. The calendar button is inside the
Group, so clicking it doesn't trigger the blur commit, andhandleOpenChangereseeds from the storedvalue. The typed date isn't lost, since it commits on the next real blur, but the calendar opens on the old date. I haven't verified this in a browser. - The
.waveform-errorrule instatus-and-output.scssno longer does anything on the status line..status-line'spadding: 0overrides its padding, so the class only adds italics the line already has. It's safe to delete, or to replace with a rule that makes errors look different, which they currently don't.



Closes CLUE-669.
Styles the WaveRunner tile's Data Setup panel to Michael's design, and makes an end date before the start date unselectable.
Why the controls had to be replaced
Neither native control can meet the design.
<input type="datetime-local">draws its calendar as browser chrome, and a<select>'s option list is OS chrome — in both cases CSS cannot reach the part the design specifies. So "style the dropdowns and the date pickers" necessarily meant replacing both with markup we own. That is the bulk of the work; the CSS is the easy half.CustomSelect, already used by the app header, the student/problem/class menus and sort-work. No new dependency, and they inherit the house keyboard behavior.DatePicker. Nothing in the repo or in Concord's own packages does this; both were checked. One new dependency, Apache-2.0.CustomSelectchanges affect every callerCustomSelectitself changes, so the app header, the student/problem/class menus and sort-work are affected too, not just WaveRunner:aria-selectedmarks the chosen option, not whichever option has the keyboard cursor. Screen readers no longer announce every option the user arrows past as "selected".ariaLabelledByprop builds the accessible name from a visible label plus the chosen value.The root cause of the first two is in
useDropdowninaccessibility-tools;CustomSelectnow overrides it.Bundle cost: +71.5 KB gzipped, measured before and after on a chunk that is lazily loaded, so only students who open a WaveRunner tile pay it. The first measurement read 0 KB because nothing imported the library yet — that number was an artifact and was redone properly.
Stored values are unchanged
The model still stores
"YYYY-MM-DD"strings.@internationalized/date'sCalendarDatecarries no timezone, so parsing and formatting round-trip exactly and no UTC/local question arises. Nothing downstream ofTimeRangechanges.Deliberate behavior changes
Called out here rather than discovered in review:
runaccepts a single-day range, matchingloadData. The two disagreed —loadDataallowedstart == endandrundid not — which was harmless only while no UI could produce it. The calendar now can._tile-metrics.scssand totals 164px, so the tile requests that side by side and twice it when the panels stack. It cannot feed back, because the layout choice depends on the tile's width.datetime-localallowed, cannot give the calendar contradictory bounds.Out of scope
The time is displayed as a fixed
12:00 AMand is not settable — deferred deliberately. Two things surfaced during review that need their own stories:/is not a separator, two-digit years do not expand, and digits spill to the next segment. None of it is configurable. Making it type naturally means a plain text field we parse ourselves, keeping React Aria for the calendar.-. There is no category concept in the WaveRunner model or the shared seismic types; the UI is a placeholder for work not yet done.Testing
127 tests across the plugin and
CustomSelect, mutation-checked at each risky point: the Clear/Cancel/OK buffering, theminValue/maxValuebounds, and the typed-date handling each fail when the mechanism under test is removed.Two tests were caught asserting nothing and rewritten. One checked a picker bound using an adjacent-month day — React Aria disables those regardless of any bound, so it passed whether or not the bound was wired. Both bounds are now asserted on in-month days.
Worth a reviewer's attention: jest does not compile SCSS, so a green suite says nothing about whether the stylesheets build. A broken stylesheet slipped through on this branch once. Every stylesheet is now compiled with
sassdirectly as part of verification.Full suite passing, tsc clean, lint 0 errors, all stylesheets compile, no hex colors — every value comes from
vars.scss.🤖 Generated with Claude Code