Skip to content

feat: CalendarPreview root and inline calendar - #895

Open
Shreyag02 wants to merge 5 commits into
chore/calendar-deps-upgradefrom
feat/calendar-preview-base
Open

feat: CalendarPreview root and inline calendar#895
Shreyag02 wants to merge 5 commits into
chore/calendar-deps-upgradefrom
feat/calendar-preview-base

Conversation

@Shreyag02

@Shreyag02 Shreyag02 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 2 of 7 in the RFC 005 stack, on top of #894.

Ships the inline calendar — a root that owns the state, plus the parts that draw a day grid.

<CalendarPreview value={date} onValueChange={setDate}>
  <CalendarPreview.Days />
</CalendarPreview>

components/calendar/ is untouched — Calendar, DatePicker and RangePicker keep working.

In Root, 10 parts, useCalendar(), docs page
Not in .Trigger / .Content / .Input (PR 3), range (PR 4), scale switcher + period views (PR 5)
Guard A test asserts the exported part list exactly, so an early part fails the build

Changes

Parts — each takes render, className, ref, a data-slot, and spreads ...props last.

Part Notes
.Days numberOfMonths. Hugs its content
.Header Composes caption + reset + nav when childless
.Caption dropdown opens our own two-column month/year scroller
.Reset Restores defaultDate. Value reset, not a view reset
.Grid The only file importing react-day-picker
.Day / .Weekday Bound to RDP's slots
.PrevMonth / .NextMonth Never disabled by bounds
.Footer Takes a string or any node

Behaviour

  • .Reset renders only when defaultDate is set and the value differs from it; it leaves the visible month alone.
  • .Caption dropdown mounts zero Selects — plain buttons in a popover we own. Picking moves the view, never selects a value.
  • Bounds (minDate / maxDate / isDateUnavailable) disable cells but never stop navigation.
  • dateInfo and tooltipMessages are now functions, not records keyed by a formatted string.
  • useCalendar() exported from the barrel: value, scale, view month, setters, availability predicate.
  • Design matched to the Figma frames — caption left / nav right, three-letter weekdays, Sunday first, two-month layout captions each grid.
  • Docs page at /docs/components/calendar-preview.

One default changed

Prop Was Now Why
showOutsideDays true false Every new frame ends the grid on the last day of the month. Opt back in with the prop

Technical Details

Decision Reason
RDP behind .Grid only Runs with hideNavigation + captionLayout='label' so it never mounts a Select. Selection props come from context, not CalendarPreviewGridProps — so ...props really is last
defaultDate separate from defaultValue useControlled ignores defaultValue once value is passed, so a controlled consumer would never see .Reset
Root renders an element .Days and .Footer are in-flow siblings; without a box they inherit the parent's layout and sit side by side in a flex row
Dates compared as day-keys, not instants A minDate carrying a time of day still leaves its own day selectable
Deleted monthNames() Went dead when the caption scroller moved to abbreviations

Header alignment — the header sat on a different grid from the days. No global box-sizing: border-box here, so:

Element Was Now
Day cell (td) 44px (40 + border + UA padding) 40px
Weekday heading (th) 42px (40 + UA padding) 40px
Drift at Saturday 13px 0px

Also removed the table's default 2px border-spacing, and gave both headers 8px inline padding so the caption aligns with the label rather than the cell edge. A week-number column is a gutter, so the header adds its width on top — read from the rendered grid with :has(), since showWeekNumber is a .Grid prop and the header is its sibling.

Muted days in the frames — April 1-16 render muted with today on the 17th. A minDate demo and a built-in "past dates disabled" draw identically. Built as a demo, no default bound: a past bound isn't expressible as an opt-out (you'd need minDate={new Date(0)}) and would break date-of-birth and "filter since" fields.

Test Plan

  • Manual testing completed
  • Build and type checking passes
Check Result
calendar-preview/ tests 356 passed
Full package suite 3063 passed, 1 skipped (skip predates this stack)
biome check clean
tsc --noEmit only the 6 errors predating this stack, none in calendar*/
build:apsara / docs build both green

