feat(03-05): mount EventForm + add New Event FAB/toolbar trigger in CalendarShell
- Import EventForm and Plus icon from lucide-react - Phone: fixed FAB bottom-right (56px, dark neutral fill per UI-SPEC) - Tablet/desktop: toolbar button above calendar content area - Both trigger setEventForm(true, 'create') via Zustand - EventForm conditionally rendered while eventFormOpen - Selectors pattern preserved to avoid unnecessary re-renders (Bug B guard)
This commit is contained in:
@@ -39,12 +39,14 @@ import {
|
|||||||
type CalendarType,
|
type CalendarType,
|
||||||
} from '@schedule-x/calendar'
|
} from '@schedule-x/calendar'
|
||||||
import { createEventsServicePlugin } from '@schedule-x/events-service'
|
import { createEventsServicePlugin } from '@schedule-x/events-service'
|
||||||
|
import { Plus } from 'lucide-react'
|
||||||
|
|
||||||
import { fetchMe, fetchEvents } from '../api/client.js'
|
import { fetchMe, fetchEvents } from '../api/client.js'
|
||||||
import { hydrateEvents } from '../lib/hydrateEvents.js'
|
import { hydrateEvents } from '../lib/hydrateEvents.js'
|
||||||
import { buildCalendarConfig, SX_FIRST_DAY_OF_WEEK } from '../lib/calendarConfig.js'
|
import { buildCalendarConfig, SX_FIRST_DAY_OF_WEEK } from '../lib/calendarConfig.js'
|
||||||
import { useCalendarStore } from '../store/calendarStore.js'
|
import { useCalendarStore } from '../store/calendarStore.js'
|
||||||
import { EventDetailPopover } from './EventDetailPopover.js'
|
import { EventDetailPopover } from './EventDetailPopover.js'
|
||||||
|
import { EventForm } from './EventForm.js'
|
||||||
import { AppNav } from './AppNav.js'
|
import { AppNav } from './AppNav.js'
|
||||||
import { ColorLegend } from './ColorLegend.js'
|
import { ColorLegend } from './ColorLegend.js'
|
||||||
import { SkeletonCalendar } from './SkeletonCalendar.js'
|
import { SkeletonCalendar } from './SkeletonCalendar.js'
|
||||||
@@ -75,6 +77,8 @@ export function CalendarShell() {
|
|||||||
const setCalendarRange = useCalendarStore((s) => s.setCalendarRange)
|
const setCalendarRange = useCalendarStore((s) => s.setCalendarRange)
|
||||||
const setOpenEventId = useCalendarStore((s) => s.setOpenEventId)
|
const setOpenEventId = useCalendarStore((s) => s.setOpenEventId)
|
||||||
const selectedView = useCalendarStore((s) => s.selectedView)
|
const selectedView = useCalendarStore((s) => s.selectedView)
|
||||||
|
const setEventForm = useCalendarStore((s) => s.setEventForm)
|
||||||
|
const eventFormOpen = useCalendarStore((s) => s.eventFormOpen)
|
||||||
const { start, end } = calendarRange
|
const { start, end } = calendarRange
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
@@ -297,7 +301,7 @@ export function CalendarShell() {
|
|||||||
// ── Full layout ────────────────────────────────────────────────────────────
|
// ── Full layout ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if (phone) {
|
if (phone) {
|
||||||
// Phone: stacked layout — AppNav top bar + content below
|
// Phone: stacked layout — AppNav top bar + content below + FAB
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -318,6 +322,37 @@ export function CalendarShell() {
|
|||||||
<InstallPrompt />
|
<InstallPrompt />
|
||||||
<CalendarContent />
|
<CalendarContent />
|
||||||
<EventDetailPopover />
|
<EventDetailPopover />
|
||||||
|
|
||||||
|
{/* New Event FAB — phone: bottom-right floating action button (UI-SPEC §Interaction Contract) */}
|
||||||
|
<button
|
||||||
|
aria-label="New Event"
|
||||||
|
onClick={() => setEventForm(true, 'create')}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 'var(--space-6)',
|
||||||
|
right: 'var(--space-6)',
|
||||||
|
width: '56px',
|
||||||
|
height: '56px',
|
||||||
|
minWidth: '56px',
|
||||||
|
minHeight: '56px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: 'var(--color-text-primary)',
|
||||||
|
color: '#ffffff',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 4px 16px rgba(0,0,0,0.18)',
|
||||||
|
zIndex: 100,
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={24} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* EventForm modal — conditionally rendered while eventFormOpen */}
|
||||||
|
{eventFormOpen && <EventForm />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -345,11 +380,50 @@ export function CalendarShell() {
|
|||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
<InstallPrompt />
|
<InstallPrompt />
|
||||||
|
|
||||||
|
{/* New Event toolbar button — tablet/desktop (UI-SPEC §Interaction Contract) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
padding: 'var(--space-2) var(--space-4)',
|
||||||
|
borderBottom: '1px solid var(--color-border-subtle)',
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setEventForm(true, 'create')}
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-text-primary)',
|
||||||
|
color: '#ffffff',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
minHeight: '44px',
|
||||||
|
padding: '0 var(--space-4)',
|
||||||
|
borderRadius: 'var(--space-1)',
|
||||||
|
fontSize: 'var(--text-label-size)',
|
||||||
|
fontWeight: 600,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 'var(--space-1)',
|
||||||
|
fontFamily: 'var(--font-family-base)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={16} aria-hidden="true" />
|
||||||
|
{/* Plain text — XSS guard */}
|
||||||
|
New Event
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<CalendarContent />
|
<CalendarContent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* EventDetailPopover — standalone mode driven by Zustand openEventId */}
|
{/* EventDetailPopover — standalone mode driven by Zustand openEventId */}
|
||||||
<EventDetailPopover />
|
<EventDetailPopover />
|
||||||
|
|
||||||
|
{/* EventForm modal — conditionally rendered while eventFormOpen */}
|
||||||
|
{eventFormOpen && <EventForm />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user