Files
buzz/desktop/tailwind.config.js
morgmart 97b1fee5c3 fix(desktop): restore true zoom by scaling the root rem (#6514)
## Summary

Follow-up to #5644. Cmd +/- had become a text-only zoom: type scaled
while rem-based padding, gaps, widths, avatars, and controls stayed
frozen, which produced cramped layouts (see [#buzz-frontend
thread](buzz://message?channel=a410ffde-c61f-416a-96e0-c296b5f5ecc9&id=1a758115cf07b00c097f6e988553908c045165325a57637519cfa7ed9c9accec)).

Root cause: #5644 introduced a virtual typography rem so the **Font
size** preference could change text without moving layout — a good
decoupling — but it also routed **Cmd +/- zoom** through that same
px-valued token and pinned the real root at 16px. One decision ("freeze
layout") was applied to two dials that shouldn't share it.

This PR gives each dial one owner and lets CSS compose them:

| Control | Changes | How |
|---|---|---|
| **Cmd +/- zoom** | Everything — true zoom | Scales the real `<html>`
font-size again (`useWebviewZoomShortcuts`) |
| **Font size preference** | Text only | Sets `data-font-size`;
`typography.css` maps it to a unitless `--buzz-type-scale`, mirroring
how density already works |

`--buzz-type-rem` becomes `calc(1rem * var(--buzz-type-scale))` —
rem-relative, so it rides on zoom automatically. Resulting text px = `16
× zoom × scale × token-ratio`. The 13 / 14 / 15px conversation contract
is unchanged at default zoom. Density and the type ramp from #5644 are
untouched.

The preference module no longer does px math or knows about zoom; the
zoom hook no longer imports the preference module. Net deletion in
production code.

## Validation

- `pnpm test` — 5,308 desktop unit tests
- `pnpm check:px-text`, `tsc --noEmit`, biome
- Playwright: `top-chrome-zoom-clearance.spec.ts` (native-chrome
clearance stays fixed under root zoom),
`inbox-refactor-screenshots.spec.ts` (zoomed row padding now asserts
`4.4px` instead of the frozen `4px`), and both `profile.spec.ts` zoom
tests (composed zoom × preference, cross-window storage reset)
- Before/after screenshots at 140% zoom in the comment below

---------

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-21 17:21:36 -07:00

142 lines
5.7 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)",
},
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: [],
};