Skip to content

feat: adopt Tailwind + AssetMapper + Stimulus as frontend stack#41

Merged
martinyde merged 11 commits into
developfrom
feature/issue-38-frontend-tooling
Jun 11, 2026
Merged

feat: adopt Tailwind + AssetMapper + Stimulus as frontend stack#41
martinyde merged 11 commits into
developfrom
feature/issue-38-frontend-tooling

Conversation

@martinydeAI

@martinydeAI martinydeAI commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Acts on the comments on #38. @yepzdk suggested Tailwind + Symfony
AssetMapper + Vanilla JS / Stimulus, on the grounds that the project
should lean on the defaults Symfony provides. This PR adopts that stack
and records the decision in an ADR.

  • Adds docs/adr/002-frontend-tooling.md documenting the choice and
    the options that were considered.
  • Installs symfony/asset-mapper, symfony/asset, symfony/twig-pack,
    symfony/stimulus-bundle, and symfonycasts/tailwind-bundle. Recipes
    applied (config/packages, importmap, base layout, stimulus bootstrap).
  • Tailwind v4 is driven by the bundle's managed binary. No
    package.json, no node_modules, no Node container in CI or
    production.
  • Replaces the assets/app.css placeholder with assets/styles/app.css
    (@import "tailwindcss";). assets/app.js now boots Stimulus via the
    generated stimulus_bootstrap.js.
  • Updates README.md with a Frontend assets section (install /
    build / watch / inspect commands) and adds an [Unreleased] / Added
    entry to CHANGELOG.md.

Scope notes

Test plan

  • itkdev-docker-compose composer install succeeds against the
    new lock file.
  • itkdev-docker-compose php bin/console tailwind:build produces
    var/tailwind/app.built.css without errors.
  • itkdev-docker-compose php bin/console debug:asset-map lists
    app, styles/app.css, and the stimulus controllers.
  • itkdev-docker-compose vendor/bin/php-cs-fixer fix --dry-run
    0 fixable files.
  • itkdev-docker-compose vendor/bin/twig-cs-fixer lint → OK.
  • docker compose --profile dev run --rm prettier '**/*.{yml,yaml,css,js}' --check → OK.
  • docker compose --profile dev run --rm markdownlint markdownlint 'docs/**/*.md' 'README.md' 'CHANGELOG.md' → OK.

closes #38

🤖 Generated with Claude Code

martinydeAI and others added 2 commits June 8, 2026 14:48
Acts on #38 per @yepzdk's recommendation: lean on the defaults Symfony
provides. Installs `symfony/asset-mapper`, `symfony/asset`,
`symfony/twig-pack`, `symfony/stimulus-bundle`, and
`symfonycasts/tailwind-bundle`. Tailwind v4 is driven by the bundle's
managed binary, so the project stays Node-free.

Decision recorded in `docs/adr/002-frontend-tooling.md`.

Refs #38

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a header comment so the next reader knows the file was generated
by the `symfony/stimulus-bundle` Flex recipe, what double-submit cookie
CSRF protection it implements, and how Flex behaves on update.

Refs #38

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinyde

Copy link
Copy Markdown
Contributor

Looks good to me.

Run `composer normalize` after the frontend tooling additions. Tweaks
the version constraint formatting (`8.1.*` → `~8.1.0`, `^2.12|^3.0` →
`^2.12 || ^3.0`) and refreshes the lock file's content hash.

Refs #38

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@martinyde martinyde changed the base branch from main to develop June 9, 2026 06:42
tuj
tuj previously approved these changes Jun 10, 2026
@martinyde martinyde merged commit f3efaba into develop Jun 11, 2026
10 checks passed
martinyde added a commit that referenced this pull request Jun 11, 2026
* feat: placeholder frontpage + do-not-merge label workflow

Adds a thin `FrontpageController` mounted at `GET /` that renders a
placeholder Twig template, identifying the project as ai-lib and
signalling that the application is under construction. The template
extends `templates/base.html.twig` from PR #41 and styles itself with
Tailwind utilities — the asset pipeline introduced there is its
first real consumer.

A `WebTestCase` smoke test asserts `/` returns 200 and the response
contains "ai-lib". The test file is committed ahead of #31; it will
start running automatically once PHPUnit lands.

