From 230e14782b1d3841ce76c17a046d16d1e69aa867 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:42:23 +0000 Subject: [PATCH 1/5] Initial plan From cb78eb17c230406aa74e8488bf284d45d1d5dd2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:52:43 +0000 Subject: [PATCH 2/5] Add Airtable-style calendar view component - Created CalendarView UI component with month/week/day views - Created calendar-view renderer for ComponentRegistry - Added support for data field mapping (title, dates, colors) - Integrated with existing component system Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- .../src/renderers/complex/calendar-view.tsx | 220 ++++++++ .../components/src/renderers/complex/index.ts | 1 + packages/components/src/ui/calendar-view.tsx | 491 ++++++++++++++++++ packages/components/src/ui/index.ts | 1 + 4 files changed, 713 insertions(+) create mode 100644 packages/components/src/renderers/complex/calendar-view.tsx create mode 100644 packages/components/src/ui/calendar-view.tsx diff --git a/packages/components/src/renderers/complex/calendar-view.tsx b/packages/components/src/renderers/complex/calendar-view.tsx new file mode 100644 index 000000000..5a5540332 --- /dev/null +++ b/packages/components/src/renderers/complex/calendar-view.tsx @@ -0,0 +1,220 @@ +import { ComponentRegistry } from '@object-ui/core'; +import { CalendarView, type CalendarEvent } from '@/ui'; +import React from 'react'; + +// Calendar View Renderer - Airtable-style calendar for displaying records as events +ComponentRegistry.register('calendar-view', + ({ schema, className, onAction, ...props }) => { + // Transform schema data to CalendarEvent format + const events: CalendarEvent[] = React.useMemo(() => { + if (!schema.data || !Array.isArray(schema.data)) return []; + + return schema.data.map((record: any, index: number) => { + // Get field values based on field mappings + const titleField = schema.titleField || 'title'; + const startField = schema.startDateField || 'start'; + const endField = schema.endDateField || 'end'; + const colorField = schema.colorField || 'color'; + const allDayField = schema.allDayField || 'allDay'; + + const title = record[titleField] || 'Untitled'; + const start = record[startField] ? new Date(record[startField]) : new Date(); + const end = record[endField] ? new Date(record[endField]) : undefined; + const allDay = record[allDayField] !== undefined ? record[allDayField] : false; + + // Handle color mapping + let color = record[colorField]; + if (color && schema.colorMapping && schema.colorMapping[color]) { + color = schema.colorMapping[color]; + } + + return { + id: record.id || record._id || index, + title, + start, + end, + allDay, + color, + data: record, + }; + }); + }, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField, schema.colorMapping]); + + const handleEventClick = React.useCallback((event: CalendarEvent) => { + if (onAction) { + onAction({ + type: 'event_click', + payload: { event: event.data, eventId: event.id } + }); + } + if (schema.onEventClick) { + schema.onEventClick(event.data); + } + }, [onAction, schema]); + + const handleDateClick = React.useCallback((date: Date) => { + if (onAction) { + onAction({ + type: 'date_click', + payload: { date } + }); + } + if (schema.onDateClick) { + schema.onDateClick(date); + } + }, [onAction, schema]); + + const handleViewChange = React.useCallback((view: "month" | "week" | "day") => { + if (onAction) { + onAction({ + type: 'view_change', + payload: { view } + }); + } + if (schema.onViewChange) { + schema.onViewChange(view); + } + }, [onAction, schema]); + + const handleNavigate = React.useCallback((date: Date) => { + if (onAction) { + onAction({ + type: 'navigate', + payload: { date } + }); + } + if (schema.onNavigate) { + schema.onNavigate(date); + } + }, [onAction, schema]); + + return ( + + ); + }, + { + label: 'Calendar View', + inputs: [ + { + name: 'data', + type: 'array', + label: 'Data', + description: 'Array of record objects to display as events' + }, + { + name: 'titleField', + type: 'string', + label: 'Title Field', + defaultValue: 'title', + description: 'Field name to use for event title' + }, + { + name: 'startDateField', + type: 'string', + label: 'Start Date Field', + defaultValue: 'start', + description: 'Field name for event start date' + }, + { + name: 'endDateField', + type: 'string', + label: 'End Date Field', + defaultValue: 'end', + description: 'Field name for event end date (optional)' + }, + { + name: 'allDayField', + type: 'string', + label: 'All Day Field', + defaultValue: 'allDay', + description: 'Field name for all-day flag' + }, + { + name: 'colorField', + type: 'string', + label: 'Color Field', + defaultValue: 'color', + description: 'Field name for event color' + }, + { + name: 'colorMapping', + type: 'object', + label: 'Color Mapping', + description: 'Map field values to colors (e.g., {meeting: "blue", deadline: "red"})' + }, + { + name: 'view', + type: 'enum', + enum: ['month', 'week', 'day'], + defaultValue: 'month', + label: 'Default View' + }, + { + name: 'defaultView', + type: 'enum', + enum: ['month', 'week', 'day'], + defaultValue: 'month', + label: 'Default View (alias)' + }, + { + name: 'currentDate', + type: 'string', + label: 'Current Date', + description: 'ISO date string for initial calendar date' + }, + { + name: 'allowCreate', + type: 'boolean', + label: 'Allow Create', + defaultValue: false, + description: 'Allow creating events by clicking on dates' + }, + { name: 'className', type: 'string', label: 'CSS Class' } + ], + defaultProps: { + view: 'month', + titleField: 'title', + startDateField: 'start', + endDateField: 'end', + allDayField: 'allDay', + colorField: 'color', + allowCreate: false, + data: [ + { + id: 1, + title: 'Team Meeting', + start: new Date(new Date().setHours(10, 0, 0, 0)).toISOString(), + end: new Date(new Date().setHours(11, 0, 0, 0)).toISOString(), + color: '#3b82f6', + allDay: false + }, + { + id: 2, + title: 'Project Deadline', + start: new Date(new Date().setDate(new Date().getDate() + 3)).toISOString(), + color: '#ef4444', + allDay: true + }, + { + id: 3, + title: 'Conference', + start: new Date(new Date().setDate(new Date().getDate() + 7)).toISOString(), + end: new Date(new Date().setDate(new Date().getDate() + 9)).toISOString(), + color: '#10b981', + allDay: true + } + ], + className: 'h-[600px] border rounded-lg' + } + } +); diff --git a/packages/components/src/renderers/complex/index.ts b/packages/components/src/renderers/complex/index.ts index 4957d296e..cde0084b2 100644 --- a/packages/components/src/renderers/complex/index.ts +++ b/packages/components/src/renderers/complex/index.ts @@ -2,3 +2,4 @@ import './carousel'; import './scroll-area'; import './resizable'; import './table'; +import './calendar-view'; diff --git a/packages/components/src/ui/calendar-view.tsx b/packages/components/src/ui/calendar-view.tsx new file mode 100644 index 000000000..4ef55b645 --- /dev/null +++ b/packages/components/src/ui/calendar-view.tsx @@ -0,0 +1,491 @@ +"use client" + +import * as React from "react" +import { ChevronLeftIcon, ChevronRightIcon, CalendarIcon } from "lucide-react" +import { cn } from "@/lib/utils" +import { Button } from "@/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/ui/select" + +export interface CalendarEvent { + id: string | number + title: string + start: Date + end?: Date + allDay?: boolean + color?: string + data?: any +} + +export interface CalendarViewProps { + events?: CalendarEvent[] + view?: "month" | "week" | "day" + currentDate?: Date + onEventClick?: (event: CalendarEvent) => void + onDateClick?: (date: Date) => void + onViewChange?: (view: "month" | "week" | "day") => void + onNavigate?: (date: Date) => void + className?: string +} + +function CalendarView({ + events = [], + view = "month", + currentDate = new Date(), + onEventClick, + onDateClick, + onViewChange, + onNavigate, + className, +}: CalendarViewProps) { + const [selectedView, setSelectedView] = React.useState(view) + const [selectedDate, setSelectedDate] = React.useState(currentDate) + + const handlePrevious = () => { + const newDate = new Date(selectedDate) + if (selectedView === "month") { + newDate.setMonth(newDate.getMonth() - 1) + } else if (selectedView === "week") { + newDate.setDate(newDate.getDate() - 7) + } else { + newDate.setDate(newDate.getDate() - 1) + } + setSelectedDate(newDate) + onNavigate?.(newDate) + } + + const handleNext = () => { + const newDate = new Date(selectedDate) + if (selectedView === "month") { + newDate.setMonth(newDate.getMonth() + 1) + } else if (selectedView === "week") { + newDate.setDate(newDate.getDate() + 7) + } else { + newDate.setDate(newDate.getDate() + 1) + } + setSelectedDate(newDate) + onNavigate?.(newDate) + } + + const handleToday = () => { + const today = new Date() + setSelectedDate(today) + onNavigate?.(today) + } + + const handleViewChange = (newView: "month" | "week" | "day") => { + setSelectedView(newView) + onViewChange?.(newView) + } + + const getDateLabel = () => { + if (selectedView === "month") { + return selectedDate.toLocaleDateString("default", { + month: "long", + year: "numeric", + }) + } else if (selectedView === "week") { + const weekStart = getWeekStart(selectedDate) + const weekEnd = new Date(weekStart) + weekEnd.setDate(weekEnd.getDate() + 6) + return `${weekStart.toLocaleDateString("default", { + month: "short", + day: "numeric", + })} - ${weekEnd.toLocaleDateString("default", { + month: "short", + day: "numeric", + year: "numeric", + })}` + } else { + return selectedDate.toLocaleDateString("default", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }) + } + } + + return ( +
+ {/* Header */} +
+
+ +
+ + +
+

