mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
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:
@@ -0,0 +1,6 @@
|
||||
src/
|
||||
tsconfig.json
|
||||
.npmignore
|
||||
*.log
|
||||
.DS_Store
|
||||
node_modules/
|
||||
+200
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user