feat: redesign dashboard v3 layout

This commit is contained in:
2026-04-06 19:52:29 +09:00
parent 1b3edd45f6
commit 5b7e4ec50d
2 changed files with 1465 additions and 101 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,294 @@
'use client';
import React from 'react';
import styled, { keyframes } from 'styled-components';
export interface PipelineNode {
id: string;
label: string;
role: string;
state: 'idle' | 'active' | 'review' | 'blocked' | 'ready';
detail: string;
}
interface ActivePipelineProps {
activeTask: string;
focus: string;
reviewLoopCount: number;
escalationCount: number;
deployState: string;
nodes: PipelineNode[];
}
const flow = keyframes`
0% { transform: translateX(0); opacity: 0.15; }
40% { opacity: 0.8; }
100% { transform: translateX(20px); opacity: 0; }
`;
const PipelineCard = styled.section`
border: 1px solid var(--border-color);
background: var(--bg-surface);
padding: var(--space-lg);
display: flex;
flex-direction: column;
gap: var(--space-lg);
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
gap: var(--space-md);
align-items: flex-start;
flex-wrap: wrap;
`;
const Eyebrow = styled.div`
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
`;
const Title = styled.h2`
font-size: 24px;
font-weight: 600;
color: var(--text-primary);
letter-spacing: -0.02em;
margin-top: 6px;
`;
const Summary = styled.p`
font-size: 13px;
color: var(--text-secondary);
line-height: 1.6;
max-width: 780px;
`;
const HeaderStats = styled.div`
display: grid;
grid-template-columns: repeat(3, minmax(110px, 1fr));
gap: var(--space-sm);
@media (max-width: 767px) {
width: 100%;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
`;
const StatBox = styled.div`
border: 1px solid var(--border-color);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
`;
const StatLabel = styled.span`
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
`;
const StatValue = styled.span`
font-size: 15px;
color: var(--text-primary);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const FlowMap = styled.div`
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 14px;
align-items: stretch;
@media (max-width: 1199px) {
grid-template-columns: 1fr;
}
`;
const NodeWrap = styled.div`
display: flex;
align-items: center;
gap: 10px;
@media (max-width: 1199px) {
flex-direction: column;
align-items: stretch;
}
`;
const Connector = styled.div<{ $active?: boolean }>`
flex: 1;
min-width: 36px;
height: 1px;
background: ${({ $active }) => ($active ? 'rgba(245,245,245,0.45)' : 'var(--border-color)')};
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: -2px;
left: -18px;
width: 18px;
height: 5px;
background: ${({ $active }) => ($active ? 'rgba(245,245,245,0.85)' : 'transparent')};
animation: ${({ $active }) => ($active ? flow : 'none')} 1.6s linear infinite;
}
@media (max-width: 1199px) {
width: 1px;
height: 24px;
min-width: 1px;
margin: 0 auto;
&::after {
top: -8px;
left: -2px;
width: 5px;
height: 10px;
animation: none;
}
}
`;
const stateBorder: Record<PipelineNode['state'], string> = {
idle: 'var(--border-color)',
active: '#6fc3ff',
review: '#ff7ac6',
blocked: '#ff8d7a',
ready: '#8dffb2',
};
const stateText: Record<PipelineNode['state'], string> = {
idle: 'IDLE',
active: 'ACTIVE',
review: 'REVIEW',
blocked: 'BLOCKED',
ready: 'READY',
};
const NodeCard = styled.div<{ $state: PipelineNode['state'] }>`
border: 1px solid ${({ $state }) => stateBorder[$state]};
padding: 14px;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 148px;
background: linear-gradient(180deg, rgba(255,255,255,0.015) 0%, rgba(255,255,255,0) 100%);
`;
const NodeTop = styled.div`
display: flex;
justify-content: space-between;
gap: var(--space-sm);
align-items: flex-start;
`;
const NodeLabel = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
const NodeName = styled.div`
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
`;
const NodeRole = styled.div`
font-size: 12px;
color: var(--text-secondary);
`;
const StateBadge = styled.span<{ $state: PipelineNode['state'] }>`
font-family: var(--font-mono);
font-size: 10px;
color: ${({ $state }) => stateBorder[$state]};
border: 1px solid ${({ $state }) => stateBorder[$state]};
padding: 4px 6px;
text-transform: uppercase;
letter-spacing: 0.08em;
white-space: nowrap;
`;
const NodeDetail = styled.div`
font-size: 13px;
line-height: 1.6;
color: var(--text-primary);
`;
const NodeHint = styled.div`
margin-top: auto;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
`;
export default function ActivePipeline({
activeTask,
focus,
reviewLoopCount,
escalationCount,
deployState,
nodes,
}: ActivePipelineProps) {
return (
<PipelineCard>
<Header>
<div>
<Eyebrow>Master flow / active pipeline</Eyebrow>
<Title>{activeTask}</Title>
<Summary>{focus}</Summary>
</div>
<HeaderStats>
<StatBox>
<StatLabel>review loop</StatLabel>
<StatValue>{reviewLoopCount}x</StatValue>
</StatBox>
<StatBox>
<StatLabel>escalations</StatLabel>
<StatValue>{escalationCount}</StatValue>
</StatBox>
<StatBox>
<StatLabel>deploy state</StatLabel>
<StatValue>{deployState}</StatValue>
</StatBox>
</HeaderStats>
</Header>
<FlowMap>
{nodes.map((node, index) => {
const connectorActive = node.state === 'active' || node.state === 'review' || node.state === 'ready';
return (
<NodeWrap key={node.id}>
<NodeCard $state={node.state}>
<NodeTop>
<NodeLabel>
<NodeName>{node.label}</NodeName>
<NodeRole>{node.role}</NodeRole>
</NodeLabel>
<StateBadge $state={node.state}>{stateText[node.state]}</StateBadge>
</NodeTop>
<NodeDetail>{node.detail}</NodeDetail>
<NodeHint>stage {String(index + 1).padStart(2, '0')}</NodeHint>
</NodeCard>
{index < nodes.length - 1 && <Connector $active={connectorActive} />}
</NodeWrap>
);
})}
</FlowMap>
</PipelineCard>
);
}