style(frontend): look&feel v2 — layered shadows, sharper radii, mono data register
Colors (GSD palette) untouched. New visual vocabulary across the whole GUI: - warm-tinted layered shadow tokens (--shadow-xs/sm/md) replacing flat rings; cards/dialogs framed with hairline border + real elevation - strict radii scale: xl cards / lg inputs / md controls / 2xl dialogs; buttons move from pills to rounded-md - third type register: --font-mono + tabular-nums on SQL, timers, dates, ids; standardized .thot-label micro-label tier - prose: framed tables with uppercase headers, elevated code blocks - SqlViewer badges moved from hardcoded Tailwind greens/reds to success/ destructive token tints; ResultsPanel inline styles replaced with classes - artifact gate: first option rendered as filled primary action Verified offline via Playwright against a mock backend replaying a real psd session (no VPN needed). tsc clean, 115/115 vitest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,7 +232,7 @@ export function AppShell() {
|
||||
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
|
||||
<div className="shrink-0 px-6 pb-6 pt-2">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<div className="rounded-2xl border border-input bg-card shadow-sm transition-colors focus-within:border-primary/40 focus-within:ring-3 focus-within:ring-ring/15">
|
||||
<div className="rounded-2xl border border-border/80 bg-card shadow-md transition-colors focus-within:border-primary/50 focus-within:ring-3 focus-within:ring-ring/15">
|
||||
<div className="px-2.5 py-2">
|
||||
<SteerInput
|
||||
sessionId={activeSessionId}
|
||||
@@ -253,7 +253,7 @@ export function AppShell() {
|
||||
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
|
||||
Thoth<span className="text-primary">II</span>
|
||||
</h1>
|
||||
<p className="mt-1.5 text-[0.7rem] uppercase tracking-[0.14em] text-muted-foreground">
|
||||
<p className="thot-label mt-1.5">
|
||||
Datamart Builder with
|
||||
<br />
|
||||
Human In The Loop
|
||||
@@ -273,7 +273,7 @@ export function AppShell() {
|
||||
|
||||
{/* L1 — rail title */}
|
||||
<div className="px-4 pb-1.5 pt-1">
|
||||
<span className="text-[0.82rem] font-bold uppercase tracking-[0.18em] text-primary">
|
||||
<span className="thot-label text-[0.8rem] font-bold tracking-[0.18em] text-primary">
|
||||
Sessions
|
||||
</span>
|
||||
</div>
|
||||
@@ -283,7 +283,7 @@ export function AppShell() {
|
||||
type="button"
|
||||
onClick={() => setActiveOpen((v) => !v)}
|
||||
aria-expanded={activeOpen}
|
||||
className="flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-[0.7rem] font-semibold uppercase tracking-[0.1em] text-foreground/65 hover:text-foreground"
|
||||
className="thot-label flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-foreground/65 hover:text-foreground"
|
||||
>
|
||||
<span className="select-none">{activeOpen ? "▾" : "▸"}</span>
|
||||
<span>Active sessions</span>
|
||||
@@ -298,7 +298,7 @@ export function AppShell() {
|
||||
type="button"
|
||||
onClick={() => setCollapsedGroups((c) => ({ ...c, [g]: !c[g] }))}
|
||||
aria-expanded={!collapsedGroups[g]}
|
||||
className="flex min-w-0 flex-1 items-center gap-1 px-1 pb-1 text-left text-[0.66rem] font-semibold uppercase tracking-[0.06em] text-muted-foreground hover:text-foreground"
|
||||
className="thot-label flex min-w-0 flex-1 items-center gap-1 px-1 pb-1 text-left hover:text-foreground"
|
||||
>
|
||||
<span className="select-none">{collapsedGroups[g] ? "▸" : "▾"}</span>
|
||||
<span className="truncate">{g}</span>
|
||||
@@ -307,7 +307,7 @@ export function AppShell() {
|
||||
type="button"
|
||||
aria-label={`Rename group ${g}`}
|
||||
onClick={() => setRenameGroupTarget(g)}
|
||||
className="rounded p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-accent group-hover/gh:opacity-100"
|
||||
className="rounded-md p-0.5 text-muted-foreground opacity-0 transition-opacity hover:bg-accent group-hover/gh:opacity-100"
|
||||
>
|
||||
<Pencil className="size-3" />
|
||||
</button>
|
||||
@@ -341,10 +341,10 @@ export function AppShell() {
|
||||
type="button"
|
||||
onClick={() => setArchiveOpen((v) => !v)}
|
||||
aria-expanded={archiveOpen}
|
||||
className="mt-2 flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-[0.7rem] font-semibold uppercase tracking-[0.1em] text-foreground/65 hover:text-foreground"
|
||||
className="thot-label mt-2 flex w-full items-center gap-1 px-1 pb-1 pt-1 text-left text-foreground/65 hover:text-foreground"
|
||||
>
|
||||
<span className="select-none">{archiveOpen ? "▾" : "▸"}</span>
|
||||
<span>Archive ({archivedList.length})</span>
|
||||
<span className="tabular-nums">Archive ({archivedList.length})</span>
|
||||
</button>
|
||||
{archiveOpen && (
|
||||
<NavSessions sessions={archivedList} activeSessionId={activeSessionId} onOpenPanel={openPanel} menuFor={menuFor} />
|
||||
@@ -409,7 +409,7 @@ export function AppShell() {
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] flex-col items-center justify-center text-center">
|
||||
<div className="flex min-h-[72vh] flex-col items-center justify-center text-center">
|
||||
<span className="font-heading text-5xl font-semibold tracking-tight text-foreground">
|
||||
Thoth<span className="text-primary">II</span>
|
||||
</span>
|
||||
|
||||
@@ -46,7 +46,7 @@ export function CentralStatus({ working }: { working: boolean }) {
|
||||
<div className="flex flex-col gap-3">
|
||||
{lastUserEntry && (
|
||||
<div className="rounded-lg border border-border/70 bg-card px-3 py-2">
|
||||
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
<span className="thot-label">
|
||||
{lastUserEntry.kind === "input" ? "You asked" : "You chose"}
|
||||
</span>
|
||||
<p className="mt-0.5 text-sm text-foreground">{lastUserEntry.text}</p>
|
||||
@@ -61,11 +61,11 @@ export function CentralStatus({ working }: { working: boolean }) {
|
||||
<span className="grid size-5 shrink-0 place-items-center text-primary">
|
||||
<WorkingSpinner spinning className="size-3.5" />
|
||||
</span>
|
||||
<span className="shrink-0 tabular-nums">{elapsed}s</span>
|
||||
<span className="shrink-0 font-mono tabular-nums">{elapsed}s</span>
|
||||
<span className="sr-only">ThothII is working</span>
|
||||
</div>
|
||||
{tailRows.length > 0 && (
|
||||
<ol className="ml-7 flex flex-col gap-0.5 overflow-hidden rounded-md border border-border/50 bg-muted/40 px-3 py-2">
|
||||
<ol className="ml-7 flex flex-col gap-0.5 overflow-hidden rounded-lg border border-border/60 bg-muted/40 px-3 py-2">
|
||||
{tailRows.map((line, i) => {
|
||||
const isLast = i === tailRows.length - 1;
|
||||
return (
|
||||
|
||||
@@ -36,7 +36,7 @@ export function ElapsedTimer({ running }: { running: boolean }) {
|
||||
const live = running && runStartRef.current != null ? Date.now() - runStartRef.current : 0;
|
||||
return (
|
||||
<span
|
||||
className="shrink-0 tabular-nums text-xs text-muted-foreground"
|
||||
className="shrink-0 font-mono tabular-nums text-xs text-muted-foreground"
|
||||
aria-label="Processing time"
|
||||
title="Processing time (excludes time spent waiting on the reviewer)"
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
|
||||
|
||||
return (
|
||||
<aside className="flex w-[30vw] max-w-[30vw] shrink-0 flex-col border-r border-border bg-sidebar">
|
||||
<div className="flex items-center justify-between border-b border-border/70 px-3 py-3">
|
||||
<div className="flex items-center justify-between border-b border-border/60 px-3 py-3">
|
||||
<h2 className="font-heading text-sm font-semibold text-foreground">Model activity</h2>
|
||||
<div className="flex items-center gap-1">
|
||||
{canExpand && (
|
||||
|
||||
@@ -39,7 +39,7 @@ export function NavSessions({ sessions, activeSessionId, onOpenPanel, menuFor }:
|
||||
}
|
||||
}}
|
||||
className={[
|
||||
"group flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1 text-left transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
|
||||
"group flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1 text-left transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
|
||||
// The in-progress (active) session is marked in yellow — both its dot
|
||||
// and its row background — so it stands out from the rest of the rail.
|
||||
active ? "bg-[oklch(var(--warning)/0.22)]" : "hover:bg-accent",
|
||||
|
||||
@@ -63,7 +63,7 @@ export function NewSessionDialog({ onCreated }: Props) {
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-lg border border-input bg-card px-3 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-primary/50 focus:ring-3 focus:ring-ring/15"
|
||||
className="w-full resize-none rounded-lg border border-input bg-card px-3 py-2 text-sm shadow-xs outline-none transition-[border-color,box-shadow] placeholder:text-muted-foreground focus:border-primary/50 focus:ring-3 focus:ring-ring/15"
|
||||
placeholder="Enter your question…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function RenameDialog({ open, initial, onOpenChange, onSubmit, title = "R
|
||||
id="rename-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-lg border border-input bg-card px-3 py-2 text-sm outline-none focus:border-primary/50 focus:ring-3 focus:ring-ring/15"
|
||||
className="w-full rounded-lg border border-input bg-card px-3 py-2 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-3 focus:ring-ring/15"
|
||||
placeholder="Session name…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
||||
<ul className="flex flex-col divide-y divide-border/50">
|
||||
{items.map((d, i) => (
|
||||
<li key={i} className="py-3 first:pt-0">
|
||||
<span className={`inline-block rounded-full px-2 py-0.5 text-[0.62rem] font-semibold uppercase tracking-wide ${decisionChipClass(d.type ?? "")}`}>
|
||||
<span className={`inline-block rounded-md px-2 py-0.5 text-[0.62rem] font-semibold uppercase tracking-wide ${decisionChipClass(d.type ?? "")}`}>
|
||||
{(d.type ?? "decision").replace(/_/g, " ")}
|
||||
</span>
|
||||
{d.subject && <p className="mt-1.5 text-sm font-medium text-foreground">{d.subject}</p>}
|
||||
@@ -77,12 +77,12 @@ export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
|
||||
return (
|
||||
<aside className="flex w-[30vw] max-w-[30vw] shrink-0 flex-col border-r border-border bg-sidebar">
|
||||
<div className="flex items-start justify-between gap-2 border-b border-border/70 px-3 py-3">
|
||||
<div className="flex items-start justify-between gap-2 border-b border-border/60 px-3 py-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate font-heading text-sm font-semibold text-foreground">
|
||||
{session.name || session.question}
|
||||
</h2>
|
||||
<p className="mt-0.5 text-[0.7rem] uppercase tracking-wide text-muted-foreground">{statusLabel(session)}</p>
|
||||
<p className="thot-label mt-0.5">{statusLabel(session)}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{resumable && (
|
||||
@@ -101,11 +101,11 @@ export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
||||
{docs.map((doc, idx) => (
|
||||
<section key={doc.key} className={idx > 0 ? "border-t border-border/50 pt-7" : ""}>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<h3 className="text-[0.7rem] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
||||
<h3 className="thot-label">
|
||||
{doc.title}
|
||||
</h3>
|
||||
{doc.phase !== "—" && (
|
||||
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[0.6rem] font-medium text-muted-foreground">
|
||||
<span className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[0.6rem] font-medium text-muted-foreground">
|
||||
{doc.phase}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -199,7 +199,7 @@ function FooterSelect({
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
{...rest}
|
||||
className={[
|
||||
"max-w-[10rem] cursor-pointer truncate rounded-md border border-border bg-card px-1.5 py-0.5 text-[0.7rem] text-foreground/80 outline-none transition-colors hover:bg-muted focus:border-primary/40 focus:ring-2 focus:ring-ring/15",
|
||||
"max-w-[10rem] cursor-pointer truncate rounded-lg border border-border bg-card px-1.5 py-0.5 text-[0.7rem] text-foreground/80 outline-none transition-colors hover:bg-muted focus:border-primary/40 focus:ring-2 focus:ring-ring/15",
|
||||
className,
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
|
||||
@@ -122,7 +122,7 @@ export function WorkflowBar({
|
||||
{phaseTitle && (
|
||||
<span
|
||||
data-testid="phase-title"
|
||||
className="text-[0.72rem] font-medium uppercase tracking-[0.12em] text-muted-foreground"
|
||||
className="thot-label"
|
||||
>
|
||||
{phaseTitle}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user