Add dark mode, export preview, and keyboard navigation

- Add theme store with light/dark/system modes and ThemeToggle component
- Prevent flash of wrong theme on initial load via inline script
- Add ExportPreviewModal for previewing session exports before download
- Add copy-to-clipboard functionality to session export
- Implement keyboard shortcuts for tree navigation (1-9 options, Esc back, Enter continue)
- Display keyboard hints in tree navigation UI
- Fix findNode to safely handle undefined structure parameter
- Update page title to "Apoklisis"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-01-28 21:19:57 -05:00
parent 4cee013733
commit 5a0dff1da9
9 changed files with 455 additions and 40 deletions

View File

@@ -0,0 +1,36 @@
import { Sun, Moon, Monitor } from 'lucide-react'
import { useThemeStore } from '@/store/themeStore'
import { cn } from '@/lib/utils'
export function ThemeToggle() {
const { theme, setTheme } = useThemeStore()
const options = [
{ value: 'light' as const, icon: Sun, label: 'Light' },
{ value: 'dark' as const, icon: Moon, label: 'Dark' },
{ value: 'system' as const, icon: Monitor, label: 'System' },
]
return (
<div className="flex items-center rounded-md border border-input bg-background p-1">
{options.map(({ value, icon: Icon, label }) => (
<button
key={value}
onClick={() => setTheme(value)}
className={cn(
'rounded p-1.5 transition-colors',
theme === value
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
aria-label={`Switch to ${label} theme`}
aria-pressed={theme === value}
>
<Icon className="h-4 w-4" />
</button>
))}
</div>
)
}
export default ThemeToggle

View File

@@ -1,5 +1,6 @@
import { Link, useLocation, useNavigate, Outlet } from 'react-router-dom'
import { useAuthStore } from '@/store/authStore'
import { ThemeToggle } from '@/components/common/ThemeToggle'
import { cn } from '@/lib/utils'
export function AppLayout() {
@@ -48,6 +49,7 @@ export function AppLayout() {
<span className="hidden text-sm text-muted-foreground sm:block">
{user?.name || user?.email}
</span>
<ThemeToggle />
<button
onClick={handleLogout}
className={cn(

View File

@@ -0,0 +1,103 @@
import { useState } from 'react'
import { Copy, Download, Check } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import { cn } from '@/lib/utils'
interface ExportPreviewModalProps {
isOpen: boolean
onClose: () => void
content: string
filename: string
format: 'markdown' | 'text' | 'html'
onDownload: () => void
}
export function ExportPreviewModal({
isOpen,
onClose,
content,
filename,
format,
onDownload,
}: ExportPreviewModalProps) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(content)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
const handleDownload = () => {
onDownload()
onClose()
}
// Reset copied state when modal closes
const handleClose = () => {
setCopied(false)
onClose()
}
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Export Preview" size="xl">
{/* Filename and format info */}
<p className="mb-3 text-sm text-muted-foreground">
Filename: <span className="font-mono text-foreground">{filename}</span>
<span className="ml-3 rounded bg-secondary px-2 py-0.5 text-xs">
{format === 'markdown' ? 'Markdown' : format === 'html' ? 'HTML' : 'Plain Text'}
</span>
</p>
{/* Content Preview */}
<div
className={cn(
'max-h-96 overflow-auto rounded-md border border-input bg-muted/50 p-4',
'font-mono text-sm text-foreground'
)}
>
<pre className="whitespace-pre-wrap">{content}</pre>
</div>
{/* Actions */}
<div className="mt-4 flex items-center justify-end gap-2">
<button
onClick={handleCopy}
className={cn(
'flex items-center gap-2 rounded-md border border-input px-3 py-2 text-sm font-medium',
'bg-background text-foreground hover:bg-accent',
'focus:outline-none focus:ring-2 focus:ring-ring'
)}
>
{copied ? (
<>
<Check className="h-4 w-4 text-green-500" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy to Clipboard
</>
)}
</button>
<button
onClick={handleDownload}
className={cn(
'flex items-center gap-2 rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground',
'hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring'
)}
>
<Download className="h-4 w-4" />
Download
</button>
</div>
</Modal>
)
}
export default ExportPreviewModal