diff --git a/apps/web/package.json b/apps/web/package.json index b59e79f..c57ceb8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -40,7 +40,12 @@ "@tiptap/extension-text-align": "^2.11.0", "@tiptap/pm": "^2.11.0", "lowlight": "^3.3.0", - "lucide-react": "^0.510.0" + "lucide-react": "^0.510.0", + "@dnd-kit/core": "^6.3.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.0", + "date-fns": "^4.1.0", + "chrono-node": "^2.7.0" }, "devDependencies": { "@yetanother/tsconfig": "workspace:*", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 9551991..2e37062 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,15 +1,24 @@ import { Routes, Route, Navigate } from 'react-router-dom'; import { ToastProvider, ToastViewport } from '@yetanother/ui'; import { NotesPage } from './notes/NotesPage'; +import { TasksPage } from './tasks/TasksPage'; +import { CalendarPage } from './calendar/CalendarPage'; +import { Sidebar } from './components/Sidebar'; +import './index.css'; export function App() { return ( -
- - } /> - } /> - +
+ +
+ + } /> + } /> + } /> + } /> + +
diff --git a/apps/web/src/calendar/CalendarHeader.tsx b/apps/web/src/calendar/CalendarHeader.tsx new file mode 100644 index 0000000..a3743bd --- /dev/null +++ b/apps/web/src/calendar/CalendarHeader.tsx @@ -0,0 +1,85 @@ +import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'; +import { useCalendarStore } from '../stores/calendar'; +import { format, addMonths, subMonths, addWeeks, subWeeks, addDays, subDays } from 'date-fns'; + +export function CalendarHeader() { + const view = useCalendarStore((s) => s.view); + const currentDate = useCalendarStore((s) => s.currentDate); + const setCurrentDate = useCalendarStore((s) => s.setCurrentDate); + + const date = new Date(currentDate); + + const navigate = (direction: 'prev' | 'next') => { + const navFns: Record Date> = { + day: direction === 'prev' ? subDays : addDays, + week: direction === 'prev' ? subWeeks : addWeeks, + month: direction === 'prev' ? subMonths : addMonths, + }; + const fn = navFns[view] ?? navFns.month!; + setCurrentDate(fn(date, 1).toISOString()); + }; + + const formatTitle = () => { + switch (view) { + case 'day': return format(date, 'EEEE, MMMM d, yyyy'); + case 'week': return `Week of ${format(date, 'MMMM d, yyyy')}`; + case 'month': return format(date, 'MMMM yyyy'); + default: return format(date, 'MMMM yyyy'); + } + }; + + return ( +
+
+

{formatTitle()}

