mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
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]>
154 lines
5.0 KiB
TypeScript
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>
|
|
)
|
|
}
|