From 61e3792118f811fac9e50f0c4d7ff0f3d35fc9cd Mon Sep 17 00:00:00 2001 From: morgmart <98432065+morgmart@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:07:51 -0700 Subject: [PATCH] refactor(desktop-next): take colour and type specimens out of cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- desktop-next/DESIGN.md | 2 + .../features/design-system/ui/ColourPage.tsx | 24 +++++-- .../design-system/ui/TypographyPage.tsx | 68 +++++++++---------- .../features/design-system/ui/primitives.tsx | 27 ++++++-- 4 files changed, 73 insertions(+), 48 deletions(-) diff --git a/desktop-next/DESIGN.md b/desktop-next/DESIGN.md index 2e964b4ca0..27bc0632dc 100644 --- a/desktop-next/DESIGN.md +++ b/desktop-next/DESIGN.md @@ -58,6 +58,8 @@ Buzz is a place where people build together and bring their agents into the room ## Density and rhythm - **Dense data renders as rows with dividers, edge to edge.** Wrapping every list item in its own card is the most common way a functional surface becomes a marketing page. +- **Content that separates itself needs no divider, and no container.** A divider is for uniform rows where the eye needs a line to track along. When each entry already carries a visible difference — a colour swatch, a type specimen, an avatar — the content is the separator, and adding a rule or a card on top is redundant structure. Space alone is enough. +- **Never judge a value against a surface it will not be used on.** A swatch on a grey fill, or a type specimen in a tinted box, is being evaluated in a context the product will never reproduce. Samples sit on the page. The one exception is a value that needs a backdrop to exist at all — translucency needs something behind it, and a white surface swatch needs a hairline or it renders as nothing. - **Cards are for widgets, galleries, and settings groups.** A card is a bordered, padded region on the page, not a different depth. - **Pick the frame before the content.** Decide what the surface is — a list, a reading column, a workspace — before filling it. - **Whitespace is generous by default.** Crowding reads as a different product. diff --git a/desktop-next/src/features/design-system/ui/ColourPage.tsx b/desktop-next/src/features/design-system/ui/ColourPage.tsx index 05ab4037de..a410145a7d 100644 --- a/desktop-next/src/features/design-system/ui/ColourPage.tsx +++ b/desktop-next/src/features/design-system/ui/ColourPage.tsx @@ -7,9 +7,18 @@ import { import { Note, PageHeader, Section, Swatch } from "./primitives"; +/** + * A role, sitting directly on the page. + * + * No card and no divider: the swatch is its own separator, and a colour judged + * on a grey card is not being judged on the surface it will actually be used on. + * The hairline stays on the swatch itself — `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 rather than decoration. + */ function RoleRow({ role }: { role: Role }) { return ( -
+
-
+
{group.roles.map((role) => ( ))} @@ -106,11 +115,14 @@ export function ColourPage() { title="Deliberate exceptions" description="Literal values exist only in the ramps, and nothing above a ramp holds one — except these. The list is short and complete on purpose: a vague exception policy is how a layered system quietly erodes." > -
+ {/* No swatch here to do the separating, so these entries keep a little + structure — the token name leads and the spacing groups it with its + reason. Still no card: this is prose, not data. */} +
{EXCEPTIONS.map((exception) => ( -
- {exception.name} -

+

+ {exception.name} +

{exception.why}

diff --git a/desktop-next/src/features/design-system/ui/TypographyPage.tsx b/desktop-next/src/features/design-system/ui/TypographyPage.tsx index 38282df09d..e140ead63e 100644 --- a/desktop-next/src/features/design-system/ui/TypographyPage.tsx +++ b/desktop-next/src/features/design-system/ui/TypographyPage.tsx @@ -5,7 +5,7 @@ import { type TypeRole, } from "@/shared/tokens/registry"; -import { Note, PageHeader, Row, Rows, Section } from "./primitives"; +import { Note, PageHeader, Row, Rows, Section, Specimens } from "./primitives"; /** * Every specimen below is set in the role it documents, so the page is the @@ -14,23 +14,21 @@ import { Note, PageHeader, Row, Rows, Section } from "./primitives"; */ function RoleSpecimen({ role }: { role: TypeRole }) { return ( - -
-

- {role.mono ? "createChannel(name, members)" : "Bring your agents in"} -

-
- {role.token} - {role.pointsAt} - - {role.size} / {role.lineHeight} / {role.tracking} / {role.weight} - -
-

{role.use}

+
+

+ {role.mono ? "createChannel(name, members)" : "Bring your agents in"} +

+
+ {role.token} + {role.pointsAt} + + {role.size} / {role.lineHeight} / {role.tracking} / {role.weight} +
- +

{role.use}

+
); } @@ -46,36 +44,34 @@ export function TypographyPage() { title="The faces" description="Both already ship in every current Buzz client, so this is a decision to keep rather than to make. Inter is drawn for interface text at small sizes, which is most of this product." > - + {TYPE_FAMILIES.map((family) => ( - -
-

- {family.name} -

- {family.token} -

- {family.use} -

-
-
+
+

+ {family.name} +

+ {family.token} +

+ {family.use} +

+
))} -
+
- + {TYPE_ROLES.map((role) => ( ))} - +
{TYPE_RAMPS.map((ramp) => ( diff --git a/desktop-next/src/features/design-system/ui/primitives.tsx b/desktop-next/src/features/design-system/ui/primitives.tsx index f4d479a640..0b93d2d666 100644 --- a/desktop-next/src/features/design-system/ui/primitives.tsx +++ b/desktop-next/src/features/design-system/ui/primitives.tsx @@ -88,24 +88,39 @@ export function Stub({ what, decide }: { what: string; decide: string[] }) { } /** - * A group of rows that reads as one region. + * A list of uniform rows — the tabular case, where every row looks alike and the + * eye needs a line to track along. Dividers, no container: the section heading + * already says these belong together, so a fill behind them adds a box without + * adding meaning. See DESIGN.md § Density and rhythm. * - * The fill does the separating, so the container needs no outline. Rows inside - * are divided by the lightest hairline in the system, because dense data wants - * an edge-to-edge divider rather than a card each — see DESIGN.md § Density. + * If the rows carry their own visual difference — a swatch, a type specimen — use + * `Specimens` instead. Content that separates itself needs no divider. */ export function Rows({ children }: { children: ReactNode }) { - return
{children}
; + return
{children}
; } export function Row({ children }: { children: ReactNode }) { return ( -
+
{children}
); } +/** + * A list whose entries are visibly different from each other — colour swatches, + * type specimens, elevation samples. + * + * No container and no dividers. The specimen is its own separator, and two + * things a colour system must never do are judge a swatch against a fill it + * will never sit on, or set a type specimen in a box that changes its contrast. + * Separation comes from space alone. + */ +export function Specimens({ children }: { children: ReactNode }) { + return
{children}
; +} + /** Renders a live swatch of whatever a CSS custom property currently holds. */ export function Swatch({ variable,