From 1edfde34e964eee160c92fc860339f1791449578 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Thu, 13 Aug 2026 11:57:07 +0200 Subject: [PATCH] refactor(design): one token language, and a gate that fails on slop Solon was speaking Tailwind slate/gray while OrangeCat and FleetCrown speak semantic surfaces. Components now use the same names (text-fg-*, bg-surface-*, border-default). Radii flatten to 8px. Brand navy is a solid surface, not a gradient. design:check is in verify so the next slate class is a red build, not a vibe. --- package.json | 3 +- scripts/design-system-check.js | 78 ++++++ src/app/(dashboard)/dashboard/page.tsx | 52 ++-- .../(dashboard)/dashboard/treasury/page.tsx | 6 +- src/app/(dashboard)/dashboard/voting/page.tsx | 12 +- src/app/(dashboard)/layout.tsx | 14 +- src/app/about/page.tsx | 70 +++--- src/app/ecosystem/page.tsx | 104 +++++--- src/app/features/page.tsx | 64 ++++- src/app/globals.css | 113 ++++----- src/app/governance/audit/page.tsx | 39 ++- src/app/governance/voting/page.tsx | 55 +++-- src/app/integration/page.tsx | 124 ++++++---- src/app/page.tsx | 23 +- src/app/security/page.tsx | 91 ++++--- src/app/treasury/bitcoin/page.tsx | 27 ++- src/components/dashboard/bitcoin-treasury.tsx | 52 ++-- src/components/dashboard/voting-interface.tsx | 223 ++++++++++-------- src/components/marketing/four-pillars.tsx | 95 ++++---- src/components/marketing/solon-hero.tsx | 18 +- src/components/ui/footer.tsx | 73 ++++-- src/components/ui/navigation.tsx | 173 +++++++++----- src/components/ui/page-layout.tsx | 13 +- tailwind.config.js | 85 ++++--- 24 files changed, 1014 insertions(+), 593 deletions(-) create mode 100644 scripts/design-system-check.js diff --git a/package.json b/package.json index 74fcd0b..596bc3b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", - "verify": "npm run lint && npm run typecheck && npm run test", + "design:check": "node scripts/design-system-check.js", + "verify": "npm run lint && npm run typecheck && npm run design:check && npm run test", "test": "vitest run", "test:e2e": "playwright test", "test:puppeteer": "BASE_URL=${BASE_URL:-http://localhost:3000} node tests/puppeteer/smoke.mjs", diff --git a/scripts/design-system-check.js b/scripts/design-system-check.js new file mode 100644 index 0000000..8250e6f --- /dev/null +++ b/scripts/design-system-check.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * Ironclad design gate. Same contract as OrangeCat / FleetCrown: + * tokens live in globals.css; components use semantic Tailwind names; + * no palette utilities, no arbitrary hex, no pillowy radii, no shadows + * standing in for hierarchy. + */ +const fs = require('fs'); +const path = require('path'); + +const ROOT = process.cwd(); +const TARGETS = ['src']; +const EXTENSIONS = new Set(['.ts', '.tsx']); +const SKIP_NAMES = new Set(['opengraph-image.tsx']); + +const FORBIDDEN = [ + { + pattern: /\b(?:text|bg|border|ring|divide)-(?:slate|gray|zinc|neutral)-\d+\b/, + message: 'Use text-fg-primary / text-fg-secondary / bg-surface-* / border-default.', + }, + { + pattern: /(?:bg|text|border)-\[[#']/, + message: 'No arbitrary hex. Define a CSS var in globals.css.', + }, + { + pattern: /\b(?:text|bg|border|ring)-white\/\d+\b/, + message: 'Use on-brand / on-brand-muted tokens.', + }, + { + pattern: /\brounded-(?:xl|2xl|3xl)\b/, + message: 'Radius is 8px (rounded-md) or below.', + }, + { + pattern: /\bshadow-(?:md|lg|xl|2xl|navy|card)\b/, + message: 'Hierarchy is border + type, not drop shadow.', + }, + { + pattern: /\b(?:bg-gradient|linear-gradient)\b/, + message: 'No component gradients. Brand surfaces live in globals.css.', + }, +]; + +function walk(dir, files = []) { + if (!fs.existsSync(dir)) return files; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.next') continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, files); + else if (EXTENSIONS.has(path.extname(entry.name)) && !SKIP_NAMES.has(entry.name)) { + files.push(full); + } + } + return files; +} + +const violations = []; +for (const target of TARGETS) { + for (const file of walk(path.join(ROOT, target))) { + const rel = path.relative(ROOT, file); + const lines = fs.readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, i) => { + for (const rule of FORBIDDEN) { + if (rule.pattern.test(line)) { + violations.push({ file: rel, line: i + 1, message: rule.message, source: line.trim() }); + } + } + }); + } +} + +if (violations.length) { + console.error(`design-system check failed: ${violations.length} violation(s)`); + for (const v of violations.slice(0, 80)) { + console.error(`${v.file}:${v.line} — ${v.message}\n ${v.source}`); + } + process.exit(1); +} +console.log('design-system check: ok'); diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index e986982..8f0d210 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -12,14 +12,21 @@ export const dynamic = "force-dynamic"; */ export default async function DashboardOverview() { let org = null; - let session: { id: string; status: string; proposalTitle: string; outcome: string | null } | null = null; + let session: { + id: string; + status: string; + proposalTitle: string; + outcome: string | null; + } | null = null; let tallyLine: string | null = null; let treasuryLine = "No treasury source registered yet."; let events: { id: string; eventType: string; createdAt: Date }[] = []; let dbError = false; try { - org = await prisma.organization.findFirst({ orderBy: { createdAt: "asc" } }); + org = await prisma.organization.findFirst({ + orderBy: { createdAt: "asc" }, + }); const s = await prisma.votingSession.findFirst({ orderBy: { opensAt: "desc" }, include: { proposal: true }, @@ -57,8 +64,9 @@ export default async function DashboardOverview() { return (

Overview

-

- The governance register is currently unreachable. No live data can be shown. +

+ The governance register is currently unreachable. No live data can be + shown.

); @@ -70,54 +78,60 @@ export default async function DashboardOverview() { {org ? `${org.name} — Overview` : "Overview"} {!org && ( -

- No organization is registered yet. Once one exists, its votes, treasury, and audit - trail appear here. +

+ No organization is registered yet. Once one exists, its votes, + treasury, and audit trail appear here.

)} {org && (
-
+
Latest vote
{session ? ( <> -
{session.proposalTitle}
-
+
+ {session.proposalTitle} +
+
{session.outcome ?? session.status} {tallyLine ? ` · ${tallyLine}` : ""}
) : ( -
No voting session opened yet.
+
+ No voting session opened yet. +
)} -
+
Treasury
-
{treasuryLine}
+
{treasuryLine}
-
+
Recent activity
{events.length === 0 ? ( -
No audit events yet.
+
+ No audit events yet. +
) : ( -