mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
A design-delta note measured the conversation variant against berd's shipping
chat surface and named three divergences that made the focus view read as "not
berd" regardless of how the rest was arranged. This takes all three literally,
on Buzz tokens rather than berd's.
**Agent identity row.** The largest gap: 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`. Added for
the `conversation` variant only — a test asserts the row does not leak into the
other variants, 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, `px-4 py-2`,
12px radius. berd's `rounded-sm` is 12px on its own scale, not Tailwind's stock
2px, so Buzz's `rounded-xl` is the exact equivalent. The cap moves from
`max-w-[85%]` to a fixed 640px measure mirroring `--chat-user-message-max-width`:
a percentage cap re-wraps the prompt on every resize of the cover, a fixed
measure holds one stable line length.
**Fenced code.** The language moves into a real header row above the frame with
the copy action opposite it, framed at Buzz's `rounded-lg` (`--radius: 0.625rem`,
exactly berd's value).
The markdown renderer is shared with channel messages, so the code recipe is
opt-in through `CodeBlockVariantContext` read at render time. 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 `default`/`compactPreview` markup stays byte-identical.
Three defects fixed on top of the restyle, each found by review or a bug pass:
- The code-recipe provider was mounted inside `MessageActivity`, which only
handles assistant items — `UserMessageBubble` returns before it, so a fence
inside a human prompt kept the legacy 16px muted frame nested inside the new
12px bubble. The recipe is a property of the *surface*, not of a role, so the
provider moves up to `AgentSessionTranscriptList`.
- The identity row read the raw `agentAvatarUrl`/`agentName` props, and the
primary channel-opened flow passes `agentAvatarUrl: null` because
`ChannelAgentSessionAgent` carries no avatar field — so every channel-opened
focus session showed initials in the identity row while the header directly
above it showed the real profile avatar. Now resolved profile-first out of the
`profiles` lookup the panel already hands down, with the props kept as
fallback: a locally managed agent can hold an avatar its relay profile never
published.
- `UserAvatar` names itself (`<img alt="${displayName} avatar">` or its
fallback initials) and the identity row puts the same name in visible text
right after it, so assistive tech read the identity twice for every agent
turn. The avatar is wrapped `aria-hidden`, verified in Chromium's real
accessibility tree over CDP (`ignoredReasons: ["ariaHiddenSubtree"]`), not
from DOM attributes. Marked at this call site rather than teaching the shared
`UserAvatar` a decorative mode, which would affect all 45 of its call sites.
The conversation test file had reached 1253 lines, past the 1000-line ceiling.
It slipped the `file-size-check` gate because that script only scans
`.ts`/`.tsx` under `src/features` — the rule in AGENTS.md plainly covers it.
Split into a shared non-test harness module (following the existing
`observedUnreadTestHarness.mjs` precedent, because duplicating the
order-sensitive TZ/locale pins into two files is exactly how they drift) plus
two test files, bodies moved verbatim.
AGENTS.md gains the `assert.ok(x === null)` rule earned here: the
`assert.equal(el, null)` form serializes the matched element and its entire
subtree to build a diff when it fails, which on a real transcript exhausts
memory and kills the runner with SIGKILL after ~100s — a genuine regression
becomes unreadable and looks like an OOM in unrelated code.
Squashed from ss-dev-01/berd-restyle (#6720) at fd2e01799.
Co-authored-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>
Signed-off-by: ss-dev-00 <a02c4e0850e5e612b4ddf95dbe2f5c56467cf27c6552203bc833ff438fb31971@buzz.block.builderlab.xyz>
150 lines
6.1 KiB
JavaScript
150 lines
6.1 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))",
|
|
},
|
|
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: [],
|
|
};
|