Skip to content

YourBOX Frontend

ADR-0007 commits YourBOX to a Twig plus Tailwind CSS plus Stimulus stack. This page documents the asset pipeline, the design tokens, the Stimulus controller layout, the Turbo integration and the testing strategy.

Assets live under app/assets/ and are mapped by the Symfony AssetMapper bundle. The Tailwind compiler runs at container build time through the symfony/ux-twig-component integration.

app/
├── assets/
│ ├── app.js # Stimulus boot, Turbo import
│ ├── controllers/ # Stimulus controllers
│ │ ├── like_dislike_controller.js
│ │ ├── comment_reply_controller.js
│ │ ├── upload_progress_controller.js
│ │ ├── video_player_controller.js
│ │ └── search_autosubmit_controller.js
│ ├── styles/
│ │ ├── app.css # Tailwind entry: @import "tailwindcss";
│ │ ├── tokens.css # Design tokens (colors, spacing, typography)
│ │ ├── components.css # Custom component classes
│ │ └── admin.css # Tailwind theme for EasyAdmin
│ └── images/
└── public/assets/ # Compiled output (gitignored)

The Dockerfile build stage runs:

Terminal window
php bin/console asset-map:compile
npx @tailwindcss/cli -i assets/styles/app.css -o public/assets/styles/app.<hash>.css --minify

The hashed filename invalidates the browser cache on every change.

The token file lives at app/assets/styles/tokens.css and exposes the palette, typography scale and spacing scale as CSS variables. The values follow the homelab visual identity used by the documentation site so that the brand is consistent across YourBOX and docs.yourbox.site.

@theme {
--color-brand-50: #fef2f2;
--color-brand-100: #fee2e2;
--color-brand-500: #ef4444;
--color-brand-700: #b91c1c;
--color-brand-900: #7f1d1d;
--font-display: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", monospace;
--radius-card: 0.75rem;
--shadow-card: 0 1px 2px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.06);
--container-prose: 70ch;
}

Pages and components use Tailwind utilities first. Repeated patterns (a video card, a comment bubble, a pagination strip) live as component classes in components.css:

.video-card {
@apply rounded-[var(--radius-card)] bg-white shadow-[var(--shadow-card)]
transition hover:shadow-lg overflow-hidden;
}
.comment-bubble {
@apply rounded-2xl bg-zinc-50 px-4 py-3 ring-1 ring-zinc-200;
}

Templates live under app/templates/ and follow the bounded contexts:

templates/
├── base.html.twig # Shell, head, nav, footer, flash
├── components/ # Reusable Twig partials
│ ├── flash.html.twig
│ ├── pagination.html.twig
│ └── video_card.html.twig
├── catalog/
│ ├── index.html.twig
│ ├── search.html.twig
│ ├── show.html.twig
│ └── upload.html.twig
├── engagement/
│ └── comment_thread.html.twig
├── identity/
│ ├── login.html.twig
│ └── profile.html.twig
└── admin/ # EasyAdmin overrides
└── layout.html.twig

base.html.twig carries the shell, the menu, the flash bag rendering and the asset import. Concrete templates extend it and only fill the content block.

Stimulus replaces every piece of jQuery in the 2020 codebase. Controllers live under app/assets/controllers/ and are wired automatically by the Symfony StimulusBundle. Each controller covers one feature:

  • like_dislike_controller.js toggles the like and dislike buttons, posts the vote through Fetch with the CSRF token, updates the counter and applies the active class on success.
  • comment_reply_controller.js toggles the reply form, manages the focus and posts the comment through a Turbo Frame so the thread updates without a full page reload.
  • upload_progress_controller.js listens for the XHR progress events on the upload form, renders a Tailwind styled progress bar and displays the queued transcoding state once the upload completes.
  • video_player_controller.js wraps the native HTML5 video element, handles keyboard shortcuts (space to play and pause, arrow keys to seek), and emits a VideoViewed event after 5 seconds of playback.
  • search_autosubmit_controller.js debounces the search input by 250 ms and triggers the surrounding Turbo Frame submission.

Turbo is enabled on the catalog, the comment threads and the search results. Link clicks within the same site swap the body without a full reload. Form submissions that target a Turbo Frame replace the frame content. The base layout opts in by adding the data-turbo-track="reload" attribute to its asset imports.

The legacy fallback bundle is opted out: requests served by the legacy controller add a Turbo-Visit-Control: reload header so that navigation between a Symfony page and a legacy page does a clean full reload. This avoids accidentally hot swapping a Turbo enabled page with a legacy DOM.

Every page passes axe-core in the Cypress test suite. Specific commitments:

  • Form fields carry an explicit label element. No placeholder is used as a label substitute.
  • Buttons have visible focus rings provided by Tailwind’s focus-visible utilities.
  • The video player exposes the native controls plus a Stimulus controller that preserves keyboard navigation.
  • The contrast ratio respects WCAG AA. The token palette is calibrated for this.
  • Jest for pure JavaScript logic inside Stimulus controllers. The DOM contract is mocked with @testing-library/dom.
  • Symfony WebTestCase for server rendered template assertions. The page source is parsed and matched against expected DOM nodes.
  • Cypress for end to end flows. The Cypress suite runs against docker compose in CI and against the VPS smoke test deployment.
  • axe-core runs inside the Cypress suite to catch accessibility regressions.

The migration is per route per the Symfony Migration page. When a route is ported:

  • The Twig template replaces the inline HTML in the corresponding 2020 PHP file.
  • The Stimulus controller replaces the jQuery snippet. The jQuery global stays available on legacy pages only.
  • The Tailwind tokens replace the per page stylesheet. The legacy CSS file under YourBOX/src/assets/stylesheets/ is deleted.
  • The Turbo Frame wrapping the new content is added once at least two pages share the same shell, otherwise the page stays a full document.