Skip to content

Commit 95a9c5e

Browse files
committed
feat(color): implement ColorResolver for flexible column color handling
1 parent bd615dc commit 95a9c5e

File tree

4 files changed

+290
-25
lines changed

4 files changed

+290
-25
lines changed

docs/content/2.essentials/2.customization.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,67 @@ public function board(Board $board): Board
116116
Customize column appearance and behavior:
117117

118118
```php
119+
use Filament\Support\Colors\Color;
120+
119121
Column::make('todo')
120122
->label('To Do')
121-
->color('gray') // gray, blue, red, green, amber, purple, pink
123+
->color('gray') // Supports multiple color formats
122124
->icon('heroicon-o-queue-list')
123125
```
124126

125-
Available colors: `gray`, `blue`, `red`, `green`, `amber`, `purple`, `pink`
127+
### Available Colors
128+
129+
Flowforge supports four flexible ways to set column colors:
130+
131+
#### 1. Semantic Colors (Filament registered)
132+
Use your application's theme colors:
133+
- `primary` - Your app's primary color
134+
- `secondary` - Secondary theme color
135+
- `success` - Green success color
136+
- `warning` - Yellow/amber warning color
137+
- `danger` - Red danger/error color
138+
- `info` - Blue informational color
139+
- `gray` - Neutral gray color
140+
141+
#### 2. Filament Color Constants
142+
Use Filament's Color class constants directly:
143+
```php
144+
use Filament\Support\Colors\Color;
145+
146+
Column::make('todo')->color(Color::Gray)
147+
Column::make('in_progress')->color(Color::Blue)
148+
Column::make('done')->color(Color::Green)
149+
```
150+
151+
Available constants: `Color::Slate`, `Color::Gray`, `Color::Zinc`, `Color::Neutral`, `Color::Stone`, `Color::Red`, `Color::Orange`, `Color::Amber`, `Color::Yellow`, `Color::Lime`, `Color::Green`, `Color::Emerald`, `Color::Teal`, `Color::Cyan`, `Color::Sky`, `Color::Blue`, `Color::Indigo`, `Color::Violet`, `Color::Purple`, `Color::Fuchsia`, `Color::Pink`, `Color::Rose`
152+
153+
#### 3. Tailwind CSS Color Names
154+
Use color names as strings (case-insensitive):
155+
```php
156+
Column::make('todo')->color('gray')
157+
Column::make('in_progress')->color('blue')
158+
Column::make('done')->color('green')
159+
```
160+
161+
#### 4. Custom Hex Colors
162+
Any valid hex color code:
163+
```php
164+
Column::make('urgent')->color('#ff0000')
165+
Column::make('normal')->color('#3b82f6')
166+
Column::make('completed')->color('#22c55e')
167+
```
168+
169+
### Complete Example
170+
```php
171+
use Filament\Support\Colors\Color;
172+
173+
->columns([
174+
Column::make('todo')
175+
->color('gray'), // Tailwind color name
176+
Column::make('in_progress')
177+
->color(Color::Blue), // Color constant
178+
Column::make('review')
179+
->color('primary'), // Semantic color
180+
Column::make('done')
181+
->color('#22c55e'), // Custom hex
182+
])

resources/views/livewire/column.blade.php

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
@props(['columnId', 'column', 'config'])
22

33
@php
4-
use Filament\Support\Colors\Color;use Filament\Support\Facades\FilamentColor;
4+
use Relaticle\Flowforge\Support\ColorResolver;
55
6-
$filamentColors = FilamentColor::getColors();
7-
$nativeColor = null;
6+
// Resolve the color once using our centralized resolver
7+
$resolvedColor = ColorResolver::resolve($column['color']);
8+
$isSemantic = ColorResolver::isSemantic($resolvedColor);
89
9-
if(filled($column['color']) && isset($filamentColors[$column['color']])) {
10-
$nativeColor = $column['color'];
11-
}elseif(filled($column['color'])){
12-
$color = Color::hex($column['color']);
13-
}else{
14-
$color = $filamentColors['primary'];
15-
}
10+
// For non-semantic colors, get the color array
11+
$colorShades = $isSemantic ? null : $resolvedColor;
1612
@endphp
1713

1814
<div
@@ -23,28 +19,34 @@ class="flowforge-column w-[300px] min-w-[300px] flex-shrink-0 border border-gray
2319
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-200">
2420
{{ $column['label'] }}
2521
</h3>
26-
@if($nativeColor)
22+
23+
{{-- Count Badge --}}
24+
@if($isSemantic)
25+
{{-- Use native Filament badge for semantic colors --}}
2726
<x-filament::badge
2827
tag="div"
29-
:color="$nativeColor"
28+
:color="$resolvedColor"
3029
class="ms-2"
3130
>
3231
{{ $column['total'] ?? (isset($column['items']) ? count($column['items']) : 0) }}
3332
</x-filament::badge>
34-
@else
33+
@elseif($colorShades)
34+
{{-- Custom badge for Color arrays --}}
3535
<div
3636
@style([
37-
"--light-bg-color: $color[50]",
38-
"--light-text-color: $color[700]",
39-
"--dark-bg-color: $color[600]",
40-
"--dark-text-color: $color[300]",
37+
Filament\Support\get_color_css_variables($resolvedColor, shades: [50, 300, 600, 700])
4138
])
4239
@class([
43-
'ms-2 items-center border px-2 py-0.5 rounded-md text-xs font-semibold',
44-
"bg-[var(--light-bg-color)] dark:bg-[var(--dark-bg-color)]/20",
45-
"text-[var(--light-text-color)] dark:text-[var(--dark-text-color)]",
46-
'border-[var(--light-text-color)]/30 dark:border-[var(--dark-text-color)]/30',
47-
])>
40+
'ms-2 items-center border px-2 py-0.5 rounded-md text-xs font-semibold',
41+
'bg-custom-50 dark:bg-custom-600/20',
42+
'text-custom-700 dark:text-custom-300',
43+
'border-custom-700/30 dark:border-custom-300/30',
44+
])>
45+
{{ $column['total'] ?? (isset($column['items']) ? count($column['items']) : 0) }}
46+
</div>
47+
@else
48+
{{-- Fallback: simple gray badge if no color --}}
49+
<div class="ms-2 items-center border px-2 py-0.5 rounded-md text-xs font-semibold bg-gray-50 dark:bg-gray-600/20 text-gray-700 dark:text-gray-300 border-gray-700/30 dark:border-gray-300/30">
4850
{{ $column['total'] ?? (isset($column['items']) ? count($column['items']) : 0) }}
4951
</div>
5052
@endif
@@ -114,4 +116,4 @@ class="text-xs text-primary-600 dark:text-primary-400 flex items-center justify-
114116
/>
115117
@endif
116118
</div>
117-
</div>
119+
</div>

src/Support/ColorResolver.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Relaticle\Flowforge\Support;
6+
7+
use Filament\Support\Colors\Color;
8+
use Filament\Support\Facades\FilamentColor;
9+
10+
/**
11+
* Resolves color values from various formats into a consistent structure.
12+
*
13+
* Supports:
14+
* - Semantic colors (primary, danger, etc.)
15+
* - Color constants (Color::Red arrays)
16+
* - Tailwind color names (red, blue, etc.)
17+
* - Hex colors (#ff0000)
18+
*/
19+
class ColorResolver
20+
{
21+
/**
22+
* Resolve a color value to either a semantic name or color array.
23+
*
24+
* @return string|array{50: string, 100: string, 200: string, 300: string, 400: string, 500: string, 600: string, 700: string, 800: string, 900: string, 950: string}|null
25+
*/
26+
public static function resolve(mixed $color): string | array | null
27+
{
28+
if (blank($color)) {
29+
return null;
30+
}
31+
32+
// If it's already a Color constant array (has numeric keys for shades)
33+
if (is_array($color) && isset($color[500])) {
34+
return $color;
35+
}
36+
37+
// If it's a string, check various formats
38+
if (is_string($color)) {
39+
// Check if it's a registered Filament semantic color
40+
$filamentColors = FilamentColor::getColors();
41+
if (isset($filamentColors[$color])) {
42+
return $color; // Return semantic name for native Filament badge
43+
}
44+
45+
// Try to get Tailwind color by name (case-insensitive)
46+
$tailwindColor = self::getTailwindColor($color);
47+
if ($tailwindColor !== null) {
48+
return $tailwindColor;
49+
}
50+
51+
// Try to parse as hex color (validate format first)
52+
if (str_starts_with($color, '#') && preg_match('/^#[0-9A-Fa-f]{6}$/', $color)) {
53+
try {
54+
return Color::hex($color);
55+
} catch (\Exception) {
56+
// Invalid hex, will fall through to null
57+
}
58+
}
59+
}
60+
61+
return null;
62+
}
63+
64+
/**
65+
* Check if the resolved color is a semantic Filament color.
66+
*/
67+
public static function isSemantic(mixed $color): bool
68+
{
69+
return is_string($color) && isset(FilamentColor::getColors()[$color]);
70+
}
71+
72+
/**
73+
* Get a Tailwind color constant by name.
74+
*/
75+
private static function getTailwindColor(string $name): ?array
76+
{
77+
$colorName = ucfirst(strtolower($name));
78+
79+
// Use reflection to check if the Color class has this constant
80+
if (defined("Filament\\Support\\Colors\\Color::{$colorName}")) {
81+
return constant("Filament\\Support\\Colors\\Color::{$colorName}");
82+
}
83+
84+
return null;
85+
}
86+
}

tests/Feature/ColumnColorTest.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Filament\Support\Colors\Color;
6+
use Relaticle\Flowforge\Column;
7+
use Relaticle\Flowforge\Support\ColorResolver;
8+
9+
describe('Column Color Handling', function () {
10+
it('can set semantic colors on columns', function () {
11+
$column = Column::make('test')
12+
->color('primary');
13+
14+
expect($column->getColor())->toBe('primary');
15+
});
16+
17+
it('can set tailwind colors on columns by name', function () {
18+
$colors = ['red', 'blue', 'green', 'amber', 'purple', 'pink', 'gray'];
19+
20+
foreach ($colors as $color) {
21+
$column = Column::make('test')->color($color);
22+
expect($column->getColor())->toBe($color);
23+
}
24+
});
25+
26+
it('can set Color constants directly', function () {
27+
$column = Column::make('test')
28+
->color(Color::Red);
29+
30+
$color = $column->getColor();
31+
expect($color)->toBeArray()
32+
->and($color)->toHaveKey(50)
33+
->and($color)->toHaveKey(500)
34+
->and($color)->toHaveKey(900);
35+
});
36+
37+
it('can set hex colors on columns', function () {
38+
$column = Column::make('test')
39+
->color('#ff0000');
40+
41+
expect($column->getColor())->toBe('#ff0000');
42+
});
43+
44+
it('can set default color on columns', function () {
45+
$column = Column::make('test')
46+
->defaultColor('gray');
47+
48+
expect($column->getColor())->toBe('gray');
49+
});
50+
51+
it('uses default color when no color is set', function () {
52+
$column = Column::make('test')
53+
->defaultColor('info');
54+
55+
expect($column->getColor())->toBe('info');
56+
});
57+
58+
it('prefers explicit color over default color', function () {
59+
$column = Column::make('test')
60+
->defaultColor('gray')
61+
->color('primary');
62+
63+
expect($column->getColor())->toBe('primary');
64+
});
65+
});
66+
67+
describe('ColorResolver', function () {
68+
it('resolves semantic colors', function () {
69+
expect(ColorResolver::resolve('primary'))->toBe('primary')
70+
->and(ColorResolver::resolve('danger'))->toBe('danger')
71+
->and(ColorResolver::resolve('success'))->toBe('success')
72+
->and(ColorResolver::isSemantic('primary'))->toBeTrue()
73+
->and(ColorResolver::isSemantic('danger'))->toBeTrue();
74+
});
75+
76+
it('resolves Tailwind color names', function () {
77+
$redColor = ColorResolver::resolve('red');
78+
expect($redColor)->toBeArray()
79+
->and($redColor)->toHaveKey(500)
80+
->and($redColor[500])->toContain('oklch');
81+
82+
$blueColor = ColorResolver::resolve('blue');
83+
expect($blueColor)->toBeArray()
84+
->and($blueColor)->toHaveKey(500);
85+
});
86+
87+
it('resolves Color constants', function () {
88+
$color = ColorResolver::resolve(Color::Green);
89+
expect($color)->toBeArray()
90+
->and($color)->toBe(Color::Green)
91+
->and($color)->toHaveKey(500);
92+
});
93+
94+
it('resolves hex colors', function () {
95+
$color = ColorResolver::resolve('#ff0000');
96+
expect($color)->toBeArray()
97+
->and($color)->toHaveKey(500);
98+
});
99+
100+
it('handles invalid colors gracefully', function () {
101+
expect(ColorResolver::resolve('invalid-color'))->toBeNull()
102+
->and(ColorResolver::resolve('not-a-color'))->toBeNull()
103+
->and(ColorResolver::resolve('#gggggg'))->toBeNull()
104+
->and(ColorResolver::resolve(''))->toBeNull()
105+
->and(ColorResolver::resolve(null))->toBeNull();
106+
});
107+
108+
it('is case-insensitive for Tailwind colors', function () {
109+
expect(ColorResolver::resolve('RED'))->toBeArray()
110+
->and(ColorResolver::resolve('Red'))->toBeArray()
111+
->and(ColorResolver::resolve('red'))->toBeArray();
112+
});
113+
114+
it('correctly identifies semantic vs non-semantic colors', function () {
115+
expect(ColorResolver::isSemantic('primary'))->toBeTrue()
116+
->and(ColorResolver::isSemantic(Color::Red))->toBeFalse()
117+
->and(ColorResolver::isSemantic('#ff0000'))->toBeFalse()
118+
->and(ColorResolver::isSemantic('red'))->toBeFalse();
119+
});
120+
});

0 commit comments

Comments
 (0)