Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/timo-web/components/layout/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
DropdownView,
SidebarButton,
TodayButton,
WeeklyButton,
ChevronButton,
} from "@repo/timo-design-system/ui";
import { cn } from "@repo/timo-design-system/utils";

Expand Down Expand Up @@ -39,13 +39,13 @@ export interface HeaderWeeklyNavProps {
const HeaderWeeklyNav = ({ onPrev, onNext, label }: HeaderWeeklyNavProps) => {
return (
<div className="flex items-center gap-2">
<WeeklyButton variant="left" onClick={onPrev} />
<ChevronButton variant="left" onClick={onPrev} />
{label && (
<span className="typo-headline-m-14 text-timo-gray-900 flex h-8 items-center rounded-[4px] bg-white px-2">
{label}
</span>
)}
<WeeklyButton variant="right" onClick={onNext} />
<ChevronButton variant="right" onClick={onNext} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
},
Expand All @@ -15,7 +15,7 @@ const meta = {
args: {
variant: "left",
},
} satisfies Meta<typeof WeeklyButton>;
} satisfies Meta<typeof ChevronButton>;

export default meta;
type Story = StoryObj<typeof meta>;
Expand All @@ -24,8 +24,8 @@ export const AllStates: Story = {
parameters: { controls: { disable: true } },
render: () => (
<div className="flex gap-4">
<WeeklyButton variant="left" />
<WeeklyButton variant="right" />
<ChevronButton variant="left" />
<ChevronButton variant="right" />
</div>
),
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<WeeklyButtonVariantTypes, ReactNode> = {
const CHEVRON_BUTTON_ICON: Record<ChevronButtonVariantTypes, ReactNode> = {
left: <ChevronLeftIcon />,
right: <ChevronRightIcon />,
};

const WEEKLY_BUTTON_ARIA_LABEL: Record<WeeklyButtonVariantTypes, string> = {
const CHEVRON_BUTTON_ARIA_LABEL: Record<ChevronButtonVariantTypes, string> = {
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) => (
<button
type="button"
onClick={onClick}
aria-label={WEEKLY_BUTTON_ARIA_LABEL[variant]}
aria-label={CHEVRON_BUTTON_ARIA_LABEL[variant]}
className={cn(
"border-timo-gray-500 flex size-8 items-center justify-center rounded-[4px] border bg-white",
className,
)}
>
{WEEKLY_BUTTON_ICON[variant]}
{CHEVRON_BUTTON_ICON[variant]}
</button>
);
Original file line number Diff line number Diff line change
@@ -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<typeof Calendar>;

export default meta;
type Story = StoryObj<typeof meta>;

const CalendarDemo = (args: CalendarProps) => {
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
new Date(2026, 6, 1),
);

return (
<Calendar
{...args}
value={selectedDate}
defaultMonth={new Date(2026, 6, 1)}
onChange={setSelectedDate}
/>
);
};
Comment thread
yumin-kim2 marked this conversation as resolved.

export const Default: Story = {
render: (args) => <CalendarDemo {...args} />,
};
165 changes: 165 additions & 0 deletions packages/timo-design-system/src/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -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",
];
Comment thread
yumin-kim2 marked this conversation as resolved.

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,
};
});
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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(),
);
Comment on lines +69 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

visibleMonth가 controlled value 변경을 따라가지 않아요.

useState(defaultMonth ?? value ?? new Date())는 마운트 시 한 번만 평가됩니다. 이후 부모가 value를 다른 달의 날짜로 바꿔도 visibleMonth는 그대로 남아 표시되는 달이 갱신되지 않습니다. Storybook 데모(Calendar.stories.tsx)처럼 value를 controlled로 관리하는 소비 패턴을 고려하면, 외부에서 값이 바뀌는 경우를 지원해야 할 가능성이 높습니다.

useEffectvalue 변경 시 visibleMonth를 동기화하거나, 최소한 이 제약을 CalendarProps 문서에 명시하는 것을 권장드려요.

🔄 value 변경 시 visibleMonth 동기화 제안
+import { useEffect, useState } from "react";
-import { useState } from "react";

 export const Calendar = ({ value, defaultMonth, onChange, className }: CalendarProps) => {
   const [visibleMonth, setVisibleMonth] = useState(
     defaultMonth ?? value ?? new Date(),
   );
+  useEffect(() => {
+    if (value) {
+      setVisibleMonth(value);
+    }
+  }, [value]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/timo-design-system/src/components/calendar/Calendar.tsx` around
lines 64 - 66, The visibleMonth state in Calendar is initialized only once with
useState(defaultMonth ?? value ?? new Date()), so it does not follow later
controlled value updates. Update Calendar so visibleMonth stays in sync when the
value prop changes, for example by adding a useEffect in Calendar.tsx that
watches value and updates setVisibleMonth accordingly while preserving
defaultMonth behavior. Use the Calendar and visibleMonth state in
packages/timo-design-system/src/components/calendar/Calendar.tsx to locate the
fix.

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 (
<div
className={cn("h-66 w-67 rounded-[4px] bg-white px-6 py-4", className)}
>
<div className="relative mb-4 h-8">
<ChevronButton
variant="left"
onClick={() => handleMoveMonth(-1)}
className="absolute top-0 left-0"
/>

<div className="flex h-full items-center justify-center gap-1">
<span className="typo-headline-b-16 text-timo-gray-700">
{MONTH_LABELS[visibleMonth.getMonth()]}
</span>
<span className="typo-headline-b-16 text-timo-gray-700">
{visibleMonth.getFullYear()}
</span>
</div>

<ChevronButton
variant="right"
onClick={() => handleMoveMonth(1)}
className="absolute top-0 right-0"
/>
</div>

<div className={cn("grid grid-cols-7 grid-rows-6 gap-x-1 gap-y-1.5")}>
{WEEKDAYS.map((weekday, index) => (
<div
key={`${weekday}-${index}`}
className="typo-headline-b-16 text-timo-gray-900 flex items-center justify-center"
>
{weekday}
</div>
))}

{calendarDates.map((calendarDate) => {
const isSelected = value
? isSameDate(calendarDate.date, value)
: false;
const isToday = isSameDate(calendarDate.date, today);

return (
<button
key={calendarDate.date.toISOString()}
type="button"
onClick={() => {
if (!calendarDate.isCurrentMonth) {
setVisibleMonth(
new Date(
calendarDate.date.getFullYear(),
calendarDate.date.getMonth(),
1,
),
);
}

onChange?.(calendarDate.date);
}}
className={cn(
"flex h-6.5 w-7 items-center justify-center",
isToday ? "typo-headline-b-14" : "typo-headline-r-14",
calendarDate.isCurrentMonth
? "text-timo-gray-700"
: "text-timo-gray-500",
isToday && "text-timo-gray-900",
isSelected && "bg-timo-blue-300 rounded-[4px] text-white",
)}
>
{calendarDate.day}
</button>
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})}
</div>
</div>
);
};
3 changes: 2 additions & 1 deletion packages/timo-design-system/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading