|
| 1 | +# Playground UX Showcase Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan phase-by-phase. Steps use checkbox (`- [ ]`) syntax for tracking. This targets the demo app under `playground/`, not the shipped library — there is no TDD/unit-test loop; each phase is verified by running the real Vite/Rsbuild build + dev server and opening the pages. |
| 4 | +
|
| 5 | +**Goal:** Turn `playground/` from a minimal two-entry app into a full showcase that (1) exercises the Reprise/bundler features end-to-end and (2) installs the curated Symfony UX set, each with an interactive demo card reachable from a dashboard (one page per demo). Vite **and** Rsbuild must both stay green. |
| 6 | + |
| 7 | +**Architecture:** A data-driven dashboard (`DashboardController` + a PHP demo registry) renders a card grid in two sections — "Reprise features" and "Symfony UX". Each card links to its own route: `FeatureController` for the Reprise-feature pages, `DemoController` for the UX demos. Site-wide assets load through the `app` entry (`reprise_entry_*('app')` in `base.html.twig`); the `/admin` page loads a second `admin` entry to prove multiple entries. UX controllers register through Reprise's Stimulus virtual module (`controllers.json` + local `assets/controllers/`); React/Vue components register via `import.meta.glob(..., { eager: true })` in `app.ts`. |
| 8 | + |
| 9 | +## Corrected assumptions (validated with the user) |
| 10 | + |
| 11 | +- **Package manager: pnpm.** `playground` is a member of the root `pnpm-workspace.yaml`; there is no local lockfile and deps use the `link:` protocol. AGENTS.md's `npm -C playground …` is stale. Use `pnpm -C playground run …` and `pnpm install` at the repo root. |
| 12 | +- **Flex is active** (`symfony/flex` + `extra.symfony.allow-contrib: true`). `composer require symfony/ux-*` auto-applies recipes: registers bundles in `config/bundles.php`, drops `config/packages/*` config, and adds entries to `assets/controllers.json`. Do **not** edit `bundles.php` by hand. |
| 13 | +- **But `synchronize_package_json: false`** (in `composer.json` `extra.symfony/flex`) — Flex will **not** touch `package.json`. JS deps are added manually to `playground/package.json`, then `pnpm install`. |
| 14 | +- **ux-translator dumps JS translations automatically on `cache:clear`** (via the `auto-scripts` block). No manual `ux:translator:dump` step. |
| 15 | +- **UX 3.4 is released.** Switch `@symfony/ux-react`/`@symfony/ux-vue` from the local `link:../../ux-3712/...` to published `^3.4.0` (npm) and drop the ux-3712 dependency. |
| 16 | + |
| 17 | +## Curated UX set (15 demos, one route each) |
| 18 | + |
| 19 | +| Package | npm dep(s) | Demo | Notes | |
| 20 | +|---|---|---|---| |
| 21 | +| ux-react | `@symfony/ux-react@^3.4`, react/react-dom (present) | React counter/form in Twig | published 3.4, `react_component()` | |
| 22 | +| ux-vue | `@symfony/ux-vue@^3.4`, vue (present) | Vue counter/form in Twig | published 3.4, `vue_component()` | |
| 23 | +| ux-chartjs | `@symfony/ux-chartjs`, `chart.js` | chart + "regenerate data" button | `ChartBuilderInterface` PHP | |
| 24 | +| ux-autocomplete | `@symfony/ux-autocomplete`, `tom-select` | searchable select | autoimport CSS | |
| 25 | +| ux-dropzone | `@symfony/ux-dropzone` | drag-drop + preview | `DropzoneType` form | |
| 26 | +| ux-cropperjs | `@symfony/ux-cropperjs`, `cropperjs` | upload + crop + preview | `CropperType` form | |
| 27 | +| ux-notify | `@symfony/ux-notify` | **degraded**: button + "requires Mercure" note | no hub | |
| 28 | +| ux-toggle-password | `@symfony/ux-toggle-password` | show/hide password | pure Stimulus | |
| 29 | +| ux-lazy-image | `@symfony/ux-lazy-image` | blur -> sharp on scroll | Twig fn + Stimulus | |
| 30 | +| ux-map (Leaflet) | present | map + markers | already wired | |
| 31 | +| ux-icons | — (PHP-only) | icon gallery (several sets) | no controllers.json entry | |
| 32 | +| ux-translator | `@symfony/ux-translator` | language switch affecting JS strings | import from dumped module | |
| 33 | +| ux-live-component | `@symfony/ux-live-component` | live search / server-rendered counter | `AsLiveComponent` | |
| 34 | +| ux-twig-component | — (PHP-only) | reusable card/alert (props + slots) | `AsTwigComponent` | |
| 35 | +| ux-turbo | `@symfony/ux-turbo`, `@hotwired/turbo` | Drive nav + lazy turbo-frame | Drive/Frame only, no Mercure | |
| 36 | + |
| 37 | +## Reprise feature showcases (6 pages, `/feature/{slug}`) |
| 38 | + |
| 39 | +1. **Multiple entries** — `/admin` renders `reprise_entry_*('admin')`; everything else uses `app`. |
| 40 | +2. **Code-splitting** — `app.ts` `import('./demos/…')` populates `entrypoints.json.dynamic`; plus one local controller marked `/* stimulusFetch: 'lazy' */`. |
| 41 | +3. **SCSS + TypeScript** — entries are `.ts`, styles `.scss` (bundler-native; Reprise reimplements neither). |
| 42 | +4. **Copied files gallery + CDN** — render copied images via `asset()`/manifest (keep the 2 `copy` rules incl. `pattern`); document/test an absolute CDN `publicPath` via env. |
| 43 | +5. **Build-contract viewer** — `/feature/build-contract` reads `public/build/{entrypoints,manifest}.json` and pretty-prints them (isProd, devServer, integrity…); reflects dev vs build live. |
| 44 | +6. **SRI** — already enabled (`integrity`); surfaced through the viewer + rendered tag attributes. |
| 45 | + |
| 46 | +## File structure |
| 47 | + |
| 48 | +- Modify: `playground/vite.config.ts`, `playground/rsbuild.config.ts` — `.ts` entries `app`/`admin`, SCSS (Vite `sass-embedded`, Rsbuild `@rsbuild/plugin-sass`), env-driven `publicPath` + explicit `manifestKeyPrefix`. |
| 49 | +- Modify: `playground/composer.json` (via `composer require`), `playground/package.json` (manual JS deps). |
| 50 | +- Rename/convert: `playground/assets/{app,admin}.js` -> `.ts`; `playground/assets/styles/{app,admin}.css` -> `.scss`. |
| 51 | +- Create: `playground/assets/demos/*.ts` (dynamically-imported modules), extra `playground/assets/controllers/*` (incl. one lazy), UX React/Vue demo components under `assets/react/controllers/`, `assets/vue/controllers/`. |
| 52 | +- Modify: `playground/assets/controllers.json` (recipes + fetch tweaks). |
| 53 | +- Create: `playground/src/Controller/{DashboardController,DemoController,FeatureController}.php`, `playground/src/Components/*` (Live + Twig), `playground/src/Form/{DropzoneDemoType,CropperDemoType}.php`, a `DemoRegistry` (PHP array/service). |
| 54 | +- Create/modify: `playground/templates/{base,dashboard}.html.twig`, `playground/templates/demo/*.html.twig`, `playground/templates/feature/*.html.twig`, `playground/templates/components/*`. |
| 55 | +- Reduce: `playground/assets/to-copy/` to ~8 images (keep the `me_5x` pattern subset so the `pattern` copy rule still matches). |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +## Phase 0 — Dependencies & bundler config |
| 60 | + |
| 61 | +- [ ] **Composer (Flex auto-registers):** |
| 62 | + ```bash |
| 63 | + composer -d playground require \ |
| 64 | + symfony/ux-chartjs symfony/ux-autocomplete symfony/ux-dropzone \ |
| 65 | + symfony/ux-cropperjs symfony/ux-toggle-password symfony/ux-lazy-image \ |
| 66 | + symfony/ux-icons symfony/ux-translator symfony/ux-live-component \ |
| 67 | + symfony/ux-twig-component symfony/ux-turbo symfony/ux-notify |
| 68 | + ``` |
| 69 | + Accept recipe contrib prompts. Verify `config/bundles.php`, `config/packages/*`, and `assets/controllers.json` were updated. |
| 70 | +- [ ] **package.json (manual — Flex sync is off):** in `playground/package.json`, replace the two `link:../../ux-3712/...` entries with `"@symfony/ux-react": "^3.4.0"`, `"@symfony/ux-vue": "^3.4.0"`; add deps `@symfony/ux-chartjs, chart.js, @symfony/ux-autocomplete, tom-select, @symfony/ux-dropzone, @symfony/ux-cropperjs, cropperjs, @symfony/ux-toggle-password, @symfony/ux-lazy-image, @symfony/ux-translator, @symfony/ux-live-component, @symfony/ux-turbo, @hotwired/turbo, @symfony/ux-notify`; add devDeps `sass-embedded, @rsbuild/plugin-sass`. Then `pnpm install` at repo root. |
| 71 | +- [ ] **Bundler configs:** in both `vite.config.ts` and `rsbuild.config.ts`: entries `{ app: './assets/app.ts', admin: './assets/admin.ts' }`; keep `stimulus`, `integrity`, the 2 `copy` rules; add SCSS (Vite: `sass-embedded` is auto-detected, no plugin; Rsbuild: add `pluginSass()`); set `const publicPath = process.env.CDN_BASE ?? '/build/'` and pass `publicPath` + `manifestKeyPrefix: 'build'` (explicit prefix is required once `publicPath` can be an absolute CDN URL). |
| 72 | +- [ ] **Verify:** `pnpm -C playground run vite:build` and `pnpm -C playground run rsbuild:build` both succeed; `public/build/{entrypoints,manifest}.json` still valid. |
| 73 | + |
| 74 | +## Phase 1 — App shell & dashboard |
| 75 | + |
| 76 | +- [ ] Convert `assets/{app,admin}.js` -> `.ts`; `styles/{app,admin}.css` -> `.scss` (update imports). Keep the existing `registerReactControllerComponents(import.meta.glob('./react/controllers/**/*.{jsx,tsx}', { eager: true }))` + Vue equivalent + `startStimulusApp()` in `app.ts`. |
| 77 | +- [ ] `base.html.twig`: nav (link to dashboard), `reprise_entry_link_tags('app')` / `reprise_entry_script_tags('app')` only — **remove** the `encore_entry_*` lines. |
| 78 | +- [ ] `DemoRegistry` (PHP array): each item `{ slug, title, description, section: 'feature'|'ux', route }`. `DashboardController` (`GET /`) passes it to `dashboard.html.twig`, rendered as a two-section card grid. |
| 79 | +- [ ] Base SCSS for cards/nav/layout in `styles/app.scss`. |
| 80 | +- [ ] **Verify:** `pnpm -C playground run vite:dev`, open `/`, dashboard renders both sections, no console errors. |
| 81 | + |
| 82 | +## Phase 2 — Reprise feature pages |
| 83 | + |
| 84 | +- [ ] `FeatureController` with one action per feature route + templates under `templates/feature/`. |
| 85 | +- [ ] **Multiple entries:** `/admin` renders `reprise_entry_*('admin')`; `admin.ts`/`admin.scss` distinct content. |
| 86 | +- [ ] **Code-splitting:** add `assets/demos/heavy.ts` (some non-trivial module), `import('./demos/heavy.ts')` in `app.ts` guarded by a DOM marker present on the code-splitting page; add one local controller `assets/controllers/lazy_hello_controller.ts` with `/* stimulusFetch: 'lazy' */`. |
| 87 | +- [ ] **SCSS+TS:** feature page documents that `.ts`/`.scss` work with zero Reprise config. |
| 88 | +- [ ] **Copied files gallery + CDN:** reduce `to-copy/` to ~8 images; gallery page renders them via `{{ asset('build/to-copy/…') }}`; document the CDN build (`CDN_BASE=https://cdn.example.com/build/ pnpm -C playground run vite:build`) and show the resulting absolute manifest URLs. |
| 89 | +- [ ] **Build-contract viewer:** `/feature/build-contract` reads `public/build/{entrypoints,manifest}.json` and pretty-prints them in `<pre>` with short explanations. |
| 90 | +- [ ] **Verify:** each feature page opens (dev); `dynamic` array populated after a build; CDN build yields absolute URLs in `manifest.json`. |
| 91 | + |
| 92 | +## Phase 3 — UX demo pages |
| 93 | + |
| 94 | +Implement the 15 cards from the table. Group by need: |
| 95 | + |
| 96 | +- [ ] **Pure Twig/Stimulus** (chartjs, autocomplete, toggle-password, lazy-image, icons, twig-component): template per demo under `templates/demo/`, `DemoController` action each. chartjs uses `ChartBuilderInterface`; icons uses `ux_icon()` across a few sets; twig-component builds one reusable component in `src/Components/`. |
| 97 | +- [ ] **Forms** (dropzone, cropperjs): `src/Form/*Type`, controller action rendering the form, template. |
| 98 | +- [ ] **Framework components** (react, vue): demo components under `assets/react/controllers/`, `assets/vue/controllers/`; pages use `react_component()` / `vue_component()`. |
| 99 | +- [ ] **Server-reactive** (live-component): `src/Components/` `AsLiveComponent` (e.g. live search or counter) + template. |
| 100 | +- [ ] **Turbo** (Drive + Frame): a page with a `turbo-frame` whose `src` hits a `DemoController` fragment action. |
| 101 | +- [ ] **notify (degraded):** page with a button + a callout explaining it needs a Mercure hub (not provisioned). |
| 102 | +- [ ] **controllers.json:** confirm recipe entries exist; keep react/vue `eager`, map `lazy`; set heavy/rarely-used controllers to `lazy` where sensible. |
| 103 | +- [ ] **Verify:** open each demo page in `vite:dev`; interact (chart regenerates, autocomplete filters, crop works, live-component updates, turbo-frame lazy-loads); no console errors. |
| 104 | + |
| 105 | +## Phase 4 — Wiring & parity |
| 106 | + |
| 107 | +- [ ] Confirm React/Vue registration + `startStimulusApp()` run once, site-wide, from `app.ts`. |
| 108 | +- [ ] `cache:clear` so ux-translator dumps JS translations; import the dumped module in the translator demo. |
| 109 | +- [ ] Mirror any bundler-specific need in **both** `vite.config.ts` and `rsbuild.config.ts` (project symmetry rule). |
| 110 | + |
| 111 | +## Final verification |
| 112 | + |
| 113 | +- [ ] **Vite:** `pnpm -C playground run vite:build` then `vite:dev`; dashboard + all pages load; `entrypoints.json` has `app`+`admin`, populated `dynamic`, `integrity` present; copied files (incl. `pattern` subset) keyed in `manifest.json`. |
| 114 | +- [ ] **Rsbuild:** `pnpm -C playground run rsbuild:build` then `rsbuild:dev`; same pages load; outputs consistent with Vite. |
| 115 | +- [ ] **Screenshots (chrome-devtools MCP):** dashboard + ~4 representative demos (react, chartjs, live-component, turbo-frame) + the build-contract page, under both dev and build. |
| 116 | +- [ ] **CDN:** a `CDN_BASE=…` build yields absolute URLs in `manifest.json`/`entrypoints.json`. |
| 117 | +- [ ] Console clean across the visited pages on both bundlers. |
0 commit comments