From 98c990ac49851229264f6b22c2f664690a951cff Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 08:31:30 +0200
Subject: [PATCH 01/24] feat: placeholder frontpage + do-not-merge label
workflow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
.github/workflows/block-on-label.yaml | 39 ++++++++++++++++++++
CHANGELOG.md | 10 +++++
src/Controller/FrontpageController.php | 30 +++++++++++++++
templates/frontpage/index.html.twig | 23 ++++++++++++
tests/Controller/FrontpageControllerTest.php | 30 +++++++++++++++
5 files changed, 132 insertions(+)
create mode 100644 .github/workflows/block-on-label.yaml
create mode 100644 src/Controller/FrontpageController.php
create mode 100644 templates/frontpage/index.html.twig
create mode 100644 tests/Controller/FrontpageControllerTest.php
diff --git a/.github/workflows/block-on-label.yaml b/.github/workflows/block-on-label.yaml
new file mode 100644
index 0000000..74ddf96
--- /dev/null
+++ b/.github/workflows/block-on-label.yaml
@@ -0,0 +1,39 @@
+### Block merge on label
+###
+### Fails the check while a `do-not-merge` label is applied to the
+### pull request. The label is used to gate merges that depend on
+### something landing elsewhere (another PR, an upstream release, a
+### review by a specific team).
+###
+### Removing the label and re-running the check turns it green. To
+### make the merge button itself unavailable while the label is set,
+### this check has to be marked as a required status check in the
+### repository's branch-protection rules.
+
+name: Block merge on label
+
+on:
+ pull_request:
+ types:
+ - opened
+ - reopened
+ - synchronize
+ - labeled
+ - unlabeled
+
+jobs:
+ block-on-label:
+ name: Block on do-not-merge label
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check for do-not-merge label
+ env:
+ LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
+ run: |
+ echo "Labels on this PR: $LABELS"
+ if echo "$LABELS" | grep -qx '"do-not-merge"' \
+ || echo "$LABELS" | grep -q '"do-not-merge"'; then
+ echo "::error::This PR carries the 'do-not-merge' label. Remove the label once the blocking dependency is resolved."
+ exit 1
+ fi
+ echo "No blocking label present."
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f5ce70..2063c7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,3 +19,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Decision recorded in [ADR 002](docs/adr/002-frontend-tooling.md).
- Base Twig layout (`templates/base.html.twig`) and frontend asset
entrypoints (`assets/app.js`, `assets/styles/app.css`).
+- Placeholder frontpage at `/` (`App\Controller\FrontpageController`)
+ extending the base layout. Identifies the project and signals that
+ the application is under construction.
+- Functional smoke test for the frontpage
+ (`tests/Controller/FrontpageControllerTest.php`); will start running
+ once PHPUnit lands (#31).
+- GitHub Action `block-on-label` that fails the check while a
+ `do-not-merge` label is applied to a pull request, providing a
+ per-PR merge gate for dependencies (e.g. another PR that must land
+ first).
diff --git a/src/Controller/FrontpageController.php b/src/Controller/FrontpageController.php
new file mode 100644
index 0000000..d7d99f4
--- /dev/null
+++ b/src/Controller/FrontpageController.php
@@ -0,0 +1,30 @@
+render('frontpage/index.html.twig');
+ }
+}
diff --git a/templates/frontpage/index.html.twig b/templates/frontpage/index.html.twig
new file mode 100644
index 0000000..9cda1b8
--- /dev/null
+++ b/templates/frontpage/index.html.twig
@@ -0,0 +1,23 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}ai-lib{% endblock %}
+
+{% block body %}
+
+
+
+ ai-lib
+
+
+ Under construction
+
+
+ The catalogue is being scaffolded. There's nothing to see here
+ yet — check the
+ project repository
+ for progress.
+
+
+
+{% endblock %}
diff --git a/tests/Controller/FrontpageControllerTest.php b/tests/Controller/FrontpageControllerTest.php
new file mode 100644
index 0000000..b7438c6
--- /dev/null
+++ b/tests/Controller/FrontpageControllerTest.php
@@ -0,0 +1,30 @@
+request('GET', '/');
+
+ self::assertResponseIsSuccessful();
+ self::assertSelectorTextContains('body', 'ai-lib');
+ }
+}
From de2c7cf5be766fcac388b5a0740b1d069e3aced3 Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 10:08:51 +0200
Subject: [PATCH 02/24] feat: frontpage previews AI Bibliotek design with
sample data
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
CHANGELOG.md | 13 +-
assets/controllers/nav_toggle_controller.js | 26 ++++
assets/styles/app.css | 152 +++++++++++++++++++
src/Controller/FrontpageController.php | 74 ++++++++-
templates/base.html.twig | 63 +++++++-
templates/frontpage/index.html.twig | 126 +++++++++++++--
tests/Controller/FrontpageControllerTest.php | 3 +-
7 files changed, 428 insertions(+), 29 deletions(-)
create mode 100644 assets/controllers/nav_toggle_controller.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2063c7f..2f2bede 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,8 +20,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Base Twig layout (`templates/base.html.twig`) and frontend asset
entrypoints (`assets/app.js`, `assets/styles/app.css`).
- Placeholder frontpage at `/` (`App\Controller\FrontpageController`)
- extending the base layout. Identifies the project and signals that
- the application is under construction.
+ that previews the AI Bibliotek design with hardcoded sample data:
+ hero, search prompt, sample-assistant rail, "Sådan virker det"
+ steps, and "Kommer snart" chips. Follows the prototype mock at
+ `itk-dev/research-projects/docs/public/projects/ai-bibliotek/mocks`.
+- Site chrome (header with brand + nav, footer) in
+ `templates/base.html.twig`, with the Fraunces/Geist font stack
+ preloaded from Google Fonts.
+- Tailwind v4 design tokens (`@theme` in `assets/styles/app.css`)
+ matching the prototype palette and typography.
+- Stimulus controller `nav_toggle_controller` driving the mobile
+ navigation menu.
- Functional smoke test for the frontpage
(`tests/Controller/FrontpageControllerTest.php`); will start running
once PHPUnit lands (#31).
diff --git a/assets/controllers/nav_toggle_controller.js b/assets/controllers/nav_toggle_controller.js
new file mode 100644
index 0000000..9b97ae3
--- /dev/null
+++ b/assets/controllers/nav_toggle_controller.js
@@ -0,0 +1,26 @@
+import { Controller } from "@hotwired/stimulus";
+
+/*
+ * Mobile navigation toggle.
+ *
+ * Mounted on the header container (`data-controller="nav-toggle"`).
+ * The toggle button uses `data-action="click->nav-toggle#toggle"` and
+ * the nav element is the `menu` target. Flips `aria-expanded` on the
+ * button and the `hidden` utility class on the menu so screen readers
+ * and the CSS toggle effect stay in sync.
+ */
+export default class extends Controller {
+ static targets = ["menu"];
+
+ toggle(event) {
+ const button = event.currentTarget;
+ const expanded = button.getAttribute("aria-expanded") === "true";
+ button.setAttribute("aria-expanded", String(!expanded));
+
+ if (!this.hasMenuTarget) {
+ return;
+ }
+ this.menuTarget.classList.toggle("hidden", expanded);
+ this.menuTarget.classList.toggle("flex", !expanded);
+ }
+}
diff --git a/assets/styles/app.css b/assets/styles/app.css
index f1d8c73..9543d24 100644
--- a/assets/styles/app.css
+++ b/assets/styles/app.css
@@ -1 +1,153 @@
@import "tailwindcss";
+
+/*
+ * Design tokens mirroring the AI Bibliotek prototype
+ * (https://github.com/itk-dev/research-projects/tree/main/docs/public/projects/ai-bibliotek/mocks).
+ * Registered via Tailwind v4's `@theme` so the values become utility
+ * variants — e.g. `bg-surface`, `text-ink`, `font-display`.
+ */
+@theme {
+ /* Surface + ink palette */
+ --color-bg: #ffffff;
+ --color-surface: #ffffff;
+ --color-surface-2: #fbf9f5;
+ --color-border: #e3ddd1;
+ --color-border-strong: #c8bea9;
+ --color-line: #ddd5c4;
+ --color-ink: #11192a;
+ --color-text: #1c2433;
+ --color-text-muted: #5b6478;
+
+ /* Brand */
+ --color-primary: #0f766e;
+ --color-primary-hover: #0c5e57;
+ --color-primary-ink: #ffffff;
+ --color-accent: #c89b3c;
+ --color-accent-soft: #f0e3c2;
+ --color-accent-deep: #8a6a1f;
+
+ /* Typography */
+ --font-display:
+ "Fraunces", "Source Serif Pro", "Iowan Old Style", Georgia, serif;
+ --font-sans:
+ "Geist", "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
+ Roboto, Helvetica, Arial, sans-serif;
+ --font-mono:
+ "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
+ monospace;
+}
+
+/*
+ * Base layer: page chrome that has to apply to regardless of
+ * which utility classes the template carries. Mirrors the prototype's
+ * body styling so the same font stack and ink colour are used
+ * everywhere.
+ */
+@layer base {
+ body {
+ font-family: var(--font-sans);
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ font-feature-settings: "ss01", "ss02";
+ }
+
+ h1,
+ h2,
+ h3,
+ h4 {
+ font-family: var(--font-display);
+ font-weight: 500;
+ color: var(--color-ink);
+ letter-spacing: -0.01em;
+ line-height: 1.15;
+ }
+}
+
+/*
+ * Component layer: small set of compound patterns that would be
+ * unwieldy as inline utility lists. Each is named after its
+ * prototype counterpart and only contains decorative bits that
+ * Tailwind utilities don't express directly (pseudo-elements,
+ * keyframes).
+ */
+@layer components {
+ /* Eyebrow label with leading decorative rule, used as a section kicker. */
+ .eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.78rem;
+ font-weight: 600;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--color-accent-deep);
+ }
+ .eyebrow::before {
+ content: "";
+ display: inline-block;
+ width: 24px;
+ height: 1px;
+ background-color: var(--color-accent-deep);
+ }
+
+ /* Staggered fade-up on the main view's direct children. */
+ @keyframes fade-up {
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+ .view-root > * {
+ animation: fade-up 220ms cubic-bezier(0.22, 1, 0.36, 1) both;
+ }
+ .view-root > *:nth-child(1) {
+ animation-delay: 0ms;
+ }
+ .view-root > *:nth-child(2) {
+ animation-delay: 40ms;
+ }
+ .view-root > *:nth-child(3) {
+ animation-delay: 80ms;
+ }
+ .view-root > *:nth-child(4) {
+ animation-delay: 120ms;
+ }
+ .view-root > *:nth-child(5) {
+ animation-delay: 160ms;
+ }
+ @media (prefers-reduced-motion: reduce) {
+ .view-root > * {
+ animation: none;
+ }
+ }
+
+ /*
+ * Animated hamburger toggle. The three bars rotate into an X when
+ * `aria-expanded="true"`. Hand-rolled because Tailwind utilities
+ * can't express child-element transforms tied to a parent ARIA
+ * state.
+ */
+ .nav-toggle-bar {
+ display: block;
+ width: 18px;
+ height: 2px;
+ background-color: var(--color-ink);
+ border-radius: 2px;
+ transition:
+ transform 220ms ease,
+ opacity 220ms ease;
+ }
+ .nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(1) {
+ transform: translateY(5.5px) rotate(45deg);
+ }
+ .nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(2) {
+ opacity: 0;
+ }
+ .nav-toggle[aria-expanded="true"] .nav-toggle-bar:nth-child(3) {
+ transform: translateY(-5.5px) rotate(-45deg);
+ }
+}
diff --git a/src/Controller/FrontpageController.php b/src/Controller/FrontpageController.php
index d7d99f4..8f2b8fd 100644
--- a/src/Controller/FrontpageController.php
+++ b/src/Controller/FrontpageController.php
@@ -10,21 +10,81 @@
class FrontpageController extends AbstractController
{
+ /**
+ * Hardcoded placeholder assistants used by the design preview.
+ *
+ * Drawn from the AI Bibliotek prototype's seed data to give an
+ * accurate first-glance impression of the catalogue. Replaced by
+ * a real repository query once the persistence layer lands.
+ */
+ private const SAMPLE_ASSISTANTS = [
+ [
+ 'kommune' => 'Aarhus Kommune',
+ 'model' => 'gpt-4o',
+ 'name' => 'Borgerservice-vejviser',
+ 'summary' => 'Hjælper sagsbehandlere med at finde den rigtige paragraf i lov om social service og opsummere borgerens situation.',
+ ],
+ [
+ 'kommune' => 'Københavns Kommune',
+ 'model' => 'claude-3.5-sonnet',
+ 'name' => 'Mødereferent',
+ 'summary' => 'Tager udgangspunkt i et indtalt møde og leverer struktureret referat med beslutninger, ansvar og deadlines.',
+ ],
+ [
+ 'kommune' => 'Odense Kommune',
+ 'model' => 'llama-3.1-70b',
+ 'name' => 'Journaliseringsassistent',
+ 'summary' => 'Foreslår journalplan-numre og overskrifter ud fra dokumentets indhold, så fagmedarbejdere kan godkende i et klik.',
+ ],
+ [
+ 'kommune' => 'Vejle Kommune',
+ 'model' => 'gpt-4o-mini',
+ 'name' => 'Skole- og dagtilbudssvar',
+ 'summary' => 'Drafter svar til forældrehenvendelser på skole- og dagtilbudsområdet med kildehenvisninger til kommunens egen vejledningssamling.',
+ ],
+ [
+ 'kommune' => 'Aalborg Kommune',
+ 'model' => 'mistral-large',
+ 'name' => 'Tilsynsrapport-assistent',
+ 'summary' => 'Læser plejehjemstilsynsrapporter og fremhæver afvigelser, opfølgningspunkter og forbedringer over tid.',
+ ],
+ ];
+
+ private const COMING_SOON = [
+ 'Deling af tools',
+ 'Deling af skills',
+ 'Ratings',
+ 'API',
+ 'Abonnér på ændringer',
+ 'Testcases',
+ ];
+
/**
* Render the placeholder frontpage.
*
- * Anonymous visitors to `/` receive a thin landing page that
- * identifies the project (ai-lib) and signals that the
- * application is under construction. The page exists so that
- * other UI work (auth, catalogue, search) has a stable entry
- * point to link back to while the richer frontpage envisioned
- * in #3 is being designed.
+ * Anonymous visitors to `/` receive a design-preview landing page
+ * that mirrors the AI Bibliotek prototype. Hero, search prompt,
+ * sample-assistant rail, "Sådan virker det" steps, and "Kommer
+ * snart" chips are rendered with hardcoded sample data — the
+ * point is to convey what the catalogue will feel like before
+ * the persistence and search layers land.
*
* @return Response the rendered `frontpage/index.html.twig` template
*/
#[Route('/', name: 'app_frontpage', methods: ['GET'])]
public function index(): Response
{
- return $this->render('frontpage/index.html.twig');
+ $kommuner = array_unique(array_column(self::SAMPLE_ASSISTANTS, 'kommune'));
+ $models = array_unique(array_column(self::SAMPLE_ASSISTANTS, 'model'));
+
+ return $this->render('frontpage/index.html.twig', [
+ 'assistants' => self::SAMPLE_ASSISTANTS,
+ 'stats' => [
+ 'assistants' => count(self::SAMPLE_ASSISTANTS),
+ 'kommuner' => count($kommuner),
+ 'models' => count($models),
+ ],
+ 'coming_soon' => self::COMING_SOON,
+ ]);
}
}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index c223fd4..5012731 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -1,10 +1,15 @@
-
+
- {% block title %}ai-lib{% endblock %}
+ {% block title %}AI Bibliotek{% endblock %}
+
+
+
+
+
{% block stylesheets %}
{% endblock %}
@@ -13,7 +18,57 @@
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
-
- {% block body %}{% endblock %}
+
+
+ Spring til indhold
+
+
+
+
+ Et fælles bibliotek over kommunale AI-assistenter.
-
- The catalogue is being scaffolded. There's nothing to see here
- yet — check the
- project repository
- for progress.
+
+ Find, del og hjemtag AI-assistenter bygget af danske myndigheder. Når én kommune løser
+ en opgave, kan resten hjemtage assistenten, eksportere konfigurationen og køre den
+ lokalt — så gode løsninger skalerer nationalt.
+
+
+ {% set steps = [
+ {lead: 'Opret bruger', body: ' og log ind som repræsentant for din myndighed.'},
+ {lead: 'Find en assistent', body: ' – søg og filtrér på kommune, sprogmodel og datafølsomhed.'},
+ {lead: 'Hjemtag', body: ' – eksportér assistentens JSON og følg vidensopskriften, der beskriver hvilke data du selv skal levere.'},
+ {lead: 'Tilpas lokalt', body: ' – importér i din egen OpenWebUI, tilføj kommunens viden og tag den i brug.'},
+ ] %}
+ {% for step in steps %}
+
+
+
Et fælles bibliotek over kommunale AI-assistenter.
-
-
+
+
Find, del og hjemtag AI-assistenter bygget af danske myndigheder. Når én kommune løser
en opgave, kan resten hjemtage assistenten, eksportere konfigurationen og køre den
lokalt — så gode løsninger skalerer nationalt.
-
-
- {% set steps = [
- {lead: 'Opret bruger', body: ' og log ind som repræsentant for din myndighed.'},
- {lead: 'Find en assistent', body: ' – søg og filtrér på kommune, sprogmodel og datafølsomhed.'},
- {lead: 'Hjemtag', body: ' – eksportér assistentens JSON og følg vidensopskriften, der beskriver hvilke data du selv skal levere.'},
- {lead: 'Tilpas lokalt', body: ' – importér i din egen OpenWebUI, tilføj kommunens viden og tag den i brug.'},
- ] %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for a in assistants %}
+
+ {% endfor %}
+
+
+
+ {% set steps = [
+ {lead: 'Opret bruger', body: ' og log ind som repræsentant for din myndighed.'},
+ {lead: 'Find en assistent', body: ' – søg og filtrér på kommune, sprogmodel og datafølsomhed.'},
+ {lead: 'Hjemtag', body: ' – eksportér assistentens JSON og følg vidensopskriften, der beskriver hvilke data du selv skal levere.'},
+ {lead: 'Tilpas lokalt', body: ' – importér i din egen OpenWebUI, tilføj kommunens viden og tag den i brug.'},
+ ] %}
+
{% for step in steps %}
-
+{% if app.environment == 'dev' %}{% endif %}
diff --git a/templates/components/TagList/Tag.html.twig b/templates/components/TagList/Tag.html.twig
index f2683ac..68cb0a4 100644
--- a/templates/components/TagList/Tag.html.twig
+++ b/templates/components/TagList/Tag.html.twig
@@ -1,3 +1,4 @@
+{% if app.environment == 'dev' %}{% endif %}
{% props label, badge = 'snart', title = 'Kommer snart' %}
{{ badge }}
+{% if app.environment == 'dev' %}{% endif %}
diff --git a/templates/frontpage/index.html.twig b/templates/frontpage/index.html.twig
index 6aac85e..00b00eb 100644
--- a/templates/frontpage/index.html.twig
+++ b/templates/frontpage/index.html.twig
@@ -3,6 +3,7 @@
{% block title %}AI Bibliotek – forhåndsvisning{% endblock %}
{% block body %}
+ {% if app.environment == 'dev' %}{% endif %}
@@ -57,4 +58,5 @@
+ {% if app.environment == 'dev' %}{% endif %}
{% endblock %}
From c12253e822af777e71af2ebd04cc0dce53f5336b Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 13:20:09 +0200
Subject: [PATCH 05/24] 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)
---
src/Controller/FrontpageController.php | 18 ++++--------------
templates/frontpage/index.html.twig | 8 --------
2 files changed, 4 insertions(+), 22 deletions(-)
diff --git a/src/Controller/FrontpageController.php b/src/Controller/FrontpageController.php
index 8f2b8fd..6f24b5c 100644
--- a/src/Controller/FrontpageController.php
+++ b/src/Controller/FrontpageController.php
@@ -50,24 +50,15 @@ class FrontpageController extends AbstractController
],
];
- private const COMING_SOON = [
- 'Deling af tools',
- 'Deling af skills',
- 'Ratings',
- 'API',
- 'Abonnér på ændringer',
- 'Testcases',
- ];
-
/**
* Render the placeholder frontpage.
*
* Anonymous visitors to `/` receive a design-preview landing page
* that mirrors the AI Bibliotek prototype. Hero, search prompt,
- * sample-assistant rail, "Sådan virker det" steps, and "Kommer
- * snart" chips are rendered with hardcoded sample data — the
- * point is to convey what the catalogue will feel like before
- * the persistence and search layers land.
+ * sample-assistant rail, and "Sådan virker det" steps are rendered
+ * with hardcoded sample data — the point is to convey what the
+ * catalogue will feel like before the persistence and search
+ * layers land.
*
* @return Response the rendered `frontpage/index.html.twig` template
*/
@@ -84,7 +75,6 @@ public function index(): Response
'kommuner' => count($kommuner),
'models' => count($models),
],
- 'coming_soon' => self::COMING_SOON,
]);
}
}
diff --git a/templates/frontpage/index.html.twig b/templates/frontpage/index.html.twig
index 00b00eb..7f36f2d 100644
--- a/templates/frontpage/index.html.twig
+++ b/templates/frontpage/index.html.twig
@@ -49,14 +49,6 @@
-
-
- {% for label in coming_soon %}
-
- {% endfor %}
-
-
-
{% if app.environment == 'dev' %}{% endif %}
{% endblock %}
From 952b2e659269ad079a1ff12f9e7f952f62ee3c18 Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 13:20:16 +0200
Subject: [PATCH 06/24] 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)
---
CLAUDE.md | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
create mode 100644 CLAUDE.md
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..196900d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,123 @@
+# Project conventions for Claude
+
+This file documents project-specific patterns. Global preferences live in
+`~/.claude/CLAUDE.md` and still apply.
+
+## Twig templates
+
+### Component-first
+
+All non-trivial markup belongs in a component under `templates/components/`,
+not inline in page or layout templates. Page templates (`templates//…`)
+should read as a thin composition of `` calls.
+
+We use **anonymous components** from `symfony/ux-twig-component` — no PHP
+backing class. The component template alone defines the contract via
+`{% props … %}`.
+
+### Directory layout
+
+Group components by domain under `templates/components/`. Nested namespaces
+become nested directories:
+
+```
+templates/components/
+ Eyebrow.html.twig #
+ Hero.html.twig #
+ Layout/SiteHeader.html.twig #
+ Nav/Link.html.twig #
+ Stats/List.html.twig #
+ Stats/Item.html.twig #
+```
+
+Use PascalCase for component file names (matches the `` casing).
+Singular vs plural follows whether the component is a container (`List`)
+or a single item (`Item`).
+
+### Component anatomy
+
+Every component template has this shape:
+
+```twig
+{% if app.environment == 'dev' %}{% endif %}
+{% props label, value, href = '#' %}
+
+ {{ label }}
+ {{ value }}
+ {% block content %}{% endblock %}
+
+{% if app.environment == 'dev' %}{% endif %}
+```
+
+Rules:
+
+1. **Always add the dev markers** — one at the very top, one at the very
+ end. They emit `` and
+ `` in dev only, so the rendered HTML
+ in DevTools tells you which template produced any region. The path comes
+ from `{{ _self }}` automatically — never hardcode it.
+2. **Declare props** with `{% props … %}` right after the opening marker.
+ Required props have no default; optional props use `prop = 'value'`.
+3. **Default slot** is `{% block content %}{% endblock %}` — content placed
+ between `…` lands here.
+4. **Named slots** use `{% block %}{% endblock %}` and are filled
+ from the call site with `…`.
+ Prefer named slots over `|raw` string props whenever the value contains
+ HTML (e.g. an inline ``).
+5. Keep Tailwind utility classes inline. Design tokens (`text-ink`,
+ `bg-surface`, `font-display`, …) come from the `@theme` block in
+ `assets/styles/app.css`; do not introduce new CSS files for one-off
+ styles.
+
+### Templates that `extends` a layout
+
+Twig forbids any content outside `{% block %}` in an extending template.
+Put the dev markers **inside** an existing block (typically `body`):
+
+```twig
+{% extends 'base.html.twig' %}
+
+{% block body %}
+ {% if app.environment == 'dev' %}{% endif %}
+
+ …
+
+ {% if app.environment == 'dev' %}{% endif %}
+{% endblock %}
+```
+
+### Calling components
+
+```twig
+
+
+ Et fælles bibliotek over kommunale AI-assistenter.
+
+ …
+
+
+
+
+
+
+```
+
+Self-close (``) when the component has no slot content.
+
+### When not to extract a component
+
+- A piece of markup used exactly once and unlikely to repeat. Inline it.
+- A wrapper that adds no parameters and no semantics. The call site is
+ clearer with the underlying utilities.
+
+A component earns its place when it has a name, a contract (props /
+slots), and at least one reason to be reused or replaced in isolation.
+
+### Stimulus + CSS hooks
+
+When a component carries a `data-controller=`, `data-action=`,
+`data-…-target=`, or a hand-rolled CSS class hook (e.g. `.nav-toggle`,
+`.nav-toggle-bar`), leave a short `{# … #}` comment in the component
+explaining why the attribute must stay verbatim — these are load-bearing
+for JS controllers in `assets/controllers/` or CSS rules in
+`assets/styles/app.css`.
From 162e5543f45160f5bc450c5d4d332c1a0fe50f26 Mon Sep 17 00:00:00 2001
From: martinyde
Date: Tue, 9 Jun 2026 13:53:19 +0200
Subject: [PATCH 07/24] Added twig node visitor
---
CLAUDE.md | 47 +++----
assets/controllers/hello_controller.js | 17 ---
src/Twig/DevTemplateMarkerExtension.php | 44 ++++++
src/Twig/DevTemplateMarkerNodeVisitor.php | 126 ++++++++++++++++++
templates/base.html.twig | 4 +-
templates/components/Box.html.twig | 2 -
templates/components/CardRail/Card.html.twig | 2 -
.../components/CardRail/Container.html.twig | 2 -
templates/components/Eyebrow.html.twig | 2 -
templates/components/Hero.html.twig | 2 -
templates/components/Layout/Brand.html.twig | 2 -
.../components/Layout/SiteFooter.html.twig | 2 -
.../components/Layout/SiteHeader.html.twig | 2 -
.../components/Layout/SkipLink.html.twig | 2 -
templates/components/Nav/Link.html.twig | 2 -
templates/components/Nav/Menu.html.twig | 2 -
templates/components/Nav/Toggle.html.twig | 2 -
templates/components/SearchBox.html.twig | 2 -
templates/components/Stats/Item.html.twig | 2 -
templates/components/Stats/List.html.twig | 2 -
templates/components/StepList/Item.html.twig | 2 -
templates/components/StepList/List.html.twig | 2 -
templates/components/TagList/List.html.twig | 2 -
templates/components/TagList/Tag.html.twig | 2 -
templates/frontpage/index.html.twig | 2 -
25 files changed, 193 insertions(+), 85 deletions(-)
delete mode 100644 assets/controllers/hello_controller.js
create mode 100644 src/Twig/DevTemplateMarkerExtension.php
create mode 100644 src/Twig/DevTemplateMarkerNodeVisitor.php
diff --git a/CLAUDE.md b/CLAUDE.md
index 196900d..65da4e3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -39,53 +39,50 @@ or a single item (`Item`).
Every component template has this shape:
```twig
-{% if app.environment == 'dev' %}{% endif %}
{% props label, value, href = '#' %}
{{ label }}{{ value }}
{% block content %}{% endblock %}
-{% if app.environment == 'dev' %}{% endif %}
```
Rules:
-1. **Always add the dev markers** — one at the very top, one at the very
- end. They emit `` and
- `` in dev only, so the rendered HTML
- in DevTools tells you which template produced any region. The path comes
- from `{{ _self }}` automatically — never hardcode it.
-2. **Declare props** with `{% props … %}` right after the opening marker.
- Required props have no default; optional props use `prop = 'value'`.
-3. **Default slot** is `{% block content %}{% endblock %}` — content placed
+1. **Declare props** with `{% props … %}` on the first line. Required
+ props have no default; optional props use `prop = 'value'`.
+2. **Default slot** is `{% block content %}{% endblock %}` — content placed
between `…` lands here.
-4. **Named slots** use `{% block %}{% endblock %}` and are filled
+3. **Named slots** use `{% block %}{% endblock %}` and are filled
from the call site with `…`.
Prefer named slots over `|raw` string props whenever the value contains
HTML (e.g. an inline ``).
-5. Keep Tailwind utility classes inline. Design tokens (`text-ink`,
+4. Keep Tailwind utility classes inline. Design tokens (`text-ink`,
`bg-surface`, `font-display`, …) come from the `@theme` block in
`assets/styles/app.css`; do not introduce new CSS files for one-off
styles.
-### Templates that `extends` a layout
+### Dev-mode template markers
-Twig forbids any content outside `{% block %}` in an extending template.
-Put the dev markers **inside** an existing block (typically `body`):
+In the `dev` environment, every project template is wrapped at compile
+time with HTML comments showing its path:
-```twig
-{% extends 'base.html.twig' %}
-
-{% block body %}
- {% if app.environment == 'dev' %}{% endif %}
-
- …
-
- {% if app.environment == 'dev' %}{% endif %}
-{% endblock %}
+```html
+
+…component output…
+
```
+This is automatic — do **not** add `{% if app.environment == 'dev' %}{% endif %}`
+lines inside templates. The injection is done by
+`App\Twig\DevTemplateMarkerNodeVisitor` (in `src/Twig/`), registered
+only in dev so prod output is unchanged.
+
+For templates that `extends` another, the visitor wraps the content of
+the `body` block specifically (the template's top-level content never
+renders in that case). Page templates should therefore put their content
+inside `{% block body %}…{% endblock %}` if they want the auto-marker.
+
### Calling components
```twig
diff --git a/assets/controllers/hello_controller.js b/assets/controllers/hello_controller.js
deleted file mode 100644
index 6fc936c..0000000
--- a/assets/controllers/hello_controller.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Controller } from "@hotwired/stimulus";
-
-/*
- * This is an example Stimulus controller!
- *
- * Any element with a data-controller="hello" attribute will cause
- * this controller to be executed. The name "hello" comes from the filename:
- * hello_controller.js -> "hello"
- *
- * Delete this file or adapt it for your use!
- */
-export default class extends Controller {
- connect() {
- this.element.textContent =
- "Hello Stimulus! Edit me in assets/controllers/hello_controller.js";
- }
-}
diff --git a/src/Twig/DevTemplateMarkerExtension.php b/src/Twig/DevTemplateMarkerExtension.php
new file mode 100644
index 0000000..419e197
--- /dev/null
+++ b/src/Twig/DevTemplateMarkerExtension.php
@@ -0,0 +1,44 @@
+ the visitors
+ * to register
+ */
+ public function getNodeVisitors(): array
+ {
+ if ('dev' !== $this->environment) {
+ return [];
+ }
+
+ return [new DevTemplateMarkerNodeVisitor()];
+ }
+}
diff --git a/src/Twig/DevTemplateMarkerNodeVisitor.php b/src/Twig/DevTemplateMarkerNodeVisitor.php
new file mode 100644
index 0000000..4d8bfae
--- /dev/null
+++ b/src/Twig/DevTemplateMarkerNodeVisitor.php
@@ -0,0 +1,126 @@
+` / `` around each template
+ * boundary. Templates that `extends` another are wrapped at their
+ * `body` block instead, because their top-level body never runs at
+ * render time.
+ *
+ * Registered only in the dev environment by
+ * {@see DevTemplateMarkerExtension::getNodeVisitors()}; the visitor
+ * itself trusts the registration and unconditionally wraps every
+ * non-namespaced template it sees.
+ */
+final class DevTemplateMarkerNodeVisitor implements NodeVisitorInterface
+{
+ public function enterNode(Node $node, Environment $env): Node
+ {
+ return $node;
+ }
+
+ /**
+ * Wrap the template body (or its `body` block, for extending
+ * templates) with begin and end marker TextNodes.
+ *
+ * @param Node $node the node being left
+ * @param Environment $env the Twig environment (unused)
+ *
+ * @return Node the original node, mutated in place when applicable
+ */
+ public function leaveNode(Node $node, Environment $env): Node
+ {
+ if (!$node instanceof ModuleNode) {
+ return $node;
+ }
+
+ $name = $node->getTemplateName();
+ if (null === $name || str_starts_with($name, '@')) {
+ // Skip framework / vendor templates loaded via a Twig namespace.
+ return $node;
+ }
+
+ $line = $node->getTemplateLine();
+ $prefix = new TextNode('', $line);
+ $suffix = new TextNode('', $line);
+
+ if ($node->hasNode('parent')) {
+ $this->wrapExtendingBody($node, $prefix, $suffix);
+
+ return $node;
+ }
+
+ $this->wrap($node, $prefix, $suffix);
+
+ return $node;
+ }
+
+ /**
+ * For templates that `extends` another, wrap the content of the
+ * `body` block — the template's top-level body never runs.
+ *
+ * Twig stores each block as a `BodyNode` containing the
+ * `BlockNode`; we iterate to find the BlockNode and replace its
+ * body with the wrapped sequence.
+ *
+ * @param ModuleNode $node the extending module
+ * @param TextNode $prefix the opening marker
+ * @param TextNode $suffix the closing marker
+ */
+ private function wrapExtendingBody(ModuleNode $node, TextNode $prefix, TextNode $suffix): void
+ {
+ if (!$node->hasNode('blocks')) {
+ return;
+ }
+ $blocks = $node->getNode('blocks');
+ if (!$blocks->hasNode('body')) {
+ return;
+ }
+ foreach ($blocks->getNode('body') as $child) {
+ if ($child instanceof BlockNode) {
+ $this->wrap($child, $prefix, $suffix);
+
+ return;
+ }
+ }
+ }
+
+ /**
+ * Lowest priority — run after other visitors so the markers stay
+ * the outermost layer of the compiled body.
+ *
+ * @return int the visitor priority
+ */
+ public function getPriority(): int
+ {
+ return 0;
+ }
+
+ /**
+ * Replace `$node`'s `body` child with a sequence: prefix, original
+ * body, suffix.
+ *
+ * @param Node $node the node whose body to wrap
+ * @param TextNode $prefix the opening marker
+ * @param TextNode $suffix the closing marker
+ */
+ private function wrap(Node $node, TextNode $prefix, TextNode $suffix): void
+ {
+ $body = $node->getNode('body');
+ $wrapped = new Node([$prefix, $body, $suffix], [], $body->getTemplateLine());
+ $node->setNode('body', $wrapped);
+ }
+}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index 24a6be9..1469cf8 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -1,5 +1,4 @@
-{% if app.environment == 'dev' %}
-{% endif %}
+
@@ -43,4 +42,3 @@
-{% if app.environment == 'dev' %}{% endif %}
diff --git a/templates/components/Box.html.twig b/templates/components/Box.html.twig
index 2fc7011..abd66ee 100644
--- a/templates/components/Box.html.twig
+++ b/templates/components/Box.html.twig
@@ -1,4 +1,3 @@
-{% if app.environment == 'dev' %}{% endif %}
{% props eyebrow, lead = null %}
{{ eyebrow }}
@@ -7,4 +6,3 @@
{% endif %}
{% block content %}{% endblock %}
-{% if app.environment == 'dev' %}{% endif %}
diff --git a/templates/components/CardRail/Card.html.twig b/templates/components/CardRail/Card.html.twig
index 1373f6f..f9bac1d 100644
--- a/templates/components/CardRail/Card.html.twig
+++ b/templates/components/CardRail/Card.html.twig
@@ -1,4 +1,3 @@
-{% if app.environment == 'dev' %}{% endif %}
{% props kommune, model, name, summary, href = '#' %}
{{ name }}
{{ summary }}
-{% if app.environment == 'dev' %}{% endif %}
diff --git a/templates/components/CardRail/Container.html.twig b/templates/components/CardRail/Container.html.twig
index bd8ad3e..ab0e8c5 100644
--- a/templates/components/CardRail/Container.html.twig
+++ b/templates/components/CardRail/Container.html.twig
@@ -1,4 +1,3 @@
-{% if app.environment == 'dev' %}{% endif %}
{% props eyebrow, linkHref = '#', linkLabel, listLabel = 'Eksempler på assistenter' %}
-
-
- Et fælles bibliotek over kommunale AI-assistenter.
-
-
- Find, del og hjemtag AI-assistenter bygget af danske myndigheder. Når én kommune løser
- en opgave, kan resten hjemtage assistenten, eksportere konfigurationen og køre den
- lokalt — så gode løsninger skalerer nationalt.
-
+
+ {{ 'frontpage.hero.heading_html'|trans|raw }}
+ {{ 'frontpage.hero.lead'|trans }}
-
-
-
+
+
+
-
+
-
+
{% for a in assistants %}
-
+
{% set steps = [
- {lead: 'Opret bruger', body: ' og log ind som repræsentant for din myndighed.'},
- {lead: 'Find en assistent', body: ' – søg og filtrér på kommune, sprogmodel og datafølsomhed.'},
- {lead: 'Hjemtag', body: ' – eksportér assistentens JSON og følg vidensopskriften, der beskriver hvilke data du selv skal levere.'},
- {lead: 'Tilpas lokalt', body: ' – importér i din egen OpenWebUI, tilføj kommunens viden og tag den i brug.'},
+ {lead: 'frontpage.steps.1.lead', body: 'frontpage.steps.1.body'},
+ {lead: 'frontpage.steps.2.lead', body: 'frontpage.steps.2.body'},
+ {lead: 'frontpage.steps.3.lead', body: 'frontpage.steps.3.body'},
+ {lead: 'frontpage.steps.4.lead', body: 'frontpage.steps.4.body'},
] %}
{% for step in steps %}
diff --git a/translations/.gitignore b/translations/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml
new file mode 100644
index 0000000..24b7dc7
--- /dev/null
+++ b/translations/messages.da.yaml
@@ -0,0 +1,63 @@
+# Danish UI strings. Hierarchical dot-notation keys.
+#
+# - Brand identity (name, tagline, initials) is NOT here — see BRAND_*
+# env vars in .env and the Twig globals in config/packages/twig.yaml.
+# - Keys ending in `_html` carry HTML and must be rendered with |raw at
+# the call site.
+# - Components apply |trans on text props themselves; call sites pass
+# keys, not literal strings.
+
+layout:
+ skip_link: 'Spring til indhold'
+ brand_aria_label: 'Til forsiden'
+ footer:
+ caption_html: 'Forhåndsvisning · ingen data endnu · designet følger prototype-mockuppet'
+ about_link: 'Om projektet'
+
+nav:
+ menu_label: 'Hovedmenu'
+ toggle_label: 'Vis menu'
+ links:
+ catalog: 'Katalog'
+ share: 'Del assistent'
+ mine: 'Mine assistenter'
+ favorites: 'Favoritter'
+ collections: 'Samlinger'
+
+search:
+ legend: 'Find en assistent'
+ submit: 'Søg'
+ placeholder: 'Søg efter borgerservice, referat, journalisering…'
+
+tag:
+ badge_default: 'snart'
+ title_default: 'Kommer snart'
+
+frontpage:
+ title: '%brand% – forhåndsvisning'
+ hero:
+ eyebrow: 'Del & hjemtag · dansk offentlig AI'
+ heading_html: 'Et fælles bibliotek over kommunale AI-assistenter.'
+ lead: 'Find, del og hjemtag AI-assistenter bygget af danske myndigheder. Når én kommune løser en opgave, kan resten hjemtage assistenten, eksportere konfigurationen og køre den lokalt — så gode løsninger skalerer nationalt.'
+ stats:
+ assistants: 'Assistenter'
+ kommuner: 'Kommuner'
+ models: 'Sprogmodeller'
+ rail:
+ eyebrow: 'Forhåndsvisning'
+ link: 'Kataloget kommer →'
+ list_label: 'Eksempler på assistenter'
+ steps:
+ eyebrow: 'Sådan virker det'
+ '1':
+ lead: 'Opret bruger'
+ body: ' og log ind som repræsentant for din myndighed.'
+ '2':
+ lead: 'Find en assistent'
+ body: ' – søg og filtrér på kommune, sprogmodel og datafølsomhed.'
+ '3':
+ lead: 'Hjemtag'
+ body: ' – eksportér assistentens JSON og følg vidensopskriften, der beskriver hvilke data du selv skal levere.'
+ '4':
+ lead: 'Tilpas lokalt'
+ body: ' – importér i din egen OpenWebUI, tilføj kommunens viden og tag den i brug.'
From c4cca3c0f5d1a773ff56d64904b65bf9d300b5c5 Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 14:33:57 +0200
Subject: [PATCH 09/24] 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)
---
templates/components/CardRail/Container.html.twig | 5 ++++-
templates/components/Stats/List.html.twig | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/templates/components/CardRail/Container.html.twig b/templates/components/CardRail/Container.html.twig
index 177f68a..0e9ba19 100644
--- a/templates/components/CardRail/Container.html.twig
+++ b/templates/components/CardRail/Container.html.twig
@@ -5,7 +5,10 @@
{{ linkLabel|trans }}
From de1696b6f12ec031f5b644ce784d151d0e94676b Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 14:34:03 +0200
Subject: [PATCH 10/24] 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)
---
.env | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/.env b/.env
index 4d598cc..cfa3bdc 100644
--- a/.env
+++ b/.env
@@ -28,3 +28,9 @@ APP_SHARE_DIR=var/share
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
+
+###> brand identity ###
+BRAND_NAME="AI Bibliotek"
+BRAND_TAGLINE="del & hjemtag assistenter · prototype"
+BRAND_INITIALS="AI"
+###< brand identity ###
From 172cf78a7edce09ee4b6cffabe0a2a5ae4f587b7 Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 14:34:10 +0200
Subject: [PATCH 11/24] docs: note Tailwind rebuild gotcha
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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)
---
CLAUDE.md | 23 +++++++++++++++++++++++
README.md | 8 +++++++-
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 35feda8..965aa77 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -193,6 +193,29 @@ defaults). Set the env vars in `.env` for committed defaults, or in
Use `{{ brand_name }}` directly in templates; do not look it up via
`|trans`. Brand identity is configuration, not localization.
+### Rebuilding Tailwind after edits
+
+The project compiles Tailwind via `symfonycasts/tailwind-bundle`. There is
+**no live watcher** by default — `cache:clear` does not rebuild CSS, and
+new utility classes added to templates will not apply until Tailwind
+recompiles `var/tailwind/app.built.css`.
+
+After editing templates or component classes (especially when adding a
+utility that wasn't already in use, e.g. `pt-2`, `grid-cols-1`), run:
+
+```sh
+docker compose exec phpfpm bin/console tailwind:build
+```
+
+For active styling work, keep a watcher open in a side terminal:
+
+```sh
+docker compose exec phpfpm bin/console tailwind:build --watch
+```
+
+AssetMapper picks up the rebuilt file on the next request — no separate
+asset-map step needed in dev.
+
### Stimulus + CSS hooks
When a component carries a `data-controller=`, `data-action=`,
diff --git a/README.md b/README.md
index ff5a224..c95af7f 100644
--- a/README.md
+++ b/README.md
@@ -76,12 +76,18 @@ itkdev-docker-compose php bin/console asset-map:compile
itkdev-docker-compose php bin/console debug:asset-map
```
+> **Heads-up:** there is no live Tailwind watcher running by default, and
+> `cache:clear` does **not** rebuild the stylesheet. After editing a
+> template that introduces a utility class not already in use (e.g.
+> `pt-2`, `grid-cols-1`), run `tailwind:build` — or keep a
+> `tailwind:build --watch` terminal open while you style.
+
Source files live under [`assets/`](assets):
- `assets/app.js` — JavaScript entrypoint, boots Stimulus.
- `assets/styles/app.css` — Tailwind entrypoint (`@import "tailwindcss";`).
- `assets/controllers/` — Stimulus controllers, auto-registered by
- filename (`hello_controller.js` → `data-controller="hello"`).
+ filename (`nav_toggle_controller.js` → `data-controller="nav-toggle"`).
## References
From 8e4a8bb7390ace663bb383b8e7400132e9a3284c Mon Sep 17 00:00:00 2001
From: martinydeAI
Date: Tue, 9 Jun 2026 14:34:18 +0200
Subject: [PATCH 12/24] 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)
---
templates/base.html.twig | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/templates/base.html.twig b/templates/base.html.twig
index a9a0c46..ebc91c3 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -37,8 +37,8 @@
{% block body %}{% endblock %}
-
- {{ 'layout.footer.caption_html'|trans({'%link%': 'https://itk-dev.github.io/research-projects/projects/ai-bibliotek/mocks/index.html#/'})|raw }}
+
+ {{ 'layout.footer.caption_html'|trans({'%link%': '#'})|raw }}