style(frontend): look&feel v2 refinements — 70% gate modal, structured cards, mono labels

- ArtifactGateWidget: gate modal 90%→70% of viewport, larger heading, more padding.
- ArtifactView StructuredValue: depth-aware hierarchy (top-level hairline dividers,
  warm bg-muted card surfaces + shadow for object-array items).
- index.css: .thot-label micro-labels switched to the mono register (SF Mono) with
  tightened tracking, distinguishing labels/meta from sans body prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 23:29:49 +02:00
committed by Marco Pancotti
co-authored by Claude Opus 4.8
parent 9fe1c9321b
commit e9b2934ef7
3 changed files with 35 additions and 17 deletions
+5 -2
View File
@@ -144,11 +144,14 @@
* Scoped so it never leaks into widgets.
*/
@layer components {
/* Standard micro-label: panel headers, nav sections, meta rows. */
/* Standard micro-label: panel headers, nav sections, meta rows. Set in the
mono register so labels/meta read as a distinct typographic layer from the
sans body prose (mono needs less tracking than the sans did). */
.thot-label {
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.12em;
letter-spacing: 0.06em;
text-transform: uppercase;
color: oklch(var(--muted-foreground));
}
+24 -11
View File
@@ -77,8 +77,10 @@ function humanizeKey(k: string): string {
/** Renders any artifact payload as readable text — NEVER raw JSON. Objects become
* labelled sections, arrays become bullet lists, primitives become paragraphs;
* strings render as markdown. Nested structures recurse. */
function StructuredValue({ value }: { value: unknown }): ReactElement {
* strings render as markdown. Nested structures recurse. `depth` drives the
* visual hierarchy: top-level sections are split by hairline rules, nested fields
* sit tighter, and object-arrays become grouped cards. */
function StructuredValue({ value, depth = 0 }: { value: unknown; depth?: number }): ReactElement {
if (value === null || value === undefined || value === "") {
return <p className="text-sm text-muted-foreground">—</p>;
}
@@ -98,21 +100,32 @@ function StructuredValue({ value }: { value: unknown }): ReactElement {
return (
<div className="flex flex-col gap-3">
{value.map((item, i) => (
<div key={i} className="rounded-lg border border-border/60 p-3">
<StructuredValue value={item} />
<div
key={i}
className="rounded-xl border border-border/70 bg-muted/50 p-4 shadow-xs"
>
<StructuredValue value={item} depth={depth + 1} />
</div>
))}
</div>
);
}
const entries = Object.entries(value as Record<string, unknown>);
return (
<div className="flex flex-col gap-4">
{Object.entries(value as Record<string, unknown>).map(([k, v]) => (
<section key={k}>
<h4 className="thot-label mb-1.5">
{humanizeKey(k)}
</h4>
<StructuredValue value={v} />
<div className="flex flex-col">
{entries.map(([k, v], i) => (
<section
key={k}
className={
i === 0
? undefined
: depth === 0
? "mt-5 border-t border-border/50 pt-5"
: "mt-3"
}
>
<h4 className="thot-label mb-1.5">{humanizeKey(k)}</h4>
<StructuredValue value={v} depth={depth + 1} />
</section>
))}
</div>
+6 -4
View File
@@ -7,7 +7,7 @@ import { ReservedControls } from "./ReservedControls";
import { LinkageHost } from "./LinkageHost";
/**
* Artifact review gate rendered as a full-screen (90%) modal: the artifact fills
* Artifact review gate rendered as a centered (70%) modal: the artifact fills
* the top (scrollable) area via ArtifactView; the action bar (options + reserved
* controls) sits at the bottom. The gate contract forbids silent dismissal, so the
* dialog has no close button and is not closeable by Esc/backdrop — the only way
@@ -30,11 +30,13 @@ export function ArtifactGateWidget({ descriptor, onRespond, sessionId }: WidgetP
<Dialog open>
<DialogContent
showCloseButton={false}
className="grid h-[90vh] w-[90vw] max-w-[90vw] grid-rows-[auto_1fr_auto] gap-3 sm:max-w-[90vw]"
className="grid h-[70vh] w-[70vw] max-w-[70vw] grid-rows-[auto_1fr_auto] gap-3.5 sm:max-w-[70vw]"
>
<DialogTitle>{descriptor.title ?? "Artifact review"}</DialogTitle>
<DialogTitle className="text-lg leading-snug">
{descriptor.title ?? "Artifact review"}
</DialogTitle>
<div className="min-h-0 overflow-auto rounded-xl border border-border/70 bg-background p-3">
<div className="min-h-0 overflow-auto rounded-xl border border-border/70 bg-background p-5">
{descriptor.artifact ? (
<GateArtifactBody artifact={descriptor.artifact} sessionId={sessionId} />
) : (