refactor: migrate page components to Design System v4

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-22 02:04:16 -04:00
parent fd28921373
commit e4ef904707
58 changed files with 1416 additions and 1416 deletions

View File

@@ -64,7 +64,7 @@ export function AuditLogsPage() {
render: (log) => (
<button
onClick={() => setExpandedId(expandedId === log.id ? null : log.id)}
className="p-1 text-muted-foreground hover:text-foreground"
className="p-1 text-[#848b9b] hover:text-[#e2e5eb]"
>
{expandedId === log.id ? (
<ChevronDown className="h-4 w-4" />
@@ -78,14 +78,14 @@ export function AuditLogsPage() {
key: 'action',
header: 'Action',
render: (log) => (
<span className="text-sm font-medium text-foreground">{log.action}</span>
<span className="text-sm font-medium text-[#e2e5eb]">{log.action}</span>
),
},
{
key: 'resource',
header: 'Resource',
render: (log) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-[#848b9b]">
{log.resource_type}{log.resource_id ? ` (${log.resource_id.slice(0, 8)}...)` : ''}
</span>
),
@@ -94,14 +94,14 @@ export function AuditLogsPage() {
key: 'user',
header: 'User',
render: (log) => (
<span className="text-sm text-muted-foreground">{log.user_email || 'System'}</span>
<span className="text-sm text-[#848b9b]">{log.user_email || 'System'}</span>
),
},
{
key: 'created_at',
header: 'Time',
render: (log) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-[#848b9b]">
{new Date(log.created_at).toLocaleString()}
</span>
),
@@ -117,8 +117,8 @@ export function AuditLogsPage() {
<button
onClick={handleExport}
className={cn(
'flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium',
'text-muted-foreground hover:bg-accent hover:text-foreground'
'flex items-center gap-2 rounded-md border border-[#1e2130] px-4 py-2 text-sm font-medium',
'text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]'
)}
>
<Download className="h-4 w-4" />
@@ -134,8 +134,8 @@ export function AuditLogsPage() {
onChange={(e) => { setActionFilter(e.target.value); setPage(1) }}
placeholder="Filter by action..."
className={cn(
'h-9 rounded-md border border-border bg-card px-3 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
'h-9 rounded-md border border-[#1e2130] bg-[#14161d] px-3 text-sm text-[#e2e5eb]',
'placeholder:text-[#848b9b] focus:outline-hidden 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-border bg-card px-3 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
'h-9 rounded-md border border-[#1e2130] bg-[#14161d] px-3 text-sm text-[#e2e5eb]',
'placeholder:text-[#848b9b] focus:outline-hidden 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-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">
<div className="rounded-md border border-[#1e2130] bg-accent p-4">
<h4 className="mb-2 text-sm font-medium text-[#e2e5eb]">Details</h4>
<pre className="overflow-x-auto rounded bg-[#14161d] p-3 text-xs text-[#848b9b]">
{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="bg-card border border-border rounded-xl p-6">
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{label}</p>
<p className="mt-1 text-3xl font-bold text-foreground">{value}</p>
<p className="text-sm text-[#848b9b]">{label}</p>
<p className="mt-1 text-3xl font-bold text-[#e2e5eb]">{value}</p>
</div>
<div className="rounded-lg bg-accent p-3 text-muted-foreground">{icon}</div>
<div className="rounded-lg bg-accent p-3 text-[#848b9b]">{icon}</div>
</div>
</div>
)
@@ -71,18 +71,18 @@ export function DashboardPage() {
{/* Recent Activity */}
{activity.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-foreground">Recent Activity</h2>
<h2 className="text-lg font-semibold text-[#e2e5eb]">Recent Activity</h2>
<div className="mt-3 space-y-2">
{activity.slice(0, 10).map((entry) => (
<div key={entry.id} className="flex items-center justify-between rounded-md border border-border px-4 py-3 text-sm">
<div key={entry.id} className="flex items-center justify-between rounded-md border border-[#1e2130] px-4 py-3 text-sm">
<div>
<span className="font-medium text-foreground">{entry.action}</span>
<span className="ml-2 text-muted-foreground">{entry.resource_type}</span>
<span className="font-medium text-[#e2e5eb]">{entry.action}</span>
<span className="ml-2 text-[#848b9b]">{entry.resource_type}</span>
{entry.user_email && (
<span className="ml-2 text-muted-foreground">by {entry.user_email}</span>
<span className="ml-2 text-[#848b9b]">by {entry.user_email}</span>
)}
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-[#848b9b]">
{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-foreground">Quick Links</h2>
<h2 className="text-lg font-semibold text-[#e2e5eb]">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 bg-card border border-border rounded-xl p-4',
'text-sm font-medium text-foreground transition-colors hover:bg-accent'
'flex items-center gap-3 bg-[#14161d] border border-[#1e2130] rounded-xl p-4',
'text-sm font-medium text-[#e2e5eb] transition-colors hover:bg-accent'
)}
>
<link.icon className="h-5 w-5 text-muted-foreground" />
<link.icon className="h-5 w-5 text-[#848b9b]" />
{link.label}
</Link>
))}

View File

@@ -95,11 +95,11 @@ export function FeatureFlagsPage() {
const flagColumns: Column<FeatureFlagResponse>[] = [
{ key: 'name', header: 'Name', render: (f) => (
<div>
<div className="font-medium text-foreground">{f.display_name}</div>
<div className="text-xs text-muted-foreground">{f.flag_key}</div>
<div className="font-medium text-[#e2e5eb]">{f.display_name}</div>
<div className="text-xs text-[#848b9b]">{f.flag_key}</div>
</div>
)},
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-muted-foreground">{f.description || '-'}</span> },
{ key: 'description', header: 'Description', render: (f) => <span className="text-sm text-[#848b9b]">{f.description || '-'}</span> },
...PLANS.map(plan => ({
key: plan,
header: plan.charAt(0).toUpperCase() + plan.slice(1),
@@ -133,10 +133,10 @@ export function FeatureFlagsPage() {
]
const overrideColumns: Column<AccountFeatureOverrideResponse>[] = [
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-muted-foreground">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-[#e2e5eb]">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'flag', header: 'Flag', render: (o) => <span className="text-sm text-[#848b9b]">{o.flag_display_name || o.flag_key || o.flag_id.slice(0, 8)}</span> },
{ key: 'enabled', header: 'Enabled', render: (o) => <StatusBadge variant={o.enabled ? 'success' : 'destructive'}>{o.enabled ? 'Yes' : 'No'}</StatusBadge> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-[#848b9b]">{o.note || '-'}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (o) => (
@@ -147,7 +147,7 @@ export function FeatureFlagsPage() {
},
]
const selectClass = cn('w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
const selectClass = cn('w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]', 'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
return (
<div className="space-y-8">
@@ -163,7 +163,7 @@ export function FeatureFlagsPage() {
/>
<div>
<h2 className="text-lg font-semibold text-foreground">Feature Matrix</h2>
<h2 className="text-lg font-semibold text-[#e2e5eb]">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." />}
@@ -173,7 +173,7 @@ export function FeatureFlagsPage() {
<div>
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-foreground">Account Overrides</h2>
<h2 className="text-lg font-semibold text-[#e2e5eb]">Account Overrides</h2>
<Button onClick={() => setOverrideOpen(true)}>
<Plus className="h-4 w-4" />
Add Override
@@ -197,15 +197,15 @@ export function FeatureFlagsPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Flag Key</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Flag Key</label>
<Input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Display Name</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Display Name</label>
<Input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Description</label>
<Input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" />
</div>
</div>
@@ -222,22 +222,22 @@ export function FeatureFlagsPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Feature Flag</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Feature Flag</label>
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={selectClass}>
<option value="">Select a flag...</option>
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
</select>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-border" />
<label htmlFor="override-enabled" className="text-sm font-medium text-foreground">Enabled</label>
<input type="checkbox" id="override-enabled" checked={overrideForm.enabled} onChange={(e) => setOverrideForm({ ...overrideForm, enabled: e.target.checked })} className="h-4 w-4 rounded border-[#1e2130]" />
<label htmlFor="override-enabled" className="text-sm font-medium text-[#e2e5eb]">Enabled</label>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Note</label>
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" />
</div>
</div>

View File

@@ -89,7 +89,7 @@ function SortOrderInput({
}
}}
className={cn(
'w-20 rounded-[8px] border border-border bg-card px-2 py-1 text-sm text-foreground',
'w-20 rounded-[8px] border border-[#1e2130] bg-[#14161d] px-2 py-1 text-sm text-[#e2e5eb]',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none',
disabled && 'cursor-not-allowed opacity-50',
)}
@@ -117,19 +117,19 @@ function FilterBar({
return (
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[180px] max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[#848b9b] pointer-events-none" />
<input
type="text"
placeholder="Search by name…"
value={search}
onChange={e => onSearchChange(e.target.value)}
className={cn(
'w-full rounded-[10px] border border-border bg-card pl-9 pr-3 py-2 text-sm text-foreground placeholder:text-muted-foreground',
'w-full rounded-lg border border-[#1e2130] bg-[#14161d] pl-9 pr-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b]',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-none',
)}
/>
</div>
<div className="flex rounded-[10px] border border-border overflow-hidden text-sm">
<div className="flex rounded-lg border border-[#1e2130] overflow-hidden text-sm">
{(['all', 'featured', 'unfeatured'] as FilterMode[]).map(mode => (
<button
key={mode}
@@ -137,8 +137,8 @@ function FilterBar({
className={cn(
'px-3 py-1.5 capitalize transition-colors',
filter === mode
? 'bg-primary text-[#101114] font-semibold'
: 'text-muted-foreground hover:text-foreground bg-card',
? 'bg-primary text-white font-semibold'
: 'text-[#848b9b] hover:text-[#e2e5eb] bg-[#14161d]',
)}
>
{mode}
@@ -166,7 +166,7 @@ function FlowsTable({
}) {
if (flows.length === 0) {
return (
<p className="py-8 text-center text-sm text-muted-foreground">No flows match the current filter.</p>
<p className="py-8 text-center text-sm text-[#848b9b]">No flows match the current filter.</p>
)
}
@@ -174,19 +174,19 @@ function FlowsTable({
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-left">
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Name</th>
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Type</th>
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Featured</th>
<th className="pb-3 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Sort Order</th>
<tr className="border-b border-[#1e2130] text-left">
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Name</th>
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Type</th>
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Featured</th>
<th className="pb-3 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Sort Order</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{flows.map(flow => (
<tr key={flow.id} className="group hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="py-3 pr-4 text-foreground font-medium">{flow.name}</td>
<td className="py-3 pr-4 text-[#e2e5eb] font-medium">{flow.name}</td>
<td className="py-3 pr-4">
<span className="font-label text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border border-border text-muted-foreground">
<span className="font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border border-[#1e2130] text-[#848b9b]">
{flow.tree_type}
</span>
</td>
@@ -234,7 +234,7 @@ function ScriptsTable({
}) {
if (scripts.length === 0) {
return (
<p className="py-8 text-center text-sm text-muted-foreground">No scripts match the current filter.</p>
<p className="py-8 text-center text-sm text-[#848b9b]">No scripts match the current filter.</p>
)
}
@@ -242,24 +242,24 @@ function ScriptsTable({
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border text-left">
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Name</th>
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Status</th>
<th className="pb-3 pr-4 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Featured</th>
<th className="pb-3 font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">Sort Order</th>
<tr className="border-b border-[#1e2130] text-left">
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Name</th>
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Status</th>
<th className="pb-3 pr-4 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Featured</th>
<th className="pb-3 font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] text-[#848b9b]">Sort Order</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{scripts.map(script => (
<tr key={script.id} className="group hover:bg-[rgba(255,255,255,0.02)] transition-colors">
<td className="py-3 pr-4 text-foreground font-medium">{script.name}</td>
<td className="py-3 pr-4 text-[#e2e5eb] font-medium">{script.name}</td>
<td className="py-3 pr-4">
<span
className={cn(
'font-label text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border',
'font-sans text-xs text-[0.625rem] uppercase tracking-[0.1em] rounded-full px-2 py-0.5 border',
script.is_active
? 'border-emerald-400/30 text-emerald-400 bg-emerald-400/10'
: 'border-border text-muted-foreground',
: 'border-[#1e2130] text-[#848b9b]',
)}
>
{script.is_active ? 'Active' : 'Inactive'}
@@ -417,23 +417,23 @@ export default function GalleryManagementPage() {
/>
{loading ? (
<div className="flex items-center justify-center py-16 text-muted-foreground text-sm">
<div className="flex items-center justify-center py-16 text-[#848b9b] text-sm">
Loading gallery items
</div>
) : (
<div className="space-y-8">
{/* Summary stats */}
<div className="flex gap-4 flex-wrap">
<div className="glass-card-static rounded-[12px] px-4 py-3 flex items-center gap-3">
<div className="card-flat rounded-[12px] px-4 py-3 flex items-center gap-3">
<Star className="h-4 w-4 text-amber-400 fill-amber-400" />
<span className="text-sm text-muted-foreground">
<span className="text-foreground font-semibold">{featuredFlowCount}</span> featured flow{featuredFlowCount !== 1 ? 's' : ''}
<span className="text-sm text-[#848b9b]">
<span className="text-[#e2e5eb] font-semibold">{featuredFlowCount}</span> featured flow{featuredFlowCount !== 1 ? 's' : ''}
</span>
</div>
<div className="glass-card-static rounded-[12px] px-4 py-3 flex items-center gap-3">
<div className="card-flat rounded-[12px] px-4 py-3 flex items-center gap-3">
<Star className="h-4 w-4 text-amber-400 fill-amber-400" />
<span className="text-sm text-muted-foreground">
<span className="text-foreground font-semibold">{featuredScriptCount}</span> featured script{featuredScriptCount !== 1 ? 's' : ''}
<span className="text-sm text-[#848b9b]">
<span className="text-[#e2e5eb] font-semibold">{featuredScriptCount}</span> featured script{featuredScriptCount !== 1 ? 's' : ''}
</span>
</div>
</div>
@@ -442,14 +442,14 @@ export default function GalleryManagementPage() {
<section>
<div className="mb-4 flex items-center justify-between gap-4 flex-wrap">
<div>
<h2 className="text-base font-semibold text-foreground">Featured Flows</h2>
<p className="text-xs text-muted-foreground mt-0.5">
<h2 className="text-base font-semibold text-[#e2e5eb]">Featured Flows</h2>
<p className="text-xs text-[#848b9b] mt-0.5">
Toggle flows to show in the gallery. Lower sort order = shown first.
</p>
</div>
</div>
<div className="glass-card-static rounded-[16px] p-5 space-y-4">
<div className="card-flat rounded-lg p-5 space-y-4">
<FilterBar
search={flowSearch}
onSearchChange={setFlowSearch}
@@ -469,14 +469,14 @@ export default function GalleryManagementPage() {
<section>
<div className="mb-4 flex items-center justify-between gap-4 flex-wrap">
<div>
<h2 className="text-base font-semibold text-foreground">Featured Scripts</h2>
<p className="text-xs text-muted-foreground mt-0.5">
<h2 className="text-base font-semibold text-[#e2e5eb]">Featured Scripts</h2>
<p className="text-xs text-[#848b9b] mt-0.5">
Toggle scripts to show in the gallery. Lower sort order = shown first.
</p>
</div>
</div>
<div className="glass-card-static rounded-[16px] p-5 space-y-4">
<div className="card-flat rounded-lg p-5 space-y-4">
<FilterBar
search={scriptSearch}
onSearchChange={setScriptSearch}

View File

@@ -73,10 +73,10 @@ export function GlobalCategoriesPage() {
}
const columns: Column<AdminCategory>[] = [
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-foreground">{c.name}</span> },
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-muted-foreground">{c.slug}</span> },
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-muted-foreground">{c.description || '-'}</span> },
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-muted-foreground">{c.tree_count}</span> },
{ key: 'name', header: 'Name', render: (c) => <span className="font-medium text-[#e2e5eb]">{c.name}</span> },
{ key: 'slug', header: 'Slug', render: (c) => <span className="text-sm text-[#848b9b]">{c.slug}</span> },
{ key: 'description', header: 'Description', render: (c) => <span className="text-sm text-[#848b9b]">{c.description || '-'}</span> },
{ key: 'tree_count', header: 'Trees', render: (c) => <span className="text-sm text-[#848b9b]">{c.tree_count}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (c) => (
@@ -124,15 +124,15 @@ export function GlobalCategoriesPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Slug</label>
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Description</label>
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
</div>
</div>
@@ -153,15 +153,15 @@ export function GlobalCategoriesPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Name</label>
<Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Slug</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Slug</label>
<Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Description</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Description</label>
<Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
</div>
</div>

View File

@@ -110,8 +110,8 @@ export function InviteCodesPage() {
}
const selectClass = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)
const columns: Column<InviteCodeResponse>[] = [
@@ -119,7 +119,7 @@ export function InviteCodesPage() {
key: 'code',
header: 'Code',
render: (c) => (
<code className="rounded bg-accent px-2 py-1 text-sm font-mono text-muted-foreground">{c.code}</code>
<code className="rounded bg-accent px-2 py-1 text-sm font-mono text-[#848b9b]">{c.code}</code>
),
},
{
@@ -130,12 +130,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-muted-foreground" />
<Mail className="h-3.5 w-3.5 text-[#848b9b]" />
)}
<span className="text-sm text-muted-foreground">{c.email}</span>
<span className="text-sm text-[#848b9b]">{c.email}</span>
</div>
) : (
<span className="text-sm text-muted-foreground">&mdash;</span>
<span className="text-sm text-[#848b9b]">&mdash;</span>
),
},
{
@@ -151,9 +151,9 @@ export function InviteCodesPage() {
key: 'trial',
header: 'Trial',
render: (c) => c.has_trial ? (
<span className="text-sm text-muted-foreground">{c.trial_duration_days}d</span>
<span className="text-sm text-[#848b9b]">{c.trial_duration_days}d</span>
) : (
<span className="text-sm text-muted-foreground">&mdash;</span>
<span className="text-sm text-[#848b9b]">&mdash;</span>
),
},
{
@@ -170,7 +170,7 @@ export function InviteCodesPage() {
key: 'expires_at',
header: 'Expires',
render: (c) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-[#848b9b]">
{c.expires_at ? new Date(c.expires_at).toLocaleDateString() : 'Never'}
</span>
),
@@ -179,7 +179,7 @@ export function InviteCodesPage() {
key: 'created_at',
header: 'Created',
render: (c) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-[#848b9b]">
{new Date(c.created_at).toLocaleDateString()}
</span>
),
@@ -254,7 +254,7 @@ export function InviteCodesPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Recipient Email</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Recipient Email</label>
<Input
type="email"
value={email}
@@ -264,7 +264,7 @@ export function InviteCodesPage() {
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Plan</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Plan</label>
<select
aria-label="Plan"
value={assignedPlan}
@@ -283,7 +283,7 @@ export function InviteCodesPage() {
{assignedPlan !== 'free' && (
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Trial Duration (days)</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Trial Duration (days)</label>
<Input
type="number"
value={trialDays}
@@ -292,12 +292,12 @@ export function InviteCodesPage() {
min={1}
max={90}
/>
<p className="mt-1 text-xs text-muted-foreground">Leave empty for no trial account gets full plan immediately.</p>
<p className="mt-1 text-xs text-[#848b9b]">Leave empty for no trial account gets full plan immediately.</p>
</div>
)}
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Expires in (days)</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Expires in (days)</label>
<Input
type="number"
value={expiresInDays}
@@ -307,7 +307,7 @@ export function InviteCodesPage() {
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Note</label>
<Input
type="text"
value={note}

View File

@@ -76,16 +76,16 @@ export function PlanLimitsPage() {
}
const planColumns: Column<PlanLimitConfig>[] = [
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-foreground capitalize">{p.plan}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-muted-foreground">{p.max_trees ?? 'Unlimited'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-muted-foreground">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-muted-foreground">{p.max_users ?? 'Unlimited'}</span> },
{ key: 'plan', header: 'Plan', render: (p) => <span className="font-medium text-[#e2e5eb] capitalize">{p.plan}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (p) => <span className="text-sm text-[#848b9b]">{p.max_trees ?? 'Unlimited'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (p) => <span className="text-sm text-[#848b9b]">{p.max_sessions_per_month ?? 'Unlimited'}</span> },
{ key: 'max_users', header: 'Max Users', render: (p) => <span className="text-sm text-[#848b9b]">{p.max_users ?? 'Unlimited'}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (p) => (
<button
onClick={() => setEditPlan({ ...p })}
className="rounded-md px-3 py-1 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
className="rounded-md px-3 py-1 text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
>
Edit
</button>
@@ -94,11 +94,11 @@ export function PlanLimitsPage() {
]
const overrideColumns: Column<AccountOverrideResponse>[] = [
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-foreground">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_trees ?? '-'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_sessions_per_month ?? '-'}</span> },
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-muted-foreground">{o.override_max_users ?? '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-muted-foreground">{o.note || '-'}</span> },
{ key: 'account', header: 'Account', render: (o) => <span className="text-sm font-medium text-[#e2e5eb]">{o.account_display_code || o.account_id.slice(0, 8)}</span> },
{ key: 'max_trees', header: 'Max Trees', render: (o) => <span className="text-sm text-[#848b9b]">{o.override_max_trees ?? '-'}</span> },
{ key: 'max_sessions', header: 'Sessions/Month', render: (o) => <span className="text-sm text-[#848b9b]">{o.override_max_sessions_per_month ?? '-'}</span> },
{ key: 'max_users', header: 'Max Users', render: (o) => <span className="text-sm text-[#848b9b]">{o.override_max_users ?? '-'}</span> },
{ key: 'note', header: 'Note', render: (o) => <span className="text-sm text-[#848b9b]">{o.note || '-'}</span> },
{
key: 'actions', header: '', className: 'w-12',
render: (o) => (
@@ -114,7 +114,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-foreground">Plan Defaults</h2>
<h2 className="text-lg font-semibold text-[#e2e5eb]">Plan Defaults</h2>
<div className="mt-3">
<DataTable columns={planColumns} data={plans} keyExtractor={(p) => p.plan} isLoading={loading} />
</div>
@@ -122,7 +122,7 @@ export function PlanLimitsPage() {
<div>
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-foreground">Account Overrides</h2>
<h2 className="text-lg font-semibold text-[#e2e5eb]">Account Overrides</h2>
<Button onClick={() => setCreateOverride(true)}>
<Plus className="h-4 w-4" />
Add Override
@@ -155,15 +155,15 @@ export function PlanLimitsPage() {
{editPlan && (
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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 })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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 })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Users (empty = unlimited)</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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 })} />
</div>
</div>
@@ -185,23 +185,23 @@ export function PlanLimitsPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Trees Override</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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 })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Sessions/Month Override</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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 })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Max Users Override</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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 })} />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Note</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Note</label>
<Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" />
</div>
</div>

View File

@@ -47,11 +47,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 bg-card border border-border rounded-xl p-6">
<div className="max-w-xl space-y-6 bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-foreground">Email Verification</h3>
<p className="text-sm text-muted-foreground">
<h3 className="font-medium text-[#e2e5eb]">Email Verification</h3>
<p className="text-sm text-[#848b9b]">
When enabled, unverified users see a banner prompting them to verify their email.
</p>
</div>
@@ -59,7 +59,7 @@ export function SettingsPage() {
onClick={() => setSettings({ ...settings, email_verification_enabled: !emailVerificationEnabled })}
className={cn(
'h-6 w-10 rounded-full transition-colors',
emailVerificationEnabled ? 'bg-gradient-brand' : 'bg-accent'
emailVerificationEnabled ? 'bg-[#22d3ee]' : 'bg-accent'
)}
>
<div className={cn(
@@ -71,8 +71,8 @@ export function SettingsPage() {
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
<p className="text-sm text-muted-foreground">
<h3 className="font-medium text-[#e2e5eb]">Maintenance Mode</h3>
<p className="text-sm text-[#848b9b]">
When enabled, users will see a maintenance message instead of the app.
</p>
</div>
@@ -93,7 +93,7 @@ export function SettingsPage() {
{maintenanceMode && (
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Maintenance Message</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Maintenance Message</label>
<Textarea
value={maintenanceMessage}
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
@@ -103,13 +103,13 @@ export function SettingsPage() {
</div>
)}
<div className="border-t border-border pt-4">
<div className="border-t border-[#1e2130] pt-4">
<button
onClick={handleSave}
disabled={saving}
className={cn(
'rounded-md px-4 py-2 text-sm font-medium',
'bg-gradient-brand text-white shadow-lg shadow-primary/20 hover:opacity-90',
'bg-[#22d3ee] text-white hover:brightness-110',
'disabled:opacity-50'
)}
>

View File

@@ -70,11 +70,11 @@ export default function SurveyInvitesPage() {
/>
{/* Create Invite Section */}
<div className="glass-card-static p-6">
<h3 className="font-heading text-sm font-semibold text-foreground mb-4">Create Invite</h3>
<div className="card-flat p-6">
<h3 className="font-heading text-sm font-semibold text-[#e2e5eb] mb-4">Create Invite</h3>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="flex-1">
<label className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1.5 block">
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1.5 block">
Recipient Name
</label>
<input
@@ -82,11 +82,11 @@ export default function SurveyInvitesPage() {
value={name}
onChange={e => setName(e.target.value)}
placeholder="John Smith"
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden"
className="w-full rounded-lg border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b] focus:border-primary/30 focus:outline-hidden"
/>
</div>
<div className="flex-1">
<label className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1.5 block">
<label className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1.5 block">
Email (optional)
</label>
<input
@@ -94,14 +94,14 @@ export default function SurveyInvitesPage() {
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="john@example.com"
className="w-full rounded-[10px] border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden"
className="w-full rounded-lg border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] placeholder:text-[#848b9b] focus:border-primary/30 focus:outline-hidden"
/>
</div>
<div className="flex gap-2">
<button
onClick={() => handleCreate(false)}
disabled={creating || !name.trim()}
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-4 py-2 text-sm font-semibold text-brand-dark shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className="inline-flex items-center gap-2 rounded-lg bg-[#22d3ee] px-4 py-2 text-sm font-semibold text-brand-dark hover:brightness-110 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Link2 className="h-4 w-4" />}
Generate Link
@@ -109,7 +109,7 @@ export default function SurveyInvitesPage() {
<button
onClick={() => handleCreate(true)}
disabled={creating || !name.trim() || !email.trim()}
className="inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-foreground hover:border-white/[0.12] active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
className="inline-flex items-center gap-2 rounded-lg bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-[#e2e5eb] hover:border-white/[0.12] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
Send Email
@@ -122,17 +122,17 @@ export default function SurveyInvitesPage() {
)}
{lastCreated && (
<div className="mt-4 rounded-[10px] border border-primary/[0.15] bg-primary/[0.04] p-4">
<div className="mt-4 rounded-lg border border-primary/[0.15] bg-primary/[0.04] p-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-xs text-muted-foreground mb-1">
<p className="text-xs text-[#848b9b] mb-1">
{lastCreated.email_sent ? 'Email sent to ' + lastCreated.recipient_email + '! Link:' : 'Share this link with ' + lastCreated.recipient_name + ':'}
</p>
<p className="truncate text-sm font-mono text-foreground">{lastCreated.survey_url}</p>
<p className="truncate text-sm font-mono text-[#e2e5eb]">{lastCreated.survey_url}</p>
</div>
<button
onClick={() => handleCopy(lastCreated.survey_url)}
className="shrink-0 rounded-lg p-2 text-muted-foreground hover:bg-brand-border hover:text-foreground transition-colors"
className="shrink-0 rounded-lg p-2 text-[#848b9b] hover:bg-brand-border hover:text-[#e2e5eb] transition-colors"
>
{copied ? <Check className="h-4 w-4 text-emerald-400" /> : <Copy className="h-4 w-4" />}
</button>
@@ -142,33 +142,33 @@ export default function SurveyInvitesPage() {
</div>
{/* Invites Table */}
<div className="glass-card-static overflow-hidden">
<div className="card-flat overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border">
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Name</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Email</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Status</th>
<th className="px-4 py-3 text-center font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Sent</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Created</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Completed</th>
<th className="px-4 py-3 text-center font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">Link</th>
<tr className="border-b border-[#1e2130]">
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Name</th>
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Email</th>
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Status</th>
<th className="px-4 py-3 text-center font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Sent</th>
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Created</th>
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Completed</th>
<th className="px-4 py-3 text-center font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">Link</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-muted-foreground">Loading...</td></tr>
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-[#848b9b]">Loading...</td></tr>
) : invites.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-muted-foreground">No invites yet</td></tr>
<tr><td colSpan={7} className="px-4 py-8 text-center text-sm text-[#848b9b]">No invites yet</td></tr>
) : (
invites.map(invite => (
<tr key={invite.id} className="border-b border-border/50 hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3 text-sm text-foreground">{invite.recipient_name}</td>
<td className="px-4 py-3 text-sm text-muted-foreground">{invite.recipient_email || '—'}</td>
<tr key={invite.id} className="border-b border-[#1e2130]/50 hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3 text-sm text-[#e2e5eb]">{invite.recipient_name}</td>
<td className="px-4 py-3 text-sm text-[#848b9b]">{invite.recipient_email || '—'}</td>
<td className="px-4 py-3">
<span className={cn(
'inline-flex items-center rounded-full px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wider',
'inline-flex items-center rounded-full px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider',
invite.status === 'completed'
? 'bg-emerald-400/10 text-emerald-400'
: 'bg-amber-400/10 text-amber-400'
@@ -178,17 +178,17 @@ export default function SurveyInvitesPage() {
</td>
<td className="px-4 py-3 text-center">
{invite.email_sent ? (
<Mail className="mx-auto h-4 w-4 text-muted-foreground" />
<Mail className="mx-auto h-4 w-4 text-[#848b9b]" />
) : (
<span className="text-muted-foreground/50"></span>
<span className="text-[#848b9b]/50"></span>
)}
</td>
<td className="px-4 py-3 font-label text-xs text-muted-foreground">{formatDate(invite.created_at)}</td>
<td className="px-4 py-3 font-label text-xs text-muted-foreground">{invite.completed_at ? formatDate(invite.completed_at) : '—'}</td>
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]">{formatDate(invite.created_at)}</td>
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]">{invite.completed_at ? formatDate(invite.completed_at) : '—'}</td>
<td className="px-4 py-3 text-center">
<button
onClick={() => handleCopy(invite.survey_url)}
className="rounded-lg p-1.5 text-muted-foreground hover:bg-brand-border hover:text-foreground transition-colors"
className="rounded-lg p-1.5 text-[#848b9b] hover:bg-brand-border hover:text-[#e2e5eb] transition-colors"
title="Copy survey link"
>
<Copy className="h-3.5 w-3.5" />

View File

@@ -41,7 +41,7 @@ const QUESTIONS: { id: string; num: string; text: string; type: 'mc' | 'mc-multi
function AnswerDisplay({ value, type }: { value: string | string[] | undefined; type: string }) {
if (!value || (Array.isArray(value) && value.length === 0)) {
return <p className="text-sm italic text-muted-foreground/60">No answer</p>
return <p className="text-sm italic text-[#848b9b]/60">No answer</p>
}
if (type === 'mc-multi' && Array.isArray(value)) {
@@ -50,7 +50,7 @@ function AnswerDisplay({ value, type }: { value: string | string[] | undefined;
{value.map((v, i) => (
<span
key={i}
className="inline-block rounded-full bg-primary/10 px-2.5 py-0.5 font-label text-[0.625rem] uppercase tracking-wider text-primary"
className="inline-block rounded-full bg-[rgba(34,211,238,0.10)] px-2.5 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider text-[#22d3ee]"
>
{v}
</span>
@@ -63,8 +63,8 @@ function AnswerDisplay({ value, type }: { value: string | string[] | undefined;
return (
<ol className="space-y-1">
{value.map((v, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-foreground/90">
<span className="font-label text-xs font-bold text-primary">{i + 1}.</span>
<li key={i} className="flex items-start gap-2 text-sm text-[#e2e5eb]/90">
<span className="font-sans text-xs text-xs font-bold text-[#22d3ee]">{i + 1}.</span>
{v}
</li>
))}
@@ -75,12 +75,12 @@ function AnswerDisplay({ value, type }: { value: string | string[] | undefined;
if (type === 'text') {
return (
<div className="border-l-2 border-primary/30 pl-3">
<p className="text-sm text-foreground/90 whitespace-pre-wrap">{String(value)}</p>
<p className="text-sm text-[#e2e5eb]/90 whitespace-pre-wrap">{String(value)}</p>
</div>
)
}
return <p className="text-sm text-foreground/90">{String(value)}</p>
return <p className="text-sm text-[#e2e5eb]/90">{String(value)}</p>
}
function ExpandedDetail({ response }: { response: SurveyResponseDetail }) {
@@ -98,16 +98,16 @@ function ExpandedDetail({ response }: { response: SurveyResponseDetail }) {
{QUESTIONS.map((q) => (
<div
key={q.id}
className="rounded-[10px] p-4"
className="rounded-lg p-4"
style={{
background: 'rgba(24, 26, 31, 0.6)',
background: '#14161d',
border: '1px solid var(--glass-border)',
}}
>
<p className="font-label text-[0.625rem] uppercase tracking-widest text-primary mb-1">
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#22d3ee] mb-1">
Q{q.num}
</p>
<p className="text-xs text-muted-foreground mb-2">{q.text}</p>
<p className="text-xs text-[#848b9b] mb-2">{q.text}</p>
<AnswerDisplay value={response.responses[q.id]} type={q.type} />
</div>
))}
@@ -150,7 +150,7 @@ function ResponseRow({
<>
<tr
className={cn(
'border-b border-border/50 transition-colors cursor-pointer',
'border-b border-[#1e2130]/50 transition-colors cursor-pointer',
!response.is_read && 'bg-primary/3',
'hover:bg-white/[0.02]'
)}
@@ -158,16 +158,16 @@ function ResponseRow({
{/* Checkbox */}
<td className="px-2 py-3 w-8" onClick={e => { e.stopPropagation(); onSelect() }}>
{isSelected ? (
<CheckSquare className="h-4 w-4 text-primary cursor-pointer" />
<CheckSquare className="h-4 w-4 text-[#22d3ee] cursor-pointer" />
) : (
<Square className="h-4 w-4 text-muted-foreground/40 cursor-pointer hover:text-muted-foreground" />
<Square className="h-4 w-4 text-[#848b9b]/40 cursor-pointer hover:text-[#848b9b]" />
)}
</td>
{/* Unread dot */}
<td className="px-1 py-3 w-6" onClick={onToggle}>
{!response.is_read && (
<Circle className="h-2.5 w-2.5 fill-primary text-primary" />
<Circle className="h-2.5 w-2.5 fill-primary text-[#22d3ee]" />
)}
</td>
@@ -175,46 +175,46 @@ function ResponseRow({
<td className="px-2 py-3 w-8" onClick={onToggle}>
<ChevronDown
className={cn(
'h-4 w-4 text-muted-foreground transition-transform',
'h-4 w-4 text-[#848b9b] transition-transform',
isExpanded && 'rotate-180'
)}
/>
</td>
<td className="px-4 py-3 font-label text-xs text-muted-foreground" onClick={onToggle}>{index + 1}</td>
<td className={cn('px-4 py-3 text-sm', !response.is_read ? 'text-foreground font-medium' : 'text-foreground')} onClick={onToggle}>
{response.respondent_name || <span className="text-muted-foreground italic">Anonymous</span>}
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]" onClick={onToggle}>{index + 1}</td>
<td className={cn('px-4 py-3 text-sm', !response.is_read ? 'text-[#e2e5eb] font-medium' : 'text-[#e2e5eb]')} onClick={onToggle}>
{response.respondent_name || <span className="text-[#848b9b] italic">Anonymous</span>}
</td>
<td className="px-4 py-3" onClick={onToggle}>
{response.source === 'invite' ? (
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wider bg-primary/10 text-primary">
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider bg-[rgba(34,211,238,0.10)] text-[#22d3ee]">
<User className="h-3 w-3" />
Invite
{response.invite_name && (
<span className="text-primary/70">({response.invite_name})</span>
<span className="text-[#22d3ee]/70">({response.invite_name})</span>
)}
</span>
) : (
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-label text-[0.625rem] uppercase tracking-wider bg-brand-border text-muted-foreground">
<span className="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-sans text-xs text-[0.625rem] uppercase tracking-wider bg-brand-border text-[#848b9b]">
<Link2 className="h-3 w-3" />
Direct
</span>
)}
</td>
<td className="px-4 py-3 font-label text-xs text-muted-foreground" onClick={onToggle}>
<td className="px-4 py-3 font-sans text-xs text-xs text-[#848b9b]" onClick={onToggle}>
{new Date(response.created_at).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</td>
<td className="px-4 py-3 text-sm text-muted-foreground" onClick={onToggle}>
<td className="px-4 py-3 text-sm text-[#848b9b]" onClick={onToggle}>
{answeredCount} / {QUESTIONS.length}
</td>
{/* Actions */}
<td className="px-3 py-3 w-10 relative">
<button
onClick={e => { e.stopPropagation(); setShowMenu(!showMenu) }}
className="p-1.5 rounded-lg hover:bg-brand-border text-muted-foreground hover:text-foreground transition-colors"
className="p-1.5 rounded-lg hover:bg-brand-border text-[#848b9b] hover:text-[#e2e5eb] transition-colors"
>
<MoreHorizontal className="h-4 w-4" />
</button>
@@ -223,18 +223,18 @@ function ResponseRow({
<div className="fixed inset-0 z-40" onClick={() => setShowMenu(false)} />
<div
className="absolute right-3 top-full z-50 mt-1 w-44 rounded-xl py-1 shadow-xl"
style={{ background: 'rgba(24, 26, 31, 0.95)', border: '1px solid var(--glass-border)', backdropFilter: 'blur(16px)' }}
style={{ background: '#14161d', border: '1px solid var(--glass-border)', }}
>
<button
onClick={() => { onMarkRead(); setShowMenu(false) }}
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/[0.04] transition-colors"
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-[#848b9b] hover:text-[#e2e5eb] hover:bg-white/[0.04] transition-colors"
>
{response.is_read ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
{response.is_read ? 'Mark Unread' : 'Mark Read'}
</button>
<button
onClick={() => { onArchive(); setShowMenu(false) }}
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/[0.04] transition-colors"
className="flex w-full items-center gap-2.5 px-3 py-2 text-xs text-[#848b9b] hover:text-[#e2e5eb] hover:bg-white/[0.04] transition-colors"
>
{response.archived_at ? <ArchiveRestore className="h-3.5 w-3.5" /> : <Archive className="h-3.5 w-3.5" />}
{response.archived_at ? 'Unarchive' : 'Archive'}
@@ -408,7 +408,7 @@ export default function SurveyResponsesPage() {
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<Loader2 className="h-6 w-6 animate-spin text-[#22d3ee]" />
</div>
)
}
@@ -416,7 +416,7 @@ export default function SurveyResponsesPage() {
if (error && !data) {
return (
<div className="px-6 py-8">
<div className="glass-card-static p-6 text-center text-rose-400">{error}</div>
<div className="card-flat p-6 text-center text-rose-400">{error}</div>
</div>
)
}
@@ -434,10 +434,10 @@ export default function SurveyResponsesPage() {
<button
onClick={() => setShowArchived(!showArchived)}
className={cn(
'inline-flex items-center gap-2 rounded-[10px] px-3 py-2 text-xs font-medium transition-colors border',
'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-xs font-medium transition-colors border',
showArchived
? 'bg-primary/10 text-primary border-primary/20'
: 'bg-white/[0.04] text-muted-foreground border-brand-border hover:border-white/[0.12]'
? 'bg-[rgba(34,211,238,0.10)] text-[#22d3ee] border-primary/20'
: 'bg-white/[0.04] text-[#848b9b] border-brand-border hover:border-white/[0.12]'
)}
>
<Archive className="h-3.5 w-3.5" />
@@ -446,7 +446,7 @@ export default function SurveyResponsesPage() {
<button
onClick={handleExport}
disabled={exporting || responses.length === 0}
className="inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-white/[0.12] disabled:opacity-50"
className="inline-flex items-center gap-2 rounded-lg bg-white/[0.04] border border-brand-border px-4 py-2 text-sm font-medium text-[#e2e5eb] transition-colors hover:border-white/[0.12] disabled:opacity-50"
>
{exporting ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -460,36 +460,36 @@ export default function SurveyResponsesPage() {
/>
{error && (
<div className="rounded-[10px] border border-rose-500/20 bg-rose-500/10 px-4 py-2 text-sm text-rose-400">
<div className="rounded-lg border border-rose-500/20 bg-rose-500/10 px-4 py-2 text-sm text-rose-400">
{error}
</div>
)}
{/* Stat cards */}
<div className="flex gap-4">
<div className="glass-card-static px-5 py-4 flex-1">
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1">
<div className="card-flat px-5 py-4 flex-1">
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1">
Total Responses
</p>
<p className="text-2xl font-heading font-bold text-gradient-brand">
<p className="text-2xl font-heading font-bold text-[#67e8f9]">
{data?.total ?? 0}
</p>
</div>
<div className="glass-card-static px-5 py-4 flex-1">
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1">
<div className="card-flat px-5 py-4 flex-1">
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1">
This Week
</p>
<p className="text-2xl font-heading font-bold text-foreground">
<p className="text-2xl font-heading font-bold text-[#e2e5eb]">
{data?.this_week ?? 0}
</p>
</div>
<div className="glass-card-static px-5 py-4 flex-1">
<p className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground mb-1">
<div className="card-flat px-5 py-4 flex-1">
<p className="font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b] mb-1">
Unread
</p>
<p className={cn(
'text-2xl font-heading font-bold',
(data?.unread ?? 0) > 0 ? 'text-primary' : 'text-foreground'
(data?.unread ?? 0) > 0 ? 'text-[#22d3ee]' : 'text-[#e2e5eb]'
)}>
{data?.unread ?? 0}
</p>
@@ -502,27 +502,27 @@ export default function SurveyResponsesPage() {
className="flex items-center gap-3 rounded-xl px-4 py-2.5"
style={{ background: 'rgba(6, 182, 212, 0.08)', border: '1px solid rgba(6, 182, 212, 0.15)' }}
>
<span className="text-sm text-primary font-medium">
<span className="text-sm text-[#22d3ee] font-medium">
{selectedIds.size} selected
</span>
<div className="flex-1" />
<button
onClick={() => handleBulkAction('mark_read')}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
>
<Eye className="h-3.5 w-3.5" />
Mark Read
</button>
<button
onClick={() => handleBulkAction('mark_unread')}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
>
<EyeOff className="h-3.5 w-3.5" />
Mark Unread
</button>
<button
onClick={() => handleBulkAction('archive')}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
>
<Archive className="h-3.5 w-3.5" />
Archive
@@ -536,7 +536,7 @@ export default function SurveyResponsesPage() {
</button>
<button
onClick={() => setSelectedIds(new Set())}
className="px-3 py-1.5 rounded-lg text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-brand-border transition-colors"
className="px-3 py-1.5 rounded-lg text-xs font-medium text-[#848b9b] hover:text-[#e2e5eb] hover:bg-brand-border transition-colors"
>
Clear
</button>
@@ -544,14 +544,14 @@ export default function SurveyResponsesPage() {
)}
{/* Table */}
<div className="glass-card-static overflow-hidden">
<div className="card-flat overflow-hidden">
<table className="w-full">
<thead>
<tr className="border-b border-border/50">
<tr className="border-b border-[#1e2130]/50">
<th className="px-2 py-3 w-8">
<button onClick={toggleSelectAll} className="text-muted-foreground/40 hover:text-muted-foreground">
<button onClick={toggleSelectAll} className="text-[#848b9b]/40 hover:text-[#848b9b]">
{selectedIds.size > 0 && selectedIds.size === responses.length ? (
<CheckSquare className="h-4 w-4 text-primary" />
<CheckSquare className="h-4 w-4 text-[#22d3ee]" />
) : (
<Square className="h-4 w-4" />
)}
@@ -559,19 +559,19 @@ export default function SurveyResponsesPage() {
</th>
<th className="px-1 py-3 w-6" />
<th className="px-2 py-3 w-8" />
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
#
</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
Respondent
</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
Source
</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
Date
</th>
<th className="px-4 py-3 text-left font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
<th className="px-4 py-3 text-left font-sans text-xs text-[0.625rem] uppercase tracking-widest text-[#848b9b]">
Answered
</th>
<th className="px-3 py-3 w-10" />
@@ -580,7 +580,7 @@ export default function SurveyResponsesPage() {
<tbody>
{responses.length === 0 ? (
<tr>
<td colSpan={9} className="px-4 py-12 text-center text-sm text-muted-foreground">
<td colSpan={9} className="px-4 py-12 text-center text-sm text-[#848b9b]">
{showArchived ? 'No archived responses.' : 'No survey responses yet.'}
</td>
</tr>

View File

@@ -189,8 +189,8 @@ export function UserDetailPage() {
}
const selectClass = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
'placeholder:text-[#848b9b] focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)
if (loading) {
@@ -224,15 +224,15 @@ export function UserDetailPage() {
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/admin/users')}
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
className="rounded-md border border-[#1e2130] p-2 text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
>
<ArrowLeft className="h-4 w-4" />
</button>
<div className="flex-1">
<h1 className="text-xl font-heading font-semibold text-foreground">
<h1 className="text-xl font-heading font-semibold text-[#e2e5eb]">
{user.full_name || user.email}
</h1>
<p className="text-sm text-muted-foreground">{user.email}</p>
<p className="text-sm text-[#848b9b]">{user.email}</p>
</div>
<div className="flex items-center gap-2">
{user.is_super_admin && (
@@ -252,21 +252,21 @@ export function UserDetailPage() {
{/* Account & Subscription */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<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">
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-[#848b9b]">
Account & Subscription
</h2>
<dl className="space-y-3">
{user.account && (
<>
<div className="flex justify-between">
<dt className="text-sm text-muted-foreground">Account</dt>
<dd className="text-sm text-foreground">{user.account.name}</dd>
<dt className="text-sm text-[#848b9b]">Account</dt>
<dd className="text-sm text-[#e2e5eb]">{user.account.name}</dd>
</div>
{user.account.display_code && (
<div className="flex justify-between">
<dt className="text-sm text-muted-foreground">Display Code</dt>
<dd className="text-sm font-mono text-muted-foreground">{user.account.display_code}</dd>
<dt className="text-sm text-[#848b9b]">Display Code</dt>
<dd className="text-sm font-mono text-[#848b9b]">{user.account.display_code}</dd>
</div>
)}
</>
@@ -274,13 +274,13 @@ export function UserDetailPage() {
{user.subscription ? (
<>
<div className="flex justify-between">
<dt className="text-sm text-muted-foreground">Plan</dt>
<dd className="text-sm font-semibold text-foreground">
<dt className="text-sm text-[#848b9b]">Plan</dt>
<dd className="text-sm font-semibold text-[#e2e5eb]">
{user.subscription.plan.charAt(0).toUpperCase() + user.subscription.plan.slice(1)}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-sm text-muted-foreground">Status</dt>
<dt className="text-sm text-[#848b9b]">Status</dt>
<dd>
<StatusBadge variant={user.subscription.status === 'trialing' ? 'warning' : 'success'}>
{user.subscription.status}
@@ -289,24 +289,24 @@ export function UserDetailPage() {
</div>
{user.subscription.current_period_end && (
<div className="flex justify-between">
<dt className="text-sm text-muted-foreground">Period End</dt>
<dd className="text-sm text-muted-foreground">{fmt(user.subscription.current_period_end)}</dd>
<dt className="text-sm text-[#848b9b]">Period End</dt>
<dd className="text-sm text-[#848b9b]">{fmt(user.subscription.current_period_end)}</dd>
</div>
)}
</>
) : (
<div className="text-sm text-muted-foreground">No subscription</div>
<div className="text-sm text-[#848b9b]">No subscription</div>
)}
<div className="flex justify-between">
<dt className="text-sm text-muted-foreground">Joined</dt>
<dd className="text-sm text-muted-foreground">{fmt(user.created_at)}</dd>
<dt className="text-sm text-[#848b9b]">Joined</dt>
<dd className="text-sm text-[#848b9b]">{fmt(user.created_at)}</dd>
</div>
</dl>
</div>
{/* Admin Actions */}
<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">
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-[#848b9b]">
Admin Actions
</h2>
<div className="space-y-3">
@@ -317,16 +317,16 @@ export function UserDetailPage() {
setSelectedPlan(user.subscription?.plan || 'free')
setPlanModalOpen(true)
}}
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"
className="flex w-full items-center gap-3 rounded-lg border border-[#1e2130] px-4 py-3 text-left text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
>
<Shield className="h-4 w-4 text-muted-foreground" />
<Shield className="h-4 w-4 text-[#848b9b]" />
Change Plan
</button>
<button
onClick={() => setTrialModalOpen(true)}
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"
className="flex w-full items-center gap-3 rounded-lg border border-[#1e2130] px-4 py-3 text-left text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
>
<Clock className="h-4 w-4 text-muted-foreground" />
<Clock className="h-4 w-4 text-[#848b9b]" />
{user.subscription?.status === 'trialing' ? 'Extend Trial' : 'Start Trial'}
</button>
</>
@@ -349,9 +349,9 @@ export function UserDetailPage() {
setResetTempPassword(null)
setResetModalOpen(true)
}}
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"
className="flex w-full items-center gap-3 rounded-lg border border-[#1e2130] px-4 py-3 text-left text-sm text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb]"
>
<KeyRound className="h-4 w-4 text-muted-foreground" />
<KeyRound className="h-4 w-4 text-[#848b9b]" />
Reset Password
</button>
<button
@@ -402,42 +402,42 @@ export function UserDetailPage() {
{/* Invite Code Used */}
{user.invite_code_used && (
<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">
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl p-6">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-[#848b9b]">
<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-muted-foreground">Code</dt>
<dd className="mt-1 font-mono text-sm text-muted-foreground">{user.invite_code_used.code}</dd>
<dt className="text-xs text-[#848b9b]">Code</dt>
<dd className="mt-1 font-mono text-sm text-[#848b9b]">{user.invite_code_used.code}</dd>
</div>
<div>
<dt className="text-xs text-muted-foreground">Plan Assigned</dt>
<dd className="mt-1 text-sm text-muted-foreground">
<dt className="text-xs text-[#848b9b]">Plan Assigned</dt>
<dd className="mt-1 text-sm text-[#848b9b]">
{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-muted-foreground">Trial Days</dt>
<dd className="mt-1 text-sm text-muted-foreground">{user.invite_code_used.trial_duration_days ?? '—'}</dd>
<dt className="text-xs text-[#848b9b]">Trial Days</dt>
<dd className="mt-1 text-sm text-[#848b9b]">{user.invite_code_used.trial_duration_days ?? '—'}</dd>
</div>
<div>
<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>
<dt className="text-xs text-[#848b9b]">Created By</dt>
<dd className="mt-1 text-sm text-[#848b9b]">{user.invite_code_used.created_by_email ?? '—'}</dd>
</div>
</dl>
</div>
)}
{/* Tabs: Sessions / Audit Logs */}
<div className="bg-card border border-border rounded-xl">
<div className="flex border-b border-border">
<div className="bg-[#14161d] border border-[#1e2130] rounded-xl">
<div className="flex border-b border-[#1e2130]">
<button
onClick={() => setActiveTab('sessions')}
className={cn(
'px-6 py-3 text-sm font-medium',
activeTab === 'sessions' ? 'border-b-2 border-foreground text-foreground' : 'text-muted-foreground hover:text-foreground'
activeTab === 'sessions' ? 'border-b-2 border-foreground text-[#e2e5eb]' : 'text-[#848b9b] hover:text-[#e2e5eb]'
)}
>
Sessions ({user.total_sessions})
@@ -446,7 +446,7 @@ export function UserDetailPage() {
onClick={() => setActiveTab('audit')}
className={cn(
'px-6 py-3 text-sm font-medium',
activeTab === 'audit' ? 'border-b-2 border-foreground text-foreground' : 'text-muted-foreground hover:text-foreground'
activeTab === 'audit' ? 'border-b-2 border-foreground text-[#e2e5eb]' : 'text-[#848b9b] hover:text-[#e2e5eb]'
)}
>
Audit Logs ({user.total_audit_logs})
@@ -458,7 +458,7 @@ export function UserDetailPage() {
user.recent_sessions.length > 0 ? (
<table className="w-full">
<thead>
<tr className="border-b border-border text-left text-xs text-muted-foreground">
<tr className="border-b border-[#1e2130] text-left text-xs text-[#848b9b]">
<th className="pb-2 font-medium">Tree</th>
<th className="pb-2 font-medium">Started</th>
<th className="pb-2 font-medium">Completed</th>
@@ -467,17 +467,17 @@ export function UserDetailPage() {
</thead>
<tbody>
{user.recent_sessions.map(s => (
<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>
<tr key={s.id} className="border-b border-[#1e2130]">
<td className="py-3 text-sm text-[#848b9b]">{s.tree_name ?? '—'}</td>
<td className="py-3 text-sm text-[#848b9b]">{fmtFull(s.started_at)}</td>
<td className="py-3 text-sm text-[#848b9b]">{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-muted-foreground"></span>
<span className="text-sm text-[#848b9b]"></span>
)}
</td>
</tr>
@@ -485,7 +485,7 @@ export function UserDetailPage() {
</tbody>
</table>
) : (
<div className="py-8 text-center text-sm text-muted-foreground">No sessions yet</div>
<div className="py-8 text-center text-sm text-[#848b9b]">No sessions yet</div>
)
)}
@@ -493,7 +493,7 @@ export function UserDetailPage() {
user.recent_audit_logs.length > 0 ? (
<table className="w-full">
<thead>
<tr className="border-b border-border text-left text-xs text-muted-foreground">
<tr className="border-b border-[#1e2130] text-left text-xs text-[#848b9b]">
<th className="pb-2 font-medium">Action</th>
<th className="pb-2 font-medium">Resource</th>
<th className="pb-2 font-medium">Time</th>
@@ -501,16 +501,16 @@ export function UserDetailPage() {
</thead>
<tbody>
{user.recent_audit_logs.map(a => (
<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 key={a.id} className="border-b border-[#1e2130]">
<td className="py-3 text-sm text-[#848b9b]">{a.action}</td>
<td className="py-3 text-sm text-[#848b9b]">{a.resource_type ?? '—'}</td>
<td className="py-3 text-sm text-[#848b9b]">{fmtFull(a.created_at)}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="py-8 text-center text-sm text-muted-foreground">No audit logs yet</div>
<div className="py-8 text-center text-sm text-[#848b9b]">No audit logs yet</div>
)
)}
</div>
@@ -530,7 +530,7 @@ export function UserDetailPage() {
}
>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Plan</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Plan</label>
<select
aria-label="Subscription plan"
value={selectedPlan}
@@ -560,11 +560,11 @@ export function UserDetailPage() {
}
>
<div className="space-y-4">
<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 className="text-sm text-[#848b9b]">
Choose how to reset the password for <span className="font-medium text-[#e2e5eb]">{user.full_name || user.email}</span>.
</p>
<div className="space-y-2">
<label className="flex items-start gap-3 rounded-lg border border-border p-3 cursor-pointer hover:bg-accent">
<label className="flex items-start gap-3 rounded-lg border border-[#1e2130] p-3 cursor-pointer hover:bg-accent">
<input
type="radio"
name="reset-mode"
@@ -574,11 +574,11 @@ export function UserDetailPage() {
className="mt-0.5"
/>
<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 className="text-sm font-medium text-[#e2e5eb]">Send Reset Email</div>
<div className="text-xs text-[#848b9b]">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-border p-3 cursor-pointer hover:bg-accent">
<label className="flex items-start gap-3 rounded-lg border border-[#1e2130] p-3 cursor-pointer hover:bg-accent">
<input
type="radio"
name="reset-mode"
@@ -588,8 +588,8 @@ export function UserDetailPage() {
className="mt-0.5"
/>
<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 className="text-sm font-medium text-[#e2e5eb]">Generate Temp Password</div>
<div className="text-xs text-[#848b9b]">A temporary password is generated. You share it manually.</div>
</div>
</label>
</div>
@@ -613,18 +613,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-border bg-card px-3 py-2 text-sm text-foreground font-mono">
<code className="flex-1 rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] font-mono">
{resetTempPassword}
</code>
<button
onClick={handleCopyResetPassword}
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
className="rounded-md border border-[#1e2130] p-2 text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb] 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-muted-foreground">
<p className="text-xs text-[#848b9b]">
The user will be required to change this password on next login.
</p>
</div>
@@ -646,7 +646,7 @@ export function UserDetailPage() {
}
>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Days to add</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Days to add</label>
<Input
type="number"
value={trialDays}
@@ -654,7 +654,7 @@ export function UserDetailPage() {
min={1}
max={90}
/>
<p className="mt-1 text-xs text-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
<p className="mt-1 text-xs text-[#848b9b]">1-90 days. Will convert to trialing status if not already.</p>
</div>
</Modal>
@@ -712,11 +712,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-muted-foreground">
<ul className="space-y-1 text-sm text-[#848b9b]">
{Object.entries(hardDeleteBlockers).map(([key, count]) => (
<li key={key} className="flex justify-between">
<span>{key.replace(/_/g, ' ')}</span>
<span className="font-mono text-muted-foreground">{count}</span>
<span className="font-mono text-[#848b9b]">{count}</span>
</li>
))}
</ul>

View File

@@ -190,8 +190,8 @@ export function UsersPage() {
sortable: true,
render: (u) => (
<div>
<div className="font-medium text-foreground">{u.name}</div>
<div className="text-xs text-muted-foreground">{u.email}</div>
<div className="font-medium text-[#e2e5eb]">{u.name}</div>
<div className="text-xs text-[#848b9b]">{u.email}</div>
</div>
),
},
@@ -226,7 +226,7 @@ export function UsersPage() {
header: 'Joined',
sortable: true,
render: (u) => (
<span className="text-sm text-muted-foreground">
<span className="text-sm text-[#848b9b]">
{new Date(u.created_at).toLocaleDateString()}
</span>
),
@@ -286,12 +286,12 @@ export function UsersPage() {
placeholder="Search by name or email..."
className="max-w-sm"
/>
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<label className="flex items-center gap-2 text-sm text-[#848b9b]">
<input
type="checkbox"
checked={showArchived}
onChange={(e) => { setShowArchived(e.target.checked); setPage(1) }}
className="rounded border-border bg-card"
className="rounded border-[#1e2130] bg-[#14161d]"
/>
Show archived
</label>
@@ -326,14 +326,14 @@ export function UsersPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Changing role for <span className="font-medium text-foreground">{roleModalUser?.name}</span>
<p className="text-sm text-[#848b9b]">
Changing role for <span className="font-medium text-[#e2e5eb]">{roleModalUser?.name}</span>
</p>
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value)}
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
@@ -357,11 +357,11 @@ export function UsersPage() {
}
>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Moving <span className="font-medium text-foreground">{moveModalUser?.name}</span> to a new account.
<p className="text-sm text-[#848b9b]">
Moving <span className="font-medium text-[#e2e5eb]">{moveModalUser?.name}</span> to a new account.
</p>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
<Input
type="text"
value={displayCode}
@@ -389,7 +389,7 @@ export function UsersPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Name</label>
<Input
type="text"
value={createForm.name}
@@ -398,7 +398,7 @@ export function UsersPage() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Email</label>
<Input
type="email"
value={createForm.email}
@@ -407,12 +407,12 @@ export function UsersPage() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Mode</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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-border bg-card px-3 py-2 text-sm text-foreground',
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
@@ -423,7 +423,7 @@ export function UsersPage() {
{createForm.account_mode === 'existing' && (
<>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
<Input
type="text"
value={createForm.account_display_code}
@@ -432,12 +432,12 @@ export function UsersPage() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Role</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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-border bg-card px-3 py-2 text-sm text-foreground',
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>
@@ -453,9 +453,9 @@ export function UsersPage() {
id="send-email"
checked={createForm.send_email}
onChange={(e) => setCreateForm(f => ({ ...f, send_email: e.target.checked }))}
className="rounded border-border bg-card"
className="rounded border-[#1e2130] bg-[#14161d]"
/>
<label htmlFor="send-email" className="text-sm text-muted-foreground">
<label htmlFor="send-email" className="text-sm text-[#848b9b]">
Send welcome email with temporary password
</label>
</div>
@@ -479,21 +479,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-foreground">Temporary Password</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Temporary Password</label>
<div className="flex items-center gap-2">
<code className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground font-mono">
<code className="flex-1 rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb] font-mono">
{tempPassword}
</code>
<button
onClick={handleCopyPassword}
className="rounded-md border border-border p-2 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
className="rounded-md border border-[#1e2130] p-2 text-[#848b9b] hover:bg-accent hover:text-[#e2e5eb] 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-muted-foreground">
<p className="text-xs text-[#848b9b]">
The user will be required to change this password on first login.
</p>
</div>
@@ -516,7 +516,7 @@ export function UsersPage() {
>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Email</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Email</label>
<Input
type="email"
value={inviteForm.email}
@@ -525,7 +525,7 @@ export function UsersPage() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Account Display Code</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">Account Display Code</label>
<Input
type="text"
value={inviteForm.account_display_code}
@@ -534,12 +534,12 @@ export function UsersPage() {
/>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-foreground">Role</label>
<label className="mb-1 block text-sm font-medium text-[#e2e5eb]">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-border bg-card px-3 py-2 text-sm text-foreground',
'w-full rounded-md border border-[#1e2130] bg-[#14161d] px-3 py-2 text-sm text-[#e2e5eb]',
'focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)}
>