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:
Vadym Samoilenko 2026-03-23 14:55:20 +00:00
parent f42a390e8b
commit 45c6b2e720
6 changed files with 456 additions and 185 deletions

View file

@ -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>

View file

@ -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>
)
}

View file

@ -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>

View file

@ -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; }

View file

@ -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>

View file

@ -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