Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/shell-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
82 changes: 82 additions & 0 deletions scripts/design-system/check-app-foundations.mjs
Original file line number Diff line number Diff line change
@@ -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",
/(?<![\w-])text-(?:xs|sm|base|lg|xl|[2-9]xl)\b|\btext-\[[^\]]+\]/g,
],
[
"custom spacing",
/(?:^|[;{\n])\s*(?:padding|margin|gap|row-gap|column-gap)(?:-[a-z-]+)?\s*:[^;{}]*\b\d*\.?\d+(?:px|rem|em)\b/g,
],
[
"custom corner",
/border-(?:radius|(?:top|bottom)-(?:left|right)-radius)\s*:\s*[1-9][\d.]*(?:px|rem)/g,
],
];

function files(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
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",
);
}
4 changes: 4 additions & 0 deletions scripts/design-system/check-color.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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."],
Expand Down
5 changes: 5 additions & 0 deletions scripts/design-system/check-contrast.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
];

/**
Expand All @@ -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"],
];

/**
Expand All @@ -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"],
];
Expand Down
9 changes: 5 additions & 4 deletions scripts/design-system/check-type.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"],
]);
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 6 additions & 9 deletions src/app/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,18 @@ export function AppearanceSettings({ appearance }: { appearance: Appearance }) {
);
return (
<section aria-labelledby="appearance-settings-title">
<h2
id="appearance-settings-title"
className="mt-0 mb-3 text-lg font-medium"
>
<h2 id="appearance-settings-title" className="mt-0 mb-6 text-label">
Appearance
</h2>
<div className="ui-card p-5 sm:p-6">
<div>
<fieldset
className="m-0 min-w-0 border-0 p-0"
aria-describedby="appearance-description"
>
<legend className="mb-2 text-base font-medium">Color mode</legend>
<legend className="mb-2 text-label">Color mode</legend>
<p
id="appearance-description"
className="mt-0 mb-5 text-sm text-muted"
className="mt-0 mb-5 text-body-sm text-muted"
>
Choose how Buzz looks on this device. Your choice is saved
automatically.
Expand Down Expand Up @@ -54,8 +51,8 @@ export function AppearanceSettings({ appearance }: { appearance: Appearance }) {
</div>
</fieldset>
<fieldset className="mt-6 min-w-0 border-0 p-0">
<legend className="mb-2 text-base font-medium">Text size</legend>
<p className="mt-0 mb-3 text-sm text-muted">
<legend className="mb-2 text-label">Text size</legend>
<p className="mt-0 mb-3 text-body-sm text-muted">
Resize text without zooming the window. Saved on this device.
</p>
<div className="flex flex-wrap items-center gap-3">
Expand Down
19 changes: 8 additions & 11 deletions src/app/DeveloperSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,21 +55,18 @@ export function DeveloperSettings({ relay }: { relay: RelayData }) {

return (
<section aria-labelledby="developer-settings-title">
<h2
id="developer-settings-title"
className="mt-0 mb-3 text-lg font-medium"
>
<h2 id="developer-settings-title" className="mt-0 mb-6 text-label">
Developer
</h2>
<div className="ui-card space-y-5 p-5 sm:p-6">
<p className="text-sm text-muted">
<p className="text-body-sm text-muted">
Diagnostics for local development. This tab only appears when the app
is served from localhost in a development build.
</p>
<div className="space-y-2">
<h3 className="m-0 text-sm font-medium">Relay broker stats</h3>
<h3 className="m-0 text-label-sm">Relay broker stats</h3>
{stats ? (
<dl className="m-0 grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-4">
<dl className="m-0 grid grid-cols-2 gap-x-6 gap-y-1 text-body-sm sm:grid-cols-4">
<div>
<dt className="text-muted">Queries</dt>
<dd className="m-0 tabular-nums">{stats.queries}</dd>
Expand All @@ -88,15 +85,15 @@ export function DeveloperSettings({ relay }: { relay: RelayData }) {
</div>
</dl>
) : (
<p role="status" className="m-0 text-sm text-muted">
<p role="status" className="m-0 text-body-sm text-muted">
Broker stats are unavailable. They exist only when the dev relay
broker is running on this origin.
</p>
)}
</div>
<div className="space-y-2">
<h3 className="m-0 text-sm font-medium">Caches</h3>
<p className="m-0 text-sm text-muted">
<h3 className="m-0 text-label-sm">Caches</h3>
<p className="m-0 text-body-sm text-muted">
Clears cached channels, messages, and media. Account, relay, and
sidebar settings are kept.
</p>
Expand All @@ -108,7 +105,7 @@ export function DeveloperSettings({ relay }: { relay: RelayData }) {
{clearing ? "Clearing…" : "Clear cache"}
</button>
{status && (
<p role="status" className="m-0 text-sm text-muted">
<p role="status" className="m-0 text-body-sm text-muted">
{status}
</p>
)}
Expand Down
17 changes: 7 additions & 10 deletions src/app/NotificationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@ export function NotificationSettings({
const { preferences, permission } = state;
return (
<section aria-labelledby="notification-settings-title">
<h2
id="notification-settings-title"
className="mt-0 mb-3 text-lg font-medium"
>
<h2 id="notification-settings-title" className="mt-0 mb-6 text-label">
Notifications
</h2>
<div className="ui-card space-y-5 p-5 sm:p-6">
<p className="text-sm text-muted">
<div className="space-y-5">
<p className="text-body-sm text-muted">
Choices are saved for this account on this device. System permission
is separate.
</p>
Expand All @@ -29,7 +26,7 @@ export function NotificationSettings({
checked={preferences.enabled}
onChange={(enabled) => notifications.updatePreferences({ enabled })}
/>
<p role="status" className="text-sm text-muted">
<p role="status" className="text-body-sm text-muted">
{state.requesting
? "Waiting for system permission…"
: permission === "granted"
Expand Down Expand Up @@ -70,7 +67,7 @@ export function NotificationSettings({
}
/>
{state.systemManaged ? (
<p className="text-sm text-muted">
<p className="text-body-sm text-muted">
Manage sound and permission in system notification settings. Desktop
clicks bring Buzz forward and open the message or thread while Buzz
is running.
Expand All @@ -82,7 +79,7 @@ export function NotificationSettings({
checked={preferences.sound}
onChange={(sound) => notifications.updatePreferences({ sound })}
/>
<p className="text-sm text-muted">
<p className="text-body-sm text-muted">
Sound uses the system default where supported. Turning it off
keeps alerts enabled.
</p>
Expand All @@ -103,7 +100,7 @@ export function NotificationSettings({
/>
))}
</fieldset>
<p className="text-sm text-muted">
<p className="text-body-sm text-muted">
Message alerts cover the selected community while Buzz is running.
Reading history and reconnecting stay quiet.
</p>
Expand Down
Loading
Loading