diff --git a/docs/article.md b/docs/article.md
index e049e50..6a93939 100644
--- a/docs/article.md
+++ b/docs/article.md
@@ -1,90 +1,123 @@
-# Forget The Build Step: A Browser-Native Editor For JSX + CSS
+---
+title: 'Forget The Build Step: A Browser-Native Editor For JSX + CSS'
+published: true
+description: '@knighted/develop is a browser-native editor for JSX and CSS — compile, lint, typecheck, and sync to GitHub without a local build step.'
+tags: javascript, webdev, showdev, react
+---
Frontend tooling is incredibly capable.
It is also often front-loaded.
-For many UI ideas, the first thing you do is not write code. You install,
-configure, and wait. The creative loop starts late.
+For many UI ideas, the first thing you do is not write code. You install, configure, and wait. The creative loop starts late.
-[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built
-for a different default: fast prototyping from anywhere you can open a browser.
+[@knighted/develop](https://github.com/knightedcodemonkey/develop) is built for a different default: fast prototyping from anywhere you can open a browser.
-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.
+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.
## The Loop, In Practice
-Open the app, edit multiple files in dynamic tabs, switch render/style modes,
-run lint/type diagnostics, and preview instantly.
+Open the app, spin up isolated workspaces, edit multiple files in dynamic tabs, switch render/style modes, run lint/type diagnostics, and preview instantly.
No local bundler is required for that inner loop.
+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:
+
+`App.tsx`
+
+```tsx
+import { Counter } from './Counter.js'
+
+export const App = () => (
+
+
+
+)
+```
+
+`Counter.tsx`
+
+```tsx
+import '../styles/app.css'
+
+type CounterProps = {
+ label: string
+}
+
+export const Counter = ({ label }: CounterProps) => {
+ const el = (
+
+ ) as HTMLButtonElement
+ let count = 0
+
+ el.onclick = () => {
+ count += 1
+ el.textContent = `${label}: ${count}`
+ el.classList.toggle('is-even', count % 2 === 0)
+ }
+
+ return el
+}
+```
+
## What The App Gives You
-- Dynamic tabbed editing (add, rename, remove, and protect required entry tabs)
-- Render mode switch: DOM or React
-- Style mode switch: CSS, CSS Modules, Less, Sass
-- Live preview with iframe-isolated style encapsulation
-- In-browser lint and type diagnostics with jump-to-source navigation
-- GitHub-connected workflows for Open PR and Push Commit
-- AI chat with tab-aware edit proposals and explicit apply/undo controls
+- **Multiple Workspaces:** Create and switch between isolated projects without losing your current files.
+- **Dynamic Tabbed Editing:** Add, rename, remove, and protect required entry tabs within any workspace.
+- **Instant Share URLs:** Encode the current workspace state into a URL you can share or bookmark.
+- **Direct GitHub Synchronization:** Open pull requests and push commits from the browser.
+- **Render Mode Switch:** Toggle instantly between DOM or React runtimes.
+- **Style Mode Switch:** Support for CSS, CSS Modules, Less, and Sass.
+- **Live Preview:** Real-time updates with iframe-isolated style encapsulation.
+- **In-Browser Diagnostics:** Full lint and type diagnostics with jump-to-source navigation.
+- **AI Integration:** Chat with tab-aware edit proposals and explicit apply/undo controls.
-This is not only "can this compile?" It is about shipping the whole iteration
-loop in one place: edit, validate, preview, sync, and refine.
+The goal is a complete iteration loop without switching tools: edit, check, preview, and sync.
## Why `@knighted/jsx` + `@knighted/css` Matter Here
The app demonstrates both libraries in realistic authoring conditions:
-- `@knighted/jsx` provides a direct path from JSX to rendered output,
- including DOM-first workflows.
-- `@knighted/css` handles modern browser-side style compilation,
- including Modules/Less/Sass modes.
+- `@knighted/jsx` provides a direct path from JSX to rendered output, including DOM-first workflows.
+- `@knighted/css` handles modern browser-side style compilation, including Modules/Less/Sass modes.
-Together they show how much of the authoring cycle modern browsers can run
-directly.
+Together they show what a browser can handle natively when you stop routing everything through a local build tool.
## "Compiler-as-a-Service" Without A Build Farm
In this project, Compiler-as-a-Service means:
- CDN delivers modules and WASM artifacts.
-- The browser session performs compile, lint, typecheck, render, and editor
- interactions locally.
+- The browser session performs compile, lint, typecheck, render, and editor interactions locally.
-It is service-oriented distribution with local execution.
-
-Mode-aware loading keeps costs aligned to usage: if you do not use Sass,
-Sass does not load.
+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.
## Why This Matters
-This does not replace production pipelines.
-
-It lowers the cost of exploration while preserving enough workflow surface to
-be useful for real component work.
+This does not replace production pipelines — it removes the setup cost for exploratory work where a full project scaffold is overkill.
-When setup friction drops, teams try more ideas. When feedback is immediate,
-they converge faster. When browser-native workspaces can sync to GitHub and
-carry chat-assisted edit proposals, collaboration is lighter too.
+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.
-For prototyping and component development, that is a meaningful shift.
+For prototyping and focused component work, that is worth something.
## Try It
+> **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.
+
- Live workbench: https://knightedcodemonkey.github.io/develop/
- Source: https://github.com/knightedcodemonkey/develop
+- 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)
If you want a fast product tour, try this sequence:
-1. Add a new tab, rename it, and make an edit.
+1. Spin up a new workspace or add a new file tab, rename it, and make an edit.
2. Toggle DOM -> React render mode.
3. Toggle CSS -> Modules -> Less -> Sass style mode.
4. Open diagnostics and jump to a reported line.
-5. Connect BYOT (Bring Your Own Token) by adding a GitHub personal access token, then run Open PR / Push Commit.
-6. Ask chat for a targeted tab update, then apply it.
+5. Copy a Share URL to send your current workspace state to someone else.
+6. Connect your GitHub personal access token (BYOT) to run an Open PR or Push Commit.
+7. Ask chat for a targeted tab update, then apply it.
-That flow tells the product story better than any architecture diagram.
+That covers most of what the app does.
diff --git a/playwright/AGENTS.md b/playwright/AGENTS.md
new file mode 100644
index 0000000..2787662
--- /dev/null
+++ b/playwright/AGENTS.md
@@ -0,0 +1,49 @@
+---
+name: knighted-develop-playwright-agent
+description: Focused guidance for Playwright E2E authoring and triage in @knighted/develop.
+---
+
+You are working in @knighted/develop Playwright E2E tests. Keep feedback loops short and avoid full-suite reruns unless explicitly requested.
+
+## Scope
+
+- Folder: playwright/
+- Focus: test behavior, selectors, test stability, and fixture/setup correctness
+- Keep changes minimal and localized to the failing behavior
+
+## Fast Failure Loop
+
+- Start with one browser at a time: Chromium first.
+- Run one spec file before running broader groups.
+- When possible, run only the failing test name(s).
+- Do not run full Playwright shards locally unless explicitly requested.
+
+## Flake Triage
+
+- Check failure output for network/CDN/API timing flakes first.
+- Retry flaky failures once.
+- If the same assertion fails again, treat it as deterministic and fix code/tests.
+- Prefer fixing stale expectations when product behavior intentionally changed.
+
+## Test Authoring Rules
+
+- Prefer semantic selectors: getByRole, getByLabel, getByText.
+- Use explicit accessible names for interactive controls.
+- Use locator() only when semantic selectors are not reliable.
+- For known WebKit dialog issues, prefer a stable dialog id and evaluate-based click for dialog confirmation controls.
+
+## PR and Workspace Assertions
+
+- When asserting Git payloads, prefer verifying exact file paths/content over fragile counts when defaults may evolve.
+- Keep assertions aligned with default workspace/tab contracts.
+
+## Validation Commands
+
+- Lint after JS/TS edits: npm run lint
+- For Playwright changes, prefer targeted execution first.
+
+## Boundaries
+
+- Do not change build/import-map scripts unless required by the test task.
+- Do not broaden CI scope or shard counts unless explicitly requested.
+- Do not modify generated outputs or lockfiles unless explicitly requested.
diff --git a/playwright/github-pr-drawer/open-pr-create.spec.ts b/playwright/github-pr-drawer/open-pr-create.spec.ts
index 8ed3c74..a05091b 100644
--- a/playwright/github-pr-drawer/open-pr-create.spec.ts
+++ b/playwright/github-pr-drawer/open-pr-create.spec.ts
@@ -25,7 +25,7 @@ import {
waitForAppReady,
} from './github-pr-drawer.helpers.js'
-test('Open PR drawer confirms and submits component/styles filepaths', async ({
+test('Open PR drawer confirms and submits default workspace filepaths', async ({
page,
}) => {
const customCommitMessage = 'chore: sync develop editor outputs'
@@ -191,7 +191,19 @@ test('Open PR drawer confirms and submits component/styles filepaths', async ({
expect(createdRefPayload?.ref).toBe('refs/heads/Develop/Open-Pr-Test')
expect(createdRefPayload?.sha).toBe('abc123mainsha')
expect(treeRequests).toHaveLength(1)
- expect((treeRequests[0]?.tree as Array>)?.length).toBe(2)
+ const submittedTree = treeRequests[0]?.tree
+ expect(Array.isArray(submittedTree)).toBe(true)
+ const submittedPaths = (submittedTree as Array>).map(
+ entry => entry.path,
+ )
+ expect(submittedPaths).toHaveLength(3)
+ expect(submittedPaths).toEqual(
+ expect.arrayContaining([
+ 'src/components/App.tsx',
+ 'src/components/Counter.tsx',
+ 'src/styles/app.css',
+ ]),
+ )
expect(commitRequests).toHaveLength(1)
expect(commitRequests[0]?.message).toBe(customCommitMessage)
expect(updateRefRequests).toHaveLength(1)
@@ -2083,7 +2095,19 @@ test('Open PR drawer uses Git Database API atomic commit path by default', async
)
expect(treeRequests).toHaveLength(1)
- expect((treeRequests[0]?.tree as Array>)?.length).toBe(2)
+ const submittedTree = treeRequests[0]?.tree
+ expect(Array.isArray(submittedTree)).toBe(true)
+ const submittedPaths = (submittedTree as Array>).map(
+ entry => entry.path,
+ )
+ expect(submittedPaths).toHaveLength(3)
+ expect(submittedPaths).toEqual(
+ expect.arrayContaining([
+ 'src/components/App.tsx',
+ 'src/components/Counter.tsx',
+ 'src/styles/app.css',
+ ]),
+ )
expect(commitRequests).toHaveLength(1)
expect(updateRefRequests).toHaveLength(1)
expect(updateRefRequests[0]?.sha).toBe('new-commit-sha')
diff --git a/src/app.js b/src/app.js
index 990ffcc..9ba30b4 100644
--- a/src/app.js
+++ b/src/app.js
@@ -36,7 +36,7 @@ import { createLayoutDiagnosticsSetup } from './modules/app-core/layout-diagnost
import { createWorkspaceControllersSetup } from './modules/app-core/workspace-controllers-setup.js'
import { createWorkspaceScopeForkActions } from './modules/app-core/workspace-scope-fork-actions.js'
import { createGitHubWorkflowsSetup } from './modules/app-core/github-workflows-setup.js'
-import { defaultCss, defaultJsx } from './modules/app-core/defaults.js'
+import { defaultCss, defaultJsx, defaultModuleJsx } from './modules/app-core/defaults.js'
import { createGitHubPrContextUiController } from './modules/app-core/github-pr-context-ui.js'
import { createGitHubTokenInfoUiController } from './modules/app-core/github-token-info-ui.js'
import {
@@ -227,8 +227,10 @@ const clearConfirmCopy = document.getElementById('clear-confirm-copy')
const clearConfirmButton = clearConfirmDialog?.querySelector('button[value="confirm"]')
const defaultComponentTabPath = 'src/components/App.tsx'
+const defaultModuleTabPath = 'src/components/Counter.tsx'
const defaultStylesTabPath = 'src/styles/app.css'
const defaultComponentTabName = 'App.tsx'
+const defaultModuleTabName = 'Counter.tsx'
const defaultStylesTabName = 'app.css'
const editorKinds = ['component', 'styles']
const editorPanelsByKind = {
@@ -279,6 +281,15 @@ const workspaceTabsState = createWorkspaceTabsState({
isActive: true,
content: defaultJsx,
},
+ {
+ id: 'counter',
+ name: defaultModuleTabName,
+ path: defaultModuleTabPath,
+ language: 'javascript-jsx',
+ role: 'module',
+ isActive: false,
+ content: defaultModuleJsx,
+ },
{
id: 'styles',
name: defaultStylesTabName,
diff --git a/src/modules/app-core/defaults.js b/src/modules/app-core/defaults.js
index ddff06f..e28f981 100644
--- a/src/modules/app-core/defaults.js
+++ b/src/modules/app-core/defaults.js
@@ -1,28 +1,37 @@
export const defaultJsx = [
+ "import { Counter } from './Counter.js'",
+ '',
+ 'export const App = () => (',
+ ' ',
+ " ",
+ ' ',
+ ')',
+ '',
+].join('\n')
+
+export const defaultModuleJsx = [
"import '../styles/app.css'",
'',
- 'type CounterButtonProps = {',
+ 'type CounterProps = {',
' label: string',
- ' onClick: (event: MouseEvent) => void',
'}',
'',
- 'const CounterButton = ({ label, onClick }: CounterButtonProps) => (',
- ' ',
- ')',
- '',
- 'const App = () => {',
+ 'export const Counter = ({ label }: CounterProps) => {',
+ ' const el = (',
+ " ',
+ ' ) as HTMLButtonElement',
' let count = 0',
- ' const handleClick = (event: MouseEvent) => {',
+ '',
+ ' el.onclick = () => {',
' count += 1',
- ' const button = event.currentTarget as HTMLButtonElement',
- ' button.textContent = `Clicks: ${count}`',
- " button.dataset.active = count % 2 === 0 ? 'false' : 'true'",
- " button.classList.toggle('is-even', count % 2 === 0)",
+ ' el.textContent = `${label}: ${count}`',
+ " el.dataset.active = count % 2 === 0 ? 'false' : 'true'",
+ " el.classList.toggle('is-even', count % 2 === 0)",
' }',
'',
- " return ",
+ ' return el',
'}',
'',
].join('\n')