Skip to content

Commit 414e2fe

Browse files
authored
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
1 parent 5f494e5 commit 414e2fe

60 files changed

Lines changed: 4694 additions & 1620 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@styleframe/theme": patch
3+
---
4+
5+
Simplify field-group recipe selector: consolidate `.input`, `.select`, `.textarea` flex-grow rules into a single joined selector.

apps/playground/AGENTS.md

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Styleframe Playground
22

3-
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.
44

55
## Package
66

77
- **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`
910
- **Runs:** fully client-side — no server, no auth, no persistence.
1011
- **Not published to npm.**
1112

@@ -14,42 +15,35 @@ Interactive client-side playground for Styleframe. Users edit three files in Cod
1415
```
1516
apps/playground/
1617
├── styleframe.config.ts # shell: theme presets + #app / html selectors
17-
├── vite.config.ts # Styleframe Vite plugin + vue()
18+
├── vite.config.ts # Styleframe Vite plugin (minify:false) + vue() + React vendoring
1819
├── index.html
1920
├── src/
20-
│ ├── App.vue # playground shell root
21+
│ ├── App.vue # playground shell root (Editor │ Output │ Tree)
2122
│ ├── main.ts # imports virtual:styleframe.css, mounts App
22-
│ ├── recipes/ # playground-local recipes (pg-*)
23-
│ │ ├── useTabRecipe.ts
24-
│ │ ├── useTabListRecipe.ts
25-
│ │ ├── useSplitPaneRecipe.ts
26-
│ │ ├── useEditorShellRecipe.ts
27-
│ │ ├── useErrorBannerRecipe.ts
28-
│ │ ├── useToolbarRecipe.ts
29-
│ │ ├── index.ts # barrel
23+
│ ├── recipes/ # playground-local recipes (pg-*), incl. useFileTreeRecipe
3024
│ │ └── playground.styleframe.ts # registers recipes into the shell instance
3125
│ ├── components/
32-
│ │ ├── SplitPane.vue
33-
│ │ ├── TabList.vue
34-
│ │ ├── EditorPane.vue # 3-tab editor (config, App, Component)
35-
│ │ ├── OutputPane.vue # 3-tab output (Preview, CSS, JS)
36-
│ │ ├── CodeOutput.vue # read-only CodeMirror
26+
│ │ ├── SplitPane.vue # Editor │ Output split
27+
│ │ ├── FileTree.vue # project tree (far right): create / rename / delete
28+
│ │ ├── EditorPane.vue # dynamic, path-keyed CodeMirror editors + tabs
29+
│ │ ├── OutputPane.vue # Preview / CSS / JS tabs
3730
│ │ ├── PreviewFrame.vue # iframe + postMessage listener
3831
│ │ └── ErrorBanner.vue
3932
│ ├── editor/
40-
│ │ ├── codemirror.ts # createEditor({parent, doc, language, onChange})
41-
│ │ └── theme.ts # CodeMirror theme extension
33+
│ │ ├── codemirror.ts # createEditor(...) + languageForPath() (ts / tsx / css)
34+
│ │ └── theme.ts
4235
│ ├── pipeline/
4336
│ │ ├── esbuild.ts # lazy esbuild-wasm init (single instance)
44-
│ │ ├── transformTs.ts
45-
│ │ ├── evalUserConfig.ts # rewrites imports, runs via new Function
37+
│ │ ├── transformTs.ts # strips TS from the config before eval
38+
│ │ ├── evalUserConfig.ts # rewrites imports, runs config via new Function
39+
│ │ ├── scanAndRegisterUtilities.ts
4640
│ │ ├── transpileStyleframe.ts
47-
│ │ ├── compileVueSfc.ts
48-
│ │ ├── buildSrcdoc.ts
41+
│ │ ├── bundlePreview.ts # esbuild-wasm bundle of all user files → one ESM module
42+
│ │ ├── buildSrcdoc.ts # CSS + React vendor + bundle → srcdoc
4943
│ │ └── pipeline.ts # orchestrator + debounce + PipelineResult
50-
│ ├── samples/ # default file contents, imported as ?raw
44+
│ ├── samples/ # default file contents (.tsx + config), imported as ?raw
5145
│ └── state/
52-
│ └── playground.ts # reactive { files, output, error, active* }
46+
│ └── playground.ts # reactive { files, output, error, active* } + file actions
5347
└── test/ # Vitest specs for pipeline units
5448
```
5549

@@ -60,36 +54,45 @@ apps/playground/
6054
| Context | Compiled by | Where CSS lives | Runtime |
6155
|---|---|---|---|
6256
| 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 |
6458

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.
6660

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);
65+
- `react` / `react-dom/client` / `react/jsx-runtime` / `@styleframe/runtime` → vendored shims/sources.
6966

7067
## Pipeline Stages
7168

72-
`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 }`.
7370

7471
| Stage | Input | Output |
7572
|---|---|---|
76-
| `config-transform` | user's `styleframe.config.ts` source | compiled JS |
73+
| `config-transform` | the `configPath` file's source | compiled JS |
7774
| `config-eval` | compiled JS | `Styleframe` instance |
75+
| `styleframe` | every `*.styleframe.ts` file (sorted) | recipes registered on the shared instance |
76+
| `scan` | every file except the config and `*.styleframe.ts` | utility classes registered on the instance |
7877
| `transpile` | `Styleframe` instance | `{ css, ts }` from `@styleframe/transpiler` |
79-
| `config-compile` | `ts` | compiled JS module for the iframe |
80-
| `vue` | App + Component SFC sources | compiled ESM modules |
81-
| `assemble` | all of the above + Vue/runtime URLs | `srcdoc` string |
78+
| `bundle` | all files + `entryPath` + generated runtime TS | one ESM preview module |
79+
| `assemble` | CSS + preview bundle + React vendor IIFE | `srcdoc` string |
80+
81+
`*.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.
8284

8385
On success, the parent assigns `iframe.srcdoc = srcdoc` and revokes the previous run's blob URLs.
8486

8587
## Key Conventions
8688

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.
9396

9497
## Testing
9598

@@ -98,11 +101,13 @@ pnpm test --filter @styleframe/playground # Vitest unit tests (jsdom)
98101
pnpm typecheck --filter @styleframe/playground
99102
```
100103

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.
102107

103108
## Related
104109

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.
108113
- `apps/storybook/` — mirror setup for Vite + Vue + `@styleframe/plugin`.

0 commit comments

Comments
 (0)