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

@@ -53,8 +53,8 @@ export function ActionMenu({ items }: ActionMenuProps) {
ref={buttonRef}
onClick={() => setOpen(!open)}
className={cn(
'rounded-md p-1.5 text-muted-foreground transition-colors',
'hover:bg-accent hover:text-accent-foreground'
'rounded-md p-1.5 text-white/50 transition-colors',
'hover:bg-white/10 hover:text-white'
)}
>
<MoreHorizontal className="h-4 w-4" />
@@ -63,8 +63,8 @@ export function ActionMenu({ items }: ActionMenuProps) {
<div
ref={menuRef}
className={cn(
'fixed z-50 min-w-[160px] rounded-md border border-border',
'bg-card py-1 shadow-lg animate-scale-in'
'fixed z-50 min-w-[160px] rounded-md border border-white/10',
'bg-black py-1 shadow-lg animate-scale-in'
)}
style={{
top: `${menuPosition.top}px`,
@@ -80,8 +80,8 @@ export function ActionMenu({ items }: ActionMenuProps) {
'flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors',
'disabled:opacity-50 disabled:pointer-events-none',
item.destructive
? 'text-destructive hover:bg-destructive/10'
: 'text-foreground hover:bg-accent'
? 'text-red-400 hover:bg-red-400/10'
: 'text-white/70 hover:bg-white/[0.06]'
)}
>
{item.icon}

View File

@@ -36,7 +36,7 @@ export function AdminLayout() {
return (
<div className="flex h-[calc(100vh-4rem)]">
{/* Desktop sidebar */}
<div className="hidden w-60 flex-shrink-0 border-r border-border bg-card md:block">
<div className="hidden w-60 flex-shrink-0 border-r border-white/[0.06] bg-black md:block">
<AdminSidebar />
</div>
@@ -44,14 +44,14 @@ export function AdminLayout() {
{mobileOpen && (
<div className="fixed inset-0 z-40 md:hidden">
<div
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={() => setMobileOpen(false)}
/>
<div className="absolute inset-y-0 left-0 w-60 border-r border-border bg-card shadow-xl">
<div className="absolute inset-y-0 left-0 w-60 border-r border-white/[0.06] bg-black shadow-xl">
<div className="flex h-12 items-center justify-end px-3">
<button
onClick={() => setMobileOpen(false)}
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent"
className="rounded-md p-1.5 text-white/50 hover:bg-white/[0.06]"
>
<X className="h-4 w-4" />
</button>
@@ -67,7 +67,7 @@ export function AdminLayout() {
{/* Mobile menu button */}
<button
onClick={() => setMobileOpen(true)}
className="mb-4 rounded-md p-2 text-muted-foreground hover:bg-accent md:hidden"
className="mb-4 rounded-md p-2 text-white/50 hover:bg-white/[0.06] md:hidden"
>
<Menu className="h-5 w-5" />
</button>

View File

@@ -39,7 +39,7 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
return (
<aside className={cn('flex h-full flex-col', className)}>
<div className="p-4">
<h2 className="font-heading text-lg font-bold text-foreground">Admin Panel</h2>
<h2 className="text-lg font-bold text-white">Admin Panel</h2>
</div>
<nav className="flex-1 space-y-1 px-3">
{navItems.map((item) => (
@@ -50,8 +50,8 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive(item.path, item.end)
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
? 'bg-white/10 text-white'
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
)}
>
<item.icon className="h-4 w-4" />
@@ -59,13 +59,13 @@ export function AdminSidebar({ className, onNavigate }: AdminSidebarProps) {
</Link>
))}
</nav>
<div className="border-t border-border p-3">
<div className="border-t border-white/[0.06] p-3">
<Link
to="/trees"
onClick={onNavigate}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium',
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
'text-white/50 hover:bg-white/[0.06] hover:text-white'
)}
>
<ArrowLeft className="h-4 w-4" />

View File

@@ -38,7 +38,7 @@ export function CategoryRow({
ref={setNodeRef}
style={style}
className={cn(
'flex items-center gap-3 rounded-lg border border-border bg-card p-4',
'flex items-center gap-3 glass-card rounded-2xl p-4',
isDragging && 'opacity-50'
)}
>
@@ -47,7 +47,7 @@ export function CategoryRow({
type="button"
{...attributes}
{...listeners}
className="cursor-grab touch-none text-muted-foreground hover:text-foreground active:cursor-grabbing"
className="cursor-grab touch-none text-white/50 hover:text-white active:cursor-grabbing"
aria-label="Drag to reorder"
>
<GripVertical className="h-5 w-5" />
@@ -56,17 +56,17 @@ export function CategoryRow({
{/* Category Info */}
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium text-foreground">{category.name}</h3>
<h3 className="font-medium text-white">{category.name}</h3>
{!category.is_active && (
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs font-medium text-white/70">
Archived
</span>
)}
</div>
{category.description && (
<p className="mt-1 text-sm text-muted-foreground">{category.description}</p>
<p className="mt-1 text-sm text-white/40">{category.description}</p>
)}
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-xs text-white/40">
{stepCount} step{stepCount !== 1 ? 's' : ''}
</p>
</div>
@@ -77,8 +77,8 @@ export function CategoryRow({
type="button"
onClick={() => onEdit(category)}
className={cn(
'rounded-md border border-input bg-background p-2 text-muted-foreground',
'hover:bg-accent hover:text-accent-foreground'
'rounded-md border border-white/10 bg-black/50 p-2 text-white/50',
'hover:bg-white/10 hover:text-white'
)}
title="Edit category"
>

View File

@@ -58,15 +58,15 @@ export function CreateCategoryModal({
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="w-full max-w-md glass-card rounded-2xl p-6 shadow-lg">
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-foreground">Create Category</h2>
<h2 className="text-lg font-semibold text-white">Create Category</h2>
<button
onClick={handleClose}
disabled={isSaving}
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
className="rounded-full p-1 text-white/50 hover:bg-white/10 hover:text-white disabled:opacity-50"
>
<X className="h-5 w-5" />
</button>
@@ -76,15 +76,15 @@ export function CreateCategoryModal({
<form onSubmit={handleSubmit} className="space-y-4">
{/* Error Message */}
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-red-400/10 p-3 text-sm text-red-400">
{error}
</div>
)}
{/* Name Field */}
<div>
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-destructive">*</span>
<label htmlFor="name" className="mb-1 block text-sm font-medium text-white">
Category Name <span className="text-red-400">*</span>
</label>
<input
id="name"
@@ -96,21 +96,21 @@ export function CreateCategoryModal({
placeholder="e.g., Network Troubleshooting"
required
className={cn(
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
'disabled:opacity-50'
)}
/>
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-xs text-white/40">
{name.length}/100 characters
</p>
</div>
{/* Description Field */}
<div>
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
<label htmlFor="description" className="mb-1 block text-sm font-medium text-white">
Description <span className="text-white/40">(optional)</span>
</label>
<textarea
id="description"
@@ -120,9 +120,9 @@ export function CreateCategoryModal({
rows={3}
placeholder="Brief description of this category..."
className={cn(
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
'disabled:opacity-50'
)}
/>
@@ -135,8 +135,8 @@ export function CreateCategoryModal({
onClick={handleClose}
disabled={isSaving}
className={cn(
'rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
'rounded-md 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'
)}
>
Cancel
@@ -145,8 +145,8 @@ export function CreateCategoryModal({
type="submit"
disabled={isSaving || !name.trim()}
className={cn(
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 disabled:opacity-50'
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50'
)}
>
{isSaving ? 'Creating...' : 'Create Category'}

View File

@@ -50,16 +50,16 @@ export function DataTable<T>({
}
return (
<div className="overflow-x-auto rounded-lg border border-border">
<div className="overflow-x-auto rounded-lg border border-white/[0.06]">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border bg-muted/50">
<tr className="border-b border-white/[0.06] bg-white/[0.02]">
{columns.map((col) => (
<th
key={col.key}
className={cn(
'px-4 py-3 text-left font-medium text-muted-foreground',
col.sortable && 'cursor-pointer select-none hover:text-foreground',
'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-white/50',
col.sortable && 'cursor-pointer select-none hover:text-white',
col.className
)}
onClick={col.sortable ? () => handleSort(col.key) : undefined}
@@ -87,10 +87,10 @@ export function DataTable<T>({
<tbody>
{isLoading ? (
Array.from({ length: skeletonRows }).map((_, i) => (
<tr key={i} className="border-b border-border last:border-0">
<tr key={i} className="border-b border-white/[0.06] last:border-0">
{columns.map((col) => (
<td key={col.key} className="px-4 py-3">
<div className="h-4 w-3/4 animate-pulse rounded bg-muted" />
<div className="h-4 w-3/4 animate-pulse rounded bg-white/10" />
</td>
))}
</tr>
@@ -99,7 +99,7 @@ export function DataTable<T>({
<tr>
<td colSpan={columns.length} className="px-4 py-12 text-center">
{emptyState || (
<span className="text-muted-foreground">No data found</span>
<span className="text-white/40">No data found</span>
)}
</td>
</tr>
@@ -107,7 +107,7 @@ export function DataTable<T>({
data.map((item) => (
<tr
key={keyExtractor(item)}
className="border-b border-border last:border-0 hover:bg-muted/30 transition-colors"
className="border-b border-white/[0.06] last:border-0 hover:bg-white/[0.04] transition-colors"
>
{columns.map((col) => (
<td key={col.key} className={cn('px-4 py-3', col.className)}>

View File

@@ -67,15 +67,15 @@ export function EditCategoryModal({
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="w-full max-w-md glass-card rounded-2xl p-6 shadow-lg">
{/* Header */}
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-foreground">Edit Category</h2>
<h2 className="text-lg font-semibold text-white">Edit Category</h2>
<button
onClick={handleClose}
disabled={isSaving}
className="rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
className="rounded-full p-1 text-white/50 hover:bg-white/10 hover:text-white disabled:opacity-50"
>
<X className="h-5 w-5" />
</button>
@@ -85,15 +85,15 @@ export function EditCategoryModal({
<form onSubmit={handleSubmit} className="space-y-4">
{/* Error Message */}
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-md bg-red-400/10 p-3 text-sm text-red-400">
{error}
</div>
)}
{/* Name Field */}
<div>
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-destructive">*</span>
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-white">
Category Name <span className="text-red-400">*</span>
</label>
<input
id="edit-name"
@@ -105,21 +105,21 @@ export function EditCategoryModal({
placeholder="e.g., Network Troubleshooting"
required
className={cn(
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
'disabled:opacity-50'
)}
/>
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-xs text-white/40">
{name.length}/100 characters
</p>
</div>
{/* Description Field */}
<div>
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-white">
Description <span className="text-white/40">(optional)</span>
</label>
<textarea
id="edit-description"
@@ -129,9 +129,9 @@ export function EditCategoryModal({
rows={3}
placeholder="Brief description of this category..."
className={cn(
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
'disabled:opacity-50'
)}
/>
@@ -144,8 +144,8 @@ export function EditCategoryModal({
onClick={handleClose}
disabled={isSaving}
className={cn(
'rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
'rounded-md 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'
)}
>
Cancel
@@ -154,8 +154,8 @@ export function EditCategoryModal({
type="submit"
disabled={isSaving || !name.trim()}
className={cn(
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 disabled:opacity-50'
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50'
)}
>
{isSaving ? 'Saving...' : 'Save Changes'}

View File

@@ -12,10 +12,10 @@ interface EmptyStateProps {
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
return (
<div className={cn('flex flex-col items-center justify-center py-12 text-center', className)}>
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
{icon && <div className="mb-4 text-white/50">{icon}</div>}
<h3 className="text-lg font-semibold text-white">{title}</h3>
{description && (
<p className="mt-1 max-w-sm text-sm text-muted-foreground">{description}</p>
<p className="mt-1 max-w-sm text-sm text-white/40">{description}</p>
)}
{action && <div className="mt-4">{action}</div>}
</div>

View File

@@ -12,7 +12,7 @@ export function PageHeader({ title, description, action, className }: PageHeader
return (
<div className={cn('flex items-start justify-between gap-4', className)}>
<div>
<h1 className="font-heading text-2xl font-bold text-foreground">{title}</h1>
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
{description && (
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
)}

View File

@@ -36,20 +36,20 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
return (
<div className="flex items-center justify-between gap-4 pt-4">
<span className="text-sm text-muted-foreground">
<span className="text-sm text-white/40">
Showing {start}-{end} of {total}
</span>
<div className="flex items-center gap-1">
<button
onClick={() => onPageChange(page - 1)}
disabled={page <= 1}
className={cn(btnBase, 'px-2 hover:bg-accent')}
className={cn(btnBase, 'px-2 text-white/50 hover:bg-white/[0.06] hover:text-white')}
>
<ChevronLeft className="h-4 w-4" />
</button>
{getPageNumbers().map((p, i) =>
p === 'ellipsis' ? (
<span key={`e${i}`} className="px-1 text-muted-foreground">...</span>
<span key={`e${i}`} className="px-1 text-white/40">...</span>
) : (
<button
key={p}
@@ -58,8 +58,8 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
btnBase,
'px-2',
p === page
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent text-muted-foreground'
? 'bg-white text-black'
: 'text-white/50 hover:bg-white/[0.06] hover:text-white'
)}
>
{p}
@@ -69,7 +69,7 @@ export function Pagination({ page, totalPages, total, pageSize, onPageChange }:
<button
onClick={() => onPageChange(page + 1)}
disabled={page >= totalPages}
className={cn(btnBase, 'px-2 hover:bg-accent')}
className={cn(btnBase, 'px-2 text-white/50 hover:bg-white/[0.06] hover:text-white')}
>
<ChevronRight className="h-4 w-4" />
</button>

View File

@@ -40,21 +40,21 @@ export function SearchInput({ value = '', onSearch, placeholder = 'Search...', c
return (
<div className={cn('relative', className)}>
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-white/50" />
<input
type="text"
value={localValue}
onChange={handleChange}
placeholder={placeholder}
className={cn(
'h-9 w-full rounded-md border border-border bg-background pl-9 pr-8 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'h-9 w-full rounded-md border border-white/10 bg-black/50 pl-9 pr-8 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
/>
{localValue && (
<button
onClick={handleClear}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-muted-foreground hover:text-foreground"
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-white/50 hover:text-white"
>
<X className="h-3.5 w-3.5" />
</button>

View File

@@ -9,10 +9,10 @@ interface StatusBadgeProps {
}
const variantClasses: Record<BadgeVariant, string> = {
success: 'bg-green-500/10 text-green-600 dark:text-green-400',
destructive: 'bg-red-500/10 text-red-600 dark:text-red-400',
warning: 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400',
default: 'bg-muted text-muted-foreground',
success: 'bg-emerald-400/10 text-emerald-400',
destructive: 'bg-red-400/10 text-red-400',
warning: 'bg-yellow-400/10 text-yellow-400',
default: 'bg-white/10 text-white/70',
}
export function StatusBadge({ variant = 'default', children, className }: StatusBadgeProps) {