Skip to content

Commit e44a906

Browse files
committed
refactor(plugin-code-server): state-driven view, full-bleed editor, Storybook
- Extract the launcher UI into a pure, state-driven view decoupled from RPC, with styles in a real CSS file instead of an injected string. - Drop the toolbar over the running editor — the iframe is now full-bleed. - Add Storybook (html-vite) with a story per UI state (connecting, not-installed, launch, launch-error, starting, running).
1 parent 2e6ffe0 commit e44a906

17 files changed

Lines changed: 1587 additions & 310 deletions

File tree

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default antfu({
66
ignores: [
77
'skills',
88
'**/dist',
9+
'**/storybook-static',
910
'**/.next',
1011
'**/out',
1112
'**/next-env.d.ts',

plugins/code-server/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
dist
2+
storybook-static
23
.turbo
34
*.tsbuildinfo
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { StorybookConfig } from '@storybook/html-vite'
2+
3+
const config: StorybookConfig = {
4+
stories: ['../src/**/*.stories.@(ts|tsx)'],
5+
framework: {
6+
name: '@storybook/html-vite',
7+
options: {},
8+
},
9+
}
10+
11+
export default config
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { Preview } from '@storybook/html-vite'
2+
import '../src/client/style.css'
3+
4+
const preview: Preview = {
5+
parameters: {
6+
layout: 'fullscreen',
7+
controls: { expanded: true },
8+
},
9+
}
10+
11+
export default preview

plugins/code-server/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,17 @@ export default {
6464

6565
Status (minus the auth cookie) is mirrored into the
6666
`devframes-plugin-code-server:state` shared state for reactive UIs.
67+
68+
## UI
69+
70+
The launcher UI is a pure, state-driven view (`src/client/view.ts`) decoupled
71+
from RPC, so every state renders in isolation. `mountCodeServer` wires the live
72+
connection to it. Each UI state has a Storybook story:
73+
74+
```sh
75+
pnpm storybook # dev
76+
pnpm build-storybook # static build
77+
```
78+
79+
Stories: connecting, not-installed, launch, launch-error, starting, running
80+
(the running story mounts a mock editor instead of a live server).

plugins/code-server/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"build": "tsdown && vite build --config src/spa/vite.config.ts",
4444
"watch": "tsdown --watch",
4545
"dev": "vite --config src/spa/vite.config.ts --host 0.0.0.0",
46+
"storybook": "storybook dev -p 6006 --host 0.0.0.0",
47+
"build-storybook": "storybook build",
4648
"test": "vitest run",
4749
"prepack": "pnpm run build"
4850
},
@@ -60,9 +62,11 @@
6062
"nostics": "catalog:deps"
6163
},
6264
"devDependencies": {
65+
"@storybook/html-vite": "catalog:storybook",
6366
"@types/node": "catalog:types",
6467
"devframe": "workspace:*",
6568
"h3": "catalog:deps",
69+
"storybook": "catalog:storybook",
6670
"tsdown": "catalog:build",
6771
"vite": "catalog:build",
6872
"vitest": "catalog:testing",
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Meta, StoryObj } from '@storybook/html-vite'
2+
import type { CodeServerViewState } from './view'
3+
import { createCodeServerView } from './view'
4+
import './style.css'
5+
6+
// A stand-in for the real code-server iframe so the "running" story renders
7+
// without a live server.
8+
const MOCK_EDITOR = `data:text/html;charset=utf-8,${encodeURIComponent(`
9+
<!doctype html><html><head><meta name="color-scheme" content="dark light" /><style>
10+
html,body{height:100%;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background:#1e1e1e;color:#d4d4d4}
11+
.bar{height:35px;background:#333;display:flex;align-items:center;padding:0 12px;font-size:12px;color:#ccc}
12+
.body{display:flex;height:calc(100% - 35px)}
13+
.side{width:48px;background:#333}
14+
.main{flex:1;padding:16px;font-size:13px;line-height:1.6}
15+
.c{color:#569cd6}.s{color:#ce9178}.f{color:#dcdcaa}
16+
</style></head><body>
17+
<div class="bar">code-server — mock editor (Storybook)</div>
18+
<div class="body"><div class="side"></div><div class="main">
19+
<div><span class="c">export function</span> <span class="f">createCodeServerDevframe</span>(<span class="c">options</span>) {</div>
20+
<div>&nbsp;&nbsp;<span class="c">return</span> <span class="f">defineDevframe</span>({ <span class="s">id</span>, <span class="s">name</span> })</div>
21+
<div>}</div>
22+
</div></div>
23+
</body></html>`)}`
24+
25+
function renderState(state: CodeServerViewState): HTMLElement {
26+
const container = document.createElement('div')
27+
container.style.cssText = 'position:relative;width:100%;height:100vh'
28+
const view = createCodeServerView(container, {
29+
actions: {
30+
launch: () => console.warn('[story] launch'),
31+
recheck: () => console.warn('[story] recheck'),
32+
},
33+
// Keep stories hermetic: never touch real cookies or hosts.
34+
resolveEditorUrl: () => MOCK_EDITOR,
35+
applyAuth: () => {},
36+
})
37+
view.update(state)
38+
return container
39+
}
40+
41+
const meta: Meta = {
42+
title: 'Code Server/Launcher',
43+
parameters: { layout: 'fullscreen' },
44+
}
45+
46+
export default meta
47+
type Story = StoryObj
48+
49+
/** Awaiting the devframe connection / first status. */
50+
export const Connecting: Story = {
51+
render: () => renderState({
52+
detection: { checked: false, installed: false, bin: 'code-server' },
53+
server: { status: 'stopped' },
54+
}),
55+
}
56+
57+
/** Binary missing — install instructions and links. */
58+
export const NotInstalled: Story = {
59+
render: () => renderState({
60+
detection: { checked: true, installed: false, bin: 'code-server' },
61+
server: { status: 'stopped' },
62+
}),
63+
}
64+
65+
/** Installed and idle — the launch screen. */
66+
export const Launch: Story = {
67+
render: () => renderState({
68+
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
69+
server: { status: 'stopped' },
70+
}),
71+
}
72+
73+
/** A previous launch failed — the error surfaces above the launch button. */
74+
export const LaunchError: Story = {
75+
render: () => renderState({
76+
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
77+
server: { status: 'error', error: 'Failed to spawn code-server: EADDRINUSE 127.0.0.1:8080' },
78+
}),
79+
}
80+
81+
/** Spawned, waiting on the readiness probe (and the auth handoff). */
82+
export const Starting: Story = {
83+
render: () => renderState({
84+
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
85+
server: { status: 'starting', port: 8080 },
86+
busy: true,
87+
}),
88+
}
89+
90+
/** Ready — the editor fills the panel, signed in, with no chrome over it. */
91+
export const Running: Story = {
92+
render: () => renderState({
93+
detection: { checked: true, installed: true, version: '4.99.0', bin: 'code-server' },
94+
server: { status: 'running', port: 8080, pid: 4242 },
95+
auth: { cookieName: 'code-server-session', cookieValue: 'a'.repeat(64) },
96+
}),
97+
}

0 commit comments

Comments
 (0)