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:
chihlasm
2026-02-06 01:58:39 -05:00
parent cf6d8bd57b
commit 90ff25003d
14 changed files with 395 additions and 129 deletions

View File

@@ -38,16 +38,16 @@ export function LoginPage() {
<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="text-center">
<div className="mb-6 flex justify-center">
<BrandLogo size="lg" />
<div className="mb-4 flex justify-center sm:mb-6">
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
</div>
<h1>
<BrandWordmark size="lg" />
</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
</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
</p>
</div>
@@ -108,11 +108,11 @@ export function LoginPage() {
type="submit"
disabled={isLoading}
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',
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all shadow-lg shadow-primary/20'
'shadow-lg shadow-primary/20'
)}
>
{isLoading ? 'Signing in...' : 'Sign in'}

View File

@@ -79,16 +79,16 @@ export function RegisterPage() {
<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="text-center">
<div className="mb-6 flex justify-center">
<BrandLogo size="lg" />
<div className="mb-4 flex justify-center sm:mb-6">
<BrandLogo size="lg" className="h-12 w-12 sm:h-16 sm:w-16" />
</div>
<h1>
<BrandWordmark size="lg" />
</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
</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
</p>
</div>
@@ -228,11 +228,11 @@ export function RegisterPage() {
type="submit"
disabled={isLoading}
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',
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
'transition-all shadow-lg shadow-primary/20'
'shadow-lg shadow-primary/20'
)}
>
{isLoading ? 'Creating account...' : 'Create account'}

View File

