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 picker was only ever handed the committed date, so a half-typed entry had nowhere to live and the segments the student had not touched fell back to their mm/dd placeholders. The field now holds a working copy: it shows what is being typed as it is typed, and commits as soon as the entry is a whole date. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each part of the date is a spinbutton that takes digits and steps with the arrow keys, but unstyled there was nothing to say so - the field read as one inert piece of text. The focused segment now highlights, placeholders are distinguishable, and the digits are tabular so stepping one part does not shuffle the others. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The segment padding added for the focus highlight pushed the fixed time onto a second line. The date, the time and the glyph no longer wrap or shrink, and the segment padding is halved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chart can render shorter than the box it sits in, and the leftover was all falling below it. The 8px gap went with it: there is only ever one child. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
CLUE-669-waverunner-tile-date-set-up-styling
|
| Run status |
|
| Run duration | 03m 38s |
| Commit |
|
| Committer | Leslie Bondaryk |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
0
|
|
|
4
|
| View all changes introduced in this branch ↗︎ | |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3028 +/- ##
==========================================
+ Coverage 86.96% 87.01% +0.05%
==========================================
Files 1057 1059 +2
Lines 60699 60823 +124
Branches 16207 16254 +47
==========================================
+ Hits 52784 52926 +142
+ Misses 7891 7875 -16
+ Partials 24 22 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Date clearing, stale status handling, control accessibility, and read-only height mutations require correction.
6 open findings
Station dropdown lacks a stable accessible label · New Model dropdown lacks a programmatic accessible label · New Clear can reset date outside picker bounds · New Stale operation errors mask current status · New Read-only rendering mutates shared row height · New Test does not cover equal-date run validation · New
What changed in this PR
Updates WaveRunner’s Data Setup UI with styled custom dropdowns and React Aria date pickers.
Changes:
- Adds bounded, buffered date selection and new default dates.
- Restyles controls, status output, and responsive tile sizing.
- Adds dependencies, documentation, and extensive tests.
| File | Description |
|---|---|
src/plugins/wave-runner/wave-runner-types.ts |
Defines responsive tile heights. |
src/plugins/wave-runner/models/wave-runner-content.ts |
Updates defaults and single-day validation. |
src/plugins/wave-runner/models/wave-runner-content.test.ts |
Tests model date behavior. |
src/plugins/wave-runner/components/wave-runner-tile.tsx |
Requests layout-dependent height. |
src/plugins/wave-runner/components/wave-runner-tile.test.tsx |
Tests layout and controls. |
src/plugins/wave-runner/components/wave-runner-tile.scss |
Updates panel layout. |
src/plugins/wave-runner/components/status-and-output.tsx |
Consolidates status messaging. |
src/plugins/wave-runner/components/status-and-output.test.tsx |
Tests graph states. |
src/plugins/wave-runner/components/status-and-output.scss |
Styles graph and status rows. |
src/plugins/wave-runner/components/date-utils.ts |
Converts stored dates. |
src/plugins/wave-runner/components/date-utils.test.ts |
Tests date conversion. |
src/plugins/wave-runner/components/date-field.tsx |
Implements the date picker. |
src/plugins/wave-runner/components/date-field.test.tsx |
Tests picker interactions. |
src/plugins/wave-runner/components/date-field.scss |
Styles date controls. |
src/plugins/wave-runner/components/data-setup.tsx |
Integrates custom controls. |
src/plugins/wave-runner/components/data-setup.test.tsx |
Tests setup controls and bounds. |
src/plugins/wave-runner/components/data-setup.scss |
Styles setup fields. |
src/plugins/wave-runner/components/_tile-metrics.scss |
Centralizes layout dimensions. |
src/plugins/wave-runner/components/_dropdown-appearance.scss |
Shares dropdown styling. |
src/plugins/wave-runner/assets/calendar-icon.svg |
Adds the calendar icon. |
package.json |
Adds date-picker dependencies. |
package-lock.json |
Locks new dependencies. |
docs/superpowers/specs/2026-10-05-clue-669-waverunner-date-setup-styling-design.md |
Documents the design. |
docs/superpowers/plans/2026-10-05-clue-669-waverunner-date-setup-styling.md |
Documents implementation steps. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
CustomSelect derives its accessible name from `title`, which data-setup.tsx
supplies only while nothing is selected (a non-empty title would otherwise
permanently mask the chosen value). Once a student picked a station or
model, the visible <label> was orphaned - a screen reader announced only
the chosen value ("Dexter Display Mine") and never said which field it was.
Add an optional `ariaLabel` prop to CustomSelect (additive, so its six other
call sites are unaffected): it sets aria-label on the trigger header and
takes precedence over title/titlePrefix when naming the dropdown's listbox.
Wire data-setup.tsx's Station and Model dropdowns to it.
Tests assert each dropdown is findable by its accessible name ("Station" /
"Model") after a selection has been made, which is what would have caught
the regression - asserting on the selected text alone would not have.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clear staged `defaultValue` (a fixed constant: kDefaultStartDate/
kDefaultEndDate) with no regard for minValue/maxValue, which the calendar
itself enforces on every other interaction. Since the end field's minValue
tracks the start field's live value, moving start later than the fixed
default left Clear with nowhere valid to land: clearing the end field after
moving start to Oct 5 staged the Oct 1 default, and OK committed an end
date before the start date - exactly the invariant the picker exists to
prevent.
Clamp the cleared value into [minValue, maxValue] before staging it for OK.
Also fixes a comment in the month-navigation dropdown that had gone stale
("CustomSelect has no aria-label prop") now that the prior commit added one.
Tests reproduce the reported scenario on both bounds.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n untested date check Two independent Copilot findings landed in the same model file and test file, so they're combined here. 1. loadDataError/runError masking (status-and-output.tsx consolidated several status rows into one line with errors first in priority, but runModel only cleared runError and loadEnvelopeData only cleared loadDataError). A failed load left its message showing straight through a later successful run, and vice versa, because neither action knew about the other's error. Each action now clears both errors when it starts. Added a model test per direction that sets one error, starts the other operation, and asserts the stale one is gone. 2. The "does not set a run error when start and end are the same day" test never called runModel - the only place the changed `endMs < startMs` check lives - so it passed whether the comparison was `<` or the old, buggy `<=`. Replaced it with a test that gets a model past the "no model"/"no metadata"/"no station" guards (mocking the download service and model runner, following the existing pattern in this file) and actually exercises the date-range check at start === end. Mutation check: reverting the production comparison to `endMs <= startMs` made the new test fail (expected "Invalid date range..." rejected; actual message was returned) - confirmed, then reverted back to `<`. Also hoists the duplicated setupTileInDocument test helper to module scope now that two describe blocks need it. 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, |


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 behaviour.DatePicker. Nothing in the repo or in Concord's own packages does this; both were checked. One new dependency, Apache-2.0.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 behaviour 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.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
94 tests across the plugin, mutation-checked at each risky point: the Clear/Cancel/OK buffering, the
minValue/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 5344 passing, tsc clean, lint 0 errors, all stylesheets compile, no hex colours — every value comes from
vars.scss.🤖 Generated with Claude Code