Files
zeekay 44935451c0 rebrand: @hanzo/waitlist (monochromatic, brand-neutral, Base-backed)
Drops the Next.js + Upstash Redis stack. The viral waitlist now ships as
a brand-neutral React widget against a Hanzo Base Go plugin.

What changed
- packages/widget/: new @hanzo/waitlist — React component, framework-free
  fetch client, drop-in <hanzo-waitlist> custom element. Zero brand
  colors, zero logos, zero brand names. Consumer brings logo via `logo`
  prop or `<slot name="logo">`. Theming via --hw-* CSS variables with
  fallback chain at usage so ancestor selectors win cleanly.
- apps/web/: Next.js demo rebuilt monochrome. Five generic presets
  (Neutral, Preset A/B/C/D) prove brand swap works without per-brand
  builds. Demo's placeholder logo lives in the demo, not the package.
- apps/api/: removed. Backend logic now lives in
  ~/work/hanzo/base/plugins/waitlist (committed separately).
- packages/sdk/: removed (Preact + Vite). Replaced by packages/widget.
- docs/: collapsed to architecture / api / widget. Old planning docs
  moved to docs/archive/.
- Latest deps: Next 16, React 19.2, Vite 8, TS 5.9, vite-plugin-dts 5,
  @vitejs/plugin-react 6, eslint-config-next 16.

Build output
- ESM 6.76kB, CJS 5.22kB, IIFE 196.99kB / 62.65kB gzip
- Widget styles: 3.89kB / 1.18kB gzip
- Demo first-load: 106kB
2026-06-09 22:57:41 -07:00

3.5 KiB

Widget

@hanzo/waitlist. React component + framework-free client + drop-in custom element. One bundle, three entry points.

Install

pnpm add @hanzo/waitlist

React

import { Waitlist } from '@hanzo/waitlist'
import '@hanzo/waitlist/styles.css'

<Waitlist waitlist="my-product" baseUrl="https://api.example.com" />

Props:

Prop Type Default Notes
waitlist string (required) slug or record id
baseUrl string current origin Hanzo Base URL
mode "inline" | "modal" "inline"
theme "auto" | "light" | "dark" "auto" overrides prefers-color-scheme
turnstileToken string — host renders Turnstile; passes the token in
hideShare boolean false
onSuccess (entry) => void —
onError ({ message, status }) => void —

Plus copy overrides (title, subtitle, submitLabel, successTitle, successSubtitle, triggerLabel).

Framework-free client

import { WaitlistClient } from '@hanzo/waitlist/client'

const c = new WaitlistClient({ baseUrl: 'https://api.example.com' })
const result = await c.join({ waitlist: 'my-product', email: '[email protected]' })
if (result.ok) console.log(result.rank)

Same module, no React, no DOM.

Drop-in custom element

<link rel="stylesheet" href="https://cdn.example.com/@hanzo/waitlist/style.css">
<script src="https://cdn.example.com/@hanzo/waitlist/waitlist.iife.js"></script>

<hanzo-waitlist
  waitlist="my-product"
  base-url="https://api.example.com"
  mode="modal"
  theme="dark">
</hanzo-waitlist>

The custom element does NOT use Shadow DOM. The widget's monochrome design wants to inherit the host's font and color tokens — isolating it would defeat the point.

Theming

Override CSS variables on .hanzo-waitlist (or any ancestor):

Variable Default Purpose
--hw-bg #ffffff / #0a0a0a surface
--hw-fg #0a0a0a / #fafafa text
--hw-muted derived from --hw-fg secondary text
--hw-border derived from --hw-fg border / outlines
--hw-accent var(--hw-fg) button + rank-number
--hw-accent-fg var(--hw-bg) text on accent
--hw-radius 12px global radius
--hw-input-radius 8px input/button radius
--hw-font system stack font family
--hw-shadow subtle drop card shadow

Example override (illustrative — your selector, your values):

.your-brand {
  --hw-accent: <accent>;
  --hw-accent-fg: <text on accent>;
  --hw-radius: <corner radius>;
}

Wrap the widget in <div class="your-brand"> and it's done.

Cloudflare Turnstile

The widget does not render the Turnstile challenge itself — brand-bound sitekey policies make that unwise. The host page mounts Turnstile, captures the token, passes it via the turnstileToken prop (or via turnstile-token attribute on the custom element).