From d6eafc4a62b0a9d746cd90af9056b57cf17fe92d Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:08:06 -0700 Subject: [PATCH 1/5] feat(board): rebuild the task board to the d215 frame (TASK-687) Header with Mine/P0/Unclaimed chips and search, a 40px focus strip, three columns plus a 120px done rail, two-line cards with hover actions, and phone segment tabs. Drawer, focus editor and create dialog unchanged. Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: otto Agent-Model: claude/claude-opus-5-5/default --- frontend/src/i18n/locales/en.json | 17 ++- frontend/src/i18n/locales/zh-CN.json | 17 ++- frontend/src/v2/__tests__/V2PodBoard.test.tsx | 15 ++ .../v2/__tests__/v2-layout-invariants.test.ts | 6 + frontend/src/v2/components/V2PodBoard.tsx | 141 +++++++++++++++--- frontend/src/v2/v2.css | 125 ++++++++++++++++ 6 files changed, 302 insertions(+), 19 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index dc348e8b37..248c587bbc 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1500,7 +1500,8 @@ "reviewLatest": "Review latest", "latestReviewed": "Latest reviewed", "discard": "Discard your unsaved focus changes?", - "saveError": "Focus could not be saved. Try again." + "saveError": "Focus could not be saved. Try again.", + "progress": "{{done}}/{{total}} done" }, "loadErrorTitle": "Couldn't load the board", "loadErrorText": "Check your connection and try again.", @@ -1561,6 +1562,20 @@ "assigned": "{{assignee}} · not claimed", "expired": "lease expired · {{holder}}", "claimed": "{{holder}} · claimed" + }, + "filter": { + "label": "Filters", + "mine": "Mine", + "p0": "P0", + "unclaimed": "Unclaimed" + }, + "search": { + "label": "Search tasks", + "placeholder": "Search tasks" + }, + "doneRail": { + "show": "Show", + "hide": "Hide" } }, "approvalCard": { diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index ea11b5b13a..e88bd64fc4 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1492,7 +1492,8 @@ "reviewLatest": "查看最新内容", "latestReviewed": "已查看最新内容", "discard": "放弃未保存的焦点更改?", - "saveError": "无法保存焦点,请重试。" + "saveError": "无法保存焦点,请重试。", + "progress": "{{done}}/{{total}}已完成" }, "loadErrorTitle": "看板加载失败", "loadErrorText": "请检查网络连接后重试。", @@ -1553,6 +1554,20 @@ "assigned": "{{assignee}} · 尚未认领", "expired": "认领已到期 · {{holder}}", "claimed": "{{holder}} · 已认领" + }, + "filter": { + "label": "筛选", + "mine": "我的", + "p0": "P0", + "unclaimed": "未认领" + }, + "search": { + "label": "搜索任务", + "placeholder": "搜索任务" + }, + "doneRail": { + "show": "展开", + "hide": "收起" } }, "approvalCard": { diff --git a/frontend/src/v2/__tests__/V2PodBoard.test.tsx b/frontend/src/v2/__tests__/V2PodBoard.test.tsx index 48d51b3442..10c1bd4a24 100644 --- a/frontend/src/v2/__tests__/V2PodBoard.test.tsx +++ b/frontend/src/v2/__tests__/V2PodBoard.test.tsx @@ -244,6 +244,21 @@ describe('V2PodBoard', () => { }); }); + test('the P0 chip narrows the columns and the done rail shows the done column', async () => { + renderBoard(); + const pending = await screen.findByRole('region', { name: 'Pending' }); + expect(within(pending).getAllByRole('article').length).toBeGreaterThan(0); + + fireEvent.click(screen.getByRole('button', { name: 'P0' })); + expect(screen.getByRole('button', { name: 'P0' })).toHaveAttribute('aria-pressed', 'true'); + expect(within(pending).queryAllByRole('article')).toHaveLength(0); + + const rail = screen.getByRole('complementary', { name: 'Done' }); + expect(screen.getByTestId('v2-board').querySelector('.v2-board__columns')).not.toHaveClass('is-showdone'); + fireEvent.click(within(rail).getByRole('button', { name: 'Show' })); + expect(screen.getByTestId('v2-board').querySelector('.v2-board__columns')).toHaveClass('is-showdone'); + }); + test('creates a task from the dialog', async () => { axios.post.mockResolvedValue({ data: { task: { taskId: 'N-9', title: 'Ship it', status: 'pending' } } }); renderBoard(); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 25ec5d2fb6..8b0a7470dd 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5285,4 +5285,10 @@ describe('pod board column scroll (TASK-698)', () => { expect(block).toMatch(/\.v2-pod-header__invite span\s*\{[^}]*clip-path:\s*inset\(50%\)/); expect(block).toMatch(/\.v2-pod-header__invite\s*\{[^}]*width:\s*28px/); }); + + test('the board keeps three columns plus a 120px done rail, and tabs only on phones (TASK-687)', () => { + const css = fs.readFileSync(path.join(__dirname, '..', 'v2.css'), 'utf8'); + expect(css).toMatch(/\.v2-board__columns \{[^}]*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\) 120px/); + expect(css).toMatch(/\.v2-board__tabs \{ display: none; \}/); + }); }); diff --git a/frontend/src/v2/components/V2PodBoard.tsx b/frontend/src/v2/components/V2PodBoard.tsx index 28474df9da..4c8e1c597c 100644 --- a/frontend/src/v2/components/V2PodBoard.tsx +++ b/frontend/src/v2/components/V2PodBoard.tsx @@ -17,6 +17,8 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; import AddIcon from '@mui/icons-material/Add'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import SearchIcon from '@mui/icons-material/Search'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useV2Api } from '../hooks/useV2Api'; import { useSocket } from '../../context/SocketContext'; import V2Avatar from './V2Avatar'; @@ -26,6 +28,13 @@ type TaskPriority = 'P0' | 'P1' | 'P2' | 'P3'; const PRIORITIES: TaskPriority[] = ['P0', 'P1', 'P2', 'P3']; type CanonicalStatus = 'pending' | 'claimed' | 'blocked' | 'done'; +type BoardFilter = 'mine' | 'p0' | 'unclaimed'; +const FILTERS: Array<{ key: BoardFilter; labelKey: string }> = [ + { key: 'mine', labelKey: 'board.filter.mine' }, + { key: 'p0', labelKey: 'board.filter.p0' }, + { key: 'unclaimed', labelKey: 'board.filter.unclaimed' }, +]; +const DONE_STATUSES = ['done', 'completed', 'complete']; interface BoardTask { taskId: string; @@ -623,14 +632,35 @@ const V2PodBoard: React.FC = () => { } }, [api, podId, newTitle, newAssignee, newPriority, creating, load, t]); + const [filters, setFilters] = useState([]); + const [searchOpen, setSearchOpen] = useState(false); + const [query, setQuery] = useState(''); + const [showDone, setShowDone] = useState(false); + const [mobileTab, setMobileTab] = useState('pending'); + const toggleFilter = (key: BoardFilter) => setFilters((prev) => ( + prev.includes(key) ? prev.filter((item) => item !== key) : [...prev, key] + )); + const visibleTasks = useMemo(() => { + const needle = query.trim().toLowerCase(); + return tasks.filter((task) => { + if (filters.includes('mine')) { + const me = [currentUser?.username, currentUser?._id].filter(Boolean); + if (!me.some((id) => id === task.assignee || id === task.claimedBy)) return false; + } + if (filters.includes('p0') && task.priority !== 'P0') return false; + if (filters.includes('unclaimed') && (task.claimedBy || task.assignee)) return false; + if (needle && !`${task.taskId} ${task.title}`.toLowerCase().includes(needle)) return false; + return true; + }); + }, [tasks, filters, query, currentUser]); const columnTasks = useMemo(() => { const byColumn = new Map(); COLUMNS.forEach((col) => { - byColumn.set(col.key, tasks.filter((task) => col.statuses.includes(task.status)).sort((a, b) => (a.priority || 'P2').localeCompare(b.priority || 'P2') + byColumn.set(col.key, visibleTasks.filter((task) => col.statuses.includes(task.status)).sort((a, b) => (a.priority || 'P2').localeCompare(b.priority || 'P2') || (a.taskNum || 0) - (b.taskNum || 0) || a.taskId.localeCompare(b.taskId))); }); return byColumn; - }, [tasks]); + }, [visibleTasks]); return (
@@ -649,28 +679,73 @@ const V2PodBoard: React.FC = () => { {t('board.taskCount', { count: tasks.length })}
+
+ {FILTERS.map((f) => ( + + ))} +
+ {searchOpen && ( + setQuery(e.target.value)} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus + /> + )} + -
-
-
-
{t('board.focus.eyebrow')}
-

{t('board.focus.title')}

-
+
+ + {t('board.focus.eyebrow')} +

+ {focusRead?.focus ? focusRead.focus.goal : t('board.focus.title')} +

+ {focusRead?.focus && focusRead.focus.nextTasks.length > 0 && ( + + {t('board.focus.progress', { + done: focusRead.focus.nextTasks.filter((task) => DONE_STATUSES.includes(task.status || '')).length, + total: focusRead.focus.nextTasks.length, + })} + + )} {focusRead?.permissions?.canEdit && ( )} -
+
+ {loading &&
} {!loading && loadError && ( @@ -723,20 +797,43 @@ const V2PodBoard: React.FC = () => { )} {!loading && !loadError && ( -
+ <> +
+ {COLUMNS.map((col) => ( + + ))} +
+
{COLUMNS.map((col) => { const items = columnTasks.get(col.key) || []; return ( -
+
+
+
{items.length === 0 && (
{t(col.emptyKey)}
)} {items.map((task) => ( -
+
{col.moves.map((move) => ( @@ -764,10 +862,19 @@ const V2PodBoard: React.FC = () => {
))} +
); })} -
+ +
+ )} {selected && ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 29960f2c40..3663f4345e 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -13909,3 +13909,128 @@ body.modern-ui.v2-canvas { .v2-legal__section { margin-bottom: 28px; } .v2-legal__section h2 { font-size: 18px; font-weight: 600; margin: 0 0 8px; } .v2-legal__section p { font-size: 15px; line-height: 1.6; color: var(--v2-text-secondary); margin: 0 0 8px; } + +/* TASK-687 / D223 — board rebuilt to the d215 board frame: 40px focus strip, + three columns + 120px done rail, two-line cards, mobile segment tabs. */ +.v2-board__head { gap: 10px; padding: 10px 24px; flex-wrap: nowrap; } +.v2-board__title-wrap { flex: 1 1 auto; min-width: 0; } +.v2-board__filters { display: flex; gap: 6px; } +.v2-root button.v2-board__chip { + height: 28px; padding: 0 10px; border-radius: 14px; font: 500 12px/16px var(--v2-font); + color: var(--v2-text-secondary); background: transparent; border: 1px solid var(--v2-border); + cursor: pointer; +} +.v2-root button.v2-board__chip.is-on { + color: var(--v2-accent-text); background: var(--v2-accent-soft); border-color: var(--v2-accent); +} +.v2-board__search { + height: 32px; width: 180px; padding: 0 10px; border-radius: 8px; font-size: 13px; + border: 1px solid var(--v2-border); background: var(--v2-bg); color: var(--v2-text-primary); +} +.v2-root button.v2-board__icon-btn { + width: 32px; height: 32px; display: inline-grid; place-items: center; padding: 0; + border-radius: 8px; border: 1px solid var(--v2-border); background: transparent; + color: var(--v2-text-secondary); cursor: pointer; +} +.v2-root button.v2-board__icon-btn:hover { background: var(--v2-surface-hover); } +.v2-board__new { min-height: 32px; } + +.v2-board__focus { margin: 0; padding: 0; border: 0; background: transparent; } +.v2-board__focus > summary { list-style: none; } +.v2-board__focus > summary::-webkit-details-marker { display: none; } +.v2-board__focus-strip { + display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 24px; cursor: pointer; + border-bottom: 1px solid var(--v2-border-soft); +} +.v2-board__focus-strip .v2-board__focus-eyebrow { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } +.v2-board__focus-strip .v2-board__focus-title { + flex: 1 1 auto; min-width: 0; margin: 0; font: 600 13px/18px var(--v2-font); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} +.v2-board__focus-progress { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); white-space: nowrap; } +.v2-board__focus-chevron { color: var(--v2-text-tertiary); transition: transform 0.15s; } +.v2-board__focus[open] .v2-board__focus-chevron { transform: rotate(180deg); } +.v2-board__focus[open] > :not(summary) { margin-left: 24px; margin-right: 24px; } +.v2-board__focus[open] { max-height: 40vh; overflow-y: auto; } + +.v2-board__tabs { display: none; } +.v2-board__columns { + display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) 120px; gap: 10px; + padding: 12px 24px 0; overflow: hidden; align-items: stretch; +} +.v2-board__columns.is-showdone { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.v2-board__col { + min-width: 0; max-width: none; padding: 0; gap: 0; border-radius: 12px; overflow: hidden; + background: var(--v2-bg-subtle); border: 0; box-shadow: inset 0 0 0 1px var(--v2-border-soft); + max-height: none; display: flex; flex-direction: column; +} +.v2-board__col--done { display: none; } +.v2-board__columns.is-showdone .v2-board__col--done { display: flex; } +.v2-board__columns.is-showdone .v2-board__donecol { display: none; } +.v2-board__col-head { display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 12px; flex: none; } +.v2-board__col-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--v2-text-placeholder); } +.v2-board__col-dot--claimed { background: var(--v2-accent); } +.v2-board__col-dot--blocked { background: var(--v2-status-blocked); } +.v2-board__col-dot--done { background: var(--v2-success, #12b76a); } +.v2-board__col-head .v2-board__col-count { margin-right: auto; } +.v2-root button.v2-board__col-add { + width: 24px; height: 24px; display: inline-grid; place-items: center; padding: 0 6px; border: 0; + background: transparent; color: var(--v2-text-secondary); border-radius: 6px; cursor: pointer; font-size: 12px; +} +.v2-root button.v2-board__col-add:hover { background: var(--v2-surface-hover); } +.v2-root button.v2-board__col-hide { width: auto; } +.v2-board__col-body { display: grid; gap: 6px; padding: 0 8px 8px; overflow-y: auto; align-content: start; flex: 1; min-height: 0; } +.v2-board__col-empty { padding: 12px 4px; font-size: 12px; color: var(--v2-text-tertiary); } + +.v2-board__card { + position: relative; display: grid; gap: 4px; padding: 8px 10px 9px; border-radius: 9px; + background: var(--v2-surface); border: 1px solid var(--v2-border-soft); +} +.v2-board__card.is-picked { outline: 2px solid var(--v2-accent); outline-offset: -1px; } +.v2-board__card-main { display: grid; gap: 4px; padding: 0; border: 0; background: none; text-align: left; cursor: pointer; min-width: 0; color: inherit; font: inherit; } +.v2-board__card-kicker { display: flex; align-items: center; gap: 6px; min-width: 0; font: 500 11px/16px var(--v2-font-mono); } +.v2-board__card-spacer { flex: 1 1 auto; } +.v2-board__card-kicker .v2-board__owner-line { min-width: 0; max-width: 55%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } +.v2-board__card-title { + font: 500 13px/18px var(--v2-font); display: -webkit-box; -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; +} +.v2-board__card-actions { + position: absolute; right: 6px; bottom: 6px; display: none; gap: 4px; padding: 2px; + background: var(--v2-surface); border-radius: 8px; box-shadow: 0 0 0 1px var(--v2-border-soft); +} +.v2-board__card:hover .v2-board__card-actions, +.v2-board__card:focus-within .v2-board__card-actions { display: flex; } + +.v2-board__donecol { + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; + border-radius: 12px; background: var(--v2-bg-subtle); box-shadow: inset 0 0 0 1px var(--v2-border-soft); +} +.v2-board__donecol-label { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } +.v2-board__donecol-count { font: 600 22px/28px var(--v2-font); } +.v2-root button.v2-board__donecol-show { + height: 28px; padding: 0 12px; border-radius: 8px; border: 1px solid var(--v2-border); + background: transparent; color: var(--v2-text-secondary); font: 500 12px/16px var(--v2-font); cursor: pointer; +} + +@media (max-width: 760px) { + .v2-board__head { padding: 10px 12px; flex-wrap: wrap; } + .v2-board__filters { order: 5; width: 100%; } + .v2-board__new-label { display: none; } + .v2-board__focus-strip { padding: 0 12px; } + .v2-board__tabs { display: flex; gap: 4px; padding: 8px 12px 0; overflow-x: auto; } + .v2-root button.v2-board__tab { + flex: 1 1 0; display: inline-flex; justify-content: center; gap: 4px; height: 32px; align-items: center; + padding: 0 8px; border: 0; border-radius: 8px; background: transparent; cursor: pointer; + font: 500 12px/16px var(--v2-font); color: var(--v2-text-secondary); white-space: nowrap; + } + .v2-root button.v2-board__tab.is-active { background: var(--v2-surface-tint); color: var(--v2-text-primary); } + .v2-board__tab-count { font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } + .v2-board__columns, .v2-board__columns.is-showdone { display: block; padding: 8px 12px 0; overflow-y: auto; flex: 1; min-height: 0; } + .v2-board__col, .v2-board__columns.is-showdone .v2-board__col--done { display: none; } + .v2-board__col.is-active, .v2-board__columns .v2-board__col--done.is-active { display: flex; background: transparent; box-shadow: none; } + .v2-board__col.is-active .v2-board__col-head { display: none; } + .v2-board__col-body { padding: 0; } + .v2-board__donecol { display: none; } + .v2-board__card-actions { position: static; display: flex; box-shadow: none; padding: 0; } +} From 06eea381ae394d8fcb877bacbde304804886cb59 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 20:13:15 -0700 Subject: [PATCH 2/5] fix(board): close the frame drift found in the 1440/390 diff (TASK-687) Full-width board, 64px header, boxed focus strip, owner line ellipsis, top-aligned done rail, cards no longer clipped, one-row mobile header. Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: otto Agent-Model: claude/claude-opus-5-5/default --- frontend/src/v2/components/V2PodBoard.tsx | 2 +- frontend/src/v2/v2.css | 38 ++++++++++++++++------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/frontend/src/v2/components/V2PodBoard.tsx b/frontend/src/v2/components/V2PodBoard.tsx index 4c8e1c597c..508c7c491f 100644 --- a/frontend/src/v2/components/V2PodBoard.tsx +++ b/frontend/src/v2/components/V2PodBoard.tsx @@ -671,7 +671,7 @@ const V2PodBoard: React.FC = () => { onClick={() => navigate(`/v2/pods/${podId}`)} >