standard colours with noce UI and lasyout changes
This commit is contained in:
@@ -46,11 +46,13 @@ final class HelpdeskDatabaseSeeder extends Seeder
|
||||
|
||||
foreach ([
|
||||
['name' => 'New', 'slug' => 'new', 'color' => 'blue', 'sort_order' => 5, 'is_closed' => false, 'is_default' => true],
|
||||
['name' => 'Open', 'slug' => 'open', 'color' => 'blue', 'sort_order' => 10, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'amber', 'sort_order' => 20, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'Stalled', 'slug' => 'stalled', 'color' => 'red', 'sort_order' => 30, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'On-Hold', 'slug' => 'on-hold', 'color' => 'purple', 'sort_order' => 40, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'Closed', 'slug' => 'closed', 'color' => 'green', 'sort_order' => 90, 'is_closed' => true, 'is_default' => false],
|
||||
['name' => 'Open', 'slug' => 'open', 'color' => 'green', 'sort_order' => 10, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'In Progress', 'slug' => 'in-progress', 'color' => 'indigo', 'sort_order' => 20, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'Waiting Customer', 'slug' => 'waiting-customer', 'color' => 'amber', 'sort_order' => 30, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'Waiting Third Party', 'slug' => 'waiting-third-party', 'color' => 'purple', 'sort_order' => 40, 'is_closed' => false, 'is_default' => false],
|
||||
['name' => 'Resolved', 'slug' => 'resolved', 'color' => 'green', 'sort_order' => 80, 'is_closed' => true, 'is_default' => false],
|
||||
['name' => 'Closed', 'slug' => 'closed', 'color' => 'gray', 'sort_order' => 90, 'is_closed' => true, 'is_default' => false],
|
||||
['name' => 'Cancelled', 'slug' => 'cancelled', 'color' => 'red', 'sort_order' => 100, 'is_closed' => true, 'is_default' => false],
|
||||
] as $row) {
|
||||
Status::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||
}
|
||||
@@ -64,9 +66,11 @@ final class HelpdeskDatabaseSeeder extends Seeder
|
||||
|
||||
Priority::query()->where('is_default', true)->update(['is_default' => false]);
|
||||
foreach ([
|
||||
['name' => 'High', 'slug' => 'high', 'color' => 'red', 'sort_order' => 10, 'is_default' => false],
|
||||
['name' => 'Medium', 'slug' => 'medium', 'color' => 'amber', 'sort_order' => 20, 'is_default' => true],
|
||||
['name' => 'Low', 'slug' => 'low', 'color' => 'green', 'sort_order' => 30, 'is_default' => false],
|
||||
['name' => 'Low', 'slug' => 'low', 'color' => 'gray', 'sort_order' => 10, 'is_default' => false],
|
||||
['name' => 'Normal', 'slug' => 'normal', 'color' => 'blue', 'sort_order' => 20, 'is_default' => true],
|
||||
['name' => 'High', 'slug' => 'high', 'color' => 'amber', 'sort_order' => 30, 'is_default' => false],
|
||||
['name' => 'Urgent', 'slug' => 'urgent', 'color' => 'red', 'sort_order' => 40, 'is_default' => false],
|
||||
['name' => 'Critical', 'slug' => 'critical', 'color' => 'red', 'sort_order' => 50, 'is_default' => false],
|
||||
] as $row) {
|
||||
Priority::query()->updateOrCreate(['slug' => $row['slug']], $row);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ final class SettingsManager extends Component
|
||||
$data = $this->validate([
|
||||
'statusForm.name' => ['required', 'string', 'max:80'],
|
||||
'statusForm.slug' => ['nullable', 'string', 'max:100'],
|
||||
'statusForm.color' => ['required', 'string', Rule::in(Color::keys())],
|
||||
'statusForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'statusForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'statusForm.is_closed' => ['boolean'],
|
||||
'statusForm.is_default' => ['boolean'],
|
||||
@@ -170,7 +170,7 @@ final class SettingsManager extends Component
|
||||
$data = $this->validate([
|
||||
'priorityForm.name' => ['required', 'string', 'max:80'],
|
||||
'priorityForm.slug' => ['nullable', 'string', 'max:100'],
|
||||
'priorityForm.color' => ['required', 'string', Rule::in(Color::keys())],
|
||||
'priorityForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'priorityForm.sort_order' => ['required', 'integer', 'min:0'],
|
||||
'priorityForm.is_default' => ['boolean'],
|
||||
])['priorityForm'];
|
||||
@@ -212,7 +212,7 @@ final class SettingsManager extends Component
|
||||
'categoryForm.company_id' => ['nullable', 'integer'],
|
||||
'categoryForm.name' => ['required', 'string', 'max:120'],
|
||||
'categoryForm.slug' => ['nullable', 'string', 'max:140'],
|
||||
'categoryForm.color' => ['required', 'string', Rule::in(Color::keys())],
|
||||
'categoryForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'categoryForm.description' => ['nullable', 'string'],
|
||||
'categoryForm.is_default' => ['boolean'],
|
||||
])['categoryForm'];
|
||||
@@ -254,7 +254,7 @@ final class SettingsManager extends Component
|
||||
'teamForm.company_id' => ['nullable', 'integer'],
|
||||
'teamForm.name' => ['required', 'string', 'max:120'],
|
||||
'teamForm.slug' => ['nullable', 'string', 'max:140'],
|
||||
'teamForm.color' => ['required', 'string', Rule::in(Color::keys())],
|
||||
'teamForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'teamForm.description' => ['nullable', 'string'],
|
||||
'teamForm.is_default' => ['boolean'],
|
||||
])['teamForm'];
|
||||
@@ -297,7 +297,7 @@ final class SettingsManager extends Component
|
||||
'projectForm.team_id' => ['nullable', 'integer', 'exists:helpdesk_teams,id'],
|
||||
'projectForm.title' => ['required', 'string', 'max:160'],
|
||||
'projectForm.slug' => ['nullable', 'string', 'max:180'],
|
||||
'projectForm.color' => ['required', 'string', Rule::in(Color::keys())],
|
||||
'projectForm.color' => ['required', 'string', Color::validationRule()],
|
||||
'projectForm.description' => ['nullable', 'string'],
|
||||
'projectForm.is_default' => ['boolean'],
|
||||
])['projectForm'];
|
||||
@@ -644,7 +644,7 @@ final class SettingsManager extends Component
|
||||
'tagForm.company_id' => ['nullable', 'integer'],
|
||||
'tagForm.name' => ['required', 'string', 'max:80'],
|
||||
'tagForm.slug' => ['nullable', 'string', 'max:100'],
|
||||
'tagForm.color' => ['required', 'string', Rule::in(Color::keys())],
|
||||
'tagForm.color' => ['required', 'string', Color::validationRule()],
|
||||
])['tagForm'];
|
||||
$data['slug'] = $this->slug($data['slug'] ?: $data['name']);
|
||||
$this->persistSettingModel(Tag::class, $this->tagForm['id'], $data);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
@props([
|
||||
'color' => 'gray',
|
||||
'label' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$background = \Modules\Helpdesk\Support\Color::css($color);
|
||||
$foreground = \Modules\Helpdesk\Support\Color::text($color);
|
||||
@endphp
|
||||
|
||||
<span
|
||||
{{ $attributes->class('inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold text-white shadow-sm ring-1 ring-white/20') }}
|
||||
style="background-color: {{ \Modules\Helpdesk\Support\Color::css($color) }};"
|
||||
{{ $attributes->class('inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold shadow-sm ring-1 ring-zinc-900/10') }}
|
||||
style="background-color: {{ $background }}; color: {{ $foreground }};"
|
||||
>
|
||||
{{ $slot }}
|
||||
{{ $label ?? $slot }}
|
||||
</span>
|
||||
|
||||
@@ -4,31 +4,68 @@
|
||||
'options' => [],
|
||||
])
|
||||
|
||||
<div {{ $attributes->class('space-y-2') }}>
|
||||
@php
|
||||
$currentValue = data_get($this, $model, 'gray');
|
||||
$currentHex = \Modules\Helpdesk\Support\Color::css($currentValue);
|
||||
$currentText = \Modules\Helpdesk\Support\Color::text($currentValue);
|
||||
$currentLabel = \Modules\Helpdesk\Support\Color::label($currentValue);
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->class('space-y-3') }}>
|
||||
<label class="block text-sm font-semibold text-zinc-700 dark:text-zinc-200">
|
||||
{{ $label }}
|
||||
</label>
|
||||
|
||||
<select
|
||||
wire:model="{{ $model }}"
|
||||
class="block h-10 w-full rounded-xl border border-zinc-300 bg-white px-3 text-sm font-semibold text-zinc-900 shadow-sm outline-none transition-colors duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
>
|
||||
@foreach ($options as $key => $option)
|
||||
<option value="{{ $key }}" style="color: {{ $option['hex'] }};">
|
||||
● {{ $option['name'] }} — {{ $option['hex'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<div class="flex flex-wrap gap-1.5" aria-label="Available Helpdesk colours">
|
||||
@foreach ($options as $key => $option)
|
||||
<span
|
||||
title="{{ $option['name'] }} {{ $option['hex'] }}"
|
||||
class="inline-flex h-5 min-w-5 items-center justify-center rounded-full border border-white/70 shadow-sm ring-1 ring-zinc-900/10"
|
||||
style="background-color: {{ $option['hex'] }}"
|
||||
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">
|
||||
<div class="space-y-2">
|
||||
<select
|
||||
wire:model.live="{{ $model }}"
|
||||
class="block h-10 w-full rounded-xl border border-zinc-300 bg-white px-3 text-sm font-semibold text-zinc-900 shadow-sm outline-none transition-colors duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/30 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100"
|
||||
>
|
||||
<span class="sr-only">{{ $option['name'] }}</span>
|
||||
</span>
|
||||
@endforeach
|
||||
@foreach ($options as $key => $option)
|
||||
<option value="{{ $key }}" style="color: {{ $option['hex'] }};">
|
||||
● {{ $option['name'] }} — {{ $option['hex'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<div class="flex flex-wrap gap-1.5" aria-label="Available Helpdesk colours">
|
||||
@foreach ($options as $key => $option)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="$set('{{ $model }}', '{{ $key }}')"
|
||||
title="{{ $option['name'] }} {{ $option['hex'] }}"
|
||||
class="inline-flex h-6 min-w-6 items-center justify-center rounded-full border border-white/80 shadow-sm ring-1 ring-zinc-900/10 transition hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500/40"
|
||||
style="background-color: {{ $option['hex'] }}"
|
||||
>
|
||||
<span class="sr-only">Use {{ $option['name'] }}</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="inline-flex min-w-28 items-center justify-center rounded-full px-3 py-2 text-xs font-bold shadow-sm ring-1 ring-zinc-900/10"
|
||||
style="background-color: {{ $currentHex }}; color: {{ $currentText }};"
|
||||
>
|
||||
{{ $currentLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-[auto_minmax(0,1fr)] md:items-center">
|
||||
<input
|
||||
type="color"
|
||||
value="{{ $currentHex }}"
|
||||
wire:change="$set('{{ $model }}', $event.target.value)"
|
||||
class="h-10 w-16 cursor-pointer rounded-xl border border-zinc-300 bg-white p-1 shadow-sm dark:border-zinc-700 dark:bg-zinc-950"
|
||||
aria-label="Choose a custom hex colour"
|
||||
/>
|
||||
|
||||
<flux:input
|
||||
wire:model.live.debounce.500ms="{{ $model }}"
|
||||
label="Custom hex colour"
|
||||
placeholder="#ffffff"
|
||||
description="Use the standard palette where possible. Custom hex values must use six characters, for example #ffffff."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Support;
|
||||
|
||||
use Closure;
|
||||
|
||||
final class Color
|
||||
{
|
||||
/**
|
||||
@@ -41,23 +43,37 @@ final class Color
|
||||
];
|
||||
|
||||
/**
|
||||
* Resolve a stored colour key or supported hex value into a CSS colour.
|
||||
* Resolve a stored palette key or supported custom hex value into a CSS colour.
|
||||
*/
|
||||
public static function css(?string $color): string
|
||||
{
|
||||
$resolved = self::normalise($color);
|
||||
|
||||
if (str_starts_with($resolved, '#')) {
|
||||
if (self::isHex($resolved)) {
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
return self::PALETTE[$resolved] ?? self::PALETTE['gray'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a readable foreground colour for a badge background.
|
||||
*/
|
||||
public static function text(?string $color): string
|
||||
{
|
||||
$hex = ltrim(self::css($color), '#');
|
||||
$red = hexdec(substr($hex, 0, 2));
|
||||
$green = hexdec(substr($hex, 2, 2));
|
||||
$blue = hexdec(substr($hex, 4, 2));
|
||||
$luminance = (($red * 299) + ($green * 587) + ($blue * 114)) / 1000;
|
||||
|
||||
return $luminance >= 150 ? '#111827' : '#ffffff';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return approved palette options for settings dropdowns.
|
||||
*
|
||||
* @return array<string, array{name: string, hex: string}>
|
||||
* @return array<string, array{name: string, hex: string, foreground: string}>
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
@@ -66,6 +82,7 @@ final class Color
|
||||
$name => [
|
||||
'name' => str($name)->headline()->toString(),
|
||||
'hex' => $hex,
|
||||
'foreground' => self::text($hex),
|
||||
],
|
||||
])
|
||||
->all();
|
||||
@@ -82,16 +99,60 @@ final class Color
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise legacy aliases and supported hex colours.
|
||||
* Normalise legacy aliases and supported custom hex colours.
|
||||
*/
|
||||
public static function normalise(?string $color): string
|
||||
{
|
||||
$value = strtolower(trim((string) $color));
|
||||
|
||||
if (preg_match('/^#[a-f0-9]{6}$/', $value) === 1) {
|
||||
if (self::isHex($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::ALIASES[$value] ?? $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the value is a supported six-character hex colour.
|
||||
*/
|
||||
public static function isHex(?string $color): bool
|
||||
{
|
||||
return preg_match('/^#[a-f0-9]{6}$/', strtolower(trim((string) $color))) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a stored colour is an approved palette key or custom hex colour.
|
||||
*/
|
||||
public static function isAllowed(?string $color): bool
|
||||
{
|
||||
$resolved = self::normalise($color);
|
||||
|
||||
return self::isHex($resolved) || in_array($resolved, self::keys(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Laravel validation rule allowing approved palette keys and six-character hex values.
|
||||
*/
|
||||
public static function validationRule(): Closure
|
||||
{
|
||||
return static function (string $attribute, mixed $value, Closure $fail): void {
|
||||
if (! self::isAllowed(is_string($value) ? $value : null)) {
|
||||
$fail('The '.$attribute.' must be an approved Helpdesk colour or a valid hex colour such as #ffffff.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable label for palette and custom hex values.
|
||||
*/
|
||||
public static function label(?string $color): string
|
||||
{
|
||||
$resolved = self::normalise($color);
|
||||
|
||||
if (self::isHex($resolved)) {
|
||||
return strtoupper($resolved);
|
||||
}
|
||||
|
||||
return str($resolved ?: 'gray')->headline()->toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ declare(strict_types=1);
|
||||
namespace Modules\Helpdesk\Tests\Unit;
|
||||
|
||||
use Modules\Helpdesk\Support\Color;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ColorPaletteTest extends TestCase
|
||||
{
|
||||
public function test_standard_palette_uses_clear_enterprise_colours(): void
|
||||
/** @test */
|
||||
public function palette_colours_resolve_to_standard_hex_values(): void
|
||||
{
|
||||
self::assertSame('#dc2626', Color::css('red'));
|
||||
self::assertSame('#16a34a', Color::css('green'));
|
||||
@@ -17,7 +18,8 @@ final class ColorPaletteTest extends TestCase
|
||||
self::assertSame('#6b7280', Color::css('gray'));
|
||||
}
|
||||
|
||||
public function test_legacy_colour_names_are_normalised(): void
|
||||
/** @test */
|
||||
public function legacy_colour_aliases_are_normalised(): void
|
||||
{
|
||||
self::assertSame('red', Color::normalise('rose'));
|
||||
self::assertSame('green', Color::normalise('emerald'));
|
||||
@@ -25,9 +27,21 @@ final class ColorPaletteTest extends TestCase
|
||||
self::assertSame('gray', Color::normalise('zinc'));
|
||||
}
|
||||
|
||||
public function test_hex_values_can_still_be_resolved_for_display(): void
|
||||
/** @test */
|
||||
public function custom_hex_colours_are_supported_and_validated(): void
|
||||
{
|
||||
self::assertSame('#ffffff', Color::css('#ffffff'));
|
||||
self::assertSame('#000000', Color::css('#000000'));
|
||||
self::assertTrue(Color::isAllowed('#ffffff'));
|
||||
self::assertFalse(Color::isAllowed('#fff'));
|
||||
self::assertFalse(Color::isAllowed('brown'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function readable_text_colour_is_calculated_for_badge_backgrounds(): void
|
||||
{
|
||||
self::assertSame('#111827', Color::text('#ffffff'));
|
||||
self::assertSame('#ffffff', Color::text('#000000'));
|
||||
self::assertSame('#ffffff', Color::text('red'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,3 +204,17 @@
|
||||
- Added a migration to clean existing database lookup colour values to the approved palette.
|
||||
- Updated Helpdesk seed data, feature coverage and unit coverage for the standardised colour behaviour.
|
||||
- Preserved append-only changelog history.
|
||||
|
||||
- version: 1.9.49
|
||||
date: 2026-06-23
|
||||
type: feature
|
||||
summary: Add Helpdesk custom hex colours, contrast-safe badges and standardised seeded workflows.
|
||||
changes:
|
||||
- Added support for six-character custom hex colours such as #ffffff while keeping the approved WMS palette as the default path.
|
||||
- Added colour picker, custom hex input, clickable palette swatches and live preview badge behaviour to Helpdesk colour selectors.
|
||||
- Added contrast-safe badge text calculation for light and dark colour backgrounds.
|
||||
- Added a reusable Helpdesk badge component so lists, Kanban cards, ticket detail pages, reports and notifications can render colours consistently.
|
||||
- Standardised seeded ticket statuses for New, Open, In Progress, Waiting Customer, Waiting Third Party, Resolved, Closed and Cancelled.
|
||||
- Standardised seeded ticket priorities for Low, Normal, High, Urgent and Critical.
|
||||
- Extended unit and file-level regression coverage for custom hex colours, colour selector UI and contrast-safe badge rendering.
|
||||
- Preserved append-only changelog history.
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"Modules\\Helpdesk\\Providers\\HelpdeskServiceProvider"
|
||||
],
|
||||
"files": [],
|
||||
"version": "1.9.48"
|
||||
"version": "1.9.49"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user