{getDateLabel()}

+
+
+ +
+
+ + {/* Calendar Grid */} +
+ {selectedView === "month" && ( + + )} + {selectedView === "week" && ( + + )} + {selectedView === "day" && ( + + )} +
+
+ ) +} + +function getWeekStart(date: Date): Date { + const d = new Date(date) + const day = d.getDay() + const diff = d.getDate() - day + return new Date(d.setDate(diff)) +} + +function getMonthDays(date: Date): Date[] { + const year = date.getFullYear() + const month = date.getMonth() + const firstDay = new Date(year, month, 1) + const lastDay = new Date(year, month + 1, 0) + const startDay = firstDay.getDay() + const days: Date[] = [] + + // Add previous month days + for (let i = startDay - 1; i >= 0; i--) { + const prevDate = new Date(firstDay) + prevDate.setDate(prevDate.getDate() - (i + 1)) + days.push(prevDate) + } + + // Add current month days + for (let i = 1; i <= lastDay.getDate(); i++) { + days.push(new Date(year, month, i)) + } + + // Add next month days + const remainingDays = 42 - days.length + for (let i = 1; i <= remainingDays; i++) { + const nextDate = new Date(lastDay) + nextDate.setDate(nextDate.getDate() + i) + days.push(nextDate) + } + + return days +} + +function isSameDay(date1: Date, date2: Date): boolean { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ) +} + +function getEventsForDate(date: Date, events: CalendarEvent[]): CalendarEvent[] { + return events.filter((event) => { + const eventStart = new Date(event.start) + const eventEnd = event.end ? new Date(event.end) : eventStart + + return date >= new Date(eventStart.setHours(0, 0, 0, 0)) && + date <= new Date(eventEnd.setHours(23, 59, 59, 999)) + }) +} + +interface MonthViewProps { + date: Date + events: CalendarEvent[] + onEventClick?: (event: CalendarEvent) => void + onDateClick?: (date: Date) => void +} + +function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps) { + const days = getMonthDays(date) + const today = new Date() + const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + + return ( +
+ {/* Week day headers */} +
+ {weekDays.map((day) => ( +
+ {day} +
+ ))} +
+ + {/* Calendar days */} +
+ {days.map((day, index) => { + const dayEvents = getEventsForDate(day, events) + const isCurrentMonth = day.getMonth() === date.getMonth() + const isToday = isSameDay(day, today) + + return ( +
onDateClick?.(day)} + > +
+ {day.getDate()} +
+
+ {dayEvents.slice(0, 3).map((event) => ( +
{ + e.stopPropagation() + onEventClick?.(event) + }} + > + {event.title} +
+ ))} + {dayEvents.length > 3 && ( +
+ +{dayEvents.length - 3} more +
+ )} +
+
+ ) + })} +
+
+ ) +} + +interface WeekViewProps { + date: Date + events: CalendarEvent[] + onEventClick?: (event: CalendarEvent) => void + onDateClick?: (date: Date) => void +} + +function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) { + const weekStart = getWeekStart(date) + const weekDays = Array.from({ length: 7 }, (_, i) => { + const day = new Date(weekStart) + day.setDate(day.getDate() + i) + return day + }) + const today = new Date() + + return ( +
+ {/* Week day headers */} +
+ {weekDays.map((day) => { + const isToday = isSameDay(day, today) + return ( +
+
+ {day.toLocaleDateString("default", { weekday: "short" })} +
+
+ {day.getDate()} +
+
+ ) + })} +
+ + {/* Week events */} +
+ {weekDays.map((day) => { + const dayEvents = getEventsForDate(day, events) + return ( +
onDateClick?.(day)} + > +
+ {dayEvents.map((event) => ( +
{ + e.stopPropagation() + onEventClick?.(event) + }} + > +
{event.title}
+ {!event.allDay && ( +
+ {event.start.toLocaleTimeString("default", { + hour: "numeric", + minute: "2-digit", + })} +
+ )} +
+ ))} +
+
+ ) + })} +
+
+ ) +} + +interface DayViewProps { + date: Date + events: CalendarEvent[] + onEventClick?: (event: CalendarEvent) => void +} + +function DayView({ date, events, onEventClick }: DayViewProps) { + const dayEvents = getEventsForDate(date, events) + const hours = Array.from({ length: 24 }, (_, i) => i) + + return ( +
+
+ {hours.map((hour) => { + const hourEvents = dayEvents.filter((event) => { + if (event.allDay) return hour === 0 + const eventHour = event.start.getHours() + return eventHour === hour + }) + + return ( +
+
+ {hour === 0 + ? "12 AM" + : hour < 12 + ? `${hour} AM` + : hour === 12 + ? "12 PM" + : `${hour - 12} PM`} +
+
+ {hourEvents.map((event) => ( +
onEventClick?.(event)} + > +
{event.title}
+ {!event.allDay && ( +
+ {event.start.toLocaleTimeString("default", { + hour: "numeric", + minute: "2-digit", + })} + {event.end && + ` - ${event.end.toLocaleTimeString("default", { + hour: "numeric", + minute: "2-digit", + })}`} +
+ )} +
+ ))} +
+
+ ) + })} +
+
+ ) +} + +export { CalendarView } diff --git a/packages/components/src/ui/index.ts b/packages/components/src/ui/index.ts index 7d71e7829..337b8fd45 100644 --- a/packages/components/src/ui/index.ts +++ b/packages/components/src/ui/index.ts @@ -8,6 +8,7 @@ export * from './breadcrumb'; export * from './button-group'; export * from './button'; export * from './calendar'; +export * from './calendar-view'; export * from './card'; export * from './carousel'; export * from './chart'; From 96fbc8e540ad2523006cb06d71642f7fe67e968f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:56:32 +0000 Subject: [PATCH 3/5] Add playground example and documentation for calendar-view - Added calendar-view example to playground - Updated component library documentation - Created comprehensive calendar-view component documentation - Tested all three view modes (month, week, day) Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- apps/playground/src/data/examples.ts | 90 +++++++++++++++++++- docs/components/calendar-view.md | 121 +++++++++++++++++++++++++++ docs/spec/component-library.md | 1 + 3 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 docs/components/calendar-view.md diff --git a/apps/playground/src/data/examples.ts b/apps/playground/src/data/examples.ts index 1af817fab..9f2e04ff2 100644 --- a/apps/playground/src/data/examples.ts +++ b/apps/playground/src/data/examples.ts @@ -638,6 +638,93 @@ export const examples = { ] } ] +}`, + + // Calendar View - Airtable-style calendar + 'calendar-view': `{ + "type": "div", + "className": "space-y-4", + "body": [ + { + "type": "div", + "className": "space-y-2", + "body": [ + { + "type": "text", + "content": "Calendar View", + "className": "text-2xl font-bold" + }, + { + "type": "text", + "content": "Airtable-style calendar for displaying records as events", + "className": "text-muted-foreground" + } + ] + }, + { + "type": "calendar-view", + "className": "h-[600px] border rounded-lg", + "view": "month", + "titleField": "title", + "startDateField": "start", + "endDateField": "end", + "colorField": "type", + "colorMapping": { + "meeting": "#3b82f6", + "deadline": "#ef4444", + "event": "#10b981", + "holiday": "#8b5cf6" + }, + "data": [ + { + "id": 1, + "title": "Team Standup", + "start": "${new Date(new Date().setHours(9, 0, 0, 0)).toISOString()}", + "end": "${new Date(new Date().setHours(9, 30, 0, 0)).toISOString()}", + "type": "meeting", + "allDay": false + }, + { + "id": 2, + "title": "Project Launch", + "start": "${new Date(new Date().setDate(new Date().getDate() + 3)).toISOString()}", + "type": "deadline", + "allDay": true + }, + { + "id": 3, + "title": "Client Meeting", + "start": "${new Date(new Date().setDate(new Date().getDate() + 5)).toISOString()}", + "end": "${new Date(new Date(new Date().setDate(new Date().getDate() + 5)).setHours(14, 0, 0, 0)).toISOString()}", + "type": "meeting", + "allDay": false + }, + { + "id": 4, + "title": "Team Building Event", + "start": "${new Date(new Date().setDate(new Date().getDate() + 7)).toISOString()}", + "end": "${new Date(new Date().setDate(new Date().getDate() + 9)).toISOString()}", + "type": "event", + "allDay": true + }, + { + "id": 5, + "title": "Code Review", + "start": "${new Date(new Date().setDate(new Date().getDate() + 1)).toISOString()}", + "end": "${new Date(new Date(new Date().setDate(new Date().getDate() + 1)).setHours(15, 0, 0, 0)).toISOString()}", + "type": "meeting", + "allDay": false + }, + { + "id": 6, + "title": "National Holiday", + "start": "${new Date(new Date().setDate(new Date().getDate() + 10)).toISOString()}", + "type": "holiday", + "allDay": true + } + ] + } + ] }` }; @@ -646,5 +733,6 @@ export type ExampleKey = keyof typeof examples; export const exampleCategories = { 'Primitives': ['simple-page', 'input-states', 'button-variants'], 'Layouts': ['grid-layout', 'dashboard', 'tabs-demo'], - 'Forms': ['form-demo'] + 'Forms': ['form-demo'], + 'Data Display': ['calendar-view'] }; diff --git a/docs/components/calendar-view.md b/docs/components/calendar-view.md new file mode 100644 index 000000000..05b491a7e --- /dev/null +++ b/docs/components/calendar-view.md @@ -0,0 +1,121 @@ +# Calendar View Component + +The `calendar-view` component is an Airtable-style calendar for displaying records as events. It provides three view modes: Month, Week, and Day. + +## Features + +- **Multiple View Modes**: Switch between Month, Week, and Day views +- **Flexible Data Mapping**: Map your data fields to event properties +- **Color Coding**: Support for color-coded events with custom color mappings +- **Interactive**: Click on events and dates (with callbacks) +- **Responsive**: Works seamlessly on different screen sizes + +## Basic Usage + +```json +{ + "type": "calendar-view", + "data": [ + { + "id": 1, + "title": "Team Meeting", + "start": "2026-01-13T10:00:00.000Z", + "end": "2026-01-13T11:00:00.000Z", + "color": "#3b82f6" + } + ] +} +``` + +## Properties + +| Property | Type | Default | Description | +|:---|:---|:---|:---| +| `data` | `array` | `[]` | Array of record objects to display as events | +| `view` | `'month' \| 'week' \| 'day'` | `'month'` | Default view mode | +| `titleField` | `string` | `'title'` | Field name to use for event title | +| `startDateField` | `string` | `'start'` | Field name for event start date | +| `endDateField` | `string` | `'end'` | Field name for event end date (optional) | +| `allDayField` | `string` | `'allDay'` | Field name for all-day flag | +| `colorField` | `string` | `'color'` | Field name for event color | +| `colorMapping` | `object` | `{}` | Map field values to colors | +| `allowCreate` | `boolean` | `false` | Allow creating events by clicking on dates | +| `className` | `string` | - | Additional CSS classes | + +## Data Structure + +Each event object in the `data` array should have the following structure: + +```typescript +{ + id: string | number; // Unique identifier + title: string; // Event title (or use custom titleField) + start: string | Date; // Start date/time (ISO string or Date) + end?: string | Date; // End date/time (optional) + allDay?: boolean; // Whether it's an all-day event + color?: string; // Event color (hex or CSS color) + [key: string]: any; // Any other custom data +} +``` + +## Examples + +### Month View with Color Mapping + +```json +{ + "type": "calendar-view", + "className": "h-[600px] border rounded-lg", + "view": "month", + "colorField": "type", + "colorMapping": { + "meeting": "#3b82f6", + "deadline": "#ef4444", + "event": "#10b981" + }, + "data": [ + { + "id": 1, + "title": "Team Standup", + "start": "2026-01-13T09:00:00.000Z", + "end": "2026-01-13T09:30:00.000Z", + "type": "meeting" + }, + { + "id": 2, + "title": "Project Deadline", + "start": "2026-01-20T00:00:00.000Z", + "type": "deadline", + "allDay": true + } + ] +} +``` + +## View Modes + +### Month View +Displays a full month calendar grid with events shown as colored bars on their respective dates. Perfect for getting a high-level overview of the month. + +### Week View +Shows a week at a time with each day in a column. Events display with their times, ideal for detailed weekly planning. + +### Day View +Displays a single day with hourly time slots from 12 AM to 11 PM. Events are positioned at their scheduled times, great for detailed daily schedules. + +## Events & Callbacks + +The calendar view supports several event callbacks through the `onAction` mechanism: + +- `event_click`: Triggered when an event is clicked +- `date_click`: Triggered when a date cell is clicked +- `view_change`: Triggered when the view mode changes +- `navigate`: Triggered when navigating between dates + +## Styling + +The calendar view is fully styled with Tailwind CSS and supports custom styling through the `className` prop. + +## Integration with ObjectQL + +When used with ObjectQL, the calendar view can automatically fetch and display records from your database. diff --git a/docs/spec/component-library.md b/docs/spec/component-library.md index 3f021e2e4..62e0ab692 100644 --- a/docs/spec/component-library.md +++ b/docs/spec/component-library.md @@ -68,6 +68,7 @@ Components for visualizing data. | `alert` | Highlighted message | `variant`, `title`, `description` | | `table` | Data-driven table | `columns`, `data`, `caption`, `footer` | | `carousel` | Slideshow component | `items`, `orientation`, `showArrows` | +| `calendar-view` | Airtable-style calendar | `data`, `view`, `titleField`, `startDateField`, `endDateField`, `colorField` | ## 6. Feedback Components Indicators for system status or feedback. From 4c56bbb7211012396b1cb702a9119b94ec6ce6fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:59:19 +0000 Subject: [PATCH 4/5] Fix code review issues in calendar-view - Remove unused CalendarIcon import - Extract default event color to constant - Fix date mutation issue in getEventsForDate function - Improve date comparison logic Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- packages/components/src/ui/calendar-view.tsx | 26 ++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/components/src/ui/calendar-view.tsx b/packages/components/src/ui/calendar-view.tsx index 4ef55b645..a58167955 100644 --- a/packages/components/src/ui/calendar-view.tsx +++ b/packages/components/src/ui/calendar-view.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { ChevronLeftIcon, ChevronRightIcon, CalendarIcon } from "lucide-react" +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/ui/button" import { @@ -12,6 +12,8 @@ import { SelectValue, } from "@/ui/select" +const DEFAULT_EVENT_COLOR = "bg-blue-500 text-white" + export interface CalendarEvent { id: string | number title: string @@ -232,10 +234,20 @@ function isSameDay(date1: Date, date2: Date): boolean { function getEventsForDate(date: Date, events: CalendarEvent[]): CalendarEvent[] { return events.filter((event) => { const eventStart = new Date(event.start) - const eventEnd = event.end ? new Date(event.end) : eventStart + const eventEnd = event.end ? new Date(event.end) : new Date(eventStart) + + // Create new date objects for comparison to avoid mutation + const dateStart = new Date(date) + dateStart.setHours(0, 0, 0, 0) + const dateEnd = new Date(date) + dateEnd.setHours(23, 59, 59, 999) + + const eventStartTime = new Date(eventStart) + eventStartTime.setHours(0, 0, 0, 0) + const eventEndTime = new Date(eventEnd) + eventEndTime.setHours(23, 59, 59, 999) - return date >= new Date(eventStart.setHours(0, 0, 0, 0)) && - date <= new Date(eventEnd.setHours(23, 59, 59, 999)) + return dateStart <= eventEndTime && dateEnd >= eventStartTime }) } @@ -296,7 +308,7 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps) key={event.id} className={cn( "text-xs px-2 py-1 rounded truncate cursor-pointer hover:opacity-80", - event.color || "bg-blue-500 text-white" + event.color || DEFAULT_EVENT_COLOR )} style={ event.color && event.color.startsWith("#") @@ -385,7 +397,7 @@ function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) { key={event.id} className={cn( "text-sm px-3 py-2 rounded cursor-pointer hover:opacity-80", - event.color || "bg-blue-500 text-white" + event.color || DEFAULT_EVENT_COLOR )} style={ event.color && event.color.startsWith("#") @@ -454,7 +466,7 @@ function DayView({ date, events, onEventClick }: DayViewProps) { key={event.id} className={cn( "px-3 py-2 rounded cursor-pointer hover:opacity-80", - event.color || "bg-blue-500 text-white" + event.color || DEFAULT_EVENT_COLOR )} style={ event.color && event.color.startsWith("#") From 4f7d2eaff4a0f79dc2dddcb3b6b108882e08c015 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:03:27 +0000 Subject: [PATCH 5/5] Address PR review feedback: Add JSDoc comments and remove redundant property - Added JSDoc comments to field name mappings (titleField, startField, etc.) for auto-documentation - Removed redundant 'defaultView' property, keeping only 'view' for clarity - Updated 'view' property label to 'View Mode' with description Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com> --- .../src/renderers/complex/calendar-view.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/components/src/renderers/complex/calendar-view.tsx b/packages/components/src/renderers/complex/calendar-view.tsx index 5a5540332..b620c9956 100644 --- a/packages/components/src/renderers/complex/calendar-view.tsx +++ b/packages/components/src/renderers/complex/calendar-view.tsx @@ -10,11 +10,15 @@ ComponentRegistry.register('calendar-view', if (!schema.data || !Array.isArray(schema.data)) return []; return schema.data.map((record: any, index: number) => { - // Get field values based on field mappings + /** Field name to use for event title display */ const titleField = schema.titleField || 'title'; + /** Field name containing the event start date/time */ const startField = schema.startDateField || 'start'; + /** Field name containing the event end date/time (optional) */ const endField = schema.endDateField || 'end'; + /** Field name to determine event color or color category */ const colorField = schema.colorField || 'color'; + /** Field name indicating if event is all-day */ const allDayField = schema.allDayField || 'allDay'; const title = record[titleField] || 'Untitled'; @@ -91,7 +95,7 @@ ComponentRegistry.register('calendar-view', return (