From 91fd4f6df9fb9177e61b5b40517ffec3be63efbe Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Mon, 14 Sep 2026 13:22:38 -0400 Subject: [PATCH 1/8] Add a focused foundation alignment proposal to the design viewer Signed-off-by: Arjun Mahanti --- docs/design-foundation-alignment.md | 49 +++++ scripts/design-system/check-contrast.mjs | 1 + tests/fixtures/design-system/main.tsx | 6 + .../design-system/ui/DesignSystemLayout.tsx | 1 + .../ui/FoundationAlignmentPage.tsx | 139 ++++++++++++++ .../design-system/ui/alignmentRules.tsx | 178 ++++++++++++++++++ .../design-system/ui/foundationAlignment.css | 105 +++++++++++ tests/fixtures/design-system/viewer.spec.ts | 99 ++++++++++ 8 files changed, 578 insertions(+) create mode 100644 docs/design-foundation-alignment.md create mode 100644 tests/fixtures/design-system/ui/FoundationAlignmentPage.tsx create mode 100644 tests/fixtures/design-system/ui/alignmentRules.tsx create mode 100644 tests/fixtures/design-system/ui/foundationAlignment.css diff --git a/docs/design-foundation-alignment.md b/docs/design-foundation-alignment.md new file mode 100644 index 00000000..3783d2e9 --- /dev/null +++ b/docs/design-foundation-alignment.md @@ -0,0 +1,49 @@ +# Foundation alignment proposal + +Status: ready for visual discussion; no shared defaults changed. + +Run `bin/pnpm design:dev` and open +`/tests/fixtures/design-system.html#/design/foundation-alignment`. +The System navigation links to the same page. The page owns the intent mapping +and renders flat color, typography, and spacing tables next to the comparison. + +## Scope + +Start from the shared system integrated by PRs #9 and #24. Compose its Panel, +PanelHeader, Button, and Switch rather than importing the older component port. +Keep Inter, Tabler, the current type weights, and the host's appearance ownership. + +Three independent viewer-only choices test existing vocabulary: + +- Completion text: primary text or green step 12, always with a check and label. +- Reading paragraph: body or body-large. Controls, headings, and metadata keep + their own roles. +- Space between content groups: the section-gap role or twice that distance. + Panel inset and row spacing stay constant. + +Both specimens start with the current tokens. They are synthetic compositions, +not claims about how a particular shipping project page looks. Controls change +only the proposal specimen; the Follow action changes local preview state. +Reloading resets experiment choices. The viewer's existing appearance preference +continues to work independently. + +No new semantic aliases are justified by this fixture alone. After selecting a +treatment, try it on a real product surface and check its repeated uses before +changing shared defaults or proposing a new role. Keep each accepted token family +in a separate draft PR. Coordinate policy edits with PR #51, which was still a +draft when this proposal began; this branch does not include its commits. + +## Reference and adaptation + +The reference is a pinned BlockUI specification snapshot, not a dependency or a +claim that every draft metric is settled: + +- [Type intent](https://github.com/squareup/design-blockinterface/blob/c89319ea3e11a35d58f19ba83f4d976d143e7485/blockUI/docs/type.roles.md) +- [Type resolution](https://github.com/squareup/design-blockinterface/blob/c89319ea3e11a35d58f19ba83f4d976d143e7485/blockUI/docs/type.resolution.draft.json) +- [Layout rhythm](https://github.com/squareup/design-blockinterface/blob/c89319ea3e11a35d58f19ba83f4d976d143e7485/blockUI/Design.md) + +The larger reading role shares the reference's 16px default size but keeps Buzz's +Inter settings. The 32px group gap is a Buzz experiment inspired by separating +larger groups; it is not an exact translation of BlockUI's observed 64px section +gap. Status color exercises an existing Buzz ramp. No proprietary assets, +reference implementation, or full token catalogue are copied. diff --git a/scripts/design-system/check-contrast.mjs b/scripts/design-system/check-contrast.mjs index 6d41337f..25551750 100644 --- a/scripts/design-system/check-contrast.mjs +++ b/scripts/design-system/check-contrast.mjs @@ -80,6 +80,7 @@ const TEXT_ROLES = [ "--text-disabled", "--purple-12", // accent text: links, active nav, chip labels "--red-12", // error text: failed session start, rejected form + "--green-12", // completion text in the foundation alignment proposal ]; /** diff --git a/tests/fixtures/design-system/main.tsx b/tests/fixtures/design-system/main.tsx index 8810e157..b37f2064 100644 --- a/tests/fixtures/design-system/main.tsx +++ b/tests/fixtures/design-system/main.tsx @@ -29,6 +29,7 @@ import { MotionPage } from "./ui/MotionPage"; import { BaseUiPage } from "./ui/BaseUiPage"; import { SystemDocumentPage } from "./ui/SystemDocumentPage"; import { MissingPage } from "./ui/MissingPage"; +import { FoundationAlignmentPage } from "./ui/FoundationAlignmentPage"; // Explicit design-only routes: no import of the app route tree or native startup. // Hash history keeps deep links reloadable on a static file host. @@ -101,6 +102,11 @@ const pages = [ path: "motion", component: MotionPage, }), + createRoute({ + getParentRoute: () => design, + path: "foundation-alignment", + component: FoundationAlignmentPage, + }), createRoute({ getParentRoute: () => design, path: "maintaining", diff --git a/tests/fixtures/design-system/ui/DesignSystemLayout.tsx b/tests/fixtures/design-system/ui/DesignSystemLayout.tsx index f4203f36..2139a152 100644 --- a/tests/fixtures/design-system/ui/DesignSystemLayout.tsx +++ b/tests/fixtures/design-system/ui/DesignSystemLayout.tsx @@ -41,6 +41,7 @@ const SECTIONS: NavSection[] = [ { heading: "System", items: [ + ["Foundation alignment", "/design/foundation-alignment"], ["Maintaining the system", "/design/maintaining"], ["DESIGN.md", "/design/design-guide"], ["AGENTS.md", "/design/agents-guide"], diff --git a/tests/fixtures/design-system/ui/FoundationAlignmentPage.tsx b/tests/fixtures/design-system/ui/FoundationAlignmentPage.tsx new file mode 100644 index 00000000..4a2877f4 --- /dev/null +++ b/tests/fixtures/design-system/ui/FoundationAlignmentPage.tsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { IconCheck } from "@tabler/icons-react"; +import { Button } from "../../../../src/shared/design-system/ui/Button"; +import { Panel } from "../../../../src/shared/design-system/ui/Panel"; +import { PanelHeader } from "../../../../src/shared/design-system/ui/PanelHeader"; +import { Switch } from "../../../../src/shared/design-system/ui/Switch"; +import { PageHeader, Section } from "./primitives"; +import { AlignmentRules } from "./alignmentRules"; +import "./foundationAlignment.css"; + +type Treatment = { + statusColor: boolean; + readingSize: boolean; + sectionSpace: boolean; +}; + +const CURRENT: Treatment = { + statusColor: false, + readingSize: false, + sectionSpace: false, +}; + +/** A viewer composition of shared components; changes are local to this proposal. */ +function ProjectSpecimen({ + label, + treatment, +}: { + label: string; + treatment: Treatment; +}) { + const [following, setFollowing] = useState(false); + return ( +
+

