c0eca8d0e1
Brings the canvas onto the warm-paper design system already shipped to landing, marketplace, and SaaS surfaces, and migrates the build from Tailwind v3 → v4 to match molecule-app. Plumbing: - swap tailwindcss v3 → v4, drop autoprefixer, add @tailwindcss/postcss - delete tailwind.config.ts (v4 reads tokens from @theme blocks in CSS) - globals.css: @import "tailwindcss" + @plugin "@tailwindcss/typography" - two @theme blocks: warm-paper light defaults + always-dark surface tokens (bg-bg / ink-mute / line-strong) for terminal/console panels - [data-theme="dark"] cascade overrides the warm-paper tokens for dark - React Flow edge stroke + scrollbar + selection colour pull from semantic tokens so they flip with the theme Theme infra (ported from molecule-app, identical contracts): - lib/theme-cookie.ts: mol_theme cookie + boot script (no "use client" so server components can read the constants) - lib/theme-provider.tsx: ThemeProvider + useTheme + cookie writer with Domain=.moleculesai.app so the preference follows the user across canvas/app/market/landing subdomains AND tenant subdomains - lib/theme.ts: ColorToken union + cssVar() helper - components/ThemeToggle.tsx: 3-way System/Light/Dark picker - layout.tsx: SSR cookie read + nonce'd inline boot script (CSP needs the explicit nonce — strict-dynamic doesn't forgive an un-nonce'd inline sibling) + ThemeProvider wrapper + bg-surface/text-ink body Component migration (62 files): - Mechanical bg-zinc-* / text-zinc-* / border-zinc-* / text-white → semantic surface/ink/line tokens via perl negative-lookahead pass (preserves opacity modifiers like /80, /60) - bg-blue-500/600 → bg-accent / bg-accent-strong - text-red-* / amber-* / emerald-* → text-bad / warm / good - Tinted-state banner backgrounds (bg-red-950, bg-amber-950, bg-blue-950 etc.) intentionally left literal — they remain readable on warm-paper in light mode without inventing new state-soft tokens - TerminalTab.tsx skipped — xterm renders to canvas, not DOM - 3 unit-test assertions updated to match new token strings (credits pillTone, AuthGate overlay class, A2AEdge accent) Verification: - pnpm test: 1214/1214 pass - pnpm tsc --noEmit: clean - next build: ✓ Compiled successfully (8 routes) - dev server inspection: html data-theme stamped, body uses bg-surface text-ink, boot script carries nonce, compiled CSS contains both @theme blocks + [data-theme="dark"] override Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
54 lines
2.2 KiB
TypeScript
54 lines
2.2 KiB
TypeScript
// credits.ts — small pure helpers for rendering credit state on /orgs.
|
|
// Kept out of page.tsx so unit tests can exercise the formatting +
|
|
// banner-kind logic in node (no jsdom) without needing to mount React.
|
|
|
|
export type CreditsBannerKind =
|
|
| "none"
|
|
| "overage" // paid plan has started burning overage this period
|
|
| "out-of-credits" // balance 0, not on a paid plan (trial ran out)
|
|
| "trial-tail"; // balance low but not zero, no paid plan yet
|
|
|
|
export interface CreditsFields {
|
|
credits_balance?: number;
|
|
plan_monthly_credits?: number;
|
|
overage_used_credits?: number;
|
|
}
|
|
|
|
// formatCredits renders an int as a compact string. 9999 → "9999",
|
|
// 12345 → "12.3k". Keeps the balance pill narrow enough to fit on one
|
|
// line next to the org slug even for the Scale plan's 30k grant.
|
|
export function formatCredits(n: number): string {
|
|
if (n < 10_000) return String(n);
|
|
return `${(n / 1000).toFixed(1)}k`;
|
|
}
|
|
|
|
// pillTone returns the tailwind classnames that color the balance pill.
|
|
// Empty / exhausted → red; within 10% of zero → amber; else zinc. The
|
|
// 10% threshold matches the banner trigger — one consistent "low"
|
|
// signal so the pill and banner agree.
|
|
export function pillTone(fields: CreditsFields): string {
|
|
const balance = fields.credits_balance ?? 0;
|
|
const monthly = fields.plan_monthly_credits ?? 0;
|
|
if (balance <= 0) return "bg-red-950 text-red-200 border-red-800";
|
|
const ratio = monthly > 0 ? balance / monthly : 1;
|
|
if (ratio < 0.1) return "bg-amber-950 text-amber-200 border-amber-800";
|
|
return "bg-surface-card text-ink border-line";
|
|
}
|
|
|
|
// bannerKind picks which (if any) banner to show under the balance
|
|
// pill. Precedence:
|
|
// 1. overage_used > 0 → "overage" (even if balance is refreshed)
|
|
// 2. balance <= 0 → "out-of-credits"
|
|
// 3. trial + low tail → "trial-tail"
|
|
// 4. otherwise → "none"
|
|
export function bannerKind(fields: CreditsFields): CreditsBannerKind {
|
|
const balance = fields.credits_balance ?? 0;
|
|
const monthly = fields.plan_monthly_credits ?? 0;
|
|
const overageUsed = fields.overage_used_credits ?? 0;
|
|
|
|
if (overageUsed > 0) return "overage";
|
|
if (balance <= 0) return "out-of-credits";
|
|
if (monthly === 0 && balance < 100) return "trial-tail";
|
|
return "none";
|
|
}
|