feat: Scheduler field-control redesign + per-instance store + lazy cron-parser#28
Merged
baymac merged 34 commits intoMay 30, 2026
Conversation
Replace the per-field At/Every (On/Every) dropdowns with a segmented ToggleButtonGroup (SegmentedControl) and restack every field row to an uppercase label above wrapping controls (FieldRow), matching the redesign mock's signature look. Value/range dropdowns are kept as-is, so there is zero capability loss (the "every N between X and Y" range still works). Also dedupe react/react-dom in the browser test project so a late MUI subpath import can't get optimized into a second React copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the component in a per-instance jotai Provider (SchedulerRoot) so two <Scheduler>s on one page no longer share and stomp the module-global atoms. The store is created on mount and discarded on unmount, which removes the manual unmount-reset hack (and its localeRef workaround) from Scheduler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
computeNextRuns now dynamically imports cron-parser (and its transitive luxon) instead of importing it at module top. The bundler splits it into an async chunk loaded only when the Next-runs panel first computes, shrinking the initial ESM entry from ~240 KB gzip to ~61 KB gzip (cron-parser's ~179 KB gzip is deferred). computeNextRuns is now async; NextRuns consumes it via effect+state with a cancel guard, and the unit tests await it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hips Completes the redesign control swap (PR3 scope): - every-mode interval -> numeric Stepper - at/on-mode values -> ChipMultiSelect (selected chips + add-menu, single-pick for non-admins) - week/month -> ToggleChipGroup + derived Any-day / Every-month segmented toggles (all-selected == cron `*`, no new atom) - the "every N between X and Y" range is preserved as an advanced RangePicker affordance, which also DRYs the duplicated range state/effects out of Minute/Hour/DayOfMonth All controls route through the existing atom setters so cron serialization is unchanged. New optional locale keys (addLabel / anyDayLabel / everyMonthLabel) with English fallback. vitest browser project force-optimizes deps so a warm cache predating the new MUI icon imports can't trigger a dup-React. 146 tests pass (4 new control-pipeline tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review feedback: bring back the old dropdown <select> for the At/Every mode selector on Minute and Hour (the segmented pills only remain on the Day-of-month On/Every selector, which was fine as-is). Also remove the newly added Week "Any day/On" and Month "Every month/On" toggles — the old version had no mode selector there, so those fields are now just their value chips. Value controls (stepper, chip-pickers, toggle chips) are unchanged. Dropped the now-unused anyDayLabel / everyMonthLabel locale keys (addLabel stays). Tests updated; 144 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correcting the previous change (which had it backwards): the At/Every (and On/Every) mode selector stays a segmented 2-way toggle (PR2). The value fields go back to the original CustomSelect dropdowns — undoing the steppers, chip-pickers, and toggle-chip groups. Removed Stepper/ChipMultiSelect/ ToggleChipGroup/RangePicker and the addLabel/anyDayLabel/everyMonthLabel locale keys. Kept: per-instance jotai store, lazy-loaded cron-parser, the FieldRow uppercase layout. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each field already shows its name in the FieldRow uppercase header (EVERY, DAY OF THE WEEK, HOUR(S)...), so the dropdown's own floating label repeated it. CustomSelect now renders the field name only as the input's aria-label (no visible floating label), keeping accessibility and getByLabelText queries intact while removing the visual duplication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dropdowns were MUI's default 40px tall and a fixed 300px wide, so a select
showing a short value ("week", "9") stretched most of the row and sat taller
than the 30px segmented toggle next to it.
- Compact the Autocomplete input to 30px (minHeight + reduced padding, using
MUI's own selector specificity so the override wins) — matches the toggle.
- Shrink size widths (sm 100->110 only where needed, md 160->140, lg 300->190).
- Period uses the small width; Hour/Minute/Day-of-month value selects use the
narrow width when single (every-mode / non-admin) and the wider one only when
showing multiple chips (at/on-mode).
- Drop the between/and connector height 40->30 to stay aligned.
142 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring back elements from the original design while keeping the current folding and the 2-way toggles: - Section (FieldRow) headers are the connector word: every / in / on-every / on / at-every / at-every (top to bottom for a yearly cron). - The select inside each section regains its own field-name label (Period, Month(s), Days, Week Days, Hour(s), Minute(s)) — complementary to the section word, not redundant. - Restore the original ~40px select height (the compact 30px looked odd); the segmented toggle is bumped to 40px so the row stays aligned. On/Every and At/Every remain 2-way toggle buttons. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For the toggle fields (Day-of-month, Hour, Minute) the segmented toggle now sits in the section header slot (replacing the redundant ON/EVERY / AT/EVERY text) instead of in the controls row, and is shrunk to a compact 28px height. Non-toggle sections (Period/Month/Week) keep their connector-word text header. Added breathing room (header bottom margin 8->14) between the header and the select. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In every-mode (and non-admin), the value select holds a single value but was still a multi-select rendering it as a chip; at the narrow width the chip wrapped and the field ballooned to ~68px tall. Now a single-value select shows its value as plain inline text on one line, so it's a normal ~40px single-line height like the other selects. Multi-select (at/on-mode) still renders chips. 142 tests pass (incl. the multi-select chips test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the uppercase section headers the toggle sits among. The button's lowercase aria-label still drives the accessible name, so role/name queries are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the every-mode range controls (between / start / and / end) in a nowrap RangeGroup so they never break apart across lines. The group can still wrap as a whole relative to the value select on a narrow card, but the range itself stays on a single line. Used by Minute/Hour/DayOfMonth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hour-range labels are on-the-hour only, so "12:00 AM" -> "12 AM". Shorter labels let the start/end selects drop from md to sm width. Labels are also the option values, but the cron logic is index-based so the change is safe; updated the #16 test to the new labels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sections with only one possible connector (Period=Every, Month=in, Week=on) now show that word as a single blue SectionTag pill mirroring the selected segment of the 2-way toggles — so every section header reads as a toggle button (single-value ones are just one marked button). FieldRow gained a headerSlot for this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a narrow (mobile) card the cron field pushed the copy/reset icons off the right edge (clipped). The cron field + copy + reset are now grouped in an Actions wrapper that wraps to a second row together while the cron field shrinks, so the action icons always stay on-screen and side by side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Toggling at/on -> every changed the mode immediately but only fixed the value (e.g. minute 0, or day "L") in a follow-up effect, so the derived cron briefly became `*/0` / `*/L` for one render — flashing an "Invalid ... cron part" error before self-correcting. The mode toggle now sets a valid non-zero interval in the SAME update (Minute/Hour/DayOfMonth), so the cron never passes through an invalid state. Added a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SectionTag used a hardcoded 9px radius while the segmented toggle's visible corners come from MUI ToggleButton (theme.shape.borderRadius, 4px by default), so the standalone EVERY/IN/ON pill looked more rounded. SectionTag now uses the same theme.shape.borderRadius, so both match (and adapt to the host theme). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the fixed 40px height on the between/and labels — it inflated the wrapped mobile line with dead whitespace — and rely on the parents' alignItems for inline centering. Give Controls/RangeGroup an explicit 14px rowGap (matching the header rhythm) while keeping the 10px column gap, so toggle -> select -> between -> range are evenly spaced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a narrow card the cron field + copy/reset wrap to their own row. marginLeft:auto right-shoved that group on the wrapped line, so the input box no longer lined up with the calendar icon above it. Push the group right via a growing Title instead (flex-grow only redistributes space on a non-wrapped line, so the wrap point is unchanged), and drop the auto margin so the wrapped row sits at the header's left padding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`title` overrides the header text next to the calendar icon (taking precedence over the locale's scheduleTitle). `color` sets the card's accent — it overrides palette.primary in a scoped ThemeProvider so the header bar, the selected toggle segment, and the section pills all recolor together; contrastText is recomputed from the color via augmentColor (falling back to a default theme when there's no parent ThemeProvider). Both default to current behavior when omitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…isabled text Demo: move the ThemeProvider/CssBaseline into DemoPage so a dark-mode icon toggle in the app bar can flip palette.mode, and add a Desktop/Mobile ToggleButtonGroup that constrains the Scheduler to a phone-width device frame (tripping its container queries — stacked layout, wrapped header). Page/code-block backgrounds now use background.default / action.hover so they follow the theme. Lib: CustomSelect disabled-input text was hardcoded `white` (invisible on the light card; only correct in dark). Use theme.palette.text.primary so disabled values stay legible at full contrast in both modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ur-range times
Validation errors were internal tokens wrapped in a redundant prefix
(e.g. "Invalid day of week cron part: Invalid single all"). Each field
validator now returns a plain reason clause and validateCronExp prepends
the field name, so messages read as sentences: "Day of week is invalid",
"Minute must be between 0 and 59", "Hour range must be low to high",
"A cron expression must have 5 parts". Also fixes the month validator's
out-of-range message (said 0–6, now 1–12).
Hour-range time labels drop the leading zero ("6 AM", not "06 AM"); the
range maps by array index so this is cosmetic only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rols The dark-mode toggle lived in the website header and recolored the whole page. Move it into the controls row beside Admin/Locale/View, keep the demo site itself on a fixed light theme (PAGE_THEME), and wrap only the Scheduler preview (and its mobile frame) in the light/dark theme via a nested ThemeProvider — so you preview the component in dark mode without darkening the surrounding page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…even The between/and labels are default body1 Typography (line-height 1.5), so on a wrapped mobile line the flex item ran ~8px taller than the glyph and padded extra space above and below "between" — making the gaps around it larger than the toggle->select gap. lineHeight 1 hugs the glyph, so toggle->select, select->between, and between->range all land at the same 14px rhythm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the outdated hero image (old label-left layout + indigo cron pill) with a fresh capture from the demo dev server in the fully expanded "every year" view, showing the redesign: Schedule header with the cron + human-readable summary, the Next-runs panel, the EVERY/IN/ON section pills, the On/Every & At/Every segmented toggles, multi-select day chips, and the hour between-range. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pp-wide dark The scoped dark-mode ThemeProvider had no baseline, so text with an inherited color (the between/and labels and other color:inherit Typography) picked up the light page's near-black color and rendered dark-on-dark — the preview "felt off" vs the old app-wide dark mode, which had a global CssBaseline. ScopedCssBaseline applies the same baseline (background.default, text.primary, color-scheme) scoped to the preview wrapper, matching app-wide dark without darkening the page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ScopedCssBaseline paints background.default; in desktop it sits flush as a square behind the card, so its dark fill peeked through the card's rounded corners (radius 12 + overflow hidden) as a dark halo. Make the desktop wrapper background transparent — the card is its own dark surface and the light page now shows behind the rounded corners. Mobile keeps the dark background (it's the phone frame). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Upgrade @mui/* to ^9, Vitest/browser to ^4, Vite to 8, Biome to 2.4 - Switch tsconfig to esnext modules + bundler resolution - CustomSelect: migrate renderTags → renderValue, pin input box-sizing - SchedulerHeader: fix field height under ScopedCssBaseline box-sizing - Scheduler: memoize accent theme via useTheme to avoid no-parent warning - FieldRow: even out stacked gap under segmented-toggle headers - gitignore .vitest-attachments/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baymac
added a commit
that referenced
this pull request
May 30, 2026
* feat: redesign Scheduler shell + add Next-runs panel (PR1) Phase 1 of the Scheduler UI redesign: a new card shell around the existing field controls (kept as dropdowns; PR2 swaps them to pills/steppers/chips). - SchedulerHeader: theme-aware (primary.main) header carrying the cron expression, copy (open to all) and reset (admin-gated); folds in the old CronExp, preserving its debounce + the two-way prop-sync (#20 fix intact). - NextRuns: live occurrence preview via cron-parser, gated behind the existing validateCronExp and guarded against throws; component-local useMemo + 30s tick (no Jotai/wall-clock impurity); Intl.* formatting (locale-aware, no new date strings); invalid -> "Enter a valid schedule", never-fires -> "No upcoming runs"; renders 5 rows, CSS-hides 4-5 on narrow widths. - Two-column card with a container query (auto/split/stacked via `layout` prop); form column gets min-width:0 + overflow-x:auto so wide "every between X and Y" rows scroll inside the column instead of spilling off-page. - CronReader moved to the top as the theme-tokened summary line. - SchedulerProps gains timezone/layout/slotProps; Locale gains OPTIONAL panel strings with English fallback (non-breaking for customLocale consumers). - Tests: nextRuns unit + contract suite (114 unit), 6 new browser tests (header, panel states, dark+RTL, admin copy/reset) — 24 browser green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: Scheduler field-control redesign + per-instance store + lazy cron-parser (#28) * feat: swap At/Every selectors to segmented pills + uppercase rows (PR2) Replace the per-field At/Every (On/Every) dropdowns with a segmented ToggleButtonGroup (SegmentedControl) and restack every field row to an uppercase label above wrapping controls (FieldRow), matching the redesign mock's signature look. Value/range dropdowns are kept as-is, so there is zero capability loss (the "every N between X and Y" range still works). Also dedupe react/react-dom in the browser test project so a late MUI subpath import can't get optimized into a second React copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: scope jotai store per Scheduler instance Wrap the component in a per-instance jotai Provider (SchedulerRoot) so two <Scheduler>s on one page no longer share and stomp the module-global atoms. The store is created on mount and discarded on unmount, which removes the manual unmount-reset hack (and its localeRef workaround) from Scheduler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf: lazy-load cron-parser into a separate chunk computeNextRuns now dynamically imports cron-parser (and its transitive luxon) instead of importing it at module top. The bundler splits it into an async chunk loaded only when the Next-runs panel first computes, shrinking the initial ESM entry from ~240 KB gzip to ~61 KB gzip (cron-parser's ~179 KB gzip is deferred). computeNextRuns is now async; NextRuns consumes it via effect+state with a cancel guard, and the unit tests await it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: swap field value controls to steppers + chip-pickers + toggle chips Completes the redesign control swap (PR3 scope): - every-mode interval -> numeric Stepper - at/on-mode values -> ChipMultiSelect (selected chips + add-menu, single-pick for non-admins) - week/month -> ToggleChipGroup + derived Any-day / Every-month segmented toggles (all-selected == cron `*`, no new atom) - the "every N between X and Y" range is preserved as an advanced RangePicker affordance, which also DRYs the duplicated range state/effects out of Minute/Hour/DayOfMonth All controls route through the existing atom setters so cron serialization is unchanged. New optional locale keys (addLabel / anyDayLabel / everyMonthLabel) with English fallback. vitest browser project force-optimizes deps so a warm cache predating the new MUI icon imports can't trigger a dup-React. 146 tests pass (4 new control-pipeline tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: mark all redesign follow-up TODOs as done Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: revert At/Every to dropdown; drop Week/Month mode toggles Per review feedback: bring back the old dropdown <select> for the At/Every mode selector on Minute and Hour (the segmented pills only remain on the Day-of-month On/Every selector, which was fine as-is). Also remove the newly added Week "Any day/On" and Month "Every month/On" toggles — the old version had no mode selector there, so those fields are now just their value chips. Value controls (stepper, chip-pickers, toggle chips) are unchanged. Dropped the now-unused anyDayLabel / everyMonthLabel locale keys (addLabel stays). Tests updated; 144 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert: keep mode selector as 2-way toggle, restore value dropdowns Correcting the previous change (which had it backwards): the At/Every (and On/Every) mode selector stays a segmented 2-way toggle (PR2). The value fields go back to the original CustomSelect dropdowns — undoing the steppers, chip-pickers, and toggle-chip groups. Removed Stepper/ChipMultiSelect/ ToggleChipGroup/RangePicker and the addLabel/anyDayLabel/everyMonthLabel locale keys. Kept: per-instance jotai store, lazy-loaded cron-parser, the FieldRow uppercase layout. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: drop redundant floating label on field dropdowns Each field already shows its name in the FieldRow uppercase header (EVERY, DAY OF THE WEEK, HOUR(S)...), so the dropdown's own floating label repeated it. CustomSelect now renders the field name only as the input's aria-label (no visible floating label), keeping accessibility and getByLabelText queries intact while removing the visual duplication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: match dropdown height to the toggle and tighten widths Dropdowns were MUI's default 40px tall and a fixed 300px wide, so a select showing a short value ("week", "9") stretched most of the row and sat taller than the 30px segmented toggle next to it. - Compact the Autocomplete input to 30px (minHeight + reduced padding, using MUI's own selector specificity so the override wins) — matches the toggle. - Shrink size widths (sm 100->110 only where needed, md 160->140, lg 300->190). - Period uses the small width; Hour/Minute/Day-of-month value selects use the narrow width when single (every-mode / non-admin) and the wider one only when showing multiple chips (at/on-mode). - Drop the between/and connector height 40->30 to stay aligned. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: name sections by connector word, restore select labels + height Bring back elements from the original design while keeping the current folding and the 2-way toggles: - Section (FieldRow) headers are the connector word: every / in / on-every / on / at-every / at-every (top to bottom for a yearly cron). - The select inside each section regains its own field-name label (Period, Month(s), Days, Week Days, Hour(s), Minute(s)) — complementary to the section word, not redundant. - Restore the original ~40px select height (the compact 30px looked odd); the segmented toggle is bumped to 40px so the row stays aligned. On/Every and At/Every remain 2-way toggle buttons. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: move on/every & at/every toggle into the section header For the toggle fields (Day-of-month, Hour, Minute) the segmented toggle now sits in the section header slot (replacing the redundant ON/EVERY / AT/EVERY text) instead of in the controls row, and is shrunk to a compact 28px height. Non-toggle sections (Period/Month/Week) keep their connector-word text header. Added breathing room (header bottom margin 8->14) between the header and the select. 142 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: single-value select renders as text, not a wrapping chip In every-mode (and non-admin), the value select holds a single value but was still a multi-select rendering it as a chip; at the narrow width the chip wrapped and the field ballooned to ~68px tall. Now a single-value select shows its value as plain inline text on one line, so it's a normal ~40px single-line height like the other selects. Multi-select (at/on-mode) still renders chips. 142 tests pass (incl. the multi-select chips test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: uppercase the on/every/at toggle labels Match the uppercase section headers the toggle sits among. The button's lowercase aria-label still drives the accessible name, so role/name queries are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: keep "between X and Y" range on one line Wrap the every-mode range controls (between / start / and / end) in a nowrap RangeGroup so they never break apart across lines. The group can still wrap as a whole relative to the value select on a narrow card, but the range itself stays on a single line. Used by Minute/Hour/DayOfMonth. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: drop ":00" from hour-range time labels, narrow the selects Hour-range labels are on-the-hour only, so "12:00 AM" -> "12 AM". Shorter labels let the start/end selects drop from md to sm width. Labels are also the option values, but the cron logic is index-based so the change is safe; updated the #16 test to the new labels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: render single-value section labels as a blue "marked" pill Sections with only one possible connector (Period=Every, Month=in, Week=on) now show that word as a single blue SectionTag pill mirroring the selected segment of the 2-way toggles — so every section header reads as a toggle button (single-value ones are just one marked button). FieldRow gained a headerSlot for this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: wrap range on narrow cards; narrow day-of-month range selects - RangeGroup stays on one line on a normal-width card but wraps below @container (max-width: 480px) so the range flows below "between" on mobile instead of clipping off the right edge. - Day-of-month range start/end selects md -> sm (ordinals like "1st"/"31st" are short). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: cap multi-select chip area height so it scrolls instead of ballooning When many/all options are selected (e.g. every day of the month), the chip area grew unbounded and the field became enormous while open. Multi-selects now cap the input at maxHeight 96 with overflow-y auto, so the chips scroll within a bounded field. Single-value selects are unaffected (one line). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: keep the two range selects together when the range wraps on mobile Glue start/and/end into a nowrap RangePair inside RangeGroup. On a narrow card "between" drops to its own line and "0 and 59" stay together on the next line, instead of the end select wrapping alone onto a third line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: cap visible chips to 3 + "+N" so multi-selects stay bounded The maxHeight+scroll cap made scrolled chips slide under the floating label (the "Week Days" strikethrough) and pushed the overflow indicator outside the box. Instead, the custom renderTags now always shows at most 3 chips plus a "+N" indicator — even when focused/open — so the field is a bounded, stable height in every state with no scroll and no label overlap. Removed the now- redundant limitTags prop from the field selects (it conflicted with the custom renderTags). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: wrap header so copy/reset stay visible on a narrow card On a narrow (mobile) card the cron field pushed the copy/reset icons off the right edge (clipped). The cron field + copy + reset are now grouped in an Actions wrapper that wraps to a second row together while the cron field shrinks, so the action icons always stay on-screen and side by side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: no transient invalid cron when toggling a field to "every" Toggling at/on -> every changed the mode immediately but only fixed the value (e.g. minute 0, or day "L") in a follow-up effect, so the derived cron briefly became `*/0` / `*/L` for one render — flashing an "Invalid ... cron part" error before self-correcting. The mode toggle now sets a valid non-zero interval in the SAME update (Minute/Hour/DayOfMonth), so the cron never passes through an invalid state. Added a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: match standalone section pill radius to the toggle SectionTag used a hardcoded 9px radius while the segmented toggle's visible corners come from MUI ToggleButton (theme.shape.borderRadius, 4px by default), so the standalone EVERY/IN/ON pill looked more rounded. SectionTag now uses the same theme.shape.borderRadius, so both match (and adapt to the host theme). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: even out vertical spacing in stacked (mobile) every-mode rows Drop the fixed 40px height on the between/and labels — it inflated the wrapped mobile line with dead whitespace — and rely on the parents' alignItems for inline centering. Give Controls/RangeGroup an explicit 14px rowGap (matching the header rhythm) while keeping the 10px column gap, so toggle -> select -> between -> range are evenly spaced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: align wrapped header cron field with the title's calendar icon On a narrow card the cron field + copy/reset wrap to their own row. marginLeft:auto right-shoved that group on the wrapped line, so the input box no longer lined up with the calendar icon above it. Push the group right via a growing Title instead (flex-grow only redistributes space on a non-wrapped line, so the wrap point is unchanged), and drop the auto margin so the wrapped row sits at the header's left padding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: add `title` and `color` props to the Scheduler `title` overrides the header text next to the calendar icon (taking precedence over the locale's scheduleTitle). `color` sets the card's accent — it overrides palette.primary in a scoped ThemeProvider so the header bar, the selected toggle segment, and the section pills all recolor together; contrastText is recomputed from the color via augmentColor (falling back to a default theme when there's no parent ThemeProvider). Both default to current behavior when omitted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(demo): dark-mode + mobile/desktop preview toggles; theme-aware disabled text Demo: move the ThemeProvider/CssBaseline into DemoPage so a dark-mode icon toggle in the app bar can flip palette.mode, and add a Desktop/Mobile ToggleButtonGroup that constrains the Scheduler to a phone-width device frame (tripping its container queries — stacked layout, wrapped header). Page/code-block backgrounds now use background.default / action.hover so they follow the theme. Lib: CustomSelect disabled-input text was hardcoded `white` (invisible on the light card; only correct in dark). Use theme.palette.text.primary so disabled values stay legible at full contrast in both modes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: human-readable cron validation messages; drop leading zero in hour-range times Validation errors were internal tokens wrapped in a redundant prefix (e.g. "Invalid day of week cron part: Invalid single all"). Each field validator now returns a plain reason clause and validateCronExp prepends the field name, so messages read as sentences: "Day of week is invalid", "Minute must be between 0 and 59", "Hour range must be low to high", "A cron expression must have 5 parts". Also fixes the month validator's out-of-range message (said 0–6, now 1–12). Hour-range time labels drop the leading zero ("6 AM", not "06 AM"); the range maps by array index so this is cosmetic only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * demo: scope dark mode to the scheduler preview, move toggle into controls The dark-mode toggle lived in the website header and recolored the whole page. Move it into the controls row beside Admin/Locale/View, keep the demo site itself on a fixed light theme (PAGE_THEME), and wrap only the Scheduler preview (and its mobile frame) in the light/dark theme via a nested ThemeProvider — so you preview the component in dark mode without darkening the surrounding page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: tighten the "between" line box so mobile every-range gaps are even The between/and labels are default body1 Typography (line-height 1.5), so on a wrapped mobile line the flex item ran ~8px taller than the glyph and padded extra space above and below "between" — making the gaps around it larger than the toggle->select gap. lineHeight 1 hugs the glyph, so toggle->select, select->between, and between->range all land at the same 14px rhythm. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: refresh demo screenshot to the redesigned component (year view) Replace the outdated hero image (old label-left layout + indigo cron pill) with a fresh capture from the demo dev server in the fully expanded "every year" view, showing the redesign: Schedule header with the cron + human-readable summary, the Next-runs panel, the EVERY/IN/ON section pills, the On/Every & At/Every segmented toggles, multi-select day chips, and the hour between-range. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * demo: wrap scheduler preview in ScopedCssBaseline so scoped dark == app-wide dark The scoped dark-mode ThemeProvider had no baseline, so text with an inherited color (the between/and labels and other color:inherit Typography) picked up the light page's near-black color and rendered dark-on-dark — the preview "felt off" vs the old app-wide dark mode, which had a global CssBaseline. ScopedCssBaseline applies the same baseline (background.default, text.primary, color-scheme) scoped to the preview wrapper, matching app-wide dark without darkening the page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * demo: fix dark-mode corner halo around the scheduler card (desktop) ScopedCssBaseline paints background.default; in desktop it sits flush as a square behind the card, so its dark fill peeked through the card's rounded corners (radius 12 + overflow hidden) as a dark halo. Make the desktop wrapper background transparent — the card is its own dark surface and the light page now shows behind the rounded corners. Mobile keeps the dark background (it's the phone frame). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump deps (MUI 9, Vitest 4, Biome 2.4) and adapt scheduler - Upgrade @mui/* to ^9, Vitest/browser to ^4, Vite to 8, Biome to 2.4 - Switch tsconfig to esnext modules + bundler resolution - CustomSelect: migrate renderTags → renderValue, pin input box-sizing - SchedulerHeader: fix field height under ScopedCssBaseline box-sizing - Scheduler: memoize accent theme via useTheme to avoid no-parent warning - FieldRow: even out stacked gap under segmented-toggle headers - gitignore .vitest-attachments/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Select a category
Related issue link
Follow-up to #27 (Scheduler redesign, PR1). Completes every remaining redesign TODO.
Background and solution
This stacks the full field-control redesign plus the deferred infra follow-ups onto PR1, in reviewable commits:
SegmentedControl(ToggleButtonGroup); every row restacked to an uppercase label over wrapping controls (FieldRow).Stepper; at/on values →ChipMultiSelect(chips + add-menu, single-pick for non-admins); week/month →ToggleChipGroupwith derivedAny day/Every monthtoggles. Theevery N between X and Yrange is preserved as the advancedRangePicker, which also DRYs the duplicated range state/effects out of Minute/Hour/DayOfMonth. All controls route through the existing atom setters, so cron serialization is unchanged; new locale keys are optional with English fallback.SchedulerRootwraps each instance in its ownProvider, so two schedulers on one page no longer stomp each other; removed the unmount-reset hack.computeNextRunsdynamic-imports it, splitting cron-parser + luxon into an async chunk and shrinking the initial ESM entry from ~240 KB → ~61 KB gzip.146 tests pass (unit + browser) and
yarn buildis green. The browser test project force-optimizes deps + dedupes React so a warm cache can't trigger the dup-ReactuseContextfailure.