Covered — exact data-slot set and every state attribute; all four .Reset visibility cases plus reset under a controlled value; caption dropdown mounts zero Selects; bounds disable cells without stopping navigation; .Footer with a string and a node.

Manual — rendered single-month, two-month and caption-dropdown layouts in a real browser against the Figma frames; measured column alignment directly (0.0px drift across all seven columns); stepped through every demo tab on the new docs page.

SQL Safety (if your PR touches *_repository.go or goqu.*)

Not applicable — TypeScript and CSS only. No Go files, no database access.

Phase 1 of RFC 005. Delivers the inline day view — a root that owns the
state and eight parts that render it. No popover, no input: `.Trigger`,
`.Content` and `.Input` land in PR 3.

    <CalendarPreview>
      <CalendarPreview.Days />
    </CalendarPreview>

The root renders no DOM of its own. It holds value, view month and scale
through `useControlled`, and hands them to the parts through a part-aware
context hook whose generic value is stored as `unknown` and cast once, at
the hook boundary. A part used outside the root throws a message naming
the part the author actually wrote, not this file.

Two things the RFC singles out:

`.Reset` is a **value** reset, keyed off `defaultDate`. It renders only
when there is something to restore — `defaultDate` is set and the value
differs — and it leaves the visible month alone. `defaultDate` is a
separate prop from `defaultValue` precisely because `useControlled`
ignores `defaultValue` once `value` is passed, so a controlled consumer
would otherwise never see the part at all.

`.Caption`'s dropdown is **our own scroller**: two columns of plain
buttons in a popup we own. No `Select` is mounted anywhere in this
component — asserted, not asserted-to — which is what keeps the
popover-dismissal loop `use-picker-popover.ts` spends 185 lines
suppressing from coming back. Picking from it moves the view; it never
selects a value.

Everything else worth naming:

- Bounds limit **selection**, never navigation. `minDate`/`maxDate` and
  `isDateUnavailable` disable cells; the nav buttons and the caption
  scroller still move the view wherever the user wants. Bounds compare as
  day-keys, so a `minDate` carrying a time of day still makes its own day
  selectable — the current family compares instants and silently disables
  it
- `dateInfo` and `tooltipMessages` are now functions. The record form
  keyed cells by a formatted string and silently missed every day once a
  `timeZone` shifted the key. Info still renders above the date number
- `.Grid` is the only file importing react-day-picker. It runs with
  `hideNavigation` and `captionLayout='label'`, and `mode`, `selected`,
  `onSelect`, `required`, `month`, `onMonthChange` and `timeZone` come
  from context rather than props — none is in `CalendarPreviewGridProps`,
  so nothing is force-overridden after the consumer's spread and
  spread-last holds for the first time in this family
- Cells carry `data-selected`, `data-draft`, `data-unavailable`,
  `data-today`, `data-outside` and `data-scale` beside their slot. At day
  scale the draft is the roving-focus cell — arrowed to, not yet entered
- `useCalendar()` ships from the barrel beside `useTour` and the other
  six, returning value, scale, view month, their setters and the
  availability predicate. Nothing more: what it returns is semver-covered

Zero `slotProps`, zero `biome-ignore`, no `forwardRef`, `<Ctx value>`
throughout, every part spreads `...props` last, and the CSS carries no
`Todo: var does not exist`.

`components/calendar/` is untouched.

100% statement, function and line coverage on the new directory, 99.6% of
branches — the one uncovered branch is a defensive guard in the root's
`reset`, unreachable while `.Reset` is the only caller.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
apsara Ready Ready Preview Sep 4, 2026 10:28am UTC

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 3d7b7e44-0bae-48ac-a8c0-0dbc9dc9cb0a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/@raystack/apsara@895

commit: e513c7f

The parts landed in the previous commit with placeholder chrome. This
matches them to reference A, and finishes the UndoIcon that commit left
imported but unmapped.

Single month: the caption moves to the left and the reset joins the two
nav buttons on the right, drawn as the undo glyph. Source order is the
visual order, so nothing reorders in CSS and tab order follows the row.

