diff --git a/.gitignore b/.gitignore index d20a8c3..08e4f6f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ vendor .php-cs-fixer.cache .phpunit.result.cache yarn.lock -composer.lock \ No newline at end of file +composer.lock +docker-compose.yml diff --git a/README.md b/README.md index 47a851a..cc71a9c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/UPGRADING.md b/UPGRADING.md index 153152f..a8acc91 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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()`: diff --git a/composer.json b/composer.json index db6c9a1..7d7470f 100644 --- a/composer.json +++ b/composer.json @@ -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": [ { @@ -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" }, "require-dev": { "symfony/framework-bundle" : "^6.0 || ^7.0 || ^8.0", diff --git a/config/services.yml b/config/services.yml index 2adb08a..8ed0f9c 100644 --- a/config/services.yml +++ b/config/services.yml @@ -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 } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index dd29022..ab68a39 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -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() diff --git a/src/Twig/Runtime/IconRuntime.php b/src/Twig/Runtime/IconRuntime.php new file mode 100644 index 0000000..fd84e93 --- /dev/null +++ b/src/Twig/Runtime/IconRuntime.php @@ -0,0 +1,70 @@ + $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)); + } +} diff --git a/src/Twig/RuntimeExtension.php b/src/Twig/RuntimeExtension.php index c482739..1aa1f2c 100644 --- a/src/Twig/RuntimeExtension.php +++ b/src/Twig/RuntimeExtension.php @@ -22,14 +22,12 @@ final class RuntimeExtension implements RuntimeExtensionInterface { /** * @param array $routes - * @param array $icons */ public function __construct( private readonly RequestStack $requestStack, private readonly EventDispatcherInterface $eventDispatcher, private readonly ContextHelper $helper, private readonly array $routes, - private readonly array $icons ) { } @@ -114,16 +112,6 @@ public function getUserDetails(): ?UserDetailsEvent return $userEvent; } - public function createIcon(string $name, bool $withIconClass = false, ?string $default = null): string - { - return ''; - } - - 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); diff --git a/src/Twig/TablerExtension.php b/src/Twig/TablerExtension.php index 09055e1..e171c2f 100644 --- a/src/Twig/TablerExtension.php +++ b/src/Twig/TablerExtension.php @@ -9,6 +9,8 @@ namespace KevinPapst\TablerBundle\Twig; +use KevinPapst\TablerBundle\Twig\Runtime\IconRuntime; +use Twig\DeprecatedCallableInfo; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -24,7 +26,10 @@ 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']), + /* @phpstan-ignore-next-line */ + new TwigFilter('tabler_icon', [IconRuntime::class, 'htmlClassAttributeValue'], [ + 'deprecation_info' => new DeprecatedCallableInfo('kevinpapst/tabler-bundle', '3.0'), + ]), ]; } @@ -34,12 +39,16 @@ 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']), new TwigFunction('tabler_unique_id', [RuntimeExtension::class, 'uniqueId']), new TwigFunction('tabler_user', [RuntimeExtension::class, 'getUserDetails']), + /* @phpstan-ignore-next-line */ + new TwigFunction('tabler_icon', [IconRuntime::class, 'renderIcon'], [ + 'is_safe' => ['html'], + 'deprecation_info' => new DeprecatedCallableInfo('kevinpapst/tabler-bundle', '3.0'), + ]), ]; } } diff --git a/templates/includes/menu.html.twig b/templates/includes/menu.html.twig index 3aba155..806d9ef 100644 --- a/templates/includes/menu.html.twig +++ b/templates/includes/menu.html.twig @@ -75,7 +75,7 @@ {% macro item_icon(item) %} {% if item.icon %} - {{ tabler_icon(item.icon, false, item.icon) }} + {{ tabler_icon(item.icon, true, item.icon) }} {% endif %} {% endmacro %} @@ -83,4 +83,4 @@ {% if item.badge is not null or item.badgeColor is not null %} {{ item.badge }} {% endif %} -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/tests/Twig/RuntimeExtensionTest.php b/tests/Twig/RuntimeExtensionTest.php index 83de2bd..3899ffd 100644 --- a/tests/Twig/RuntimeExtensionTest.php +++ b/tests/Twig/RuntimeExtensionTest.php @@ -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 diff --git a/tests/Twig/TablerExtensionTest.php b/tests/Twig/TablerExtensionTest.php index 74d6e33..d580ab7 100644 --- a/tests/Twig/TablerExtensionTest.php +++ b/tests/Twig/TablerExtensionTest.php @@ -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', 'tabler_icon']; $sut = new TablerExtension(); $this->assertCount(\count($expected), $sut->getFunctions()); $result = array_map(function ($function) {