{label}

+ + Launch notes} + /> +
+
+

Project overview

+

+ Bring the release notes, open questions, and next steps together + so everyone can pick up where the team left off. +

+
+
+
Before we share
+
    +
  • + Review the draft + + +
  • +
  • + Resolve open questions + In progress +
  • +
+
+
+ +

+ {following ? "Following in this preview." : "Preview only."} +

+
+
+
+
+ ); +} + +/** Compare a small set of foundation choices before changing shared defaults. */ +export function FoundationAlignmentPage() { + const [treatment, setTreatment] = useState(CURRENT); + return ( + <> + +
+
+ + Proposed choices + + + setTreatment((value) => ({ ...value, statusColor })) + } + /> + + setTreatment((value) => ({ ...value, readingSize })) + } + /> + + setTreatment((value) => ({ ...value, sectionSpace })) + } + /> +
+

+ Both samples start alike. These are viewer compositions, not captures + of a shipping screen. Shared defaults stay unchanged. +

+
+ + +
+
+ + + ); +} diff --git a/tests/fixtures/design-system/ui/alignmentRules.tsx b/tests/fixtures/design-system/ui/alignmentRules.tsx new file mode 100644 index 00000000..ee2c0eec --- /dev/null +++ b/tests/fixtures/design-system/ui/alignmentRules.tsx @@ -0,0 +1,178 @@ +import type { ReactNode } from "react"; +import { Section } from "./primitives"; + +// An intent crosswalk, not another token inventory. Values remain owned by the +// shared styles and registry. Reference metrics are a snapshot, not Buzz defaults. +const RULES = [ + { + title: "Color", + rows: [ + [ + "Reading hierarchy", + "Primary content, supporting copy, metadata", + "text-primary / text-secondary / text-tertiary", + "Already expressed. Keep three levels.", + ], + [ + "Surface hierarchy", + "Choose the surface by its role", + "bg-panel / bg-float; shared Panel", + "Keep Buzz’s light/dark pairs and component ownership.", + ], + [ + "Status", + "Color communicates an actual outcome", + "text-green-12 with a check and “Complete”", + "Try the existing green ramp. No new success alias yet.", + ], + [ + "Boundaries", + "Use separation appropriate to the content", + "border-primary", + "Keep the shared divider; do not import extra border weights.", + ], + ], + }, + { + title: "Typography", + rows: [ + [ + "Screen identity", + "page-title", + "text-title", + "Map intent; retain Buzz’s current metrics.", + ], + [ + "Content group", + "section-title", + "text-heading", + "Keep 16px-equivalent / 600 structure.", + ], + [ + "Reading text", + "body-medium; reference default 16px", + "text-body → text-body-lg in the reading paragraph", + "Compare 14 with 16px-equivalent locally, not a global body change.", + ], + [ + "Control labels", + "label-medium / label-small", + "Shared component type; text-body / text-body-sm in compositions", + "Keep component-owned labels. Add a role only if a repeated need emerges.", + ], + [ + "Supporting detail", + "caption", + "text-body-sm", + "Keep the 12px-equivalent sans floor.", + ], + [ + "Weight", + "Regular / Medium (400 / 500)", + "Content / structure (400 / 600)", + "Retain Buzz’s deliberate weight distinction in this pass.", + ], + [ + "Numbers and micro data", + "Dedicated numeral and monospace roles", + "Existing mono roles where appropriate", + "Defer additional roles until a real product surface needs them.", + ], + ], + }, + { + title: "Spacing", + rows: [ + [ + "Fine spacing", + "8px-based layout rhythm", + "space-1…6: 4 / 8 / 12 / 16 / 20 / 24px-equivalent", + "Keep the fine steps for dense controls and rows.", + ], + [ + "Panel inset", + "Context-dependent container spacing", + "space-panel-inset: 20px-equivalent", + "Keep the existing inset to isolate the section-gap comparison.", + ], + [ + "Content groups", + "Larger gaps distinguish larger groups", + "space-section-gap → twice that gap", + "Try 16 → 32px-equivalent between groups. A Buzz proposal, not an exact BlockUI mapping.", + ], + [ + "Page rhythm", + "Reference observations include 32px title/content and 64px section gaps", + "Choose relationships for the actual Buzz surface", + "Defer page-scale changes; do not apply those distances to every control.", + ], + ], + }, +] as const; + +function RuleTable({ + title, + children, +}: { + title: string; + children: ReactNode; +}) { + return ( +
+ + + + + + + + + + + {children} +
+ {title}: reference intent, Buzz mapping, and first-pass decision +
Use caseBlockUI referenceBuzz mappingFirst pass
+
+ ); +} + +/** Flat, explicit mappings keep proposals distinct from the system's rules. */ +export function AlignmentRules() { + return ( + <> + {RULES.map(({ title, rows }) => ( +
+ + {rows.map(([use, reference, mapping, decision]) => ( + + {use} + {reference} + {mapping} + {decision} + + ))} + +
+ ))} +
+

+ Choose the useful changes, try them on a real product surface, then + promote repeated decisions into shared tokens. This proposal adds no + global tokens, component variants, or theme controls. +

+

+ Reference: BlockUI’s September 2026 specification snapshot. Some + reference metrics remain provisional. The examples use Inter and + Tabler; no proprietary fonts, icons, or implementation are included. +

+
+ + ); +} diff --git a/tests/fixtures/design-system/ui/foundationAlignment.css b/tests/fixtures/design-system/ui/foundationAlignment.css new file mode 100644 index 00000000..a9a5ff0d --- /dev/null +++ b/tests/fixtures/design-system/ui/foundationAlignment.css @@ -0,0 +1,105 @@ +@layer components { + .alignment-options { + display: flex; + flex-direction: column; + gap: var(--space-3); + max-width: 24rem; + border: 0; + padding: 0; + } + + .alignment-options legend { + margin-bottom: var(--space-3); + } + + .alignment-comparison { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; + gap: var(--space-6); + } + + .alignment-specimen, + .alignment-group { + display: flex; + min-width: 0; + flex-direction: column; + gap: var(--space-row-gap); + } + + .alignment-project { + display: flex; + flex-direction: column; + gap: var(--space-section-gap); + padding: var(--space-panel-inset); + } + + /* A viewer-only relationship under evaluation, not a shared density mode. */ + .alignment-project[data-spacing="proposed"] { + gap: calc(var(--space-section-gap) * 2); + } + + .alignment-tasks { + list-style: none; + padding: 0; + margin: 0; + } + + .alignment-tasks li { + display: flex; + align-items: baseline; + flex-wrap: wrap; + justify-content: space-between; + gap: var(--space-row-gap); + padding-block: var(--space-row-gap); + border-bottom: 1px solid var(--border-primary); + } + + .alignment-status { + display: inline-flex; + align-items: center; + gap: var(--space-1); + } + + .alignment-status svg { + flex-shrink: 0; + } + + .alignment-table-wrap { + overflow-x: auto; + } + + .alignment-table { + width: 100%; + border-collapse: collapse; + text-align: left; + } + + .alignment-table :is(th, td) { + min-width: 8rem; + padding: var(--space-3); + vertical-align: top; + border-bottom: 1px solid var(--border-primary); + overflow-wrap: anywhere; + } + + .alignment-table th { + font-weight: 600; + color: var(--text-primary); + } + + .alignment-table td { + color: var(--text-secondary); + } + + html[data-keyboard-navigation] .alignment-table-wrap:focus-visible { + outline: 2px solid var(--purple-8); + outline-offset: 2px; + } + + @media (max-width: 70rem) { + .alignment-comparison { + grid-template-columns: minmax(0, 1fr); + } + } +} diff --git a/tests/fixtures/design-system/viewer.spec.ts b/tests/fixtures/design-system/viewer.spec.ts index 566fa8e4..458dedcd 100644 --- a/tests/fixtures/design-system/viewer.spec.ts +++ b/tests/fixtures/design-system/viewer.spec.ts @@ -34,6 +34,7 @@ test("built viewer loads every specimen and foundation without app connections", "Glass", "Motion", "Base UI backing", + "Foundation alignment", "Maintaining the system", "DESIGN.md", "AGENTS.md", @@ -50,6 +51,104 @@ test("built viewer loads every specimen and foundation without app connections", expect(sockets).toEqual([]); }); +test("foundation proposals are independent, local, and usable in both modes", async ({ + page, +}) => { + await page.goto(`${viewer}#/design/foundation-alignment`); + const current = page.getByRole("region", { + name: "Current tokens", + exact: true, + }); + const proposal = page.getByRole("region", { + name: "Selected proposal", + exact: true, + }); + const color = page.getByRole("switch", { name: "Status color" }); + const reading = page.getByRole("switch", { name: "Larger reading text" }); + const spacing = page.getByRole("switch", { name: "More section space" }); + + for (const mode of ["light", "dark"]) { + const theme = page.getByRole("button", { name: `Use ${mode} mode` }); + if (await theme.count()) await theme.click(); + await expect(proposal.locator("[data-reading]")).toHaveCSS( + "font-size", + "14px", + ); + await expect(proposal.locator(".alignment-project")).toHaveCSS( + "row-gap", + "16px", + ); + const neutral = await current + .locator("[data-status]") + .evaluate((el) => getComputedStyle(el).color); + await expect(proposal.locator("[data-status]")).toHaveCSS("color", neutral); + + await color.click(); + await expect(proposal.locator("[data-status]")).not.toHaveCSS( + "color", + neutral, + ); + await expect(proposal.locator("[data-reading]")).toHaveCSS( + "font-size", + "14px", + ); + await expect(proposal.locator(".alignment-project")).toHaveCSS( + "row-gap", + "16px", + ); + await reading.focus(); + await page.keyboard.press("Space"); + await expect(reading).toBeChecked(); + await expect(proposal.locator("[data-reading]")).toHaveCSS( + "font-size", + "16px", + ); + await spacing.click(); + await expect(proposal.locator(".alignment-project")).toHaveCSS( + "row-gap", + "32px", + ); + await expect(current.locator("[data-reading]")).toHaveCSS( + "font-size", + "14px", + ); + await expect(current.locator(".alignment-project")).toHaveCSS( + "row-gap", + "16px", + ); + await expect(current.locator("[data-status]")).toHaveCSS("color", neutral); + + for (const width of [390, 800, 1280]) { + await page.setViewportSize({ width, height: 900 }); + await expect(page.getByRole("table")).toHaveCount(3); + await expect(page.getByRole("table").first()).toBeVisible(); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= window.innerWidth, + ), + ).toBe(true); + } + await color.click(); + await reading.click(); + await spacing.focus(); + await page.keyboard.press("Space"); + await expect(spacing).not.toBeChecked(); + } + await proposal + .getByRole("button", { name: "Follow project", exact: true }) + .click(); + await expect( + proposal.getByRole("button", { name: "Following project" }), + ).toHaveAttribute("aria-pressed", "true"); + await expect( + current.getByRole("button", { name: "Follow project", exact: true }), + ).toHaveAttribute("aria-pressed", "false"); + await page.reload(); + await expect(color).not.toBeChecked(); + await expect(reading).not.toBeChecked(); + await expect(spacing).not.toBeChecked(); +}); + test("narrow, intermediate and wide layouts preserve theme and keyboard interaction", async ({ page, browserName, From 824e1c17bf74bcb1be25ff9e8c999a95212c8cfc Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Mon, 14 Sep 2026 14:41:05 -0400 Subject: [PATCH 2/8] Checkpoint shared design foundations and app styling before PR split Signed-off-by: Arjun Mahanti --- package.json | 2 +- .../design-system/check-app-foundations.mjs | 82 +++++ scripts/design-system/check-color.mjs | 4 + scripts/design-system/check-contrast.mjs | 4 + scripts/design-system/check-type.mjs | 9 +- src/app/AppearanceSettings.tsx | 15 +- src/app/NotificationSettings.tsx | 17 +- src/app/PluginImport.tsx | 22 +- src/app/ProfileSettings.tsx | 8 +- src/app/Settings.module.css | 62 ++++ src/app/Settings.tsx | 80 ++--- src/app/shell/AppShell.tsx | 5 +- src/app/shell/Home.tsx | 21 +- src/app/shell/PageSearch.tsx | 4 +- src/app/shell/PanelLaunchers.tsx | 2 +- src/app/shell/ProfileButton.tsx | 4 +- src/bundled/agents/AgentsPage.tsx | 6 +- src/bundled/bestie/index.tsx | 4 +- src/bundled/channels/Channels.module.css | 149 +++++---- src/bundled/emoji/Emoji.module.css | 101 +++--- src/bundled/emoji/emoji-mart.ts | 79 +++-- src/bundled/github/GitHub.module.css | 43 +-- src/bundled/mentions/MentionCompletion.tsx | 2 +- src/bundled/mentions/MentionPicker.tsx | 2 +- src/bundled/mentions/Mentions.module.css | 34 +- src/bundled/profiles/Profiles.module.css | 12 +- src/bundled/projects/index.tsx | 2 +- src/bundled/terminal/TerminalPanel.tsx | 2 +- src/bundled/workflows/workflows.css | 44 +-- src/bundled/workflows/workflows.journey.mjs | 4 +- .../communities/Communities.module.css | 90 +++--- src/features/communities/ProfileFields.tsx | 4 +- .../conversation/Completions.module.css | 37 ++- src/features/messages/Messages.module.css | 305 ++++++++++-------- src/features/messages/ThreadPanel.test.tsx | 20 +- src/features/messages/ThreadPanel.tsx | 26 +- src/features/panels/PanelCard.tsx | 24 +- src/features/panels/Panels.module.css | 32 +- src/shared/Avatar.tsx | 2 +- src/shared/design-system/DESIGN.md | 22 ++ .../design-system/styles/components.css | 26 +- src/shared/design-system/styles/tokens.css | 90 +++--- .../design-system/styles/typography.css | 237 +++----------- src/shared/design-system/tokens/registry.ts | 174 +++++----- src/shared/design-system/ui/PanelHeader.tsx | 2 +- src/shared/styles/globals.css | 150 +++++---- src/shared/styles/tokens.css | 155 +++++---- src/shared/theme/tokens.test.ts | 42 ++- tests/browser/appearance.spec.mjs | 51 ++- tests/browser/emoji.spec.mjs | 36 +-- tests/browser/gifs.spec.mjs | 33 +- tests/browser/mentions.spec.mjs | 4 +- 52 files changed, 1313 insertions(+), 1074 deletions(-) create mode 100644 scripts/design-system/check-app-foundations.mjs create mode 100644 src/app/Settings.module.css diff --git a/package.json b/package.json index 91eff3fb..20d0256a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "design:build": "pnpm design:typecheck && vite build --config vite.design.config.ts", "design:preview": "vite preview --config vite.design.config.ts", "design:typecheck": "tsc -p tsconfig.design.json", - "design:check": "node scripts/design-system/check-type.mjs && node scripts/design-system/check-color.mjs && node scripts/design-system/check-contrast.mjs", + "design:check": "node scripts/design-system/check-type.mjs && node scripts/design-system/check-color.mjs && node scripts/design-system/check-contrast.mjs && node scripts/design-system/check-app-foundations.mjs", "design:census": "node scripts/design-system/token-consumers.mjs", "design:test": "vitest run --config vitest.design.config.ts", "design:test:browser": "pnpm build && pnpm design:build && playwright test --config tests/fixtures/design-system/playwright.config.ts" diff --git a/scripts/design-system/check-app-foundations.mjs b/scripts/design-system/check-app-foundations.mjs new file mode 100644 index 00000000..565883ad --- /dev/null +++ b/scripts/design-system/check-app-foundations.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node +/** Keep application UI on the shared foundations, including CSS inside adapters. + * Layout dimensions, image geometry and terminal ANSI/artwork are not UI tokens. + * The existing type/color guards separately cover the shared system and viewer. + */ +import { readdirSync, readFileSync } from "node:fs"; +import { join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../../src", import.meta.url)); +const rules = [ + ["literal color", /#[\da-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla)\(/gi], + ["custom text size", /font-size\s*:(?!\s*(?:var\(|inherit\b))\s*[^;\n]+/g], + ["custom font weight", /font-weight\s*:\s*\d+/g], + [ + "custom font family", + /font-family\s*:(?!\s*(?:var\(|inherit\b))\s*[^;\n]+/g, + ], + [ + "stock palette", + /\b(?:bg|text|border|ring|outline)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|[1-9]00)\b/g, + ], + ["mixed surface color", /\b(?:bg|text|border)-[\w-]+\/\d+\b/g], + [ + "legacy text utility", + /(? { + const path = join(dir, entry.name); + if (path.includes("/shared/design-system")) return []; + if (entry.isDirectory()) return files(path); + if (!/\.(?:css|tsx|ts)$/.test(path) || /(?:\.test\.|fixture)/.test(path)) + return []; + return [path]; + }); +} + +const failures = []; +for (const path of files(root)) { + // Ignore comments without changing reported line numbers. Plain TS is included + // because Emoji Mart's shadow-root stylesheet lives in its adapter module. + const source = readFileSync(path, "utf8").replace( + /\/\*[\s\S]*?\*\//g, + (comment) => comment.replace(/[^\n]/g, " "), + ); + for (const [rule, pattern] of rules) { + for (const match of source.matchAll(pattern)) { + // Non-CSS strings can contain event IDs, channel hashtags or selector IDs. + if ( + rule === "literal color" && + !path.endsWith(".css") && + !/[:[]\s*$/.test( + source.slice(Math.max(0, match.index - 4), match.index), + ) + ) + continue; + const line = source.slice(0, match.index).split("\n").length; + failures.push( + `${relative(root, path)}:${line}: ${rule}: ${match[0].trim()}`, + ); + } + } +} +if (failures.length) { + console.error(failures.join("\n")); + process.exitCode = 1; +} else { + console.log( + "✓ App foundations: no private colors, text sizes, spacing or corners", + ); +} diff --git a/scripts/design-system/check-color.mjs b/scripts/design-system/check-color.mjs index a869a30e..cb483fd2 100644 --- a/scripts/design-system/check-color.mjs +++ b/scripts/design-system/check-color.mjs @@ -341,6 +341,10 @@ function auditLayers() { // What this still catches is the mistake it was written for: a role invented // by symmetry, restating one step, that no design asked for. const NAME_IS_EARNED = new Map([ + [ + "--border-control", + "An input boundary must clear 3:1 against its surface, unlike a decorative divider.", + ], ["--text-primary", "Three text levels, enforced by name."], ["--text-secondary", "Three text levels, enforced by name."], ["--text-tertiary", "Three text levels, enforced by name."], diff --git a/scripts/design-system/check-contrast.mjs b/scripts/design-system/check-contrast.mjs index 25551750..abf3ff18 100644 --- a/scripts/design-system/check-contrast.mjs +++ b/scripts/design-system/check-contrast.mjs @@ -80,6 +80,7 @@ const TEXT_ROLES = [ "--text-disabled", "--purple-12", // accent text: links, active nav, chip labels "--red-12", // error text: failed session start, rejected form + "--amber-12", // warning text in delivery notices and dialogs "--green-12", // completion text in the foundation alignment proposal ]; @@ -99,6 +100,7 @@ const PAIRS = [ // now that the roles are gone. Still measured as a pair, because the text // follows the fill: move the fill and this has to be re-measured. ["--neutral-1", "--neutral-11"], + ["--neutral-1", "--neutral-12"], ]; /** @@ -109,6 +111,8 @@ const PAIRS = [ * cursor — and the hover is the harder one, which is where the gap was. */ const TINT_PAIRS = [ + ["--amber-12", "--amber-3"], + ["--green-12", "--green-3"], ["--purple-12", "--purple-3"], ["--purple-12", "--purple-4"], ]; diff --git a/scripts/design-system/check-type.mjs b/scripts/design-system/check-type.mjs index 161e6a47..e2525c04 100644 --- a/scripts/design-system/check-type.mjs +++ b/scripts/design-system/check-type.mjs @@ -39,6 +39,9 @@ const VIEWER = fileURLToPath( /** Size roles a component may use. Kept in sync with typography.css. */ const SIZE_ROLES = [ + "label", + "label-sm", + "caption", "display", "title", "heading", @@ -58,8 +61,6 @@ const SIZE_ROLES = [ */ const RETIRED_ROLES = new Map([ ["subheading", "text-heading, or text-body-lg if it is prose"], - ["label", "text-body, or text-body-sm in dense chrome"], - ["caption", "text-body-sm"], ["meta", "text-body-sm"], ["code", "text-mono"], ]); @@ -132,9 +133,9 @@ const RULES = [ // // `font-semibold` and `font-normal` are absent from this list on purpose: // they are the two legal weights. - pattern: /\bfont-(?:thin|extralight|light|medium|bold|extrabold|black)\b/g, + pattern: /\bfont-(?:thin|extralight|light|bold|extrabold|black)\b/g, message: - "off-ramp font weight — the system is 400 and 600. Bold is font-semibold. If a one-off genuinely needs another weight, add it to OVERRIDES with a reason.", + "off-ramp font weight — the system is 400 and 500. Emphasis is font-medium; legacy font-semibold resolves to 500. If a one-off genuinely needs another weight, add it to OVERRIDES with a reason.", }, { id: "retired-role", diff --git a/src/app/AppearanceSettings.tsx b/src/app/AppearanceSettings.tsx index 5490fe54..8d7032b7 100644 --- a/src/app/AppearanceSettings.tsx +++ b/src/app/AppearanceSettings.tsx @@ -10,21 +10,18 @@ export function AppearanceSettings({ appearance }: { appearance: Appearance }) { ); return (
-

+

Appearance

-
+
- Color mode + Color mode

Choose how Buzz looks on this device. Your choice is saved automatically. @@ -54,8 +51,8 @@ export function AppearanceSettings({ appearance }: { appearance: Appearance }) {

- Text size -

+ Text size +

Resize text without zooming the window. Saved on this device.

diff --git a/src/app/NotificationSettings.tsx b/src/app/NotificationSettings.tsx index 6ff66cea..706f6729 100644 --- a/src/app/NotificationSettings.tsx +++ b/src/app/NotificationSettings.tsx @@ -13,14 +13,11 @@ export function NotificationSettings({ const { preferences, permission } = state; return (
-

+

Notifications

-
-

+

+

Choices are saved for this account on this device. System permission is separate.

@@ -29,7 +26,7 @@ export function NotificationSettings({ checked={preferences.enabled} onChange={(enabled) => notifications.updatePreferences({ enabled })} /> -

+

{state.requesting ? "Waiting for system permission…" : permission === "granted" @@ -70,7 +67,7 @@ export function NotificationSettings({ } /> {state.systemManaged ? ( -

+

Manage sound and permission in system notification settings. Desktop clicks bring Buzz forward and open the message or thread while Buzz is running. @@ -82,7 +79,7 @@ export function NotificationSettings({ checked={preferences.sound} onChange={(sound) => notifications.updatePreferences({ sound })} /> -

+

Sound uses the system default where supported. Turning it off keeps alerts enabled.

@@ -103,7 +100,7 @@ export function NotificationSettings({ /> ))}
-

+

Message alerts cover the selected community while Buzz is running. Reading history and reconnecting stay quiet.

diff --git a/src/app/PluginImport.tsx b/src/app/PluginImport.tsx index eb8e7955..7c52fc74 100644 --- a/src/app/PluginImport.tsx +++ b/src/app/PluginImport.tsx @@ -34,7 +34,7 @@ export function PluginImport({ if (!imports) return ( -

+

Open the desktop app to load plugins from a folder or Git repository.

); @@ -109,7 +109,7 @@ export function PluginImport({ void load(() => imports.git(repository, reference)); }} > -