diff --git a/apps/test123/20260227-072646/.gitignore b/apps/test123/20260227-072646/.gitignore new file mode 100644 index 0000000..74c83bb --- /dev/null +++ b/apps/test123/20260227-072646/.gitignore @@ -0,0 +1,39 @@ +# dependencies (bun install) +node_modules + +# dev data +data/ + +# honk honk +.claude/settings.local.json + +# output +dist/ +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/apps/test123/20260227-072646/.npmrc b/apps/test123/20260227-072646/.npmrc new file mode 100644 index 0000000..6c57d5c --- /dev/null +++ b/apps/test123/20260227-072646/.npmrc @@ -0,0 +1 @@ +registry=https://npm.nose.space diff --git a/apps/test123/20260227-072646/CLAUDE.md b/apps/test123/20260227-072646/CLAUDE.md new file mode 100644 index 0000000..84d6dfb --- /dev/null +++ b/apps/test123/20260227-072646/CLAUDE.md @@ -0,0 +1,307 @@ +# 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 `