Adding a tool to the hub
The hub is a registry plus a page. If you touched more than two files, check why.
Everything about a tile — its title, blurb, icon, status pill, accent, and where it points — is
one entry in site/src/tools.ts. Home.tsx maps over TOOLS to draw the grid; App.tsx holds
the routes. Nothing else enumerates tools, so never add a second list.
All commands run from site/, not the repo root.
1. The registry entry
// site/src/tools.ts — TOOLS is the single source of truth
{
slug: 'kelly', // unique; matches the route/folder name
title: 'Kelly Stake Sizer',
blurb: 'One or two sentences, present tense, no marketing.',
icon: Calculator, // a LucideIcon, imported at the top of the file
href: '/kelly',
kind: 'route', // 'route' | 'static' | 'external'
status: 'live', // 'live' | 'wip' | 'soon'
accent: 'teal', // must exist in ACCENT in Home.tsx
tag: 'Calculator', // optional short category label on the tile
}
Then, by kind — each one obliges exactly one more thing:
kind |
href |
You must also |
Rendered as |
route |
/slug |
add <Route path="/slug" element={<Page />} /> in App.tsx and import the page |
<Link>, client-side |
static |
/slug/index.html — the explicit file |
drop the page under site/public/slug/ |
<a>, full page load |
external |
the absolute URL |
nothing |
<a target="_blank" rel="noopener noreferrer"> |
[!WARNING]
A static tool's href must name index.html. Vite's dev server does not auto-serve a
public/ directory index, so /spectrum/ 404s locally while working fine on Vercel — a break
that only shows up for whoever runs npm run dev next. /spectrum/index.html resolves
identically in both. This is written into the comment above the spectrum entry; keep it there.
2. The accent must be a literal
ACCENT in site/src/pages/Home.tsx is a Record<Accent, …> of fully spelled-out Tailwind
class strings. Tailwind v4's JIT scans source text — it cannot see text-${color}-400. A new
accent needs three things or the tile renders unstyled:
- the key added to the
Accent union in tools.ts,
- a row in the literal
ACCENT map with icon, corner and hoverBorder spelled out in full,
- no interpolation anywhere in between.
The seven that exist — sky emerald violet amber rose teal cyan — cover more tools
than the hub has. Reuse one before adding one. The STATUS map right below it works the same
way and already covers live / wip / soon; a tool that is not ready ships as soon, it does
not ship absent.
3. Building a route page
New calculator pages follow the two that exist — read pages/OddsConverter.tsx before writing a
third. The pattern and its reuse rules are in
references/page-patterns.md.
The short version:
- Math goes in a pure module, never in the component.
src/odds.ts already holds American /
decimal / fractional / implied conversion, three de-vig methods (multiplicative, power, Shin)
and parlay joint-probability math, with zero imports so scripts/check-odds.ts can assert
against it. Reuse it. Re-deriving a conversion inside a page puts it outside the guard.
- Chrome goes in
components/CalcUi.tsx — Panel, Select, Stat are shared by both
calculator pages.
- New non-trivial math earns a
check:* script and a line in .github/workflows/ci.yml.
That is the house convention, not an optional extra: every other module of real logic has one.
4. Routes that are not tools
/setup is a <Route> in App.tsx with no TOOLS entry — it is the self-hosting page, linked
from the Home hero, not a tile. That is deliberate and it is the precedent: a page that is not a
tool gets a route and stays out of the registry. <Route path="*"> falls back to Home, so a
typo'd href silently renders the hub instead of 404ing — click the tile after adding it.
Deep links work in production because vercel.json rewrites /((?!api/).*) to /index.html. If a
new client route 404s on Vercel but works in dev, that rewrite is the first place to look.
5. Checklist
- One entry in
TOOLS — slug unique, blurb factual, tag short.
- Branch on
kind (§1). route → <Route> + import. static → public/<slug>/ + an
index.html href. external → done.
- Accent exists as a literal in
Home.tsx (§2) — or reuse one of the seven.
- Math in a pure module, chrome from
CalcUi.tsx (§3).
- New logic gets a
check:* script wired into CI.
npm run lint — tsc --noEmit catches an accent key that is not in the union.
npm run dev and click the tile, then reload on the deep link. The * fallback hides a
bad href behind a working-looking page.
- Update
CLAUDE.md's layout block if the tool added a directory.
6. Anti-patterns
- A second list of tools. Nav, footer, sitemap — all of them map over
TOOLS.
- An interpolated Tailwind class (
text-${accent}-400). JIT never emits it.
href: '/spectrum/' for a static tool. Works on Vercel, 404s in dev.
- Math inside the page component. It escapes
check:odds.
- Trusting the tile to prove the route.
<Route path="*"> renders Home for anything.
- Shipping a half-finished tool by leaving it out of
TOOLS. Mark it soon.
- Running
npm at the repo root. The whole app is in site/.
Related
| For |
See |
The calculator page pattern, odds.ts and CalcUi reuse |
references/page-patterns.md |
The 187-record dataset behind the /spectrum static tool |
the edge-spectrum-dataset skill |
| Adding an API endpoint a new tool needs |
the edge-spectrum-endpoint skill |
| Branch flow, preview deploys, CI gate order |
the repo's CLAUDE.md |
1---2name: edge-spectrum-hub-tool3description: Add, change, retire or debug a tool on the Edge Spectrum hub — the `TOOLS` registry in site/src/tools.ts, the three `kind` branches (route / static / external) and what each one obliges you to wire, the literal `ACCENT` map Tailwind's JIT depends on, and the calculator page pattern built on src/odds.ts and components/CalcUi.tsx. Use for "add a tool to the hub", "new calculator page", "new tile", "the tile links nowhere", "the accent colour renders unstyled", "the static page 404s in dev but works on Vercel", "deep link reloads to the home page", "mark a tool as WIP / soon", "remove a tool", and any work on Home.tsx, App.tsx, the hub nav, or a page under site/src/pages/.4---56# Adding a tool to the hub78**The hub is a registry plus a page. If you touched more than two files, check why.**910Everything about a tile — its title, blurb, icon, status pill, accent, and where it points — is11one entry in `site/src/tools.ts`. `Home.tsx` maps over `TOOLS` to draw the grid; `App.tsx` holds12the routes. Nothing else enumerates tools, so **never add a second list.**1314All commands run from **`site/`**, not the repo root.1516---1718## 1. The registry entry1920```ts21// site/src/tools.ts — TOOLS is the single source of truth22{23 slug: 'kelly', // unique; matches the route/folder name24 title: 'Kelly Stake Sizer',25 blurb: 'One or two sentences, present tense, no marketing.',26 icon: Calculator, // a LucideIcon, imported at the top of the file27 href: '/kelly',28 kind: 'route', // 'route' | 'static' | 'external'29 status: 'live', // 'live' | 'wip' | 'soon'30 accent: 'teal', // must exist in ACCENT in Home.tsx31 tag: 'Calculator', // optional short category label on the tile32}33```3435Then, **by `kind`** — each one obliges exactly one more thing:3637| `kind` | `href` | You must also | Rendered as |38|:---|:---|:---|:---|39| `route` | `/slug` | add `<Route path="/slug" element={<Page />} />` in `App.tsx` **and** import the page | `<Link>`, client-side |40| `static` | `/slug/index.html` — **the explicit file** | drop the page under `site/public/slug/` | `<a>`, full page load |41| `external` | the absolute URL | nothing | `<a target="_blank" rel="noopener noreferrer">` |4243> [!WARNING]44> **A `static` tool's `href` must name `index.html`.** Vite's dev server does *not* auto-serve a45> `public/` directory index, so `/spectrum/` 404s locally while working fine on Vercel — a break46> that only shows up for whoever runs `npm run dev` next. `/spectrum/index.html` resolves47> identically in both. This is written into the comment above the spectrum entry; keep it there.4849## 2. The accent must be a literal5051`ACCENT` in `site/src/pages/Home.tsx` is a `Record<Accent, …>` of **fully spelled-out Tailwind52class strings**. Tailwind v4's JIT scans source text — it cannot see `text-${color}-400`. A new53accent needs three things or the tile renders unstyled:54551. the key added to the `Accent` union in `tools.ts`,562. a row in the literal `ACCENT` map with `icon`, `corner` and `hoverBorder` spelled out in full,573. no interpolation anywhere in between.5859The seven that exist — `sky` `emerald` `violet` `amber` `rose` `teal` `cyan` — cover more tools60than the hub has. **Reuse one before adding one.** The `STATUS` map right below it works the same61way and already covers `live` / `wip` / `soon`; a tool that is not ready ships as `soon`, it does62not ship absent.6364## 3. Building a `route` page6566New calculator pages follow the two that exist — read `pages/OddsConverter.tsx` before writing a67third. The pattern and its reuse rules are in68[`references/page-patterns.md`](references/page-patterns.md).6970The short version:7172- **Math goes in a pure module, never in the component.** `src/odds.ts` already holds American /73 decimal / fractional / implied conversion, three de-vig methods (multiplicative, power, Shin)74 and parlay joint-probability math, with **zero imports** so `scripts/check-odds.ts` can assert75 against it. Reuse it. Re-deriving a conversion inside a page puts it outside the guard.76- **Chrome goes in `components/CalcUi.tsx`** — `Panel`, `Select`, `Stat` are shared by both77 calculator pages.78- **New non-trivial math earns a `check:*` script** and a line in `.github/workflows/ci.yml`.79 That is the house convention, not an optional extra: every other module of real logic has one.8081## 4. Routes that are not tools8283`/setup` is a `<Route>` in `App.tsx` with **no** `TOOLS` entry — it is the self-hosting page, linked84from the Home hero, not a tile. That is deliberate and it is the precedent: a page that is not a85*tool* gets a route and stays out of the registry. `<Route path="*">` falls back to `Home`, so a86typo'd `href` silently renders the hub instead of 404ing — **click the tile after adding it.**8788Deep links work in production because `vercel.json` rewrites `/((?!api/).*)` to `/index.html`. If a89new client route 404s on Vercel but works in dev, that rewrite is the first place to look.9091---9293## 5. Checklist94951. **One entry in `TOOLS`** — `slug` unique, `blurb` factual, `tag` short.962. **Branch on `kind`** (§1). `route` → `<Route>` + import. `static` → `public/<slug>/` + an97 `index.html` href. `external` → done.983. **Accent exists as a literal** in `Home.tsx` (§2) — or reuse one of the seven.994. **Math in a pure module**, chrome from `CalcUi.tsx` (§3).1005. **New logic gets a `check:*` script** wired into CI.1016. `npm run lint` — `tsc --noEmit` catches an accent key that is not in the union.1027. `npm run dev` and **click the tile**, then reload on the deep link. The `*` fallback hides a103 bad `href` behind a working-looking page.1048. Update `CLAUDE.md`'s layout block if the tool added a directory.105106---107108## 6. Anti-patterns109110- **A second list of tools.** Nav, footer, sitemap — all of them map over `TOOLS`.111- **An interpolated Tailwind class** (`text-${accent}-400`). JIT never emits it.112- **`href: '/spectrum/'` for a static tool.** Works on Vercel, 404s in dev.113- **Math inside the page component.** It escapes `check:odds`.114- **Trusting the tile to prove the route.** `<Route path="*">` renders `Home` for anything.115- **Shipping a half-finished tool by leaving it out of `TOOLS`.** Mark it `soon`.116- **Running `npm` at the repo root.** The whole app is in `site/`.117118---119120## Related121122| For | See |123|:---|:---|124| The calculator page pattern, `odds.ts` and `CalcUi` reuse | [`references/page-patterns.md`](references/page-patterns.md) |125| The 187-record dataset behind the `/spectrum` static tool | the `edge-spectrum-dataset` skill |126| Adding an API endpoint a new tool needs | the `edge-spectrum-endpoint` skill |127| Branch flow, preview deploys, CI gate order | the repo's `CLAUDE.md` |