From 37ca6f63c753c02f9f43fe481fed688f6a7cb935 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Thu, 10 Sep 2026 05:47:38 -0700 Subject: [PATCH 1/2] feat(design-system): add BlockUI logic and grammar reference Signed-off-by: Arjun Mahanti --- desktop-next/README.md | 9 + desktop-next/TESTING.md | 4 + .../src/app/routes/design/grammar.tsx | 6 + .../design-system/catalog/catalog.css | 19 + .../design-system/grammar/composition.ts | 427 ++++++++++++++++++ .../design-system/grammar/foundations.ts | 258 +++++++++++ .../features/design-system/grammar/types.ts | 11 + .../design-system/grammar/typography.ts | 148 ++++++ .../design-system/ui/DesignSystemLayout.tsx | 1 + .../features/design-system/ui/GrammarPage.tsx | 115 +++++ desktop-next/tests/grammar.spec.ts | 81 ++++ 11 files changed, 1079 insertions(+) create mode 100644 desktop-next/src/app/routes/design/grammar.tsx create mode 100644 desktop-next/src/features/design-system/grammar/composition.ts create mode 100644 desktop-next/src/features/design-system/grammar/foundations.ts create mode 100644 desktop-next/src/features/design-system/grammar/types.ts create mode 100644 desktop-next/src/features/design-system/grammar/typography.ts create mode 100644 desktop-next/src/features/design-system/ui/GrammarPage.tsx create mode 100644 desktop-next/tests/grammar.spec.ts diff --git a/desktop-next/README.md b/desktop-next/README.md index aa6dcc8890e..ecd40aaa76a 100644 --- a/desktop-next/README.md +++ b/desktop-next/README.md @@ -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 diff --git a/desktop-next/TESTING.md b/desktop-next/TESTING.md index 2fe483fe2bc..d6cb05f73b6 100644 --- a/desktop-next/TESTING.md +++ b/desktop-next/TESTING.md @@ -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. diff --git a/desktop-next/src/app/routes/design/grammar.tsx b/desktop-next/src/app/routes/design/grammar.tsx new file mode 100644 index 00000000000..58fc3e37dad --- /dev/null +++ b/desktop-next/src/app/routes/design/grammar.tsx @@ -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, +}); diff --git a/desktop-next/src/features/design-system/catalog/catalog.css b/desktop-next/src/features/design-system/catalog/catalog.css index 8d30a5b24c0..218b903ebaf 100644 --- a/desktop-next/src/features/design-system/catalog/catalog.css +++ b/desktop-next/src/features/design-system/catalog/catalog.css @@ -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; } diff --git a/desktop-next/src/features/design-system/grammar/composition.ts b/desktop-next/src/features/design-system/grammar/composition.ts new file mode 100644 index 00000000000..e5c70425b1f --- /dev/null +++ b/desktop-next/src/features/design-system/grammar/composition.ts @@ -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.", + ], + ], + }, +]; diff --git a/desktop-next/src/features/design-system/grammar/foundations.ts b/desktop-next/src/features/design-system/grammar/foundations.ts new file mode 100644 index 00000000000..4230ee0f17a --- /dev/null +++ b/desktop-next/src/features/design-system/grammar/foundations.ts @@ -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.", + ], + ], + }, +]; diff --git a/desktop-next/src/features/design-system/grammar/types.ts b/desktop-next/src/features/design-system/grammar/types.ts new file mode 100644 index 00000000000..bbfcf7a0dae --- /dev/null +++ b/desktop-next/src/features/design-system/grammar/types.ts @@ -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/"; diff --git a/desktop-next/src/features/design-system/grammar/typography.ts b/desktop-next/src/features/design-system/grammar/typography.ts new file mode 100644 index 00000000000..503b0c5d907 --- /dev/null +++ b/desktop-next/src/features/design-system/grammar/typography.ts @@ -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.", + ], + ], + }, +]; diff --git a/desktop-next/src/features/design-system/ui/DesignSystemLayout.tsx b/desktop-next/src/features/design-system/ui/DesignSystemLayout.tsx index 955e60e515e..a80fdc5feda 100644 --- a/desktop-next/src/features/design-system/ui/DesignSystemLayout.tsx +++ b/desktop-next/src/features/design-system/ui/DesignSystemLayout.tsx @@ -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"], ], diff --git a/desktop-next/src/features/design-system/ui/GrammarPage.tsx b/desktop-next/src/features/design-system/ui/GrammarPage.tsx new file mode 100644 index 00000000000..1bc61629f64 --- /dev/null +++ b/desktop-next/src/features/design-system/ui/GrammarPage.tsx @@ -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 ( +
+ +

+ BlockUI reference · Usage, boundaries, and composition · Source links + require Block access +

