diff --git a/frontend/src/index.css b/frontend/src/index.css index 3734f6a6..e892e0fa 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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)); } diff --git a/frontend/src/viewers/ArtifactView.tsx b/frontend/src/viewers/ArtifactView.tsx index bcbbcc3e..0241c574 100644 --- a/frontend/src/viewers/ArtifactView.tsx +++ b/frontend/src/viewers/ArtifactView.tsx @@ -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

—

; } @@ -98,21 +100,32 @@ function StructuredValue({ value }: { value: unknown }): ReactElement { return (
{value.map((item, i) => ( -
- +
+
))}
); } + const entries = Object.entries(value as Record); return ( -
- {Object.entries(value as Record).map(([k, v]) => ( -
-

- {humanizeKey(k)} -

- +
+ {entries.map(([k, v], i) => ( +
+

{humanizeKey(k)}

+
))}
diff --git a/frontend/src/widgets/ArtifactGateWidget.tsx b/frontend/src/widgets/ArtifactGateWidget.tsx index 59cd471e..cdc78fd7 100644 --- a/frontend/src/widgets/ArtifactGateWidget.tsx +++ b/frontend/src/widgets/ArtifactGateWidget.tsx @@ -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 - {descriptor.title ?? "Artifact review"} + + {descriptor.title ?? "Artifact review"} + -
+
{descriptor.artifact ? ( ) : (