Repository: tutur3u/platform
Pull request #2743: Feat/calendar/calendar cell
Author: login: Adinorio
Created: 2025-05-08T16:02:52Z | Merged: 2025-05-09T01:28:32Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced calendar cell interaction with delayed tooltips and improved hover behavior, including cursor tracking and dynamic label display.
  - Added confirmation dialog for copying time blocks to all days in the time range picker.

- **Improvements**
  - Improved validation and error handling for setting and editing time ranges, including user feedback for invalid inputs and actions.
  - Enhanced robustness and UI clarity in time range selection, with better handling of edge cases and clearer tooltips.
  - Default hour settings are now automatically initialized if missing.
  - Added validation for calendar settings loaded from storage to ensure data integrity.

- **Bug Fixes**
  - Prevented overlapping or invalid time blocks and ensured safe state handling in time range settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx b/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
index 0d2800727b..9d72a0c10f 100644
--- a/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
@@ -21,9 +21,22 @@ const CalendarCell = ({ date, hour }: CalendarCellProps) => {
   const dragStartRef = useRef<{ date: Date; y: number } | null>(null);
   const previewRef = useRef<HTMLDivElement | null>(null);
   const tz = settings?.timezone?.timezone;
+  const [hoveredSlot, setHoveredSlot] = useState<'hour' | 'half-hour' | null>(
+    null
+  );
+  const [showBothLabels, setShowBothLabels] = useState(false);
+  const [showTooltip, setShowTooltip] = useState(false);
+  const [tooltipLocked, setTooltipLocked] = useState(false);
+  const hoverTimeout = useRef<NodeJS.Timeout | null>(null);
+  const tooltipRef = useRef<HTMLDivElement | null>(null);
+  const cursorPosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
+  const rafRef = useRef<number | null>(null);
 
   const id = `cell-${date}-${hour}`;
 
+  // Add a stable tooltip id for aria-describedby
+  const tooltipId = `calendar-tooltip-${id}`;
+
   // Format time for display - only show when hovering
   const formatTime = (hour: number, minute: number = 0) => {
     const base = dayjs(date);
@@ -287,6 +300,95 @@ const CalendarCell = ({ date, hour }: CalendarCellProps) => {
     [isDragging, date, yToTime, addEmptyEventWithDuration]
   );
 
+  // Helper to handle mouse enter for each slot
+  const handleSlotMouseEnter = (slot: 'hour' | 'half-hour') => {
+    setHoveredSlot(slot);
+    if (!tooltipLocked) {
+      setShowBothLabels(false);
+      setShowTooltip(false);
+      if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
+      hoverTimeout.current = setTimeout(() => {
+        setShowBothLabels(true);
+        setShowTooltip(true);
+        setTooltipLocked(true);
+      }, 1000);
+    } else {
+      setShowTooltip(true);
+    }
+  };
+
+  // Helper to handle mouse leave for each slot
+  const handleSlotMouseLeave = () => {
+    setHoveredSlot(null);
+    // Do not reset tooltip/labels here; only reset on cell leave
+    if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
+  };
+
+  // Throttled tooltip position update
+  const updateTooltipPosition = () => {
+    if (tooltipRef.current) {
+      tooltipRef.current.style.left = `${cursorPosRef.current.x}px`;
+      tooltipRef.current.style.top = `${cursorPosRef.current.y}px`;
+    }
+    rafRef.current = null;
+  };
+
+  const handleSlotMouseMove = (
+    e: React.MouseEvent<HTMLButtonElement, MouseEvent>
+  ) => {
+    const offset = 8;
+    const flipOffset = 16;
+    let x = e.clientX + offset;
+    let y = e.clientY + offset;
+    const tooltipWidth = 200;
+    const tooltipHeight = 40;
+    const padding = 8;
+    if (x + tooltipWidth > window.innerWidth - padding) {
+      x = e.clientX - tooltipWidth - flipOffset;
+    }
+    if (y + tooltipHeight > window.innerHeight - padding) {
+      y = e.clientY - tooltipHeight - flipOffset;
+    }
+    cursorPosRef.current = { x, y };
+    if (!rafRef.current) {
+      rafRef.current = requestAnimationFrame(updateTooltipPosition);
+    }
+    if (tooltipLocked) {
+      setShowTooltip(true);
+    }
+  };
+
+  // Handle leaving the cell: reset everything
+  const handleCellMouseLeave = () => {
+    setIsHovering(false);
+    setHoveredSlot(null);
+    setShowBothLabels(false);
+    setShowTooltip(false);
+    setTooltipLocked(false);
+    if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
+  };
+
+  const handleSlotFocus = (slot: 'hour' | 'half-hour') => {
+    setHoveredSlot(slot);
+    if (!tooltipLocked) {
+      setShowBothLabels(false);
+      setShowTooltip(false);
+      if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
+      hoverTimeout.current = setTimeout(() => {
+        setShowBothLabels(true);
+        setShowTooltip(true);
+        setTooltipLocked(true);
+      }, 1000);
+    } else {
+      setShowTooltip(true);
+    }
+  };
+
+  const handleSlotBlur = () => {
+    setHoveredSlot(null);
+    setShowTooltip(false);
+  };
+
   // Set up event listeners
   useEffect(() => {
     const cell = cellRef.current;
@@ -313,6 +415,13 @@ const CalendarCell = ({ date, hour }: CalendarCellProps) => {
     };
   }, [handleMouseDown, handleMouseMove, handleMouseUp]);
 
+  useEffect(() => {
+    return () => {
+      if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
+      if (rafRef.current) cancelAnimationFrame(rafRef.current);
+    };
+  }, []);
+
   return (
     <div
       id={id}
@@ -329,40 +438,74 @@ const CalendarCell = ({ date, hour }: CalendarCellProps) => {
         e.preventDefault();
       }}
       onMouseEnter={() => setIsHovering(true)}
-      onMouseLeave={() => setIsHovering(false)}
+      onMouseLeave={handleCellMouseLeave}
       data-hour={hour}
       data-date={date}
     >
+      {/* Show only the hovered label before 1s, both after 1s */}
+      {showBothLabels ? (
+        <>
+          <span className="text-muted-foreground/70 absolute left-2 top-2 text-xs font-medium">
+            {formatTime(hour)}
+          </span>
+          <span className="text-muted-foreground/70 absolute bottom-2 left-2 text-xs font-medium">
+            {formatTime(hour, 30)}
+          </span>
+        </>
+      ) : hoveredSlot === 'hour' ? (
+        <span className="text-muted-foreground/70 absolute left-2 top-2 text-xs font-medium">
+          {formatTime(hour)}
+        </span>
+      ) : hoveredSlot === 'half-hour' ? (
+        <span className="text-muted-foreground/70 absolute bottom-2 left-2 text-xs font-medium">
+          {formatTime(hour, 30)}
+        </span>
+      ) : null}
       {/* Full cell clickable area */}
       <button
         className="absolute inset-0 h-1/2 w-full cursor-pointer focus:outline-none"
         onClick={() => handleCreateEvent()}
-        title={`Create event at ${formatTime(hour)}`}
-      >
-        {/* Only show time on hover */}
-        {isHovering && (
-          <span className="text-muted-foreground/70 absolute left-2 top-2 text-xs font-medium">
-            {formatTime(hour)}
-          </span>
-        )}
-      </button>
-
+        onMouseEnter={() => handleSlotMouseEnter('hour')}
+        onMouseLeave={handleSlotMouseLeave}
+        onMouseMove={handleSlotMouseMove}
+        onFocus={() => handleSlotFocus('hour')}
+        onBlur={handleSlotBlur}
+        aria-describedby={tooltipId}
+      />
       {/* Half-hour marker */}
       <div className="border-border/30 absolute left-0 right-0 top-1/2 border-t border-dashed" />
-
       {/* Half-hour clickable area */}
       <button
         className="absolute inset-x-0 top-1/2 h-1/2 cursor-pointer focus:outline-none"
         onClick={() => handleCreateEvent(true)}
-        title={`Create event at ${formatTime(hour, 30)}`}
-      >
-        {/* Only show time on hover */}
-        {isHovering && (
-          <span className="text-muted-foreground/70 absolute left-2 top-2 text-xs font-medium">
-            {formatTime(hour, 30)}
-          </span>
-        )}
-      </button>
+        onMouseEnter={() => handleSlotMouseEnter('half-hour')}
+        onMouseLeave={handleSlotMouseLeave}
+        onMouseMove={handleSlotMouseMove}
+        onFocus={() => handleSlotFocus('half-hour')}
+        onBlur={handleSlotBlur}
+        aria-describedby={tooltipId}
+      />
+      {/* Custom tooltip near cursor, only one at a time, fixed position, only after 1s */}
+      {showTooltip && hoveredSlot && (
+        <div
+          ref={tooltipRef}
+          id={tooltipId}
+          role="tooltip"
+          aria-live="polite"
+          className="pointer-events-none rounded-md bg-neutral-900 px-3 py-1.5 text-xs font-medium text-white shadow-lg"
+          style={{
+            position: 'fixed',
+            left: cursorPosRef.current.x,
+            top: cursorPosRef.current.y,
+            minWidth: 120,
+            maxWidth: 200,
+            zIndex: 10000,
+            whiteSpace: 'nowrap',
+          }}
+        >
+          {`Create an event at ${hoveredSlot === 'hour' ? formatTime(hour) : formatTime(hour, 30)}`}
+        </div>
+      )}
     </div>
   );
 };
