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. */} +