Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/api/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export async function POST(req: Request) {
? b.maxRetries
: current.maxRetries,
};
next.theme = current.theme;
if (b.theme === "light" || b.theme === "dark") {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
next.theme = b.theme;
} else if (b.theme === null) {
// Explicitly clearing the theme preference (back to system).
delete next.theme;
}
Comment thread
msjabata25 marked this conversation as resolved.
saveSettings(next);
return NextResponse.json(next);
}
96 changes: 88 additions & 8 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,85 @@

--background: var(--surface-canvas);
--foreground: var(--ink-900);

/* Buttons */
--button-primary-bg: #111113;
--button-primary-hover: #000000;

/* Feedback colors (correct/wrong indicators) */
--feedback-correct-bg: #d1fae5;
--feedback-correct-border: #6ee7b7;
--feedback-correct-text: #064e3b;
--feedback-correct-icon: #059669;
--feedback-wrong-bg: #ffe4e6;
--feedback-wrong-border: #fda4af;
--feedback-wrong-text: #881337;
--feedback-wrong-icon: #e11d48;

/* Shadow tokens — opaque so they can be overridden per theme */
--shadow-popover: 0 8px 24px rgba(17, 17, 19, 0.08);
--shadow-modal: 0 24px 64px rgba(17, 17, 19, 0.18);
--shadow-nav: 0 2px 8px rgba(17, 17, 19, 0.06);
--shadow-tooltip: 0 8px 24px rgba(17, 17, 19, 0.10);
--shadow-tag-hover: 0 1px 4px rgba(17, 17, 19, 0.08);
--shadow-viz-icon: 0 2px 8px rgba(17, 17, 19, 0.08);
--shadow-banner: 0 8px 24px rgba(17, 17, 19, 0.08);
}

/* ── Dark theme ─────────────────────────────────────────────── */

html.dark {
color-scheme: dark;

--surface-canvas: #0a0a0d;
--surface-raised: #16161b;
--surface-sunken: #1f1f25;
--surface-overlay: rgba(22, 22, 27, 0.92);

--border-subtle: #26262e;
--border-default: #32323a;
--border-strong: #42424a;

--ink-900: #eeeef2;
--ink-700: #c8c8d0;
--ink-500: #888894;
--ink-400: #686874;
--ink-300: #484854;

--accent-50: #181838;
--accent-100: #222255;
--accent-500: #7e8cff;
--accent-600: #6a78e8;
--accent-700: #909cff;

--tag-rose-bg: #5c2834; --tag-rose-fg: #f5a8b8; --tag-rose-ring: #7a3848;
--tag-amber-bg: #5c3d1a; --tag-amber-fg: #f5c87a; --tag-amber-ring: #7a5528;
--tag-emerald-bg: #1a3d2e; --tag-emerald-fg: #6ad4a8; --tag-emerald-ring: #285540;
--tag-violet-bg: #2e1a4a; --tag-violet-fg: #b8a0f0; --tag-violet-ring: #4a2870;
--tag-sky-bg: #1a2e4a; --tag-sky-fg: #80b8f0; --tag-sky-ring: #284a70;

--button-primary-bg: #383840;
--button-primary-hover: #4e4e58;

--feedback-correct-bg: #064e3b;
--feedback-correct-border: #059669;
--feedback-correct-text: #a7f3d0;
--feedback-correct-icon: #34d399;
--feedback-wrong-bg: #4c0519;
--feedback-wrong-border: #be123c;
--feedback-wrong-text: #fecdd3;
--feedback-wrong-icon: #fb7185;

--shadow-popover: 0 8px 24px rgba(0, 0, 0, 0.5);
--shadow-modal: 0 24px 64px rgba(0, 0, 0, 0.55);
--shadow-nav: 0 2px 8px rgba(0, 0, 0, 0.45);
--shadow-tooltip: 0 8px 24px rgba(0, 0, 0, 0.6);
--shadow-tag-hover: 0 1px 4px rgba(0, 0, 0, 0.5);
--shadow-viz-icon: 0 2px 8px rgba(0, 0, 0, 0.5);
--shadow-banner: 0 8px 24px rgba(0, 0, 0, 0.5);

--background: var(--surface-canvas);
--foreground: var(--ink-900);
}

