mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
feat(design-system): add BlockUI logic and grammar reference
Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -28,6 +28,15 @@ Direct links and refreshes work with the same SPA fallback as the rest of the
|
||||
site; unknown component IDs have a recovery link. Theme and density preferences
|
||||
are shared.
|
||||
|
||||
## Logic & grammar
|
||||
|
||||
`/design/grammar` is a compact BlockUI reference under System. Expand a topic to
|
||||
read its selection rules, usage boundaries, and links to the verified upstream
|
||||
snapshot. Topic links support direct loading and refresh. It covers foundations,
|
||||
component contracts, applet composition, and generation constraints; the final
|
||||
topic records differences from Buzz's active token contract and upstream gaps.
|
||||
Source links require Block access, but the page has no private runtime dependency.
|
||||
|
||||
## Appearance
|
||||
|
||||
The navigation contains independent theme and Normal / Compact controls. Density
|
||||
|
||||
@@ -55,3 +55,7 @@ nested navigation links, unknown-ID recovery, input and keyboard interactions,
|
||||
portal density, refresh, and appearance preferences. Representative pages are
|
||||
checked at 390/884/1440 in both densities and audited in both themes, with distinct
|
||||
per-component screenshots.
|
||||
|
||||
Grammar-page coverage checks sidebar navigation, keyboard disclosure controls,
|
||||
reopening the same topic link, hash links across reloads, all 19 typography rows,
|
||||
source-link destinations, responsive tables, and accessibility in both themes.
|
||||
|
||||
6
desktop-next/src/app/routes/design/grammar.tsx
Normal file
6
desktop-next/src/app/routes/design/grammar.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { GrammarPage } from "@/features/design-system/ui/GrammarPage";
|
||||
|
||||
export const Route = createFileRoute("/design/grammar")({
|
||||
component: GrammarPage,
|
||||
});
|
||||
@@ -47,6 +47,25 @@
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.grammar-section {
|
||||
scroll-margin-top: var(--space-panel-inset);
|
||||
}
|
||||
.grammar-section:target {
|
||||
outline: 2px solid var(--ring-focus);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
.grammar-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
.grammar-table th,
|
||||
.grammar-table td {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
vertical-align: top;
|
||||
}
|
||||
.grammar-table th:first-child {
|
||||
width: 24%;
|
||||
}
|
||||
.component-page > header h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
427
desktop-next/src/features/design-system/grammar/composition.ts
Normal file
427
desktop-next/src/features/design-system/grammar/composition.ts
Normal file
@@ -0,0 +1,427 @@
|
||||
import type { GrammarSection } from "./types";
|
||||
|
||||
export const compositionGrammar: GrammarSection[] = [
|
||||
{
|
||||
id: "intent",
|
||||
title: "Intent, archetype, and mode",
|
||||
sources: [
|
||||
"docs/intent-resolution.spec.md",
|
||||
"docs/archetypes-and-modes.spec.md",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Pipeline",
|
||||
"Domain → archetype → mode → anchor → visualization → support → density.",
|
||||
"Data shape chooses the visualization after the archetype is selected.",
|
||||
],
|
||||
[
|
||||
"Track",
|
||||
"Monitor a changing value.",
|
||||
"A goal, event review, or derived recommendation has a different job.",
|
||||
],
|
||||
[
|
||||
"Goal",
|
||||
"Show progress toward a target or limit.",
|
||||
"A timeline or member avatar does not turn a goal into Activity.",
|
||||
],
|
||||
[
|
||||
"Activity",
|
||||
"Review events, dates, people, or recent items.",
|
||||
"Use the user’s job, not the appearance of the chart, to classify it.",
|
||||
],
|
||||
[
|
||||
"Insight",
|
||||
"Explain a derived finding or suggestion.",
|
||||
"Avoid relabeling an ordinary raw metric as an insight.",
|
||||
],
|
||||
[
|
||||
"Archetype tie-break",
|
||||
"The user’s verb overrides the domain default; for vague intent, try Insight → Goal → Activity → Track.",
|
||||
"Track is the final fallback, accompanied by a warning.",
|
||||
],
|
||||
[
|
||||
"Status",
|
||||
"The answer leads with current state.",
|
||||
"A supporting button or toggle does not make it Action mode.",
|
||||
],
|
||||
[
|
||||
"Plan",
|
||||
"The answer leads with a future date, schedule, or forecast.",
|
||||
"Use Action when a recommendation or decision leads the answer.",
|
||||
],
|
||||
[
|
||||
"Action",
|
||||
"The anchor leads with the recommendation; the decision control belongs in support.",
|
||||
"This test checks the resolved output; intent-side signal extraction remains proposed.",
|
||||
],
|
||||
[
|
||||
"Unknown domain",
|
||||
"Use neutral tokens and resolve by the user’s job.",
|
||||
"Do not invent product tokens or a new taxonomy member.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "slots",
|
||||
title: "Applet slots and compatibility",
|
||||
sources: [
|
||||
"docs/slot-rules.spec.md",
|
||||
"docs/intent-resolution.spec.md",
|
||||
"docs/anti-slop.spec.md",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Reading order",
|
||||
"Heading → A: anchor → B: visualization → C: support.",
|
||||
"One subject, one lead answer, at most one visualization, and one support area; no nested applets.",
|
||||
],
|
||||
[
|
||||
"Heading",
|
||||
"A short subject identifier.",
|
||||
"Applet kickers use the source’s small mono treatment; desktop artifact headings have different rules.",
|
||||
],
|
||||
[
|
||||
"amount",
|
||||
"A single figure that answers the question.",
|
||||
"Applet bare amounts use regular headline-large, overriding the general numeral rule.",
|
||||
],
|
||||
[
|
||||
"amountWithTrend",
|
||||
"A value whose change is part of the answer.",
|
||||
"Requires a comparison value; otherwise fall back to amount.",
|
||||
],
|
||||
[
|
||||
"multiAmount",
|
||||
"Two figures that need each other for meaning.",
|
||||
"Use amount when only one figure exists.",
|
||||
],
|
||||
[
|
||||
"text",
|
||||
"A named period, subject, or conclusion.",
|
||||
"It can accompany a chart when both concern the same topic.",
|
||||
],
|
||||
[
|
||||
"Visualization",
|
||||
"Develop the anchor’s subject with one data-appropriate figure.",
|
||||
"A composite can count as one figure when it has one clear primary element.",
|
||||
],
|
||||
[
|
||||
"Support",
|
||||
"A useful takeaway or one available next step.",
|
||||
"Actions belong only here; missing actions become context text, not dead buttons.",
|
||||
],
|
||||
[
|
||||
"Actions",
|
||||
"One secondary CTA; two only for an established binary choice.",
|
||||
"The applet itself is the primary action. Size permission does not prove renderer readiness.",
|
||||
],
|
||||
[
|
||||
"Shell",
|
||||
"The applet reference specifies radius 32 and padding 24 at each size.",
|
||||
"These are applet-specific reference values, not overrides of Buzz density tokens.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "visualization",
|
||||
title: "Data shape → visualization",
|
||||
sources: [
|
||||
"docs/intent-resolution.spec.md",
|
||||
"docs/archetypes-and-modes.spec.md",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Per-period ranges",
|
||||
"rangeBar",
|
||||
"Distinguish volatility from a simple trend.",
|
||||
],
|
||||
[
|
||||
"Trend over time",
|
||||
"line",
|
||||
"Do not use a smooth trend to imply unavailable ranges.",
|
||||
],
|
||||
[
|
||||
"Future projection",
|
||||
"forecast",
|
||||
"Keep projected and observed values distinguishable.",
|
||||
],
|
||||
[
|
||||
"Progress to one target",
|
||||
"progressRing or progressBar",
|
||||
"A collection of goals may need an activityList.",
|
||||
],
|
||||
[
|
||||
"Parts of a whole",
|
||||
"barBreakdown",
|
||||
"Keep the focal category separate from neutral context.",
|
||||
],
|
||||
["Dated events", "calendar", "Ordered steps instead call for timeline."],
|
||||
[
|
||||
"Steps or milestones",
|
||||
"timeline",
|
||||
"Must survive the available height, or simplify.",
|
||||
],
|
||||
[
|
||||
"Recent items",
|
||||
"activityList",
|
||||
"Avoid cramming a multi-row list into a short band.",
|
||||
],
|
||||
[
|
||||
"People",
|
||||
"avatarGroup",
|
||||
"Membership can support a primary progress visualization.",
|
||||
],
|
||||
[
|
||||
"A specific card",
|
||||
"cardAsset",
|
||||
"Use an existing taxonomy member and verified asset.",
|
||||
],
|
||||
[
|
||||
"Locations",
|
||||
"map",
|
||||
"The reference marks glossary availability as pending; verify renderer support.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "palettes",
|
||||
title: "Archetype palettes",
|
||||
sources: ["docs/archetypes-and-modes.spec.md"],
|
||||
rows: [
|
||||
[
|
||||
"Track",
|
||||
"Range/line series, breakdown, item list, or metric.",
|
||||
"Amounts lead; support can be context, one action, attribution, or a reversible toggle.",
|
||||
],
|
||||
[
|
||||
"Goal",
|
||||
"Progress, forecast, goals, payoff timeline, shared participants, or line.",
|
||||
"Amounts lead; support is context or one action.",
|
||||
],
|
||||
[
|
||||
"Activity",
|
||||
"Calendar, item list, timeline, participants, or line.",
|
||||
"An amount or text leads; support is context, one action, or a labeled push.",
|
||||
],
|
||||
[
|
||||
"Insight",
|
||||
"Breakdown, line, forecast, progress, or map.",
|
||||
"A conclusion or amount leads; support is context, one action, or toggle.",
|
||||
],
|
||||
[
|
||||
"Validation",
|
||||
"Check the selected visualization against the archetype’s palette.",
|
||||
"Do not change archetype merely to justify an out-of-palette visualization.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mode-matrix",
|
||||
title: "The 12 default compositions",
|
||||
sources: ["docs/archetypes-and-modes.spec.md"],
|
||||
rows: [
|
||||
[
|
||||
"Track · Status",
|
||||
"Amount + trend → range/line → context or toggle.",
|
||||
"Defaults describe Large applets; apply size rules afterward.",
|
||||
],
|
||||
[
|
||||
"Track · Plan",
|
||||
"Amount → forecast → schedule context.",
|
||||
"Keep the future framing explicit.",
|
||||
],
|
||||
[
|
||||
"Track · Action",
|
||||
"Recommendation → metric/line → action.",
|
||||
"Recommendation leads; the metric becomes evidence.",
|
||||
],
|
||||
[
|
||||
"Goal · Status",
|
||||
"Amount → progress or multiple goals → action.",
|
||||
"One target and many targets use different visualizations.",
|
||||
],
|
||||
[
|
||||
"Goal · Plan",
|
||||
"Amount → forecast → context.",
|
||||
"The goal persists while framing becomes forward-looking.",
|
||||
],
|
||||
[
|
||||
"Goal · Action",
|
||||
"Recommendation → metric → action.",
|
||||
"One clear next step.",
|
||||
],
|
||||
[
|
||||
"Activity · Status",
|
||||
"Amount → recent items → context.",
|
||||
"Report current or recent state.",
|
||||
],
|
||||
[
|
||||
"Activity · Plan",
|
||||
"Period label → calendar → context.",
|
||||
"The slot rules retain a support line even where older matrix shorthand omits it.",
|
||||
],
|
||||
[
|
||||
"Activity · Action",
|
||||
"Recommendation → participants/metric → action.",
|
||||
"Two controls require a meaningful binary decision.",
|
||||
],
|
||||
[
|
||||
"Insight · Status",
|
||||
"Amount → line/breakdown → context.",
|
||||
"Keep one synthesized subject.",
|
||||
],
|
||||
[
|
||||
"Insight · Plan",
|
||||
"Amount → line → attributed context.",
|
||||
"Make the future scope clear.",
|
||||
],
|
||||
[
|
||||
"Insight · Action",
|
||||
"Recommendation → breakdown → action.",
|
||||
"The evidence must support the recommendation.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "degradation",
|
||||
title: "Size and missing-data fallbacks",
|
||||
sources: ["docs/slot-rules.spec.md"],
|
||||
rows: [
|
||||
[
|
||||
"Large → Medium",
|
||||
"Reduce height at full width, remove secondary anchor detail, and compact the visualization.",
|
||||
"Keep the main answer and support; action controls remain valid at Medium.",
|
||||
],
|
||||
[
|
||||
"Medium → Small",
|
||||
"Reduce width, step the anchor down, collapse paired amounts, and remove secondary labels.",
|
||||
"This is content adaptation, not uniform scaling or Buzz’s Normal/Compact switch.",
|
||||
],
|
||||
[
|
||||
"Small support",
|
||||
"Replace the action control with a status takeaway.",
|
||||
"Only the source-verified compact +/− pattern is an interactive exception; do not generalize it.",
|
||||
],
|
||||
[
|
||||
"Visualization fit",
|
||||
"Compact a single band or object before dropping it.",
|
||||
"A multi-row timeline or list may fail to fit; the Medium band can be shorter than Small.",
|
||||
],
|
||||
[
|
||||
"Minimum",
|
||||
"Keep the subject heading, primary answer, and a line of support.",
|
||||
"An Action-mode text answer may merge into support at Medium without losing its meaning.",
|
||||
],
|
||||
[
|
||||
"Missing comparison",
|
||||
"Replace amountWithTrend with amount.",
|
||||
"Never fabricate movement or comparison data.",
|
||||
],
|
||||
[
|
||||
"Missing figure",
|
||||
"Use a text takeaway.",
|
||||
"An empty numeric placeholder is not an answer.",
|
||||
],
|
||||
[
|
||||
"Sparse or absent series",
|
||||
"Try a metric or short list; omit the visualization when no meaningful data remains.",
|
||||
"Collapse its space instead of rendering an empty slot.",
|
||||
],
|
||||
[
|
||||
"Missing action",
|
||||
"Show useful context.",
|
||||
"Never offer an unavailable operation as a working button.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "generation",
|
||||
title: "Generated responses and consequence",
|
||||
sources: ["Design.md", "docs/role-join.md", "docs/anti-slop.spec.md"],
|
||||
rows: [
|
||||
[
|
||||
"Response grammar",
|
||||
"Answer → evidence → meaning → actions.",
|
||||
"This response recipe is related to, but not identical to, an applet’s Heading/A/B/C slots.",
|
||||
],
|
||||
[
|
||||
"Structure versus values",
|
||||
"Generated content selects supported structure; the renderer supplies design values.",
|
||||
"The response contract is not a channel for arbitrary CSS or new token values.",
|
||||
],
|
||||
[
|
||||
"Specificity",
|
||||
"Lead with one concrete answer, evidence on that subject, and a useful interpretation.",
|
||||
"Avoid co-equal metric grids, filler text, and interpretations that only repeat the answer.",
|
||||
],
|
||||
[
|
||||
"Traceability",
|
||||
"Tie component and slot choices to a documented rule or reference example.",
|
||||
"A visually plausible invention is not proof of supported composition.",
|
||||
],
|
||||
[
|
||||
"Artifact",
|
||||
"A persistent work object with identity, context, lifecycle, and ordered content blocks.",
|
||||
"A hero is conditional; a block can be bare or card-backed. An applet is not synonymous with every card.",
|
||||
],
|
||||
[
|
||||
"R0 / R1 / R2",
|
||||
"Informational → personalized read → recommendation; add source evidence and reasoning as consequence grows.",
|
||||
"The source’s applet corpus supports these levels; this is a reference policy.",
|
||||
],
|
||||
[
|
||||
"R3 / R4 / R5",
|
||||
"Prepared draft → reversible execution → sensitive execution; require preview, confirmation, receipt, and appropriate review/audit.",
|
||||
"The source marks these as forward-looking and blocks R3+ generation pending a reviewed baseline.",
|
||||
],
|
||||
[
|
||||
"Authorization",
|
||||
"Show → explain → suggest → draft → preview → confirm → execute; escalate when needed.",
|
||||
"Do not promote intent into authorization or show success before backend confirmation.",
|
||||
],
|
||||
[
|
||||
"Readiness",
|
||||
"Check target support, required states, provenance, and known gaps before generation.",
|
||||
"A component being legal in the size grammar does not mean it is approved or implemented.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "buzz-boundary",
|
||||
title: "Reference status and Buzz differences",
|
||||
sources: [
|
||||
"docs/type.resolution.draft.json",
|
||||
"docs/color.roles.md",
|
||||
"docs/role-join.md",
|
||||
"docs/archetypes-and-modes.spec.md",
|
||||
"foundations/typography/typography-foundation.md",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Reference snapshot",
|
||||
"Summarized from the linked BlockUI sources, verified September 10, 2026.",
|
||||
"Drafts, proposed rules, and unresolved platform differences retain their source status.",
|
||||
],
|
||||
[
|
||||
"Buzz fonts and type",
|
||||
"Buzz uses Inter and JetBrains Mono with its existing ten-role, rem-based scale.",
|
||||
"This page documents BlockUI’s nineteen roles; it does not silently replace the Buzz type contract.",
|
||||
],
|
||||
[
|
||||
"Buzz conventions",
|
||||
"Buzz retains sentence-case labels, semantic geometry, and shared Normal/Compact density.",
|
||||
"BlockUI’s mono uppercase applet kicker, applet sizes, and specific numeric settings are reference-specific.",
|
||||
],
|
||||
[
|
||||
"Usage coverage",
|
||||
"The upstream role join distinguishes exact authored rules, family matches, and gaps.",
|
||||
"Do not assume every available token has complete usage guidance.",
|
||||
],
|
||||
[
|
||||
"Open questions",
|
||||
"Source gaps include intent-side signal extraction, role pairing, responsive type breakpoints, and some platform bindings.",
|
||||
"This outline does not resolve those gaps or implement the BlockUI planner in Buzz.",
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
258
desktop-next/src/features/design-system/grammar/foundations.ts
Normal file
258
desktop-next/src/features/design-system/grammar/foundations.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import type { GrammarSection } from "./types";
|
||||
|
||||
export const foundationGrammar: GrammarSection[] = [
|
||||
{
|
||||
id: "role-resolution",
|
||||
title: "Names, values, and usage",
|
||||
sources: [
|
||||
"docs/role-join.md",
|
||||
"docs/color.roles.md",
|
||||
"foundations/roles/role-usage.json",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Resolution",
|
||||
"Choose a semantic role, then resolve its primitive and platform binding.",
|
||||
"A similarly named platform token may have a different value.",
|
||||
],
|
||||
[
|
||||
"Role join",
|
||||
"Connect the token’s dot-notation key to its usage rule; slash names can be input aliases.",
|
||||
"Family-level guidance is less specific than an authored member rule.",
|
||||
],
|
||||
[
|
||||
"Usage contract",
|
||||
"Consult when, why, and do_nots together.",
|
||||
"A legal token can still be wrong for the situation.",
|
||||
],
|
||||
[
|
||||
"Missing guidance",
|
||||
"Report that no rule has been authored when resolution has no exact usage match.",
|
||||
"Do not manufacture a rule or claim complete coverage.",
|
||||
],
|
||||
[
|
||||
"Color naming",
|
||||
"Primary / secondary / tertiary is the chosen ordinal vocabulary.",
|
||||
"The reference snapshot still contains older standard / subtle / muted keys; match their meaning.",
|
||||
],
|
||||
[
|
||||
"New roles",
|
||||
"Add a role only for a distinct intent that existing roles cannot express.",
|
||||
"A new size or visual preference alone is not a new semantic role.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "color",
|
||||
title: "Color and emphasis",
|
||||
sources: [
|
||||
"docs/color.roles.md",
|
||||
"docs/color.resolution.draft.json",
|
||||
"foundations/roles/role-usage.json",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Channel",
|
||||
"Choose text, icon, surface, or border according to what receives the color.",
|
||||
"An unassigned channel is a deliberate limit, not permission to invent a fill.",
|
||||
],
|
||||
[
|
||||
"Primary text",
|
||||
"Titles, key values, names, body content, and primary labels.",
|
||||
"Use quieter roles for supporting metadata.",
|
||||
],
|
||||
[
|
||||
"Secondary text",
|
||||
"Supporting explanations and lower-priority content.",
|
||||
"Quiet content remains readable and available.",
|
||||
],
|
||||
[
|
||||
"Tertiary text",
|
||||
"Timestamps, placeholders, annotations, and other scaffolding.",
|
||||
"Do not substitute disabled styling for lower emphasis.",
|
||||
],
|
||||
[
|
||||
"Inverse",
|
||||
"Pair inverse content with an inverted surface.",
|
||||
"Elevation alone does not imply inversion; both colors must work in either theme.",
|
||||
],
|
||||
[
|
||||
"Icons",
|
||||
"Match the corresponding content role.",
|
||||
"Do not introduce a competing emphasis level beside its label.",
|
||||
],
|
||||
[
|
||||
"Surfaces",
|
||||
"Distinguish page, inset/group, selected/emphasis, card, and floating-overlay jobs.",
|
||||
"Platform-specific fills may resolve differently; follow the selected platform.",
|
||||
],
|
||||
[
|
||||
"Borders",
|
||||
"Subtle separates; standard outlines a control; prominent emphasizes an edge; focus marks keyboard focus.",
|
||||
"Do not use a high-emphasis outline for an ordinary divider.",
|
||||
],
|
||||
[
|
||||
"Status",
|
||||
"Use semantic color for a real error, caution, or confirmation.",
|
||||
"Product-specific channel limits apply; do not invent a success background.",
|
||||
],
|
||||
[
|
||||
"Brand",
|
||||
"Use only where an identity treatment is explicitly assigned.",
|
||||
"Brand is not a general-purpose accent for actions, status, or decoration.",
|
||||
],
|
||||
[
|
||||
"Trend",
|
||||
"Use signs, arrows, and words for ordinary up/down/flat changes.",
|
||||
"Direction alone does not warrant success or error color.",
|
||||
],
|
||||
[
|
||||
"Chart focus",
|
||||
"Keep context marks neutral and distinguish the focal item with an assigned product color.",
|
||||
"The focal encoding is separate from trend or status semantics.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "states",
|
||||
title: "Interaction states",
|
||||
sources: ["docs/color.resolution.draft.json", "Design.md"],
|
||||
rows: [
|
||||
[
|
||||
"Rest",
|
||||
"Use the role’s resolved default value.",
|
||||
"State changes derive from that resting role.",
|
||||
],
|
||||
[
|
||||
"Hover",
|
||||
"Move the surface one step toward stronger contrast.",
|
||||
"Text and icons stay stable; hover is a pointer affordance.",
|
||||
],
|
||||
[
|
||||
"Pressed",
|
||||
"The reference color model moves the surface two contrast steps.",
|
||||
"Follow the target platform’s active-state behavior.",
|
||||
],
|
||||
[
|
||||
"Selected",
|
||||
"Use a persistent neutral contrast change.",
|
||||
"A narrow product-specific colored selection exception is not a global accent rule.",
|
||||
],
|
||||
[
|
||||
"Focus",
|
||||
"Show the keyboard focus ring without changing the fill.",
|
||||
"Keep focus distinct from hover and selection.",
|
||||
],
|
||||
[
|
||||
"Disabled",
|
||||
"Use disabled tokens and actual unavailable-control semantics.",
|
||||
"Do not use disabled to mean merely secondary, or rely only on color.",
|
||||
],
|
||||
[
|
||||
"Loading",
|
||||
"Represent the pending action in the control’s state contract.",
|
||||
"Preserve its accessible name and avoid implying completion.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "geometry",
|
||||
title: "Spacing, shape, and grouping",
|
||||
sources: [
|
||||
"Design.md",
|
||||
"docs/shape.resolution.draft.json",
|
||||
"foundations/roles/role-usage.json",
|
||||
"foundations/elevation/README.md",
|
||||
"foundations/motion/README.md",
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
"Spacing",
|
||||
"BlockUI’s structural reference starts from an eight-unit rhythm.",
|
||||
"Observed spacing examples are not universal prescriptions; specific surface rules take precedence.",
|
||||
],
|
||||
[
|
||||
"Grouping",
|
||||
"Try whitespace first, a divider next, and a container when stronger separation is needed.",
|
||||
"Do not frame every related pair of elements.",
|
||||
],
|
||||
[
|
||||
"control",
|
||||
"Input-scale fields and small interactive elements.",
|
||||
"Avoid content-container corners on small controls.",
|
||||
],
|
||||
[
|
||||
"menu",
|
||||
"Lightweight menus and transient popovers.",
|
||||
"A primary modal or owned content surface uses a container role.",
|
||||
],
|
||||
[
|
||||
"container",
|
||||
"Cards and primary content or modal containers.",
|
||||
"The applet shell has its own more specific geometry.",
|
||||
],
|
||||
[
|
||||
"overlay / sheet",
|
||||
"Large edge-attached overlays and bottom sheets.",
|
||||
"The source documents platform differences rather than one universal radius.",
|
||||
],
|
||||
[
|
||||
"pill",
|
||||
"Fully rounded shapes wider than they are tall.",
|
||||
"Resolve full rounding as a shape, not an arbitrary corner value.",
|
||||
],
|
||||
[
|
||||
"circular",
|
||||
"Square avatars, round icon controls, and other equal-sided shapes.",
|
||||
"Do not force variable-width text into a circle.",
|
||||
],
|
||||
[
|
||||
"Elevation / motion",
|
||||
"Consult the foundation handoffs for platform-specific behavior and open work.",
|
||||
"Elevation is a proposed lane in this snapshot; the motion comparison is not one settled universal grammar.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "components",
|
||||
title: "Component contracts",
|
||||
sources: ["Design.md"],
|
||||
rows: [
|
||||
[
|
||||
"Base component",
|
||||
"A product-independent job with reusable content, state, interaction, or accessibility behavior.",
|
||||
"Complexity does not disqualify a Table or Calendar from being a base component.",
|
||||
],
|
||||
[
|
||||
"Product composition",
|
||||
"A domain-specific workflow assembled from base components.",
|
||||
"Repeated use alone does not make its product policy a base contract.",
|
||||
],
|
||||
[
|
||||
"Button axes",
|
||||
"Choose emphasis/tone and size independently.",
|
||||
"A screen-level CTA is a layout role, not a special size token.",
|
||||
],
|
||||
[
|
||||
"Action hierarchy",
|
||||
"Usually one primary action per task context; supporting actions use secondary treatment.",
|
||||
"Reserve ghost treatment for genuinely contextual or lower-priority actions.",
|
||||
],
|
||||
[
|
||||
"Button anatomy",
|
||||
"A text label with optional leading and trailing content.",
|
||||
"Icon-only controls need an accessible name.",
|
||||
],
|
||||
[
|
||||
"Ownership",
|
||||
"Button owns the trigger; menu owns popup items, focus, keyboard navigation, and dismissal.",
|
||||
"Select and Combobox own form-value selection; links own navigation.",
|
||||
],
|
||||
[
|
||||
"Fields",
|
||||
"Associate a label, help or validation, and the input.",
|
||||
"Do not remove functional help when simplifying surrounding documentation.",
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
11
desktop-next/src/features/design-system/grammar/types.ts
Normal file
11
desktop-next/src/features/design-system/grammar/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/** A compact, cited summary of a BlockUI decision and its limits. */
|
||||
export interface GrammarSection {
|
||||
id: string;
|
||||
title: string;
|
||||
sources: string[];
|
||||
rows: Array<[choice: string, use: string, boundary: string]>;
|
||||
}
|
||||
|
||||
/** Verified upstream snapshot; reference links never load assets into the catalog. */
|
||||
export const BLOCKUI_REFERENCE =
|
||||
"https://github.com/squareup/design-blockinterface/blob/c89319ea3e11a35d58f19ba83f4d976d143e7485/blockUI/";
|
||||
148
desktop-next/src/features/design-system/grammar/typography.ts
Normal file
148
desktop-next/src/features/design-system/grammar/typography.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { GrammarSection } from "./types";
|
||||
|
||||
export const typographyGrammar: GrammarSection[] = [
|
||||
{
|
||||
id: "type-selection",
|
||||
title: "Choosing typography",
|
||||
sources: ["docs/type.roles.md", "docs/type.resolution.draft.json"],
|
||||
rows: [
|
||||
[
|
||||
"Content first",
|
||||
"Choose Body for prose, Label for names, Link for navigation, Numeral for amounts, and Display for identity.",
|
||||
"Equal size does not make roles interchangeable.",
|
||||
],
|
||||
[
|
||||
"Weight",
|
||||
"Regular supports reading; Medium identifies controls, structure, and numeric values.",
|
||||
"Weight belongs to the role; it is not an independent emphasis dial.",
|
||||
],
|
||||
[
|
||||
"Complete setting",
|
||||
"Resolve size, leading, tracking, weight, and family together.",
|
||||
"Avoid assembling an unregistered combination.",
|
||||
],
|
||||
[
|
||||
"Numbers",
|
||||
"General numeral roles use Medium, tabular figures, and zero tracking.",
|
||||
"Applet anchors have a more specific rule: regular headline roles, described below.",
|
||||
],
|
||||
[
|
||||
"Links",
|
||||
"Use the Link roles and their underline treatment.",
|
||||
"A button is an action component; a link is navigation.",
|
||||
],
|
||||
[
|
||||
"Monospace",
|
||||
"BlockUI reserves Cash Sans Mono for its smallest Detail tier.",
|
||||
"Buzz uses open-source fonts and has a separate code role; this reference does not change it.",
|
||||
],
|
||||
[
|
||||
"Collisions",
|
||||
"Use page-title for screen identity and numeral-small for an amount, even at the same size.",
|
||||
"Use the role’s exclusion rule to resolve ambiguity.",
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "type-roles",
|
||||
title: "The 19 typography roles",
|
||||
sources: ["docs/type.roles.md"],
|
||||
rows: [
|
||||
[
|
||||
"display/hero",
|
||||
"Expressive welcome or brand statement.",
|
||||
"Use page-title for ordinary screen identity; numeral-large for a number.",
|
||||
],
|
||||
[
|
||||
"display/numeral-large",
|
||||
"The main amount the screen concerns.",
|
||||
"Use hero for words; numeral-small for a supporting amount.",
|
||||
],
|
||||
[
|
||||
"display/headline-large",
|
||||
"Leading editorial or marketing headline.",
|
||||
"Use page-title for application structure.",
|
||||
],
|
||||
[
|
||||
"display/page-title",
|
||||
"Screen identity or modal title.",
|
||||
"Use section-title for a content group; headline-small for editorial tone.",
|
||||
],
|
||||
[
|
||||
"display/headline-small",
|
||||
"Secondary editorial headline.",
|
||||
"Use page-title for a structural screen title.",
|
||||
],
|
||||
[
|
||||
"display/numeral-small",
|
||||
"A supporting metric, card amount, or row total.",
|
||||
"Use a title role when the content names a screen.",
|
||||
],
|
||||
[
|
||||
"display/section-title",
|
||||
"Heading that groups related content.",
|
||||
"Use page-title for the whole screen.",
|
||||
],
|
||||
[
|
||||
"body/body-large",
|
||||
"A lead paragraph or blockquote.",
|
||||
"Use body-medium for ordinary prose; label-large for a name.",
|
||||
],
|
||||
[
|
||||
"body/label-large",
|
||||
"A prominent control or group label.",
|
||||
"Use body-large for prose; label-medium for standard labels.",
|
||||
],
|
||||
[
|
||||
"body/body-medium",
|
||||
"Default paragraphs, primary content, and messages.",
|
||||
"Use label-medium for controls; body-small for supporting text.",
|
||||
],
|
||||
[
|
||||
"body/label-medium",
|
||||
"Form labels, control names, and primary row labels.",
|
||||
"Use body-medium for running prose.",
|
||||
],
|
||||
[
|
||||
"body/link-medium",
|
||||
"A link at the normal reading scale.",
|
||||
"Use Button for a full action control.",
|
||||
],
|
||||
[
|
||||
"body/body-small",
|
||||
"Supporting copy and dense table cells.",
|
||||
"Use body-medium for the main reading content.",
|
||||
],
|
||||
[
|
||||
"body/label-small",
|
||||
"Badge text, chip labels, and table headers.",
|
||||
"Use body-small for reading text.",
|
||||
],
|
||||
[
|
||||
"body/link-small",
|
||||
"A link in a dense interface.",
|
||||
"Use a larger Link role when compactness is unnecessary.",
|
||||
],
|
||||
[
|
||||
"detail/caption",
|
||||
"Metadata, timestamps, help, errors, and footnotes.",
|
||||
"Use Body for primary content.",
|
||||
],
|
||||
[
|
||||
"detail/body-xsmall",
|
||||
"Fine-print identifiers and monospace data.",
|
||||
"Use caption for prose.",
|
||||
],
|
||||
[
|
||||
"detail/label-xsmall",
|
||||
"Very small data labels and chart annotations.",
|
||||
"Use label-small for ordinary compact labels.",
|
||||
],
|
||||
[
|
||||
"detail/link-xsmall",
|
||||
"A small link inside monospace data.",
|
||||
"Use a Body Link role outside micro-data contexts.",
|
||||
],
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -27,6 +27,7 @@ const SECTIONS: Array<{ heading: string; items: NavItem[] }> = [
|
||||
{
|
||||
heading: "System",
|
||||
items: [
|
||||
["Logic & grammar", "/design/grammar"],
|
||||
["Vocabulary", "/design/vocabulary"],
|
||||
["Growing the system", "/design/growth"],
|
||||
],
|
||||
|
||||
115
desktop-next/src/features/design-system/ui/GrammarPage.tsx
Normal file
115
desktop-next/src/features/design-system/ui/GrammarPage.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Table } from "@buzz/ui";
|
||||
import { useEffect } from "react";
|
||||
import { foundationGrammar } from "../grammar/foundations";
|
||||
import { typographyGrammar } from "../grammar/typography";
|
||||
import { compositionGrammar } from "../grammar/composition";
|
||||
import { BLOCKUI_REFERENCE } from "../grammar/types";
|
||||
import { PageHeader } from "./primitives";
|
||||
|
||||
const sections = [
|
||||
foundationGrammar[0],
|
||||
...typographyGrammar,
|
||||
...foundationGrammar.slice(1),
|
||||
...compositionGrammar,
|
||||
];
|
||||
|
||||
/** Cited BlockUI reference guidance, separate from Buzz's active token contract. */
|
||||
export function GrammarPage() {
|
||||
useEffect(() => {
|
||||
const revealTopic = () => {
|
||||
const topic = document.getElementById(window.location.hash.slice(1));
|
||||
if (topic instanceof HTMLDetailsElement) {
|
||||
topic.open = true;
|
||||
topic.scrollIntoView({ block: "start" });
|
||||
}
|
||||
};
|
||||
revealTopic();
|
||||
window.addEventListener("hashchange", revealTopic);
|
||||
return () => window.removeEventListener("hashchange", revealTopic);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<article className="grammar-page">
|
||||
<PageHeader title="Logic & grammar" />
|
||||
<p className="text-caption text-secondary mb-6">
|
||||
BlockUI reference · Usage, boundaries, and composition · Source links
|
||||
require Block access
|
||||
</p>
|
||||
<nav
|
||||
aria-label="Grammar topics"
|
||||
className="flex flex-wrap gap-x-5 gap-y-2 mb-8"
|
||||
>
|
||||
{sections.map((section) => (
|
||||
<a
|
||||
key={section.id}
|
||||
href={`#${section.id}`}
|
||||
onClick={() => {
|
||||
const topic = document.getElementById(section.id);
|
||||
if (topic instanceof HTMLDetailsElement) topic.open = true;
|
||||
}}
|
||||
className="text-caption text-accent underline"
|
||||
>
|
||||
{section.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
<div className="flex flex-col gap-4">
|
||||
{sections.map((section) => (
|
||||
<details
|
||||
key={section.id}
|
||||
id={section.id}
|
||||
className="grammar-section rounded-control bg-inset p-4"
|
||||
open={section.id === "role-resolution"}
|
||||
>
|
||||
<summary className="cursor-pointer text-subheading text-primary">
|
||||
{section.title}
|
||||
</summary>
|
||||
<div className="hidden sm:block">
|
||||
<Table className="grammar-table mt-5">
|
||||
<caption className="bui-sr-only">{section.title}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Choice</th>
|
||||
<th scope="col">Use</th>
|
||||
<th scope="col">Boundary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{section.rows.map(([choice, use, boundary]) => (
|
||||
<tr key={choice}>
|
||||
<th scope="row">{choice}</th>
|
||||
<td>{use}</td>
|
||||
<td>{boundary}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
<dl className="sm:hidden mt-4 flex flex-col gap-5">
|
||||
{section.rows.map(([choice, use, boundary]) => (
|
||||
<div key={choice} className="flex flex-col gap-2">
|
||||
<dt className="text-label text-primary break-words">
|
||||
{choice}
|
||||
</dt>
|
||||
<dd className="text-body text-primary">{use}</dd>
|
||||
<dd className="text-caption text-secondary">{boundary}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 mt-4">
|
||||
{section.sources.map((source) => (
|
||||
<a
|
||||
key={source}
|
||||
href={`${BLOCKUI_REFERENCE}${source}`}
|
||||
className="text-meta text-accent underline break-all"
|
||||
>
|
||||
{source.split("/").at(-1)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
81
desktop-next/tests/grammar.spec.ts
Normal file
81
desktop-next/tests/grammar.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { waitForAnimations } from "../../desktop/tests/helpers/animations";
|
||||
|
||||
test("grammar navigation opens focused topics and preserves deep links", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/design/typography");
|
||||
await page
|
||||
.getByRole("navigation", { name: "Design system", exact: true })
|
||||
.getByRole("link", { name: "Logic & grammar", exact: true })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Logic & grammar", exact: true }),
|
||||
).toBeVisible();
|
||||
const topic = page.locator("#type-roles");
|
||||
const link = page
|
||||
.getByRole("navigation", { name: "Grammar topics" })
|
||||
.getByRole("link", { name: "The 19 typography roles", exact: true });
|
||||
await link.focus();
|
||||
await link.press("Enter");
|
||||
await expect(topic).toHaveAttribute("open", "");
|
||||
await expect(topic.locator("tbody tr")).toHaveCount(19);
|
||||
await expect(
|
||||
topic.getByRole("rowheader", { name: "body/label-medium", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
topic.getByRole("link", { name: "type.roles.md", exact: true }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
/github\.com\/squareup\/design-blockinterface\/blob\/[a-f0-9]{40}\/blockUI\/docs\/type\.roles\.md$/,
|
||||
);
|
||||
await topic.locator("summary").press("Space");
|
||||
await expect(topic).not.toHaveAttribute("open", "");
|
||||
// A second click on the same hash must reopen a manually closed topic.
|
||||
await link.click();
|
||||
await expect(topic).toHaveAttribute("open", "");
|
||||
await page.reload();
|
||||
await expect(topic).toHaveAttribute("open", "");
|
||||
await expect(topic).toBeInViewport();
|
||||
await page.goto("/design/grammar#buzz-boundary");
|
||||
await expect(page.locator("#buzz-boundary")).toHaveAttribute("open", "");
|
||||
await expect(
|
||||
page.getByRole("rowheader", { name: "Buzz fonts and type", exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("grammar tables fit both densities and themes with accessible disclosures", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
await page.goto("/design/grammar");
|
||||
for (const width of [390, 1440]) {
|
||||
await page.setViewportSize({ width, height: 960 });
|
||||
for (const density of ["Normal", "Compact"]) {
|
||||
await page.getByRole("button", { name: density, exact: true }).click();
|
||||
for (const section of await page.locator(".grammar-section").all()) {
|
||||
if (
|
||||
!(await section.evaluate((el) => (el as HTMLDetailsElement).open))
|
||||
) {
|
||||
await section.locator("summary").click();
|
||||
}
|
||||
}
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => document.documentElement.scrollWidth <= innerWidth,
|
||||
),
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
for (const mode of ["light", "dark"]) {
|
||||
if (mode === "dark")
|
||||
await page.getByRole("button", { name: "Switch to dark mode" }).click();
|
||||
const audit = await new AxeBuilder({ page })
|
||||
.disableRules(["color-contrast"])
|
||||
.analyze();
|
||||
expect(audit.violations).toEqual([]);
|
||||
await page.locator("#type-roles").scrollIntoViewIfNeeded();
|
||||
await waitForAnimations(page);
|
||||
await page.screenshot({ path: testInfo.outputPath(`grammar-${mode}.png`) });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user