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

@@ -2,7 +2,7 @@ import { Outlet } from 'react-router-dom'
export function AccountLayout() {
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 />
</div>
)

View File

@@ -69,7 +69,7 @@ export function DeleteAccountModal({ onClose }: Props) {
onClick={onClose}
className={cn(
'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

View File

@@ -45,7 +45,7 @@ export function LeaveAccountModal({ accountName, onClose }: Props) {
onClick={onClose}
className={cn(
'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

View File

@@ -90,7 +90,7 @@ export function TransferOwnershipModal({ members, onClose, onTransferred }: Prop
onClick={onClose}
className={cn(
'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

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface CreateCategoryModalProps {
isOpen: boolean
@@ -93,7 +94,7 @@ export function CreateCategoryModal({
<label htmlFor="name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-red-400">*</span>
</label>
<input
<Input
id="name"
type="text"
value={name}
@@ -102,12 +103,6 @@ export function CreateCategoryModal({
maxLength={100}
placeholder="e.g., Network Troubleshooting"
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">
{name.length}/100 characters
@@ -118,19 +113,13 @@ export function CreateCategoryModal({
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={isSaving}
rows={3}
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>
</form>

View File

@@ -1,8 +1,9 @@
import { useState } from 'react'
import { cn } from '@/lib/utils'
import type { StepCategoryListItem } from '@/types'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface EditCategoryModalProps {
isOpen: boolean
@@ -105,7 +106,7 @@ export function EditCategoryModal({
<label htmlFor="edit-name" className="mb-1 block text-sm font-medium text-foreground">
Category Name <span className="text-red-400">*</span>
</label>
<input
<Input
id="edit-name"
type="text"
value={name}
@@ -114,12 +115,6 @@ export function EditCategoryModal({
maxLength={100}
placeholder="e.g., Network Troubleshooting"
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">
{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">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="edit-description"
value={description}
onChange={(e) => setDescription(e.target.value)}
disabled={isSaving}
rows={3}
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>
</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 ${
role === 'assistant'
? '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} />}
@@ -29,7 +29,7 @@ export function ChatMessage({ role, content, suggestedFlows }: ChatMessageProps)
className={`rounded-2xl px-4 py-3 text-[0.875rem] leading-relaxed ${
role === 'user'
? '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" />

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',
isActive
? '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" />
@@ -116,14 +116,14 @@ function ChatItem({
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
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'}
>
<Pin size={12} className={chat.pinned ? 'text-primary' : ''} />
</button>
<button
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"
>
<Trash2 size={12} />

View File

@@ -233,8 +233,8 @@ export function ConcludeSessionModal({
className={cn(
'w-full flex items-center gap-4 p-4 rounded-xl border transition-all text-left',
'hover:scale-[1.01] active:scale-[0.99]',
'bg-[rgba(255,255,255,0.02)] border-brand-border',
'hover:border-[rgba(255,255,255,0.12)] hover:bg-[rgba(255,255,255,0.04)]'
'bg-white/[0.02] border-brand-border',
'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)}>
@@ -282,7 +282,7 @@ export function ConcludeSessionModal({
: 'What still needs to be done, where you left off...'
}
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)' }}
/>
</div>
@@ -308,7 +308,7 @@ export function ConcludeSessionModal({
{/* Generated summary */}
<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)' }}
>
<div className="flex items-center justify-between mb-3">
@@ -335,7 +335,7 @@ export function ConcludeSessionModal({
<div />
<button
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
</button>
@@ -346,7 +346,7 @@ export function ConcludeSessionModal({
<>
<button
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
</button>
@@ -407,7 +407,7 @@ export function ConcludeSessionModal({
</button>
<button
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
</button>

View File

@@ -18,7 +18,7 @@ export function SuggestedFlowCard({ flow }: SuggestedFlowCardProps) {
return (
<button
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">
<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 ${
msg.role === 'user'
? '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" />
@@ -131,7 +131,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
))}
{loading && (
<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" />
</div>
</div>
@@ -162,7 +162,7 @@ export function CopilotPanel({ isOpen, onClose, treeId, sessionId, currentNodeId
onKeyDown={handleKeyDown}
placeholder="Ask about this step..."
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)' }}
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"`}
rows={4}
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
/>
@@ -83,7 +83,7 @@ export function AIPromptDialog({
<button
onClick={onClose}
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
</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 ${
msg.role === 'user'
? '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" />
@@ -59,7 +59,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
))}
{isLoading && (
<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" />
</div>
</div>
@@ -77,7 +77,7 @@ export function ChatTab({ messages, input, onInputChange, onSend, isLoading }: C
onKeyDown={handleKeyDown}
placeholder="Ask AI to help..."
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)' }}
disabled={isLoading}
/>

View File

@@ -26,7 +26,7 @@ export function SuggestionsTab({ suggestions }: SuggestionsTabProps) {
const config = STATUS_CONFIG[s.status]
const StatusIcon = config.icon
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">
<span className="font-label text-[0.625rem] uppercase tracking-widest text-muted-foreground">
{s.action_type.replace(/_/g, ' ')}

View File

@@ -1,5 +1,5 @@
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 { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
@@ -8,6 +8,7 @@ import { BrandLogo } from '@/components/common/BrandLogo'
import { TopBar } from './TopBar'
import { Sidebar } from './Sidebar'
import { EmailVerificationBanner } from './EmailVerificationBanner'
import { ViewTransitionOutlet } from './ViewTransitionOutlet'
import { cn } from '@/lib/utils'
export function AppLayout() {
@@ -183,9 +184,9 @@ export function AppLayout() {
)}
{/* Main Content */}
<main className="main-content">
<main className="main-content flex flex-col overflow-hidden">
<EmailVerificationBanner />
<Outlet />
<ViewTransitionOutlet />
</main>
</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',
isDragging
? '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()}
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 type { TreeListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { StaggerList } from '@/components/common/StaggerList'
import { cn } from '@/lib/utils'
import { usePermissions } from '@/hooks/usePermissions'
import { getTreeEditorPath } from '@/lib/routing'
@@ -33,7 +34,7 @@ export function TreeGridView({
const { canEditTree, canDeleteTree } = usePermissions()
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) => (
<div
key={tree.id}
@@ -182,6 +183,6 @@ export function TreeGridView({
</div>
</div>
))}
</div>
</StaggerList>
)
}

View File

@@ -213,7 +213,7 @@ export function IntakeFormModal({ isOpen, fields, treeName, onSubmit, onCancel }
return (
<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 */}
<div className="border-b border-border px-6 py-4">
<h2 className="text-lg font-semibold text-foreground">Project Information</h2>

View File

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

View File

@@ -1,7 +1,8 @@
import { useState } from 'react'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import { Textarea } from '@/components/ui/Textarea'
interface SaveSessionAsTreeModalProps {
isOpen: boolean
@@ -60,7 +61,7 @@ export function SaveSessionAsTreeModal({
<label htmlFor="treeName" className="mb-1 block text-sm font-medium text-foreground">
Tree Name <span className="text-muted-foreground">(optional)</span>
</label>
<input
<Input
id="treeName"
type="text"
value={treeName}
@@ -68,12 +69,6 @@ export function SaveSessionAsTreeModal({
placeholder={defaultTreeName || "Auto-generated if left blank"}
disabled={isSaving}
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>
@@ -82,19 +77,13 @@ export function SaveSessionAsTreeModal({
<label htmlFor="description" className="mb-1 block text-sm font-medium text-foreground">
Description <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add a description for this tree"
disabled={isSaving}
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>

View File

@@ -160,7 +160,7 @@ export function SessionFilters({ filters, onChange, onClear, trees }: SessionFil
</button>
{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 */}
<div className="mb-3 flex gap-2">
<button

View File

@@ -8,6 +8,7 @@ import { toast } from '@/lib/toast'
import { Spinner } from '@/components/common/Spinner'
import { Modal } from '@/components/common/Modal'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
interface ShareSessionModalProps {
sessionId: string
@@ -246,15 +247,11 @@ export function ShareSessionModal({ sessionId, sessionLabel, isOpen, onClose }:
<label className="mb-2 block text-sm font-medium text-foreground">
Share Name <span className="text-muted-foreground">(optional)</span>
</label>
<input
<Input
type="text"
value={shareName}
onChange={(e) => setShareName(e.target.value.slice(0, 100))}
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}
/>
</div>

View File

@@ -4,6 +4,7 @@ import { StarRating } from '@/components/common/StarRating'
import { cn } from '@/lib/utils'
import type { Step } from '@/types'
import { Button } from '@/components/ui/Button'
import { Textarea } from '@/components/ui/Textarea'
interface StepRatingData {
rating: number
@@ -103,7 +104,7 @@ export function StepRatingModal({
{librarySteps.map((step) => {
const rating = getRating(step.id)
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 */}
<h3 className="font-medium text-foreground">{step.title}</h3>
<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">
Review <span className="text-muted-foreground">(optional)</span>
</label>
<textarea
<Textarea
id={`review-${step.id}`}
value={rating?.review || ''}
onChange={(e) => handleReviewChange(step.id, e.target.value)}
@@ -172,12 +173,6 @@ export function StepRatingModal({
maxLength={500}
rows={2}
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">
{rating?.review?.length || 0}/500

View File

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

View File

@@ -10,7 +10,7 @@ const buttonVariants = cva(
primary:
'bg-gradient-brand text-brand-dark font-semibold shadow-lg shadow-primary/20 hover:opacity-90',
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:
'bg-red-400/10 text-red-400 border border-red-400/20 hover:bg-red-400/20',
ghost:
@@ -60,4 +60,5 @@ export function Button({
)
}
// eslint-disable-next-line react-refresh/only-export-components
export { buttonVariants }

View File

@@ -12,7 +12,7 @@ export function Input({ className, error, id, ...props }: InputProps) {
className={cn(
'flex h-9 w-full rounded-md 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 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',
error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20',
className

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils'
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
type SkeletonProps = React.HTMLAttributes<HTMLDivElement>
export function Skeleton({ className, ...props }: SkeletonProps) {
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 }) {
return (
<div className="flex items-center gap-4 px-4 py-3">
@@ -34,7 +36,7 @@ export function TableRowSkeleton({ cols = 4 }: { cols?: number }) {
<Skeleton
key={i}
className="h-4"
style={{ width: `${20 + Math.random() * 30}%` }}
style={{ width: COL_WIDTHS[i % COL_WIDTHS.length] }}
/>
))}
</div>
@@ -45,7 +47,7 @@ export function ListSkeleton({ count = 5, className }: { count?: number; classNa
return (
<div className={cn('space-y-3', className)}>
{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" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-2/3" />

View File

@@ -12,7 +12,7 @@ export function Textarea({ className, error, id, ...props }: TextareaProps) {
className={cn(
'flex w-full rounded-md 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 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',
error && 'border-red-400/50 focus:border-red-400 focus:ring-red-400/20',
className