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"
|
||||
|
||||
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: {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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>}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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>;
|
||||
}
|
||||
|
||||
@@ -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) })}
|
||||
>
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user