# Hype + Forge

A complete guide to building web apps with Hype and Forge.

**Hype** is a thin wrapper around [Hono](https://hono.dev) for building web apps with Bun. It provides file-based routing, automatic TypeScript transpilation, SSE, and a default HTML5 layout — all without a build step. Since `Hype extends Hono`, every Hono API works out of the box.

**Forge** is a component library that generates real CSS from JSX definitions. Each `define()` call produces a typed component with scoped CSS classes. It works on both server and client — on the server it collects CSS for `<Styles />` to inline, in the browser it auto-injects a `<style>` tag.

## Table of Contents

**Setup**
- [Getting Started](#getting-started)
- [Project Structure](#project-structure)

**Hype — Server Framework**
- [Server Entry Point](#server-entry-point)
- [Routing](#routing)
- [Layouts](#layouts)
- [Pages](#pages)
- [Client-Side JavaScript](#client-side-javascript)
- [Styling](#styling)
- [Server-Sent Events (SSE)](#server-sent-events-sse)
- [Custom API Routes](#custom-api-routes)
- [Sub-Routers](#sub-routers)
- [Static Files](#static-files)

**Forge — Components**
- [Basic Components](#basic-components)
- [HTML Tags](#html-tags)
- [CSS Properties](#css-properties)
- [States](#states)
- [Variants](#variants)
- [Parts](#parts)
- [Custom Render](#custom-render)
- [Selectors](#selectors)
- [Scopes](#scopes)
- [Themes](#themes)

**Integration**
- [SSR with Forge](#ssr-with-forge)
- [SPA with Forge](#spa-with-forge)
- [Shared Components](#shared-components)
- [Choosing SSR vs SPA](#choosing-ssr-vs-spa)

**Reference**
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Path Aliases](#path-aliases)
- [Utility Functions](#utility-functions)

---

# Setup

## Getting Started

```sh
bun add @because/hype @because/forge

mkdir -p src/server src/pages src/client src/shared src/css pub
```

Add scripts to `package.json`:

```json
{
  "scripts": {
    "start": "bun run src/server/index.ts",
    "dev": "bun run --hot src/server/index.ts"
  }
}
```

Add the required `tsconfig.json`:

```json
{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "jsxImportSource": "hono/jsx",
    "allowJs": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "baseUrl": ".",
    "paths": {
      "$*": ["src/server/*"],
      "#*": ["src/client/*"],
      "@*": ["src/shared/*"]
    }
  }
}
```

Create a server and page:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype()

export default app.defaults
```

```tsx
// src/pages/index.tsx
export default () => (
  <section>
    <h1>Hello, world!</h1>
  </section>
)
```

```sh
bun dev
```

You have a server-rendered page at `http://localhost:3000`.

---

## Project Structure

```
.
├── package.json
├── tsconfig.json
├── pub/                    # Static files (served as-is at /)
│   └── img/
│       └── logo.png        # => /img/logo.png
├── src/
│   ├── server/
│   │   └── index.ts        # Server entry point
│   ├── pages/
│   │   ├── index.tsx        # => GET /
│   │   ├── about.tsx        # => GET /about
│   │   └── _layout.tsx      # Custom layout (optional)
│   ├── client/
│   │   └── main.ts          # Client JS (auto-included by default layout)
│   ├── shared/
│   │   ├── components.tsx   # Forge components (shared between SSR and SPA)
│   │   └── themes.tsx       # Forge themes
│   └── css/
│       └── main.css         # App CSS (auto-included by default layout)
```

- `src/pages/` — SSR pages, one file per route
- `src/client/` — Client-side TypeScript, transpiled and bundled on demand
- `src/shared/` — Isomorphic code, available to both server and client
- `src/css/` — Stylesheets
- `src/server/` — Server-only code
- `pub/` — Static assets served directly

---

# Hype — Server Framework

## Server Entry Point

Always export `app.defaults` as the default export:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype()

export default app.defaults
// => { port, fetch, idleTimeout: 255 }
```

This triggers lazy route registration and auto-port selection. Bun picks it up as the server config when the file is the entry point.

---

## Routing

### File-based routing

Files in `src/pages/` are automatically mapped to GET routes:

| File | URL |
|------|-----|
| `src/pages/index.tsx` | `/` |
| `src/pages/about.tsx` | `/about` |
| `src/pages/contact.tsx` | `/contact` |

Files prefixed with `_` are private and won't be served (returns 404). This is used for layouts and other internal files.

### Custom routes

Since Hype extends Hono, you can define any route directly:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype()

// JSON API
app.get('/api/users/:id', (c) => {
  return c.json({ id: c.req.param('id'), name: 'Chris' })
})

// Form handling
app.post('/api/contact', async (c) => {
  const body = await c.req.parseBody()
  console.log('Message:', body.message)
  return c.redirect('/thanks')
})

// Custom HTML
app.get('/custom', (c) => {
  return c.html('<h1>Custom page</h1>')
})

export default app.defaults
```

Custom routes are defined before the file-based routes, so they take priority.

---

## Layouts

### Default layout

By default, Hype wraps every page in a simple HTML5 layout that includes:

- `<meta charset="utf-8">`
- `<meta name="viewport">` for responsive design
- `<meta name="color-scheme" content="light dark">` for automatic dark mode
- `<link>` to `/css/main.css` (your `src/css/main.css`)
- `<script>` loading `/client/main.ts` (your `src/client/main.ts`)
- Content wrapped in `<body><main>...</main></body>`

Optionally includes the CSS reset and/or Pico CSS based on your config.

### Custom layout

Create `src/pages/_layout.tsx` to replace the default layout:

```tsx
// src/pages/_layout.tsx
import type { FC } from 'hono/jsx'

const Layout: FC = ({ children, title, props }) => (
  <html lang="en">
    <head>
      <title>{title ?? 'My App'}</title>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />

      <link href="/css/main.css" rel="stylesheet" />
      <script src="/client/main.ts" type="module"></script>
    </head>
    <body>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
      <main>{children}</main>
      <footer>Built with Hype</footer>
    </body>
  </html>
)

export default Layout
```

The layout receives:
- `children` — the rendered page content
- `title` — page title (defaults to `'hype'`)
- `props` — the `HypeProps` passed to the constructor (useful for conditional CSS)

### No layout

Disable the layout entirely for full control (used in SPA mode):

```ts
const app = new Hype({ layout: false })
```

---

## Pages

Pages are `.tsx` files in `src/pages/` that export a default function (or raw JSX).

### Function export (recommended)

```tsx
// src/pages/index.tsx
export default ({ c, req }) => (
  <section>
    <h1>Home</h1>
  </section>
)
```

### Static JSX export

```tsx
// src/pages/about.tsx
export default (
  <section>
    <h1>About</h1>
  </section>
)
```

### Accessing the request

Page components receive `c` (the Hono context) and `req` (the Hono request) as props:

```tsx
// src/pages/greet.tsx
export default ({ req }) => (
  <section>
    <h1>Hello, {req.query('name') ?? 'stranger'}!</h1>
  </section>
)
```

For the full Hono context:

```tsx
// src/pages/debug.tsx
export default ({ c, req }) => {
  const ua = req.header('user-agent')
  return (
    <section>
      <h1>Request Info</h1>
      <p>URL: {req.url}</p>
      <p>User-Agent: {ua}</p>
    </section>
  )
}
```

### Async data in pages

Since pages render on the server, you can use top-level await:

```tsx
// src/pages/users.tsx
const users = await fetch('https://api.example.com/users').then(r => r.json())

export default () => (
  <section>
    <h1>Users</h1>
    <ul>
      {users.map(u => <li>{u.name}</li>)}
    </ul>
  </section>
)
```

Note: top-level data is fetched once at import time and cached. For per-request data, use the `c` context:

```tsx
// src/pages/profile.tsx
export default async ({ c, req }) => {
  const userId = req.query('id')
  const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json())

  return (
    <section>
      <h1>{user.name}</h1>
    </section>
  )
}
```

### Private pages

Prefix a file with `_` to prevent it from being served:

```
src/pages/_layout.tsx    # not a route — used as the layout
src/pages/_helpers.tsx   # not a route — internal helpers
src/pages/index.tsx      # GET /
```

---

## Client-Side JavaScript

### Automatic transpilation

TypeScript files in `src/client/` and `src/shared/` are automatically transpiled and bundled by Bun when requested by the browser. The URL maps directly to the file path:

| File | URL |
|------|-----|
| `src/client/main.ts` | `/client/main.ts` |
| `src/client/app.tsx` | `/client/app.js` |
| `src/shared/utils.ts` | `/shared/utils.ts` |

You can request `.ts` or `.js` extensions — Hype resolves `.ts` and `.tsx` files automatically.

### Module imports

Client-side files can import from each other using relative paths:

```ts
// src/client/main.ts
import { initBurger } from './burger'

initBurger()
```

```ts
// src/client/burger.ts
export function initBurger() {
  document.addEventListener('click', (ev) => {
    const el = (ev?.target as HTMLElement).closest('.burger') as HTMLImageElement
    if (!el) return
    el.src = '/img/bite.png'
  })
}
```

Imports are bundled — the full dependency graph is included in the output, so the browser only needs one request.

### The default layout auto-includes `main.ts`

When using the default layout, `src/client/main.ts` is automatically loaded as a module. Just create the file and it works.

---

## Styling

### External CSS

Put your styles in `src/css/main.css`. The default layout auto-includes it:

```css
/* src/css/main.css */
section {
  max-width: 500px;
  margin: 0 auto;
}
```

You can also serve additional CSS files from `src/css/`:

```html
<link href="/css/components.css" rel="stylesheet" />
```

### Pico CSS

Enable the bundled [Pico CSS](https://picocss.com) for classless styling:

```ts
const app = new Hype({ pico: true })
```

Or include it in a custom layout:

```html
<link href="/css/pico.css" rel="stylesheet" />
```

### CSS Reset

Enable the bundled CSS reset (Josh W. Comeau's reset):

```ts
const app = new Hype({ reset: true })
```

Or include it in a custom layout:

```html
<link href="/css/reset.css" rel="stylesheet" />
```

### Combining options

```ts
const app = new Hype({ pico: true, reset: true })
```

---

## Server-Sent Events (SSE)

Hype provides `app.sse()` for streaming data to the browser.

### Server

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype()

// Stream the current time every second
app.sse('/api/time', (send) => {
  send({ time: Date.now() })
  const interval = setInterval(() => send({ time: Date.now() }), 1000)
  return () => clearInterval(interval) // cleanup on disconnect
})

export default app.defaults
```

The `send` function:
- Automatically `JSON.stringify`s objects; strings are sent as-is
- Accepts an optional second argument for named events: `send(data, 'eventName')`

The handler receives `(send, c)` where `c` is the Hono context:

```ts
app.sse('/api/user-events', (send, c) => {
  const userId = c.req.query('userId')
  // subscribe to user-specific events...
})
```

Return a cleanup function to handle client disconnection.

### Client

```tsx
// src/pages/sse.tsx
export default () => (
  <section>
    <h1>SSE Demo</h1>
    <div id="time" style="font-size: 2em; font-family: monospace;"></div>

    <script src="/client/main.ts" type="module"></script>
  </section>
)
```

```ts
// src/client/main.ts
const timeEl = document.getElementById('time')
const events = new EventSource('/api/time')

events.onmessage = (e) => {
  const data = JSON.parse(e.data)
  timeEl!.textContent = new Date(data.time).toLocaleTimeString()
}

events.onerror = () => {
  timeEl!.textContent = 'Disconnected'
}
```

### Named events

```ts
// Server
app.sse('/api/feed', (send) => {
  send({ type: 'user_joined', name: 'Chris' }, 'activity')
  send({ message: 'Hello everyone!' }, 'chat')
})
```

```js
// Client
const events = new EventSource('/api/feed')

events.addEventListener('activity', (e) => {
  console.log('Activity:', JSON.parse(e.data))
})

events.addEventListener('chat', (e) => {
  console.log('Chat:', JSON.parse(e.data))
})
```

### Test with curl

```sh
curl -N http://localhost:3000/api/time
```

---

## Custom API Routes

Since Hype extends Hono, use any Hono routing method:

```ts
import { Hype } from '@because/hype'

const app = new Hype()

// GET with params
app.get('/api/users/:id', (c) => {
  return c.json({ id: c.req.param('id') })
})

// POST with body parsing
app.post('/api/users', async (c) => {
  const body = await c.req.json()
  return c.json({ created: true, name: body.name }, 201)
})

// Form submission
app.post('/contact', async (c) => {
  const form = await c.req.parseBody()
  console.log(form.email, form.message)
  return c.redirect('/')
})

// Delete
app.delete('/api/users/:id', (c) => {
  return c.json({ deleted: true })
})

export default app.defaults
```

### Redirect back

Use `redirectBack()` to redirect to the referrer:

```ts
import { Hype, redirectBack } from '@because/hype'

const app = new Hype()

app.post('/api/like', async (c) => {
  // ... handle the like
  return redirectBack(c, '/') // falls back to '/' if no referrer
})
```

---

## Sub-Routers

Use `Hype.router()` to create sub-routers without duplicate middleware:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'
import api from './api'

const app = new Hype()
app.route('/api', api)

export default app.defaults
```

```ts
// src/server/api.ts
import { Hype } from '@because/hype'

const api = Hype.router()

api.get('/users', (c) => c.json([{ id: 1, name: 'Chris' }]))
api.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
api.post('/users', async (c) => {
  const body = await c.req.json()
  return c.json(body, 201)
})

export default api
```

`Hype.router()` creates a Hype instance that skips middleware registration (no duplicate logging, static file serving, etc.).

---

## Static Files

Files in `pub/` are served at the root URL:

```
pub/img/logo.png     => /img/logo.png
pub/favicon.ico      => /favicon.ico
pub/robots.txt       => /robots.txt
```

Use them in pages:

```tsx
export default () => (
  <section>
    <img src="/img/logo.png" width="200" />
  </section>
)
```

---

# Forge — Components

## Basic Components

Import `define` and start creating components. Each call to `define` generates real CSS classes and returns a JSX component.

```tsx
import { define } from 'forge'

const Box = define('Box', {
  padding: 20,
  background: '#111',
})

// Renders: <div class="Box">...</div>
<Box>Hello</Box>
```

### Named components

Pass a name as the first argument. The name becomes the CSS class.

```tsx
const Card = define('Card', {
  padding: 20,
  background: '#111',
  borderRadius: 8,
})

// <div class="Card">...</div>
```

Names must be unique — defining the same name twice throws an error.

### Anonymous components

Omit the name and Forge generates one from the base tag: `Div`, `Button2`, `Anchor3`, etc.

```tsx
const Box = define({ display: 'flex', gap: 16 })
// class="Div"

const Link = define({ base: 'a', color: 'blue' })
// class="Anchor"
```

---

## HTML Tags

By default, components render as `<div>`. Use `base` to change the tag:

```tsx
const Button = define('Button', {
  base: 'button',
  padding: 20,
  cursor: 'pointer',
})
// <button class="Button">...</button>

const Heading = define('Heading', {
  base: 'h1',
  fontSize: 28,
})
// <h1 class="Heading">...</h1>
```

### Attribute shorthand

Set default attributes right in the base string:

```tsx
const Radio = define('Radio', {
  base: 'input[type=radio]',
})
// <input type="radio" class="Radio" />

const Checkbox = define('Checkbox', {
  base: 'input[type=checkbox]',
})
// <input type="checkbox" class="Checkbox" />
```

Props passed at usage time override base attributes.

---

## CSS Properties

Write CSS properties in camelCase. They compile to real CSS at definition time.

```tsx
const Card = define('Card', {
  display: 'flex',
  flexDirection: 'column',
  gap: 16,
  padding: 20,
  backgroundColor: '#111',
  borderRadius: 8,
  boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
})
```

Generated CSS:

```css
.Card {
  background-color: #111;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 20px;
}
```

### Numeric values

Numbers are auto-converted to `px`:

```tsx
{ padding: 20 }     // padding: 20px
{ margin: 0 }       // margin: 0px
{ fontSize: 14 }    // font-size: 14px
{ borderRadius: 8 } // border-radius: 8px
```

Except for unitless properties, which stay as plain numbers:

```tsx
{ opacity: 0.5 }    // opacity: 0.5
{ zIndex: 10 }      // z-index: 10
{ flex: 1 }         // flex: 1
{ flexGrow: 2 }     // flex-grow: 2
{ flexShrink: 0 }   // flex-shrink: 0
{ fontWeight: 400 }  // font-weight: 400
{ lineHeight: 1.6 } // line-height: 1.6
{ order: 3 }        // order: 3
```

Strings are passed through as-is:

```tsx
{ padding: '12px 24px' }    // padding: 12px 24px
{ border: '1px solid #222' } // border: 1px solid #222
{ margin: '0 auto' }        // margin: 0 auto
```

### All supported properties

Every standard CSS property is supported in camelCase form: layout (`display`, `position`, `flexDirection`, `gridTemplateColumns`, etc.), box model (`margin`, `padding`, `width`, `height`, etc.), typography (`fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, etc.), visual (`background`, `border`, `boxShadow`, `opacity`, etc.), animation (`transition`, `animation`, `transform`, etc.), and SVG (`fill`, `stroke`, `strokeWidth`, etc.).

---

## States

Pseudo-selectors like `:hover`, `:focus`, `:active`, and `:disabled`.

```tsx
const Button = define('Button', {
  base: 'button',
  padding: 20,
  background: 'blue',
  cursor: 'pointer',

  states: {
    ':hover': { background: 'darkblue' },
    ':active': { transform: 'translateY(1px)' },
    ':focus': { outline: '2px solid white' },
    ':disabled': { opacity: 0.3, cursor: 'not-allowed' },
  },
})
```

The colon is optional — `hover` and `:hover` are equivalent:

```tsx
states: {
  hover: { background: 'darkblue' },  // same as ':hover'
}
```

### Complex pseudo-selectors

Use the full string for compound selectors:

```tsx
states: {
  ':not(:disabled):hover': { background: 'darkblue' },
  ':not(:disabled):active': { transform: 'translateY(1px)' },
}
```

Generated CSS:

```css
.Button:not(:disabled):hover { background: darkblue; }
.Button:not(:disabled):active { transform: translateY(1px); }
```

---

## Variants

Variants are typed props that apply conditional CSS classes. They replace inline styles with a clean, declarative API.

### Boolean variants

A variant whose value is a style object. Activated by passing `true`.

```tsx
const Button = define('Button', {
  base: 'button',
  padding: 20,
  background: 'blue',

  variants: {
    disabled: {
      opacity: 0.3,
      cursor: 'not-allowed',
    },
    rounded: {
      borderRadius: 999,
    },
  },
})
```

```tsx
<Button>Normal</Button>
<Button disabled>Disabled</Button>
<Button rounded>Pill</Button>
<Button disabled rounded>Both</Button>
```

Generated CSS:

```css
.Button { padding: 20px; background: blue; }
.Button.disabled { cursor: not-allowed; opacity: 0.3; }
.Button.rounded { border-radius: 999px; }
```

HTML output for `<Button disabled rounded>`:

```html
<button class="Button disabled rounded">Both</button>
```

Variant props are consumed by Forge and **not** passed to the HTML element.

### Keyed variants

A variant whose value is an object of named options. Activated by passing a string.

```tsx
const Button = define('Button', {
  base: 'button',
  padding: 16,

  variants: {
    intent: {
      primary: { background: 'blue', color: 'white' },
      secondary: { background: '#333', color: '#ccc' },
      danger: { background: 'red', color: 'white' },
      ghost: { background: 'transparent', color: 'gray' },
    },
    size: {
      small: { padding: '8px 16px', fontSize: 12 },
      large: { padding: '16px 32px', fontSize: 16 },
    },
  },
})
```

```tsx
<Button intent="primary">Save</Button>
<Button intent="danger" size="large">Delete Account</Button>
<Button intent="ghost" size="small">Cancel</Button>
```

Generated CSS:

```css
.Button.intent-primary { background: blue; color: white; }
.Button.intent-danger { background: red; color: white; }
.Button.size-small { font-size: 12px; padding: 8px 16px; }
.Button.size-large { font-size: 16px; padding: 16px 32px; }
```

### Variants with states

Variants can include their own pseudo-selectors:

```tsx
variants: {
  intent: {
    danger: {
      background: 'red',
      states: {
        ':not(:disabled):hover': { background: '#cc0000' },
      },
    },
    secondary: {
      background: '#333',
      states: {
        ':not(:disabled):hover': { borderColor: 'green' },
      },
    },
  },
}
```

### Combining multiple variants

Multiple keyed and boolean variants can be used together freely:

```tsx
<Button intent="primary" size="small">Small Primary</Button>
<Button intent="danger" size="large" disabled>Large Danger Disabled</Button>
```

---

## Parts

Parts are named sub-components within a component. They get their own CSS classes and can have their own base tags, states, and selectors.

```tsx
const Card = define('Card', {
  padding: 20,
  background: '#111',

  parts: {
    Header: {
      base: 'h2',
      fontSize: 24,
      marginBottom: 12,
    },
    Body: {
      fontSize: 14,
      lineHeight: 1.6,
      color: '#888',
    },
    Footer: {
      base: 'footer',
      marginTop: 16,
      paddingTop: 12,
      borderTop: '1px solid #333',
      fontSize: 12,
    },
  },

  render({ props, parts: { Root, Header, Body, Footer } }) {
    return (
      <Root>
        <Header>{props.title}</Header>
        <Body>{props.children}</Body>
        <Footer>{props.footer}</Footer>
      </Root>
    )
  },
})
```

```tsx
<Card title="Welcome" footer="Last updated today">
  This is the card body content.
</Card>
```

Generated CSS classes:

```css
.Card { padding: 20px; background: #111; }
.Card_Header { font-size: 24px; margin-bottom: 12px; }
.Card_Body { color: #888; font-size: 14px; line-height: 1.6; }
.Card_Footer { border-top: 1px solid #333; font-size: 12px; margin-top: 16px; padding-top: 12px; }
```

### Parts with states

Parts can have their own pseudo-selectors:

```tsx
parts: {
  Tab: {
    base: 'button',
    color: '#888',
    borderBottom: '1px solid transparent',

    states: {
      ':hover': { color: '#fff' },
    },
  },
}
```

### Variants that affect parts

Variants can override styles on specific parts:

```tsx
const UserProfile = define('UserProfile', {
  padding: 24,

  parts: {
    Avatar: { base: 'img', width: 64, height: 64, borderRadius: '50%' },
    Name: { fontSize: 18 },
    Bio: { fontSize: 14, color: '#888' },
  },

  variants: {
    size: {
      compact: {
        padding: 16,              // override root
        parts: {
          Avatar: { width: 48, height: 48 },  // override part
          Name: { fontSize: 16 },
        },
      },
      large: {
        padding: 32,
        parts: {
          Avatar: { width: 96, height: 96 },
          Name: { fontSize: 24 },
        },
      },
    },
    verified: {
      parts: {
        Avatar: { border: '2px solid gold' },
      },
    },
  },

  render({ props, parts: { Root, Avatar, Name, Bio } }) {
    return (
      <Root>
        <Avatar src={props.avatarUrl} alt={props.name} />
        <Name>{props.name}{props.verified && ' ✓'}</Name>
        <Bio>{props.bio}</Bio>
      </Root>
    )
  },
})
```

```tsx
<UserProfile size="compact" name="Alex" ... />
<UserProfile size="large" verified name="Jordan" ... />
```

Generated CSS:

```css
.UserProfile { padding: 24px; }
.UserProfile_Avatar { border-radius: 50%; height: 64px; width: 64px; }
.UserProfile.size-compact { padding: 16px; }
.UserProfile_Avatar.size-compact { height: 48px; width: 48px; }
.UserProfile_Avatar.verified { border: 2px solid gold; }
```

### Applying variants to parts in render

Inside `render()`, you can pass variant props directly to part components:

```tsx
const Tabs = define('Tabs', {
  display: 'flex',

  parts: {
    Tab: {
      base: 'button',
      color: '#888',
      borderBottom: '1px solid transparent',
    },
  },

  variants: {
    active: {
      parts: {
        Tab: {
          color: 'green',
          borderBottom: '1px solid green',
        },
      },
    },
  },

  render({ props, parts: { Root, Tab } }) {
    return (
      <Root>
        {props.items?.map((item: any) => (
          <Tab active={item.active}>{item.label}</Tab>
        ))}
      </Root>
    )
  },
})
```

The `active` prop on `<Tab>` adds the variant class to that specific tab instance. It is not passed through to the HTML.

---

## Custom Render

Override the default rendering with a `render` function. It receives `props` (everything passed to the component) and `parts` (component functions for Root and all named parts).

```tsx
const FormGroup = define('FormGroup', {
  marginBottom: 24,

  parts: {
    Label: { base: 'label', display: 'block', fontSize: 14, marginBottom: 8 },
    Helper: { fontSize: 12, color: '#888', marginTop: 6 },
    Error: { fontSize: 12, color: 'red', marginTop: 6 },
  },

  render({ props, parts: { Root, Label, Helper, Error } }) {
    return (
      <Root>
        {props.label && <Label>{props.label}</Label>}
        {props.children}
        {props.helper && <Helper>{props.helper}</Helper>}
        {props.error && <Error>{props.error}</Error>}
      </Root>
    )
  },
})
```

```tsx
<FormGroup label="Email" helper="We'll never share your email">
  <Input type="email" placeholder="you@example.com" />
</FormGroup>

<FormGroup label="Username" error="Username is already taken">
  <Input status="error" value="admin" />
</FormGroup>
```

### Destructuring props

Destructure to separate custom props from HTML passthrough props:

```tsx
render({ props: { title, subtitle, ...rest }, parts: { Root, H2, P } }) {
  return (
    <Root {...rest}>
      <H2>{title}</H2>
      <P>{subtitle}</P>
    </Root>
  )
}
```

### Without render

If no `render` is provided, the component renders its children into the root tag:

```tsx
const Box = define('Box', { padding: 20 })

// Equivalent to:
// render({ props, parts: { Root } }) {
//   return <Root {...props}>{props.children}</Root>
// }
```

---

## Selectors

The `selectors` key lets you write custom CSS selectors. Use `&` for the current element and `@PartName` to reference other parts.

### Basic selectors

```tsx
const NavLink = define('NavLink', {
  base: 'a',
  color: '#888',
  textDecoration: 'none',

  selectors: {
    '&:hover': { color: '#fff' },
    '&[aria-current]': { color: '#fff', textDecoration: 'underline' },
  },
})
```

```tsx
<NavLink href="/home" aria-current="page">Home</NavLink>
<NavLink href="/about">About</NavLink>
```

### Cross-part selectors

Reference other parts with `@PartName`. This is the mechanism that enables CSS-only interactive components.

```tsx
const Checkbox = define('Checkbox', {
  parts: {
    Input: {
      base: 'input[type=checkbox]',
      display: 'none',
    },
    Label: {
      base: 'label',
      padding: 10,
      cursor: 'pointer',
      color: 'gray',

      selectors: {
        // When the Input is checked, style this Label
        '@Input:checked + &': {
          color: 'green',
          fontWeight: 'bold',
        },
        // When the Input is disabled, style this Label
        '@Input:disabled + &': {
          opacity: 0.5,
          cursor: 'not-allowed',
        },
      },
    },
  },

  render({ props, parts: { Root, Input, Label } }) {
    return (
      <Root>
        <Input id={props.id} checked={props.checked} disabled={props.disabled} />
        <Label for={props.id}>{props.label}</Label>
      </Root>
    )
  },
})
```

The `@Input` in `@Input:checked + &` is replaced with `.Checkbox_Input`, and `&` is replaced with `.Checkbox_Label`, producing:

```css
.Checkbox_Input:checked + .Checkbox_Label {
  color: green;
  font-weight: bold;
}
```

### CSS-only tab switcher

Hidden radio inputs + sibling selectors = tabs without JavaScript:

```tsx
const TabSwitcher = define('TabSwitcher', {
  parts: {
    Input: {
      base: 'input[type=radio]',
      display: 'none',
    },
    TabBar: {
      display: 'flex',
      borderBottom: '1px solid #333',
    },
    TabLabel: {
      base: 'label',
      padding: '12px 24px',
      color: '#888',
      cursor: 'pointer',

      states: {
        ':hover': { color: '#fff' },
      },

      selectors: {
        '@Input:checked + &': {
          color: 'green',
          borderBottom: '1px solid green',
        },
      },
    },
    Content: {
      display: 'none',
      padding: 24,

      selectors: {
        // General sibling combinator: when a radio is checked,
        // show the corresponding content panel
        '@Input:checked ~ &': { display: 'block' },
      },
    },
  },

  render({ props, parts: { Root, Input, TabBar, TabLabel, Content } }) {
    return (
      <Root>
        <TabBar>
          {props.tabs?.map((tab: any, i: number) => (
            <>
              <Input id={`${props.name}-${tab.id}`} name={props.name} checked={i === 0} />
              <TabLabel for={`${props.name}-${tab.id}`}>{tab.label}</TabLabel>
            </>
          ))}
        </TabBar>
        {props.tabs?.map((tab: any) => (
          <Content>{tab.content}</Content>
        ))}
      </Root>
    )
  },
})
```

```tsx
<TabSwitcher
  name="demo"
  tabs={[
    { id: 'one', label: 'Tab 1', content: <p>First panel</p> },
    { id: 'two', label: 'Tab 2', content: <p>Second panel</p> },
    { id: 'three', label: 'Tab 3', content: <p>Third panel</p> },
  ]}
/>
```

### Selector reference

| Selector | Meaning |
|---|---|
| `&:hover` | This element on hover |
| `&[aria-current]` | This element when attribute is present |
| `@Input:checked + &` | This element when adjacent Input is checked |
| `@Input:checked ~ &` | This element when any preceding Input sibling is checked |
| `@Input:disabled + &` | This element when adjacent Input is disabled |
| `@Input:checked + &:hover` | This element on hover, but only when Input is checked |

---

## Scopes

When building a family of related components, `createScope` prefixes all names automatically:

```tsx
import { createScope } from 'forge'

const { define } = createScope('Button')

const Button = define('Root', {     // CSS class: "Button" (Root is special)
  base: 'button',
  padding: 20,
})

const ButtonRow = define('Row', {   // CSS class: "ButtonRow"
  display: 'flex',
  gap: 16,
})

const ButtonIcon = define('Icon', { // CSS class: "ButtonIcon"
  width: 20,
  height: 20,
})
```

`Root` is the special case — `define('Root', ...)` with scope `'Button'` produces class `Button`, not `ButtonRoot`.

---

## Themes

Built-in CSS custom properties with type safety.

### Define themes

Create a theme file with your tokens:

```tsx
// darkTheme.tsx
export default {
  'colors-bg': '#0a0a0a',
  'colors-bgElevated': '#111',
  'colors-fg': '#00ff00',
  'colors-fgMuted': '#888',
  'colors-border': '#222',
  'colors-accent': '#00ff00',
  'colors-accentDim': '#008800',

  'fonts-mono': "'Monaco', 'Menlo', monospace",

  'spacing-sm': '12px',
  'spacing-md': '16px',
  'spacing-lg': '24px',
  'spacing-xl': '32px',

  'radius-sm': '4px',
  'radius-md': '8px',
} as const
```

```tsx
// lightTheme.tsx
export default {
  'colors-bg': '#f5f5f0',
  'colors-bgElevated': '#fff',
  'colors-fg': '#0a0a0a',
  'colors-fgMuted': '#666',
  'colors-border': '#ddd',
  'colors-accent': '#0066cc',
  'colors-accentDim': '#004499',

  'fonts-mono': "'Monaco', 'Menlo', monospace",

  'spacing-sm': '12px',
  'spacing-md': '16px',
  'spacing-lg': '24px',
  'spacing-xl': '32px',

  'radius-sm': '4px',
  'radius-md': '8px',
} as const
```

### Register themes

```tsx
// themes.tsx
import { createThemes } from 'forge'
import darkTheme from './darkTheme'
import lightTheme from './lightTheme'

export const theme = createThemes({
  dark: darkTheme,
  light: lightTheme,
})
```

`createThemes` returns a typed function. It only accepts keys that exist in your theme objects.

### Use theme values

`theme('key')` returns `var(--theme-key)`:

```tsx
import { theme } from './themes'

const Card = define('Card', {
  padding: theme('spacing-lg'),        // var(--theme-spacing-lg)
  background: theme('colors-bgElevated'), // var(--theme-colors-bgElevated)
  color: theme('colors-fg'),           // var(--theme-colors-fg)
  border: `1px solid ${theme('colors-border')}`,
  borderRadius: theme('radius-md'),
})
```

### Switch themes

Themes are controlled by the `data-theme` attribute:

```tsx
// Set initial theme
<body data-theme="dark">

// Switch at runtime
document.body.setAttribute('data-theme', 'light')
```

Generated CSS:

```css
[data-theme="dark"] {
  --theme-colors-bg: #0a0a0a;
  --theme-colors-fg: #00ff00;
  /* ... */
}
[data-theme="light"] {
  --theme-colors-bg: #f5f5f0;
  --theme-colors-fg: #0a0a0a;
  /* ... */
}
```

### Other theme APIs

```tsx
// Register a single theme
import { createTheme } from 'forge'
createTheme('dark', { bgColor: '#000', fgColor: '#fff' })

// Extend existing themes with new tokens
import { extendThemes } from 'forge'
extendThemes({ dark: { 'colors-success': '#0f0' } })

// Untyped fallback for dynamic theme keys
import { themeVar } from 'forge'
themeVar('colors-bg')  // 'var(--theme-colors-bg)'
```

---

# Integration

## SSR with Forge

In SSR mode, Forge components render on the server and `<Styles />` inlines all generated CSS into the page. Because Hype imports pages at startup, all `define()` calls run before any request — so `<Styles />` always has the complete CSS.

### Server

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype({ layout: false })

export default app.defaults
```

### Layout with `<Styles />`

```tsx
// src/pages/_layout.tsx
import type { FC } from 'hono/jsx'
import { Styles } from 'forge'

const Layout: FC = ({ children, title }) => (
  <html lang="en">
    <head>
      <title>{title ?? 'My App'}</title>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="color-scheme" content="light dark" />
      <Styles />
    </head>
    <body data-theme="dark">{children}</body>
  </html>
)

export default Layout
```

### Shared components

```tsx
// src/shared/components.tsx
import { define } from 'forge'

export const Button = define('Button', {
  base: 'button',
  padding: '10px 20px',
  background: '#111',
  color: '#fff',
  border: 'none',
  borderRadius: 6,
  cursor: 'pointer',

  states: {
    hover: { background: '#333' },
  },

  variants: {
    intent: {
      primary: { background: 'blue' },
      danger: { background: 'red' },
    },
  },
})

export const Card = define('Card', {
  padding: 24,
  background: '#111',
  borderRadius: 8,
  border: '1px solid #222',

  parts: {
    Title: { base: 'h2', fontSize: 20, marginBottom: 12 },
    Body: { fontSize: 14, color: '#888', lineHeight: 1.6 },
  },

  render({ props, parts: { Root, Title, Body } }) {
    return (
      <Root>
        <Title>{props.title}</Title>
        <Body>{props.children}</Body>
      </Root>
    )
  },
})
```

### Pages

Use Forge components directly in Hype pages — they're just JSX:

```tsx
// src/pages/index.tsx
import { Button, Card } from '@components'

export default () => (
  <main>
    <Card title="Welcome">
      This page is server-rendered with Forge components.
    </Card>
    <Button intent="primary">Get Started</Button>
  </main>
)
```

### Adding client interactivity to SSR

For interactivity on SSR pages, use a client-side script alongside Forge components:

```tsx
// src/pages/index.tsx
import { Button } from '@components'

export default () => (
  <section>
    <Button intent="primary" id="my-btn">Click me</Button>
    <script src="/client/main.ts" type="module"></script>
  </section>
)
```

```ts
// src/client/main.ts
document.getElementById('my-btn')?.addEventListener('click', () => {
  alert('Clicked!')
})
```

### SSR with Forge + SSE

A server-rendered page with styled components that updates live:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype({ layout: false })

let count = 0

app.sse('/api/counter', (send) => {
  send({ count })
  const interval = setInterval(() => send({ count: ++count }), 1000)
  return () => clearInterval(interval)
})

export default app.defaults
```

```tsx
// src/pages/index.tsx
import { Card, Badge } from '@components'

export default () => (
  <section>
    <Card title="Live Counter">
      Count: <Badge><span id="count">--</span></Badge>
    </Card>
    <script src="/client/main.ts" type="module"></script>
  </section>
)
```

```ts
// src/client/main.ts
const el = document.getElementById('count')!
const events = new EventSource('/api/counter')
events.onmessage = (e) => {
  el.textContent = JSON.parse(e.data).count
}
```

---

## SPA with Forge

In SPA mode, Hype serves a minimal HTML shell and Forge runs entirely in the browser. Forge auto-injects a `<style id="forge-styles">` tag into `document.head` — no `<Styles />` needed.

### Server

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype({ layout: false })

export default app.defaults
```

### HTML Shell

The page is just a mount point. No Forge imports here — everything runs client-side:

```tsx
// src/pages/index.tsx
export default () => (
  <html lang="en">
    <head>
      <title>My SPA</title>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <meta name="color-scheme" content="light dark" />
    </head>
    <body data-theme="dark">
      <div id="root" />
      <script src="/client/app.js" type="module" />
    </body>
  </html>
)
```

No `<Styles />`, no CSS link. Forge handles it in the browser.

### Client entry

```tsx
// src/client/app.tsx
import { render } from 'hono/jsx/dom'
import { useState } from 'hono/jsx/dom'
import { define } from 'forge'

const Button = define('Button', {
  base: 'button',
  padding: '10px 20px',
  background: 'blue',
  color: 'white',
  border: 'none',
  borderRadius: 6,
  cursor: 'pointer',

  states: {
    hover: { background: 'darkblue' },
  },
})

const Card = define('Card', {
  padding: 24,
  background: '#111',
  borderRadius: 8,
})

function App() {
  const [count, setCount] = useState(0)

  return (
    <Card>
      <h1>Count: {count}</h1>
      <Button onClick={() => setCount(c => c + 1)}>Increment</Button>
    </Card>
  )
}

render(<App />, document.getElementById('root')!)
```

When `define()` runs in the browser, Forge immediately injects the CSS. Hype's auto-transpilation bundles everything — Forge included — into a single JS file served at `/client/app.js`.

### Extracting components

Keep components in separate files and import them. Hype's bundler resolves all imports:

```tsx
// src/client/components.tsx
import { define } from 'forge'

export const Button = define('Button', {
  base: 'button',
  padding: '10px 20px',
  background: 'blue',
  color: 'white',
  border: 'none',
  borderRadius: 6,
  cursor: 'pointer',
})

export const Stack = define('Stack', {
  display: 'flex',
  flexDirection: 'column',
  gap: 16,
})
```

```tsx
// src/client/app.tsx
import { render } from 'hono/jsx/dom'
import { Button, Stack } from './components'

function App() {
  return (
    <Stack>
      <h1>My App</h1>
      <Button>Click me</Button>
    </Stack>
  )
}

render(<App />, document.getElementById('root')!)
```

### Passing server data to the SPA

Inject data from the server into the HTML shell, then read it client-side:

```tsx
// src/pages/index.tsx
const config = {
  apiUrl: process.env.API_URL ?? '/api',
  version: '1.0.0',
}

export default () => (
  <html lang="en">
    <head>
      <meta charset="utf-8" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    </head>
    <body data-theme="dark">
      <div id="root" />
      <script dangerouslySetInnerHTML={{
        __html: `window.__CONFIG__ = ${JSON.stringify(config)};`
      }} />
      <script src="/client/app.js" type="module" />
    </body>
  </html>
)
```

```tsx
// src/client/app.tsx
const config = (window as any).__CONFIG__
```

Use `dangerouslySetInnerHTML` — Hono's JSX escapes `<script>` content by default.

### Cache-busting

Use the git hash to bust asset caches:

```tsx
// src/pages/index.tsx
import { $ } from 'bun'

const GIT_HASH = await $`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => 'dev')

export default () => (
  <html lang="en">
    <head>
      <link href={`/css/main.css?${GIT_HASH}`} rel="stylesheet" />
    </head>
    <body>
      <div id="root" />
      <script src={`/client/app.js?${GIT_HASH}`} type="module" />
    </body>
  </html>
)
```

### SPA with API routes

Hype serves the shell and API; Forge renders the client:

```ts
// src/server/index.ts
import { Hype } from '@because/hype'

const app = new Hype({ layout: false })

const todos: { id: number; text: string }[] = []
let nextId = 1

app.get('/api/todos', (c) => c.json(todos))

app.post('/api/todos', async (c) => {
  const { text } = await c.req.json()
  const todo = { id: nextId++, text }
  todos.push(todo)
  return c.json(todo, 201)
})

app.delete('/api/todos/:id', (c) => {
  const id = Number(c.req.param('id'))
  const idx = todos.findIndex(t => t.id === id)
  if (idx !== -1) todos.splice(idx, 1)
  return c.json({ deleted: true })
})

export default app.defaults
```

```tsx
// src/client/app.tsx
import { render } from 'hono/jsx/dom'
import { useState, useEffect } from 'hono/jsx/dom'
import { define } from 'forge'

const Stack = define('Stack', {
  display: 'flex',
  flexDirection: 'column',
  gap: 12,
  padding: 24,
  maxWidth: 500,
  margin: '0 auto',
})

const TodoItem = define('TodoItem', {
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
  padding: 12,
  background: '#111',
  borderRadius: 6,
})

const Button = define('Button', {
  base: 'button',
  padding: '8px 16px',
  background: '#3b82f6',
  color: 'white',
  border: 'none',
  borderRadius: 6,
  cursor: 'pointer',

  states: {
    hover: { background: '#2563eb' },
  },

  variants: {
    danger: {
      background: '#ef4444',
      states: { hover: { background: '#dc2626' } },
    },
  },
})

const Input = define('Input', {
  base: 'input',
  padding: 10,
  background: '#111',
  color: '#fff',
  border: '1px solid #333',
  borderRadius: 6,
  flex: 1,
  marginRight: 8,
})

function App() {
  const [todos, setTodos] = useState<{ id: number; text: string }[]>([])
  const [text, setText] = useState('')

  useEffect(() => {
    fetch('/api/todos').then(r => r.json()).then(setTodos)
  }, [])

  const add = async () => {
    if (!text.trim()) return
    const res = await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ text }),
    })
    const todo = await res.json()
    setTodos(prev => [...prev, todo])
    setText('')
  }

  const remove = async (id: number) => {
    await fetch(`/api/todos/${id}`, { method: 'DELETE' })
    setTodos(prev => prev.filter(t => t.id !== id))
  }

  return (
    <Stack>
      <h1>Todos</h1>
      <div style={{ display: 'flex' }}>
        <Input
          value={text}
          onInput={(e: any) => setText(e.target.value)}
          placeholder="What needs doing?"
        />
        <Button onClick={add}>Add</Button>
      </div>
      {todos.map(todo => (
        <TodoItem key={todo.id}>
          <span>{todo.text}</span>
          <Button danger onClick={() => remove(todo.id)}>Delete</Button>
        </TodoItem>
      ))}
    </Stack>
  )
}

render(<App />, document.getElementById('root')!)
```

---

## Shared Components

When a component has no client-side hooks or event handlers, it works identically in both SSR pages and SPA code. Put these in `src/shared/`:

```tsx
// src/shared/components.tsx
import { define } from 'forge'

export const Badge = define('Badge', {
  base: 'span',
  display: 'inline-block',
  padding: '4px 10px',
  fontSize: 12,
  borderRadius: 999,
  background: '#3b82f6',
  color: 'white',

  variants: {
    muted: {
      background: '#333',
      color: '#888',
    },
  },
})
```

Use it in an SSR page:

```tsx
// src/pages/index.tsx
import { Badge } from '@components'

export default () => (
  <section>
    <h1>Status: <Badge>Active</Badge></h1>
  </section>
)
```

Use the same component in a SPA:

```tsx
// src/client/app.tsx
import { Badge } from '@components'

function StatusBar() {
  return <Badge muted>Draft</Badge>
}
```

Hype's bundler handles the import in both cases. On the server, `define()` registers CSS that `<Styles />` collects. In the browser, `define()` injects CSS into the document head.

---

## Choosing SSR vs SPA

| | SSR | SPA |
|---|---|---|
| **Styles** | `<Styles />` in the layout head | Auto-injected by Forge in browser |
| **Interactivity** | Client scripts via `/client/main.ts` | Full `hono/jsx/dom` with hooks |
| **Routing** | File-based, one page per route | Client-side, single HTML shell |
| **Data** | Fetch in page components (`c`, `req`) | Fetch in `useEffect`, SSE, etc. |
| **Best for** | Content sites, dashboards, forms | Interactive apps, real-time UIs |

Both approaches use the same Forge components. The only difference is where `define()` runs (server vs browser) and how the CSS gets into the page (`<Styles />` vs auto-injection).

---

# Reference

## Configuration

### Constructor options

```ts
const app = new Hype({
  pico: true,       // Include Pico CSS (default: false)
  reset: true,      // Include CSS reset (default: false)
  prettyHTML: true,  // Pretty-print HTML in dev (default: true in dev, false in prod)
  layout: true,     // Wrap pages in layout (default: true)
  logging: true,    // HTTP request logging (default: true)
  ok: true,         // Add GET /ok healthcheck (default: false)
})
```

---

## Environment Variables

| Variable | Effect |
|----------|--------|
| `PORT` | Server port (default: `3000`) |
| `NODE_ENV` | Set to `production` to disable auto-port, HTML prettification, and enable transpile caching |
| `NO_AUTOPORT` | Disable auto-port selection even in dev |

In dev mode, if port 3000 is busy, Hype automatically tries 3001, 3002, etc. (up to 100 attempts).

---

## Path Aliases

The recommended `tsconfig.json` defines three path aliases:

| Alias | Maps to | Use for |
|-------|---------|---------|
| `$*` | `src/server/*` | Server-only imports |
| `#*` | `src/client/*` | Client-only imports |
| `@*` | `src/shared/*` | Isomorphic imports |

```ts
// In server code:
import { db } from '$db'         // => src/server/db.ts

// In client code:
import { format } from '@utils'  // => src/shared/utils.ts
```

---

## Utility Functions

Hype exports a collection of helpers from `@because/hype` (or `@because/hype/utils`):

### Randomness

```ts
import { rand, randRange, randItem, randIndex, shuffle, weightedRand, randomId } from '@because/hype'

rand()            // 1 or 2 (coin flip)
rand(6)           // 1-6 (roll a die)
rand(20)          // 1-20 (d20)
randRange(5, 10)  // 5-10 inclusive
randItem(['a', 'b', 'c'])   // random element
randIndex(['a', 'b', 'c'])  // random index (0, 1, or 2)
shuffle([1, 2, 3, 4, 5])    // shuffled copy
weightedRand()    // 1-10, lower numbers more likely
randomId()        // e.g. "k7x2m1"
```

### Arrays

```ts
import { times, unique } from '@because/hype'

times(5)                    // [1, 2, 3, 4, 5]
unique([1, 1, 2, 2, 3, 3]) // [1, 2, 3]
```

### Colors

```ts
import { lightenColor, darkenColor } from '@because/hype'

lightenColor('#3498db', 0.5)  // blend halfway to white
darkenColor('#3498db', 0.5)   // blend halfway to black
```

### Strings

```ts
import { capitalize } from '@because/hype'

capitalize('hello')  // "Hello"
```

### Dark mode detection (client-side)

```ts
import { isDarkMode } from '@because/hype'

if (isDarkMode()) {
  // user prefers dark mode
}
```

### Transpilation

```ts
import { transpile } from '@because/hype'

const js = await transpile('./src/client/app.tsx')
// => bundled ESM JavaScript string
```

### Forge SSR helpers

```tsx
// Inline all Forge CSS as a <style> tag
import { Styles } from 'forge'
<Styles />

// Get raw CSS string (e.g. to write to a file)
import { stylesToCSS } from 'forge'
const css = stylesToCSS()
Bun.write('public/main.css', css)
```

### Forge HMR

Forge automatically clears and re-registers styles when modules are hot-reloaded via `import.meta.hot.dispose`. No setup needed.

---

## Hono Middleware

Any Hono middleware works with Hype:

```ts
import { Hype } from '@because/hype'
import { cors } from 'hono/cors'
import { basicAuth } from 'hono/basic-auth'

const app = new Hype()

// CORS for API routes
app.use('/api/*', cors())

// Basic auth for admin
app.use('/admin/*', basicAuth({
  username: 'admin',
  password: process.env.ADMIN_PASSWORD!,
}))

export default app.defaults
```
