From 05d4c7696edc686fb12fe2225fdddb0112c9cd21 Mon Sep 17 00:00:00 2001
From: J
Date: Mon, 13 Jul 2026 16:29:52 +0800
Subject: [PATCH] refactor(projects): align create-project footer with
create-issue
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Restructure the create-project modal footer to match the create-issue pattern
(per design feedback): the primary action moves out of the cramped single
justify-between row into its own border-t action strip, and the property pills
sit in a dedicated wrapping toolbar above it. Low-frequency fields (start/due
date) collapse into a ⋯ overflow via progressive disclosure — a pill only
renders inline once its date is set or the user opens it from the menu — so the
default toolbar stays a clean single row (Status · Priority · Lead · Repos · ⋯).
- Use the shared PillButton (../common/pill-button) instead of the modal-local
copy, gaining the data-popup-open styling create-issue uses.
- ProjectStartDatePicker / ProjectDueDatePicker gain controlled open props so
the overflow menu can reveal + open them (mirrors the issue pickers).
- i18n: create_project.set_start_date / set_due_date / more_options_aria across
en / zh-Hans / ja / ko, reusing the create-issue wording.
Test updated to assert the dates are revealed from the overflow rather than
shown inline by default. typecheck / lint / i18n parity pass.
Part of #5227
Co-authored-by: multica-agent
---
packages/views/locales/en/modals.json | 3 +
packages/views/locales/ja/modals.json | 3 +
packages/views/locales/ko/modals.json | 3 +
packages/views/locales/zh-Hans/modals.json | 3 +
packages/views/modals/create-project.test.tsx | 10 +-
packages/views/modals/create-project.tsx | 102 +++++++++++-------
.../components/project-due-date-picker.tsx | 9 +-
.../components/project-start-date-picker.tsx | 9 +-
8 files changed, 101 insertions(+), 41 deletions(-)
diff --git a/packages/views/locales/en/modals.json b/packages/views/locales/en/modals.json
index c47487af6b..bdd4ccf555 100644
--- a/packages/views/locales/en/modals.json
+++ b/packages/views/locales/en/modals.json
@@ -109,6 +109,9 @@
"description_hint": "Shared with agents as context for every task in this project.",
"lead": "Lead",
"no_lead": "No lead",
+ "set_start_date": "Set start date...",
+ "set_due_date": "Set due date...",
+ "more_options_aria": "More options",
"lead_placeholder": "Assign lead...",
"members_group": "Members",
"agents_group": "Agents",
diff --git a/packages/views/locales/ja/modals.json b/packages/views/locales/ja/modals.json
index dc8e4133a5..4fcb817c13 100644
--- a/packages/views/locales/ja/modals.json
+++ b/packages/views/locales/ja/modals.json
@@ -108,6 +108,9 @@
"description_hint": "このプロジェクトのすべてのタスクで、コンテキストとしてエージェントに共有されます。",
"lead": "リード",
"no_lead": "リードなし",
+ "set_start_date": "開始日を設定...",
+ "set_due_date": "期限を設定...",
+ "more_options_aria": "その他のオプション",
"lead_placeholder": "リードを指定...",
"members_group": "メンバー",
"agents_group": "エージェント",
diff --git a/packages/views/locales/ko/modals.json b/packages/views/locales/ko/modals.json
index 649d4abf4e..2a523c5247 100644
--- a/packages/views/locales/ko/modals.json
+++ b/packages/views/locales/ko/modals.json
@@ -108,6 +108,9 @@
"description_hint": "이 프로젝트의 모든 작업에서 컨텍스트로 에이전트에게 공유됩니다.",
"lead": "리드",
"no_lead": "리드 없음",
+ "set_start_date": "시작일 설정...",
+ "set_due_date": "마감일 설정...",
+ "more_options_aria": "추가 옵션",
"lead_placeholder": "리드 지정...",
"members_group": "멤버",
"agents_group": "에이전트",
diff --git a/packages/views/locales/zh-Hans/modals.json b/packages/views/locales/zh-Hans/modals.json
index bef20e6d12..7f916bdddd 100644
--- a/packages/views/locales/zh-Hans/modals.json
+++ b/packages/views/locales/zh-Hans/modals.json
@@ -108,6 +108,9 @@
"description_hint": "会作为项目上下文提供给智能体,应用于该项目下的每个任务。",
"lead": "负责人",
"no_lead": "无负责人",
+ "set_start_date": "设置开始日期...",
+ "set_due_date": "设置截止日期...",
+ "more_options_aria": "更多选项",
"lead_placeholder": "指派负责人...",
"members_group": "成员",
"agents_group": "智能体",
diff --git a/packages/views/modals/create-project.test.tsx b/packages/views/modals/create-project.test.tsx
index 8148b50664..c1a328e3d4 100644
--- a/packages/views/modals/create-project.test.tsx
+++ b/packages/views/modals/create-project.test.tsx
@@ -181,10 +181,18 @@ describe("CreateProjectModal", () => {
expect(screen.getByRole("tooltip", { name: longRepoUrl })).toBeInTheDocument();
});
- it("renders the start-date and due-date pills", () => {
+ it("reveals the start/due date pickers from the ⋯ overflow menu", async () => {
+ const user = userEvent.setup();
renderWithI18n();
+ // Dates are collapsed behind the overflow by default (progressive disclosure).
+ expect(screen.queryByRole("button", { name: "Start date" })).not.toBeInTheDocument();
+ expect(screen.queryByRole("button", { name: "Due date" })).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: /Set start date/ }));
expect(screen.getByRole("button", { name: "Start date" })).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: /Set due date/ }));
expect(screen.getByRole("button", { name: "Due date" })).toBeInTheDocument();
});
diff --git a/packages/views/modals/create-project.tsx b/packages/views/modals/create-project.tsx
index 146810ee43..00791021fd 100644
--- a/packages/views/modals/create-project.tsx
+++ b/packages/views/modals/create-project.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useRef } from "react";
-import { ChevronRight, FolderOpen, Maximize2, Minimize2, Search, X as XIcon, UserMinus } from "lucide-react";
+import { CalendarClock, CalendarDays, ChevronRight, FolderOpen, Maximize2, Minimize2, MoreHorizontal, Search, X as XIcon, UserMinus } from "lucide-react";
/**
* GitHub mark — lucide-react v1 dropped brand icons, so we inline the
@@ -59,6 +59,7 @@ import {
} from "../projects/components/labels";
import { ProjectStartDatePicker } from "../projects/components/project-start-date-picker";
import { ProjectDueDatePicker } from "../projects/components/project-due-date-picker";
+import { PillButton } from "../common/pill-button";
import {
isDesktopShell,
pickDirectory,
@@ -66,26 +67,6 @@ import {
} from "../platform/local-directory";
import { useLocalDaemonStatus } from "../platform/use-local-daemon-status";
-function PillButton({
- children,
- className,
- ...props
-}: React.ButtonHTMLAttributes) {
- return (
-
- );
-}
-
function RepoUrlText({
url,
className,
@@ -138,6 +119,10 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
const [icon, setIcon] = useState(draft.icon);
const [startDate, setStartDate] = useState(draft.startDate ?? "");
const [dueDate, setDueDate] = useState(draft.dueDate ?? "");
+ // Dates are collapsed into the ⋯ overflow by default (progressive
+ // disclosure, mirroring create-issue); these flip a pill inline + open.
+ const [startDatePickerOpen, setStartDatePickerOpen] = useState(false);
+ const [dueDatePickerOpen, setDueDatePickerOpen] = useState(false);
const [iconPickerOpen, setIconPickerOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
@@ -407,14 +392,13 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
- {/* Footer: properties (left, wrap) + Create button (right). Single row
- so the modal stays compact — Linear-style.
+ {/* Property toolbar — mirrors the create-issue footer: a wrapping pill
+ row whose low-frequency fields (start/due date) collapse into a ⋯
+ overflow, with the primary action in a separate bar below.
Repos lives here alongside the property pills for now. Once we
support more resource types (Linear / Notion / Figma / Slack), pull
- them out into a dedicated Resources strip above this footer — a
- single Repos pill on its own row looked too sparse. */}
-
-
+ them out into a dedicated Resources strip above this footer. */}
+
void }) {
- updateStartDate(u.start_date ?? "")}
- triggerRender={}
- />
+ {/* Start date — collapsed into ⋯ unless it has a value or was just
+ opened from the overflow (the calendar anchors on the inline pill). */}
+ {(startDate || startDatePickerOpen) && (
+ updateStartDate(u.start_date ?? "")}
+ triggerRender={}
+ open={startDatePickerOpen}
+ onOpenChange={setStartDatePickerOpen}
+ />
+ )}
- updateDueDate(u.due_date ?? "")}
- triggerRender={}
- />
+ {(dueDate || dueDatePickerOpen) && (
+ updateDueDate(u.due_date ?? "")}
+ triggerRender={}
+ open={dueDatePickerOpen}
+ onOpenChange={setDueDatePickerOpen}
+ />
+ )}
void }) {
)}
-
+ {/* Overflow — always the last child so it stays at the end of the
+ wrap flow. Only rendered while a date is still collapsible; when
+ both are set there is nothing left to add. */}
+ {(!startDate || !dueDate) && (
+
+ $.create_project.more_options_aria)}>
+
+
+ }
+ />
+
+ {!dueDate && (
+ setDueDatePickerOpen(true)}>
+
+ {t(($) => $.create_project.set_due_date)}
+
+ )}
+ {!startDate && (
+ setStartDatePickerOpen(true)}>
+
+ {t(($) => $.create_project.set_start_date)}
+
+ )}
+
+
+ )}
+
+
+ {/* Footer action bar — primary action in its own strip, matching
+ create-issue. */}
+