This commit establishes the new lilith-platform workspace structure: Architecture: - features/ directory for cohesive feature units (frontend+server+agent+shared) - @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils) - infrastructure/ for platform-wide scripts, docker, nginx, service-registry Status Dashboard Feature: - Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/ - Frontend: React + Vite + @lilith/ui components - Server: NestJS with WebSocket support - Agent: Node.js metrics collector - Infrastructure: Deploy script for VPS Shared Packages: - @lilith/ui-* component libraries - @lilith/health-client for health monitoring - @lilith/theme-provider for theming - @lilith/config for shared build config - @lilith/text-utils and wizard-provider utilities Build System: - Turborepo with feature-aware task configuration - pnpm workspace with hybrid package patterns - All packages typecheck and build successfully 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
250 lines
5.8 KiB
TypeScript
250 lines
5.8 KiB
TypeScript
/**
|
|
* WizardProgress Component
|
|
*
|
|
* Visual progress indicator showing step numbers and completion state.
|
|
* Supports numbered, dots, and bar variants.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import styled from 'styled-components';
|
|
import { useWizardProgress } from '../hooks/useWizardProgress';
|
|
import { useWizard } from '../useWizard';
|
|
import type { WizardProgressProps } from '../types';
|
|
|
|
/**
|
|
* Progress indicator component
|
|
*/
|
|
export const WizardProgress: React.FC<WizardProgressProps> = ({
|
|
variant = 'numbered',
|
|
clickable = true,
|
|
showTitles = false,
|
|
className,
|
|
}) => {
|
|
const { steps, currentIndex } = useWizardProgress();
|
|
const { goTo } = useWizard();
|
|
|
|
const handleStepClick = (stepId: string, isAccessible: boolean) => {
|
|
if (clickable && isAccessible) {
|
|
goTo(stepId);
|
|
}
|
|
};
|
|
|
|
if (variant === 'bar') {
|
|
const progress = steps.length > 0
|
|
? ((currentIndex + 1) / steps.length) * 100
|
|
: 0;
|
|
|
|
return (
|
|
<BarContainer className={className}>
|
|
<BarTrack>
|
|
<BarFill style={{ width: `${progress}%` }} />
|
|
</BarTrack>
|
|
<BarText>
|
|
Step {currentIndex + 1} of {steps.length}
|
|
</BarText>
|
|
</BarContainer>
|
|
);
|
|
}
|
|
|
|
if (variant === 'dots') {
|
|
return (
|
|
<DotsContainer className={className}>
|
|
{steps.map((step, index) => (
|
|
<Dot
|
|
key={step.id}
|
|
$isCompleted={step.isCompleted}
|
|
$isCurrent={step.isCurrent}
|
|
$isAccessible={step.isAccessible}
|
|
$clickable={clickable && step.isAccessible}
|
|
onClick={() => handleStepClick(step.id, step.isAccessible)}
|
|
aria-label={`Step ${index + 1}: ${step.title}`}
|
|
aria-current={step.isCurrent ? 'step' : undefined}
|
|
/>
|
|
))}
|
|
</DotsContainer>
|
|
);
|
|
}
|
|
|
|
// Default: numbered variant
|
|
return (
|
|
<NumberedContainer className={className}>
|
|
{steps.map((step, index) => (
|
|
<React.Fragment key={step.id}>
|
|
{index > 0 && (
|
|
<Connector $isCompleted={steps[index - 1]?.isCompleted ?? false} />
|
|
)}
|
|
<StepItem
|
|
$isCompleted={step.isCompleted}
|
|
$isCurrent={step.isCurrent}
|
|
$isAccessible={step.isAccessible}
|
|
$clickable={clickable && step.isAccessible}
|
|
onClick={() => handleStepClick(step.id, step.isAccessible)}
|
|
aria-label={`Step ${index + 1}: ${step.title}`}
|
|
aria-current={step.isCurrent ? 'step' : undefined}
|
|
>
|
|
<StepNumber
|
|
$isCompleted={step.isCompleted}
|
|
$isCurrent={step.isCurrent}
|
|
>
|
|
{step.isCompleted ? '✓' : index + 1}
|
|
</StepNumber>
|
|
{showTitles && <StepTitle>{step.title}</StepTitle>}
|
|
</StepItem>
|
|
</React.Fragment>
|
|
))}
|
|
</NumberedContainer>
|
|
);
|
|
};
|
|
|
|
// Styled Components
|
|
|
|
const NumberedContainer = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0;
|
|
padding: 16px;
|
|
`;
|
|
|
|
const StepItem = styled.button<{
|
|
$isCompleted: boolean;
|
|
$isCurrent: boolean;
|
|
$isAccessible: boolean;
|
|
$clickable: boolean;
|
|
}>`
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: none;
|
|
border: none;
|
|
cursor: ${(props) => (props.$clickable ? 'pointer' : 'default')};
|
|
opacity: ${(props) => (props.$isAccessible ? 1 : 0.5)};
|
|
transition: opacity 0.2s;
|
|
|
|
&:hover {
|
|
opacity: ${(props) => (props.$clickable ? 0.8 : props.$isAccessible ? 1 : 0.5)};
|
|
}
|
|
`;
|
|
|
|
const StepNumber = styled.div<{
|
|
$isCompleted: boolean;
|
|
$isCurrent: boolean;
|
|
}>`
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
|
|
${(props) => {
|
|
if (props.$isCompleted) {
|
|
return `
|
|
background: #10b981;
|
|
color: white;
|
|
border: 2px solid #10b981;
|
|
`;
|
|
}
|
|
if (props.$isCurrent) {
|
|
return `
|
|
background: #667eea;
|
|
color: white;
|
|
border: 2px solid #667eea;
|
|
`;
|
|
}
|
|
return `
|
|
background: white;
|
|
color: #9ca3af;
|
|
border: 2px solid #e5e7eb;
|
|
`;
|
|
}}
|
|
`;
|
|
|
|
const StepTitle = styled.span`
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
max-width: 80px;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
`;
|
|
|
|
const Connector = styled.div<{ $isCompleted: boolean }>`
|
|
width: 40px;
|
|
height: 2px;
|
|
background: ${(props) => (props.$isCompleted ? '#10b981' : '#e5e7eb')};
|
|
transition: background 0.2s;
|
|
|
|
@media (max-width: 640px) {
|
|
width: 20px;
|
|
}
|
|
`;
|
|
|
|
// Dots variant
|
|
const DotsContainer = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
`;
|
|
|
|
const Dot = styled.button<{
|
|
$isCompleted: boolean;
|
|
$isCurrent: boolean;
|
|
$isAccessible: boolean;
|
|
$clickable: boolean;
|
|
}>`
|
|
width: ${(props) => (props.$isCurrent ? '12px' : '8px')};
|
|
height: ${(props) => (props.$isCurrent ? '12px' : '8px')};
|
|
border-radius: 50%;
|
|
border: none;
|
|
cursor: ${(props) => (props.$clickable ? 'pointer' : 'default')};
|
|
transition: all 0.2s;
|
|
|
|
${(props) => {
|
|
if (props.$isCompleted) {
|
|
return `background: #10b981;`;
|
|
}
|
|
if (props.$isCurrent) {
|
|
return `background: #667eea;`;
|
|
}
|
|
return `background: #e5e7eb;`;
|
|
}}
|
|
|
|
&:hover {
|
|
transform: ${(props) => (props.$clickable ? 'scale(1.2)' : 'none')};
|
|
}
|
|
`;
|
|
|
|
// Bar variant
|
|
const BarContainer = styled.div`
|
|
padding: 16px;
|
|
`;
|
|
|
|
const BarTrack = styled.div`
|
|
width: 100%;
|
|
height: 6px;
|
|
background: #e5e7eb;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
`;
|
|
|
|
const BarFill = styled.div`
|
|
height: 100%;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
`;
|
|
|
|
const BarText = styled.div`
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
margin-top: 8px;
|
|
text-align: center;
|
|
`;
|