feat: add mobile responsiveness, design consistency, and micro-interactions
- Add mobile hamburger menu with slide-out nav drawer (AppLayout) - Make modals responsive: full-width on mobile, slide-up animation - Scratchpad becomes full-screen overlay on mobile with backdrop - Folder sidebar hidden on mobile, opens as slide-over drawer - Tree editor shows "Desktop Required" gate on mobile - Stack action buttons vertically on mobile (sessions, detail pages) - Increase touch targets throughout (buttons, close icons) - Add CSS animations: fade-in, slide-in-left, scale-in, btn-press - Add card hover lift effect and consistent border highlights - Standardize page padding (px-4 py-6 sm:px-6 sm:py-8) - Responsive headings (text-2xl sm:text-3xl) - CustomStepModal goes full-screen on mobile - Tighten auth page spacing on mobile Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,13 +39,13 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
|||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'max-w-sm',
|
sm: 'max-w-sm',
|
||||||
md: 'max-w-md',
|
md: 'max-w-md',
|
||||||
lg: 'max-w-lg',
|
lg: 'max-w-full sm:max-w-lg',
|
||||||
xl: 'max-w-4xl',
|
xl: 'max-w-full sm:max-w-4xl',
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
className="fixed inset-0 z-50 flex items-end justify-center p-0 sm:items-center sm:p-4"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-title"
|
aria-labelledby="modal-title"
|
||||||
@@ -60,19 +60,21 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
|||||||
{/* Modal Content */}
|
{/* Modal Content */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex max-h-[85vh] w-full flex-col rounded-lg border border-border bg-card shadow-lg',
|
'relative flex w-full flex-col border border-border bg-card shadow-lg',
|
||||||
|
'max-h-[100vh] rounded-t-lg sm:max-h-[85vh] sm:rounded-lg',
|
||||||
|
'animate-scale-in',
|
||||||
sizeClasses[size]
|
sizeClasses[size]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header - Fixed at top */}
|
{/* Header - Fixed at top */}
|
||||||
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-6 py-4">
|
<div className="flex flex-shrink-0 items-center justify-between border-b border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||||
<h2 id="modal-title" className="text-lg font-semibold text-card-foreground">
|
<h2 id="modal-title" className="text-lg font-semibold text-card-foreground">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md p-1 text-muted-foreground transition-colors',
|
'rounded-md p-1.5 text-muted-foreground transition-colors sm:p-1',
|
||||||
'hover:bg-accent hover:text-accent-foreground',
|
'hover:bg-accent hover:text-accent-foreground',
|
||||||
'focus:outline-none focus:ring-2 focus:ring-ring'
|
'focus:outline-none focus:ring-2 focus:ring-ring'
|
||||||
)}
|
)}
|
||||||
@@ -83,13 +85,13 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md' }:
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body - Scrollable */}
|
{/* Body - Scrollable */}
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-4">
|
<div className="flex-1 overflow-y-auto px-4 py-4 sm:px-6">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - Fixed at bottom */}
|
{/* Footer - Fixed at bottom */}
|
||||||
{footer && (
|
{footer && (
|
||||||
<div className="flex-shrink-0 border-t border-border px-6 py-4">
|
<div className="flex-shrink-0 border-t border-border px-4 py-3 sm:px-6 sm:py-4">
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
|
||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
import { usePermissions } from '@/hooks/usePermissions'
|
import { usePermissions } from '@/hooks/usePermissions'
|
||||||
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
import { ThemeToggle } from '@/components/common/ThemeToggle'
|
||||||
import { BrandLogo } from '@/components/common/BrandLogo'
|
import { BrandLogo } from '@/components/common/BrandLogo'
|
||||||
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
import { BrandWordmark } from '@/components/common/BrandWordmark'
|
||||||
|
import { Menu, X } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function AppLayout() {
|
export function AppLayout() {
|
||||||
@@ -11,12 +13,39 @@ export function AppLayout() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { user, logout } = useAuthStore()
|
const { user, logout } = useAuthStore()
|
||||||
const { effectiveRole } = usePermissions()
|
const { effectiveRole } = usePermissions()
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
|
setMobileMenuOpen(false)
|
||||||
await logout()
|
await logout()
|
||||||
navigate('/login')
|
navigate('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close mobile menu on route change - key-based reset
|
||||||
|
const [prevPath, setPrevPath] = useState(location.pathname)
|
||||||
|
if (prevPath !== location.pathname) {
|
||||||
|
setPrevPath(location.pathname)
|
||||||
|
if (mobileMenuOpen) setMobileMenuOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close on Escape
|
||||||
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') setMobileMenuOpen(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mobileMenuOpen) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}, [mobileMenuOpen, handleKeyDown])
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/trees', label: 'Trees' },
|
{ path: '/trees', label: 'Trees' },
|
||||||
{ path: '/sessions', label: 'Sessions' },
|
{ path: '/sessions', label: 'Sessions' },
|
||||||
@@ -29,6 +58,15 @@ export function AppLayout() {
|
|||||||
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
<header className="sticky top-0 z-50 border-b border-border bg-card backdrop-blur-sm">
|
||||||
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
<div className="container mx-auto flex h-16 items-center justify-between px-4">
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-8">
|
||||||
|
{/* Mobile hamburger */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileMenuOpen(true)}
|
||||||
|
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
|
||||||
|
aria-label="Open menu"
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<Link to="/trees" className="flex items-center gap-2">
|
<Link to="/trees" className="flex items-center gap-2">
|
||||||
<BrandLogo size="sm" />
|
<BrandLogo size="sm" />
|
||||||
<BrandWordmark size="sm" />
|
<BrandWordmark size="sm" />
|
||||||
@@ -39,7 +77,7 @@ export function AppLayout() {
|
|||||||
key={item.path}
|
key={item.path}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
'relative rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||||
location.pathname.startsWith(item.path)
|
location.pathname.startsWith(item.path)
|
||||||
? 'bg-accent text-accent-foreground'
|
? 'bg-accent text-accent-foreground'
|
||||||
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||||
@@ -73,7 +111,7 @@ export function AppLayout() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md px-3 py-1.5 text-sm font-medium',
|
'hidden rounded-md px-3 py-1.5 text-sm font-medium sm:block',
|
||||||
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -83,8 +121,99 @@ export function AppLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* Mobile Nav Drawer */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 sm:hidden">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-background/80 backdrop-blur-sm animate-fade-in"
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<nav className="absolute inset-y-0 left-0 w-72 border-r border-border bg-card shadow-xl animate-slide-in-left">
|
||||||
|
<div className="flex h-16 items-center justify-between border-b border-border px-4">
|
||||||
|
<Link to="/trees" className="flex items-center gap-2">
|
||||||
|
<BrandLogo size="sm" />
|
||||||
|
<BrandWordmark size="sm" />
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||||
|
aria-label="Close menu"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col p-4">
|
||||||
|
{/* User info */}
|
||||||
|
<div className="mb-4 border-b border-border pb-4">
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{user?.name || user?.email}
|
||||||
|
</p>
|
||||||
|
{effectiveRole && effectiveRole !== 'engineer' && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'mt-1 inline-block rounded-full px-2 py-0.5 text-xs font-medium',
|
||||||
|
effectiveRole === 'super_admin' && 'bg-red-500/10 text-red-600 dark:text-red-400',
|
||||||
|
effectiveRole === 'team_admin' && 'bg-blue-500/10 text-blue-600 dark:text-blue-400',
|
||||||
|
effectiveRole === 'viewer' && 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{effectiveRole === 'super_admin' ? 'Super Admin' :
|
||||||
|
effectiveRole === 'team_admin' ? 'Team Admin' :
|
||||||
|
'Viewer'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav items */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={cn(
|
||||||
|
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
|
||||||
|
location.pathname.startsWith(item.path)
|
||||||
|
? 'bg-accent text-accent-foreground'
|
||||||
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme toggle */}
|
||||||
|
<div className="mt-4 border-t border-border pt-4">
|
||||||
|
<div className="flex items-center justify-between px-3 py-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Theme</span>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logout */}
|
||||||
|
<div className="mt-2">
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className={cn(
|
||||||
|
'w-full rounded-md px-3 py-2.5 text-left text-sm font-medium',
|
||||||
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main>
|
<main className="animate-fade-in">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Folder, ChevronDown, ChevronRight, Plus, MoreVertical, Pencil, Trash2, FolderPlus } from 'lucide-react'
|
import { Folder, ChevronDown, ChevronRight, Plus, MoreVertical, Pencil, Trash2, FolderPlus, X } from 'lucide-react'
|
||||||
import { foldersApi } from '@/api'
|
import { foldersApi } from '@/api'
|
||||||
import type { FolderListItem, FolderTreeItem } from '@/types'
|
import type { FolderListItem, FolderTreeItem } from '@/types'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
@@ -9,6 +9,8 @@ interface FolderSidebarProps {
|
|||||||
onFolderSelect: (folderId: string | null) => void
|
onFolderSelect: (folderId: string | null) => void
|
||||||
onCreateFolder: (parentId?: string | null) => void
|
onCreateFolder: (parentId?: string | null) => void
|
||||||
onEditFolder: (folder: FolderListItem) => void
|
onEditFolder: (folder: FolderListItem) => void
|
||||||
|
mobileOpen?: boolean
|
||||||
|
onMobileClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build tree structure from flat folder list
|
// Build tree structure from flat folder list
|
||||||
@@ -233,6 +235,8 @@ export function FolderSidebar({
|
|||||||
onFolderSelect,
|
onFolderSelect,
|
||||||
onCreateFolder,
|
onCreateFolder,
|
||||||
onEditFolder,
|
onEditFolder,
|
||||||
|
mobileOpen = false,
|
||||||
|
onMobileClose,
|
||||||
}: FolderSidebarProps) {
|
}: FolderSidebarProps) {
|
||||||
const [folders, setFolders] = useState<FolderListItem[]>([])
|
const [folders, setFolders] = useState<FolderListItem[]>([])
|
||||||
const [folderTree, setFolderTree] = useState<FolderTreeItem[]>([])
|
const [folderTree, setFolderTree] = useState<FolderTreeItem[]>([])
|
||||||
@@ -349,8 +353,33 @@ export function FolderSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-56 shrink-0 border-r border-border bg-card">
|
{/* Mobile backdrop */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm md:hidden"
|
||||||
|
onClick={onMobileClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={cn(
|
||||||
|
'w-56 shrink-0 border-r border-border bg-card',
|
||||||
|
'hidden md:block',
|
||||||
|
mobileOpen && 'fixed inset-y-0 left-0 z-50 block animate-slide-in-left md:relative md:animate-none'
|
||||||
|
)}>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
{/* Mobile close button */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<div className="mb-3 flex items-center justify-between md:hidden">
|
||||||
|
<span className="text-sm font-medium text-card-foreground">Folders</span>
|
||||||
|
<button
|
||||||
|
onClick={onMobileClose}
|
||||||
|
className="rounded-md p-1.5 text-muted-foreground hover:bg-accent"
|
||||||
|
aria-label="Close folders"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
className="flex w-full items-center gap-2 text-sm font-medium text-card-foreground"
|
className="flex w-full items-center gap-2 text-sm font-medium text-card-foreground"
|
||||||
|
|||||||
@@ -137,12 +137,22 @@ export function ScratchpadSidebar({ sessionId, initialContent, onSave, onOpenCha
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Mobile backdrop */}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-30 bg-background/80 backdrop-blur-sm sm:hidden"
|
||||||
|
onClick={() => setIsCollapsed(true)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Panel overlay */}
|
{/* Panel overlay */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed right-2 top-1/2 z-40 -translate-y-1/2',
|
'fixed z-40',
|
||||||
'flex h-[55vh] w-[420px] flex-col',
|
'inset-0 sm:inset-auto sm:right-2 sm:top-1/2 sm:-translate-y-1/2',
|
||||||
'rounded-lg border border-border bg-card shadow-xl',
|
'flex w-full flex-col sm:h-[55vh] sm:w-[420px]',
|
||||||
|
'border-border bg-card shadow-xl sm:rounded-lg sm:border',
|
||||||
'transition-transform duration-200 ease-out',
|
'transition-transform duration-200 ease-out',
|
||||||
isCollapsed ? 'translate-x-full' : 'translate-x-0'
|
isCollapsed ? 'translate-x-full' : 'translate-x-0'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ export function CustomStepModal({ isOpen, onClose, onInsertStep }: CustomStepMod
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-end justify-center bg-background/80 backdrop-blur-sm sm:items-center sm:p-4">
|
||||||
<div className="relative flex h-[90vh] w-full max-w-4xl flex-col rounded-lg border border-border bg-card shadow-lg">
|
<div className="relative flex h-[95vh] w-full max-w-full flex-col border border-border bg-card shadow-lg sm:h-[90vh] sm:max-w-4xl sm:rounded-lg">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b border-border p-4">
|
<div className="flex items-center justify-between border-b border-border p-4">
|
||||||
<h2 className="text-lg font-semibold">Add Custom Step</h2>
|
<h2 className="text-lg font-semibold">Add Custom Step</h2>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-md p-1 hover:bg-accent"
|
className="rounded-md p-1.5 hover:bg-accent"
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
>
|
>
|
||||||
<X className="h-5 w-5" />
|
<X className="h-5 w-5" />
|
||||||
|
|||||||
@@ -84,8 +84,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in-up {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-from-left {
|
||||||
|
from { transform: translateX(-100%); }
|
||||||
|
to { transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-from-bottom {
|
||||||
|
from { opacity: 0; transform: translateY(16px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale-in {
|
||||||
|
from { opacity: 0; transform: scale(0.95); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.text-gradient-brand {
|
.text-gradient-brand {
|
||||||
@apply bg-gradient-brand bg-clip-text text-transparent;
|
@apply bg-gradient-brand bg-clip-text text-transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fade-in-up 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-left {
|
||||||
|
animation: slide-in-from-left 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-bottom {
|
||||||
|
animation: slide-in-from-bottom 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-scale-in {
|
||||||
|
animation: scale-in 150ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button press feedback for primary action buttons */
|
||||||
|
.btn-press {
|
||||||
|
@apply active:scale-[0.98] transition-transform;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ export function LoginPage() {
|
|||||||
<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">
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-6 flex justify-center">
|
<div className="mb-4 flex justify-center sm:mb-6">
|
||||||
<BrandLogo size="lg" />
|
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<BrandWordmark size="lg" />
|
<BrandWordmark size="lg" />
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-lg font-medium text-gradient-brand">
|
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
|
||||||
Decision Tree Platform
|
Decision Tree Platform
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
|
||||||
Sign in to your account
|
Sign in to your account
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,11 +108,11 @@ export function LoginPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white',
|
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
|
||||||
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
||||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'transition-all shadow-lg shadow-primary/20'
|
'shadow-lg shadow-primary/20'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Signing in...' : 'Sign in'}
|
{isLoading ? 'Signing in...' : 'Sign in'}
|
||||||
|
|||||||
@@ -79,16 +79,16 @@ export function RegisterPage() {
|
|||||||
<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">
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="w-full max-w-md space-y-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-6 flex justify-center">
|
<div className="mb-4 flex justify-center sm:mb-6">
|
||||||
<BrandLogo size="lg" />
|
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<BrandWordmark size="lg" />
|
<BrandWordmark size="lg" />
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-lg font-medium text-gradient-brand">
|
<p className="mt-2 text-base font-medium text-gradient-brand sm:mt-3 sm:text-lg">
|
||||||
Decision Tree Platform
|
Decision Tree Platform
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground sm:mt-2">
|
||||||
Create your account
|
Create your account
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,11 +228,11 @@ export function RegisterPage() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white',
|
'w-full rounded-md px-4 py-2.5 text-sm font-semibold text-white btn-press',
|
||||||
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
'bg-gradient-brand hover:bg-gradient-brand-hover',
|
||||||
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
|
||||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'transition-all shadow-lg shadow-primary/20'
|
'shadow-lg shadow-primary/20'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Creating account...' : 'Create account'}
|
{isLoading ? 'Creating account...' : 'Create account'}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export function SessionDetailPage() {
|
|||||||
|
|
||||||
if (error || !session) {
|
if (error || !session) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||||
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
<div className="rounded-md bg-destructive/10 p-4 text-destructive">
|
||||||
{error || 'Session not found'}
|
{error || 'Session not found'}
|
||||||
</div>
|
</div>
|
||||||
@@ -129,75 +129,77 @@ export function SessionDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8 flex items-start justify-between">
|
<div className="mb-8">
|
||||||
<div>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<button
|
<div>
|
||||||
onClick={() => navigate('/sessions')}
|
<button
|
||||||
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
|
onClick={() => navigate('/sessions')}
|
||||||
>
|
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
|
||||||
← Back to sessions
|
|
||||||
</button>
|
|
||||||
<h1 className="text-3xl font-bold text-foreground">
|
|
||||||
{session.ticket_number || 'Session Details'}
|
|
||||||
</h1>
|
|
||||||
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-1',
|
|
||||||
session.completed_at ? 'text-green-600' : 'text-yellow-600'
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
|
← Back to sessions
|
||||||
|
</button>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">
|
||||||
|
{session.ticket_number || 'Session Details'}
|
||||||
|
</h1>
|
||||||
|
<div className="mt-2 flex items-center gap-4 text-sm text-muted-foreground">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-2 w-2 rounded-full',
|
'flex items-center gap-1',
|
||||||
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
|
session.completed_at ? 'text-green-600' : 'text-yellow-600'
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
{session.completed_at ? 'Completed' : 'In Progress'}
|
<span
|
||||||
</span>
|
className={cn(
|
||||||
{session.client_name && <span>Client: {session.client_name}</span>}
|
'h-2.5 w-2.5 rounded-full',
|
||||||
|
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{session.completed_at ? 'Completed' : 'In Progress'}
|
||||||
|
</span>
|
||||||
|
{session.client_name && <span>Client: {session.client_name}</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Export */}
|
{/* Export */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<select
|
<select
|
||||||
value={exportFormat}
|
value={exportFormat}
|
||||||
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
|
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
|
||||||
aria-label="Export format"
|
aria-label="Export format"
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border border-input bg-background px-3 py-2 text-sm',
|
'w-full rounded-md border border-input bg-background px-3 py-2 text-sm sm:w-auto',
|
||||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<option value="markdown">Markdown</option>
|
<option value="markdown">Markdown</option>
|
||||||
<option value="text">Plain Text</option>
|
<option value="text">Plain Text</option>
|
||||||
<option value="html">HTML</option>
|
<option value="html">HTML</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
disabled={isExporting}
|
disabled={isExporting}
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border border-input bg-background p-2 text-muted-foreground',
|
'rounded-md border border-input bg-background p-2 text-muted-foreground',
|
||||||
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
'hover:bg-accent hover:text-accent-foreground disabled:opacity-50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handlePreview}
|
onClick={handlePreview}
|
||||||
disabled={isExporting}
|
disabled={isExporting}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
'flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||||
'hover:bg-primary/90 disabled:opacity-50'
|
'hover:bg-primary/90 disabled:opacity-50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
{isExporting ? 'Loading...' : 'Preview'}
|
{isExporting ? 'Loading...' : 'Preview'}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export function SessionHistoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 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-3xl font-bold text-foreground">Session History</h1>
|
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Session History</h1>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
View and manage your troubleshooting sessions
|
View and manage your troubleshooting sessions
|
||||||
</p>
|
</p>
|
||||||
@@ -88,14 +88,14 @@ export function SessionHistoryPage() {
|
|||||||
{sessions.map((session) => (
|
{sessions.map((session) => (
|
||||||
<div
|
<div
|
||||||
key={session.id}
|
key={session.id}
|
||||||
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-shadow hover:shadow-md"
|
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-block h-2 w-2 rounded-full',
|
'inline-block h-2.5 w-2.5 rounded-full',
|
||||||
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
|
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -122,7 +122,7 @@ export function SessionHistoryPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/sessions/${session.id}`)}
|
onClick={() => navigate(`/sessions/${session.id}`)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border border-input px-3 py-1.5 text-sm font-medium',
|
'rounded-md border border-input px-3 py-2 text-sm font-medium',
|
||||||
'hover:bg-accent hover:text-accent-foreground'
|
'hover:bg-accent hover:text-accent-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -132,7 +132,7 @@ export function SessionHistoryPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
|
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
|
'rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||||
'hover:bg-primary/90'
|
'hover:bg-primary/90'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ export function SettingsPage() {
|
|||||||
const { theme } = useThemeStore()
|
const { theme } = useThemeStore()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 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">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Settings className="h-8 w-8 text-primary" />
|
<Settings className="h-8 w-8 text-primary" />
|
||||||
<h1 className="text-3xl font-bold text-foreground">Settings</h1>
|
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
Manage your application preferences
|
Manage your application preferences
|
||||||
@@ -22,14 +22,14 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
<div className="max-w-2xl space-y-6">
|
<div className="max-w-2xl space-y-6">
|
||||||
{/* Appearance Section */}
|
{/* Appearance Section */}
|
||||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||||
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
|
<h2 className="text-lg font-semibold text-card-foreground">Appearance</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Customize how ResolutionFlow looks on your device
|
Customize how ResolutionFlow looks on your device
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label className="block text-sm font-medium text-card-foreground">
|
<label className="block font-label text-sm font-medium text-card-foreground">
|
||||||
Theme
|
Theme
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -42,7 +42,7 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Export Preferences Section */}
|
{/* Export Preferences Section */}
|
||||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||||
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
|
<h2 className="text-lg font-semibold text-card-foreground">Export Preferences</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Configure default settings for session exports
|
Configure default settings for session exports
|
||||||
@@ -51,7 +51,7 @@ export function SettingsPage() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<label
|
<label
|
||||||
htmlFor="export-format"
|
htmlFor="export-format"
|
||||||
className="block text-sm font-medium text-card-foreground"
|
className="block font-label text-sm font-medium text-card-foreground"
|
||||||
>
|
>
|
||||||
Default Export Format
|
Default Export Format
|
||||||
</label>
|
</label>
|
||||||
@@ -76,7 +76,7 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* About Section */}
|
{/* About Section */}
|
||||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
|
<div className="rounded-lg border border-border bg-card p-4 shadow-sm sm:p-6">
|
||||||
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
|
<h2 className="text-lg font-semibold text-card-foreground">About</h2>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
ResolutionFlow - Decision Tree Platform
|
ResolutionFlow - Decision Tree Platform
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useParams, useNavigate, useBlocker } from 'react-router-dom'
|
import { useParams, useNavigate, useBlocker } from 'react-router-dom'
|
||||||
import { useStore } from 'zustand'
|
import { useStore } from 'zustand'
|
||||||
import { Undo2, Redo2, Save, CheckCircle2 } from 'lucide-react'
|
import { Undo2, Redo2, Save, CheckCircle2, Monitor } from 'lucide-react'
|
||||||
import { treesApi } from '@/api'
|
import { treesApi } from '@/api'
|
||||||
import type { TreeCreate, TreeUpdate } from '@/types'
|
import type { TreeCreate, TreeUpdate } from '@/types'
|
||||||
import { useTreeEditorStore, useTreeEditorTemporal } from '@/store/treeEditorStore'
|
import { useTreeEditorStore, useTreeEditorTemporal } from '@/store/treeEditorStore'
|
||||||
@@ -42,6 +42,15 @@ export function TreeEditorPage() {
|
|||||||
const [showDraftPrompt, setShowDraftPrompt] = useState(false)
|
const [showDraftPrompt, setShowDraftPrompt] = useState(false)
|
||||||
const [saveError, setSaveError] = useState<string | null>(null)
|
const [saveError, setSaveError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Mobile detection
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
const check = () => setIsMobile(window.innerWidth < 768)
|
||||||
|
check()
|
||||||
|
window.addEventListener('resize', check)
|
||||||
|
return () => window.removeEventListener('resize', check)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Calculate if there are blocking errors
|
// Calculate if there are blocking errors
|
||||||
const hasBlockingErrors = validationErrors.some(e => e.severity === 'error')
|
const hasBlockingErrors = validationErrors.some(e => e.severity === 'error')
|
||||||
|
|
||||||
@@ -203,17 +212,30 @@ export function TreeEditorPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mobile warning
|
// Mobile gate: show read-only message
|
||||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center px-6 text-center">
|
||||||
|
<Monitor className="mb-4 h-12 w-12 text-muted-foreground" />
|
||||||
|
<h2 className="mb-2 text-xl font-semibold text-foreground">Desktop Required</h2>
|
||||||
|
<p className="mb-6 max-w-sm text-sm text-muted-foreground">
|
||||||
|
The tree editor requires a larger screen for the best experience. Please open this page on a desktop or tablet in landscape mode.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/trees')}
|
||||||
|
className={cn(
|
||||||
|
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||||
|
'hover:bg-primary/90'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Back to Library
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)] flex-col">
|
<div className="flex h-[calc(100vh-4rem)] flex-col">
|
||||||
{/* Mobile Warning */}
|
|
||||||
{isMobile && (
|
|
||||||
<div className="bg-yellow-100 px-4 py-2 text-center text-sm text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-200">
|
|
||||||
Desktop recommended for tree editing. Viewing mode only on mobile.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Draft Restore Prompt */}
|
{/* Draft Restore Prompt */}
|
||||||
{showDraftPrompt && (
|
{showDraftPrompt && (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom'
|
import { useNavigate, Link } from 'react-router-dom'
|
||||||
import { Plus, Pencil, Globe, Lock, X, Trash2 } from 'lucide-react'
|
import { Plus, Pencil, Globe, Lock, X, Trash2, FolderOpen } from 'lucide-react'
|
||||||
import { treesApi, categoriesApi, foldersApi } from '@/api'
|
import { treesApi, categoriesApi, foldersApi } from '@/api'
|
||||||
import type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
|
import type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
|
||||||
import { TagBadges } from '@/components/common/TagBadges'
|
import { TagBadges } from '@/components/common/TagBadges'
|
||||||
@@ -29,6 +29,9 @@ export function TreeLibraryPage() {
|
|||||||
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
|
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
|
||||||
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null)
|
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Mobile folder sidebar state
|
||||||
|
const [mobileFolderOpen, setMobileFolderOpen] = useState(false)
|
||||||
|
|
||||||
// Delete confirmation state
|
// Delete confirmation state
|
||||||
const [treeToDelete, setTreeToDelete] = useState<TreeListItem | null>(null)
|
const [treeToDelete, setTreeToDelete] = useState<TreeListItem | null>(null)
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
|
||||||
@@ -152,17 +155,22 @@ export function TreeLibraryPage() {
|
|||||||
{/* Folder Sidebar */}
|
{/* Folder Sidebar */}
|
||||||
<FolderSidebar
|
<FolderSidebar
|
||||||
selectedFolderId={selectedFolderId}
|
selectedFolderId={selectedFolderId}
|
||||||
onFolderSelect={setSelectedFolderId}
|
onFolderSelect={(id) => {
|
||||||
|
setSelectedFolderId(id)
|
||||||
|
setMobileFolderOpen(false)
|
||||||
|
}}
|
||||||
onCreateFolder={handleCreateFolder}
|
onCreateFolder={handleCreateFolder}
|
||||||
onEditFolder={handleEditFolder}
|
onEditFolder={handleEditFolder}
|
||||||
|
mobileOpen={mobileFolderOpen}
|
||||||
|
onMobileClose={() => setMobileFolderOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
|
||||||
<div className="mb-8 flex items-start justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-foreground">Decision Trees</h1>
|
<h1 className="text-2xl font-bold text-foreground sm:text-3xl">Decision Trees</h1>
|
||||||
<p className="mt-2 text-muted-foreground">
|
<p className="mt-2 text-muted-foreground">
|
||||||
Select a troubleshooting tree to start a new session
|
Select a troubleshooting tree to start a new session
|
||||||
</p>
|
</p>
|
||||||
@@ -183,6 +191,18 @@ export function TreeLibraryPage() {
|
|||||||
|
|
||||||
{/* Search and Filter */}
|
{/* Search and Filter */}
|
||||||
<div className="mb-4 flex flex-col gap-4 sm:flex-row">
|
<div className="mb-4 flex flex-col gap-4 sm:flex-row">
|
||||||
|
{/* Mobile folder button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileFolderOpen(true)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium md:hidden',
|
||||||
|
'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
|
||||||
|
selectedFolderId && 'border-primary text-primary'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-4 w-4" />
|
||||||
|
Folders
|
||||||
|
</button>
|
||||||
<div className="flex flex-1 gap-2">
|
<div className="flex flex-1 gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -294,7 +314,7 @@ export function TreeLibraryPage() {
|
|||||||
{trees.map((tree) => (
|
{trees.map((tree) => (
|
||||||
<div
|
<div
|
||||||
key={tree.id}
|
key={tree.id}
|
||||||
className="rounded-lg border border-border bg-card p-6 shadow-sm transition-shadow hover:shadow-md"
|
className="rounded-lg border border-border bg-card p-4 shadow-sm transition-all hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-md sm:p-6"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-start justify-between gap-2">
|
<div className="mb-2 flex items-start justify-between gap-2">
|
||||||
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
|
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
|
||||||
@@ -336,7 +356,7 @@ export function TreeLibraryPage() {
|
|||||||
<Link
|
<Link
|
||||||
to={`/trees/${tree.id}/edit`}
|
to={`/trees/${tree.id}/edit`}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md border border-input p-1.5 text-muted-foreground',
|
'rounded-md border border-input p-2 text-muted-foreground',
|
||||||
'hover:bg-accent hover:text-accent-foreground'
|
'hover:bg-accent hover:text-accent-foreground'
|
||||||
)}
|
)}
|
||||||
title="Edit tree"
|
title="Edit tree"
|
||||||
@@ -364,7 +384,7 @@ export function TreeLibraryPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleStartSession(tree.id)}
|
onClick={() => handleStartSession(tree.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
|
'rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
|
||||||
'hover:bg-primary/90'
|
'hover:bg-primary/90'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ export function TreeNavigationPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-4rem)]">
|
<div className="h-[calc(100vh-4rem)]">
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className={cn('h-full overflow-y-auto px-4 py-8 transition-[padding] duration-200', scratchpadOpen && 'pr-[440px]')}>
|
<div className={cn('h-full overflow-y-auto px-4 py-8 transition-[padding] duration-200', scratchpadOpen && 'sm:pr-[440px]')}>
|
||||||
<div className="mx-auto max-w-4xl">
|
<div className="mx-auto max-w-4xl">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
@@ -659,7 +659,7 @@ export function TreeNavigationPage() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/sessions')}
|
onClick={() => navigate('/sessions')}
|
||||||
className="text-sm text-muted-foreground hover:text-foreground"
|
className="rounded-md px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||||
>
|
>
|
||||||
Exit
|
Exit
|
||||||
</button>
|
</button>
|
||||||
@@ -753,9 +753,10 @@ export function TreeNavigationPage() {
|
|||||||
{/* Add Custom Step Button */}
|
{/* Add Custom Step Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCustomStepModal(true)}
|
onClick={() => setShowCustomStepModal(true)}
|
||||||
className="mt-2 text-sm text-primary hover:underline"
|
className="mt-2 inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-sm text-primary hover:bg-primary/10"
|
||||||
>
|
>
|
||||||
+ Add Custom Step
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
Add Custom Step
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user