refactor: adopt shared Input/Textarea components (#101)

* refactor: adopt shared Input/Textarea components across 15 files

Replace 42 raw <input>/<textarea> elements with <Input>/<Textarea>
from components/ui/. Consistent focus states, error handling, and
styling across all form fields.

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

* refactor: replace hardcoded rgba/hex colors with Tailwind tokens

- rgba(255,255,255,0.xx) → bg-white/[0.xx], border-white/[0.xx]
- rgba(6,182,212,0.3) → border-primary/30 (focus states)
- #0a0a0a → bg-background
- Inline style hex colors → var(--color-primary), var(--color-brand-gradient-to)
- 28 files updated, zero hardcoded rgba() patterns remaining

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

* feat: add PageMeta to 16 pages for SEO and proper browser tab titles

Public pages (Login, Register, Forgot/Reset Password, Verify Email,
Survey Thank You) get descriptions for SEO. Authenticated pages
(Dashboard, Flow Library, My Flows, Session History, AI Assistant,
Account Settings, Step Library, My Shares, Feedback, Guides) get
proper tab titles.

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

* feat: add page transitions and staggered list animations

- ViewTransitionOutlet: wraps Outlet with fade-in-up animation keyed
  to route path. Sidebar/topbar stay still, only content area animates.
- StaggerList: reusable component that cascades children with
  incremental delay (50ms default). Pure CSS via @utility stagger-item.
- Applied stagger to TreeGridView, MyTreesPage cards, SessionHistoryPage.
- New stagger-fade-in keyframe in @theme block.

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

* fix: ViewTransitionOutlet needs h-full for React Flow canvas

The wrapper div broke the height chain needed by TreeEditorPage's
h-full layout, causing React Flow canvas to collapse to zero height.

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

* fix: main-content flex layout for tree editor + scrollable pages

Main content area is now flex-col so the ViewTransitionOutlet wrapper
gets an explicit computed height via flex-1 min-h-0. This makes h-full
resolve correctly in the tree editor (React Flow canvas) while still
allowing overflow-y-auto scrolling for normal pages.

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

* fix: resolve ESLint errors in Button and Skeleton components

- Button: suppress react-refresh/only-export-components for buttonVariants re-export
- Skeleton: replace empty interface with type alias, replace Math.random() with static widths array

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

* feat: add PageMeta, animation classes, and layout fixes to remaining pages

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 #101.
This commit is contained in:
chihlasm
2026-03-09 16:12:21 -04:00
committed by GitHub
parent b28a096738
commit 5095b0d8df
65 changed files with 352 additions and 298 deletions

View File

@@ -42,6 +42,11 @@
When adding new pages/components: use "ResolutionFlow" branding, ice-cyan gradient accent theme, `.glass-card` / `.glass-card-static` containers, `text-foreground`/`text-muted-foreground` hierarchy. Primary actions use `bg-gradient-brand`. Pages render inside the app shell (CSS Grid: topbar + sidebar + main). Use "Flows" not "Trees" in all user-facing text; use "Projects" not "Procedures" for procedural flows. Reference [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) for layout patterns, navigation, and component specs. When adding new pages/components: use "ResolutionFlow" branding, ice-cyan gradient accent theme, `.glass-card` / `.glass-card-static` containers, `text-foreground`/`text-muted-foreground` hierarchy. Primary actions use `bg-gradient-brand`. Pages render inside the app shell (CSS Grid: topbar + sidebar + main). Use "Flows" not "Trees" in all user-facing text; use "Projects" not "Procedures" for procedural flows. Reference [UI-DESIGN-SYSTEM.md](UI-DESIGN-SYSTEM.md) for layout patterns, navigation, and component specs.
## Implementation Principles
- Prefer correct architecture over minimal diff
- If two approaches exist, implement the one that scales, not the one that's faster to write
- Flag any "simpler approach" tradeoffs for product owner review before proceeding
--- ---
## Current State ## Current State

View File

@@ -2,7 +2,7 @@ import { Outlet } from 'react-router-dom'
export function AccountLayout() { export function AccountLayout() {
return ( return (
<div className="container mx-auto max-w-(--breakpoint-lg) px-4 py-6"> <div className="overflow-y-auto h-full container mx-auto max-w-(--breakpoint-lg) px-4 py-6">
<Outlet /> <Outlet />
</div> </div>
) )

View File

@@ -69,7 +69,7 @@ export function DeleteAccountModal({ onClose }: Props) {
onClick={onClose} onClick={onClose}
className={cn( className={cn(
'rounded-[10px] px-4 py-2 text-sm font-medium', 'rounded-[10px] px-4 py-2 text-sm font-medium',
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground' 'bg-white/[0.04] border border-brand-border text-foreground'
)} )}
> >
Cancel Cancel

View File

@@ -45,7 +45,7 @@ export function LeaveAccountModal({ accountName, onClose }: Props) {
onClick={onClose} onClick={onClose}
className={cn( className={cn(
'rounded-[10px] px-4 py-2 text-sm font-medium', 'rounded-[10px] px-4 py-2 text-sm font-medium',
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground' 'bg-white/[0.04] border border-brand-border text-foreground'
)} )}
> >
Cancel Cancel

View File

@@ -90,7 +90,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
onClick={onClose} onClick={onClose}
className={cn( className={cn(
'rounded-[10px] px-4 py-2 text-sm font-medium', 'rounded-[10px] px-4 py-2 text-sm font-medium',
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground' 'bg-white/[0.04] border border-brand-border text-foreground'
)} )}
> >
Cancel Cancel

View File

@@ -1,7 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import { cn } from '@/lib/utils'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface CreateCategoryModalProps { interface CreateCategoryModalProps {
isOpen: boolean isOpen: boolean
@@ -93,7 +94,7 @@ export function CreateCategoryModal({
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-red-400">*</span> Category Name <span className="text-red-400">*</span>
</label> </label>
<input <Input
id="name" id="name"
type="text" type="text"
value={name} value={name}
@@ -102,12 +103,6 @@ export function CreateCategoryModal({
maxLength={100} maxLength={100}
placeholder="e.g., Network Troubleshooting" placeholder="e.g., Network Troubleshooting"
required required
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
{name.length}/100 characters {name.length}/100 characters
@@ -118,19 +113,13 @@ export function CreateCategoryModal({
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span> Description <span className="text-muted-foreground">(optional)</span>
</label> </label>
<textarea <Textarea
id="description" id="description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
disabled={isSaving} disabled={isSaving}
rows={3} rows={3}
placeholder="Brief description of this category..." placeholder="Brief description of this category..."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
</div> </div>
</form> </form>

View File

@@ -1,8 +1,9 @@
import { useState } from 'react' import { useState } from 'react'
import { cn } from '@/lib/utils'
import type { StepCategoryListItem } from '@/types' import type { StepCategoryListItem } from '@/types'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface EditCategoryModalProps { interface EditCategoryModalProps {
isOpen: boolean isOpen: boolean
@@ -105,7 +106,7 @@ export function EditCategoryModal({
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-red-400">*</span> Category Name <span className="text-red-400">*</span>
</label> </label>
<input <Input
id="edit-name" id="edit-name"
type="text" type="text"
value={name} value={name}
@@ -114,12 +115,6 @@ export function EditCategoryModal({
maxLength={100} maxLength={100}
placeholder="e.g., Network Troubleshooting" placeholder="e.g., Network Troubleshooting"
required required
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
<p className="mt-1 text-xs text-muted-foreground"> <p className="mt-1 text-xs text-muted-foreground">
{name.length}/100 characters {name.length}/100 characters
@@ -130,19 +125,13 @@ export function EditCategoryModal({
<label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor="edit-description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span> Description <span className="text-muted-foreground">(optional)</span>
</label> </label>
<textarea <Textarea
id="edit-description" id="edit-description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
disabled={isSaving} disabled={isSaving}
rows={3} rows={3}
placeholder="Brief description of this category..." placeholder="Brief description of this category..."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
</div> </div>
</form> </form>

View File

@@ -17,7 +17,7 @@ export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps)
className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ className={`shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
role === 'assistant' role === 'assistant'
? 'bg-primary/15 text-primary' ? 'bg-primary/15 text-primary'
: 'bg-[rgba(255,255,255,0.08)] text-muted-foreground' : 'bg-white/[0.08] text-muted-foreground'
}`} }`}
> >
{role === 'assistant' ? <Sparkles size={14} /> : <User size={14} />} {role === 'assistant' ? <Sparkles size={14} /> : <User size={14} />}
@@ -29,7 +29,7 @@ export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps)
className={`rounded-2xl px-4 py-3 text-[0.875rem] leading-relaxed ${ className={`rounded-2xl px-4 py-3 text-[0.875rem] leading-relaxed ${
role === 'user' role === 'user'
? 'bg-primary/15 text-foreground' ? 'bg-primary/15 text-foreground'
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-brand-border' : 'bg-white/[0.04] text-foreground border border-brand-border'
}`} }`}
> >
<MarkdownContent content={content} className="text-[0.875rem] leading-relaxed" /> <MarkdownContent content={content} className="text-[0.875rem] leading-relaxed" />

View File

@@ -103,7 +103,7 @@ function ChatItem({
'group flex items-center gap-2 px-3 py-2.5 mx-1.5 rounded-lg cursor-pointer transition-colors', 'group flex items-center gap-2 px-3 py-2.5 mx-1.5 rounded-lg cursor-pointer transition-colors',
isActive isActive
? 'bg-primary/10 text-foreground' ? 'bg-primary/10 text-foreground'
: 'text-muted-foreground hover:bg-[rgba(255,255,255,0.04)] hover:text-foreground' : 'text-muted-foreground hover:bg-white/[0.04] hover:text-foreground'
)} )}
> >
<MessageSquare size={14} className="shrink-0" /> <MessageSquare size={14} className="shrink-0" />
@@ -116,14 +116,14 @@ function ChatItem({
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button <button
onClick={e => { e.stopPropagation(); onTogglePin() }} onClick={e => { e.stopPropagation(); onTogglePin() }}
className="p-1 rounded hover:bg-[rgba(255,255,255,0.08)]" className="p-1 rounded hover:bg-white/[0.08]"
title={chat.pinned ? 'Unpin' : 'Pin'} title={chat.pinned ? 'Unpin' : 'Pin'}
> >
<Pin size={12} className={chat.pinned ? 'text-primary' : ''} /> <Pin size={12} className={chat.pinned ? 'text-primary' : ''} />
</button> </button>
<button <button
onClick={e => { e.stopPropagation(); onDelete() }} onClick={e => { e.stopPropagation(); onDelete() }}
className="p-1 rounded hover:bg-[rgba(255,255,255,0.08)] text-muted-foreground hover:text-rose-400" className="p-1 rounded hover:bg-white/[0.08] text-muted-foreground hover:text-rose-400"
title="Delete" title="Delete"
> >
<Trash2 size={12} /> <Trash2 size={12} />

View File

@@ -233,8 +233,8 @@ export function ConcludeSessionModal({
className={cn( className={cn(
'w-full flex items-center gap-4 p-4 rounded-xl border transition-all text-left', 'w-full flex items-center gap-4 p-4 rounded-xl border transition-all text-left',
'hover:scale-[1.01] active:scale-[0.99]', 'hover:scale-[1.01] active:scale-[0.99]',
'bg-[rgba(255,255,255,0.02)] border-brand-border', 'bg-white/[0.02] border-brand-border',
'hover:border-[rgba(255,255,255,0.12)] hover:bg-[rgba(255,255,255,0.04)]' 'hover:border-white/[0.12] hover:bg-white/[0.04]'
)} )}
> >
<div className={cn('w-10 h-10 rounded-xl flex items-center justify-center', o.bg)}> <div className={cn('w-10 h-10 rounded-xl flex items-center justify-center', o.bg)}>
@@ -282,7 +282,7 @@ export function ConcludeSessionModal({
: 'What still needs to be done, where you left off...' : 'What still needs to be done, where you left off...'
} }
rows={4} rows={4}
className="w-full resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]" className="w-full resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-primary/30"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
/> />
</div> </div>
@@ -308,7 +308,7 @@ export function ConcludeSessionModal({
{/* Generated summary */} {/* Generated summary */}
<div <div
className="rounded-xl border p-5 bg-[rgba(255,255,255,0.02)]" className="rounded-xl border p-5 bg-white/[0.02]"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
> >
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
@@ -335,7 +335,7 @@ export function ConcludeSessionModal({
<div /> <div />
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-[rgba(255,255,255,0.04)] border border-brand-border hover:border-[rgba(255,255,255,0.12)] transition-all" className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-white/[0.04] border border-brand-border hover:border-white/[0.12] transition-all"
> >
Cancel Cancel
</button> </button>
@@ -346,7 +346,7 @@ export function ConcludeSessionModal({
<> <>
<button <button
onClick={() => setStep('select-outcome')} onClick={() => setStep('select-outcome')}
className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-[rgba(255,255,255,0.04)] border border-brand-border hover:border-[rgba(255,255,255,0.12)] transition-all" className="px-4 py-2 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-white/[0.04] border border-brand-border hover:border-white/[0.12] transition-all"
> >
Back Back
</button> </button>
@@ -407,7 +407,7 @@ export function ConcludeSessionModal({
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className="px-4 py-2.5 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-[rgba(255,255,255,0.04)] border border-brand-border hover:border-[rgba(255,255,255,0.12)] transition-all" className="px-4 py-2.5 rounded-[10px] text-sm text-muted-foreground hover:text-foreground bg-white/[0.04] border border-brand-border hover:border-white/[0.12] transition-all"
> >
Done Done
</button> </button>

View File

@@ -18,7 +18,7 @@ export function SuggestedFlowCard({ flow }: SuggestedFlowCardProps) {
return ( return (
<button <button
onClick={handleClick} onClick={handleClick}
className="w-full text-left glass-card-static p-3 rounded-xl hover:border-[rgba(255,255,255,0.12)] transition-colors group" className="w-full text-left glass-card-static p-3 rounded-xl hover:border-white/[0.12] transition-colors group"
> >
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<Box size={14} className="text-primary mt-0.5 shrink-0" /> <Box size={14} className="text-primary mt-0.5 shrink-0" />

View File

@@ -0,0 +1,43 @@
import { Children, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface StaggerListProps {
children: ReactNode
className?: string
/** Base delay before the first item animates (ms). Default: 0 */
baseDelay?: number
/** Delay increment between each item (ms). Default: 50 */
staggerMs?: number
}
/**
* Wraps each child in a stagger-item animation container.
* Children fade in sequentially with a configurable delay.
*
* Usage:
* <StaggerList className="grid grid-cols-3 gap-4">
* {items.map(item => <Card key={item.id} ... />)}
* </StaggerList>
*/
export function StaggerList({
children,
className,
baseDelay = 0,
staggerMs = 50,
}: StaggerListProps) {
const items = Children.toArray(children)
return (
<div className={cn(className)}>
{items.map((child, i) => (
<div
key={i}
className="stagger-item"
style={{ '--stagger-index': i, animationDelay: `${baseDelay + i * staggerMs}ms` } as React.CSSProperties}
>
{child}
</div>
))}
</div>
)
}

View File

@@ -122,7 +122,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${ className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${
msg.role === 'user' msg.role === 'user'
? 'bg-primary/15 text-foreground' ? 'bg-primary/15 text-foreground'
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-brand-border' : 'bg-white/[0.04] text-foreground border border-brand-border'
}`} }`}
> >
<MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" /> <MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" />
@@ -131,7 +131,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
))} ))}
{loading && ( {loading && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-xl px-3.5 py-2.5"> <div className="bg-white/[0.04] border border-brand-border rounded-xl px-3.5 py-2.5">
<Loader2 size={16} className="animate-spin text-primary" /> <Loader2 size={16} className="animate-spin text-primary" />
</div> </div>
</div> </div>
@@ -162,7 +162,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Ask about this step..." placeholder="Ask about this step..."
rows={1} rows={1}
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]" className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-hidden focus:border-primary/30"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
disabled={loading || initializing} disabled={loading || initializing}
/> />

View File

@@ -68,7 +68,7 @@ export function AIPromptDialog({
placeholder={`Example: "A flow for troubleshooting VPN connectivity issues when users can't connect to the corporate network"`} placeholder={`Example: "A flow for troubleshooting VPN connectivity issues when users can't connect to the corporate network"`}
rows={4} rows={4}
disabled={isGenerating} disabled={isGenerating}
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden resize-none disabled:opacity-50" className="w-full rounded-xl border border-border bg-background px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary/30 focus:outline-hidden resize-none disabled:opacity-50"
autoFocus autoFocus
/> />
@@ -83,7 +83,7 @@ export function AIPromptDialog({
<button <button
onClick={onClose} onClick={onClose}
disabled={isGenerating} disabled={isGenerating}
className="rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-4 py-2 text-sm text-foreground hover:border-[rgba(255,255,255,0.12)] transition-colors disabled:opacity-50" className="rounded-[10px] bg-white/[0.04] border border-brand-border px-4 py-2 text-sm text-foreground hover:border-white/[0.12] transition-colors disabled:opacity-50"
> >
Cancel Cancel
</button> </button>

View File

@@ -50,7 +50,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${ className={`max-w-[85%] rounded-xl px-3.5 py-2.5 text-[0.8125rem] leading-relaxed ${
msg.role === 'user' msg.role === 'user'
? 'bg-primary/15 text-foreground' ? 'bg-primary/15 text-foreground'
: 'bg-[rgba(255,255,255,0.04)] text-foreground border border-brand-border' : 'bg-white/[0.04] text-foreground border border-brand-border'
}`} }`}
> >
<MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" /> <MarkdownContent content={msg.content} className="text-[0.8125rem] leading-relaxed" />
@@ -59,7 +59,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
))} ))}
{isLoading && ( {isLoading && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-xl px-3.5 py-2.5"> <div className="bg-white/[0.04] border border-brand-border rounded-xl px-3.5 py-2.5">
<Loader2 size={16} className="animate-spin text-primary" /> <Loader2 size={16} className="animate-spin text-primary" />
</div> </div>
</div> </div>
@@ -77,7 +77,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Ask AI to help..." placeholder="Ask AI to help..."
rows={1} rows={1}
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]" className="flex-1 resize-none rounded-xl border bg-card text-foreground text-[0.8125rem] placeholder:text-muted-foreground px-3.5 py-2.5 focus:outline-hidden focus:border-primary/30"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
disabled={isLoading} disabled={isLoading}
/> />

View File

@@ -26,7 +26,7 @@ export function SuggestionsTab({ suggestions }: SuggestionsTabProps) {
const config = STATUS_CONFIG[s.status] const config = STATUS_CONFIG[s.status]
const StatusIcon = config.icon const StatusIcon = config.icon
return ( return (
<div key={s.id} className="rounded-lg border border-border bg-[rgba(255,255,255,0.02)] px-3 py-2"> <div key={s.id} className="rounded-lg border border-border bg-white/[0.02] px-3 py-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground"> <span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
{s.action_type.replace(/_/g, ' ')} {s.action_type.replace(/_/g, ' ')}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback } from 'react'
import { Outlet, useLocation, useNavigate, Link } from 'react-router-dom' import { useLocation, useNavigate, Link } from 'react-router-dom'
import { Menu, X, LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, Settings, LogOut, Shield } from 'lucide-react' import { Menu, X, LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, Settings, LogOut, Shield } from 'lucide-react'
import { useAuthStore } from '@/store/authStore' import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions' import { usePermissions } from '@/hooks/usePermissions'
@@ -8,6 +8,7 @@ import { BrandLogo } from '@/components/common/BrandLogo'
import { TopBar } from './TopBar' import { TopBar } from './TopBar'
import { Sidebar } from './Sidebar' import { Sidebar } from './Sidebar'
import { EmailVerificationBanner } from './EmailVerificationBanner' import { EmailVerificationBanner } from './EmailVerificationBanner'
import { ViewTransitionOutlet } from './ViewTransitionOutlet'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function AppLayout() { export function AppLayout() {
@@ -183,9 +184,9 @@ export function AppLayout() {
)} )}
{/* Main Content */} {/* Main Content */}
<main className="main-content"> <main className="main-content flex flex-col overflow-hidden">
<EmailVerificationBanner /> <EmailVerificationBanner />
<Outlet /> <ViewTransitionOutlet />
</main> </main>
</div> </div>
</> </>

View File

@@ -0,0 +1,26 @@
import { Outlet, useLocation } from 'react-router-dom'
/**
* Wraps <Outlet> with a fade-in animation keyed to the route path.
* When the route changes, React unmounts/remounts the wrapper div,
* triggering the CSS animation. Sidebar and topbar stay still.
*
* Uses flex-1 + min-h-0 to fill the main content area exactly.
* overflow-y-auto enables scrolling for normal pages while full-height
* pages (tree editor, etc.) use their own overflow:hidden to take
* the exact container height without scrolling.
*/
export function ViewTransitionOutlet() {
const location = useLocation()
// Use the first two path segments as the key to avoid re-animating
// on param changes within the same page (e.g., /trees/123 → /trees/456)
const segments = location.pathname.split('/').filter(Boolean)
const routeKey = segments.slice(0, 2).join('/') || '/'
return (
<div key={routeKey} className="flex-1 min-h-0 animate-fade-in-up">
<Outlet />
</div>
)
}

View File

@@ -122,7 +122,7 @@ export function ImportFlowModal({ onClose }: ImportFlowModalProps) {
'flex flex-col items-center justify-center rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors cursor-pointer', 'flex flex-col items-center justify-center rounded-lg border-2 border-dashed px-4 py-8 text-center transition-colors cursor-pointer',
isDragging isDragging
? 'border-primary/50 bg-primary/5' ? 'border-primary/50 bg-primary/5'
: 'border-border hover:border-[rgba(255,255,255,0.12)]' : 'border-border hover:border-white/[0.12]'
)} )}
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
onDragOver={(e) => { e.preventDefault(); setIsDragging(true) }} onDragOver={(e) => { e.preventDefault(); setIsDragging(true) }}

View File

@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom'
import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Star, Download } from 'lucide-react' import { Pencil, Globe, Lock, Trash2, GitBranch, FileText, Wrench, Star, Download } from 'lucide-react'
import type { TreeListItem } from '@/types' import type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges' import { TagBadges } from '@/components/common/TagBadges'
import { StaggerList } from '@/components/common/StaggerList'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { usePermissions } from '@/hooks/usePermissions' import { usePermissions } from '@/hooks/usePermissions'
import { getTreeEditorPath } from '@/lib/routing' import { getTreeEditorPath } from '@/lib/routing'
@@ -33,7 +34,7 @@ export function TreeGridView({
const { canEditTree, canDeleteTree } = usePermissions() const { canEditTree, canDeleteTree } = usePermissions()
return ( return (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <StaggerList className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trees.map((tree) => ( {trees.map((tree) => (
<div <div
key={tree.id} key={tree.id}
@@ -182,6 +183,6 @@ export function TreeGridView({
</div> </div>
</div> </div>
))} ))}
</div> </StaggerList>
) )
} }

View File

@@ -213,7 +213,7 @@ export function IntakeFormModal({ isOpen, fields, treeName, onSubmit, onCancel }
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-xs">
<div className="mx-4 w-full max-w-lg rounded-2xl border border-border bg-[#0a0a0a] shadow-xl"> <div className="mx-4 w-full max-w-lg rounded-2xl border border-border bg-background shadow-xl">
{/* Header */} {/* Header */}
<div className="border-b border-border px-6 py-4"> <div className="border-b border-border px-6 py-4">
<h2 className="text-lg font-semibold text-foreground">Project Information</h2> <h2 className="text-lg font-semibold text-foreground">Project Information</h2>

View File

@@ -1,8 +1,9 @@
import { useState } from 'react' import { useState } from 'react'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { GitFork } from 'lucide-react' import { GitFork } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface ForkTreeModalProps { interface ForkTreeModalProps {
isOpen: boolean isOpen: boolean
@@ -83,16 +84,12 @@ export function ForkTreeModal({
<label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium text-foreground"> <label htmlFor="tree-name" className="mb-1.5 block text-sm font-medium text-foreground">
Tree Name <span className="text-red-400">*</span> Tree Name <span className="text-red-400">*</span>
</label> </label>
<input <Input
id="tree-name" id="tree-name"
type="text" type="text"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
placeholder="My Custom Tree" placeholder="My Custom Tree"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20'
)}
/> />
</div> </div>
@@ -100,17 +97,13 @@ export function ForkTreeModal({
<label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium text-foreground"> <label htmlFor="tree-description" className="mb-1.5 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span> Description <span className="text-muted-foreground">(optional)</span>
</label> </label>
<textarea <Textarea
id="tree-description" id="tree-description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Describe what this tree helps troubleshoot..." placeholder="Describe what this tree helps troubleshoot..."
rows={3} rows={3}
className={cn( className="resize-none"
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
'resize-none'
)}
/> />
</div> </div>
</div> </div>

View File

@@ -1,7 +1,8 @@
import { useState } from 'react' import { useState } from 'react'
import { X } from 'lucide-react' import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface SaveSessionAsTreeModalProps { interface SaveSessionAsTreeModalProps {
isOpen: boolean isOpen: boolean
@@ -60,7 +61,7 @@ export function SaveSessionAsTreeModal({
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground">
Tree Name <span className="text-muted-foreground">(optional)</span> Tree Name <span className="text-muted-foreground">(optional)</span>
</label> </label>
<input <Input
id="treeName" id="treeName"
type="text" type="text"
value={treeName} value={treeName}
@@ -68,12 +69,6 @@ export function SaveSessionAsTreeModal({
placeholder={defaultTreeName || "Auto-generated if left blank"} placeholder={defaultTreeName || "Auto-generated if left blank"}
disabled={isSaving} disabled={isSaving}
maxLength={255} maxLength={255}
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
</div> </div>
@@ -82,19 +77,13 @@ export function SaveSessionAsTreeModal({
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span> Description <span className="text-muted-foreground">(optional)</span>
</label> </label>
<textarea <Textarea
id="description" id="description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Add a description for this tree" placeholder="Add a description for this tree"
disabled={isSaving} disabled={isSaving}
rows={3} rows={3}
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
</div> </div>

View File

@@ -160,7 +160,7 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
</button> </button>
{showDatePicker && ( {showDatePicker && (
<div className="absolute left-0 top-full z-50 mt-2 rounded-lg border border-border bg-[#0a0a0a] p-4 shadow-lg"> <div className="absolute left-0 top-full z-50 mt-2 rounded-lg border border-border bg-background p-4 shadow-lg">
{/* Date Type Toggle */} {/* Date Type Toggle */}
<div className="mb-3 flex gap-2"> <div className="mb-3 flex gap-2">
<button <button

View File

@@ -8,6 +8,7 @@ import { toast } from '@/lib/toast'
import { Spinner } from '@/components/common/Spinner' import { Spinner } from '@/components/common/Spinner'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
interface ShareSessionModalProps { interface ShareSessionModalProps {
sessionId: string sessionId: string
@@ -246,15 +247,11 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
<label className="mb-2 block text-sm font-medium text-foreground"> <label className="mb-2 block text-sm font-medium text-foreground">
Share Name <span className="text-muted-foreground">(optional)</span> Share Name <span className="text-muted-foreground">(optional)</span>
</label> </label>
<input <Input
type="text" type="text"
value={shareName} value={shareName}
onChange={(e) => setShareName(e.target.value.slice(0, 100))} onChange={(e) => setShareName(e.target.value.slice(0, 100))}
placeholder="e.g. Training link, Customer escalation" placeholder="e.g. Training link, Customer escalation"
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground placeholder-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
maxLength={100} maxLength={100}
/> />
</div> </div>

View File

@@ -4,6 +4,7 @@ import { StarRating } from '@/components/common/StarRating'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import type { Step } from '@/types' import type { Step } from '@/types'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Textarea } from '@/components/ui/Textarea'
interface StepRatingData { interface StepRatingData {
rating: number rating: number
@@ -103,7 +104,7 @@ export function StepRatingModal({
{librarySteps.map((step) => { {librarySteps.map((step) => {
const rating = getRating(step.id) const rating = getRating(step.id)
return ( return (
<div key={step.id} className="rounded-lg border border-border bg-[#0a0a0a] p-4"> <div key={step.id} className="rounded-lg border border-border bg-background p-4">
{/* Step Title */} {/* Step Title */}
<h3 className="font-medium text-foreground">{step.title}</h3> <h3 className="font-medium text-foreground">{step.title}</h3>
<p className="mt-1 text-sm text-muted-foreground capitalize">{step.step_type}</p> <p className="mt-1 text-sm text-muted-foreground capitalize">{step.step_type}</p>
@@ -164,7 +165,7 @@ export function StepRatingModal({
<label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-foreground"> <label htmlFor={`review-${step.id}`} className="mb-1 block text-sm font-medium text-foreground">
Review <span className="text-muted-foreground">(optional)</span> Review <span className="text-muted-foreground">(optional)</span>
</label> </label>
<textarea <Textarea
id={`review-${step.id}`} id={`review-${step.id}`}
value={rating?.review || ''} value={rating?.review || ''}
onChange={(e) => handleReviewChange(step.id, e.target.value)} onChange={(e) => handleReviewChange(step.id, e.target.value)}
@@ -172,12 +173,6 @@ export function StepRatingModal({
maxLength={500} maxLength={500}
rows={2} rows={2}
placeholder="Share your experience with this step..." placeholder="Share your experience with this step..."
className={cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:opacity-50'
)}
/> />
<p className="mt-1 text-xs text-muted-foreground text-right"> <p className="mt-1 text-xs text-muted-foreground text-right">
{rating?.review?.length || 0}/500 {rating?.review?.length || 0}/500

View File

@@ -4,6 +4,8 @@ import { cn } from '@/lib/utils'
import { stepCategoriesApi } from '@/api/stepCategories' import { stepCategoriesApi } from '@/api/stepCategories'
import type { StepCreate, StepCategory, StepCommand } from '@/types/step' import type { StepCreate, StepCategory, StepCommand } from '@/types/step'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface StepFormProps { interface StepFormProps {
onSubmit: (data: StepCreate) => void onSubmit: (data: StepCreate) => void
@@ -173,20 +175,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
<label htmlFor="title" className="mb-2 block text-sm font-medium text-foreground"> <label htmlFor="title" className="mb-2 block text-sm font-medium text-foreground">
Title <span className="text-red-400">*</span> Title <span className="text-red-400">*</span>
</label> </label>
<input <Input
id="title" id="title"
type="text" type="text"
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
placeholder="Enter step title" placeholder="Enter step title"
className={cn( error={errors.title}
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
errors.title ? 'border-red-400/50' : 'border-border'
)}
/> />
{errors.title && (
<p className="mt-1 text-xs text-red-400">{errors.title}</p>
)}
</div> </div>
{/* Instructions */} {/* Instructions */}
@@ -195,20 +191,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
Instructions <span className="text-red-400">*</span> Instructions <span className="text-red-400">*</span>
<span className="ml-2 text-xs font-normal text-muted-foreground">(Markdown supported)</span> <span className="ml-2 text-xs font-normal text-muted-foreground">(Markdown supported)</span>
</label> </label>
<textarea <Textarea
id="instructions" id="instructions"
value={instructions} value={instructions}
onChange={(e) => setInstructions(e.target.value)} onChange={(e) => setInstructions(e.target.value)}
placeholder="Describe what to do in this step..." placeholder="Describe what to do in this step..."
rows={6} rows={6}
className={cn( error={errors.instructions}
'w-full rounded-md border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20',
errors.instructions ? 'border-red-400/50' : 'border-border'
)}
/> />
{errors.instructions && (
<p className="mt-1 text-xs text-red-400">{errors.instructions}</p>
)}
</div> </div>
{/* Help Text */} {/* Help Text */}
@@ -216,13 +206,12 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
<label htmlFor="helpText" className="mb-2 block text-sm font-medium text-foreground"> <label htmlFor="helpText" className="mb-2 block text-sm font-medium text-foreground">
Help Text <span className="text-xs font-normal text-muted-foreground">(Optional)</span> Help Text <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
</label> </label>
<textarea <Textarea
id="helpText" id="helpText"
value={helpText} value={helpText}
onChange={(e) => setHelpText(e.target.value)} onChange={(e) => setHelpText(e.target.value)}
placeholder="Additional context or tips..." placeholder="Additional context or tips..."
rows={3} rows={3}
className="w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20"
/> />
</div> </div>
@@ -256,31 +245,22 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
</button> </button>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<input <Input
type="text" type="text"
value={cmd.label} value={cmd.label}
onChange={(e) => updateCommand(index, 'label', e.target.value)} onChange={(e) => updateCommand(index, 'label', e.target.value)}
placeholder="Command label (e.g., 'Restart service')" placeholder="Command label (e.g., 'Restart service')"
className={cn( className="py-1.5"
'w-full rounded-md border bg-card px-3 py-1.5 text-sm text-foreground', error={errors[`command_${index}_label`]}
errors[`command_${index}_label`] ? 'border-red-400/50' : 'border-border'
)}
/> />
<input <Input
type="text" type="text"
value={cmd.command} value={cmd.command}
onChange={(e) => updateCommand(index, 'command', e.target.value)} onChange={(e) => updateCommand(index, 'command', e.target.value)}
placeholder="Command (e.g., 'systemctl restart nginx')" placeholder="Command (e.g., 'systemctl restart nginx')"
className={cn( className="py-1.5 font-mono"
'w-full rounded-md border bg-card px-3 py-1.5 font-mono text-sm text-foreground', error={errors[`command_${index}_command`]}
errors[`command_${index}_command`] ? 'border-red-400/50' : 'border-border'
)}
/> />
{(errors[`command_${index}_label`] || errors[`command_${index}_command`]) && (
<p className="text-xs text-red-400">
{errors[`command_${index}_label`] || errors[`command_${index}_command`]}
</p>
)}
</div> </div>
</div> </div>
))} ))}
@@ -312,14 +292,14 @@ export function StepForm({ onSubmit, onCancel, initialData, submitLabel, isSubmi
Tags <span className="text-xs font-normal text-muted-foreground">(Optional)</span> Tags <span className="text-xs font-normal text-muted-foreground">(Optional)</span>
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<input <Input
id="tagInput" id="tagInput"
type="text" type="text"
value={tagInput} value={tagInput}
onChange={(e) => setTagInput(e.target.value)} onChange={(e) => setTagInput(e.target.value)}
onKeyDown={handleTagInputKeyDown} onKeyDown={handleTagInputKeyDown}
placeholder="Type tag and press Enter" placeholder="Type tag and press Enter"
className="flex-1 rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground focus:outline-hidden focus:border-primary focus:ring-1 focus:ring-primary/20" className="flex-1"
/> />
<button <button
type="button" type="button"

View File

@@ -10,7 +10,7 @@ const buttonVariants = cva(
primary: primary:
'bg-gradient-brand text-brand-dark font-semibold shadow-lg shadow-primary/20 hover:opacity-90', 'bg-gradient-brand text-brand-dark font-semibold shadow-lg shadow-primary/20 hover:opacity-90',
secondary: secondary:
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground hover:border-[rgba(255,255,255,0.12)] hover:bg-brand-border', 'bg-white/[0.04] border border-brand-border text-foreground hover:border-white/[0.12] hover:bg-brand-border',
destructive: destructive:
'bg-red-400/10 text-red-400 border border-red-400/20 hover:bg-red-400/20', 'bg-red-400/10 text-red-400 border border-red-400/20 hover:bg-red-400/20',
ghost: ghost:
@@ -60,4 +60,5 @@ export function Button({
) )
} }
// eslint-disable-next-line react-refresh/only-export-components
export { buttonVariants } export { buttonVariants }

View File

@@ -12,7 +12,7 @@ export function Input({ className, error, id, ...props }: InputProps) {
className={cn( className={cn(
'flex h-9 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'flex h-9 w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground', 'placeholder:text-muted-foreground',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20', 'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:cursor-not-allowed disabled:opacity-50', 'disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20', error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20',
className className

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {} type SkeletonProps = React.HTMLAttributes<HTMLDivElement>
export function Skeleton({ className, ...props }: SkeletonProps) { export function Skeleton({ className, ...props }: SkeletonProps) {
return ( return (
@@ -27,6 +27,8 @@ export function CardSkeleton({ className }: { className?: string }) {
) )
} }
const COL_WIDTHS = ['35%', '45%', '25%', '40%', '30%', '50%']
export function TableRowSkeleton({ cols = 4 }: { cols?: number }) { export function TableRowSkeleton({ cols = 4 }: { cols?: number }) {
return ( return (
<div className="flex items-center gap-4 px-4 py-3"> <div className="flex items-center gap-4 px-4 py-3">
@@ -34,7 +36,7 @@ export function TableRowSkeleton({ cols = 4 }: { cols?: number }) {
<Skeleton <Skeleton
key={i} key={i}
className="h-4" className="h-4"
style={{ width: `${20 + Math.random() * 30}%` }} style={{ width: COL_WIDTHS[i % COL_WIDTHS.length] }}
/> />
))} ))}
</div> </div>
@@ -45,7 +47,7 @@ export function ListSkeleton({ count = 5, className }: { count?: number; classNa
return ( return (
<div className={cn('space-y-3', className)}> <div className={cn('space-y-3', className)}>
{Array.from({ length: count }).map((_, i) => ( {Array.from({ length: count }).map((_, i) => (
<div key={i} className="flex items-center gap-3 px-4 py-3 rounded-lg bg-[rgba(255,255,255,0.02)]"> <div key={i} className="flex items-center gap-3 px-4 py-3 rounded-lg bg-white/[0.02]">
<Skeleton className="size-8 rounded-full shrink-0" /> <Skeleton className="size-8 rounded-full shrink-0" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<Skeleton className="h-4 w-2/3" /> <Skeleton className="h-4 w-2/3" />

View File

@@ -12,7 +12,7 @@ export function Textarea({ className, error, id, ...props }: TextareaProps) {
className={cn( className={cn(
'flex w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', 'flex w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground',
'placeholder:text-muted-foreground', 'placeholder:text-muted-foreground',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20', 'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'disabled:cursor-not-allowed disabled:opacity-50', 'disabled:cursor-not-allowed disabled:opacity-50',
error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20', error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20',
className className

View File

@@ -152,6 +152,11 @@
80% { transform: rotate(-2deg); } 80% { transform: rotate(-2deg); }
100% { transform: rotate(0deg); } 100% { transform: rotate(0deg); }
} }
@keyframes stagger-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
} }
/* ── Root CSS variables (non-theme: glass, shadows, layout) ── */ /* ── Root CSS variables (non-theme: glass, shadows, layout) ── */
@@ -254,6 +259,12 @@
animation: breatheGlow 3s ease-in-out infinite alternate; animation: breatheGlow 3s ease-in-out infinite alternate;
} }
@utility stagger-item {
opacity: 0;
animation: stagger-fade-in 350ms var(--ease-out-smooth) forwards;
animation-delay: calc(var(--stagger-index, 0) * 50ms);
}
@utility rdp-custom { @utility rdp-custom {
@apply text-foreground; @apply text-foreground;
& .rdp-month { @apply w-full; } & .rdp-month { @apply w-full; }
@@ -300,7 +311,8 @@
.main-content { .main-content {
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
overflow-y: auto; /* Each page handles its own scrolling — full-height pages use
overflow-hidden, scrollable pages use overflow-y-auto */
} }
@media (max-width: 767px) { @media (max-width: 767px) {

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock } from 'lucide-react' import { Building2, Users, Mail, Crown, Loader2, AlertCircle, Check, X, Settings, FolderTree, Server, RefreshCw, MessageSquareText, UserCog, AlertTriangle, Clock } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { accountsApi } from '@/api/accounts' import { accountsApi } from '@/api/accounts'
import type { Account, AccountMember, AccountInvite } from '@/types' import type { Account, AccountMember, AccountInvite } from '@/types'
import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal' import { TransferOwnershipModal } from '@/components/account/TransferOwnershipModal'
@@ -159,6 +160,8 @@ export function AccountSettingsPage() {
const sub = subscription?.subscription const sub = subscription?.subscription
return ( return (
<>
<PageMeta title="Account Settings" />
<div> <div>
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -678,6 +681,7 @@ export function AccountSettingsPage() {
<DeleteAccountModal onClose={() => setShowDeleteModal(false)} /> <DeleteAccountModal onClose={() => setShowDeleteModal(false)} />
)} )}
</div> </div>
</>
) )
} }

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { Sparkles, Send, Loader2, Flag } from 'lucide-react' import { Sparkles, Send, Loader2, Flag } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { assistantChatApi } from '@/api/assistantChat' import { assistantChatApi } from '@/api/assistantChat'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import { ChatSidebar } from '@/components/assistant/ChatSidebar' import { ChatSidebar } from '@/components/assistant/ChatSidebar'
@@ -179,6 +180,8 @@ export default function AssistantChatPage() {
} }
return ( return (
<>
<PageMeta title="AI Assistant" />
<div className="flex h-[calc(100vh-3.5rem)]"> <div className="flex h-[calc(100vh-3.5rem)]">
{/* Sidebar */} {/* Sidebar */}
<ChatSidebar <ChatSidebar
@@ -223,7 +226,7 @@ export default function AssistantChatPage() {
<div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center"> <div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center">
<Sparkles size={14} className="text-primary" /> <Sparkles size={14} className="text-primary" />
</div> </div>
<div className="bg-[rgba(255,255,255,0.04)] border border-brand-border rounded-2xl px-4 py-3"> <div className="bg-white/[0.04] border border-brand-border rounded-2xl px-4 py-3">
<Loader2 size={16} className="animate-spin text-primary" /> <Loader2 size={16} className="animate-spin text-primary" />
</div> </div>
</div> </div>
@@ -242,7 +245,7 @@ export default function AssistantChatPage() {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Ask about IT, networking, troubleshooting..." placeholder="Ask about IT, networking, troubleshooting..."
rows={3} rows={3}
className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]" className="flex-1 resize-none rounded-xl border bg-card text-foreground text-sm placeholder:text-muted-foreground px-4 py-3 focus:outline-hidden focus:border-primary/30"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
disabled={loading} disabled={loading}
/> />
@@ -303,5 +306,6 @@ export default function AssistantChatPage() {
chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'} chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'}
/> />
</div> </div>
</>
) )
} }

View File

@@ -99,7 +99,7 @@ export default function BatchStatusPage() {
const progressPercent = total > 0 ? Math.round((completed / total) * 100) : 0 const progressPercent = total > 0 ? Math.round((completed / total) * 100) : 0
return ( return (
<div className="container mx-auto max-w-3xl space-y-6 px-4 py-6 sm:px-6 sm:py-8"> <div className="overflow-y-auto h-full container mx-auto max-w-3xl space-y-6 px-4 py-6 sm:px-6 sm:py-8">
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<button <button

View File

@@ -1,5 +1,6 @@
import { useState, useRef, useEffect } from 'react' import { useState, useRef, useEffect } from 'react'
import { MessageSquareText, Send, CheckCircle2, ChevronDown } from 'lucide-react' import { MessageSquareText, Send, CheckCircle2, ChevronDown } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { useAuthStore } from '@/store/authStore' import { useAuthStore } from '@/store/authStore'
import { feedbackApi } from '@/api' import { feedbackApi } from '@/api'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -130,6 +131,8 @@ export function FeedbackPage() {
} }
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="Feedback" />
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
{/* Page header */} {/* Page header */}
<div className="mb-8"> <div className="mb-8">
@@ -273,6 +276,7 @@ export function FeedbackPage() {
)} )}
</div> </div>
</div> </div>
</div>
) )
} }

View File

@@ -2,6 +2,7 @@ import { useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { BrandLogo } from '@/components/common/BrandLogo' import { BrandLogo } from '@/components/common/BrandLogo'
import { PageMeta } from '@/components/common/PageMeta'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function ForgotPasswordPage() { export function ForgotPasswordPage() {
@@ -25,6 +26,8 @@ export function ForgotPasswordPage() {
} }
return ( return (
<>
<PageMeta title="Forgot Password" description="Reset your ResolutionFlow password" />
<div className="flex min-h-screen items-center justify-center bg-card 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="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
@@ -109,6 +112,7 @@ export function ForgotPasswordPage() {
)} )}
</div> </div>
</div> </div>
</>
) )
} }

View File

@@ -9,7 +9,7 @@ export default function GuideDetailPage() {
if (!guide) { if (!guide) {
return ( return (
<div className="flex flex-col items-center justify-center h-full text-center p-6"> <div className="flex flex-col items-center justify-center h-full text-center p-6 overflow-y-auto">
<h2 className="text-lg font-heading font-semibold text-foreground mb-2">Guide Not Found</h2> <h2 className="text-lg font-heading font-semibold text-foreground mb-2">Guide Not Found</h2>
<p className="text-sm text-muted-foreground mb-4">The guide you're looking for doesn't exist.</p> <p className="text-sm text-muted-foreground mb-4">The guide you're looking for doesn't exist.</p>
<Link <Link
@@ -25,7 +25,7 @@ export default function GuideDetailPage() {
const Icon = guide.icon const Icon = guide.icon
return ( return (
<div className="p-6 max-w-3xl mx-auto"> <div className="overflow-y-auto h-full p-6 max-w-3xl mx-auto">
{/* Breadcrumb */} {/* Breadcrumb */}
<nav className="flex items-center gap-1.5 text-xs text-muted-foreground mb-6"> <nav className="flex items-center gap-1.5 text-xs text-muted-foreground mb-6">
<Link to="/guides" className="hover:text-primary transition-colors"> <Link to="/guides" className="hover:text-primary transition-colors">

View File

@@ -1,9 +1,12 @@
import { BookOpen } from 'lucide-react' import { BookOpen } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { guides } from '@/data/guides' import { guides } from '@/data/guides'
import { GuideCard } from '@/components/guides/GuideCard' import { GuideCard } from '@/components/guides/GuideCard'
export default function GuidesHubPage() { export default function GuidesHubPage() {
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="Guides" />
<div className="p-6 max-w-5xl mx-auto"> <div className="p-6 max-w-5xl mx-auto">
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
@@ -25,5 +28,6 @@ export default function GuidesHubPage() {
))} ))}
</div> </div>
</div> </div>
</div>
) )
} }

View File

@@ -3,6 +3,7 @@ import { Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore' import { useAuthStore } from '@/store/authStore'
import { BrandLogo } from '@/components/common/BrandLogo' import { BrandLogo } from '@/components/common/BrandLogo'
import { PasswordInput } from '@/components/common/PasswordInput' import { PasswordInput } from '@/components/common/PasswordInput'
import { PageMeta } from '@/components/common/PageMeta'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function LoginPage() { export function LoginPage() {
@@ -40,6 +41,8 @@ export function LoginPage() {
} }
return ( return (
<>
<PageMeta title="Sign In" description="Sign in to your ResolutionFlow account" />
<div className="flex min-h-screen items-center justify-center bg-background px-4"> <div className="flex min-h-screen items-center justify-center bg-background px-4">
{/* Atmosphere orbs */} {/* Atmosphere orbs */}
<div <div
@@ -106,7 +109,7 @@ export function LoginPage() {
className={cn( className={cn(
'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5', 'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5',
'text-foreground placeholder:text-muted-foreground', 'text-foreground placeholder:text-muted-foreground',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20', 'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'transition-colors' 'transition-colors'
)} )}
placeholder="you@example.com" placeholder="you@example.com"
@@ -127,7 +130,7 @@ export function LoginPage() {
className={cn( className={cn(
'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5', 'block w-full rounded-[10px] border border-border bg-card px-3 py-2.5',
'text-foreground placeholder:text-muted-foreground', 'text-foreground placeholder:text-muted-foreground',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20', 'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20',
'transition-colors' 'transition-colors'
)} )}
placeholder="••••••••••" placeholder="••••••••••"
@@ -164,6 +167,7 @@ export function LoginPage() {
</form> </form>
</div> </div>
</div> </div>
</>
) )
} }

View File

@@ -124,7 +124,7 @@ export default function MaintenanceFlowDetailPage() {
const hasActiveBatch = recentSessions.some(s => s.started_at && !s.completed_at && s.batch_id) const hasActiveBatch = recentSessions.some(s => s.started_at && !s.completed_at && s.batch_id)
return ( return (
<div className="container mx-auto max-w-4xl space-y-6 px-4 py-6 sm:px-6 sm:py-8"> <div className="overflow-y-auto h-full container mx-auto max-w-4xl space-y-6 px-4 py-6 sm:px-6 sm:py-8">
{/* Active batch banner */} {/* Active batch banner */}
{hasActiveBatch && !bannerDismissed && ( {hasActiveBatch && !bannerDismissed && (
<ActiveBatchBanner <ActiveBatchBanner

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { Globe, Users, Copy, Check, Link2, ExternalLink, Trash2, ArrowLeft } from 'lucide-react' import { Globe, Users, Copy, Check, Link2, ExternalLink, Trash2, ArrowLeft } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Spinner } from '@/components/common/Spinner' import { Spinner } from '@/components/common/Spinner'
import { EmptyState } from '@/components/common/EmptyState' import { EmptyState } from '@/components/common/EmptyState'
@@ -121,6 +122,8 @@ export default function MySharesPage() {
} }
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="My Shares" />
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
{/* Back link */} {/* Back link */}
<Link <Link
@@ -241,5 +244,6 @@ export default function MySharesPage() {
confirmLabel="Revoke" confirmLabel="Revoke"
/> />
</div> </div>
</div>
) )
} }

View File

@@ -1,6 +1,8 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, Link } from 'react-router-dom' import { useNavigate, Link } from 'react-router-dom'
import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench } from 'lucide-react' import { Play, Pencil, Share2, Trash2, GitBranch, Clock, TrendingUp, FolderTree, Plus, ListOrdered, ChevronDown, Wrench } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { StaggerList } from '@/components/common/StaggerList'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions' import { sessionsApi } from '@/api/sessions'
@@ -116,6 +118,8 @@ export function MyTreesPage() {
} }
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="My Flows" />
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <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 className="mb-6 flex items-center justify-between sm:mb-8">
<div> <div>
@@ -214,7 +218,7 @@ export function MyTreesPage() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <StaggerList className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{trees.map((tree) => ( {trees.map((tree) => (
<div <div
key={tree.id} key={tree.id}
@@ -349,7 +353,7 @@ export function MyTreesPage() {
</div> </div>
</div> </div>
))} ))}
</div> </StaggerList>
)} )}
{/* Delete Confirmation */} {/* Delete Confirmation */}
@@ -389,6 +393,7 @@ export function MyTreesPage() {
)} )}
</div> </div>
</div>
) )
} }

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useMemo, useCallback } from 'react' import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react' import { Search, Loader2, Star, ChevronLeft, ChevronRight, GitBranch } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { sessionsApi } from '@/api/sessions' import { sessionsApi } from '@/api/sessions'
import type { TreeListItem, TreeFilters } from '@/types' import type { TreeListItem, TreeFilters } from '@/types'
@@ -277,6 +278,8 @@ export function QuickStartPage() {
] ]
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="Dashboard" />
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Greeting */} {/* Greeting */}
<div className="fade-in" style={{ animationDelay: '100ms' }}> <div className="fade-in" style={{ animationDelay: '100ms' }}>
@@ -646,6 +649,7 @@ export function QuickStartPage() {
)} )}
</div> </div>
</div>
) )
} }

View File

@@ -4,6 +4,7 @@ import { useAuthStore } from '@/store/authStore'
import { inviteApi } from '@/api/invite' import { inviteApi } from '@/api/invite'
import { BrandLogo } from '@/components/common/BrandLogo' import { BrandLogo } from '@/components/common/BrandLogo'
import { PasswordInput } from '@/components/common/PasswordInput' import { PasswordInput } from '@/components/common/PasswordInput'
import { PageMeta } from '@/components/common/PageMeta'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function RegisterPage() { export function RegisterPage() {
@@ -76,6 +77,8 @@ export function RegisterPage() {
} }
return ( return (
<>
<PageMeta title="Create Account" description="Create your ResolutionFlow account to start building guided troubleshooting flows" />
<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-black px-4">
{/* Subtle radial overlay */} {/* Subtle radial overlay */}
<div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" /> <div className="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
@@ -251,6 +254,7 @@ export function RegisterPage() {
</form> </form>
</div> </div>
</div> </div>
</>
) )
} }

View File

@@ -4,6 +4,7 @@ import { authApi } from '@/api/auth'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import { BrandLogo } from '@/components/common/BrandLogo' import { BrandLogo } from '@/components/common/BrandLogo'
import { PasswordInput } from '@/components/common/PasswordInput' import { PasswordInput } from '@/components/common/PasswordInput'
import { PageMeta } from '@/components/common/PageMeta'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function ResetPasswordPage() { export function ResetPasswordPage() {
@@ -72,6 +73,8 @@ export function ResetPasswordPage() {
} }
return ( return (
<>
<PageMeta title="Reset Password" description="Set a new password for your ResolutionFlow account" />
<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-black 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="pointer-events-none fixed inset-0 bg-[radial-gradient(circle_at_50%_0%,rgba(100,100,120,0.03),transparent_50%)]" />
@@ -180,6 +183,7 @@ export function ResetPasswordPage() {
)} )}
</div> </div>
</div> </div>
</>
) )
} }

View File

@@ -340,7 +340,7 @@ export function SessionDetailPage() {
const outcomeConfig = session.outcome ? OUTCOME_CONFIG[session.outcome] : null const outcomeConfig = session.outcome ? OUTCOME_CONFIG[session.outcome] : null
return ( return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <div className="overflow-y-auto h-full container mx-auto px-4 py-6 sm:px-6 sm:py-8">
{/* Back nav */} {/* Back nav */}
<button <button
onClick={() => navigate('/sessions')} onClick={() => navigate('/sessions')}

View File

@@ -1,5 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
import { StaggerList } from '@/components/common/StaggerList'
import { sessionsApi } from '@/api/sessions' import { sessionsApi } from '@/api/sessions'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import type { Session, TreeListItem } from '@/types' import type { Session, TreeListItem } from '@/types'
@@ -154,6 +156,8 @@ export function SessionHistoryPage() {
} }
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="Session History" />
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-2xl font-heading font-bold text-foreground sm:text-3xl">Session History</h1> <h1 className="text-2xl font-heading font-bold text-foreground sm:text-3xl">Session History</h1>
@@ -210,7 +214,8 @@ export function SessionHistoryPage() {
} }
/> />
) : ( ) : (
<div className="space-y-4"> <>
<StaggerList className="space-y-4">
{sessions.map((session) => ( {sessions.map((session) => (
<div <div
key={session.id} key={session.id}
@@ -298,18 +303,20 @@ export function SessionHistoryPage() {
</div> </div>
</div> </div>
))} ))}
{hasMore ? ( </StaggerList>
<p className="text-center text-sm text-muted-foreground py-4"> {hasMore ? (
Showing the 50 most recent sessions <p className="text-center text-sm text-muted-foreground py-4">
</p> Showing the 50 most recent sessions
) : sessions.length > 0 ? ( </p>
<p className="text-center text-sm text-muted-foreground py-4"> ) : sessions.length > 0 ? (
Showing all {sessions.length} sessions <p className="text-center text-sm text-muted-foreground py-4">
</p> Showing all {sessions.length} sessions
) : null} </p>
</div> ) : null}
</>
)} )}
</div> </div>
</div>
) )
} }

View File

@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { Bookmark, Trash2 } from 'lucide-react' import { Bookmark, Trash2 } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { useAuthStore } from '@/store/authStore' import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions' import { usePermissions } from '@/hooks/usePermissions'
@@ -87,6 +88,8 @@ export default function StepLibraryPage() {
} }
return ( return (
<>
<PageMeta title="Step Library" />
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{/* Page Header */} {/* Page Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-4"> <div className="flex items-center justify-between border-b border-border px-6 py-4">
@@ -167,5 +170,6 @@ export default function StepLibraryPage() {
)} )}
</div> </div>
</>
) )
} }

View File

@@ -274,13 +274,13 @@ export default function SurveyPage() {
return ( return (
<div className="min-h-screen bg-background text-foreground"> <div className="min-h-screen bg-background text-foreground">
<div className="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true"> <div className="pointer-events-none fixed inset-0 overflow-hidden" aria-hidden="true">
<div className="absolute -top-32 right-0 h-[500px] w-[500px] rounded-full opacity-[0.03]" style={{ background: 'radial-gradient(circle, #06b6d4, transparent 70%)' }} /> <div className="absolute -top-32 right-0 h-[500px] w-[500px] rounded-full opacity-[0.03]" style={{ background: 'radial-gradient(circle, var(--color-primary), transparent 70%)' }} />
<div className="absolute -bottom-32 left-0 h-[400px] w-[400px] rounded-full opacity-[0.02]" style={{ background: 'radial-gradient(circle, #a855f7, transparent 70%)' }} /> <div className="absolute -bottom-32 left-0 h-[400px] w-[400px] rounded-full opacity-[0.02]" style={{ background: 'radial-gradient(circle, #a855f7, transparent 70%)' }} />
</div> </div>
<div className="relative z-10 mx-auto max-w-[680px] px-4 sm:px-5"> <div className="relative z-10 mx-auto max-w-[680px] px-4 sm:px-5">
<div className="text-center pt-20 sm:pt-32 animate-fade-in-up"> <div className="text-center pt-20 sm:pt-32 animate-fade-in-up">
<div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(6, 182, 212, 0.1)' }}> <div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(6, 182, 212, 0.1)' }}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
</div> </div>
<h2 className="font-heading text-2xl font-bold mb-2.5">Already Submitted</h2> <h2 className="font-heading text-2xl font-bold mb-2.5">Already Submitted</h2>
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto leading-relaxed mb-3"> <p className="text-muted-foreground text-sm max-w-[440px] mx-auto leading-relaxed mb-3">
@@ -341,7 +341,7 @@ export default function SurveyPage() {
{/* Hero — visible only on first slide */} {/* Hero — visible only on first slide */}
{currentSlide === 0 && !isComplete && ( {currentSlide === 0 && !isComplete && (
<div className="text-center pt-10 pb-8 sm:pt-[72px] sm:pb-10 animate-fade-in-up"> <div className="text-center pt-10 pb-8 sm:pt-[72px] sm:pb-10 animate-fade-in-up">
<div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] sm:text-[11px] font-semibold font-label uppercase tracking-widest mb-4 sm:mb-5" style={{ background: 'rgba(6, 182, 212, 0.1)', border: '1px solid rgba(6, 182, 212, 0.15)', color: '#06b6d4' }}> <div className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] sm:text-[11px] font-semibold font-label uppercase tracking-widest mb-4 sm:mb-5" style={{ background: 'rgba(6, 182, 212, 0.1)', border: '1px solid rgba(6, 182, 212, 0.15)', color: 'var(--color-primary)' }}>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg> <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
FlowPilot Research FlowPilot Research
</div> </div>
@@ -353,15 +353,15 @@ export default function SurveyPage() {
</p> </p>
<div className="flex flex-wrap justify-center gap-4 sm:gap-7 mt-4 sm:mt-5 text-[11px] sm:text-[12px] text-muted-foreground"> <div className="flex flex-wrap justify-center gap-4 sm:gap-7 mt-4 sm:mt-5 text-[11px] sm:text-[12px] text-muted-foreground">
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
~5 minutes ~5 minutes
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
Confidential Confidential
</span> </span>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
16 questions 16 questions
</span> </span>
</div> </div>
@@ -376,7 +376,7 @@ export default function SurveyPage() {
key={i} key={i}
className="flex-1 h-1 sm:h-[3px] rounded-full transition-colors duration-300" className="flex-1 h-1 sm:h-[3px] rounded-full transition-colors duration-300"
style={{ style={{
background: i < currentSlide ? '#34d399' : i === currentSlide ? 'linear-gradient(90deg, #06b6d4, #22d3ee)' : 'var(--color-border)', background: i < currentSlide ? 'oklch(0.76 0.15 163)' : i === currentSlide ? 'linear-gradient(90deg, var(--color-primary), var(--color-brand-gradient-to))' : 'var(--color-border)',
}} }}
/> />
))} ))}
@@ -424,7 +424,7 @@ export default function SurveyPage() {
{isComplete && ( {isComplete && (
<div className="text-center pt-10 sm:pt-16 animate-fade-in-up"> <div className="text-center pt-10 sm:pt-16 animate-fade-in-up">
<div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)' }}> <div className="w-16 h-16 mx-auto mb-5 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)' }}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#34d399" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="oklch(0.76 0.15 163)" strokeWidth="2.5"><path d="M20 6L9 17l-5-5"/></svg>
</div> </div>
<h2 className="font-heading text-2xl font-bold mb-2.5">Response Submitted!</h2> <h2 className="font-heading text-2xl font-bold mb-2.5">Response Submitted!</h2>
<p className="text-muted-foreground text-sm max-w-[440px] mx-auto mb-6 sm:mb-8 leading-relaxed"> <p className="text-muted-foreground text-sm max-w-[440px] mx-auto mb-6 sm:mb-8 leading-relaxed">
@@ -445,7 +445,7 @@ export default function SurveyPage() {
placeholder="your@email.com" placeholder="your@email.com"
className="flex-1 rounded-[9px] px-3.5 py-2.5 text-sm text-foreground placeholder:text-brand-text-muted focus:outline-hidden" className="flex-1 rounded-[9px] px-3.5 py-2.5 text-sm text-foreground placeholder:text-brand-text-muted focus:outline-hidden"
style={{ background: 'rgba(16, 17, 20, 0.6)', border: '1px solid var(--glass-border)' }} style={{ background: 'rgba(16, 17, 20, 0.6)', border: '1px solid var(--glass-border)' }}
onFocus={e => { e.currentTarget.style.borderColor = 'rgba(6, 182, 212, 0.4)' }} onFocus={e => { e.currentTarget.style.borderColor = 'var(--color-primary)' }}
onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)' }} onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)' }}
disabled={emailSending} disabled={emailSending}
/> />
@@ -519,8 +519,8 @@ export default function SurveyPage() {
function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string; details: string } }) { function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string; details: string } }) {
return ( return (
<div className="rounded-[10px] p-3.5 px-4 sm:p-4 sm:px-5 mb-4 text-[13px]" style={{ background: 'linear-gradient(135deg, rgba(6, 182, 212, 0.06), rgba(139, 92, 246, 0.03))', border: '1px solid rgba(6, 182, 212, 0.12)' }}> <div className="rounded-[10px] p-3.5 px-4 sm:p-4 sm:px-5 mb-4 text-[13px]" style={{ background: 'linear-gradient(135deg, rgba(6, 182, 212, 0.06), rgba(139, 92, 246, 0.03))', border: '1px solid color-mix(in srgb, var(--color-primary) 12%, transparent)' }}>
<div className="font-label text-[10px] uppercase tracking-widest mb-2 font-semibold" style={{ color: '#06b6d4' }}>{scenario.title}</div> <div className="font-label text-[10px] uppercase tracking-widest mb-2 font-semibold" style={{ color: 'var(--color-primary)' }}>{scenario.title}</div>
<div className="sm:flex gap-2 mb-1"> <div className="sm:flex gap-2 mb-1">
<span className="text-muted-foreground font-medium whitespace-nowrap">Symptom:</span> <span className="text-muted-foreground font-medium whitespace-nowrap">Symptom:</span>
<span className="text-muted-foreground/80">{scenario.symptom}</span> <span className="text-muted-foreground/80">{scenario.symptom}</span>
@@ -535,8 +535,8 @@ function ScenarioBox({ scenario }: { scenario: { title: string; symptom: string;
function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQuestion; answer?: string | string[]; setAnswer: (id: string, val: string | string[]) => void }) { function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQuestion; answer?: string | string[]; setAnswer: (id: string, val: string | string[]) => void }) {
return ( return (
<div className="glass-card-static p-4 sm:p-7 mb-3 sm:mb-4 transition-[border-color] duration-200 focus-within:border-[rgba(6,182,212,0.25)]!"> <div className="glass-card-static p-4 sm:p-7 mb-3 sm:mb-4 transition-[border-color] duration-200 focus-within:border-primary/25!">
<div className="font-label text-[11px] mb-1.5 font-medium" style={{ color: '#06b6d4' }}>Q{q.num}</div> <div className="font-label text-[11px] mb-1.5 font-medium" style={{ color: 'var(--color-primary)' }}>Q{q.num}</div>
<div className="font-heading text-[14px] sm:text-[15px] font-semibold text-foreground leading-snug mb-1">{q.text}</div> <div className="font-heading text-[14px] sm:text-[15px] font-semibold text-foreground leading-snug mb-1">{q.text}</div>
{q.hint && <div className="text-[12px] text-muted-foreground mb-3 sm:mb-4 leading-snug">{q.hint}</div>} {q.hint && <div className="text-[12px] text-muted-foreground mb-3 sm:mb-4 leading-snug">{q.hint}</div>}
{!q.hint && <div className="mb-3 sm:mb-4" />} {!q.hint && <div className="mb-3 sm:mb-4" />}
@@ -550,12 +550,12 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none" className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none"
style={{ style={{
background: answer === opt ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)', background: answer === opt ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
border: `1px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}`, border: `1px solid ${answer === opt ? 'var(--color-primary)' : 'var(--glass-border)'}`,
color: answer === opt ? 'var(--color-foreground)' : 'var(--color-muted-foreground)', color: answer === opt ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
}} }}
> >
<div className="w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center transition-all duration-150 mt-0.5" style={{ border: `2px solid ${answer === opt ? '#06b6d4' : 'var(--glass-border)'}` }}> <div className="w-[18px] h-[18px] rounded-full shrink-0 flex items-center justify-center transition-all duration-150 mt-0.5" style={{ border: `2px solid ${answer === opt ? 'var(--color-primary)' : 'var(--glass-border)'}` }}>
{answer === opt && <div className="w-2 h-2 rounded-full" style={{ background: '#06b6d4' }} />} {answer === opt && <div className="w-2 h-2 rounded-full" style={{ background: 'var(--color-primary)' }} />}
</div> </div>
<span className="leading-snug">{opt}</span> <span className="leading-snug">{opt}</span>
</button> </button>
@@ -577,11 +577,11 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none" className="flex items-start gap-3 px-3.5 py-3 sm:px-4 rounded-[9px] text-left text-[13px] sm:text-sm transition-all duration-150 select-none"
style={{ style={{
background: selected ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)', background: selected ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
border: `1px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`, border: `1px solid ${selected ? 'var(--color-primary)' : 'var(--glass-border)'}`,
color: selected ? 'var(--color-foreground)' : 'var(--color-muted-foreground)', color: selected ? 'var(--color-foreground)' : 'var(--color-muted-foreground)',
}} }}
> >
<div className="w-[18px] h-[18px] rounded-[5px] shrink-0 flex items-center justify-center text-[11px] transition-all duration-150 mt-0.5" style={{ border: `2px solid ${selected ? '#06b6d4' : 'var(--glass-border)'}`, background: selected ? '#06b6d4' : 'transparent', color: selected ? 'white' : 'transparent' }}> <div className="w-[18px] h-[18px] rounded-[5px] shrink-0 flex items-center justify-center text-[11px] transition-all duration-150 mt-0.5" style={{ border: `2px solid ${selected ? 'var(--color-primary)' : 'var(--glass-border)'}`, background: selected ? 'var(--color-primary)' : 'transparent', color: selected ? 'white' : 'transparent' }}>
{'\u2713'} {'\u2713'}
</div> </div>
<span className="leading-snug">{opt}</span> <span className="leading-snug">{opt}</span>
@@ -605,7 +605,7 @@ function QuestionCard({ question: q, answer, setAnswer }: { question: SurveyQues
background: 'rgba(16, 17, 20, 0.6)', background: 'rgba(16, 17, 20, 0.6)',
border: '1px solid var(--glass-border)', border: '1px solid var(--glass-border)',
}} }}
onFocus={e => { e.currentTarget.style.borderColor = 'rgba(6, 182, 212, 0.4)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(6, 182, 212, 0.1)' }} onFocus={e => { e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.boxShadow = '0 0 0 3px rgba(6, 182, 212, 0.1)' }}
onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)'; e.currentTarget.style.boxShadow = 'none' }} onBlur={e => { e.currentTarget.style.borderColor = 'var(--glass-border)'; e.currentTarget.style.boxShadow = 'none' }}
/> />
)} )}
@@ -621,7 +621,7 @@ function RangeInput({ question: q, value, onChange }: { question: SurveyQuestion
const numVal = value ? parseInt(value) : q.min || 0 const numVal = value ? parseInt(value) : q.min || 0
return ( return (
<div className="py-2"> <div className="py-2">
<div className="text-center font-label text-2xl font-semibold mb-3" style={{ color: '#06b6d4' }}> <div className="text-center font-label text-2xl font-semibold mb-3" style={{ color: 'var(--color-primary)' }}>
{numVal}{q.suffix || ''} {numVal}{q.suffix || ''}
</div> </div>
<input <input
@@ -633,7 +633,7 @@ function RangeInput({ question: q, value, onChange }: { question: SurveyQuestion
onChange={e => onChange(e.target.value + (q.suffix || ''))} onChange={e => onChange(e.target.value + (q.suffix || ''))}
className="w-full h-2 sm:h-1 rounded-full appearance-none cursor-pointer touch-none" className="w-full h-2 sm:h-1 rounded-full appearance-none cursor-pointer touch-none"
style={{ style={{
background: `linear-gradient(to right, #06b6d4 0%, #06b6d4 ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) 100%)`, background: `linear-gradient(to right, var(--color-primary) 0%, var(--color-primary) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) ${((numVal - (q.min || 0)) / ((q.max || 10) - (q.min || 0))) * 100}%, var(--color-border) 100%)`,
}} }}
/> />
<div className="flex justify-between text-[11px] text-muted-foreground mt-2.5"> <div className="flex justify-between text-[11px] text-muted-foreground mt-2.5">
@@ -730,7 +730,7 @@ function DragRank({ items, onChange }: { items: string[]; onChange: (items: stri
className="flex items-center gap-2.5 sm:gap-3 px-3 py-3 sm:px-4 sm:py-2.5 rounded-[9px] text-[13px] sm:text-sm transition-all duration-150 select-none" className="flex items-center gap-2.5 sm:gap-3 px-3 py-3 sm:px-4 sm:py-2.5 rounded-[9px] text-[13px] sm:text-sm transition-all duration-150 select-none"
style={{ style={{
background: overIdx === idx ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)', background: overIdx === idx ? 'rgba(6, 182, 212, 0.1)' : 'rgba(16, 17, 20, 0.6)',
border: `1px solid ${overIdx === idx || draggingIdx === idx ? '#06b6d4' : 'var(--glass-border)'}`, border: `1px solid ${overIdx === idx || draggingIdx === idx ? 'var(--color-primary)' : 'var(--glass-border)'}`,
opacity: draggingIdx === idx ? 0.5 : 1, opacity: draggingIdx === idx ? 0.5 : 1,
cursor: 'grab', cursor: 'grab',
color: 'var(--color-muted-foreground)', color: 'var(--color-muted-foreground)',
@@ -739,7 +739,7 @@ function DragRank({ items, onChange }: { items: string[]; onChange: (items: stri
<div className="shrink-0 text-brand-text-muted"> <div className="shrink-0 text-brand-text-muted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></svg>
</div> </div>
<div className="font-label text-[11px] font-semibold w-5 text-center shrink-0" style={{ color: '#06b6d4' }}>{idx + 1}</div> <div className="font-label text-[11px] font-semibold w-5 text-center shrink-0" style={{ color: 'var(--color-primary)' }}>{idx + 1}</div>
<div className="flex-1 leading-snug">{item}</div> <div className="flex-1 leading-snug">{item}</div>
</div> </div>
))} ))}

View File

@@ -1,7 +1,10 @@
import { BrandLogo } from '@/components/common/BrandLogo' import { BrandLogo } from '@/components/common/BrandLogo'
import { PageMeta } from '@/components/common/PageMeta'
export default function SurveyThankYouPage() { export default function SurveyThankYouPage() {
return ( return (
<>
<PageMeta title="Thank You" description="Thank you for your feedback on ResolutionFlow" />
<div className="min-h-screen bg-background text-foreground"> <div className="min-h-screen bg-background text-foreground">
{/* Atmosphere orbs */} {/* Atmosphere orbs */}
<div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true"> <div className="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true">
@@ -29,7 +32,7 @@ export default function SurveyThankYouPage() {
<div className="text-center pt-[120px] animate-fade-in-up"> <div className="text-center pt-[120px] animate-fade-in-up">
{/* Success icon */} {/* Success icon */}
<div className="w-20 h-20 mx-auto mb-6 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)', border: '1px solid rgba(52, 211, 153, 0.15)' }}> <div className="w-20 h-20 mx-auto mb-6 rounded-full flex items-center justify-center" style={{ background: 'rgba(52, 211, 153, 0.1)', border: '1px solid rgba(52, 211, 153, 0.15)' }}>
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#34d399" strokeWidth="2"> <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="oklch(0.76 0.15 163)" strokeWidth="2">
<path d="M20 6L9 17l-5-5"/> <path d="M20 6L9 17l-5-5"/>
</svg> </svg>
</div> </div>
@@ -52,7 +55,7 @@ export default function SurveyThankYouPage() {
className="glass-card-static p-6 text-center max-w-[480px] mx-auto" className="glass-card-static p-6 text-center max-w-[480px] mx-auto"
> >
<div className="flex items-center justify-center gap-2 mb-3"> <div className="flex items-center justify-center gap-2 mb-3">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" strokeWidth="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg> </svg>
<span className="font-label text-[0.625rem] uppercase tracking-widest text-primary font-semibold"> <span className="font-label text-[0.625rem] uppercase tracking-widest text-primary font-semibold">
@@ -72,5 +75,6 @@ export default function SurveyThankYouPage() {
</div> </div>
</div> </div>
</div> </div>
</>
) )
} }

View File

@@ -1,6 +1,7 @@
import { useEffect, useState, useCallback, useMemo } from 'react' import { useEffect, useState, useCallback, useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import { X, RotateCcw, Play, FileUp } from 'lucide-react' import { X, RotateCcw, Play, FileUp } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { treesApi } from '@/api/trees' import { treesApi } from '@/api/trees'
import { categoriesApi } from '@/api/categories' import { categoriesApi } from '@/api/categories'
@@ -265,6 +266,8 @@ export function TreeLibraryPage() {
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
return ( return (
<div className="overflow-y-auto h-full">
<PageMeta title="Flow Library" />
<div className="min-h-full"> <div className="min-h-full">
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8"> <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 className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
@@ -596,6 +599,7 @@ export function TreeLibraryPage() {
/> />
)} )}
</div> </div>
</div>
) )
} }

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { useSearchParams, Link } from 'react-router-dom' import { useSearchParams, Link } from 'react-router-dom'
import { CheckCircle2, XCircle, Loader2 } from 'lucide-react' import { CheckCircle2, XCircle, Loader2 } from 'lucide-react'
import { authApi } from '@/api/auth' import { authApi } from '@/api/auth'
import { PageMeta } from '@/components/common/PageMeta'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function VerifyEmailPage() { export function VerifyEmailPage() {
@@ -23,6 +24,8 @@ export function VerifyEmailPage() {
}, [token]) }, [token])
return ( return (
<>
<PageMeta title="Verify Email" description="Verify your ResolutionFlow email address" />
<div className="flex min-h-screen items-center justify-center bg-background p-4"> <div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="glass-card-static w-full max-w-md p-8 text-center"> <div className="glass-card-static w-full max-w-md p-8 text-center">
{status === 'loading' && ( {status === 'loading' && (
@@ -55,8 +58,8 @@ export function VerifyEmailPage() {
<Link <Link
to="/" to="/"
className={cn( className={cn(
'mt-6 inline-flex items-center rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-6 py-2 text-sm font-medium text-foreground', 'mt-6 inline-flex items-center rounded-[10px] bg-white/[0.04] border border-brand-border px-6 py-2 text-sm font-medium text-foreground',
'hover:border-[rgba(255,255,255,0.12)]' 'hover:border-white/[0.12]'
)} )}
> >
Go to Dashboard Go to Dashboard
@@ -65,6 +68,7 @@ export function VerifyEmailPage() {
)} )}
</div> </div>
</div> </div>
</>
) )
} }

View File

@@ -73,7 +73,7 @@ export default function ChatRetentionSettingsPage() {
onChange={e => setRetentionDays(e.target.value)} onChange={e => setRetentionDays(e.target.value)}
min={1} min={1}
max={365} max={365}
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]" className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-primary/30"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
/> />
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
@@ -91,7 +91,7 @@ export default function ChatRetentionSettingsPage() {
onChange={e => setMaxCount(e.target.value)} onChange={e => setMaxCount(e.target.value)}
min={10} min={10}
max={10000} max={10000}
className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-[rgba(6,182,212,0.3)]" className="w-full rounded-xl border bg-card text-foreground text-sm px-4 py-2.5 focus:outline-hidden focus:border-primary/30"
style={{ borderColor: 'var(--glass-border)' }} style={{ borderColor: 'var(--glass-border)' }}
/> />
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">

View File

@@ -10,7 +10,7 @@ import type { UserUpdate } from '@/types'
const inputClass = cn( const inputClass = cn(
'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2', 'mt-1 block w-full rounded-[10px] border border-border bg-card px-3 py-2',
'text-foreground placeholder:text-muted-foreground', 'text-foreground placeholder:text-muted-foreground',
'focus:border-[rgba(6,182,212,0.3)] focus:outline-hidden focus:ring-1 focus:ring-primary/20' 'focus:border-primary/30 focus:outline-hidden focus:ring-1 focus:ring-primary/20'
) )
export function ProfileSettingsPage() { export function ProfileSettingsPage() {
@@ -145,8 +145,8 @@ export function ProfileSettingsPage() {
to="/change-password" to="/change-password"
className={cn( className={cn(
'inline-flex items-center rounded-[10px] px-4 py-2 text-sm font-medium', 'inline-flex items-center rounded-[10px] px-4 py-2 text-sm font-medium',
'bg-[rgba(255,255,255,0.04)] border border-brand-border text-foreground', 'bg-white/[0.04] border border-brand-border text-foreground',
'hover:border-[rgba(255,255,255,0.12)]' 'hover:border-white/[0.12]'
)} )}
> >
Change Password Change Password

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react' import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { cn } from '@/lib/utils' import { Input } from '@/components/ui/Input'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { PageHeader } from '@/components/common/PageHeader' import { PageHeader } from '@/components/common/PageHeader'
@@ -79,8 +79,6 @@ export function TeamCategoriesPage() {
setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' }) setForm({ name: cat.name, slug: cat.slug, description: cat.description || '' })
} }
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-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
@@ -141,15 +139,15 @@ export function TeamCategoriesPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1 block text-sm font-medium text-foreground">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} /> <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>
<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-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} /> <Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} />
</div> </div>
<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-foreground">Description</label>
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} /> <Input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" />
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -166,15 +164,15 @@ export function TeamCategoriesPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1 block text-sm font-medium text-foreground">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} /> <Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
</div> </div>
<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-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} className={inputCn} /> <Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} />
</div> </div>
<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-foreground">Description</label>
<input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" className={inputCn} /> <Input type="text" value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} placeholder="Optional" />
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, ToggleLeft } from 'lucide-react' import { Plus, Trash2, ToggleLeft } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin' import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin' import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
@@ -146,7 +147,7 @@ export function FeatureFlagsPage() {
}, },
] ]
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-hidden focus:border-primary focus:ring-2 focus:ring-primary/20') 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')
return ( return (
<div className="space-y-8"> <div className="space-y-8">
@@ -197,15 +198,15 @@ export function FeatureFlagsPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <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-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} /> <Input type="text" value={createForm.flag_key} onChange={(e) => setCreateForm({ ...createForm, flag_key: e.target.value })} placeholder="e.g. custom_branding" />
</div> </div>
<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-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} /> <Input type="text" value={createForm.display_name} onChange={(e) => setCreateForm({ ...createForm, display_name: e.target.value })} placeholder="e.g. Custom Branding" />
</div> </div>
<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-foreground">Description</label>
<input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" className={inputCn} /> <Input type="text" value={createForm.description ?? ''} onChange={(e) => setCreateForm({ ...createForm, description: e.target.value || null })} placeholder="Optional description" />
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -222,11 +223,11 @@ export function FeatureFlagsPage() {
<div className="space-y-4"> <div className="space-y-4">
<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-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} /> <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>
<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-foreground">Feature Flag</label>
<select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={inputCn}> <select value={overrideForm.flag_id} onChange={(e) => setOverrideForm({ ...overrideForm, flag_id: e.target.value })} className={selectClass}>
<option value="">Select a flag...</option> <option value="">Select a flag...</option>
{flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)} {flags.map(f => <option key={f.id} value={f.id}>{f.display_name}</option>)}
</select> </select>
@@ -237,7 +238,7 @@ export function FeatureFlagsPage() {
</div> </div>
<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-foreground">Note</label>
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" className={inputCn} /> <Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason" />
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -1,12 +1,12 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react' import { Plus, Trash2, Pencil, FolderTree } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin' import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin' import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { adminApi } from '@/api/admin' import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
import type { AdminCategory, GlobalCategoryCreate } from '@/types/admin' import type { AdminCategory, GlobalCategoryCreate } from '@/types/admin'
export function GlobalCategoriesPage() { export function GlobalCategoriesPage() {
@@ -88,8 +88,6 @@ export function GlobalCategoriesPage() {
}, },
] ]
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-hidden focus:border-primary focus:ring-2 focus:ring-primary/20')
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<PageHeader <PageHeader
@@ -127,15 +125,15 @@ export function GlobalCategoriesPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1 block text-sm font-medium text-foreground">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} /> <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>
<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-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} /> <Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
</div> </div>
<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-foreground">Description</label>
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} /> <Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -156,15 +154,15 @@ export function GlobalCategoriesPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1 block text-sm font-medium text-foreground">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} /> <Input type="text" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="e.g. Networking" />
</div> </div>
<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-foreground">Slug</label>
<input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" className={inputCn} /> <Input type="text" value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} placeholder="e.g. networking" />
</div> </div>
<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-foreground">Description</label>
<input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" className={inputCn} /> <Input type="text" value={form.description ?? ''} onChange={(e) => setForm({ ...form, description: e.target.value || null })} placeholder="Optional description" />
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react' import { Plus, Copy, Trash2, Ticket, Mail, MailCheck, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin' import { DataTable, PageHeader, StatusBadge, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin' import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
@@ -108,7 +109,7 @@ export function InviteCodesPage() {
} }
} }
const inputClass = cn( const selectClass = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', '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' 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
) )
@@ -254,12 +255,11 @@ export function InviteCodesPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <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-foreground">Recipient Email</label>
<input <Input
type="email" type="email"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="Optional — will send invite email" placeholder="Optional — will send invite email"
className={inputClass}
/> />
</div> </div>
@@ -273,7 +273,7 @@ export function InviteCodesPage() {
setAssignedPlan(plan) setAssignedPlan(plan)
if (plan === 'free') setTrialDays('') if (plan === 'free') setTrialDays('')
}} }}
className={inputClass} className={selectClass}
> >
{PLAN_OPTIONS.map(o => ( {PLAN_OPTIONS.map(o => (
<option key={o.value} value={o.value}>{o.label}</option> <option key={o.value} value={o.value}>{o.label}</option>
@@ -284,14 +284,13 @@ export function InviteCodesPage() {
{assignedPlan !== 'free' && ( {assignedPlan !== 'free' && (
<div> <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-foreground">Trial Duration (days)</label>
<input <Input
type="number" type="number"
value={trialDays} value={trialDays}
onChange={(e) => setTrialDays(e.target.value)} onChange={(e) => setTrialDays(e.target.value)}
placeholder="e.g. 14 (1-90)" placeholder="e.g. 14 (1-90)"
min={1} min={1}
max={90} max={90}
className={inputClass}
/> />
<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-muted-foreground">Leave empty for no trial account gets full plan immediately.</p>
</div> </div>
@@ -299,23 +298,21 @@ export function InviteCodesPage() {
<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-foreground">Expires in (days)</label>
<input <Input
type="number" type="number"
value={expiresInDays} value={expiresInDays}
onChange={(e) => setExpiresInDays(e.target.value)} onChange={(e) => setExpiresInDays(e.target.value)}
placeholder="Leave empty for no expiry" placeholder="Leave empty for no expiry"
className={inputClass}
/> />
</div> </div>
<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-foreground">Note</label>
<input <Input
type="text" type="text"
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
placeholder="Optional note (e.g. who this is for)" placeholder="Optional note (e.g. who this is for)"
className={inputClass}
/> />
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { Plus, Trash2, Gauge } from 'lucide-react' import { Plus, Trash2, Gauge } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin' import { DataTable, PageHeader, ActionMenu, EmptyState } from '@/components/admin'
import type { Column } from '@/components/admin' import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { adminApi } from '@/api/admin' import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils'
import type { PlanLimitConfig, AccountOverrideResponse, AccountOverrideCreate } from '@/types/admin' import type { PlanLimitConfig, AccountOverrideResponse, AccountOverrideCreate } from '@/types/admin'
export function PlanLimitsPage() { export function PlanLimitsPage() {
@@ -109,11 +109,6 @@ export function PlanLimitsPage() {
}, },
] ]
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-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
)
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" /> <PageHeader title="Plan Limits" description="Configure plan tier limits and account-specific overrides" />
@@ -161,15 +156,15 @@ export function PlanLimitsPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <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-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} /> <Input type="number" value={editPlan.max_trees ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_trees: e.target.value ? parseInt(e.target.value) : null })} />
</div> </div>
<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-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} /> <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>
<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-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} /> <Input type="number" value={editPlan.max_users ?? ''} onChange={(e) => setEditPlan({ ...editPlan, max_users: e.target.value ? parseInt(e.target.value) : null })} />
</div> </div>
</div> </div>
)} )}
@@ -191,23 +186,23 @@ export function PlanLimitsPage() {
<div className="space-y-4"> <div className="space-y-4">
<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-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} /> <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>
<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-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} /> <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>
<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-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} /> <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>
<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-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} /> <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>
<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-foreground">Note</label>
<input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" className={inputCn} /> <Input type="text" value={overrideForm.note ?? ''} onChange={(e) => setOverrideForm({ ...overrideForm, note: e.target.value || null })} placeholder="Reason for override" />
</div> </div>
</div> </div>
</Modal> </Modal>

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { PageHeader } from '@/components/admin' import { PageHeader } from '@/components/admin'
import { Textarea } from '@/components/ui/Textarea'
import { adminApi } from '@/api/admin' import { adminApi } from '@/api/admin'
import { toast } from '@/lib/toast' import { toast } from '@/lib/toast'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -93,15 +94,11 @@ export function SettingsPage() {
{maintenanceMode && ( {maintenanceMode && (
<div> <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-foreground">Maintenance Message</label>
<textarea <Textarea
value={maintenanceMessage} value={maintenanceMessage}
onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })} onChange={(e) => setSettings({ ...settings, maintenance_message: e.target.value })}
rows={3} rows={3}
placeholder="We're performing scheduled maintenance. Please check back later." placeholder="We're performing scheduled maintenance. Please check back later."
className={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'
)}
/> />
</div> </div>
)} )}

