Files
buzz/desktop/tailwind.config.js
ss-dev-01 f36b699220 style(desktop): bring the conversation variant closer to berd's recipes
dev-00's design-delta note measured the conversation variant against
berd's shipping chat surface and named three divergences that make the
focus view read as "not berd" regardless of how the rest is arranged.
This takes all three literally, on Buzz tokens rather than berd's.

**Agent identity row.** The largest gap was that agent prose carried no
attribution at all, so a reply read as unowned body text in a full-cover
view. berd labels every agent turn with a 20px round avatar plus the name
at `text-xs`, `mb-0.5`, `gap-1` (`MessageBubble.tsx:961-981`). Added for
the `conversation` variant only — the other variants' markup is pinned
byte-for-byte by the baseline fixture, and a test asserts the row does
not leak into them so a failure names the cause instead of printing a
markup diff.

**Prompt bubble.** berd's user turn is a soft fill with no border at all,
`px-4 py-2`, and a 12px radius (`MessageBubble.tsx:990`). berd's
`rounded-sm` is 12px on its own scale (`globals.css --radius-sm: 12px`),
not Tailwind's stock 2px; Buzz's `rounded-xl` is the exact equivalent, so
the radius drops from 16px rather than collapsing to a hairline. The cap
moves from `max-w-[85%]` to a fixed 640px measure mirroring berd's
`--chat-user-message-max-width` (`globals.css:615`): a percentage cap
re-wraps the prompt on every resize of the cover, while a fixed measure
holds one stable line length.

**Fenced code.** berd puts the language in a real header row above the
frame with the copy action opposite it, and frames the code at a 10px
radius on the page background behind a subtle border, with no shadow
(`ai-elements/code-block.tsx:379`, `:395`, `:528-529`). Buzz's
`rounded-lg` is `--radius: 0.625rem` — exactly berd's `rounded-[0.625rem]`.

The markdown renderer is shared with channel messages, so this recipe is
opt-in: `CodeBlockVariantContext` is read at render time by
`MarkdownCodeBlock` and provided by `MessageActivity`. A prop would have
to thread through `createMarkdownComponents`, whose component map must
stay module-stable and whose parsed-node cache keys on a variant string —
the same reason `VideoReviewMarkdownContext` already exists. A context
provider renders no DOM, so `MessageActivity` can wrap unconditionally
and `default`/`compactPreview` markup stays byte-identical. A companion
test renders the same fenced block through `default` and asserts the
original chrome (16px radius, muted fill, `pr-12`, absolutely-positioned
copy button) is untouched.

Fenced blocks need `ThemeProvider` and `TooltipProvider` to mount, so
those tests use a separate `renderTranscriptWithCodeChrome` helper; the
byte-for-byte fixture keeps rendering through the exact tree it was
captured with.

Both new code-block tests were proven non-vacuous by mutation: forcing
the provider to `default` everywhere fails only the focus-header test,
and forcing `focusProse` everywhere fails only the channel-message guard.
That guard is written as `assert.ok(x === null)` rather than
`assert.equal(x, null)` — on failure the latter serializes the matched
jsdom element and its subtree to build a diff, which exhausts memory
instead of printing the message.

Verified at this tree: full desktop suite 5427 passing / 0 failing,
`tsc --noEmit` clean, `biome check` at main's baseline, px-text /
pubkey-truncation / file-size checks clean.

Signed-off-by: ss-dev-01 <11939edb7df583f855dbef923f2358f1184538f88ca452e19e7e35e42ad6d796@buzz.block.builderlab.xyz>
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
2026-08-24 13:08:42 -07:00

154 lines
6.2 KiB
JavaScript

