diff --git a/docs/shell-design.md b/docs/shell-design.md index 098bbf88..1d074418 100644 --- a/docs/shell-design.md +++ b/docs/shell-design.md @@ -38,6 +38,9 @@ semantic tokens, UI authoring rules and the local component reference. - `Settings.tsx` presents Profile, Plugins and Appearance as selectable sections in a left sidebar, opening on Profile. When the content area is narrow (including beside a companion panel), the section buttons form a compact row above the content. + Navigation and details scroll together inside the solid container at narrow + widths, so wrapped navigation cannot consume the detail pane's height. Wide + layouts keep independently scrolling navigation and details. Native buttons use normal Tab/Enter navigation and expose the current section. `ProfileSettings.tsx` edits the local default inline with Save and Cancel, sharing fields and validation with community setup. Cancel restores the saved 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 6d41337f..abf3ff18 100644 --- a/scripts/design-system/check-contrast.mjs +++ b/scripts/design-system/check-contrast.mjs @@ -80,6 +80,8 @@ 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 ]; /** @@ -98,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"], ]; /** @@ -108,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/DeveloperSettings.tsx b/src/app/DeveloperSettings.tsx index 8f5d34f5..2e9c66d4 100644 --- a/src/app/DeveloperSettings.tsx +++ b/src/app/DeveloperSettings.tsx @@ -55,21 +55,18 @@ export function DeveloperSettings({ relay }: { relay: RelayData }) { return (
-

+

Developer

-

+

Diagnostics for local development. This tab only appears when the app is served from localhost in a development build.

-

Relay broker stats

+

Relay broker stats

{stats ? ( -
+
Queries
{stats.queries}
@@ -88,15 +85,15 @@ export function DeveloperSettings({ relay }: { relay: RelayData }) {
) : ( -

+

Broker stats are unavailable. They exist only when the dev relay broker is running on this origin.

)}
-

Caches

-

+

Caches

+

Clears cached channels, messages, and media. Account, relay, and sidebar settings are kept.

@@ -108,7 +105,7 @@ export function DeveloperSettings({ relay }: { relay: RelayData }) { {clearing ? "Clearing…" : "Clear cache"} {status && ( -

+

{status}

)} 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)); }} > -