From 622d515aef2cb6d80b208734f5bfe1899d1e2298 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 30 Jun 2026 08:58:52 +0200 Subject: [PATCH 1/3] Add components for page header, card header, emptystate and kpi --- composer.json | 1 + composer.lock | 91 ++++++++++++++++++++- config/bundles.php | 1 + config/packages/twig_component.yaml | 5 ++ symfony.lock | 12 +++ templates/admin/areas/edit.html.twig | 21 ++--- templates/admin/areas/index.html.twig | 17 +--- templates/admin/areas/new.html.twig | 9 +- templates/admin/contacts/edit.html.twig | 19 ++--- templates/admin/contacts/index.html.twig | 17 +--- templates/admin/contacts/new.html.twig | 9 +- templates/admin/departments/edit.html.twig | 19 ++--- templates/admin/departments/index.html.twig | 17 +--- templates/admin/departments/new.html.twig | 9 +- templates/admin/users/edit.html.twig | 15 ++-- templates/admin/users/index.html.twig | 12 +-- templates/components/Card/Header.html.twig | 6 ++ templates/components/EmptyState.html.twig | 6 ++ templates/components/Kpi.html.twig | 7 ++ templates/components/Page/Header.html.twig | 12 +++ templates/dashboard/_stats.html.twig | 24 +----- templates/dashboard/index.html.twig | 64 ++++----------- templates/initiative/_results.html.twig | 5 +- templates/initiative/edit.html.twig | 19 ++--- templates/initiative/index.html.twig | 14 +--- templates/initiative/new.html.twig | 12 +-- templates/initiative/show.html.twig | 27 +++--- 27 files changed, 248 insertions(+), 222 deletions(-) create mode 100644 config/packages/twig_component.yaml create mode 100644 templates/components/Card/Header.html.twig create mode 100644 templates/components/EmptyState.html.twig create mode 100644 templates/components/Kpi.html.twig create mode 100644 templates/components/Page/Header.html.twig diff --git a/composer.json b/composer.json index 21aa8f6..7f3eebb 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "symfony/translation": "~8.1.0", "symfony/twig-bundle": "~8.1.0", "symfony/ux-turbo": "^3.2", + "symfony/ux-twig-component": "^3.2", "symfony/validator": "~8.1.0", "symfony/yaml": "~8.1.0", "twig/extra-bundle": "^2.12 || ^3.0", diff --git a/composer.lock b/composer.lock index e047caf..8d4a2e5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "12a9cfde9017aae91381d9a166055131", + "content-hash": "1690432fc2541ccfd5e69c603125ca0e", "packages": [ { "name": "composer/semver", @@ -6863,6 +6863,95 @@ ], "time": "2026-06-19T07:14:15+00:00" }, + { + "name": "symfony/ux-twig-component", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/ux-twig-component.git", + "reference": "a9b93b10fe2056a6f93ebc1f9e3509f7fafb5bf2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/ux-twig-component/zipball/a9b93b10fe2056a6f93ebc1f9e3509f7fafb5bf2", + "reference": "a9b93b10fe2056a6f93ebc1f9e3509f7fafb5bf2", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.2|^3.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "twig/twig": "^3.10.3" + }, + "conflict": { + "symfony/config": "<6.4" + }, + "require-dev": { + "phpunit/phpunit": "^11.1|^12.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/stimulus-bundle": "^2.9.1|^3.0", + "symfony/twig-bundle": "^7.4|^8.0", + "twig/extra-bundle": "^3.10.3", + "twig/html-extra": "^3.10.3" + }, + "type": "symfony-bundle", + "extra": { + "thanks": { + "url": "https://github.com/symfony/ux", + "name": "symfony/ux" + } + }, + "autoload": { + "psr-4": { + "Symfony\\UX\\TwigComponent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Twig components for Symfony", + "homepage": "https://symfony.com", + "keywords": [ + "components", + "symfony-ux", + "twig" + ], + "support": { + "source": "https://github.com/symfony/ux-twig-component/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-11T20:32:06+00:00" + }, { "name": "symfony/validator", "version": "v8.1.0", diff --git a/config/bundles.php b/config/bundles.php index 2d685b3..f4c7831 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -17,4 +17,5 @@ Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true], DH\AuditorBundle\DHAuditorBundle::class => ['all' => true], ITKDev\EntityBundle\ITKDevEntityBundle::class => ['all' => true], + Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], ]; diff --git a/config/packages/twig_component.yaml b/config/packages/twig_component.yaml new file mode 100644 index 0000000..fd17ac6 --- /dev/null +++ b/config/packages/twig_component.yaml @@ -0,0 +1,5 @@ +twig_component: + anonymous_template_directory: 'components/' + defaults: + # Namespace & directory for components + App\Twig\Components\: 'components/' diff --git a/symfony.lock b/symfony.lock index 9cb14e8..d301674 100644 --- a/symfony.lock +++ b/symfony.lock @@ -310,6 +310,18 @@ "config/packages/ux_turbo.yaml" ] }, + "symfony/ux-twig-component": { + "version": "3.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.13", + "ref": "f367ae2a1faf01c503de2171f1ec22567febeead" + }, + "files": [ + "config/packages/twig_component.yaml" + ] + }, "symfony/validator": { "version": "8.1", "recipe": { diff --git a/templates/admin/areas/edit.html.twig b/templates/admin/areas/edit.html.twig index 4660e79..62c59aa 100644 --- a/templates/admin/areas/edit.html.twig +++ b/templates/admin/areas/edit.html.twig @@ -3,18 +3,15 @@ {% block title %}{{ 'area.edit.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + +
+ + +
+ {{ include('admin/areas/_form.html.twig', {button_label: 'action.save'}) }} {% endblock %} diff --git a/templates/admin/areas/index.html.twig b/templates/admin/areas/index.html.twig index c5c3ac7..123a90f 100644 --- a/templates/admin/areas/index.html.twig +++ b/templates/admin/areas/index.html.twig @@ -3,22 +3,13 @@ {% block title %}{{ 'area.index.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ 'action.new'|trans }} +
{% if areas is empty %} -
-
{{ 'area.empty.title'|trans }}
-

{{ 'area.empty.hint'|trans }}

-
+ {% else %}
diff --git a/templates/admin/areas/new.html.twig b/templates/admin/areas/new.html.twig index cb4bb46..7688242 100644 --- a/templates/admin/areas/new.html.twig +++ b/templates/admin/areas/new.html.twig @@ -3,11 +3,10 @@ {% block title %}{{ 'area.new.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ include('admin/areas/_form.html.twig', {button_label: 'action.create'}) }} {% endblock %} diff --git a/templates/admin/contacts/edit.html.twig b/templates/admin/contacts/edit.html.twig index 556bf1a..fb95e98 100644 --- a/templates/admin/contacts/edit.html.twig +++ b/templates/admin/contacts/edit.html.twig @@ -3,17 +3,14 @@ {% block title %}{{ 'contact.edit.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + + + + + {{ include('admin/contacts/_form.html.twig', {button_label: 'action.save'}) }} {% endblock %} diff --git a/templates/admin/contacts/index.html.twig b/templates/admin/contacts/index.html.twig index 5b6ce36..201ca01 100644 --- a/templates/admin/contacts/index.html.twig +++ b/templates/admin/contacts/index.html.twig @@ -3,22 +3,13 @@ {% block title %}{{ 'contact.index.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ 'action.new'|trans }} +
{% if contacts is empty %} -
-
{{ 'contact.empty.title'|trans }}
-

{{ 'contact.empty.hint'|trans }}

-
+ {% else %}
diff --git a/templates/admin/contacts/new.html.twig b/templates/admin/contacts/new.html.twig index 572ab03..032dd02 100644 --- a/templates/admin/contacts/new.html.twig +++ b/templates/admin/contacts/new.html.twig @@ -3,11 +3,10 @@ {% block title %}{{ 'contact.new.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ include('admin/contacts/_form.html.twig', {button_label: 'action.create'}) }} {% endblock %} diff --git a/templates/admin/departments/edit.html.twig b/templates/admin/departments/edit.html.twig index be49636..4f4fe63 100644 --- a/templates/admin/departments/edit.html.twig +++ b/templates/admin/departments/edit.html.twig @@ -3,17 +3,14 @@ {% block title %}{{ 'department.edit.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + + + + + {{ include('admin/departments/_form.html.twig', {button_label: 'action.save'}) }} {% endblock %} diff --git a/templates/admin/departments/index.html.twig b/templates/admin/departments/index.html.twig index cce96b6..8f2724e 100644 --- a/templates/admin/departments/index.html.twig +++ b/templates/admin/departments/index.html.twig @@ -3,22 +3,13 @@ {% block title %}{{ 'department.index.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ 'action.new'|trans }} +
{% if departments is empty %} -
-
{{ 'department.empty.title'|trans }}
-

{{ 'department.empty.hint'|trans }}

-
+ {% else %}
diff --git a/templates/admin/departments/new.html.twig b/templates/admin/departments/new.html.twig index b7e40da..4733285 100644 --- a/templates/admin/departments/new.html.twig +++ b/templates/admin/departments/new.html.twig @@ -3,11 +3,10 @@ {% block title %}{{ 'department.new.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ include('admin/departments/_form.html.twig', {button_label: 'action.create'}) }} {% endblock %} diff --git a/templates/admin/users/edit.html.twig b/templates/admin/users/edit.html.twig index 5d8d3c5..3d7109c 100644 --- a/templates/admin/users/edit.html.twig +++ b/templates/admin/users/edit.html.twig @@ -3,17 +3,14 @@ {% block title %}{{ 'admin.users.edit'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + {{ form_start(form, {attr: {class: 'form'}}) }}
diff --git a/templates/admin/users/index.html.twig b/templates/admin/users/index.html.twig index 53fb1da..74ffef2 100644 --- a/templates/admin/users/index.html.twig +++ b/templates/admin/users/index.html.twig @@ -3,15 +3,9 @@ {% block title %}{{ 'admin.users.title'|trans }} · {{ 'app.name'|trans }}{% endblock %} {% block admin_content %} - + + {{ 'admin.users.new'|trans }} +
diff --git a/templates/components/Card/Header.html.twig b/templates/components/Card/Header.html.twig new file mode 100644 index 0000000..43fe80d --- /dev/null +++ b/templates/components/Card/Header.html.twig @@ -0,0 +1,6 @@ +{% props title, sub = null %} + + + {{ title }} + {% if sub %}

{{ sub }}

{% endif %} +
diff --git a/templates/components/EmptyState.html.twig b/templates/components/EmptyState.html.twig new file mode 100644 index 0000000..878b12b --- /dev/null +++ b/templates/components/EmptyState.html.twig @@ -0,0 +1,6 @@ +{% props title, hint = null %} + + +
{{ title }}
+ {% if hint %}

{{ hint }}

{% endif %} +
diff --git a/templates/components/Kpi.html.twig b/templates/components/Kpi.html.twig new file mode 100644 index 0000000..70b6ed8 --- /dev/null +++ b/templates/components/Kpi.html.twig @@ -0,0 +1,7 @@ +{% props accent, label, value, hint, live = null %} + + +
{{ label }}
+
{{ value }}
+
{{ hint }}
+
diff --git a/templates/components/Page/Header.html.twig b/templates/components/Page/Header.html.twig new file mode 100644 index 0000000..b1804c2 --- /dev/null +++ b/templates/components/Page/Header.html.twig @@ -0,0 +1,12 @@ +{% props title = null, subtitle = null %} + +{% set subtitleContent %}{% block subtitle %}{{ subtitle }}{% endblock %}{% endset %} + + +
+ {% block breadcrumb %}{% endblock %} +

{% block title %}{{ title }}{% endblock %}

+ {% if subtitleContent|trim %}

{{ subtitleContent }}

{% endif %} +
+
{% block content %}{% endblock %}
+ diff --git a/templates/dashboard/_stats.html.twig b/templates/dashboard/_stats.html.twig index 536e4c1..51107e4 100644 --- a/templates/dashboard/_stats.html.twig +++ b/templates/dashboard/_stats.html.twig @@ -1,20 +1,4 @@ -
-
{{ 'dashboard.total'|trans }}
-
{{ viz.kpis.total }}
-
{{ 'dashboard.total_hint'|trans }}
-
-
-
{{ 'dashboard.in_progress'|trans }}
-
{{ viz.kpis.inProgress }}
-
{{ 'dashboard.in_progress_hint'|trans }}
-
-
-
{{ 'dashboard.departments_involved'|trans }}
-
{{ viz.kpis.departments }}
-
{{ 'dashboard.departments_involved_hint'|trans }}
-
-
-
{{ 'dashboard.collaboration_count'|trans }}
-
{{ viz.kpis.collaboration }}
-
{{ 'dashboard.collaboration_count_hint'|trans }}
-
+ + + + diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig index 435a044..9583b4a 100644 --- a/templates/dashboard/index.html.twig +++ b/templates/dashboard/index.html.twig @@ -4,16 +4,10 @@ {% block body %}
- + + {{ 'nav.initiatives'|trans }} + {{ 'action.new'|trans }} + {{ turbo_stream_from('activities') }} @@ -24,10 +18,7 @@
-
- {{ 'dashboard.unfinished.title'|trans }} -

{{ 'dashboard.unfinished.sub'|trans }}

-
+
{% if unfinishedInitiatives is empty and incompleteContacts is empty %}
@@ -59,10 +50,7 @@
-
- {{ 'activity.title'|trans }} -

{{ 'activity.subtitle'|trans }}

-
+
{% if recent is empty %} @@ -88,70 +76,46 @@
-
- {{ 'dashboard.themes_heatmap'|trans }} -

{{ 'dashboard.themes_heatmap_sub'|trans }}

-
+
-
- {{ 'dashboard.collaboration'|trans }} -

{{ 'dashboard.collaboration_sub'|trans }}

-
+
-
- {{ 'dashboard.status_by_dept'|trans }} -

{{ 'dashboard.status_by_dept_sub'|trans }}

-
+
-
- {{ 'dashboard.by_status'|trans }} -

{{ 'dashboard.by_status_sub'|trans }}

-
+
-
- {{ 'dashboard.funding_sources'|trans }} -

{{ 'dashboard.funding_sources_sub'|trans }}

-
+
-
- {{ 'dashboard.budget_by_dept'|trans }} -

{{ 'dashboard.budget_by_dept_sub'|trans }}

-
+
-
- {{ 'dashboard.timeline'|trans }} -

{{ 'dashboard.timeline_sub'|trans }}

-
+
-
- {{ 'dashboard.reach'|trans }} -

{{ 'dashboard.reach_sub'|trans }}

-
+
diff --git a/templates/initiative/_results.html.twig b/templates/initiative/_results.html.twig index ea725b4..982f330 100644 --- a/templates/initiative/_results.html.twig +++ b/templates/initiative/_results.html.twig @@ -19,10 +19,7 @@ {% endmacro %} {% if pagination.items is empty %} -
-
{{ 'initiative.empty.title'|trans }}
-

{{ 'initiative.empty.hint'|trans }}

-
+ {% else %}
diff --git a/templates/initiative/edit.html.twig b/templates/initiative/edit.html.twig index 5388dcd..0934648 100644 --- a/templates/initiative/edit.html.twig +++ b/templates/initiative/edit.html.twig @@ -4,21 +4,18 @@ {% block body %}
- + +
+ + + + {{ include('initiative/_form.html.twig', {button_label: 'action.save', autosave: true}) }}
{% endblock %} diff --git a/templates/initiative/index.html.twig b/templates/initiative/index.html.twig index c336dfb..edf52f8 100644 --- a/templates/initiative/index.html.twig +++ b/templates/initiative/index.html.twig @@ -4,16 +4,10 @@ {% block body %}
- + + + {{ 'action.new'|trans }} + {{ form_start(form, {attr: { class: 'filters', diff --git a/templates/initiative/new.html.twig b/templates/initiative/new.html.twig index 580d5c3..97e6714 100644 --- a/templates/initiative/new.html.twig +++ b/templates/initiative/new.html.twig @@ -4,15 +4,17 @@ {% block body %}
- + + + {{ 'initiative.new.title'|trans }} + + {{ include('initiative/_form.html.twig', {button_label: 'action.create', autosave: true}) }}
{% endblock %} diff --git a/templates/initiative/show.html.twig b/templates/initiative/show.html.twig index b84439e..4ceb819 100644 --- a/templates/initiative/show.html.twig +++ b/templates/initiative/show.html.twig @@ -13,23 +13,20 @@ {% block body %} {% import _self as h %}
- + + + {% if initiative.status %}{{ initiative.status.labelKey|trans }}{% endif %} + + {{ 'action.edit'|trans }} +
-
{{ 'initiative.show.details'|trans }}
+
{% if initiative.description %}

{{ initiative.description }}

{% else %}

{% endif %} {% if initiative.statusAdditional %} @@ -46,7 +43,7 @@
-
{{ 'initiative.show.contacts'|trans }}
+
{% if initiative.contacts|length > 0 %} {% for contact in initiative.contacts %} @@ -67,7 +64,7 @@ {% if initiative.images|length > 0 or initiative.attachments|length > 0 %}
-
{{ 'initiative.show.media'|trans }}
+
{% if initiative.images|length > 0 %}
@@ -93,7 +90,7 @@
-
{{ 'initiative.show.classification'|trans }}
+
{{ 'initiative.area'|trans }}
From 2064f87d9e72cc92648f0428f7f59a4ef20df9b3 Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 30 Jun 2026 09:29:29 +0200 Subject: [PATCH 2/3] Updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c562a9..1655671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* [PR-23](https://github.com/itk-dev/itk-project-database/pull/23) + Introduce reusable Twig components (page header, card header, empty state, KPI) + to replace repeated markup. * [PR-22](https://github.com/itk-dev/itk-project-database/pull/22) Use the ITK logo in the nav and login, tidy the dashboard header, and refresh the login screen. From 0b8a25894e1ae42420ae81255883da78ae0ceb3d Mon Sep 17 00:00:00 2001 From: Jeppe Krogh Date: Tue, 30 Jun 2026 09:32:49 +0200 Subject: [PATCH 3/3] Coding standards --- config/packages/twig_component.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/packages/twig_component.yaml b/config/packages/twig_component.yaml index fd17ac6..1d7b9c4 100644 --- a/config/packages/twig_component.yaml +++ b/config/packages/twig_component.yaml @@ -1,5 +1,5 @@ twig_component: - anonymous_template_directory: 'components/' - defaults: - # Namespace & directory for components - App\Twig\Components\: 'components/' + anonymous_template_directory: 'components/' + defaults: + # Namespace & directory for components + App\Twig\Components\: 'components/'