+ +
+ {sections.map((section) => ( +
+ + {section.title} + +
+ + + + + + + + + + + {section.rows.map(([choice, use, boundary]) => ( + + + + + + ))} + +
{section.title}
ChoiceUseBoundary
{choice}{use}{boundary}
+
+
+ {section.rows.map(([choice, use, boundary]) => ( +
+
+ {choice} +
+
{use}
+
{boundary}
+
+ ))} +
+
+ {section.sources.map((source) => ( + + {source.split("/").at(-1)} + + ))} +
+
+ ))} +
+
+ ); +} diff --git a/desktop-next/tests/grammar.spec.ts b/desktop-next/tests/grammar.spec.ts new file mode 100644 index 00000000000..b15af111fbb --- /dev/null +++ b/desktop-next/tests/grammar.spec.ts @@ -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`) }); + } +}); From e06c7e5362e264f38e6de397381a1fc97a239f3c Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Thu, 10 Sep 2026 06:00:03 -0700 Subject: [PATCH 2/2] fix(design-system): show grammar rules in flat tables Signed-off-by: Arjun Mahanti --- desktop-next/README.md | 6 +- desktop-next/TESTING.md | 4 +- .../design-system/catalog/catalog.css | 4 + .../features/design-system/ui/GrammarPage.tsx | 84 +++++++------------ desktop-next/tests/grammar.spec.ts | 30 +++---- 5 files changed, 48 insertions(+), 80 deletions(-) diff --git a/desktop-next/README.md b/desktop-next/README.md index ecd40aaa76a..2b7c58953c5 100644 --- a/desktop-next/README.md +++ b/desktop-next/README.md @@ -30,9 +30,9 @@ 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, +`/design/grammar` is a BlockUI reference under System. Every topic is displayed +as a flat table of selection rules and usage boundaries, with links to the +verified upstream snapshot. Tables scroll horizontally on narrow screens. 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. diff --git a/desktop-next/TESTING.md b/desktop-next/TESTING.md index d6cb05f73b6..e6db922b66f 100644 --- a/desktop-next/TESTING.md +++ b/desktop-next/TESTING.md @@ -56,6 +56,6 @@ 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, +Grammar-page coverage checks sidebar navigation, always-visible tables, +keyboard topic links, hash links across reloads, all 19 typography rows, source-link destinations, responsive tables, and accessibility in both themes. diff --git a/desktop-next/src/features/design-system/catalog/catalog.css b/desktop-next/src/features/design-system/catalog/catalog.css index 218b903ebaf..30abf08056e 100644 --- a/desktop-next/src/features/design-system/catalog/catalog.css +++ b/desktop-next/src/features/design-system/catalog/catalog.css @@ -57,6 +57,10 @@ .grammar-table { table-layout: fixed; } +.grammar-table:focus-visible { + outline: 2px solid var(--ring-focus); + outline-offset: -2px; +} .grammar-table th, .grammar-table td { white-space: normal; diff --git a/desktop-next/src/features/design-system/ui/GrammarPage.tsx b/desktop-next/src/features/design-system/ui/GrammarPage.tsx index 1bc61629f64..529fccfa666 100644 --- a/desktop-next/src/features/design-system/ui/GrammarPage.tsx +++ b/desktop-next/src/features/design-system/ui/GrammarPage.tsx @@ -1,5 +1,4 @@ import { Table } from "@buzz/ui"; -import { useEffect } from "react"; import { foundationGrammar } from "../grammar/foundations"; import { typographyGrammar } from "../grammar/typography"; import { compositionGrammar } from "../grammar/composition"; @@ -15,19 +14,6 @@ const sections = [ /** 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 (
@@ -43,59 +29,45 @@ export function GrammarPage() { { - const topic = document.getElementById(section.id); - if (topic instanceof HTMLDetailsElement) topic.open = true; - }} className="text-caption text-accent underline" > {section.title} ))} -
+
{sections.map((section) => ( -
- +

{section.title} -

-
- - - - - - - + +
{section.title}
ChoiceUseBoundary
+ + + + + + + + + + {section.rows.map(([choice, use, boundary]) => ( + + + + - - - {section.rows.map(([choice, use, boundary]) => ( - - - - - - ))} - -
{section.title}
ChoiceUseBoundary
{choice}{use}{boundary}
{choice}{use}{boundary}
-
-
- {section.rows.map(([choice, use, boundary]) => ( -
-
- {choice} -
-
{use}
-
{boundary}
-
- ))} -
+ ))} + +
{section.sources.map((source) => ( ))}
-
+ ))}
diff --git a/desktop-next/tests/grammar.spec.ts b/desktop-next/tests/grammar.spec.ts index b15af111fbb..dfe787e0752 100644 --- a/desktop-next/tests/grammar.spec.ts +++ b/desktop-next/tests/grammar.spec.ts @@ -2,9 +2,7 @@ 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, -}) => { +test("grammar shows all tables and preserves topic links", async ({ page }) => { await page.goto("/design/typography"); await page .getByRole("navigation", { name: "Design system", exact: true }) @@ -13,13 +11,17 @@ test("grammar navigation opens focused topics and preserves deep links", async ( await expect( page.getByRole("heading", { name: "Logic & grammar", exact: true }), ).toBeVisible(); + await expect(page.locator(".grammar-page details")).toHaveCount(0); + await expect(page.locator(".grammar-page table")).toHaveCount(15); + for (const table of await page.locator(".grammar-page table").all()) { + await expect(table).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 }), @@ -30,22 +32,15 @@ test("grammar navigation opens focused topics and preserves deep links", async ( "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 ({ +test("grammar tables fit both densities and themes", async ({ page, }, testInfo) => { await page.goto("/design/grammar"); @@ -53,19 +48,16 @@ test("grammar tables fit both densities and themes with accessible disclosures", 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); } + const audit = await new AxeBuilder({ page }) + .disableRules(["color-contrast"]) + .analyze(); + expect(audit.violations).toEqual([]); } for (const mode of ["light", "dark"]) { if (mode === "dark")