feat(brand): scaffold @bootnode-io/brand npm package

Matches the canonical brand-package shape (parallel to @hanzo/brand,
@luxfi/brand, @zooai/brand, @osage/brand, etc.):

- package.json — name @bootnode-io/brand, version 0.1.0, public,
  exports ./, ./brand.json, ./loader, ./assets/*
- brand.json — canonical schema (brand, chains, iam, rpc,
  walletConnect, api blocks)
- src/index.ts + src/loader.ts — TS source compiled to dist/ via tsup
  (cjs+esm+dts)
- tsconfig.json — ES2020 + bundler resolution
- .npmignore — ship dist/, assets/, brand.json only

Existing config.ts + components.tsx + DESIGN.md preserved.

Publish (when npm @bootnode-io scope is reserved + creds available):
  cd brand && npm publish --access public
This commit is contained in:
2026-05-19 08:56:13 -07:00
parent ba01d2e29e
commit b2af6aff33
7 changed files with 432 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
src/
tsconfig.json
.npmignore
*.log
.DS_Store
node_modules/
+200
View File
@@ -0,0 +1,200 @@
# DESIGN.md — Bootnode brand
> White-label brand kit — Hanzo / Lux / Zoo / Pars node operator skins
A short, opinionated design spec. Mirrors the spirit of Material 3 / Google
Web Design Spec, sized to the Bootnode brand. Lives next to `LLM.md` —
`LLM.md` describes what's built, this file describes how it should look.
The canonical brand JSON for Bootnode is `brand.json` in this repo. That
file is the source of truth for tokens; this file is the source of truth
for **how to use them**.
---
## 1. Foundation
### 1.1 Voice
Operator-grade. Status-page tone. Numbers always.
### 1.2 Brand atom
White-label container — runtime-detects host org (hanzo, lux, zoo, pars) and themes accordingly. No standalone identity. Inherits monochrome (Hanzo/Lux), 6-color (Zoo), or indigo (Pars).
### 1.3 Layout grid
- Max content width: `max-w-7xl` (1280px) for grids, `max-w-3xl` (768px) for centered text blocks
- Horizontal padding: `px-4 sm:px-6 lg:px-8`
- Vertical rhythm: `py-20 lg:py-32` for hero sections, `py-16` to `py-20` for content sections, `py-16` for the final CTA
### 1.4 Density
Generous. If a section feels crowded, remove a component before reducing spacing.
---
## 2. Color tokens
### 2.1 Source of truth
All color tokens live in `brand.json` (this repo). CSS variables are derived
at runtime from those tokens. Never hardcode hex literals in component files
unless the file IS the brand reference (color swatches, logo SVG).
### 2.2 Palette
The bootnode brand has **no fixed palette** — it inherits from the active host
org. See `config.ts` for the `BrandOrgConfig` mapping. When developing a
bootnode surface, theme tokens are read from the host's `brand.json` at
runtime.
### 2.3 Opacity ladder
Use opacity steps in 5-point increments (`/5`, `/10`, `/15`, `/20`, `/30`,
`/40`, `/60`, `/80`, full). Higher opacity = higher rank. Avoid arbitrary
values like `/12` or `/37`.
### 2.4 Semantic colors (the *only* reserved hued exceptions for brands without a saturated primary)
| Where | Color | Reason |
|---|---|---|
| Error / destructive | `red-500` family | User-blocking error or destructive confirmation |
| Success / online | `green-400/500` | Live-status, "online" affordance |
| Pricing — "Free" / "Save N%" callouts | `green-400` text | Positive financial callout |
| macOS window-chrome dot trio | red/yellow/green @ 60% | OS affordance reproduction |
| External brand logos | brand's official hex | Recognizing third-party identity |
For brands with a saturated primary (Zoo, Pars, Liquidity, Osage, Zen, MIGA,
Cyrus), prefer the brand's own palette over the generic Tailwind red/green
above where the brand maps cleanly (e.g. Osage red `#b8312f` for errors,
Zoo green `#00A652` for success).
---
## 3. Typography
### 3.1 Family
Inherits from host org. Default Geist Sans.
### 3.2 Type scale (Tailwind classes)
| Class | Use |
|---|---|
| `text-5xl lg:text-7xl font-bold` | Hero `<h1>` |
| `text-4xl font-bold` | Section `<h2>` |
| `text-2xl font-bold` | Subsection `<h3>` |
| `text-xl text-muted-foreground` | Hero tagline, lead paragraph |
| `text-sm text-muted-foreground` | Body |
| `text-xs text-muted-foreground` | Helper, footnote, badge label |
| `text-[10px] font-semibold uppercase tracking-wider text-muted-foreground` | Category eyebrow |
---
## 4. Components
### 4.1 Card
```tsx
<div className="p-6 rounded-xl border border-neutral-800 bg-neutral-900/50">
<Icon className="h-6 w-6 mb-3 text-foreground/80" />
<h3 className="font-semibold mb-2">Title</h3>
<p className="text-sm text-muted-foreground">Body…</p>
</div>
```
### 4.2 Button (primary / secondary)
NEVER use `<Button asChild>` — it expects exactly one child element and
breaks the build under React 18 strict children. Use plain `<a>` or
`<Link>` with className strings.
```tsx
<Link href="/x" className="inline-flex items-center justify-center gap-2 bg-primary text-primary-foreground hover:bg-primary/90 px-6 py-3 rounded-md text-sm font-medium">
Get started
</Link>
```
### 4.3 Hero pattern (every product page starts with this)
```tsx
<section className="py-20 lg:py-32 relative">
<div className="absolute inset-0 bg-gradient-to-b from-white/10 to-transparent" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="text-center max-w-3xl mx-auto mb-16">
<h1 className="text-5xl lg:text-7xl font-bold mb-6">Product Title</h1>
<p className="text-xl text-muted-foreground mb-8">Tagline ≤140 chars.</p>
</div>
</div>
</section>
```
### 4.4 Final resources block (every product page ends with this)
```tsx
<section className="py-16 border-t border-neutral-800">
<div className="max-w-3xl mx-auto px-4 text-center">
<h2 className="text-2xl font-bold mb-4">Get started</h2>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<a href="<docs URL>" target="_blank" rel="noopener noreferrer">Read the docs</a>
<a href="<github URL>" target="_blank" rel="noopener noreferrer">View on GitHub</a>
</div>
</div>
</section>
```
---
## 5. Motion
- Library: `framer-motion`
- Hero entry: `initial={{ opacity: 0, y: 20 }}` → `animate={{ opacity: 1, y: 0 }}` with `duration: 0.5` and `delay: 0.05 * index` stagger
- Hover: pure CSS via `transition-colors` (no JS)
- Background: a single radial gradient pulse, 8s ease-in-out infinite
- No bouncy springs, no carousel auto-play, no parallax
---
## 6. Iconography
- Library: `lucide-react`. No other icon set.
- Sizing: `h-3 w-3` (inline body), `h-4 w-4` (CTA), `h-6 w-6` (card eyebrow), `h-10 w-10` (section accent)
---
## 7. Page rules
### 7.1 Every product page MUST
1. Live at a flat `/<slug>` URL
2. Have a unique `<h1>` — no two product pages share a title
3. Have a unique first-600-char body signature
4. End with the resources block (§ 4.4)
5. Carry only the colors permitted by § 2.2 / § 2.4
### 7.2 Forbidden
- `<Button asChild>` — breaks build
- Hardcoded hex literals (use brand.json tokens)
- Decorative hue outside the brand palette
- Multiple typefaces
- Multiple URL schemes for the same product
---
## 8. Accessibility (non-negotiable)
- Contrast: every body-text on background combination clears WCAG AA at 16px
- Focus rings: visible — never `outline-none` without a custom ring
- Keyboard: every dropdown / dialog / mobile menu reachable via Tab; closeable via Escape
- Reduced motion: honor `prefers-reduced-motion`
- Alt text: every `<img>` and decorative SVG either has alt or `aria-hidden="true"`
---
## 9. Verification
Build clean, run e2e, visual-diff against the canonical pages of the
primary app (*.bootnode.* (per host org)). If a page reads visibly different from the
canonical reference, something is wrong.
---
## 10. When in doubt
The brand atom (§ 1.2) is the test. Every component, every page, every
copy decision should reinforce that atom. If your work doesn't, change
your work — not the atom.
+75
View File
@@ -0,0 +1,75 @@
{
"brand": {
"name": "Bootnode",
"title": "Bootnode",
"description": "Enterprise blockchain infrastructure — multi-chain RPC, Token API, NFT API, smart wallets",
"shortName": "Bootnode",
"labsName": "Bootnode",
"legalEntity": "Bootnode",
"walletName": "Bootnode Wallet",
"protocolName": "Bootnode",
"copyrightHolder": "Bootnode",
"appDomain": "bootno.de",
"docsDomain": "docs.bootno.de",
"infoDomain": "info.bootno.de",
"gatewayDomain": "api.bootno.de",
"wsDomain": "ws.bootno.de",
"helpUrl": "https://docs.bootno.de",
"termsUrl": "https://bootno.de/terms",
"privacyUrl": "https://bootno.de/privacy",
"downloadUrl": "https://bootno.de",
"complianceEmail": "[email protected]",
"supportEmail": "[email protected]",
"twitter": "",
"linkedin": "",
"github": "https://github.com/hanzoai/bootnode",
"discord": "",
"logoUrl": "https://cdn.jsdelivr.net/npm/@bootnode-io/brand@latest/assets/logo/logo.svg",
"faviconUrl": "https://cdn.jsdelivr.net/npm/@bootnode-io/brand@latest/assets/logo/favicon.png",
"primaryColor": "#000000",
"theme": {
"light": {
"accent1": "#000000",
"surface1": "#FFFFFF",
"surface2": "#FAFAFA",
"neutral1": "#0A0A0A",
"neutral2": "rgba(10, 10, 10, 0.6)"
},
"dark": {
"accent1": "#FFFFFF",
"surface1": "#000000",
"surface2": "#111111",
"neutral1": "#FAFAFA",
"neutral2": "rgba(250, 250, 250, 0.6)"
}
}
},
"chains": {
"defaultChainId": 1,
"supported": [1, 137, 42161, 8453, 43114, 56, 10]
},
"iam": {
"issuer": "https://iam.bootno.de",
"jwksUrl": "https://iam.bootno.de/.well-known/jwks",
"tenantClaim": "org_id",
"tenantHeader": "X-Org-Id"
},
"rpc": {
"mainnet": "https://rpc.bootno.de",
"testnet": "https://testnet.bootno.de"
},
"walletConnect": {
"projectId": "",
"metadata": {
"name": "Bootnode",
"description": "Enterprise blockchain infrastructure",
"url": "https://bootno.de",
"icons": ["https://cdn.jsdelivr.net/npm/@bootnode-io/brand@latest/assets/logo/favicon.png"]
}
},
"api": {
"production": "https://api.bootno.de",
"staging": "https://staging.api.bootno.de",
"development": "http://localhost:8000"
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"name": "@bootnode-io/brand",
"version": "0.1.0",
"description": "Official brand assets and design system for Bootnode",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./brand.json": "./brand.json",
"./loader": {
"import": "./dist/loader.mjs",
"require": "./dist/loader.js",
"types": "./dist/loader.d.ts"
},
"./assets/*": "./assets/*"
},
"files": [
"dist",
"assets",
"brand.json"
],
"scripts": {
"build": "tsup src/index.ts src/loader.ts --format cjs,esm --dts --clean",
"dev": "tsup src/index.ts src/loader.ts --format cjs,esm --dts --watch",
"type-check": "tsc --noEmit"
},
"keywords": [
"bootnode",
"brand",
"design-system",
"ui",
"assets",
"logo"
],
"author": "Bootnode",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/bootnode/bootnode",
"directory": "brand"
},
"homepage": "https://github.com/bootnode/bootnode/tree/main/brand",
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.5.1",
"typescript": "^5.9.3"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* @bootnode-io/brand — official brand assets and design system for Bootnode
*/
export { brand, loadBrand, getBrandUrl } from './loader'
export type { BrandConfig, BrandTheme } from './loader'
import brandJson from '../brand.json'
/** Static export of brand.json contents. */
export const config = brandJson
+66
View File
@@ -0,0 +1,66 @@
/**
* Runtime brand loader. Hydrates the singleton from /brand.json.
*/
export interface BrandTheme {
accent1: string
surface1: string
surface2: string
neutral1: string
neutral2: string
}
export interface BrandConfig {
name: string
title: string
description: string
appDomain: string
docsDomain: string
gatewayDomain: string
wsDomain: string
supportEmail: string
github: string
logoUrl: string
faviconUrl: string
primaryColor: string
theme: {
light: BrandTheme
dark: BrandTheme
}
}
const _state = {
name: '',
title: '',
description: '',
appDomain: '',
docsDomain: '',
gatewayDomain: '',
wsDomain: '',
supportEmail: '',
github: '',
logoUrl: '',
faviconUrl: '',
primaryColor: '',
theme: {
light: { accent1: '', surface1: '', surface2: '', neutral1: '', neutral2: '' },
dark: { accent1: '', surface1: '', surface2: '', neutral1: '', neutral2: '' },
},
}
export const brand: BrandConfig = _state
/** loadBrand reads brand.json at runtime and hydrates the singleton. */
export async function loadBrand(url = '/brand.json'): Promise<BrandConfig> {
const r = await fetch(url)
if (!r.ok) throw new Error(`loadBrand: ${r.status} ${r.statusText}`)
const j = await r.json()
Object.assign(_state, j.brand || {})
return _state
}
/** getBrandUrl returns a brand-scoped URL. */
export function getBrandUrl(path: string): string {
const domain = _state.appDomain || ''
return `https://${domain}${path.startsWith('/') ? path : '/' + path}`
}
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2020", "DOM"],
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}