feat: implement monochrome design system across entire frontend
Migrate all 84 frontend files from the old themed/colored design to a monochrome glass-morphism design system. Pure black backgrounds, white text with opacity levels, glass-card components with backdrop-blur, and functional color reserved for status indicators only. Foundation: remap CSS variables to monochrome, simplify Tailwind config, remove theme toggle, convert brand logo/wordmark to white. Pages: all 14 pages updated. Components: all common, library, session, step-library, tree-editor, tree-preview, admin, and subscription components converted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,51 +6,41 @@ interface BrandLogoProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolutionFlow brand logo icon.
|
||||
* ResolutionFlow brand logo icon — white monochrome.
|
||||
* sm (32x32) for header/navbar, lg (80x80) for login/register pages.
|
||||
*/
|
||||
export function BrandLogo({ size = 'sm', className }: BrandLogoProps) {
|
||||
const sizeClasses = size === 'sm' ? 'h-8 w-8' : 'h-20 w-20'
|
||||
|
||||
// The SVG scales via viewBox - same paths work at any size.
|
||||
// Stroke widths are tuned per size for visual clarity.
|
||||
const strokeBase = size === 'sm' ? 1 : 2
|
||||
const strokeThick = size === 'sm' ? 1.25 : 2.5
|
||||
const dashArray = size === 'sm' ? '1 1.5' : '2 3'
|
||||
const nodeR = size === 'sm' ? { outer: 2.5, inner: 2.75 } : { outer: 5, inner: 5.5 }
|
||||
const hubR = size === 'sm' ? { glow: 5, solid: 3.5 } : { glow: 10, solid: 7 }
|
||||
|
||||
// Positions scale with viewBox
|
||||
const vb = size === 'sm' ? '0 0 40 40' : '0 0 80 80'
|
||||
const s = size === 'sm' ? 1 : 2 // scale factor
|
||||
const s = size === 'sm' ? 1 : 2
|
||||
|
||||
return (
|
||||
<svg viewBox={vb} fill="none" className={cn(sizeClasses, className)}>
|
||||
<defs>
|
||||
<linearGradient id="brand-logo-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#818cf8" />
|
||||
<stop offset="100%" stopColor="#a78bfa" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* Input nodes */}
|
||||
<circle cx={5 * s} cy={7 * s} r={nodeR.outer} fill="url(#brand-logo-grad)" opacity="0.35" />
|
||||
<circle cx={5 * s} cy={15 * s} r={nodeR.inner} fill="url(#brand-logo-grad)" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={25 * s} r={nodeR.inner} fill="url(#brand-logo-grad)" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={33 * s} r={nodeR.outer} fill="url(#brand-logo-grad)" opacity="0.35" />
|
||||
<circle cx={5 * s} cy={7 * s} r={nodeR.outer} fill="white" opacity="0.35" />
|
||||
<circle cx={5 * s} cy={15 * s} r={nodeR.inner} fill="white" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={25 * s} r={nodeR.inner} fill="white" opacity="0.5" />
|
||||
<circle cx={5 * s} cy={33 * s} r={nodeR.outer} fill="white" opacity="0.35" />
|
||||
|
||||
{/* Connecting lines */}
|
||||
<path d={`M${7.5 * s} ${7 * s}L${14 * s} ${17 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
<path d={`M${7.75 * s} ${15 * s}L${14 * s} ${19 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.75 * s} ${25 * s}L${14 * s} ${21 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.5 * s} ${33 * s}L${14 * s} ${23 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
<path d={`M${7.5 * s} ${7 * s}L${14 * s} ${17 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
<path d={`M${7.75 * s} ${15 * s}L${14 * s} ${19 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.75 * s} ${25 * s}L${14 * s} ${21 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" opacity="0.6" />
|
||||
<path d={`M${7.5 * s} ${33 * s}L${14 * s} ${23 * s}`} stroke="white" strokeWidth={strokeBase} strokeLinecap="round" strokeDasharray={dashArray} opacity="0.45" />
|
||||
|
||||
{/* Central hub */}
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.glow} fill="url(#brand-logo-grad)" opacity="0.15" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.solid} fill="url(#brand-logo-grad)" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.glow} fill="white" opacity="0.15" />
|
||||
<circle cx={18 * s} cy={20 * s} r={hubR.solid} fill="white" />
|
||||
|
||||
{/* Output arrow */}
|
||||
<path d={`M${21.5 * s} ${20 * s}H${35 * s}M${35 * s} ${20 * s}L${30 * s} ${15 * s}M${35 * s} ${20 * s}L${30 * s} ${25 * s}`} stroke="url(#brand-logo-grad)" strokeWidth={strokeThick} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d={`M${21.5 * s} ${20 * s}H${35 * s}M${35 * s} ${20 * s}L${30 * s} ${15 * s}M${35 * s} ${20 * s}L${30 * s} ${25 * s}`} stroke="white" strokeWidth={strokeThick} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,20 +6,19 @@ interface BrandWordmarkProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* ResolutionFlow wordmark with gradient "Flow" text.
|
||||
* ResolutionFlow wordmark — clean white text.
|
||||
* sm for header/navbar, lg for login/register pages.
|
||||
*/
|
||||
export function BrandWordmark({ size = 'sm', className }: BrandWordmarkProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'font-heading font-bold',
|
||||
size === 'sm' ? 'text-xl' : 'text-3xl tracking-tight',
|
||||
'font-semibold tracking-tight text-white',
|
||||
size === 'sm' ? 'text-xl' : 'text-3xl',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="text-foreground">Resolution</span>
|
||||
<span className="text-gradient-brand">Flow</span>
|
||||
ResolutionFlow
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ export function ConfirmDialog({
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'rounded-md border border-border px-4 py-2 text-sm font-medium',
|
||||
'text-card-foreground hover:bg-accent',
|
||||
'rounded-xl border border-white/10 px-4 py-2 text-sm font-medium',
|
||||
'text-white/60 hover:bg-white/10 hover:text-white',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
@@ -45,11 +45,11 @@ export function ConfirmDialog({
|
||||
onClick={onConfirm}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'rounded-md px-4 py-2 text-sm font-medium text-white',
|
||||
'rounded-xl px-4 py-2 text-sm font-medium',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
confirmVariant === 'destructive'
|
||||
? 'bg-destructive hover:bg-destructive/90'
|
||||
: 'bg-primary hover:bg-primary/90'
|
||||
? 'bg-red-400/10 text-red-400 hover:bg-red-400/20 border border-red-400/20'
|
||||
: 'bg-white text-black hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
{isLoading ? 'Processing...' : confirmLabel}
|
||||
@@ -57,7 +57,7 @@ export function ConfirmDialog({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{message}</p>
|
||||
<p className="text-sm text-white/70">{message}</p>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,22 +34,22 @@ export class ErrorBoundary extends Component<Props, State> {
|
||||
return (
|
||||
<div className="flex min-h-[400px] flex-col items-center justify-center p-8">
|
||||
<div className="max-w-md text-center">
|
||||
<h2 className="mb-2 text-xl font-semibold text-foreground">
|
||||
<h2 className="mb-2 text-xl font-semibold text-red-400">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
<p className="mb-4 text-white/70">
|
||||
An unexpected error occurred. Please try refreshing the page.
|
||||
</p>
|
||||
{this.state.error && (
|
||||
<pre className="mb-4 overflow-auto rounded bg-muted p-3 text-left text-xs text-muted-foreground">
|
||||
<pre className="mb-4 overflow-auto rounded-xl bg-white/5 border border-white/[0.06] p-3 text-left text-xs text-red-400">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
)}
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Refresh Page
|
||||
|
||||
@@ -52,7 +52,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -60,23 +60,23 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
||||
{/* Modal Content */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex w-full flex-col border border-border bg-card shadow-lg',
|
||||
'max-h-[100vh] rounded-t-lg sm:max-h-[85vh] sm:rounded-lg',
|
||||
'relative flex w-full flex-col border border-white/[0.06] bg-[#0a0a0a] shadow-lg',
|
||||
'max-h-[100vh] rounded-t-2xl sm:max-h-[85vh] sm:rounded-2xl',
|
||||
'animate-scale-in',
|
||||
sizeClasses[size]
|
||||
)}
|
||||
>
|
||||
{/* Header - Fixed at top */}
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<h2 id="modal-title" className="text-lg font-semibold text-card-foreground">
|
||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-white/[0.06] px-4 py-3 sm:px-6 sm:py-4">
|
||||
<h2 id="modal-title" className="text-lg font-semibold text-white">
|
||||
{title}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'rounded-md p-1.5 text-muted-foreground transition-colors sm:p-1',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
'rounded-md p-1.5 text-white/40 transition-colors sm:p-1',
|
||||
'hover:bg-white/10 hover:text-white',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/20'
|
||||
)}
|
||||
aria-label="Close modal"
|
||||
>
|
||||
@@ -91,7 +91,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
||||
|
||||
{/* Footer - Fixed at bottom */}
|
||||
{footer && (
|
||||
<div className="flex-shrink-0 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||
<div className="flex-shrink-0 border-t border-white/[0.06] px-4 py-3 sm:px-6 sm:py-4">
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function PageLoader() {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-background">
|
||||
<div className="flex h-screen items-center justify-center bg-black">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<p className="text-sm text-muted-foreground">Loading...</p>
|
||||
<div className="h-12 w-12 animate-spin rounded-full border-4 border-white/20 border-t-white" />
|
||||
<p className="text-sm text-white/40">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -17,19 +17,19 @@ export function RouteError() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-background p-8">
|
||||
<div className="flex min-h-screen flex-col items-center justify-center bg-black p-8">
|
||||
<div className="max-w-md text-center">
|
||||
<h1 className="mb-2 text-4xl font-bold text-foreground">Oops!</h1>
|
||||
<h2 className="mb-2 text-xl font-semibold text-foreground">{errorMessage}</h2>
|
||||
<h1 className="mb-2 text-4xl font-bold text-white">Oops!</h1>
|
||||
<h2 className="mb-2 text-xl font-semibold text-red-400">{errorMessage}</h2>
|
||||
{errorDetails && (
|
||||
<p className="mb-4 text-muted-foreground">{errorDetails}</p>
|
||||
<p className="mb-4 text-white/70">{errorDetails}</p>
|
||||
)}
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className={cn(
|
||||
'rounded-md border border-input px-4 py-2 text-sm font-medium',
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
'rounded-xl border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
|
||||
'hover:bg-white/10 hover:text-white'
|
||||
)}
|
||||
>
|
||||
Go Back
|
||||
@@ -37,8 +37,8 @@ export function RouteError() {
|
||||
<button
|
||||
onClick={() => navigate('/trees')}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
>
|
||||
Go Home
|
||||
|
||||
@@ -48,14 +48,14 @@ export function StarRating({
|
||||
sizeClasses[size],
|
||||
star <= value
|
||||
? 'fill-yellow-400 text-yellow-400'
|
||||
: 'fill-none text-muted-foreground',
|
||||
: 'fill-none text-white/30',
|
||||
!readonly && 'hover:text-yellow-300'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
{showCount && (
|
||||
<span className="ml-1 text-sm text-muted-foreground">
|
||||
<span className="ml-1 text-sm text-white/40">
|
||||
({value}/5)
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -37,8 +37,8 @@ export function TagBadges({
|
||||
'rounded-full transition-colors',
|
||||
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||
variant === 'default'
|
||||
? 'bg-primary/10 text-primary hover:bg-primary/20'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80',
|
||||
? 'bg-white/10 text-white/70 hover:bg-white/15'
|
||||
: 'bg-white/5 text-white/40 hover:bg-white/10',
|
||||
!onTagClick && 'cursor-default'
|
||||
)}
|
||||
>
|
||||
@@ -50,7 +50,7 @@ export function TagBadges({
|
||||
className={cn(
|
||||
'rounded-full',
|
||||
size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-2.5 py-1 text-sm',
|
||||
'bg-muted text-muted-foreground'
|
||||
'bg-white/5 text-white/40'
|
||||
)}
|
||||
title={tags.slice(maxVisible).join(', ')}
|
||||
>
|
||||
|
||||
@@ -118,11 +118,11 @@ export function TagInput({
|
||||
<div ref={wrapperRef} className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-wrap gap-1.5 rounded-md border px-2 py-1.5',
|
||||
'bg-background text-foreground',
|
||||
'focus-within:border-primary focus-within:ring-1 focus-within:ring-primary',
|
||||
'flex flex-wrap gap-1.5 rounded-xl border px-2 py-1.5',
|
||||
'bg-black/50 text-white',
|
||||
'focus-within:border-white/30 focus-within:ring-1 focus-within:ring-white/20',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : '',
|
||||
'border-input'
|
||||
'border-white/10'
|
||||
)}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
@@ -132,7 +132,7 @@ export function TagInput({
|
||||
key={tag}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs',
|
||||
'bg-primary/10 text-primary'
|
||||
'bg-white/10 text-white/70'
|
||||
)}
|
||||
>
|
||||
{tag}
|
||||
@@ -143,7 +143,7 @@ export function TagInput({
|
||||
e.stopPropagation()
|
||||
removeTag(tag)
|
||||
}}
|
||||
className="rounded-full p-0.5 hover:bg-primary/20"
|
||||
className="rounded-full p-0.5 hover:bg-white/20"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -167,8 +167,8 @@ export function TagInput({
|
||||
placeholder={tags.length === 0 ? placeholder : ''}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex-1 min-w-[80px] border-0 bg-transparent px-1 py-0.5 text-sm',
|
||||
'placeholder:text-muted-foreground',
|
||||
'flex-1 min-w-[80px] border-0 bg-transparent px-1 py-0.5 text-sm text-white',
|
||||
'placeholder:text-white/40',
|
||||
'focus:outline-none focus:ring-0'
|
||||
)}
|
||||
/>
|
||||
@@ -179,8 +179,8 @@ export function TagInput({
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute z-10 mt-1 w-full rounded-md border border-input',
|
||||
'bg-popover shadow-lg'
|
||||
'absolute z-10 mt-1 w-full rounded-xl border border-white/[0.06]',
|
||||
'bg-[#0a0a0a] shadow-lg'
|
||||
)}
|
||||
>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
@@ -189,13 +189,13 @@ export function TagInput({
|
||||
type="button"
|
||||
onClick={() => addTag(suggestion.name)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between px-3 py-2 text-sm',
|
||||
'hover:bg-accent',
|
||||
index === selectedIndex && 'bg-accent'
|
||||
'flex w-full items-center justify-between px-3 py-2 text-sm text-white/70',
|
||||
'hover:bg-white/10',
|
||||
index === selectedIndex && 'bg-white/10'
|
||||
)}
|
||||
>
|
||||
<span>{suggestion.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-xs text-white/40">
|
||||
{suggestion.usage_count} trees
|
||||
</span>
|
||||
</button>
|
||||
@@ -208,8 +208,8 @@ export function TagInput({
|
||||
type="button"
|
||||
onClick={() => addTag(inputValue)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 border-t border-input px-3 py-2 text-sm',
|
||||
'hover:bg-accent text-primary'
|
||||
'flex w-full items-center gap-2 border-t border-white/[0.06] px-3 py-2 text-sm',
|
||||
'hover:bg-white/10 text-white'
|
||||
)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -220,7 +220,7 @@ export function TagInput({
|
||||
)}
|
||||
|
||||
{/* Helper text */}
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
<p className="mt-1 text-xs text-white/40">
|
||||
{tags.length}/{maxTags} tags. Press Enter or comma to add.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -11,17 +11,17 @@ export function UpgradePrompt({ feature, className }: UpgradePromptProps) {
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-4',
|
||||
'glass-card rounded-2xl border border-white/[0.06] p-4',
|
||||
className
|
||||
)}>
|
||||
<h3 className="font-semibold text-foreground">Plan Limit Reached</h3>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
<h3 className="font-semibold text-white">Plan Limit Reached</h3>
|
||||
<p className="mt-1 text-sm text-white/70">
|
||||
Your {plan} plan doesn't allow you to {feature}. Upgrade your plan to continue.
|
||||
</p>
|
||||
<button
|
||||
className={cn(
|
||||
'mt-3 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
'mt-3 rounded-xl bg-white px-4 py-2 text-sm font-medium text-black',
|
||||
'hover:bg-white/90'
|
||||
)}
|
||||
onClick={() => window.location.href = '/account'}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user