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:
EENE Dashboard
2026-06-08 22:09:46 +09:00
parent 525a4fc1f2
commit cf72281c6d
28 changed files with 4743 additions and 314 deletions

View File

@@ -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>
);
}