diff --git a/packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx b/packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx
index 6b6f8cdff1..0bc110a36d 100644
--- a/packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx
@@ -68,7 +68,8 @@ const loadSettingsFromStorage = (): Partial<CalendarSettings> | null => {
     const storedSettings = localStorage.getItem('calendarSettings');
     if (storedSettings) {
       console.log('Loading settings from localStorage');
-      return JSON.parse(storedSettings) as Partial<CalendarSettings>;
+      const parsed = JSON.parse(storedSettings);
+      return isValidPartialCalendarSettings(parsed) ? parsed : null;
     }
   } catch (error) {
     console.error('Failed to load settings from localStorage:', error);
@@ -76,6 +77,23 @@ const loadSettingsFromStorage = (): Partial<CalendarSettings> | null => {
   return null;
 };
 
+// Type guard for Partial<CalendarSettings>
+function isValidPartialCalendarSettings(
+  obj: any
+): obj is Partial<CalendarSettings> {
+  if (!obj || typeof obj !== 'object') return false;
+  // Only check a few critical keys for safety
+  if ('personalHours' in obj && typeof obj.personalHours !== 'object')
+    return false;
+  if ('workHours' in obj && typeof obj.workHours !== 'object') return false;
+  if ('meetingHours' in obj && typeof obj.meetingHours !== 'object')
+    return false;
+  if ('appearance' in obj && typeof obj.appearance !== 'object') return false;
+  if ('notifications' in obj && typeof obj.notifications !== 'object')
+    return false;
+  return true;
+}
+
 type CalendarSettingsContextType = {
   settings: CalendarSettings;
   updateSettings: <K extends keyof CalendarSettings>(
diff --git a/packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx b/packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
index 9d9c88e4aa..57ee847d6a 100644
--- a/packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
@@ -1,6 +1,10 @@
 'use client';
 
-import { TimeRangePicker, WeekTimeRanges } from './TimeRangePicker';
+import {
+  TimeRangePicker,
+  WeekTimeRanges,
+  defaultWeekTimeRanges,
+} from './TimeRangePicker';
 import { createClient } from '@tuturuuu/supabase/next/client';
 import { Badge } from '@tuturuuu/ui/badge';
 import { Card, CardContent, CardHeader, CardTitle } from '@tuturuuu/ui/card';
@@ -20,11 +24,11 @@ type HoursSettingsProps = {
 };
 
 export function HoursSettings({ wsId }: HoursSettingsProps) {
-  const [value, setValue] = useState<{
-    personalHours?: WeekTimeRanges | null;
-    workHours?: WeekTimeRanges | null;
-    meetingHours?: WeekTimeRanges | null;
-  } | null>(null);
+  const [value, setValue] = useState<HoursSettingsData>({
+    personalHours: defaultWeekTimeRanges,
+    workHours: defaultWeekTimeRanges,
+    meetingHours: defaultWeekTimeRanges,
+  });
 
   useEffect(() => {
     const fetchHours = async () => {
@@ -36,17 +40,68 @@ export function HoursSettings({ wsId }: HoursSettingsProps) {
         .eq('ws_id', wsId);
 
       if (error) {
-        console.error(error);
+        console.error('Error fetching hours:', error);
+        toast.error(
+          'Failed to load hour settings. Please refresh or try again later.'
+        );
+        return;
+      }
+
+      // If no data exists, create default settings
+      if (!data || data.length === 0) {
+        const makeDefaultData = () => structuredClone(defaultWeekTimeRanges);
+        const defaultSettings = [
+          {
+            type: 'PERSONAL' as const,
+            data: JSON.stringify(makeDefaultData()),
+            ws_id: wsId,
+          },
+          {
+            type: 'WORK' as const,
+            data: JSON.stringify(makeDefaultData()),
+            ws_id: wsId,
+          },
+          {
+            type: 'MEETING' as const,
+            data: JSON.stringify(makeDefaultData()),
+            ws_id: wsId,
+          },
+        ];
+
+        const { error: insertError } = await supabase
+          .from('workspace_calendar_hour_settings')
+          .insert(defaultSettings)
+          .select();
+
+        if (insertError) {
+          console.error('Error creating default settings:', insertError);
+          return;
+        }
+
+        setValue({
+          personalHours: defaultWeekTimeRanges,
+          workHours: defaultWeekTimeRanges,
+          meetingHours: defaultWeekTimeRanges,
+        });
         return;
       }
 
       setValue({
-        personalHours: data?.find((h) => h.type === 'PERSONAL')
-          ?.data as WeekTimeRanges | null,
-        workHours: data?.find((h) => h.type === 'WORK')
-          ?.data as WeekTimeRanges | null,
-        meetingHours: data?.find((h) => h.type === 'MEETING')
-          ?.data as WeekTimeRanges | null,
+        personalHours: isValidWeekTimeRanges(
+          safeParse(data?.find((h) => h.type === 'PERSONAL')?.data)
+        )
+          ? safeParse(data?.find((h) => h.type === 'PERSONAL')?.data)
+          : defaultWeekTimeRanges,
+        workHours: isValidWeekTimeRanges(
+          safeParse(data?.find((h) => h.type === 'WORK')?.data)
+        )
+          ? safeParse(data?.find((h) => h.type === 'WORK')?.data)
+          : defaultWeekTimeRanges,
+        meetingHours: isValidWeekTimeRanges(
+          safeParse(data?.find((h) => h.type === 'MEETING')?.data)
+        )
+          ? safeParse(data?.find((h) => h.type === 'MEETING')?.data)
+          : defaultWeekTimeRanges,
       });
     };
 
@@ -74,15 +129,18 @@ export function HoursSettings({ wsId }: HoursSettingsProps) {
 
     const { error } = await supabase
       .from('workspace_calendar_hour_settings')
-      .upsert({
-        data: newHours,
-        type: 'PERSONAL',
-        ws_id: wsId,
-      })
-      .eq('ws_id', wsId);
+      .upsert(
+        {
+          data: JSON.stringify(newHours),
+          type: 'PERSONAL',
+          ws_id: wsId,
+        },
+        { onConflict: 'ws_id,type' }
+      );
 
     if (error) {
-      console.error(error);
+      console.error('Error updating personal hours:', error);
+      toast.error('Failed to update personal hours');
       return;
     }
 
@@ -104,14 +162,18 @@ export function HoursSettings({ wsId }: HoursSettingsProps) {
 
     const { error } = await supabase
       .from('workspace_calendar_hour_settings')
-      .upsert({
-        data: newHours,
-        type: 'WORK',
-        ws_id: wsId,
-      });
+      .upsert(
+        {
+          data: JSON.stringify(newHours),
+          type: 'WORK',
+          ws_id: wsId,
+        },
+        { onConflict: 'ws_id,type' }
+      );
 
     if (error) {
-      console.error(error);
+      console.error('Error updating work hours:', error);
+      toast.error('Failed to update work hours');
       return;
     }
 
@@ -133,14 +195,18 @@ export function HoursSettings({ wsId }: HoursSettingsProps) {
 
     const { error } = await supabase
       .from('workspace_calendar_hour_settings')
-      .upsert({
-        data: newHours,
-        type: 'MEETING',
-        ws_id: wsId,
-      });
+      .upsert(
+        {
+          data: JSON.stringify(newHours),
+          type: 'MEETING',
+          ws_id: wsId,
+        },
+        { onConflict: 'ws_id,type' }
+      );
 
     if (error) {
-      console.error(error);
+      console.error('Error updating meeting hours:', error);
+      toast.error('Failed to update meeting hours');
       return;
     }
 
@@ -275,3 +341,35 @@ export function HoursSettings({ wsId }: HoursSettingsProps) {
     </div>
   );
 }
+
+// Type guard for WeekTimeRanges
+function isValidWeekTimeRanges(obj: any): obj is WeekTimeRanges {
+  if (!obj || typeof obj !== 'object') return false;
+  const days = [
+    'monday',
+    'tuesday',
+    'wednesday',
+    'thursday',
+    'friday',
+    'saturday',
+    'sunday',
+  ];
+  return days.every(
+    (day) =>
+      obj[day] &&
+      typeof obj[day].enabled === 'boolean' &&
+      Array.isArray(obj[day].timeBlocks)
+  );
+}
+
+// Safe JSON parse helper
+function safeParse(data: any): any {
+  if (typeof data === 'string') {
+    try {
+      return JSON.parse(data);
+    } catch {
+      return undefined;
+    }
+  }
+  return data;
+}
diff --git a/packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx b/packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
index 588b5d7274..00b7af282f 100644
--- a/packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
@@ -1,5 +1,16 @@
 'use client';
 
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+  AlertDialogTrigger,
+} from '@tuturuuu/ui/alert-dialog';
 import { Button } from '@tuturuuu/ui/button';
 import { Input } from '@tuturuuu/ui/input';
 import { Label } from '@tuturuuu/ui/label';
@@ -8,6 +19,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
 import { cn } from '@tuturuuu/utils/format';
 import { Copy, Plus, Trash2 } from 'lucide-react';
 import { useState } from 'react';
+import { toast } from 'sonner';
 
 export type TimeBlock = {
   startTime: string;
@@ -63,6 +75,16 @@ type TimeRangePickerProps = {
   compact?: boolean;
 };
 
+// Utility to ensure time is always in HH:mm format for UI
+function normalizeTimeString(time: string): string {
+  if (typeof time !== 'string') return '00:00';
+  const parts = time.split(':');
+  const h = parts[0] !== undefined ? Number(parts[0]) : 0;
+  const m = parts[1] !== undefined ? Number(parts[1]) : 0;
+  if (isNaN(h) || isNaN(m)) return '00:00';
+  return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
+}
+
 export function TimeRangePicker({
   label,
   value,
@@ -72,123 +94,355 @@ export function TimeRangePicker({
   compact = false,
 }: TimeRangePickerProps) {
   const [activeDay, setActiveDay] = useState<keyof WeekTimeRanges>('monday');
+  const [showCopyDialog, setShowCopyDialog] = useState(false);
+  const [pendingCopy, setPendingCopy] = useState(false);
 
-  const days: Array<{
-    key: keyof WeekTimeRanges;
-    label: string;
-    fullLabel: string;
-    type: 'weekday' | 'weekend';
-  }> = [
-    { key: 'monday', label: 'M', fullLabel: 'Monday', type: 'weekday' },
-    { key: 'tuesday', label: 'T', fullLabel: 'Tuesday', type: 'weekday' },
-    { key: 'wednesday', label: 'W', fullLabel: 'Wednesday', type: 'weekday' },
-    { key: 'thursday', label: 'T', fullLabel: 'Thursday', type: 'weekday' },
-    { key: 'friday', label: 'F', fullLabel: 'Friday', type: 'weekday' },
-    { key: 'saturday', label: 'S', fullLabel: 'Saturday', type: 'weekend' },
-    { key: 'sunday', label: 'S', fullLabel: 'Sunday', type: 'weekend' },
-  ];
+  // Use defaultWeekTimeRanges if value is null
+  const timeRanges = value || defaultWeekTimeRanges;
 
-  // Filter days based on dayFilter
-  const filteredDays = days.filter((day) => {
-    if (dayFilter === 'all') return true;
-    return day.type === dayFilter;
-  });
+  // Ensure each day has a valid timeBlocks array
+  const safeTimeRanges: WeekTimeRanges = {
+    monday: {
+      enabled:
+        !!timeRanges.monday && typeof timeRanges.monday.enabled === 'boolean'
+          ? timeRanges.monday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.monday?.timeBlocks)
+        ? timeRanges.monday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+    tuesday: {
+      enabled:
+        !!timeRanges.tuesday && typeof timeRanges.tuesday.enabled === 'boolean'
+          ? timeRanges.tuesday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.tuesday?.timeBlocks)
+        ? timeRanges.tuesday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+    wednesday: {
+      enabled:
+        !!timeRanges.wednesday &&
+        typeof timeRanges.wednesday.enabled === 'boolean'
+          ? timeRanges.wednesday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.wednesday?.timeBlocks)
+        ? timeRanges.wednesday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+    thursday: {
+      enabled:
+        !!timeRanges.thursday &&
+        typeof timeRanges.thursday.enabled === 'boolean'
+          ? timeRanges.thursday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.thursday?.timeBlocks)
+        ? timeRanges.thursday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+    friday: {
+      enabled:
+        !!timeRanges.friday && typeof timeRanges.friday.enabled === 'boolean'
+          ? timeRanges.friday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.friday?.timeBlocks)
+        ? timeRanges.friday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+    saturday: {
+      enabled:
+        !!timeRanges.saturday &&
+        typeof timeRanges.saturday.enabled === 'boolean'
+          ? timeRanges.saturday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.saturday?.timeBlocks)
+        ? timeRanges.saturday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+    sunday: {
+      enabled:
+        !!timeRanges.sunday && typeof timeRanges.sunday.enabled === 'boolean'
+          ? timeRanges.sunday.enabled
+          : false,
+      timeBlocks: Array.isArray(timeRanges.sunday?.timeBlocks)
+        ? timeRanges.sunday.timeBlocks.map((b) => ({ ...b }))
+        : [defaultTimeBlock],
+    },
+  };
+
+  // Helper to convert time string to minutes
+  const timeToMinutes = (t: string) => {
+    const [h, m] = t.split(':').map(Number);
+    if (typeof h !== 'number' || isNaN(h) || typeof m !== 'number' || isNaN(m))
+      return 0;
+    return h * 60 + m;
+  };
+  // Helper to convert minutes to time string
+  const minutesToTime = (min: number) => {
+    const h = Math.floor(min / 60);
+    const m = min % 60;
+    return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
+  };
+
+  // Validation for a single block
+  const validateTimeRange = (
+    startTime: string,
+    endTime: string,
+    prevEnd?: string,
+    nextStart?: string
+  ): {
+    isValid: boolean;
+    message?: string;
+    correctedEnd?: string;
+    correctedStart?: string;
+  } => {
+    const startMin = timeToMinutes(startTime);
+    const endMin = timeToMinutes(endTime);
+    if (endMin <= startMin) {
+      return { isValid: false, message: 'End time must be after start time' };
+    }
+    if (endMin - startMin < 30) {
+      return {
+        isValid: false,
+        message: 'Time block must be at least 30 minutes',
+      };
+    }
+    if (startMin < 0) {
+      return {
+        isValid: false,
+        message: 'Start time cannot be before 12:00 AM',
+      };
+    }
+    if (endMin > 1439) {
+      return { isValid: false, message: 'End time cannot be after 11:59 PM' };
+    }
+    if (prevEnd && startMin < timeToMinutes(prevEnd)) {
+      return {
+        isValid: false,
+        message: 'Start time overlaps with previous block',
+        correctedStart: prevEnd,
+      };
+    }
+    if (nextStart && endMin > timeToMinutes(nextStart)) {
+      return {
+        isValid: false,
+        message: 'End time overlaps with next block',
+        correctedEnd: nextStart,
+      };
+    }
+    return { isValid: true };
+  };
+
+  // Can we add another block?
+  const canAddMoreBlocks = (day: keyof WeekTimeRanges) => {
+    const blocks = safeTimeRanges[day]?.timeBlocks || [];
+    if (blocks.length === 0) return true;
+    const lastBlock = blocks[blocks.length - 1];
+    if (!lastBlock) return true;
+    const lastEnd = timeToMinutes(lastBlock.endTime);
+    // If less than 30 min left in the day, can't add
+    return lastEnd <= 1409; // 1409 = 23*60+29
+  };
 
+  // Reason for disabling add button
+  const addBlockDisabledReason = (day: keyof WeekTimeRanges) => {
+    const blocks = safeTimeRanges[day]?.timeBlocks || [];
+    if (blocks.length === 0) return '';
+    const lastBlock = blocks[blocks.length - 1];
+    if (!lastBlock) return '';
+    const lastEnd = timeToMinutes(lastBlock.endTime);
+    if (lastEnd > 1409) return 'No time left in the day for a 30-minute block';
+    return '';
+  };
+
+  // Add a new time block
+  const addTimeBlock = (day: keyof WeekTimeRanges) => {
+    const newTimeRanges = { ...safeTimeRanges };
+    const blocks = newTimeRanges[day]?.timeBlocks || [];
+    const lastBlock = blocks[blocks.length - 1];
+    let newStartMin = lastBlock ? timeToMinutes(lastBlock.endTime) : 540; // 09:00
+    let newEndMin = newStartMin + 30;
+    if (newEndMin > 1439) newEndMin = 1439; // Clamp to 11:59 PM
+    if (newEndMin - newStartMin < 30) {
+      toast.error('Not enough time left in the day for a 30-minute block');
+      return;
+    }
+    const newStartTime = minutesToTime(newStartMin);
+    const newEndTime = minutesToTime(newEndMin);
+    // Prevent overlap
+    if (
+      blocks.length > 0 &&
+      lastBlock &&
+      timeToMinutes(newStartTime) < timeToMinutes(lastBlock.endTime)
+    ) {
+      toast.error('New block overlaps with previous block');
+      return;
+    }
+    if (!newTimeRanges[day]) {
+      newTimeRanges[day] = { ...defaultTimeRange };
+    }
+    newTimeRanges[day].timeBlocks.push({
+      startTime: newStartTime,
+      endTime: newEndTime,
+    });
+    onChange(newTimeRanges as WeekTimeRanges);
+  };
+
+  // Handle time input changes with validation and auto-correction
   const handleTimeChange = (
     day: keyof WeekTimeRanges,
     blockIndex: number,
     field: keyof TimeBlock,
     newValue: string
   ) => {
-    const newTimeRanges = { ...value };
-    if (newTimeRanges[day]?.timeBlocks?.[blockIndex]) {
-      newTimeRanges[day].timeBlocks[blockIndex] = {
-        ...newTimeRanges[day].timeBlocks[blockIndex],
-        [field]: newValue,
-      };
-      onChange(newTimeRanges as WeekTimeRanges);
+    const newTimeRanges = { ...safeTimeRanges };
+    const blocks = newTimeRanges[day]?.timeBlocks || [];
+    const block = blocks[blockIndex];
+    if (!block) return;
+
+    const prevBlock = blockIndex > 0 ? blocks[blockIndex - 1] : undefined;
+    const nextBlock =
+      blockIndex < blocks.length - 1 ? blocks[blockIndex + 1] : undefined;
+    const prevEnd = prevBlock?.endTime;
+    const nextStart = nextBlock?.startTime;
+
+    let updatedBlock = {
+      ...block,
+      [field]: newValue,
+    };
+    const { isValid, message, correctedEnd, correctedStart } =
+      validateTimeRange(
+        field === 'startTime' ? newValue : updatedBlock.startTime,
+        field === 'endTime' ? newValue : updatedBlock.endTime,
+        prevEnd,
+        nextStart
+      );
+    if (!isValid) {
+      // Auto-correct if possible
+      if (correctedEnd) {
+        updatedBlock.endTime = correctedEnd;
+        toast.error(message + '. Auto-corrected end time.');
+      } else if (correctedStart) {
+        updatedBlock.startTime = correctedStart;
+        toast.error(message + '. Auto-corrected start time.');
+      } else {
+        toast.error(message);
+        return;
+      }
     }
+    blocks[blockIndex] = updatedBlock;
+    onChange(newTimeRanges as WeekTimeRanges);
   };
 
   const handleDayToggle = (day: keyof WeekTimeRanges, enabled: boolean) => {
-    const newTimeRanges = { ...value };
+    const newTimeRanges = { ...safeTimeRanges };
     if (!newTimeRanges[day]) {
       newTimeRanges[day] = { ...defaultTimeRange };
     }
-    newTimeRanges[day] = {
-      ...newTimeRanges[day],
-      enabled,
-    };
-    onChange(newTimeRanges as WeekTimeRanges);
-  };
-
-  const addTimeBlock = (day: keyof WeekTimeRanges) => {
-    const newTimeRanges = { ...value };
-    const lastBlock =
-      newTimeRanges[day]?.timeBlocks?.[
-        newTimeRanges[day].timeBlocks.length - 1
-      ];
-
-    // Create a new block starting 1 hour after the last block ends
-    const lastEndTime = lastBlock ? lastBlock.endTime : '17:00';
-    const parts = lastEndTime.split(':');
-    const hours = parseInt(parts[0] || '0', 10);
-    const minutes = parseInt(parts[1] || '0', 10);
-
-    const newStartHour = (hours + 1) % 24;
-    const newEndHour = (newStartHour + 1) % 24;
-
-    const newStartTime = `${newStartHour.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
-    const newEndTime = `${newEndHour.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
-
-    if (newTimeRanges[day]?.timeBlocks) {
-      newTimeRanges[day].timeBlocks.push({
-        startTime: newStartTime,
-        endTime: newEndTime,
-      });
-      onChange(newTimeRanges as WeekTimeRanges);
+    const dayRange = newTimeRanges[day];
+    if (!dayRange) {
+      newTimeRanges[day] = { ...defaultTimeRange, enabled };
+    } else {
+      dayRange.enabled = enabled;
     }
+    onChange(newTimeRanges as WeekTimeRanges);
   };
 
   const removeTimeBlock = (day: keyof WeekTimeRanges, blockIndex: number) => {
-    const newTimeRanges = { ...value };
+    const newTimeRanges = { ...safeTimeRanges };
+    if (!newTimeRanges[day]) {
+      newTimeRanges[day] = { ...defaultTimeRange };
+    }
+    const dayRange = newTimeRanges[day];
+    if (!dayRange) return;
 
-    // Don't remove the last block
-    if ((newTimeRanges?.[day]?.timeBlocks?.length || 0) <= 1) return;
+    const blocks = dayRange.timeBlocks || [];
+    if (blocks.length <= 1) return;
 
-    newTimeRanges[day]?.timeBlocks?.splice(blockIndex, 1);
+    blocks.splice(blockIndex, 1);
     onChange(newTimeRanges as WeekTimeRanges);
   };
 
-  const copyToAllDays = () => {
-    const currentDaySettings = value?.[activeDay];
-    const newTimeRanges = { ...value };
+  const handleCopyToAllDays = () => {
+    setShowCopyDialog(true);
+  };
 
+  const confirmCopyToAllDays = () => {
+    setShowCopyDialog(false);
+    setPendingCopy(true);
+    // Actually perform the copy
+    const currentDaySettings = safeTimeRanges[activeDay];
+    const newTimeRanges = { ...safeTimeRanges };
     days.forEach(({ key }) => {
       if (key !== activeDay) {
-        newTimeRanges[key] = { ...currentDaySettings } as DayTimeRange;
+        newTimeRanges[key] = {
+          ...newTimeRanges[key], // preserve enabled status
+          timeBlocks: currentDaySettings.timeBlocks.map((block) => ({
+            ...block,
+          })),
+        };
       }
     });
-
     onChange(newTimeRanges as WeekTimeRanges);
+    setPendingCopy(false);
   };
 
+  const days: Array<{
+    key: keyof WeekTimeRanges;
+    label: string;
+    fullLabel: string;
+    type: 'weekday' | 'weekend';
+  }> = [
+    { key: 'monday', label: 'M', fullLabel: 'Monday', type: 'weekday' },
+    { key: 'tuesday', label: 'T', fullLabel: 'Tuesday', type: 'weekday' },
+    { key: 'wednesday', label: 'W', fullLabel: 'Wednesday', type: 'weekday' },
+    { key: 'thursday', label: 'T', fullLabel: 'Thursday', type: 'weekday' },
+    { key: 'friday', label: 'F', fullLabel: 'Friday', type: 'weekday' },
+    { key: 'saturday', label: 'S', fullLabel: 'Saturday', type: 'weekend' },
+    { key: 'sunday', label: 'S', fullLabel: 'Sunday', type: 'weekend' },
+  ];
+
+  // Filter days based on dayFilter
+  const filteredDays = days.filter((day) => {
+    if (dayFilter === 'all') return true;
+    return day.type === dayFilter;
+  });
+
   return (
     <div className="space-y-4">
       {showDaySelector && !compact && (
         <div className="flex items-center justify-between">
           {label && <Label className="text-base">{label}</Label>}
-          <Tooltip>
-            <TooltipTrigger asChild>
-              <Button variant="outline" onClick={copyToAllDays}>
+          <AlertDialog open={showCopyDialog} onOpenChange={setShowCopyDialog}>
+            <AlertDialogTrigger asChild>
+              <Button
+                variant="outline"
+                onClick={handleCopyToAllDays}
+                disabled={pendingCopy}
+              >
                 <Copy className="h-4 w-4" />
                 <span>Copy to all days</span>
               </Button>
-            </TooltipTrigger>
-            <TooltipContent>
-              Copy current day settings to all days
-            </TooltipContent>
-          </Tooltip>
+            </AlertDialogTrigger>
+            <AlertDialogContent>
+              <AlertDialogHeader>
+                <AlertDialogTitle>Copy to all days?</AlertDialogTitle>
+                <AlertDialogDescription>
+                  This will overwrite the hours for all days with the current
+                  day's settings. Are you sure you want to continue?
+                </AlertDialogDescription>
+              </AlertDialogHeader>
+              <AlertDialogFooter>
+                <AlertDialogCancel>Cancel</AlertDialogCancel>
+                <AlertDialogAction
+                  onClick={confirmCopyToAllDays}
+                  disabled={pendingCopy}
+                >
+                  Yes, copy to all days
+                </AlertDialogAction>
+              </AlertDialogFooter>
+            </AlertDialogContent>
+          </AlertDialog>
         </div>
       )}
 
@@ -204,16 +458,18 @@ export function TimeRangePicker({
                     size="sm"
                     className={cn(
                       'h-9 w-9 p-0',
-                      !value?.[key]?.enabled && 'opacity-50'
+                      !safeTimeRanges[key]?.enabled && 'opacity-50'
                     )}
                     onClick={() => setActiveDay(key)}
+                    disabled={!safeTimeRanges[key]?.enabled}
+                    tabIndex={safeTimeRanges[key]?.enabled ? 0 : -1}
                   >
                     {dayLabel}
                   </Button>
                 </TooltipTrigger>
                 <TooltipContent side="bottom">
                   {fullLabel}
-                  {!value?.[key]?.enabled && ' (Disabled)'}
+                  {!safeTimeRanges[key]?.enabled && ' (Disabled)'}
                 </TooltipContent>
               </Tooltip>
             ))}
@@ -234,44 +490,67 @@ export function TimeRangePicker({
               <div className="flex items-center gap-2">
                 <Switch
                   id={`enable-${key}`}
-                  checked={value?.[key]?.enabled || false}
+                  checked={safeTimeRanges[key]?.enabled || false}
                   onCheckedChange={(checked) => handleDayToggle(key, checked)}
                 />
                 <Label
                   htmlFor={`enable-${key}`}
                   className={cn(
                     'font-medium',
-                    !value?.[key]?.enabled && 'text-muted-foreground',
+                    !safeTimeRanges[key]?.enabled && 'text-muted-foreground',
                     compact && 'text-sm'
                   )}
                 >
                   {fullLabel}
                 </Label>
               </div>
-              <Button
-                variant="outline"
-                size="sm"
-                className={cn('gap-1', compact && 'h-6 text-xs')}
-                onClick={() => addTimeBlock(key)}
-                disabled={!value?.[key]?.enabled}
-              >
-                <Plus className={cn('h-3.5 w-3.5', compact && 'h-3 w-3')} />
-                <span className="text-xs">Add Time Block</span>
-              </Button>
+              <Tooltip>
+                <TooltipTrigger asChild>
+                  <span>
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      className={cn('gap-1', compact && 'h-6 text-xs')}
+                      onClick={() => addTimeBlock(key)}
+                      disabled={
+                        !safeTimeRanges[key]?.enabled || !canAddMoreBlocks(key)
+                      }
+                      aria-disabled={
+                        !safeTimeRanges[key]?.enabled || !canAddMoreBlocks(key)
+                      }
+                    >
+                      <Plus
+                        className={cn('h-3.5 w-3.5', compact && 'h-3 w-3')}
+                      />
+                      <span className="text-xs">
+                        {!canAddMoreBlocks(key)
+                          ? 'Maximum blocks reached'
+                          : 'Add Time Block'}
+                      </span>
+                    </Button>
+                  </span>
+                </TooltipTrigger>
+                <TooltipContent>
+                  {!canAddMoreBlocks(key)
+                    ? addBlockDisabledReason(key) ||
+                      'No time left in the day for a 30-minute block'
+                    : 'Add a new time block'}
+                </TooltipContent>
+              </Tooltip>
             </div>
 
-            {/* Time block inputs */}
-            {value?.[key]?.enabled && (
+            {/* Time block inputs with improved visual separation */}
+            {safeTimeRanges[key]?.enabled && (
               <div
                 className={cn(
                   'space-y-2',
                   compact && 'max-h-[150px] overflow-y-auto pr-1'
                 )}
               >
-                {value[key]?.timeBlocks.map((block, blockIndex) => (
+                {safeTimeRanges[key]?.timeBlocks?.map((block, blockIndex) => (
                   <div
                     key={blockIndex}
-                    className="hover:bg-muted/30 flex items-center gap-2 rounded-md border p-2 transition-colors"
+                    className="hover:bg-muted/30 border-muted bg-muted/10 flex items-center gap-2 rounded-md border p-3 shadow-sm transition-colors"
                   >
                     <div className="grid flex-1 grid-cols-2 gap-2">
                       <div className="space-y-1">
@@ -284,7 +563,16 @@ export function TimeRangePicker({
                         <Input
                           id={`${key}-start-${blockIndex}`}
                           type="time"
-                          value={block.startTime}
+                          value={normalizeTimeString(block.startTime)}
+                          min={
+                            blockIndex > 0
+                              ? (safeTimeRanges[key]?.timeBlocks?.[
+                                  blockIndex - 1
+                                ]?.endTime ?? '00:00')
+                              : '00:00'
+                          }
+                          max={block.endTime}
+                          step="60"
                           onChange={(e) =>
                             handleTimeChange(
                               key,
@@ -293,7 +581,17 @@ export function TimeRangePicker({
                               e.target.value
                             )
                           }
-                          className={cn('h-8', compact && 'h-7 text-xs')}
+                          className={cn(
+                            'h-8',
+                            compact && 'h-7 text-xs',
+                            'bg-background text-foreground',
+                            'appearance-none',
+                            'border-muted border',
+                            'focus:ring-primary focus:outline-none focus:ring-2'
+                          )}
+                          autoComplete="off"
+                          spellCheck={false}
+                          inputMode="numeric"
                         />
                       </div>
                       <div className="space-y-1">
@@ -306,7 +604,17 @@ export function TimeRangePicker({
                         <Input
                           id={`${key}-end-${blockIndex}`}
                           type="time"
-                          value={block.endTime}
+                          value={normalizeTimeString(block.endTime)}
+                          min={block.startTime}
+                          max={
+                            blockIndex <
+                            (safeTimeRanges[key]?.timeBlocks?.length ?? 0) - 1
+                              ? (safeTimeRanges[key]?.timeBlocks?.[
+                                  blockIndex + 1
+                                ]?.startTime ?? '23:59')
+                              : '23:59'
+                          }
+                          step="60"
                           onChange={(e) =>
                             handleTimeChange(
                               key,
@@ -315,7 +623,17 @@ export function TimeRangePicker({
                               e.target.value
                             )
                           }
-                          className={cn('h-8', compact && 'h-7 text-xs')}
+                          className={cn(
+                            'h-8',
+                            compact && 'h-7 text-xs',
+                            'bg-background text-foreground',
+                            'appearance-none',
+                            'border-muted border',
+                            'focus:ring-primary focus:outline-none focus:ring-2'
+                          )}
+                          autoComplete="off"
+                          spellCheck={false}
+                          inputMode="numeric"
                         />
                       </div>
                     </div>
@@ -324,7 +642,7 @@ export function TimeRangePicker({
                         variant="ghost"
                         size="icon"
                         onClick={() => removeTimeBlock(key, blockIndex)}
-                        disabled={value[key]?.timeBlocks.length <= 1}
+                        disabled={safeTimeRanges[key]?.timeBlocks?.length <= 1}
                         className={cn('h-8 w-8', compact && 'h-7 w-7')}
                       >
                         <Trash2


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (1)</summary><blockquote>

`105-114`: _⚠️ Potential issue_

**`upsert(...).eq()` is not valid PostgREST syntax**

`eq()` filters are ignored (and currently throw in strict mode) after an `upsert`.  
To target the composite key (`ws_id`, `type`) supply `onConflict` instead:

```diff
-const { error } = await supabase
-  .from('workspace_calendar_hour_settings')
-  .upsert({
-    data: newHours,
-    type: 'PERSONAL',
-    ws_id: wsId,
-  })
-  .eq('ws_id', wsId)
-  .eq('type', 'PERSONAL');
+const { error } = await supabase
+  .from('workspace_calendar_hour_settings')
+  .upsert(
+    {
+      data: newHours,
+      type: 'PERSONAL',
+      ws_id: wsId,
+    },
+    { onConflict: 'ws_id,type' }
+  );
```

Repeat the same adjustment for the Work & Meeting handlers (lines 137-146 and 169-177).

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 112-113: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L112-L113
Added lines #L112 - L113 were not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (2)</summary><blockquote>

`137-146`: Same `upsert + eq` concern as outlined above – please apply the `onConflict` pattern here as well.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 143-145: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L143-L145
Added lines #L143 - L145 were not covered by tests

</details>

</details>

---

`169-177`: Same `upsert + eq` concern as outlined above – please apply the `onConflict` pattern here as well.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 175-177: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L175-L177
Added lines #L175 - L177 were not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (1)</summary><blockquote>

`192-221`: **Mutating `safeTimeRanges` still mutates state**

`newTimeRanges = { ...safeTimeRanges };` is a shallow copy; nested arrays remain shared.  
Adopt the same deep-clone strategy recommended above before modifying `blocks[blockIndex]`.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (1)</summary><blockquote>

`39-41`: **Surface the fetch error to the end-user**

`console.error` is useful for debugging, but silent failures leave the user unaware that hour settings could not be loaded.  Consider surfacing the error through a toast or UI banner so the user knows to retry or contact support.

```diff
-if (error) {
-  console.error('Error fetching hours:', error);
-  return;
-}
+if (error) {
+  console.error('Error fetching hours:', error);
+  toast.error('Failed to load hour settings. Please refresh or try again later.');
+  return;
+}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 39-41: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L39-L41
Added lines #L39 - L41 were not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (2)</summary><blockquote>

`319-335`: **High-frequency re-renders while tracking the cursor**

`handleSlotMouseMove` calls `setCursorPos` on every `mousemove`, causing React to re-render ~60 fps.  
This can be expensive on slower devices.

Two lightweight alternatives:
1. Store `cursorPos` in a `useRef` and position the tooltip with an imperative style update.
2. Throttle the state update with `requestAnimationFrame` or `lodash.throttle`.

Either keeps the tooltip buttery-smooth without spamming React’s reconciler.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 319-325: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L319-L325
Added lines #L319 - L325 were not covered by tests

---

[warning] 327-329: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L327-L329
Added lines #L327 - L329 were not covered by tests

</details>

</details>

---

`434-450`: **Accessibility: Missing `aria-describedby` / focus-trigger support**

The tooltip is purely hover-driven. Keyboard users tabbing onto the slot buttons will not see the tooltip.  
Consider:

```jsx
<button
  aria-describedby={tooltipId}
  onFocus={() => handleSlotMouseEnter('hour')}
  ...
/>
```

and give the tooltip a stable `id={tooltipId}` so screen-readers announce it.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 434-447: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L434-L447
Added lines #L434 - L447 were not covered by tests

---

[warning] 449-450: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L449-L450
Added lines #L449 - L450 were not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (2)</summary><blockquote>

`294-313`: **Copy-to-all button still enabled while the dialog is open**

`pendingCopy` disables the trigger but resets to `false` immediately after `onChange`, so the user can double-click and execute the action multiple times.  
You might instead disable the `<AlertDialogAction>` while `pendingCopy` is `true` and only reset once the parent confirms persistence.

---

`329-338`: **Selector button opacity should also disable focus**

The day buttons at reduced opacity are still focusable via keyboard.  
Add `disabled` or `tabIndex={-1}` when the day is disabled to reflect its interactive state.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 76f609b33759e1ff9513bccac092adc8ee38d4f9 and dbb60fb89f6cd327ba7aa025813a01fe016412b8.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx` (3 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx` (6 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx` (12 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (2)</summary>

* `defaultWeekTimeRanges` (59-67)
* `WeekTimeRanges` (34-42)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx</summary>

[warning] 3-3: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L3
Added line #L3 was not covered by tests

---

[warning] 23-27: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L23-L27
Added lines #L23 - L27 were not covered by tests

---

[warning] 39-41: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L39-L41
Added lines #L39 - L41 were not covered by tests

---

[warning] 44-61: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L44-L61
Added lines #L44 - L61 were not covered by tests

---

[warning] 63-65: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L63-L65
Added lines #L63 - L65 were not covered by tests

---

[warning] 67-70: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L67-L70
Added lines #L67 - L70 were not covered by tests

---

[warning] 72-76: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L72-L76
Added lines #L72 - L76 were not covered by tests

---

[warning] 81-83: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L81-L83
Added lines #L81 - L83 were not covered by tests

---

[warning] 90-90: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L90
Added line #L90 was not covered by tests

---

[warning] 92-92: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L92
Added line #L92 was not covered by tests

---

[warning] 112-113: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L112-L113
Added lines #L112 - L113 were not covered by tests

---

[warning] 116-117: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L116-L117
Added lines #L116 - L117 were not covered by tests

---

[warning] 143-145: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L143-L145
Added lines #L143 - L145 were not covered by tests

---

[warning] 148-149: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L148-L149
Added lines #L148 - L149 were not covered by tests

---

[warning] 175-177: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L175-L177
Added lines #L175 - L177 were not covered by tests

---

[warning] 180-181: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L180-L181
Added lines #L180 - L181 were not covered by tests

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx</summary>

[warning] 24-29: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L24-L29
Added lines #L24 - L29 were not covered by tests

---

[warning] 297-309: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L297-L309
Added lines #L297 - L309 were not covered by tests

---

[warning] 312-313: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L312-L313
Added lines #L312 - L313 were not covered by tests

---

[warning] 315-316: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L315-L316
Added lines #L315 - L316 were not covered by tests

---

[warning] 319-325: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L319-L325
Added lines #L319 - L325 were not covered by tests

---

[warning] 327-329: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L327-L329
Added lines #L327 - L329 were not covered by tests

---

[warning] 331-338: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L331-L338
Added lines #L331 - L338 were not covered by tests

---

[warning] 341-348: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L341-L348
Added lines #L341 - L348 were not covered by tests

---

[warning] 392-392: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L392
Added line #L392 was not covered by tests

---

[warning] 397-414: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L397-L414
Added lines #L397 - L414 were not covered by tests

---

[warning] 419-422: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L419-L422
Added lines #L419 - L422 were not covered by tests

---

[warning] 429-432: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L429-L432
Added lines #L429 - L432 were not covered by tests

---

[warning] 434-447: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L434-L447
Added lines #L434 - L447 were not covered by tests

---

[warning] 449-450: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L449-L450
Added lines #L449 - L450 were not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (1)</summary>

`341-348`: **Resetting tooltip lock on rapid pointer leave / enter**

`tooltipLocked` is reset only on full cell leave. If the user quickly moves from one slot to the other inside the same cell, the tooltip stays locked to the previous slot.  
You might want to unlock when `hoveredSlot` changes after the lock, or provide an ESC shortcut to dismiss.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 341-348: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L341-L348
Added lines #L341 - L348 were not covered by tests

</details>

</details>

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (1)</summary>

`140-157`: _⚠️ Potential issue_

**Off-by-one: you can still add a block that ends at 24:29**

`canAddMoreBlocks` forbids adding when `lastEnd > 1409` (23:29).  
The new block is 30 min, so if the last block ends at exactly 23:29 you’ll create a block finishing at **23:59** – outside the valid range.

```diff
-return lastEnd <= 1409; // 1409 = 23*60+29
+return lastEnd <= 1409 - 30; // ensure there is *at least* 30 min left
```




> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:09:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (2)</summary><blockquote>

`71-74`: **Robust data validation with fallback to defaults**

The implementation effectively validates parsed data using the new type guard function and falls back to defaults when validation fails, preventing runtime errors from corrupt or unexpected data formats.


Consider refactoring these long lines for improved readability:

```diff
-personalHours: isValidWeekTimeRanges(safeParse(data?.find((h) => h.type === 'PERSONAL')?.data)) ? safeParse(data?.find((h) => h.type === 'PERSONAL')?.data) : defaultWeekTimeRanges,
+const parsedPersonalHours = safeParse(data?.find((h) => h.type === 'PERSONAL')?.data);
+personalHours: isValidWeekTimeRanges(parsedPersonalHours) ? parsedPersonalHours : structuredClone(defaultWeekTimeRanges),
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 72-74: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L72-L74
Added lines #L72 - L74 were not covered by tests

</details>

</details>

---

`159-162`: **Consistent upsert pattern across all handlers**

The pattern for upserting data is consistently applied across all handler functions.


Consider extracting this repeated pattern into a reusable helper function:

```typescript
const updateHoursSettings = async (type: 'PERSONAL' | 'WORK' | 'MEETING', newHours: WeekTimeRanges) => {
  const supabase = createClient();
  
  const { error } = await supabase
    .from('workspace_calendar_hour_settings')
    .upsert({
      data: JSON.stringify(newHours),
      type,
      ws_id: wsId,
    }, { onConflict: 'ws_id,type' });
    
  if (error) {
    console.error(`Error updating ${type.toLowerCase()} hours:`, error);
    toast.error(`Failed to update ${type.toLowerCase()} hours`);
    return false;
  }
  
  toast.success(`${type} hours updated`);
  return true;
};
```

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (1)</summary><blockquote>

`485-504`: **Well-implemented accessible tooltip**

The tooltip implementation includes proper ARIA attributes and positioning, making it both visually appealing and accessible.


Consider adding a subtle entrance animation for the tooltip to make the appearance smoother:

```diff
<div
  ref={tooltipRef}
  id={tooltipId}
  role="tooltip"
  aria-live="polite"
  className="pointer-events-none bg-neutral-900 text-white rounded-md px-3 py-1.5 shadow-lg text-xs font-medium"
  style={{
    position: 'fixed',
    left: cursorPosRef.current.x,
    top: cursorPosRef.current.y,
    minWidth: 120,
    maxWidth: 200,
    zIndex: 10000,
    whiteSpace: 'nowrap',
+   opacity: 0,
+   animation: 'fadeIn 0.2s ease-in-out forwards',
  }}
>
```

And add this to your global CSS:

```css
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 485-500: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L485-L500
Added lines #L485 - L500 were not covered by tests

---

[warning] 502-503: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L502-L503
Added lines #L502 - L503 were not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 88fa7976c320e898f42f7e1228d5a6e8a11b51ae and 20b72ea6dbf38fed4f3b1b708e5e239f646aedc8.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx` (4 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx` (1 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx` (7 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx` (9 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (2)</summary>

* `defaultWeekTimeRanges` (59-67)
* `WeekTimeRanges` (34-42)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx</summary>

[warning] 71-72: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L71-L72
Added lines #L71 - L72 were not covered by tests

---

[warning] 81-82: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L81-L82
Added lines #L81 - L82 were not covered by tests

---

[warning] 84-90: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L84-L90
Added lines #L84 - L90 were not covered by tests

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx</summary>

[warning] 24-31: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L24-L31
Added lines #L24 - L31 were not covered by tests

---

[warning] 36-36: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L36
Added line #L36 was not covered by tests

---

[warning] 302-316: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L302-L316
Added lines #L302 - L316 were not covered by tests

---

[warning] 319-320: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L319-L320
Added lines #L319 - L320 were not covered by tests

---

[warning] 322-323: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L322-L323
Added lines #L322 - L323 were not covered by tests

---

[warning] 326-332: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L326-L332
Added lines #L326 - L332 were not covered by tests

---

[warning] 334-355: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L334-L355
Added lines #L334 - L355 were not covered by tests

---

[warning] 358-365: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L358-L365
Added lines #L358 - L365 were not covered by tests

---

[warning] 367-381: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L367-L381
Added lines #L367 - L381 were not covered by tests

---

[warning] 383-386: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L383-L386
Added lines #L383 - L386 were not covered by tests

---

[warning] 414-419: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L414-L419
Added lines #L414 - L419 were not covered by tests

---

[warning] 437-437: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L437
Added line #L437 was not covered by tests

---

[warning] 442-459: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L442-L459
Added lines #L442 - L459 were not covered by tests

---

[warning] 464-470: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L464-L470
Added lines #L464 - L470 were not covered by tests

---

[warning] 477-483: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L477-L483
Added lines #L477 - L483 were not covered by tests

---

[warning] 485-500: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L485-L500
Added lines #L485 - L500 were not covered by tests

---

[warning] 502-503: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L502-L503
Added lines #L502 - L503 were not covered by tests

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx</summary>

[warning] 3-3: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L3
Added line #L3 was not covered by tests

---

[warning] 23-27: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L23-L27
Added lines #L23 - L27 were not covered by tests

---

[warning] 39-42: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L39-L42
Added lines #L39 - L42 were not covered by tests

---

[warning] 45-51: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L45-L51
Added lines #L45 - L51 were not covered by tests

---

[warning] 53-56: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L53-L56
Added lines #L53 - L56 were not covered by tests

---

[warning] 58-61: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L58-L61
Added lines #L58 - L61 were not covered by tests

---

[warning] 63-67: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L63-L67
Added lines #L63 - L67 were not covered by tests

---

[warning] 72-74: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L72-L74
Added lines #L72 - L74 were not covered by tests

---

[warning] 81-81: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L81
Added line #L81 was not covered by tests

---

[warning] 83-83: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L83
Added line #L83 was not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx (1)</summary>

`71-72`: **Improved data validation for localStorage parsed settings**

The addition of validation before returning parsed localStorage data is a good defensive programming practice that prevents potential runtime errors if corrupted or malformed data is encountered.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 71-72: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L71-L72
Added lines #L71 - L72 were not covered by tests

</details>

</details>

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (6)</summary>

`39-42`: **Enhanced error handling with user feedback**

Good improvement to add error toast notifications when fetch operations fail, helping users understand when there are issues loading data.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 39-42: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L39-L42
Added lines #L39 - L42 were not covered by tests

</details>

</details>

---

`45-61`: **Well-implemented default settings initialization**

Excellent approach to handle the case where no settings exist in the database. The use of `structuredClone` to create deep copies prevents shared references between different hour types, addressing the issue raised in a previous review.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 45-51: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L45-L51
Added lines #L45 - L51 were not covered by tests

---

[warning] 53-56: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L53-L56
Added lines #L53 - L56 were not covered by tests

---

[warning] 58-61: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L58-L61
Added lines #L58 - L61 were not covered by tests

</details>

</details>

---

`99-102`: **Properly stringified data for database storage**

Using `JSON.stringify` ensures the object is properly serialized before storing in the database.

---

`129-132`: **Specified conflict resolution in upsert operation**

Added `onConflict` parameter properly specifies which fields to match when determining if a record should be updated or inserted.

---

`302-311`: **Well-implemented type guard for WeekTimeRanges**

The type guard effectively validates that the parsed object matches the expected structure of `WeekTimeRanges`, checking for required properties and their types.

---

`314-323`: **Safe JSON parsing with error handling**

The `safeParse` helper function properly handles potential JSON parsing errors without throwing exceptions, returning undefined instead when parsing fails.

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (8)</summary>

`24-31`: **Enhanced UI with delayed tooltips and hover states**

Added state variables and refs to implement a more sophisticated hover experience with delayed tooltips and progressive information display.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 24-31: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L24-L31
Added lines #L24 - L31 were not covered by tests

</details>

</details>

---

`36-36`: **Improved accessibility with stable tooltip ID**

Creating a stable ID for the tooltip and linking it via aria-describedby improves accessibility for screen reader users.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 36-36: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L36
Added line #L36 was not covered by tests

</details>

</details>

---

`302-316`: **Well-implemented delayed tooltip behavior**

The implementation uses a timeout to delay showing the full tooltip for 1 second, which prevents tooltips from appearing too quickly during normal browsing.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 302-316: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L302-L316
Added lines #L302 - L316 were not covered by tests

</details>

</details>

---

`326-332`: **Performance optimization with requestAnimationFrame**

Using requestAnimationFrame for tooltip positioning provides smooth updates while preventing excessive DOM updates.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 326-332: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L326-L332
Added lines #L326 - L332 were not covered by tests

</details>

</details>

---

`334-355`: **Smart tooltip positioning to handle screen edges**

The tooltip positioning logic intelligently handles edge cases to ensure tooltips remain within the viewport, providing a better user experience.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 334-355: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L334-L355
Added lines #L334 - L355 were not covered by tests

</details>

</details>

---

`367-381`: **Improved keyboard accessibility**

Adding focus handlers that mirror mouse event behavior ensures the component is accessible to keyboard users, which is an important accessibility improvement.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 367-381: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L367-L381
Added lines #L367 - L381 were not covered by tests

</details>

</details>

---

`414-419`: **Fixed timeout cleanup on component unmount**

This cleanup effect properly clears timeouts and animation frames when the component unmounts, addressing the issue mentioned in previous review comments and preventing potential memory leaks.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 414-419: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L414-L419
Added lines #L414 - L419 were not covered by tests

</details>

</details>

---

`442-459`: **Enhanced progressive information display**

The conditional rendering shows either the hovered label initially or both labels after the delay, providing a clean, progressive disclosure of information to the user.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 442-459: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L442-L459
Added lines #L442 - L459 were not covered by tests

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:49:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx (1)</summary><blockquote>

`80-95`: **Type guard implementation is solid but needs tests**

The type guard implementation is pragmatic, focusing on critical object properties rather than deep validation. This balances safety and performance well.

However, as noted in the previous review, unit tests are still needed for this validation logic.


```shell
#!/bin/bash
# Check if there are tests for this file or function
rg -A 3 "isValidPartialCalendarSettings.*test" --type ts
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 81-84: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L81-L84
Added lines #L81 - L84 were not covered by tests

---

[warning] 86-95: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L86-L95
Added lines #L86 - L95 were not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (1)</summary><blockquote>

`27-31`: _🛠️ Refactor suggestion_

**Shared object references in initial state**

The initial state still uses the same object reference for all three hour types, which could lead to unintended mutations affecting multiple hour types if the object is directly modified.


```diff
const [value, setValue] = useState<HoursSettingsData>({
-  personalHours: defaultWeekTimeRanges,
-  workHours: defaultWeekTimeRanges,
-  meetingHours: defaultWeekTimeRanges,
+  personalHours: structuredClone(defaultWeekTimeRanges),
+  workHours: structuredClone(defaultWeekTimeRanges),
+  meetingHours: structuredClone(defaultWeekTimeRanges),
});
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 27-31: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L27-L31
Added lines #L27 - L31 were not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6da7dc497229ef28bbf3a8d246faf79d169431f4 and 63a3d09ca9123a2bf30ebbcc7adadda338891071.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx` (4 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx` (1 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx` (7 hunks)
* `packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx` (11 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (2)</summary>

* `defaultWeekTimeRanges` (59-67)
* `WeekTimeRanges` (34-42)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx</summary>

[warning] 24-33: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L24-L33
Added lines #L24 - L33 were not covered by tests

---

[warning] 38-38: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L38
Added line #L38 was not covered by tests

---

[warning] 304-318: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L304-L318
Added lines #L304 - L318 were not covered by tests

---

[warning] 321-322: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L321-L322
Added lines #L321 - L322 were not covered by tests

---

[warning] 324-325: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L324-L325
Added lines #L324 - L325 were not covered by tests

---

[warning] 328-334: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L328-L334
Added lines #L328 - L334 were not covered by tests

---

[warning] 336-359: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L336-L359
Added lines #L336 - L359 were not covered by tests

---

[warning] 362-369: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L362-L369
Added lines #L362 - L369 were not covered by tests

---

[warning] 371-385: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L371-L385
Added lines #L371 - L385 were not covered by tests

---

[warning] 387-390: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L387-L390
Added lines #L387 - L390 were not covered by tests

---

[warning] 418-423: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L418-L423
Added lines #L418 - L423 were not covered by tests

---

[warning] 441-441: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L441
Added line #L441 was not covered by tests

---

[warning] 446-463: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L446-L463
Added lines #L446 - L463 were not covered by tests

---

[warning] 468-474: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L468-L474
Added lines #L468 - L474 were not covered by tests

---

[warning] 481-487: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L481-L487
Added lines #L481 - L487 were not covered by tests

---

[warning] 489-504: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L489-L504
Added lines #L489 - L504 were not covered by tests

---

[warning] 506-507: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L506-L507
Added lines #L506 - L507 were not covered by tests

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx</summary>

[warning] 71-72: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L71-L72
Added lines #L71 - L72 were not covered by tests

---

[warning] 81-84: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L81-L84
Added lines #L81 - L84 were not covered by tests

---

[warning] 86-95: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L86-L95
Added lines #L86 - L95 were not covered by tests

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx</summary>

[warning] 3-3: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L3
Added line #L3 was not covered by tests

---

[warning] 27-31: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L27-L31
Added lines #L27 - L31 were not covered by tests

---

[warning] 43-48: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L43-L48
Added lines #L43 - L48 were not covered by tests

---

[warning] 51-69: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L51-L69
Added lines #L51 - L69 were not covered by tests

---

[warning] 71-74: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L71-L74
Added lines #L71 - L74 were not covered by tests

---

[warning] 76-79: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L76-L79
Added lines #L76 - L79 were not covered by tests

---

[warning] 81-85: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L81-L85
Added lines #L81 - L85 were not covered by tests

---

[warning] 90-104: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L90-L104
Added lines #L90 - L104 were not covered by tests

---

[warning] 132-139: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L132-L139
Added lines #L132 - L139 were not covered by tests

---

[warning] 142-143: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L142-L143
Added lines #L142 - L143 were not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (10)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: build (22)
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Verify generated types
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Prettier Check (23)
* GitHub Check: Deploy-Preview

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx (1)</summary>

`71-72`: **Improved security for localStorage data validation**

Good use of the new type guard to validate parsed data before using it. This prevents potential runtime errors if localStorage contains corrupted or malformed data.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 71-72: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L71-L72
Added lines #L71 - L72 were not covered by tests

</details>

</details>

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx (4)</summary>

`43-48`: **Improved error handling with user feedback**

Good addition of error logging and toast notifications to inform users when hour settings fail to load.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 43-48: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L43-L48
Added lines #L43 - L48 were not covered by tests

</details>

</details>

---

`50-86`: **Well-implemented fallback for missing data**

The code now properly handles the case when no settings exist in the database by creating defaults with proper deep cloning via `structuredClone`. This prevents the shared reference issues that were previously flagged.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 51-69: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L51-L69
Added lines #L51 - L69 were not covered by tests

---

[warning] 71-74: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L71-L74
Added lines #L71 - L74 were not covered by tests

---

[warning] 76-79: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L76-L79
Added lines #L76 - L79 were not covered by tests

---

[warning] 81-85: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L81-L85
Added lines #L81 - L85 were not covered by tests

</details>

</details>

---

`90-105`: **Robust data validation and fallback**

Good use of type guards and safe parsing to validate fetched data before using it. The conditional checks ensure corrupt data doesn't break the application and fallback to defaults when needed.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 90-104: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L90-L104
Added lines #L90 - L104 were not covered by tests

</details>

</details>

---

`132-139`: **Improved upsert operations with explicit conflict handling**

The upsert operations now use explicit conflict resolution with proper JSON stringification. This ensures data integrity in concurrent update scenarios.



Also applies to: 165-172, 198-205

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 132-139: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L132-L139
Added lines #L132 - L139 were not covered by tests

</details>

</details>

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (6)</summary>

`24-33`: **Well-structured state management for improved tooltips**

Good addition of state variables to track hover state, tooltip visibility, and timing. This separates concerns and makes the component's behavior more maintainable.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 24-33: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L24-L33
Added lines #L24 - L33 were not covered by tests

</details>

</details>

---

`38-38`: **Improved accessibility with stable tooltip ID**

Good addition of a stable tooltip ID for aria-describedby attributes, which improves accessibility for screen readers.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 38-38: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L38
Added line #L38 was not covered by tests

</details>

</details>

---

`303-390`: **Comprehensive mouse interaction handlers with improved UX**

Well-implemented handlers for mouse enter, leave, move, focus, and blur events. The delayed tooltip display with locking mechanism provides a better user experience by reducing tooltip flicker and providing more stable visual feedback.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 304-318: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L304-L318
Added lines #L304 - L318 were not covered by tests

---

[warning] 321-322: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L321-L322
Added lines #L321 - L322 were not covered by tests

---

[warning] 324-325: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L324-L325
Added lines #L324 - L325 were not covered by tests

---

[warning] 328-334: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L328-L334
Added lines #L328 - L334 were not covered by tests

---

[warning] 336-359: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L336-L359
Added lines #L336 - L359 were not covered by tests

---

[warning] 362-369: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L362-L369
Added lines #L362 - L369 were not covered by tests

---

[warning] 371-385: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L371-L385
Added lines #L371 - L385 were not covered by tests

---

[warning] 387-390: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L387-L390
Added lines #L387 - L390 were not covered by tests

</details>

</details>

---

`418-423`: **Proper cleanup of timers and animation frames**

Excellent addition of cleanup for both timeout and animation frame references in the useEffect cleanup function. This addresses the previous review comment about preventing memory leaks and state updates after unmount.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 418-423: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L418-L423
Added lines #L418 - L423 were not covered by tests

</details>

</details>

---

`446-463`: **Improved conditional rendering of time labels**

Good implementation of conditional rendering that shows either the hovered label or both labels after a delay. This creates a cleaner UI while still providing all necessary information when needed.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 446-463: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L446-L463
Added lines #L446 - L463 were not covered by tests

</details>

</details>

---

`489-508`: **Accessible tooltip with proper ARIA attributes**

Well-implemented tooltip with proper ARIA attributes and positioning logic. The fixed positioning with viewport boundary checks ensures the tooltip is always visible and doesn't get cut off at screen edges.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 489-504: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L489-L504
Added lines #L489 - L504 were not covered by tests

---

[warning] 506-507: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L506-L507
Added lines #L506 - L507 were not covered by tests

</details>

</details>

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx (10)</summary>

`3-13`: **Added confirmation dialog for destructive actions**

Good use of AlertDialog for the "Copy to all days" action, preventing accidental overwrites of existing configurations. This improves user experience by requiring confirmation for potentially destructive actions.



Also applies to: 97-98

---

`79-86`: **Useful time normalization utility**

Well-implemented utility function for normalizing time strings to ensure consistent HH:mm format. This handles various edge cases like invalid inputs and missing parts gracefully.

---

`104-171`: **Comprehensive data validation with safe defaults**

Excellent implementation of `safeTimeRanges` that deeply validates and normalizes the time range data structure. This prevents null/undefined errors and ensures each day has valid data, while properly cloning objects to avoid shared references.

---

`174-185`: **Useful time conversion utilities**

Well-implemented utilities for converting between time strings and minutes, which simplifies comparison logic throughout the component. This makes the time manipulation code more robust and readable.

---

`188-234`: **Thorough time range validation**

Comprehensive validation function that checks for multiple constraints:
- End time must be after start time
- Minimum duration of 30 minutes
- Time boundaries (0:00 to 23:59)
- No overlaps with adjacent blocks

The addition of auto-correction suggestions is particularly useful for improving user experience.

---

`259-289`: **Improved time block addition with validation**

The block addition logic now includes comprehensive validation, boundary checks, and user feedback via toast messages. This prevents creating invalid time blocks while guiding users with helpful error messages.

---

`291-335`: **Robust time change handling with auto-correction**

Excellent implementation of time change handling that validates inputs, prevents overlaps, and provides auto-correction with appropriate feedback. This makes the time picker much more user-friendly and resilient against invalid inputs.

---

`366-388`: **Improved copy functionality with confirmation**

The reworked copy functionality with confirmation dialog prevents accidental data loss. Good approach to preserve each day's enabled status while copying only the time blocks, which maintains user preferences.

---

`507-539`: **Enhanced UI feedback with tooltips**

Good addition of tooltips to explain disabled states and provide context for actions. This improves discoverability and helps users understand why certain actions might be unavailable.

---

`584-595`: **Improved input styling and attributes**

Enhanced input attributes with min/max constraints, proper classes for styling, and additional attributes like autoComplete and spellCheck. These improvements make the inputs more user-friendly and visually consistent.



Also applies to: 626-637

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T21:17:01Z
user: login: vhpx
body: LGTM! Thanks @Adinorio.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-08T21:17:50Z

Review comments:
diff_hunk: @@ -36,30 +36,60 @@
         .eq('ws_id', wsId);
 
       if (error) {
-        console.error(error);
+        console.error('Error fetching hours:', error);
+        return;
+      }
+
+      // If no data exists, create default settings
+      if (!data || data.length === 0) {
+        const defaultSettings: { type: "WORK" | "PERSONAL" | "MEETING"; data: WeekTimeRanges; ws_id: string }[] = [
+          {
+            type: "PERSONAL",
+            data: defaultWeekTimeRanges,
+            ws_id: wsId,
+          },
+          {
+            type: "WORK",
+            data: defaultWeekTimeRanges,
+            ws_id: wsId,
+          },
+          {
+            type: "MEETING",
+            data: defaultWeekTimeRanges,
+            ws_id: wsId,
+          },
+        ];
path: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Avoid sharing the same `defaultWeekTimeRanges` object instance**

`defaultSettings` re-uses the imported constant directly.  Because React state later mutates nested `timeBlocks`, all three DB rows – and the in-memory defaults – end up sharing the same object reference, so editing “Work” hours can inadvertently mutate “Personal” hours.

Clone the structure (deeply) for every row:

```diff
-const defaultSettings = [
-  {
-    type: "PERSONAL",
-    data: defaultWeekTimeRanges,
-    ws_id: wsId,
-  },
+const makeDefaultData = () => structuredClone(defaultWeekTimeRanges);
+const defaultSettings = [
+  {
+    type: "PERSONAL",
+    data: makeDefaultData(),
+    ws_id: wsId,
+  },
   ...
 ]
```
Use `structuredClone`, `JSON.parse(JSON.stringify())`, or a dedicated helper to ensure rows are fully independent.  



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 44-61: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L44-L61
Added lines #L44 - L61 were not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
around lines 45 to 61, the defaultSettings array assigns the same
defaultWeekTimeRanges object to each type, causing shared references and
unintended mutations. To fix this, deep clone defaultWeekTimeRanges for each
entry using structuredClone, JSON.parse(JSON.stringify()), or a similar deep
copy method so that each type has an independent copy and mutations do not
affect others.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit c41d2df
created_at: 2025-05-08T16:09:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,6 +293,60 @@
     [isDragging, date, yToTime, addEmptyEventWithDuration]
   );
 
+  // Helper to handle mouse enter for each slot
+  const handleSlotMouseEnter = (slot: 'hour' | 'half-hour') => {
+    setHoveredSlot(slot);
+    if (!tooltipLocked) {
+      setShowBothLabels(false);
+      setShowTooltip(false);
+      if (hoverTimeout.current) clearTimeout(hoverTimeout.current);
+      hoverTimeout.current = setTimeout(() => {
+        setShowBothLabels(true);
+        setShowTooltip(true);
+        setTooltipLocked(true);
+      }, 1000);
+    }
+  };
path: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Timeout is never cleared on component unmount**

`hoverTimeout.current` is only cleared on `handleCellMouseLeave`, so navigating away while the timer is active will try to update state on an un-mounted component and trigger a warning.

```diff
 useEffect(() => {
   ...
   return () => {
+    if (hoverTimeout.current) clearTimeout(hoverTimeout.current); // 💡 clean-up
     cell.removeEventListener('mousedown', handleMouseDown);
     ...
   };
 }, [handleMouseDown, handleMouseMove, handleMouseUp]);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
useEffect(() => {
  // …existing setup…
  cell.addEventListener('mousedown', handleMouseDown);
  cell.addEventListener('mousemove', handleMouseMove);
  cell.addEventListener('mouseup', handleMouseUp);

  return () => {
    // 💡 clean up any pending hover timeout on unmount
    if (hoverTimeout.current) {
      clearTimeout(hoverTimeout.current);
    }

    cell.removeEventListener('mousedown', handleMouseDown);
    cell.removeEventListener('mousemove', handleMouseMove);
    cell.removeEventListener('mouseup', handleMouseUp);
  };
}, [handleMouseDown, handleMouseMove, handleMouseUp]);
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 297-309: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx#L297-L309
Added lines #L297 - L309 were not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx around lines
297 to 309, the hoverTimeout timer is set but never cleared on component
unmount, which can cause state updates on an unmounted component. To fix this,
add a cleanup function in a useEffect hook that clears hoverTimeout.current when
the component unmounts, preventing any pending timeouts from triggering state
updates after unmount.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit c41d2df
created_at: 2025-05-08T16:09:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,46 +84,144 @@
   compact = false,
 }: TimeRangePickerProps) {
   const [activeDay, setActiveDay] = useState<keyof WeekTimeRanges>('monday');
+  const [showCopyDialog, setShowCopyDialog] = useState(false);
+  const [pendingCopy, setPendingCopy] = useState(false);
 
-  const days: Array<{
-    key: keyof WeekTimeRanges;
-    label: string;
-    fullLabel: string;
-    type: 'weekday' | 'weekend';
-  }> = [
-    { key: 'monday', label: 'M', fullLabel: 'Monday', type: 'weekday' },
-    { key: 'tuesday', label: 'T', fullLabel: 'Tuesday', type: 'weekday' },
-    { key: 'wednesday', label: 'W', fullLabel: 'Wednesday', type: 'weekday' },
-    { key: 'thursday', label: 'T', fullLabel: 'Thursday', type: 'weekday' },
-    { key: 'friday', label: 'F', fullLabel: 'Friday', type: 'weekday' },
-    { key: 'saturday', label: 'S', fullLabel: 'Saturday', type: 'weekend' },
-    { key: 'sunday', label: 'S', fullLabel: 'Sunday', type: 'weekend' },
-  ];
+  // Use defaultWeekTimeRanges if value is null
+  const timeRanges = value || defaultWeekTimeRanges;
 
-  // Filter days based on dayFilter
-  const filteredDays = days.filter((day) => {
-    if (dayFilter === 'all') return true;
-    return day.type === dayFilter;
-  });
+  // Ensure each day has a valid timeBlocks array
+  const safeTimeRanges = Object.entries(timeRanges).reduce((acc, [day, dayRange]) => {
+    acc[day as keyof WeekTimeRanges] = {
+      enabled: dayRange?.enabled ?? false,
+      timeBlocks: dayRange?.timeBlocks ?? [{ ...defaultTimeBlock }],
+    };
+    return acc;
+  }, {} as WeekTimeRanges);
+
+  // Helper to convert time string to minutes
+  const timeToMinutes = (t: string) => {
+    const [h, m] = t.split(':').map(Number);
+    return h * 60 + m;
+  };
+  // Helper to convert minutes to time string
+  const minutesToTime = (min: number) => {
+    const h = Math.floor(min / 60);
+    const m = min % 60;
+    return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
+  };
+
+  // Validation for a single block
+  const validateTimeRange = (startTime: string, endTime: string, prevEnd?: string, nextStart?: string): { isValid: boolean; message?: string; correctedEnd?: string; correctedStart?: string } => {
+    const startMin = timeToMinutes(startTime);
+    const endMin = timeToMinutes(endTime);
+    if (endMin <= startMin) {
+      return { isValid: false, message: 'End time must be after start time' };
+    }
+    if (endMin - startMin < 30) {
+      return { isValid: false, message: 'Time block must be at least 30 minutes' };
+    }
+    if (startMin < 0) {
+      return { isValid: false, message: 'Start time cannot be before 12:00 AM' };
+    }
+    if (endMin > 1439) {
+      return { isValid: false, message: 'End time cannot be after 11:59 PM' };
+    }
+    if (prevEnd && startMin < timeToMinutes(prevEnd)) {
+      return { isValid: false, message: 'Start time overlaps with previous block', correctedStart: prevEnd };
+    }
+    if (nextStart && endMin > timeToMinutes(nextStart)) {
+      return { isValid: false, message: 'End time overlaps with next block', correctedEnd: nextStart };
+    }
+    return { isValid: true };
+  };
+
+  // Can we add another block?
+  const canAddMoreBlocks = (day: keyof WeekTimeRanges) => {
+    const blocks = safeTimeRanges[day]?.timeBlocks || [];
+    if (blocks.length === 0) return true;
+    const lastBlock = blocks[blocks.length - 1];
+    const lastEnd = timeToMinutes(lastBlock.endTime);
+    // If less than 30 min left in the day, can't add
+    return lastEnd <= 1409; // 1409 = 23*60+29
+  };
+
+  // Reason for disabling add button
+  const addBlockDisabledReason = (day: keyof WeekTimeRanges) => {
+    const blocks = safeTimeRanges[day]?.timeBlocks || [];
+    if (blocks.length === 0) return '';
+    const lastBlock = blocks[blocks.length - 1];
+    const lastEnd = timeToMinutes(lastBlock.endTime);
+    if (lastEnd > 1409) return 'No time left in the day for a 30-minute block';
+    return '';
+  };
+
+  // Add a new time block
+  const addTimeBlock = (day: keyof WeekTimeRanges) => {
+    const newTimeRanges = { ...safeTimeRanges };
+    const blocks = newTimeRanges[day]?.timeBlocks || [];
+    const lastBlock = blocks[blocks.length - 1];
+    let newStartMin = lastBlock ? timeToMinutes(lastBlock.endTime) : 540; // 09:00
+    let newEndMin = newStartMin + 30;
+    if (newEndMin > 1439) newEndMin = 1439; // Clamp to 11:59 PM
+    if (newEndMin - newStartMin < 30) {
+      toast.error('Not enough time left in the day for a 30-minute block');
+      return;
+    }
+    const newStartTime = minutesToTime(newStartMin);
+    const newEndTime = minutesToTime(newEndMin);
+    // Prevent overlap
+    if (blocks.length > 0 && newStartTime < blocks[blocks.length - 1].endTime) {
+      toast.error('New block overlaps with previous block');
+      return;
+    }
path: packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**String comparison used for overlap check**

`newStartTime < blocks[...].endTime` compares **strings**, not times.  
`'10:00' < '9:30' // true`, so valid schedules may be rejected.

Reuse `timeToMinutes`:

```diff
-if (blocks.length > 0 && newStartTime < blocks[blocks.length - 1].endTime) {
+if (
+  blocks.length > 0 &&
+  timeToMinutes(newStartTime) < timeToMinutes(blocks[blocks.length - 1].endTime)
+) {
```

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
between lines 159 and 177, the overlap check compares time strings directly,
which can lead to incorrect results due to lexicographical comparison. To fix
this, convert both newStartTime and the last block's endTime to minutes using
timeToMinutes before comparing, ensuring a proper numerical comparison of times.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit c41d2df
created_at: 2025-05-08T16:09:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,46 +84,144 @@
   compact = false,
 }: TimeRangePickerProps) {
   const [activeDay, setActiveDay] = useState<keyof WeekTimeRanges>('monday');
+  const [showCopyDialog, setShowCopyDialog] = useState(false);
+  const [pendingCopy, setPendingCopy] = useState(false);
 
-  const days: Array<{
-    key: keyof WeekTimeRanges;
-    label: string;
-    fullLabel: string;
-    type: 'weekday' | 'weekend';
-  }> = [
-    { key: 'monday', label: 'M', fullLabel: 'Monday', type: 'weekday' },
-    { key: 'tuesday', label: 'T', fullLabel: 'Tuesday', type: 'weekday' },
-    { key: 'wednesday', label: 'W', fullLabel: 'Wednesday', type: 'weekday' },
-    { key: 'thursday', label: 'T', fullLabel: 'Thursday', type: 'weekday' },
-    { key: 'friday', label: 'F', fullLabel: 'Friday', type: 'weekday' },
-    { key: 'saturday', label: 'S', fullLabel: 'Saturday', type: 'weekend' },
-    { key: 'sunday', label: 'S', fullLabel: 'Sunday', type: 'weekend' },
-  ];
+  // Use defaultWeekTimeRanges if value is null
+  const timeRanges = value || defaultWeekTimeRanges;
 
-  // Filter days based on dayFilter
-  const filteredDays = days.filter((day) => {
-    if (dayFilter === 'all') return true;
-    return day.type === dayFilter;
-  });
+  // Ensure each day has a valid timeBlocks array
+  const safeTimeRanges = Object.entries(timeRanges).reduce((acc, [day, dayRange]) => {
+    acc[day as keyof WeekTimeRanges] = {
+      enabled: dayRange?.enabled ?? false,
+      timeBlocks: dayRange?.timeBlocks ?? [{ ...defaultTimeBlock }],
+    };
+    return acc;
+  }, {} as WeekTimeRanges);
+
path: packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**State mutation risk – deep-clone `timeBlocks`**

`safeTimeRanges` shallow-copies the day objects but **retains the original `timeBlocks` array reference**.  
Downstream mutations (`push`, `splice`) therefore mutate the prop object coming from the parent, breaking React’s one-way-data-flow and causing “state updated during render” edge-cases.

Clone at both levels:

```diff
 const safeTimeRanges = Object.fromEntries(
   Object.entries(timeRanges).map(([day, range]) => [
     day,
-    {
-      enabled: range?.enabled ?? false,
-      timeBlocks: range?.timeBlocks ?? [{ ...defaultTimeBlock }],
-    },
+    {
+      enabled: range?.enabled ?? false,
+      timeBlocks: (range?.timeBlocks ?? [defaultTimeBlock]).map((b) => ({ ...b })),
+    },
   ])
 ) as WeekTimeRanges;
```



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/ui/src/components/ui/legacy/calendar/settings/TimeRangePicker.tsx
around lines 93 to 101, the current code shallow-copies day objects but retains
the original timeBlocks array reference, risking mutation of props from the
parent. To fix this, deep-clone the timeBlocks array by creating a new array
with cloned objects for each day, ensuring no references to the original arrays
remain. This prevents unintended mutations and maintains React's one-way data
flow.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit c41d2df
created_at: 2025-05-08T16:09:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,14 +68,27 @@
     const storedSettings = localStorage.getItem('calendarSettings');
     if (storedSettings) {
       console.log('Loading settings from localStorage');
-      return JSON.parse(storedSettings) as Partial<CalendarSettings>;
+      const parsed = JSON.parse(storedSettings);
+      return isValidPartialCalendarSettings(parsed) ? parsed : null;
     }
   } catch (error) {
     console.error('Failed to load settings from localStorage:', error);
   }
   return null;
 };
 
+// Type guard for Partial<CalendarSettings>
+function isValidPartialCalendarSettings(obj: any): obj is Partial<CalendarSettings> {
+  if (!obj || typeof obj !== 'object') return false;
+  // Only check a few critical keys for safety
+  if ('personalHours' in obj && typeof obj.personalHours !== 'object') return false;
+  if ('workHours' in obj && typeof obj.workHours !== 'object') return false;
+  if ('meetingHours' in obj && typeof obj.meetingHours !== 'object') return false;
+  if ('appearance' in obj && typeof obj.appearance !== 'object') return false;
+  if ('notifications' in obj && typeof obj.notifications !== 'object') return false;
+  return true;
+}
path: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Well-implemented type guard for calendar settings**

The type guard function implements a pragmatic approach by focusing on critical properties rather than performing deep validation. This balances safety and performance.


However, consider adding unit tests for this validation logic since static analysis indicates these lines aren't covered by tests.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are tests for this file or function
rg -A 3 "isValidPartialCalendarSettings" --type ts
rg -A 3 "loadSettingsFromStorage.*test" --type ts
```

Length of output: 1207

---



</details>

**Add unit tests for `isValidPartialCalendarSettings`**

The type guard in `packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx` (lines 80–90) is solid, but it isn’t covered by any existing tests. Please add unit tests to cover at least:

- A valid partial settings object returns `true`
- Non-object or `null` inputs return `false`
- Objects with critical keys of the wrong type return `false`

You can place these in a new test file alongside the context—e.g.  
`packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.test.tsx`

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 81-82: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L81-L82
Added lines #L81 - L82 were not covered by tests

---

[warning] 84-90: packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx#L84-L90
Added lines #L84 - L90 were not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In
packages/ui/src/components/ui/legacy/calendar/settings/CalendarSettingsContext.tsx
around lines 80 to 90, the type guard function isValidPartialCalendarSettings
lacks unit test coverage. Add a new test file named
CalendarSettingsContext.test.tsx in the same directory and write tests that
verify the function returns true for valid partial settings objects, false for
non-object or null inputs, and false for objects where critical keys have
incorrect types.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T16:49:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,11 +20,11 @@
 };
 
 export function HoursSettings({ wsId }: HoursSettingsProps) {
-  const [value, setValue] = useState<{
-    personalHours?: WeekTimeRanges | null;
-    workHours?: WeekTimeRanges | null;
-    meetingHours?: WeekTimeRanges | null;
-  } | null>(null);
+  const [value, setValue] = useState<HoursSettingsData>({
+    personalHours: defaultWeekTimeRanges,
+    workHours: defaultWeekTimeRanges,
+    meetingHours: defaultWeekTimeRanges,
+  });
path: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Potential issue with shared object references in initial state**

The initial state uses the same `defaultWeekTimeRanges` object reference for all three hour types, which could lead to unintended mutations affecting multiple hour types if the object is directly modified.


Create independent copies for each type:

```diff
const [value, setValue] = useState<HoursSettingsData>({
-  personalHours: defaultWeekTimeRanges,
-  workHours: defaultWeekTimeRanges,
-  meetingHours: defaultWeekTimeRanges,
+  personalHours: structuredClone(defaultWeekTimeRanges),
+  workHours: structuredClone(defaultWeekTimeRanges),
+  meetingHours: structuredClone(defaultWeekTimeRanges),
});
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const [value, setValue] = useState<HoursSettingsData>({
    personalHours: structuredClone(defaultWeekTimeRanges),
    workHours: structuredClone(defaultWeekTimeRanges),
    meetingHours: structuredClone(defaultWeekTimeRanges),
  });
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 23-27: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx#L23-L27
Added lines #L23 - L27 were not covered by tests

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
around lines 23 to 27, the initial state assigns the same defaultWeekTimeRanges
object reference to personalHours, workHours, and meetingHours, which risks
unintended shared mutations. To fix this, create independent copies of
defaultWeekTimeRanges for each hour type by cloning or deep copying the object
so that modifying one does not affect the others.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T16:49:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -275,3 +341,35 @@
     </div>
   );
 }
+
+// Type guard for WeekTimeRanges
+function isValidWeekTimeRanges(obj: any): obj is WeekTimeRanges {
+  if (!obj || typeof obj !== 'object') return false;
+  const days = [
+    'monday',
+    'tuesday',
+    'wednesday',
+    'thursday',
+    'friday',
+    'saturday',
+    'sunday',
+  ];
+  return days.every(
+    (day) =>
+      obj[day] &&
+      typeof obj[day].enabled === 'boolean' &&
+      Array.isArray(obj[day].timeBlocks)
+  );
+}
+
+// Safe JSON parse helper
+function safeParse(data: any): any {
+  if (typeof data === 'string') {
+    try {
+      return JSON.parse(data);
+    } catch {
+      return undefined;
+    }
+  }
+  return data;
+}
path: packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Well-structured validation helpers**

The added type guard and safe parsing functions are well-implemented. The type guard performs comprehensive validation of the `WeekTimeRanges` structure, checking for all required properties and their types.

Consider adding unit tests for these validation helpers to ensure they work correctly in all scenarios.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are tests for these functions
rg -A 3 "isValidWeekTimeRanges.*test" --type ts
rg -A 3 "safeParse.*test" --type ts
```

Length of output: 87

---



</details>

**Add unit tests for `isValidWeekTimeRanges` and `safeParse`**

These two helpers are well-implemented, but there are currently no tests covering them. Please add unit tests to verify their behavior under various scenarios:

• Location: `packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx`  
• Tests for `isValidWeekTimeRanges`:  
  - Returns `true` for a fully populated, correctly typed `WeekTimeRanges` object.  
  - Returns `false` when any day is missing, has `enabled` of the wrong type, or `timeBlocks` is not an array.  
• Tests for `safeParse`:  
  - Parses valid JSON strings correctly.  
  - Returns `undefined` for invalid JSON strings.  
  - Returns non‐string inputs unchanged.

Consider placing tests alongside the component (e.g. `HoursSettings.test.tsx`) or in a dedicated `__tests__` folder.

<details>
<summary>🤖 Prompt for AI Agents (early access)</summary>

```
In packages/ui/src/components/ui/legacy/calendar/settings/HoursSettings.tsx
around lines 345 to 375, add unit tests for the isValidWeekTimeRanges and
safeParse helper functions. For isValidWeekTimeRanges, write tests that confirm
it returns true for a complete and correctly typed WeekTimeRanges object, and
false when any day is missing, has an incorrect enabled type, or timeBlocks is
not an array. For safeParse, write tests that verify it correctly parses valid
JSON strings, returns undefined for invalid JSON strings, and returns non-string
inputs unchanged. Place these tests either in HoursSettings.test.tsx or a
dedicated __tests__ folder near the component.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-08T21:17:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: graphite-app[bot]
created_at: 2025-05-08T16:02:59Z
body: ## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge-queue</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/tutur3u?ref=merge-queue-instructions-comment&prId=6197304478).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-08T16:03:00Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update refactors and enhances several calendar-related UI components. It introduces delayed tooltips and improved hover states in the calendar cell, robust validation and error handling in the time range picker, and ensures default data initialization and improved error reporting in hour settings. User feedback and UI clarity are notably improved across these components. Additionally, local storage loading of calendar settings now includes validation for data integrity.

## Changes

| File(s)                                                                                  | Change Summary                                                                                                                                                                                                                                                                                                                                                                                        |
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `.../calendar/CalendarCell.tsx`                                                          | Enhanced hover interaction and tooltip logic: added state variables for hovered slot, tooltip visibility, cursor position, and locking; implemented delayed tooltip display; updated mouse event handlers; conditional rendering for labels and tooltip; replaced static hover logic with dynamic, cursor-tracked tooltips.                                        |
| `.../calendar/settings/HoursSettings.tsx`                                                | Changed state initialization to default hour settings; added logic to insert defaults if no data exists; improved error handling and logging; upsert queries now filter by type; added toast error notifications; minor code style and formatting adjustments; introduced `HoursSettingsData` type for structured state.                                         |
| `.../calendar/settings/TimeRangePicker.tsx`                                              | Refactored for validation and robustness: introduced safe state management, comprehensive time block validation, error toasts, improved add/remove logic, confirmation dialog for "Copy to all days", UI improvements (tooltips, input attributes), and safer handling of time ranges. No changes to public API, all changes are internal.                         |
| `.../calendar/settings/CalendarSettingsContext.tsx`                                     | Added validation of parsed localStorage calendar settings data using a new type guard before returning; returns null if validation fails to ensure data integrity.                                                                                                                                                                                                                                   |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant CalendarCell
    participant Tooltip

    User->>CalendarCell: Hover over hour/half-hour slot
    CalendarCell->>CalendarCell: Set hoveredSlot, start 1s timer
    Note over CalendarCell: After 1s delay
    CalendarCell->>CalendarCell: Set showBothLabels, showTooltip, lock tooltip
    CalendarCell->>Tooltip: Render tooltip at cursor position
    User->>CalendarCell: Move mouse within slot
    CalendarCell->>Tooltip: Update tooltip position
    User->>CalendarCell: Leave slot or cell
    CalendarCell->>CalendarCell: Reset hover state, hide tooltip, clear timer
```

```mermaid
sequenceDiagram
    participant User
    participant TimeRangePicker
    participant Toast
    participant AlertDialog

    User->>TimeRangePicker: Change time block start/end
    TimeRangePicker->>TimeRangePicker: Validate time block
    alt Invalid input
        TimeRangePicker->>Toast: Show error message
        TimeRangePicker->>TimeRangePicker: Auto-correct if possible
    end
    User->>TimeRangePicker: Add new time block
    TimeRangePicker->>TimeRangePicker: Check for space/overlap
    alt Cannot add
        TimeRangePicker->>Toast: Show error message
    else Can add
        TimeRangePicker->>TimeRangePicker: Add block
    end
    User->>TimeRangePicker: Click "Copy to all days"
    TimeRangePicker->>AlertDialog: Show confirmation modal
    User->>AlertDialog: Confirm
    AlertDialog->>TimeRangePicker: Copy blocks to all days
```

## Suggested reviewers

- vhpx

## Poem

> In the garden of code, where time slots bloom,  
> A rabbit hops by, dispelling the gloom.  
> Tooltips now hover, with patience and grace,  
> Time blocks are smarter, no errors to chase.  
> With careful new logic and dialogs that sing,  
> The calendar’s ready for what users bring!  
> 🐇⏰✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=tutur3u/platform&utm_content=2743):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-08T16:03:33Z
body: ## [Codecov](https://app.codecov.io/gh/tutur3u/platform/pull/2743?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) Report
Attention: Patch coverage is `0%` with `641 lines` in your changes missing coverage. Please review.
> Project coverage is 0.76%. Comparing base [(`76f609b`)](https://app.codecov.io/gh/tutur3u/platform/commit/76f609b33759e1ff9513bccac092adc8ee38d4f9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) to head [(`63a3d09`)](https://app.codecov.io/gh/tutur3u/platform/commit/63a3d09ca9123a2bf30ebbcc7adadda338891071?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u).
> Report is 10 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/tutur3u/platform/pull/2743?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) | Patch % | Lines |
|---|---|---|
| [...ts/ui/legacy/calendar/settings/TimeRangePicker.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fcomponents%2Fui%2Flegacy%2Fcalendar%2Fsettings%2FTimeRangePicker.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2NvbXBvbmVudHMvdWkvbGVnYWN5L2NhbGVuZGFyL3NldHRpbmdzL1RpbWVSYW5nZVBpY2tlci50c3g=) | 0.00% | [363 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |
| [...src/components/ui/legacy/calendar/CalendarCell.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fcomponents%2Fui%2Flegacy%2Fcalendar%2FCalendarCell.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2NvbXBvbmVudHMvdWkvbGVnYWN5L2NhbGVuZGFyL0NhbGVuZGFyQ2VsbC50c3g=) | 0.00% | [145 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |
| [...ents/ui/legacy/calendar/settings/HoursSettings.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fcomponents%2Fui%2Flegacy%2Fcalendar%2Fsettings%2FHoursSettings.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2NvbXBvbmVudHMvdWkvbGVnYWN5L2NhbGVuZGFyL3NldHRpbmdzL0hvdXJzU2V0dGluZ3MudHN4) | 0.00% | [117 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |
| [...gacy/calendar/settings/CalendarSettingsContext.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fcomponents%2Fui%2Flegacy%2Fcalendar%2Fsettings%2FCalendarSettingsContext.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2NvbXBvbmVudHMvdWkvbGVnYWN5L2NhbGVuZGFyL3NldHRpbmdzL0NhbGVuZGFyU2V0dGluZ3NDb250ZXh0LnRzeA==) | 0.00% | [16 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2743?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2743      +/-   ##
==========================================
- Coverage    0.76%    0.76%   -0.01%     
==========================================
  Files        2347     2347              
  Lines      275220   275735     +515     
  Branches     2787     2787              
==========================================
  Hits         2119     2119              
- Misses     271081   271596     +515     
  Partials     2020     2020              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tutur3u/platform/pull/2743?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE