Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 81213d8f36 | |||
| 3b6521bfce | |||
| 2c93518654 | |||
| cea8ed8d95 | |||
| 52adc81dbd | |||
| f54c745de0 | |||
| 2687a584b9 | |||
| 423b696525 | |||
| b994b83fdb | |||
| 12169001d2 | |||
| cc7a2d5cd4 | |||
| 1bd216f4a5 | |||
| 3fb9714010 | |||
| 1e8ac58578 | |||
| d7ca3d9285 | |||
| 197f9a109e | |||
| 67465e9790 | |||
| d0a84f135d | |||
| bf035399ee | |||
| 40f9aaceef | |||
| a7aabcebef | |||
| fd6bf6ad4f | |||
| 78afab2450 | |||
| df76af9cdf | |||
| 25d5441184 | |||
| 5046edb4c9 | |||
| 6a9cd627e2 | |||
| 23e02068c9 | |||
| 949d268b84 | |||
| 5f1404d77c | |||
| 93258abd38 | |||
| b4cf626468 | |||
| fa04d7e463 | |||
| 9e85f5f9d6 | |||
| 9cb7bf23a6 | |||
| 1b9751324b | |||
| c84f2b0973 | |||
| 4ecf0f2feb | |||
| 80d24dc948 | |||
| bf2f473038 | |||
| 0d9fbc3aed | |||
| 14fd9ca2ec | |||
| 93c4dd1e64 | |||
| 61eaa62406 | |||
| bcd97f20a3 | |||
| 9c1c0c1ba1 | |||
| 42fcaef53c | |||
| beeeb2bf50 | |||
| f0c19dad5e | |||
| 62e279e0c9 | |||
| f6f3c64f25 | |||
| b7a3f273a3 | |||
| 03e2e39185 | |||
| 9141e21a02 | |||
| b25bbfd33c | |||
| eebd38d17f | |||
| 2a7230298e | |||
| ebc73e1b24 | |||
| 6883382420 | |||
| fc9161b2d8 | |||
| 623fc60af2 |
@@ -5,3 +5,4 @@ composer.lock
|
||||
.phpunit.result.cache
|
||||
/var
|
||||
.php-cs-fixer.cache
|
||||
/dev/var
|
||||
|
||||
+25
-1
@@ -1,6 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
## [X.X.X] - XXXX-XX-XX
|
||||
## [x.x.x] - xxxx-xx-xx
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Create badges from objects or as standalone elements.
|
||||
|
||||
IMAGE HERE
|
||||

|
||||
|
||||
```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`):
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,3 +5,5 @@ services:
|
||||
dockerfile: Containerfile
|
||||
volumes:
|
||||
- ./:/code
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
+5
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 400 KiB |
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?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 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 TEAL;
|
||||
case VIOLET;
|
||||
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::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::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::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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{}
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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();
|
||||
$colour = strtoupper($colour);
|
||||
$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));
|
||||
|
||||
Reference in New Issue
Block a user