diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index ef86c84..f383f15 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -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 = () => ( + + + + +) +const IconGrid = () => ( + + + + +) +const IconSettings = () => ( + + + + +) +const IconUpload = () => ( + + + + + +) +const IconPlus = () => ( + + + +) + 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 (
setContextMenu(null)} > - {/* Logo */} -
+ {/* ── Logo ── */} +
- {/* Upload Brief */} -
+ {/* ── Nav items ── */} +
+ +
- {/* Sheets list */} -
-
- + {/* ── Divider ── */} +
+ + {/* ── Sheets list ── */} +
+
+ Sheets + > + +
- {sheets.map(sheet => ( -
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 ? ( - 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()} - /> - ) : ( - <> - 📋 -
-
- {sheet.name} + {sheets.map(sheet => { + const active = activeSheetId === sheet.id + return ( +
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 ? ( + 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()} + /> + ) : ( + <> + + + +
+
+ {sheet.name} +
+
{sheet.itemCount} items
-
{sheet.itemCount} items
-
- - )} -
- ))} + + )} +
+ ) + })} {sheets.length === 0 && ( -
- No sheets yet.
Click + to create one. +
+
+ +
+
No sheets yet
+
Click + to create one
)}
- {/* Admin link */} + {/* ── Admin ── */} {user?.role === 'admin' && ( -
+
)} - {/* Context menu */} + {/* ── Context menu ── */} {contextMenu && (
e.stopPropagation()} > {[ @@ -153,8 +239,8 @@ export default function Sidebar() { diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index 81029d8..a773c52 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -2,6 +2,12 @@ import { useNavigate, useLocation } from 'react-router-dom' import { useAuthStore } from '../../stores/useAuthStore' import api from '../../api/client' +const IconChevronLeft = () => ( + + + +) + 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 (
+ {/* ── Left: breadcrumb + back ── */}
{!isDashboard && ( )} - + {!isDashboard && ( + / + )} + {breadcrumb}
-
- {user && ( - <> - - {user.name || user.email} - {user.role === 'admin' && ( - - ADMIN - - )} - - - - )} -
+ {(user.name || user.email || 'U')[0].toUpperCase()} +
+
+
+ {user.name || user.email} +
+ {user.role === 'admin' && ( +
ADMIN
+ )} +
+
+ +
+ )} ) } diff --git a/frontend/src/components/sheet/CommandBar.tsx b/frontend/src/components/sheet/CommandBar.tsx index 42a7ecd..5167542 100644 --- a/frontend/src/components/sheet/CommandBar.tsx +++ b/frontend/src/components/sheet/CommandBar.tsx @@ -1,6 +1,20 @@ import { useState, useRef } from 'react' import { useSpeechRecognition } from '../../hooks/useSpeechRecognition' +const IconMic = ({ active }: { active: boolean }) => ( + + + + + +) +const IconSend = () => ( + + + +) + 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 )}
-
+
{quickStarters.map(qs => ( diff --git a/frontend/src/index.css b/frontend/src/index.css index f4ea508..c4a33cc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; } diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index e4f5eb9..70b98ee 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -4,6 +4,25 @@ import { useSheetStore } from '../stores/useSheetStore' import { useJobStore } from '../stores/useJobStore' import JobProgressCard from '../components/brief/JobProgressCard' +const IconArrow = () => ( + + + +) +const IconDoc = () => ( + + + + + +) +const IconGrid = () => ( + + + + +) + 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 ( -
-
-

Dashboard

+
+ {/* ── Header ── */} +
+

+ Dashboard +

Manage your Activation Calendar sheets or extract deliverables from a brief.

- {/* Quick actions */} -
+ {/* ── Quick actions ── */} +
+ {/* Upload Brief — primary CTA */} + {/* New Sheet — secondary */}
-
- {/* Recent sheets */} + {/* ── Content grid ── */} +
+ {/* Recent Sheets */}
-

- Recent Sheets -

-
+
+

+ Recent Sheets +

+ {sheets.length > 5 && ( + + )} +
+
{sheets.slice(0, 5).map(sheet => ( ))} {sheets.length === 0 && ( -
No sheets yet.
+
+
No sheets yet. Click "New Sheet" to start.
+
)}
- {/* Recent jobs */} + {/* Recent Brief Extractions */}
-

- Recent Brief Extractions +

+ Recent Extractions

-
+
{recentJobs.map(job => ( ))} {recentJobs.length === 0 && ( -
No extractions yet.
+
+
No extractions yet. Upload a brief to start.
+
)}
diff --git a/frontend/src/pages/SheetPage.tsx b/frontend/src/pages/SheetPage.tsx index c449a1a..6dde19c 100644 --- a/frontend/src/pages/SheetPage.tsx +++ b/frontend/src/pages/SheetPage.tsx @@ -167,27 +167,36 @@ export default function SheetPage() { return (
- {/* Header */} -
+ {/* ── Header ── */} +
-

+

{sheetMeta?.name || 'Sheet'}

-
- {deliverables.length} items{saving ? ' · Saving…' : ''} +
+ {deliverables.length} items + {saving && ( + <> + · + Saving… + + )}