/** @type {import('tailwindcss').Config} */
export default {
theme: {
extend: {
// Sub-`text-xs` ramp for meta text (timestamps, count badges, tracking
// labels) and tiny glyphs. These follow the virtual typography rem
// (`--buzz-type-rem` in styles/globals/typography.css), which is
// rem-relative: Cmd +/- zooms it with the rest of the layout, and the
// Font size preference nudges it alone. Do NOT reintroduce arbitrary `text-[…rem]` / `text-[…px]` literals;
// the px-text guard rejects them. Stock scale picks up from xs.
fontSize: {
"2xs": "calc(var(--buzz-type-rem) * 0.6875)", // 11px at 16px type rem
"3xs": "calc(var(--buzz-type-rem) * 0.5)", // 8px at 16px type rem
badge: "calc(var(--buzz-type-rem) * 0.625)", // 10px at 16px type rem
// Shared channel, DM, thread, and composer type. Variables keep app-wide
// font size and keyboard zoom consistent without branching components.
message: [
"var(--conversation-message-font-size)",
{ lineHeight: "var(--conversation-message-line-height)" },
],
"message-timestamp": [
"var(--conversation-timestamp-font-size)",
{ lineHeight: "var(--conversation-timestamp-line-height)" },
],
// 40px at the 16px type rem — onboarding page titles.
title: [
"calc(var(--buzz-type-rem) * 2.5)",
{ lineHeight: "1.15", letterSpacing: "-0.02em" },
],
// 36px at the 16px type rem — backup-step private key.
"nsec-key": [
"calc(var(--buzz-type-rem) * 2.25)",
{ lineHeight: "1.3" },
],
},
lineHeight: {
// Keep fixed Tailwind line-height utilities in the typography scale so
// Cmd +/- cannot enlarge glyphs inside an unchanged line box. Single-
// line surfaces keep their existing truncate/overflow behavior.
3: "calc(var(--buzz-type-rem) * 0.75)",
4: "var(--buzz-type-rem)",
5: "calc(var(--buzz-type-rem) * 1.25)",
6: "calc(var(--buzz-type-rem) * 1.5)",
7: "calc(var(--buzz-type-rem) * 1.75)",
8: "calc(var(--buzz-type-rem) * 2)",
"message-author": "var(--conversation-author-line-height)",
},
boxShadow: {
"content-edge": "-1px -1px 0 0 hsl(var(--sidebar-border) / 0.45)",
// Edge + elevation for a surface anchored to the right of the content
// area, whose only exposed edge faces left. Tailwind's stock shadows are
// all y-offset, so they cast almost nothing sideways — `shadow-xl` on a
// left-facing edge is nearly invisible. Both layers run -x so they wrap
// the surface's rounded left corners: the hairline draws the boundary
// (and carries dark mode, where a black shadow reads as nothing), the
// soft layer carries the lift. A left-only `border` can't do this job —
// it tapers out at each corner instead of turning it.
"panel-left":
"-1px 0 0 0 hsl(var(--border) / 0.8), -16px 0 32px -12px rgb(0 0 0 / 0.18)",
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
maxWidth: {
// Focus-mode agent prompt bubble. Mirrors berd's
// `--chat-user-message-max-width: 640px` (shared/styles/globals.css):
// a fixed measure, not a percentage of the column, so the prompt keeps
// one stable line length as the full-cover view is resized. Not a text
// size, so the px-text guard does not apply.
"prompt-bubble": "640px",
},
spacing: {
4.5: "1.125rem",
"conversation-body": "var(--conversation-body-gap)",
"conversation-list": "var(--conversation-list-item-gap)",
"conversation-paragraph": "var(--conversation-paragraph-gap)",
"conversation-row": "var(--conversation-row-padding-block)",
},
fontFamily: {
sans: [
'"Inter Variable"',
"Inter",
'"Avenir Next"',
'"Segoe UI"',
"sans-serif",
],
},
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
notification: {
DEFAULT: "hsl(var(--notification))",
foreground: "hsl(var(--notification-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
active: "hsl(var(--sidebar-active))",
"active-foreground": "hsl(var(--sidebar-active-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
status: {
added: "var(--status-added)",
deleted: "var(--status-deleted)",
modified: "var(--status-modified)",
},
warning: {
DEFAULT: "var(--ui-warning)",
bg: "var(--ui-warning-bg)",
},
},
},
},
plugins: [],
};