@@ -114,7 +114,7 @@ export function SessionDetailPage() {
if (error || !session) {
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">
{error || 'Session not found'}
</div>
@@ -129,75 +129,77 @@ export function SessionDetailPage() {
}
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 */}
<div className="mb-8 flex items-start justify-between">
<div>
<button
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'
)}
<div className="mb-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<button
onClick={() => navigate('/sessions')}
className="mb-2 text-sm text-muted-foreground hover:text-foreground"
>
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
className={cn(
'h-2 w-2 rounded-full',
session.completed_at ? 'bg-green-500' : 'bg-yellow-500'
'flex items-center gap-1',
session.completed_at ? 'text-green-600' : 'text-yellow-600'
)}
/>
{session.completed_at ? 'Completed' : 'In Progress'}
</span>
{session.client_name && <span>Client: {session.client_name}</span>}
>
<span
className={cn(
'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>
{/* Export */}
<div className="flex items-center gap-2">
<select
value={exportFormat}
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
aria-label="Export format"
className={cn(
'rounded-md border border-input bg-background px-3 py-2 text-sm',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
)}
>
<option value="markdown">Markdown</option>
<option value="text">Plain Text</option>
<option value="html">HTML</option>
</select>
<button
onClick={handleCopy}
disabled={isExporting}
title="Copy to clipboard"
className={cn(
'rounded-md border border-input bg-background p-2 text-muted-foreground',
'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" />}
</button>
<button
onClick={handlePreview}
disabled={isExporting}
className={cn(
'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'
)}
>
<Eye className="h-4 w-4" />
{isExporting ? 'Loading...' : 'Preview'}
</button>
{/* Export */}
<div className="flex items-center gap-2">
<select
value={exportFormat}
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
aria-label="Export format"
className={cn(
'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'
)}
>
<option value="markdown">Markdown</option>
<option value="text">Plain Text</option>
<option value="html">HTML</option>
</select>
<button
onClick={handleCopy}
disabled={isExporting}
title="Copy to clipboard"
className={cn(
'rounded-md border border-input bg-background p-2 text-muted-foreground',
'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" />}
</button>
<button
onClick={handlePreview}
disabled={isExporting}
className={cn(
'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'
)}
>
<Eye className="h-4 w-4" />
{isExporting ? 'Loading...' : 'Preview'}
</button>
</div>
</div>
</div>

View File

@@ -35,9 +35,9 @@ export function SessionHistoryPage() {
}
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">
<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">
View and manage your troubleshooting sessions
</p>
@@ -88,14 +88,14 @@ export function SessionHistoryPage() {
{sessions.map((session) => (
<div
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 className="flex items-center gap-2">
<span
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'
)}
/>
@@ -122,7 +122,7 @@ export function SessionHistoryPage() {
<button
onClick={() => navigate(`/sessions/${session.id}`)}
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'
)}
>
@@ -132,7 +132,7 @@ export function SessionHistoryPage() {
<button
onClick={() => navigate(`/trees/${session.tree_id}/navigate`, { state: { sessionId: session.id } })}
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'
)}
>

View File

@@ -9,11 +9,11 @@ export function SettingsPage() {
const { theme } = useThemeStore()
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="flex items-center gap-3">
<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>
<p className="mt-2 text-muted-foreground">
Manage your application preferences
@@ -22,14 +22,14 @@ export function SettingsPage() {
<div className="max-w-2xl space-y-6">
{/* 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>
<p className="mt-1 text-sm text-muted-foreground">
Customize how ResolutionFlow looks on your device
</p>
<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
</label>
<p className="text-sm text-muted-foreground">
@@ -42,7 +42,7 @@ export function SettingsPage() {
</div>
{/* 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>
<p className="mt-1 text-sm text-muted-foreground">
Configure default settings for session exports
@@ -51,7 +51,7 @@ export function SettingsPage() {
<div className="mt-4">
<label
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
</label>
@@ -76,7 +76,7 @@ export function SettingsPage() {
</div>
{/* 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>
<p className="mt-1 text-sm text-muted-foreground">
ResolutionFlow - Decision Tree Platform

View File

@@ -1,7 +1,7 @@
import { useEffect, useState, useCallback } from 'react'
import { useParams, useNavigate, useBlocker } from 'react-router-dom'
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 type { TreeCreate, TreeUpdate } from '@/types'
import { useTreeEditorStore, useTreeEditorTemporal } from '@/store/treeEditorStore'
@@ -42,6 +42,15 @@ export function TreeEditorPage() {
const [showDraftPrompt, setShowDraftPrompt] = useState(false)
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
const hasBlockingErrors = validationErrors.some(e => e.severity === 'error')
@@ -203,17 +212,30 @@ export function TreeEditorPage() {
)
}
// Mobile warning
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
// Mobile gate: show read-only message
if (isMobile) {
return (
<div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center px-6 text-center">
<Monitor className="mb-4 h-12 w-12 text-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 (
<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 */}
{showDraftPrompt && (

View File

@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback } from 'react'
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 type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
@@ -29,6 +29,9 @@ export function TreeLibraryPage() {
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null)
// Mobile folder sidebar state
const [mobileFolderOpen, setMobileFolderOpen] = useState(false)
// Delete confirmation state
const [treeToDelete, setTreeToDelete] = useState<TreeListItem | null>(null)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
@@ -152,17 +155,22 @@ export function TreeLibraryPage() {
{/* Folder Sidebar */}
<FolderSidebar
selectedFolderId={selectedFolderId}
onFolderSelect={setSelectedFolderId}
onFolderSelect={(id) => {
setSelectedFolderId(id)
setMobileFolderOpen(false)
}}
onCreateFolder={handleCreateFolder}
onEditFolder={handleEditFolder}
mobileOpen={mobileFolderOpen}
onMobileClose={() => setMobileFolderOpen(false)}
/>
{/* Main Content */}
<div className="flex-1 overflow-auto">
<div className="container mx-auto px-4 py-8">
<div className="mb-8 flex items-start justify-between">
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<div className="mb-6 flex flex-col gap-4 sm:mb-8 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="text-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">
Select a troubleshooting tree to start a new session
</p>
@@ -183,6 +191,18 @@ export function TreeLibraryPage() {
{/* Search and Filter */}
<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">
<input
type="text"
@@ -294,7 +314,7 @@ export function TreeLibraryPage() {
{trees.map((tree) => (
<div
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">
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
@@ -336,7 +356,7 @@ export function TreeLibraryPage() {
<Link
to={`/trees/${tree.id}/edit`}
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'
)}
title="Edit tree"
@@ -364,7 +384,7 @@ export function TreeLibraryPage() {
type="button"
onClick={() => handleStartSession(tree.id)}
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'
)}
>

View File

@@ -643,7 +643,7 @@ export function TreeNavigationPage() {
return (
<div className="h-[calc(100vh-4rem)]">
{/* 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">
{/* Header */}
<div className="mb-6 flex items-center justify-between">
@@ -659,7 +659,7 @@ export function TreeNavigationPage() {
</div>
<button
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
</button>
@@ -753,9 +753,10 @@ export function TreeNavigationPage() {
{/* Add Custom Step Button */}
<button
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>
</>
)}