feat: UI design system - sidebar layout, workspace system, and shell redesign (#77)

* feat: add workspace system and sidebar layout (UI design system Phase A+B)

Backend: Workspace model, migration (036), schemas, CRUD API endpoints.
Adds workspace_id to trees and categories, seeds 4 default workspaces
per account, auto-assigns existing trees by tree_type.

Frontend: Complete AppLayout rewrite from top-nav to CSS Grid shell
with persistent sidebar + topbar. New components: WorkspaceSwitcher,
NavItem, CategoryList, TagCloud, TopBar, Sidebar. Dashboard components:
QuickStats, FiltersBar, SectionGroup, TreeListItem, SessionsPanel.
WorkspaceStore with localStorage persistence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add command palette search, dashboard rewrite, and shell height fixes (Phase C)

- Add ⌘K command palette with debounced search across flows and sessions
- Rewrite QuickStartPage as dashboard with stats, filters, sessions panel
- Fix h-[calc(100vh-4rem)] → h-full across all pages for CSS Grid shell
- Add active session count badge to sidebar Sessions nav item

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add sidebar collapse, category/tag filtering, and workspace CRUD (Phase D)

- Sidebar collapse/expand toggle with icon-only rail mode (persisted)
- Sidebar category/tag clicks navigate to /trees with URL params
- TreeLibraryPage syncs filters from URL search params bidirectionally
- Workspace create modal with icon picker and auto-slug generation
- TopBar logo adapts to collapsed sidebar state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add Quick Launch modal with actions and recent flows

- Zap button opens Quick Launch with create/navigate shortcuts
- Shows recent flows for quick session start
- Keyboard navigation support (arrows + enter)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add activity notifications panel with session feed

- Bell icon shows dot indicator for recent activity
- Dropdown panel shows recent sessions with status icons
- Links to session detail and sessions list page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: remove workspace system, add pinned flows and label renames

Replace workspace system with pinned flows API (pin/unpin/list/reorder).
Rename user-facing labels: Tree→Flow, Procedure→Project. Add sidebar
nav sub-items for flow type filtering. Remove 11 workspace files,
add migrations 037-038, clean all workspace references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: collapsed sidebar layout scaling and toggle button size

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate auth pages to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate TreeLibraryPage to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate session pages to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate TreeEditorPage to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate TreeNavigationPage to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate session sharing components to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove workspace dropdown animation (dead code)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate common components to new design system

Migrate 15 components from monochrome glass-card design to purple gradient
accent design system tokens (bg-card, border-border, text-foreground,
bg-gradient-brand, etc.)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate procedural and step library components to new design system

Migrate 10 components from monochrome glass-card design to purple gradient
accent design system tokens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate admin pages and components to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate remaining pages to new design system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: migrate remaining components to new design system

Migrates 38 files: tree-editor forms, session modals, step library,
common components, library views, tree preview, and misc UI to use
design tokens (bg-card, border-border, text-foreground, bg-accent,
bg-gradient-brand) replacing old monochrome patterns.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: keep brand text visible on sidebar collapse, hide sub-items until hover

- TopBar: always show "ResolutionFlow" text regardless of sidebar state
- NavItem: sub-items (Troubleshooting, Projects) hidden by default,
  revealed on hover or when a child route is active

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #77.
This commit is contained in:
chihlasm
2026-02-15 22:45:19 -05:00
committed by GitHub
parent ef829f06a4
commit fa709faa60
138 changed files with 5011 additions and 3796 deletions

View File

@@ -130,7 +130,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -154,23 +154,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-white/50" />
<h1 className="text-2xl font-bold text-white sm:text-3xl">Account Settings</h1>
<Building2 className="h-8 w-8 text-muted-foreground" />
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Account Settings</h1>
</div>
<p className="mt-2 text-white/40">
<p className="mt-2 text-muted-foreground">
Manage your account, subscription, and team
</p>
</div>
<div className="max-w-3xl space-y-6">
{/* Account Info Section */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Account Information</h2>
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground">Account Information</h2>
<div className="mt-4 space-y-4">
{/* Account Name */}
<div>
<label className="block text-sm font-medium text-white">
<label className="block text-sm font-medium text-foreground">
Account Name
</label>
{isEditingName ? (
@@ -180,9 +180,9 @@ export function AccountSettingsPage() {
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
className={cn(
'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'
'flex-1 rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
autoFocus
onKeyDown={(e) => {
@@ -197,8 +197,8 @@ export function AccountSettingsPage() {
onClick={handleSaveName}
disabled={isSavingName}
className={cn(
'rounded-md bg-white p-2 text-black',
'hover:bg-white/90 disabled:opacity-50'
'rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 p-2',
'hover:opacity-90 disabled:opacity-50'
)}
>
{isSavingName ? (
@@ -212,18 +212,18 @@ export function AccountSettingsPage() {
setEditedName(account?.name ?? '')
setIsEditingName(false)
}}
className="rounded-md border border-white/10 p-2 text-white/40 hover:bg-white/10"
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent"
>
<X className="h-4 w-4" />
</button>
</div>
) : (
<div className="mt-1 flex items-center gap-2">
<span className="text-sm text-white">{account?.name}</span>
<span className="text-sm text-foreground">{account?.name}</span>
{isAccountOwner && (
<button
onClick={() => setIsEditingName(true)}
className="text-xs text-white hover:underline"
className="text-xs text-foreground hover:underline"
>
Edit
</button>
@@ -234,10 +234,10 @@ export function AccountSettingsPage() {
{/* Display Code */}
<div>
<label className="block text-sm font-medium text-white">
<label className="block text-sm font-medium text-foreground">
Display Code
</label>
<p className="mt-1 text-sm font-mono text-white/40">
<p className="mt-1 text-sm font-mono text-muted-foreground">
{account?.display_code}
</p>
</div>
@@ -245,8 +245,8 @@ export function AccountSettingsPage() {
</div>
{/* Subscription Section */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-white">Subscription</h2>
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<h2 className="text-lg font-semibold text-foreground">Subscription</h2>
<div className="mt-4 space-y-4">
{/* Plan & Status */}
@@ -254,9 +254,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-white/10 text-white/70',
plan === 'pro' && 'bg-white/10 text-white',
plan === 'team' && 'bg-white/10 text-white'
plan === 'free' && 'bg-accent text-muted-foreground',
plan === 'pro' && 'bg-accent text-foreground',
plan === 'team' && 'bg-accent text-foreground'
)}
>
<Crown className="h-3.5 w-3.5" />
@@ -270,7 +270,7 @@ export function AccountSettingsPage() {
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 === 'orphaned' && 'bg-accent text-muted-foreground'
)}
>
{sub.status.charAt(0).toUpperCase() + sub.status.slice(1).replace('_', ' ')}
@@ -279,7 +279,7 @@ export function AccountSettingsPage() {
</div>
{sub?.current_period_end && (
<p className="text-sm text-white/40">
<p className="text-sm text-muted-foreground">
Current period ends: {new Date(sub.current_period_end).toLocaleDateString()}
</p>
)}
@@ -322,32 +322,32 @@ export function AccountSettingsPage() {
{/* Team Members Section (owners only) */}
{isAccountOwner && (
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Users className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Team Members</h2>
<Users className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Team Members</h2>
</div>
{members.length === 0 ? (
<p className="mt-4 text-sm text-white/40">No team members yet.</p>
<p className="mt-4 text-sm text-muted-foreground">No team members yet.</p>
) : (
<div className="mt-4 divide-y divide-white/[0.06]">
<div className="mt-4 divide-y divide-border">
{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-white">{member.name}</p>
<p className="text-xs text-white/40">{member.email}</p>
<p className="text-sm font-medium text-foreground">{member.name}</p>
<p className="text-xs text-muted-foreground">{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-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 === 'owner' && 'bg-accent text-foreground',
member.account_role === 'engineer' && 'bg-accent text-muted-foreground',
member.account_role === 'viewer' && 'bg-accent text-muted-foreground'
)}
>
{member.account_role}
@@ -360,7 +360,7 @@ export function AccountSettingsPage() {
{member.account_role !== 'owner' && (
<button
onClick={() => handleRemoveMember(member.id)}
className="text-white/40 hover:text-red-400"
className="text-muted-foreground hover:text-red-400"
title="Remove member"
>
<X className="h-4 w-4" />
@@ -376,10 +376,10 @@ export function AccountSettingsPage() {
{/* Invite Member Section (owners only) */}
{isAccountOwner && (
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Mail className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Invite Member</h2>
<Mail className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Invite Member</h2>
</div>
<form onSubmit={handleInvite} className="mt-4 space-y-3">
@@ -391,17 +391,17 @@ export function AccountSettingsPage() {
onChange={(e) => setInviteEmail(e.target.value)}
required
className={cn(
'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'
'flex-1 rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className={cn(
'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'
'rounded-md border border-border bg-card px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="engineer">Engineer</option>
@@ -411,8 +411,8 @@ export function AccountSettingsPage() {
type="submit"
disabled={isInviting || !inviteEmail.trim()}
className={cn(
'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'
'rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium',
'hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
{isInviting ? (
@@ -437,8 +437,8 @@ export function AccountSettingsPage() {
{/* Pending Invites */}
{invites.length > 0 && (
<div className="mt-6">
<h3 className="text-sm font-medium text-white">Pending Invites</h3>
<div className="mt-2 divide-y divide-white/[0.06]">
<h3 className="text-sm font-medium text-foreground">Pending Invites</h3>
<div className="mt-2 divide-y divide-border">
{invites
.filter((inv) => !inv.used_at)
.map((invite) => (
@@ -447,21 +447,21 @@ export function AccountSettingsPage() {
className="flex items-center justify-between py-2"
>
<div>
<p className="text-sm text-white">{invite.email}</p>
<p className="text-xs text-white/40">
<p className="text-sm text-foreground">{invite.email}</p>
<p className="text-xs text-muted-foreground">
{invite.expires_at
? `Expires ${new Date(invite.expires_at).toLocaleDateString()}`
: 'No expiration'}
</p>
</div>
<div className="flex items-center gap-2">
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs text-white/70">
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs text-muted-foreground">
{invite.role}
</span>
<button
onClick={() => handleResendInvite(invite.id)}
disabled={resendingId === invite.id}
className="text-white/40 hover:text-white disabled:opacity-50"
className="text-muted-foreground hover:text-foreground disabled:opacity-50"
title="Resend invite"
>
{resendingId === invite.id ? (
@@ -483,34 +483,34 @@ export function AccountSettingsPage() {
{isAccountOwner && (
<Link
to="/account/categories"
className="glass-card rounded-2xl p-4 sm:p-6 flex items-center justify-between group hover:border-white/20 transition-all"
className="bg-card border border-border rounded-xl p-4 sm:p-6 flex items-center justify-between group hover:border-border transition-all"
>
<div className="flex items-center gap-3">
<FolderTree className="h-5 w-5 text-white/50" />
<FolderTree className="h-5 w-5 text-muted-foreground" />
<div>
<h2 className="text-lg font-semibold text-white">Team Categories</h2>
<p className="text-sm text-white/40">Manage tree categories for your team</p>
<h2 className="text-lg font-semibold text-foreground">Team Categories</h2>
<p className="text-sm text-muted-foreground">Manage tree categories for your team</p>
</div>
</div>
<span className="text-white/30 group-hover:text-white transition-colors">&rarr;</span>
<span className="text-muted-foreground group-hover:text-foreground transition-colors">&rarr;</span>
</Link>
)}
{/* Preferences Section */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-white/50" />
<h2 className="text-lg font-semibold text-white">Preferences</h2>
<Settings className="h-5 w-5 text-muted-foreground" />
<h2 className="text-lg font-semibold text-foreground">Preferences</h2>
</div>
<div className="mt-4">
<label
htmlFor="export-format"
className="block text-sm font-medium text-white"
className="block text-sm font-medium text-foreground"
>
Default Export Format
</label>
<p className="text-sm text-white/40">
<p className="text-sm text-muted-foreground">
This format will be pre-selected when exporting sessions
</p>
<select
@@ -521,9 +521,9 @@ export function AccountSettingsPage() {
toast.success('Preference saved')
}}
className={cn(
'mt-2 block w-full max-w-xs 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'
'mt-2 block w-full max-w-xs rounded-xl border border-border bg-card px-3 py-2',
'text-sm text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="markdown">Markdown (.md)</option>
@@ -554,24 +554,24 @@ function UsageStat({
return (
<div className="glass-stat rounded-md p-3">
<p className="text-xs font-medium text-white/40">{label}</p>
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<p
className={cn(
'mt-1 text-lg font-semibold',
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-white'
isAtLimit ? 'text-red-400' : isNearLimit ? 'text-yellow-400' : 'text-foreground'
)}
>
{current}
<span className="text-sm font-normal text-white/40">
<span className="text-sm font-normal text-muted-foreground">
{' '}/ {isUnlimited ? 'Unlimited' : max}
</span>
</p>
{!isUnlimited && (
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/10">
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-accent">
<div
className={cn(
'h-full rounded-full transition-all',
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-white'
isAtLimit ? 'bg-red-400' : isNearLimit ? 'bg-yellow-500' : 'bg-primary'
)}
style={{ width: `${percentage}%` }}
/>

View File

@@ -145,7 +145,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -155,18 +155,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-white sm:text-3xl">
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
Step Categories
</h1>
<p className="mt-2 text-white/40">
<p className="mt-2 text-muted-foreground">
Manage categories for organizing step library
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className={cn(
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium',
'hover:opacity-90'
)}
>
<Plus className="h-4 w-4" />
@@ -181,16 +181,16 @@ export function AdminCategoriesPage() {
type="checkbox"
checked={includeArchived}
onChange={(e) => setIncludeArchived(e.target.checked)}
className="h-4 w-4 rounded border-white/10 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-0"
className="h-4 w-4 rounded border-border text-foreground focus:ring-2 focus:ring-primary/20 focus:ring-offset-0"
/>
<span className="text-sm text-white/40">Show archived categories</span>
<span className="text-sm text-muted-foreground">Show archived categories</span>
</label>
</div>
{/* Categories List */}
{categories.length === 0 ? (
<div className="glass-card rounded-2xl p-12 text-center">
<p className="text-white/40">
<div className="bg-card border border-border rounded-xl p-12 text-center">
<p className="text-muted-foreground">
No categories found. Create your first category to get started.
</p>
</div>

View File

@@ -66,7 +66,7 @@ export function ChangePasswordPage() {
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
</div>
</div>
<h1 className="text-3xl font-bold text-white tracking-tight">
<h1 className="text-3xl font-bold font-heading text-foreground tracking-tight">
Change Password
</h1>
{isForced && (
@@ -77,7 +77,7 @@ export function ChangePasswordPage() {
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
{error && (
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{error}
@@ -85,7 +85,7 @@ export function ChangePasswordPage() {
)}
<div>
<label htmlFor="current-password" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="current-password" className="mb-1 block text-sm font-medium text-foreground">
Current Password
</label>
<PasswordInput
@@ -95,16 +95,16 @@ export function ChangePasswordPage() {
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
/>
</div>
<div>
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-foreground">
New Password
</label>
<PasswordInput
@@ -114,20 +114,20 @@ export function ChangePasswordPage() {
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="At least 10 characters"
/>
<p className="mt-1 text-xs text-white/40">
<p className="mt-1 text-xs text-muted-foreground">
Must include uppercase, lowercase, and a digit.
</p>
</div>
<div>
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-foreground">
Confirm New Password
</label>
<PasswordInput
@@ -137,9 +137,9 @@ export function ChangePasswordPage() {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
/>
@@ -150,8 +150,8 @@ export function ChangePasswordPage() {
disabled={isLoading}
className={cn(
'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',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-black',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all'
)}

View File

@@ -25,26 +25,26 @@ export function ForgotPasswordPage() {
}
return (
<div className="flex min-h-screen items-center justify-center bg-black px-4">
<div className="flex min-h-screen items-center justify-center bg-card px-4">
<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">
<div className="w-16 h-16 rounded-2xl bg-white flex items-center justify-center sm:w-20 sm:h-20">
<div className="w-16 h-16 rounded-2xl bg-gradient-brand 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 className="text-3xl font-bold text-white tracking-tight">
<h1 className="text-3xl font-bold text-foreground tracking-tight">
Reset Password
</h1>
<p className="mt-2 text-sm text-white/40">
<p className="mt-2 text-sm text-muted-foreground">
Enter your email and we'll send you a link to reset your password.
</p>
</div>
{submitted ? (
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="rounded-xl border border-green-400/20 bg-green-400/10 p-4 text-sm text-green-400">
If an account with that email exists, we've sent a password reset link.
Check your inbox and follow the instructions.
@@ -52,7 +52,7 @@ export function ForgotPasswordPage() {
<div className="text-center">
<Link
to="/login"
className="text-sm text-white/60 hover:text-white transition-colors"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Back to sign in
</Link>
@@ -60,9 +60,9 @@ export function ForgotPasswordPage() {
</div>
) : (
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div>
<label htmlFor="email" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="email" className="mb-1 block text-sm font-medium text-foreground">
Email Address
</label>
<input
@@ -73,9 +73,9 @@ export function ForgotPasswordPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="you@example.com"
@@ -87,8 +87,8 @@ export function ForgotPasswordPage() {
disabled={isLoading || !email}
className={cn(
'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',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-background',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all'
)}
@@ -99,7 +99,7 @@ export function ForgotPasswordPage() {
<div className="text-center">
<Link
to="/login"
className="text-sm text-white/60 hover:text-white transition-colors"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Back to sign in
</Link>

View File

@@ -51,19 +51,19 @@ export function LoginPage() {
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
</div>
</div>
<h1 className="text-3xl font-bold text-white tracking-tight">
<h1 className="text-3xl font-bold font-heading text-foreground tracking-tight">
ResolutionFlow
</h1>
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
<p className="mt-2 text-base font-medium text-muted-foreground sm:mt-3 sm:text-lg">
Decision Tree Platform
</p>
<p className="mt-1 text-sm text-white/40 sm:mt-2">
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
Sign in to your account
</p>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
{(error || localError) && (
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{localError || error}
@@ -71,7 +71,7 @@ export function LoginPage() {
)}
<div>
<label htmlFor="email" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="email" className="mb-1 block text-sm font-medium text-foreground">
Email address
</label>
<input
@@ -83,9 +83,9 @@ export function LoginPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="you@example.com"
@@ -93,7 +93,7 @@ export function LoginPage() {
</div>
<div>
<label htmlFor="password" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="password" className="mb-1 block text-sm font-medium text-foreground">
Password
</label>
<PasswordInput
@@ -104,9 +104,9 @@ export function LoginPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="••••••••••"
@@ -114,7 +114,7 @@ export function LoginPage() {
</div>
<div className="text-right">
<Link to="/forgot-password" className="text-xs text-white/40 hover:text-white/60 transition-colors">
<Link to="/forgot-password" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
Forgot password?
</Link>
</div>
@@ -124,8 +124,8 @@ export function LoginPage() {
disabled={isLoading}
className={cn(
'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',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-black',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all'
)}
@@ -134,9 +134,9 @@ export function LoginPage() {
</button>
</div>
<p className="text-center text-sm text-white/40">
<p className="text-center text-sm text-muted-foreground">
Don't have an account?{' '}
<Link to="/register" className="font-medium text-white hover:underline">
<Link to="/register" className="font-medium text-foreground hover:underline">
Register
</Link>
</p>

View File

@@ -96,7 +96,7 @@ export default function MySharesPage() {
if (loading) {
return (
<div className="flex items-center justify-center py-32">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -105,12 +105,12 @@ export default function MySharesPage() {
if (error) {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="glass-card rounded-xl p-6 border border-red-400/20">
<div className="bg-card border border-red-400/20 rounded-xl p-6">
<div className="text-center">
<p className="text-red-400 text-sm mb-4">{error}</p>
<button
onClick={fetchShares}
className="bg-white text-black hover:bg-white/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
className="bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
>
Try again
</button>
@@ -125,7 +125,7 @@ export default function MySharesPage() {
{/* Back link */}
<Link
to="/sessions"
className="inline-flex items-center gap-1.5 text-sm text-white/40 hover:text-white/70 transition-colors mb-6"
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mb-6"
>
<ArrowLeft className="h-4 w-4" />
Back to sessions
@@ -133,21 +133,21 @@ export default function MySharesPage() {
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-white">My Shared Sessions</h1>
<p className="text-white/40 mt-1">Manage your session share links</p>
<h1 className="text-2xl font-heading font-bold text-foreground">My Shared Sessions</h1>
<p className="text-muted-foreground mt-1">Manage your session share links</p>
</div>
{/* Empty state */}
{shares.length === 0 ? (
<div className="glass-card rounded-xl p-12 text-center">
<Link2 className="h-12 w-12 text-white/20 mx-auto mb-4" />
<h2 className="text-lg font-semibold text-white mb-2">No shared sessions</h2>
<p className="text-white/40 text-sm mb-6">
<div className="bg-card border border-border rounded-xl p-12 text-center">
<Link2 className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h2 className="text-lg font-heading font-semibold text-foreground mb-2">No shared sessions</h2>
<p className="text-muted-foreground text-sm mb-6">
Share a session from the session detail page to create a link
</p>
<button
onClick={() => navigate('/sessions')}
className="bg-white text-black hover:bg-white/90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
className="bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90 rounded-md px-4 py-2 text-sm font-medium transition-colors"
>
Go to Sessions
</button>
@@ -159,10 +159,10 @@ export default function MySharesPage() {
const isCopied = copiedId === share.id
return (
<div key={share.id} className="glass-card rounded-xl p-5">
<div key={share.id} className="bg-card border border-border rounded-xl p-5">
{/* Top row: badge + name */}
<div className="flex items-center gap-3 mb-3">
<span className="inline-flex items-center gap-1.5 text-xs rounded-full px-2 py-0.5 bg-white/10 text-white/60">
<span className="inline-flex items-center gap-1.5 text-xs rounded-full px-2 py-0.5 bg-accent text-muted-foreground">
{share.visibility === 'public' ? (
<Globe className="h-3 w-3" />
) : (
@@ -170,18 +170,18 @@ export default function MySharesPage() {
)}
{share.visibility === 'public' ? 'Public' : 'Account Only'}
</span>
<span className="text-sm font-medium text-white">
<span className="text-sm font-medium text-foreground">
{share.share_name || 'Untitled share'}
</span>
</div>
{/* Session info */}
<p className="text-sm text-white/50 mb-2">
<p className="text-sm text-muted-foreground mb-2">
Session ID: {share.session_id.slice(0, 8)}...
</p>
{/* Meta row */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-white/40 mb-4">
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground mb-4">
<span>Created {formatRelativeTime(share.created_at)}</span>
<span className="hidden sm:inline">·</span>
<span>
@@ -203,7 +203,7 @@ export default function MySharesPage() {
'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors',
isCopied
? 'bg-emerald-400/10 text-emerald-400'
: 'bg-white text-black hover:bg-white/90'
: 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90'
)}
>
{isCopied ? (
@@ -216,7 +216,7 @@ export default function MySharesPage() {
<Link
to={`/sessions/${share.session_id}`}
className="inline-flex items-center gap-1.5 border border-white/10 text-white/60 hover:bg-white/10 rounded-md px-3 py-1.5 text-sm transition-colors"
className="inline-flex items-center gap-1.5 border border-border text-muted-foreground hover:bg-accent rounded-md px-3 py-1.5 text-sm transition-colors"
>
<ExternalLink className="h-3.5 w-3.5" />
View Session

View File

@@ -112,8 +112,8 @@ export function MyTreesPage() {
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-6 flex items-center justify-between sm:mb-8">
<div>
<h1 className="text-2xl font-bold text-white sm:text-3xl">My Flows</h1>
<p className="mt-2 text-white/40">
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">My Flows</h1>
<p className="mt-2 text-muted-foreground">
Your forked and custom flows
</p>
</div>
@@ -121,7 +121,7 @@ export function MyTreesPage() {
<div className="relative">
<button
onClick={() => setShowCreateMenu(!showCreateMenu)}
className="flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
className="flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
<Plus className="h-4 w-4" />
Create New
@@ -130,27 +130,27 @@ export function MyTreesPage() {
{showCreateMenu && (
<>
<div className="fixed inset-0 z-10" onClick={() => setShowCreateMenu(false)} />
<div className="absolute right-0 z-20 mt-1 w-56 rounded-lg border border-white/10 bg-black/95 p-1 shadow-xl backdrop-blur-sm">
<div className="absolute right-0 z-20 mt-1 w-56 rounded-lg border border-border bg-card p-1 shadow-xl backdrop-blur-sm">
<Link
to="/trees/new"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-white hover:bg-white/10"
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<FolderTree className="h-4 w-4 text-white/50" />
<FolderTree className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Troubleshooting Tree</div>
<div className="text-xs text-white/40">Branching decision flow</div>
<div className="text-xs text-muted-foreground">Branching decision flow</div>
</div>
</Link>
<Link
to="/flows/new"
onClick={() => setShowCreateMenu(false)}
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-white hover:bg-white/10"
className="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm text-foreground hover:bg-accent"
>
<ListOrdered className="h-4 w-4 text-white/50" />
<ListOrdered className="h-4 w-4 text-muted-foreground" />
<div>
<div className="font-medium">Procedural Flow</div>
<div className="text-xs text-white/40">Step-by-step procedure</div>
<div className="text-xs text-muted-foreground">Step-by-step procedure</div>
</div>
</Link>
</div>
@@ -163,21 +163,21 @@ 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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
) : trees.length === 0 ? (
<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 flows yet</h2>
<p className="mb-4 text-sm text-white/40">
<div className="rounded-lg border border-dashed border-border bg-accent px-4 py-12 text-center">
<FolderTree className="mx-auto mb-4 h-12 w-12 text-muted-foreground" />
<h2 className="mb-2 text-lg font-semibold text-foreground">No personal flows yet</h2>
<p className="mb-4 text-sm text-muted-foreground">
Fork a flow from the library to customize it for your workflow
</p>
<div className="flex items-center justify-center gap-3">
<Link
to="/trees"
className={cn(
'inline-flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'inline-flex items-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium',
'hover:opacity-90'
)}
>
Browse Library
@@ -186,8 +186,8 @@ export function MyTreesPage() {
<Link
to="/trees/new"
className={cn(
'inline-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'
'inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
>
<Plus className="h-4 w-4" />
@@ -201,15 +201,15 @@ export function MyTreesPage() {
{trees.map((tree) => (
<div
key={tree.id}
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover sm:p-6"
className="bg-card border border-border rounded-xl p-4 transition-all hover:border-border/80 sm:p-6"
>
{/* Header */}
<div className="mb-3 flex items-start justify-between gap-2">
<div className="flex items-center gap-2">
{tree.tree_type === 'procedural' && (
<ListOrdered className="h-4 w-4 shrink-0 text-white/40" />
<ListOrdered className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
<h3 className="font-semibold text-white">{tree.name}</h3>
<h3 className="font-semibold text-foreground">{tree.name}</h3>
</div>
<div className="flex items-center gap-1.5">
{tree.tree_type === 'procedural' && (
@@ -218,7 +218,7 @@ export function MyTreesPage() {
</span>
)}
{tree.category_info && (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white/70">
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-muted-foreground">
{tree.category_info.name}
</span>
)}
@@ -226,19 +226,19 @@ export function MyTreesPage() {
</div>
{/* Description */}
<p className="mb-3 text-sm text-white/40 line-clamp-2">
<p className="mb-3 text-sm text-muted-foreground 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-white/5 px-2 py-1.5 text-sm">
<GitBranch className="h-4 w-4 text-white/40" />
<span className="text-white/40">
<div className="mb-3 flex items-center gap-2 rounded-md bg-accent px-2 py-1.5 text-sm">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">
Forked from{' '}
<Link
to={`/trees/${tree.parent_tree_id}/navigate`}
className="font-medium text-white hover:underline"
className="font-medium text-foreground hover:underline"
>
original
</Link>
@@ -254,7 +254,7 @@ export function MyTreesPage() {
)}
{/* Stats */}
<div className="mb-4 flex items-center gap-4 text-xs text-white/30">
<div className="mb-4 flex items-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
<span>{formatDate(tree.lastUsed)}</span>
@@ -271,8 +271,8 @@ export function MyTreesPage() {
type="button"
onClick={() => handleStartSession(tree)}
className={cn(
'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'
'flex flex-1 items-center justify-center gap-2 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-3 py-2 text-sm font-medium',
'hover:opacity-90'
)}
>
<Play className="h-4 w-4" />
@@ -282,8 +282,8 @@ export function MyTreesPage() {
<Link
to={getEditPath(tree)}
className={cn(
'rounded-md border border-white/10 p-2 text-white/40',
'hover:bg-white/10 hover:text-white'
'rounded-md border border-border p-2 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Edit tree"
>
@@ -297,8 +297,8 @@ export function MyTreesPage() {
setShowShareModal(true)
}}
className={cn(
'rounded-md border border-white/10 p-2 text-white/40',
'hover:bg-white/10 hover:text-white'
'rounded-md border border-border p-2 text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
title="Share tree"
>
@@ -311,7 +311,7 @@ export function MyTreesPage() {
setShowDeleteConfirm(true)
}}
className={cn(
'rounded-md border border-white/10 p-2 text-white/40',
'rounded-md border border-border p-2 text-muted-foreground',
'hover:bg-red-400/10 hover:text-red-400'
)}
title="Delete tree"

View File

@@ -97,7 +97,7 @@ export function ProceduralEditorPage() {
if (isLoading) {
return (
<div className="flex min-h-[50vh] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -109,13 +109,13 @@ export function ProceduralEditorPage() {
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/my-trees')}
className="rounded-md p-2 text-white/40 hover:bg-white/10 hover:text-white"
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ArrowLeft className="h-5 w-5" />
</button>
<div className="flex items-center gap-2">
<ListOrdered className="h-5 w-5 text-white/50" />
<h1 className="text-xl font-bold text-white sm:text-2xl">
<ListOrdered className="h-5 w-5 text-muted-foreground" />
<h1 className="text-xl font-bold text-foreground sm:text-2xl">
{isEditMode ? 'Edit Procedure' : 'New Procedure'}
</h1>
</div>
@@ -123,19 +123,19 @@ export function ProceduralEditorPage() {
<div className="flex items-center gap-2">
{isDirty && (
<span className="text-xs text-white/40">Unsaved changes</span>
<span className="text-xs text-muted-foreground">Unsaved changes</span>
)}
<button
onClick={() => handleSave('draft')}
disabled={isSaving}
className="flex items-center gap-1.5 rounded-md border border-white/10 px-3 py-2 text-sm text-white/60 hover:bg-white/10 hover:text-white disabled:opacity-50"
className="flex items-center gap-1.5 rounded-md border border-border px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50"
>
Save Draft
</button>
<button
onClick={() => handleSave('published')}
disabled={isSaving}
className="flex items-center gap-1.5 rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
className="flex items-center gap-1.5 rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
<Save className="h-4 w-4" />
{isSaving ? 'Saving...' : 'Publish'}
@@ -146,44 +146,44 @@ export function ProceduralEditorPage() {
{/* Content */}
<div className="space-y-6">
{/* Metadata */}
<div className="glass-card rounded-2xl p-4 sm:p-6">
<h2 className="mb-4 text-lg font-semibold text-white">Details</h2>
<div className="bg-card border border-border rounded-xl p-4 sm:p-6">
<h2 className="mb-4 text-lg font-semibold text-foreground">Details</h2>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white/60">Name</label>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Domain Controller Build"
className="w-full rounded-lg 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"
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white/60">Description</label>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of this procedure..."
rows={2}
className="w-full rounded-lg 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"
className="w-full rounded-lg border border-border bg-card px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-sm font-medium text-white/60">Tags</label>
<label className="mb-1 block text-sm font-medium text-muted-foreground">Tags</label>
<TagInput tags={tags} onChange={setTags} />
</div>
<div className="flex items-end pb-1">
<label className="flex items-center gap-2 text-sm text-white/60">
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<input
type="checkbox"
checked={isPublic}
onChange={(e) => setIsPublic(e.target.checked)}
className="rounded border-white/20"
className="rounded border-border"
/>
Public (visible to all users)
</label>

View File

@@ -278,7 +278,7 @@ export function ProceduralNavigationPage() {
if (isLoading) {
return (
<div className="flex min-h-[50vh] items-center justify-center">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -330,19 +330,19 @@ export function ProceduralNavigationPage() {
const currentStepState = currentStep ? stepStates.get(currentStep.id) : undefined
return (
<div className="flex h-[calc(100vh-4rem)] flex-col">
<div className="flex h-full flex-col">
{/* Top bar */}
<div className="border-b border-white/[0.06] px-4 py-3 sm:px-6">
<div className="border-b border-border px-4 py-3 sm:px-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="rounded-md p-1.5 text-white/40 hover:bg-white/10 hover:text-white lg:hidden"
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground lg:hidden"
>
{sidebarOpen ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</button>
<ListOrdered className="h-5 w-5 text-white/40" />
<h1 className="text-sm font-semibold text-white sm:text-base">{tree.name}</h1>
<ListOrdered className="h-5 w-5 text-muted-foreground" />
<h1 className="text-sm font-semibold text-foreground sm:text-base">{tree.name}</h1>
</div>
</div>
<div className="mt-2">
@@ -360,7 +360,7 @@ export function ProceduralNavigationPage() {
{/* Left sidebar - step checklist */}
<div
className={cn(
'border-r border-white/[0.06] bg-black/30 transition-all duration-200',
'border-r border-border bg-card transition-all duration-200',
sidebarOpen ? 'w-72 p-3' : 'w-0 overflow-hidden p-0'
)}
>
@@ -375,10 +375,10 @@ export function ProceduralNavigationPage() {
{/* View Parameters button */}
{Object.keys(sessionVariables).length > 0 && (
<div className="mt-3 border-t border-white/[0.06] pt-3">
<div className="mt-3 border-t border-border pt-3">
<button
onClick={() => setParamsOpen(true)}
className="flex w-full items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-xs text-white/40 hover:bg-white/[0.06] hover:text-white/60"
className="flex w-full items-center gap-2 rounded-lg border border-border px-3 py-2 text-xs text-muted-foreground hover:bg-accent hover:text-muted-foreground"
>
<Settings2 className="h-3.5 w-3.5" />
View Parameters ({Object.keys(sessionVariables).length})
@@ -413,15 +413,15 @@ export function ProceduralNavigationPage() {
{paramsOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
className="absolute inset-0 bg-background/60 backdrop-blur-sm"
onClick={() => setParamsOpen(false)}
/>
<div className="relative w-full max-w-md rounded-2xl border border-white/10 bg-black/95 shadow-2xl backdrop-blur-sm">
<div className="flex items-center justify-between border-b border-white/[0.06] px-5 py-4">
<h3 className="text-sm font-semibold text-white">Project Parameters</h3>
<div className="relative w-full max-w-md rounded-2xl border border-border bg-card shadow-2xl backdrop-blur-sm">
<div className="flex items-center justify-between border-b border-border px-5 py-4">
<h3 className="text-sm font-semibold text-foreground">Project Parameters</h3>
<button
onClick={() => setParamsOpen(false)}
className="rounded-lg p-1 text-white/40 hover:bg-white/10 hover:text-white"
className="rounded-lg p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
@@ -429,9 +429,9 @@ export function ProceduralNavigationPage() {
<div className="max-h-[60vh] overflow-y-auto p-5">
<div className="space-y-2">
{Object.entries(sessionVariables).map(([key, value]) => (
<div key={key} className="flex items-baseline justify-between gap-4 rounded-lg bg-white/[0.03] px-3 py-2">
<span className="text-xs font-medium text-white/40">{key.replace(/_/g, ' ')}</span>
<span className="text-right text-sm text-white/70">{value || 'N/A'}</span>
<div key={key} className="flex items-baseline justify-between gap-4 rounded-lg bg-accent px-3 py-2">
<span className="text-xs font-medium text-muted-foreground">{key.replace(/_/g, ' ')}</span>
<span className="text-right text-sm text-muted-foreground">{value || 'N/A'}</span>
</div>
))}
</div>

View File

@@ -1,12 +1,17 @@
import { useState, useEffect, useRef } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Search, Clock, ArrowRight, Play, Loader2, Sparkles } from 'lucide-react'
import { Search, Plus, Loader2 } 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 { getTreeNavigatePath } from '@/lib/routing'
import { usePermissions } from '@/hooks/usePermissions'
import { QuickStats } from '@/components/dashboard/QuickStats'
import { FiltersBar } from '@/components/dashboard/FiltersBar'
import { SectionGroup } from '@/components/dashboard/SectionGroup'
import { SessionsPanel } from '@/components/dashboard/SessionsPanel'
import { TreeListItem as TreeListItemComponent } from '@/components/dashboard/TreeListItem'
function timeAgo(dateStr: string): string {
const now = Date.now()
@@ -18,48 +23,42 @@ function timeAgo(dateStr: string): string {
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h ago`
const days = Math.floor(hours / 24)
if (days === 1) return 'Yesterday'
return `${days}d ago`
}
export function QuickStartPage() {
const navigate = useNavigate()
const { canCreateTrees } = usePermissions()
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; tree_type?: string }[]>([])
const [isLoading, setIsLoading] = useState(true)
const searchRef = useRef<HTMLDivElement>(null)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Load sessions on mount
const [trees, setTrees] = useState<TreeListItem[]>([])
const [activeSessions, setActiveSessions] = useState<Session[]>([])
const [allSessions, setAllSessions] = useState<Session[]>([])
const [isLoading, setIsLoading] = useState(true)
const [activeFilter, setActiveFilter] = useState('all')
// Load data on mount
useEffect(() => {
async function loadData() {
try {
const [active, recent] = await Promise.all([
const [treeList, active, recent] = await Promise.all([
treesApi.list({ sort_by: 'updated_at' }),
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; tree_type?: 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,
tree_type: s.tree_snapshot?.tree_type,
})
}
}
setRecentTrees(deduped)
setTrees(treeList)
setActiveSessions(active)
setAllSessions(recent)
} catch (err) {
console.error('Failed to load sessions:', err)
console.error('Failed to load dashboard data:', err)
} finally {
setIsLoading(false)
}
@@ -70,31 +69,25 @@ export function QuickStartPage() {
// 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)
} catch {
setSearchResults([])
} finally {
setIsSearching(false)
}
}, 300)
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current)
}
return () => { if (debounceRef.current) clearTimeout(debounceRef.current) }
}, [query])
// Close dropdown on outside click
@@ -108,213 +101,154 @@ export function QuickStartPage() {
return () => document.removeEventListener('mousedown', handleClick)
}, [])
// Compute stats
const totalTrees = trees.length
const openSessions = activeSessions.length
const todaySessions = allSessions.filter(s => {
const d = new Date(s.started_at)
const now = new Date()
return d.toDateString() === now.toDateString()
}).length
const completedSessions = allSessions.filter(s => s.completed_at).length
// Filter trees
const filteredTrees = activeFilter === 'all'
? trees
: activeFilter === 'recent'
? trees.slice(0, 10)
: trees
// Map sessions for SessionsPanel
const recentSessionItems = allSessions.slice(0, 5).map(s => ({
id: s.id,
treeName: s.tree_snapshot?.name || 'Unknown',
status: (s.completed_at ? 'completed' : 'in_progress') as 'completed' | 'in_progress',
ticketNumber: s.ticket_number || undefined,
timeAgo: timeAgo(s.started_at),
}))
const filters = [
{ id: 'all', label: 'All' },
{ id: 'recent', label: 'Recently Used' },
{ id: 'my', label: 'My Flows' },
{ id: 'team', label: 'Team Flows' },
]
return (
<div className="container mx-auto px-4 py-12">
{/* Hero Section */}
<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-white/50" />
<span className="text-sm text-white/70 font-medium">DECISION TREE PLATFORM</span>
<div className="p-6 space-y-6">
{/* Page Header */}
<div className="flex items-start justify-between">
<div>
<h1 className="font-heading text-[1.375rem] font-bold tracking-tight text-foreground">
Dashboard
</h1>
<p className="mt-1 text-sm text-muted-foreground">
Welcome back. Here&apos;s what&apos;s happening with your flows.
</p>
</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>
{/* Description */}
<p className="text-lg text-white/40 mb-10 max-w-2xl mx-auto leading-relaxed">
Search our library of proven flows 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-white/40" />
<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 flow..."
className="flex-1 bg-transparent py-4 px-4 text-white placeholder:text-white/30 focus:outline-none"
/>
{isSearching && (
<Loader2 className="mr-4 h-5 w-5 animate-spin text-white/30" />
)}
</div>
</div>
{/* Search Results Dropdown */}
{showResults && (
<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-8">
<Loader2 className="h-5 w-5 animate-spin text-white/40" />
</div>
) : searchResults.length === 0 ? (
<div className="px-4 py-8 text-center text-sm text-white/40">
No results found
</div>
) : (
<ul className="max-h-80 overflow-y-auto py-1">
{searchResults.map((tree) => (
<li key={tree.id}>
<button
onClick={() => navigate(getTreeNavigatePath(tree.id, tree.tree_type))}
className="w-full px-5 py-3.5 text-left transition-all hover:bg-white/[0.06]"
>
<div className="text-sm font-medium text-white">
{tree.name}
</div>
{tree.description && (
<div className="mt-0.5 line-clamp-1 text-xs text-white/40">
{tree.description}
</div>
)}
</button>
</li>
))}
</ul>
)}
</div>
<div className="flex items-center gap-2">
{canCreateTrees && (
<Link
to="/trees/new"
className="flex items-center gap-2 rounded-lg bg-gradient-brand px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-primary/20 hover:opacity-90 transition-opacity"
>
<Plus size={16} />
Create Flow
</Link>
)}
</div>
</div>
{/* Continue Session Section */}
{activeSessions.length > 0 && (
<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>
{/* Quick Stats */}
<QuickStats
stats={[
{ label: 'Active Flows', value: totalTrees, gradient: true },
{ label: 'Sessions Today', value: todaySessions, color: '#f59e0b' },
{ label: 'Open Sessions', value: openSessions, meta: `${completedSessions} completed` },
{ label: 'Docs Generated', value: completedSessions },
]}
/>
{/* 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-white/50" />
</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>
{/* Filters */}
<FiltersBar filters={filters} activeFilter={activeFilter} onFilterChange={setActiveFilter} />
{/* Search (inline, not hero) */}
<div ref={searchRef} className="relative">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => query.length >= 2 && setShowResults(true)}
placeholder="Search flows, sessions, tags…"
className="w-full rounded-lg border border-border bg-card py-2.5 pl-9 pr-4 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
/>
{showResults && (
<div className="absolute z-10 mt-1 w-full rounded-lg border border-border bg-card shadow-xl overflow-hidden">
{isSearching ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
<button
onClick={() =>
navigate(getTreeNavigatePath(activeSessions[0].tree_id, activeSessions[0].tree_snapshot?.tree_type), {
state: { sessionId: activeSessions[0].id },
})
}
className="px-5 py-2.5 bg-white text-black rounded-xl font-semibold hover:bg-white/90 transition-all hover:scale-105"
>
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(getTreeNavigatePath(session.tree_id, session.tree_snapshot?.tree_type), {
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>
) : searchResults.length === 0 ? (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">No results found</div>
) : (
<ul className="max-h-72 overflow-y-auto py-1">
{searchResults.map((tree) => (
<li key={tree.id}>
<button
onClick={() => navigate(getTreeNavigatePath(tree.id, tree.tree_type))}
className="w-full px-4 py-3 text-left transition-colors hover:bg-accent"
>
<div className="text-sm font-medium text-foreground">{tree.name}</div>
{tree.description && (
<div className="mt-0.5 line-clamp-1 text-xs text-muted-foreground">{tree.description}</div>
)}
</div>
<Play className="mt-0.5 h-4 w-4 flex-shrink-0 text-white/50" />
</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>
)}
</button>
</li>
))}
</ul>
)}
</div>
)}
</div>
{/* Recent Trees Section */}
{!isLoading && recentTrees.length > 0 && (
<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 Flows</h2>
{/* Recent Sessions */}
<SessionsPanel sessions={recentSessionItems} delay={150} />
{/* Tree/Flow List */}
{isLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<SectionGroup
title="All Flows"
count={filteredTrees.length}
delay={200}
>
{filteredTrees.slice(0, 20).map(tree => (
<TreeListItemComponent
key={tree.id}
id={tree.id}
name={tree.name}
description={tree.description}
treeType={tree.tree_type || 'troubleshooting'}
category={tree.category_info ? { name: tree.category_info.name, color: undefined } : null}
tags={tree.tags}
usageCount={tree.usage_count}
updatedAt={tree.updated_at}
/>
))}
{filteredTrees.length > 20 && (
<Link
to="/trees"
className="text-sm text-white/60 hover:text-white font-medium transition-colors"
className="block rounded-lg border border-border bg-card px-4 py-3 text-center text-sm text-muted-foreground hover:text-foreground hover:border-border/80 transition-colors"
>
View all
View all {filteredTrees.length} flows
</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(getTreeNavigatePath(tree.tree_id, tree.tree_type))}
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 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-white/40" />
</div>
</div>
<div className="truncate text-sm font-bold text-white mb-2">
{tree.name}
</div>
<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>
))}
</div>
</div>
)}
</SectionGroup>
)}
{/* Footer */}
<div className="mx-auto max-w-4xl text-center">
<Link
to="/trees"
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 Flows
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
)
}

View File

@@ -87,19 +87,19 @@ export function RegisterPage() {
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
</div>
</div>
<h1 className="text-3xl font-bold text-white tracking-tight">
<h1 className="text-3xl font-bold font-heading text-foreground tracking-tight">
ResolutionFlow
</h1>
<p className="mt-2 text-base font-medium text-white/60 sm:mt-3 sm:text-lg">
<p className="mt-2 text-base font-medium text-muted-foreground sm:mt-3 sm:text-lg">
Decision Tree Platform
</p>
<p className="mt-1 text-sm text-white/40 sm:mt-2">
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
Create your account
</p>
</div>
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
{(error || localError) && (
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
{localError || error}
@@ -107,7 +107,7 @@ export function RegisterPage() {
)}
<div>
<label htmlFor="inviteCode" className="block text-sm font-medium text-white">
<label htmlFor="inviteCode" className="block text-sm font-medium text-foreground">
Invite code
</label>
<input
@@ -121,18 +121,18 @@ export function RegisterPage() {
}}
onBlur={(e) => validateInviteCode(e.target.value)}
className={cn(
'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',
'mt-1 block w-full rounded-xl border bg-card px-3 py-2 font-mono tracking-wider',
'text-foreground placeholder:text-muted-foreground',
'focus:outline-none focus:ring-1',
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'
inviteCodeStatus === 'idle' && 'border-border focus:border-primary focus:ring-primary/20',
inviteCodeStatus === 'checking' && 'border-border focus:border-primary focus:ring-primary/20'
)}
placeholder="ABCD1234"
/>
{inviteCodeStatus === 'checking' && (
<p className="mt-1 text-xs text-white/40">Validating...</p>
<p className="mt-1 text-xs text-muted-foreground">Validating...</p>
)}
{inviteCodeStatus === 'valid' && (
<p className="mt-1 text-xs text-emerald-400">{inviteCodeMessage}</p>
@@ -143,7 +143,7 @@ export function RegisterPage() {
</div>
<div>
<label htmlFor="name" className="block text-sm font-medium text-white">
<label htmlFor="name" className="block text-sm font-medium text-foreground">
Full name
</label>
<input
@@ -155,16 +155,16 @@ export function RegisterPage() {
value={name}
onChange={(e) => setName(e.target.value)}
className={cn(
'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'
'mt-1 block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
placeholder="John Smith"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-white">
<label htmlFor="email" className="block text-sm font-medium text-foreground">
Email address
</label>
<input
@@ -176,16 +176,16 @@ export function RegisterPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
className={cn(
'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'
'mt-1 block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-white">
<label htmlFor="password" className="block text-sm font-medium text-foreground">
Password
</label>
<PasswordInput
@@ -196,19 +196,19 @@ export function RegisterPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
className={cn(
'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'
'mt-1 block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
placeholder="••••••••••"
/>
<p className="mt-1 text-xs text-white/30">
<p className="mt-1 text-xs text-muted-foreground">
Must be at least 10 characters
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-white">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-foreground">
Confirm password
</label>
<PasswordInput
@@ -219,9 +219,9 @@ export function RegisterPage() {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={cn(
'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'
'mt-1 block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
placeholder="••••••••••"
/>
@@ -232,8 +232,8 @@ export function RegisterPage() {
disabled={isLoading}
className={cn(
'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',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-black',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all'
)}
@@ -242,9 +242,9 @@ export function RegisterPage() {
</button>
</div>
<p className="text-center text-sm text-white/40">
<p className="text-center text-sm text-muted-foreground">
Already have an account?{' '}
<Link to="/login" className="font-medium text-white hover:underline">
<Link to="/login" className="font-medium text-foreground hover:underline">
Sign in
</Link>
</p>

View File

@@ -82,24 +82,24 @@ export function ResetPasswordPage() {
<BrandLogo size="lg" className="h-10 w-10 invert sm:h-12 sm:w-12" />
</div>
</div>
<h1 className="text-3xl font-bold text-white tracking-tight">
<h1 className="text-3xl font-bold font-heading text-foreground tracking-tight">
Reset Password
</h1>
</div>
{verifying ? (
<div className="glass-card rounded-2xl p-6 text-center">
<p className="text-white/60">Verifying reset link...</p>
<div className="bg-card border border-border rounded-xl p-6 text-center">
<p className="text-muted-foreground">Verifying reset link...</p>
</div>
) : !token || !valid ? (
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-4 text-sm text-red-400">
This reset link is invalid or has expired. Please request a new one.
</div>
<div className="text-center">
<Link
to="/forgot-password"
className="text-sm text-white/60 hover:text-white transition-colors"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Request new reset link
</Link>
@@ -107,10 +107,10 @@ export function ResetPasswordPage() {
</div>
) : (
<form onSubmit={handleSubmit} className="mt-8 space-y-6">
<div className="glass-card rounded-2xl p-6 space-y-4">
<div className="bg-card border border-border rounded-xl p-6 space-y-4">
{email && (
<p className="text-sm text-white/60">
Resetting password for <span className="font-medium text-white">{email}</span>
<p className="text-sm text-muted-foreground">
Resetting password for <span className="font-medium text-foreground">{email}</span>
</p>
)}
@@ -121,7 +121,7 @@ export function ResetPasswordPage() {
)}
<div>
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="new-password" className="mb-1 block text-sm font-medium text-foreground">
New Password
</label>
<PasswordInput
@@ -131,20 +131,20 @@ export function ResetPasswordPage() {
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
placeholder="At least 10 characters"
/>
<p className="mt-1 text-xs text-white/40">
<p className="mt-1 text-xs text-muted-foreground">
Must include uppercase, lowercase, and a digit.
</p>
</div>
<div>
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-white">
<label htmlFor="confirm-password" className="mb-1 block text-sm font-medium text-foreground">
Confirm New Password
</label>
<PasswordInput
@@ -154,9 +154,9 @@ export function ResetPasswordPage() {
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className={cn(
'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',
'block w-full rounded-xl border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'transition-colors'
)}
/>
@@ -167,8 +167,8 @@ export function ResetPasswordPage() {
disabled={isLoading}
className={cn(
'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',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'focus:outline-none focus:ring-2 focus:ring-primary/30 focus:ring-offset-2 focus:ring-offset-black',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all'
)}

View File

@@ -289,7 +289,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-primary" />
</div>
)
}
@@ -302,7 +302,7 @@ export function SessionDetailPage() {
</div>
<button
onClick={() => navigate('/sessions')}
className="mt-4 text-white hover:underline"
className="mt-4 text-foreground hover:underline"
>
Back to sessions
</button>
@@ -318,14 +318,14 @@ export function SessionDetailPage() {
<div>
<button
onClick={() => navigate('/sessions')}
className="mb-2 text-sm text-white/40 hover:text-white"
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
>
Back to sessions
</button>
<h1 className="text-2xl font-bold text-white sm:text-3xl">
<h1 className="text-2xl font-heading font-bold text-foreground sm:text-3xl">
{session.ticket_number || 'Session Details'}
</h1>
<div className="mt-2 flex items-center gap-4 text-sm text-white/40">
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
<span
className={cn(
'flex items-center gap-1',
@@ -342,23 +342,23 @@ export function SessionDetailPage() {
</span>
{session.client_name && <span>Client: {session.client_name}</span>}
{session.completed_at && (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white">
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-foreground">
Duration: {getTotalDuration()}
</span>
)}
{outcomeLabel && (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-xs text-white">
<span className="rounded-full bg-accent px-2 py-0.5 text-xs text-foreground">
Outcome: {outcomeLabel}
</span>
)}
</div>
{session.outcome_notes && (
<p className="mt-2 text-sm text-white/60">Outcome Notes: {session.outcome_notes}</p>
<p className="mt-2 text-sm text-muted-foreground">Outcome Notes: {session.outcome_notes}</p>
)}
{session.next_steps && (
<div className="mt-2">
<span className="text-sm text-white/40">Next Steps:</span>
<p className="mt-0.5 text-sm text-white/60 whitespace-pre-wrap">{session.next_steps}</p>
<span className="text-sm text-muted-foreground">Next Steps:</span>
<p className="mt-0.5 text-sm text-muted-foreground whitespace-pre-wrap">{session.next_steps}</p>
</div>
)}
</div>
@@ -384,8 +384,8 @@ export function SessionDetailPage() {
<button
onClick={handleCopyForTicket}
className={cn(
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
{copiedPsa ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
@@ -399,8 +399,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-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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground sm:w-auto',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="markdown">Markdown</option>
@@ -414,8 +414,8 @@ export function SessionDetailPage() {
onChange={(e) => setMaxStepIndex(e.target.value ? Number(e.target.value) : null)}
aria-label="Export through step"
className={cn(
'rounded-md 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'
'rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="">All steps</option>
@@ -431,8 +431,8 @@ export function SessionDetailPage() {
onChange={(e) => setDetailLevel(e.target.value as 'standard' | 'full')}
aria-label="Detail level"
className={cn(
'rounded-md 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'
'rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="standard">Standard</option>
@@ -443,8 +443,8 @@ export function SessionDetailPage() {
disabled={isExporting}
title="Copy to clipboard"
className={cn(
'rounded-md border border-white/10 bg-transparent p-2 text-white/60',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
'rounded-md border border-border bg-card p-2 text-muted-foreground',
'hover:bg-accent hover:text-foreground disabled:opacity-50'
)}
>
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
@@ -453,8 +453,8 @@ export function SessionDetailPage() {
onClick={handlePreview}
disabled={isExporting}
className={cn(
'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'
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90 disabled:opacity-50'
)}
>
<Eye className="h-4 w-4" />

View File

@@ -151,14 +151,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-white sm:text-3xl">Session History</h1>
<p className="mt-2 text-white/40">
<h1 className="text-2xl font-heading font-bold text-foreground sm:text-3xl">Session History</h1>
<p className="mt-2 text-muted-foreground">
Search and filter your troubleshooting sessions
</p>
</div>
{/* Filter Tabs */}
<div className="mb-6 flex gap-2 border-b border-white/[0.06]">
<div className="mb-6 flex gap-2 border-b border-border">
{(['all', 'active', 'completed'] as const).map((tab) => (
<button
key={tab}
@@ -166,8 +166,8 @@ export function SessionHistoryPage() {
className={cn(
'px-4 py-2 text-sm font-medium transition-colors',
filter === tab
? 'border-b-2 border-white text-white'
: 'text-white/40 hover:text-white'
? 'border-b-2 border-primary text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
@@ -188,22 +188,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-primary" />
</div>
) : sessions.length === 0 ? (
<div className="py-12 text-center text-white/40">
<div className="py-12 text-center text-muted-foreground">
No sessions found.{' '}
{filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from ? (
<button
onClick={handleClearFilters}
className="text-white hover:underline"
className="text-foreground hover:underline"
>
Clear filters
</button>
) : (
<button
onClick={() => navigate('/trees')}
className="text-white hover:underline"
className="text-foreground hover:underline"
>
Start a new session
</button>
@@ -214,7 +214,7 @@ export function SessionHistoryPage() {
{sessions.map((session) => (
<div
key={session.id}
className="glass-card rounded-2xl p-4 transition-all hover:glass-card-hover"
className="bg-card border border-border rounded-xl p-4 transition-all hover:bg-accent/50"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="flex-1">
@@ -226,11 +226,11 @@ export function SessionHistoryPage() {
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
)}
/>
<span className="font-medium text-white">
<span className="font-medium text-foreground">
{session.ticket_number || 'No ticket'}
</span>
{session.client_name && (
<span className="rounded-full bg-white/10 px-2.5 py-0.5 text-xs font-medium text-white">
<span className="rounded-full bg-accent px-2.5 py-0.5 text-xs font-medium text-foreground">
{session.client_name}
</span>
)}
@@ -242,7 +242,7 @@ export function SessionHistoryPage() {
session.outcome === 'workaround' && 'bg-amber-500/20 text-amber-300',
session.outcome === 'escalated' && 'bg-blue-500/20 text-blue-300',
session.outcome === 'unresolved' && 'bg-rose-500/20 text-rose-300',
!session.outcome && 'bg-white/10 text-white/70'
!session.outcome && 'bg-accent text-muted-foreground'
)}
>
{formatOutcomeLabel(session.outcome)}
@@ -251,12 +251,12 @@ export function SessionHistoryPage() {
</div>
{/* Tree Name */}
<p className="mt-1 text-sm text-white/40">
<p className="mt-1 text-sm text-muted-foreground">
<span className="font-medium">Tree:</span> {getTreeName(session)}
</p>
{/* Timestamps */}
<p className="mt-1 text-sm text-white/40">
<p className="mt-1 text-sm text-muted-foreground">
Started: {formatDate(session.started_at)}
{session.completed_at && (
<> · Completed: {formatDate(session.completed_at)}</>
@@ -264,7 +264,7 @@ export function SessionHistoryPage() {
</p>
{/* Stats */}
<p className="mt-1 text-sm text-white/40">
<p className="mt-1 text-sm text-muted-foreground">
{session.decisions.length} decision{session.decisions.length !== 1 ? 's' : ''} recorded
{session.scratchpad && session.scratchpad.trim() && (
<span> · Has notes</span>
@@ -277,8 +277,8 @@ export function SessionHistoryPage() {
<button
onClick={() => navigate(`/sessions/${session.id}`)}
className={cn(
'rounded-md border border-white/10 px-3 py-2 text-sm font-medium text-white/60',
'hover:bg-white/10 hover:text-white'
'rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
>
View Details
@@ -287,8 +287,8 @@ export function SessionHistoryPage() {
<button
onClick={() => navigate(getTreeNavigatePath(session.tree_id, session.tree_snapshot?.tree_type), { state: { sessionId: session.id } })}
className={cn(
'rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'rounded-md bg-gradient-brand px-3 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Resume

View File

@@ -53,16 +53,16 @@ function ErrorCard({ error }: { error: ErrorState }) {
const Icon = iconMap[error.type]
return (
<div className="flex min-h-screen items-center justify-center bg-black px-4">
<div className="glass-card w-full max-w-md rounded-2xl p-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white/5">
<Icon className="h-8 w-8 text-white/40" />
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="bg-card border border-border w-full max-w-md rounded-xl p-8 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-accent">
<Icon className="h-8 w-8 text-muted-foreground" />
</div>
<h1 className="mb-2 text-xl font-semibold text-white">{titleMap[error.type]}</h1>
<p className="mb-6 text-sm text-white/50">{error.message}</p>
<h1 className="mb-2 text-xl font-heading font-semibold text-foreground">{titleMap[error.type]}</h1>
<p className="mb-6 text-sm text-muted-foreground">{error.message}</p>
<Link
to="/"
className="inline-block rounded-lg bg-white px-6 py-2.5 text-sm font-medium text-black hover:bg-white/90"
className="inline-block rounded-lg bg-gradient-brand px-6 py-2.5 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
>
Go to ResolutionFlow
</Link>
@@ -142,10 +142,10 @@ export function SharedSessionPage() {
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center bg-black">
<div className="flex min-h-screen items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-8 w-8 animate-spin text-white/40" />
<p className="text-sm text-white/40">Loading shared session...</p>
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">Loading shared session...</p>
</div>
</div>
)
@@ -160,17 +160,17 @@ export function SharedSessionPage() {
}
return (
<div className="min-h-screen bg-black">
<div className="min-h-screen bg-background">
{/* Minimal header */}
<header className="border-b border-white/[0.06] px-6 py-4">
<header className="border-b border-border px-6 py-4">
<div className="mx-auto flex max-w-7xl items-center justify-between">
<Link to="/" className="flex items-center gap-2">
<BrandLogo size="sm" />
<span className="text-lg font-semibold text-white">ResolutionFlow</span>
<span className="text-lg font-heading font-semibold text-foreground">ResolutionFlow</span>
</Link>
<Link
to="/login"
className="rounded-lg border border-white/10 px-4 py-2 text-sm text-white/60 hover:bg-white/10 hover:text-white"
className="rounded-lg border border-border px-4 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
Sign In
</Link>
@@ -182,14 +182,14 @@ export function SharedSessionPage() {
{/* Metadata section */}
<div className="mb-8">
{data.share_name && (
<h1 className="mb-2 text-2xl font-bold text-white">{data.share_name}</h1>
<h1 className="mb-2 text-2xl font-heading font-bold text-foreground">{data.share_name}</h1>
)}
<p className="text-lg text-white/70">
<span className="text-white/40">Tree:</span> {data.tree_name}
<p className="text-lg text-muted-foreground">
<span className="text-muted-foreground">Tree:</span> {data.tree_name}
</p>
{(data.ticket_number || data.client_name) && (
<p className="mt-1 text-sm text-white/50">
<p className="mt-1 text-sm text-muted-foreground">
{data.ticket_number && (
<span>Ticket: #{data.ticket_number}</span>
)}
@@ -202,7 +202,7 @@ export function SharedSessionPage() {
</p>
)}
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-white/40">
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
<span>Started: {formatDate(data.started_at)}</span>
{data.completed_at && (
<>
@@ -250,9 +250,9 @@ export function SharedSessionPage() {
</main>
{/* Footer */}
<footer className="py-8 text-center text-sm text-white/30">
<footer className="py-8 text-center text-sm text-muted-foreground">
Powered by{' '}
<Link to="/" className="underline hover:text-white/50">
<Link to="/" className="underline hover:text-foreground">
ResolutionFlow
</Link>
</footer>

View File

@@ -345,7 +345,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -353,17 +353,17 @@ export function TreeEditorPage() {
// Mobile gate: show read-only message
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-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">
<div className="flex h-full 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-heading font-semibold text-foreground">Desktop Required</h2>
<p className="mb-6 max-w-sm text-sm text-muted-foreground">
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-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Back to Library
@@ -373,22 +373,22 @@ export function TreeEditorPage() {
}
return (
<div className="flex h-[calc(100vh-4rem)] flex-col">
<div className="flex h-full flex-col">
{/* Draft Restore Prompt */}
{showDraftPrompt && (
<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">
<div className="bg-card border border-border rounded-xl w-full max-w-md p-6 shadow-lg">
<h2 className="mb-2 text-lg font-heading font-semibold text-foreground">Restore Draft?</h2>
<p className="mb-4 text-sm text-muted-foreground">
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-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'flex-1 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Restore Draft
@@ -396,8 +396,8 @@ export function TreeEditorPage() {
<button
onClick={handleDiscardDraft}
className={cn(
'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'
'flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
>
Start Fresh
@@ -410,17 +410,17 @@ export function TreeEditorPage() {
{/* Unsaved Changes Dialog */}
{blocker.state === 'blocked' && (
<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">
<div className="bg-card border border-border rounded-xl w-full max-w-md p-6 shadow-lg">
<h2 className="mb-2 text-lg font-heading font-semibold text-foreground">Unsaved Changes</h2>
<p className="mb-4 text-sm text-muted-foreground">
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-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'flex-1 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Stay
@@ -428,8 +428,8 @@ export function TreeEditorPage() {
<button
onClick={handleBlockerProceed}
className={cn(
'flex-1 rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-red-400',
'hover:bg-white/10'
'flex-1 rounded-md border border-border px-4 py-2 text-sm font-medium text-red-400',
'hover:bg-accent'
)}
>
Leave Without Saving
@@ -440,27 +440,27 @@ export function TreeEditorPage() {
)}
{/* Toolbar */}
<div className="flex items-center justify-between border-b border-white/[0.06] bg-black px-4 py-2">
<div className="flex items-center justify-between border-b border-border bg-card px-4 py-2">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/trees')}
className="text-sm text-white/50 hover:text-white"
className="text-sm text-muted-foreground hover:text-foreground"
>
Back to Library
</button>
<h1 className="text-lg font-semibold text-white">
<h1 className="text-lg font-heading font-semibold text-foreground">
{isEditMode ? 'Edit Tree' : 'Create New Tree'}
{name && <span className="ml-2 text-white/40">- {name}</span>}
{name && <span className="ml-2 text-muted-foreground">- {name}</span>}
</h1>
<div className="flex items-center gap-2">
{treeStatus === 'draft' && (
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
<span className="inline-flex items-center gap-1 rounded-full bg-yellow-900/30 px-2 py-0.5 text-xs font-medium text-yellow-400 border border-yellow-500/20">
<FileText className="h-3 w-3" />
Draft
</span>
)}
{isDirty && (
<span className="rounded-full bg-yellow-500/20 px-2 py-0.5 text-xs text-yellow-600 dark:text-yellow-400">
<span className="rounded-full bg-yellow-500/20 px-2 py-0.5 text-xs text-yellow-400">
Unsaved
</span>
)}
@@ -469,7 +469,7 @@ export function TreeEditorPage() {
<div className="flex items-center gap-2">
{/* Mode Toggle */}
<div className="flex items-center rounded-md border border-white/[0.06]">
<div className="flex items-center rounded-md border border-border">
<button
type="button"
onClick={() => setEditorMode('form')}
@@ -477,14 +477,14 @@ export function TreeEditorPage() {
className={cn(
'flex items-center gap-1.5 rounded-l-md px-3 py-1.5 text-xs font-medium transition-colors',
editorMode === 'form'
? 'bg-white/10 text-white'
: 'text-white/40 hover:bg-white/[0.06] hover:text-white/60'
? 'bg-accent text-foreground'
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
)}
>
<LayoutList className="h-3.5 w-3.5" />
Flow
</button>
<div className="h-5 w-px bg-white/[0.06]" />
<div className="h-5 w-px bg-border" />
<button
type="button"
onClick={() => setEditorMode('code')}
@@ -492,8 +492,8 @@ export function TreeEditorPage() {
className={cn(
'flex items-center gap-1.5 rounded-r-md px-3 py-1.5 text-xs font-medium transition-colors',
editorMode === 'code'
? 'bg-white/10 text-white'
: 'text-white/40 hover:bg-white/[0.06] hover:text-white/60'
? 'bg-accent text-foreground'
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
)}
>
<Code2 className="h-3.5 w-3.5" />
@@ -501,10 +501,10 @@ export function TreeEditorPage() {
</button>
</div>
<div className="mx-1 h-6 w-px bg-white/[0.06]" />
<div className="mx-1 h-6 w-px bg-border" />
{/* Undo/Redo */}
<div className="flex items-center rounded-md border border-white/[0.06]">
<div className="flex items-center rounded-md border border-border">
<button
type="button"
onClick={handleUndo}
@@ -513,13 +513,13 @@ export function TreeEditorPage() {
className={cn(
'rounded-l-md p-2 transition-colors',
pastStates.length > 0
? 'text-white hover:bg-white/[0.06] active:bg-white/[0.12]'
: 'text-white/20 cursor-not-allowed'
? 'text-foreground hover:bg-accent/50 active:bg-accent'
: 'text-muted-foreground/50 cursor-not-allowed'
)}
>
<Undo2 className="h-4 w-4" />
</button>
<div className="h-6 w-px bg-white/[0.06]" />
<div className="h-6 w-px bg-border" />
<button
type="button"
onClick={handleRedo}
@@ -528,15 +528,15 @@ export function TreeEditorPage() {
className={cn(
'rounded-r-md p-2 transition-colors',
futureStates.length > 0
? 'text-white hover:bg-white/[0.06] active:bg-white/[0.12]'
: 'text-white/20 cursor-not-allowed'
? 'text-foreground hover:bg-accent/50 active:bg-accent'
: 'text-muted-foreground/50 cursor-not-allowed'
)}
>
<Redo2 className="h-4 w-4" />
</button>
</div>
<div className="mx-2 h-6 w-px bg-white/[0.06]" />
<div className="mx-2 h-6 w-px bg-border" />
{/* Validate */}
<button
@@ -544,8 +544,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-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'
'flex items-center gap-2 rounded-md border border-border bg-card px-3 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground disabled:opacity-50'
)}
>
<CheckCircle2 className="h-4 w-4" />
@@ -558,8 +558,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-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'
'flex items-center gap-2 rounded-md border border-border bg-card px-3 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<Save className="h-4 w-4" />
@@ -572,8 +572,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-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed'
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
<CheckCircle2 className="h-4 w-4" />

View File

@@ -1,12 +1,11 @@
import { useEffect, useState, useCallback } from 'react'
import { useNavigate, Link, useSearchParams } from 'react-router-dom'
import { Plus, X, FolderOpen, RotateCcw, Play } from 'lucide-react'
import { Plus, X, RotateCcw, Play } from 'lucide-react'
import { treesApi } from '@/api/trees'
import { categoriesApi } from '@/api/categories'
import { foldersApi } from '@/api/folders'
import { sessionsApi } from '@/api/sessions'
import type { TreeListItem, CategoryListItem, FolderListItem, Session } from '@/types'
import { FolderSidebar } from '@/components/library/FolderSidebar'
import { FolderEditModal } from '@/components/library/FolderEditModal'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { TreeGridView } from '@/components/library/TreeGridView'
@@ -27,8 +26,10 @@ export function TreeLibraryPage() {
const [trees, setTrees] = useState<TreeListItem[]>([])
const [categories, setCategories] = useState<CategoryListItem[]>([])
const [folders, setFolders] = useState<FolderListItem[]>([])
const [selectedCategoryId, setSelectedCategoryId] = useState<string>('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const urlCategory = searchParams.get('category') || ''
const urlTags = searchParams.get('tags')
const [selectedCategoryId, setSelectedCategoryId] = useState<string>(urlCategory)
const [selectedTags, setSelectedTags] = useState<string[]>(urlTags ? urlTags.split(',') : [])
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [isLoading, setIsLoading] = useState(true)
@@ -40,7 +41,7 @@ export function TreeLibraryPage() {
urlType === 'troubleshooting' || urlType === 'procedural' ? urlType : 'all'
)
// Sync type filter when URL changes (e.g. clicking nav sub-items)
// Sync filters when URL changes (e.g. clicking sidebar categories/tags or nav sub-items)
useEffect(() => {
const t = searchParams.get('type')
if (t === 'troubleshooting' || t === 'procedural') {
@@ -48,6 +49,9 @@ export function TreeLibraryPage() {
} else {
setTypeFilter('all')
}
setSelectedCategoryId(searchParams.get('category') || '')
const tagsParam = searchParams.get('tags')
setSelectedTags(tagsParam ? tagsParam.split(',') : [])
}, [searchParams])
// View preferences from store
@@ -59,9 +63,6 @@ export function TreeLibraryPage() {
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null)
// Mobile folder sidebar state
const [mobileFolderOpen, setMobileFolderOpen] = useState(false)
// Delete confirmation state
const [treeToDelete, setTreeToDelete] = useState<TreeListItem | null>(null)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
@@ -208,12 +209,6 @@ export function TreeLibraryPage() {
setFolderModalOpen(true)
}
const handleEditFolder = (folder: FolderListItem) => {
setEditingFolder(folder)
setNewFolderParentId(null)
setFolderModalOpen(true)
}
const handleDeleteTree = async () => {
if (!treeToDelete) return
setIsDeleting(true)
@@ -251,46 +246,33 @@ export function TreeLibraryPage() {
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
return (
<div className="flex h-[calc(100vh-4rem)]">
{/* Folder Sidebar */}
<FolderSidebar
selectedFolderId={selectedFolderId}
onFolderSelect={(id) => {
setSelectedFolderId(id)
setMobileFolderOpen(false)
}}
onCreateFolder={handleCreateFolder}
onEditFolder={handleEditFolder}
mobileOpen={mobileFolderOpen}
onMobileClose={() => setMobileFolderOpen(false)}
/>
<div className="flex h-full">
{/* Main Content */}
<div className="flex-1 overflow-auto">
<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-white sm:text-3xl">
{typeFilter === 'procedural' ? 'Procedures' : typeFilter === 'troubleshooting' ? 'Troubleshooting Flows' : 'Flow Library'}
<h1 className="text-2xl font-bold font-heading text-foreground sm:text-3xl">
{typeFilter === 'procedural' ? 'Projects' : typeFilter === 'troubleshooting' ? 'Troubleshooting Flows' : 'Flow Library'}
</h1>
<p className="mt-2 text-white/40">
<p className="mt-2 text-muted-foreground">
{typeFilter === 'procedural'
? 'Step-by-step procedures for project work'
? 'Step-by-step projects and runbooks'
: typeFilter === 'troubleshooting'
? 'Branching decision flows for troubleshooting'
: 'Browse and start troubleshooting flows and procedures'}
: 'Browse and start troubleshooting flows and projects'}
</p>
</div>
{canCreateTrees && (
<Link
to={typeFilter === 'procedural' ? '/flows/new' : '/trees/new'}
className={cn(
'flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'flex items-center gap-2 rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
<Plus className="h-4 w-4" />
{typeFilter === 'procedural' ? 'Create Procedure' : 'Create Flow'}
{typeFilter === 'procedural' ? 'New Project' : 'Create Flow'}
</Link>
)}
</div>
@@ -298,18 +280,6 @@ export function TreeLibraryPage() {
{/* Search and Filter */}
<div className="mb-4 space-y-4">
<div className="flex flex-col gap-4 sm:flex-row">
{/* Mobile folder button */}
<button
onClick={() => setMobileFolderOpen(true)}
className={cn(
'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" />
Folders
</button>
<div className="flex flex-1 gap-2">
<input
type="text"
@@ -318,16 +288,16 @@ export function TreeLibraryPage() {
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className={cn(
'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'
'flex-1 rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
<button
onClick={handleSearch}
className={cn(
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Search
@@ -339,8 +309,8 @@ export function TreeLibraryPage() {
onChange={(e) => setSelectedCategoryId(e.target.value)}
aria-label="Filter by category"
className={cn(
'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'
'rounded-md border border-border bg-card px-3 py-2',
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
>
<option value="">All Categories</option>
@@ -355,7 +325,7 @@ export function TreeLibraryPage() {
{/* View Controls */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="flex rounded-lg border border-white/10 p-0.5">
<div className="flex rounded-lg border border-border p-0.5">
{(['all', 'troubleshooting', 'procedural'] as const).map((t) => (
<button
key={t}
@@ -363,11 +333,11 @@ export function TreeLibraryPage() {
className={cn(
'rounded-md px-3 py-1 text-xs font-medium transition-colors',
typeFilter === t
? 'bg-white/10 text-white'
: 'text-white/40 hover:text-white/60'
? 'bg-accent text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
>
{t === 'all' ? 'All' : t === 'troubleshooting' ? 'Troubleshooting' : 'Procedures'}
{t === 'all' ? 'All' : t === 'troubleshooting' ? 'Troubleshooting' : 'Projects'}
</button>
))}
</div>
@@ -377,9 +347,9 @@ export function TreeLibraryPage() {
type="checkbox"
checked={showDrafts}
onChange={(e) => setShowDrafts(e.target.checked)}
className="h-4 w-4 rounded border-white/20 text-white focus:ring-2 focus:ring-white/20 focus:ring-offset-2"
className="h-4 w-4 rounded border-border text-primary focus:ring-2 focus:ring-primary/20 focus:ring-offset-2"
/>
<span className="text-sm text-white/40">Show my drafts</span>
<span className="text-sm text-muted-foreground">Show my drafts</span>
</label>
</div>
<ViewToggle view={treeLibraryView} onChange={setTreeLibraryView} />
@@ -389,24 +359,24 @@ export function TreeLibraryPage() {
{/* Active Filters */}
{hasActiveFilters && (
<div className="mb-6 flex flex-wrap items-center gap-2">
<span className="text-sm text-white/40">Filters:</span>
<span className="text-sm text-muted-foreground">Filters:</span>
{selectedFolderId && (
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm text-foreground">
Folder
<button
onClick={() => setSelectedFolderId(null)}
className="rounded-full p-0.5 hover:bg-white/20"
className="rounded-full p-0.5 hover:bg-accent"
>
<X className="h-3 w-3" />
</button>
</span>
)}
{selectedCategoryId && (
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white">
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm text-foreground">
{categories.find((c) => c.id === selectedCategoryId)?.name}
<button
onClick={() => setSelectedCategoryId('')}
className="rounded-full p-0.5 hover:bg-white/20"
className="rounded-full p-0.5 hover:bg-accent"
>
<X className="h-3 w-3" />
</button>
@@ -415,12 +385,12 @@ export function TreeLibraryPage() {
{selectedTags.map((tag) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-sm text-white"
className="inline-flex items-center gap-1 rounded-full bg-accent px-3 py-1 text-sm text-foreground"
>
{tag}
<button
onClick={() => removeTagFilter(tag)}
className="rounded-full p-0.5 hover:bg-white/20"
className="rounded-full p-0.5 hover:bg-accent"
>
<X className="h-3 w-3" />
</button>
@@ -428,7 +398,7 @@ export function TreeLibraryPage() {
))}
<button
onClick={clearAllFilters}
className="text-sm text-white/40 hover:text-white"
className="text-sm text-muted-foreground hover:text-foreground"
>
Clear all
</button>
@@ -439,12 +409,12 @@ export function TreeLibraryPage() {
{visibleIncompleteSessions.length > 0 && (
<div className="mb-6 space-y-2">
{visibleIncompleteSessions.map(s => (
<div key={s.id} className="glass-card flex items-center justify-between rounded-xl p-4">
<div key={s.id} className="bg-card border border-border flex items-center justify-between rounded-xl p-4">
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-white">
<p className="truncate font-medium text-foreground">
{s.tree_snapshot?.name || 'Unknown tree'}
</p>
<p className="text-sm text-white/40">
<p className="text-sm text-muted-foreground">
{s.client_name && `${s.client_name} · `}
Started {formatTimeAgo(s.started_at)}
</p>
@@ -452,14 +422,14 @@ export function TreeLibraryPage() {
<div className="flex items-center gap-2">
<button
onClick={() => navigate(getTreeNavigatePath(s.tree_id, s.tree_snapshot?.tree_type), { state: { sessionId: s.id } })}
className="flex items-center gap-1.5 rounded-md bg-white px-3 py-1.5 text-sm font-medium text-black hover:bg-white/90"
className="flex items-center gap-1.5 rounded-md bg-gradient-brand px-3 py-1.5 text-sm font-medium text-white shadow-lg shadow-primary/20 hover:opacity-90"
>
<Play className="h-3.5 w-3.5" />
Resume
</button>
<button
onClick={() => dismissSession(s.id)}
className="rounded-md p-1.5 text-white/30 hover:bg-white/10 hover:text-white"
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
@@ -477,8 +447,8 @@ export function TreeLibraryPage() {
state: { prefillClientName: lastSessionData.client_name, prefillTicketNumber: lastSessionData.ticket_number },
})}
className={cn(
'flex items-center gap-2 rounded-lg border border-white/10 px-4 py-2.5 text-sm text-white/60',
'hover:border-white/20 hover:bg-white/[0.04] hover:text-white'
'flex items-center gap-2 rounded-lg border border-border px-4 py-2.5 text-sm text-muted-foreground',
'hover:border-border hover:bg-accent hover:text-foreground'
)}
>
<RotateCcw className="h-4 w-4" />
@@ -491,10 +461,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-primary" />
</div>
) : trees.length === 0 ? (
<div className="py-12 text-center text-white/40">
<div className="py-12 text-center text-muted-foreground">
No flows found.{' '}
{(searchQuery || hasActiveFilters) && 'Try adjusting your filters.'}
</div>

View File

@@ -516,7 +516,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-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-4 border-border border-t-foreground" />
</div>
)
}
@@ -529,7 +529,7 @@ export function TreeNavigationPage() {
</div>
<button
onClick={() => navigate('/trees')}
className="mt-4 text-white/50 hover:text-white hover:underline"
className="mt-4 text-muted-foreground hover:text-foreground hover:underline"
>
Back to trees
</button>
@@ -541,17 +541,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-white">{tree.name}</h1>
<p className="mb-6 text-white/40">{tree.description}</p>
<h1 className="mb-2 text-2xl font-bold font-heading text-foreground">{tree.name}</h1>
<p className="mb-6 text-muted-foreground">{tree.description}</p>
<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">
<div className="bg-card border border-border rounded-xl space-y-4 p-6">
<h2 className="font-semibold text-foreground">Session Details</h2>
<p className="text-sm text-muted-foreground">
Optional: Add ticket and client info for easier tracking
</p>
<div>
<label className="block text-sm font-medium text-white">
<label className="block text-sm font-medium text-foreground">
Ticket Number
</label>
<input
@@ -560,15 +560,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-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'
'mt-1 block w-full rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="block text-sm font-medium text-white">
<label className="block text-sm font-medium text-foreground">
Client Name
</label>
<input
@@ -577,9 +577,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-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'
'mt-1 block w-full rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
@@ -587,8 +587,8 @@ export function TreeNavigationPage() {
<button
onClick={startSession}
className={cn(
'w-full rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'w-full rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Start Troubleshooting
@@ -609,7 +609,7 @@ export function TreeNavigationPage() {
}
return (
<div className="h-[calc(100vh-4rem)]">
<div className="h-full">
{/* Main Content */}
<div className={cn('h-full overflow-y-auto px-4 py-8 transition-[padding] duration-200', scratchpadOpen && 'sm:pr-[440px]')}>
<div className="mx-auto max-w-4xl">
@@ -617,9 +617,9 @@ export function TreeNavigationPage() {
<div className="mb-6 flex items-center justify-between">
<div>
<div className="flex items-center gap-3">
<h1 className="text-xl font-bold text-white">{tree.name}</h1>
<h1 className="text-xl font-bold font-heading text-foreground">{tree.name}</h1>
{timerDisplay && (
<span className="flex items-center gap-1.5 rounded-full bg-white/10 px-2.5 py-0.5 text-sm text-white/60">
<span className="flex items-center gap-1.5 rounded-full bg-accent px-2 py-0.5 text-[0.6875rem] font-label text-muted-foreground">
<Clock className="h-4 w-4" />
{timerDisplay}
</span>
@@ -627,14 +627,14 @@ export function TreeNavigationPage() {
<button
type="button"
onClick={() => setShortcutsModalOpen(true)}
className="flex items-center justify-center rounded-full p-1 text-white/30 hover:bg-white/10 hover:text-white/60"
className="flex items-center justify-center rounded-full p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
title="Keyboard shortcuts"
>
<HelpCircle className="h-4 w-4" />
</button>
</div>
{(ticketNumber || clientName) && (
<p className="text-sm text-white/40">
<p className="text-sm text-muted-foreground">
{ticketNumber && `Ticket: ${ticketNumber}`}
{ticketNumber && clientName && ' · '}
{clientName && `Client: ${clientName}`}
@@ -647,8 +647,8 @@ export function TreeNavigationPage() {
<button
onClick={() => setShowSharePopover(!showSharePopover)}
className={cn(
'flex items-center gap-1.5 rounded-md border border-white/10 px-3 py-1.5 text-xs font-medium text-white/60',
'hover:bg-white/10 hover:text-white transition-colors'
'flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground transition-colors'
)}
>
<Copy className="h-3.5 w-3.5" />
@@ -656,7 +656,7 @@ export function TreeNavigationPage() {
<ChevronDown className="h-3 w-3" />
</button>
{showSharePopover && (
<div className="absolute right-0 mt-1 z-50 glass-card rounded-lg p-1 min-w-[220px]">
<div className="absolute right-0 mt-1 z-50 bg-card border border-border rounded-xl p-1 min-w-[220px]">
{/* Copy Progress Summary */}
<button
onClick={() => {
@@ -665,8 +665,8 @@ export function TreeNavigationPage() {
}}
disabled={isCopyingForTicket}
className={cn(
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 w-full text-left cursor-pointer transition-colors',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground w-full text-left cursor-pointer transition-colors',
'hover:bg-accent hover:text-foreground disabled:opacity-50'
)}
>
{copiedForTicket ? <Check className="h-4 w-4 text-emerald-400" /> : <Clipboard className="h-4 w-4" />}
@@ -680,15 +680,15 @@ export function TreeNavigationPage() {
}}
disabled={isCopyingShareLink}
className={cn(
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 w-full text-left cursor-pointer transition-colors',
'hover:bg-white/10 hover:text-white disabled:opacity-50'
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground w-full text-left cursor-pointer transition-colors',
'hover:bg-accent hover:text-foreground disabled:opacity-50'
)}
>
{copiedShareLink ? <Check className="h-4 w-4 text-emerald-400" /> : <Link2 className="h-4 w-4" />}
{isCopyingShareLink ? 'Loading...' : copiedShareLink ? 'Copied!' : 'Copy Share Link'}
</button>
{/* Divider */}
<div className="border-t border-white/[0.06] my-1" />
<div className="border-t border-border my-1" />
{/* Manage Share Links */}
<button
onClick={() => {
@@ -696,8 +696,8 @@ export function TreeNavigationPage() {
setShowShareModal(true)
}}
className={cn(
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-white/70 w-full text-left cursor-pointer transition-colors',
'hover:bg-white/10 hover:text-white'
'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground w-full text-left cursor-pointer transition-colors',
'hover:bg-accent hover:text-foreground'
)}
>
<Settings className="h-4 w-4" />
@@ -708,7 +708,7 @@ export function TreeNavigationPage() {
</div>
<button
onClick={() => navigate('/sessions')}
className="rounded-md px-3 py-1.5 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
Exit
</button>
@@ -724,17 +724,17 @@ export function TreeNavigationPage() {
const truncatedLabel = label.length > 30 ? `${label.slice(0, 30)}...` : label
return (
<span key={nodeId} className="flex items-center gap-2 whitespace-nowrap">
{index > 0 && <span className="text-white/40"></span>}
{index > 0 && <span className="text-muted-foreground"></span>}
{index < pathTaken.length - 1 ? (
<button
type="button"
onClick={() => handleBreadcrumbJump(nodeId, index)}
className="text-white/40 hover:text-white/70 hover:underline"
className="text-muted-foreground hover:text-foreground hover:underline"
>
{truncatedLabel}
</button>
) : (
<span className="font-medium text-white">
<span className="font-medium text-foreground">
{truncatedLabel}
</span>
)}
@@ -744,15 +744,15 @@ export function TreeNavigationPage() {
</div>
{/* Current Node */}
<div className="glass-card rounded-2xl p-6 shadow-sm">
<div className="bg-card border border-border rounded-xl p-6 shadow-sm">
{/* Decision Node */}
{currentNode && currentNode.type === 'decision' && (
<>
<h2 className="mb-2 text-xl font-semibold text-white">
<h2 className="mb-2 text-xl font-semibold font-heading text-foreground">
{currentNode.question}
</h2>
{currentNode.help_text && (
<div className="mb-4 text-sm text-white/50">
<div className="mb-4 text-sm text-muted-foreground">
<MarkdownContent content={currentNode.help_text} />
</div>
)}
@@ -763,8 +763,8 @@ export function TreeNavigationPage() {
onClick={() => handleSelectOption(option.id, option.label, option.next_node_id)}
disabled={!!selectingOption}
className={cn(
'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]',
'w-full rounded-md border border-border p-3 text-left text-foreground transition-colors',
'hover:border-primary/30 hover:bg-accent',
'flex items-center gap-3',
selectingOption && selectingOption !== option.id && 'opacity-50 pointer-events-none'
)}
@@ -772,10 +772,10 @@ export function TreeNavigationPage() {
{index < 9 && (
selectingOption === option.id ? (
<span className="flex h-6 w-6 shrink-0 items-center justify-center">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/20 border-t-white" />
<span className="h-4 w-4 animate-spin rounded-full border-2 border-border border-t-foreground" />
</span>
) : (
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10 text-xs font-medium text-white/50">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-accent text-xs font-medium text-muted-foreground">
{index + 1}
</span>
)
@@ -787,7 +787,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-white/40">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Your Custom Steps
</p>
{customStepFlow.customSteps
@@ -798,12 +798,12 @@ export function TreeNavigationPage() {
key={cs.id}
onClick={() => customStepFlow.handleNavigateToCustomStep(cs)}
className={cn(
'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',
'w-full rounded-md border border-primary/30 bg-primary/10 p-3 text-left text-foreground transition-colors',
'hover:border-primary/50 hover:bg-primary/20',
'flex items-center gap-3'
)}
>
<span className="flex-shrink-0 rounded-full bg-purple-900 px-2 py-0.5 text-xs font-medium text-purple-100">
<span className="flex-shrink-0 rounded-full bg-primary/20 px-2 py-0.5 text-xs font-medium text-primary">
Custom
</span>
<span>{cs.step_data.title}</span>
@@ -815,7 +815,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-white/50 hover:bg-white/10 hover:text-white"
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Plus className="h-3.5 w-3.5" />
Add Custom Step
@@ -825,18 +825,18 @@ export function TreeNavigationPage() {
{/* Custom Step Node */}
{currentCustomStep && (
<div className="rounded-lg border border-purple-800 bg-purple-900/20 p-4">
<div className="rounded-lg border border-primary/30 bg-primary/10 p-4">
{/* Custom Step Badge */}
<span className="mb-2 inline-block rounded-full bg-purple-900 px-2 py-1 text-xs font-medium text-purple-100">
<span className="mb-2 inline-block rounded-full bg-primary/20 px-2 py-1 text-xs font-medium text-primary">
Custom Step
</span>
<h2 className="mb-2 text-xl font-semibold text-white">
<h2 className="mb-2 text-xl font-semibold font-heading text-foreground">
{currentCustomStep.step_data.title}
</h2>
{currentCustomStep.step_data.content.instructions && (
<div className="mb-4 text-white/60">
<div className="mb-4 text-muted-foreground">
<MarkdownContent content={currentCustomStep.step_data.content.instructions} />
</div>
)}
@@ -849,13 +849,13 @@ 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-white">Commands:</p>
<p className="mb-2 text-sm font-medium text-foreground">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-white/40">{cmd.label}</p>
<p className="mb-1 text-xs text-muted-foreground">{cmd.label}</p>
<div className="group relative">
<code className="block rounded bg-white/10 p-2 pr-8 text-sm font-mono">
<code className="block rounded bg-accent p-2 pr-8 text-sm font-mono">
{cmd.command}
</code>
<button
@@ -867,7 +867,7 @@ export function TreeNavigationPage() {
{copiedCommand === cmd.command ? (
<Check className="h-3.5 w-3.5 text-green-400" />
) : (
<Clipboard className="h-3.5 w-3.5 text-white/40 hover:text-white" />
<Clipboard className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
)}
</button>
</div>
@@ -879,7 +879,7 @@ export function TreeNavigationPage() {
<button
type="button"
onClick={() => setCommandOutputOpen(!commandOutputOpen)}
className="flex items-center gap-1.5 text-sm text-white/50 hover:text-white"
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
>
<Terminal className="h-3.5 w-3.5" />
<span>Paste Output (Optional)</span>
@@ -894,12 +894,12 @@ export function TreeNavigationPage() {
rows={4}
maxLength={10000}
className={cn(
'block w-full rounded-md border border-white/10 bg-white/10 px-3 py-2',
'font-mono text-sm text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
'block w-full rounded-md border border-border bg-accent px-3 py-2',
'font-mono text-sm text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
<p className="mt-1 text-right text-xs text-white/30">
<p className="mt-1 text-right text-xs text-muted-foreground">
{commandOutput.length.toLocaleString()} / 10,000
</p>
</div>
@@ -913,13 +913,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-700 pt-4">
<div className="mt-6 border-t border-primary/30 pt-4">
<button
type="button"
onClick={handleCustomContinueToDescendant}
className={cn(
'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'
'flex w-full items-center justify-between rounded-md bg-gradient-brand px-4 py-3 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
<span>Continue to: {targetLabel.length > 50 ? `${targetLabel.slice(0, 50)}...` : targetLabel}</span>
@@ -931,7 +931,7 @@ export function TreeNavigationPage() {
{/* Custom Branch Controls */}
{customStepFlow.customBranchMode && (
<div className="mt-6 border-t border-purple-700 pt-4">
<div className="mt-6 border-t border-primary/30 pt-4">
<p className="mb-3 text-sm text-amber-400">
Building custom branch - add steps until the issue is resolved
</p>
@@ -939,8 +939,8 @@ export function TreeNavigationPage() {
<button
onClick={() => customStepFlow.setShowCustomStepModal(true)}
className={cn(
'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'
'flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground',
'hover:bg-accent hover:text-foreground'
)}
>
<Plus className="h-4 w-4" />
@@ -966,21 +966,21 @@ export function TreeNavigationPage() {
{/* Action Node */}
{currentNode && currentNode.type === 'action' && (
<>
<h2 className="mb-2 text-xl font-semibold text-white">
<h2 className="mb-2 text-xl font-semibold font-heading text-foreground">
{currentNode.title}
</h2>
{currentNode.description && (
<div className="mb-4 text-white/60">
<div className="mb-4 text-muted-foreground">
<MarkdownContent content={currentNode.description} />
</div>
)}
{currentNode.commands && currentNode.commands.length > 0 && (
<div className="mb-4">
<p className="mb-2 text-sm font-medium text-white">Commands:</p>
<p className="mb-2 text-sm font-medium text-foreground">Commands:</p>
<div className="space-y-1">
{currentNode.commands.map((cmd, index) => (
<div key={index} className="group relative">
<code className="block rounded bg-white/10 p-2 pr-8 text-sm font-mono">
<code className="block rounded bg-accent p-2 pr-8 text-sm font-mono">
{cmd}
</code>
<button
@@ -992,7 +992,7 @@ export function TreeNavigationPage() {
{copiedCommand === cmd ? (
<Check className="h-3.5 w-3.5 text-green-400" />
) : (
<Clipboard className="h-3.5 w-3.5 text-white/40 hover:text-white" />
<Clipboard className="h-3.5 w-3.5 text-muted-foreground hover:text-foreground" />
)}
</button>
</div>
@@ -1003,7 +1003,7 @@ export function TreeNavigationPage() {
<button
type="button"
onClick={() => setCommandOutputOpen(!commandOutputOpen)}
className="flex items-center gap-1.5 text-sm text-white/50 hover:text-white"
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
>
<Terminal className="h-3.5 w-3.5" />
<span>Paste Output (Optional)</span>
@@ -1018,12 +1018,12 @@ export function TreeNavigationPage() {
rows={4}
maxLength={10000}
className={cn(
'block w-full rounded-md border border-white/10 bg-white/10 px-3 py-2',
'font-mono text-sm text-white placeholder:text-white/40',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
'block w-full rounded-md border border-border bg-accent px-3 py-2',
'font-mono text-sm text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
<p className="mt-1 text-right text-xs text-white/30">
<p className="mt-1 text-right text-xs text-muted-foreground">
{commandOutput.length.toLocaleString()} / 10,000
</p>
</div>
@@ -1032,7 +1032,7 @@ export function TreeNavigationPage() {
</div>
)}
{currentNode.expected_outcome && (
<p className="mb-4 text-sm text-white/40">
<p className="mb-4 text-sm text-muted-foreground">
<strong>Expected outcome:</strong> {currentNode.expected_outcome}
</p>
)}
@@ -1040,8 +1040,8 @@ export function TreeNavigationPage() {
<button
onClick={() => handleContinue()}
className={cn(
'rounded-md bg-white px-4 py-2 text-sm font-medium text-black',
'hover:bg-white/90'
'rounded-md bg-gradient-brand px-4 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
'hover:opacity-90'
)}
>
Continue
@@ -1058,18 +1058,18 @@ export function TreeNavigationPage() {
Solution
</span>
</div>
<h2 className="mb-2 text-xl font-semibold text-white">
<h2 className="mb-2 text-xl font-semibold font-heading text-foreground">
{currentNode.title}
</h2>
{currentNode.description && (
<div className="mb-4 text-white/60">
<div className="mb-4 text-muted-foreground">
<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-white">Resolution steps:</p>
<ol className="list-inside list-decimal space-y-1 text-sm text-white/40">
<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">
{currentNode.resolution_steps.map((step, index) => (
<li key={index}>{step}</li>
))}
@@ -1090,8 +1090,8 @@ export function TreeNavigationPage() {
)}
{/* Notes */}
<div className="mt-6 border-t border-white/[0.06] pt-4">
<label className="block text-sm font-medium text-white">
<div className="mt-6 border-t border-border pt-4">
<label className="block text-sm font-medium text-foreground">
Notes (optional)
</label>
<textarea
@@ -1101,9 +1101,9 @@ export function TreeNavigationPage() {
placeholder="Add any notes for this step..."
rows={2}
className={cn(
'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'
'mt-1 block w-full rounded-md border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
@@ -1112,15 +1112,15 @@ export function TreeNavigationPage() {
{pathTaken.length > 1 && (
<button
onClick={handleGoBack}
className="mt-4 text-sm text-white/50 hover:text-white"
className="mt-4 text-sm text-muted-foreground hover:text-foreground"
>
Go back <span className="text-xs text-white/30">[Esc]</span>
Go back <span className="text-xs text-muted-foreground">[Esc]</span>
</button>
)}
{/* Keyboard Shortcuts Hint */}
{currentNode && (
<div className="mt-4 border-t border-white/[0.06] pt-3 text-xs text-white/40">
<div className="mt-4 border-t border-border pt-3 text-xs text-muted-foreground">
<span className="font-medium">Keyboard:</span>{' '}
{currentNode.type === 'decision' && currentOptions.length > 0 && (
<span>1-{Math.min(currentOptions.length, 9)} select option</span>
@@ -1189,24 +1189,24 @@ export function TreeNavigationPage() {
>
<div className="space-y-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-white/60">Select option</span>
<span className="rounded bg-white/10 px-2 py-0.5 font-mono text-xs text-white/80">1-9</span>
<span className="text-muted-foreground">Select option</span>
<span className="rounded bg-accent px-2 py-0.5 font-mono text-xs text-foreground">1-9</span>
</div>
<div className="flex items-center justify-between">
<span className="text-white/60">Go back</span>
<span className="rounded bg-white/10 px-2 py-0.5 font-mono text-xs text-white/80">Esc</span>
<span className="text-muted-foreground">Go back</span>
<span className="rounded bg-accent px-2 py-0.5 font-mono text-xs text-foreground">Esc</span>
</div>
<div className="flex items-center justify-between">
<span className="text-white/60">Continue / Complete</span>
<span className="rounded bg-white/10 px-2 py-0.5 font-mono text-xs text-white/80">Enter</span>
<span className="text-muted-foreground">Continue / Complete</span>
<span className="rounded bg-accent px-2 py-0.5 font-mono text-xs text-foreground">Enter</span>
</div>
<div className="flex items-center justify-between">
<span className="text-white/60">Focus notes</span>
<span className="rounded bg-white/10 px-2 py-0.5 font-mono text-xs text-white/80">Tab</span>
<span className="text-muted-foreground">Focus notes</span>
<span className="rounded bg-accent px-2 py-0.5 font-mono text-xs text-foreground">Tab</span>
</div>
<div className="flex items-center justify-between">
<span className="text-white/60">Show shortcuts</span>
<span className="rounded bg-white/10 px-2 py-0.5 font-mono text-xs text-white/80">?</span>
<span className="text-muted-foreground">Show shortcuts</span>
<span className="rounded bg-accent px-2 py-0.5 font-mono text-xs text-foreground">?</span>
</div>
</div>
</Modal>

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-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')
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/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="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>
<h1 className="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>
</div>
<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')}>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-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-white/10" />
<div key={i} className="h-16 animate-pulse rounded-lg bg-accent" />
))}
</div>
) : categories.length === 0 ? (
<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 className="flex flex-col items-center justify-center bg-card border border-border rounded-xl py-16">
<FolderTree className="h-12 w-12 text-muted-foreground" />
<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>
) : (
<div className="space-y-2">
{categories.map((cat) => (
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-white/[0.06] px-4 py-3">
<div key={cat.id} className="flex items-center justify-between rounded-lg border border-border px-4 py-3">
<div>
<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>
<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>
</div>
<div className="flex items-center gap-1">
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-white/50 hover:bg-white/[0.06] hover:text-white">
<button onClick={() => openEdit(cat)} className="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground">
<Pencil className="h-4 w-4" />
</button>
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-white/50 hover:bg-red-400/10 hover:text-red-400">
<button onClick={() => handleDelete(cat.id)} className="rounded-md p-1.5 text-muted-foreground 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-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>
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Slug</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Description</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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>
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Save</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Slug</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Description</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white/50 hover:text-white"
className="p-1 text-muted-foreground hover:text-foreground"
>
{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-white">{log.action}</span>
<span className="text-sm font-medium text-foreground">{log.action}</span>
),
},
{
key: 'resource',
header: 'Resource',
render: (log) => (
<span className="text-sm text-white/40">
<span className="text-sm text-muted-foreground">
{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-white/40">{log.user_email || 'System'}</span>
<span className="text-sm text-muted-foreground">{log.user_email || 'System'}</span>
),
},
{
key: 'created_at',
header: 'Time',
render: (log) => (
<span className="text-sm text-white/40">
<span className="text-sm text-muted-foreground">
{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-white/10 px-4 py-2 text-sm font-medium',
'text-white/60 hover:bg-white/10 hover:text-white'
'flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium',
'text-muted-foreground hover:bg-accent hover:text-foreground'
)}
>
<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-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'
'h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/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-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'
'h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/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-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">
<div className="rounded-md border border-border bg-accent p-4">
<h4 className="mb-2 text-sm font-medium text-foreground">Details</h4>
<pre className="overflow-x-auto rounded bg-card p-3 text-xs text-muted-foreground">
{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="glass-card rounded-2xl p-6">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-white/40">{label}</p>
<p className="mt-1 text-3xl font-bold text-white">{value}</p>
<p className="text-sm text-muted-foreground">{label}</p>
<p className="mt-1 text-3xl font-bold text-foreground">{value}</p>
</div>
<div className="rounded-lg bg-white/[0.06] p-3 text-white/50">{icon}</div>
<div className="rounded-lg bg-accent p-3 text-muted-foreground">{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-white/10" />
<div key={i} className="h-32 animate-pulse rounded-lg bg-accent" />
))}
</div>
) : metrics && (
@@ -71,18 +71,18 @@ export function DashboardPage() {
{/* Recent Activity */}
{activity.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-white">Recent Activity</h2>
<h2 className="text-lg font-semibold text-foreground">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-white/[0.06] px-4 py-3 text-sm">
<div key={entry.id} className="flex items-center justify-between rounded-md border border-border px-4 py-3 text-sm">
<div>
<span className="font-medium text-white">{entry.action}</span>
<span className="ml-2 text-white/40">{entry.resource_type}</span>
<span className="font-medium text-foreground">{entry.action}</span>
<span className="ml-2 text-muted-foreground">{entry.resource_type}</span>
{entry.user_email && (
<span className="ml-2 text-white/40">by {entry.user_email}</span>
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
)}
</div>
<span className="text-xs text-white/40">
<span className="text-xs text-muted-foreground">
{new Date(entry.created_at).toLocaleString()}
</span>
</div>
@@ -93,18 +93,18 @@ export function DashboardPage() {
{/* Quick Links */}
<div>
<h2 className="text-lg font-semibold text-white">Quick Links</h2>
<h2 className="text-lg font-semibold text-foreground">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 glass-card rounded-2xl p-4',
'text-sm font-medium text-white transition-colors hover:bg-white/[0.06]'
'flex items-center gap-3 bg-card border border-border rounded-xl p-4',
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
)}
>
<link.icon className="h-5 w-5 text-white/50" />
<link.icon className="h-5 w-5 text-muted-foreground" />
{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-white">{f.display_name}</div>
<div className="text-xs text-white/40">{f.flag_key}</div>
<div className="font-medium text-foreground">{f.display_name}</div>
<div className="text-xs text-muted-foreground">{f.flag_key}</div>
</div>
)},
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-white/40">{f.description || '-'}</span> },
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-muted-foreground">{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-emerald-400' : 'bg-white/10'
enabled ? 'bg-emerald-400' : 'bg-accent'
)}
>
<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-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: '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: '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-white/40">{o.note || '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{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-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')
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/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-white text-black hover:bg-white/90')}>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90')}>
<Plus className="h-4 w-4" />
Create Flag
</button>
@@ -161,7 +161,7 @@ export function FeatureFlagsPage() {
/>
<div>
<h2 className="text-lg font-semibold text-white">Feature Matrix</h2>
<h2 className="text-lg font-semibold text-foreground">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="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')}>
<h2 className="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-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-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-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>
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleCreate} disabled={!createForm.flag_key || !createForm.display_name} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Flag Key</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Display Name</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Description</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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>
<button onClick={() => setOverrideOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code || !overrideForm.flag_id} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Feature Flag</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white/10" />
<label htmlFor="override-enabled" className="text-sm font-medium text-white">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-border" />
<label htmlFor="override-enabled" className="text-sm font-medium text-foreground">Enabled</label>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Note</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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: '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: 'actions', header: '', className: 'w-12',
render: (c) => (
@@ -87,7 +87,7 @@ export function GlobalCategoriesPage() {
},
]
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')
const inputCn = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/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-white text-black hover:bg-white/90')}>
<button onClick={() => setCreateOpen(true)} className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-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-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>
<button onClick={() => setCreateOpen(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleCreate} disabled={!form.name || !form.slug} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Slug</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Description</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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>
<button onClick={() => setEditCategory(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleUpdate} disabled={!form.name || !form.slug} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Save</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Slug</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Description</label>
<label className="mb-1 block text-sm font-medium text-foreground">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

@@ -108,8 +108,8 @@ export function InviteCodesPage() {
}
const inputClass = 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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)
const columns: Column<InviteCodeResponse>[] = [
@@ -117,7 +117,7 @@ export function InviteCodesPage() {
key: 'code',
header: 'Code',
render: (c) => (
<code className="rounded bg-white/10 px-2 py-1 text-sm font-mono text-white/70">{c.code}</code>
<code className="rounded bg-accent px-2 py-1 text-sm font-mono text-muted-foreground">{c.code}</code>
),
},
{
@@ -128,12 +128,12 @@ export function InviteCodesPage() {
{c.email_sent ? (
<MailCheck className="h-3.5 w-3.5 text-emerald-400" />
) : (
<Mail className="h-3.5 w-3.5 text-white/30" />
<Mail className="h-3.5 w-3.5 text-muted-foreground" />
)}
<span className="text-sm text-white/60">{c.email}</span>
<span className="text-sm text-muted-foreground">{c.email}</span>
</div>
) : (
<span className="text-sm text-white/30">&mdash;</span>
<span className="text-sm text-muted-foreground">&mdash;</span>
),
},
{
@@ -149,9 +149,9 @@ export function InviteCodesPage() {
key: 'trial',
header: 'Trial',
render: (c) => c.has_trial ? (
<span className="text-sm text-white/60">{c.trial_duration_days}d</span>
<span className="text-sm text-muted-foreground">{c.trial_duration_days}d</span>
) : (
<span className="text-sm text-white/30">&mdash;</span>
<span className="text-sm text-muted-foreground">&mdash;</span>
),
},
{
@@ -168,7 +168,7 @@ export function InviteCodesPage() {
key: 'expires_at',
header: 'Expires',
render: (c) => (
<span className="text-sm text-white/40">
<span className="text-sm text-muted-foreground">
{c.expires_at ? new Date(c.expires_at).toLocaleDateString() : 'Never'}
</span>
),
@@ -177,7 +177,7 @@ export function InviteCodesPage() {
key: 'created_at',
header: 'Created',
render: (c) => (
<span className="text-sm text-white/40">
<span className="text-sm text-muted-foreground">
{new Date(c.created_at).toLocaleDateString()}
</span>
),
@@ -219,7 +219,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-white text-black hover:bg-white/90'
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90'
)}
>
<Plus className="h-4 w-4" />
@@ -251,14 +251,14 @@ export function InviteCodesPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => { setCreateOpen(false); resetForm() }}
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"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleCreate}
disabled={creating}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
{creating ? 'Creating...' : 'Create'}
</button>
@@ -267,7 +267,7 @@ export function InviteCodesPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Recipient Email</label>
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
<input
type="email"
value={email}
@@ -278,7 +278,7 @@ export function InviteCodesPage() {
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Plan</label>
<label className="mb-1 block text-sm font-medium text-foreground">Plan</label>
<select
aria-label="Plan"
value={assignedPlan}
@@ -297,7 +297,7 @@ export function InviteCodesPage() {
{assignedPlan !== 'free' && (
<div>
<label className="mb-1 block text-sm font-medium text-white">Trial Duration (days)</label>
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
<input
type="number"
value={trialDays}
@@ -307,12 +307,12 @@ export function InviteCodesPage() {
max={90}
className={inputClass}
/>
<p className="mt-1 text-xs text-white/40">Leave empty for no trial account gets full plan immediately.</p>
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial account gets full plan immediately.</p>
</div>
)}
<div>
<label className="mb-1 block text-sm font-medium text-white">Expires in (days)</label>
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
<input
type="number"
value={expiresInDays}
@@ -323,7 +323,7 @@ export function InviteCodesPage() {
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Note</label>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<input
type="text"
value={note}

View File

@@ -75,16 +75,16 @@ export function PlanLimitsPage() {
}
const planColumns: Column<PlanLimitConfig>[] = [
{ 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: '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: 'actions', header: '', className: 'w-12',
render: (p) => (
<button
onClick={() => setEditPlan({ ...p })}
className="rounded-md px-3 py-1 text-sm text-white/50 hover:bg-white/[0.06] hover:text-white"
className="rounded-md px-3 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
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-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: '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: 'actions', header: '', className: 'w-12',
render: (o) => (
@@ -109,8 +109,8 @@ export function PlanLimitsPage() {
]
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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/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="text-lg font-semibold text-white">Plan Defaults</h2>
<h2 className="text-lg font-semibold text-foreground">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="text-lg font-semibold text-white">Account Overrides</h2>
<h2 className="text-lg font-semibold text-foreground">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-white text-black hover:bg-white/90')}
className={cn('flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium', 'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-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-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>
<button onClick={() => setEditPlan(null)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleSavePlan} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90">Save</button>
</div>
}
>
{editPlan && (
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Max Trees (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Max Sessions/Month (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Max Users (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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>
<button onClick={() => setCreateOverride(false)} className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-foreground">Cancel</button>
<button onClick={handleCreateOverride} disabled={!overrideForm.account_display_code} className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50">Create</button>
</div>
}
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Max Trees Override</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Max Sessions/Month Override</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Max Users Override</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white">Note</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-white/10" />
<div className="h-40 animate-pulse rounded-lg bg-accent" />
</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 glass-card rounded-2xl p-6">
<div className="max-w-xl space-y-6 bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-white">Maintenance Mode</h3>
<p className="text-sm text-white/40">
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
<p className="text-sm text-muted-foreground">
When enabled, users will see a maintenance message instead of the app.
</p>
</div>
@@ -57,39 +57,40 @@ export function SettingsPage() {
onClick={() => setSettings({ ...settings, maintenance_mode: !maintenanceMode })}
className={cn(
'h-6 w-10 rounded-full transition-colors',
maintenanceMode ? 'bg-red-400' : 'bg-white/10'
maintenanceMode ? 'bg-red-400' : 'bg-accent'
)}
>
<div className={cn(
'h-4 w-4 rounded-full bg-white transition-transform',
maintenanceMode ? 'translate-x-5' : 'translate-x-1'
)} />
</button>
</div>
{maintenanceMode && (
<div>
<label className="mb-1 block text-sm font-medium text-white">Maintenance Message</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
)}
<div className="border-t border-white/[0.06] pt-4">
<div className="border-t border-border pt-4">
<button
onClick={handleSave}
disabled={saving}
className={cn(
'rounded-md px-4 py-2 text-sm font-medium',
'bg-white text-black hover:bg-white/90',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'disabled:opacity-50'
)}
>

View File

@@ -170,21 +170,21 @@ export function UserDetailPage() {
}
const inputClass = 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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-white/20 border-t-white" />
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-foreground" />
</div>
)
}
if (!user) {
return (
<div className="py-20 text-center text-white/40">User not found</div>
<div className="py-20 text-center text-muted-foreground">User not found</div>
)
}
@@ -197,15 +197,15 @@ export function UserDetailPage() {
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/admin/users')}
className="rounded-md border border-white/10 p-2 text-white/60 hover:bg-white/10 hover:text-white"
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
</button>
<div className="flex-1">
<h1 className="text-xl font-semibold text-white">
<h1 className="text-xl font-semibold text-foreground">
{user.full_name || user.email}
</h1>
<p className="text-sm text-white/40">{user.email}</p>
<p className="text-sm text-muted-foreground">{user.email}</p>
</div>
<div className="flex items-center gap-2">
{user.is_super_admin && (
@@ -225,21 +225,21 @@ export function UserDetailPage() {
{/* Account & Subscription */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="glass-card rounded-2xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-white/40">
<div className="bg-card border border-border rounded-xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Account & Subscription
</h2>
<dl className="space-y-3">
{user.account && (
<>
<div className="flex justify-between">
<dt className="text-sm text-white/60">Account</dt>
<dd className="text-sm text-white">{user.account.name}</dd>
<dt className="text-sm text-muted-foreground">Account</dt>
<dd className="text-sm text-foreground">{user.account.name}</dd>
</div>
{user.account.display_code && (
<div className="flex justify-between">
<dt className="text-sm text-white/60">Display Code</dt>
<dd className="text-sm font-mono text-white/70">{user.account.display_code}</dd>
<dt className="text-sm text-muted-foreground">Display Code</dt>
<dd className="text-sm font-mono text-muted-foreground">{user.account.display_code}</dd>
</div>
)}
</>
@@ -247,13 +247,13 @@ export function UserDetailPage() {
{user.subscription ? (
<>
<div className="flex justify-between">
<dt className="text-sm text-white/60">Plan</dt>
<dd className="text-sm font-semibold text-white">
<dt className="text-sm text-muted-foreground">Plan</dt>
<dd className="text-sm font-semibold text-foreground">
{user.subscription.plan.charAt(0).toUpperCase() + user.subscription.plan.slice(1)}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm text-white/60">Status</dt>
<dt className="text-sm text-muted-foreground">Status</dt>
<dd>
<StatusBadge variant={user.subscription.status === 'trialing' ? 'warning' : 'success'}>
{user.subscription.status}
@@ -262,24 +262,24 @@ export function UserDetailPage() {
</div>
{user.subscription.current_period_end && (
<div className="flex justify-between">
<dt className="text-sm text-white/60">Period End</dt>
<dd className="text-sm text-white/70">{fmt(user.subscription.current_period_end)}</dd>
<dt className="text-sm text-muted-foreground">Period End</dt>
<dd className="text-sm text-muted-foreground">{fmt(user.subscription.current_period_end)}</dd>
</div>
)}
</>
) : (
<div className="text-sm text-white/40">No subscription</div>
<div className="text-sm text-muted-foreground">No subscription</div>
)}
<div className="flex justify-between">
<dt className="text-sm text-white/60">Joined</dt>
<dd className="text-sm text-white/70">{fmt(user.created_at)}</dd>
<dt className="text-sm text-muted-foreground">Joined</dt>
<dd className="text-sm text-muted-foreground">{fmt(user.created_at)}</dd>
</div>
</dl>
</div>
{/* Admin Actions */}
<div className="glass-card rounded-2xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-white/40">
<div className="bg-card border border-border rounded-xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Admin Actions
</h2>
<div className="space-y-3">
@@ -290,16 +290,16 @@ export function UserDetailPage() {
setSelectedPlan(user.subscription?.plan || 'free')
setPlanModalOpen(true)
}}
className="flex w-full items-center gap-3 rounded-lg border border-white/10 px-4 py-3 text-left text-sm text-white/70 hover:bg-white/5 hover:text-white"
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Shield className="h-4 w-4 text-white/40" />
<Shield className="h-4 w-4 text-muted-foreground" />
Change Plan
</button>
<button
onClick={() => setTrialModalOpen(true)}
className="flex w-full items-center gap-3 rounded-lg border border-white/10 px-4 py-3 text-left text-sm text-white/70 hover:bg-white/5 hover:text-white"
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Clock className="h-4 w-4 text-white/40" />
<Clock className="h-4 w-4 text-muted-foreground" />
{user.subscription?.status === 'trialing' ? 'Extend Trial' : 'Start Trial'}
</button>
</>
@@ -310,9 +310,9 @@ export function UserDetailPage() {
setResetTempPassword(null)
setResetModalOpen(true)
}}
className="flex w-full items-center gap-3 rounded-lg border border-white/10 px-4 py-3 text-left text-sm text-white/70 hover:bg-white/5 hover:text-white"
className="flex w-full items-center gap-3 rounded-lg border border-border px-4 py-3 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<KeyRound className="h-4 w-4 text-white/40" />
<KeyRound className="h-4 w-4 text-muted-foreground" />
Reset Password
</button>
<button
@@ -363,42 +363,42 @@ export function UserDetailPage() {
{/* Invite Code Used */}
{user.invite_code_used && (
<div className="glass-card rounded-2xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-white/40">
<div className="bg-card border border-border rounded-xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
<Ticket className="mr-2 inline h-4 w-4" />
Invite Code Used
</h2>
<dl className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div>
<dt className="text-xs text-white/40">Code</dt>
<dd className="mt-1 font-mono text-sm text-white/70">{user.invite_code_used.code}</dd>
<dt className="text-xs text-muted-foreground">Code</dt>
<dd className="mt-1 font-mono text-sm text-muted-foreground">{user.invite_code_used.code}</dd>
</div>
<div>
<dt className="text-xs text-white/40">Plan Assigned</dt>
<dd className="mt-1 text-sm text-white/70">
<dt className="text-xs text-muted-foreground">Plan Assigned</dt>
<dd className="mt-1 text-sm text-muted-foreground">
{user.invite_code_used.assigned_plan.charAt(0).toUpperCase() + user.invite_code_used.assigned_plan.slice(1)}
</dd>
</div>
<div>
<dt className="text-xs text-white/40">Trial Days</dt>
<dd className="mt-1 text-sm text-white/70">{user.invite_code_used.trial_duration_days ?? '—'}</dd>
<dt className="text-xs text-muted-foreground">Trial Days</dt>
<dd className="mt-1 text-sm text-muted-foreground">{user.invite_code_used.trial_duration_days ?? '—'}</dd>
</div>
<div>
<dt className="text-xs text-white/40">Created By</dt>
<dd className="mt-1 text-sm text-white/70">{user.invite_code_used.created_by_email ?? '—'}</dd>
<dt className="text-xs text-muted-foreground">Created By</dt>
<dd className="mt-1 text-sm text-muted-foreground">{user.invite_code_used.created_by_email ?? '—'}</dd>
</div>
</dl>
</div>
)}
{/* Tabs: Sessions / Audit Logs */}
<div className="glass-card rounded-2xl">
<div className="flex border-b border-white/[0.06]">
<div className="bg-card border border-border rounded-xl">
<div className="flex border-b border-border">
<button
onClick={() => setActiveTab('sessions')}
className={cn(
'px-6 py-3 text-sm font-medium',
activeTab === 'sessions' ? 'border-b-2 border-white text-white' : 'text-white/40 hover:text-white/60'
activeTab === 'sessions' ? 'border-b-2 border-foreground text-foreground' : 'text-muted-foreground hover:text-foreground'
)}
>
Sessions ({user.total_sessions})
@@ -407,7 +407,7 @@ export function UserDetailPage() {
onClick={() => setActiveTab('audit')}
className={cn(
'px-6 py-3 text-sm font-medium',
activeTab === 'audit' ? 'border-b-2 border-white text-white' : 'text-white/40 hover:text-white/60'
activeTab === 'audit' ? 'border-b-2 border-foreground text-foreground' : 'text-muted-foreground hover:text-foreground'
)}
>
Audit Logs ({user.total_audit_logs})
@@ -419,7 +419,7 @@ export function UserDetailPage() {
user.recent_sessions.length > 0 ? (
<table className="w-full">
<thead>
<tr className="border-b border-white/[0.06] text-left text-xs text-white/40">
<tr className="border-b border-border text-left text-xs text-muted-foreground">
<th className="pb-2 font-medium">Tree</th>
<th className="pb-2 font-medium">Started</th>
<th className="pb-2 font-medium">Completed</th>
@@ -428,17 +428,17 @@ export function UserDetailPage() {
</thead>
<tbody>
{user.recent_sessions.map(s => (
<tr key={s.id} className="border-b border-white/[0.03]">
<td className="py-3 text-sm text-white/70">{s.tree_name ?? '—'}</td>
<td className="py-3 text-sm text-white/40">{fmtFull(s.started_at)}</td>
<td className="py-3 text-sm text-white/40">{fmtFull(s.completed_at)}</td>
<tr key={s.id} className="border-b border-border">
<td className="py-3 text-sm text-muted-foreground">{s.tree_name ?? '—'}</td>
<td className="py-3 text-sm text-muted-foreground">{fmtFull(s.started_at)}</td>
<td className="py-3 text-sm text-muted-foreground">{fmtFull(s.completed_at)}</td>
<td className="py-3">
{s.outcome ? (
<StatusBadge variant={s.outcome === 'resolved' ? 'success' : 'default'}>
{s.outcome}
</StatusBadge>
) : (
<span className="text-sm text-white/30"></span>
<span className="text-sm text-muted-foreground"></span>
)}
</td>
</tr>
@@ -446,7 +446,7 @@ export function UserDetailPage() {
</tbody>
</table>
) : (
<div className="py-8 text-center text-sm text-white/40">No sessions yet</div>
<div className="py-8 text-center text-sm text-muted-foreground">No sessions yet</div>
)
)}
@@ -454,7 +454,7 @@ export function UserDetailPage() {
user.recent_audit_logs.length > 0 ? (
<table className="w-full">
<thead>
<tr className="border-b border-white/[0.06] text-left text-xs text-white/40">
<tr className="border-b border-border text-left text-xs text-muted-foreground">
<th className="pb-2 font-medium">Action</th>
<th className="pb-2 font-medium">Resource</th>
<th className="pb-2 font-medium">Time</th>
@@ -462,16 +462,16 @@ export function UserDetailPage() {
</thead>
<tbody>
{user.recent_audit_logs.map(a => (
<tr key={a.id} className="border-b border-white/[0.03]">
<td className="py-3 text-sm text-white/70">{a.action}</td>
<td className="py-3 text-sm text-white/40">{a.resource_type ?? '—'}</td>
<td className="py-3 text-sm text-white/40">{fmtFull(a.created_at)}</td>
<tr key={a.id} className="border-b border-border">
<td className="py-3 text-sm text-muted-foreground">{a.action}</td>
<td className="py-3 text-sm text-muted-foreground">{a.resource_type ?? '—'}</td>
<td className="py-3 text-sm text-muted-foreground">{fmtFull(a.created_at)}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="py-8 text-center text-sm text-white/40">No audit logs yet</div>
<div className="py-8 text-center text-sm text-muted-foreground">No audit logs yet</div>
)
)}
</div>
@@ -487,13 +487,13 @@ export function UserDetailPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setPlanModalOpen(false)}
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent"
>
Cancel
</button>
<button
onClick={handleChangePlan}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
Update Plan
</button>
@@ -501,7 +501,7 @@ export function UserDetailPage() {
}
>
<div>
<label className="mb-1 block text-sm font-medium text-white">Plan</label>
<label className="mb-1 block text-sm font-medium text-foreground">Plan</label>
<select
aria-label="Subscription plan"
value={selectedPlan}
@@ -525,14 +525,14 @@ export function UserDetailPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setResetModalOpen(false)}
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent"
>
Cancel
</button>
<button
onClick={handleResetPassword}
disabled={resetLoading}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
{resetLoading ? 'Resetting...' : 'Reset Password'}
</button>
@@ -540,11 +540,11 @@ export function UserDetailPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-white/70">
Choose how to reset the password for <span className="font-medium text-white">{user.full_name || user.email}</span>.
<p className="text-sm text-muted-foreground">
Choose how to reset the password for <span className="font-medium text-foreground">{user.full_name || user.email}</span>.
</p>
<div className="space-y-2">
<label className="flex items-start gap-3 rounded-lg border border-white/10 p-3 cursor-pointer hover:bg-white/5">
<label className="flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer hover:bg-accent">
<input
type="radio"
name="reset-mode"
@@ -554,11 +554,11 @@ export function UserDetailPage() {
className="mt-0.5"
/>
<div>
<div className="text-sm font-medium text-white">Send Reset Email</div>
<div className="text-xs text-white/40">User receives an email with a reset link (30 min expiry)</div>
<div className="text-sm font-medium text-foreground">Send Reset Email</div>
<div className="text-xs text-muted-foreground">User receives an email with a reset link (30 min expiry)</div>
</div>
</label>
<label className="flex items-start gap-3 rounded-lg border border-white/10 p-3 cursor-pointer hover:bg-white/5">
<label className="flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer hover:bg-accent">
<input
type="radio"
name="reset-mode"
@@ -568,8 +568,8 @@ export function UserDetailPage() {
className="mt-0.5"
/>
<div>
<div className="text-sm font-medium text-white">Generate Temp Password</div>
<div className="text-xs text-white/40">A temporary password is generated. You share it manually.</div>
<div className="text-sm font-medium text-foreground">Generate Temp Password</div>
<div className="text-xs text-muted-foreground">A temporary password is generated. You share it manually.</div>
</div>
</label>
</div>
@@ -586,7 +586,7 @@ export function UserDetailPage() {
<div className="flex justify-end">
<button
onClick={() => { setResetTempPassword(null); setResetModalOpen(false) }}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
Done
</button>
@@ -598,18 +598,18 @@ export function UserDetailPage() {
This password will not be shown again. Copy it now.
</div>
<div className="flex items-center gap-2">
<code className="flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white font-mono">
<code className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground font-mono">
{resetTempPassword}
</code>
<button
onClick={handleCopyResetPassword}
className="rounded-md border border-white/10 p-2 text-white/60 hover:bg-white/10 hover:text-white transition-colors"
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
title="Copy password"
>
{resetCopied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-white/40">
<p className="text-xs text-muted-foreground">
The user will be required to change this password on next login.
</p>
</div>
@@ -625,13 +625,13 @@ export function UserDetailPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setTrialModalOpen(false)}
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent"
>
Cancel
</button>
<button
onClick={handleExtendTrial}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
{user.subscription?.status === 'trialing' ? 'Extend' : 'Start Trial'}
</button>
@@ -639,7 +639,7 @@ export function UserDetailPage() {
}
>
<div>
<label className="mb-1 block text-sm font-medium text-white">Days to add</label>
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
<input
type="number"
value={trialDays}
@@ -648,7 +648,7 @@ export function UserDetailPage() {
max={90}
className={inputClass}
/>
<p className="mt-1 text-xs text-white/40">1-90 days. Will convert to trialing status if not already.</p>
<p className="mt-1 text-xs text-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
</div>
</Modal>
@@ -662,14 +662,14 @@ export function UserDetailPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setHardDeleteModalOpen(false)}
className="rounded-md border border-white/10 px-4 py-2 text-sm font-medium text-white/60 hover:bg-white/10"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-accent"
>
Cancel
</button>
{hardDeleteBlockers && Object.keys(hardDeleteBlockers).length === 0 && (
<button
onClick={handleHardDelete}
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-foreground hover:bg-red-700"
>
Delete Permanently
</button>
@@ -683,11 +683,11 @@ export function UserDetailPage() {
<div className="rounded-xl border border-red-400/20 bg-red-400/10 p-3 text-sm text-red-400">
This user cannot be deleted because they have dependencies:
</div>
<ul className="space-y-1 text-sm text-white/70">
<ul className="space-y-1 text-sm text-muted-foreground">
{Object.entries(hardDeleteBlockers).map(([key, count]) => (
<li key={key} className="flex justify-between">
<span>{key.replace(/_/g, ' ')}</span>
<span className="font-mono text-white/40">{count}</span>
<span className="font-mono text-muted-foreground">{count}</span>
</li>
))}
</ul>

View File

@@ -188,8 +188,8 @@ export function UsersPage() {
sortable: true,
render: (u) => (
<div>
<div className="font-medium text-white">{u.name}</div>
<div className="text-xs text-white/40">{u.email}</div>
<div className="font-medium text-foreground">{u.name}</div>
<div className="text-xs text-muted-foreground">{u.email}</div>
</div>
),
},
@@ -224,7 +224,7 @@ export function UsersPage() {
header: 'Joined',
sortable: true,
render: (u) => (
<span className="text-sm text-white/40">
<span className="text-sm text-muted-foreground">
{new Date(u.created_at).toLocaleDateString()}
</span>
),
@@ -268,14 +268,14 @@ export function UsersPage() {
<div className="flex items-center gap-3">
<button
onClick={() => setShowInviteModal(true)}
className="flex items-center gap-2 rounded-lg border border-white/10 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
className="flex items-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-accent transition-colors"
>
<Mail className="h-4 w-4" />
Invite User
</button>
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 rounded-lg bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 transition-colors"
className="flex items-center gap-2 rounded-lg bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 transition-colors"
>
<UserPlus className="h-4 w-4" />
Create User
@@ -290,12 +290,12 @@ export function UsersPage() {
placeholder="Search by name or email..."
className="max-w-sm"
/>
<label className="flex items-center gap-2 text-sm text-white/60">
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<input
type="checkbox"
checked={showArchived}
onChange={(e) => { setShowArchived(e.target.checked); setPage(1) }}
className="rounded border-white/20 bg-black/50"
className="rounded border-border bg-card"
/>
Show archived
</label>
@@ -326,13 +326,13 @@ export function UsersPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setRoleModalUser(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"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground/60 hover:bg-accent hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleRoleChange}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
Save
</button>
@@ -340,15 +340,15 @@ export function UsersPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-white/70">
Changing role for <span className="font-medium text-white">{roleModalUser?.name}</span>
<p className="text-sm text-muted-foreground">
Changing role for <span className="font-medium text-foreground">{roleModalUser?.name}</span>
</p>
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value)}
className={cn(
'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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
<option value="engineer">Engineer</option>
@@ -367,14 +367,14 @@ export function UsersPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setMoveModalUser(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"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground/60 hover:bg-accent hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleMoveAccount}
disabled={!displayCode}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
Move
</button>
@@ -382,19 +382,19 @@ export function UsersPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-white/70">
Moving <span className="font-medium text-white">{moveModalUser?.name}</span> to a new account.
<p className="text-sm text-muted-foreground">
Moving <span className="font-medium text-foreground">{moveModalUser?.name}</span> to a new account.
</p>
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-foreground">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-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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
@@ -411,14 +411,14 @@ export function UsersPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setShowCreateModal(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"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground/60 hover:bg-accent hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleCreateUser}
disabled={createLoading || !createForm.email || !createForm.name}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90 disabled:opacity-50"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
{createLoading ? 'Creating...' : 'Create User'}
</button>
@@ -427,39 +427,39 @@ export function UsersPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Name</label>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input
type="text"
value={createForm.name}
onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))}
placeholder="Full name"
className={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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Email</label>
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
<input
type="email"
value={createForm.email}
onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))}
placeholder="user@example.com"
className={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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Mode</label>
<label className="mb-1 block text-sm font-medium text-foreground">Account Mode</label>
<select
value={createForm.account_mode}
onChange={(e) => setCreateForm(f => ({ ...f, account_mode: e.target.value as 'existing' | 'personal' }))}
className={cn(
'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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
<option value="personal">Personal (new account)</option>
@@ -469,26 +469,26 @@ export function UsersPage() {
{createForm.account_mode === 'existing' && (
<>
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input
type="text"
value={createForm.account_display_code}
onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))}
placeholder="e.g. ABC12345"
className={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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Role</label>
<label className="mb-1 block text-sm font-medium text-foreground">Account Role</label>
<select
value={createForm.account_role}
onChange={(e) => setCreateForm(f => ({ ...f, account_role: e.target.value as 'engineer' | 'viewer' }))}
className={cn(
'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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
<option value="engineer">Engineer</option>
@@ -503,9 +503,9 @@ export function UsersPage() {
id="send-email"
checked={createForm.send_email}
onChange={(e) => setCreateForm(f => ({ ...f, send_email: e.target.checked }))}
className="rounded border-white/20 bg-black/50"
className="rounded border-border bg-card"
/>
<label htmlFor="send-email" className="text-sm text-white/70">
<label htmlFor="send-email" className="text-sm text-muted-foreground">
Send welcome email with temporary password
</label>
</div>
@@ -522,7 +522,7 @@ export function UsersPage() {
<div className="flex justify-end">
<button
onClick={() => setTempPassword(null)}
className="rounded-md bg-white px-4 py-2 text-sm font-medium text-black hover:bg-white/90"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90"
>
Done
</button>
@@ -534,21 +534,21 @@ export function UsersPage() {
This password will not be shown again. Copy it now.
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Temporary Password</label>
<label className="mb-1 block text-sm font-medium text-foreground">Temporary Password</label>
<div className="flex items-center gap-2">
<code className="flex-1 rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white font-mono">
<code className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground font-mono">
{tempPassword}
</code>
<button
onClick={handleCopyPassword}
className="rounded-md border border-white/10 p-2 text-white/60 hover:bg-white/10 hover:text-white transition-colors"
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
title="Copy password"
>
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4" />}
</button>
</div>
</div>
<p className="text-xs text-white/40">
<p className="text-xs text-muted-foreground">
The user will be required to change this password on first login.
</p>
</div>
@@ -564,14 +564,14 @@ export function UsersPage() {
<div className="flex justify-end gap-3">
<button
onClick={() => setShowInviteModal(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"
className="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground/60 hover:bg-accent hover:text-foreground"
>
Cancel
</button>
<button
onClick={handleInviteUser}
disabled={inviteLoading || !inviteForm.email || !inviteForm.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"
className="rounded-md bg-gradient-brand text-white shadow-lg shadow-primary/20 px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-50"
>
{inviteLoading ? 'Sending...' : 'Send Invite'}
</button>
@@ -580,39 +580,39 @@ export function UsersPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-white">Email</label>
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))}
placeholder="user@example.com"
className={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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<input
type="text"
value={inviteForm.account_display_code}
onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))}
placeholder="e.g. ABC12345"
className={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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-white">Role</label>
<label className="mb-1 block text-sm font-medium text-foreground">Role</label>
<select
value={inviteForm.role}
onChange={(e) => setInviteForm(f => ({ ...f, role: e.target.value as 'engineer' | 'viewer' }))}
className={cn(
'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'
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
<option value="engineer">Engineer</option>