Skip to content

Commit 80d454c

Browse files
committed
minor #68 [Playground] Rebuild as a Reprise + Symfony UX showcase (Kocal)
This PR was merged into the main branch. Discussion ---------- [Playground] Rebuild as a Reprise + Symfony UX showcase | Q | A | -------------- | --- | Bug fix? | no | New feature? | no | Deprecations? | no | Documentation? | no | Issues | - | License | MIT The `playground/` demo app goes from a minimal two-entry fixture to a full showcase of Reprise on both Vite and Rsbuild. No library or `src/` changes. It's now a Symfony 7 app with a dashboard and one dedicated page per feature: multiple entries, code splitting, copied files (including a `pattern` regex rule), a live `entrypoints.json`/`manifest.json` viewer, and SRI. Around a dozen Symfony UX packages are wired in: React, Vue, Chart.js, Autocomplete, Dropzone, Cropper.js, Map, Icons, Translator (with an ICU plural example), Live Component, Twig Component, and Turbo. The React and Vue pages are interactive islands driven by Symfony props: a WCAG contrast checker in both flavors. Styling is Tailwind CSS v4, with one `.scss` entry kept so Sass stays exercised. Builds verified on both Vite and Rsbuild. Commits ------- 6389cad [Playground] Rebuild as a Reprise + Symfony UX showcase
2 parents a702f33 + 6389cad commit 80d454c

160 files changed

Lines changed: 2217 additions & 259 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.

playground/assets/admin.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

playground/assets/admin.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import './styles/admin.scss'
2+
3+
console.info('[admin] This log comes from the separate "admin" Reprise entrypoint.')

playground/assets/app.js

Lines changed: 0 additions & 21 deletions
This file was deleted.

playground/assets/app.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import './styles/app.css'
2+
import { startStimulusApp } from '@symfony/reprise/stimulus'
3+
import { registerReactControllerComponents } from '@symfony/ux-react'
4+
import { registerVueControllerComponents } from '@symfony/ux-vue'
5+
6+
registerReactControllerComponents(import.meta.glob('./react/controllers/**/*.{jsx,tsx}', { eager: true }))
7+
registerVueControllerComponents(import.meta.glob('./vue/controllers/**/*.vue', { eager: true }))
8+
9+
startStimulusApp()
10+
11+
// Code-splitting showcase: the confetti module is pulled in only on click, so it ships as its own
12+
// chunk (listed in entrypoints.json -> dynamic) instead of the main entry. Delegated on `document` so it
13+
// keeps working across Turbo navigations — the entry script runs once, not on every page.
14+
document.addEventListener('click', (event) => {
15+
if (event.target instanceof Element && event.target.closest('[data-confetti]')) {
16+
void import('./demos/confetti').then((m) => m.celebrate())
17+
}
18+
})

playground/assets/calc.js

Lines changed: 0 additions & 18 deletions
This file was deleted.

playground/assets/controllers.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,60 @@
1717
"enabled": true,
1818
"fetch": "eager"
1919
}
20+
},
21+
"@symfony/ux-turbo": {
22+
"turbo-core": {
23+
"enabled": true,
24+
"fetch": "eager",
25+
"autoimport": {
26+
"@symfony/ux-turbo/dist/mercure_stream_source_element.js": true
27+
}
28+
}
29+
},
30+
"@symfony/ux-chartjs": {
31+
"chart": {
32+
"enabled": true,
33+
"fetch": "lazy"
34+
}
35+
},
36+
"@symfony/ux-autocomplete": {
37+
"autocomplete": {
38+
"enabled": true,
39+
"fetch": "lazy",
40+
"autoimport": {
41+
"tom-select/dist/css/tom-select.default.css": true,
42+
"tom-select/dist/css/tom-select.bootstrap4.css": false,
43+
"tom-select/dist/css/tom-select.bootstrap5.css": false
44+
}
45+
}
46+
},
47+
"@symfony/ux-cropperjs": {
48+
"cropper": {
49+
"enabled": true,
50+
"fetch": "lazy",
51+
"autoimport": {
52+
"cropperjs/dist/cropper.min.css": true,
53+
"@symfony/ux-cropperjs/dist/style.min.css": true
54+
}
55+
}
56+
},
57+
"@symfony/ux-dropzone": {
58+
"dropzone": {
59+
"enabled": true,
60+
"fetch": "lazy",
61+
"autoimport": {
62+
"@symfony/ux-dropzone/dist/style.min.css": true
63+
}
64+
}
65+
},
66+
"@symfony/ux-live-component": {
67+
"live": {
68+
"enabled": true,
69+
"fetch": "lazy",
70+
"autoimport": {
71+
"@symfony/ux-live-component/dist/live.min.css": true
72+
}
73+
}
2074
}
2175
},
2276
"entrypoints": []
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Controller } from '@hotwired/stimulus'
2+
import { trans } from '../translator.js'
3+
4+
export default class extends Controller {
5+
static targets = ['greeting', 'apples']
6+
7+
locale = 'en'
8+
count = 3
9+
10+
connect() {
11+
this.render()
12+
}
13+
14+
english() {
15+
this.locale = 'en'
16+
this.render()
17+
}
18+
19+
french() {
20+
this.locale = 'fr'
21+
this.render()
22+
}
23+
24+
more() {
25+
this.count += 1
26+
this.render()
27+
}
28+
29+
fewer() {
30+
this.count = Math.max(0, this.count - 1)
31+
this.render()
32+
}
33+
34+
render() {
35+
this.greetingTarget.textContent = trans('greeting', { '%name%': 'Reprise' }, 'messages', this.locale)
36+
this.applesTarget.textContent = trans('apples', { count: this.count }, 'messages', this.locale)
37+
}
38+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const COLORS = ['#10b981', '#f59e0b', '#ef4444', '#3b82f6', '#a855f7']
2+
3+
export function celebrate(): void {
4+
for (let i = 0; i < 90; i++) {
5+
const piece = document.createElement('div')
6+
piece.style.cssText = `position:fixed;top:-12px;left:${(i * 53) % 100}vw;width:9px;height:9px;`
7+
+ `background:${COLORS[i % COLORS.length]};z-index:9999;pointer-events:none;border-radius:2px`
8+
document.body.appendChild(piece)
9+
piece
10+
.animate(
11+
[
12+
{ transform: 'translateY(0) rotate(0deg)', opacity: 1 },
13+
{ transform: `translateY(100vh) rotate(${540 + i * 7}deg)`, opacity: 0.15 },
14+
],
15+
{ duration: 1600 + (i % 12) * 120, easing: 'cubic-bezier(0.2, 0.6, 0.4, 1)' },
16+
)
17+
.addEventListener('finish', () => piece.remove())
18+
}
19+
}

playground/assets/demos/wcag.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const linearize = (channel: number): number =>
2+
channel <= 0.03928 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4
3+
4+
function luminance(hex: string): number {
5+
const [r, g, b] = [1, 3, 5].map((i) => linearize(parseInt(hex.slice(i, i + 2), 16) / 255))
6+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
7+
}
8+
9+
/** WCAG 2.x contrast ratio (1–21) between two `#rrggbb` colors. */
10+
export function contrastRatio(a: string, b: string): number {
11+
const la = luminance(a)
12+
const lb = luminance(b)
13+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05)
14+
}

0 commit comments

Comments
 (0)