Also adds `.github/workflows/block-on-label.yaml`: a per-PR merge
gate that fails the check while a `do-not-merge` label is applied.
Useful when a PR depends on another PR/release/review that needs to
land first. The workflow only fails the check — making the merge
button itself unavailable requires marking this check as a required
status check in branch protection (out of scope for this PR).

Refs #40

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: frontpage previews AI Bibliotek design with sample data

Evolves the placeholder frontpage from a single "under construction"
message into a faithful preview of the AI Bibliotek prototype, so
visitors get a first-glance impression of the catalogue before any
real data exists.

Mirrors the home view from
`itk-dev/research-projects/docs/public/projects/ai-bibliotek/mocks`:

- Hero with eyebrow, headline, lede paragraph, and three-stat block
  (assistant / kommune / model counts driven by the sample data).
- Non-functional search prompt.
- Horizontal rail of five sample assistants drawn from the prototype's
  seed data (Borgerservice-vejviser, Mødereferent, Journaliserings-
  assistent, Skole- og dagtilbudssvar, Tilsynsrapport-assistent).
- "Sådan virker det" four-step ordered list.
- "Kommer snart" chip grid for teased future features.

The prototype's CSS is **converted to Tailwind v4 utilities** rather
than imported verbatim. Design tokens (palette, fonts) move into
`@theme` blocks in `assets/styles/app.css`, so they become Tailwind
variants (`bg-surface`, `text-ink`, `font-display`, etc.). A small
`@layer components` block keeps the pseudo-element and keyframe
patterns that utility classes can't express (`eyebrow::before`,
fade-up stagger, hamburger toggle).

`templates/base.html.twig` gets the prototype's chrome — header with
brand mark + responsive nav, footer, Fraunces/Geist fonts preloaded
from Google Fonts. A new `nav_toggle_controller` Stimulus controller
drives the mobile menu.

Sample data lives in `FrontpageController` as `const SAMPLE_ASSISTANTS`
— intentionally inline so the day this is replaced by a real
`AssistantRepository`, the diff is small and obvious.

Refs #40

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: split frontpage into anonymous Twig components

Install symfony/ux-twig-component and refactor the placeholder frontpage and base layout into 17 anonymous components under templates/components/ (Layout, Nav, Hero, Stats, SearchBox, CardRail, Box, StepList, TagList, Eyebrow). Rendered HTML and Stimulus / CSS hooks are unchanged; future sections can compose the same primitives instead of duplicating utility lists.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: emit dev-mode HTML comments around every Twig template

Wrap each template's output in begin + end HTML comments that print the template path via Twig's _self variable. Gated on app.environment == 'dev' so prod output is unchanged. Makes it trivial to identify which template produced any DOM region from browser DevTools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: drop Kommer snart section from frontpage

Removes the Coming Soon chip list from templates/frontpage/index.html.twig and the now-unused COMING_SOON const + template variable from FrontpageController. The TagList components stay in templates/components/ as reusable primitives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add project Twig component conventions to CLAUDE.md

Documents the anonymous-component-first structure under templates/components/, the {% props %} + named-slot contract, the dev-mode {{ _self }} begin/end markers, and when to extract vs. inline. Intended as the reference future contributors (and future Claude sessions) read before adding new templates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Added twig node visitor

* feat: add Danish translations + brand env vars

Install symfony/translation with default locale da and a single translations/messages.da.yaml file holding every user-facing label, ARIA string, placeholder, page title, and copy block. Components translate text props internally via |trans, so call sites pass keys (e.g. label="frontpage.stats.assistants") rather than literal Danish text. The hero heading and footer caption use _html-suffixed keys rendered with |raw.

Brand identity (name, tagline, initials) is sourced from BRAND_* environment variables, exposed as Twig globals (brand_name, brand_tagline, brand_initials) with parameter-backed defaults in config/services.yaml so the page works whether or not .env defines them. Brand strings are configuration, not localization, and are not translated.

CLAUDE.md documents the new Translations and Brand-identity sections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: card hover top border + stats stacking on narrow viewports

