Skip to content

fix!: expose honest Temporal values in RootState (remove type lies) - #18

Merged
dogmar merged 2 commits into
mainfrom
fix/rootstate-types
Jun 15, 2026
Merged

fix!: expose honest Temporal values in RootState (remove type lies)#18
dogmar merged 2 commits into
mainfrom
fix/rootstate-types

Conversation

@dogmar

@dogmar dogmar commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Audit fix E (5 of 5). Breaking.

Summary

The render-prop RootState (state.root, passed to every component's render function) lied about its types, and was entirely empty inside WeeksView. This fixes both and makes RootState honest.

The two runtime lies (audit finding)

  1. MonthView built selectedDates/rangeStart/rangeEnd with as anyTemporal.PlainDate behind the consumer's format type. With format="Date", state.root.rangeStart.getFullYear() type-checks but crashes (it's a PlainDate). And selected was the format value while the others were PlainDate — inconsistent.
  2. WeeksView used {} as RootState in its MonthViewState shims, so every state.root.* read undefined at runtime inside WeeksView (state.root.viewing.year → crash). There are two such shims (WeeksView root + the inner Grid); both are now real.

Fix (Option 1, chosen by the maintainer)

RootState's date fields (selected, selectedDates, rangeStart, rangeEnd) are now Temporal.PlainDate — the internal representation — matching focused/viewing which already were. The format-typed values still flow via value/defaultValue/onValueChange; RootState is for rendering (e.g. selected?.toLocaleString(locale)).

  • The selection part is built once in CalendarProvider as baseRootState (on the calendar state context); each view spreads it and adds its own focused/viewing, so MonthView and WeeksView expose identical state. selected is the first committed date (the PlainDate behind the old selected DateValueObject — semantics verified to match).
  • Because the date fields are no longer format-typed, RootState becomes non-generic, which removes all ~12 as any casts and 2 {} as RootState shims, and de-genericizes the ~15 leaf component State/Props types whose only F use was root (the leaf display components become non-generic). Genuinely format-generic types — Root*, MonthView*, WeeksView*, CalendarProvider*, DateRange, RawValueForFormat — keep F.
  • Format subpaths regenerated; website example/lib files de-genericized to match.

Breaking change

state.root.selected/selectedDates/rangeStart/rangeEnd are now Temporal.PlainDate instead of the configured format. Display via toLocaleString; the format-typed values remain available through the value props. The leaf render components (DateString, Grid, NavButton, etc.) are no longer generic over the format.

Tests

  • month-view.test.tsx "rootState values": under format="Date", state.root.selected is a Temporal.PlainDate (not a JS Date) — fails pre-fix.
  • weeks-view.test.tsx "rootState shim is populated": the useMonthViewState() shim path exposes a populated rootState (viewing/focused/selected/…) — fails pre-fix ({}, all undefined).
  • vp run ready: lint+typecheck 0/0, package + website suites green.
  • Pre-commit reviewed: verified selected semantics, ISO viewing, memo deps, de-genericization soundness, and idempotent subpath regeneration.

🤖 Generated with Claude Code

dogmar and others added 2 commits June 14, 2026 13:57
BREAKING CHANGE: the render-prop `RootState` (`state.root`) now exposes
all selection values as `Temporal.PlainDate` instead of the configured
value `format`. The format-typed values still flow via
`value`/`defaultValue`/`onValueChange`; `RootState` is for rendering
(e.g. `selected?.toLocaleString(locale)`). `focused`/`viewing` were
already Temporal. `RootState` is now non-generic.

Fixes two runtime lies the audit flagged:
- MonthView built `selectedDates`/`rangeStart`/`rangeEnd` with `as any`
  (PlainDate behind a format type) and `selected` as the format value;
  now all are honest PlainDate.
- WeeksView's MonthViewState shims used `{} as RootState`, so every
  `state.root` field read `undefined` inside WeeksView. Now both the
  WeeksView-root and inner-Grid shims expose a real, populated rootState.

The selection part of rootState is built once in CalendarProvider
(`baseRootState`, on the calendar state context) and spread by each view,
which adds its own `focused`/`viewing` — so MonthView and WeeksView are
identical. `selected` is the first committed date (the PlainDate behind
the old `selected` DateValueObject).

Because the date fields are no longer format-typed, `RootState` and the
~15 leaf component State/Props types that referenced `RootState<F>` lose
their now-unused `<F>` generic (the leaf display components become
non-generic). Genuinely format-generic types (Root/MonthView/WeeksView/
CalendarProvider, DateRange, RawValueForFormat) keep `F`. Removes ~12
`as any` and 2 `{} as RootState` casts; format subpaths regenerated.

Adds regression tests: MonthView rootState exposes PlainDate under
format="Date" (was a JS Date); WeeksView shim rootState is populated
(was all undefined).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The semantic-release publish failed npm provenance validation
("repository.url" is "", expected to match the GitHub repo). Add the
repository (with monorepo directory), homepage, and bugs fields so the
published package's provenance bundle validates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dogmar
dogmar merged commit decc682 into main Jun 15, 2026
6 checks passed
@dogmar
dogmar deleted the fix/rootstate-types branch June 15, 2026 02:38
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant