Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ vendor
.php-cs-fixer.cache
.phpunit.result.cache
yarn.lock
composer.lock
composer.lock
docker-compose.yml
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ You can check it out here at Github in the [TablerBundle-Demo](https://github.co
- ContextHelper for dynamic layout changes (e.g. based on user preferences)
- Translations for: english, german, italian, czech, spanish, russian, arabic, finnish, japanese, swedish, portuguese (brazilian), dutch, french, turkish, danish, chinese, slovakian, basque, polish, esperanto, hebrew, romanian ([please help translating it to more languages](https://hosted.weblate.org/projects/kimai/theme/))
- Based on Bootstrap 5
- Supports FontAwesome 5
- Supports [Symfony UX icons](https://ux.symfony.com/icons)

## Installation

Expand Down
60 changes: 60 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,66 @@

## 2.1

### tabler_icon

The `tabler_icon` feature is deprecated and should no longer be used.

#### What changed

Historically, TablerBundle provided its own icon rendering and aliasing layer:

- Icons were configured in `tabler.yaml` under `tabler.icons`
- Icons were rendered through the `tabler_icon` Twig extension

This approach is being phased out in favor of Symfony UX Icons, which is now the recommended and supported way to render icons and manage aliases.

#### What to use instead

Use the Symfony UX icon system:

- Render icons with `ux_icon(...)`
- Configure aliases in `ux_icons.yaml` using the [`aliases` section](https://symfony.com/bundles/ux-icons/current/index.html#icon-aliases)

This fully replaces the previous `tabler.yaml` -> `icons` configuration.

#### Migration steps

1. Move icon aliases from `tabler.yaml` under `tabler.icons` to `ux_icons.yaml` under `ux_icons.aliases`.
2. Go to [Ux icon search page](https://ux.symfony.com/icons?set=tabler) and find related icon to yours before
1. Previous configuration (`tabler.yaml`):
```yaml
tabler:
icons:
user: fas fa-user
settings: fas fa-cogs
thumb_up: thumbs-up
```

2. New configuration (`ux_icons.yaml`):
```yaml
ux_icons:
aliases:
user: tabler:user
settings: tabler:settings
thumb_up: tabler:thumb-up
```
3. Rename all usages of `tabler_icon` in Twig templates to `ux_icon`.
1. Before:
```twig
{{ tabler_icon('user') }}
```

2. After:
```twig
{{ ux_icon('user') }}
```

#### Notes

- Direct icon identifiers such as `tabler:user` can still be used directly with `ux_icon(...)`.
- Aliases are optional and only required if you want to keep short logical icon names.
- Support for `tabler_icon` will be removed in a future major version `3.0`.

### Dropdown

In macro `dropdown()`:
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "kevinpapst/tabler-bundle",
"type": "symfony-bundle",
"description": "Admin/Backend theme bundle for Symfony based on Tabler.io",
"description": "Theme bundle for Symfony based on Tabler.io",
"license": "MIT",
"authors": [
{
Expand All @@ -21,7 +21,8 @@
"symfony/security-core": "^6.0 || ^7.0 || ^8.0",
"symfony/translation": "^6.0 || ^7.0 || ^8.0",
"symfony/twig-bridge": "^6.0 || ^7.0 || ^8.0",
"twig/twig": "^3.0"
"twig/twig": "^3.0",
"symfony/ux-icons": "^2.0"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should move symfony/ux-icons to suggest section and make the dependency optional.
I will see if I find a way.

Copy link
Collaborator Author

@cavasinf cavasinf Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was doing that at the beginning, but since we allow icon alias references from MenuItem, we NEED to be able to generate the icon and not only recommend it.

That said, my idea would be to allow developers to pass an html string to that parameter. Why?

  • It gives them full control over what is rendered in the menu
  • They are not forced to use FontAwesome (as today) or a future ux-icon solution

TBH, I was thinking the same approach could apply to every component that accepts an icon alias. We should only accept an HTML string (still BC-compatible if an alias is provided) and simply render icon|raw inside the component.

For your example with that f***ing icon class, you will be able to add/remove it yourself, and not being forced by the menu template.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make it required now, and suggest it for 3.0 later

},
"require-dev": {
"symfony/framework-bundle" : "^6.0 || ^7.0 || ^8.0",
Expand Down
14 changes: 13 additions & 1 deletion config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@ services:
- '@event_dispatcher'
- '@tabler_bundle.context_helper'
- '%tabler_bundle.routes%'
- '%tabler_bundle.icons%'
tags:
- { name: twig.runtime }

KevinPapst\TablerBundle\Twig\Extension\IconExtension:
class: KevinPapst\TablerBundle\Twig\Extension\IconExtension
tags:
- { name: twig.extension }

KevinPapst\TablerBundle\Twig\Runtime\IconRuntime:
class: KevinPapst\TablerBundle\Twig\Runtime\IconRuntime
arguments:
$iconRenderer: '@.ux_icons.icon_renderer'
$icons: '%tabler_bundle.icons%'
tags:
- { name: twig.runtime }

Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->append($this->getKnpMenuConfig())
->append($this->getRouteAliasesConfig())
->arrayNode('icons')
->setDeprecated('kevinpapst/TablerBundle', '2.1.0')
->defaultValue([])
->scalarPrototype()
->end()
Expand Down
40 changes: 40 additions & 0 deletions src/Twig/Extension/IconExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/*
* This file is part of the Tabler bundle, created by Kevin Papst (www.kevinpapst.de).
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace KevinPapst\TablerBundle\Twig\Extension;

use KevinPapst\TablerBundle\Twig\Runtime\IconRuntime;
use Twig\DeprecatedCallableInfo;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;

class IconExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
/* @phpstan-ignore-next-line */
new TwigFilter('tabler_icon', [IconRuntime::class, 'htmlClassAttributeValue'], [
'deprecation_info' => new DeprecatedCallableInfo('kevinpapst/tabler-bundle', '3.0'),
]),
];
}

public function getFunctions(): array
{
return [
/* @phpstan-ignore-next-line */
new TwigFunction('tabler_icon', [IconRuntime::class, 'renderIcon'], [
'is_safe' => ['html'],
'deprecation_info' => new DeprecatedCallableInfo('kevinpapst/tabler-bundle', '3.0'),
]),
];
}
}
70 changes: 70 additions & 0 deletions src/Twig/Runtime/IconRuntime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/*
* This file is part of the Tabler bundle, created by Kevin Papst (www.kevinpapst.de).
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace KevinPapst\TablerBundle\Twig\Runtime;

use Symfony\UX\Icons\IconRendererInterface;
use Twig\Extension\RuntimeExtensionInterface;

/**
* @deprecated Use Symfony UX instead
*/
class IconRuntime implements RuntimeExtensionInterface
{
/**
* @param array<string, string> $icons
*/
public function __construct(
private readonly IconRendererInterface $iconRenderer,
private readonly array $icons,
) {
}

/**
* @deprecated Use Symfony UX instead
*/
public function renderIcon(string $name, bool $withIconClass = false, ?string $default = null): string
{
$safeName = str_replace('-', '_', $name);
if (isset($this->icons[$safeName])) {
// Tabler icon shortcut
$fontawesomeFullName = $this->icons[$safeName];
} elseif (str_contains($name, ' ')) {
// Fontawesome with space
$fontawesomeFullName = $name;
} elseif (str_contains($name, ':')) {
// Ux icon
return $this->iconRenderer->renderIcon($name, [
'class' => $withIconClass ? 'icon' : '',
]);
} else {
return $this->htmlClassAttributeValue($name, $withIconClass, $default);
}

[$typeNameAbbreviation, $iconFullName] = explode(' ', $fontawesomeFullName);
$iconName = preg_replace('/^fa-/', '', $iconFullName);
$sets = match ($typeNameAbbreviation) {
'far' => 'fa-regular',
'fab' => 'fa-brands',
default => 'fa-solid',
};

return $this->iconRenderer->renderIcon("$sets:$iconName", [
'class' => $withIconClass ? 'icon' : '',
]);
}

/**
* @deprecated Use Symfony UX instead
*/
public function htmlClassAttributeValue(string $name, bool $withIconClass = false, ?string $default = null): string
{
return ($withIconClass ? 'icon ' : '') . ($this->icons[str_replace('-', '_', $name)] ?? ($default ?? $name));
}
}
12 changes: 0 additions & 12 deletions src/Twig/RuntimeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ final class RuntimeExtension implements RuntimeExtensionInterface
{
/**
* @param array<string, string|null> $routes
* @param array<string, string> $icons
*/
public function __construct(
private readonly RequestStack $requestStack,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly ContextHelper $helper,
private readonly array $routes,
private readonly array $icons
) {
}

Expand Down Expand Up @@ -114,16 +112,6 @@ public function getUserDetails(): ?UserDetailsEvent
return $userEvent;
}

public function createIcon(string $name, bool $withIconClass = false, ?string $default = null): string
{
return '<i class="' . $this->icon($name, $withIconClass, $default) . '"></i>';
}

public function icon(string $name, bool $withIconClass = false, ?string $default = null): string
{
return ($withIconClass ? 'icon ' : '') . ($this->icons[str_replace('-', '_', $name)] ?? ($default ?? $name));
}

public function uniqueId(string $prefix = '', bool $more_entropy = false): string
{
return uniqid($prefix, $more_entropy);
Expand Down
2 changes: 0 additions & 2 deletions src/Twig/TablerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public function getFilters(): array
new TwigFilter('tabler_container', [RuntimeExtension::class, 'containerClass']),
new TwigFilter('tabler_body', [RuntimeExtension::class, 'bodyClass']),
new TwigFilter('tabler_route', [RuntimeExtension::class, 'getRouteByAlias']),
new TwigFilter('tabler_icon', [RuntimeExtension::class, 'icon']),
];
}

Expand All @@ -34,7 +33,6 @@ public function getFilters(): array
public function getFunctions(): array
{
return [
new TwigFunction('tabler_icon', [RuntimeExtension::class, 'createIcon'], ['is_safe' => ['html']]),
new TwigFunction('tabler_menu', [RuntimeExtension::class, 'getMenu']),
new TwigFunction('tabler_notifications', [RuntimeExtension::class, 'getNotifications']),
new TwigFunction('tabler_theme', [RuntimeExtension::class, 'theme']),
Expand Down
4 changes: 2 additions & 2 deletions templates/includes/menu.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@

{% macro item_icon(item) %}
{% if item.icon %}
<span class="nav-link-icon d-md-none d-lg-inline-block text-center">{{ tabler_icon(item.icon, false, item.icon) }}</span>
<span class="nav-link-icon d-md-none d-lg-inline-block text-center">{{ tabler_icon(item.icon, true, item.icon) }}</span>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is now the correct way to use the .icon class with Symfony UX Icons, which render SVGs instead of webfonts.
So the icon class has always been required on menu icon elements from the start.

image

Copy link
Owner

@kevinpapst kevinpapst Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so. This is a rather visually unacceptable BC break for everyone, who cannot immediately switch to ux-icons, like me... Can we either make this somehow configurable OR provide a CSS snippet to fix the problem?

image

Looks worse, when its not so zoomed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks worse, when its not so zoomed

Wait, this behavior happens when the .icon class is used with webfonts.
In your screenshot, is it a webfont or an SVG?

With this PR, all <i> elements are replaced by <svg>, so the zoom effect should no longer occur.

image

{% endif %}
{% endmacro %}

{% macro item_badge(item) %}
{% if item.badge is not null or item.badgeColor is not null %}
<span class="badge badge-sm bg-{{ item.badgeColor|default('blue') }} text-{{ item.badgeColor|default('blue') }}-fg text-uppercase ms-2">{{ item.badge }}</span>
{% endif %}
{% endmacro %}
{% endmacro %}
7 changes: 1 addition & 6 deletions tests/Twig/RuntimeExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,10 @@ private function getSut(array $options): RuntimeExtension
'hello' => null,
];

$icons = [
'foo' => 'fas fa-times',
'mail' => 'fas fa-envelope',
];

$dispatcher = new EventDispatcher();
$requestStack = new RequestStack();

return new RuntimeExtension($requestStack, $dispatcher, $contextHelper, $routes, $icons);
return new RuntimeExtension($requestStack, $dispatcher, $contextHelper, $routes);
}

public function testGetRouteByAlias(): void
Expand Down
4 changes: 2 additions & 2 deletions tests/Twig/TablerExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class TablerExtensionTest extends TestCase
{
public function testGetFilters(): void
{
$expected = ['tabler_container', 'tabler_body', 'tabler_route', 'tabler_icon'];
$expected = ['tabler_container', 'tabler_body', 'tabler_route'];
$sut = new TablerExtension();
$this->assertCount(\count($expected), $sut->getFilters());
$result = array_map(function ($filter) {
Expand All @@ -30,7 +30,7 @@ public function testGetFilters(): void

public function testGetFunctions(): void
{
$expected = ['tabler_icon', 'tabler_menu', 'tabler_notifications', 'tabler_theme', 'tabler_unique_id', 'tabler_user'];
$expected = ['tabler_menu', 'tabler_notifications', 'tabler_theme', 'tabler_unique_id', 'tabler_user'];
$sut = new TablerExtension();
$this->assertCount(\count($expected), $sut->getFunctions());
$result = array_map(function ($function) {
Expand Down