CardRail container: add pt-2 so the card's -translate-y-0.5 lift and its shadow-md aren't clipped by overflow-y:auto (which the spec forces on us via overflow-x:auto). Stats list: switch grid-cols-3 to grid-cols-1 sm:grid-cols-3 so the long single-word Danish labels (SPROGMODELLER) get their own row below the sm breakpoint instead of overflowing the column and pushing the page horizontally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: ship brand identity defaults in .env

Adds BRAND_NAME, BRAND_TAGLINE, BRAND_INITIALS as committed defaults so the parameter fallbacks in config/services.yaml are only a safety net. .env.local can override per-deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: note Tailwind rebuild gotcha

There is no live watcher and cache:clear does not rebuild the stylesheet — new utility classes only apply after tailwind:build runs. CLAUDE.md gets a dedicated subsection so future sessions stop being puzzled; README.md gets a heads-up callout next to the build commands. Also fixes a stale hello_controller.js reference in README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: stub footer links to # until destinations are decided

Drops the hardcoded github.com/itk-dev/ai-lib and prototype-mockup URLs from base.html.twig so the placeholder frontpage doesn't ship pointers we haven't committed to yet. Both footer link href and the %link% substitution in the caption become '#' and can be wired to real targets (env var or route) later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: cards wrap into rows instead of horizontal scroll

The CardRail was a horizontal snap-scroll rail. Its flex children with min-w-[260px] forced the rail's min-content to ~1364px, which propagated up through the section into the view-root's implicit grid column and dragged the entire page horizontally on narrow viewports.

Switching the container to 'flex flex-wrap gap-4' and the card to 'grow basis-[260px] max-w-[400px]' lets cards reflow onto new rows as the viewport narrows. Also adds grid-cols-1 to view-root so its implicit column gets minmax(0, 1fr) and no future overflow-x child triggers the same propagation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: promote section eyebrows to <h2>, card titles to <h3>

Eyebrow gains an 'as' prop defaulting to h2 — section kickers are now real subheadings under the hero's h1. Hero overrides with as="p" because its eyebrow sits above the h1 and isn't a heading. Card titles become h3, completing the h1 -> h2 -> h3 hierarchy.

To keep the visual identical, .eyebrow declares font-family: var(--font-sans) (the @layer base h1..h4 rule would otherwise force the display serif onto a <h2 class="eyebrow">), and the card title gets tracking-normal to cancel the -0.01em letter-spacing applied to every heading by that same base rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci: fix yaml-lint, composer-normalized, and phpunit checks

Three failing PR checks addressed:

- yaml-lint: translations/messages.da.yaml reformatted with Prettier.

- composer-normalized: pin symfony/translation as ~8.1.0 instead of 8.1.* so composer normalize stops rewriting it on every CI run; composer.lock refreshed.

- PHPUnit + coverage gate: assets/styles/app.css imports tailwindcss via Tailwind v4's directive, which AssetMapper can't resolve until bin/console tailwind:build has run. The Tests workflow now runs tailwind:build after composer install and before phpunit, mirroring the local-dev flow documented in README/CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(changelog): drop test-related entries as baseline noise

PHPUnit harness and the frontpage smoke test stay in the codebase; having tests is the default expectation for the project, not a noteworthy user-facing change. Drops the two related bullets from the [Unreleased] block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Added tests for nodevisitor

* Added tests for nodevisitor

* Reverted CHANGELOG change

* Reverted a deleted method in Kernel

* Added Kernel test

* refactor: .env is the only source of BRAND_* defaults

Drops the brand.default_* parameters in config/services.yaml and the corresponding default: fallbacks in the Twig globals. .env already ships the BRAND_NAME/BRAND_TAGLINE/BRAND_INITIALS values, so the parameter-backed safety net just duplicated that source and would silently drift if one side changed without the other. Addresses tuj's review on PR #43.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(a11y): SearchBox label describes the field, not the submit action

The <label> and the input's aria-label both echoed submitLabel ("Søg"), so the field's accessible name duplicated the submit button rather than describing the input. The placeholder isn't a substitute for a label — it disappears on input and has weak screen-reader support. Use legend ("Find en assistent") for the <label> and drop the now-redundant aria-label. Addresses yepzdk's review on PR #43.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Updated .env

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: martinyde <yde001@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decide on frontend tooling (CSS framework + asset pipeline)

3 participants