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: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
78 changes: 78 additions & 0 deletions scripts/design-system-check.js
Original file line number Diff line number Diff line change
@@ -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');
52 changes: 33 additions & 19 deletions src/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -57,8 +64,9 @@ export default async function DashboardOverview() {
return (
<main className="space-y-6">
<h1 className="text-3xl font-bold text-navy">Overview</h1>
<p className="text-slate-600">
The governance register is currently unreachable. No live data can be shown.
<p className="text-fg-secondary">
The governance register is currently unreachable. No live data can be
shown.
</p>
</main>
);
Expand All @@ -70,54 +78,60 @@ export default async function DashboardOverview() {
{org ? `${org.name} — Overview` : "Overview"}
</h1>
{!org && (
<p className="text-slate-600">
No organization is registered yet. Once one exists, its votes, treasury, and audit
trail appear here.
<p className="text-fg-secondary">
No organization is registered yet. Once one exists, its votes,
treasury, and audit trail appear here.
</p>
)}
{org && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
href="/dashboard/voting"
className="block rounded-lg border border-gray-200 p-5 hover:border-navy transition-colors"
className="block rounded-lg border border-default p-5 hover:border-navy transition-colors"
>
<div className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-2">
<div className="text-sm font-semibold text-fg-secondary uppercase tracking-wide mb-2">
Latest vote
</div>
{session ? (
<>
<div className="font-semibold text-navy">{session.proposalTitle}</div>
<div className="mt-1 text-sm text-slate-600">
<div className="font-semibold text-navy">
{session.proposalTitle}
</div>
<div className="mt-1 text-sm text-fg-secondary">
{session.outcome ?? session.status}
{tallyLine ? ` · ${tallyLine}` : ""}
</div>
</>
) : (
<div className="text-sm text-slate-600">No voting session opened yet.</div>
<div className="text-sm text-fg-secondary">
No voting session opened yet.
</div>
)}
</Link>

<Link
href="/dashboard/treasury"
className="block rounded-lg border border-gray-200 p-5 hover:border-navy transition-colors"
className="block rounded-lg border border-default p-5 hover:border-navy transition-colors"
>
<div className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-2">
<div className="text-sm font-semibold text-fg-secondary uppercase tracking-wide mb-2">
Treasury
</div>
<div className="text-sm text-slate-600">{treasuryLine}</div>
<div className="text-sm text-fg-secondary">{treasuryLine}</div>
</Link>

<Link
href="/governance/audit"
className="block rounded-lg border border-gray-200 p-5 hover:border-navy transition-colors"
className="block rounded-lg border border-default p-5 hover:border-navy transition-colors"
>
<div className="text-sm font-semibold text-slate-500 uppercase tracking-wide mb-2">
<div className="text-sm font-semibold text-fg-secondary uppercase tracking-wide mb-2">
Recent activity
</div>
{events.length === 0 ? (
<div className="text-sm text-slate-600">No audit events yet.</div>
<div className="text-sm text-fg-secondary">
No audit events yet.
</div>
) : (
<ul className="space-y-1 text-sm text-slate-600">
<ul className="space-y-1 text-sm text-fg-secondary">
{events.map((e) => (
<li key={e.id} className="truncate">
{e.eventType.toLowerCase().replace(/_/g, " ")} ·{" "}
Expand Down
6 changes: 4 additions & 2 deletions src/app/(dashboard)/dashboard/treasury/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export default async function TreasuryPage() {
let org = null;
let dbError = false;
try {
org = await prisma.organization.findFirst({ orderBy: { createdAt: "asc" } });
org = await prisma.organization.findFirst({
orderBy: { createdAt: "asc" },
});
} catch {
dbError = true;
}
Expand All @@ -17,7 +19,7 @@ export default async function TreasuryPage() {
return (
<main className="space-y-6">
<h1 className="text-3xl font-bold">Treasury</h1>
<p className="text-slate-600">
<p className="text-fg-secondary">
{dbError
? "The treasury register is currently unreachable. No balance can be shown."
: "No organization is registered yet, so there is no treasury to show."}
Expand Down
12 changes: 7 additions & 5 deletions src/app/(dashboard)/dashboard/voting/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export default async function VotingPage() {
return (
<main className="space-y-6">
<h1 className="text-3xl font-bold">Voting</h1>
<p className="text-slate-600">
The voting register is currently unreachable. No session data can be shown.
<p className="text-fg-secondary">
The voting register is currently unreachable. No session data can be
shown.
</p>
</main>
);
Expand All @@ -31,9 +32,10 @@ export default async function VotingPage() {
return (
<main className="space-y-6">
<h1 className="text-3xl font-bold">Voting</h1>
<p className="text-slate-600">
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.
<p className="text-fg-secondary">
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.
</p>
</main>
);
Expand Down
14 changes: 9 additions & 5 deletions src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ const TABS = [
{ title: "Voting", href: "/dashboard/voting" },
];

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const pathname = usePathname();

return (
<div className="py-8 space-y-8">
<div className="bg-white border border-gray-200 rounded-lg shadow-sm">
<div className="bg-surface-base border border-default rounded-lg shadow-sm">
<nav className="flex gap-1 p-1" aria-label="Dashboard sections">
{TABS.map((tab) => {
const active = pathname === tab.href;
Expand All @@ -25,8 +29,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
aria-current={active ? "page" : undefined}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
active
? "bg-solon-light text-navy ring-1 ring-gray-200"
: "text-gray-700 hover:text-navy hover:bg-gray-50"
? "bg-surface-raised text-navy ring-1 ring-default"
: "text-fg-primary hover:text-navy hover:bg-surface-raised"
}`}
>
{tab.title}
Expand All @@ -35,7 +39,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
})}
</nav>
</div>
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-6">
<div className="bg-surface-base border border-default rounded-lg shadow-sm p-6">
{children}
</div>
</div>
Expand Down
Loading