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.
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 (
- The governance register is currently unreachable. No live data can be shown.
+
+ The governance register is currently unreachable. No live data can be
+ shown.
Overview
-
- 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 && (+
{dbError
? "The treasury register is currently unreachable. No balance can be shown."
: "No organization is registered yet, so there is no treasury to show."}
diff --git a/src/app/(dashboard)/dashboard/voting/page.tsx b/src/app/(dashboard)/dashboard/voting/page.tsx
index cdcabc3..ab01487 100644
--- a/src/app/(dashboard)/dashboard/voting/page.tsx
+++ b/src/app/(dashboard)/dashboard/voting/page.tsx
@@ -20,8 +20,9 @@ export default async function VotingPage() {
return (
- The voting register is currently unreachable. No session data can be shown.
+
+ The voting register is currently unreachable. No session data can be
+ shown.
- No voting session has been opened yet. When one opens, registered members vote
- here by signing the canonical vote message with their own Bitcoin wallet.
+
+ No voting session has been opened yet. When one opens, registered
+ members vote here by signing the canonical vote message with their own
+ Bitcoin wallet.
Voting
- Voting
-
- Most governance runs on trust: trust the treasurer, trust the minutes, trust that the - vote was counted. Solon replaces that trust with verification. Votes are Bitcoin - signed messages anyone can re-check. Decisions are published as self-verifying - documents. The audit trail is append-only. The treasury is watch-only — Solon never - holds keys or funds. + Most governance runs on trust: trust the treasurer, trust the + minutes, trust that the vote was counted. Solon replaces that + trust with verification. Votes are Bitcoin signed messages anyone + can re-check. Decisions are published as self-verifying documents. + The audit trail is append-only. The treasury is watch-only — Solon + never holds keys or funds.
- Solon is the governance pillar of a three-product stack, and it practices what it - ships: its first production organization governs the stack itself, with AI agents - from the sibling products registered as voting members and humans holding the red - lines. + Solon is the governance pillar of a three-product stack, and it + practices what it ships: its first production organization governs + the stack itself, with AI agents from the sibling products + registered as voting members and humans holding the red lines.
- Solon is operated as part of the OrangeCat stack and governed on its own rails. The - current voting members — including the sibling products' agents, The Cat and Loki — - and every policy they govern are public on the ecosystem page. +
+ Solon is operated as part of the OrangeCat stack and governed on its + own rails. The current voting members — including the sibling + products' agents, The Cat and Loki — and every policy they + govern are public on the ecosystem page.
-- The live governed state — members, policies, decisions — is public. So is the code. +
+ The live governed state — members, policies, decisions — is + public. So is the code.
{description}
+{description}
{pillar.description}
{pillar.tie}
- Red lines: {humansOnlyCategories.map((c) => CATEGORY_LABEL[c].toLowerCase()).join(", ")}{" "} - are decided by humans only. Agents propose anywhere, but can never vote to expand their - own suffrage. +
+ Red lines:{" "} + {humansOnlyCategories + .map((c) => CATEGORY_LABEL[c].toLowerCase()) + .join(", ")}{" "} + are decided by humans only. Agents propose anywhere, but can never + vote to expand their own suffrage.
@@ -193,20 +205,22 @@ export default async function EcosystemPage() { What is governed here, live {dbError && ( -- The governance register is currently unreachable, so no live state can be shown. +
+ The governance register is currently unreachable, so no live state + can be shown.
)} {!dbError && !org && ( -- No organization is registered yet — there is nothing governed to show. +
+ No organization is registered yet — there is nothing governed to + show.
)} {org && ( <> -- Everything below is read from the same database the public API serves — nothing is - staged. +
+ Everything below is read from the same database the public API + serves — nothing is staged.
No active policies.
++ No active policies. +
) : (
+
{JSON.stringify(p.content, null, 2)}
No proposals yet.
+No proposals yet.
) : ({description}
+{description}
+
The audit register is currently unreachable. No events can be shown.
)} {!dbError && !org && ( -- No organization is registered yet, so there is no audit trail to show. +
+ No organization is registered yet, so there is no audit trail to + show.
)} {org && ( <> -- {events.length} most recent events for {org.name}. - Audit events are append-only: no code path updates or deletes them. +
+ {events.length} most recent events for{" "} + {org.name}. Audit events + are append-only: no code path updates or deletes them.
+{JSON.stringify(e.payload, null, 2)}diff --git a/src/app/governance/voting/page.tsx b/src/app/governance/voting/page.tsx index 4e1ae26..3fa0a8d 100644 --- a/src/app/governance/voting/page.tsx +++ b/src/app/governance/voting/page.tsx @@ -1,4 +1,4 @@ -import PageLayout from '@/components/ui/page-layout'; +import PageLayout from "@/components/ui/page-layout"; /** * Explains the voting mechanism honestly. Live sessions and tallies render in @@ -11,44 +11,53 @@ export default function VotingSystemPage() { description="Votes are Bitcoin signed messages — verified cryptographically, not trusted" >--How a vote works
-+
+-+ How a vote works +
+
- - A voting session opens on a proposal. Its rules — who is eligible, the - threshold, the quorum — are fixed when it opens. + A voting session opens on a proposal. Its rules — who is eligible, + the threshold, the quorum — are fixed when it opens.
- - A member signs the canonical vote message (session id, choice, and their own - address) with their Bitcoin wallet — Sparrow, Electrum, or Bitcoin - Core's
signmessage. + A member signs the canonical vote message (session id, choice, and + their own address) with their Bitcoin wallet — Sparrow, Electrum, + or Bitcoin Core's{" "} +signmessage.- - The server recovers the public key from the signature and checks it resolves - to a registered member. No signature, no vote — there is no other way in. + The server recovers the public key from the signature and checks + it resolves to a registered member. No signature, no vote — there + is no other way in.
- - The weighted tally is computed from stored, verified votes only, and every - vote's signature stays on record so anyone can re-verify it. + The weighted tally is computed from stored, verified votes only, + and every vote's signature stays on record so anyone can + re-verify it.
-What this gives you
-+
+diff --git a/src/app/integration/page.tsx b/src/app/integration/page.tsx index cbb6899..07e8c5e 100644 --- a/src/app/integration/page.tsx +++ b/src/app/integration/page.tsx @@ -1,4 +1,4 @@ -import PageLayout from '@/components/ui/page-layout'; +import PageLayout from "@/components/ui/page-layout"; /** * Documents the API that exists today — nothing aspirational. The surface is @@ -12,114 +12,150 @@ export default function IntegrationPage() { description="The current public API — every endpoint listed here is live" >+ What this gives you +
+
- - Cryptographic verification — a vote is - valid because the math says so, not because an administrator does. + Cryptographic verification{" "} + — a vote is valid because the math says so, not because an + administrator does.
- - Replay protection — the signed text - binds session, choice, and voter, so a signature cannot be lifted onto - another vote. + Replay protection — the + signed text binds session, choice, and voter, so a signature + cannot be lifted onto another vote.
- - Weighted voting — members carry a - voting weight; the tally is weighted accordingly and the weights are public. + Weighted voting — members + carry a voting weight; the tally is weighted accordingly and the + weights are public.
--Cast a cryptographic vote
-- A vote is a Bitcoin signed message. Sign the canonical vote message with the - wallet that holds your registered member address, then POST the signature: +
++ Cast a cryptographic vote +
++ A vote is a Bitcoin signed message. Sign the canonical vote message + with the wallet that holds your registered member address, then POST + the signature:
---# Message to sign (exact text):++-+ # Message to sign (exact text): +Solon votesession:<sessionId>choice:<yes|no|abstain>voter:<your-bitcoin-address>
-# Submit the signed vote-curl -X POST /api/sessions/<sessionId>/votes \--H "Content-Type: application/json" \+# Submit the signed vote++ curl -X POST /api/sessions/<sessionId>/votes \ +++ -H "Content-Type: application/json" \ +- -d '{'{'}"choice":"yes","address":"1...","signature":"<base64>"{'}'}' + -d '{"{"} + "choice":"yes","address":"1...","signature":"<base64>" + {"}"}'- The server recovers the public key from the signature and only stores the vote - if it resolves to a registered member. Invalid signatures return 401; verified - but ineligible votes return 422 — with the reason in both cases. +
+ The server recovers the public key from the signature and only + stores the vote if it resolves to a registered member. Invalid + signatures return 401; verified but ineligible votes return 422 — + with the reason in both cases.
+-Live endpoints
-
- -
+POST /api/proposals- file a signed proposal + + file a signed proposal +- -
+POST /api/proposals/[proposalId]/open- open the voting session + + open the voting session +- -
+GET /api/sessions/[sessionId]- session, snapshotted rules, live tally + + session, snapshotted rules, live tally +- -
+POST /api/sessions/[sessionId]/votes- cast a signed vote + + cast a signed vote +- -
+POST /api/sessions/[sessionId]/close- close after the window and decide the outcome + + close after the window and decide the outcome +- -
+GET /api/orgs/[slug]- organization + public member roster + + organization + public member roster +- -
+GET /api/orgs/[slug]/proposals- all proposals with session state + + all proposals with session state +- -
+GET /api/orgs/[slug]/policies/[key]- policy version history + + policy version history +- -
+GET /api/orgs/[slug]/audit- append-only audit stream + + append-only audit stream +- -
+GET /api/orgs/[slug]/treasury- live on-chain treasury balances + + live on-chain treasury balances +- -
+GET /api/v1/decisions/[sessionId]- - self-verifying decision document — re-verify it, don't trust it + + self-verifying decision document — re-verify it, don't + trust it- -
+GET /api/health- service health + + service health ++
All reads are public and auth-free — transparency is the product. On - finalization Solon emits a
decision.finalized{" "} + finalization Solon emits a{" "} +decision.finalized{" "} webhook (HMAC-signed); consumers are expected to fetch the decision document and re-verify every signature locally rather than trust the notification. diff --git a/src/app/page.tsx b/src/app/page.tsx index 65d3de9..2b8ac6a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,12 @@ -import SolonHero from '@/components/marketing/solon-hero' -import { FourPillars } from '@/components/marketing/four-pillars' -import en from '@/i18n/en.json' +import SolonHero from "@/components/marketing/solon-hero"; +import { FourPillars } from "@/components/marketing/four-pillars"; +import en from "@/i18n/en.json"; export default function Home() { - const t = en.home + const t = en.home; return (- {/* Hero — navy anchor with live treasury ledger + icon pillars */}@@ -27,24 +26,22 @@ export default function Home() { {/* Call to Action — navy anchor closes the page */} -@@ -19,7 +18,7 @@ export default function Home() { {t.pillars_section.title}
-+
{t.pillars_section.subtitle}
+ - ) + ); } diff --git a/src/app/security/page.tsx b/src/app/security/page.tsx index b761210..fb05434 100644 --- a/src/app/security/page.tsx +++ b/src/app/security/page.tsx @@ -1,4 +1,4 @@ -import PageLayout from '@/components/ui/page-layout'; +import PageLayout from "@/components/ui/page-layout"; /** * The real security model, stated plainly. Every claim on this page maps to @@ -18,79 +18,86 @@ export default function SecurityPage() { title="No key custody, ever" description="Solon never holds a private key. There is nothing to steal from Solon that lets an attacker vote or move funds." details={[ - 'Members register a Bitcoin address; the key stays in their own wallet or environment', - 'Agent members (the Cat, Loki) sign on their own machines — Solon only ever sees signatures', - 'The treasury is watch-only: independently verifiable on-chain addresses, no spending capability', + "Members register a Bitcoin address; the key stays in their own wallet or environment", + "Agent members (the Cat, Loki) sign on their own machines — Solon only ever sees signatures", + "The treasury is watch-only: independently verifiable on-chain addresses, no spending capability", ]} />{t.cta.title}
-- {t.cta.subtitle} -
+{t.cta.subtitle}
{t.cta.primary} {t.cta.secondary} @@ -52,5 +49,5 @@ export default function Home() { +{/* Where to verify the claims */} -What this buys you
Nothing to seize
-- Compromising Solon's servers yields no keys and no funds — only records that were - already public. +
+ Compromising Solon's servers yields no keys and no funds — + only records that were already public.
Nothing to forge
-- A vote that doesn't verify against the member's Bitcoin address is rejected. Solon - cannot invent votes, and neither can an attacker. +
+ A vote that doesn't verify against the member's + Bitcoin address is rejected. Solon cannot invent votes, and + neither can an attacker.
Nothing to rewrite
-- Decisions travel with their evidence. Consumers recount the tally themselves — - Solon's word is evidence, not authority. +
+ Decisions travel with their evidence. Consumers recount the + tally themselves — Solon's word is evidence, not authority.
+- Verify, don't trust:{' '} - + Verify, don't trust:{" "} + how voting works - {' · '} - + {" · "} + the live audit trail - {' · '} + {" · "} +
{title}
-{description}
+{description}
{details.map((detail, index) => ( -
- -