feat(desktop-new): prototype channel-backed Sessions client - #7366
Closed
morgmart wants to merge 32 commits into
Closed
feat(desktop-new): prototype channel-backed Sessions client#7366morgmart wants to merge 32 commits into
morgmart wants to merge 32 commits into
Conversation
…design system route Starts a second desktop client rather than reworking the existing one, so the design system can be built from scratch without unpicking 476 raw colour usages and 72 hover values first. The existing desktop/ client is untouched. What lands: - The colour system in three layers: private ramps hold values, public roles hold meanings, components only ever use roles. Tailwind's default palette is deleted with --color-*: initial, so a raw palette class is a build error. - A machine-readable token registry, so the design system documentation renders from the system itself and cannot drift from it. - /design — foundations, vocabulary, and the growth procedure, with a live light/dark toggle. Typography, spacing, radius, and motion are stubs that state what is still to decide rather than pretending to a system. - DESIGN.md for the judgement tokens cannot express, and AGENTS.md for the rules an agent needs before writing any UI. Accent is a slot rather than a colour: nothing above the ramp knows the hue, so it can change, become a preference, or vary per theme without touching a component. Every dark value is authored rather than observed, because the design exploration it derives from is light-only. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
The design system site rendered black borders and dividers while the tokens said `#d4d4d4`. The border roles were declared in layer 2 and never registered with Tailwind, so `border-primary` silently resolved to `--color-primary` — the *text* colour — and every hairline drew at text contrast. Tailwind's `--color-*` is a single namespace and text/borders share the emphasis names while holding different values, so borders now register as `--border-color-*`. Add typography as the second foundation, same three layers as colour: a nine-step size ramp plus leading, tracking, and weight ramps in layer 1; ten public roles in layer 2, each carrying its whole setting via Tailwind v4's `--text-<name>--<property>` convention, so `text-body` alone produces correctly set text and two supposedly identical labels cannot drift apart. Inter Variable and JetBrains Mono, both already shipped in every current Buzz client. Every size derives from one virtual rem, so keyboard zoom and the font-size preference work by construction. Soften the documentation surface to match where the product is going: regions separated by soft fills rather than outlines, hairlines only for genuine boundaries and never above `border-secondary`, no page-wide gradient behind reading columns, and no all-caps labels anywhere. The `/design/typography` page renders from the registry, so it cannot drift from the system. Add `scripts/check-type.mjs` enforcing the four rules a token cannot express — no arbitrary sizes (px or rem), no `uppercase`, no hand-applied tracking, no size role paired with a weight or leading utility. Also collapse the nav to a wrapped row below `lg`; the fixed 256px column overflowed the viewport under 420px. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
A swatch on a grey fill is being judged against a surface the product will never reproduce, and the dividers between swatches were redundant — each colour already separates itself. Same for type specimens: a box around a size sample changes the contrast you are evaluating it in. Roles and specimens now sit directly on the page, separated by space alone. The distinction is between two list shapes rather than a preference: `Rows` keeps dividers for uniform rows where the eye needs a line to track along (the size and tracking ramps, the audit table); `Specimens` has neither container nor divider for entries that carry their own visible difference. The swatch keeps its hairline. `bg-panel` is white on a white page, so without it the most-used role in the system renders as nothing — that is a genuine boundary, not decoration, and it is what makes the no-container treatment safe in both modes. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
* origin/main: chore(release): release Buzz Desktop version 0.5.21 (#7301) fix(scripts): copy global-agent-config.json in buzz-adopt-prod-agents (#7303) feat(relay): add early startup lifecycle logs (#7258) docs(nip-fi): add Blossom kind-24242 media possession-proof exception (#7278) fix(desktop): wrap message tables within the available pane (#7279) 🤖 fix(desktop): harden smoke E2E tests against Bestie overlay and toast timing (#7270) Show status and huddle indicators beside names (#7112) Add mobile voice notes (#7121) perf(desktop): publish mention sends before waking agents (#7154) Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…d-text roles The colour page explains the system but never lists the tokens themselves, so there is nowhere to answer "what does bg-inset actually paint". Add /design/color/table: every token in one table — the name you type, the base token it resolves through, and the value it paints. The values are read from the live CSS rather than typed into the registry. A hex column written by hand is a second source of truth for the same fact: edit a ramp and the documentation is silently wrong. This needs two different reads, because neither answers both questions — getComputedStyle returns the resolved value (#ffffff) and has already lost the indirection, while walking document.styleSheets finds the declared value (var(--neutral-1)), which is the only place the chain still exists. So CSSOM for what a token points at, computed style for what colour that is. It re-resolves on scheme change, which is visible in dark mode where bg-panel points at neutral 3 rather than neutral 1. The table immediately found a real defect. The registry has documented text-on-danger, text-on-success, text-on-warning, and text-on-info since the status groups were generated, but tokens.css never declared them — all four resolved to nothing, so text on a danger button fell back to inherited near-black on red. Declare them and register them with Tailwind. text-on-warning is neutral-12, not white: the warning fill is amber (#eab308) and white on it fails contrast. Also spell it "Color" in the nav, per the product's US English. The route is now /design/color, with the table nested under it. The file is color_.table.tsx — the trailing underscore keeps the URL while opting out of nesting, because otherwise /design/color becomes a layout route whose component must render an Outlet, and ColourPage does not, so the parent page rendered in place of the table. Component and type names still say Colour; renaming those is a separate change from what the interface says. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
The value column had been holding a single unbroken gradient literal, which under the default `auto` layout claimed most of the table's width and pushed the other two columns into a scroll. Switch to `table-fixed` with an explicit colgroup so the thirds hold regardless of content, and let cells wrap instead of scrolling — the horizontal scroll container is gone. The value uses `break-all` rather than `break-words`: a gradient is one token with no spaces to break at, so word-boundary wrapping still overflows. The swatch stays pinned to the first line while the value wraps beside it, so the kind label moves under the value rather than competing with it for the same line. Verified equal columns and zero overflowing cells at 380, 480, 700, 1024, and 1280px. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…gible The default accent was #3b82f6, and white label text on it measures WCAG 2 3.68:1 — below the 4.5:1 AA minimum for normal text. That applied to every primary button and active sidebar row in a fresh install, not to a setting someone had opted into. Move it to #2563eb, one step deeper: WCAG 5.17:1 and APCA Lc 80, so it now passes both standards. Radix, Bootstrap, and Tailwind's blue-600 all sit in this range, which is where a blue has to be to carry white text. Darkening the fill is the fix rather than switching the label to black. Black on the old hue scores WCAG 5.71 and would "pass", but measures APCA Lc 40 — perceptually unreadable, and visibly worse. This is the documented WCAG 2 flaw on saturated blue: the formula underweights the blue channel and ignores polarity. Apple ships white on #0088ff–#3daefc in Messages at WCAG 2.4–3.5 for the same reason. DESIGN.md now records the position (judge with APCA, report WCAG, constrain the fill rather than degrade the text) so it is a decision with evidence rather than a preference. Retiring a hex is not only cosmetic: it leaves the old value in people's storage, where it is no longer in ACCENT_COLORS and therefore fails the community-theme allowlist — parseCommunityThemePreference would return null and discard the user's syntax theme and follow-system flag along with the accent. Both storage paths now migrate the retired value instead: the community preference maps it on parse, and the local accent upgrades on read (all three read sites go through one helper). A regression test covers the migration and asserts an arbitrary hex is still rejected, so the migration cannot become a general escape hatch; removing the mapping fails that test. The communityThemeSync fixture moves to a current accent — it covers coordinate logic, and a retired hex there would be rewritten mid-assertion for reasons unrelated to what it tests. Verified: 6112 desktop tests pass, tsc clean, biome clean. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Three fixes, and a guard so they cannot regress silently. `text-on-warning` pointed at `var(--neutral-12)`, which is near-black in light mode and near-white in dark. The warning fill is the same amber in both, so dark mode put #f5f5f5 on #eab308: APCA Lc 34, WCAG 1.76:1. It is a literal now — a paired-text token follows its fill, never the mode. The dark neutral text steps were too dim. `text-secondary` (#8f8f8f) measured Lc 38–41 and `text-tertiary` (#6e6e6e) Lc 23–26 against an Lc 60 body target, while passing WCAG AA at 4.4–5.6:1 — legal, and visibly murky. Raise dark neutral-9 to #a6a6a6 and neutral-10 to #c1c1c1, sized against bg-float (#2a2a2a), the lightest dark surface and so the binding case. Light neutral-9 moves #a3a3a3 -> #909090 for the same reason at the meta target. Both ramps stay monotonic, and bg-inverse-hover (the other consumer of neutral-10) still carries its paired text at Lc 70. This is APCA's polarity asymmetry doing real work: light-on-dark needs more separation than the same WCAG ratio implies, so the dark ramp's text steps sit above where mirroring the light ramp would put them. `text-disabled` stays below target deliberately — low contrast is the signal that a control is unavailable, and WCAG exempts inactive controls for the same reason. Raising it would make disabled read as enabled. Recorded as the one exception, with that reason. The guard is the durable part. `scripts/check-contrast.mjs` measures every text role against every surface it can sit on, in both modes, and fails the build below target. It parses `tokens.css` and resolves each `var()` chain rather than holding a copied list of values, so it cannot drift from the system it audits. `scripts/apca.mjs` is the algorithm, asserted against the published reference values on every run so a bad edit to the maths fails loudly rather than quietly shifting every verdict. Verified by falsification: reverting either fix fails the check, and perturbing one APCA exponent trips the reference assertion. Also corrects an earlier misreading: the two `.dark` blocks are not duplicate declarations. The first holds the dark ramp, the second overrides surface roles so the panel lifts off pure black. That is the intended layering. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…en the audit A second pass over the colour system, checking the surfaces the first audit did not cover. The gap was tint hovers. DESIGN.md says a tint carries meaning and takes coloured text, but the audit only measured text against the four neutral surfaces — so `--bg-*-tint-hover`, the hardest surface in each coloured family, was never checked. Four pairings were below target: light danger (Lc 56.6) and warning (59.2), and dark accent and info (58.4 each). All four passed WCAG AA, which is why nothing else caught them. Size step 11 against the tint hover rather than the neutral panel, the same rule already applied to the neutral ramp: light danger-11 #b91c1c -> #a80008, light warning-11 #a16207 -> #9c5e00, dark accent-11 and info-11 #8fbaff -> #95c0ff. Minimum moves in OKLCH holding hue and chroma, so the hues do not shift. The audit now covers every tint and tint hover in all five families. Verified by falsification: reverting any one of the four fails the check. Two findings that are deliberately not changes: Hairline dividers stay at 1.2-1.8:1. WCAG's 3:1 non-text rule covers boundaries needed to identify a control or its state, not grouping lines; Radix ships 1.29 and Apple 1.26 for the same job. Raising them would draw the box the fill already implies. Recorded in DESIGN.md, along with the note that a control's own outline is a different question when the input layer lands. `--ring-focus` is an accent against accent: legible on every neutral surface at 3.0-3.5:1, but 1.1:1 on `bg-accent`, where it vanishes. Not yet reachable — the token is unregistered and unused — so the constraint is documented at the token instead of being solved speculatively: the component layer that introduces focus owns a surface-coloured offset, and there is no second ring token. Glass surfaces were checked by compositing each translucent value over the app gradient's own stops; all clear the target in both modes, which the relit neutral ramp is what earns. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…essions * origin/main: Add generic information-flow control core (#7293) feat(buzz-acp): update base prompt; add buzz context and skills to Pi agents (#7335) fix(desktop): restore mention chip identity icons (#7338) Persist video playback speed preference (#7336) Verify ACP relay events before prompt routing (#7010) fix(buzz-acp): bound busy-owner hold to prevent cross-channel starvation (#7337) feat(desktop): invite owned agents from standalone forums (#7125) fix(desktop): authorize remote mentions at publication (#7124) fix(acp): rename system tag to agent-instructions (#7332) fix(desktop): bind duplicate mention selections to exact recipients (#7133) refactor(relay): extract NIP-29 membership authorization (#7285) chore(release): release Buzz Desktop version 0.5.22 (#7308) feat(desktop): preserve mentions across copy and paste (#7228) test(desktop): await Bestie drag and profile hover endpoints (#7294) Collapse contiguous join messages (#7262) Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
🔐 Codex Security Review
|
Answers the blocking question in the Desktop New plan before the composer is designed: does staged character input (ja/ko/zh) survive beside an inline atom tile? It does. Twelve tests, Chromium composition driven through CDP Input.imeSetComposition plus a WebKit pass. Measured findings: - Composition beside a tile is safe in every position; a committed composition leaves the tile address byte-identical. No fence needed. - selectable:false is load-bearing. With selectable:true an arrow key selects the tile and the next keystroke replaces it. - atom:true is redundant for a contentless node (isAtom = isLeaf || spec.atom) and kept as documentation of intent. - Whole-tile deletion works natively; no custom Backspace handler required. - One real gap: the caret position before a leading tile. ArrowLeft reaches it, Home and a pointer click do not, in both engines. Those two tests carry test.fail() until the caret-boundary layer lands. The harness is temporary and deleted when the real composer lands. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Implements the foundation the composer sits on: a reference is an address (kind + identity), never the letters of a name. One shared InlineTile serves both the composer and the conversation, mounted in the editor through a React node view. - shared/tiles/address.ts: kind + id, canonical buzz:// form, parse/format. - shared/tiles/faceResolver.ts: resolves the displayed face from an address at render time, so a rename never edits anyone's draft or enters undo history. Community-scoped cache with an explicit reset. - shared/ui/InlineTile.tsx: the one component, person/agent/channel kinds. - features/composer/tileNode.ts: the document node. Stores only the address. - New semantic tokens: --space-tile-inset, --space-tile-gap, --radius-tile. - Specimen page at /design/components/inline-tile covering kinds, in-sentence use, read-only, unresolved, truncation, and activation. Findings while building, all now covered by tests: - A React node view REPAIRS a caret boundary. With static markup, Home before a leading tile landed after it; with the node view both Home and ArrowLeft land correctly. Only the pointer click on a leading tile's left edge still fails — now the single known caret gap, still marked test.fail(). - useSyncExternalStore needs a reference-stable snapshot. Building a fresh unresolved face per call was an infinite render loop that unmounted the whole editor. Unresolved faces are cached like resolved ones. - An unresolved tile must not announce an abbreviated identity to a screen reader. TileFace now carries , and the accessible name says "Unresolved person" rather than reading the stand-in aloud. Falsifiability checked by breaking each protection in turn: selectable:false, the address text projection, and the community reset each turn tests red. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Accent hue is now Tailwind purple. The ramp is a slot, so this is five values in tokens.css and every role follows; steps 2 and 3 are purple-100/200 exactly because the chip design was drawn against them and the tint pair is what a person sees most. Dark mode cannot use those two — near-white reads as a light surface on a dark panel — so it holds the hue at dark violets instead. Renamed InlineTile to InlineChip throughout, plus shared/tiles to shared/chips, tileNode to chipNode, and every token, class, and identifier. A chip is what this is. Redesigned to match Figma 700-2782: - One treatment for every kind: accent tint behind primary text. Kinds are told apart by sigil and, where the label is ambiguous, an icon — not by colour. This replaces the earlier per-kind colour split, which spent palette on a distinction the sigil already makes. - Label is primary text, not accent text; the tint does the colour work. - Two new kinds the design draws: message and link, both icon-led with no sigil. AUTHORABLE_CHIP_KINDS records that v1's picker still offers only person, agent, and channel; the others arrive by another route. - Dropped avatar and status dot. The design has neither, and neither was earning its place at text size. - Unresolved is now a real state: neutral fill, lesser text, and never a control, because there is nothing to open. - Chip inset and gap intentionally sit below the 4px spacing floor (3px inset, 4px gap) — a chip hugs its label. Recorded on the tokens. Eight new rendered-contract tests bound to the real specimen page: sigils and selective icons, assistive naming, unresolved-is-never-a-control, read-only claims no stop, activation reports the address, truncation, never breaking across a line, and hover deepening the tint without moving the text. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
The colour system had three layers, and the middle one held two different kinds of thing under one name. --neutral-1..12 is a real scale; --accent-2 and --danger-9 are five specific jobs (tint, tint-hover, border, fill, text) wearing ramp-style numbering. So --accent-2 read as 'a light purple' when it meant 'the surface an accent tint sits on'. Underneath it was 114 hand-picked hex values with nothing enforcing that two tokens doing the same job agreed. They drifted: --accent-2 and --tint-purple were the same purple in light mode and two different purples in dark. I introduced that yesterday and did not notice. Four layers now: 0 palette --palette-purple-3 every hue, 12 steps, authored per mode 1 families --accent-tint the five jobs a hue does, named not numbered 2 roles --bg-accent-tint what a thing is in the interface 3 components bg-accent-tint Tailwind utilities Dark stops being freehand. A family points at a palette step and .dark redefines the whole palette, so 50 hand-picked dark values are simply gone. This also answers a concrete trap. Tailwind's purple gets MORE saturated as it descends (950 is 87% saturation), so a dark tint drawn from its bottom end reads as oversaturated, and the only way to reach a muted deep purple was to write purple-950/50 in a component. That value is correct — it composites to within a few percent of the palette step now in use — but as an expression it has no name, no light/dark pair, and nothing the contrast guard can measure. The palette's dark ramps are authored for dark surfaces, so it is a step you pick. New scripts/check-color.mjs enforces it: rejects opacity modifiers on colour utilities, color-mix() and alpha rgb()/hsl() in component code, components reaching past the role layer, and the layering itself — palette steps must be literals, family steps must reference the palette, two tokens doing one job must resolve to one step, and dark must not restate families. Falsified by reintroducing the original drift and each other failure mode in turn. Values are Radix Colors (MIT), transcribed rather than depended on; Radix is not on Block's Tech Radar so this is a values-only copy with no package. Its twelve-step contract is the one this system already described in comments, step for step. Two documented divergences, both found by the contrast guard: family text takes step 12 rather than the 11 Radix names 'low-contrast text' (Radix sizes 11 for WCAG 4.5:1; every hue's 11 measured Lc 55-61 against our Lc 60 target), and dark neutral surfaces stay hand-authored because they were sized against the real panel stack. The colour page now renders the palette, so picking a step is visual. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…oard Two changes, both about finding out whether the colour system actually works. Deleted the six categorical tint roles. Evidence: zero component usage — not one reference outside the token file and the registry. They were hand-picked literals of exactly the kind the palette exists to prevent, and the open question 'how many categorical colours does the product need, and does a person choose one or does the product assign it' is unanswered, so naming them now was guessing. The eight palette hues stay; a categorical role gets named when a feature needs to distinguish things. That deletion turned a guard check vacuous, which is its own finding: the drift check named four specific pairs (--tint-purple vs --accent-tint, etc.) and would have passed silently forever once those tokens were gone. Replaced with the general rule — no two family steps may resolve to the same palette step — which catches the same class for any future family without naming pairs. Falsified by pointing --danger-tint at the purple ramp. Added /proving-ground: a fictional fleet-monitoring dashboard, deliberately nothing to do with Buzz. Specimen pages are honest but sympathetic — one component at a time, on a chosen backdrop, at a chosen size. A dashboard puts forty decisions next to each other, which is where a missing role shows up. It is also the surface a generated theme has to be judged on: 'Neon Tokyo' can look fine as twelve swatches and be unusable as a page. Built only from role tokens, and it held: no new roles were needed, contrast passes both modes, and the layout works at 1440 and 620. Page-local parts live in the feature rather than shared/ui on purpose — this page exists to find out what repeats, so promoting its pieces first would be guessing. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
A card sits on the panel and is grouped by a hairline. Fill is reserved for bg-hover, and only where the card is clickable, so a filled card always means you are pointing at this rather than merely this is a box. The region tiles were using bg-inset, which means pushed in - an input or a code block - the opposite gesture from grouping, and read as a grid of empty text fields inside a filled panel. No new role is needed: a card with no default fill has nothing to name. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
A local Vite config can wrap the committed one to append a personal dev-only plugin. Ignoring it makes that structurally uncommittable rather than something to remember not to stage. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
It was meant to stress the design system by putting many decisions next to each other. It did surface one real finding - the neutral hover step is tuned for hovering a filled component, not a bare row, so the same token is too weak for a quiet button and too strong for a bare row - but the page was ~85% bespoke CSS routed around the shared components, so most of what it showed was its own scaffolding rather than the system. The app itself is the better proving ground. Removing this rather than carrying a mockup whose defects have to be triaged separately from the product's. Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Registers all eight palette ramps as Tailwind classes, so a screen writes `bg-purple-9` or `text-red-12` directly. This reverses the rule the colour guard used to enforce, and the reversal turns on one fact: every step in this palette is authored per mode, so `neutral-4` is one value in light and another in dark and a component naming it behaves correctly in both. That is what makes a raw step safe here and unsafe in stock Tailwind, where `neutral-200` is a single literal. Once a step is mode-aware, a role whose light and dark values are the same step is a name in front of a number, and a name in front of a number hides the decision instead of recording it. `bg-accent` was `purple-9`; `text-error` was `red-12`. Nineteen roles were exactly that. The fifteen that remain each earn the name: the four structural surfaces take a different step per mode, so no class can express them; the emphasis roles enforce a rule a ramp cannot state, which is that there are three levels of text and one border weight; the glass materials are a bundled treatment; and `--text-on-accent` follows its fill rather than the mode. Also fixes a namespace collision that had shipped a real defect. `--color-x` is not one utility, it is all of them — one declaration defines `bg-x`, `text-x`, `border-x` and `ring-x` from a single value — so `text-danger` resolved to the saturated fill (red-9) instead of the text step. Three error messages rendered at APCA Lc 34 on a dark panel against a target of 60, and `check-contrast` passed the whole time because it measured `--text-danger`, a token no class could reach. Text roles now register as `--text-color-*`, matching the `--border-color-*` split that fixed the same bug for borders. Guards updated to enforce the new rule rather than the old one: `check-color` now rejects a role whose two modes agree and names the class to write instead, and `check-contrast` measures the ramp steps screens actually use, so deleting a role cannot leave it auditing a name nobody types. `registry.test.ts` binds the documented token list to the stylesheet, and `pnpm census` reports every role's readers without failing a build. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
A glass surface is four things at once — a translucent fill, a blur behind it, a directional rim, and sometimes an outer lift — and they were four separate things a consumer assembled by hand. Every consumer did, differently. The four chrome surfaces meant to match disagreed about whether to carry an outer shadow, and four inner panels set a translucent fill with no blur at all. `glass-primary` and `glass-secondary` are Tailwind `@utility` blocks carrying the whole material, plus `-interactive` variants that add the hover. Named by stacking depth rather than by component: an earlier cut named them chrome / panel / float and it was wrong twice over, because `chrome` and `float` both landed on glass-3 — 66% opaque in dark, which is why chrome did not read as glass — and naming a material after a component invites a second material the first time a component wants a different depth. `box-shadow` is composed inside each material because it has to be: a rim and a lift both set it, so as separate utilities the second silently overwrites the first. That is a structural argument for bundling, not a preference. The fills are deliberately not registered as colour utilities, so a bare `bg-glass-primary` class cannot exist. Removing a redundant fill from the four workspace panels fixed something no token held. They each set the same `glass-2` as the container they exactly cover — measured 610x551 inside 612x553, the two pixels being its border — and two identical translucent layers composite: 0.52 over 0.52 is 0.77, between glass-4 and glass-5. Panels meant to be the most translucent surface in the system read as nearly solid. That in turn exposed a bug the density had been masking since the dock landed: dockview's theme sets `--dv-group-view-background-color` to a literal `white` and `DockWorkspace` passes `themeLight` in both modes, so an opaque white sheet sat behind the glass. Reset here, because every other dockview surface the variable feeds was already overridden to transparent. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
The old name described one variant's appearance — a segmented control — which is exactly why a second appearance felt like it needed a second component. `Tabs` names the behaviour, so both variants sit under it naturally. Base UI's own export is also `Tabs`, so it is imported as `BaseTabs`; that reads better anyway, since `BaseTabs.Root` says plainly which layer each part comes from. `variant="chrome" | "panel"` is the chrome/surface axis DESIGN.md had deferred until a second component made the case. `IconButton` made it once; this made it twice, on a real screen. The failure: the chrome container is `glass-2`, which over a white panel composites to pure white, and its selected pill is `neutral-1` — also pure white. Container and selection became one colour with only a shadow between them, and no guard could see it because the component had no way to state which background it expected. Retinting `--bg-chrome-selected` would only have moved the collision. One component with a variant rather than two components, because behaviour, keyboard model, accessibility, props, and the Base UI parts underneath are identical — a sibling would duplicate all of it to change how selection is drawn, and the two would drift exactly as the four hand-assembled chrome surfaces did. The panel variant still emits `role="tablist"`, correct `aria-selected`, roving `tabIndex`, and arrow-key navigation, which is the concrete payoff. Each variant states all three of its states in full rather than inheriting a colour and overriding part of it. When a shared rule and a variant rule both set a property, the more specific selector wins regardless of source order, and a variant rule is always more specific — so the shared `[data-selected]` rule lost and the selected tab silently rendered as unselected. Hover carries `:not([data-selected])` because hover and selected genuinely overlap: a cursor can sit on the selected tab, and DESIGN.md § State says a selected item in a toggle group is not interactive. Those two selectors score identical specificity, so without the exclusion only source order would keep it right. This follows Chrome's modern CSS guidance: use `:not()` to say a state is irrelevant, never to defend one rule from another. `WorkspaceSurface` is renamed to `Panel` in the same change, since both touch the same rules in components.css. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
`ComponentAnatomy` is a table per component variant: each part, and the token that draws its fill, text, border, shadow, blur, and radius. It answers the one question a specimen cannot, because a picture of a control does not say whether that pill is `bg-chrome-selected` or a hardcoded white. It reads the live component rather than a hand-written list, for the same reason the token table does — a typed-out list of parts and tokens is a second source of truth that goes stale silently. Two things this needed that were not obvious: Vite injects app CSS by setting `textContent` on a `<style>` tag at dev time and those rules are not reachable through `sheet.cssRules`, so the stylesheet text is parsed instead; and matching declarations are ranked by specificity before source order, because ranking by order alone reported a selected tab as unselected once a variant rule outranked a shared one. Both failures rendered a plausible table that was wrong, which is the failure mode it exists to prevent. Every specimen frame now carries a border. It was `bg-panel` on a page that is also `bg-panel` — white on white in light mode, `#1c1c1c` on `#1c1c1c` in dark, with no border and no shadow — so every plain specimen on every component page had an invisible container and a reader could not tell where the documentation stopped and the running component began. It hid because the gradient frames always read as containers, so the pages using them looked correct. A hairline rather than a tinted fill, because a specimen must show the component against the surface it is designed for. Measured at `neutral-4` first: 1.23:1 against the light page and 1.08:1 against the dark one — technically drawn, effectively invisible. `neutral-6` is 1.41 and 1.35. The overview thumbnails had the same defect and take the same fix. The colour table splits into Roles and Ramps, because the ramps — 96 steps across eight hues, the layer every role resolves to — were not shown anywhere. Blur moves off the glass page onto elevation, where the other depth values live. The glass page drops its text-only cards and shows each material over the real app backdrop, since translucency can only be judged against what shows through it. Frames scroll their own overflow: chrome tabs with icons and a trailing action are 377px of unshrinkable inline-flex, so at 420px the whole page used to scroll sideways. A specimen you swipe beats a documentation page that slides. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
AGENTS.md held two stale copies of the colour system — one section said two layers, another said three and listed an `--accent-1…12` ramp that only exists in a comment explaining its removal. Documenting one subject twice in one file is how that happened, so the duplicates are deleted rather than reconciled: DESIGN.md holds what to do and why, AGENTS.md holds what this is, how to run it, and what will fail you. DESIGN.md gains the colour structure it was missing, rewritten around the reversal in this branch — build from the ramps, and a role exists only when a step cannot express it. The three cases where a name is earned are stated explicitly, since `check-color` now enforces two of them. The step-to-role table survives as guidance for which step to reach for; it is no longer a generator, which is what it was when it produced twenty status roles nobody had designed. Both files also record what is deliberately unresolved: contrast on glass, where the guard measures opaque surfaces only and a translucent surface's real background is the fill composited over a gradient that changes across the screen. Measured, dark mode's brightest glass region puts `text-secondary` at Lc 58 and `text-tertiary` at Lc 43 against targets of 60 and 45. Three candidate fixes are listed and none is chosen, because it wants real content on screen rather than a token edit. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
`text-danger` was the role that resolved to the wrong red; error text is `text-red-12`, measured against every surface it lands on. `border-secondary` was one of three indistinguishable border weights and is now the single `border-primary`. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
…t came from Both label sets came with the transcribed values and described a generic scale's intentions: step 4 as "component hover", step 6 as "subtle border", step 7 as "border". The product then decided otherwise and nobody updated the comments, so the file said step 4 was a hover step while it held `border-primary`, the one border weight, and said steps 6 and 7 were for borders while they had no readers at all. A designer opening `tokens.css` or `/design/color` was told the wrong thing by the only documentation to hand. The neutral ramp's comments now name the roles that actually point at each step, including the two steps that have no reader — an empty step is worth seeing, because it is either a gap or a deletion nobody made yet. `STEP_JOBS` keeps generic wording, because it is shared by all eight hues and a hue's step 3 is a tinted surface whatever the product does with it. But it now matches DESIGN.md's step-to-role table exactly, which it did not, and it says so about itself: general intention here, real use in the neutral ramp's comments. The reason it drifted is that it was never rendered. Every palette step carried this text and no page displayed it, so it could only be checked by reading the source. The colour page now shows each step's job under its swatch, which makes the next divergence visible by looking rather than by audit. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Light mode keeps neutral-4; dark mode moves to neutral-6. A hairline needs more separation on a dark surface than the same step number provides, and the old single value was invisible where it mattered: measured against every surface a border sits on, neutral-4 is 1.08:1 on a dark panel and 1.09:1 on the floating composer. Drawn, and not perceptible. neutral-6 is 1.35 and 1.14. Not neutral-5, which is one step and the obvious answer. `bg-float` and `bg-chrome-selected` are both neutral-5 in dark, so a border on a floating surface would have measured 1.00:1 — the same colour as the thing it bounds. This is the asymmetry the role exists for. Light and dark do not have to reach for the same step, and no `border-neutral-4` class could express it; it is the same test the four structural surfaces pass. The /design specimen frames drop their hardcoded `neutral-6` and read the role again. They only named a step directly because the role was invisible in dark, so documentation furniture and product boundaries had diverged for a reason that no longer holds. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
A shared role that fails in a real context is a defect in the shared decision, not a reason to name a nearby ramp step locally. The specimen frames exposed this with border-primary: neutral-4 was visible enough in light mode and 1.08:1 against a dark panel. The first response bypassed it with neutral-6 just for the documentation. The correct response was to measure every reader, repair the role to neutral-4 light / neutral-6 dark, and return every caller to the same role. DESIGN.md now gives the complete decision order: use a component first; add a variant if the background changes; choose a surface by its spatial job; use the shared boundary role; choose ramp steps directly for a hue decision; add a proposed role only when one step cannot express both modes; and stop and ask rather than making a local exception. It also records why the border uses two different steps and why neutral-5 could not be the one-step dark adjustment. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
The vault's Colour Token System duplicated the token specification, documented a families layer, three border weights, component-named glass, and nine obsolete neutral values. It is deleted; all remaining Buzz Redesign vault links now point to DESIGN.md as the sole visual-system authority. DESIGN.md gains a practical path from context to token. Use a component first; add a variant when the context changes; choose a surface by spatial job; use the shared boundary role; choose public ramp steps for one-step hue decisions; make a proposed role only when one step cannot express both modes; and stop rather than creating a local exception. The border incident is retained as the worked rule: when a shared role fails, repair the role and return every caller to it. Also corrects two stale statements inside DESIGN.md itself: ramps and roles are both public, not roles alone, and growing the system now starts with "write the mode-aware ramp step directly" rather than the obsolete instruction to search the role list first. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
PanelHeader is a header row, not a panel. Its specimen now renders only its own optional icon, title, and actions — no invented Panel around it — and its action is the existing IconButton supplied through the `actions` slot rather than a raw button recreated in the specimen. Panel owns only a surface: fill, border, shadow, rounded silhouette, and clipping. It owns neither a header nor content, so its standalone specimen is now an empty Panel against the gradient it is designed to sit on. The fake header and copy made the live component appear to own a complete work surface, which it does not. The two-panel pseudo-workspace is deleted. Resizing and split geometry belong to DockWorkspace, which owns live layout, resize seams, and persistence policy; a Panel can sit in ordinary layout, a fixed composition, or a workspace and must not pretend it owns any of them. Real resizing gets documented on DockWorkspace's page when that component has a specimen with a running dock. Signed-off-by: Morgan Martin <morganmartin@squareup.com> Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces
desktop-new, a from-scratch Buzz desktop client experiment focused on channel-backed Sessions and a design-system-first interface.desktop-nexttodesktop-new/design/componentsStatus
Draft. The backend/runtime vertical slice works, but the current product shell is not the intended final visual direction. The next step is to rebuild the populated Messages surface from the shared components, using Cynthia’s
cc-experimentprototype as the executable visual reference and Figma node185:9217as the parity target.Composer work is intentionally deferred until its inline-token/chip editing contract is designed.
Validation
Known follow-ups