@theme inline {
Expand All @@ -67,12 +146,12 @@ body {
/* Scrollbars — neutral, almost invisible */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
background: rgba(17, 17, 19, 0.10);
background: color-mix(in srgb, var(--ink-900) 12%, transparent);
border-radius: 8px;
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background: rgba(17, 17, 19, 0.20); background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--ink-900) 22%, transparent); background-clip: padding-box; }
::-webkit-scrollbar-track { background: transparent; }

/* ── Components ─────────────────────────────────────────────── */
Expand Down Expand Up @@ -174,7 +253,7 @@ html[data-platform="win32"] .tab-bar {
color: var(--ink-500);
cursor: pointer;
}
.tab-icon-btn:hover { background: rgba(17, 17, 19, 0.05); color: var(--ink-900); }
.tab-icon-btn:hover { background: var(--surface-sunken); color: var(--ink-900); }

/* "Note" / "Collections" inline label chip (matches mockup) */
.chip-soft {
Expand All @@ -201,7 +280,7 @@ html[data-platform="win32"] .tab-bar {
line-height: 1.4;
color: var(--ink-500);
}
.chip-plain:hover { color: var(--ink-900); background: rgba(17, 17, 19, 0.04); }
.chip-plain:hover { color: var(--ink-900); background: var(--surface-sunken); }

.viz-type-chip {
display: inline-flex;
Expand Down Expand Up @@ -234,7 +313,7 @@ html[data-platform="win32"] .tab-bar {
.viz-legend-icon:hover,
.viz-legend-icon:focus-visible {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(17, 17, 19, 0.08);
box-shadow: var(--shadow-viz-icon);
}
.viz-tooltip {
pointer-events: none;
Expand All @@ -249,8 +328,9 @@ html[data-platform="win32"] .tab-bar {
overflow-wrap: anywhere;
border: 1px solid var(--border-subtle);
border-radius: 6px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 8px 24px rgba(17, 17, 19, 0.10);
background: var(--surface-overlay);
backdrop-filter: blur(4px);
box-shadow: var(--shadow-tooltip);
color: var(--ink-900);
opacity: 0;
padding: 5px 8px;
Expand Down Expand Up @@ -294,7 +374,7 @@ html[data-platform="win32"] .tab-bar {
}
.tag-pill:hover:not([aria-disabled="true"]) {
transform: translateY(-0.5px);
box-shadow: 0 1px 4px rgba(17, 17, 19, 0.08);
box-shadow: var(--shadow-tag-hover);
}
.tag-pill[aria-disabled="true"] {
cursor: wait;
Expand Down
23 changes: 21 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import CodexHealthBanner from "@/components/CodexHealthBanner";
import ThemeProvider from "@/components/ThemeProvider";
import { loadSettings } from "@/lib/settings-store";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -24,14 +26,31 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const theme = loadSettings().theme;

return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
suppressHydrationWarning
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased${theme === "dark" ? " dark" : ""}`}
>
<body className="h-full flex flex-col overflow-hidden bg-[var(--surface-canvas)] text-[var(--ink-900)]">
<script
dangerouslySetInnerHTML={{
__html: `
try {
var t = ${JSON.stringify(theme ?? null)};
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch(e) {}
`,
}}
/>
<CodexHealthBanner />
{children}
<ThemeProvider initialTheme={theme}>{children}</ThemeProvider>
</body>
</html>
);
Comment thread
msjabata25 marked this conversation as resolved.
Expand Down
12 changes: 6 additions & 6 deletions app/library/library-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ function LibraryRowItem({
}) {
const title = useMemo(() => titleOf(row.filename), [row.filename]);
return (
<li className="group relative flex items-center gap-4 rounded-xl border border-[var(--border-subtle)] bg-white p-4 transition hover:border-[var(--border-strong)]">
<li className="group relative flex items-center gap-4 rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-raised)] p-4 transition hover:border-[var(--border-strong)]">
<Link
href={`/viewer/${row.id}`}
className="flex flex-1 items-center gap-4"
Expand Down Expand Up @@ -311,7 +311,7 @@ function TagsBadge({
return (
<span
title="Open this PDF to start tagging concepts"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
>
<TagIcon className="h-3 w-3" />
no tags yet
Expand All @@ -323,7 +323,7 @@ function TagsBadge({
return (
<span
title={`Tag detection in progress — ${analyzedPages} of ${numPages} pages scanned`}
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
>
<Loader2 className="h-3 w-3 animate-spin text-[var(--accent-600)]" />
{analyzedPages}/{numPages} pages
Expand All @@ -337,7 +337,7 @@ function TagsBadge({
className={
allReady
? "hidden shrink-0 items-center gap-1.5 rounded-md border border-emerald-200 bg-emerald-50 px-2 py-1 text-[10.5px] text-emerald-700 sm:inline-flex"
: "hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
: "hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
}
>
<TagIcon className="h-3 w-3" />
Expand All @@ -357,7 +357,7 @@ function KGBadge({
return (
<span
title="Knowledge graph will build on first open"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
>
<Network className="h-3 w-3" />
pending
Expand All @@ -368,7 +368,7 @@ function KGBadge({
return (
<span
title="Knowledge graph is being built"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
className="hidden shrink-0 items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[10.5px] text-[var(--ink-500)] sm:inline-flex"
>
<Loader2 className="h-3 w-3 animate-spin text-[var(--accent-600)]" />
building
Expand Down
10 changes: 5 additions & 5 deletions app/viewer/[docId]/viewer-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export default function ViewerClient({ docId }: { docId: string }) {
useEffect(() => {
const onChange = (e: Event) => {
const detail = (e as CustomEvent).detail as
| { autoGenerate?: boolean; maxRetries?: number }
| { autoGenerate?: boolean; maxRetries?: number; theme?: string }
| undefined;
if (!detail) return;
if (typeof detail.autoGenerate === "boolean") setAutoGenerate(detail.autoGenerate);
Expand Down Expand Up @@ -450,7 +450,7 @@ export default function ViewerClient({ docId }: { docId: string }) {
</div>

<div className="flex min-h-0 flex-1 gap-2 bg-[var(--surface-canvas)] p-2">
<div className="min-w-0 flex-1 overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-white">
<div className="min-w-0 flex-1 overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-raised)]">
<PdfViewer
pdfUrl={meta.pdfUrl}
numPages={meta.numPages}
Expand All @@ -461,7 +461,7 @@ export default function ViewerClient({ docId }: { docId: string }) {
detecting={detecting}
/>
</div>
<div className="flex w-[44%] min-w-[420px] max-w-[720px] flex-col overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-white">
<div className="flex w-[44%] min-w-[420px] max-w-[720px] flex-col overflow-hidden rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-raised)]">
<RightPane
docId={docId}
mode={rightPaneMode}
Expand Down Expand Up @@ -526,7 +526,7 @@ function TagsChip({
: "Visualization agent — scanning each page for the concepts worth tagging.";
return (
<span className="viz-tooltip-anchor relative inline-flex">
<div className="flex items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[11px]">
<div className="flex items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[11px]">
{spinning ? (
<RefreshCw className="h-3 w-3 animate-spin text-[var(--accent-600)]" />
) : (
Expand Down Expand Up @@ -648,7 +648,7 @@ function KGStatusBadge({ docId }: { docId: string }) {

return (
<span className="viz-tooltip-anchor relative inline-flex">
<div className="flex items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-1 text-[11px]">
<div className="flex items-center gap-1.5 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-1 text-[11px]">
{icon}
<span className={`font-medium ${valueTone}`}>{label.split(" ")[0]}</span>
<span className={tone}>{label.split(" ").slice(1).join(" ")}</span>
Expand Down
4 changes: 2 additions & 2 deletions components/AccountButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function AccountButton() {
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.12 }}
className="absolute right-0 top-full z-30 mt-1.5 w-[22rem] overflow-hidden rounded-lg border border-[var(--border-subtle)] bg-white shadow-[0_8px_24px_rgba(17,17,19,0.08)]"
className="absolute right-0 top-full z-30 mt-1.5 w-[22rem] overflow-hidden rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-raised)] shadow-[var(--shadow-popover)]"
>
<AccountPanel refreshKey={open ? "open" : "closed"} />
</motion.div>
Expand Down Expand Up @@ -171,7 +171,7 @@ function AccountPanel({ refreshKey }: { refreshKey: string }) {
onClick={handleLogout}
disabled={loggingOut || (!data?.account && !err)}
title="Sign out of Codex and return to the setup wizard"
className="inline-flex items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-white px-2 py-0.5 text-[10.5px] font-medium text-[var(--ink-700)] transition hover:border-rose-200 hover:bg-rose-50 hover:text-rose-700 disabled:opacity-50"
className="inline-flex items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-raised)] px-2 py-0.5 text-[10.5px] font-medium text-[var(--ink-700)] transition hover:border-[var(--feedback-wrong-border)] hover:bg-[var(--feedback-wrong-bg)] hover:text-[var(--feedback-wrong-text)] disabled:opacity-50"
>
{loggingOut ? (
<RefreshCw className="h-2.5 w-2.5 animate-spin" />
Expand Down
11 changes: 6 additions & 5 deletions components/CodexHealthBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,18 @@ export default function CodexHealthBanner() {
"You've hit your usage quota. Try again later — your work is saved.";
}
} else {
icon = <AlertTriangle className="h-4 w-4 text-amber-600" />;
icon = <AlertTriangle className="h-4 w-4 text-[var(--tag-amber-fg)]" />;
title = "Last Codex call failed";
body = view.message ?? "Unknown error. Try again.";
}

const palette =
const isCritical =
view.kind === "auth_lost" ||
view.kind === "binary_missing" ||
view.kind === "model_unsupported"
? "border-rose-200 bg-rose-50 text-rose-900"
: "border-amber-200 bg-amber-50 text-amber-900";
view.kind === "model_unsupported";
const palette = isCritical
? "border-[var(--feedback-wrong-border)] bg-[var(--feedback-wrong-bg)] text-[var(--feedback-wrong-text)]"
: "border-[var(--tag-amber-ring)] bg-[var(--tag-amber-bg)] text-[var(--tag-amber-fg)]";

return (
<div
Expand Down
12 changes: 6 additions & 6 deletions components/PdfViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ export default function PdfViewer({

return (
<div className="relative h-full">
<div ref={scrollRef} className="relative flex h-full flex-col overflow-y-auto bg-white">
{detecting && (
<div className="sticky top-0 z-20 flex items-center gap-2 border-b border-[var(--border-subtle)] bg-white/90 px-4 py-2 text-[12px] text-[var(--ink-500)] backdrop-blur">
<div ref={scrollRef} className="relative flex h-full flex-col overflow-y-auto bg-[var(--surface-raised)]">
{detecting && (
<div className="sticky top-0 z-20 flex items-center gap-2 border-b border-[var(--border-subtle)] bg-[var(--surface-raised)]/90 px-4 py-2 text-[12px] text-[var(--ink-500)] backdrop-blur">
<Loader2 className="h-3.5 w-3.5 animate-spin text-[var(--accent-600)]" />
codex is reading your document and tagging concepts…
</div>
Expand All @@ -227,7 +227,7 @@ export default function PdfViewer({
</div>

{/* Page navigation — discreet cluster centered at the bottom */}
<div className="pointer-events-none absolute bottom-3 left-1/2 z-30 flex -translate-x-1/2 items-center gap-1 rounded-lg border border-[var(--border-subtle)] bg-white/95 p-1 shadow-[0_2px_8px_rgba(17,17,19,0.06)] backdrop-blur">
<div className="pointer-events-none absolute bottom-3 left-1/2 z-30 flex -translate-x-1/2 items-center gap-1 rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-raised)]/95 p-1 shadow-[var(--shadow-nav)] backdrop-blur">
<button
type="button"
onClick={() => goToPage(currentPage - 1)}
Expand Down Expand Up @@ -287,7 +287,7 @@ export default function PdfViewer({
</div>

{/* Zoom cluster — bottom-right of the document panel */}
<div className="pointer-events-none absolute bottom-3 right-3 z-30 flex items-center gap-1 rounded-lg border border-[var(--border-subtle)] bg-white/95 p-1 shadow-[0_2px_8px_rgba(17,17,19,0.06)] backdrop-blur">
<div className="pointer-events-none absolute bottom-3 right-3 z-30 flex items-center gap-1 rounded-lg border border-[var(--border-subtle)] bg-[var(--surface-raised)]/95 p-1 shadow-[var(--shadow-nav)] backdrop-blur">
{zoomEditing ? (
<div className="pointer-events-auto flex items-center px-1">
<input
Expand Down Expand Up @@ -407,7 +407,7 @@ function PdfPage({
return (
<div
data-page={pageNumber - 1}
className="relative shrink-0 bg-white"
className="relative shrink-0 bg-[var(--surface-sunken)]"
style={{
width: pdfWidth * scale,
height: pdfHeight * scale,
Expand Down
Loading