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.
- package/.claude/launch.json +11 -0
- package/CHANGELOG.md +37 -0
- package/README.md +87 -0
- package/devtools/CHANGELOG.md +26 -0
- package/devtools/LICENSE +15 -0
- package/devtools/README.md +164 -0
- package/devtools/client/BeastDevtools.btsx +185 -0
- package/devtools/client/CodeView.btsx +61 -0
- package/devtools/client/ComponentsPanel.btsx +212 -0
- package/devtools/client/FileList.btsx +35 -0
- package/devtools/client/InspectorPanel.btsx +131 -0
- package/devtools/client/RefactorPanel.btsx +304 -0
- package/devtools/client/api.ts +69 -0
- package/devtools/client/compile.test.ts +22 -0
- package/devtools/client/devtools.css +1279 -0
- package/devtools/client/highlight.ts +210 -0
- package/devtools/client/mount.ts +16 -0
- package/devtools/client/runtime.ts +168 -0
- package/devtools/client/util.ts +136 -0
- package/devtools/package.json +73 -0
- package/devtools/server/analyze.test.ts +148 -0
- package/devtools/server/analyze.ts +737 -0
- package/devtools/server/diff.ts +82 -0
- package/devtools/server/line-map.ts +63 -0
- package/devtools/server/octane-bundler.d.ts +17 -0
- package/devtools/server/project.ts +369 -0
- package/devtools/server/refactor.test.ts +224 -0
- package/devtools/server/refactor.ts +224 -0
- package/devtools/server/source-scan.ts +377 -0
- package/devtools/shared/types.ts +208 -0
- package/devtools/test/fixtures/App.btsx +193 -0
- package/devtools/tsconfig.build.json +17 -0
- package/devtools/vite.ts +159 -0
- package/favicon.ico +0 -0
- package/index.html +14 -0
- package/package.json +34 -0
- package/public/beast.svg +1 -0
- package/src/App.btsx +144 -0
- package/src/AppHeader.btsx +24 -0
- package/src/LeftArticle.btsx +22 -0
- package/src/RightArticle.btsx +19 -0
- package/src/env.d.ts +8 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.ts +8 -0
- package/src/style.css +44 -0
- package/tsconfig.json +39 -0
- 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') →
|
|
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
|
+
}
|
package/devtools/vite.ts
ADDED
|
@@ -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
|
+
}
|
package/public/beast.svg
ADDED
|
@@ -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') →
|
|
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