ape-calendar
v0.11.0
Published
FullCalendar + react-day-picker based React calendar suite (macro + mini calendar, popovers, hooks). Self-contained with own Radix primitives.
Readme
ape-calendar
FullCalendar + react-day-picker based React calendar suite. Self-contained: ships its own Radix Popover primitive, no external UI library required.
Status: scaffold. Components are migrated package-by-package from
cms-portal. See the monorepo README for progress.
Install
npm install ape-calendar
# peer deps
npm install react react-domImport the stylesheet once at the app root:
import 'ape-calendar/style.css'Usage (planned)
import { MacroCalendar, MiniMonthCalendar, useCalendarState } from 'ape-calendar'
const { state, actions } = useCalendarState()
<MacroCalendar
events={events}
activeView={state.activeView}
selectedDate={state.selectedDate}
locale="es-CO"
onEditEvent={openForm}
onDeleteEvent={confirmDelete}
onEventTimeChange={({ event, startAt, endAt, revert }) =>
updateApi(event.id, { startAt, endAt }).catch(revert)
}
/>Toolbar & view controller
The calendar stays fully controlled (the consumer owns activeView / selectedDate). useCalendarController encapsulates the standard navigation behavior so every portal behaves the same — spread its handlers onto CalendarToolbar and MacroCalendar:
- Hoy → today in day view (re-pressing it falls back to week).
- Picking a day from month or list → that day's week.
- prev/next step by the current view;
periodLabelis localized for you.
import {
CalendarToolbar,
MacroCalendar,
StatusFilterSelect,
EventTypeChip,
Select,
useCalendarController,
} from 'ape-calendar'
const cal = useCalendarController({ initialView: 'week', firstDay: 1, locale: 'es-CO' })
<CalendarToolbar
activeView={cal.activeView}
onChangeView={cal.onChangeView}
onNavigate={cal.onNavigate}
todayActive={cal.todayActive}
periodLabel={cal.periodLabel}
views={['month', 'week', 'list']} // month ▦ · calendar(day/week) ▤ · list ☰
filtersSlot={
<>
<StatusFilterSelect placeholder="Estado del evento" options={statusOptions} selected={selectedStatuses} onToggle={toggleStatus} />
{selectedTypes.map(t => <EventTypeChip key={t} label={labels[t]} onRemove={() => clearType(t)} />)}
<Select placeholder="Regional" options={regionalOptions} value={regional} onChange={setRegional} />
</>
}
/>
<MacroCalendar
events={events}
activeView={cal.activeView}
selectedDate={cal.selectedDate}
onDateSelected={cal.onSelectDate}
onSwitchToDay={cal.onSelectDate}
onVisibleRangeChange={cal.onVisibleRangeChange}
/* …event callbacks (onEditEvent, onDeleteEvent, onEventTimeChange) */
/>The day view has no dedicated switcher button — it is reached via Hoy. The filters (status, type chips, regional) are composed by the consumer into filtersSlot; the library provides the building blocks (Select, StatusFilterSelect, EventTypeChip) but never owns the filtering.
Theming (CSS variables)
All colors and a few layout tokens are driven by --cal-* CSS variables declared on
:root. Override them on any parent scope (e.g. .my-app, :root, or the element you
wrap the calendar in) to retheme — no Tailwind config or build step required.
Status — base colors
| Variable | Default |
| ------------------------- | --------- |
| --cal-status-default | #cbd5e1 |
| --cal-status-draft | #D4D4D4 |
| --cal-status-published | #39A900 |
| --cal-status-scheduled | #005DCA |
| --cal-status-archived | #D4D4D4 |
| --cal-status-cancelled | #8C1712 |
| --cal-status-finalizado | #9747FC |
| --cal-status-selected | #005DCA |
Status — chip / block surfaces + text
Each status exposes bg, bg-hover, and text (e.g. --cal-status-draft-bg).
| Status | -bg | -bg-hover | -text |
| ------------ | --------- | ----------- | --------- |
| draft | #fafafa | #f4f4f5 | #52525b |
| published | #ecfdf3 | #d1fadf | #1d6b00 |
| scheduled | #e8f1fd | #cfe0fb | #003a82 |
| archived | #fafafa | #f4f4f5 | #52525b |
| cancelled | #fdecec | #fbd2d2 | #5c0f0c |
| finalizado | #f4ebff | #e6d3ff | #4b1a8a |
Status — badge (pill) surfaces + text
EventStatusBadge uses its own per-status trio (bg, text, border) so pills can be
themed independently from grid chips. Note that by default scheduled renders amber
as a pill but blue as a chip — override deliberately if you want them to match.
| Status | -bg | -text | -border |
| ------------ | --------- | --------- | --------- |
| draft | #f1f5f9 | #334155 | #e2e8f0 |
| published | #f0fdf4 | #15803d | #bbf7d0 |
| scheduled | #fffbeb | #b45309 | #fde68a |
| archived | #fafafa | #52525b | #e4e4e7 |
| cancelled | #fdecec | #5c0f0c | #f5c5c3 |
| finalizado | #f4ebff | #4b1a8a | #d9bcff |
Surfaces, text & UI
| Variable | Default |
| ------------------------------ | ------------------------ |
| --cal-surface | #ffffff |
| --cal-surface-subtle | #f8fafc |
| --cal-mini-surface | #f1f5f9 |
| --cal-macro-surface | var(--cal-surface) |
| --cal-border | #e2e8f0 |
| --cal-border-subtle | #f1f5f9 |
| --cal-text | #0f172a |
| --cal-text-strong | #1e293b |
| --cal-text-secondary | #334155 |
| --cal-text-muted | #64748b |
| --cal-text-faint | #94a3b8 |
| --cal-primary | #1d4ed8 |
| --cal-primary-hover | #1e40af |
| --cal-danger | #dc2626 |
| --cal-danger-bg | #fef2f2 |
| --cal-focus-ring | #93c5fd |
| --cal-hover-subtle | #f1f5f9 |
| --cal-hover-muted | #e2e8f0 |
| --cal-primary-subtle-bg | #eff6ff |
| --cal-icon-btn-hover | rgb(255 255 255 / 0.7) |
| --cal-type-chip-remove-hover | rgb(0 0 0 / 0.08) |
| --cal-toolbar-tabs-bg | rgb(248 250 252 / 0.9) |
| --cal-outside-text | #cbd5e1 |
| --cal-today-text | #0369a1 |
| --cal-empty-border | #d1d5db |
| --cal-accent-subtle-bg | #dbeafe |
| --cal-more-link-text | rgb(71 85 105) |
| --cal-more-link-text-hover | rgb(15 23 42) |
Mini calendar — flat variant
| Variable | Default |
| --------------------------- | -------------------- |
| --cal-mini-flat-surface | var(--cal-surface) |
| --cal-mini-outside-text | #aac4e4 |
| --cal-mini-week-band | #e8f1fd |
| --cal-mini-week-band-text | var(--cal-primary) |
Dark mode
The library ships no .dark selectors or media queries — it stays theme-agnostic and
the consumer owns the theme by overriding CSS variables. Because every visual surface is
token-driven, dark mode is one override block scoped to the root class
(ape-calendar, exported as APE_CALENDAR_ROOT_CLASS). Add the class to an ancestor of
the calendar and to <body> so portaled popovers inherit it too (components portal
through containers that already carry the class):
.dark .ape-calendar {
--cal-surface: var(--color-card);
--cal-macro-surface: var(--color-card);
--cal-mini-surface: var(--color-card-hover);
--cal-border: var(--color-edge);
--cal-fc-border: var(--color-edge);
--cal-text: var(--color-content-primary);
--cal-text-strong: var(--color-content-primary);
--cal-text-secondary: var(--cal-text-muted);
--cal-fc-text: var(--color-content-secondary);
}Tips learned from production usage (CMS Portal):
- Map accents to your app's semantic tokens instead of raw hexes so both themes stay in sync.
- Keep status colors untouched: chips/pills carry their own light surface + dark text
on purpose (domain semantics). If you want darker status surfaces in dark mode, override
the per-status
-bg/-bg-hover/-texttokens. - In week/day views, event block titles use
--cal-status-*-textover a nearly transparent background. On dark surfaces those default dark texts lose contrast — lighten them via the same per-status-texttokens if you use time-grid views in dark mode. - Deepen shadows (
--cal-fc-shadow) slightly; black-alpha shadows otherwise work on both themes.
FullCalendar bridge tokens
Scoped under .cal-macro-root .fc so they never leak to other FullCalendar instances.
| Variable | Default |
| -------------------------- | --------- |
| --cal-fc-border | #e6effa |
| --cal-fc-slot-height | 52px |
| --cal-fc-text | #334155 |
| --cal-fc-font-size | 0.75rem |
| --cal-fc-small-font-size | 0.7rem |
Optional capabilities
The library is flexible like FullCalendar — every visual capability is opt-in via a prop or slot. Omit it and nothing extra renders or costs.
Availability band (availability)
MacroCalendar accepts an optional availability: AvailabilitySchedule ({ daysOfWeek: number[]; startTime: 'HH:mm'; endTime: 'HH:mm' }[]). When provided, it paints a subtle colored "horario de atención" band in the time-grid views (week/day), via recurring background events. When omitted, no band renders. Theme it through --cal-availability-bg.
const availability = [
{ daysOfWeek: [1, 2, 3, 4, 5], startTime: '08:00', endTime: '17:00' },
]
<MacroCalendar availability={availability} ... />The configuration UI for this schedule lives in ape-calendar-form as AvailabilityScheduleDialog — also opt-in.
Agenda day header slot (renderAgendaDayHeader)
The list view (activeView === 'list', rendered by EventAgendaList) exposes a per-day header slot. Use it to surface availability pills ("Horario de atención" / "No tenemos atención") or any other per-day annotation. The library does not derive this from availability — your portal decides what to show.
<MacroCalendar
availability={availability}
renderAgendaDayHeader={(date, { hasEvents }) => {
const slot = availability.find(s => s.daysOfWeek.includes(date.getDay()))
return slot
? <span>Horario de atención · {slot.startTime}–{slot.endTime}</span>
: <span>No tenemos atención</span>
}}
...
/>EventAgendaList is also exported standalone for portals that want to compose the agenda in a custom layout.
Drag granularity (slotDuration / snapDuration)
Both props are optional, default '00:30:00' and '00:15:00'. Override per portal needs (e.g. snapDuration="00:05:00" for tighter resize, or '01:00:00' for hour-only steps).
Messages
User-facing strings are passed via the messages prop. Some fields are plain strings,
others are callbacks — the table below documents the exact shape per component.
MacroCalendar — MacroCalendarMessages
| Field | Type | Default |
| ----------------- | -------------------------------------------------------------------------------- | --------------- |
| moreLink | string \| ((count: number) => string) — the string form interpolates {count} | +{count} more |
| allDayText | string | 'All day' |
| todayHeaderAria | string | 'Today' |
// both are valid:
<MacroCalendar messages={{ moreLink: '+{count} más' }} />
<MacroCalendar messages={{ moreLink: count => `+${count} más` }} />MoreEventsPopover — MoreEventsPopoverMessages
All fields are callbacks.
| Field | Type |
| ------------ | --------------------------- |
| header | (count: number) => string |
| editAria | (title: string) => string |
| deleteAria | (title: string) => string |
Companion package
For event creation/edit forms, install ape-calendar-form.
