beast-devtools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.claude/launch.json +11 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +87 -0
  4. package/devtools/CHANGELOG.md +26 -0
  5. package/devtools/LICENSE +15 -0
  6. package/devtools/README.md +164 -0
  7. package/devtools/client/BeastDevtools.btsx +185 -0
  8. package/devtools/client/CodeView.btsx +61 -0
  9. package/devtools/client/ComponentsPanel.btsx +212 -0
  10. package/devtools/client/FileList.btsx +35 -0
  11. package/devtools/client/InspectorPanel.btsx +131 -0
  12. package/devtools/client/RefactorPanel.btsx +304 -0
  13. package/devtools/client/api.ts +69 -0
  14. package/devtools/client/compile.test.ts +22 -0
  15. package/devtools/client/devtools.css +1279 -0
  16. package/devtools/client/highlight.ts +210 -0
  17. package/devtools/client/mount.ts +16 -0
  18. package/devtools/client/runtime.ts +168 -0
  19. package/devtools/client/util.ts +136 -0
  20. package/devtools/package.json +73 -0
  21. package/devtools/server/analyze.test.ts +148 -0
  22. package/devtools/server/analyze.ts +737 -0
  23. package/devtools/server/diff.ts +82 -0
  24. package/devtools/server/line-map.ts +63 -0
  25. package/devtools/server/octane-bundler.d.ts +17 -0
  26. package/devtools/server/project.ts +369 -0
  27. package/devtools/server/refactor.test.ts +224 -0
  28. package/devtools/server/refactor.ts +224 -0
  29. package/devtools/server/source-scan.ts +377 -0
  30. package/devtools/shared/types.ts +208 -0
  31. package/devtools/test/fixtures/App.btsx +193 -0
  32. package/devtools/tsconfig.build.json +17 -0
  33. package/devtools/vite.ts +159 -0
  34. package/favicon.ico +0 -0
  35. package/index.html +14 -0
  36. package/package.json +34 -0
  37. package/public/beast.svg +1 -0
  38. package/src/App.btsx +144 -0
  39. package/src/AppHeader.btsx +24 -0
  40. package/src/LeftArticle.btsx +22 -0
  41. package/src/RightArticle.btsx +19 -0
  42. package/src/env.d.ts +8 -0
  43. package/src/lib/utils.ts +6 -0
  44. package/src/main.ts +8 -0
  45. package/src/style.css +44 -0
  46. package/tsconfig.json +39 -0
  47. package/vite.config.ts +19 -0
