feat: complete frontend redesign — all 6 pages implemented per prototype specs

Fix UI components (Badge, Button, Card, Input, ProgressBar, ScoreBar,
ChatPanel) to use CSS variables instead of old theme object pattern.
Clean up barrel exports (common/index.ts, layout/index.ts) and remove
stale router/tabs import from shell-config.ts. Build: tsc zero errors,
Vite production build succeeds (1760 modules, 270 kB JS, 22 kB CSS).
This commit is contained in:
2026-06-03 18:42:42 +08:00
parent 6414d67b3b
commit bc8ccc1143
10 changed files with 92 additions and 107 deletions

View File

@@ -1,3 +1 @@
export { TLogo } from './TLogo'; // common UI helpers — add exports here as new shared components are created
export { ThemeToggle } from './ThemeToggle';
export { TPattern } from './TPattern';

View File

@@ -1,4 +1,3 @@
export { AppShell } from './AppShell'; export { AppShell } from './AppShell';
export { ContentLayout } from './ContentLayout'; export { Sidebar } from './Sidebar';
export { FooterLayout } from './FooterLayout'; export { Topbar } from './Topbar';
export { HeaderLayout } from './HeaderLayout';

View File

@@ -1,5 +1,3 @@
import { appTabs } from '../../router/tabs';
export const shellFrameClassName = 'mx-auto w-full max-w-[1680px] px-8'; export const shellFrameClassName = 'mx-auto w-full max-w-[1680px] px-8';
export const shellMeta = { export const shellMeta = {
@@ -8,5 +6,3 @@ export const shellMeta = {
status: 'ONLINE', status: 'ONLINE',
surface: 'Desktop Web', surface: 'Desktop Web',
} as const; } as const;
export const shellModuleSummary = appTabs.map((tab) => tab.label).join(' / ');

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
interface BadgeProps { interface BadgeProps {
children: React.ReactNode; children: React.ReactNode;
@@ -7,32 +6,29 @@ interface BadgeProps {
size?: 'sm' | 'md'; size?: 'sm' | 'md';
} }
const colorMap: Record<string, { bg: string; text: string }> = {
accent: { bg: 'var(--accent)', text: '#fff' },
green: { bg: 'var(--success)', text: '#fff' },
orange: { bg: 'var(--warn)', text: '#fff' },
red: { bg: 'var(--danger)', text: '#fff' },
};
export const Badge: React.FC<BadgeProps> = ({ export const Badge: React.FC<BadgeProps> = ({
children, children,
color = 'accent', color = 'accent',
size = 'sm', size = 'sm',
}) => { }) => {
const { theme } = useTheme(); const sizeStyles: Record<string, string> = {
const colorStyles = {
accent: { bg: theme.gradientAccent, text: '#fff' },
green: { bg: theme.green, text: '#fff' },
orange: { bg: theme.orange, text: '#fff' },
red: { bg: '#ff4444', text: '#fff' },
};
const sizeStyles = {
sm: 'px-2 py-0.5 text-xs', sm: 'px-2 py-0.5 text-xs',
md: 'px-3 py-1 text-sm', md: 'px-3 py-1 text-sm',
}; };
const c = colorMap[color] ?? colorMap.accent;
return ( return (
<span <span
className={`${sizeStyles[size]} rounded font-mono font-medium`} className={`${sizeStyles[size]} rounded font-mono font-medium`}
style={{ style={{ background: c.bg, color: c.text }}
background: colorStyles[color].bg,
color: colorStyles[color].text,
}}
> >
{children} {children}
</span> </span>

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
interface ButtonProps { interface ButtonProps {
variant?: 'primary' | 'secondary'; variant?: 'primary' | 'secondary';
@@ -18,8 +17,6 @@ export const Button: React.FC<ButtonProps> = ({
disabled = false, disabled = false,
className = '', className = '',
}) => { }) => {
const { theme } = useTheme();
const baseStyles = ` const baseStyles = `
inline-flex items-center justify-center inline-flex items-center justify-center
font-semibold rounded-xl cursor-pointer font-semibold rounded-xl cursor-pointer
@@ -27,13 +24,13 @@ export const Button: React.FC<ButtonProps> = ({
disabled:cursor-not-allowed disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50
`; `;
const sizeStyles = { const sizeStyles: Record<string, string> = {
sm: 'px-3 py-1.5 text-xs', sm: 'px-3 py-1.5 text-xs',
md: 'px-5 py-3 text-sm', md: 'px-5 py-3 text-sm',
lg: 'px-8 py-5 text-base', lg: 'px-8 py-5 text-base',
}; };
const variantStyles = { const variantStyles: Record<string, string> = {
primary: ` primary: `
bg-gradient-to-r from-t-accent to-t-accent-dark bg-gradient-to-r from-t-accent to-t-accent-dark
text-white hover:shadow-t-accent hover:-translate-y-0.5 text-white hover:shadow-t-accent hover:-translate-y-0.5
@@ -50,9 +47,6 @@ export const Button: React.FC<ButtonProps> = ({
onClick={onClick} onClick={onClick}
disabled={disabled} disabled={disabled}
className={`${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]} ${className}`} className={`${baseStyles} ${sizeStyles[size]} ${variantStyles[variant]} ${className}`}
style={{
color: variant === 'primary' ? '#fff' : theme.text2,
}}
> >
{children} {children}
</button> </button>

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
interface CardProps { interface CardProps {
accent?: boolean; accent?: boolean;
@@ -18,9 +17,7 @@ export const Card: React.FC<CardProps> = ({
className = '', className = '',
onClick, onClick,
}) => { }) => {
const { theme, isDark } = useTheme(); const paddingStyles: Record<string, string> = {
const paddingStyles = {
sm: 'p-4', sm: 'p-4',
md: 'p-5', md: 'p-5',
lg: 'p-8', lg: 'p-8',
@@ -30,22 +27,22 @@ export const Card: React.FC<CardProps> = ({
<div <div
onClick={onClick} onClick={onClick}
className={` className={`
rounded-xl border transition-all duration-200 cursor-default relative rounded-xl border transition-all duration-200 cursor-default
${paddingStyles[padding]} ${paddingStyles[padding]}
${accent ? 'border-t-accent' : 'border-t-border'} ${accent ? 'border-[color:var(--accent)]' : 'border-[color:var(--border)]'}
${highlight ? 'border-2 border-t-accent' : ''} ${highlight ? 'border-2 border-[color:var(--accent)]' : ''}
${!isDark ? 'shadow-t-card' : ''} ${onClick ? 'cursor-pointer hover:border-[color:var(--accent)]' : ''}
${onClick ? 'cursor-pointer hover:border-t-accent' : ''}
${className} ${className}
`} `}
style={{ style={{
backgroundColor: theme.bgCard, backgroundColor: 'var(--surface)',
boxShadow: 'var(--shadow-card)',
}} }}
> >
{accent && ( {accent && (
<div <div
className="absolute top-0 left-0 right-0 h-[3px] rounded-t-xl" className="absolute top-0 left-0 right-0 h-[3px] rounded-t-xl"
style={{ background: theme.gradientAccent }} style={{ background: 'linear-gradient(90deg, var(--accent), var(--accent-hover))' }}
/> />
)} )}
{children} {children}

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
interface InputProps { interface InputProps {
value: string; value: string;
@@ -18,8 +17,6 @@ export const Input: React.FC<InputProps> = ({
className = '', className = '',
type = 'text', type = 'text',
}) => { }) => {
const { theme } = useTheme();
return ( return (
<input <input
type={type} type={type}
@@ -28,17 +25,24 @@ export const Input: React.FC<InputProps> = ({
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
placeholder={placeholder} placeholder={placeholder}
className={` className={`
w-full px-4 py-3 text-sm w-full px-4 py-3 text-sm rounded-lg outline-none
bg-t-bg-card border border-t-border rounded-lg focus:ring-1
text-t-text outline-none placeholder:text-[color:var(--muted)]
focus:border-t-accent focus:ring-1 focus:ring-t-accent
placeholder:text-t-text3
${className} ${className}
`} `}
style={{ style={{
backgroundColor: theme.bgCard, backgroundColor: 'var(--surface)',
borderColor: theme.border, borderColor: 'var(--border)',
color: theme.text, border: '1px solid var(--border)',
color: 'var(--fg)',
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = 'var(--accent)';
e.currentTarget.style.boxShadow = '0 0 0 1px var(--accent)';
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = 'var(--border)';
e.currentTarget.style.boxShadow = 'none';
}} }}
/> />
); );

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
interface ProgressBarProps { interface ProgressBarProps {
percent: number; percent: number;
@@ -7,36 +6,39 @@ interface ProgressBarProps {
showLabel?: boolean; showLabel?: boolean;
} }
const colorBg: Record<string, string> = {
accent: 'linear-gradient(90deg, var(--accent), var(--accent-hover))',
green: 'linear-gradient(90deg, var(--success), #00ff88)',
orange: 'linear-gradient(90deg, var(--warn), #ffaa00)',
red: 'var(--danger)',
};
export const ProgressBar: React.FC<ProgressBarProps> = ({ export const ProgressBar: React.FC<ProgressBarProps> = ({
percent, percent,
color = 'accent', color = 'accent',
showLabel = false, showLabel = false,
}) => { }) => {
const { theme } = useTheme();
const colorStyles = {
accent: theme.gradientAccent,
green: `linear-gradient(90deg, ${theme.green}, #00ff88)`,
orange: `linear-gradient(90deg, ${theme.orange}, #ffaa00)`,
red: '#ff4444',
};
return ( return (
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className="h-2 rounded-full flex-1" className="h-2 rounded-full flex-1"
style={{ backgroundColor: theme.bgHover }} style={{ backgroundColor: 'var(--border)' }}
> >
<div <div
className="h-full rounded-full transition-all duration-300" className="h-full rounded-full transition-all duration-300"
style={{ style={{
width: `${percent}%`, width: `${percent}%`,
background: colorStyles[color], background: colorBg[color] ?? colorBg.accent,
}} }}
/> />
</div> </div>
{showLabel && ( {showLabel && (
<span className="font-mono text-xs text-t-accent">{percent}%</span> <span
className="font-mono text-xs"
style={{ color: 'var(--accent)' }}
>
{percent}%
</span>
)} )}
</div> </div>
); );

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
interface ScoreBarProps { interface ScoreBarProps {
score: number; // 0-100 score: number; // 0-100
@@ -12,27 +11,25 @@ export const ScoreBar: React.FC<ScoreBarProps> = ({
label, label,
accent = false, accent = false,
}) => { }) => {
const { theme } = useTheme();
return ( return (
<div <div
className="p-5 rounded-xl border" className="p-5 rounded-xl border"
style={{ style={{
backgroundColor: theme.bgCard, backgroundColor: 'var(--surface)',
borderColor: accent ? theme.accent : theme.border, borderColor: accent ? 'var(--accent)' : 'var(--border)',
}} }}
> >
{label && ( {label && (
<div <div
className="font-mono text-xs mb-2" className="font-mono text-xs mb-2"
style={{ color: theme.text3, letterSpacing: '1px' }} style={{ color: 'var(--muted)', letterSpacing: '1px' }}
> >
{label} {label}
</div> </div>
)} )}
<div <div
className="font-mono text-3xl font-bold" className="font-mono text-3xl font-bold"
style={{ color: accent ? theme.accent : theme.text }} style={{ color: accent ? 'var(--accent)' : 'var(--fg)' }}
> >
{score} {score}
</div> </div>
@@ -42,7 +39,7 @@ export const ScoreBar: React.FC<ScoreBarProps> = ({
key={i} key={i}
className="w-2 h-4 rounded-sm" className="w-2 h-4 rounded-sm"
style={{ style={{
backgroundColor: i < score / 10 ? theme.accent : theme.bgHover, backgroundColor: i < score / 10 ? 'var(--accent)' : 'var(--border)',
}} }}
/> />
))} ))}

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from '../../contexts';
import type { ComplianceChunk } from '../../types'; import type { ComplianceChunk } from '../../types';
interface ChatPanelProps { interface ChatPanelProps {
@@ -29,7 +28,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
'法规的具体要求是什么?', '法规的具体要求是什么?',
], ],
}) => { }) => {
const { theme } = useTheme();
const activeChunk = chunks.find(c => c.id === activeChunkId); const activeChunk = chunks.find(c => c.id === activeChunkId);
return ( return (
@@ -40,8 +38,8 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
top: 0, top: 0,
bottom: 0, bottom: 0,
width: 420, width: 420,
background: theme.bgCard, background: 'var(--surface)',
borderLeft: `1px solid ${theme.border}`, borderLeft: '1px solid var(--border)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
zIndex: 50, zIndex: 50,
@@ -51,14 +49,14 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
{/* Chat Header */} {/* Chat Header */}
<div style={{ <div style={{
padding: '20px 24px', padding: '20px 24px',
borderBottom: `1px solid ${theme.border}`, borderBottom: '1px solid var(--border)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}}> }}>
<div> <div>
<div style={{ fontSize: 14, fontWeight: 600, color: theme.text }}></div> <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg)' }}></div>
<div className="mono" style={{ fontSize: 11, color: theme.text3 }}> <div className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>
#{activeChunk?.index} · {activeChunk?.regulations.length} #{activeChunk?.index} · {activeChunk?.regulations.length}
</div> </div>
</div> </div>
@@ -68,7 +66,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
width: 32, width: 32,
height: 32, height: 32,
borderRadius: 8, borderRadius: 8,
background: theme.bgHover, background: 'var(--rail-hover)',
border: 'none', border: 'none',
cursor: 'pointer', cursor: 'pointer',
display: 'flex', display: 'flex',
@@ -77,7 +75,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
}} }}
> >
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6L18 18" stroke={theme.text3} strokeWidth="2" strokeLinecap="round"/> <path d="M18 6L6 18M6 6L18 18" stroke="var(--muted)" strokeWidth="2" strokeLinecap="round"/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -85,15 +83,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
{/* Current Chunk Info */} {/* Current Chunk Info */}
<div style={{ <div style={{
padding: '16px 24px', padding: '16px 24px',
background: theme.bgHover, background: 'var(--rail-hover)',
borderBottom: `1px solid ${theme.border}`, borderBottom: '1px solid var(--border)',
}}> }}>
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8, color: theme.text }}> <div style={{ fontSize: 13, fontWeight: 500, marginBottom: 8, color: 'var(--fg)' }}>
{activeChunk?.intent} {activeChunk?.intent}
</div> </div>
<div style={{ <div style={{
fontSize: 12, fontSize: 12,
color: theme.text2, color: 'var(--muted)',
lineHeight: 1.5, lineHeight: 1.5,
maxHeight: 60, maxHeight: 60,
overflow: 'hidden', overflow: 'hidden',
@@ -122,7 +120,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
width: 32, width: 32,
height: 32, height: 32,
borderRadius: 8, borderRadius: 8,
background: theme.gradientAccent, background: 'linear-gradient(90deg, var(--accent), var(--accent-hover))',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -136,13 +134,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
<div style={{ <div style={{
maxWidth: '80%', maxWidth: '80%',
padding: '12px 16px', padding: '12px 16px',
background: msg.role === 'user' ? theme.gradientAccent : theme.bgElevated, background: msg.role === 'user'
? 'linear-gradient(90deg, var(--accent), var(--accent-hover))'
: 'var(--surface)',
borderRadius: 12, borderRadius: 12,
color: msg.role === 'user' ? '#fff' : theme.text, color: msg.role === 'user' ? '#fff' : 'var(--fg)',
fontSize: 14, fontSize: 14,
lineHeight: 1.6, lineHeight: 1.6,
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
border: msg.role === 'assistant' ? `1px solid ${theme.border}` : 'none', border: msg.role === 'assistant' ? '1px solid var(--border)' : 'none',
}}> }}>
{msg.content} {msg.content}
</div> </div>
@@ -154,7 +154,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
width: 32, width: 32,
height: 32, height: 32,
borderRadius: 8, borderRadius: 8,
background: theme.gradientAccent, background: 'linear-gradient(90deg, var(--accent), var(--accent-hover))',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -165,15 +165,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
</div> </div>
<div style={{ <div style={{
padding: '12px 16px', padding: '12px 16px',
background: theme.bgElevated, background: 'var(--surface)',
borderRadius: 12, borderRadius: 12,
border: `1px solid ${theme.border}`, border: '1px solid var(--border)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 8, gap: 8,
}}> }}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: theme.accent, animation: 'pulse 1s infinite' }} /> <div style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--accent)', animation: 'pulse 1s infinite' }} />
<span style={{ fontSize: 13, color: theme.text2 }}>...</span> <span style={{ fontSize: 13, color: 'var(--muted)' }}>...</span>
</div> </div>
</div> </div>
)} )}
@@ -193,10 +193,10 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
style={{ style={{
padding: '6px 12px', padding: '6px 12px',
fontSize: 12, fontSize: 12,
background: theme.bgHover, background: 'var(--rail-hover)',
border: `1px solid ${theme.border}`, border: '1px solid var(--border)',
borderRadius: 6, borderRadius: 6,
color: theme.text2, color: 'var(--muted)',
cursor: 'pointer', cursor: 'pointer',
}} }}
>{q}</button> >{q}</button>
@@ -206,7 +206,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
{/* Chat Input */} {/* Chat Input */}
<div style={{ <div style={{
padding: '16px 24px', padding: '16px 24px',
borderTop: `1px solid ${theme.border}`, borderTop: '1px solid var(--border)',
display: 'flex', display: 'flex',
gap: 12, gap: 12,
}}> }}>
@@ -219,10 +219,10 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
flex: 1, flex: 1,
padding: 12, padding: 12,
fontSize: 14, fontSize: 14,
background: theme.bgHover, background: 'var(--rail-hover)',
border: `1px solid ${theme.border}`, border: '1px solid var(--border)',
borderRadius: 8, borderRadius: 8,
color: theme.text, color: 'var(--fg)',
outline: 'none', outline: 'none',
}} }}
/> />
@@ -233,8 +233,10 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({
padding: '12px 20px', padding: '12px 20px',
fontSize: 14, fontSize: 14,
fontWeight: 600, fontWeight: 600,
background: chatLoading || !chatInput.trim() ? theme.bgHover : theme.gradientAccent, background: chatLoading || !chatInput.trim()
color: chatLoading || !chatInput.trim() ? theme.text3 : '#fff', ? 'var(--rail-hover)'
: 'linear-gradient(90deg, var(--accent), var(--accent-hover))',
color: chatLoading || !chatInput.trim() ? 'var(--muted)' : '#fff',
border: 'none', border: 'none',
borderRadius: 8, borderRadius: 8,
cursor: chatLoading || !chatInput.trim() ? 'not-allowed' : 'pointer', cursor: chatLoading || !chatInput.trim() ? 'not-allowed' : 'pointer',