65 Commits

Author SHA1 Message Date
brabli f0730c268e Update to version 2.2.0 2026-05-06 12:34:52 +01:00
brabli f06dbf90bc Add semantic badge colours 2026-05-06 12:34:34 +01:00
brabli 44f878715b Fix grammar 2026-05-06 12:34:15 +01:00
brabli edb8a05bee Fix tag 2026-05-06 12:34:08 +01:00
brabli 81213d8f36 Updatechangelog 2026-05-01 15:42:03 +01:00
brabli 3b6521bfce Set default badge style to fully rounded 2026-05-01 15:39:56 +01:00
brabli 2c93518654 Fix issues with glossy class 2026-05-01 15:38:29 +01:00
brabli cea8ed8d95 Update command to work on mac 2026-05-01 15:38:15 +01:00
brabli 52adc81dbd Update radial gradient 2026-05-01 15:14:54 +01:00
brabli f54c745de0 Fix border on glossy badges 2026-05-01 15:12:15 +01:00
brabli 2687a584b9 Increase border transparency 2026-05-01 15:12:07 +01:00
brabli 423b696525 Update logic 2026-04-30 16:35:19 +01:00
brabli b994b83fdb Update border colour 2026-04-30 16:35:15 +01:00
brabli 12169001d2 Update changelog 2026-04-30 15:52:53 +01:00
brabli cc7a2d5cd4 Update readme 2026-04-30 15:52:32 +01:00
brabli 1bd216f4a5 Remove glossy from outline badges 2026-04-30 15:52:20 +01:00
brabli 3fb9714010 Update showcase 2026-04-30 15:52:00 +01:00
brabli 1e8ac58578 Add aria labels 2026-04-30 15:51:56 +01:00
brabli d7ca3d9285 Update image 2026-04-30 15:51:41 +01:00
brabli 197f9a109e Add showcase image 2026-04-30 15:32:40 +01:00
brabli 67465e9790 Remove icon name 2026-04-30 15:31:42 +01:00
brabli d0a84f135d Remove unneeded service 2026-04-30 15:28:01 +01:00
brabli bf035399ee Update changelog 2026-04-30 15:25:01 +01:00
brabli 40f9aaceef Update readme 2026-04-30 15:20:39 +01:00
brabli a7aabcebef Update showcase 2026-04-30 15:20:29 +01:00
brabli fd6bf6ad4f Update badge logic 2026-04-30 15:20:24 +01:00
brabli 78afab2450 Update colours 2026-04-30 15:20:16 +01:00
brabli df76af9cdf Create badgeable trait 2026-04-30 14:40:45 +01:00
brabli 25d5441184 Rename sheen to glossy 2026-04-30 14:40:39 +01:00
brabli 5046edb4c9 Add shadow sm class to base 2026-04-30 14:27:41 +01:00
brabli 6a9cd627e2 Add serve command 2026-04-30 12:23:30 +01:00
brabli 23e02068c9 Update compose.yml 2026-04-30 12:23:18 +01:00
brabli 949d268b84 Add dev deps to serve twig files 2026-04-30 12:23:09 +01:00
brabli 5f1404d77c Add showcase code 2026-04-30 12:22:50 +01:00
brabli 93258abd38 Update logic 2026-04-30 12:17:08 +01:00
brabli b4cf626468 Render icon 2026-04-30 12:16:58 +01:00
brabli fa04d7e463 Update gitignore 2026-04-30 12:16:31 +01:00
brabli 9e85f5f9d6 Update readme 2026-04-29 17:33:13 +01:00
brabli 9cb7bf23a6 Render icon 2026-04-29 17:33:03 +01:00
brabli 1b9751324b Remove conditional 2026-04-29 17:07:00 +01:00
brabli c84f2b0973 Add icon param 2026-04-29 17:05:12 +01:00
brabli 4ecf0f2feb Update method sig 2026-04-29 16:15:18 +01:00
brabli 80d24dc948 Add get badge icon method 2026-04-29 16:09:37 +01:00
brabli bf2f473038 Replace tabs with 4 spaces 2026-04-27 16:57:04 +01:00
brabli 0d9fbc3aed Update readme 2025-11-13 16:37:38 +00:00
brabli 14fd9ca2ec Tweak error message 2025-11-13 16:36:58 +00:00
brabli 93c4dd1e64 Add accent badge colour 2025-11-13 16:34:58 +00:00
brabli 61eaa62406 Update changelog 2025-11-13 16:30:22 +00:00
brabli bcd97f20a3 Add zinc badge colour 2025-11-13 16:30:11 +00:00
brabli 9c1c0c1ba1 Add new colours 2025-11-13 16:26:35 +00:00
brabli 42fcaef53c Update changelog 2025-02-26 11:44:15 +00:00
brabli beeeb2bf50 Add mit license 2025-02-26 11:43:15 +00:00
brabli f0c19dad5e Update readme 2025-02-26 11:41:21 +00:00
brabli 62e279e0c9 Use new badge colour enum name 2025-02-26 11:36:55 +00:00
brabli f6f3c64f25 Rename badge enum to badgecolour 2025-02-26 11:36:29 +00:00
brabli b7a3f273a3 Organise colours alphabetically, add NAVY colour 2025-02-26 11:33:58 +00:00
brabli 03e2e39185 Update changelog 2025-02-26 11:33:43 +00:00
brabli 9141e21a02 Update readme 2025-02-26 10:59:42 +00:00
brabli b25bbfd33c Set label property 2025-02-26 10:46:04 +00:00
brabli eebd38d17f Update changelog 2025-02-26 10:41:14 +00:00
brabli 2a7230298e Add label attribute 2025-02-26 10:41:07 +00:00
brabli ebc73e1b24 Update docblock 2025-02-26 09:55:28 +00:00
brabli 6883382420 Pass in attributes to badge template 2025-02-26 09:49:21 +00:00
brabli fc9161b2d8 Autoformat 2025-02-26 09:49:10 +00:00
brabli 623fc60af2 Replace makefile with justfile 2025-02-26 09:47:03 +00:00
21 changed files with 577 additions and 100 deletions
+1
View File
@@ -5,3 +5,4 @@ composer.lock
.phpunit.result.cache
/var
.php-cs-fixer.cache
/dev/var
+29 -1
View File
@@ -1,6 +1,34 @@
# Changelog
## [X.X.X] - XXXX-XX-XX
## [x.x.x] - xxxx-xx-xx
## [2.2.0] - 2026-05-05
- Add semantic badge colours (`SUCCESS`, `WARNING`, `DANGER`)
- Fix typos
## [2.1.0] - 2026-05-01
- Fix glossy badge border colour
- Change default badge style to be fully rounded
- Reduce intensity of outline badge borders
## [2.0.0] - 2026-04-30
- Add new colours
- Update existing colours and styles slightly
- Add `getBadgeIcon` method to `BadgeableInterface`
- Add `BadgeableTrait` for a default implementation of `BadgeableInterface`
- Add new attribute `glossy` to render a glossy finish over the badge
- Add new attribute `icon` to render an icon next to the the badge label
- Add `aria` attributes to badges
## [1.1.0] - 2025-11-13
- Add new badge colours
- Tweak error message
## [1.0.0] - 2025-02-26
- Add label attribute to set badge text
- Rename `Badge` enum to `BadgeColour`
- Pass misc attributes to badge template
- Update documentation and docblocks
## [0.1.1] - 2024-09-18
- Fixed black badge text colour not being black
+7 -1
View File
@@ -1 +1,7 @@
License
Copyright © 2025-present PCM Systems
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-16
View File
@@ -1,16 +0,0 @@
PHP = docker compose run php
.PHONY: composer_install composer_update static_analysis tests
composer_install:
@$(PHP) composer install
composer_update:
@$(PHP) composer update
static_analysis:
@$(PHP) vendor/bin/psalm
tests:
@$(PHP) rm -rf var/cache
@$(PHP) vendor/bin/phpunit
+70 -13
View File
@@ -2,7 +2,7 @@
Create badges from objects or as standalone elements.
IMAGE HERE
![PCM Badge Bundle showcase](docs/showcase.png)
```twig
<twig:Pcm:Badge :obj="job.kind">{{ job.kind.label }}</twig:Pcm:Badge>
@@ -16,30 +16,40 @@ Create badges from objects or as standalone elements.
<br>
# Badgeable interface
# Creating badges with `BadgeableInterface`
Any object that you would like to be able to be turned into a badge must implement `BadgeableInterface`.
This interface specifies a single method `getBadgeColour()` and is used to determine what colour the badge should be rendered as.
You can use `BadgeableTrait` to get a default implementation.
This method can contain as much logic in it as you'd like to return different colours of badge under different circumstances. EG:
The interface specifies two methods:
- `getBadgeColour(): BadgeColour` — returns the enum case that determines the badge's colour.
- `getBadgeIcon(): ?string` — returns an [Iconify](https://iconify.design) icon name (e.g. `material-symbols:add-alert`), or `null` for no icon.
Either method can contain as much logic as you like:
```php
// Job.php
public function getBadgeColour(): Badge
public function getBadgeColour(): BadgeColour
{
return match ($this->getKind()) {
JobKind::Investigation => Badge::BLUE,
JobKind::Interrogation => Badge::RED,
JobKind::Shootout => Badge::BLACK,
JobKind::SipWhiskey => Badge::FOREST,
default => Badge::GREY
JobKind::Investigation => BadgeColour::BLUE,
JobKind::Interrogation => BadgeColour::RED,
JobKind::Shootout => BadgeColour::BLACK,
JobKind::SipWhiskey => BadgeColour::FOREST,
default => BadgeColour::GREY
};
}
public function getBadgeIcon(): ?string
{
return null;
}
```
You then specify the object using the `obj` prop when rendering the badge:
You then specify the object using the `:obj` prop when rendering the badge:
```twig
{# job.html.twig #}
@@ -47,6 +57,26 @@ You then specify the object using the `obj` prop when rendering the badge:
<twig:Pcm:Badge :obj="job">{{ job.kind.value }}</twig:Pcm:Badge>
```
## Using `BadgeableTrait`
If you only care about one of the interface methods, use `BadgeableTrait` for sensible defaults (`BadgeColour::DEFAULT` and no icon) and override only what you need:
```php
use Pcm\BadgeBundle\Interface\BadgeableInterface;
use Pcm\BadgeBundle\Trait\BadgeableTrait;
class Job implements BadgeableInterface
{
use BadgeableTrait;
public function getBadgeColour(): BadgeColour
{
return BadgeColour::BLUE;
}
// getBadgeIcon() inherited from the trait — returns null
}
```
<br>
# Standalone badges
@@ -70,11 +100,24 @@ A badge must contain either an `obj` or a `colour`, but not both.
obj="{{ job.kind }}"
```
`colour` - One of the available colours specified by the `Badge` enum. For a full list of acceptable values type in some junk and read the exception message.
`colour` - One of the available colours specified by the `BadgeColour` enum. The palette covers the usual primaries plus tones like `cyan`, `indigo`, `purple`, `slate`, `stone`, `teal` and `violet` — see `src/Enum/BadgeColour.php` for the full list, or pass an invalid value and read the exception message.
`outline` - A boolean attribute that changes the style of the badge to an outline.
`class` - Extra classes you want to add to the badge element. These will override the base classes in case of conflicts.
`glossy` - A boolean attribute that adds a gradient sheen to the badge. Cannot be combined with `outline` — passing both will throw an exception.
`icon` - Render an icon to the left of the label. Pass an [Iconify](https://iconify.design) name (e.g. `icon="material-symbols:add-alert"`) to use a specific icon, or pass it as a boolean (`icon`) when using `:obj` to use the icon returned by `getBadgeIcon()`. Requires `symfony/ux-icons` in the host project.
`class` - Extra classes you want to add to the badge element. These are merged with the badge base classes taking priority in case of conflicts.
`label` - Badge label text. Content inside the content block will be prioritised over the label attribute if present.
```php
{# Both of these render the same markup. #}
<twig:Pcm:Badge colour="red" label="Warning!" />
<twig:Pcm:Badge colour="red">Warning!</twig:Pcm:Badge>
```
<br>
@@ -84,3 +127,17 @@ obj="{{ job.kind }}"
pcm_badge:
base_classes: "base classes here"
```
<br>
# Local development
The bundle ships with a small Symfony preview app under `dev/` that renders many different badge variants.
```bash
just composer_install # install dependencies
just serve # start the preview at http://localhost:8000
```
Edit `dev/templates/showcase.html.twig` to add or tweak variants — changes are picked up on refresh. Other handy recipes (see `justfile`):
+12 -3
View File
@@ -1,4 +1,13 @@
<div class="{{ this.finalClasses }}">
{% block content %}
{% endblock %}
<div {{ attributes }} class="{{ this.finalClasses }}"{% if this.label %} aria-label="{{ this.label }}"{% endif %}>
<span class="inline-flex gap-1 items-center justify-center">
{% if this.icon %}
<twig:ux:icon name="{{ this.icon }}" class="size-[1em] shrink-0" aria-hidden="true" />
{% endif %}
{% if block('content') is not empty %}
{% block content %}{% endblock %}
{% else %}
{{- this.label -}}
{% endif %}
</span>
</div>
+2
View File
@@ -5,3 +5,5 @@ services:
dockerfile: Containerfile
volumes:
- ./:/code
ports:
- "8000:8000"
+5 -1
View File
@@ -20,7 +20,8 @@
},
"autoload-dev": {
"psr-4": {
"Pcm\\BadgeBundle\\Tests\\": "tests/"
"Pcm\\BadgeBundle\\Tests\\": "tests/",
"Pcm\\BadgeBundle\\Dev\\": "dev/src/"
}
},
"require": {
@@ -32,6 +33,9 @@
},
"require-dev": {
"symfony/test-pack": "^1.1",
"symfony/twig-bundle": "^7.1",
"symfony/ux-icons": "^2.18",
"symfony/http-client": "^7.1",
"friendsofphp/php-cs-fixer": "^3.61"
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
return static function (DefinitionConfigurator $definition): void {
$definition->rootNode()
->children()
->scalarNode('base_classes')->defaultValue('text-center rounded max-w-max text-xs px-2 py-1 border min-w-max')->end()
->scalarNode('base_classes')->defaultValue('shadow-sm text-center rounded-full max-w-max text-xs px-2 py-1 border min-w-max')->end()
->end()
;
};
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
use Pcm\BadgeBundle\Dev\Kernel;
use Symfony\Component\HttpFoundation\Request;
require_once dirname(__DIR__, 2).'/vendor/autoload.php';
$_SERVER['APP_ENV'] = $_SERVER['APP_ENV'] ?? 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? '1';
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
+76
View File
@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Pcm\BadgeBundle\Dev\Controller;
use Pcm\BadgeBundle\Enum\BadgeColour;
use Pcm\BadgeBundle\Interface\BadgeableInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ShowcaseController extends AbstractController
{
#[Route('/', name: 'showcase')]
public function __invoke(): Response
{
$colours = array_map(fn (BadgeColour $c) => strtolower($c->name), BadgeColour::cases());
$samples = [
new SampleBadgeable('Investigation', BadgeColour::BLUE),
new SampleBadgeable('Interrogation', BadgeColour::RED),
new SampleBadgeable('Shootout', BadgeColour::BLACK),
new SampleBadgeable('Sip Whiskey', BadgeColour::FOREST),
new SampleBadgeable('Unknown', BadgeColour::GREY),
];
$iconSamples = [
new SampleIconBadge('Investigation', BadgeColour::BLUE, 'mdi:magnify'),
new SampleIconBadge('Interrogation', BadgeColour::RED, 'bi:cassette-fill'),
new SampleIconBadge('Shootout', BadgeColour::BLACK, 'mdi:gun'),
new SampleIconBadge('Sip Whiskey', BadgeColour::FOREST, 'flowbite:whiskey-glass-outline'),
new SampleIconBadge('Unknown', BadgeColour::GREY, 'carbon:unknown'),
];
return $this->render('showcase.html.twig', [
'colours' => $colours,
'samples' => $samples,
'icon_samples' => $iconSamples
]);
}
}
final readonly class SampleBadgeable implements BadgeableInterface
{
public function __construct(public string $label, private BadgeColour $colour)
{
}
public function getBadgeColour(): BadgeColour
{
return $this->colour;
}
public function getBadgeIcon(): ?string
{
return null;
}
}
final readonly class SampleIconBadge implements BadgeableInterface
{
public function __construct(public string $label, private BadgeColour $colour, private string $icon)
{
}
public function getBadgeColour(): BadgeColour
{
return $this->colour;
}
public function getBadgeIcon(): ?string
{
return $this->icon;
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Pcm\BadgeBundle\Dev;
use Pcm\BadgeBundle\PcmBadgeBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
use Symfony\UX\Icons\UXIconsBundle;
use Symfony\UX\TwigComponent\TwigComponentBundle;
final class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function registerBundles(): iterable
{
return [
new FrameworkBundle(),
new TwigBundle(),
new TwigComponentBundle(),
new UXIconsBundle(),
new PcmBadgeBundle(),
];
}
public function getProjectDir(): string
{
return \dirname(__DIR__);
}
public function getCacheDir(): string
{
return $this->getProjectDir().'/var/cache/'.$this->environment;
}
public function getLogDir(): string
{
return $this->getProjectDir().'/var/log';
}
private function configureContainer(ContainerConfigurator $container): void
{
$container->extension('framework', [
'secret' => 'dev',
'router' => ['utf8' => true],
'test' => false,
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
]);
$container->extension('twig', [
'default_path' => $this->getProjectDir().'/templates',
]);
// Default config for the bundle. Tweak to preview different base classes.
$container->extension('pcm_badge', []);
// Fetch icons on-demand from iconify.design so no JS toolchain is needed.
$container->extension('ux_icons', [
'icon_dir' => '%kernel.project_dir%/var/icons',
'iconify' => ['enabled' => true, 'on_demand' => true],
]);
$container->services()
->defaults()
->autowire()
->autoconfigure()
->load(__NAMESPACE__.'\\Controller\\', __DIR__.'/Controller/')
->public()
;
}
private function configureRoutes(RoutingConfigurator $routes): void
{
$routes->import(__DIR__.'/Controller/', 'attribute');
}
}
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PCM Badge Bundle — Dev Preview</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
accent: '#7c3aed',
primary: '#0f172a',
},
fontSize: {
'2xs': ['0.625rem', '0.875rem'],
},
},
},
};
</script>
</head>
<body class="bg-zinc-50 text-zinc-900 p-8 font-sans">
<h1 class="text-2xl font-bold mb-2">PCM Badge Bundle</h1>
<p class="text-zinc-600 mb-8">Local dev preview. Edit <code class="bg-zinc-200 px-1 rounded">dev/templates/showcase.html.twig</code> to add new variants.</p>
<section class="mb-10">
<h2 class="text-lg font-semibold mb-3">Solid</h2>
<div class="flex flex-wrap gap-2">
{% for colour in colours %}
<twig:Pcm:Badge colour="{{ colour }}">{{ colour }}</twig:Pcm:Badge>
{% endfor %}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold mb-3">Outline</h2>
<div class="flex flex-wrap gap-2">
{% for colour in colours %}
<twig:Pcm:Badge colour="{{ colour }}" outline>{{ colour }}</twig:Pcm:Badge>
{% endfor %}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold mb-3">Solid glossy</h2>
<div class="flex flex-wrap gap-2">
{% for colour in colours %}
<twig:Pcm:Badge glossy colour="{{ colour }}">{{ colour }}</twig:Pcm:Badge>
{% endfor %}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold mb-3">Solid icon</h2>
<div class="flex flex-wrap gap-2">
{% for colour in colours %}
<twig:Pcm:Badge icon="material-symbols:add" colour="{{ colour }}">{{ colour }}</twig:Pcm:Badge>
{% endfor %}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold mb-3">Outline icon</h2>
<div class="flex flex-wrap gap-2">
{% for colour in colours %}
<twig:Pcm:Badge icon="material-symbols:add" colour="{{ colour }}" outline>{{ colour }}</twig:Pcm:Badge>
{% endfor %}
</div>
</section>
<section class="mb-10">
<h2 class="text-lg font-semibold mb-3">Solid icon glossy</h2>
<div class="flex flex-wrap gap-2">
{% for colour in colours %}
<twig:Pcm:Badge glossy icon="material-symbols:edit-outline" colour="{{ colour }}">{{ colour }}</twig:Pcm:Badge>
{% endfor %}
</div>
</section>
</body>
</html>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

+19
View File
@@ -0,0 +1,19 @@
php := "docker compose run php"
composer_install:
@{{php}} composer install
composer_update:
@{{php}} composer update
static_analysis:
@{{php}} vendor/bin/psalm
tests:
@{{php}} rm -rf var/cache
@{{php}} vendor/bin/phpunit
# Run the local Symfony preview app at http://localhost:8000
serve:
@docker compose run --rm --service-ports php sh -c "rm -rf dev/var/cache && php -S 0.0.0.0:8000 -t dev/public"
-46
View File
@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Pcm\BadgeBundle\Enum;
use Pcm\BadgeBundle\Model\BadgePalette;
enum Badge
{
case AMBER;
case BLACK;
case BLUE;
case DEFAULT;
case FOREST;
case GREEN;
case GREY;
case MAROON;
case OCHRE;
case ORANGE;
case RED;
case ROSE;
case STRIPE;
case YELLOW;
public function getPalette(): BadgePalette
{
return match ($this) {
$this::RED => new BadgePalette('text-red-600', 'border-red-600', 'bg-red-600'),
$this::MAROON => new BadgePalette('text-red-900', 'border-red-900', 'bg-red-900'),
$this::BLUE => new BadgePalette('text-sky-700', 'border-sky-700', 'bg-sky-700'),
$this::FOREST => new BadgePalette('text-green-800', 'border-green-800', 'bg-green-800'),
$this::GREEN => new BadgePalette('text-green-600', 'border-green-600', 'bg-green-600'),
$this::OCHRE => new BadgePalette('text-yellow-800', 'border-yellow-800', 'bg-yellow-800'),
$this::ORANGE => new BadgePalette('text-orange-500', 'border-orange-500', 'bg-orange-500'),
$this::AMBER => new BadgePalette('text-amber-500', 'border-amber-500', 'bg-amber-500'),
$this::ROSE => new BadgePalette('text-rose-600', 'border-rose-600', 'bg-rose-600'),
$this::GREY => new BadgePalette('text-neutral-400', 'border-neutral-400', 'bg-neutral-400'),
$this::YELLOW => new BadgePalette('text-yellow-500', 'border-yellow-500', 'bg-yellow-500'),
$this::STRIPE => new BadgePalette('text-indigo-500', 'border-indigo-500', 'bg-indigo-500'),
$this::BLACK => new BadgePalette('text-zinc-900', 'border-zinc-900', 'bg-zinc-900'),
$this::DEFAULT => new BadgePalette('text-primary', 'border-primary', 'bg-primary'),
};
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Pcm\BadgeBundle\Enum;
use Pcm\BadgeBundle\Model\BadgePalette;
enum BadgeColour
{
case ACCENT;
case AMBER;
case BLACK;
case BLUE;
case BROWN;
case CYAN;
case DANGER;
case DEFAULT;
case EMERALD;
case FOREST;
case FUCHSIA;
case GOLD;
case GREEN;
case GREY;
case INDIGO;
case LIME;
case MAROON;
case NAVY;
case OCHRE;
case ORANGE;
case PINK;
case PURPLE;
case RED;
case ROSE;
case SKY;
case SLATE;
case STONE;
case STRIPE;
case SUCCESS;
case TEAL;
case VIOLET;
case WARNING;
case YELLOW;
case ZINC;
public function getPalette(): BadgePalette
{
return match ($this) {
$this::ACCENT => new BadgePalette('text-accent', 'border-accent/30', 'bg-accent'),
$this::AMBER => new BadgePalette('text-amber-600', 'border-amber-600/30', 'bg-amber-600'),
$this::BLACK => new BadgePalette('text-zinc-900', 'border-zinc-900/30', 'bg-zinc-900'),
$this::BLUE => new BadgePalette('text-blue-700', 'border-blue-700/30', 'bg-blue-700'),
$this::BROWN => new BadgePalette('text-stone-700', 'border-stone-700/30', 'bg-stone-700'),
$this::CYAN => new BadgePalette('text-cyan-600', 'border-cyan-600/30', 'bg-cyan-600'),
$this::DANGER => new BadgePalette('text-danger', 'border-danger/30', 'bg-danger'),
$this::DEFAULT => new BadgePalette('text-primary', 'border-primary/30', 'bg-primary'),
$this::EMERALD => new BadgePalette('text-emerald-600', 'border-emerald-600/30', 'bg-emerald-600'),
$this::FOREST => new BadgePalette('text-green-800', 'border-green-800/30', 'bg-green-800'),
$this::FUCHSIA => new BadgePalette('text-fuchsia-600', 'border-fuchsia-600/30', 'bg-fuchsia-600'),
$this::GOLD => new BadgePalette('text-amber-500', 'border-amber-500/30', 'bg-amber-500'),
$this::GREEN => new BadgePalette('text-green-600', 'border-green-600/30', 'bg-green-600'),
$this::GREY => new BadgePalette('text-neutral-500', 'border-neutral-500/30', 'bg-neutral-500'),
$this::INDIGO => new BadgePalette('text-indigo-700', 'border-indigo-700/30', 'bg-indigo-700'),
$this::LIME => new BadgePalette('text-lime-600', 'border-lime-600/30', 'bg-lime-600'),
$this::MAROON => new BadgePalette('text-red-900', 'border-red-900/30', 'bg-red-900'),
$this::NAVY => new BadgePalette('text-blue-900', 'border-blue-900/30', 'bg-blue-900'),
$this::OCHRE => new BadgePalette('text-yellow-800', 'border-yellow-800/30', 'bg-yellow-800'),
$this::ORANGE => new BadgePalette('text-orange-500', 'border-orange-500/30', 'bg-orange-500'),
$this::PINK => new BadgePalette('text-pink-500', 'border-pink-500/30', 'bg-pink-500'),
$this::PURPLE => new BadgePalette('text-purple-600', 'border-purple-600/30', 'bg-purple-600'),
$this::RED => new BadgePalette('text-red-600', 'border-red-600/30', 'bg-red-600'),
$this::ROSE => new BadgePalette('text-rose-600', 'border-rose-600/30', 'bg-rose-600'),
$this::SKY => new BadgePalette('text-sky-600', 'border-sky-600/30', 'bg-sky-600'),
$this::SLATE => new BadgePalette('text-slate-600', 'border-slate-600/30', 'bg-slate-600'),
$this::STONE => new BadgePalette('text-stone-500', 'border-stone-500/30', 'bg-stone-500'),
$this::STRIPE => new BadgePalette('text-indigo-500', 'border-indigo-500/30', 'bg-indigo-500'),
$this::SUCCESS => new BadgePalette('text-success', 'border-success/30', 'bg-success'),
$this::TEAL => new BadgePalette('text-teal-600', 'border-teal-600/30', 'bg-teal-600'),
$this::VIOLET => new BadgePalette('text-violet-600', 'border-violet-600/30', 'bg-violet-600'),
$this::WARNING => new BadgePalette('text-warning', 'border-warning/30', 'bg-warning'),
$this::YELLOW => new BadgePalette('text-yellow-600', 'border-yellow-600/30', 'bg-yellow-600'),
$this::ZINC => new BadgePalette('text-zinc-500', 'border-zinc-500/30', 'bg-zinc-500'),
};
}
}
+15 -3
View File
@@ -4,13 +4,25 @@ declare(strict_types=1);
namespace Pcm\BadgeBundle\Interface;
use Pcm\BadgeBundle\Enum\Badge;
use Pcm\BadgeBundle\Enum\BadgeColour;
/**
* Enables this class to be rendered as a badge using the Badge and BadgeOutline Twig components.
* Allows rendering the implementing class as a badge using the PCM Badge twig component.
*/
interface BadgeableInterface
{
public function getBadgeColour(): Badge;
/**
* Get the badge colour that should be used when rendering this object as a badge.
*
* @return BadgeColour Colour of badge to render
*/
public function getBadgeColour(): BadgeColour;
/**
* Get the badge icon name
*
* @return ?string Icon name or `null` if no icon exists.
*/
public function getBadgeIcon(): ?string;
}
+3 -1
View File
@@ -15,5 +15,7 @@ final readonly class BadgePalette
* @param string $backgroundColourClass Background colour Tailwind class
*/
public function __construct(public string $textColourClass, public string $borderColourClass, public string $backgroundColourClass)
{}
{
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Pcm\BadgeBundle\Trait;
use Pcm\BadgeBundle\Enum\BadgeColour;
/**
* Provides a default implementation of {@see Pcm\BadgeBundle\Interface\BadgeableInterface}
*/
trait BadgeableTrait
{
public function getBadgeColour(): BadgeColour
{
return BadgeColour::DEFAULT;
}
public function getBadgeIcon(): ?string
{
return null;
}
}
+44 -13
View File
@@ -4,36 +4,61 @@ declare(strict_types=1);
namespace Pcm\BadgeBundle\Twig\Component;
use Pcm\BadgeBundle\Enum\Badge as EnumBadge;
use Pcm\BadgeBundle\Enum\BadgeColour;
use Pcm\BadgeBundle\Interface\BadgeableInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use TailwindMerge\TailwindMerge;
#[AsTwigComponent(name: 'Pcm:Badge', template: "@PcmBadge/Badge.html.twig")]
#[AsTwigComponent(name: 'Pcm:Badge', template: '@PcmBadge/Badge.html.twig')]
final class Badge
{
public string $finalClasses;
public ?string $label = null;
public ?string $icon = null;
public function __construct(private string $baseClasses)
{
}
/**
* @param ?BadgeableInterface $obj The object to be converted into a badge.
* @param ?BadgeableInterface $obj The object to be converted into a badge
* @param ?string $class Extra classes to add to the badge element.
* These will override the base classes in case
* of conflicts.
* @param ?string $colour Specify the colour of an objectless badge.
* @param bool $outline If the badge should be rendered as an outline.
* @param ?string $colour Manually specify the colour of a badge
* @param bool $outline Whether the badge should be rendered with an outline
* @param ?string $icon
*/
public function mount(?BadgeableInterface $obj = null, ?string $class = null, string $colour = null, bool $outline = false): void
public function mount(?BadgeableInterface $obj = null, ?string $class = null, ?string $colour = null, ?string $label = null, bool $outline = false, ?string $icon = null, bool $glossy = false): void
{
if ($outline && $glossy) {
throw new \RuntimeException('The "outline" and "glossy" props cannot be used together.');
}
if (!$icon) {
$this->icon = null;
} else if ("1" === $icon) {
if (null === $obj) {
throw new \RuntimeException("Missing icon name.");
}
$this->icon = $obj->getBadgeIcon();
if (null === $this->icon) {
throw new \RuntimeException("Missing icon name.");
}
} else {
$this->icon = $icon;
}
$this->label = $label;
if (!$obj && !$colour) {
throw new \RuntimeException(sprintf("You must specify either a colour an instance of \"%s\".", BadgeableInterface::class));
throw new \RuntimeException(sprintf('You must specify either a colour or an instance of "%s".', BadgeableInterface::class));
}
if ($obj && $colour) {
throw new \RuntimeException(sprintf("You have specified both the colour \"%s\" and an instance of \"%s\". Please use one or the other.", $colour, $obj::class));
throw new \RuntimeException(sprintf('You have specified both the colour "%s" and an instance of "%s". Please use one or the other.', $colour, $obj::class));
}
if ($obj) {
@@ -41,15 +66,15 @@ final class Badge
}
if ($colour) {
$cases = array_map(fn(EnumBadge $b) => strtolower($b->name), EnumBadge::cases());
$cases = array_map(fn (BadgeColour $b) => strtolower($b->name), BadgeColour::cases());
if (!in_array($colour, $cases)) {
$formattedCases = implode(", ", array_map(fn(string $s) => '"'.$s.'"', $cases));
throw new \RuntimeException(sprintf('"%s" is not a valid Badge colour. Available options are: %s', $colour, $formattedCases));
$formattedCases = implode(', ', array_map(fn (string $s) => '"'.$s.'"', $cases));
throw new \RuntimeException(sprintf('"%s" is not a valid badge colour. Available options are: %s.', $colour, $formattedCases));
}
$colour = strtoupper($colour);
$palette = EnumBadge::{$colour}->getPalette();
$palette = BadgeColour::{$colour}->getPalette();
}
$merger = TailwindMerge::instance();
@@ -57,7 +82,13 @@ final class Badge
if (true === $outline) {
$classes = sprintf('bg-white %s %s %s %s', $palette->borderColourClass, $palette->textColourClass, $this->baseClasses, $class);
} else {
$classes = sprintf('text-white %s %s %s %s', $palette->borderColourClass, $palette->backgroundColourClass, $this->baseClasses, $class);
$glossyClass = $glossy ? 'bg-origin-border bg-[image:radial-gradient(ellipse_at_-10%_-50%,rgba(255,255,255,0.35),transparent_70%),linear-gradient(to_bottom,rgba(255,255,255,0.05),rgba(0,0,0,0.12))]' : '';
$classes = sprintf('text-white %s %s %s [border-color:transparent] %s', $palette->backgroundColourClass, $glossyClass, $this->baseClasses, $class);
}
if ($this->icon !== null) {
$classes = sprintf("flex gap-1 items-center %s", $classes);
}
$this->finalClasses = $merger->merge(trim($classes));