View File

@@ -82,7 +82,7 @@ export default function SurveyInvitesPage() {
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
placeholder="John Smith" 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-[rgba(6,182,212,0.3)] focus:outline-hidden" 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"
/> />
</div> </div>
<div className="flex-1"> <div className="flex-1">
@@ -94,7 +94,7 @@ export default function SurveyInvitesPage() {
value={email} value={email}
onChange={e => setEmail(e.target.value)} onChange={e => setEmail(e.target.value)}
placeholder="john@example.com" 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-[rgba(6,182,212,0.3)] focus:outline-hidden" 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"
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -109,7 +109,7 @@ export default function SurveyInvitesPage() {
<button <button
onClick={() => handleCreate(true)} onClick={() => handleCreate(true)}
disabled={creating || !name.trim() || !email.trim()} disabled={creating || !name.trim() || !email.trim()}
className="inline-flex items-center gap-2 rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-4 py-2 text-sm font-medium text-foreground hover:border-[rgba(255,255,255,0.12)] active:scale-[0.97] disabled:opacity-50 disabled:cursor-not-allowed transition-all" 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"
> >
{creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />} {creating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
Send Email Send Email
@@ -122,7 +122,7 @@ export default function SurveyInvitesPage() {
)} )}
{lastCreated && ( {lastCreated && (
<div className="mt-4 rounded-[10px] border border-[rgba(6,182,212,0.15)] bg-[rgba(6,182,212,0.04)] p-4"> <div className="mt-4 rounded-[10px] border border-primary/[0.15] bg-primary/[0.04] p-4">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-xs text-muted-foreground mb-1"> <p className="text-xs text-muted-foreground mb-1">
@@ -163,7 +163,7 @@ export default function SurveyInvitesPage() {
<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-muted-foreground">No invites yet</td></tr>
) : ( ) : (
invites.map(invite => ( invites.map(invite => (
<tr key={invite.id} className="border-b border-border/50 hover:bg-[rgba(255,255,255,0.02)] transition-colors"> <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-foreground">{invite.recipient_name}</td>
<td className="px-4 py-3 text-sm text-muted-foreground">{invite.recipient_email || '—'}</td> <td className="px-4 py-3 text-sm text-muted-foreground">{invite.recipient_email || '—'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">

View File

@@ -152,7 +152,7 @@ function ResponseRow({
className={cn( className={cn(
'border-b border-border/50 transition-colors cursor-pointer', 'border-b border-border/50 transition-colors cursor-pointer',
!response.is_read && 'bg-primary/3', !response.is_read && 'bg-primary/3',
'hover:bg-[rgba(255,255,255,0.02)]' 'hover:bg-white/[0.02]'
)} )}
> >
{/* Checkbox */} {/* Checkbox */}
@@ -227,14 +227,14 @@ function ResponseRow({
> >
<button <button
onClick={() => { onMarkRead(); setShowMenu(false) }} 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-[rgba(255,255,255,0.04)] transition-colors" 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"
> >
{response.is_read ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />} {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'} {response.is_read ? 'Mark Unread' : 'Mark Read'}
</button> </button>
<button <button
onClick={() => { onArchive(); setShowMenu(false) }} 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-[rgba(255,255,255,0.04)] transition-colors" 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"
> >
{response.archived_at ? <ArchiveRestore className="h-3.5 w-3.5" /> : <Archive className="h-3.5 w-3.5" />} {response.archived_at ? <ArchiveRestore className="h-3.5 w-3.5" /> : <Archive className="h-3.5 w-3.5" />}
{response.archived_at ? 'Unarchive' : 'Archive'} {response.archived_at ? 'Unarchive' : 'Archive'}
@@ -437,7 +437,7 @@ export default function SurveyResponsesPage() {
'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-[10px] px-3 py-2 text-xs font-medium transition-colors border',
showArchived showArchived
? 'bg-primary/10 text-primary border-primary/20' ? 'bg-primary/10 text-primary border-primary/20'
: 'bg-[rgba(255,255,255,0.04)] text-muted-foreground border-brand-border hover:border-[rgba(255,255,255,0.12)]' : 'bg-white/[0.04] text-muted-foreground border-brand-border hover:border-white/[0.12]'
)} )}
> >
<Archive className="h-3.5 w-3.5" /> <Archive className="h-3.5 w-3.5" />
@@ -446,7 +446,7 @@ export default function SurveyResponsesPage() {
<button <button
onClick={handleExport} onClick={handleExport}
disabled={exporting || responses.length === 0} disabled={exporting || responses.length === 0}
className="inline-flex items-center gap-2 rounded-[10px] bg-[rgba(255,255,255,0.04)] border border-brand-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-[rgba(255,255,255,0.12)] disabled:opacity-50" 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"
> >
{exporting ? ( {exporting ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { useParams, useNavigate } from 'react-router-dom' import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Shield, Crown, UserCheck, UserX, Clock, Ticket, KeyRound, Copy, Check, Archive, ArchiveRestore, Trash2 } from 'lucide-react' import { ArrowLeft, Shield, Crown, UserCheck, UserX, Clock, Ticket, KeyRound, Copy, Check, Archive, ArchiveRestore, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { StatusBadge } from '@/components/admin' import { StatusBadge } from '@/components/admin'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
import { Spinner } from '@/components/common/Spinner' import { Spinner } from '@/components/common/Spinner'
@@ -187,7 +188,7 @@ export function UserDetailPage() {
} }
} }
const inputClass = cn( const selectClass = cn(
'w-full rounded-md border border-border bg-card px-3 py-2 text-sm text-foreground', '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' 'placeholder:text-muted-foreground focus:outline-hidden focus:border-primary focus:ring-2 focus:ring-primary/20'
) )
@@ -534,7 +535,7 @@ export function UserDetailPage() {
aria-label="Subscription plan" aria-label="Subscription plan"
value={selectedPlan} value={selectedPlan}
onChange={(e) => setSelectedPlan(e.target.value)} onChange={(e) => setSelectedPlan(e.target.value)}
className={inputClass} className={selectClass}
> >
{PLAN_OPTIONS.map(p => ( {PLAN_OPTIONS.map(p => (
<option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option> <option key={p} value={p}>{p.charAt(0).toUpperCase() + p.slice(1)}</option>
@@ -646,13 +647,12 @@ export function UserDetailPage() {
> >
<div> <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-foreground">Days to add</label>
<input <Input
type="number" type="number"
value={trialDays} value={trialDays}
onChange={(e) => setTrialDays(e.target.value)} onChange={(e) => setTrialDays(e.target.value)}
min={1} min={1}
max={90} max={90}
className={inputClass}
/> />
<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-muted-foreground">1-90 days. Will convert to trialing status if not already.</p>
</div> </div>

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { UserCheck, UserX, Shield, ArrowRightLeft, ExternalLink, UserPlus, Copy, Check, Mail } from 'lucide-react' import { UserCheck, UserX, Shield, ArrowRightLeft, ExternalLink, UserPlus, Copy, Check, Mail } from 'lucide-react'
import { Button } from '@/components/ui/Button' import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { DataTable, Pagination, SearchInput, PageHeader, StatusBadge, ActionMenu } from '@/components/admin' import { DataTable, Pagination, SearchInput, PageHeader, StatusBadge, ActionMenu } from '@/components/admin'
import type { Column } from '@/components/admin' import type { Column } from '@/components/admin'
import { Modal } from '@/components/common/Modal' import { Modal } from '@/components/common/Modal'
@@ -361,15 +362,11 @@ export function UsersPage() {
</p> </p>
<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-foreground">Account Display Code</label>
<input <Input
type="text" type="text"
value={displayCode} value={displayCode}
onChange={(e) => setDisplayCode(e.target.value)} onChange={(e) => setDisplayCode(e.target.value)}
placeholder="e.g. ABC-1234" placeholder="e.g. ABC-1234"
className={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'
)}
/> />
</div> </div>
</div> </div>
@@ -393,28 +390,20 @@ export function UsersPage() {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="mb-1 block text-sm font-medium text-foreground">Name</label> <label className="mb-1 block text-sm font-medium text-foreground">Name</label>
<input <Input
type="text" type="text"
value={createForm.name} value={createForm.name}
onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))} onChange={(e) => setCreateForm(f => ({ ...f, name: e.target.value }))}
placeholder="Full name" placeholder="Full name"
className={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'
)}
/> />
</div> </div>
<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-foreground">Email</label>
<input <Input
type="email" type="email"
value={createForm.email} value={createForm.email}
onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))} onChange={(e) => setCreateForm(f => ({ ...f, email: e.target.value }))}
placeholder="user@example.com" placeholder="user@example.com"
className={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'
)}
/> />
</div> </div>
<div> <div>
@@ -435,15 +424,11 @@ 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-foreground">Account Display Code</label>
<input <Input
type="text" type="text"
value={createForm.account_display_code} value={createForm.account_display_code}
onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))} onChange={(e) => setCreateForm(f => ({ ...f, account_display_code: e.target.value }))}
placeholder="e.g. ABC12345" placeholder="e.g. ABC12345"
className={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'
)}
/> />
</div> </div>
<div> <div>
@@ -532,28 +517,20 @@ export function UsersPage() {
<div className="space-y-4"> <div className="space-y-4">
<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-foreground">Email</label>
<input <Input
type="email" type="email"
value={inviteForm.email} value={inviteForm.email}
onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))} onChange={(e) => setInviteForm(f => ({ ...f, email: e.target.value }))}
placeholder="user@example.com" placeholder="user@example.com"
className={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'
)}
/> />
</div> </div>
<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-foreground">Account Display Code</label>
<input <Input
type="text" type="text"
value={inviteForm.account_display_code} value={inviteForm.account_display_code}
onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))} onChange={(e) => setInviteForm(f => ({ ...f, account_display_code: e.target.value }))}
placeholder="e.g. ABC12345" placeholder="e.g. ABC12345"
className={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'
)}
/> />
</div> </div>
<div> <div>