Several months: there is no single header to hold one caption, so each
month captions itself. `.Days` drops the header above the grid and binds
react-day-picker's MonthCaption slot instead — previous on the first
month, next on the last, a spacer holding the absent button's place so
every caption centres on its own grid. No reset in this layout; the
Calendar Header component in the file has three variants and none of the
two-month ones carries it.

Also from the frames: weekday headings go to three letters, the caption
abbreviates to `Apr 2024`, the scroller lists `Jan`/`Feb`/`Mar`, its
chip and selected row take neutral grey rather than accent, and the
popover anchors to the caption's start edge over the grid.

`showOutsideDays` now defaults to false. Every new frame ends its grid on
the last day of the month with the leading cells blank, and no frame
shows an outside day. This diverges from today's DatePicker, which is
priced in — the rewrite ships no shim.

One frame detail is deliberately not encoded: April 1-16 render muted
with today on the 17th, which a `minDate` demonstration and a built-in
past bound draw identically. Read as a demonstration, so no default
bound is applied — a past bound is not expressible as an opt-out and
would break date-of-birth and "filter since" fields. Flagged for design.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shreyag02 and others added 2 commits September 4, 2026 15:04
The header sat on a different grid from the days beneath it, in three
compounding ways.

There is no global `box-sizing: border-box` here, so the cells were
sizing content-box: a day cell carried its 1px border and the user
agent's 1px cell padding on top of the 40px column and came out 44px,
while a weekday heading came out 42px. Two pixels per column, and by
Saturday the heading stood 13px clear of the days under it. Cells now
size to the border box with the agent's padding dropped, so both are the
same 40px column and every column measures zero drift.

The table's default 2px border-spacing ringed the grid, leaving the
header wider than the columns it captions. It is now zero.

Aligning the header to the column box was still optically wrong, because
a weekday label is centred inside its cell rather than flush to it: the
caption read as sitting left of "Sun". Both headers take 8px of inline
padding, which is the bearing that label leaves. A week-number column is
a gutter rather than a date column, so the single-month header adds its
width on top — asked of the rendered grid through `:has()`, since
`showWeekNumber` is a `.Grid` prop and the header is its sibling.

Also, against the standing rules: the two spacer spans in the two-month
header existed only to carry a class, so the header is three fixed grid
tracks and the empty one reserves itself. `monthNames()` went dead when
the caption scroller moved to abbreviations, and is deleted with its
test. Comments across the module are cut back to the constraints that
are not obvious from the code — file-header essays and JSDoc restating a
signature are gone, and `date-adapter.ts` loses 89 lines without losing
a fact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers what this PR ships: composition and children overriding a part's
computed content, `.Reset` keyed off `defaultDate`, selection bounds that
never clamp navigation, grid layout with outside days off by default,
`dateInfo` and `tooltipMessages` as functions, and the caption's own
month and year scroller. Slot and cell-state tables are included, since
both are semver-covered.

`useCalendar()` is documented as a fenced example rather than a live one:
`noInline` is not set on the docs' react-live provider, so a demo that
declares a component would render a runtime error on the page.

The footer demos wrap their two parts in a column. The root renders no
element of its own, so `.Days` and `.Footer` stack on normal block flow
but sit side by side inside the flex row the preview centres with — the
`.Footer` section says so.

Also corrects the icon count in the demo scope comment, which the undo
glyph made stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The root was a bare provider, so `.Days` and `.Footer` were in-flow
siblings with no box of their own and inherited whatever the surrounding
layout did — side by side inside a flex row, which is what the docs
preview centres with. It now renders a column that hugs its content, so
the documented composition stacks anywhere and needs no wrapper.

`data-slot="calendar-preview"`, plus `render`, `className` and `ref` and
`...props` last, which is what every other part already takes. The exact
slot-set assertion filtered on the `calendar-preview-` prefix and so
could never have seen the root's own slot; it filters on the bare name
now.

`CalendarPreviewProps` extends the div props with `defaultValue` omitted:
`HTMLAttributes` declares it as a form value, which is not what it means
here.

This is orthogonal to the scale contract in the RFC review — the value
shape, `trailingValue`, the draft and the conversion rule are all state,
and none of them says anything about what the root renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant