fix(02): wire calendar-controls plugin and Zustand selectors

Bug A — navigation no-op: replace $app.calendarState private-API poking
with the official @schedule-x/calendar-controls plugin. CalendarShell
creates the plugin once via useState stable initialiser and passes it to
ViewToolbar as `controls`. ViewToolbar calls controls.setDate(PlainDate)
and controls.setView(id) for all navigation and view-switching. Step size
matches the active view: day→±1 day, week→±1 week, month-*→±1 month.

Bug B — popover-open calendar flash: replace the unselected
useCalendarStore() destructuring in CalendarShell and ViewToolbar with
per-field selectors. Neither component now subscribes to openEventId, so
popover open/close no longer triggers a re-render that rebuilds the
Schedule-X config.

- Add @schedule-x/calendar-controls@4.6.0 dependency
- Update CalendarShell.test.tsx: add vi.mock for calendar-controls
- typecheck, vitest (37/37), build all pass
This commit is contained in:
Lucas Berger
2026-06-05 14:45:27 -04:00
parent 1a24b00de9
commit 5d82f859fd
5 changed files with 100 additions and 54 deletions
+11 -3
View File
@@ -37,6 +37,7 @@ import {
type CalendarType,
} from '@schedule-x/calendar'
import { createEventsServicePlugin } from '@schedule-x/events-service'
import { createCalendarControlsPlugin } from '@schedule-x/calendar-controls'
import { fetchMe, fetchEvents } from '../api/client.js'
import { hydrateEvents } from '../lib/hydrateEvents.js'
@@ -67,7 +68,13 @@ function isPhone(): boolean {
// ── Component ──────────────────────────────────────────────────────────────
export function CalendarShell() {
const { calendarRange, setCalendarRange, setOpenEventId, selectedView } = useCalendarStore()
// Use per-field selectors so CalendarShell does NOT subscribe to openEventId.
// Without selectors, any popover open/close triggers a full re-render here,
// which rebuilds the Schedule-X config and causes a visible calendar flash (Bug B).
const calendarRange = useCalendarStore((s) => s.calendarRange)
const setCalendarRange = useCalendarStore((s) => s.setCalendarRange)
const setOpenEventId = useCalendarStore((s) => s.setOpenEventId)
const selectedView = useCalendarStore((s) => s.selectedView)
const { start, end } = calendarRange
const queryClient = useQueryClient()
@@ -89,6 +96,7 @@ export function CalendarShell() {
// Create plugins once (stable across renders)
const eventsService = useState(() => createEventsServicePlugin())[0]
const calendarControls = useState(() => createCalendarControlsPlugin())[0]
// Build members list from /api/me for AppNav + ColorLegend
const members = useMemo(() => {
@@ -140,7 +148,7 @@ export function CalendarShell() {
},
},
},
[eventsService],
[eventsService, calendarControls],
)
// Sync TanStack Query result into Schedule-X eventsService (Pitfall 4 guard)
@@ -198,7 +206,7 @@ export function CalendarShell() {
}}
>
{/* ViewToolbar */}
<ViewToolbar calendarApp={calendar} />
<ViewToolbar controls={calendarControls} />
{/* Main calendar area */}
<div style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden' }}>