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

@@ -110,7 +110,7 @@ export function AccountSettingsPage() {
if (isLoading) {
return (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
)
}
@@ -118,7 +118,7 @@ export function AccountSettingsPage() {
if (error) {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
{error}
@@ -134,23 +134,23 @@ export function AccountSettingsPage() {
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-8">
<div className="flex items-center gap-3">
<Building2 className="h-8 w-8 text-primary" />
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Account Settings</h1>
<Building2 className="h-8 w-8 text-white/50" />
<h1 className="text-2xl font-bold text-white sm:text-3xl">Account Settings</h1>
</div>
<p className="mt-2 text-muted-foreground">
<p className="mt-2 text-white/40">
Manage your account, subscription, and team
</p>
</div>
<div className="max-w-3xl space-y-6">
{/* Account Info Section */}
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<h2 className="text-lg font-semibold text-card-foreground">Account Information</h2>
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Account Information</h2>
<div className="mt-4 space-y-4">
{/* Account Name */}
<div>
<label className="block font-label text-sm font-medium text-card-foreground">
<label className="block text-sm font-medium text-white">
Account Name
</label>
{isEditingName ? (
@@ -160,9 +160,9 @@ export function AccountSettingsPage() {
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className={cn(
'flex-1 rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
autoFocus
onKeyDown={(e) => {
@@ -177,8 +177,8 @@ export function AccountSettingsPage() {
onClick={handleSaveName}
disabled={isSavingName}
className={cn(
'rounded-md bg-primary p-2 text-primary-foreground',
'hover:bg-primary/90 disabled:opacity-50'
'rounded-md bg-white p-2 text-black',
'hover:bg-white/90 disabled:opacity-50'
)}
>
{isSavingName ? (
@@ -192,18 +192,18 @@ export function AccountSettingsPage() {
setEditedName(account?.name ?? '')
setIsEditingName(false)
}}
className="rounded-md border border-input p-2 text-muted-foreground hover:bg-accent"
className="rounded-md border border-white/10 p-2 text-white/40 hover:bg-white/10"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<div className="mt-1 flex items-center gap-2">
<span className="text-sm text-foreground">{account?.name}</span>
<span className="text-sm text-white">{account?.name}</span>
{isAccountOwner && (
<button
onClick={() => setIsEditingName(true)}
className="text-xs text-primary hover:underline"
className="text-xs text-white hover:underline"
>
Edit
</button>
@@ -214,10 +214,10 @@ export function AccountSettingsPage() {
{/* Display Code */}
<div>
<label className="block font-label text-sm font-medium text-card-foreground">
<label className="block text-sm font-medium text-white">
Display Code
</label>
<p className="mt-1 text-sm font-mono text-muted-foreground">
<p className="mt-1 text-sm font-mono text-white/40">
{account?.display_code}
</p>
</div>
@@ -225,8 +225,8 @@ export function AccountSettingsPage() {
</div>
{/* Subscription Section */}
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<h2 className="text-lg font-semibold text-card-foreground">Subscription</h2>
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Subscription</h2>
<div className="mt-4 space-y-4">
{/* Plan & Status */}
@@ -234,9 +234,9 @@ export function AccountSettingsPage() {
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-medium',
plan === 'free' && 'bg-secondary text-secondary-foreground',
plan === 'pro' && 'bg-primary/10 text-primary',
plan === 'team' && 'bg-primary/20 text-primary'
plan === 'free' && 'bg-white/10 text-white/70',
plan === 'pro' && 'bg-white/10 text-white',
plan === 'team' && 'bg-white/10 text-white'
)}
>
<Crown className="h-3.5 w-3.5" />
@@ -246,11 +246,11 @@ export function AccountSettingsPage() {
<span
className={cn(
'inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium',
sub.status === 'active' && 'bg-green-500/10 text-green-600',
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-600',
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-600',
sub.status === 'canceled' && 'bg-destructive/10 text-destructive',
sub.status === 'orphaned' && 'bg-muted text-muted-foreground'
sub.status === 'active' && 'bg-green-500/10 text-emerald-400',
sub.status === 'trialing' && 'bg-blue-500/10 text-blue-400',
sub.status === 'past_due' && 'bg-yellow-500/10 text-yellow-400',
sub.status === 'canceled' && 'bg-red-400/10 text-red-400',
sub.status === 'orphaned' && 'bg-white/10 text-white/40'
)}
>
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
@@ -259,7 +259,7 @@ export function AccountSettingsPage() {
</div>
{sub?.current_period_end && (
<p className="text-sm text-muted-foreground">
<p className="text-sm text-white/40">
Current period ends: {new Date(sub.current_period_end).toLocaleDateString()}
</p>
)}
@@ -302,45 +302,45 @@ export function AccountSettingsPage() {
{/* Team Members Section (owners only) */}
{isAccountOwner && (
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold text-card-foreground">Team Members</h2>
<Users className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Team Members</h2>
</div>
{members.length === 0 ? (
<p className="mt-4 text-sm text-muted-foreground">No team members yet.</p>
<p className="mt-4 text-sm text-white/40">No team members yet.</p>
) : (
<div className="mt-4 divide-y divide-border">
<div className="mt-4 divide-y divide-white/[0.06]">
{members.map((member) => (
<div
key={member.id}
className="flex items-center justify-between py-3 first:pt-0 last:pb-0"
>
<div>
<p className="text-sm font-medium text-foreground">{member.name}</p>
<p className="text-xs text-muted-foreground">{member.email}</p>
<p className="text-sm font-medium text-white">{member.name}</p>
<p className="text-xs text-white/40">{member.email}</p>
</div>
<div className="flex items-center gap-3">
<span
className={cn(
'rounded-full px-2.5 py-0.5 text-xs font-medium',
member.account_role === 'owner' && 'bg-primary/10 text-primary',
member.account_role === 'engineer' && 'bg-secondary text-secondary-foreground',
member.account_role === 'viewer' && 'bg-muted text-muted-foreground'
member.account_role === 'owner' && 'bg-white/10 text-white',
member.account_role === 'engineer' && 'bg-white/10 text-white/70',
member.account_role === 'viewer' && 'bg-white/10 text-white/40'
)}
>
{member.account_role}
</span>
{!member.is_active && (
<span className="rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive">
<span className="rounded-full bg-red-400/10 px-2 py-0.5 text-xs text-red-400">
Inactive
</span>
)}
{member.account_role !== 'owner' && (
<button
onClick={() => handleRemoveMember(member.id)}
className="text-muted-foreground hover:text-destructive"
className="text-white/40 hover:text-red-400"
title="Remove member"
>
<X className="h-4 w-4" />
@@ -356,10 +356,10 @@ export function AccountSettingsPage() {
{/* Invite Member Section (owners only) */}
{isAccountOwner && (
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Mail className="h-5 w-5 text-primary" />
<h2 className="text-lg font-semibold text-card-foreground">Invite Member</h2>
<Mail className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Invite Member</h2>
</div>
<form onSubmit={handleInvite} className="mt-4 space-y-3">
@@ -371,17 +371,17 @@ export function AccountSettingsPage() {
onChange={(e) => setInviteEmail(e.target.value)}
required
className={cn(
'flex-1 rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className={cn(
'rounded-md border border-input bg-background px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
>
<option value="engineer">Engineer</option>
@@ -391,8 +391,8 @@ export function AccountSettingsPage() {
type="submit"
disabled={isInviting || !inviteEmail.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 disabled:cursor-not-allowed'
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{isInviting ? (
@@ -407,18 +407,18 @@ export function AccountSettingsPage() {
</div>
{inviteError && (
<p className="text-sm text-destructive">{inviteError}</p>
<p className="text-sm text-red-400">{inviteError}</p>
)}
{inviteSuccess && (
<p className="text-sm text-green-600">{inviteSuccess}</p>
<p className="text-sm text-emerald-400">{inviteSuccess}</p>
)}
</form>
{/* Pending Invites */}
{invites.length > 0 && (
<div className="mt-6">
<h3 className="text-sm font-medium text-card-foreground">Pending Invites</h3>
<div className="mt-2 divide-y divide-border">
<h3 className="text-sm font-medium text-white">Pending Invites</h3>
<div className="mt-2 divide-y divide-white/[0.06]">
{invites
.filter((inv) => !inv.used_at)
.map((invite) => (
@@ -427,12 +427,12 @@ export function AccountSettingsPage() {
className="flex items-center justify-between py-2"
>
<div>
<p className="text-sm text-foreground">{invite.email}</p>
<p className="text-xs text-muted-foreground">
<p className="text-sm text-white">{invite.email}</p>
<p className="text-xs text-white/40">
Expires {new Date(invite.expires_at).toLocaleDateString()}
</p>
</div>
<span className="rounded-full bg-secondary px-2.5 py-0.5 text-xs text-secondary-foreground">
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
{invite.role}
</span>
</div>
@@ -463,25 +463,25 @@ function UsageStat({
const isAtLimit = !isUnlimited && current >= max
return (
<div className="rounded-md border border-border bg-background p-3">
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<div className="glass-stat rounded-md p-3">
<p className="text-xs font-medium text-white/40">{label}</p>
<p
className={cn(
'mt-1 text-lg font-semibold',
isAtLimit ? 'text-destructive' : isNearLimit ? 'text-yellow-600' : 'text-foreground'
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-white'
)}
>
{current}
<span className="text-sm font-normal text-muted-foreground">
<span className="text-sm font-normal text-white/40">
{' '}/ {isUnlimited ? 'Unlimited' : max}
</span>
</p>
{!isUnlimited && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-muted">
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/10">
<div
className={cn(
'h-full rounded-full transition-all',
isAtLimit ? 'bg-destructive' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-white'
)}
style={{ width: `${percentage}%` }}
/>

View File

@@ -144,7 +144,7 @@ export function AdminCategoriesPage() {
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
)
}
@@ -154,18 +154,18 @@ export function AdminCategoriesPage() {
{/* Header */}
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
<h1 className="text-2xl font-bold text-white sm:text-3xl">
Step Categories
</h1>
<p className="mt-2 text-muted-foreground">
<p className="mt-2 text-white/40">
Manage categories for organizing step library
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
<Plus className="h-4 w-4" />
@@ -180,16 +180,16 @@ export function AdminCategoriesPage() {
type="checkbox"
checked={includeArchived}
onChange={(e) => setIncludeArchived(e.target.checked)}
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
className="h-4 w-4 rounded border-white/10 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-0"
/>
<span className="text-sm text-muted-foreground">Show archived categories</span>
<span className="text-sm text-white/40">Show archived categories</span>
</label>
</div>
{/* Categories List */}
{categories.length === 0 ? (
<div className="rounded-lg border border-border bg-card p-12 text-center">
<p className="text-muted-foreground">
<div className="glass-card rounded-2xl p-12 text-center">
<p className="text-white/40">
No categories found. Create your first category to get started.
</p>
</div>

View File

@@ -2,7 +2,6 @@ import { useState } from 'react'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore'
import { BrandLogo } from '@/components/common/BrandLogo'
import { BrandWordmark } from '@/components/common/BrandWordmark'
import { cn } from '@/lib/utils'
export function LoginPage() {
@@ -35,33 +34,38 @@ export function LoginPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-md space-y-8">
<div className="flex min-h-screen items-center justify-center bg-black px-4">
{/* Subtle radial overlay */}
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
<div className="relative w-full max-w-md space-y-8">
<div className="text-center">
<div className="mb-4 flex justify-center sm:mb-6">
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
</div>
</div>
<h1>
<BrandWordmark size="lg" />
<h1 className="text-3xl font-bold text-white tracking-tight">
ResolutionFlow
</h1>
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
Decision Tree Platform
</p>
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
<p className="mt-1 text-sm text-white/40 sm:mt-2">
Sign in to your account
</p>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-lg">
<div className="glass-card rounded-2xl p-6 space-y-4">
{(error || localError) && (
<div className="rounded-md border border-destructive/20 bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{localError || error}
</div>
)}
<div>
<label htmlFor="email" className="mb-1 block text-sm font-medium text-foreground">
<label htmlFor="email" className="mb-1 block text-sm font-medium text-white">
Email address
</label>
<input
@@ -73,9 +77,9 @@ export function LoginPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
'block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20',
'block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/30',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
'transition-colors'
)}
placeholder="you@example.com"
@@ -83,7 +87,7 @@ export function LoginPage() {
</div>
<div>
<label htmlFor="password" className="mb-1 block text-sm font-medium text-foreground">
<label htmlFor="password" className="mb-1 block text-sm font-medium text-white">
Password
</label>
<input
@@ -95,9 +99,9 @@ export function LoginPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
className={cn(
'block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20',
'block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/30',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20',
'transition-colors'
)}
placeholder="••••••••••"
@@ -108,20 +112,20 @@ export function LoginPage() {
type="submit"
disabled={isLoading}
className={cn(
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
'bg-gradient-brand hover:bg-gradient-brand-hover',
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
'bg-white text-black hover:bg-white/90',
'focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-black',
'disabled:cursor-not-allowed disabled:opacity-50',
'shadow-lg shadow-primary/20'
'transition-all'
)}
>
{isLoading ? 'Signing in...' : 'Sign in'}
</button>
</div>
<p className="text-center text-sm text-muted-foreground">
<p className="text-center text-sm text-white/40">
Don't have an account?{' '}
<Link to="/register" className="font-medium text-gradient-brand hover:underline">
<Link to="/register" className="font-medium text-white hover:underline">
Register
</Link>
</p>

View File

@@ -108,10 +108,8 @@ export function MyTreesPage() {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-6 sm:mb-8">
<h1 className="font-heading text-3xl font-bold sm:text-4xl">
<span className="text-gradient-brand">My Trees</span>
</h1>
<p className="mt-2 text-muted-foreground">
<h1 className="text-2xl font-bold text-white sm:text-3xl">My Trees</h1>
<p className="mt-2 text-white/40">
Your forked and custom decision trees
</p>
</div>
@@ -119,20 +117,20 @@ export function MyTreesPage() {
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
) : trees.length === 0 ? (
<div className="rounded-lg border border-dashed border-border bg-card/50 px-4 py-12 text-center">
<FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground opacity-50" />
<h2 className="mb-2 text-lg font-semibold text-foreground">No personal trees yet</h2>
<p className="mb-4 text-sm text-muted-foreground">
<div className="rounded-lg border border-dashed border-white/10 bg-white/[0.02] px-4 py-12 text-center">
<FolderTree className="mx-auto mb-4 h-12 w-12 text-white/20" />
<h2 className="mb-2 text-lg font-semibold text-white">No personal trees yet</h2>
<p className="mb-4 text-sm text-white/40">
Fork a tree from the library to customize it for your workflow
</p>
<Link
to="/trees"
className={cn(
'inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'inline-flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Browse Trees
@@ -143,32 +141,32 @@ export function MyTreesPage() {
{trees.map((tree) => (
<div
key={tree.id}
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover sm:p-6"
>
{/* Header */}
<div className="mb-3 flex items-start justify-between gap-2">
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
<h3 className="font-semibold text-white">{tree.name}</h3>
{tree.category_info && (
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
{tree.category_info.name}
</span>
)}
</div>
{/* Description */}
<p className="mb-3 text-sm text-muted-foreground line-clamp-2">
<p className="mb-3 text-sm text-white/40 line-clamp-2">
{tree.description || 'No description available'}
</p>
{/* Fork Badge */}
{tree.parent_tree_id && (
<div className="mb-3 flex items-center gap-2 rounded-md bg-accent/50 px-2 py-1.5 text-sm">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">
<div className="mb-3 flex items-center gap-2 rounded-md bg-white/5 px-2 py-1.5 text-sm">
<GitBranch className="h-4 w-4 text-white/40" />
<span className="text-white/40">
Forked from{' '}
<Link
to={`/trees/${tree.parent_tree_id}/navigate`}
className="font-medium text-primary hover:underline"
className="font-medium text-white hover:underline"
>
original
</Link>
@@ -184,7 +182,7 @@ export function MyTreesPage() {
)}
{/* Stats */}
<div className="mb-4 flex items-center gap-4 text-xs text-muted-foreground">
<div className="mb-4 flex items-center gap-4 text-xs text-white/30">
<div className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
<span>{formatDate(tree.lastUsed)}</span>
@@ -201,8 +199,8 @@ export function MyTreesPage() {
type="button"
onClick={() => handleStartSession(tree.id)}
className={cn(
'flex flex-1 items-center justify-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex flex-1 items-center justify-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
<Play className="h-4 w-4" />
@@ -212,8 +210,8 @@ export function MyTreesPage() {
<Link
to={`/trees/${tree.id}/edit`}
className={cn(
'rounded-md border border-input p-2 text-muted-foreground',
'hover:bg-accent hover:text-accent-foreground'
'rounded-md border border-white/10 p-2 text-white/40',
'hover:bg-white/10 hover:text-white'
)}
title="Edit tree"
>
@@ -227,8 +225,8 @@ export function MyTreesPage() {
setShowShareModal(true)
}}
className={cn(
'rounded-md border border-input p-2 text-muted-foreground',
'hover:bg-accent hover:text-accent-foreground'
'rounded-md border border-white/10 p-2 text-white/40',
'hover:bg-white/10 hover:text-white'
)}
title="Share tree"
>
@@ -241,8 +239,8 @@ export function MyTreesPage() {
setShowDeleteConfirm(true)
}}
className={cn(
'rounded-md border border-input p-2 text-muted-foreground',
'hover:bg-destructive/10 hover:text-destructive'
'rounded-md border border-white/10 p-2 text-white/40',
'hover:bg-red-400/10 hover:text-red-400'
)}
title="Delete tree"
>

View File

@@ -0,0 +1,315 @@
import { useState, useEffect, useRef } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Search, Clock, ArrowRight, Play, Loader2, TrendingUp, Sparkles, Zap } from 'lucide-react'
import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions'
import type { TreeListItem } from '@/types'
import type { Session } from '@/types/session'
import { cn } from '@/lib/utils'
function timeAgo(dateStr: string): string {
const now = Date.now()
const then = new Date(dateStr).getTime()
const diffMs = now - then
const minutes = Math.floor(diffMs / 60000)
if (minutes < 1) return 'just now'
if (minutes < 60) return `${minutes}m ago`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
return `${days}d ago`
}
export function QuickStartPage() {
const navigate = useNavigate()
const [query, setQuery] = useState('')
const [searchResults, setSearchResults] = useState<TreeListItem[]>([])
const [isSearching, setIsSearching] = useState(false)
const [showResults, setShowResults] = useState(false)
const [activeSessions, setActiveSessions] = useState<Session[]>([])
const [recentTrees, setRecentTrees] = useState<{ tree_id: string; name: string; lastUsed: string }[]>([])
const [isLoading, setIsLoading] = useState(true)
const searchRef = useRef<HTMLDivElement>(null)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Load sessions on mount
useEffect(() => {
async function loadData() {
try {
const [active, recent] = await Promise.all([
sessionsApi.list({ completed: false, size: 5 }),
sessionsApi.list({ size: 10 }),
])
setActiveSessions(active.slice(0, 3))
// Deduplicate recent sessions by tree_id, max 5
const seen = new Set<string>()
const deduped: { tree_id: string; name: string; lastUsed: string }[] = []
for (const s of recent) {
if (!seen.has(s.tree_id) && deduped.length < 5) {
seen.add(s.tree_id)
deduped.push({
tree_id: s.tree_id,
name: s.tree_snapshot?.name || 'Unnamed Tree',
lastUsed: s.started_at,
})
}
}
setRecentTrees(deduped)
} catch (err) {
console.error('Failed to load sessions:', err)
} finally {
setIsLoading(false)
}
}
loadData()
}, [])
// Debounced search
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current)
if (query.length < 2) {
setSearchResults([])
setShowResults(false)
setIsSearching(false)
return
}
setIsSearching(true)
setShowResults(true)
debounceRef.current = setTimeout(async () => {
try {
const results = await treesApi.search(query, 8)
setSearchResults(results)
} catch (err) {
console.error('Search failed:', err)
setSearchResults([])
} finally {
setIsSearching(false)
}
}, 300)
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current)
}
}, [query])
// Close dropdown on outside click
useEffect(() => {
function handleClick(e: MouseEvent) {
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
setShowResults(false)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [])
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950">
{/* Animated background grid */}
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(to_right,#4f4f4f12_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f12_1px,transparent_1px)] bg-[size:64px_64px] [mask-image:radial-gradient(ellipse_80%_50%_at_50%_0%,#000_70%,transparent_110%)]" />
<div className="relative container mx-auto px-4 py-12">
{/* Hero Section */}
<div className="mx-auto max-w-3xl">
{/* Badge */}
<div className="flex justify-center mb-6 animate-in fade-in slide-in-from-bottom-4 duration-700">
<div className="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-violet-500/10 to-purple-500/10 px-4 py-2 border border-violet-500/20 backdrop-blur-sm">
<Sparkles className="h-4 w-4 text-violet-400" />
<span className="text-sm font-medium text-violet-300">AI-Powered Troubleshooting</span>
</div>
</div>
{/* Title */}
<h1 className="font-heading text-5xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-br from-white via-slate-200 to-slate-400 leading-tight animate-in fade-in slide-in-from-bottom-4 duration-700 delay-100">
What are you troubleshooting?
</h1>
<p className="text-center text-slate-400 mt-4 text-lg animate-in fade-in slide-in-from-bottom-4 duration-700 delay-200">
Search our library of proven decision trees or continue where you left off
</p>
{/* Enhanced Search Bar */}
<div ref={searchRef} className="relative mt-8 animate-in fade-in slide-in-from-bottom-4 duration-700 delay-300">
<div className="relative group">
{/* Glow effect */}
<div className="absolute -inset-0.5 bg-gradient-to-r from-violet-600 to-purple-600 rounded-xl blur opacity-20 group-hover:opacity-40 transition duration-300" />
<div className="relative">
<Search className="absolute left-5 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400 transition-colors group-hover:text-violet-400" />
<input
type="text"
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query.length >= 2 && setShowResults(true)}
placeholder="Paste ticket subject or search for a tree..."
className={cn(
'w-full rounded-xl border border-slate-700/50 bg-slate-900/90 backdrop-blur-xl py-5 pl-14 pr-5 text-lg',
'text-white placeholder:text-slate-500',
'focus:outline-none focus:ring-2 focus:ring-violet-500/50 focus:border-violet-500/50',
'transition-all duration-300'
)}
/>
{query && (
<Zap className="absolute right-5 top-1/2 h-5 w-5 -translate-y-1/2 text-violet-400 animate-pulse" />
)}
</div>
</div>
{/* Enhanced Search Results Dropdown */}
{showResults && (
<div className="absolute z-10 mt-2 w-full rounded-xl border border-slate-700/50 bg-slate-900/95 backdrop-blur-xl shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
{isSearching ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-violet-400" />
</div>
) : searchResults.length === 0 ? (
<div className="px-4 py-8 text-center">
<div className="text-slate-400 text-sm">No results found</div>
<div className="text-slate-500 text-xs mt-1">Try a different search term</div>
</div>
) : (
<ul className="max-h-96 overflow-y-auto py-2">
{searchResults.map((tree, idx) => (
<li key={tree.id} style={{ animationDelay: `${idx * 50}ms` }} className="animate-in fade-in slide-in-from-top-1 duration-200">
<button
onClick={() => navigate(`/trees/${tree.id}/navigate`)}
className="w-full px-5 py-4 text-left transition-all hover:bg-slate-800/50 group border-b border-slate-800/50 last:border-0"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-white group-hover:text-violet-300 transition-colors">
{tree.name}
</div>
{tree.description && (
<div className="mt-1 line-clamp-2 text-xs text-slate-400">
{tree.description}
</div>
)}
</div>
<ArrowRight className="h-4 w-4 flex-shrink-0 text-slate-600 group-hover:text-violet-400 group-hover:translate-x-1 transition-all" />
</div>
</button>
</li>
))}
</ul>
)}
</div>
)}
</div>
</div>
{/* Continue Session Section */}
{activeSessions.length > 0 && (
<div className="mx-auto mt-16 max-w-6xl animate-in fade-in slide-in-from-bottom-4 duration-700 delay-500">
<div className="flex items-center gap-3 mb-5">
<div className="flex items-center gap-2">
<div className="h-8 w-1 bg-gradient-to-b from-violet-500 to-purple-600 rounded-full" />
<h2 className="font-heading text-xl font-bold text-white">
Continue Session
</h2>
</div>
<div className="flex-1 h-px bg-gradient-to-r from-slate-700/50 to-transparent" />
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{activeSessions.map((session, idx) => (
<button
key={session.id}
onClick={() =>
navigate(`/trees/${session.tree_id}/navigate`, {
state: { sessionId: session.id },
})
}
style={{ animationDelay: `${(idx + 5) * 100}ms` }}
className="group relative rounded-xl border border-slate-700/50 bg-slate-900/50 backdrop-blur-sm p-5 text-left transition-all hover:border-violet-500/50 hover:shadow-lg hover:shadow-violet-500/10 hover:-translate-y-1 animate-in fade-in slide-in-from-bottom-2 duration-500"
>
{/* Animated corner accent */}
<div className="absolute top-0 right-0 h-16 w-16 bg-gradient-to-bl from-violet-500/20 to-transparent rounded-tr-xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="truncate text-base font-semibold text-white group-hover:text-violet-300 transition-colors">
{session.tree_snapshot?.name || 'Unnamed Tree'}
</div>
{(session.ticket_number || session.client_name) && (
<div className="mt-1.5 truncate text-sm text-slate-400">
{[session.ticket_number, session.client_name]
.filter(Boolean)
.join(' • ')}
</div>
)}
</div>
<div className="flex-shrink-0 rounded-full bg-violet-500/10 p-2 group-hover:bg-violet-500/20 transition-colors">
<Play className="h-4 w-4 text-violet-400 group-hover:scale-110 transition-transform" />
</div>
</div>
<div className="mt-4 flex items-center gap-2 text-xs text-slate-500">
<Clock className="h-3.5 w-3.5" />
<span>Started {timeAgo(session.started_at)}</span>
</div>
{/* Progress indicator (optional - you can remove this) */}
<div className="absolute bottom-0 left-0 right-0 h-1 bg-slate-800/50 rounded-b-xl overflow-hidden">
<div className="h-full bg-gradient-to-r from-violet-500 to-purple-600 w-2/3" />
</div>
</button>
))}
</div>
</div>
)}
{/* Recent Trees Section */}
{!isLoading && recentTrees.length > 0 && (
<div className="mx-auto mt-12 max-w-6xl animate-in fade-in slide-in-from-bottom-4 duration-700 delay-700">
<div className="flex items-center gap-3 mb-5">
<div className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-violet-400" />
<h2 className="font-heading text-xl font-bold text-white">
Recent Trees
</h2>
</div>
<div className="flex-1 h-px bg-gradient-to-r from-slate-700/50 to-transparent" />
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
{recentTrees.map((tree, idx) => (
<button
key={tree.tree_id}
onClick={() => navigate(`/trees/${tree.tree_id}/navigate`)}
style={{ animationDelay: `${(idx + 8) * 100}ms` }}
className="group relative rounded-xl border border-slate-700/50 bg-slate-900/30 backdrop-blur-sm p-4 text-left transition-all hover:border-violet-500/50 hover:bg-slate-900/50 hover:-translate-y-1 animate-in fade-in slide-in-from-bottom-2 duration-500"
>
<div className="truncate text-sm font-medium text-white group-hover:text-violet-300 transition-colors">
{tree.name}
</div>
<div className="mt-2 flex items-center gap-1.5 text-xs text-slate-500">
<Clock className="h-3 w-3" />
<span>{timeAgo(tree.lastUsed)}</span>
</div>
</button>
))}
</div>
</div>
)}
{/* Footer CTA */}
<div className="mx-auto mt-16 max-w-4xl text-center animate-in fade-in duration-700 delay-1000">
<Link
to="/trees"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-gradient-to-r from-violet-600 to-purple-600 text-white font-medium hover:from-violet-500 hover:to-purple-500 transition-all hover:shadow-lg hover:shadow-violet-500/25 hover:scale-105"
>
Browse All Trees
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</div>
)
}
export default QuickStartPage

View File

@@ -1,11 +1,11 @@
import { useState, useEffect, useRef } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Search, Clock, ArrowRight, Play, Loader2 } from 'lucide-react'
import { Search, Clock, ArrowRight, Play, Loader2, Sparkles } from 'lucide-react'
import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions'
import type { TreeListItem } from '@/types'
import type { Session } from '@/types/session'
import { cn } from '@/lib/utils'
function timeAgo(dateStr: string): string {
const now = Date.now()
@@ -107,39 +107,61 @@ export function QuickStartPage() {
}, [])
return (
<div className="container mx-auto px-4 py-8">
<div className="container mx-auto px-4 py-12">
{/* Hero Section */}
<div className="mx-auto max-w-2xl text-center">
<h1 className="font-heading text-3xl font-bold text-foreground">
What are you troubleshooting?
<div className="mb-16 text-center max-w-4xl mx-auto">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
<Sparkles className="w-4 h-4 text-cyan-400" />
<span className="text-sm text-white/70 font-medium">DECISION TREE PLATFORM</span>
</div>
{/* Main heading */}
<h1 className="text-4xl md:text-6xl font-bold text-white mb-6 tracking-tight leading-tight">
What are you<br />
<span className="text-white/60">troubleshooting?</span>
</h1>
<div ref={searchRef} className="relative mt-6">
<div className="relative">
<Search className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query.length >= 2 && setShowResults(true)}
placeholder="Paste ticket subject or search for a tree..."
className={cn(
'w-full rounded-lg border border-border bg-card py-3 pl-12 pr-4 text-lg',
'text-foreground placeholder:text-muted-foreground',
'focus:outline-none focus:ring-2 focus:ring-primary/50'
{/* Description */}
<p className="text-lg text-white/40 mb-10 max-w-2xl mx-auto leading-relaxed">
Search our library of proven decision trees or continue where you left off
</p>
{/* Search Bar */}
<div ref={searchRef} className="relative max-w-2xl mx-auto group">
<div className="absolute inset-0 bg-white/5 rounded-2xl blur-2xl opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative glass-card rounded-2xl p-1">
<div className="flex items-center bg-black/50 rounded-xl">
<Search className="ml-5 w-5 h-5 text-blue-400" />
<input
type="text"
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query.length >= 2 && setShowResults(true)}
placeholder="Paste ticket subject or search for a tree..."
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
/>
{query.length >= 2 && (
<button
onClick={() => {/* search already fires on type */}}
className="mr-2 px-5 py-2.5 bg-white text-black font-semibold rounded-lg hover:bg-white/90 transition-all"
>
Search
</button>
)}
/>
</div>
</div>
{/* Search Results Dropdown */}
{showResults && (
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-lg">
<div className="absolute z-10 mt-2 w-full glass-card rounded-2xl shadow-[0_0_40px_rgba(0,0,0,0.5)] overflow-hidden">
{isSearching ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<div className="flex items-center justify-center py-8">
<Loader2 className="h-5 w-5 animate-spin text-white/40" />
</div>
) : searchResults.length === 0 ? (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
<div className="px-4 py-8 text-center text-sm text-white/40">
No results found
</div>
) : (
@@ -148,13 +170,13 @@ export function QuickStartPage() {
<li key={tree.id}>
<button
onClick={() => navigate(`/trees/${tree.id}/navigate`)}
className="w-full px-4 py-3 text-left transition-colors hover:bg-accent"
className="w-full px-5 py-3.5 text-left transition-all hover:bg-white/[0.06]"
>
<div className="text-sm font-medium text-foreground">
<div className="text-sm font-medium text-white">
{tree.name}
</div>
{tree.description && (
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">
<div className="mt-0.5 line-clamp-1 text-xs text-white/40">
{tree.description}
</div>
)}
@@ -170,65 +192,115 @@ export function QuickStartPage() {
{/* Continue Session Section */}
{activeSessions.length > 0 && (
<div className="mx-auto mt-12 max-w-4xl">
<h2 className="font-heading text-lg font-semibold text-foreground">
Continue Session
</h2>
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{activeSessions.map((session) => (
<div className="mx-auto max-w-4xl mb-12">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">Active Sessions</h2>
</div>
{/* Primary active session — Bright Glow card */}
<div className="glass-card-glow backdrop-blur-xl rounded-2xl p-8 mb-4">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3 mb-3">
<div className="w-12 h-12 rounded-xl bg-white/15 border border-white/30 flex items-center justify-center">
<Play className="w-6 h-6 text-violet-400" />
</div>
<div>
<div className="text-xs text-white/70 font-semibold uppercase tracking-wider mb-1">
Active Session
</div>
<h3 className="text-xl font-bold text-white">
{activeSessions[0].tree_snapshot?.name || 'Unnamed Tree'}
</h3>
</div>
</div>
<button
key={session.id}
onClick={() =>
navigate(`/trees/${session.tree_id}/navigate`, {
state: { sessionId: session.id },
navigate(`/trees/${activeSessions[0].tree_id}/navigate`, {
state: { sessionId: activeSessions[0].id },
})
}
className="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent"
className="px-5 py-2.5 bg-white text-black rounded-xl font-semibold hover:bg-white/90 transition-all hover:scale-105"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-foreground">
{session.tree_snapshot?.name || 'Unnamed Tree'}
</div>
{(session.ticket_number || session.client_name) && (
<div className="mt-1 truncate text-xs text-muted-foreground">
{[session.ticket_number, session.client_name]
.filter(Boolean)
.join(' - ')}
</div>
)}
</div>
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-primary" />
</div>
<div className="mt-2 flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>{timeAgo(session.started_at)}</span>
</div>
Continue
</button>
))}
</div>
<p className="text-sm text-white/50 mt-1">
{[activeSessions[0].ticket_number, activeSessions[0].client_name]
.filter(Boolean)
.join(' \u2022 ')}
{activeSessions[0].started_at && ` \u2022 Started ${timeAgo(activeSessions[0].started_at)}`}
</p>
</div>
{/* Additional active sessions */}
{activeSessions.length > 1 && (
<div className="grid gap-3 sm:grid-cols-2">
{activeSessions.slice(1).map((session) => (
<button
key={session.id}
onClick={() =>
navigate(`/trees/${session.tree_id}/navigate`, {
state: { sessionId: session.id },
})
}
className="glass-card hover:glass-card-hover rounded-2xl p-5 text-left transition-all hover:scale-[1.02] cursor-pointer"
>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-bold text-white">
{session.tree_snapshot?.name || 'Unnamed Tree'}
</div>
{(session.ticket_number || session.client_name) && (
<div className="mt-1 truncate text-xs text-white/40">
{[session.ticket_number, session.client_name]
.filter(Boolean)
.join(' - ')}
</div>
)}
</div>
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-violet-400" />
</div>
<div className="mt-3 flex items-center gap-1.5 text-xs text-white/30">
<Clock className="h-3.5 w-3.5" />
<span>{timeAgo(session.started_at)}</span>
</div>
</button>
))}
</div>
)}
</div>
)}
{/* Recent Trees Section */}
{!isLoading && recentTrees.length > 0 && (
<div className="mx-auto mt-10 max-w-4xl">
<h2 className="font-heading text-lg font-semibold text-foreground">
Recent Trees
</h2>
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<div className="mx-auto max-w-4xl mb-12">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">Recent Trees</h2>
<Link
to="/trees"
className="text-sm text-white/60 hover:text-white font-medium transition-colors"
>
View all
</Link>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{recentTrees.map((tree) => (
<button
key={tree.tree_id}
onClick={() => navigate(`/trees/${tree.tree_id}/navigate`)}
className="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-primary/50 hover:bg-accent"
className="glass-card hover:glass-card-hover rounded-2xl p-5 text-left transition-all hover:scale-[1.02] cursor-pointer"
>
<div className="truncate text-sm font-medium text-foreground">
<div className="flex items-start justify-between mb-3">
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center">
<Search className="w-5 h-5 text-blue-400" />
</div>
</div>
<div className="truncate text-sm font-bold text-white mb-2">
{tree.name}
</div>
<div className="mt-1 flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
<span>{timeAgo(tree.lastUsed)}</span>
<div className="flex items-center gap-1.5 text-xs text-white/30">
<Clock className="h-3.5 w-3.5" />
<span>Last used {timeAgo(tree.lastUsed)}</span>
</div>
</button>
))}
@@ -237,10 +309,10 @@ export function QuickStartPage() {
)}
{/* Footer */}
<div className="mx-auto mt-12 max-w-4xl text-center">
<div className="mx-auto max-w-4xl text-center">
<Link
to="/trees"
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary hover:underline"
className="inline-flex items-center gap-2 px-6 py-3 bg-white/10 border border-white/20 text-white font-medium rounded-xl hover:bg-white/20 transition-all"
>
Browse All Trees
<ArrowRight className="h-4 w-4" />

View File

@@ -3,7 +3,6 @@ import { Link, useNavigate } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore'
import { inviteApi } from '@/api'
import { BrandLogo } from '@/components/common/BrandLogo'
import { BrandWordmark } from '@/components/common/BrandWordmark'
import { cn } from '@/lib/utils'
export function RegisterPage() {
@@ -76,33 +75,38 @@ export function RegisterPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="w-full max-w-md space-y-8">
<div className="flex min-h-screen items-center justify-center bg-black px-4">
{/* Subtle radial overlay */}
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
<div className="relative w-full max-w-md space-y-8">
<div className="text-center">
<div className="mb-4 flex justify-center sm:mb-6">
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
</div>
</div>
<h1>
<BrandWordmark size="lg" />
<h1 className="text-3xl font-bold text-white tracking-tight">
ResolutionFlow
</h1>
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
Decision Tree Platform
</p>
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
<p className="mt-1 text-sm text-white/40 sm:mt-2">
Create your account
</p>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="space-y-4 rounded-lg border border-border bg-card p-6 shadow-sm">
<div className="glass-card rounded-2xl p-6 space-y-4">
{(error || localError) && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{localError || error}
</div>
)}
<div>
<label htmlFor="inviteCode" className="block text-sm font-medium text-foreground">
<label htmlFor="inviteCode" className="block text-sm font-medium text-white">
Invite code
</label>
<input
@@ -116,29 +120,29 @@ export function RegisterPage() {
}}
onBlur={(e) => validateInviteCode(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border bg-background px-3 py-2 font-mono tracking-wider',
'text-foreground placeholder:text-muted-foreground',
'mt-1 block w-full rounded-xl border bg-black/50 px-3 py-2 font-mono tracking-wider',
'text-white placeholder:text-white/30',
'focus:outline-none focus:ring-1',
inviteCodeStatus === 'valid' && 'border-green-500 focus:border-green-500 focus:ring-green-500',
inviteCodeStatus === 'invalid' && 'border-destructive focus:border-destructive focus:ring-destructive',
inviteCodeStatus === 'idle' && 'border-input focus:border-primary focus:ring-primary',
inviteCodeStatus === 'checking' && 'border-input focus:border-primary focus:ring-primary'
inviteCodeStatus === 'valid' && 'border-emerald-400/50 focus:border-emerald-400 focus:ring-emerald-400/30',
inviteCodeStatus === 'invalid' && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/30',
inviteCodeStatus === 'idle' && 'border-white/10 focus:border-white/30 focus:ring-white/20',
inviteCodeStatus === 'checking' && 'border-white/10 focus:border-white/30 focus:ring-white/20'
)}
placeholder="ABCD1234"
/>
{inviteCodeStatus === 'checking' && (
<p className="mt-1 text-xs text-muted-foreground">Validating...</p>
<p className="mt-1 text-xs text-white/40">Validating...</p>
)}
{inviteCodeStatus === 'valid' && (
<p className="mt-1 text-xs text-green-600">{inviteCodeMessage}</p>
<p className="mt-1 text-xs text-emerald-400">{inviteCodeMessage}</p>
)}
{inviteCodeStatus === 'invalid' && (
<p className="mt-1 text-xs text-destructive">{inviteCodeMessage}</p>
<p className="mt-1 text-xs text-red-400">{inviteCodeMessage}</p>
)}
</div>
<div>
<label htmlFor="name" className="block text-sm font-medium text-foreground">
<label htmlFor="name" className="block text-sm font-medium text-white">
Full name
</label>
<input
@@ -150,16 +154,16 @@ export function RegisterPage() {
value={name}
onChange={(e) => setName(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/30',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
placeholder="John Smith"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground">
<label htmlFor="email" className="block text-sm font-medium text-white">
Email address
</label>
<input
@@ -171,16 +175,16 @@ export function RegisterPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/30',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground">
<label htmlFor="password" className="block text-sm font-medium text-white">
Password
</label>
<input
@@ -192,19 +196,19 @@ export function RegisterPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/30',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
placeholder="••••••••••"
/>
<p className="mt-1 text-xs text-muted-foreground">
<p className="mt-1 text-xs text-white/30">
Must be at least 10 characters
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
Confirm password
</label>
<input
@@ -216,9 +220,9 @@ export function RegisterPage() {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/30',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
placeholder="••••••••••"
/>
@@ -228,20 +232,20 @@ export function RegisterPage() {
type="submit"
disabled={isLoading}
className={cn(
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
'bg-gradient-brand hover:bg-gradient-brand-hover',
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'w-full rounded-xl px-4 py-2.5 text-sm font-semibold btn-press',
'bg-white text-black hover:bg-white/90',
'focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-black',
'disabled:cursor-not-allowed disabled:opacity-50',
'shadow-lg shadow-primary/20'
'transition-all'
)}
>
{isLoading ? 'Creating account...' : 'Create account'}
</button>
</div>
<p className="text-center text-sm text-muted-foreground">
<p className="text-center text-sm text-white/40">
Already have an account?{' '}
<Link to="/login" className="font-medium text-gradient-brand hover:underline">
<Link to="/login" className="font-medium text-white hover:underline">
Sign in
</Link>
</p>

View File

@@ -223,7 +223,7 @@ export function SessionDetailPage() {
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
)
}
@@ -231,12 +231,12 @@ export function SessionDetailPage() {
if (error || !session) {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
<div className="rounded-md border border-red-400/20 bg-red-400/10 p-4 text-red-400">
{error || 'Session not found'}
</div>
<button
onClick={() => navigate('/sessions')}
className="mt-4 text-primary hover:underline"
className="mt-4 text-white hover:underline"
>
Back to sessions
</button>
@@ -252,18 +252,18 @@ export function SessionDetailPage() {
<div>
<button
onClick={() => navigate('/sessions')}
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
className="mb-2 text-sm text-white/40 hover:text-white"
>
Back to sessions
</button>
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
<h1 className="text-2xl font-bold text-white sm:text-3xl">
{session.ticket_number || 'Session Details'}
</h1>
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
<div className="mt-2 flex items-center gap-4 text-sm text-white/40">
<span
className={cn(
'flex items-center gap-1',
session.completed_at ? 'text-green-600' : 'text-yellow-600'
session.completed_at ? 'text-emerald-400' : 'text-yellow-400'
)}
>
<span
@@ -286,8 +286,8 @@ export function SessionDetailPage() {
onClick={() => setShowSaveAsTreeModal(true)}
disabled={isSavingTree}
className={cn(
'flex items-center gap-2 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'
'flex items-center gap-2 rounded-md border border-white/10 bg-transparent px-4 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
)}
>
<Save className="h-4 w-4" />
@@ -299,8 +299,8 @@ export function SessionDetailPage() {
<button
onClick={handleCopyForTicket}
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
{copiedPsa ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
@@ -314,8 +314,8 @@ export function SessionDetailPage() {
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
aria-label="Export format"
className={cn(
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm sm:w-auto',
'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 sm:w-auto',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
>
<option value="markdown">Markdown</option>
@@ -328,18 +328,18 @@ export function SessionDetailPage() {
disabled={isExporting}
title="Copy to clipboard"
className={cn(
'rounded-md border border-input bg-background p-2 text-muted-foreground',
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
'rounded-md border border-white/10 bg-transparent p-2 text-white/60',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
)}
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
</button>
<button
onClick={handlePreview}
disabled={isExporting}
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 disabled:opacity-50'
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50'
)}
>
<Eye className="h-4 w-4" />
@@ -352,37 +352,37 @@ export function SessionDetailPage() {
{/* Timeline */}
<div className="mb-8">
<h2 className="mb-4 text-lg font-semibold text-foreground">Decision Timeline</h2>
<h2 className="mb-4 text-lg font-semibold text-white">Decision Timeline</h2>
<div className="space-y-4">
<div className="flex items-center gap-3 text-sm">
<span className="h-3 w-3 rounded-full bg-primary" />
<span className="text-muted-foreground">
<span className="h-3 w-3 rounded-full bg-white" />
<span className="text-white/40">
Session started: {formatDate(session.started_at)}
</span>
</div>
{session.decisions.map((decision, index) => (
<div key={index} className="ml-1 border-l-2 border-border pl-6">
<div key={index} className="ml-1 border-l-2 border-white/[0.06] pl-6">
<div className="relative">
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-border" />
<div className="rounded-lg border border-border bg-card p-4">
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-white/20" />
<div className="glass-card rounded-xl p-4">
{decision.question && (
<p className="font-medium text-card-foreground">{decision.question}</p>
<p className="font-medium text-white">{decision.question}</p>
)}
{decision.answer && (
<p className="mt-1 text-sm text-primary">Answer: {decision.answer}</p>
<p className="mt-1 text-sm text-white">Answer: {decision.answer}</p>
)}
{decision.action_performed && (
<p className="mt-1 text-sm text-muted-foreground">
<p className="mt-1 text-sm text-white/40">
Action: {decision.action_performed}
</p>
)}
{decision.notes && (
<p className="mt-2 rounded bg-muted/50 p-2 text-sm text-muted-foreground">
<p className="mt-2 rounded bg-white/5 p-2 text-sm text-white/40">
Notes: {decision.notes}
</p>
)}
<p className="mt-2 text-xs text-muted-foreground">
<p className="mt-2 text-xs text-white/40">
{formatDate(decision.timestamp)}
</p>
</div>
@@ -393,7 +393,7 @@ export function SessionDetailPage() {
{session.completed_at && (
<div className="flex items-center gap-3 text-sm">
<span className="h-3 w-3 rounded-full bg-green-500" />
<span className="text-green-600">
<span className="text-emerald-400">
Session completed: {formatDate(session.completed_at)}
</span>
</div>

View File

@@ -142,14 +142,14 @@ export function SessionHistoryPage() {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-8">
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Session History</h1>
<p className="mt-2 text-muted-foreground">
<h1 className="text-2xl font-bold text-white sm:text-3xl">Session History</h1>
<p className="mt-2 text-white/40">
Search and filter your troubleshooting sessions
</p>
</div>
{/* Filter Tabs */}
<div className="mb-6 flex gap-2 border-b border-border">
<div className="mb-6 flex gap-2 border-b border-white/[0.06]">
{(['all', 'active', 'completed'] as const).map((tab) => (
<button
key={tab}
@@ -157,8 +157,8 @@ export function SessionHistoryPage() {
className={cn(
'px-4 py-2 text-sm font-medium transition-colors',
filter === tab
? 'border-b-2 border-primary text-primary'
: 'text-muted-foreground hover:text-foreground'
? 'border-b-2 border-white text-white'
: 'text-white/40 hover:text-white'
)}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
@@ -179,22 +179,22 @@ export function SessionHistoryPage() {
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
) : sessions.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<div className="py-12 text-center text-white/40">
No sessions found.{' '}
{filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from ? (
<button
onClick={handleClearFilters}
className="text-primary hover:underline"
className="text-white hover:underline"
>
Clear filters
</button>
) : (
<button
onClick={() => navigate('/trees')}
className="text-primary hover:underline"
className="text-white hover:underline"
>
Start a new session
</button>
@@ -205,7 +205,7 @@ export function SessionHistoryPage() {
{sessions.map((session) => (
<div
key={session.id}
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md"
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex-1">
@@ -217,23 +217,23 @@ export function SessionHistoryPage() {
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
)}
/>
<span className="font-medium text-card-foreground">
<span className="font-medium text-white">
{session.ticket_number || 'No ticket'}
</span>
{session.client_name && (
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs font-medium">
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs font-medium text-white">
{session.client_name}
</span>
)}
</div>
{/* Tree Name */}
<p className="mt-1 text-sm text-muted-foreground">
<p className="mt-1 text-sm text-white/40">
<span className="font-medium">Tree:</span> {getTreeName(session)}
</p>
{/* Timestamps */}
<p className="mt-1 text-sm text-muted-foreground">
<p className="mt-1 text-sm text-white/40">
Started: {formatDate(session.started_at)}
{session.completed_at && (
<> · Completed: {formatDate(session.completed_at)}</>
@@ -241,7 +241,7 @@ export function SessionHistoryPage() {
</p>
{/* Stats */}
<p className="mt-1 text-sm text-muted-foreground">
<p className="mt-1 text-sm text-white/40">
{session.decisions.length} decision{session.decisions.length !== 1 ? 's' : ''} recorded
{session.scratchpad && session.scratchpad.trim() && (
<span> · Has notes</span>
@@ -254,8 +254,8 @@ export function SessionHistoryPage() {
<button
onClick={() => navigate(`/sessions/${session.id}`)}
className={cn(
'rounded-md border border-input px-3 py-2 text-sm font-medium',
'hover:bg-accent hover:text-accent-foreground'
'rounded-md border border-white/10 px-3 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white'
)}
>
View Details
@@ -264,8 +264,8 @@ export function SessionHistoryPage() {
<button
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
className={cn(
'rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Resume

View File

@@ -1,13 +1,10 @@
import { Settings } from 'lucide-react'
import { useUserPreferencesStore } from '@/store/userPreferencesStore'
import { useThemeStore } from '@/store/themeStore'
import { cn } from '@/lib/utils'
import { ThemeToggle } from '@/components/common/ThemeToggle'
import { toast } from '@/lib/toast'
export function SettingsPage() {
const { defaultExportFormat, setDefaultExportFormat } = useUserPreferencesStore()
const { theme } = useThemeStore()
const handleExportFormatChange = (format: 'markdown' | 'text' | 'html') => {
setDefaultExportFormat(format)
@@ -18,50 +15,30 @@ export function SettingsPage() {
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-8">
<div className="flex items-center gap-3">
<Settings className="h-8 w-8 text-primary" />
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Settings</h1>
<Settings className="h-8 w-8 text-white/50" />
<h1 className="text-2xl font-bold text-white sm:text-3xl">Settings</h1>
</div>
<p className="mt-2 text-muted-foreground">
<p className="mt-2 text-white/40">
Manage your application preferences
</p>
</div>
<div className="max-w-2xl space-y-6">
{/* Appearance Section */}
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
<p className="mt-1 text-sm text-muted-foreground">
Customize how ResolutionFlow looks on your device
</p>
<div className="mt-4">
<label className="block font-label text-sm font-medium text-card-foreground">
Theme
</label>
<p className="text-sm text-muted-foreground">
Current: {theme.charAt(0).toUpperCase() + theme.slice(1)}
</p>
<div className="mt-2">
<ThemeToggle />
</div>
</div>
</div>
{/* Export Preferences Section */}
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
<p className="mt-1 text-sm text-muted-foreground">
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Export Preferences</h2>
<p className="mt-1 text-sm text-white/40">
Configure default settings for session exports
</p>
<div className="mt-4">
<label
htmlFor="export-format"
className="block font-label text-sm font-medium text-card-foreground"
className="block text-sm font-medium text-white"
>
Default Export Format
</label>
<p className="text-sm text-muted-foreground">
<p className="text-sm text-white/40">
This format will be pre-selected when exporting sessions
</p>
<select
@@ -69,9 +46,9 @@ export function SettingsPage() {
value={defaultExportFormat}
onChange={(e) => handleExportFormatChange(e.target.value as 'markdown' | 'text' | 'html')}
className={cn(
'mt-2 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-sm text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-2 block w-full rounded-xl border border-white/10 bg-black/50 px-3 py-2',
'text-sm text-white',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
>
<option value="markdown">Markdown (.md)</option>
@@ -82,12 +59,12 @@ export function SettingsPage() {
</div>
{/* About Section */}
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
<p className="mt-1 text-sm text-muted-foreground">
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">About</h2>
<p className="mt-1 text-sm text-white/40">
ResolutionFlow - Decision Tree Platform
</p>
<p className="mt-2 text-sm text-muted-foreground">
<p className="mt-2 text-sm text-white/40">
Transform troubleshooting into guided workflows
</p>
</div>

View File

@@ -248,7 +248,7 @@ export function TreeEditorPage() {
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
)
}
@@ -257,16 +257,16 @@ export function TreeEditorPage() {
if (isMobile) {
return (
<div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center px-6 text-center">
<Monitor className="mb-4 h-12 w-12 text-muted-foreground" />
<h2 className="mb-2 text-xl font-semibold text-foreground">Desktop Required</h2>
<p className="mb-6 max-w-sm text-sm text-muted-foreground">
<Monitor className="mb-4 h-12 w-12 text-white/50" />
<h2 className="mb-2 text-xl font-semibold text-white">Desktop Required</h2>
<p className="mb-6 max-w-sm text-sm text-white/40">
The tree editor requires a larger screen for the best experience. Please open this page on a desktop or tablet in landscape mode.
</p>
<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-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Back to Library
@@ -280,18 +280,18 @@ export function TreeEditorPage() {
{/* Draft Restore Prompt */}
{showDraftPrompt && (
<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">
<h2 className="mb-2 text-lg font-semibold">Restore Draft?</h2>
<p className="mb-4 text-sm text-muted-foreground">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="glass-card rounded-2xl w-full max-w-md p-6 shadow-lg">
<h2 className="mb-2 text-lg font-semibold text-white">Restore Draft?</h2>
<p className="mb-4 text-sm text-white/40">
You have an unsaved draft from a previous session. Would you like to restore it?
</p>
<div className="flex gap-2">
<button
onClick={handleRestoreDraft}
className={cn(
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Restore Draft
@@ -299,8 +299,8 @@ export function TreeEditorPage() {
<button
onClick={handleDiscardDraft}
className={cn(
'flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium',
'hover:bg-accent'
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white'
)}
>
Start Fresh
@@ -312,18 +312,18 @@ export function TreeEditorPage() {
{/* Unsaved Changes Dialog */}
{blocker.state === 'blocked' && (
<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">
<h2 className="mb-2 text-lg font-semibold">Unsaved Changes</h2>
<p className="mb-4 text-sm text-muted-foreground">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="glass-card rounded-2xl w-full max-w-md p-6 shadow-lg">
<h2 className="mb-2 text-lg font-semibold text-white">Unsaved Changes</h2>
<p className="mb-4 text-sm text-white/40">
You have unsaved changes. Are you sure you want to leave?
</p>
<div className="flex gap-2">
<button
onClick={handleBlockerReset}
className={cn(
'flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex-1 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Stay
@@ -331,8 +331,8 @@ export function TreeEditorPage() {
<button
onClick={handleBlockerProceed}
className={cn(
'flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium text-destructive',
'hover:bg-accent'
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-red-400',
'hover:bg-white/10'
)}
>
Leave Without Saving
@@ -343,17 +343,17 @@ export function TreeEditorPage() {
)}
{/* Toolbar */}
<div className="flex items-center justify-between border-b border-border bg-card px-4 py-2">
<div className="flex items-center justify-between border-b border-white/[0.06] bg-black px-4 py-2">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/trees')}
className="text-sm text-muted-foreground hover:text-foreground"
className="text-sm text-white/50 hover:text-white"
>
Back to Library
</button>
<h1 className="text-lg font-semibold">
<h1 className="text-lg font-semibold text-white">
{isEditMode ? 'Edit Tree' : 'Create New Tree'}
{name && <span className="ml-2 text-muted-foreground">- {name}</span>}
{name && <span className="ml-2 text-white/40">- {name}</span>}
</h1>
<div className="flex items-center gap-2">
{treeStatus === 'draft' && (
@@ -372,7 +372,7 @@ export function TreeEditorPage() {
<div className="flex items-center gap-2">
{/* Undo/Redo */}
<div className="flex items-center rounded-md border border-border">
<div className="flex items-center rounded-md border border-white/[0.06]">
<button
type="button"
onClick={() => undo()}
@@ -381,13 +381,13 @@ export function TreeEditorPage() {
className={cn(
'rounded-l-md p-2 transition-colors',
pastStates.length > 0
? 'text-foreground hover:bg-accent'
: 'text-muted-foreground/40 cursor-not-allowed'
? 'text-white hover:bg-white/[0.06]'
: 'text-white/20 cursor-not-allowed'
)}
>
<Undo2 className="h-4 w-4" />
</button>
<div className="h-6 w-px bg-border" />
<div className="h-6 w-px bg-white/[0.06]" />
<button
type="button"
onClick={() => redo()}
@@ -396,15 +396,15 @@ export function TreeEditorPage() {
className={cn(
'rounded-r-md p-2 transition-colors',
futureStates.length > 0
? 'text-foreground hover:bg-accent'
: 'text-muted-foreground/40 cursor-not-allowed'
? 'text-white hover:bg-white/[0.06]'
: 'text-white/20 cursor-not-allowed'
)}
>
<Redo2 className="h-4 w-4" />
</button>
</div>
<div className="mx-2 h-6 w-px bg-border" />
<div className="mx-2 h-6 w-px bg-white/[0.06]" />
{/* Validate */}
<button
@@ -412,8 +412,8 @@ export function TreeEditorPage() {
disabled={isSaving}
title="Validate tree structure (checks for errors and warnings)"
className={cn(
'flex items-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm font-medium',
'hover:bg-accent disabled:opacity-50'
'flex items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
)}
>
<CheckCircle2 className="h-4 w-4" />
@@ -426,8 +426,8 @@ export function TreeEditorPage() {
disabled={isSaving || !isDirty}
title="Save as draft (Ctrl+S when draft or has errors)"
className={cn(
'flex items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm font-medium',
'hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed'
'flex items-center gap-2 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<Save className="h-4 w-4" />
@@ -440,8 +440,8 @@ export function TreeEditorPage() {
disabled={isSaving || !isDirty || hasBlockingErrors}
title={hasBlockingErrors ? 'Fix validation errors before publishing (Ctrl+S when no errors)' : 'Publish tree (Ctrl+S when no errors)'}
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed'
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<CheckCircle2 className="h-4 w-4" />

View File

@@ -199,8 +199,8 @@ export function TreeLibraryPage() {
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Decision Trees</h1>
<p className="mt-2 text-muted-foreground">
<h1 className="text-2xl font-bold text-white sm:text-3xl">Decision Trees</h1>
<p className="mt-2 text-white/40">
Select a troubleshooting tree to start a new session
</p>
</div>
@@ -208,8 +208,8 @@ export function TreeLibraryPage() {
<Link
to="/trees/new"
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
<Plus className="h-4 w-4" />
@@ -225,9 +225,9 @@ export function TreeLibraryPage() {
<button
onClick={() => setMobileFolderOpen(true)}
className={cn(
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium md:hidden',
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
selectedFolderId && 'border-primary text-primary'
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium md:hidden',
'text-white/40 hover:bg-white/10 hover:text-white',
selectedFolderId && 'border-white/30 text-white'
)}
>
<FolderOpen className="h-4 w-4" />
@@ -241,16 +241,16 @@ export function TreeLibraryPage() {
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className={cn(
'flex-1 rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
<button
onClick={handleSearch}
className={cn(
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Search
@@ -262,8 +262,8 @@ export function TreeLibraryPage() {
onChange={(e) => setSelectedCategoryId(e.target.value)}
aria-label="Filter by category"
className={cn(
'rounded-md border border-input bg-background px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
>
<option value="">All Categories</option>
@@ -284,9 +284,9 @@ export function TreeLibraryPage() {
type="checkbox"
checked={showDrafts}
onChange={(e) => setShowDrafts(e.target.checked)}
className="h-4 w-4 rounded border-input text-primary focus:ring-2 focus:ring-primary focus:ring-offset-2"
className="h-4 w-4 rounded border-white/20 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-2"
/>
<span className="text-sm text-muted-foreground">Show my drafts</span>
<span className="text-sm text-white/40">Show my drafts</span>
</label>
</div>
<ViewToggle view={treeLibraryView} onChange={setTreeLibraryView} />
@@ -296,24 +296,24 @@ export function TreeLibraryPage() {
{/* Active Filters */}
{hasActiveFilters && (
<div className="mb-6 flex flex-wrap items-center gap-2">
<span className="text-sm text-muted-foreground">Filters:</span>
<span className="text-sm text-white/40">Filters:</span>
{selectedFolderId && (
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm">
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
Folder
<button
onClick={() => setSelectedFolderId(null)}
className="rounded-full p-0.5 hover:bg-accent-foreground/10"
className="rounded-full p-0.5 hover:bg-white/20"
>
<X className="h-3 w-3" />
</button>
</span>
)}
{selectedCategoryId && (
<span className="inline-flex items-center gap-1 rounded-full bg-secondary px-3 py-1 text-sm">
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
{categories.find((c) => c.id === selectedCategoryId)?.name}
<button
onClick={() => setSelectedCategoryId('')}
className="rounded-full p-0.5 hover:bg-secondary-foreground/10"
className="rounded-full p-0.5 hover:bg-white/20"
>
<X className="h-3 w-3" />
</button>
@@ -322,12 +322,12 @@ export function TreeLibraryPage() {
{selectedTags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-3 py-1 text-sm text-primary"
className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white"
>
{tag}
<button
onClick={() => removeTagFilter(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>
@@ -335,7 +335,7 @@ export function TreeLibraryPage() {
))}
<button
onClick={clearAllFilters}
className="text-sm text-muted-foreground hover:text-foreground"
className="text-sm text-white/40 hover:text-white"
>
Clear all
</button>
@@ -345,10 +345,10 @@ export function TreeLibraryPage() {
{/* Loading State */}
{isLoading ? (
<div className="flex justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
) : trees.length === 0 ? (
<div className="py-12 text-center text-muted-foreground">
<div className="py-12 text-center text-white/40">
No trees found.{' '}
{(searchQuery || hasActiveFilters) && 'Try adjusting your filters.'}
</div>

View File

@@ -267,7 +267,7 @@ export function TreeNavigationPage() {
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
</div>
)
}
@@ -275,12 +275,12 @@ export function TreeNavigationPage() {
if (error || !tree) {
return (
<div className="container mx-auto px-4 py-8">
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
<div className="rounded-md bg-red-400/10 p-4 text-red-400">
{error || 'Tree not found'}
</div>
<button
onClick={() => navigate('/trees')}
className="mt-4 text-primary hover:underline"
className="mt-4 text-white/50 hover:text-white hover:underline"
>
Back to trees
</button>
@@ -292,17 +292,17 @@ export function TreeNavigationPage() {
if (showMetadataForm) {
return (
<div className="container mx-auto max-w-lg px-4 py-8">
<h1 className="mb-2 text-2xl font-bold text-foreground">{tree.name}</h1>
<p className="mb-6 text-muted-foreground">{tree.description}</p>
<h1 className="mb-2 text-2xl font-bold text-white">{tree.name}</h1>
<p className="mb-6 text-white/40">{tree.description}</p>
<div className="space-y-4 rounded-lg border border-border bg-card p-6">
<h2 className="font-semibold text-card-foreground">Session Details</h2>
<p className="text-sm text-muted-foreground">
<div className="glass-card rounded-2xl space-y-4 p-6">
<h2 className="font-semibold text-white">Session Details</h2>
<p className="text-sm text-white/40">
Optional: Add ticket and client info for easier tracking
</p>
<div>
<label className="block text-sm font-medium text-foreground">
<label className="block text-sm font-medium text-white">
Ticket Number
</label>
<input
@@ -311,15 +311,15 @@ export function TreeNavigationPage() {
onChange={(e) => setTicketNumber(e.target.value)}
placeholder="e.g., INC0012345"
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
</div>
<div>
<label className="block text-sm font-medium text-foreground">
<label className="block text-sm font-medium text-white">
Client Name
</label>
<input
@@ -328,9 +328,9 @@ export function TreeNavigationPage() {
onChange={(e) => setClientName(e.target.value)}
placeholder="e.g., Acme Corp"
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
</div>
@@ -338,8 +338,8 @@ export function TreeNavigationPage() {
<button
onClick={startSession}
className={cn(
'w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'w-full rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Start Troubleshooting
@@ -352,7 +352,7 @@ export function TreeNavigationPage() {
if (!currentNode && !currentCustomStep) {
return (
<div className="container mx-auto px-4 py-8">
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
<div className="rounded-md bg-red-400/10 p-4 text-red-400">
Invalid tree structure
</div>
</div>
@@ -367,9 +367,9 @@ export function TreeNavigationPage() {
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-foreground">{tree.name}</h1>
<h1 className="text-xl font-bold text-white">{tree.name}</h1>
{(ticketNumber || clientName) && (
<p className="text-sm text-muted-foreground">
<p className="text-sm text-white/40">
{ticketNumber && `Ticket: ${ticketNumber}`}
{ticketNumber && clientName && ' · '}
{clientName && `Client: ${clientName}`}
@@ -378,7 +378,7 @@ export function TreeNavigationPage() {
</div>
<button
onClick={() => navigate('/sessions')}
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
className="rounded-md px-3 py-1.5 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
>
Exit
</button>
@@ -392,12 +392,12 @@ export function TreeNavigationPage() {
const label = node?.question || node?.title || customStep?.step_data.title || nodeId
return (
<span key={nodeId} className="flex items-center gap-2 whitespace-nowrap">
{index > 0 && <span className="text-muted-foreground"></span>}
{index > 0 && <span className="text-white/40"></span>}
<span
className={cn(
index === pathTaken.length - 1
? 'font-medium text-foreground'
: 'text-muted-foreground'
? 'font-medium text-white'
: 'text-white/40'
)}
>
{label.length > 30 ? `${label.slice(0, 30)}...` : label}
@@ -408,15 +408,15 @@ export function TreeNavigationPage() {
</div>
{/* Current Node */}
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
<div className="glass-card rounded-2xl p-6 shadow-sm">
{/* Decision Node */}
{currentNode && currentNode.type === 'decision' && (
<>
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
<h2 className="mb-2 text-xl font-semibold text-white">
{currentNode.question}
</h2>
{currentNode.help_text && (
<div className="mb-4 text-sm text-muted-foreground">
<div className="mb-4 text-sm text-white/50">
<MarkdownContent content={currentNode.help_text} />
</div>
)}
@@ -426,13 +426,13 @@ export function TreeNavigationPage() {
key={option.id}
onClick={() => handleSelectOption(option.id, option.label, option.next_node_id)}
className={cn(
'w-full rounded-md border border-input p-3 text-left transition-colors',
'hover:border-primary hover:bg-accent',
'w-full rounded-md border border-white/10 p-3 text-left text-white transition-colors',
'hover:border-white/30 hover:bg-white/[0.06]',
'flex items-center gap-3'
)}
>
{index < 9 && (
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-muted text-xs font-medium text-muted-foreground">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 text-xs font-medium text-white/50">
{index + 1}
</span>
)}
@@ -443,7 +443,7 @@ export function TreeNavigationPage() {
{/* Previously-created custom steps at this node */}
{customStepFlow.customSteps.filter(cs => cs.inserted_after_node_id === currentNodeId).length > 0 && (
<div className="mt-2 space-y-2">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
<p className="text-xs font-medium uppercase tracking-wide text-white/40">
Your Custom Steps
</p>
{customStepFlow.customSteps
@@ -454,13 +454,12 @@ export function TreeNavigationPage() {
key={cs.id}
onClick={() => customStepFlow.handleNavigateToCustomStep(cs)}
className={cn(
'w-full rounded-md border border-purple-300 bg-purple-50 p-3 text-left transition-colors',
'hover:border-purple-500 hover:bg-purple-100',
'dark:border-purple-700 dark:bg-purple-900/20 dark:hover:border-purple-500 dark:hover:bg-purple-900/40',
'w-full rounded-md border border-purple-700 bg-purple-900/20 p-3 text-left text-white transition-colors',
'hover:border-purple-500 hover:bg-purple-900/40',
'flex items-center gap-3'
)}
>
<span className="flex-shrink-0 rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
<span className="flex-shrink-0 rounded-full bg-purple-900 px-2 py-0.5 text-xs font-medium text-purple-100">
Custom
</span>
<span>{cs.step_data.title}</span>
@@ -472,7 +471,7 @@ export function TreeNavigationPage() {
{/* Add Custom Step Button */}
<button
onClick={() => customStepFlow.setShowCustomStepModal(true)}
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-primary hover:bg-primary/10"
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-white/50 hover:bg-white/10 hover:text-white"
>
<Plus className="h-3.5 w-3.5" />
Add Custom Step
@@ -482,18 +481,18 @@ export function TreeNavigationPage() {
{/* Custom Step Node */}
{currentCustomStep && (
<div className="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-800 dark:bg-purple-900/20">
<div className="rounded-lg border border-purple-800 bg-purple-900/20 p-4">
{/* Custom Step Badge */}
<span className="mb-2 inline-block rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-100">
<span className="mb-2 inline-block rounded-full bg-purple-900 px-2 py-1 text-xs font-medium text-purple-100">
Custom Step
</span>
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
<h2 className="mb-2 text-xl font-semibold text-white">
{currentCustomStep.step_data.title}
</h2>
{currentCustomStep.step_data.content.instructions && (
<div className="mb-4 text-muted-foreground">
<div className="mb-4 text-white/60">
<MarkdownContent content={currentCustomStep.step_data.content.instructions} />
</div>
)}
@@ -506,12 +505,12 @@ export function TreeNavigationPage() {
{currentCustomStep.step_data.content.commands && currentCustomStep.step_data.content.commands.length > 0 && (
<div className="mb-4">
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
<div className="space-y-2">
{currentCustomStep.step_data.content.commands.map((cmd, index) => (
<div key={index}>
<p className="mb-1 text-xs text-muted-foreground">{cmd.label}</p>
<code className="block rounded bg-muted p-2 text-sm font-mono">
<p className="mb-1 text-xs text-white/40">{cmd.label}</p>
<code className="block rounded bg-white/10 p-2 text-sm font-mono">
{cmd.command}
</code>
</div>
@@ -525,13 +524,13 @@ export function TreeNavigationPage() {
const targetNode = findNode(customStepFlow.pendingContinuationNodeId, tree?.tree_structure)
const targetLabel = targetNode?.question || targetNode?.title || 'next step'
return (
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
<div className="mt-6 border-t border-purple-700 pt-4">
<button
type="button"
onClick={customStepFlow.handleContinueToDescendant}
className={cn(
'flex w-full items-center justify-between rounded-md bg-primary px-4 py-3 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'flex w-full items-center justify-between rounded-md bg-white px-4 py-3 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
<span>Continue to: {targetLabel.length > 50 ? `${targetLabel.slice(0, 50)}...` : targetLabel}</span>
@@ -543,16 +542,16 @@ export function TreeNavigationPage() {
{/* Custom Branch Controls */}
{customStepFlow.customBranchMode && (
<div className="mt-6 border-t border-purple-200 pt-4 dark:border-purple-700">
<p className="mb-3 text-sm text-amber-600 dark:text-amber-400">
<div className="mt-6 border-t border-purple-700 pt-4">
<p className="mb-3 text-sm text-amber-400">
Building custom branch - add steps until the issue is resolved
</p>
<div className="flex flex-wrap gap-3">
<button
onClick={() => customStepFlow.setShowCustomStepModal(true)}
className={cn(
'flex items-center gap-2 rounded-md border border-input px-4 py-2 text-sm font-medium',
'bg-background hover:bg-accent hover:text-accent-foreground'
'flex items-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white'
)}
>
<Plus className="h-4 w-4" />
@@ -578,22 +577,22 @@ export function TreeNavigationPage() {
{/* Action Node */}
{currentNode && currentNode.type === 'action' && (
<>
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
<h2 className="mb-2 text-xl font-semibold text-white">
{currentNode.title}
</h2>
{currentNode.description && (
<div className="mb-4 text-muted-foreground">
<div className="mb-4 text-white/60">
<MarkdownContent content={currentNode.description} />
</div>
)}
{currentNode.commands && currentNode.commands.length > 0 && (
<div className="mb-4">
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
<div className="space-y-1">
{currentNode.commands.map((cmd, index) => (
<code
key={index}
className="block rounded bg-muted p-2 text-sm font-mono"
className="block rounded bg-white/10 p-2 text-sm font-mono"
>
{cmd}
</code>
@@ -602,7 +601,7 @@ export function TreeNavigationPage() {
</div>
)}
{currentNode.expected_outcome && (
<p className="mb-4 text-sm text-muted-foreground">
<p className="mb-4 text-sm text-white/40">
<strong>Expected outcome:</strong> {currentNode.expected_outcome}
</p>
)}
@@ -610,8 +609,8 @@ export function TreeNavigationPage() {
<button
onClick={() => handleContinue()}
className={cn(
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90'
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
)}
>
Continue
@@ -624,22 +623,22 @@ export function TreeNavigationPage() {
{currentNode && currentNode.type === 'solution' && (
<>
<div className="mb-4 flex items-center gap-2">
<span className="rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
<span className="rounded-full bg-green-900/30 px-2 py-1 text-xs font-medium text-green-400">
Solution
</span>
</div>
<h2 className="mb-2 text-xl font-semibold text-card-foreground">
<h2 className="mb-2 text-xl font-semibold text-white">
{currentNode.title}
</h2>
{currentNode.description && (
<div className="mb-4 text-muted-foreground">
<div className="mb-4 text-white/60">
<MarkdownContent content={currentNode.description} />
</div>
)}
{currentNode.resolution_steps && currentNode.resolution_steps.length > 0 && (
<div className="mb-4">
<p className="mb-2 text-sm font-medium text-foreground">Resolution steps:</p>
<ol className="list-inside list-decimal space-y-1 text-sm text-muted-foreground">
<p className="mb-2 text-sm font-medium text-white">Resolution steps:</p>
<ol className="list-inside list-decimal space-y-1 text-sm text-white/40">
{currentNode.resolution_steps.map((step, index) => (
<li key={index}>{step}</li>
))}
@@ -660,8 +659,8 @@ export function TreeNavigationPage() {
)}
{/* Notes */}
<div className="mt-6 border-t border-border pt-4">
<label className="block text-sm font-medium text-foreground">
<div className="mt-6 border-t border-white/[0.06] pt-4">
<label className="block text-sm font-medium text-white">
Notes (optional)
</label>
<textarea
@@ -670,9 +669,9 @@ export function TreeNavigationPage() {
placeholder="Add any notes for this step..."
rows={2}
className={cn(
'mt-1 block w-full rounded-md border border-input bg-background px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'mt-1 block w-full rounded-md border border-white/10 bg-black/50 px-3 py-2',
'text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
</div>
@@ -681,7 +680,7 @@ export function TreeNavigationPage() {
{pathTaken.length > 1 && (
<button
onClick={handleGoBack}
className="mt-4 text-sm text-muted-foreground hover:text-foreground"
className="mt-4 text-sm text-white/50 hover:text-white"
>
Go back
</button>
@@ -689,7 +688,7 @@ export function TreeNavigationPage() {
{/* Keyboard Shortcuts Hint */}
{currentNode && (
<div className="mt-4 border-t border-border pt-3 text-xs text-muted-foreground">
<div className="mt-4 border-t border-white/[0.06] pt-3 text-xs text-white/40">
<span className="font-medium">Keyboard:</span>{' '}
{currentNode.type === 'decision' && currentOptions.length > 0 && (
<span>1-{Math.min(currentOptions.length, 9)} select option</span>

View File

@@ -77,16 +77,16 @@ export function TeamCategoriesPage() {
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
}
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="font-heading text-2xl font-bold text-foreground">Team Categories</h1>
<p className="mt-1 text-sm text-muted-foreground">Manage tree categories for your team</p>
<h1 className="text-2xl font-bold text-white">Team Categories</h1>
<p className="mt-1 text-sm text-white/40">Manage tree categories for your team</p>
</div>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
<Plus className="h-4 w-4" />
Create Category
</button>
@@ -95,30 +95,30 @@ export function TeamCategoriesPage() {
{loading ? (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
<div key={i} className="h-16 animate-pulse rounded-lg bg-white/10" />
))}
</div>
) : categories.length === 0 ? (
<div className="flex flex-col items-center justify-center rounded-lg border border-border bg-card py-16">
<FolderTree className="h-12 w-12 text-muted-foreground/50" />
<h3 className="mt-4 font-medium text-foreground">No team categories</h3>
<p className="mt-1 text-sm text-muted-foreground">Create categories to organize your team's trees.</p>
<div className="flex flex-col items-center justify-center glass-card rounded-2xl py-16">
<FolderTree className="h-12 w-12 text-white/30" />
<h3 className="mt-4 font-medium text-white">No team categories</h3>
<p className="mt-1 text-sm text-white/40">Create categories to organize your team's trees.</p>
</div>
) : (
<div className="space-y-2">
{categories.map((cat) => (
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-border bg-card px-4 py-3">
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-white/[0.06] px-4 py-3">
<div>
<span className="font-medium text-foreground">{cat.name}</span>
<span className="ml-3 text-sm text-muted-foreground">{cat.slug}</span>
{cat.description && <span className="ml-3 text-sm text-muted-foreground">- {cat.description}</span>}
<span className="ml-3 text-xs text-muted-foreground">{cat.tree_count} trees</span>
<span className="font-medium text-white">{cat.name}</span>
<span className="ml-3 text-sm text-white/40">{cat.slug}</span>
{cat.description && <span className="ml-3 text-sm text-white/40">- {cat.description}</span>}
<span className="ml-3 text-xs text-white/40">{cat.tree_count} trees</span>
</div>
<div className="flex items-center gap-1">
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground">
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-white/50 hover:bg-white/[0.06] hover:text-white">
<Pencil className="h-4 w-4" />
</button>
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-muted-foreground hover:bg-destructive/10 hover:text-destructive">
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-white/50 hover:bg-red-400/10 hover:text-red-400">
<Trash2 className="h-4 w-4" />
</button>
</div>
@@ -131,22 +131,22 @@ export function TeamCategoriesPage() {
<Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Category" size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-white">Description</label>
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
</div>
</div>
@@ -156,22 +156,22 @@ export function TeamCategoriesPage() {
<Modal isOpen={!!editCategory} onClose={() => setEditCategory(null)} title="Edit Category" size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Save</button>
<button onClick={() => setEditCategory(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Save</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-white">Description</label>
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} />
</div>
</div>

View File

@@ -64,7 +64,7 @@ export function AuditLogsPage() {
render: (log) => (
<button
onClick={() => setExpandedId(expandedId === log.id ? null : log.id)}
className="p-1 text-muted-foreground hover:text-foreground"
className="p-1 text-white/50 hover:text-white"
>
{expandedId === log.id ? (
<ChevronDown className="h-4 w-4" />
@@ -78,14 +78,14 @@ export function AuditLogsPage() {
key: 'action',
header: 'Action',
render: (log) => (
<span className="text-sm font-medium text-foreground">{log.action}</span>
<span className="text-sm font-medium text-white">{log.action}</span>
),
},
{
key: 'resource',
header: 'Resource',
render: (log) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-white/40">
{log.resource_type}{log.resource_id ? ` (${log.resource_id.slice(0, 8)}...)` : ''}
</span>
),
@@ -94,14 +94,14 @@ export function AuditLogsPage() {
key: 'user',
header: 'User',
render: (log) => (
<span className="text-sm text-muted-foreground">{log.user_email || 'System'}</span>
<span className="text-sm text-white/40">{log.user_email || 'System'}</span>
),
},
{
key: 'created_at',
header: 'Time',
render: (log) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-white/40">
{new Date(log.created_at).toLocaleString()}
</span>
),
@@ -117,8 +117,8 @@ export function AuditLogsPage() {
<button
onClick={handleExport}
className={cn(
'flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium',
'text-card-foreground hover:bg-accent'
'flex items-center gap-2 rounded-md border border-white/10 px-4 py-2 text-sm font-medium',
'text-white/60 hover:bg-white/10 hover:text-white'
)}
>
<Download className="h-4 w-4" />
@@ -134,8 +134,8 @@ export function AuditLogsPage() {
onChange={(e) => { setActionFilter(e.target.value); setPage(1) }}
placeholder="Filter by action..."
className={cn(
'h-9 rounded-md border border-border bg-background px-3 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'h-9 rounded-md border border-white/10 bg-black/50 px-3 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
/>
<input
@@ -144,8 +144,8 @@ export function AuditLogsPage() {
onChange={(e) => { setResourceFilter(e.target.value); setPage(1) }}
placeholder="Filter by resource type..."
className={cn(
'h-9 rounded-md border border-border bg-background px-3 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'h-9 rounded-md border border-white/10 bg-black/50 px-3 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
/>
</div>
@@ -166,9 +166,9 @@ export function AuditLogsPage() {
{/* Expanded details row */}
{expandedId && logs.find(l => l.id === expandedId)?.details && (
<div className="rounded-md border border-border bg-muted/30 p-4">
<h4 className="mb-2 text-sm font-medium text-foreground">Details</h4>
<pre className="overflow-x-auto rounded bg-muted p-3 text-xs text-muted-foreground">
<div className="rounded-md border border-white/[0.06] bg-white/[0.02] p-4">
<h4 className="mb-2 text-sm font-medium text-white">Details</h4>
<pre className="overflow-x-auto rounded bg-black/50 p-3 text-xs text-white/40">
{JSON.stringify(logs.find(l => l.id === expandedId)?.details, null, 2)}
</pre>
</div>

View File

@@ -14,13 +14,13 @@ interface MetricCardProps {
function MetricCard({ label, value, icon }: MetricCardProps) {
return (
<div className="rounded-lg border border-border bg-card p-6">
<div className="glass-card rounded-2xl p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{label}</p>
<p className="mt-1 text-3xl font-bold text-foreground">{value}</p>
<p className="text-sm text-white/40">{label}</p>
<p className="mt-1 text-3xl font-bold text-white">{value}</p>
</div>
<div className="rounded-lg bg-muted/50 p-3 text-muted-foreground">{icon}</div>
<div className="rounded-lg bg-white/[0.06] p-3 text-white/50">{icon}</div>
</div>
</div>
)
@@ -56,7 +56,7 @@ export function DashboardPage() {
{loading ? (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="h-32 animate-pulse rounded-lg bg-muted" />
<div key={i} className="h-32 animate-pulse rounded-lg bg-white/10" />
))}
</div>
) : metrics && (
@@ -71,18 +71,18 @@ export function DashboardPage() {
{/* Recent Activity */}
{activity.length > 0 && (
<div>
<h2 className="font-heading text-lg font-semibold text-foreground">Recent Activity</h2>
<h2 className="text-lg font-semibold text-white">Recent Activity</h2>
<div className="mt-3 space-y-2">
{activity.slice(0, 10).map((entry) => (
<div key={entry.id} className="flex items-center justify-between rounded-md border border-border bg-card px-4 py-3 text-sm">
<div key={entry.id} className="flex items-center justify-between rounded-md border border-white/[0.06] px-4 py-3 text-sm">
<div>
<span className="font-medium text-foreground">{entry.action}</span>
<span className="ml-2 text-muted-foreground">{entry.resource_type}</span>
<span className="font-medium text-white">{entry.action}</span>
<span className="ml-2 text-white/40">{entry.resource_type}</span>
{entry.user_email && (
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
<span className="ml-2 text-white/40">by {entry.user_email}</span>
)}
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-white/40">
{new Date(entry.created_at).toLocaleString()}
</span>
</div>
@@ -93,18 +93,18 @@ export function DashboardPage() {
{/* Quick Links */}
<div>
<h2 className="font-heading text-lg font-semibold text-foreground">Quick Links</h2>
<h2 className="text-lg font-semibold text-white">Quick Links</h2>
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
{quickLinks.map((link) => (
<Link
key={link.to}
to={link.to}
className={cn(
'flex items-center gap-3 rounded-lg border border-border bg-card p-4',
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
'flex items-center gap-3 glass-card rounded-2xl p-4',
'text-sm font-medium text-white transition-colors hover:bg-white/[0.06]'
)}
>
<link.icon className="h-5 w-5 text-muted-foreground" />
<link.icon className="h-5 w-5 text-white/50" />
{link.label}
</Link>
))}

View File

@@ -93,11 +93,11 @@ export function FeatureFlagsPage() {
const flagColumns: Column<FeatureFlagResponse>[] = [
{ key: 'name', header: 'Name', render: (f) => (
<div>
<div className="font-medium text-foreground">{f.display_name}</div>
<div className="text-xs text-muted-foreground">{f.flag_key}</div>
<div className="font-medium text-white">{f.display_name}</div>
<div className="text-xs text-white/40">{f.flag_key}</div>
</div>
)},
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-muted-foreground">{f.description || '-'}</span> },
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-white/40">{f.description || '-'}</span> },
...PLANS.map(plan => ({
key: plan,
header: plan.charAt(0).toUpperCase() + plan.slice(1),
@@ -109,7 +109,7 @@ export function FeatureFlagsPage() {
onClick={() => handleTogglePlan(f.id, plan, enabled)}
className={cn(
'h-6 w-10 rounded-full transition-colors',
enabled ? 'bg-green-500' : 'bg-muted'
enabled ? 'bg-emerald-400' : 'bg-white/10'
)}
>
<div className={cn(
@@ -131,10 +131,10 @@ export function FeatureFlagsPage() {
]
const overrideColumns: Column<AccountFeatureOverrideResponse>[] = [
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-muted-foreground">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-white">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-white/40">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
{ key: 'enabled', header: 'Enabled', render: (o) => <StatusBadge variant={o.enabled ? 'success' : 'destructive'}>{o.enabled ? 'Yes' : 'No'}</StatusBadge> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-white/40">{o.note || '-'}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (o) => (
@@ -145,7 +145,7 @@ export function FeatureFlagsPage() {
},
]
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
return (
<div className="space-y-8">
@@ -153,7 +153,7 @@ export function FeatureFlagsPage() {
title="Feature Flags"
description="Manage feature availability per plan and account"
action={
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
<Plus className="h-4 w-4" />
Create Flag
</button>
@@ -161,7 +161,7 @@ export function FeatureFlagsPage() {
/>
<div>
<h2 className="font-heading text-lg font-semibold text-foreground">Feature Matrix</h2>
<h2 className="text-lg font-semibold text-white">Feature Matrix</h2>
<div className="mt-3">
<DataTable columns={flagColumns} data={flags} keyExtractor={(f) => f.id} isLoading={loading}
emptyState={<EmptyState icon={<ToggleLeft className="h-12 w-12" />} title="No feature flags" description="Create feature flags to control availability per plan." />}
@@ -171,8 +171,8 @@ export function FeatureFlagsPage() {
<div>
<div className="flex items-center justify-between">
<h2 className="font-heading text-lg font-semibold text-foreground">Account Overrides</h2>
<button onClick={() => setOverrideOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
<h2 className="text-lg font-semibold text-white">Account Overrides</h2>
<button onClick={() => setOverrideOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
<Plus className="h-4 w-4" />
Add Override
</button>
@@ -188,22 +188,22 @@ export function FeatureFlagsPage() {
<Modal isOpen={createOpen} onClose={() => setCreateOpen(false)} title="Create Feature Flag" size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Flag Key</label>
<label className="mb-1 block text-sm font-medium text-white">Flag Key</label>
<input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
<label className="mb-1 block text-sm font-medium text-white">Display Name</label>
<input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-white">Description</label>
<input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
</div>
</div>
@@ -213,29 +213,29 @@ export function FeatureFlagsPage() {
<Modal isOpen={overrideOpen} onClose={() => setOverrideOpen(false)} title="Add Account Override" size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
<label className="mb-1 block text-sm font-medium text-white">Feature Flag</label>
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={inputCn}>
<option value="">Select a flag...</option>
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
</select>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-border" />
<label htmlFor="override-enabled" className="text-sm font-medium text-foreground">Enabled</label>
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-white/10" />
<label htmlFor="override-enabled" className="text-sm font-medium text-white">Enabled</label>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<label className="mb-1 block text-sm font-medium text-white">Note</label>
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" className={inputCn} />
</div>
</div>

View File

@@ -72,10 +72,10 @@ export function GlobalCategoriesPage() {
}
const columns: Column<AdminCategory>[] = [
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-foreground">{c.name}</span> },
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-muted-foreground">{c.slug}</span> },
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-muted-foreground">{c.description || '-'}</span> },
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-muted-foreground">{c.tree_count}</span> },
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-white">{c.name}</span> },
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-white/40">{c.slug}</span> },
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-white/40">{c.description || '-'}</span> },
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-white/40">{c.tree_count}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (c) => (
@@ -87,7 +87,7 @@ export function GlobalCategoriesPage() {
},
]
const inputCn = cn('w-full rounded-md border border-border bg-background px-3 py-2 text-sm', 'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring')
const inputCn = cn('w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white', 'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20')
return (
<div className="space-y-6">
@@ -95,7 +95,7 @@ export function GlobalCategoriesPage() {
title="Global Categories"
description="Manage tree categories available to all accounts"
action={
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}>
<Plus className="h-4 w-4" />
Create Category
</button>
@@ -118,22 +118,22 @@ export function GlobalCategoriesPage() {
size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<input type="text" value={form.name} onChange={(e) => { const name = e.target.value; setForm(f => ({ ...f, name, slug: generateSlug(name) })) }} placeholder="e.g. Networking" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-white">Description</label>
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
</div>
</div>
@@ -147,22 +147,22 @@ export function GlobalCategoriesPage() {
size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Save</button>
<button onClick={() => setEditCategory(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Save</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<label className="mb-1 block text-sm font-medium text-white">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-white">Description</label>
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} />
</div>
</div>

View File

@@ -72,7 +72,7 @@ export function InviteCodesPage() {
key: 'code',
header: 'Code',
render: (c) => (
<code className="rounded bg-muted px-2 py-1 text-sm font-mono">{c.code}</code>
<code className="rounded bg-white/10 px-2 py-1 text-sm font-mono text-white/70">{c.code}</code>
),
},
{
@@ -89,7 +89,7 @@ export function InviteCodesPage() {
key: 'expires_at',
header: 'Expires',
render: (c) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-white/40">
{c.expires_at ? new Date(c.expires_at).toLocaleDateString() : 'Never'}
</span>
),
@@ -98,7 +98,7 @@ export function InviteCodesPage() {
key: 'created_at',
header: 'Created',
render: (c) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-white/40">
{new Date(c.created_at).toLocaleDateString()}
</span>
),
@@ -135,7 +135,7 @@ export function InviteCodesPage() {
onClick={() => setCreateOpen(true)}
className={cn(
'flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium',
'bg-primary text-primary-foreground hover:bg-primary/90'
'bg-white text-black hover:bg-white/90'
)}
>
<Plus className="h-4 w-4" />
@@ -167,13 +167,13 @@ export function InviteCodesPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setCreateOpen(false)}
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
>
Cancel
</button>
<button
onClick={handleCreate}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
>
Create
</button>
@@ -182,15 +182,15 @@ export function InviteCodesPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
<label className="mb-1 block text-sm font-medium text-white">Expires in (days)</label>
<input
type="number"
value={expiresInDays}
onChange={(e) => setExpiresInDays(e.target.value)}
placeholder="Leave empty for no expiry"
className={cn(
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
/>
</div>

View File

@@ -75,16 +75,16 @@ export function PlanLimitsPage() {
}
const planColumns: Column<PlanLimitConfig>[] = [
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-foreground capitalize">{p.plan}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-muted-foreground">{p.max_trees ?? 'Unlimited'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-muted-foreground">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-muted-foreground">{p.max_users ?? 'Unlimited'}</span> },
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-white capitalize">{p.plan}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-white/40">{p.max_trees ?? 'Unlimited'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-white/40">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-white/40">{p.max_users ?? 'Unlimited'}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (p) => (
<button
onClick={() => setEditPlan({ ...p })}
className="rounded-md px-3 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
className="rounded-md px-3 py-1 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
>
Edit
</button>
@@ -93,11 +93,11 @@ export function PlanLimitsPage() {
]
const overrideColumns: Column<AccountOverrideResponse>[] = [
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_trees ?? '-'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_sessions_per_month ?? '-'}</span> },
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_users ?? '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-white">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-white/40">{o.override_max_trees ?? '-'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-white/40">{o.override_max_sessions_per_month ?? '-'}</span> },
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-white/40">{o.override_max_users ?? '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-white/40">{o.note || '-'}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (o) => (
@@ -109,8 +109,8 @@ export function PlanLimitsPage() {
]
const inputCn = cn(
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)
return (
@@ -118,7 +118,7 @@ export function PlanLimitsPage() {
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
<div>
<h2 className="font-heading text-lg font-semibold text-foreground">Plan Defaults</h2>
<h2 className="text-lg font-semibold text-white">Plan Defaults</h2>
<div className="mt-3">
<DataTable columns={planColumns} data={plans} keyExtractor={(p) => p.plan} isLoading={loading} />
</div>
@@ -126,10 +126,10 @@ export function PlanLimitsPage() {
<div>
<div className="flex items-center justify-between">
<h2 className="font-heading text-lg font-semibold text-foreground">Account Overrides</h2>
<h2 className="text-lg font-semibold text-white">Account Overrides</h2>
<button
onClick={() => setCreateOverride(true)}
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-primary text-primary-foreground hover:bg-primary/90')}
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-white text-black hover:bg-white/90')}
>
<Plus className="h-4 w-4" />
Add Override
@@ -154,23 +154,23 @@ export function PlanLimitsPage() {
size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setEditPlan(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleSavePlan} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save</button>
<button onClick={() => setEditPlan(null)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleSavePlan} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90">Save</button>
</div>
}
>
{editPlan && (
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-white">Max Trees (empty = unlimited)</label>
<input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-white">Max Sessions/Month (empty = unlimited)</label>
<input type="number" value={editPlan.max_sessions_per_month ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-white">Max Users (empty = unlimited)</label>
<input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
</div>
</div>
@@ -185,30 +185,30 @@ export function PlanLimitsPage() {
size="sm"
footer={
<div className="flex justify-end gap-3">
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent">Cancel</button>
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50">Create</button>
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white">Cancel</button>
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<input type="text" value={overrideForm.account_display_code} onChange={(e) => setOverrideForm({ ...overrideForm, account_display_code: e.target.value })} placeholder="e.g. ABC-1234" className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
<label className="mb-1 block text-sm font-medium text-white">Max Trees Override</label>
<input type="number" value={overrideForm.override_max_trees ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_trees: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
<label className="mb-1 block text-sm font-medium text-white">Max Sessions/Month Override</label>
<input type="number" value={overrideForm.override_max_sessions_per_month ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_sessions_per_month: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
<label className="mb-1 block text-sm font-medium text-white">Max Users Override</label>
<input type="number" value={overrideForm.override_max_users ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, override_max_users: e.target.value ? parseInt(e.target.value) : null })} className={inputCn} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<label className="mb-1 block text-sm font-medium text-white">Note</label>
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" className={inputCn} />
</div>
</div>

View File

@@ -36,7 +36,7 @@ export function SettingsPage() {
return (
<div className="space-y-6">
<PageHeader title="Platform Settings" description="Global platform configuration" />
<div className="h-40 animate-pulse rounded-lg bg-muted" />
<div className="h-40 animate-pulse rounded-lg bg-white/10" />
</div>
)
}
@@ -45,11 +45,11 @@ export function SettingsPage() {
<div className="space-y-6">
<PageHeader title="Platform Settings" description="Global platform configuration" />
<div className="max-w-xl space-y-6 rounded-lg border border-border bg-card p-6">
<div className="max-w-xl space-y-6 glass-card rounded-2xl p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
<p className="text-sm text-muted-foreground">
<h3 className="font-medium text-white">Maintenance Mode</h3>
<p className="text-sm text-white/40">
When enabled, users will see a maintenance message instead of the app.
</p>
</div>
@@ -57,7 +57,7 @@ export function SettingsPage() {
onClick={() => setSettings({ ...settings, maintenance_mode: !maintenanceMode })}
className={cn(
'h-6 w-10 rounded-full transition-colors',
maintenanceMode ? 'bg-destructive' : 'bg-muted'
maintenanceMode ? 'bg-red-400' : 'bg-white/10'
)}
>
<div className={cn(
@@ -69,27 +69,27 @@ export function SettingsPage() {
{maintenanceMode && (
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
<label className="mb-1 block text-sm font-medium text-white">Maintenance Message</label>
<textarea
value={maintenanceMessage}
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
rows={3}
placeholder="We're performing scheduled maintenance. Please check back later."
className={cn(
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
/>
</div>
)}
<div className="border-t border-border pt-4">
<div className="border-t border-white/[0.06] pt-4">
<button
onClick={handleSave}
disabled={saving}
className={cn(
'rounded-md px-4 py-2 text-sm font-medium',
'bg-primary text-primary-foreground hover:bg-primary/90',
'bg-white text-black hover:bg-white/90',
'disabled:opacity-50'
)}
>

View File

@@ -98,8 +98,8 @@ export function UsersPage() {
sortable: true,
render: (u) => (
<div>
<div className="font-medium text-foreground">{u.name}</div>
<div className="text-xs text-muted-foreground">{u.email}</div>
<div className="font-medium text-white">{u.name}</div>
<div className="text-xs text-white/40">{u.email}</div>
</div>
),
},
@@ -129,7 +129,7 @@ export function UsersPage() {
header: 'Joined',
sortable: true,
render: (u) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-white/40">
{new Date(u.created_at).toLocaleDateString()}
</span>
),
@@ -197,13 +197,13 @@ export function UsersPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setRoleModalUser(null)}
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
>
Cancel
</button>
<button
onClick={handleRoleChange}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
>
Save
</button>
@@ -211,15 +211,15 @@ export function UsersPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Changing role for <span className="font-medium text-foreground">{roleModalUser?.name}</span>
<p className="text-sm text-white/70">
Changing role for <span className="font-medium text-white">{roleModalUser?.name}</span>
</p>
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value)}
className={cn(
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
'focus:outline-none focus:ring-2 focus:ring-ring'
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
>
<option value="engineer">Engineer</option>
@@ -238,14 +238,14 @@ export function UsersPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setMoveModalUser(null)}
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-card-foreground hover:bg-accent"
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10 hover:text-white"
>
Cancel
</button>
<button
onClick={handleMoveAccount}
disabled={!displayCode}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
>
Move
</button>
@@ -253,19 +253,19 @@ export function UsersPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Moving <span className="font-medium text-foreground">{moveModalUser?.name}</span> to a new account.
<p className="text-sm text-white/70">
Moving <span className="font-medium text-white">{moveModalUser?.name}</span> to a new account.
</p>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<input
type="text"
value={displayCode}
onChange={(e) => setDisplayCode(e.target.value)}
placeholder="e.g. ABC-1234"
className={cn(
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm',
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
'w-full rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'placeholder:text-white/40 focus:outline-none focus:border-white/30 focus:ring-2 focus:ring-white/20'
)}
/>
</div>