fix(frontend): finish the token migration — raw grey classes -> theme tokens
UI_REDESIGN chunk 4 was never finished: 39 app/component files still used raw Tailwind greys, kept readable in light mode only by the !important override sheet. In dark mode text-gray-600 is ~2.3:1 on surface, and the light override maps it to #94a3b8 (~2.6:1 on white) — unreadable in both. Mechanical mapping (886 classes): gray-100/200 -> ink, 300/400 -> ink-2, 500/600/700 -> ink-muted, bg 950/900/800 -> page/surface/raised, border 800 -> line, 700/600 -> line-strong, plus hover/divide/placeholder. Alpha suffixes kept (tokens are color-mix, so /60 works). bg-gray-700, text-white and marketing pages left alone. text-[9px] -> text-[10px]. Frontend unverified: no Node on the authoring box (#207). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3968b49b80
commit
bf206334e3
@@ -39,11 +39,11 @@ function CreateKeyModal({ onClose, onCreated }: { onClose: () => void; onCreated
|
||||
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
|
||||
<Card padding="lg" className="w-full max-w-lg space-y-4">
|
||||
<h2 className="text-white font-semibold">Key created</h2>
|
||||
<p className="text-xs text-gray-400">
|
||||
<p className="text-xs text-ink-2">
|
||||
Copy this key now — it won't be shown again. This is a sample key from the demo module in{" "}
|
||||
<code className="text-gray-300">lib/apiKeys.ts</code>; it doesn't authenticate against anything.
|
||||
<code className="text-ink-2">lib/apiKeys.ts</code>; it doesn't authenticate against anything.
|
||||
</p>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-3">
|
||||
<div className="bg-raised border border-line-strong rounded-lg p-3">
|
||||
<p className="text-xs text-indigo-300 break-all font-mono">{rawKey}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -61,11 +61,11 @@ function CreateKeyModal({ onClose, onCreated }: { onClose: () => void; onCreated
|
||||
<h2 className="text-white font-semibold mb-4">New API key</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Label</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">Label</label>
|
||||
<input
|
||||
required value={name} onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Ops dashboard integration"
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -96,9 +96,9 @@ export default function ApiKeysSettingsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="bg-indigo-600/10 border border-indigo-600/40 rounded-xl p-4">
|
||||
<p className="text-indigo-300 text-sm font-semibold">Preview feature</p>
|
||||
<p className="text-gray-400 text-xs mt-1 leading-relaxed">
|
||||
<p className="text-ink-2 text-xs mt-1 leading-relaxed">
|
||||
Organization API keys aren't backed by a real endpoint yet — this screen runs against an in-memory
|
||||
demo module (<code className="text-gray-300">lib/apiKeys.ts</code>) so the flow can be reviewed end to
|
||||
demo module (<code className="text-ink-2">lib/apiKeys.ts</code>) so the flow can be reviewed end to
|
||||
end. See that file for the exact backend routes a real integration needs.
|
||||
</p>
|
||||
</div>
|
||||
@@ -108,7 +108,7 @@ export default function ApiKeysSettingsPage() {
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">{loading ? "Loading…" : `${active.length} active key${active.length !== 1 ? "s" : ""}`}</p>
|
||||
<p className="text-sm text-ink-muted">{loading ? "Loading…" : `${active.length} active key${active.length !== 1 ? "s" : ""}`}</p>
|
||||
<Button size="sm" onClick={() => setShowCreate(true)}>+ Create key</Button>
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function ApiKeysSettingsPage() {
|
||||
<Card padding="none" className="overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-gray-500 uppercase tracking-wider border-b border-gray-800 bg-gray-900">
|
||||
<tr className="text-xs text-ink-muted uppercase tracking-wider border-b border-line bg-surface">
|
||||
<th className="px-4 py-3 text-left">Label</th>
|
||||
<th className="px-4 py-3 text-left">Key</th>
|
||||
<th className="px-4 py-3 text-left hidden sm:table-cell">Created</th>
|
||||
@@ -129,11 +129,11 @@ export default function ApiKeysSettingsPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map((k) => (
|
||||
<tr key={k.key_id} className="border-b border-gray-800 last:border-0">
|
||||
<tr key={k.key_id} className="border-b border-line last:border-0">
|
||||
<td className="px-4 py-3 text-white">{k.name}</td>
|
||||
<td className="px-4 py-3 text-gray-500 font-mono text-xs">{k.key_prefix}…</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs hidden sm:table-cell">{fmtDate(k.created_at)}</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs hidden sm:table-cell">{k.last_used_at ? fmtDate(k.last_used_at) : "Never"}</td>
|
||||
<td className="px-4 py-3 text-ink-muted font-mono text-xs">{k.key_prefix}…</td>
|
||||
<td className="px-4 py-3 text-ink-2 text-xs hidden sm:table-cell">{fmtDate(k.created_at)}</td>
|
||||
<td className="px-4 py-3 text-ink-2 text-xs hidden sm:table-cell">{k.last_used_at ? fmtDate(k.last_used_at) : "Never"}</td>
|
||||
<td className="px-4 py-3">
|
||||
{k.revoked ? <Badge tone="danger">Revoked</Badge> : <Badge tone="success">Active</Badge>}
|
||||
</td>
|
||||
|
||||
@@ -54,12 +54,12 @@ function UsageBar({ label, used, limit }: { label: string; used: number; limit:
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-baseline justify-between mb-1.5">
|
||||
<span className="text-xs text-gray-400 font-mono">{label}</span>
|
||||
<span className="text-xs font-mono text-gray-300">
|
||||
<span className="text-xs text-ink-2 font-mono">{label}</span>
|
||||
<span className="text-xs font-mono text-ink-2">
|
||||
{used} / {limit === "unlimited" ? "∞" : limit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 rounded-full bg-gray-800 overflow-hidden">
|
||||
<div className="h-2 rounded-full bg-raised overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all ${nearLimit ? "bg-orange-500" : "bg-indigo-500"}`}
|
||||
style={{ width: limit === "unlimited" ? "8%" : `${pct}%` }}
|
||||
@@ -142,7 +142,7 @@ export default function BillingSettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<p className="text-xs text-gray-600 font-mono">
|
||||
<p className="text-xs text-ink-muted font-mono">
|
||||
Demo data — this page isn't connected to a live payment processor. See lib/billing.ts for the integration plan.
|
||||
</p>
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function BillingSettingsPage() {
|
||||
<UsageBar label="Seats" used={usage.seatsUsed} limit={usage.seatsLimit} />
|
||||
<UsageBar label="Nodes" used={usage.nodesUsed} limit={usage.nodesLimit} />
|
||||
</div>
|
||||
<div className="mt-5 pt-5 border-t border-gray-800">
|
||||
<div className="mt-5 pt-5 border-t border-line">
|
||||
<Button variant="secondary" size="sm" onClick={handleManageBilling} disabled={portalBusy}>
|
||||
{portalBusy ? "Opening…" : "Manage payment method & invoices"}
|
||||
</Button>
|
||||
@@ -179,10 +179,10 @@ export default function BillingSettingsPage() {
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`rounded-xl border p-4 flex flex-col ${p.highlighted ? "border-indigo-600/40" : "border-gray-800"}`}
|
||||
className={`rounded-xl border p-4 flex flex-col ${p.highlighted ? "border-indigo-600/40" : "border-line"}`}
|
||||
>
|
||||
<p className="text-white font-semibold text-sm">{p.name}</p>
|
||||
<p className="text-gray-500 text-xs mt-1 flex-1">{p.tagline}</p>
|
||||
<p className="text-ink-muted text-xs mt-1 flex-1">{p.tagline}</p>
|
||||
{(UNBUILT_CLAIMS[p.id] ?? []).length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{(UNBUILT_CLAIMS[p.id] ?? []).map((claim) => (
|
||||
@@ -212,17 +212,17 @@ export default function BillingSettingsPage() {
|
||||
<Card>
|
||||
<CardHeader title="Invoice history" />
|
||||
{invoices.length === 0 ? (
|
||||
<p className="text-gray-600 text-sm">No invoices yet.</p>
|
||||
<p className="text-ink-muted text-sm">No invoices yet.</p>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-800">
|
||||
<div className="divide-y divide-line">
|
||||
{invoices.map((inv) => (
|
||||
<div key={inv.id} className="flex items-center justify-between gap-4 py-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-gray-200 text-sm">{inv.description}</p>
|
||||
<p className="text-gray-600 text-xs font-mono">{fmtDate(inv.date)}</p>
|
||||
<p className="text-ink text-sm">{inv.description}</p>
|
||||
<p className="text-ink-muted text-xs font-mono">{fmtDate(inv.date)}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<span className="text-gray-300 text-sm font-mono">${inv.amountUsd.toFixed(2)}</span>
|
||||
<span className="text-ink-2 text-sm font-mono">${inv.amountUsd.toFixed(2)}</span>
|
||||
<Badge tone={INVOICE_TONE[inv.status]}>{inv.status}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,15 +43,15 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
|
||||
description="Organization profile, team access, node ownership, API keys, and billing."
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-1 bg-gray-900 border border-gray-800 rounded-lg p-1 w-fit max-w-full overflow-x-auto">
|
||||
<div className="flex flex-wrap gap-1 bg-surface border border-line rounded-lg p-1 w-fit max-w-full overflow-x-auto">
|
||||
{[...TABS, ...(isAdmin ? ADMIN_TABS : [])].map((t) => (
|
||||
<Link
|
||||
key={t.href}
|
||||
href={t.href}
|
||||
className={`text-sm font-mono px-4 py-1.5 rounded-md transition-colors whitespace-nowrap ${
|
||||
pathname === t.href || pathname.startsWith(t.href + "/")
|
||||
? "bg-gray-800 text-white"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
? "bg-raised text-white"
|
||||
: "text-ink-muted hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
|
||||
@@ -46,8 +46,8 @@ function InviteModal({ onClose, onCreated }: { onClose: () => void; onCreated: (
|
||||
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4">
|
||||
<Card padding="lg" className="w-full max-w-md space-y-4">
|
||||
<h2 className="text-white font-semibold">Member invited</h2>
|
||||
<p className="text-xs text-gray-400">Share this one-time invite link so they can set their password. It expires after use.</p>
|
||||
<div className="bg-gray-800 border border-gray-700 rounded-lg p-3">
|
||||
<p className="text-xs text-ink-2">Share this one-time invite link so they can set their password. It expires after use.</p>
|
||||
<div className="bg-raised border border-line-strong rounded-lg p-3">
|
||||
<p className="text-xs text-indigo-300 break-all">{inviteLink}</p>
|
||||
</div>
|
||||
<Button onClick={onClose} fullWidth>Done</Button>
|
||||
@@ -62,18 +62,18 @@ function InviteModal({ onClose, onCreated }: { onClose: () => void; onCreated: (
|
||||
<h2 className="text-white font-semibold mb-4">Invite a member</h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Email</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">Email</label>
|
||||
<input
|
||||
type="email" required value={email} onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
placeholder="teammate@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Role</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">Role</label>
|
||||
<select
|
||||
value={role} onChange={(e) => setRole(e.target.value as UserRole)}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
>
|
||||
<option value="admin">Admin — full access</option>
|
||||
<option value="operator">Operator — owns nodes</option>
|
||||
@@ -130,7 +130,7 @@ export default function MembersSettingsPage() {
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-ink-muted">
|
||||
{loading ? "Loading members…" : `${users.length} member${users.length !== 1 ? "s" : ""}`}
|
||||
</p>
|
||||
<Button size="sm" onClick={() => setShowInvite(true)}>+ Invite member</Button>
|
||||
@@ -144,7 +144,7 @@ export default function MembersSettingsPage() {
|
||||
<Card padding="none" className="overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-gray-500 uppercase tracking-wider border-b border-gray-800 bg-gray-900">
|
||||
<tr className="text-xs text-ink-muted uppercase tracking-wider border-b border-line bg-surface">
|
||||
<th className="px-4 py-3 text-left">Member</th>
|
||||
<th className="px-4 py-3 text-left">Role</th>
|
||||
<th className="px-4 py-3 text-left hidden sm:table-cell">Owned nodes</th>
|
||||
@@ -155,10 +155,10 @@ export default function MembersSettingsPage() {
|
||||
{loading
|
||||
? Array.from({ length: 3 }).map((_, i) => <SkeletonRow key={i} cols={4} />)
|
||||
: users.map((u) => (
|
||||
<tr key={u.uid} className="border-b border-gray-800 last:border-0">
|
||||
<tr key={u.uid} className="border-b border-line last:border-0">
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-white">{u.display_name || u.email}</p>
|
||||
{u.display_name && <p className="text-gray-600 text-xs">{u.email}</p>}
|
||||
{u.display_name && <p className="text-ink-muted text-xs">{u.email}</p>}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{u.uid === user?.uid ? (
|
||||
@@ -168,7 +168,7 @@ export default function MembersSettingsPage() {
|
||||
value={u.role}
|
||||
disabled={savingUid === u.uid}
|
||||
onChange={(e) => handleRoleChange(u, e.target.value as UserRole)}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-xs text-white focus:outline-none focus:border-indigo-500 disabled:opacity-50"
|
||||
className="bg-raised border border-line-strong rounded-lg px-2 py-1 text-xs text-white focus:outline-none focus:border-indigo-500 disabled:opacity-50"
|
||||
>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="operator">Operator</option>
|
||||
@@ -176,7 +176,7 @@ export default function MembersSettingsPage() {
|
||||
</select>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-400 text-xs hidden sm:table-cell">
|
||||
<td className="px-4 py-3 text-ink-2 text-xs hidden sm:table-cell">
|
||||
{u.role === "operator" ? u.owned_node_ids.length : "—"}
|
||||
</td>
|
||||
<td className="px-4 py-3 hidden md:table-cell">
|
||||
@@ -189,7 +189,7 @@ export default function MembersSettingsPage() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-600 font-mono">
|
||||
<p className="text-xs text-ink-muted font-mono">
|
||||
Need to disable or delete a member? Use the full user admin panel under Admin → Users.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +114,7 @@ function EnrollmentTokensPanel() {
|
||||
</p>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<p className="flex-1 text-xs text-indigo-100 font-mono break-all bg-gray-900 rounded px-2 py-1.5">{justMinted}</p>
|
||||
<p className="flex-1 text-xs text-indigo-100 font-mono break-all bg-surface rounded px-2 py-1.5">{justMinted}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigator.clipboard?.writeText(justMinted).then(() => {
|
||||
@@ -133,7 +133,7 @@ function EnrollmentTokensPanel() {
|
||||
: <>(check the broker host)</>}:
|
||||
</p>
|
||||
<div className="flex items-start gap-2">
|
||||
<pre className="flex-1 text-xs text-indigo-100 font-mono whitespace-pre-wrap break-all bg-gray-900 rounded px-2 py-1.5">{installCmd}</pre>
|
||||
<pre className="flex-1 text-xs text-indigo-100 font-mono whitespace-pre-wrap break-all bg-surface rounded px-2 py-1.5">{installCmd}</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigator.clipboard?.writeText(installCmd).then(() => {
|
||||
@@ -163,7 +163,7 @@ function EnrollmentTokensPanel() {
|
||||
value={label}
|
||||
onChange={(e) => setLabel(e.target.value)}
|
||||
placeholder="Node ID, e.g. node-003"
|
||||
className="flex-1 min-w-[12rem] bg-gray-800 border border-gray-700 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
className="flex-1 min-w-[12rem] bg-raised border border-line-strong rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-indigo-500"
|
||||
/>
|
||||
<Button type="submit" size="sm" disabled={minting || !label.trim()}>
|
||||
{minting ? "Minting…" : "New token"}
|
||||
@@ -176,11 +176,11 @@ function EnrollmentTokensPanel() {
|
||||
<SkeletonRow cols={1} />
|
||||
</div>
|
||||
) : tokens.length === 0 ? (
|
||||
<p className="text-gray-600 text-xs">No enrollment tokens yet.</p>
|
||||
<p className="text-ink-muted text-xs">No enrollment tokens yet.</p>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-gray-500 uppercase tracking-wider border-b border-gray-800">
|
||||
<tr className="text-xs text-ink-muted uppercase tracking-wider border-b border-line">
|
||||
<th className="py-2 text-left">Label</th>
|
||||
<th className="py-2 text-left hidden sm:table-cell">Created</th>
|
||||
<th className="py-2 text-left">Status</th>
|
||||
@@ -189,14 +189,14 @@ function EnrollmentTokensPanel() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{tokens.map((t) => (
|
||||
<tr key={t.token_id} className="border-b border-gray-800 last:border-0">
|
||||
<tr key={t.token_id} className="border-b border-line last:border-0">
|
||||
<td className="py-2 text-white">{t.label}</td>
|
||||
<td className="py-2 text-gray-500 text-xs hidden sm:table-cell">
|
||||
<td className="py-2 text-ink-muted text-xs hidden sm:table-cell">
|
||||
{new Date(t.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="py-2 text-xs">
|
||||
{t.revoked ? (
|
||||
<span className="text-gray-600">Revoked</span>
|
||||
<span className="text-ink-muted">Revoked</span>
|
||||
) : (
|
||||
<span className="text-green-400">Active · {t.uses} use{t.uses !== 1 ? "s" : ""}</span>
|
||||
)}
|
||||
@@ -283,7 +283,7 @@ export default function NodeOwnershipSettingsPage() {
|
||||
<EnrollmentTokensPanel />
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-ink-muted">
|
||||
Assign each node to the operator responsible for it. Operators only see and manage the nodes assigned to them here.
|
||||
</p>
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function NodeOwnershipSettingsPage() {
|
||||
<Card padding="none" className="overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-gray-500 uppercase tracking-wider border-b border-gray-800 bg-gray-900">
|
||||
<tr className="text-xs text-ink-muted uppercase tracking-wider border-b border-line bg-surface">
|
||||
<th className="px-4 py-3 text-left">Node</th>
|
||||
<th className="px-4 py-3 text-left hidden sm:table-cell">Status</th>
|
||||
<th className="px-4 py-3 text-left">Owner</th>
|
||||
@@ -302,17 +302,17 @@ export default function NodeOwnershipSettingsPage() {
|
||||
{loading ? (
|
||||
Array.from({ length: 3 }).map((_, i) => <SkeletonRow key={i} cols={3} />)
|
||||
) : nodes.length === 0 ? (
|
||||
<tr><td colSpan={3} className="px-4 py-8 text-center text-gray-600 text-sm">No nodes registered yet.</td></tr>
|
||||
<tr><td colSpan={3} className="px-4 py-8 text-center text-ink-muted text-sm">No nodes registered yet.</td></tr>
|
||||
) : (
|
||||
nodes.map((n) => {
|
||||
const owner = ownerByNode.get(n.node_id);
|
||||
return (
|
||||
<tr key={n.node_id} className="border-b border-gray-800 last:border-0">
|
||||
<tr key={n.node_id} className="border-b border-line last:border-0">
|
||||
<td className="px-4 py-3">
|
||||
<Link href={`/nodes/${n.node_id}`} className="text-white hover:text-indigo-300 transition-colors">
|
||||
{n.name}
|
||||
</Link>
|
||||
<p className="text-gray-600 text-xs font-mono">{n.node_id}</p>
|
||||
<p className="text-ink-muted text-xs font-mono">{n.node_id}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3 hidden sm:table-cell"><StatusBadge status={n.status} /></td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -320,7 +320,7 @@ export default function NodeOwnershipSettingsPage() {
|
||||
value={owner?.uid ?? UNASSIGNED}
|
||||
disabled={savingNodeId === n.node_id}
|
||||
onChange={(e) => reassign(n.node_id, e.target.value)}
|
||||
className="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1.5 text-xs text-white focus:outline-none focus:border-indigo-500 disabled:opacity-50 max-w-[14rem]"
|
||||
className="bg-raised border border-line-strong rounded-lg px-2 py-1.5 text-xs text-white focus:outline-none focus:border-indigo-500 disabled:opacity-50 max-w-[14rem]"
|
||||
>
|
||||
<option value={UNASSIGNED}>Unassigned</option>
|
||||
{assignable.map((u) => (
|
||||
|
||||
@@ -16,7 +16,7 @@ import { PreferredBotsCard } from "@/components/discord/PreferredBotsCard";
|
||||
function StatTile({ label, value }: { label: string; value: string | number }) {
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider font-mono">{label}</p>
|
||||
<p className="text-xs text-ink-muted uppercase tracking-wider font-mono">{label}</p>
|
||||
<p className="text-2xl font-bold text-white font-mono mt-1">{value}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -69,12 +69,12 @@ export default function OrganizationSettingsPage() {
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 block mb-1">Organization name</label>
|
||||
<label className="text-xs text-ink-2 block mb-1">Organization name</label>
|
||||
<input
|
||||
value={orgName}
|
||||
disabled={orgLoading || !canEdit}
|
||||
onChange={(e) => setOrgName(e.target.value)}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500 disabled:opacity-50"
|
||||
className="w-full bg-raised border border-line-strong rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500 disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
{saveError && <p className="text-red-400 text-xs">{saveError}</p>}
|
||||
@@ -110,7 +110,7 @@ export default function OrganizationSettingsPage() {
|
||||
value={sub ? sub.status.replace("_", " ") : "—"}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 pt-5 border-t border-gray-800 flex items-center gap-4 text-sm">
|
||||
<div className="mt-5 pt-5 border-t border-line flex items-center gap-4 text-sm">
|
||||
<Link href="/settings/billing" className="text-indigo-400 hover:text-indigo-300 transition-colors">
|
||||
Manage plan & billing →
|
||||
</Link>
|
||||
@@ -124,7 +124,7 @@ export default function OrganizationSettingsPage() {
|
||||
|
||||
<PreferredBotsCard canEdit={canEdit} />
|
||||
|
||||
<div className="bg-gray-900 border border-red-800/60 rounded-xl p-5">
|
||||
<div className="bg-surface border border-red-800/60 rounded-xl p-5">
|
||||
<CardHeader title="Danger zone" subtitle="Destructive organization-level actions." />
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button variant="danger" size="sm" disabled title="Not available in this build — contact support">
|
||||
|
||||
Reference in New Issue
Block a user