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:
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
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:
|
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:
|
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",
|
"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:
|
ghost:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function Card({
|
|||||||
data-slot="card"
|
data-slot="card"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Popup
|
<DialogPrimitive.Popup
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -102,7 +102,7 @@ function DialogFooter({
|
|||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+51
-9
@@ -15,6 +15,14 @@
|
|||||||
:root {
|
:root {
|
||||||
--font-sans: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Arial, sans-serif;
|
--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-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 */
|
--background: 0.9918 0.0011 17.2; /* surface */
|
||||||
--foreground: 0.2678 0.0097 355.6; /* text */
|
--foreground: 0.2678 0.0097 355.6; /* text */
|
||||||
@@ -52,6 +60,7 @@
|
|||||||
|
|
||||||
.dark,
|
.dark,
|
||||||
[data-bs-theme="dark"] {
|
[data-bs-theme="dark"] {
|
||||||
|
--shadow-tint: 0 0 0;
|
||||||
--background: 0.2178 0 90;
|
--background: 0.2178 0 90;
|
||||||
--foreground: 0.9310 0 90;
|
--foreground: 0.9310 0 90;
|
||||||
--card: 0.2350 0 90;
|
--card: 0.2350 0 90;
|
||||||
@@ -98,7 +107,12 @@
|
|||||||
h1, h2, h3, h4 {
|
h1, h2, h3, h4 {
|
||||||
font-family: var(--font-heading);
|
font-family: var(--font-heading);
|
||||||
font-optical-sizing: auto;
|
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 {
|
::selection {
|
||||||
@@ -130,6 +144,15 @@
|
|||||||
* Scoped so it never leaks into widgets.
|
* Scoped so it never leaks into widgets.
|
||||||
*/
|
*/
|
||||||
@layer components {
|
@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 {
|
.thot-prose {
|
||||||
color: oklch(var(--foreground));
|
color: oklch(var(--foreground));
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
@@ -145,25 +168,27 @@
|
|||||||
margin: 1.1em 0 0.5em;
|
margin: 1.1em 0 0.5em;
|
||||||
letter-spacing: -0.01em;
|
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 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 ul, .thot-prose ol { margin: 0.6em 0; padding-left: 1.3em; }
|
||||||
.thot-prose li { margin: 0.25em 0; }
|
.thot-prose li { margin: 0.25em 0; }
|
||||||
.thot-prose a { color: oklch(var(--primary)); text-decoration: underline; text-underline-offset: 2px; }
|
.thot-prose a { color: oklch(var(--primary)); text-decoration: underline; text-underline-offset: 2px; }
|
||||||
.thot-prose code {
|
.thot-prose code {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
font-family: var(--font-mono);
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
background: oklch(var(--muted));
|
background: oklch(var(--muted));
|
||||||
padding: 0.12em 0.4em;
|
padding: 0.12em 0.4em;
|
||||||
border-radius: 0.3rem;
|
border-radius: 0.3rem;
|
||||||
|
border: 1px solid oklch(var(--border) / 0.6);
|
||||||
}
|
}
|
||||||
.thot-prose pre {
|
.thot-prose pre {
|
||||||
background: oklch(var(--muted));
|
background: oklch(var(--muted));
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.9rem 1rem;
|
||||||
border-radius: var(--radius);
|
border-radius: calc(var(--radius) + 2px);
|
||||||
overflow-x: auto;
|
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 pre code { background: transparent; padding: 0; }
|
||||||
.thot-prose blockquote {
|
.thot-prose blockquote {
|
||||||
@@ -172,7 +197,24 @@
|
|||||||
color: oklch(var(--muted-foreground));
|
color: oklch(var(--muted-foreground));
|
||||||
margin: 0.7em 0;
|
margin: 0.7em 0;
|
||||||
}
|
}
|
||||||
.thot-prose table { border-collapse: collapse; width: 100%; margin: 0.7em 0; font-size: 0.875rem; }
|
.thot-prose table {
|
||||||
.thot-prose th, .thot-prose td { border: 1px solid oklch(var(--border)); padding: 0.4em 0.6em; text-align: left; }
|
border-collapse: separate;
|
||||||
.thot-prose th { background: oklch(var(--muted)); font-weight: 600; }
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ export function AppShell() {
|
|||||||
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
|
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
|
||||||
<div className="shrink-0 px-6 pb-6 pt-2">
|
<div className="shrink-0 px-6 pb-6 pt-2">
|
||||||
<div className="mx-auto max-w-3xl">
|
<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">
|
<div className="px-2.5 py-2">
|
||||||
<SteerInput
|
<SteerInput
|
||||||
sessionId={activeSessionId}
|
sessionId={activeSessionId}
|
||||||
@@ -253,7 +253,7 @@ export function AppShell() {
|
|||||||
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
|
<h1 className="font-heading text-xl font-semibold leading-none tracking-tight text-foreground">
|
||||||
Thoth<span className="text-primary">II</span>
|
Thoth<span className="text-primary">II</span>
|
||||||
</h1>
|
</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
|
Datamart Builder with
|
||||||
<br />
|
<br />
|
||||||
Human In The Loop
|
Human In The Loop
|
||||||
@@ -273,7 +273,7 @@ export function AppShell() {
|
|||||||
|
|
||||||
{/* L1 — rail title */}
|
{/* L1 — rail title */}
|
||||||
<div className="px-4 pb-1.5 pt-1">
|
<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
|
Sessions
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,7 +283,7 @@ export function AppShell() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setActiveOpen((v) => !v)}
|
onClick={() => setActiveOpen((v) => !v)}
|
||||||
aria-expanded={activeOpen}
|
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 className="select-none">{activeOpen ? "▾" : "▸"}</span>
|
||||||
<span>Active sessions</span>
|
<span>Active sessions</span>
|
||||||
@@ -298,7 +298,7 @@ export function AppShell() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCollapsedGroups((c) => ({ ...c, [g]: !c[g] }))}
|
onClick={() => setCollapsedGroups((c) => ({ ...c, [g]: !c[g] }))}
|
||||||
aria-expanded={!collapsedGroups[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="select-none">{collapsedGroups[g] ? "▸" : "▾"}</span>
|
||||||
<span className="truncate">{g}</span>
|
<span className="truncate">{g}</span>
|
||||||
@@ -307,7 +307,7 @@ export function AppShell() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={`Rename group ${g}`}
|
aria-label={`Rename group ${g}`}
|
||||||
onClick={() => setRenameGroupTarget(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" />
|
<Pencil className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -341,10 +341,10 @@ export function AppShell() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setArchiveOpen((v) => !v)}
|
onClick={() => setArchiveOpen((v) => !v)}
|
||||||
aria-expanded={archiveOpen}
|
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 className="select-none">{archiveOpen ? "▾" : "▸"}</span>
|
||||||
<span>Archive ({archivedList.length})</span>
|
<span className="tabular-nums">Archive ({archivedList.length})</span>
|
||||||
</button>
|
</button>
|
||||||
{archiveOpen && (
|
{archiveOpen && (
|
||||||
<NavSessions sessions={archivedList} activeSessionId={activeSessionId} onOpenPanel={openPanel} menuFor={menuFor} />
|
<NavSessions sessions={archivedList} activeSessionId={activeSessionId} onOpenPanel={openPanel} menuFor={menuFor} />
|
||||||
@@ -409,7 +409,7 @@ export function AppShell() {
|
|||||||
|
|
||||||
function EmptyState() {
|
function EmptyState() {
|
||||||
return (
|
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">
|
<span className="font-heading text-5xl font-semibold tracking-tight text-foreground">
|
||||||
Thoth<span className="text-primary">II</span>
|
Thoth<span className="text-primary">II</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function CentralStatus({ working }: { working: boolean }) {
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{lastUserEntry && (
|
{lastUserEntry && (
|
||||||
<div className="rounded-lg border border-border/70 bg-card px-3 py-2">
|
<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"}
|
{lastUserEntry.kind === "input" ? "You asked" : "You chose"}
|
||||||
</span>
|
</span>
|
||||||
<p className="mt-0.5 text-sm text-foreground">{lastUserEntry.text}</p>
|
<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">
|
<span className="grid size-5 shrink-0 place-items-center text-primary">
|
||||||
<WorkingSpinner spinning className="size-3.5" />
|
<WorkingSpinner spinning className="size-3.5" />
|
||||||
</span>
|
</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>
|
<span className="sr-only">ThothII is working</span>
|
||||||
</div>
|
</div>
|
||||||
{tailRows.length > 0 && (
|
{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) => {
|
{tailRows.map((line, i) => {
|
||||||
const isLast = i === tailRows.length - 1;
|
const isLast = i === tailRows.length - 1;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function ElapsedTimer({ running }: { running: boolean }) {
|
|||||||
const live = running && runStartRef.current != null ? Date.now() - runStartRef.current : 0;
|
const live = running && runStartRef.current != null ? Date.now() - runStartRef.current : 0;
|
||||||
return (
|
return (
|
||||||
<span
|
<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"
|
aria-label="Processing time"
|
||||||
title="Processing time (excludes time spent waiting on the reviewer)"
|
title="Processing time (excludes time spent waiting on the reviewer)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex w-[30vw] max-w-[30vw] shrink-0 flex-col border-r border-border bg-sidebar">
|
<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>
|
<h2 className="font-heading text-sm font-semibold text-foreground">Model activity</h2>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{canExpand && (
|
{canExpand && (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function NavSessions({ sessions, activeSessionId, onOpenPanel, menuFor }:
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={[
|
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
|
// 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.
|
// and its row background — so it stands out from the rest of the rail.
|
||||||
active ? "bg-[oklch(var(--warning)/0.22)]" : "hover:bg-accent",
|
active ? "bg-[oklch(var(--warning)/0.22)]" : "hover:bg-accent",
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function NewSessionDialog({ onCreated }: Props) {
|
|||||||
value={question}
|
value={question}
|
||||||
onChange={(e) => setQuestion(e.target.value)}
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
rows={3}
|
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…"
|
placeholder="Enter your question…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function RenameDialog({ open, initial, onOpenChange, onSubmit, title = "R
|
|||||||
id="rename-name"
|
id="rename-name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
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…"
|
placeholder="Session name…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function DocBody({ doc }: { doc: SessionDocument }) {
|
|||||||
<ul className="flex flex-col divide-y divide-border/50">
|
<ul className="flex flex-col divide-y divide-border/50">
|
||||||
{items.map((d, i) => (
|
{items.map((d, i) => (
|
||||||
<li key={i} className="py-3 first:pt-0">
|
<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, " ")}
|
{(d.type ?? "decision").replace(/_/g, " ")}
|
||||||
</span>
|
</span>
|
||||||
{d.subject && <p className="mt-1.5 text-sm font-medium text-foreground">{d.subject}</p>}
|
{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 (
|
return (
|
||||||
<aside className="flex w-[30vw] max-w-[30vw] shrink-0 flex-col border-r border-border bg-sidebar">
|
<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">
|
<div className="min-w-0">
|
||||||
<h2 className="truncate font-heading text-sm font-semibold text-foreground">
|
<h2 className="truncate font-heading text-sm font-semibold text-foreground">
|
||||||
{session.name || session.question}
|
{session.name || session.question}
|
||||||
</h2>
|
</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>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
{resumable && (
|
{resumable && (
|
||||||
@@ -101,11 +101,11 @@ export function SessionDocumentsPanel({ session, onClose, onResume }: Props) {
|
|||||||
{docs.map((doc, idx) => (
|
{docs.map((doc, idx) => (
|
||||||
<section key={doc.key} className={idx > 0 ? "border-t border-border/50 pt-7" : ""}>
|
<section key={doc.key} className={idx > 0 ? "border-t border-border/50 pt-7" : ""}>
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<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}
|
{doc.title}
|
||||||
</h3>
|
</h3>
|
||||||
{doc.phase !== "—" && (
|
{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}
|
{doc.phase}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ function FooterSelect({
|
|||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
{...rest}
|
{...rest}
|
||||||
className={[
|
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,
|
className,
|
||||||
].filter(Boolean).join(" ")}
|
].filter(Boolean).join(" ")}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export function WorkflowBar({
|
|||||||
{phaseTitle && (
|
{phaseTitle && (
|
||||||
<span
|
<span
|
||||||
data-testid="phase-title"
|
data-testid="phase-title"
|
||||||
className="text-[0.72rem] font-medium uppercase tracking-[0.12em] text-muted-foreground"
|
className="thot-label"
|
||||||
>
|
>
|
||||||
{phaseTitle}
|
{phaseTitle}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function StructuredValue({ value }: { value: unknown }): ReactElement {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{Object.entries(value as Record<string, unknown>).map(([k, v]) => (
|
{Object.entries(value as Record<string, unknown>).map(([k, v]) => (
|
||||||
<section key={k}>
|
<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)}
|
{humanizeKey(k)}
|
||||||
</h4>
|
</h4>
|
||||||
<StructuredValue value={v} />
|
<StructuredValue value={v} />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function MermaidBlock({ code }: { code: string }) {
|
|||||||
);
|
);
|
||||||
}, [code]);
|
}, [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>;
|
if (!svg) return <pre className="text-xs opacity-50">{code}</pre>;
|
||||||
// eslint-disable-next-line react/no-danger
|
// eslint-disable-next-line react/no-danger
|
||||||
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
|
||||||
@@ -36,7 +36,7 @@ const components: Components = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
pre({ children }) {
|
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>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,8 +43,9 @@ export function ResultsPanel({ sessionId }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Limit selector */}
|
{/* Limit selector */}
|
||||||
<div style={{ marginBottom: 8, display: "flex", gap: 8, alignItems: "center" }}>
|
<div className="mb-2 flex items-center gap-2">
|
||||||
<select
|
<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}
|
value={limit}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setLimit(e.target.value === "all" ? 1_000_000 : Number(e.target.value))
|
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>
|
<option value="all">all</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Loading / error states */}
|
{/* Loading / error states */}
|
||||||
@@ -70,8 +76,10 @@ export function ResultsPanel({ sessionId }: Props) {
|
|||||||
{/* Grid for everything else */}
|
{/* Grid for everything else */}
|
||||||
{data && !isScalar && (
|
{data && !isScalar && (
|
||||||
<>
|
<>
|
||||||
<div className="ag-theme-alpine" style={{ height: 400, width: "100%" }}>
|
<div className="overflow-hidden rounded-xl border border-border/70 shadow-xs">
|
||||||
<AgGridReact columnDefs={columnDefs} rowData={rowData} />
|
<div className="ag-theme-alpine" style={{ height: 400, width: "100%" }}>
|
||||||
|
<AgGridReact columnDefs={columnDefs} rowData={rowData} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{data.truncated && <div>(results truncated)</div>}
|
{data.truncated && <div>(results truncated)</div>}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -133,13 +133,13 @@ export function SchemaLinkingViewer({ linking }: { linking: SchemaLinking }) {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setView("chart")}
|
onClick={() => setView("chart")}
|
||||||
disabled={oversized}
|
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
|
Graph
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setView("table")}
|
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
|
Table
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_CLASSES: Record<string, string> = {
|
const STATUS_CLASSES: Record<string, string> = {
|
||||||
passed: "bg-green-100 text-green-800",
|
passed: "bg-[oklch(var(--success)/0.15)] text-[oklch(0.45_0.12_165)] ring-1 ring-[oklch(var(--success)/0.3)]",
|
||||||
failed: "bg-red-100 text-red-800",
|
failed: "bg-destructive/10 text-destructive ring-1 ring-destructive/25",
|
||||||
untested: "bg-gray-100 text-gray-600",
|
untested: "bg-muted text-muted-foreground ring-1 ring-border",
|
||||||
};
|
};
|
||||||
|
|
||||||
function Block({ block }: { block: SqlBlock }) {
|
function Block({ block }: { block: SqlBlock }) {
|
||||||
@@ -43,7 +43,7 @@ function Block({ block }: { block: SqlBlock }) {
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sql-block border rounded mb-2">
|
<div className="sql-block rounded-lg border border-border/70 mb-2">
|
||||||
<button
|
<button
|
||||||
className="w-full flex items-center gap-2 px-3 py-2 text-left font-mono text-sm hover:bg-muted/30"
|
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)}
|
onClick={() => setOpen((o) => !o)}
|
||||||
@@ -58,7 +58,7 @@ function Block({ block }: { block: SqlBlock }) {
|
|||||||
)}
|
)}
|
||||||
{badge && (
|
{badge && (
|
||||||
<span
|
<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}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
@@ -71,7 +71,7 @@ function Block({ block }: { block: SqlBlock }) {
|
|||||||
// eslint-disable-next-line react/no-danger
|
// eslint-disable-next-line react/no-danger
|
||||||
<div dangerouslySetInnerHTML={{ __html: html }} />
|
<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 && (
|
{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">
|
<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="sql-viewer">
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
<button
|
<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={() =>
|
onClick={() =>
|
||||||
setLayout((l) => (l === "vertical" ? "horizontal" : "vertical"))
|
setLayout((l) => (l === "vertical" ? "horizontal" : "vertical"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
>
|
>
|
||||||
<DialogTitle>{descriptor.title ?? "Artifact review"}</DialogTitle>
|
<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 ? (
|
{descriptor.artifact ? (
|
||||||
<ArtifactView artifact={descriptor.artifact} />
|
<ArtifactView artifact={descriptor.artifact} />
|
||||||
) : (
|
) : (
|
||||||
@@ -42,7 +42,7 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 ? (
|
{pendingLinkage ? (
|
||||||
<LinkageHost
|
<LinkageHost
|
||||||
parentResponse={pendingLinkage.parentResponse}
|
parentResponse={pendingLinkage.parentResponse}
|
||||||
@@ -52,10 +52,14 @@ export function ArtifactGateWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{descriptor.options?.map((o) => (
|
{descriptor.options?.map((o, i) => (
|
||||||
<button
|
<button
|
||||||
key={o.id}
|
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)}
|
onClick={() => handleOption(o.id)}
|
||||||
>
|
>
|
||||||
{o.label}
|
{o.label}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function ArtifactWidget({ descriptor }: WidgetProps) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{descriptor.title && <p className="font-medium">{descriptor.title}</p>}
|
{descriptor.title && <p className="font-medium">{descriptor.title}</p>}
|
||||||
{descriptor.artifact?.content && (
|
{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}
|
{descriptor.artifact.content}
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import type { WidgetProps } from "./types";
|
|||||||
export function FallbackWidget({ descriptor, onRespond }: WidgetProps) {
|
export function FallbackWidget({ descriptor, onRespond }: WidgetProps) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
return (
|
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>
|
<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>
|
<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 border rounded p-1" value={text} onChange={(e) => setText(e.target.value)} />
|
<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="border rounded px-2 py-1" onClick={() => onRespond({ id: descriptor.id, control: "freetext", text })}>Send</button>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ import type { WidgetProps } from "./types";
|
|||||||
export function FreetextWidget({ descriptor, onRespond }: WidgetProps) {
|
export function FreetextWidget({ descriptor, onRespond }: WidgetProps) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
return (
|
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 && (
|
{descriptor.title && (
|
||||||
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
|
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
|
||||||
{descriptor.title}
|
{descriptor.title}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<textarea
|
<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}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
placeholder="Type your answer…"
|
placeholder="Type your answer…"
|
||||||
/>
|
/>
|
||||||
<button
|
<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 })}
|
onClick={() => onRespond({ id: descriptor.id, kind: "freetext", text })}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { WidgetProps } from "./types";
|
import type { WidgetProps } from "./types";
|
||||||
export function InfoWidget({ descriptor }: WidgetProps) {
|
export function InfoWidget({ descriptor }: WidgetProps) {
|
||||||
const color = descriptor.level === "error" ? "text-red-600" : descriptor.level === "warning" ? "text-amber-600" : "text-foreground";
|
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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
const isDisabled = descriptor.allow_empty === false && checked.size === 0;
|
const isDisabled = descriptor.allow_empty === false && checked.size === 0;
|
||||||
|
|
||||||
return (
|
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.title && (
|
||||||
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
|
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
|
||||||
{descriptor.title}
|
{descriptor.title}
|
||||||
@@ -46,7 +46,9 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
{options.map((o) => (
|
{options.map((o) => (
|
||||||
<label
|
<label
|
||||||
key={o.id}
|
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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -61,7 +63,7 @@ export function MultiselectWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<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}
|
disabled={isDisabled}
|
||||||
onClick={() => onRespond({ id: descriptor.id, kind: "multiselect", choices: Array.from(checked) })}
|
onClick={() => onRespond({ id: descriptor.id, kind: "multiselect", choices: Array.from(checked) })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ export function ReservedControls({
|
|||||||
{otherOpen && (
|
{otherOpen && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<textarea
|
<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}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
placeholder="Describe your alternative…"
|
placeholder="Describe your alternative…"
|
||||||
/>
|
/>
|
||||||
<button
|
<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()}
|
disabled={!text.trim()}
|
||||||
onClick={() => onControl("freetext", text)}
|
onClick={() => onControl("freetext", text)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ReservedControls } from "./ReservedControls";
|
|||||||
|
|
||||||
export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
|
export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
|
||||||
return (
|
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) && (
|
{(descriptor.title || descriptor.intro) && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{descriptor.title && (
|
{descriptor.title && (
|
||||||
@@ -27,7 +27,7 @@ export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
|
|||||||
>
|
>
|
||||||
<span>{o.label}</span>
|
<span>{o.label}</span>
|
||||||
{o.recommended && (
|
{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
|
consigliato
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -53,9 +53,16 @@ export default {
|
|||||||
md: "calc(var(--radius) - 2px)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
|
boxShadow: {
|
||||||
|
xs: "var(--shadow-xs)",
|
||||||
|
sm: "var(--shadow-sm)",
|
||||||
|
DEFAULT: "var(--shadow-sm)",
|
||||||
|
md: "var(--shadow-md)",
|
||||||
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["var(--font-sans)", "sans-serif"],
|
sans: ["var(--font-sans)", "sans-serif"],
|
||||||
heading: ["var(--font-heading)", "sans-serif"],
|
heading: ["var(--font-heading)", "sans-serif"],
|
||||||
|
mono: ["var(--font-mono)", "monospace"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user