Skip to content
ilinxa/pro-ui

Event Calendar

alphav0.4.0

Editable event calendar with month, week, day, and agenda views — multi-day spans, drag and resize editing, clipboard support, and keyboard navigation.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-06-22Author: ilinxa

Context

Event Calendar is the fifth surface onto the canonical TaskItem the rest of the task family renders: task-card (list), task-tree (outline), kanban-board (board), gantt-timeline (continuous timeline), and now event-calendar (date grid). A product's 'Calendar' tab is literally the same task data its other tabs show, with no adapter. v0.2 adds the editing layer additively over v0.1's display surface, reusing gantt's controlled-echo vocabulary + the shared TaskPermissions matrix; editable defaults off so consumers opt in. Because every task surface speaks the same TaskItem, copy/paste rides a shared 'ilinxa/task' clipboard envelope — a task copied in the calendar pastes into gantt/kanban/tree and back. Compound structure: EventCalendarRoot (headless provider) + flat parts (Toolbar / MonthView / WeekView / DayView / AgendaView / MiniNav / Inspector / QuickComposer / ContextMenu / edit overlays) + Tier-C primitives (CalendarEventChip / CalendarEventBar / CalendarTimeBlock / MonthDayCell / TimeGrid / TimeGutter / NowIndicator / AgendaRow / EventTooltip / EventEditorPanel / CalendarSkeleton) + the EventCalendar assembly. Each view is its own module so a month-only consumer never pulls the week/day time-grid code; the full-card tooltip + detail editor lazy-load task-card, so the default lightweight tooltip keeps it out of the bundle.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/event-calendar

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/event-calendar-fixtures
Optional feature slicesInstall on top of the base for opt-in heavier capability.
pnpm dlx shadcn@latest add @ilinxa/event-calendar-editing

Drag/resize editing, quick-compose, clipboard, keyboard mutations, and permission-gated actions.

Preview

Switch views · M/W/D/A · ←/→ navigate · click an event
August 2026
Mon
Tue
Wed
Thu
Fri
Sat
Sun
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6

Demo source

demo.tsxtsx

Usage

When to use

Reach for EventCalendar when you already hold the canonical TaskItem[] (the data behind task-card, task-tree, kanban-board, and gantt-timeline) and want a date-grid surface — month, week, day, or agenda — with no adapter. It is the read-only display sibling of the gantt; editing lands in v0.2.

Basic example

import { EventCalendar } from "@/components/event-calendar"

export function Example({ tasks }) {
  return (
    <EventCalendar
      data={tasks}                 // TaskItem[]
      statusOptions={statusOptions}
      defaultView="month"          // "month" | "week" | "day" | "agenda"
      now={serverNow}              // SSR-stable "now"
      showMiniNav
      onTaskClick={(item) => openDetail(item)}
      onRangeChange={({ start, end }) => fetchWindow(start, end)}
    />
  )
}

Editing (opt-in feature)

Editing (drag/resize/create, clipboard, keyboard mutations, permission-gated actions) ships as a separate feature slice — installing the base package alone keeps it out of your bundle entirely. Install @ilinxa/event-calendar-editing, import calendarEditing, and pass it alongside editable.

import { EventCalendar } from "@/components/event-calendar"
import { calendarEditing } from "@/components/event-calendar/features/editing"

<EventCalendar
  editable
  editing={calendarEditing}   // wires drag/resize/create/clipboard/keyboard
  data={tasks}
  onChange={setTasks}         // controlled — echo the mutated forest back
  statusOptions={statusOptions}
  permissions={permissions}   // optional — the shared TaskPermissions matrix
/>

editable without editing wired stays fully read-only (byte-identical to a base-only install) — the calendar logs one console.warn in development and never throws.

Lighter (hand-assembled subset)

import {
  EventCalendarRoot, CalendarToolbar, CalendarMonthView,
} from "@/components/event-calendar"

// month-only — the week/day time-grid code never enters your bundle
<EventCalendarRoot data={tasks} statusOptions={statusOptions} views={["month"]}>
  <CalendarToolbar />
  <CalendarMonthView />
</EventCalendarRoot>

All-day vs timed

  • A classifyEvent(item) predicate wins when it returns a kind.
  • Otherwise a date-only string ("2026-06-22", no T) is an all-day event (parsed floating-local — no timezone off-by-one); a full timestamp is timed.
  • With no end and a full timestamp, the item is a milestone (a marker / dot).

Notes

  • Fully controlled — no internal data state. Pass now for an SSR-stable first paint.
  • Keyboard: M/W/D/A switch views, ←/→ + PageUp/PageDown step the period, T jumps to today.
  • The default hover tooltip is a native title; pass renderTooltip (e.g. CalendarFullCardTooltip) for a rich card.

Features

  • Four views: Month (date cells with multi-day spanning bars + chips + '+N more' overflow), Week + Day (hour time-grids: all-day band + lane-packed timed blocks + now-line), Agenda (day-grouped chronological list)
  • Consumes the canonical TaskItem[] directly — same data as task-card / task-tree / kanban-board / gantt-timeline; no adapter
  • Opt-in editing (editable, default off → read-only): drag-to-reschedule + edge-resize + draw/double-click create + quick-composer + right-click menu (Edit/Rename/Status/Priority/Copy/Cut/Delete) + selected-event inspector + modal detail editor + inline rename; controlled-echo events + onChange, no internal data state, gated by the shared TaskPermissions matrix
  • Full keyboard editing: M/W/D/A switch views, ←/→ step the period, T today; focus an event → ←/→ move, Shift+←/→ (+↑/↓ in the time grid) resize, Enter edit, F2 rename, Delete remove; focus a day → Enter quick-create
  • Cross-surface copy / cut / paste: events copy as a portable TaskItem envelope through the OS clipboard (⌘/Ctrl+C·X·V), so a task copied here pastes into gantt / kanban / tree — paste-target decides all-day vs timed
  • All-day / timed / milestone derived via a three-layer rule: consumer classifyEvent predicate → date-only strings (parsed as floating-local, no TZ off-by-one) → span heuristic; all-day⇄timed conversion via paste-target or drag onto the all-day band
  • Status-driven event color (statusColors + colorBy, default 'status'; colorBy='urgency' restores the v0.1 deadline ramp) imported from task-card; per-item borderColor override; high-priority Flag; overdue + inactive treatments
  • Cursor (view + focus date) controlled OR uncontrolled; period nav, view switch, optional jump-to-date mini-nav (shadcn calendar), onRangeChange for lazy windowed fetch; height-responsive month overflow (maxEventsPerCell overrides)
  • SSR-safe first paint (now prop seeds; client interval refreshes); finite-date guards (unparseable dates render label-only, never throw); all-day floating-local round-trip (no off-by-one)
  • Compound: EventCalendarRoot + flat view parts + edit overlays + Tier-C primitives + the EventCalendar assembly; each view its own module (tree-shakeable); a month-only subset drops the time-grid code; the detail editor lazy-loads task-card

Tags

event-calendarcalendarmonthweekdayagendascheduletodo

Dependencies

shadcn primitives: avatar, badge, button, calendar, context-menu, input, popover, skeleton
npm peer deps: date-fns@^4.1.0, lucide-react@^1.11.0, @dnd-kit/core@^6.3.1, @dnd-kit/utilities@^3.2.2
internal: task-card