mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
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>
154 lines
6.2 KiB
JavaScript
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: [],
|
|
};
|