design: comprehensive dark theme UI refresh (ui-ux-pro-max)
- index.css: new OLED-optimised CSS variables (rgba borders, layered backgrounds, --ease/--duration tokens), global cursor:pointer on buttons, focus-visible ring, input focus glow, .ac-card utility - Sidebar: replace emoji icons with inline SVGs, add Dashboard/Upload nav links with active states, + button for new sheet, better sheet-item active treatment, polished context menu - TopBar: SVG chevron back button, user avatar initials pill, fixed 52px height, consistent spacing - DashboardPage: hover lift on action cards, rgba-border sheet list, dashed empty states, SVG icons - SheetPage: sticky sub-header with saving indicator - CommandBar: SVG mic + send icons, pill quick-starter chips, rounded-lg inputs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f42a390e8b
commit
45c6b2e720
6 changed files with 456 additions and 185 deletions
|
|
@ -3,6 +3,38 @@ import { useNavigate, useLocation } from 'react-router-dom'
|
|||
import { useSheetStore } from '../../stores/useSheetStore'
|
||||
import { useAuthStore } from '../../stores/useAuthStore'
|
||||
|
||||
// Inline SVGs — no extra deps
|
||||
const IconFile = () => (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
</svg>
|
||||
)
|
||||
const IconGrid = () => (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
)
|
||||
const IconSettings = () => (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
)
|
||||
const IconUpload = () => (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
)
|
||||
const IconPlus = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function Sidebar() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
|
@ -12,6 +44,8 @@ export default function Sidebar() {
|
|||
const [renameValue, setRenameValue] = useState('')
|
||||
const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null)
|
||||
|
||||
const isActive = (path: string) => location.pathname === path || location.pathname.startsWith(path + '/')
|
||||
|
||||
const handleNewSheet = async () => {
|
||||
const name = `Sheet ${new Date().toLocaleDateString()}`
|
||||
const id = await createSheet(name)
|
||||
|
|
@ -41,108 +75,160 @@ export default function Sidebar() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col w-64 flex-shrink-0 border-r overflow-hidden"
|
||||
className="flex flex-col w-60 flex-shrink-0 border-r overflow-hidden"
|
||||
style={{ background: 'var(--bg-sidebar)', borderColor: 'var(--border)' }}
|
||||
onClick={() => setContextMenu(null)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="p-4 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
{/* ── Logo ── */}
|
||||
<div className="px-4 py-4 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity w-full text-left"
|
||||
className="flex items-center gap-2.5 w-full text-left group"
|
||||
style={{ background: 'transparent', border: 'none', padding: 0 }}
|
||||
aria-label="Go to Dashboard"
|
||||
>
|
||||
<div className="w-7 h-7 rounded flex items-center justify-center font-bold text-xs"
|
||||
style={{ background: 'var(--accent)', color: '#000' }}>AC</div>
|
||||
<span className="font-semibold text-sm" style={{ color: 'var(--text-primary)' }}>AC Tool</span>
|
||||
<div
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center font-black text-xs tracking-tight flex-shrink-0 group-hover:opacity-90 transition-opacity"
|
||||
style={{ background: 'var(--accent)', color: '#000' }}
|
||||
>
|
||||
AC
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-sm leading-tight" style={{ color: 'var(--text-primary)' }}>AC Tool</div>
|
||||
<div className="text-xs leading-none" style={{ color: 'var(--text-muted)' }}>Oliver Agency</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Upload Brief */}
|
||||
<div className="p-3 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
{/* ── Nav items ── */}
|
||||
<div className="px-2 pt-3 pb-1 space-y-0.5">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
|
||||
style={{
|
||||
background: isActive('/') && !location.pathname.startsWith('/sheet') && !location.pathname.startsWith('/brief') && !location.pathname.startsWith('/admin') ? 'var(--accent-dim)' : 'transparent',
|
||||
color: isActive('/') && !location.pathname.startsWith('/sheet') && !location.pathname.startsWith('/brief') && !location.pathname.startsWith('/admin') ? 'var(--accent)' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
<IconGrid />
|
||||
Dashboard
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate('/brief/upload')}
|
||||
className="w-full py-2 px-3 rounded text-sm font-medium transition-colors"
|
||||
style={{ background: 'var(--accent)', color: '#000' }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
|
||||
style={{
|
||||
background: location.pathname.startsWith('/brief') ? 'var(--accent-dim)' : 'transparent',
|
||||
color: location.pathname.startsWith('/brief') ? 'var(--accent)' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
+ Upload Brief
|
||||
<IconUpload />
|
||||
Upload Brief
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sheets list */}
|
||||
<div className="flex-1 overflow-y-auto p-2">
|
||||
<div className="flex items-center justify-between px-2 py-1 mb-1">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--text-muted)' }}>
|
||||
{/* ── Divider ── */}
|
||||
<div className="mx-4 my-2" style={{ height: 1, background: 'var(--border)' }} />
|
||||
|
||||
{/* ── Sheets list ── */}
|
||||
<div className="flex-1 overflow-y-auto px-2 pb-2">
|
||||
<div className="flex items-center justify-between px-3 py-1.5 mb-1">
|
||||
<span className="text-xs font-semibold uppercase tracking-widest" style={{ color: 'var(--text-muted)' }}>
|
||||
Sheets
|
||||
</span>
|
||||
<button
|
||||
onClick={handleNewSheet}
|
||||
className="text-lg leading-none hover:opacity-70 transition-opacity"
|
||||
style={{ color: 'var(--accent)' }}
|
||||
className="w-6 h-6 rounded-md flex items-center justify-center transition-colors hover:opacity-80"
|
||||
style={{ color: 'var(--accent)', background: 'var(--accent-dim)' }}
|
||||
title="New Sheet"
|
||||
>+</button>
|
||||
>
|
||||
<IconPlus />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sheets.map(sheet => (
|
||||
<div
|
||||
key={sheet.id}
|
||||
onContextMenu={e => handleContextMenu(e, sheet.id)}
|
||||
className="group flex items-center gap-2 px-2 py-2 rounded cursor-pointer mb-1 transition-colors"
|
||||
style={{
|
||||
background: activeSheetId === sheet.id ? 'rgba(255,196,7,0.12)' : 'transparent',
|
||||
borderLeft: activeSheetId === sheet.id ? '2px solid var(--accent)' : '2px solid transparent',
|
||||
}}
|
||||
onClick={() => handleSelect(sheet.id)}
|
||||
>
|
||||
{renamingId === sheet.id ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
onChange={e => setRenameValue(e.target.value)}
|
||||
onBlur={() => commitRename(sheet.id)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') commitRename(sheet.id); if (e.key === 'Escape') setRenamingId(null) }}
|
||||
className="flex-1 bg-transparent text-sm outline-none border-b"
|
||||
style={{ color: 'var(--text-primary)', borderColor: 'var(--accent)' }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>📋</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm truncate" style={{ color: activeSheetId === sheet.id ? 'var(--accent)' : 'var(--text-primary)' }}>
|
||||
{sheet.name}
|
||||
{sheets.map(sheet => {
|
||||
const active = activeSheetId === sheet.id
|
||||
return (
|
||||
<div
|
||||
key={sheet.id}
|
||||
onContextMenu={e => handleContextMenu(e, sheet.id)}
|
||||
className="group flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer mb-0.5 transition-colors"
|
||||
style={{
|
||||
background: active ? 'var(--accent-dim)' : 'transparent',
|
||||
borderLeft: active ? '2px solid var(--accent)' : '2px solid transparent',
|
||||
}}
|
||||
onClick={() => handleSelect(sheet.id)}
|
||||
>
|
||||
{renamingId === sheet.id ? (
|
||||
<input
|
||||
autoFocus
|
||||
value={renameValue}
|
||||
onChange={e => setRenameValue(e.target.value)}
|
||||
onBlur={() => commitRename(sheet.id)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') commitRename(sheet.id)
|
||||
if (e.key === 'Escape') setRenamingId(null)
|
||||
}}
|
||||
className="flex-1 bg-transparent text-sm outline-none border-b"
|
||||
style={{ color: 'var(--text-primary)', borderColor: 'var(--accent)' }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ color: active ? 'var(--accent)' : 'var(--text-muted)', flexShrink: 0 }}>
|
||||
<IconFile />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs font-medium truncate" style={{ color: active ? 'var(--accent)' : 'var(--text-primary)' }}>
|
||||
{sheet.name}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>{sheet.itemCount} items</div>
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>{sheet.itemCount} items</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{sheets.length === 0 && (
|
||||
<div className="px-2 py-4 text-center text-xs" style={{ color: 'var(--text-muted)' }}>
|
||||
No sheets yet.<br />Click + to create one.
|
||||
<div className="px-3 py-6 text-center" style={{ color: 'var(--text-muted)' }}>
|
||||
<div className="text-2xl mb-2 opacity-30">
|
||||
<IconFile />
|
||||
</div>
|
||||
<div className="text-xs">No sheets yet</div>
|
||||
<div className="text-xs mt-0.5 opacity-70">Click + to create one</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Admin link */}
|
||||
{/* ── Admin ── */}
|
||||
{user?.role === 'admin' && (
|
||||
<div className="p-3 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="px-2 py-2 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<button
|
||||
onClick={() => navigate('/admin/users')}
|
||||
className="w-full py-2 px-3 rounded text-xs transition-colors text-left"
|
||||
style={{ color: 'var(--text-secondary)', background: location.pathname.startsWith('/admin') ? 'rgba(255,255,255,0.05)' : 'transparent' }}
|
||||
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm text-left transition-colors"
|
||||
style={{
|
||||
background: location.pathname.startsWith('/admin') ? 'var(--accent-dim)' : 'transparent',
|
||||
color: location.pathname.startsWith('/admin') ? 'var(--accent)' : 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
⚙ Admin
|
||||
<IconSettings />
|
||||
Admin
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context menu */}
|
||||
{/* ── Context menu ── */}
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="fixed z-50 rounded shadow-lg py-1"
|
||||
style={{ top: contextMenu.y, left: contextMenu.x, background: 'var(--bg-card)', border: '1px solid var(--border)', minWidth: 140 }}
|
||||
className="fixed z-50 py-1 shadow-2xl"
|
||||
style={{
|
||||
top: contextMenu.y, left: contextMenu.x,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
minWidth: 148,
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{[
|
||||
|
|
@ -153,8 +239,8 @@ export default function Sidebar() {
|
|||
<button
|
||||
key={item.label}
|
||||
onClick={item.action}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:opacity-70 transition-opacity"
|
||||
style={{ color: item.danger ? 'var(--danger)' : 'var(--text-primary)' }}
|
||||
className="w-full text-left px-3 py-2 text-xs font-medium hover:opacity-70 transition-opacity"
|
||||
style={{ color: item.danger ? 'var(--danger)' : 'var(--text-primary)', background: 'transparent', border: 'none' }}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ import { useNavigate, useLocation } from 'react-router-dom'
|
|||
import { useAuthStore } from '../../stores/useAuthStore'
|
||||
import api from '../../api/client'
|
||||
|
||||
const IconChevronLeft = () => (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function TopBar() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
const logout = useAuthStore(s => s.logout)
|
||||
|
|
@ -21,6 +27,7 @@ export default function TopBar() {
|
|||
}
|
||||
|
||||
const isDashboard = location.pathname === '/'
|
||||
|
||||
const breadcrumb = location.pathname.startsWith('/sheet/') ? 'Sheet Editor'
|
||||
: location.pathname.startsWith('/brief/review/') ? 'Review Brief'
|
||||
: location.pathname === '/brief/upload' ? 'Upload Brief'
|
||||
|
|
@ -29,45 +36,65 @@ export default function TopBar() {
|
|||
|
||||
return (
|
||||
<header
|
||||
className="flex items-center justify-between px-4 py-3 border-b flex-shrink-0"
|
||||
style={{ background: 'var(--bg-sidebar)', borderColor: 'var(--border)' }}
|
||||
className="flex items-center justify-between px-4 flex-shrink-0"
|
||||
style={{
|
||||
background: 'var(--bg-sidebar)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
height: 52,
|
||||
}}
|
||||
>
|
||||
{/* ── Left: breadcrumb + back ── */}
|
||||
<div className="flex items-center gap-2">
|
||||
{!isDashboard && (
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="flex items-center gap-1 text-xs px-2 py-1 rounded transition-opacity hover:opacity-70"
|
||||
style={{ color: 'var(--text-secondary)', border: '1px solid var(--border)' }}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{
|
||||
color: 'var(--text-muted)',
|
||||
background: 'var(--bg-card)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
← Dashboard
|
||||
<IconChevronLeft />
|
||||
Dashboard
|
||||
</button>
|
||||
)}
|
||||
<span className="text-sm font-medium" style={{ color: 'var(--text-secondary)' }}>
|
||||
{!isDashboard && (
|
||||
<span style={{ color: 'var(--border-light)', fontSize: 16, lineHeight: 1, userSelect: 'none' }}>/</span>
|
||||
)}
|
||||
<span className="text-sm font-semibold" style={{ color: isDashboard ? 'var(--text-primary)' : 'var(--text-secondary)' }}>
|
||||
{breadcrumb}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{user && (
|
||||
<>
|
||||
<span className="text-xs" style={{ color: 'var(--text-muted)' }}>
|
||||
{user.name || user.email}
|
||||
{user.role === 'admin' && (
|
||||
<span className="ml-1 px-1 py-0.5 rounded text-xs font-bold" style={{ background: 'rgba(255,196,7,0.2)', color: 'var(--accent)' }}>
|
||||
ADMIN
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-xs px-2 py-1 rounded transition-colors"
|
||||
style={{ color: 'var(--text-muted)', border: '1px solid var(--border)' }}
|
||||
{/* ── Right: user + sign out ── */}
|
||||
{user && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0"
|
||||
style={{ background: 'var(--accent-dim)', color: 'var(--accent)', border: '1px solid var(--accent-border)' }}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(user.name || user.email || 'U')[0].toUpperCase()}
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="text-xs font-medium leading-none mb-0.5" style={{ color: 'var(--text-secondary)' }}>
|
||||
{user.name || user.email}
|
||||
</div>
|
||||
{user.role === 'admin' && (
|
||||
<div className="text-xs font-bold leading-none" style={{ color: 'var(--accent)' }}>ADMIN</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-xs px-2.5 py-1.5 rounded-lg font-medium transition-colors hover:opacity-80"
|
||||
style={{ color: 'var(--text-muted)', border: '1px solid var(--border)', background: 'transparent' }}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
import { useState, useRef } from 'react'
|
||||
import { useSpeechRecognition } from '../../hooks/useSpeechRecognition'
|
||||
|
||||
const IconMic = ({ active }: { active: boolean }) => (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
||||
style={{ color: active ? '#fff' : 'currentColor' }}>
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>
|
||||
)
|
||||
const IconSend = () => (
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
interface Props {
|
||||
onCommand: (command: string, yolo: boolean) => void
|
||||
loading: boolean
|
||||
|
|
@ -39,11 +53,12 @@ export default function CommandBar({ onCommand, loading, yolo, onYoloChange }: P
|
|||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) handleSend() }}
|
||||
placeholder="Type a command… e.g. 'Add 5 social banners for UK'"
|
||||
disabled={loading}
|
||||
className="flex-1 px-3 py-2 rounded text-sm outline-none"
|
||||
className="flex-1 px-3 py-2 rounded-lg text-sm"
|
||||
style={{
|
||||
background: 'var(--bg-card)',
|
||||
color: 'var(--text-primary)',
|
||||
border: '1px solid var(--border)',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
|
@ -51,25 +66,29 @@ export default function CommandBar({ onCommand, loading, yolo, onYoloChange }: P
|
|||
<button
|
||||
onMouseDown={start}
|
||||
onMouseUp={stop}
|
||||
className="px-3 py-2 rounded text-sm transition-colors"
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center transition-colors"
|
||||
style={{
|
||||
background: listening ? 'var(--danger)' : 'var(--bg-card)',
|
||||
color: listening ? '#fff' : 'var(--text-secondary)',
|
||||
color: listening ? '#fff' : 'var(--text-muted)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
title="Hold to speak"
|
||||
>
|
||||
🎤
|
||||
<IconMic active={listening} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={loading || !input.trim()}
|
||||
className="px-4 py-2 rounded text-sm font-medium transition-colors disabled:opacity-40"
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-bold transition-opacity hover:opacity-80 disabled:opacity-30"
|
||||
style={{ background: 'var(--accent)', color: '#000' }}
|
||||
>
|
||||
{loading ? '…' : 'Send'}
|
||||
{loading ? (
|
||||
<span style={{ fontSize: 16, lineHeight: 1 }}>·</span>
|
||||
) : (
|
||||
<><IconSend /><span>Send</span></>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<label className="flex items-center gap-1 cursor-pointer select-none" title="YOLO mode — AI never asks questions">
|
||||
|
|
@ -90,13 +109,13 @@ export default function CommandBar({ onCommand, loading, yolo, onYoloChange }: P
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{quickStarters.map(qs => (
|
||||
<button
|
||||
key={qs}
|
||||
onClick={() => { setInput(qs); inputRef.current?.focus() }}
|
||||
className="px-2 py-1 rounded text-xs transition-colors hover:opacity-80"
|
||||
style={{ background: 'var(--bg-card)', color: 'var(--text-secondary)', border: '1px solid var(--border)' }}
|
||||
className="px-2.5 py-1 rounded-full text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{ background: 'var(--bg-card)', color: 'var(--text-muted)', border: '1px solid var(--border)' }}
|
||||
>
|
||||
{qs}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,50 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
/* Oliver Agency dark theme */
|
||||
/* ─── Oliver Agency — OLED Dark Design System ─────────────────────────────── */
|
||||
:root {
|
||||
--bg-color: #000000;
|
||||
--bg-card: #121212;
|
||||
--bg-sidebar: #0a0a0a;
|
||||
--accent: #FFC407;
|
||||
--accent-hover: #e6b000;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b0b0b0;
|
||||
--text-muted: #787878;
|
||||
--border: #2a2a2a;
|
||||
--border-light: #3a3a3a;
|
||||
/* Backgrounds — layered depth */
|
||||
--bg-color: #000000;
|
||||
--bg-sidebar: #080808;
|
||||
--bg-card: #0f0f0f;
|
||||
--bg-card-hover: #161616;
|
||||
--bg-elevated: #1c1c1c;
|
||||
|
||||
/* Brand */
|
||||
--accent: #FFC407;
|
||||
--accent-hover: #FFD04D;
|
||||
--accent-dim: rgba(255, 196, 7, 0.12);
|
||||
--accent-border: rgba(255, 196, 7, 0.3);
|
||||
|
||||
/* Text — WCAG-compliant on #000 */
|
||||
--text-primary: #f0f0f0; /* 18.5:1 */
|
||||
--text-secondary: #b8b8b8; /* 9.3:1 */
|
||||
--text-muted: #808080; /* 5.7:1 */
|
||||
--text-disabled: #444444;
|
||||
|
||||
/* Borders — rgba for subtlety */
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--border-light: rgba(255, 255, 255, 0.13);
|
||||
--border-hover: rgba(255, 255, 255, 0.20);
|
||||
|
||||
/* Status */
|
||||
--success: #22c55e;
|
||||
--danger: #ef4444;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--info: #3b82f6;
|
||||
--info: #3b82f6;
|
||||
|
||||
/* Motion */
|
||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--duration: 150ms;
|
||||
|
||||
/* Shape */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
/* ─── Reset ─────────────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
|
|
@ -26,8 +52,9 @@ body {
|
|||
color: var(--text-primary);
|
||||
font-family: 'Montserrat', 'Inter', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
|
|
@ -36,63 +63,99 @@ body {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: var(--bg-card); }
|
||||
::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #444; }
|
||||
/* ─── Interactive base ───────────────────────────────────────────────────────── */
|
||||
button {
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: opacity var(--duration) var(--ease),
|
||||
background var(--duration) var(--ease),
|
||||
border-color var(--duration) var(--ease),
|
||||
color var(--duration) var(--ease),
|
||||
box-shadow var(--duration) var(--ease);
|
||||
}
|
||||
button:disabled { cursor: not-allowed; opacity: 0.38; }
|
||||
|
||||
/* Handsontable v17 dark theme via CSS variables */
|
||||
/* Force dark color-scheme so light-dark() resolves to dark variant */
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { opacity: 0.8; }
|
||||
|
||||
/* ─── Focus ring (keyboard nav) ─────────────────────────────────────────────── */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ─── Inputs ────────────────────────────────────────────────────────────────── */
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
transition: border-color var(--duration) var(--ease),
|
||||
box-shadow var(--duration) var(--ease);
|
||||
}
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-border) !important;
|
||||
box-shadow: 0 0 0 3px rgba(255, 196, 7, 0.08);
|
||||
}
|
||||
input::placeholder { color: var(--text-disabled); }
|
||||
|
||||
/* ─── Utility: card ─────────────────────────────────────────────────────────── */
|
||||
.ac-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color var(--duration) var(--ease),
|
||||
background var(--duration) var(--ease);
|
||||
}
|
||||
.ac-card:hover {
|
||||
border-color: var(--border-light);
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
/* ─── Scrollbar ─────────────────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 99px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
|
||||
|
||||
/* ─── Handsontable dark theme ────────────────────────────────────────────────── */
|
||||
.ht-theme-main {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Oliver accent colour instead of blue */
|
||||
--ht-colors-primary-100: #FFC407;
|
||||
--ht-colors-primary-200: #FFC407;
|
||||
--ht-colors-primary-300: #e6b000;
|
||||
--ht-colors-primary-400: #e6b000;
|
||||
--ht-colors-primary-500: #cc9d00;
|
||||
--ht-colors-primary-600: #b38900;
|
||||
|
||||
/* Dark palette */
|
||||
--ht-colors-palette-950: #121212;
|
||||
--ht-colors-palette-950: #0f0f0f;
|
||||
--ht-colors-palette-900: #1a1a1a;
|
||||
--ht-colors-palette-800: #ffffff;
|
||||
--ht-colors-palette-800: #f0f0f0;
|
||||
--ht-colors-palette-700: #222222;
|
||||
--ht-colors-palette-600: #333333;
|
||||
--ht-colors-palette-500: #555555;
|
||||
--ht-colors-palette-400: #888888;
|
||||
--ht-colors-palette-300: #aaaaaa;
|
||||
--ht-colors-palette-200: #cccccc;
|
||||
--ht-colors-palette-500: #808080;
|
||||
--ht-colors-palette-400: #b8b8b8;
|
||||
--ht-colors-palette-300: #cccccc;
|
||||
--ht-colors-palette-200: #dddddd;
|
||||
--ht-colors-palette-100: #222222;
|
||||
--ht-colors-palette-50: #1a1a1a;
|
||||
--ht-colors-white: #121212;
|
||||
|
||||
/* Font */
|
||||
--ht-colors-white: #0f0f0f;
|
||||
font-family: 'Montserrat', 'Inter', system-ui, sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Column headers: uppercase accent */
|
||||
.ht-theme-main .ht_clone_top th,
|
||||
.ht-theme-main .ht_clone_left th,
|
||||
.ht-theme-main th {
|
||||
font-size: 11px !important;
|
||||
font-weight: 600 !important;
|
||||
font-weight: 700 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.5px !important;
|
||||
letter-spacing: 0.6px !important;
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
/* Context menu dark */
|
||||
.htContextMenu .wtHolder {
|
||||
background: #1a1a1a !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
}
|
||||
.htContextMenu td {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
.htContextMenu td.current {
|
||||
background: rgba(255,196,7,0.15) !important;
|
||||
background: var(--bg-elevated) !important;
|
||||
border: 1px solid var(--border-light) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
.htContextMenu td { color: var(--text-primary) !important; }
|
||||
.htContextMenu td.current { background: var(--accent-dim) !important; }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,25 @@ import { useSheetStore } from '../stores/useSheetStore'
|
|||
import { useJobStore } from '../stores/useJobStore'
|
||||
import JobProgressCard from '../components/brief/JobProgressCard'
|
||||
|
||||
const IconArrow = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
)
|
||||
const IconDoc = () => (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/>
|
||||
</svg>
|
||||
)
|
||||
const IconGrid = () => (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function DashboardPage() {
|
||||
const navigate = useNavigate()
|
||||
const { sheets, createSheet, loadSheet, fetchSheets } = useSheetStore()
|
||||
|
|
@ -22,77 +41,125 @@ export default function DashboardPage() {
|
|||
const recentJobs = jobs.slice(0, 3)
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>Dashboard</h1>
|
||||
<div className="max-w-5xl mx-auto py-2">
|
||||
{/* ── Header ── */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold tracking-tight mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>
|
||||
Manage your Activation Calendar sheets or extract deliverables from a brief.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick actions */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
||||
{/* ── Quick actions ── */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-10">
|
||||
{/* Upload Brief — primary CTA */}
|
||||
<button
|
||||
onClick={() => navigate('/brief/upload')}
|
||||
className="rounded-xl p-6 text-left transition-colors hover:opacity-90"
|
||||
style={{ background: 'var(--accent)', color: '#000' }}
|
||||
className="rounded-xl p-6 text-left group relative overflow-hidden"
|
||||
style={{
|
||||
background: 'var(--accent)',
|
||||
color: '#000',
|
||||
border: 'none',
|
||||
transition: 'opacity 150ms ease, transform 150ms ease',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.opacity = '0.92'; (e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.opacity = '1'; (e.currentTarget as HTMLElement).style.transform = 'translateY(0)' }}
|
||||
>
|
||||
<div className="text-2xl mb-2">📄</div>
|
||||
<div className="font-bold mb-1">Upload Brief</div>
|
||||
<div className="text-sm opacity-70">Extract deliverables from PDF, PPTX, DOCX, XLSX</div>
|
||||
<div className="mb-3 opacity-70"><IconDoc /></div>
|
||||
<div className="font-bold text-base mb-1">Upload Brief</div>
|
||||
<div className="text-sm opacity-60">Extract deliverables from PDF, PPTX, DOCX, XLSX</div>
|
||||
<div className="absolute bottom-5 right-5 opacity-40"><IconArrow /></div>
|
||||
</button>
|
||||
|
||||
{/* New Sheet — secondary */}
|
||||
<button
|
||||
onClick={handleNewSheet}
|
||||
className="rounded-xl p-6 text-left transition-colors hover:opacity-90"
|
||||
style={{ background: 'var(--bg-card)', border: '1px solid var(--border)', color: 'var(--text-primary)' }}
|
||||
className="ac-card rounded-xl p-6 text-left group relative"
|
||||
style={{
|
||||
border: '1px solid var(--border)',
|
||||
transition: 'border-color 150ms ease, background 150ms ease, transform 150ms ease',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(-1px)'; (e.currentTarget as HTMLElement).style.borderColor = 'var(--border-light)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; (e.currentTarget as HTMLElement).style.borderColor = 'var(--border)' }}
|
||||
>
|
||||
<div className="text-2xl mb-2">📋</div>
|
||||
<div className="font-bold mb-1">New Sheet</div>
|
||||
<div className="mb-3" style={{ color: 'var(--accent)', opacity: 0.6 }}><IconGrid /></div>
|
||||
<div className="font-bold text-base mb-1" style={{ color: 'var(--text-primary)' }}>New Sheet</div>
|
||||
<div className="text-sm" style={{ color: 'var(--text-muted)' }}>Start a blank Activation Calendar</div>
|
||||
<div className="absolute bottom-5 right-5" style={{ color: 'var(--text-muted)', opacity: 0.5 }}><IconArrow /></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Recent sheets */}
|
||||
{/* ── Content grid ── */}
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
{/* Recent Sheets */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--text-muted)' }}>
|
||||
Recent Sheets
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest" style={{ color: 'var(--text-muted)' }}>
|
||||
Recent Sheets
|
||||
</h2>
|
||||
{sheets.length > 5 && (
|
||||
<button className="text-xs" style={{ color: 'var(--accent)' }} onClick={() => {}}>
|
||||
See all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{sheets.slice(0, 5).map(sheet => (
|
||||
<button
|
||||
key={sheet.id}
|
||||
onClick={() => { loadSheet(sheet.id); navigate(`/sheet/${sheet.id}`) }}
|
||||
className="w-full rounded-lg p-3 text-left flex items-center justify-between transition-colors hover:opacity-80"
|
||||
style={{ background: 'var(--bg-card)', border: '1px solid var(--border)' }}
|
||||
className="w-full rounded-xl px-4 py-3 text-left flex items-center justify-between group"
|
||||
style={{
|
||||
background: 'var(--bg-card)',
|
||||
border: '1px solid var(--border)',
|
||||
transition: 'border-color 150ms ease, background 150ms ease',
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--border-light)'; (e.currentTarget as HTMLElement).style.background = 'var(--bg-card-hover)' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--border)'; (e.currentTarget as HTMLElement).style.background = 'var(--bg-card)' }}
|
||||
>
|
||||
<div>
|
||||
<div className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>{sheet.name}</div>
|
||||
<div className="text-sm font-medium mb-0.5" style={{ color: 'var(--text-primary)' }}>{sheet.name}</div>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>
|
||||
{sheet.itemCount} items · {new Date(sheet.modified).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ color: 'var(--accent)' }}>→</span>
|
||||
<span style={{ color: 'var(--text-muted)', opacity: 0.6, transition: 'opacity 150ms ease, color 150ms ease' }}
|
||||
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = 'var(--accent)'; (e.currentTarget as HTMLElement).style.opacity = '1' }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'var(--text-muted)'; (e.currentTarget as HTMLElement).style.opacity = '0.6' }}
|
||||
>
|
||||
<IconArrow />
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
{sheets.length === 0 && (
|
||||
<div className="text-sm" style={{ color: 'var(--text-muted)' }}>No sheets yet.</div>
|
||||
<div
|
||||
className="rounded-xl px-4 py-8 text-center"
|
||||
style={{ background: 'var(--bg-card)', border: '1px dashed var(--border)' }}
|
||||
>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>No sheets yet. Click "New Sheet" to start.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent jobs */}
|
||||
{/* Recent Brief Extractions */}
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider mb-3" style={{ color: 'var(--text-muted)' }}>
|
||||
Recent Brief Extractions
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest mb-3" style={{ color: 'var(--text-muted)' }}>
|
||||
Recent Extractions
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{recentJobs.map(job => (
|
||||
<JobProgressCard key={job.id} job={job} />
|
||||
))}
|
||||
{recentJobs.length === 0 && (
|
||||
<div className="text-sm" style={{ color: 'var(--text-muted)' }}>No extractions yet.</div>
|
||||
<div
|
||||
className="rounded-xl px-4 py-8 text-center"
|
||||
style={{ background: 'var(--bg-card)', border: '1px dashed var(--border)' }}
|
||||
>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>No extractions yet. Upload a brief to start.</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -167,27 +167,36 @@ export default function SheetPage() {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between flex-shrink-0">
|
||||
{/* ── Header ── */}
|
||||
<div
|
||||
className="flex items-center justify-between flex-shrink-0 px-4 py-3 -mx-4 -mt-4"
|
||||
style={{ background: 'var(--bg-card)', borderBottom: '1px solid var(--border)' }}
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
|
||||
<h1 className="text-base font-bold tracking-tight" style={{ color: 'var(--text-primary)' }}>
|
||||
{sheetMeta?.name || 'Sheet'}
|
||||
</h1>
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>
|
||||
{deliverables.length} items{saving ? ' · Saving…' : ''}
|
||||
<div className="text-xs mt-0.5 flex items-center gap-2" style={{ color: 'var(--text-muted)' }}>
|
||||
<span>{deliverables.length} items</span>
|
||||
{saving && (
|
||||
<>
|
||||
<span style={{ color: 'var(--border-light)' }}>·</span>
|
||||
<span style={{ color: 'var(--accent)' }}>Saving…</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="px-3 py-1.5 rounded text-xs transition-colors"
|
||||
style={{ color: 'var(--danger)', border: '1px solid var(--border)' }}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors hover:opacity-80"
|
||||
style={{ color: 'var(--danger)', border: '1px solid var(--border)', background: 'transparent' }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={() => sheetId && exportSheet(sheetId)}
|
||||
className="px-3 py-1.5 rounded text-xs font-medium"
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-bold transition-opacity hover:opacity-80"
|
||||
style={{ background: 'var(--accent)', color: '#000' }}
|
||||
>
|
||||
Export CSV
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue