/** * CalendarTree - left sidebar calendar selector for the Calendar plugin. * * Groups calendars by type (personal, team, project, company) with color dots, * visibility checkboxes, and a "New Calendar" button at the bottom. * Pattern follows DMS SourceTree. */ import React, { useState } from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import type { Calendar, CalendarType } from '@/api/calendar'; const TYPE_ORDER: CalendarType[] = ['personal', 'team', 'project', 'company']; interface CalendarRowProps { calendar: Calendar; selected: boolean; visible: boolean; onSelect: () => void; onToggleVisibility: () => void; } function CalendarRow({ calendar, selected, visible, onSelect, onToggleVisibility }: CalendarRowProps) { return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(); } }} tabIndex={0} role="button" aria-label={calendar.name} data-testid={`calendar-tree-row-${calendar.id}`} > { e.stopPropagation(); onToggleVisibility(); }} onClick={(e) => e.stopPropagation()} className="flex-shrink-0 h-3.5 w-3.5 rounded border-secondary-300 text-primary-600 focus:ring-primary-500" aria-label={`toggle visibility ${calendar.name}`} data-testid={`calendar-tree-visibility-${calendar.id}`} />
); } interface TypeSectionProps { type: CalendarType; calendars: Calendar[]; activeCalendarId: string | null; visibleCalendarIds: Set; onSelect: (id: string) => void; onToggleVisibility: (id: string) => void; } function TypeSection({ type, calendars, activeCalendarId, visibleCalendarIds, onSelect, onToggleVisibility, }: TypeSectionProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(true); if (calendars.length === 0) return null; return (
setExpanded((p) => !p)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setExpanded((p) => !p); } }} tabIndex={0} role="button" aria-expanded={expanded} aria-label={t(`calendar.type.${type}`)} > {t(`calendar.type.${type}`)} {calendars.length}
{expanded && ( )}
); } export interface CalendarTreeProps { calendars: Calendar[]; activeCalendarId: string | null; visibleCalendarIds: Set; onSelect: (id: string) => void; onToggleVisibility: (id: string) => void; onCreate: () => void; loading: boolean; } export function CalendarTree({ calendars, activeCalendarId, visibleCalendarIds, onSelect, onToggleVisibility, onCreate, loading, }: CalendarTreeProps) { const { t } = useTranslation(); if (loading) { return (
{[1, 2, 3].map((i) => (
))}
); } const grouped = new Map(); for (const type of TYPE_ORDER) { grouped.set(type, []); } for (const cal of calendars) { const list = grouped.get(cal.type) ?? grouped.get('personal')!; list.push(cal); } return ( ); } export default CalendarTree;