Files
bootnode/console/components/chain-onboarding-selector.tsx
zeekayandHanzo Dev da98c0de75 feat(console): one icon set — every glyph comes from @luxfi/ui/icons
74 files imported `lucide-react` directly, pinned ^1.26.0, while @luxfi/ui —
which this console renders alongside — resolves 1.27. Two Lucide copies from two
resolutions in one bundle is how a set stops being one set. Two more glyphs were
not from any icon library at all: the node list drew its network in/out arrows as
the literal characters "↓" and "↑", and the chain onboarding selector drew its
tick as "✓". Those are font glyphs — they take the text metrics, not the icon
box, so they sit on a different baseline and scale with the font, not `size-*`.

One source now: `@luxfi/ui/icons`, Lucide as @hanzo/gui standardises on it and
@luxfi/ui themes it. `lucide-react` is out of package.json, so per-surface drift
is no longer expressible; @luxfi/ui comes in at 7.4.3, which is where `./icons`
lives. The three text glyphs are ArrowDown / ArrowUp / Check.

All 121 distinct icon names this console uses were checked against the 6014
symbols @luxfi/ui/icons exports before the rewrite — zero missing, with a
negative control. `next build` is clean: 78 routes, Next 16.2.11 + Turbopack.

The d3 chart roots (node-globe, node-map, globe-view, donut-chart, fleet-map)
keep their <svg>: those are plots, not glyphs. The command menu's "↑↓" stays —
it is a keyboard legend inside <kbd>, and it means the keys.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:57:55 -07:00

154 lines
5.0 KiB
TypeScript

"use client"
import { useMemo, useState } from "react"
import { Check } from "@luxfi/ui/icons"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export interface ChainOnboardingItem {
id: string
deployKey: string
name: string
category: string
icon: string
live: boolean
}
interface ChainOnboardingSelectorProps {
items: ChainOnboardingItem[]
onDeploySelected: (deployKeys: string[]) => Promise<void> | void
isDeploying?: boolean
}
export function ChainOnboardingSelector({
items,
onDeploySelected,
isDeploying = false,
}: ChainOnboardingSelectorProps) {
const [query, setQuery] = useState("")
const [selected, setSelected] = useState<string[]>([])
const visibleItems = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) {
return items
}
return items.filter((item) =>
`${item.name} ${item.category} ${item.deployKey}`.toLowerCase().includes(q),
)
}, [items, query])
const liveSelected = selected.filter((id) =>
items.some((item) => item.id === id && item.live),
)
const toggle = (id: string, live: boolean) => {
if (!live) return
setSelected((prev) =>
prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id],
)
}
const deploySelected = async () => {
if (liveSelected.length === 0) return
const deployKeys = liveSelected
.map((id) => items.find((item) => item.id === id)?.deployKey)
.filter((value): value is string => Boolean(value))
await onDeploySelected(Array.from(new Set(deployKeys)))
setSelected([])
}
return (
<div className="rounded-2xl border border-zinc-200 bg-gradient-to-b from-white to-zinc-50 p-4 md:p-6">
<div className="mb-4 text-center">
<h3 className="text-2xl font-semibold tracking-tight">What chains are you building on?</h3>
<p className="mt-1 text-sm text-zinc-500">Select your stack now. You can always add more later.</p>
</div>
<div className="mb-4">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search chains (e.g. Ethereum, Base, Solana)"
className="h-10"
/>
</div>
<div className="grid grid-cols-2 gap-2">
{visibleItems.map((item) => {
const active = selected.includes(item.id)
return (
<button
key={item.id}
type="button"
disabled={!item.live}
onClick={() => toggle(item.id, item.live)}
className={`flex items-center justify-between rounded-xl border px-3 py-2.5 text-left transition ${
active
? "border-blue-600 bg-blue-50 text-blue-700"
: item.live
? "border-zinc-200 bg-white text-zinc-800 hover:border-zinc-400"
: "border-zinc-200 bg-zinc-100 text-zinc-400"
} ${!item.live ? "cursor-not-allowed" : ""}`}
>
<span className="flex items-center gap-2">
<span
className={`inline-flex h-4 w-4 items-center justify-center rounded-sm border ${
active ? "border-blue-600 bg-blue-600 text-white" : "border-zinc-300"
}`}
>
{active ? <Check className="size-3" /> : null}
</span>
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-zinc-100 text-sm overflow-hidden">
{item.icon.startsWith("/") ? (
<img src={item.icon} alt={item.name} className="h-6 w-6 rounded-full object-cover" />
) : (
item.icon
)}
</span>
<span className="font-medium">{item.name}</span>
</span>
<span
className={`rounded-full px-2 py-1 text-[10px] font-semibold uppercase tracking-wide ${
item.live
? "bg-emerald-100 text-emerald-700"
: "bg-zinc-200 text-zinc-500"
}`}
>
{item.live ? "Live" : "Soon"}
</span>
</button>
)
})}
</div>
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t border-zinc-200 pt-4">
<p className="text-sm text-zinc-600">
{liveSelected.length} live chain{liveSelected.length === 1 ? "" : "s"} selected
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setSelected([])}
disabled={selected.length === 0 || isDeploying}
>
Clear
</Button>
<Button
size="sm"
onClick={deploySelected}
disabled={liveSelected.length === 0 || isDeploying}
>
{isDeploying ? "Deploying…" : "Deploy Selected Chains"}
</Button>
</div>
</div>
</div>
)
}