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,