diff --git a/packages/core/projects/draft-store.ts b/packages/core/projects/draft-store.ts
index 8b10b9768..29d6bbebe 100644
--- a/packages/core/projects/draft-store.ts
+++ b/packages/core/projects/draft-store.ts
@@ -12,6 +12,9 @@ interface ProjectDraft {
leadType?: "member" | "agent";
leadId?: string;
icon?: string;
+ // Calendar days ("YYYY-MM-DD"); empty/undefined means unset.
+ startDate?: string;
+ dueDate?: string;
}
const EMPTY_DRAFT: ProjectDraft = {
@@ -22,6 +25,8 @@ const EMPTY_DRAFT: ProjectDraft = {
leadType: undefined,
leadId: undefined,
icon: undefined,
+ startDate: undefined,
+ dueDate: undefined,
};
interface ProjectDraftStore {
diff --git a/packages/views/locales/en/projects.json b/packages/views/locales/en/projects.json
index e52c4dac6..595bfb3f0 100644
--- a/packages/views/locales/en/projects.json
+++ b/packages/views/locales/en/projects.json
@@ -68,6 +68,9 @@
"section_properties": "Properties",
"section_progress": "Progress",
"section_description": "Description",
+ "prop_start_date": "Start date",
+ "prop_due_date": "Due date",
+ "clear_date": "Clear date",
"description_placeholder": "Add description...",
"description_hint": "Shared with agents as context for every task in this project.",
"empty_issues_title": "No issues linked",
diff --git a/packages/views/locales/ja/projects.json b/packages/views/locales/ja/projects.json
index f8ad32ea2..1ffdff855 100644
--- a/packages/views/locales/ja/projects.json
+++ b/packages/views/locales/ja/projects.json
@@ -67,6 +67,9 @@
"section_properties": "プロパティ",
"section_progress": "進捗",
"section_description": "説明",
+ "prop_start_date": "開始日",
+ "prop_due_date": "期限",
+ "clear_date": "日付をクリア",
"description_placeholder": "説明を追加...",
"description_hint": "このプロジェクトのすべてのタスクで、コンテキストとしてエージェントに共有されます。",
"empty_issues_title": "リンクされたイシューはありません",
diff --git a/packages/views/locales/ko/projects.json b/packages/views/locales/ko/projects.json
index 9da75dbd2..ccbc96a7d 100644
--- a/packages/views/locales/ko/projects.json
+++ b/packages/views/locales/ko/projects.json
@@ -67,6 +67,9 @@
"section_properties": "속성",
"section_progress": "진행률",
"section_description": "설명",
+ "prop_start_date": "시작일",
+ "prop_due_date": "마감일",
+ "clear_date": "날짜 지우기",
"description_placeholder": "설명 추가...",
"description_hint": "이 프로젝트의 모든 작업에서 컨텍스트로 에이전트에게 공유됩니다.",
"empty_issues_title": "연결된 이슈가 없습니다",
diff --git a/packages/views/locales/zh-Hans/projects.json b/packages/views/locales/zh-Hans/projects.json
index 7fed25b6d..f8c6e1108 100644
--- a/packages/views/locales/zh-Hans/projects.json
+++ b/packages/views/locales/zh-Hans/projects.json
@@ -67,6 +67,9 @@
"section_properties": "属性",
"section_progress": "进度",
"section_description": "描述",
+ "prop_start_date": "开始日期",
+ "prop_due_date": "截止日期",
+ "clear_date": "清除日期",
"description_placeholder": "添加描述...",
"description_hint": "会作为项目上下文提供给智能体,应用于该项目下的每个任务。",
"empty_issues_title": "还没有关联的 issue",
diff --git a/packages/views/modals/create-project.test.tsx b/packages/views/modals/create-project.test.tsx
index 9f3004fac..8148b5066 100644
--- a/packages/views/modals/create-project.test.tsx
+++ b/packages/views/modals/create-project.test.tsx
@@ -89,6 +89,17 @@ vi.mock("../common/actor-avatar", () => ({
ActorAvatar: () => ,
}));
+// Stub the date pickers so this test doesn't pull the real Calendar (and its
+// buttonVariants import) into the modal's module graph; the pickers have their
+// own test. The stubs render the placeholder label so the pills are assertable.
+vi.mock("../projects/components/project-start-date-picker", () => ({
+ ProjectStartDatePicker: () => ,
+}));
+
+vi.mock("../projects/components/project-due-date-picker", () => ({
+ ProjectDueDatePicker: () => ,
+}));
+
vi.mock("@multica/ui/components/ui/dialog", () => ({
Dialog: ({ children }: { children: React.ReactNode }) =>
{children}
,
DialogContent: ({ children }: { children: React.ReactNode }) => {children}
,
@@ -170,6 +181,13 @@ describe("CreateProjectModal", () => {
expect(screen.getByRole("tooltip", { name: longRepoUrl })).toBeInTheDocument();
});
+ it("renders the start-date and due-date pills", () => {
+ renderWithI18n();
+
+ expect(screen.getByRole("button", { name: "Start date" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Due date" })).toBeInTheDocument();
+ });
+
it("filters workspace repositories by search text", async () => {
const user = userEvent.setup();
diff --git a/packages/views/modals/create-project.tsx b/packages/views/modals/create-project.tsx
index 07589b0b4..146810ee4 100644
--- a/packages/views/modals/create-project.tsx
+++ b/packages/views/modals/create-project.tsx
@@ -57,6 +57,8 @@ import {
useProjectStatusLabels,
useProjectPriorityLabels,
} from "../projects/components/labels";
+import { ProjectStartDatePicker } from "../projects/components/project-start-date-picker";
+import { ProjectDueDatePicker } from "../projects/components/project-due-date-picker";
import {
isDesktopShell,
pickDirectory,
@@ -134,6 +136,8 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
const [leadType, setLeadType] = useState<"member" | "agent" | undefined>(draft.leadType);
const [leadId, setLeadId] = useState(draft.leadId);
const [icon, setIcon] = useState(draft.icon);
+ const [startDate, setStartDate] = useState(draft.startDate ?? "");
+ const [dueDate, setDueDate] = useState(draft.dueDate ?? "");
const [iconPickerOpen, setIconPickerOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
@@ -212,6 +216,8 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
setDraft({ leadType: type, leadId: id });
};
const updateIcon = (v: string | undefined) => { setIcon(v); setDraft({ icon: v }); };
+ const updateStartDate = (v: string) => { setStartDate(v); setDraft({ startDate: v || undefined }); };
+ const updateDueDate = (v: string) => { setDueDate(v); setDraft({ dueDate: v || undefined }); };
const [leadOpen, setLeadOpen] = useState(false);
const [leadFilter, setLeadFilter] = useState("");
@@ -266,6 +272,8 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
priority,
lead_type: leadType,
lead_id: leadId,
+ start_date: startDate || undefined,
+ due_date: dueDate || undefined,
// Server attaches these in the same transaction as the project.
resources,
});
@@ -541,6 +549,18 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
+ updateStartDate(u.start_date ?? "")}
+ triggerRender={}
+ />
+
+ updateDueDate(u.due_date ?? "")}
+ triggerRender={}
+ />
+
{
diff --git a/packages/views/projects/components/project-date-pickers.test.tsx b/packages/views/projects/components/project-date-pickers.test.tsx
new file mode 100644
index 000000000..52fdebdd9
--- /dev/null
+++ b/packages/views/projects/components/project-date-pickers.test.tsx
@@ -0,0 +1,54 @@
+import { describe, expect, it, vi } from "vitest";
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { renderWithI18n } from "../../test/i18n";
+import { ProjectStartDatePicker } from "./project-start-date-picker";
+import { ProjectDueDatePicker } from "./project-due-date-picker";
+
+describe("ProjectStartDatePicker", () => {
+ it("shows the placeholder label when no date is set", () => {
+ renderWithI18n();
+ expect(screen.getByText("Start date")).toBeInTheDocument();
+ });
+
+ it("shows the formatted calendar day when a date is set", () => {
+ renderWithI18n(
+ ,
+ );
+ expect(screen.getByText("Mar 1")).toBeInTheDocument();
+ });
+
+ it("emits start_date: null when the date is cleared", async () => {
+ const onUpdate = vi.fn();
+ const user = userEvent.setup();
+ renderWithI18n(
+ ,
+ );
+ await user.click(screen.getByText("Mar 1")); // open popover
+ await user.click(screen.getByRole("button", { name: "Clear date" }));
+ expect(onUpdate).toHaveBeenCalledWith({ start_date: null });
+ });
+});
+
+describe("ProjectDueDatePicker", () => {
+ it("shows the placeholder label when no date is set", () => {
+ renderWithI18n();
+ expect(screen.getByText("Due date")).toBeInTheDocument();
+ });
+
+ it("shows the formatted calendar day when a date is set", () => {
+ renderWithI18n();
+ expect(screen.getByText("Mar 1")).toBeInTheDocument();
+ });
+
+ it("emits due_date: null when the date is cleared", async () => {
+ const onUpdate = vi.fn();
+ const user = userEvent.setup();
+ renderWithI18n(
+ ,
+ );
+ await user.click(screen.getByText("Mar 1")); // open popover
+ await user.click(screen.getByRole("button", { name: "Clear date" }));
+ expect(onUpdate).toHaveBeenCalledWith({ due_date: null });
+ });
+});
diff --git a/packages/views/projects/components/project-detail.tsx b/packages/views/projects/components/project-detail.tsx
index 5c8e866f8..2b1914933 100644
--- a/packages/views/projects/components/project-detail.tsx
+++ b/packages/views/projects/components/project-detail.tsx
@@ -25,6 +25,8 @@ import { useNavigation } from "../../navigation";
import { TitleEditor, ContentEditor, type ContentEditorRef } from "../../editor";
import { PriorityIcon } from "../../issues/components/priority-icon";
import { ProjectResourcesSection } from "./project-resources-section";
+import { ProjectStartDatePicker } from "./project-start-date-picker";
+import { ProjectDueDatePicker } from "./project-due-date-picker";
import { IssueSurface } from "../../issues/surface/issue-surface";
import { Skeleton } from "@multica/ui/components/ui/skeleton";
import { Button } from "@multica/ui/components/ui/button";
@@ -398,6 +400,12 @@ export function ProjectDetail({ projectId }: { projectId: string }) {
+ $.detail.prop_start_date)}>
+
+
+ $.detail.prop_due_date)}>
+
+
}
diff --git a/packages/views/projects/components/project-due-date-picker.tsx b/packages/views/projects/components/project-due-date-picker.tsx
new file mode 100644
index 000000000..5455ba8be
--- /dev/null
+++ b/packages/views/projects/components/project-due-date-picker.tsx
@@ -0,0 +1,87 @@
+"use client";
+
+import { useState } from "react";
+import { CalendarDays } from "lucide-react";
+import type { UpdateProjectRequest } from "@multica/core/types";
+import {
+ toDateOnly,
+ dateOnlyToLocalDate,
+ formatDateOnly,
+ isPastDateOnly,
+} from "@multica/core/issues/date";
+import { Calendar } from "@multica/ui/components/ui/calendar";
+import {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+} from "@multica/ui/components/ui/popover";
+import { Button } from "@multica/ui/components/ui/button";
+import { useT } from "../../i18n";
+
+/**
+ * Project due-date picker. Mirrors the issue DueDatePicker
+ * (packages/views/issues/components/pickers/due-date-picker.tsx), including the
+ * overdue `text-destructive` styling, but typed to UpdateProjectRequest and
+ * scoped to the "projects" i18n namespace. See ProjectStartDatePicker for the
+ * shared-component rationale.
+ */
+export function ProjectDueDatePicker({
+ dueDate,
+ onUpdate,
+ triggerRender,
+ align = "start",
+}: {
+ dueDate: string | null;
+ onUpdate: (updates: Partial) => void;
+ /** Custom trigger element (e.g. a pill button in the create modal). */
+ triggerRender?: React.ReactElement;
+ align?: "start" | "center" | "end";
+}) {
+ const { t } = useT("projects");
+ const [open, setOpen] = useState(false);
+ const date = dateOnlyToLocalDate(dueDate);
+ const isOverdue = isPastDateOnly(dueDate);
+
+ return (
+
+
+
+ {date ? (
+
+ {formatDateOnly(dueDate, { month: "short", day: "numeric" }, "en-US")}
+
+ ) : (
+ {t(($) => $.detail.prop_due_date)}
+ )}
+
+
+ {
+ onUpdate({ due_date: d ? toDateOnly(d) : null });
+ setOpen(false);
+ }}
+ />
+ {date && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/packages/views/projects/components/project-start-date-picker.tsx b/packages/views/projects/components/project-start-date-picker.tsx
new file mode 100644
index 000000000..21e6142db
--- /dev/null
+++ b/packages/views/projects/components/project-start-date-picker.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { useState } from "react";
+import { CalendarClock } from "lucide-react";
+import type { UpdateProjectRequest } from "@multica/core/types";
+import {
+ toDateOnly,
+ dateOnlyToLocalDate,
+ formatDateOnly,
+} from "@multica/core/issues/date";
+import { Calendar } from "@multica/ui/components/ui/calendar";
+import {
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+} from "@multica/ui/components/ui/popover";
+import { Button } from "@multica/ui/components/ui/button";
+import { useT } from "../../i18n";
+
+/**
+ * Project start-date picker. Mirrors the issue StartDatePicker
+ * (packages/views/issues/components/pickers/start-date-picker.tsx) — same
+ * calendar-day contract and clear idiom, reusing @multica/core/issues/date —
+ * but typed to UpdateProjectRequest and scoped to the "projects" i18n
+ * namespace. The same component serves both the create-project modal (map the
+ * emitted value into local draft state) and the project sidebar (pass the
+ * update mutation straight through).
+ */
+export function ProjectStartDatePicker({
+ startDate,
+ onUpdate,
+ triggerRender,
+ align = "start",
+}: {
+ startDate: string | null;
+ onUpdate: (updates: Partial) => void;
+ /** Custom trigger element (e.g. a pill button in the create modal). */
+ triggerRender?: React.ReactElement;
+ align?: "start" | "center" | "end";
+}) {
+ const { t } = useT("projects");
+ const [open, setOpen] = useState(false);
+ const date = dateOnlyToLocalDate(startDate);
+
+ return (
+
+
+
+ {date ? (
+ {formatDateOnly(startDate, { month: "short", day: "numeric" }, "en-US")}
+ ) : (
+ {t(($) => $.detail.prop_start_date)}
+ )}
+
+
+ {
+ onUpdate({ start_date: d ? toDateOnly(d) : null });
+ setOpen(false);
+ }}
+ />
+ {date && (
+
+
+
+ )}
+
+
+ );
+}