From d240657059531a5eb42b96f01ff46549b0314e31 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Fri, 5 Jun 2026 14:54:40 -0400 Subject: [PATCH] fix(02): remove custom ViewToolbar; use Schedule-X built-in header - Remove render and its import from CalendarShell - Remove createCalendarControlsPlugin import, useState instance, and plugin array entry (calendar-controls only served the custom toolbar) - Delete ViewToolbar.tsx (no longer referenced anywhere) - Remove calendar-controls mock from CalendarShell.test.tsx - CSS audit confirmed no rules hide Schedule-X weekday-name row; no CSS changes needed - All four views (day/week/month-grid/month-agenda) remain; Schedule-X's native header exposes them in its own view switcher --- .../pwa/src/components/CalendarShell.test.tsx | 35 ---- apps/pwa/src/components/CalendarShell.tsx | 14 +- apps/pwa/src/components/ViewToolbar.tsx | 192 ------------------ 3 files changed, 5 insertions(+), 236 deletions(-) delete mode 100644 apps/pwa/src/components/ViewToolbar.tsx diff --git a/apps/pwa/src/components/CalendarShell.test.tsx b/apps/pwa/src/components/CalendarShell.test.tsx index adcd83b..6d10b02 100644 --- a/apps/pwa/src/components/CalendarShell.test.tsx +++ b/apps/pwa/src/components/CalendarShell.test.tsx @@ -56,41 +56,6 @@ vi.mock('@schedule-x/event-modal', () => ({ })), })) -// Mock @schedule-x/calendar-controls so CalendarShell can create the plugin -vi.mock('@schedule-x/calendar-controls', () => ({ - createCalendarControlsPlugin: vi.fn(() => ({ - name: 'calendarControls', - beforeRender: vi.fn(), - onRender: vi.fn(), - setDate: vi.fn(), - setView: vi.fn(), - getDate: vi.fn(() => Temporal.Now.plainDateISO()), - getView: vi.fn(() => 'month-grid'), - setFirstDayOfWeek: vi.fn(), - setLocale: vi.fn(), - setViews: vi.fn(), - setDayBoundaries: vi.fn(), - setWeekOptions: vi.fn(), - setCalendars: vi.fn(), - setMinDate: vi.fn(), - setMaxDate: vi.fn(), - setMonthGridOptions: vi.fn(), - setTimezone: vi.fn(), - setResources: vi.fn(), - getFirstDayOfWeek: vi.fn(), - getLocale: vi.fn(), - getViews: vi.fn(() => []), - getDayBoundaries: vi.fn(), - getWeekOptions: vi.fn(), - getCalendars: vi.fn(() => ({})), - getMinDate: vi.fn(), - getMaxDate: vi.fn(), - getMonthGridOptions: vi.fn(), - getResources: vi.fn(() => []), - getRange: vi.fn(() => null), - })), -})) - // Mock the API client vi.mock('../api/client.js', () => ({ fetchMe: vi.fn(), diff --git a/apps/pwa/src/components/CalendarShell.tsx b/apps/pwa/src/components/CalendarShell.tsx index 9be6484..ac45271 100644 --- a/apps/pwa/src/components/CalendarShell.tsx +++ b/apps/pwa/src/components/CalendarShell.tsx @@ -14,10 +14,12 @@ * - Event popover: driven exclusively by Zustand openEventId via onEventClick → standalone * EventDetailPopover; createEventModalPlugin and customComponents.eventModal are NOT used * - Threat T-02d-01: all event fields are plain-text JSX children — no raw HTML injection + * - Navigation: Schedule-X's built-in header is the sole navigation bar (Today/‹›/view switcher + * + weekday-name row). The custom ViewToolbar and calendar-controls plugin have been removed. * * Layout: - * - Phone (≤767px): AppNav top bar → ViewToolbar → calendar grid (primary focal point) - * - Tablet/Desktop (≥768px): AppNav left sidebar (240px) + main area (ViewToolbar + grid) + * - Phone (≤767px): AppNav top bar → Schedule-X (header + grid) + * - Tablet/Desktop (≥768px): AppNav left sidebar (240px) + main area (Schedule-X header + grid) * * State branches: * isLoading (initial) → SkeletonCalendar @@ -37,7 +39,6 @@ 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' @@ -45,7 +46,6 @@ import { buildCalendarConfig, SX_FIRST_DAY_OF_WEEK } from '../lib/calendarConfig import { useCalendarStore } from '../store/calendarStore.js' import { EventDetailPopover } from './EventDetailPopover.js' import { AppNav } from './AppNav.js' -import { ViewToolbar } from './ViewToolbar.js' import { ColorLegend } from './ColorLegend.js' import { SkeletonCalendar } from './SkeletonCalendar.js' import { EmptyState } from './EmptyState.js' @@ -96,7 +96,6 @@ 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(() => { @@ -148,7 +147,7 @@ export function CalendarShell() { }, }, }, - [eventsService, calendarControls], + [eventsService], ) // Sync TanStack Query result into Schedule-X eventsService (Pitfall 4 guard) @@ -205,9 +204,6 @@ export function CalendarShell() { overflow: 'hidden', }} > - {/* ViewToolbar */} - - {/* Main calendar area */}
{isInitialLoading ? ( diff --git a/apps/pwa/src/components/ViewToolbar.tsx b/apps/pwa/src/components/ViewToolbar.tsx deleted file mode 100644 index b15bbf5..0000000 --- a/apps/pwa/src/components/ViewToolbar.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/** - * ViewToolbar — calendar navigation and view switcher. - * - * UI-SPEC §ViewToolbar: - * - Buttons: Today | < | > | [Day] [Week] [Month] [Agenda] - * - Font: 13px label weight - * - Active view: subtle surface tint (NOT accent) — --color-member-0 at 12% opacity - * - Touch targets: 44px minimum height - * - role="button", keyboard-activatable with Enter/Space - * - * Reviewer note: ViewToolbar is secondary chrome — accent colors NOT on chrome. - * Active state uses a subtle surface tint, not the accent color directly. - * - * Schedule-X integration: navigation and view-switching via the official - * @schedule-x/calendar-controls plugin (createCalendarControlsPlugin). - * The plugin is created once in CalendarShell and passed down as `controls`. - * - * Zustand subscription: uses per-field selectors so ViewToolbar only re-renders - * when selectedView or setSelectedView change — NOT when openEventId changes. - */ - -import { useCalendarStore } from '../store/calendarStore.js' -import type { createCalendarControlsPlugin } from '@schedule-x/calendar-controls' - -type ViewId = 'day' | 'week' | 'month-grid' | 'month-agenda' - -interface ViewConfig { - id: ViewId - label: string -} - -const VIEWS: ViewConfig[] = [ - { id: 'day', label: 'Day' }, - { id: 'week', label: 'Week' }, - { id: 'month-grid', label: 'Month' }, - { id: 'month-agenda', label: 'Agenda' }, -] - -type CalendarControlsPlugin = ReturnType - -interface ViewToolbarProps { - controls: CalendarControlsPlugin | null -} - -export function ViewToolbar({ controls }: ViewToolbarProps) { - // Per-field selectors: do NOT subscribe to openEventId. - // If the whole store is destructured unselected, every popover open/close - // causes a re-render here, which flashes the toolbar (Bug B). - const selectedView = useCalendarStore((s) => s.selectedView) - const setSelectedView = useCalendarStore((s) => s.setSelectedView) - - /** - * Navigate using the calendar-controls plugin API. - * - today: setDate(Temporal.Now.plainDateISO()) - * - prev/next: read current date, step by the active view's unit, call setDate() - * - * Step mapping (matches Schedule-X built-in backward/forward behaviour): - * day → ±1 day - * week → ±1 week - * month-grid → ±1 month - * month-agenda → ±1 month - */ - const navigate = (direction: 'prev' | 'next' | 'today') => { - if (!controls) return - - if (direction === 'today') { - controls.setDate(Temporal.Now.plainDateISO()) - return - } - - // Get the current date from the controls plugin (returns Temporal.PlainDate) - const current = controls.getDate() - - let step: Temporal.DurationLike - if (selectedView === 'day') { - step = { days: 1 } - } else if (selectedView === 'week') { - step = { weeks: 1 } - } else { - // month-grid or month-agenda - step = { months: 1 } - } - - const newDate = direction === 'prev' ? current.subtract(step) : current.add(step) - controls.setDate(newDate) - } - - const switchView = (viewId: ViewId) => { - if (!controls) return - controls.setView(viewId) - setSelectedView(viewId) - } - - const buttonBase: React.CSSProperties = { - background: 'none', - border: '1px solid var(--color-border)', - cursor: 'pointer', - minHeight: '44px', - padding: '0 var(--space-3)', - fontSize: 'var(--text-label-size)', - fontWeight: 'var(--text-label-weight)', - lineHeight: 'var(--text-label-line-height)', - color: 'var(--color-text-primary)', - borderRadius: 'var(--space-1)', - fontFamily: 'var(--font-family-base)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - transition: 'background 0.1s', - } - - const activeButtonStyle: React.CSSProperties = { - ...buttonBase, - // Subtle surface tint — NOT accent color (UI-SPEC 60/30/10 rule) - // Active state: --color-member-0 at 12% opacity over white - background: 'rgba(74, 144, 217, 0.12)', - fontWeight: 600, - border: '1px solid rgba(74, 144, 217, 0.3)', - } - - return ( -
- {/* Today button */} - - - {/* Prev / Next */} - - - - {/* Spacer */} -
- - {/* View switcher */} -
- {VIEWS.map((view) => ( - - ))} -
-
- ) -}