# Toes - Guide to Writing Apps Toes manages and runs web apps, each on its own port. Apps are server-rendered TypeScript using **Hype** (wraps Hono) and **Forge** (CSS-in-JS). Runtime is **Bun**. ## Required Components Every toes app/tool must have: 1. **`.npmrc`** pointing to `registry=https://npm.nose.space` (the private registry for `@because/*` packages) 2. **`package.json`** with a `scripts.toes` entry (this is how toes discovers and runs apps) 3. **HTTP `GET /ok`** returning 200 (health check endpoint — toes polls this every 30s and restarts unresponsive apps) ## App vs Tool An **app** shows in the sidebar and opens in its own browser tab. A **tool** renders as a tab inside the dashboard (in an iframe). It receives `?app=` to know the selected app. The only code difference is `"toes": { "tool": true }` in package.json and some extra imports from `@because/toes`. ## Required Files Every app needs `.npmrc`, `tsconfig.json`, `package.json`, and `index.tsx`. **.npmrc** -- always this exact content: ``` registry=https://npm.nose.space ``` **tsconfig.json** -- use exactly, do not improvise: ```json { "compilerOptions": { "lib": ["ESNext"], "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, "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "baseUrl": ".", "paths": { "$*": ["src/server/*"], "#*": ["src/client/*"], "@*": ["src/shared/*"] } } } ``` **package.json** for an app: ```json { "name": "my-app", "private": true, "module": "index.tsx", "type": "module", "scripts": { "toes": "bun run --watch index.tsx" }, "toes": { "icon": "🖥️" }, "dependencies": { "@because/forge": "*", "@because/hype": "*" }, "devDependencies": { "@types/bun": "latest" } } ``` For a **tool**, add `@because/toes` to dependencies and set `"tool": true` (or a string for a custom tab label like `"tool": ".env"`): ```json { "toes": { "icon": "🔧", "tool": true }, "dependencies": { "@because/forge": "*", "@because/hype": "*", "@because/toes": "*" } } ``` ## Hype Hype wraps Hono. It adds `app.defaults` (the Bun server export), `app.sse()` for server-sent events, and `Hype.router()` for sub-routers. Everything else is standard Hono. ```tsx import { Hype } from '@because/hype' const app = new Hype() app.get('/', c => c.html(

Hello

)) app.get('/ok', c => c.text('ok')) // Health check -- required export default app.defaults ``` Constructor options: `prettyHTML` (default true, tools should set false), `layout` (default true), `logging` (default true). **SSE** -- the one non-Hono addition: ```tsx app.sse('/stream', (send, c) => { send({ hello: 'world' }) const interval = setInterval(() => send({ time: Date.now() }), 1000) return () => clearInterval(interval) // cleanup on disconnect }) ``` **Sub-routers:** ```tsx const api = Hype.router() api.get('/items', c => c.json([])) app.route('/api', api) // mounts at /api/items ``` ## Forge Forge creates styled JSX components via `define()`. Properties use camelCase CSS. Numbers auto-convert to `px` (except `flex`, `opacity`, `zIndex`, `fontWeight`). ```tsx import { define, stylesToCSS } from '@because/forge' const Box = define('Box', { padding: 20, borderRadius: '6px', }) // content renders
content
``` **`base`** -- set the HTML element (default `div`): ```tsx const Button = define('Button', { base: 'button', padding: '8px 16px' }) const Link = define('Link', { base: 'a', textDecoration: 'none' }) ``` **`states`** -- pseudo-classes: ```tsx const Item = define('Item', { padding: 12, states: { ':hover': { backgroundColor: '#eee' }, ':last-child': { borderBottom: 'none' }, }, }) ``` **`selectors`** -- nested CSS (`&` = the component): ```tsx const List = define('List', { selectors: { '& > li:last-child': { borderBottom: 'none' }, }, }) ``` **`variants`** -- conditional styles via props: ```tsx const Button = define('Button', { base: 'button', variants: { variant: { primary: { backgroundColor: '#2563eb', color: 'white' }, danger: { backgroundColor: '#dc2626', color: 'white' }, }, }, }) // ``` **Serving CSS** -- apps serve `stylesToCSS()` from a route. Tools prepend `baseStyles`: ```tsx app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) ) ``` ## Theme Tokens Tools import `theme` from `@because/toes/tools`. It returns CSS variables that resolve per light/dark mode. ```tsx import { baseStyles, ToolScript, theme } from '@because/toes/tools' const Container = define('Container', { color: theme('colors-text'), border: `1px solid ${theme('colors-border')}`, }) ``` Available tokens: | Token | Use for | |-------|---------| | `colors-bg`, `colors-bgSubtle`, `colors-bgElement`, `colors-bgHover` | Backgrounds | | `colors-text`, `colors-textMuted`, `colors-textFaint` | Text | | `colors-border` | Borders | | `colors-link` | Links | | `colors-primary`, `colors-primaryText` | Primary actions | | `colors-error`, `colors-dangerBorder`, `colors-dangerText` | Errors/danger | | `colors-success`, `colors-successBg` | Success states | | `colors-statusRunning`, `colors-statusStopped` | Status indicators | | `fonts-sans`, `fonts-mono` | Font stacks | | `spacing-xs` (4), `spacing-sm` (8), `spacing-md` (12), `spacing-lg` (16), `spacing-xl` (24) | Spacing (px) | | `radius-md` (6px) | Border radius | ## Writing a Tool Tools need three extra things vs apps: 1. `` in `` (handles dark mode + iframe height communication) 2. `baseStyles` prepended to CSS output 3. Handle the `?app=` query param ```tsx import { Hype } from '@because/hype' import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme } from '@because/toes/tools' import type { Child } from 'hono/jsx' const APPS_DIR = process.env.APPS_DIR! const app = new Hype({ prettyHTML: false }) const Container = define('Container', { fontFamily: theme('fonts-sans'), padding: '20px', paddingTop: 0, maxWidth: '800px', margin: '0 auto', color: theme('colors-text'), }) function Layout({ title, children }: { title: string; children: Child }) { return ( {title} {children} ) } app.get('/ok', c => c.text('ok')) app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) ) app.get('/', async c => { const appName = c.req.query('app') if (!appName) return c.html(

No app selected

) // ... tool logic using join(APPS_DIR, appName, 'current') for file paths return c.html(...) }) export default app.defaults ``` **Environment variables** available to tools: `APPS_DIR`, `TOES_URL` (base URL of Toes server), `PORT`, `TOES_DIR`. **Accessing app files:** always use `join(APPS_DIR, appName, 'current')`. **Calling the Toes API:** `fetch(\`${TOES_URL}/api/apps\`)`, `fetch(\`${TOES_URL}/api/apps/${name}\`)`. **Linking between tools:** `View Code`. ## Patterns **Fire-and-forget with polling** -- for long-running ops, don't await in POST. Use `` to poll while running. **Inline client JS** -- use `