+
+ + + +
+
+ +
+
+ {(['day', 'week', 'month'] as const).map((v) => ( + + ))} +
+ +
+
+ ); +} diff --git a/apps/web/src/calendar/CalendarPage.tsx b/apps/web/src/calendar/CalendarPage.tsx new file mode 100644 index 0000000..60e006c --- /dev/null +++ b/apps/web/src/calendar/CalendarPage.tsx @@ -0,0 +1,19 @@ +import { CalendarHeader } from './CalendarHeader'; +import { MonthView } from './MonthView'; +import { useCalendarStore } from '../stores/calendar'; + +export function CalendarPage() { + const view = useCalendarStore((s) => s.view); + + return ( +
+ + {view === 'month' && } + {(view === 'day' || view === 'week') && ( +
+ Day/Week view coming soon +
+ )} +
+ ); +} diff --git a/apps/web/src/calendar/MonthView.tsx b/apps/web/src/calendar/MonthView.tsx new file mode 100644 index 0000000..cdf5530 --- /dev/null +++ b/apps/web/src/calendar/MonthView.tsx @@ -0,0 +1,80 @@ +import { useCalendarStore, type CalendarEvent } from '../stores/calendar'; +import { + startOfMonth, endOfMonth, startOfWeek, endOfWeek, eachDayOfInterval, + isSameMonth, isSameDay, isToday, format, +} from 'date-fns'; +import { cn } from '@yetanother/utils'; + +function getEventsForDay(events: CalendarEvent[], day: Date) { + return events.filter((e) => isSameDay(new Date(e.startTime), day)); +} + +export function MonthView() { + const currentDate = useCalendarStore((s) => s.currentDate); + const events = useCalendarStore((s) => s.events); + + const date = new Date(currentDate); + const monthStart = startOfMonth(date); + const monthEnd = endOfMonth(date); + const calStart = startOfWeek(monthStart, { weekStartsOn: 0 }); + const calEnd = endOfWeek(monthEnd, { weekStartsOn: 0 }); + + const days = eachDayOfInterval({ start: calStart, end: calEnd }); + + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + return ( +
+
+ {dayNames.map((name) => ( +
+ {name} +
+ ))} +
+ +
+ {days.map((day) => { + const dayEvents = getEventsForDay(events, day); + const isCurrentMonth = isSameMonth(day, date); + + return ( +
+
+ {format(day, 'd')} +
+ +
+ {dayEvents.slice(0, 3).map((event) => ( +
+ {event.title} +
+ ))} + {dayEvents.length > 3 && ( +
+ +{dayEvents.length - 3} more +
+ )} +
+
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx new file mode 100644 index 0000000..c5eedc7 --- /dev/null +++ b/apps/web/src/components/Sidebar.tsx @@ -0,0 +1,58 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import { FileText, CheckSquare, Calendar, Search, Settings } from 'lucide-react'; +import { cn } from '@yetanother/utils'; + +const items = [ + { path: '/notes', label: 'Notes', icon: FileText }, + { path: '/tasks', label: 'Tasks', icon: CheckSquare }, + { path: '/calendar', label: 'Calendar', icon: Calendar }, +]; + +export function Sidebar() { + const location = useLocation(); + const navigate = useNavigate(); + + return ( + + ); +} diff --git a/apps/web/src/stores/calendar.ts b/apps/web/src/stores/calendar.ts new file mode 100644 index 0000000..f8edb32 --- /dev/null +++ b/apps/web/src/stores/calendar.ts @@ -0,0 +1,50 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface CalendarEvent { + id: string; + title: string; + description: string; + startTime: string; + endTime: string; + allDay: boolean; + recurrenceRule: string | null; + color: string; + workspaceId: string; + taskId: string | null; + createdAt: string; + updatedAt: string; +} + +interface CalendarState { + events: CalendarEvent[]; + view: 'day' | 'week' | 'month' | 'year' | 'schedule'; + currentDate: string; + setEvents: (events: CalendarEvent[]) => void; + addEvent: (event: CalendarEvent) => void; + updateEvent: (id: string, updates: Partial) => void; + removeEvent: (id: string) => void; + setView: (view: CalendarState['view']) => void; + setCurrentDate: (date: string) => void; +} + +export const useCalendarStore = create()( + persist( + (set) => ({ + events: [], + view: 'month', + currentDate: new Date().toISOString(), + setEvents: (events) => set({ events }), + addEvent: (event) => set((state) => ({ events: [...state.events, event] })), + updateEvent: (id, updates) => + set((state) => ({ + events: state.events.map((e) => (e.id === id ? { ...e, ...updates } : e)), + })), + removeEvent: (id) => + set((state) => ({ events: state.events.filter((e) => e.id !== id) })), + setView: (view) => set({ view }), + setCurrentDate: (date) => set({ currentDate: date }), + }), + { name: 'yetanother-calendar', partialize: (state) => ({ events: state.events }) }, + ), +); diff --git a/apps/web/src/stores/tasks.ts b/apps/web/src/stores/tasks.ts new file mode 100644 index 0000000..fc00844 --- /dev/null +++ b/apps/web/src/stores/tasks.ts @@ -0,0 +1,88 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface Task { + id: string; + title: string; + description: string; + priority: 1 | 2 | 3 | 4 | 5 | null; + status: 'active' | 'completed' | 'deleted'; + dueDate: string | null; + durationMinutes: number | null; + tags: string[]; + subtasks: Subtask[]; + projectId: string | null; + parentId: string | null; + workspaceId: string; + startTime: string | null; + endTime: string | null; + createdAt: string; + updatedAt: string; + completedAt: string | null; +} + +export interface Subtask { + id: string; + title: string; + completed: boolean; +} + +export interface TaskProject { + id: string; + name: string; + color: string; +} + +interface TasksState { + tasks: Task[]; + projects: TaskProject[]; + view: 'inbox' | 'today' | 'upcoming' | 'projects' | 'anytime' | 'completed'; + selectedTaskId: string | null; + setTasks: (tasks: Task[]) => void; + addTask: (task: Task) => void; + updateTask: (id: string, updates: Partial) => void; + removeTask: (id: string) => void; + toggleComplete: (id: string) => void; + setView: (view: TasksState['view']) => void; + setSelectedTaskId: (id: string | null) => void; + setProjects: (projects: TaskProject[]) => void; + addProject: (project: TaskProject) => void; +} + +export const useTasksStore = create()( + persist( + (set) => ({ + tasks: [], + projects: [], + view: 'inbox', + selectedTaskId: null, + setTasks: (tasks) => set({ tasks }), + addTask: (task) => set((state) => ({ tasks: [task, ...state.tasks] })), + updateTask: (id, updates) => + set((state) => ({ + tasks: state.tasks.map((t) => (t.id === id ? { ...t, ...updates } : t)), + })), + removeTask: (id) => + set((state) => ({ + tasks: state.tasks.filter((t) => t.id !== id), + })), + toggleComplete: (id) => + set((state) => ({ + tasks: state.tasks.map((t) => + t.id === id + ? { + ...t, + status: t.status === 'completed' ? 'active' as const : 'completed' as const, + completedAt: t.status === 'completed' ? null : new Date().toISOString(), + } + : t, + ), + })), + setView: (view) => set({ view }), + setSelectedTaskId: (id) => set({ selectedTaskId: id }), + setProjects: (projects) => set({ projects }), + addProject: (project) => set((state) => ({ projects: [...state.projects, project] })), + }), + { name: 'yetanother-tasks', partialize: (state) => ({ tasks: state.tasks, projects: state.projects }) }, + ), +); diff --git a/apps/web/src/tasks/TaskInput.tsx b/apps/web/src/tasks/TaskInput.tsx new file mode 100644 index 0000000..4dc823f --- /dev/null +++ b/apps/web/src/tasks/TaskInput.tsx @@ -0,0 +1,78 @@ +import { useState, useCallback, useRef } from 'react'; +import { Plus, Calendar, Clock } from 'lucide-react'; +import { useTasksStore, type Task } from '../stores/tasks'; + +function parseNaturalLanguage(input: string): Partial { + const parsed: Partial = { title: input, priority: null, dueDate: null, durationMinutes: null, tags: [] }; + + const priorityMatch = input.match(/!([1-5])/); + if (priorityMatch) { + parsed.priority = parseInt(priorityMatch[1]!) as 1 | 2 | 3 | 4 | 5; + parsed.title = input.replace(priorityMatch[0], '').trim(); + } + + return parsed; +} + +export function TaskInput() { + const [value, setValue] = useState(''); + const inputRef = useRef(null); + const addTask = useTasksStore((s) => s.addTask); + + const handleSubmit = useCallback(() => { + const trimmed = value.trim(); + if (!trimmed) return; + + const parsed = parseNaturalLanguage(trimmed); + const now = new Date().toISOString(); + + const task: Task = { + id: crypto.randomUUID(), + title: parsed.title ?? trimmed, + description: '', + priority: parsed.priority ?? null, + status: 'active', + dueDate: parsed.dueDate ?? null, + durationMinutes: parsed.durationMinutes ?? null, + tags: [], + subtasks: [], + projectId: null, + parentId: null, + workspaceId: crypto.randomUUID(), + startTime: null, + endTime: null, + createdAt: now, + updatedAt: now, + completedAt: null, + }; + + addTask(task); + setValue(''); + inputRef.current?.focus(); + }, [value, addTask]); + + return ( +
+ + setValue(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }} + placeholder="Add a task... (!1-5 for priority)" + className="flex-1 bg-transparent text-sm focus:outline-none placeholder:text-muted-foreground" + /> +
+ + +
+
+ ); +} diff --git a/apps/web/src/tasks/TaskItem.tsx b/apps/web/src/tasks/TaskItem.tsx new file mode 100644 index 0000000..9adb853 --- /dev/null +++ b/apps/web/src/tasks/TaskItem.tsx @@ -0,0 +1,91 @@ +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { GripVertical, CheckCircle2, Circle, Trash2 } from 'lucide-react'; +import { useTasksStore, type Task } from '../stores/tasks'; +import { cn } from '@yetanother/utils'; + +const priorityColors: Record = { + 1: 'border-l-red-500', + 2: 'border-l-orange-400', + 3: 'border-l-blue-400', + 4: 'border-l-gray-300', + 5: 'border-l-gray-100', +}; + +interface TaskItemProps { task: Task } + +export function TaskItem({ task }: TaskItemProps) { + const toggleComplete = useTasksStore((s) => s.toggleComplete); + const removeTask = useTasksStore((s) => s.removeTask); + + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ + + + +
+ + {task.title} + + {task.dueDate && ( + + {new Date(task.dueDate).toLocaleDateString()} + + )} +
+ + {task.priority && ( + + P{task.priority} + + )} + + +
+ ); +} diff --git a/apps/web/src/tasks/TaskList.tsx b/apps/web/src/tasks/TaskList.tsx new file mode 100644 index 0000000..44f6014 --- /dev/null +++ b/apps/web/src/tasks/TaskList.tsx @@ -0,0 +1,53 @@ +import { useMemo } from 'react'; +import { DndContext, closestCenter, type DragEndEvent } from '@dnd-kit/core'; +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { useTasksStore } from '../stores/tasks'; +import { TaskItem } from './TaskItem'; +import { isToday, isFuture } from 'date-fns'; + +function filterTasks(tasks: ReturnType['tasks'], view: string) { + return tasks.filter((t) => { + if (t.status === 'deleted') return false; + if (view === 'completed') return t.status === 'completed'; + if (t.status === 'completed') return false; + + switch (view) { + case 'inbox': return !t.projectId && !t.dueDate; + case 'today': return t.dueDate ? isToday(new Date(t.dueDate)) : false; + case 'upcoming': return t.dueDate ? isFuture(new Date(t.dueDate)) : false; + case 'anytime': return !t.dueDate; + default: return true; + } + }); +} + +export function TaskList() { + const tasks = useTasksStore((s) => s.tasks); + const view = useTasksStore((s) => s.view); + const updateTask = useTasksStore((s) => s.updateTask); + + const filtered = useMemo(() => filterTasks(tasks, view), [tasks, view]); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + updateTask(active.id as string, {}); + }; + + return ( + + t.id)} strategy={verticalListSortingStrategy}> +
+ {filtered.length === 0 && ( +
+ No tasks in this view +
+ )} + {filtered.map((task) => ( + + ))} +
+
+
+ ); +} diff --git a/apps/web/src/tasks/TaskViews.tsx b/apps/web/src/tasks/TaskViews.tsx new file mode 100644 index 0000000..bf8608e --- /dev/null +++ b/apps/web/src/tasks/TaskViews.tsx @@ -0,0 +1,38 @@ +import { Inbox, CalendarDays, ListChecks, LayoutDashboard, Archive } from 'lucide-react'; +import { useTasksStore } from '../stores/tasks'; +import { cn } from '@yetanother/utils'; + +const views = [ + { id: 'inbox' as const, label: 'Inbox', icon: Inbox }, + { id: 'today' as const, label: 'Today', icon: CalendarDays }, + { id: 'upcoming' as const, label: 'Upcoming', icon: ListChecks }, + { id: 'projects' as const, label: 'Projects', icon: LayoutDashboard }, + { id: 'anytime' as const, label: 'Anytime', icon: Inbox }, + { id: 'completed' as const, label: 'Completed', icon: Archive }, +]; + +export function TaskViews() { + const view = useTasksStore((s) => s.view); + const setView = useTasksStore((s) => s.setView); + + return ( +
+ {views.map((v) => ( + + ))} +
+ ); +} diff --git a/apps/web/src/tasks/TasksPage.tsx b/apps/web/src/tasks/TasksPage.tsx new file mode 100644 index 0000000..5a01f52 --- /dev/null +++ b/apps/web/src/tasks/TasksPage.tsx @@ -0,0 +1,38 @@ +import { TaskInput } from './TaskInput'; +import { TaskList } from './TaskList'; +import { TaskViews } from './TaskViews'; +import { useTasksStore } from '../stores/tasks'; + +export function TasksPage() { + const view = useTasksStore((s) => s.view); + + const viewLabels: Record = { + inbox: 'Inbox', + today: 'Today', + upcoming: 'Upcoming', + projects: 'Projects', + anytime: 'Anytime', + completed: 'Completed', + }; + + return ( +
+
+
+

Tasks

+
+ +
+ +
+
+

{viewLabels[view] ?? 'Tasks'}

+
+ +
+ +
+
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 922c442..7fad51e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,6 +99,15 @@ importers: apps/web: dependencies: + '@dnd-kit/core': + specifier: ^6.3.0 + version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + '@dnd-kit/utilities': + specifier: ^3.2.0 + version: 3.2.2(react@19.2.7) '@tanstack/react-query': specifier: ^5.75.0 version: 5.101.2(react@19.2.7) @@ -162,6 +171,12 @@ importers: '@yetanother/utils': specifier: workspace:* version: link:../../packages/utils + chrono-node: + specifier: ^2.7.0 + version: 2.10.0 + date-fns: + specifier: ^4.1.0 + version: 4.4.0 jotai: specifier: ^2.12.0 version: 2.20.2(@babel/core@7.29.7(supports-color@7.2.0))(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7) @@ -531,6 +546,28 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -2243,6 +2280,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chrono-node@2.10.0: + resolution: {integrity: sha512-5fJ4zr5W/5DEf+8FMPMXF2qk9L5dc1rAP9Pw009iMlKAghPgx5o7aUcnjEMe6PTwqiqTa5yzmfSm05EHuwr31Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} @@ -2337,6 +2378,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -4403,6 +4447,31 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.7)': + dependencies: + react: 19.2.7 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.7) + '@dnd-kit/utilities': 3.2.2(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@dnd-kit/utilities': 3.2.2(react@19.2.7) + react: 19.2.7 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.7)': + dependencies: + react: 19.2.7 + tslib: 2.8.1 + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -6014,6 +6083,8 @@ snapshots: dependencies: readdirp: 4.1.2 + chrono-node@2.10.0: {} + citty@0.1.6: dependencies: consola: 3.4.2 @@ -6097,6 +6168,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + date-fns@4.4.0: {} + dateformat@4.6.3: {} debug@4.4.3(supports-color@7.2.0):