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:
chihlasm
2026-02-09 21:41:29 -05:00
parent 1381aaae99
commit f4ce1595d6
88 changed files with 2976 additions and 1596 deletions

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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(', ')}
>

View File

@@ -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>

View File

@@ -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'}
>