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:
2026-07-05 13:04:46 +02:00
co-authored by Claude Fable 5
parent cc1ffb59fa
commit 4cd1cbc493
30 changed files with 140 additions and 77 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
{
variants: {
variant: {
+3 -3
View File
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-full border border-transparent bg-clip-padding text-sm font-semibold tracking-[0.005em] whitespace-nowrap transition-all outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/25 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-semibold tracking-[0.005em] whitespace-nowrap transition-all outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/25 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-[oklch(var(--primary-hover))]",
default: "bg-primary text-primary-foreground shadow-xs hover:bg-[oklch(var(--primary-hover))]",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
"border-border bg-card shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,oklch(var(--secondary)),oklch(var(--foreground))_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
+1 -1
View File
@@ -12,7 +12,7 @@ function Card({
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border/70 bg-card py-(--card-spacing) text-sm text-card-foreground shadow-sm [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
+2 -2
View File
@@ -53,7 +53,7 @@ function DialogContent({
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
@@ -102,7 +102,7 @@ function DialogFooter({
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-2xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
+1 -1
View File
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base shadow-xs transition-[border-color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
+51 -9
View File
@@ -15,6 +15,14 @@
:root {
--font-sans: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Arial, sans-serif;
--font-heading: "Fraunces", "Source Serif Pro", Georgia, "Times New Roman", serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
/* Layered elevation, tinted with the foreground hue so depth reads warm on
warm surfaces. Borders mark structure; shadows mark elevation. */
--shadow-tint: 0.2678 0.0097 355.6;
--shadow-xs: 0 1px 2px oklch(var(--shadow-tint) / 0.05);
--shadow-sm: 0 1px 2px oklch(var(--shadow-tint) / 0.05), 0 2px 6px -1px oklch(var(--shadow-tint) / 0.05);
--shadow-md: 0 2px 4px -2px oklch(var(--shadow-tint) / 0.06), 0 12px 32px -8px oklch(var(--shadow-tint) / 0.1);
--background: 0.9918 0.0011 17.2; /* surface */
--foreground: 0.2678 0.0097 355.6; /* text */
@@ -52,6 +60,7 @@
.dark,
[data-bs-theme="dark"] {
--shadow-tint: 0 0 0;
--background: 0.2178 0 90;
--foreground: 0.9310 0 90;
--card: 0.2350 0 90;
@@ -98,7 +107,12 @@
h1, h2, h3, h4 {
font-family: var(--font-heading);
font-optical-sizing: auto;
letter-spacing: -0.01em;
letter-spacing: -0.015em;
}
code, pre, kbd, samp {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
::selection {
@@ -130,6 +144,15 @@
* Scoped so it never leaks into widgets.
*/
@layer components {
/* Standard micro-label: panel headers, nav sections, meta rows. */
.thot-label {
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: oklch(var(--muted-foreground));
}
.thot-prose {
color: oklch(var(--foreground));
font-size: 0.9375rem;
@@ -145,25 +168,27 @@
margin: 1.1em 0 0.5em;
letter-spacing: -0.01em;
}
.thot-prose h1 { font-size: 1.4rem; }
.thot-prose h1 { font-size: 1.45rem; }
.thot-prose h2 { font-size: 1.2rem; }
.thot-prose h3 { font-size: 1.05rem; }
.thot-prose h3 { font-size: 1.02rem; }
.thot-prose ul, .thot-prose ol { margin: 0.6em 0; padding-left: 1.3em; }
.thot-prose li { margin: 0.25em 0; }
.thot-prose a { color: oklch(var(--primary)); text-decoration: underline; text-underline-offset: 2px; }
.thot-prose code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-family: var(--font-mono);
font-size: 0.85em;
background: oklch(var(--muted));
padding: 0.12em 0.4em;
border-radius: 0.3rem;
border: 1px solid oklch(var(--border) / 0.6);
}
.thot-prose pre {
background: oklch(var(--muted));
padding: 0.9rem 1rem;
border-radius: var(--radius);
border-radius: calc(var(--radius) + 2px);
overflow-x: auto;
border: 1px solid oklch(var(--border));
border: 1px solid oklch(var(--border) / 0.7);
box-shadow: var(--shadow-xs);
}
.thot-prose pre code { background: transparent; padding: 0; }
.thot-prose blockquote {
@@ -172,7 +197,24 @@
color: oklch(var(--muted-foreground));
margin: 0.7em 0;
}
.thot-prose table { border-collapse: collapse; width: 100%; margin: 0.7em 0; font-size: 0.875rem; }
.thot-prose th, .thot-prose td { border: 1px solid oklch(var(--border)); padding: 0.4em 0.6em; text-align: left; }
.thot-prose th { background: oklch(var(--muted)); font-weight: 600; }
.thot-prose table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
margin: 0.7em 0;
font-size: 0.875rem;
border: 1px solid oklch(var(--border) / 0.8);
border-radius: calc(var(--radius) + 2px);
overflow: hidden;
}
.thot-prose th, .thot-prose td { padding: 0.45em 0.7em; text-align: left; }
.thot-prose td { border-top: 1px solid oklch(var(--border) / 0.6); font-variant-numeric: tabular-nums; }
.thot-prose th {
background: oklch(var(--muted));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: oklch(var(--muted-foreground));
}
}
+9 -9
View File
@@ -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>
+3 -3
View File
@@ -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 (
+1 -1
View File
@@ -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)"
>
+1 -1
View File
@@ -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 && (
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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>
+5 -5
View File
@@ -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>
)}
+1 -1
View File
@@ -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(" ")}
>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -109,7 +109,7 @@ function StructuredValue({ value }: { value: unknown }): ReactElement {
<div className="flex flex-col gap-4">
{Object.entries(value as Record<string, unknown>).map(([k, v]) => (
<section key={k}>
<h4 className="mb-1.5 text-[0.72rem] font-semibold uppercase tracking-[0.1em] text-muted-foreground">
<h4 className="thot-label mb-1.5">
{humanizeKey(k)}
</h4>
<StructuredValue value={v} />
+2 -2
View File
@@ -16,7 +16,7 @@ function MermaidBlock({ code }: { code: string }) {
);
}, [code]);
if (error) return <pre className="text-red-500 text-xs">{error}</pre>;
if (error) return <pre className="text-destructive text-xs">{error}</pre>;
if (!svg) return <pre className="text-xs opacity-50">{code}</pre>;
// eslint-disable-next-line react/no-danger
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
@@ -36,7 +36,7 @@ const components: Components = {
);
},
pre({ children }) {
return <pre className="overflow-auto rounded bg-muted p-3 text-sm">{children}</pre>;
return <pre className="overflow-auto rounded-lg bg-muted p-3 text-sm shadow-xs">{children}</pre>;
},
};
+12 -4
View File
@@ -43,8 +43,9 @@ export function ResultsPanel({ sessionId }: Props) {
return (
<div>
{/* Limit selector */}
<div style={{ marginBottom: 8, display: "flex", gap: 8, alignItems: "center" }}>
<div className="mb-2 flex items-center gap-2">
<select
className="rounded-lg border border-input bg-card px-2 py-1 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-2 focus:ring-ring/20"
value={limit}
onChange={(e) =>
setLimit(e.target.value === "all" ? 1_000_000 : Number(e.target.value))
@@ -55,7 +56,12 @@ export function ResultsPanel({ sessionId }: Props) {
<option value="all">all</option>
</select>
<button onClick={handleExport}>Export CSV</button>
<button
className="rounded-md border border-border/70 px-3 py-1 text-sm shadow-xs transition-colors hover:bg-accent"
onClick={handleExport}
>
Export CSV
</button>
</div>
{/* Loading / error states */}
@@ -70,8 +76,10 @@ export function ResultsPanel({ sessionId }: Props) {
{/* Grid for everything else */}
{data && !isScalar && (
<>
<div className="ag-theme-alpine" style={{ height: 400, width: "100%" }}>
<AgGridReact columnDefs={columnDefs} rowData={rowData} />
<div className="overflow-hidden rounded-xl border border-border/70 shadow-xs">
<div className="ag-theme-alpine" style={{ height: 400, width: "100%" }}>
<AgGridReact columnDefs={columnDefs} rowData={rowData} />
</div>
</div>
{data.truncated && <div>(results truncated)</div>}
</>
+2 -2
View File
@@ -133,13 +133,13 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
<button
onClick={() => setView("chart")}
disabled={oversized}
className={view === "chart" ? "font-bold" : ""}
className={`rounded-md border border-border/70 px-2 py-1 text-xs shadow-xs transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50 ${view === "chart" ? "font-semibold" : ""}`}
>
Graph
</button>
<button
onClick={() => setView("table")}
className={view === "table" ? "font-bold" : ""}
className={`rounded-md border border-border/70 px-2 py-1 text-xs shadow-xs transition-colors hover:bg-accent ${view === "table" ? "font-semibold" : ""}`}
>
Table
</button>
+7 -7
View File
@@ -18,9 +18,9 @@ const STATUS_LABELS: Record<string, string> = {
};
const STATUS_CLASSES: Record<string, string> = {
passed: "bg-green-100 text-green-800",
failed: "bg-red-100 text-red-800",
untested: "bg-gray-100 text-gray-600",
passed: "bg-[oklch(var(--success)/0.15)] text-[oklch(0.45_0.12_165)] ring-1 ring-[oklch(var(--success)/0.3)]",
failed: "bg-destructive/10 text-destructive ring-1 ring-destructive/25",
untested: "bg-muted text-muted-foreground ring-1 ring-border",
};
function Block({ block }: { block: SqlBlock }) {
@@ -43,7 +43,7 @@ function Block({ block }: { block: SqlBlock }) {
: "";
return (
<div className="sql-block border rounded mb-2">
<div className="sql-block rounded-lg border border-border/70 mb-2">
<button
className="w-full flex items-center gap-2 px-3 py-2 text-left font-mono text-sm hover:bg-muted/30"
onClick={() => setOpen((o) => !o)}
@@ -58,7 +58,7 @@ function Block({ block }: { block: SqlBlock }) {
)}
{badge && (
<span
className={`ml-auto text-xs px-1.5 py-0.5 rounded ${badgeClass}`}
className={`ml-auto text-xs px-1.5 py-0.5 rounded-md ${badgeClass}`}
>
{badge}
</span>
@@ -71,7 +71,7 @@ function Block({ block }: { block: SqlBlock }) {
// eslint-disable-next-line react/no-danger
<div dangerouslySetInnerHTML={{ __html: html }} />
) : (
<pre className="text-sm opacity-50">{block.sql}</pre>
<pre className="font-mono text-sm opacity-50">{block.sql}</pre>
)}
{block.comments && Object.keys(block.comments).length > 0 && (
<dl className="mt-2 text-xs text-muted-foreground grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5">
@@ -96,7 +96,7 @@ export function SqlViewer({ blocks }: { blocks: SqlBlock[] }) {
<div className="sql-viewer">
<div className="flex gap-2 mb-2">
<button
className="text-xs px-2 py-1 border rounded hover:bg-muted/30"
className="text-xs px-2 py-1 rounded-md border border-border/70 shadow-xs transition-colors hover:bg-accent"
onClick={() =>
setLayout((l) => (l === "vertical" ? "horizontal" : "vertical"))
}
+8 -4
View File
@@ -34,7 +34,7 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) {
>
<DialogTitle>{descriptor.title ?? "Artifact review"}</DialogTitle>
<div className="min-h-0 overflow-auto rounded border bg-background p-3">
<div className="min-h-0 overflow-auto rounded-xl border border-border/70 bg-background p-3">
{descriptor.artifact ? (
<ArtifactView artifact={descriptor.artifact} />
) : (
@@ -42,7 +42,7 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) {
)}
</div>
<div className="flex flex-col gap-2 border-t pt-3">
<div className="flex flex-col gap-2 border-t border-border/60 pt-3">
{pendingLinkage ? (
<LinkageHost
parentResponse={pendingLinkage.parentResponse}
@@ -52,10 +52,14 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) {
) : (
<>
<div className="flex flex-wrap gap-2">
{descriptor.options?.map((o) => (
{descriptor.options?.map((o, i) => (
<button
key={o.id}
className="rounded border px-3 py-2 text-left hover:bg-accent"
className={
i === 0
? "rounded-lg bg-primary px-3.5 py-2 text-left font-semibold text-primary-foreground shadow-xs transition-colors hover:bg-[oklch(var(--primary-hover))]"
: "rounded-lg border border-border/70 px-3 py-2 text-left shadow-xs transition-colors hover:bg-accent"
}
onClick={() => handleOption(o.id)}
>
{o.label}
+1 -1
View File
@@ -10,7 +10,7 @@ export function ArtifactWidget({ descriptor }: WidgetProps) {
<div className="space-y-2">
{descriptor.title && <p className="font-medium">{descriptor.title}</p>}
{descriptor.artifact?.content && (
<pre className="overflow-auto max-h-64 bg-muted p-3 text-sm rounded border">
<pre className="overflow-auto max-h-64 bg-muted p-3 text-sm font-mono rounded-lg border border-border/70 shadow-xs">
{descriptor.artifact.content}
</pre>
)}
+4 -4
View File
@@ -4,11 +4,11 @@ import type { WidgetProps } from "./types";
export function FallbackWidget({ descriptor, onRespond }: WidgetProps) {
const [text, setText] = useState("");
return (
<div className="border rounded p-3 space-y-2">
<div className="rounded-xl border border-border/70 bg-card p-3 shadow-sm space-y-2">
<p className="text-sm text-amber-600">Unsupported widget (kind: {descriptor.widget}) — respond manually</p>
<pre className="text-xs overflow-auto max-h-48 bg-muted p-2">{JSON.stringify(descriptor, null, 2)}</pre>
<textarea className="w-full border rounded p-1" value={text} onChange={(e) => setText(e.target.value)} />
<button className="border rounded px-2 py-1" onClick={() => onRespond({ id: descriptor.id, control: "freetext", text })}>Send</button>
<pre className="text-xs font-mono overflow-auto max-h-48 rounded-lg bg-muted p-2">{JSON.stringify(descriptor, null, 2)}</pre>
<textarea className="w-full rounded-lg border border-input bg-card p-2 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-2 focus:ring-ring/20" value={text} onChange={(e) => setText(e.target.value)} />
<button className="rounded-md border border-border/70 px-2 py-1 text-sm shadow-xs transition-colors hover:bg-accent" onClick={() => onRespond({ id: descriptor.id, control: "freetext", text })}>Send</button>
</div>
);
}
+3 -3
View File
@@ -4,20 +4,20 @@ import type { WidgetProps } from "./types";
export function FreetextWidget({ descriptor, onRespond }: WidgetProps) {
const [text, setText] = useState("");
return (
<div className="space-y-3">
<div className="space-y-3 rounded-xl border border-border/70 bg-card p-4 shadow-sm">
{descriptor.title && (
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
{descriptor.title}
</p>
)}
<textarea
className="min-h-24 w-full resize-y rounded-lg border border-input bg-card p-3 text-sm outline-none transition-colors focus:border-primary/40 focus:ring-2 focus:ring-ring/20"
className="min-h-24 w-full resize-y rounded-lg border border-input bg-card p-3 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-2 focus:ring-ring/20"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type your answer…"
/>
<button
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-[oklch(var(--primary-hover))]"
className="rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-xs transition-colors hover:bg-[oklch(var(--primary-hover))]"
onClick={() => onRespond({ id: descriptor.id, kind: "freetext", text })}
>
Send
+1 -1
View File
@@ -1,5 +1,5 @@
import type { WidgetProps } from "./types";
export function InfoWidget({ descriptor }: WidgetProps) {
const color = descriptor.level === "error" ? "text-red-600" : descriptor.level === "warning" ? "text-amber-600" : "text-foreground";
return <p className={`text-sm ${color}`}>{descriptor.text ?? descriptor.title}</p>;
return <p className={`text-sm leading-relaxed ${color}`}>{descriptor.text ?? descriptor.title}</p>;
}
+5 -3
View File
@@ -21,7 +21,7 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) {
const isDisabled = descriptor.allow_empty === false && checked.size === 0;
return (
<div className="space-y-4">
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-4 shadow-sm">
{descriptor.title && (
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
{descriptor.title}
@@ -46,7 +46,9 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) {
{options.map((o) => (
<label
key={o.id}
className="flex cursor-pointer items-center gap-2.5 rounded-lg border border-transparent px-2.5 py-2 text-sm text-foreground transition-colors hover:bg-accent"
className={`flex cursor-pointer items-center gap-2.5 rounded-lg border px-2.5 py-2 text-sm text-foreground transition-colors hover:bg-accent ${
checked.has(o.id) ? "border-border/60 bg-accent/60" : "border-transparent"
}`}
>
<input
type="checkbox"
@@ -61,7 +63,7 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) {
</div>
<div>
<button
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-[oklch(var(--primary-hover))] disabled:cursor-not-allowed disabled:opacity-50"
className="rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-xs transition-colors hover:bg-[oklch(var(--primary-hover))] disabled:cursor-not-allowed disabled:opacity-50"
disabled={isDisabled}
onClick={() => onRespond({ id: descriptor.id, kind: "multiselect", choices: Array.from(checked) })}
>
+2 -2
View File
@@ -28,13 +28,13 @@ export function ReservedControls({
{otherOpen && (
<div className="flex flex-col gap-2">
<textarea
className="min-h-20 w-full resize-y rounded-lg border border-input bg-card p-3 text-sm outline-none transition-colors focus:border-primary/40 focus:ring-2 focus:ring-ring/20"
className="min-h-20 w-full resize-y rounded-lg border border-input bg-card p-3 text-sm shadow-xs outline-none transition-[border-color,box-shadow] focus:border-primary/50 focus:ring-2 focus:ring-ring/20"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Describe your alternative…"
/>
<button
className="self-start rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-[oklch(var(--primary-hover))] disabled:cursor-not-allowed disabled:opacity-50"
className="self-start rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground shadow-xs transition-colors hover:bg-[oklch(var(--primary-hover))] disabled:cursor-not-allowed disabled:opacity-50"
disabled={!text.trim()}
onClick={() => onControl("freetext", text)}
>
+2 -2
View File
@@ -3,7 +3,7 @@ import { ReservedControls } from "./ReservedControls";
export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
return (
<div className="space-y-4">
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-4 shadow-sm">
{(descriptor.title || descriptor.intro) && (
<div className="space-y-1.5">
{descriptor.title && (
@@ -27,7 +27,7 @@ export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
>
<span>{o.label}</span>
{o.recommended && (
<span className="shrink-0 rounded-full bg-[oklch(var(--success)/0.16)] px-2 py-0.5 text-[0.65rem] font-medium text-[oklch(0.5_0.13_165)]">
<span className="shrink-0 rounded-md bg-[oklch(var(--success)/0.16)] px-2 py-0.5 text-[0.65rem] font-medium text-[oklch(0.5_0.13_165)]">
consigliato
</span>
)}
+7
View File
@@ -53,9 +53,16 @@ export default {
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
boxShadow: {
xs: "var(--shadow-xs)",
sm: "var(--shadow-sm)",
DEFAULT: "var(--shadow-sm)",
md: "var(--shadow-md)",
},
fontFamily: {
sans: ["var(--font-sans)", "sans-serif"],
heading: ["var(--font-heading)", "sans-serif"],
mono: ["var(--font-mono)", "monospace"],
},
},
},