feat: quarter board theme, hub column, and team panel UX
Apply preview-style 4-dept layout with center hub, PM/assignee team status linking, task type label updates, and remove task keywords. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,14 +4,7 @@ import { CSS } from '@dnd-kit/utilities';
|
||||
import type { DraggableAttributes } from '@dnd-kit/core';
|
||||
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
import type { Task } from '../../types';
|
||||
|
||||
const STATUS_DOT: Record<string, string> = {
|
||||
IN_PROGRESS: 'ongoing',
|
||||
REVIEW: 'hold',
|
||||
TODO: 'hold',
|
||||
CANCELLED: 'hold',
|
||||
DONE: 'done',
|
||||
};
|
||||
import { DonutGauge } from './DonutGauge';
|
||||
|
||||
function fmtDate(iso: string | null | undefined): string {
|
||||
if (!iso) return '';
|
||||
@@ -32,51 +25,6 @@ function firstDescriptionLine(text: string | null | undefined): string {
|
||||
return line ?? '';
|
||||
}
|
||||
|
||||
function statusDotClass(status: string): string {
|
||||
return STATUS_DOT[status] ?? 'hold';
|
||||
}
|
||||
|
||||
function SemiCircleGauge({ value }: { value: number }) {
|
||||
const p = Math.min(100, Math.max(0, value));
|
||||
const stroke = 6.75;
|
||||
const w = 88;
|
||||
const h = 56;
|
||||
const cx = 44;
|
||||
const r = 32;
|
||||
/** arc 좌·우 끝 = 숫자 세로 중앙 (100%도 여유 있게) */
|
||||
const cy = 46;
|
||||
const arcLen = Math.PI * r;
|
||||
const dash = (p / 100) * arcLen;
|
||||
const path = `M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="board-gauge"
|
||||
style={{ ['--gauge-cy' as string]: `${(cy / h) * 100}%` }}
|
||||
aria-label={`진행률 ${p}%`}
|
||||
>
|
||||
<svg className="board-gauge-svg" viewBox={`0 0 ${w} ${h}`} aria-hidden>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
stroke="#d4e8de"
|
||||
strokeWidth={stroke}
|
||||
strokeLinecap="butt"
|
||||
/>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
stroke="#29724f"
|
||||
strokeWidth={stroke}
|
||||
strokeLinecap="butt"
|
||||
strokeDasharray={`${dash} ${arcLen}`}
|
||||
/>
|
||||
</svg>
|
||||
<span className="board-gauge-value">{p}%</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SectionOption = { value: string; label: string };
|
||||
|
||||
export function SortableTaskCard({
|
||||
@@ -87,6 +35,7 @@ export function SortableTaskCard({
|
||||
task: Task;
|
||||
variant?: 'project' | 'routine';
|
||||
sectionOptions?: SectionOption[];
|
||||
accent?: string;
|
||||
onSelect?: (task: Task) => void;
|
||||
}) {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: task.id });
|
||||
@@ -155,9 +104,8 @@ export function TaskCard({
|
||||
onKeyDown: dragListeners?.onKeyDown as React.KeyboardEventHandler<HTMLDivElement> | undefined,
|
||||
};
|
||||
|
||||
const dotClass = statusDotClass(task.status);
|
||||
|
||||
if (variant === 'routine') {
|
||||
const descLine = firstDescriptionLine(task.description);
|
||||
return (
|
||||
<div
|
||||
ref={dragRef}
|
||||
@@ -168,8 +116,8 @@ export function TaskCard({
|
||||
className="board-routine-item"
|
||||
{...dragHandlers}
|
||||
>
|
||||
<span className={`board-status-dot board-status-dot--${dotClass}`} aria-hidden />
|
||||
<span className="board-routine-name">{task.title}</span>
|
||||
<span className="board-project-title">{task.title}</span>
|
||||
{descLine && <p className="board-project-desc">• {descLine}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -179,33 +127,45 @@ export function TaskCard({
|
||||
const showProgress = task.showProgress !== false;
|
||||
|
||||
return (
|
||||
<div
|
||||
<article
|
||||
ref={dragRef}
|
||||
style={dragStyle}
|
||||
{...dragAttributes}
|
||||
data-task-card="true"
|
||||
data-task-id={task.id}
|
||||
className="board-project-card"
|
||||
className="project-sub-card"
|
||||
{...dragHandlers}
|
||||
>
|
||||
<div className="board-project-top">
|
||||
<div className="board-project-main">
|
||||
<div className="board-project-title-row">
|
||||
<span className={`board-status-dot board-status-dot--${dotClass}`} aria-hidden />
|
||||
<span className="board-project-title">{task.title}</span>
|
||||
</div>
|
||||
{dateRange && <p className="board-project-date">{dateRange}</p>}
|
||||
<div className="project-sub-body">
|
||||
<div className="project-fields">
|
||||
<div className="project-sub-title">{task.title}</div>
|
||||
{dateRange && (
|
||||
<div className="project-field">
|
||||
<span className="project-field-label">작업 기간</span>
|
||||
<span className="project-field-value">{dateRange}</span>
|
||||
</div>
|
||||
)}
|
||||
{descLine && (
|
||||
<div className="project-field">
|
||||
<span className="project-field-label">주요 내용</span>
|
||||
<span className="project-field-value">{descLine}</span>
|
||||
</div>
|
||||
)}
|
||||
{task.showIssue && task.issueNote && (
|
||||
<div className="project-field">
|
||||
<span className="project-field-label">이슈</span>
|
||||
<span className="project-field-value" style={{ color: '#c0392b' }}>
|
||||
{task.issueNote}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showProgress && <SemiCircleGauge value={task.progress} />}
|
||||
{showProgress && (
|
||||
<div className="progress-col">
|
||||
<DonutGauge task={task} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{descLine && (
|
||||
<p className="board-project-desc">• {descLine}</p>
|
||||
)}
|
||||
|
||||
{task.showIssue && task.issueNote && (
|
||||
<p className="board-project-issue">▶ {task.issueNote}</p>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user