@@ -0,0 +1,193 @@
1
+ import { useState } from 'octane'
2
+
3
+ module
4
+ type PanelId = 'language' | 'integration' | 'skills'
5
+ type PanelSide = 'left' | 'right'
6
+
7
+ interface Props {
8
+ docsUrl: string
9
+ }
10
+
11
+ interface LeftPanel {
12
+ docsPath: string
13
+ eyebrow: string
14
+ title: string
15
+ description: string
16
+ tags: string[]
17
+ note: string
18
+ }
19
+
20
+ interface RightPanel {
21
+ eyebrow: string
22
+ title: string
23
+ description: string
24
+ note: string
25
+ status: string
26
+ action?: {
27
+ label: string
28
+ docsPath: string
29
+ }
30
+ }
31
+
32
+ interface Panel {
33
+ id: PanelId
34
+ label: string
35
+ left: LeftPanel
36
+ right: RightPanel
37
+ copyableNotes?: boolean
38
+ }
39
+
40
+ const panels: Panel[] = [
41
+ {
42
+ id: 'language',
43
+ label: 'Language',
44
+ left: {
45
+ docsPath: '/docs/language',
46
+ eyebrow: 'Input',
47
+ title: 'Structured components',
48
+ description: 'Write familiar elements, selectors, attributes, text, and interpolation in .btsx—without closing tags. Leading spaces define the component tree.',
49
+ tags: ['.btsx', 'typed props', 'native control flow', 'child scopes'],
50
+ note: 'elements, props, and control flow'
51
+ },
52
+ right: {
53
+ eyebrow: 'Output',
54
+ title: 'Compiles to Octane',
55
+ description: 'Beast keeps TypeScript expressions intact, emits source-located TSRX, and leaves final validation and runtime compilation to Octane.',
56
+ note: 'small syntax. native output.',
57
+ status: 'Source-located',
58
+ action: {
59
+ label: 'Explore the language',
60
+ docsPath: '/docs/language'
61
+ }
62
+ }
63
+ },
64
+ {
65
+ id: 'integration',
66
+ label: 'Integration',
67
+ left: {
68
+ docsPath: '/docs/vite',
69
+ eyebrow: 'Beast → Octane → Vite',
70
+ title: 'Vite',
71
+ description: 'The Vite plugin compiles .btsx modules in memory before Octane processes the generated TSRX, so normal imports stay in one module graph.',
72
+ tags: ['HMR', 'SSR transforms', 'production builds'],
73
+ note: 'octane@0.4.3 · vite@^8.0.16'
74
+ },
75
+ right: {
76
+ eyebrow: 'Beast Styling',
77
+ title: 'Tailwind v4',
78
+ description: 'Use standard source imports, Tailwind utilities, and the same Beast–Octane pipeline for development, SSR, and production builds.',
79
+ note: 'Integrated by default.',
80
+ status: 'Tailwind v4'
81
+ }
82
+ },
83
+ {
84
+ id: 'skills',
85
+ label: 'Skills',
86
+ copyableNotes: true,
87
+ left: {
88
+ docsPath: '/docs/skills',
89
+ eyebrow: 'Beast Core',
90
+ title: 'Beast Skills',
91
+ description: 'The Beast skill gives coding agents a repeatable workflow to establish scope, scaffold or locate a project, author BTSX, diagnose compiler output, and verify the build.',
92
+ tags: ['scaffold', 'diagnose', 'verify'],
93
+ note: 'npx skills add https://github.com/phtn/beast-skill --skill beast'
94
+ },
95
+ right: {
96
+ eyebrow: 'Migrate to beast',
97
+ title: 'React to Beast',
98
+ description: 'Audit routes, state, styling, and behavior; port the application to BTSX while preserving the user-visible experience and recording migration evidence.',
99
+ note: 'npx skills add https://github.com/phtn/react-to-beast --skill react-to-beast',
100
+ status: 'migration ready',
101
+ action: {
102
+ label: 'Explore React migration',
103
+ docsPath: '/docs/react-to-beast'
104
+ }
105
+ }
106
+ }
107
+ ]
108
+
109
+ props { docsUrl }: Props
110
+ setup const copyNote = async (side: PanelSide, note: string) => { await navigator.clipboard.writeText(note); setCopiedSide(side); window.setTimeout(() => setCopiedSide(null), 1600); };
111
+ setup
112
+ const [activeId, setActiveId] = useState<PanelId>('language');
113
+ const [copiedSide, setCopiedSide] = useState<PanelSide | null>(null);
114
+ const active = panels.find((panel) => panel.id === activeId) ?? panels[0];
115
+
116
+ main(className='grid min-h-screen min-w-80 place-items-center p-3 sm:p-8 lg:p-16')
117
+ section(
118
+ ~ aria-labelledby="showcase-title"
119
+ ~ className='w-full max-w-[1080px] overflow-hidden rounded-[1.75rem] border border-white/85 bg-white/75 p-4 shadow-[0_24px_55px_rgba(28,28,25,0.09)] backdrop-blur-xl sm:rounded-[2.8rem] sm:p-6 lg:p-8'
120
+ ~ )
121
+ header(className='flex flex-col items-start justify-between gap-6 px-1 pb-6 pt-2 md:flex-row md:items-center md:gap-8 md:px-3 md:pb-8 md:pt-3')
122
+ h1#showcase-title(className='m-0 max-w-xl text-[clamp(1.7rem,3.3vw,2.7rem)] font-medium leading-[1.08] tracking-[-0.045em] text-[#161616]')
123
+ div(className='flex items-center text-[#161616]')
124
+ img(src='/beast.svg' alt='Beast' className='size-6 sm:size-7 md:size-8 lg:size-9 opacity-60')
125
+ p(className='ml-3 opacity-70') Beast
126
+ span(className='ml-6 mr-3 opacity-50') &rarr;
127
+ span(className='text-[#ff415a]') Octane
128
+ nav(className='inline-flex w-full shrink-0 items-center rounded-full border border-black/[0.025] bg-[#f0f0ee] p-1 md:w-auto' aria-label="Product workflow" role="tablist")
129
+ each panel in panels key panel.id
130
+ button(
131
+ ~ type="button"
132
+ ~ id={`tab-${panel.id}`}
133
+ ~ role="tab"
134
+ ~ aria-controls="workflow-panel"
135
+ ~ aria-selected={activeId === panel.id}
136
+ ~ className={activeId === panel.id ? 'inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 rounded-full border-0 bg-white/95 px-2.5 py-3 text-xs font-semibold text-[#242422] shadow-[0_1px_4px_rgba(26,26,24,0.14)] outline-none transition focus-visible:ring-3 focus-visible:ring-orange-200 sm:px-2 sm:text-sm md:min-w-32'
137
+ ~ : 'inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 rounded-full border-0 bg-transparent px-2.5 py-3 text-xs font-semibold text-[#595956] outline-none transition hover:text-[#111] focus-visible:ring-3 focus-visible:ring-orange-200 sm:px-2 sm:text-sm md:min-w-32'}
138
+ ~ onClick={() => { setActiveId(panel.id); setCopiedSide(null); }}
139
+ ~ )
140
+ | #{panel.label}
141
+
142
+ section#workflow-panel(
143
+ ~ role="tabpanel"
144
+ ~ aria-live="polite"
145
+ ~ aria-labelledby={`tab-${activeId}`}
146
+ ~ className='infinite-lines grid grid-cols-1 gap-4 rounded-3xl bg-[#eeeeeb]/90 p-2 sm:rounded-[2rem] sm:p-4 lg:grid-cols-[0.92fr_1.08fr]'
147
+ ~ )
148
+
149
+ // LEFT
150
+ article(className='opacity-0 flex min-h-[400px] flex-col rounded-[1.15rem] border border-black/8 bg-white/5 backdrop-blur-3xl p-6 sm:min-h-[430px] sm:rounded-[1.45rem] sm:p-9 lg:min-h-[480px] lg:p-10')
151
+ div
152
+ p(className='mb-6 mt-0 text-[0.72rem] font-bold uppercase tracking-[0.11em] text-[#9b9b96]') #{active.left.eyebrow}
153
+ h2(className='m-0 max-w-sm text-[clamp(1.55rem,2.8vw,2.05rem)] font-medium leading-[1.16] tracking-[-0.035em] text-[#161616]') #{active.left.title}
154
+ p(className='mb-0 mt-5 max-w-md text-base leading-relaxed text-[#73736f]') #{active.left.description}
155
+ ul(className='mb-0 mt-9 flex list-none flex-wrap gap-2 p-0' aria-label="Issue tags")
156
+ each tag, index in active.left.tags key tag
157
+ li(className={index === 1 ? 'rounded-full border border-black/10 px-3 py-2 text-xs font-semibold text-[#3f3f3c] shadow-xs' : 'rounded-full border border-black/10 bg-white/65 px-3 py-2 text-xs font-semibold text-[#3f3f3c] shadow-xs'}) #{tag}
158
+ if active.copyableNotes
159
+ button(
160
+ ~ type="button"
161
+ ~ className='group mb-5 mt-auto flex w-full cursor-pointer items-center justify-between gap-4 rounded-xl border border-black/8 bg-white/55 px-4 py-3 text-left outline-none transition hover:bg-white/85 focus-visible:ring-3 focus-visible:ring-orange-200'
162
+ ~ aria-label={`Copy note: ${active.left.note}`}
163
+ ~ onClick={() => copyNote('left', active.left.note)}
164
+ ~ )
165
+ code(className='min-w-0 max-w-[35ch] overflow-x-auto whitespace-nowrap text-xs text-[#666661] scrollbar-none') #{active.left.note}
166
+ span(className='shrink-0 text-xs font-semibold text-[#343431]') #{copiedSide === 'left' ? 'Copied' : 'Copy'}
167
+ else
168
+ code(className='mb-0 mt-auto pb-5 pt-16 text-sm text-[#858681]') #{active.left.note}
169
+
170
+ // RIGHT
171
+ article(className='flex min-h-[390px] flex-col rounded-[1.15rem] bg-[radial-gradient(circle_at_88%_10%,rgba(255,255,255,0.07),transparent_28rem),linear-gradient(145deg,#2a2b29,#111211_72%)] p-6 text-[#f6f6f2] shadow-[0_22px_36px_rgba(15,16,14,0.18)] sm:min-h-[430px] sm:rounded-[1.45rem] sm:p-9 lg:min-h-[510px] lg:p-10')
172
+ div
173
+ p(className='mb-6 mt-0 text-[0.72rem] font-bold uppercase tracking-[0.11em] text-[#858681]') #{active.right.eyebrow}
174
+ h2(className='m-0 max-w-sm text-[clamp(1.55rem,2.8vw,2.05rem)] font-medium leading-[1.16] tracking-[-0.035em]') #{active.right.title}
175
+ p(className='mb-0 mt-5 max-w-md text-base leading-relaxed text-[#a4a5a0]') #{active.right.description}
176
+ if active.copyableNotes
177
+ button(
178
+ ~ type="button"
179
+ ~ className='group mb-5 mt-auto flex w-full cursor-pointer items-center justify-between gap-4 rounded-xl border border-white/10 bg-white/[0.045] px-4 py-3 text-left outline-none transition hover:bg-white/[0.075] focus-visible:ring-3 focus-visible:ring-orange-200'
180
+ ~ aria-label={`Copy note: ${active.right.note}`}
181
+ ~ onClick={() => copyNote('right', active.right.note)}
182
+ ~ )
183
+ code(className='min-w-0 max-w-[40ch] overflow-x-auto whitespace-nowrap text-xs text-[#a4a5a0] scrollbar-none') #{active.right.note}
184
+ span(className='shrink-0 text-xs font-semibold text-[#f6f6f2]') #{copiedSide === 'right' ? 'Copied' : 'Copy'}
185
+ else
186
+ code(className='mb-0 mt-auto pb-5 pt-16 text-sm text-[#858681]') #{active.right.note}
187
+
188
+ footer(className='pt-0')
189
+ a(href='https://beast-docs.vercel.app/'
190
+ ~ target='_blank'
191
+ ~ rel='noopener noreferrer'
192
+ ~ className='mt-1 px-1 py-0.5 ml-3 underline underline-offset-4 decoration-dotted')
193
+ span(className='font-mono text-xs tracking-wider') BEAST DOCS
@@ -0,0 +1,17 @@
1
+ {
2
+ // Compiles the Vite plugin (Node side) to dist/. The overlay in client/ ships
3
+ // as source and is compiled by the host app's Beast/Octane toolchain.
4
+ "compilerOptions": {
5
+ "target": "ES2022",
6
+ "module": "NodeNext",
7
+ "moduleResolution": "NodeNext",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "declaration": true,
11
+ "rootDir": ".",
12
+ "outDir": "dist",
13
+ "types": ["node"]
14
+ },
15
+ "include": ["vite.ts", "server/**/*.ts", "shared/**/*.ts"],
16
+ "exclude": ["**/*.test.ts"]
17
+ }
@@ -0,0 +1,159 @@
1
+ import { existsSync } from 'node:fs'
2
+ import type { IncomingMessage } from 'node:http'
3
+ import { dirname, relative, resolve, sep } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { searchForWorkspaceRoot, type Connect, type Plugin } from 'vite'
6
+ import { BeastProject } from './server/project.js'
7
+ import { RefactorError } from './server/refactor.js'
8
+ import { API_BASE, DEFAULT_SETTINGS, SOURCE_CHANGED_EVENT, type AnalyzerSettings, type ApplyRequest } from './shared/types.js'
9
+
10
+ export interface BeastDevtoolsOptions {
11
+ /** Directories, relative to the Vite root, scanned for `.btsx` sources. Default: `['src']`. */
12
+ include?: string[]
13
+ /** Default analyzer thresholds; the overlay can override them per session. */
14
+ analyzer?: Partial<AnalyzerSettings>
15
+ }
16
+
17
+ // This module runs from source (`vite.ts`) during development and from
18
+ // `dist/vite.js` once published; the overlay sources sit in `client/` either way.
19
+ const HERE = dirname(fileURLToPath(import.meta.url))
20
+ const PACKAGE_ROOT = [HERE, resolve(HERE, '..')].find((dir) => existsSync(resolve(dir, 'client/mount.ts'))) ?? HERE
21
+ const CLIENT_ENTRY = resolve(PACKAGE_ROOT, 'client/mount.ts')
22
+
23
+ /**
24
+ * Beast DevTools: an in-page overlay for Beast + Octane apps during `vite dev`.
25
+ *
26
+ * Pair it with `beastOctane({ octane: { profile: 'auto' } })` so Octane exposes
27
+ * its runtime inspection hook; the overlay explains how when it is missing.
28
+ */
29
+ export function beastDevtools(options: BeastDevtoolsOptions = {}): Plugin {
30
+ const defaults: AnalyzerSettings = { ...DEFAULT_SETTINGS, ...options.analyzer }
31
+ let root = process.cwd()
32
+ let project: BeastProject | null = null
33
+
34
+ return {
35
+ name: 'beast:devtools',
36
+ apply: 'serve',
37
+
38
+ config(config) {
39
+ const appRoot = resolve(config.root ?? process.cwd())
40
+ return {
41
+ // The overlay must share the app's Octane runtime: a second copy would
42
+ // install its own inspection hook and see only the overlay's root.
43
+ resolve: { dedupe: ['octane'] },
44
+ // A linked package can live outside the workspace root Vite serves.
45
+ server: { fs: { allow: [searchForWorkspaceRoot(appRoot), PACKAGE_ROOT] } },
46
+ }
47
+ },
48
+
49
+ configResolved(config) {
50
+ root = config.root
51
+ project = new BeastProject({ root, include: options.include ?? ['src'], exclude: [PACKAGE_ROOT] })
52
+ },
53
+
54
+ configureServer(server) {
55
+ server.middlewares.use(API_BASE, apiMiddleware(() => project, defaults))
56
+
57
+ const notify = (file: string) => {
58
+ if (!file.endsWith('.btsx') || file.startsWith(PACKAGE_ROOT + sep)) return
59
+ project?.invalidate(file)
60
+ server.ws.send({ type: 'custom', event: SOURCE_CHANGED_EVENT, data: { path: toPosix(relative(root, file)) } })
61
+ }
62
+ server.watcher.on('change', notify)
63
+ server.watcher.on('add', notify)
64
+ server.watcher.on('unlink', notify)
65
+ },
66
+
67
+ transformIndexHtml() {
68
+ const inRoot = CLIENT_ENTRY.startsWith(root + sep)
69
+ const src = inRoot ? `/${toPosix(relative(root, CLIENT_ENTRY))}` : `/@fs/${toPosix(CLIENT_ENTRY).replace(/^\//, '')}`
70
+ return [{ tag: 'script', attrs: { type: 'module', src }, injectTo: 'body' }]
71
+ },
72
+ }
73
+ }
74
+
75
+ function apiMiddleware(getProject: () => BeastProject | null, defaults: AnalyzerSettings): Connect.NextHandleFunction {
76
+ return async (req, res, next) => {
77
+ const project = getProject()
78
+ if (project === null || req.url === undefined || (req.method !== 'GET' && req.method !== 'POST')) return next()
79
+
80
+ const url = new URL(req.url, 'http://beast.devtools')
81
+ const send = (status: number, body: unknown) => {
82
+ res.statusCode = status
83
+ res.setHeader('Content-Type', 'application/json')
84
+ res.setHeader('Cache-Control', 'no-store')
85
+ res.end(JSON.stringify(body))
86
+ }
87
+
88
+ try {
89
+ if (req.method === 'GET') {
90
+ const settings = readSettings((name) => url.searchParams.get(name), defaults)
91
+ if (url.pathname === '/project') return send(200, project.report(settings))
92
+ if (url.pathname === '/file') {
93
+ const report = project.file(url.searchParams.get('path') ?? '', settings)
94
+ return report === null ? send(404, { error: 'Unknown .btsx file' }) : send(200, report)
95
+ }
96
+ return send(404, { error: `Unknown endpoint ${url.pathname}` })
97
+ }
98
+
99
+ // Writes are same-origin JSON only: a cross-site page can neither send
100
+ // this content type without a preflight nor pass the origin check.
101
+ const refusal = writeRefusal(req)
102
+ if (refusal !== null) return send(403, { error: refusal })
103
+ const body = await readJson(req)
104
+ if (url.pathname === '/apply') {
105
+ const request: ApplyRequest = {
106
+ path: String(body.path ?? ''),
107
+ hash: String(body.hash ?? ''),
108
+ suggestionId: String(body.suggestionId ?? ''),
109
+ target: body.target === 'file' ? 'file' : 'inline',
110
+ dryRun: body.dryRun !== false,
111
+ settings: readSettings((name) => (body.settings as Record<string, unknown> | undefined)?.[name], defaults),
112
+ }
113
+ return send(200, project.apply(request))
114
+ }
115
+ if (url.pathname === '/undo') return send(200, project.undo(String(body.id ?? '')))
116
+ return send(404, { error: `Unknown endpoint ${url.pathname}` })
117
+ } catch (error) {
118
+ if (error instanceof RefactorError) return send(error.status, { error: error.message })
119
+ return send(500, { error: error instanceof Error ? error.message : String(error) })
120
+ }
121
+ }
122
+ }
123
+
124
+ function writeRefusal(req: IncomingMessage): string | null {
125
+ if (!(req.headers['content-type'] ?? '').includes('application/json')) return 'Expected a JSON request.'
126
+ const site = req.headers['sec-fetch-site']
127
+ if (site !== undefined && site !== 'same-origin' && site !== 'none') return 'Cross-site requests cannot modify files.'
128
+ const origin = req.headers.origin
129
+ if (origin !== undefined && new URL(origin).host !== req.headers.host) return 'Cross-origin requests cannot modify files.'
130
+ return null
131
+ }
132
+
133
+ async function readJson(req: IncomingMessage): Promise<Record<string, unknown>> {
134
+ let text = ''
135
+ for await (const chunk of req) {
136
+ text += chunk
137
+ if (text.length > 64 * 1024) throw new RefactorError('Request body too large.', 422)
138
+ }
139
+ const value: unknown = JSON.parse(text || '{}')
140
+ if (value === null || typeof value !== 'object') throw new RefactorError('Expected a JSON object.', 422)
141
+ return value as Record<string, unknown>
142
+ }
143
+
144
+ function readSettings(get: (name: string) => unknown, defaults: AnalyzerSettings): AnalyzerSettings {
145
+ const read = (name: keyof AnalyzerSettings, min: number, max: number) => {
146
+ const raw = get(name)
147
+ const value = Number(raw)
148
+ return raw != null && raw !== '' && Number.isInteger(value) ? Math.min(max, Math.max(min, value)) : defaults[name]
149
+ }
150
+ return {
151
+ depthLimit: read('depthLimit', 1, 20),
152
+ minLines: read('minLines', 2, 200),
153
+ fileLines: read('fileLines', 2, 1000),
154
+ }
155
+ }
156
+
157
+ function toPosix(path: string): string {
158
+ return path.split(sep).join('/')
159
+ }
package/favicon.ico ADDED
Binary file
package/index.html ADDED
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" href="/favicon.ico?v=2" />
7
+ <meta name="description" content="Explore the Beast language, Vite integration, and agent skills." />
8
+ <title>Beast — Language, Integration & Skills</title>
9
+ </head>
10
+ <body>
11
+ <div id="app"></div>
12
+ <script type="module" src="/src/main.ts"></script>
13
+ </body>
14
+ </html>
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "beast-devtools",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "workspaces": [
7
+ "devtools"
8
+ ],
9
+ "scripts": {
10
+ "dev": "bun run --cwd devtools build && vite",
11
+ "build": "vite build",
12
+ "preview": "vite preview",
13
+ "typecheck": "tsrx-tsc --noEmit",
14
+ "test": "bun test devtools",
15
+ "check": "bun run typecheck && bun run test && bun run --cwd devtools build && bun run build",
16
+ "devtools:pack": "bun run --cwd devtools build && cd devtools && npm pack --dry-run"
17
+ },
18
+ "dependencies": {
19
+ "beast-tsrx": "0.4.3",
20
+ "clsx": "^2.1.1",
21
+ "octane": "0.4.3",
22
+ "tailwind-merge": "^3.6.0",
23
+ "@octanejs/base-ui": "0.1.55"
24
+ },
25
+ "devDependencies": {
26
+ "@beastjs/devtools": "workspace:*",
27
+ "@types/node": "^26.2.0",
28
+ "@tsrx/typescript-plugin": "0.3.135",
29
+ "typescript": "^5.9.3",
30
+ "tailwindcss": "^4.3.3",
31
+ "vite": "^8.0.16",
32
+ "@tailwindcss/vite": "^4.3.3"
33
+ }
34
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="m8.393 1.002-.205.006a7.3 7.3 0 0 0-3.024.742 7.43 7.43 0 0 0-3.786 4.317 7.3 7.3 0 0 0-.375 2.145 7.34 7.34 0 0 0 .941 3.808 7.5 7.5 0 0 0 2.653 2.731 7.37 7.37 0 0 0 4.231 1.042 7 7 0 0 0 .917-.11 7.4 7.4 0 0 0 4.446-2.673 7.373 7.373 0 0 0 1.597-4.109c.012-.157.012-.828 0-.994a7.3 7.3 0 0 0-.747-2.768 7.3 7.3 0 0 0-1.128-1.67 11 11 0 0 0-.577-.577 7.4 7.4 0 0 0-2.612-1.516 7.5 7.5 0 0 0-2.099-.368 6 6 0 0 1-.232-.006M8.11 3.488a5.4 5.4 0 0 0-.808.114 5 5 0 0 0-1.074.379l-.047.024 1.11 1.11 1.11 1.109 1.109-1.11 1.11-1.11-.048-.023a4.9 4.9 0 0 0-1.811-.487 8 8 0 0 0-.651-.006M3.955 6.281a4.9 4.9 0 0 0-.48 1.854 4.886 4.886 0 0 0 .503 2.441l.024.047 1.11-1.11L6.22 8.405l-1.11-1.11L4 6.184zm7.736 1.014-1.11 1.11 1.11 1.109 1.11 1.11.023-.048c.196-.385.357-.878.435-1.328.062-.36.087-.758.068-1.105a5 5 0 0 0-.497-1.898l-.03-.06zM7.29 11.697l-1.108 1.108.097.046c.562.27 1.164.424 1.817.468.165.01.606.005.761-.01a5 5 0 0 0 1.668-.458l.097-.046-1.108-1.108c-.61-.61-1.11-1.109-1.112-1.109s-.502.499-1.112 1.109" clip-rule="evenodd"/></svg>
package/src/App.btsx ADDED
@@ -0,0 +1,144 @@
1
+ import { useState } from 'octane'
2
+ import AppHeader from './AppHeader.btsx'
3
+ import LeftArticle from './LeftArticle.btsx'
4
+ import RightArticle from './RightArticle.btsx'
5
+
6
+ module
7
+ export type PanelId = 'language' | 'integration' | 'skills'
8
+ export type PanelSide = 'left' | 'right'
9
+
10
+ interface Props {
11
+ docsUrl: string
12
+ }
13
+
14
+ interface LeftPanel {
15
+ docsPath: string
16
+ eyebrow: string
17
+ title: string
18
+ description: string
19
+ tags: string[]
20
+ note: string
21
+ }
22
+
23
+ interface RightPanel {
24
+ eyebrow: string
25
+ title: string
26
+ description: string
27
+ note: string
28
+ status: string
29
+ action?: {
30
+ label: string
31
+ docsPath: string
32
+ }
33
+ }
34
+
35
+ interface Panel {
36
+ id: PanelId
37
+ label: string
38
+ left: LeftPanel
39
+ right: RightPanel
40
+ copyableNotes?: boolean
41
+ }
42
+
43
+ export const panels: Panel[] = [
44
+ {
45
+ id: 'language',
46
+ label: 'Language',
47
+ left: {
48
+ docsPath: '/docs/language',
49
+ eyebrow: 'Input',
50
+ title: 'Structured components',
51
+ description: 'Write familiar elements, selectors, attributes, text, and interpolation in .btsx—without closing tags. Leading spaces define the component tree.',
52
+ tags: ['.btsx', 'typed props', 'native control flow', 'child scopes'],
53
+ note: 'elements, props, and control flow'
54
+ },
55
+ right: {
56
+ eyebrow: 'Output',
57
+ title: 'Compiles to Octane',
58
+ description: 'Beast keeps TypeScript expressions intact, emits source-located TSRX, and leaves final validation and runtime compilation to Octane.',
59
+ note: 'small syntax. native output.',
60
+ status: 'Source-located',
61
+ action: {
62
+ label: 'Explore the language',
63
+ docsPath: '/docs/language'
64
+ }
65
+ }
66
+ },
67
+ {
68
+ id: 'integration',
69
+ label: 'Integration',
70
+ left: {
71
+ docsPath: '/docs/vite',
72
+ eyebrow: 'Beast → Octane → Vite',
73
+ title: 'Vite',
74
+ description: 'The Vite plugin compiles .btsx modules in memory before Octane processes the generated TSRX, so normal imports stay in one module graph.',
75
+ tags: ['HMR', 'SSR transforms', 'production builds'],
76
+ note: 'octane@0.4.3 · vite@^8.0.16'
77
+ },
78
+ right: {
79
+ eyebrow: 'Beast Styling',
80
+ title: 'Tailwind v4',
81
+ description: 'Use standard source imports, Tailwind utilities, and the same Beast–Octane pipeline for development, SSR, and production builds.',
82
+ note: 'Integrated by default.',
83
+ status: 'Tailwind v4'
84
+ }
85
+ },
86
+ {
87
+ id: 'skills',
88
+ label: 'Skills',
89
+ copyableNotes: true,
90
+ left: {
91
+ docsPath: '/docs/skills',
92
+ eyebrow: 'Beast Core',
93
+ title: 'Beast Skills',
94
+ description: 'The Beast skill gives coding agents a repeatable workflow to establish scope, scaffold or locate a project, author BTSX, diagnose compiler output, and verify the build.',
95
+ tags: ['scaffold', 'diagnose', 'verify'],
96
+ note: 'npx skills add https://github.com/phtn/beast-skill --skill beast'
97
+ },
98
+ right: {
99
+ eyebrow: 'Migrate to beast',
100
+ title: 'React to Beast',
101
+ description: 'Audit routes, state, styling, and behavior; port the application to BTSX while preserving the user-visible experience and recording migration evidence.',
102
+ note: 'npx skills add https://github.com/phtn/react-to-beast --skill react-to-beast',
103
+ status: 'migration ready',
104
+ action: {
105
+ label: 'Explore React migration',
106
+ docsPath: '/docs/react-to-beast'
107
+ }
108
+ }
109
+ }
110
+ ]
111
+
112
+ props { docsUrl }: Props
113
+ setup const copyNote = async (side: PanelSide, note: string) => { await navigator.clipboard.writeText(note); setCopiedSide(side); window.setTimeout(() => setCopiedSide(null), 1600); };
114
+ setup
115
+ const [activeId, setActiveId] = useState<PanelId>('language');
116
+ const [copiedSide, setCopiedSide] = useState<PanelSide | null>(null);
117
+ const active = panels.find((panel) => panel.id === activeId) ?? panels[0];
118
+
119
+ main(className='grid min-h-screen min-w-80 place-items-center p-3 sm:p-8 lg:p-16')
120
+ section(
121
+ ~ aria-labelledby="showcase-title"
122
+ ~ className='w-full max-w-[1080px] overflow-hidden rounded-[1.75rem] border border-white/85 bg-white/75 p-4 shadow-[0_24px_55px_rgba(28,28,25,0.09)] backdrop-blur-xl sm:rounded-[2.8rem] sm:p-6 lg:p-8'
123
+ ~ )
124
+ AppHeader(activeId={activeId} setActiveId={setActiveId} setCopiedSide={setCopiedSide})
125
+
126
+ section#workflow-panel(
127
+ ~ role="tabpanel"
128
+ ~ aria-live="polite"
129
+ ~ aria-labelledby={`tab-${activeId}`}
130
+ ~ className='infinite-lines grid grid-cols-1 gap-4 rounded-3xl bg-[#eeeeeb]/90 p-2 sm:rounded-[2rem] sm:p-4 lg:grid-cols-[0.92fr_1.08fr]'
131
+ ~ )
132
+
133
+ // LEFT
134
+ LeftArticle(active={active} copyNote={copyNote} copiedSide={copiedSide})
135
+
136
+ // RIGHT
137
+ RightArticle(active={active} copyNote={copyNote} copiedSide={copiedSide})
138
+
139
+ footer(className='pt-0')
140
+ a(href='https://beast-docs.vercel.app/'
141
+ ~ target='_blank'
142
+ ~ rel='noopener noreferrer'
143
+ ~ className='mt-1 px-1 py-0.5 ml-3 underline underline-offset-4 decoration-dotted')
144
+ span(className='font-mono text-xs tracking-wider') BEAST DOCS
@@ -0,0 +1,24 @@
1
+ import { panels } from './App.btsx'
2
+ import type { PanelId, PanelSide } from './App.btsx'
3
+ props { activeId, setActiveId, setCopiedSide }: { activeId: PanelId; setActiveId: (value: PanelId) => void; setCopiedSide: (value: PanelSide | null) => void }
4
+
5
+ header(className='flex flex-col items-start justify-between gap-6 px-1 pb-6 pt-2 md:flex-row md:items-center md:gap-8 md:px-3 md:pb-8 md:pt-3')
6
+ h1#showcase-title(className='m-0 max-w-xl text-[clamp(1.7rem,3.3vw,2.7rem)] font-medium leading-[1.08] tracking-[-0.045em] text-[#161616]')
7
+ div(className='flex items-center text-[#161616]')
8
+ img(src='/beast.svg' alt='Beast' className='size-6 sm:size-7 md:size-8 lg:size-9 opacity-60')
9
+ p(className='ml-3 opacity-70') Beast
10
+ span(className='ml-6 mr-3 opacity-50') &rarr;
11
+ span(className='text-[#ff415a]') Octane
12
+ nav(className='inline-flex w-full shrink-0 items-center rounded-full border border-black/[0.025] bg-[#f0f0ee] p-1 md:w-auto' aria-label="Product workflow" role="tablist")
13
+ each panel in panels key panel.id
14
+ button(
15
+ ~ type="button"
16
+ ~ id={`tab-${panel.id}`}
17
+ ~ role="tab"
18
+ ~ aria-controls="workflow-panel"
19
+ ~ aria-selected={activeId === panel.id}
20
+ ~ className={activeId === panel.id ? 'inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 rounded-full border-0 bg-white/95 px-2.5 py-3 text-xs font-semibold text-[#242422] shadow-[0_1px_4px_rgba(26,26,24,0.14)] outline-none transition focus-visible:ring-3 focus-visible:ring-orange-200 sm:px-2 sm:text-sm md:min-w-32'
21
+ ~ : 'inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 rounded-full border-0 bg-transparent px-2.5 py-3 text-xs font-semibold text-[#595956] outline-none transition hover:text-[#111] focus-visible:ring-3 focus-visible:ring-orange-200 sm:px-2 sm:text-sm md:min-w-32'}
22
+ ~ onClick={() => { setActiveId(panel.id); setCopiedSide(null); }}
23
+ ~ )
24
+ | #{panel.label}
@@ -0,0 +1,22 @@
1
+ import type { PanelSide } from './App.btsx'
2
+ props { active, copyNote, copiedSide }: { active: any; copyNote: any; copiedSide: PanelSide | null }
3
+
4
+ article(className='opacity-0 flex min-h-[400px] flex-col rounded-[1.15rem] border border-black/8 bg-white/5 backdrop-blur-3xl p-6 sm:min-h-[430px] sm:rounded-[1.45rem] sm:p-9 lg:min-h-[480px] lg:p-10')
5
+ div
6
+ p(className='mb-6 mt-0 text-[0.72rem] font-bold uppercase tracking-[0.11em] text-[#9b9b96]') #{active.left.eyebrow}
7
+ h2(className='m-0 max-w-sm text-[clamp(1.55rem,2.8vw,2.05rem)] font-medium leading-[1.16] tracking-[-0.035em] text-[#161616]') #{active.left.title}
8
+ p(className='mb-0 mt-5 max-w-md text-base leading-relaxed text-[#73736f]') #{active.left.description}
9
+ ul(className='mb-0 mt-9 flex list-none flex-wrap gap-2 p-0' aria-label="Issue tags")
10
+ each tag, index in active.left.tags key tag
11
+ li(className={index === 1 ? 'rounded-full border border-black/10 px-3 py-2 text-xs font-semibold text-[#3f3f3c] shadow-xs' : 'rounded-full border border-black/10 bg-white/65 px-3 py-2 text-xs font-semibold text-[#3f3f3c] shadow-xs'}) #{tag}
12
+ if active.copyableNotes
13
+ button(
14
+ ~ type="button"
15
+ ~ className='group mb-5 mt-auto flex w-full cursor-pointer items-center justify-between gap-4 rounded-xl border border-black/8 bg-white/55 px-4 py-3 text-left outline-none transition hover:bg-white/85 focus-visible:ring-3 focus-visible:ring-orange-200'
16
+ ~ aria-label={`Copy note: ${active.left.note}`}
17
+ ~ onClick={() => copyNote('left', active.left.note)}
18
+ ~ )
19
+ code(className='min-w-0 max-w-[35ch] overflow-x-auto whitespace-nowrap text-xs text-[#666661] scrollbar-none') #{active.left.note}
20
+ span(className='shrink-0 text-xs font-semibold text-[#343431]') #{copiedSide === 'left' ? 'Copied' : 'Copy'}
21
+ else
22
+ code(className='mb-0 mt-auto pb-5 pt-16 text-sm text-[#858681]') #{active.left.note}
@@ -0,0 +1,19 @@
1
+ import type { PanelSide } from './App.btsx'
2
+ props { active, copyNote, copiedSide }: { active: any; copyNote: any; copiedSide: PanelSide | null }
3
+
4
+ article(className='flex min-h-[390px] flex-col rounded-[1.15rem] bg-[radial-gradient(circle_at_88%_10%,rgba(255,255,255,0.07),transparent_28rem),linear-gradient(145deg,#2a2b29,#111211_72%)] p-6 text-[#f6f6f2] shadow-[0_22px_36px_rgba(15,16,14,0.18)] sm:min-h-[430px] sm:rounded-[1.45rem] sm:p-9 lg:min-h-[510px] lg:p-10')
5
+ div
6
+ p(className='mb-6 mt-0 text-[0.72rem] font-bold uppercase tracking-[0.11em] text-[#858681]') #{active.right.eyebrow}
7
+ h2(className='m-0 max-w-sm text-[clamp(1.55rem,2.8vw,2.05rem)] font-medium leading-[1.16] tracking-[-0.035em]') #{active.right.title}
8
+ p(className='mb-0 mt-5 max-w-md text-base leading-relaxed text-[#a4a5a0]') #{active.right.description}
9
+ if active.copyableNotes
10
+ button(
11
+ ~ type="button"
12
+ ~ className='group mb-5 mt-auto flex w-full cursor-pointer items-center justify-between gap-4 rounded-xl border border-white/10 bg-white/[0.045] px-4 py-3 text-left outline-none transition hover:bg-white/[0.075] focus-visible:ring-3 focus-visible:ring-orange-200'
13
+ ~ aria-label={`Copy note: ${active.right.note}`}
14
+ ~ onClick={() => copyNote('right', active.right.note)}
15
+ ~ )
16
+ code(className='min-w-0 max-w-[40ch] overflow-x-auto whitespace-nowrap text-xs text-[#a4a5a0] scrollbar-none') #{active.right.note}
17
+ span(className='shrink-0 text-xs font-semibold text-[#f6f6f2]') #{copiedSide === 'right' ? 'Copied' : 'Copy'}
18
+ else
19
+ code(className='mb-0 mt-auto pb-5 pt-16 text-sm text-[#858681]') #{active.right.note}
package/src/env.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.btsx' {
4
+ import type { ComponentBody } from 'octane'
5
+
6
+ const component: ComponentBody
7
+ export default component
8
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }