diff --git a/apps/timo-web/components/layout/header/Header.tsx b/apps/timo-web/components/layout/header/Header.tsx index 5ace3dcd..7729c246 100644 --- a/apps/timo-web/components/layout/header/Header.tsx +++ b/apps/timo-web/components/layout/header/Header.tsx @@ -4,7 +4,7 @@ import { DropdownView, SidebarButton, TodayButton, - WeeklyButton, + ChevronButton, } from "@repo/timo-design-system/ui"; import { cn } from "@repo/timo-design-system/utils"; @@ -39,13 +39,13 @@ export interface HeaderWeeklyNavProps { const HeaderWeeklyNav = ({ onPrev, onNext, label }: HeaderWeeklyNavProps) => { return (
- + {label && ( {label} )} - +
); }; diff --git a/packages/timo-design-system/src/components/button/weekly-button/WeeklyButton.stories.tsx b/packages/timo-design-system/src/components/button/chevron-button/ChevronButton.stories.tsx similarity index 66% rename from packages/timo-design-system/src/components/button/weekly-button/WeeklyButton.stories.tsx rename to packages/timo-design-system/src/components/button/chevron-button/ChevronButton.stories.tsx index 88ccc0be..e5e312e1 100644 --- a/packages/timo-design-system/src/components/button/weekly-button/WeeklyButton.stories.tsx +++ b/packages/timo-design-system/src/components/button/chevron-button/ChevronButton.stories.tsx @@ -1,10 +1,10 @@ -import { WeeklyButton } from "./WeeklyButton"; +import { ChevronButton } from "./ChevronButton"; import type { Meta, StoryObj } from "@storybook/react"; const meta = { - title: "Components/Button/WeeklyButton", - component: WeeklyButton, + title: "Components/Button/ChevronButton", + component: ChevronButton, parameters: { layout: "centered", }, @@ -15,7 +15,7 @@ const meta = { args: { variant: "left", }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -24,8 +24,8 @@ export const AllStates: Story = { parameters: { controls: { disable: true } }, render: () => (
- - + +
), }; diff --git a/packages/timo-design-system/src/components/button/weekly-button/WeeklyButton.tsx b/packages/timo-design-system/src/components/button/chevron-button/ChevronButton.tsx similarity index 55% rename from packages/timo-design-system/src/components/button/weekly-button/WeeklyButton.tsx rename to packages/timo-design-system/src/components/button/chevron-button/ChevronButton.tsx index 7d49617d..f3770f93 100644 --- a/packages/timo-design-system/src/components/button/weekly-button/WeeklyButton.tsx +++ b/packages/timo-design-system/src/components/button/chevron-button/ChevronButton.tsx @@ -3,38 +3,38 @@ import { cn } from "../../../lib"; import type { ReactNode } from "react"; -export type WeeklyButtonVariantTypes = "left" | "right"; +export type ChevronButtonVariantTypes = "left" | "right"; -const WEEKLY_BUTTON_ICON: Record = { +const CHEVRON_BUTTON_ICON: Record = { left: , right: , }; -const WEEKLY_BUTTON_ARIA_LABEL: Record = { +const CHEVRON_BUTTON_ARIA_LABEL: Record = { left: "이전", right: "다음", }; -export interface WeeklyButtonProps { - variant: WeeklyButtonVariantTypes; +export interface ChevronButtonProps { + variant: ChevronButtonVariantTypes; onClick?: () => void; className?: string; } -export const WeeklyButton = ({ +export const ChevronButton = ({ variant, onClick, className, -}: WeeklyButtonProps) => ( +}: ChevronButtonProps) => ( ); diff --git a/packages/timo-design-system/src/components/calendar/Calendar.stories.tsx b/packages/timo-design-system/src/components/calendar/Calendar.stories.tsx new file mode 100644 index 00000000..08e75ce5 --- /dev/null +++ b/packages/timo-design-system/src/components/calendar/Calendar.stories.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; + +import { Calendar } from "./Calendar"; + +import type { CalendarProps } from "./Calendar"; +import type { Meta, StoryObj } from "@storybook/react"; + +const meta = { + title: "Components/Calendar", + component: Calendar, + parameters: { + layout: "centered", + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const CalendarDemo = (args: CalendarProps) => { + const [selectedDate, setSelectedDate] = useState( + new Date(2026, 6, 1), + ); + + return ( + + ); +}; + +export const Default: Story = { + render: (args) => , +}; diff --git a/packages/timo-design-system/src/components/calendar/Calendar.tsx b/packages/timo-design-system/src/components/calendar/Calendar.tsx new file mode 100644 index 00000000..ad80aebe --- /dev/null +++ b/packages/timo-design-system/src/components/calendar/Calendar.tsx @@ -0,0 +1,165 @@ +import { useState } from "react"; + +import { cn } from "../../lib"; +import { ChevronButton } from "../button/chevron-button/ChevronButton"; + +const WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"]; + +const MONTH_LABELS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +interface CalendarDate { + date: Date; + day: number; + isCurrentMonth: boolean; +} + +const getCalendarDates = (month: Date): CalendarDate[] => { + const year = month.getFullYear(); + const monthIndex = month.getMonth(); + const firstDate = new Date(year, monthIndex, 1); + const firstDay = firstDate.getDay(); + const lastDate = new Date(year, monthIndex + 1, 0); + const lastDay = lastDate.getDate(); + + const calendarCellCount = firstDay + lastDay > 35 ? 42 : 35; + + const startDate = new Date(year, monthIndex, 1 - firstDay); + + return Array.from({ length: calendarCellCount }, (_, index) => { + const date = new Date( + startDate.getFullYear(), + startDate.getMonth(), + startDate.getDate() + index, + ); + + return { + date, + day: date.getDate(), + isCurrentMonth: date.getMonth() === monthIndex, + }; + }); +}; + +export interface CalendarProps { + value?: Date; + defaultMonth?: Date; + onChange?: (date: Date) => void; + className?: string; +} + +export const Calendar = ({ + value, + defaultMonth, + onChange, + className, +}: CalendarProps) => { + const [visibleMonth, setVisibleMonth] = useState( + defaultMonth ?? value ?? new Date(), + ); + const calendarDates = getCalendarDates(visibleMonth); + const today = new Date(); + const isSameDate = (a: Date, b: Date) => { + return ( + a.getFullYear() === b.getFullYear() && + a.getMonth() === b.getMonth() && + a.getDate() === b.getDate() + ); + }; + + const handleMoveMonth = (amount: number) => { + setVisibleMonth( + (prev) => new Date(prev.getFullYear(), prev.getMonth() + amount, 1), + ); + }; + + return ( +
+
+ handleMoveMonth(-1)} + className="absolute top-0 left-0" + /> + +
+ + {MONTH_LABELS[visibleMonth.getMonth()]} + + + {visibleMonth.getFullYear()} + +
+ + handleMoveMonth(1)} + className="absolute top-0 right-0" + /> +
+ +
+ {WEEKDAYS.map((weekday, index) => ( +
+ {weekday} +
+ ))} + + {calendarDates.map((calendarDate) => { + const isSelected = value + ? isSameDate(calendarDate.date, value) + : false; + const isToday = isSameDate(calendarDate.date, today); + + return ( + + ); + })} +
+
+ ); +}; diff --git a/packages/timo-design-system/src/components/index.ts b/packages/timo-design-system/src/components/index.ts index d13011e2..55c38bb2 100644 --- a/packages/timo-design-system/src/components/index.ts +++ b/packages/timo-design-system/src/components/index.ts @@ -32,6 +32,7 @@ export type { } from "./layout/modal/Modal"; export { SidebarButton } from "./button/sidebar-button/SidebarButton"; export { TodayButton } from "./button/today-button/TodayButton"; -export { WeeklyButton } from "./button/weekly-button/WeeklyButton"; +export { ChevronButton } from "./button/chevron-button/ChevronButton"; export { TimeSelector } from "./time/time-selector/TimeSelector"; export { RepeatSelector } from "./repeat/repeat-selector/RepeatSelector"; +export { Calendar } from "./calendar/Calendar";