From 2b14c617f79ec2fda088d32f3ddd1f53fc3b80f8 Mon Sep 17 00:00:00 2001
From: TJ Baker <1617679+zaridan@users.noreply.github.com>
Date: Mon, 20 Jul 2026 11:29:01 -0700
Subject: [PATCH 1/4] chore(web): re-pull the 16 design-system components
verbatim (HT-94)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
PR #103 stopped Biome from reformatting web/src/components/ds/ on arrival,
but it could not un-mangle the 16 components already there. This does that:
each file re-fetched from the Claude Design "Helpthread" project via
DesignSync and written byte-for-byte — double quotes, semicolons, original
import order and line wrapping restored.
Equivalence was proven before overwriting, not assumed. Both the pre-change
and post-change trees were formatted through one canonical Biome config and
diffed; the diff is empty across all 32 files. So no style value, prop, or
branch of logic changed — the whole historical drift really was formatting,
and nothing had been hand-edited in the app or moved in the design project.
Byte fidelity spot-checked against fresh get_file responses; all 32 files
end with a newline and none carry CRLF.
Gates (each on its own exit code): biome check . = 0, npm run typecheck = 0,
npm run build = 0.
Stacked on feat/ht-93-ds-new-primitives — the biome.json override there is a
precondition for the biome gate to pass on verbatim files.
---
web/src/components/ds/core/Avatar.d.ts | 14 +-
web/src/components/ds/core/Avatar.jsx | 56 ++---
web/src/components/ds/core/Button.d.ts | 16 +-
web/src/components/ds/core/Button.jsx | 77 ++-----
web/src/components/ds/core/DropdownMenu.d.ts | 14 +-
web/src/components/ds/core/DropdownMenu.jsx | 34 +--
web/src/components/ds/core/EmptyState.d.ts | 8 +-
web/src/components/ds/core/EmptyState.jsx | 33 +--
web/src/components/ds/core/IconButton.d.ts | 16 +-
web/src/components/ds/core/IconButton.jsx | 55 ++---
web/src/components/ds/core/Kbd.d.ts | 4 +-
web/src/components/ds/core/Kbd.jsx | 22 +-
web/src/components/ds/core/MenuItem.d.ts | 14 +-
web/src/components/ds/core/MenuItem.jsx | 52 ++---
web/src/components/ds/core/Skeleton.d.ts | 10 +-
web/src/components/ds/core/Skeleton.jsx | 20 +-
web/src/components/ds/core/StatusPill.d.ts | 8 +-
web/src/components/ds/core/StatusPill.jsx | 42 +---
web/src/components/ds/core/TagChip.d.ts | 6 +-
web/src/components/ds/core/TagChip.jsx | 54 +----
web/src/components/ds/core/TextInput.d.ts | 14 +-
web/src/components/ds/core/TextInput.jsx | 29 +--
web/src/components/ds/core/Toast.d.ts | 10 +-
web/src/components/ds/core/Toast.jsx | 19 +-
.../components/ds/inbox/ConversationRow.d.ts | 30 +--
.../components/ds/inbox/ConversationRow.jsx | 202 +++---------------
web/src/components/ds/inbox/FolderItem.d.ts | 14 +-
web/src/components/ds/inbox/FolderItem.jsx | 59 ++---
web/src/components/ds/inbox/MessageBand.d.ts | 22 +-
web/src/components/ds/inbox/MessageBand.jsx | 141 +++---------
web/src/components/ds/inbox/ToolbarBand.d.ts | 8 +-
web/src/components/ds/inbox/ToolbarBand.jsx | 29 +--
32 files changed, 306 insertions(+), 826 deletions(-)
diff --git a/web/src/components/ds/core/Avatar.d.ts b/web/src/components/ds/core/Avatar.d.ts
index b52c00e..0bdee97 100644
--- a/web/src/components/ds/core/Avatar.d.ts
+++ b/web/src/components/ds/core/Avatar.d.ts
@@ -1,13 +1,13 @@
export interface AvatarProps {
/** initials + tone derive from this */
- email?: string
+ email?: string;
/** explicit initials override */
- initials?: string
- size?: number
+ initials?: string;
+ size?: number;
/** the Agent avatar: accent fill, "S" */
- agent?: boolean
+ agent?: boolean;
/** white ring + shadow (threads, context panel) */
- ring?: boolean
- style?: React.CSSProperties
+ ring?: boolean;
+ style?: React.CSSProperties;
}
-export declare function Avatar(props: AvatarProps): JSX.Element
+export declare function Avatar(props: AvatarProps): JSX.Element;
diff --git a/web/src/components/ds/core/Avatar.jsx b/web/src/components/ds/core/Avatar.jsx
index 7b1bf01..85abaf5 100644
--- a/web/src/components/ds/core/Avatar.jsx
+++ b/web/src/components/ds/core/Avatar.jsx
@@ -1,46 +1,32 @@
-import React from 'react'
+import React from "react";
const PALETTE = [
- ['oklch(0.9 0.03 60)', 'oklch(0.42 0.07 60)'],
- ['oklch(0.9 0.03 150)', 'oklch(0.4 0.06 150)'],
- ['oklch(0.9 0.03 250)', 'oklch(0.42 0.06 250)'],
- ['oklch(0.9 0.03 320)', 'oklch(0.42 0.06 320)'],
- ['oklch(0.9 0.035 95)', 'oklch(0.42 0.06 95)'],
-]
+ ["oklch(0.9 0.03 60)", "oklch(0.42 0.07 60)"],
+ ["oklch(0.9 0.03 150)", "oklch(0.4 0.06 150)"],
+ ["oklch(0.9 0.03 250)", "oklch(0.42 0.06 250)"],
+ ["oklch(0.9 0.03 320)", "oklch(0.42 0.06 320)"],
+ ["oklch(0.9 0.035 95)", "oklch(0.42 0.06 95)"],
+];
function initialsOf(email) {
- const local = String(email || '').split('@')[0]
- const parts = local.split(/[._-]+/).filter(Boolean)
- if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase()
- return local.slice(0, 2).toUpperCase()
+ const local = String(email || "").split("@")[0];
+ const parts = local.split(/[._-]+/).filter(Boolean);
+ if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
+ return local.slice(0, 2).toUpperCase();
}
/** Initials avatar. Tone is a stable hash of the email; agent=true fills with accent.
* ring adds the white ring + shadow used in threads and the context panel. */
export function Avatar({ email, initials, size = 32, agent = false, ring = true, style }) {
- let h = 0
- for (const ch of String(email || initials || '?')) h = (h * 31 + ch.charCodeAt(0)) >>> 0
- const [bg, fg] = agent ? ['var(--ht-accent)', 'var(--ht-on-accent)'] : PALETTE[h % PALETTE.length]
+ let h = 0;
+ for (const ch of String(email || initials || "?")) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
+ const [bg, fg] = agent ? ["var(--ht-accent)", "var(--ht-on-accent)"] : PALETTE[h % PALETTE.length];
return (
-
= 48 ? '3px' : '2px') + ' solid var(--ht-surface)' : 'none',
- boxShadow: ring ? (size >= 48 ? 'var(--ht-shadow-md)' : 'var(--ht-shadow-sm)') : 'none',
- ...style,
- }}
- >
- {initials || (agent ? 'S' : initialsOf(email))}
+
= 48 ? "3px" : "2px") + " solid var(--ht-surface)" : "none",
+ boxShadow: ring ? (size >= 48 ? "var(--ht-shadow-md)" : "var(--ht-shadow-sm)") : "none", ...style }}>
+ {initials || (agent ? "S" : initialsOf(email))}
- )
+ );
}
diff --git a/web/src/components/ds/core/Button.d.ts b/web/src/components/ds/core/Button.d.ts
index 278e31e..8c3936d 100644
--- a/web/src/components/ds/core/Button.d.ts
+++ b/web/src/components/ds/core/Button.d.ts
@@ -1,13 +1,13 @@
/** The brand's one button. Sentence case labels; destructive actions arm on first press instead of opening a modal. */
export interface ButtonProps {
/** primary | outline | ghost | destructive */
- variant?: 'primary' | 'outline' | 'ghost' | 'destructive'
+ variant?: "primary" | "outline" | "ghost" | "destructive";
/** destructive two-step arm state ("Confirm" fill) */
- armed?: boolean
- disabled?: boolean
- title?: string
- onClick?: () => void
- style?: React.CSSProperties
- children: React.ReactNode
+ armed?: boolean;
+ disabled?: boolean;
+ title?: string;
+ onClick?: () => void;
+ style?: React.CSSProperties;
+ children: React.ReactNode;
}
-export declare function Button(props: ButtonProps): JSX.Element
+export declare function Button(props: ButtonProps): JSX.Element;
diff --git a/web/src/components/ds/core/Button.jsx b/web/src/components/ds/core/Button.jsx
index 580fa08..69830c2 100644
--- a/web/src/components/ds/core/Button.jsx
+++ b/web/src/components/ds/core/Button.jsx
@@ -1,72 +1,27 @@
-import React from 'react'
+import React from "react";
/** Primary action button. variant: primary | outline | ghost | destructive.
* Destructive supports the two-step arm pattern via the `armed` prop. */
-export function Button({
- variant = 'primary',
- armed = false,
- disabled = false,
- onClick,
- title,
- style,
- children,
-}) {
+export function Button({ variant = "primary", armed = false, disabled = false, onClick, title, style, children }) {
const base = {
- display: 'inline-flex',
- alignItems: 'center',
- gap: 8,
- whiteSpace: 'nowrap',
- font: 'inherit',
- fontSize: '13.5px',
- fontWeight: 600,
- borderRadius: 'var(--ht-radius-md)',
- padding: '8px 18px',
- cursor: disabled ? 'default' : 'pointer',
- border: 'none',
- }
+ display: "inline-flex", alignItems: "center", gap: 8, whiteSpace: "nowrap",
+ font: "inherit", fontSize: "13.5px", fontWeight: 600, borderRadius: "var(--ht-radius-md)",
+ padding: "8px 18px", cursor: disabled ? "default" : "pointer", border: "none",
+ };
const variants = {
- primary: {
- color: 'var(--ht-on-accent)',
- background: disabled
- ? 'color-mix(in oklab, var(--ht-accent) 42%, var(--ht-bg))'
- : 'var(--ht-accent)',
- },
- outline: {
- color: 'var(--ht-ink)',
- background: 'var(--ht-surface)',
- border: '1px solid var(--ht-border)',
- padding: '7px 17px',
- },
- ghost: { color: 'var(--ht-ink-muted)', background: 'none' },
+ primary: { color: "var(--ht-on-accent)", background: disabled ? "color-mix(in oklab, var(--ht-accent) 42%, var(--ht-bg))" : "var(--ht-accent)" },
+ outline: { color: "var(--ht-ink)", background: "var(--ht-surface)", border: "1px solid var(--ht-border)", padding: "7px 17px" },
+ ghost: { color: "var(--ht-ink-muted)", background: "none" },
destructive: armed
- ? {
- color: 'var(--ht-surface)',
- background: 'var(--ht-critical)',
- border: '1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)',
- padding: '7px 17px',
- }
- : {
- color: 'var(--ht-critical)',
- background: 'transparent',
- border: '1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)',
- padding: '7px 17px',
- },
- }
+ ? { color: "var(--ht-surface)", background: "var(--ht-critical)", border: "1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)", padding: "7px 17px" }
+ : { color: "var(--ht-critical)", background: "transparent", border: "1px solid color-mix(in oklab, var(--ht-critical) 40%, transparent)", padding: "7px 17px" },
+ };
return (
-
- )
+ );
}
diff --git a/web/src/components/ds/core/DropdownMenu.d.ts b/web/src/components/ds/core/DropdownMenu.d.ts
index 3185b32..13d88c5 100644
--- a/web/src/components/ds/core/DropdownMenu.d.ts
+++ b/web/src/components/ds/core/DropdownMenu.d.ts
@@ -1,9 +1,9 @@
export interface DropdownMenuProps {
- open: boolean
- onClose: () => void
- align?: 'left' | 'right'
- top?: number
- minWidth?: number
- children: React.ReactNode
+ open: boolean;
+ onClose: () => void;
+ align?: "left" | "right";
+ top?: number;
+ minWidth?: number;
+ children: React.ReactNode;
}
-export declare function DropdownMenu(props: DropdownMenuProps): JSX.Element
+export declare function DropdownMenu(props: DropdownMenuProps): JSX.Element;
diff --git a/web/src/components/ds/core/DropdownMenu.jsx b/web/src/components/ds/core/DropdownMenu.jsx
index d7d2f30..615077f 100644
--- a/web/src/components/ds/core/DropdownMenu.jsx
+++ b/web/src/components/ds/core/DropdownMenu.jsx
@@ -1,35 +1,17 @@
-import React from 'react'
+import React from "react";
/** Anchored dropdown surface with backdrop dismissal. Wrap the trigger in a
* position:relative parent and render this when open. */
-export function DropdownMenu({
- open,
- onClose,
- align = 'left',
- top = 36,
- minWidth = 160,
- children,
-}) {
- if (!open) return null
+export function DropdownMenu({ open, onClose, align = "left", top = 36, minWidth = 160, children }) {
+ if (!open) return null;
return (
<>
-
-
+
+
{children}
>
- )
+ );
}
diff --git a/web/src/components/ds/core/EmptyState.d.ts b/web/src/components/ds/core/EmptyState.d.ts
index dc1bf43..807fac2 100644
--- a/web/src/components/ds/core/EmptyState.d.ts
+++ b/web/src/components/ds/core/EmptyState.d.ts
@@ -1,7 +1,7 @@
export interface EmptyStateProps {
- title: string
- body?: string
+ title: string;
+ body?: string;
/** italic celebration ("Inbox zero.") */
- celebrate?: boolean
+ celebrate?: boolean;
}
-export declare function EmptyState(props: EmptyStateProps): JSX.Element
+export declare function EmptyState(props: EmptyStateProps): JSX.Element;
diff --git a/web/src/components/ds/core/EmptyState.jsx b/web/src/components/ds/core/EmptyState.jsx
index f91fe32..93d057b 100644
--- a/web/src/components/ds/core/EmptyState.jsx
+++ b/web/src/components/ds/core/EmptyState.jsx
@@ -1,33 +1,12 @@
-import React from 'react'
+import React from "react";
/** Typographic empty state. celebrate=true is the italic "Inbox zero." treatment. */
export function EmptyState({ title, body, celebrate = false }) {
return (
-
-
- {title}
-
- {body && (
-
- {body}
-
- )}
+
+
{title}
+ {body &&
{body}
}
- )
+ );
}
diff --git a/web/src/components/ds/core/IconButton.d.ts b/web/src/components/ds/core/IconButton.d.ts
index 47597ad..1864c00 100644
--- a/web/src/components/ds/core/IconButton.d.ts
+++ b/web/src/components/ds/core/IconButton.d.ts
@@ -1,13 +1,13 @@
export interface IconButtonProps {
- title: string
- onClick?: () => void
+ title: string;
+ onClick?: () => void;
/** lit background (menu open / active) */
- active?: boolean
+ active?: boolean;
/** "header" = on the accent top bar */
- tone?: 'default' | 'header'
- size?: number
- style?: React.CSSProperties
+ tone?: "default" | "header";
+ size?: number;
+ style?: React.CSSProperties;
/** a solid 13-16px SVG glyph */
- children: React.ReactNode
+ children: React.ReactNode;
}
-export declare function IconButton(props: IconButtonProps): JSX.Element
+export declare function IconButton(props: IconButtonProps): JSX.Element;
diff --git a/web/src/components/ds/core/IconButton.jsx b/web/src/components/ds/core/IconButton.jsx
index 83fcd4e..4b4bf10 100644
--- a/web/src/components/ds/core/IconButton.jsx
+++ b/web/src/components/ds/core/IconButton.jsx
@@ -1,49 +1,18 @@
-import React from 'react'
+import React from "react";
/** 32px square icon action. tone "header" renders on the accent top bar. */
-export function IconButton({
- title,
- onClick,
- active = false,
- tone = 'default',
- size = 32,
- style,
- children,
-}) {
- const header = tone === 'header'
- const fg = header
- ? 'color-mix(in oklab, var(--ht-header-fg) 85%, transparent)'
- : 'var(--ht-accent)'
- const hoverBg = header
- ? 'color-mix(in oklab, var(--ht-header-fg) 16%, transparent)'
- : 'var(--ht-accent-soft)'
+export function IconButton({ title, onClick, active = false, tone = "default", size = 32, style, children }) {
+ const header = tone === "header";
+ const fg = header ? "color-mix(in oklab, var(--ht-header-fg) 85%, transparent)" : "var(--ht-accent)";
+ const hoverBg = header ? "color-mix(in oklab, var(--ht-header-fg) 16%, transparent)" : "var(--ht-accent-soft)";
return (
-