You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(playground): design system blocks with multi-file TSX samples and file tree (#272)
* feat(playground): add design system blocks with multi-file TSX samples and file tree
Overhaul the playground app to support multi-file editing with a new
FileTree component. Replace Vue SFC samples with TSX + Styleframe config
pairs covering Avatar, Badge, Button, Callout, Card, Checkbox, Input, and
Spinner. Introduce bundlePreview pipeline (replaces compileVueSfc), new
playground UI recipes (file tab, file tree, brand mark, editor shell), and
update state management and build config accordingly.
Also simplifies the field-group recipe to consolidate .input/.select/.textarea
flex-grow selectors into a single joined CSS selector.
* fix(ci): fix format and failing field-group test
- Format apps/playground/test/pipeline.test.ts with oxfmt
- Replace combined CSS selector key with individual selectors in
useFieldGroupRecipe so the seam-selector test can find each child rule
* test(playground): expand bundlePreview, codemirror, and state tests
Interactive client-side playground for Styleframe. Users edit three files in CodeMirror; an iframe renders the live result.
3
+
Interactive client-side playground for Styleframe. Users edit an arbitrary set of files (React `tsx` components + a `styleframe.config.ts`) in CodeMirror; an iframe renders the live result.
4
4
5
5
## Package
6
6
7
7
-**Name:**`@styleframe/playground`
8
-
-**Build tool:** Vite 7 + Vue 3
8
+
-**Shell:** Vite + Vue 3 (the playground UI is a Vue app; it is invisible to users)
9
+
-**Preview:** React 19 + `tsx`, bundled in the browser with `esbuild-wasm`
9
10
-**Runs:** fully client-side — no server, no auth, no persistence.
10
11
-**Not published to npm.**
11
12
@@ -14,42 +15,35 @@ Interactive client-side playground for Styleframe. Users edit three files in Cod
| Context | Compiled by | Where CSS lives | Runtime |
61
55
|---|---|---|---|
62
56
| Shell UI |`@styleframe/plugin/vite` at dev/build time |`<style>` in parent document |`@styleframe/runtime` bundled into parent |
63
-
| User-edited preview |`esbuild-wasm` + `@styleframe/transpiler` at runtime |`<style>` inside iframe `srcdoc`|`@styleframe/runtime`loaded via iframe importmap|
57
+
| User-edited preview |`esbuild-wasm` + `@styleframe/transpiler` at runtime |`<style>` inside iframe `srcdoc`|React + `@styleframe/runtime`inlined into the preview bundle|
64
58
65
-
The iframe has `sandbox="allow-scripts allow-same-origin"` so it can fetch blob URLs and use an importmap. Cross-file imports between user files are rewritten to blob URLs:
59
+
The iframe has `sandbox="allow-scripts allow-same-origin"`. There is **no importmap**: `bundlePreview` resolves everything through an in-memory virtual-FS esbuild plugin and inlines it. React is the one exception — it is vendored once as an IIFE (`vite.config.ts` → `buildReactVendor`) that publishes `globalThis.PGReactVendor`, runs first as a classic `<script>`, and is read by thin shims in the bundle. This keeps a single React instance and the whole preview offline.
66
60
67
-
-`./Component.vue` inside App.vue → blob URL of compiled Component
68
-
-`./styleframe.config` inside Component.vue → blob URL of the compiled runtime TS module (which exports `card`, `cardHeader`, etc.)
61
+
The virtual-FS plugin resolves:
62
+
- relative imports (`./Card`, `../foo/Bar`) against the in-memory `files` map (trying `.tsx/.ts/.jsx/.js/.css` + `index.*`);
63
+
-`virtual:styleframe` → the generated runtime TS module (recipe functions like `card`, `button`);
64
+
-`virtual:styleframe.css` → empty (the CSS is injected separately);
`runPipeline(input)` runs these sequentially, and returns `{ ok: false, stage, error }` if any step throws:
69
+
`runPipeline(input)` runs these sequentially, and returns `{ ok: false, stage, error }` if any step throws. `input` is `{ files, configPath, entryPath }`.
`*.styleframe.ts` extension files are evaluated like `@styleframe/plugin` loads them: `import { styleframe } from "virtual:styleframe"` returns the same instance the config created (`evalStyleframeFile` shims `styleframe()` to return it), so each file's recipe registrations mutate the shared instance. They are authoring-only — never scanned or bundled into the preview.
82
+
83
+
`transpileStyleframe` runs with `treeshake: false` so the preview shows the **complete** generated CSS — the scanner cannot see runtime recipe calls, so tree-shaking would drop recipe/utility CSS the rendered components depend on.
82
84
83
85
On success, the parent assigns `iframe.srcdoc = srcdoc` and revokes the previous run's blob URLs.
84
86
85
87
## Key Conventions
86
88
87
-
1.**Shell styling is via Styleframe only.** No inline styles except for a handful of layout-critical resets in `App.vue`/`SplitPane.vue` (`flex: 1`, `min-height: 0`, etc.). All colors/spacing/borders come from the recipes in `src/recipes/`.
88
-
2.**User file specifiers.** User code imports recipes from `"./styleframe.config"` (no extension). The pipeline rewrites that to a blob URL. User code imports `Component.vue` from `"./Component.vue"` — same treatment.
89
-
3.**Sample files are in `src/samples/`**, imported via `?raw`. Edit these to change what the playground shows on first load.
90
-
4.**CodeMirror 6 used directly.** No wrapper library. Language is selected per file via `@codemirror/lang-javascript` (`typescript: true`) or `@codemirror/lang-vue`.
91
-
5.**Parent never evaluates untrusted code.** The user's config runs inside `new Function(...)` with a rewritten source; the Vue SFCs only run inside the iframe.
92
-
6.**The last valid preview stays on screen.** The error banner overlays the affected tab; blob URLs from the previous successful run are only revoked when the next run succeeds.
89
+
1.**Shell styling is via Styleframe only.** No inline styles except a handful of layout-critical resets in `App.vue` (`flex: 1`, `min-height: 0`). All chrome comes from the recipes in `src/recipes/`.
90
+
2.**User code mirrors a real Styleframe app.** The default project lives under `src/`, with each component in `src/components/<Name>/` paired with a `<Name>.styleframe.ts` extension file that registers its recipe(s). Components import the compiled recipes from `"virtual:styleframe"`; `styleframe.config.ts` (root) holds presets only and exports the instance as default. `src/App.tsx` is the entry — its default export is the preview root.
91
+
3.**Dynamic files and folders.**`state/playground.ts` keys files by path and exposes `createFile` / `createFolder` / `renameFile` / `deleteFile` / `deleteFolder` (the config and entry files, and any folder containing them, are protected). Empty folders are tracked in `state.folders`. `FileTree.vue` drives them.
92
+
4.**Sample files are in `src/samples/`** (`.tsx`, `.styleframe.sample.ts`, config), imported via `?raw`, and excluded from the shell `tsconfig` (they target the user's `virtual:styleframe`, not the shell's). The `.styleframe.sample.ts` suffix keeps them from matching the shell plugin's `**/*.styleframe.ts` glob.
93
+
5.**CodeMirror 6 used directly.** Language per file via `languageForPath` (`@codemirror/lang-javascript` with `jsx` for `.tsx`, plain `ts`, or `@codemirror/lang-css`).
94
+
6.**Parent never evaluates untrusted code.** Only the config runs in `new Function(...)`; components run only inside the iframe.
95
+
7.**The last valid preview stays on screen.** Blob URLs from the previous successful run are revoked only when the next run succeeds.
93
96
94
97
## Testing
95
98
@@ -98,11 +101,13 @@ pnpm test --filter @styleframe/playground # Vitest unit tests (jsdom)
98
101
pnpm typecheck --filter @styleframe/playground
99
102
```
100
103
101
-
Manual verification: run `pnpm dev:playground`, edit each of the three files, confirm the three right-hand tabs update, and force a syntax error to see the banner behavior.
104
+
> Adding a `pg-*` recipe requires running `pnpm dev:playground` (or `build`) once so the plugin regenerates `.styleframe/styleframe.d.ts` before `typecheck` passes.
105
+
106
+
Manual verification: run `pnpm dev:playground`, create a file from the tree, import it into `App.tsx`, press Ctrl/Cmd+S, and confirm the preview, CSS, and JS tabs update.
102
107
103
108
## Related
104
109
105
-
-`theme/src/recipes/card/`— recipes used by the default sample `Component.vue`.
106
-
-`engine/transpiler/src/transpile.ts` — `transpile(instance)`API consumed by `src/pipeline/transpileStyleframe.ts`.
107
-
-`engine/runtime/src/runtime.ts` — `createRecipe` consumed by the iframe's compiled config module.
110
+
-`theme/src/recipes/card/`, `theme/src/recipes/button/`— recipes used by the default samples.
111
+
-`engine/transpiler/src/transpile.ts` — `transpile(instance)` consumed by `src/pipeline/transpileStyleframe.ts`.
112
+
-`engine/runtime/src/runtime.ts` — `createRecipe` consumed by the preview's generated runtime module.
108
113
-`apps/storybook/` — mirror setup for Vite + Vue + `@styleframe/plugin`.
0 commit comments