diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..cb6e507 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs` +5983e7ef7fcff2be90e5660cfab296dee4bcd7c1 # prettier, 87 files diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 6b1008e..781407b 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -18,10 +18,10 @@ name: Auto-merge on: workflow_run: - workflows: ['CI'] + workflows: ["CI"] types: [completed] schedule: - - cron: '*/10 * * * *' + - cron: "*/10 * * * *" workflow_dispatch: {} permissions: diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..b698663 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,31 @@ +# Build output and vendored trees — formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Generated during a build, so it is absent locally and present in CI — which +# makes a clean local --check no evidence at all. Contentlayer's output also +# uses import assertions, which prettier's parser rejects outright. +.contentlayer +.astro +.svelte-kit +storybook-static +test-results +playwright-report + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a2f11f0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/i18n/de.json b/i18n/de.json index 55c2e75..8a9009d 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -1,31 +1,31 @@ { - "solon": { - "title": "SOLON", - "tagline": "Bitcoin-native Governance für das digitale Zeitalter", - "subtag": "Transparente Entscheidungen und Schatzkammern für Organisationen aus Menschen und KI-Agenten.", - "cta_primary": "Letzte Abstimmung ansehen", - "cta_secondary": "Schatzkammer ansehen", - "bullets": { - "transparency": "Watch-only-Schatzkammern — Solon hält keine Schlüssel, keine Gelder", - "democracy": "Bitcoin-signierte Stimmen, von jedem überprüfbar", - "global": "Unveränderliches, öffentliches Audit-Protokoll" - } - }, - "home": { - "made_in": "Hergestellt in der Schweiz", - "pillars_section": { - "title": "Was Solon regiert", - "subtitle": "Schatzkammer, Abstimmung, Entscheidungen und Audit — als ein System entworfen, jede Aussage am laufenden System überprüfbar." - }, - "demo_section": { - "title": "Transparenz in Aktion", - "subtitle": "Stimmen sind Bitcoin-signierte Nachrichten. Die Schatzkammer ist watch-only on-chain. Das Audit-Protokoll ist unveränderlich und öffentlich." + "solon": { + "title": "SOLON", + "tagline": "Bitcoin-native Governance für das digitale Zeitalter", + "subtag": "Transparente Entscheidungen und Schatzkammern für Organisationen aus Menschen und KI-Agenten.", + "cta_primary": "Letzte Abstimmung ansehen", + "cta_secondary": "Schatzkammer ansehen", + "bullets": { + "transparency": "Watch-only-Schatzkammern — Solon hält keine Schlüssel, keine Gelder", + "democracy": "Bitcoin-signierte Stimmen, von jedem überprüfbar", + "global": "Unveränderliches, öffentliches Audit-Protokoll" + } }, - "cta": { - "title": "Regieren im Offenen", - "subtitle": "Jede Entscheidung, jede Stimme, jeder Satoshi — signiert, protokolliert und von jedem überprüfbar.", - "primary": "Dashboard öffnen", - "secondary": "Funktionen ansehen" + "home": { + "made_in": "Hergestellt in der Schweiz", + "pillars_section": { + "title": "Was Solon regiert", + "subtitle": "Schatzkammer, Abstimmung, Entscheidungen und Audit — als ein System entworfen, jede Aussage am laufenden System überprüfbar." + }, + "demo_section": { + "title": "Transparenz in Aktion", + "subtitle": "Stimmen sind Bitcoin-signierte Nachrichten. Die Schatzkammer ist watch-only on-chain. Das Audit-Protokoll ist unveränderlich und öffentlich." + }, + "cta": { + "title": "Regieren im Offenen", + "subtitle": "Jede Entscheidung, jede Stimme, jeder Satoshi — signiert, protokolliert und von jedem überprüfbar.", + "primary": "Dashboard öffnen", + "secondary": "Funktionen ansehen" + } } - } -} \ No newline at end of file +} diff --git a/i18n/en.json b/i18n/en.json index 1a5ab07..3faa382 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,31 +1,31 @@ { - "solon": { - "title": "SOLON", - "tagline": "Bitcoin-Native Governance for the Digital Age", - "subtag": "Transparent decision-making and treasuries for organizations of humans and AI agents.", - "cta_primary": "See the latest vote", - "cta_secondary": "See the treasury", - "bullets": { - "transparency": "Watch-only treasuries — Solon holds no keys, no funds", - "democracy": "Bitcoin-signed votes anyone can re-verify", - "global": "Append-only audit trail, public by default" - } - }, - "home": { - "made_in": "Made in Switzerland", - "pillars_section": { - "title": "What Solon Governs", - "subtitle": "Treasury, voting, decisions, and audit — designed as one system, with every claim verifiable against the running platform." - }, - "demo_section": { - "title": "See Transparency in Action", - "subtitle": "Votes are Bitcoin-signed messages. The treasury is watch-only on-chain. The audit trail is append-only and public." + "solon": { + "title": "SOLON", + "tagline": "Bitcoin-Native Governance for the Digital Age", + "subtag": "Transparent decision-making and treasuries for organizations of humans and AI agents.", + "cta_primary": "See the latest vote", + "cta_secondary": "See the treasury", + "bullets": { + "transparency": "Watch-only treasuries — Solon holds no keys, no funds", + "democracy": "Bitcoin-signed votes anyone can re-verify", + "global": "Append-only audit trail, public by default" + } }, - "cta": { - "title": "Govern in the open", - "subtitle": "Every decision and every vote — signed, recorded, and re-verifiable by anyone.", - "primary": "Open the Dashboard", - "secondary": "See the features" + "home": { + "made_in": "Made in Switzerland", + "pillars_section": { + "title": "What Solon Governs", + "subtitle": "Treasury, voting, decisions, and audit — designed as one system, with every claim verifiable against the running platform." + }, + "demo_section": { + "title": "See Transparency in Action", + "subtitle": "Votes are Bitcoin-signed messages. The treasury is watch-only on-chain. The audit trail is append-only and public." + }, + "cta": { + "title": "Govern in the open", + "subtitle": "Every decision and every vote — signed, recorded, and re-verifiable by anyone.", + "primary": "Open the Dashboard", + "secondary": "See the features" + } } - } -} \ No newline at end of file +} diff --git a/i18n/fr.json b/i18n/fr.json index c7f9445..2067c15 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -1,31 +1,31 @@ { - "solon": { - "title": "SOLON", - "tagline": "Gouvernance native Bitcoin pour l'ère numérique", - "subtag": "Décisions et trésoreries transparentes pour des organisations d'humains et d'agents IA.", - "cta_primary": "Voir le dernier vote", - "cta_secondary": "Voir la trésorerie", - "bullets": { - "transparency": "Trésoreries en lecture seule — Solon ne détient ni clés ni fonds", - "democracy": "Votes signés Bitcoin, vérifiables par tous", - "global": "Journal d'audit public et immuable" - } - }, - "home": { - "made_in": "Conçu en Suisse", - "pillars_section": { - "title": "Ce que Solon gouverne", - "subtitle": "Trésorerie, vote, décisions et audit — conçus comme un seul système, chaque affirmation vérifiable sur la plateforme en marche." - }, - "demo_section": { - "title": "La transparence en action", - "subtitle": "Les votes sont des messages signés Bitcoin. La trésorerie est en lecture seule on-chain. Le journal d'audit est immuable et public." + "solon": { + "title": "SOLON", + "tagline": "Gouvernance native Bitcoin pour l'ère numérique", + "subtag": "Décisions et trésoreries transparentes pour des organisations d'humains et d'agents IA.", + "cta_primary": "Voir le dernier vote", + "cta_secondary": "Voir la trésorerie", + "bullets": { + "transparency": "Trésoreries en lecture seule — Solon ne détient ni clés ni fonds", + "democracy": "Votes signés Bitcoin, vérifiables par tous", + "global": "Journal d'audit public et immuable" + } }, - "cta": { - "title": "Gouverner au grand jour", - "subtitle": "Chaque décision, chaque vote, chaque satoshi — signé, enregistré et vérifiable par tous.", - "primary": "Ouvrir le tableau de bord", - "secondary": "Voir les fonctionnalités" + "home": { + "made_in": "Conçu en Suisse", + "pillars_section": { + "title": "Ce que Solon gouverne", + "subtitle": "Trésorerie, vote, décisions et audit — conçus comme un seul système, chaque affirmation vérifiable sur la plateforme en marche." + }, + "demo_section": { + "title": "La transparence en action", + "subtitle": "Les votes sont des messages signés Bitcoin. La trésorerie est en lecture seule on-chain. Le journal d'audit est immuable et public." + }, + "cta": { + "title": "Gouverner au grand jour", + "subtitle": "Chaque décision, chaque vote, chaque satoshi — signé, enregistré et vérifiable par tous.", + "primary": "Ouvrir le tableau de bord", + "secondary": "Voir les fonctionnalités" + } } - } -} \ No newline at end of file +} diff --git a/i18n/it.json b/i18n/it.json index 10948aa..2eb948f 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -1,31 +1,31 @@ { - "solon": { - "title": "SOLON", - "tagline": "Governance nativa Bitcoin per l'era digitale", - "subtag": "Decisioni e tesorerie trasparenti per organizzazioni di persone e agenti IA.", - "cta_primary": "Vedi l'ultimo voto", - "cta_secondary": "Vedi la tesoreria", - "bullets": { - "transparency": "Tesorerie in sola lettura — Solon non detiene chiavi né fondi", - "democracy": "Voti firmati Bitcoin, verificabili da chiunque", - "global": "Registro di audit pubblico e immutabile" - } - }, - "home": { - "made_in": "Realizzato in Svizzera", - "pillars_section": { - "title": "Cosa governa Solon", - "subtitle": "Tesoreria, voto, decisioni e audit — progettati come un unico sistema, ogni affermazione verificabile sulla piattaforma." - }, - "demo_section": { - "title": "La trasparenza in azione", - "subtitle": "I voti sono messaggi firmati Bitcoin. La tesoreria è in sola lettura on-chain. Il registro di audit è immutabile e pubblico." + "solon": { + "title": "SOLON", + "tagline": "Governance nativa Bitcoin per l'era digitale", + "subtag": "Decisioni e tesorerie trasparenti per organizzazioni di persone e agenti IA.", + "cta_primary": "Vedi l'ultimo voto", + "cta_secondary": "Vedi la tesoreria", + "bullets": { + "transparency": "Tesorerie in sola lettura — Solon non detiene chiavi né fondi", + "democracy": "Voti firmati Bitcoin, verificabili da chiunque", + "global": "Registro di audit pubblico e immutabile" + } }, - "cta": { - "title": "Governare alla luce del sole", - "subtitle": "Ogni decisione, ogni voto, ogni satoshi — firmato, registrato e verificabile da chiunque.", - "primary": "Apri la dashboard", - "secondary": "Vedi le funzionalità" + "home": { + "made_in": "Realizzato in Svizzera", + "pillars_section": { + "title": "Cosa governa Solon", + "subtitle": "Tesoreria, voto, decisioni e audit — progettati come un unico sistema, ogni affermazione verificabile sulla piattaforma." + }, + "demo_section": { + "title": "La trasparenza in azione", + "subtitle": "I voti sono messaggi firmati Bitcoin. La tesoreria è in sola lettura on-chain. Il registro di audit è immutabile e pubblico." + }, + "cta": { + "title": "Governare alla luce del sole", + "subtitle": "Ogni decisione, ogni voto, ogni satoshi — firmato, registrato e verificabile da chiunque.", + "primary": "Apri la dashboard", + "secondary": "Vedi le funzionalità" + } } - } -} \ No newline at end of file +} diff --git a/next.config.js b/next.config.js index 40d7a42..4a3a161 100644 --- a/next.config.js +++ b/next.config.js @@ -3,7 +3,7 @@ const nextConfig = { // Standalone output is required by the self-hosted Hetzner deploy // (scripts/hetzner/deploy.sh stages the standalone build, same as every // sibling app on the box). - output: 'standalone', + output: "standalone", }; -module.exports = nextConfig; \ No newline at end of file +module.exports = nextConfig; diff --git a/package-lock.json b/package-lock.json index 53a8c16..7f87093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,12 +35,16 @@ "eslint": "10.9.1", "eslint-config-next": "16.3.2", "postcss": "8.5.26", + "prettier": "3.9.6", "prisma": "7.9.1", "puppeteer": "25.8.0", "ts-node": "10.9.2", "tsx": "^4.23.12", "typescript": "5.5.3", "vitest": "^4.1.11" + }, + "engines": { + "node": ">=20" } }, "node_modules/@alloc/quick-lru": { @@ -8837,6 +8841,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prisma": { "version": "7.9.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.9.1.tgz", diff --git a/package.json b/package.json index dd22b3e..181eb93 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,15 @@ "lint": "eslint .", "typecheck": "tsc --noEmit", "design:check": "node scripts/design-system-check.js", - "verify": "npm run lint && npm run typecheck && npm run design:check && npm run test", + "verify": "npm run format:check && 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", "test:puppeteer:mega": "BASE_URL=${BASE_URL:-http://localhost:3000} node tests/puppeteer/mega-menu.mjs", "prisma:generate": "prisma generate", - "prisma:push": "prisma db push" + "prisma:push": "prisma db push", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@fleet/design-tokens": "github:catomean/design-tokens#v1.1.0", @@ -48,6 +50,7 @@ "eslint": "10.9.1", "eslint-config-next": "16.3.2", "postcss": "8.5.26", + "prettier": "3.9.6", "prisma": "7.9.1", "puppeteer": "25.8.0", "ts-node": "10.9.2", diff --git a/playwright.config.ts b/playwright.config.ts index 9d34351..5d883c4 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,17 +1,16 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - testDir: './tests/e2e', + testDir: "./tests/e2e", fullyParallel: true, - reporter: 'list', + reporter: "list", use: { - baseURL: process.env.BASE_URL || 'http://localhost:3000', - trace: 'on-first-retry', + baseURL: process.env.BASE_URL || "http://localhost:3000", + trace: "on-first-retry", }, projects: [ - { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, - { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, - { name: 'webkit', use: { ...devices['Desktop Safari'] } }, + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + { name: "firefox", use: { ...devices["Desktop Firefox"] } }, + { name: "webkit", use: { ...devices["Desktop Safari"] } }, ], }); - diff --git a/prisma.config.ts b/prisma.config.ts index a854118..a5f030c 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -1,9 +1,9 @@ -import 'dotenv/config'; -import { defineConfig, env } from '@prisma/config'; +import "dotenv/config"; +import { defineConfig, env } from "@prisma/config"; export default defineConfig({ - schema: 'prisma/schema.prisma', + schema: "prisma/schema.prisma", datasource: { - url: env('DATABASE_URL'), + url: env("DATABASE_URL"), }, }); diff --git a/scripts/add-member.ts b/scripts/add-member.ts index 183975f..290a466 100644 --- a/scripts/add-member.ts +++ b/scripts/add-member.ts @@ -50,11 +50,15 @@ async function main() { process.exit(1); } if (type === "AGENT" && !values.system) { - console.error("agents need --system (e.g. orangecat:cat) — which system attests this member's votes"); + console.error( + "agents need --system (e.g. orangecat:cat) — which system attests this member's votes", + ); process.exit(1); } if (type === "AGENT" && values["oc-actor"]) { - console.error("--oc-actor is for HUMAN members only — agents are recognized by their API key, not a login"); + console.error( + "--oc-actor is for HUMAN members only — agents are recognized by their API key, not a login", + ); process.exit(1); } @@ -65,7 +69,9 @@ async function main() { } const existing = await prisma.member.findUnique({ - where: { organizationId_bitcoinAddress: { organizationId: organization.id, bitcoinAddress: address } }, + where: { + organizationId_bitcoinAddress: { organizationId: organization.id, bitcoinAddress: address }, + }, }); if (existing) { console.log(`member already registered: ${existing.id} (${existing.displayName})`); @@ -113,7 +119,9 @@ async function main() { process.exit(1); } const plaintext = `sk_solon_${randomBytes(24).toString("hex")}`; - await prisma.agentApiKey.create({ data: { memberId: member.id, keyHash: sha256Hex(plaintext) } }); + await prisma.agentApiKey.create({ + data: { memberId: member.id, keyHash: sha256Hex(plaintext) }, + }); console.log(`API key (shown once, store it in the agent's env now):\n${plaintext}`); } } diff --git a/scripts/add-treasury-source.ts b/scripts/add-treasury-source.ts index 3b4b6d3..d9bf95c 100644 --- a/scripts/add-treasury-source.ts +++ b/scripts/add-treasury-source.ts @@ -57,9 +57,7 @@ async function main() { address, }, }); - console.log( - `treasury source created: ${source.id} (${source.label} ${source.address})`, - ); + console.log(`treasury source created: ${source.id} (${source.label} ${source.address})`); } main() diff --git a/scripts/agent-vote.ts b/scripts/agent-vote.ts index 0a83635..d3b6ee6 100644 --- a/scripts/agent-vote.ts +++ b/scripts/agent-vote.ts @@ -37,7 +37,9 @@ async function main() { const keyEnv = values["key-env"] ?? "SOLON_AGENT_PRIVKEY"; const privateKeyHex = process.env[keyEnv]; if (!privateKeyHex) { - console.error(`env ${keyEnv} is not set — the agent's private key lives in its own env, nowhere else`); + console.error( + `env ${keyEnv} is not set — the agent's private key lives in its own env, nowhere else`, + ); process.exit(1); } diff --git a/scripts/design-system-check.js b/scripts/design-system-check.js index 348c547..049aca2 100644 --- a/scripts/design-system-check.js +++ b/scripts/design-system-check.js @@ -5,71 +5,74 @@ * no palette utilities, no arbitrary hex, no pillowy radii, no shadows * standing in for hierarchy. */ -const fs = require('fs'); -const path = require('path'); +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 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.', + 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.', + message: "No arbitrary hex. Define a CSS var in globals.css.", }, { pattern: /\b(?:text|bg|border|ring)-white\/\d+\b/, - message: 'Use bg-surface-* / border-* tokens, not white with an opacity.', + message: "Use bg-surface-* / border-* tokens, not white with an opacity.", }, { // The vocabulary is shared with OrangeCat and FleetCrown. These were Solon's // private, blue-tinted palette — the reason the three sites looked like three // companies. They are gone from tailwind.config.js; this keeps them gone. - pattern: /\b(?:text|bg|border|ring|from|to|via|divide)-(?:navy|navy-light|navy-dark|solon-[a-z]+)\b/, + pattern: + /\b(?:text|bg|border|ring|from|to|via|divide)-(?:navy|navy-light|navy-dark|solon-[a-z]+)\b/, message: - 'Legacy Solon palette. Use the shared tokens: bg-surface-*, text-fg-*, bg-accent, text-bitcoin.', + "Legacy Solon palette. Use the shared tokens: bg-surface-*, text-fg-*, bg-accent, text-bitcoin.", }, { // Radii are named by role so all three products round corners the same way. pattern: /\brounded-(?:sm|md|lg|xl|2xl|3xl|full)\b/, - message: 'Use rounded-control / rounded-surface / rounded-pill.', + message: "Use rounded-control / rounded-surface / rounded-pill.", }, { pattern: /\bshadow-(?:sm|md|lg|xl|2xl|navy|card)\b/, - message: 'Hierarchy is border + type, not drop shadow.', + 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.', + message: "No component gradients. Brand surfaces live in globals.css.", }, { // max-w-7xl was the old per-page container. Width is one decision now. pattern: /\bmax-w-7xl\b/, - message: 'Use the .section-shell class (or max-w-shell) so every page lines up.', + message: "Use the .section-shell class (or max-w-shell) so every page lines up.", }, { // The display face (Instrument Serif) ships ONE weight. Asking for a bolder // one makes the browser synthesize a fake bold, which looks cheap and is // invisible in code review. Weight belongs to the face, in the token // package, not to the component — that is what keeps a face swap one line. - pattern: /\bfont-display\b[^"']*\bfont-(?:medium|semibold|bold|extrabold|black)\b|\bfont-(?:medium|semibold|bold|extrabold|black)\b[^"']*\bfont-display\b/, + pattern: + /\bfont-display\b[^"']*\bfont-(?:medium|semibold|bold|extrabold|black)\b|\bfont-(?:medium|semibold|bold|extrabold|black)\b[^"']*\bfont-display\b/, message: - 'Do not set a weight next to font-display — the display face has one weight, owned by @fleet/design-tokens.', + "Do not set a weight next to font-display — the display face has one weight, owned by @fleet/design-tokens.", }, { // .font-display already applies the face's tracking. Re-declaring it means // a future face swap silently keeps the OLD face's tracking. - pattern: /\bfont-display\b[^"']*\btracking-display\b|\btracking-display\b[^"']*\bfont-display\b/, - message: 'Redundant: .font-display already applies --tracking-display.', + pattern: + /\bfont-display\b[^"']*\btracking-display\b|\btracking-display\b[^"']*\bfont-display\b/, + message: "Redundant: .font-display already applies --tracking-display.", }, { pattern: /font-\[['"]?[A-Z]/, - message: 'Never name a typeface in a component. Use font-display / font-sans / font-mono.', + message: "Never name a typeface in a component. Use font-display / font-sans / font-mono.", }, { // Size floor. The display face is a high-contrast serif: its thin strokes @@ -77,9 +80,10 @@ const FORBIDDEN = [ // LIGHTER than the 16px sans paragraph under it and the hierarchy inverts. // Display type starts at text-2xl; below that use the sans at font-semibold. // The uppercase, open-tracked wordmark is the sanctioned exception (.wordmark). - pattern: /\bfont-display\b[^"']*\btext-(?:xs|sm|base|lg|xl)\b|\btext-(?:xs|sm|base|lg|xl)\b[^"']*\bfont-display\b/, + pattern: + /\bfont-display\b[^"']*\btext-(?:xs|sm|base|lg|xl)\b|\btext-(?:xs|sm|base|lg|xl)\b[^"']*\bfont-display\b/, message: - 'Display face below its size floor — use text-2xl+ with font-display, or the sans at font-semibold.', + "Display face below its size floor — use text-2xl+ with font-display, or the sans at font-semibold.", }, ]; @@ -97,7 +101,7 @@ const FORBIDDEN = [ const DISPLAY_SCALE = /\btext-(?:2xl|3xl|4xl|5xl|6xl|7xl|display-[123])\b/; function missingDisplayFace(line) { for (const m of line.matchAll(/className=(?:"([^"]*)"|\{`([^`]*)`\})/g)) { - const cls = m[1] ?? m[2] ?? ''; + const cls = m[1] ?? m[2] ?? ""; if (!DISPLAY_SCALE.test(cls)) continue; if (/\bfont-(?:display|mono)\b/.test(cls)) continue; if (/\bwordmark\b/.test(cls)) continue; @@ -118,7 +122,7 @@ const TOKEN_REDEFINITION = 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; + 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)) { @@ -132,7 +136,7 @@ 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'); + const lines = fs.readFileSync(file, "utf8").split("\n"); lines.forEach((line, i) => { for (const rule of FORBIDDEN) { if (rule.pattern.test(line)) { @@ -144,7 +148,7 @@ for (const target of TARGETS) { file: rel, line: i + 1, message: - 'Heading at display size without font-display. Above text-2xl the display face is the default — add font-display (or font-mono if this is data).', + "Heading at display size without font-display. Above text-2xl the display face is the default — add font-display (or font-mono if this is data).", source: line.trim(), }); } @@ -153,17 +157,17 @@ for (const target of TARGETS) { } // globals.css must stay free of token definitions — the package owns them. -const GLOBALS = path.join(ROOT, 'src/app/globals.css'); +const GLOBALS = path.join(ROOT, "src/app/globals.css"); if (fs.existsSync(GLOBALS)) { - fs.readFileSync(GLOBALS, 'utf8') - .split('\n') + fs.readFileSync(GLOBALS, "utf8") + .split("\n") .forEach((line, i) => { if (TOKEN_REDEFINITION.test(line)) { violations.push({ - file: 'src/app/globals.css', + file: "src/app/globals.css", line: i + 1, message: - 'Token redefined locally. Tokens live in @fleet/design-tokens so all three products share them.', + "Token redefined locally. Tokens live in @fleet/design-tokens so all three products share them.", source: line.trim(), }); } @@ -177,4 +181,4 @@ if (violations.length) { } process.exit(1); } -console.log('design-system check: ok'); +console.log("design-system check: ok"); diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 8db5955..dcfd4a4 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -72,8 +72,7 @@ export default async function DashboardOverview() {

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.

); @@ -86,8 +85,8 @@ export default async function DashboardOverview() { {!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 && } @@ -102,18 +101,14 @@ export default async function DashboardOverview() { {session ? ( <> -
- {session.proposalTitle} -
+
{session.proposalTitle}
{session.outcome ?? session.status} {tallyLine ? ` · ${tallyLine}` : ""}
) : ( -
- No voting session opened yet. -
+
No voting session opened yet.
)} @@ -135,9 +130,7 @@ export default async function DashboardOverview() { Recent activity {events.length === 0 ? ( -
- No audit events yet. -
+
No audit events yet.
) : (