Skip to content

Commit de78f9a

Browse files
docs: update article. (#146)
1 parent 2e5702c commit de78f9a

5 files changed

Lines changed: 189 additions & 63 deletions

File tree

docs/article.md

Lines changed: 77 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,123 @@
1-
# Forget The Build Step: A Browser-Native Editor For JSX + CSS
1+
---
2+
title: 'Forget The Build Step: A Browser-Native Editor For JSX + CSS'
3+
published: true
4+
description: '@knighted/develop is a browser-native editor for JSX and CSS — compile, lint, typecheck, and sync to GitHub without a local build step.'
5+
tags: javascript, webdev, showdev, react
6+
---
27

38
Frontend tooling is incredibly capable.
49

510
It is also often front-loaded.
611

7-
For many UI ideas, the first thing you do is not write code. You install,
8-
configure, and wait. The creative loop starts late.
12+
For many UI ideas, the first thing you do is not write code. You install, configure, and wait. The creative loop starts late.
913

10-
[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built
11-
for a different default: fast prototyping from anywhere you can open a browser.
14+
[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built for a different default: fast prototyping from anywhere you can open a browser.
1215

13-
It is a browser-native editor/workbench for
14-
[@knighted/jsx](https://github.com/knightedcodemonkey/jsx) and
15-
[@knighted/css](https://github.com/knightedcodemonkey/css), delivered through
16-
CDN ESM with mode-aware loading.
16+
It is a browser-native editor/workbench for [@knighted/jsx](https://github.com/knightedcodemonkey/jsx) and [@knighted/css](https://github.com/knightedcodemonkey/css), delivered through CDN ESM with mode-aware loading.
1717

1818
## The Loop, In Practice
1919

20-
Open the app, edit multiple files in dynamic tabs, switch render/style modes,
21-
run lint/type diagnostics, and preview instantly.
20+
Open the app, spin up isolated workspaces, edit multiple files in dynamic tabs, switch render/style modes, run lint/type diagnostics, and preview instantly.
2221

2322
No local bundler is required for that inner loop.
2423

24+
In DOM mode, JSX expressions resolve to real DOM nodes — no virtual DOM, no diffing, no reconciler. Tabs are standard ESM modules too, so your entry tab can import from sibling tabs with relative paths:
25+
26+
`App.tsx`
27+
28+
```tsx
29+
import { Counter } from './Counter.js'
30+
31+
export const App = () => (
32+
<main>
33+
<Counter label="Clicks" />
34+
</main>
35+
)
36+
```
37+
38+
`Counter.tsx`
39+
40+
```tsx
41+
import '../styles/app.css'
42+
43+
type CounterProps = {
44+
label: string
45+
}
46+
47+
export const Counter = ({ label }: CounterProps) => {
48+
const el = (
49+
<button class="counter-button" type="button">
50+
{label}: 0
51+
</button>
52+
) as HTMLButtonElement
53+
let count = 0
54+
55+
el.onclick = () => {
56+
count += 1
57+
el.textContent = `${label}: ${count}`
58+
el.classList.toggle('is-even', count % 2 === 0)
59+
}
60+
61+
return el
62+
}
63+
```
64+
2565
## What The App Gives You
2666

27-
- Dynamic tabbed editing (add, rename, remove, and protect required entry tabs)
28-
- Render mode switch: DOM or React
29-
- Style mode switch: CSS, CSS Modules, Less, Sass
30-
- Live preview with iframe-isolated style encapsulation
31-
- In-browser lint and type diagnostics with jump-to-source navigation
32-
- GitHub-connected workflows for Open PR and Push Commit
33-
- AI chat with tab-aware edit proposals and explicit apply/undo controls
67+
- **Multiple Workspaces:** Create and switch between isolated projects without losing your current files.
68+
- **Dynamic Tabbed Editing:** Add, rename, remove, and protect required entry tabs within any workspace.
69+
- **Instant Share URLs:** Encode the current workspace state into a URL you can share or bookmark.
70+
- **Direct GitHub Synchronization:** Open pull requests and push commits from the browser.
71+
- **Render Mode Switch:** Toggle instantly between DOM or React runtimes.
72+
- **Style Mode Switch:** Support for CSS, CSS Modules, Less, and Sass.
73+
- **Live Preview:** Real-time updates with iframe-isolated style encapsulation.
74+
- **In-Browser Diagnostics:** Full lint and type diagnostics with jump-to-source navigation.
75+
- **AI Integration:** Chat with tab-aware edit proposals and explicit apply/undo controls.
3476

35-
This is not only "can this compile?" It is about shipping the whole iteration
36-
loop in one place: edit, validate, preview, sync, and refine.
77+
The goal is a complete iteration loop without switching tools: edit, check, preview, and sync.
3778

3879
## Why `@knighted/jsx` + `@knighted/css` Matter Here
3980

4081
The app demonstrates both libraries in realistic authoring conditions:
4182

42-
- `@knighted/jsx` provides a direct path from JSX to rendered output,
43-
including DOM-first workflows.
44-
- `@knighted/css` handles modern browser-side style compilation,
45-
including Modules/Less/Sass modes.
83+
- `@knighted/jsx` provides a direct path from JSX to rendered output, including DOM-first workflows.
84+
- `@knighted/css` handles modern browser-side style compilation, including Modules/Less/Sass modes.
4685

47-
Together they show how much of the authoring cycle modern browsers can run
48-
directly.
86+
Together they show what a browser can handle natively when you stop routing everything through a local build tool.
4987

5088
## "Compiler-as-a-Service" Without A Build Farm
5189

5290
In this project, Compiler-as-a-Service means:
5391

5492
- CDN delivers modules and WASM artifacts.
55-
- The browser session performs compile, lint, typecheck, render, and editor
56-
interactions locally.
93+
- The browser session performs compile, lint, typecheck, render, and editor interactions locally.
5794

58-
It is service-oriented distribution with local execution.
59-
60-
Mode-aware loading keeps costs aligned to usage: if you do not use Sass,
61-
Sass does not load.
95+
It is service-oriented distribution with local execution. Mode-aware loading means you only download what you use: skip Sass mode and the Sass bundle never loads.
6296

6397
## Why This Matters
6498

65-
This does not replace production pipelines.
66-
67-
It lowers the cost of exploration while preserving enough workflow surface to
68-
be useful for real component work.
99+
This does not replace production pipelines — it removes the setup cost for exploratory work where a full project scaffold is overkill.
69100

70-
When setup friction drops, teams try more ideas. When feedback is immediate,
71-
they converge faster. When browser-native workspaces can sync to GitHub and
72-
carry chat-assisted edit proposals, collaboration is lighter too.
101+
The loop is complete enough for real component work: write, render, diagnose, push. If you can share a URL or open a PR from the same tab you are editing in, the feedback cycle gets shorter.
73102

74-
For prototyping and component development, that is a meaningful shift.
103+
For prototyping and focused component work, that is worth something.
75104

76105
## Try It
77106

107+
> **Note:** The app loads its compiler and runtime from CDN on first visit. If something fails to initialize, a hard reload (`Cmd/Ctrl + Shift + R`) is usually enough to recover.
108+
78109
- Live workbench: https://knightedcodemonkey.github.io/develop/
79110
- Source: https://github.com/knightedcodemonkey/develop
111+
- Worker DOM snapshot (Share URL): [Open workspace example](https://knightedcodemonkey.github.io/develop/?sws=H4sIAAAAAAAAE4VWYW_bNhD9Kwd2m-1VkqXUdVM5SpumHVYsWYskxT7UxUJTJ5kJRQokbdc1_N8HklacpN1mwLZ4vHs8vnt39oYsURuuJMmziDDVtBpNWJP6G29JRJhGarE8sSTPXhw-H2ejLB2n6bOIGElbM1eW5BvCS5KTlfm7epaVmLGX8fj5GOMRKw_il-noMD4o8RDT6vBFOUYSEbNoURsssXzvAklEVkrfmpYyvGSqRZIToRgV9zf-wHVnzvMKqY1dvkqitHFZ1i9iJkaGRERjqwLmjBqH1FAuSUTmSN1ZLnL448hW_7loZqhJLhdCuPUVt8JB_KX0LWp4--Hcu50qafGrvbTUul0uKbN8if5wWaI-V6Wzl6ohEamUtKfGfNKC5GRubWvy4dAZTVIrVQukLTcJU82QGXPwqqINF-vivbSoc26piFRrvkWrem5fp1E2SpJnB1GWpknyMk0n2WPLL7v4CzVTVgWALvZeUOddctMKui7MirpiWzozJP-8qydKq9ckIpI27jonbZtY89UxQO2c5MRotmfSDPf7gsp6QWsXdEOX1DDNWxvf-D2tPKMdNjcngbvc6gVGxDCthCB56uQoLUpLcvdkLJy0LRTQH0BxDJupBAhmphaOq3cCCjgq-RKMXQsspp742PBvmMNzjc0EvGGFvJ7bHGZKlJMpOc7S9GhY8uUxUAO_X52fveXLdwIblHayP2TlBXCqSoQCrp0dQKAFY6m2V7xx5hZ1pXRDJcNEqlV_MNn7Wc5uDRSQ7mzVQjLLlfQb_UG4j3t5x6dPJ93aoKiSVhl7jsbQGvtZmkIc3AZ3XryCfjjiCLI0vYfnEHyCamG9S-QcHES__13CEO8v5BY7zF99yGCwP28bHnZf4Q5-8_o7zqAAiSsIHdT_dHGWhInyYXaDzH66OOu77TdCzfqf9yx_iWADdt1iDj3atoIz6uga7uXUg23IyB0YAhMlm8CS0wnuhQJ7kSSucU-DsqCAS6u5rPuYlNTS7n6OzWCBowLSQQdvUTdcUou7y279p3trtAstH8pv11p5JfDrBG4WxvJqHe9EnTN0-UxgRtltrdVClvmT7CAbZWwCTAml8ydVVU1gHuSapelyPgEqeC1jbrExHcKUHG_ubrcNUp5M5Xbi21nXaD_q37jAj__Ts9y85dqu7_pwLVmY-mEWCmrspbddYKMsXs5ptxVcT--6lTet0hZ6STL0XJghbduEGdNzxXJFhdOQ8JuFtUp-1Kp1veFLJegMRQ7G18UZlDwVnN3m0MelIw7O1cLgO_fsC7xUvJzKrYMOsnuA7YSwCaBRBwXb_AcJeLC-O_FoFkKZoMYUU7JjNw7mKfG6LKbkbr3DLTa7h-1x0NHGn-ub5GgYvI-ncrBP9buJ5kaFP86Nin0nzaksBYbki38j4r7S4WkBWbd2CLOODR-bsIXWKO2V10c3-AIZu9kXgkPYo5659omYHH4KwtteP3B2fWPQJuE3EYpdRj_DARRFASm8gl5FhcEe5NBzaus9iPe0n3FjE6vqWmC_x03s0u5Fj6EGvgvDCOha8GH5fQWKXpdx2tsX6x6pWxgeew2RIPRzVfKKY_noL8_2S0TCra7o7P39X8jHf5LS8Xh0cHh4-N9w238AZi93_38JAAA)
80112

81113
If you want a fast product tour, try this sequence:
82114

83-
1. Add a new tab, rename it, and make an edit.
115+
1. Spin up a new workspace or add a new file tab, rename it, and make an edit.
84116
2. Toggle DOM -> React render mode.
85117
3. Toggle CSS -> Modules -> Less -> Sass style mode.
86118
4. Open diagnostics and jump to a reported line.
87-
5. Connect BYOT (Bring Your Own Token) by adding a GitHub personal access token, then run Open PR / Push Commit.
88-
6. Ask chat for a targeted tab update, then apply it.
119+
5. Copy a Share URL to send your current workspace state to someone else.
120+
6. Connect your GitHub personal access token (BYOT) to run an Open PR or Push Commit.
121+
7. Ask chat for a targeted tab update, then apply it.
89122

90-
That flow tells the product story better than any architecture diagram.
123+
That covers most of what the app does.

playwright/AGENTS.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
name: knighted-develop-playwright-agent
3+
description: Focused guidance for Playwright E2E authoring and triage in @knighted/develop.
4+
---
5+
6+
You are working in @knighted/develop Playwright E2E tests. Keep feedback loops short and avoid full-suite reruns unless explicitly requested.
7+
8+
## Scope
9+
10+
- Folder: playwright/
11+
- Focus: test behavior, selectors, test stability, and fixture/setup correctness
12+
- Keep changes minimal and localized to the failing behavior
13+
14+
## Fast Failure Loop
15+
16+
- Start with one browser at a time: Chromium first.
17+
- Run one spec file before running broader groups.
18+
- When possible, run only the failing test name(s).
19+
- Do not run full Playwright shards locally unless explicitly requested.
20+
21+
## Flake Triage
22+
23+
- Check failure output for network/CDN/API timing flakes first.
24+
- Retry flaky failures once.
25+
- If the same assertion fails again, treat it as deterministic and fix code/tests.
26+
- Prefer fixing stale expectations when product behavior intentionally changed.
27+
28+
## Test Authoring Rules
29+
30+
- Prefer semantic selectors: getByRole, getByLabel, getByText.
31+
- Use explicit accessible names for interactive controls.
32+
- Use locator() only when semantic selectors are not reliable.
33+
- For known WebKit dialog issues, prefer a stable dialog id and evaluate-based click for dialog confirmation controls.
34+
35+
## PR and Workspace Assertions
36+
37+
- When asserting Git payloads, prefer verifying exact file paths/content over fragile counts when defaults may evolve.
38+
- Keep assertions aligned with default workspace/tab contracts.
39+
40+
## Validation Commands
41+
42+
- Lint after JS/TS edits: npm run lint
43+
- For Playwright changes, prefer targeted execution first.
44+
45+
## Boundaries
46+
47+
- Do not change build/import-map scripts unless required by the test task.
48+
- Do not broaden CI scope or shard counts unless explicitly requested.
49+
- Do not modify generated outputs or lockfiles unless explicitly requested.

playwright/github-pr-drawer/open-pr-create.spec.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
waitForAppReady,
2626
} from './github-pr-drawer.helpers.js'
2727

28-
test('Open PR drawer confirms and submits component/styles filepaths', async ({
28+
test('Open PR drawer confirms and submits default workspace filepaths', async ({
2929
page,
3030
}) => {
3131
const customCommitMessage = 'chore: sync develop editor outputs'
@@ -191,7 +191,19 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
191191
expect(createdRefPayload?.ref).toBe('refs/heads/Develop/Open-Pr-Test')
192192
expect(createdRefPayload?.sha).toBe('abc123mainsha')
193193
expect(treeRequests).toHaveLength(1)
194-
expect((treeRequests[0]?.tree as Array<Record<string, unknown>>)?.length).toBe(2)
194+
const submittedTree = treeRequests[0]?.tree
195+
expect(Array.isArray(submittedTree)).toBe(true)
196+
const submittedPaths = (submittedTree as Array<Record<string, unknown>>).map(
197+
entry => entry.path,
198+
)
199+
expect(submittedPaths).toHaveLength(3)
200+
expect(submittedPaths).toEqual(
201+
expect.arrayContaining([
202+
'src/components/App.tsx',
203+
'src/components/Counter.tsx',
204+
'src/styles/app.css',
205+
]),
206+
)
195207
expect(commitRequests).toHaveLength(1)
196208
expect(commitRequests[0]?.message).toBe(customCommitMessage)
197209
expect(updateRefRequests).toHaveLength(1)
@@ -2083,7 +2095,19 @@ test('Open PR drawer uses Git Database API atomic commit path by default', async
20832095
)
20842096

20852097
expect(treeRequests).toHaveLength(1)
2086-
expect((treeRequests[0]?.tree as Array<Record<string, unknown>>)?.length).toBe(2)
2098+
const submittedTree = treeRequests[0]?.tree
2099+
expect(Array.isArray(submittedTree)).toBe(true)
2100+
const submittedPaths = (submittedTree as Array<Record<string, unknown>>).map(
2101+
entry => entry.path,
2102+
)
2103+
expect(submittedPaths).toHaveLength(3)
2104+
expect(submittedPaths).toEqual(
2105+
expect.arrayContaining([
2106+
'src/components/App.tsx',
2107+
'src/components/Counter.tsx',
2108+
'src/styles/app.css',
2109+
]),
2110+
)
20872111
expect(commitRequests).toHaveLength(1)
20882112
expect(updateRefRequests).toHaveLength(1)
20892113
expect(updateRefRequests[0]?.sha).toBe('new-commit-sha')

src/app.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { createLayoutDiagnosticsSetup } from './modules/app-core/layout-diagnost
3636
import { createWorkspaceControllersSetup } from './modules/app-core/workspace-controllers-setup.js'
3737
import { createWorkspaceScopeForkActions } from './modules/app-core/workspace-scope-fork-actions.js'
3838
import { createGitHubWorkflowsSetup } from './modules/app-core/github-workflows-setup.js'
39-
import { defaultCss, defaultJsx } from './modules/app-core/defaults.js'
39+
import { defaultCss, defaultJsx, defaultModuleJsx } from './modules/app-core/defaults.js'
4040
import { createGitHubPrContextUiController } from './modules/app-core/github-pr-context-ui.js'
4141
import { createGitHubTokenInfoUiController } from './modules/app-core/github-token-info-ui.js'
4242
import {
@@ -227,8 +227,10 @@ const clearConfirmCopy = document.getElementById('clear-confirm-copy')
227227
const clearConfirmButton = clearConfirmDialog?.querySelector('button[value="confirm"]')
228228

229229
const defaultComponentTabPath = 'src/components/App.tsx'
230+
const defaultModuleTabPath = 'src/components/Counter.tsx'
230231
const defaultStylesTabPath = 'src/styles/app.css'
231232
const defaultComponentTabName = 'App.tsx'
233+
const defaultModuleTabName = 'Counter.tsx'
232234
const defaultStylesTabName = 'app.css'
233235
const editorKinds = ['component', 'styles']
234236
const editorPanelsByKind = {
@@ -279,6 +281,15 @@ const workspaceTabsState = createWorkspaceTabsState({
279281
isActive: true,
280282
content: defaultJsx,
281283
},
284+
{
285+
id: 'counter',
286+
name: defaultModuleTabName,
287+
path: defaultModuleTabPath,
288+
language: 'javascript-jsx',
289+
role: 'module',
290+
isActive: false,
291+
content: defaultModuleJsx,
292+
},
282293
{
283294
id: 'styles',
284295
name: defaultStylesTabName,

src/modules/app-core/defaults.js

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
11
export const defaultJsx = [
2+
"import { Counter } from './Counter.js'",
3+
'',
4+
'export const App = () => (',
5+
' <main>',
6+
" <Counter label='Clicks' />",
7+
' </main>',
8+
')',
9+
'',
10+
].join('\n')
11+
12+
export const defaultModuleJsx = [
213
"import '../styles/app.css'",
314
'',
4-
'type CounterButtonProps = {',
15+
'type CounterProps = {',
516
' label: string',
6-
' onClick: (event: MouseEvent) => void',
717
'}',
818
'',
9-
'const CounterButton = ({ label, onClick }: CounterButtonProps) => (',
10-
' <button class="counter-button" type="button" onClick={onClick}>',
11-
' {label}',
12-
' </button>',
13-
')',
14-
'',
15-
'const App = () => {',
19+
'export const Counter = ({ label }: CounterProps) => {',
20+
' const el = (',
21+
" <button class='counter-button' type='button'>",
22+
' {label}: 0',
23+
' </button>',
24+
' ) as HTMLButtonElement',
1625
' let count = 0',
17-
' const handleClick = (event: MouseEvent) => {',
26+
'',
27+
' el.onclick = () => {',
1828
' count += 1',
19-
' const button = event.currentTarget as HTMLButtonElement',
20-
' button.textContent = `Clicks: ${count}`',
21-
" button.dataset.active = count % 2 === 0 ? 'false' : 'true'",
22-
" button.classList.toggle('is-even', count % 2 === 0)",
29+
' el.textContent = `${label}: ${count}`',
30+
" el.dataset.active = count % 2 === 0 ? 'false' : 'true'",
31+
" el.classList.toggle('is-even', count % 2 === 0)",
2332
' }',
2433
'',
25-
" return <CounterButton label='Clicks: 0' onClick={handleClick} />",
34+
' return el',
2635
'}',
2736
'',
2837
].join('\n')

0 commit comments

Comments
 (0)