feat(#91): add multi color and palette support

This commit is contained in:
Benjamin Nussbaum 2025-11-22 16:43:33 +01:00
parent 61b9ff56e0
commit 568bd69fea
19 changed files with 1696 additions and 185 deletions

View file

@ -1,26 +1,43 @@
<?php
use App\Models\DeviceModel;
use App\Models\DevicePalette;
use Livewire\Volt\Component;
new class extends Component {
new class extends Component
{
public $deviceModels;
public $devicePalettes;
public $name;
public $label;
public $description;
public $width;
public $height;
public $colors;
public $bit_depth;
public $scale_factor = 1.0;
public $rotation = 0;
public $mime_type = 'image/png';
public $offset_x = 0;
public $offset_y = 0;
public $published_at;
public $palette_id;
protected $rules = [
'name' => 'required|string|max:255|unique:device_models,name',
'label' => 'required|string|max:255',
@ -40,62 +57,58 @@ new class extends Component {
public function mount()
{
$this->deviceModels = DeviceModel::all();
$this->devicePalettes = DevicePalette::all();
return view('livewire.device-models.index');
}
public function createDeviceModel(): void
{
$this->validate();
DeviceModel::create([
'name' => $this->name,
'label' => $this->label,
'description' => $this->description,
'width' => $this->width,
'height' => $this->height,
'colors' => $this->colors,
'bit_depth' => $this->bit_depth,
'scale_factor' => $this->scale_factor,
'rotation' => $this->rotation,
'mime_type' => $this->mime_type,
'offset_x' => $this->offset_x,
'offset_y' => $this->offset_y,
'published_at' => $this->published_at,
]);
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at']);
\Flux::modal('create-device-model')->close();
$this->deviceModels = DeviceModel::all();
session()->flash('message', 'Device model created successfully.');
}
public $editingDeviceModelId;
public function editDeviceModel(DeviceModel $deviceModel): void
public $viewingDeviceModelId;
public function openDeviceModelModal(?string $deviceModelId = null, bool $viewOnly = false): void
{
$this->editingDeviceModelId = $deviceModel->id;
$this->name = $deviceModel->name;
$this->label = $deviceModel->label;
$this->description = $deviceModel->description;
$this->width = $deviceModel->width;
$this->height = $deviceModel->height;
$this->colors = $deviceModel->colors;
$this->bit_depth = $deviceModel->bit_depth;
$this->scale_factor = $deviceModel->scale_factor;
$this->rotation = $deviceModel->rotation;
$this->mime_type = $deviceModel->mime_type;
$this->offset_x = $deviceModel->offset_x;
$this->offset_y = $deviceModel->offset_y;
$this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i');
if ($deviceModelId) {
$deviceModel = DeviceModel::findOrFail($deviceModelId);
if ($viewOnly) {
$this->viewingDeviceModelId = $deviceModel->id;
$this->editingDeviceModelId = null;
} else {
$this->editingDeviceModelId = $deviceModel->id;
$this->viewingDeviceModelId = null;
}
$this->name = $deviceModel->name;
$this->label = $deviceModel->label;
$this->description = $deviceModel->description;
$this->width = $deviceModel->width;
$this->height = $deviceModel->height;
$this->colors = $deviceModel->colors;
$this->bit_depth = $deviceModel->bit_depth;
$this->scale_factor = $deviceModel->scale_factor;
$this->rotation = $deviceModel->rotation;
$this->mime_type = $deviceModel->mime_type;
$this->offset_x = $deviceModel->offset_x;
$this->offset_y = $deviceModel->offset_y;
$this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i');
$this->palette_id = $deviceModel->palette_id;
} else {
$this->editingDeviceModelId = null;
$this->viewingDeviceModelId = null;
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id']);
$this->mime_type = 'image/png';
$this->scale_factor = 1.0;
$this->rotation = 0;
$this->offset_x = 0;
$this->offset_y = 0;
}
}
public function updateDeviceModel(): void
public function saveDeviceModel(): void
{
$deviceModel = DeviceModel::findOrFail($this->editingDeviceModelId);
$this->validate([
'name' => 'required|string|max:255|unique:device_models,name,' . $deviceModel->id,
$rules = [
'name' => 'required|string|max:255',
'label' => 'required|string|max:255',
'description' => 'required|string',
'width' => 'required|integer|min:1',
@ -108,38 +121,96 @@ new class extends Component {
'offset_x' => 'required|integer',
'offset_y' => 'required|integer',
'published_at' => 'nullable|date',
]);
'palette_id' => 'nullable|exists:device_palettes,id',
];
$deviceModel->update([
'name' => $this->name,
'label' => $this->label,
'description' => $this->description,
'width' => $this->width,
'height' => $this->height,
'colors' => $this->colors,
'bit_depth' => $this->bit_depth,
'scale_factor' => $this->scale_factor,
'rotation' => $this->rotation,
'mime_type' => $this->mime_type,
'offset_x' => $this->offset_x,
'offset_y' => $this->offset_y,
'published_at' => $this->published_at,
]);
if ($this->editingDeviceModelId) {
$rules['name'] = 'required|string|max:255|unique:device_models,name,'.$this->editingDeviceModelId;
} else {
$rules['name'] = 'required|string|max:255|unique:device_models,name';
}
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'editingDeviceModelId']);
\Flux::modal('edit-device-model-' . $deviceModel->id)->close();
$this->validate($rules);
if ($this->editingDeviceModelId) {
$deviceModel = DeviceModel::findOrFail($this->editingDeviceModelId);
$deviceModel->update([
'name' => $this->name,
'label' => $this->label,
'description' => $this->description,
'width' => $this->width,
'height' => $this->height,
'colors' => $this->colors,
'bit_depth' => $this->bit_depth,
'scale_factor' => $this->scale_factor,
'rotation' => $this->rotation,
'mime_type' => $this->mime_type,
'offset_x' => $this->offset_x,
'offset_y' => $this->offset_y,
'published_at' => $this->published_at,
'palette_id' => $this->palette_id ?: null,
]);
$message = 'Device model updated successfully.';
} else {
DeviceModel::create([
'name' => $this->name,
'label' => $this->label,
'description' => $this->description,
'width' => $this->width,
'height' => $this->height,
'colors' => $this->colors,
'bit_depth' => $this->bit_depth,
'scale_factor' => $this->scale_factor,
'rotation' => $this->rotation,
'mime_type' => $this->mime_type,
'offset_x' => $this->offset_x,
'offset_y' => $this->offset_y,
'published_at' => $this->published_at,
'palette_id' => $this->palette_id ?: null,
'source' => 'manual',
]);
$message = 'Device model created successfully.';
}
$this->reset(['name', 'label', 'description', 'width', 'height', 'colors', 'bit_depth', 'scale_factor', 'rotation', 'mime_type', 'offset_x', 'offset_y', 'published_at', 'palette_id', 'editingDeviceModelId', 'viewingDeviceModelId']);
Flux::modal('device-model-modal')->close();
$this->deviceModels = DeviceModel::all();
session()->flash('message', 'Device model updated successfully.');
session()->flash('message', $message);
}
public function deleteDeviceModel(DeviceModel $deviceModel): void
public function deleteDeviceModel(string $deviceModelId): void
{
$deviceModel = DeviceModel::findOrFail($deviceModelId);
$deviceModel->delete();
$this->deviceModels = DeviceModel::all();
session()->flash('message', 'Device model deleted successfully.');
}
public function duplicateDeviceModel(string $deviceModelId): void
{
$deviceModel = DeviceModel::findOrFail($deviceModelId);
$this->editingDeviceModelId = null;
$this->viewingDeviceModelId = null;
$this->name = $deviceModel->name.' (Copy)';
$this->label = $deviceModel->label;
$this->description = $deviceModel->description;
$this->width = $deviceModel->width;
$this->height = $deviceModel->height;
$this->colors = $deviceModel->colors;
$this->bit_depth = $deviceModel->bit_depth;
$this->scale_factor = $deviceModel->scale_factor;
$this->rotation = $deviceModel->rotation;
$this->mime_type = $deviceModel->mime_type;
$this->offset_x = $deviceModel->offset_x;
$this->offset_y = $deviceModel->offset_y;
$this->published_at = $deviceModel->published_at?->format('Y-m-d\TH:i');
$this->palette_id = $deviceModel->palette_id;
$this->js('Flux.modal("device-model-modal").show()');
}
}
?>
@ -148,10 +219,19 @@ new class extends Component {
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Device Models</h2>
{{-- <flux:modal.trigger name="create-device-model">--}}
{{-- <flux:button icon="plus" variant="primary">Add Device Model</flux:button>--}}
{{-- </flux:modal.trigger>--}}
<div class="flex items-center space-x-2">
<h2 class="text-2xl font-semibold dark:text-gray-100">Device Models</h2>
<flux:dropdown>
<flux:button icon="chevron-down" variant="ghost"></flux:button>
<flux:menu>
<flux:menu.item href="{{ route('devices') }}">Devices</flux:menu.item>
<flux:menu.item href="{{ route('device-palettes.index') }}">Device Palettes</flux:menu.item>
</flux:menu>
</flux:dropdown>
</div>
<flux:modal.trigger name="device-model-modal">
<flux:button wire:click="openDeviceModelModal()" icon="plus" variant="primary">Add Device Model</flux:button>
</flux:modal.trigger>
</div>
@if (session()->has('message'))
<div class="mb-4">
@ -164,157 +244,104 @@ new class extends Component {
</div>
@endif
<flux:modal name="create-device-model" class="md:w-96">
<flux:modal name="device-model-modal" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Add Device Model</flux:heading>
<flux:heading size="lg">
@if ($viewingDeviceModelId)
View Device Model
@elseif ($editingDeviceModelId)
Edit Device Model
@else
Add Device Model
@endif
</flux:heading>
</div>
<form wire:submit="createDeviceModel">
<form wire:submit="saveDeviceModel">
<div class="mb-4">
<flux:input label="Name" wire:model="name" id="name" class="block mt-1 w-full" type="text"
name="name" autofocus/>
name="name" autofocus :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="mb-4">
<flux:input label="Label" wire:model="label" id="label" class="block mt-1 w-full"
type="text"
name="label"/>
name="label" :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="mb-4">
<flux:input label="Description" wire:model="description" id="description"
class="block mt-1 w-full" name="description"/>
class="block mt-1 w-full" name="description" :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Width" wire:model="width" id="width" class="block mt-1 w-full"
type="number"
name="width"/>
name="width" :disabled="(bool) $viewingDeviceModelId"/>
<flux:input label="Height" wire:model="height" id="height" class="block mt-1 w-full"
type="number"
name="height"/>
name="height" :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Colors" wire:model="colors" id="colors" class="block mt-1 w-full"
type="number"
name="colors"/>
name="colors" :disabled="(bool) $viewingDeviceModelId"/>
<flux:input label="Bit Depth" wire:model="bit_depth" id="bit_depth"
class="block mt-1 w-full" type="number"
name="bit_depth"/>
name="bit_depth" :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Scale Factor" wire:model="scale_factor" id="scale_factor"
class="block mt-1 w-full" type="number"
name="scale_factor" step="0.1"/>
name="scale_factor" step="0.1" :disabled="(bool) $viewingDeviceModelId"/>
<flux:input label="Rotation" wire:model="rotation" id="rotation" class="block mt-1 w-full"
type="number"
name="rotation"/>
name="rotation" :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="mb-4">
<flux:input label="MIME Type" wire:model="mime_type" id="mime_type"
class="block mt-1 w-full" type="text"
name="mime_type"/>
<flux:select label="MIME Type" wire:model="mime_type" id="mime_type" name="mime_type" :disabled="(bool) $viewingDeviceModelId">
<flux:select.option>image/png</flux:select.option>
<flux:select.option>image/bmp</flux:select.option>
</flux:select>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Offset X" wire:model="offset_x" id="offset_x" class="block mt-1 w-full"
type="number"
name="offset_x"/>
name="offset_x" :disabled="(bool) $viewingDeviceModelId"/>
<flux:input label="Offset Y" wire:model="offset_y" id="offset_y" class="block mt-1 w-full"
type="number"
name="offset_y"/>
name="offset_y" :disabled="(bool) $viewingDeviceModelId"/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">Create Device Model</flux:button>
<div class="mb-4">
<flux:select label="Color Palette" wire:model="palette_id" id="palette_id" name="palette_id" :disabled="(bool) $viewingDeviceModelId">
<flux:select.option value="">None</flux:select.option>
@foreach ($devicePalettes as $palette)
<flux:select.option value="{{ $palette->id }}">{{ $palette->description ?? $palette->name }} ({{ $palette->name }})</flux:select.option>
@endforeach
</flux:select>
</div>
@if (!$viewingDeviceModelId)
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">{{ $editingDeviceModelId ? 'Update' : 'Create' }} Device Model</flux:button>
</div>
@else
<div class="flex">
<flux:spacer/>
<flux:button type="button" wire:click="duplicateDeviceModel({{ $viewingDeviceModelId }})" variant="primary">Duplicate</flux:button>
</div>
@endif
</form>
</div>
</flux:modal>
@foreach ($deviceModels as $deviceModel)
<flux:modal name="edit-device-model-{{ $deviceModel->id }}" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Edit Device Model</flux:heading>
</div>
<form wire:submit="updateDeviceModel">
<div class="mb-4">
<flux:input label="Name" wire:model="name" id="edit_name" class="block mt-1 w-full"
type="text"
name="edit_name"/>
</div>
<div class="mb-4">
<flux:input label="Label" wire:model="label" id="edit_label" class="block mt-1 w-full"
type="text"
name="edit_label"/>
</div>
<div class="mb-4">
<flux:input label="Description" wire:model="description" id="edit_description"
class="block mt-1 w-full" name="edit_description"/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Width" wire:model="width" id="edit_width" class="block mt-1 w-full"
type="number"
name="edit_width"/>
<flux:input label="Height" wire:model="height" id="edit_height"
class="block mt-1 w-full" type="number"
name="edit_height"/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Colors" wire:model="colors" id="edit_colors"
class="block mt-1 w-full" type="number"
name="edit_colors"/>
<flux:input label="Bit Depth" wire:model="bit_depth" id="edit_bit_depth"
class="block mt-1 w-full" type="number"
name="edit_bit_depth"/>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Scale Factor" wire:model="scale_factor" id="edit_scale_factor"
class="block mt-1 w-full" type="number"
name="edit_scale_factor" step="0.1"/>
<flux:input label="Rotation" wire:model="rotation" id="edit_rotation"
class="block mt-1 w-full" type="number"
name="edit_rotation"/>
</div>
<div class="mb-4">
<flux:select label="MIME Type" wire:model="mime_type" id="edit_mime_type" name="edit_mime_type">
<flux:select.option>image/png</flux:select.option>
<flux:select.option>image/bmp</flux:select.option>
</flux:select>
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<flux:input label="Offset X" wire:model="offset_x" id="edit_offset_x"
class="block mt-1 w-full" type="number"
name="edit_offset_x"/>
<flux:input label="Offset Y" wire:model="offset_y" id="edit_offset_y"
class="block mt-1 w-full" type="number"
name="edit_offset_y"/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">Update Device Model</flux:button>
</div>
</form>
</div>
</flux:modal>
@endforeach
<table
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
data-flux-table>
@ -369,14 +396,25 @@ new class extends Component {
>
<div class="flex items-center gap-4">
<flux:button.group>
<flux:modal.trigger name="edit-device-model-{{ $deviceModel->id }}">
<flux:button wire:click="editDeviceModel({{ $deviceModel->id }})" icon="pencil"
@if ($deviceModel->source === 'api')
<flux:modal.trigger name="device-model-modal">
<flux:button wire:click="openDeviceModelModal('{{ $deviceModel->id }}', true)" icon="eye"
iconVariant="outline">
</flux:button>
</flux:modal.trigger>
<flux:button wire:click="duplicateDeviceModel('{{ $deviceModel->id }}')" icon="document-duplicate"
iconVariant="outline">
</flux:button>
</flux:modal.trigger>
<flux:button wire:click="deleteDeviceModel({{ $deviceModel->id }})" icon="trash"
iconVariant="outline">
</flux:button>
@else
<flux:modal.trigger name="device-model-modal">
<flux:button wire:click="openDeviceModelModal('{{ $deviceModel->id }}')" icon="pencil"
iconVariant="outline">
</flux:button>
</flux:modal.trigger>
<flux:button wire:click="deleteDeviceModel('{{ $deviceModel->id }}')" icon="trash"
iconVariant="outline">
</flux:button>
@endif
</flux:button.group>
</div>
</td>

View file

@ -0,0 +1,384 @@
<?php
use App\Models\DevicePalette;
use Livewire\Volt\Component;
new class extends Component
{
public $devicePalettes;
public $name;
public $description;
public $grays = 2;
public $colors = [];
public $framework_class = '';
public $colorInput = '';
protected $rules = [
'name' => 'required|string|max:255|unique:device_palettes,name',
'description' => 'nullable|string|max:255',
'grays' => 'required|integer|min:1|max:256',
'colors' => 'nullable|array',
'colors.*' => 'string|regex:/^#[0-9A-Fa-f]{6}$/',
'framework_class' => 'nullable|string|max:255',
];
public function mount()
{
$this->devicePalettes = DevicePalette::all();
return view('livewire.device-palettes.index');
}
public function addColor(): void
{
$this->validate(['colorInput' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/'], [
'colorInput.regex' => 'Color must be a valid hex color (e.g., #FF0000)',
]);
if (! in_array($this->colorInput, $this->colors)) {
$this->colors[] = $this->colorInput;
}
$this->colorInput = '';
}
public function removeColor(int $index): void
{
unset($this->colors[$index]);
$this->colors = array_values($this->colors);
}
public $editingDevicePaletteId;
public $viewingDevicePaletteId;
public function openDevicePaletteModal(?string $devicePaletteId = null, bool $viewOnly = false): void
{
if ($devicePaletteId) {
$devicePalette = DevicePalette::findOrFail($devicePaletteId);
if ($viewOnly) {
$this->viewingDevicePaletteId = $devicePalette->id;
$this->editingDevicePaletteId = null;
} else {
$this->editingDevicePaletteId = $devicePalette->id;
$this->viewingDevicePaletteId = null;
}
$this->name = $devicePalette->name;
$this->description = $devicePalette->description;
$this->grays = $devicePalette->grays;
// Ensure colors is always an array and properly decoded
// The model cast should handle JSON decoding, but we'll be explicit
$colors = $devicePalette->getAttribute('colors');
if ($colors === null) {
$this->colors = [];
} elseif (is_string($colors)) {
$decoded = json_decode($colors, true);
$this->colors = is_array($decoded) ? array_values($decoded) : [];
} elseif (is_array($colors)) {
$this->colors = array_values($colors); // Re-index array
} else {
$this->colors = [];
}
$this->framework_class = $devicePalette->framework_class;
} else {
$this->editingDevicePaletteId = null;
$this->viewingDevicePaletteId = null;
$this->reset(['name', 'description', 'grays', 'colors', 'framework_class']);
}
$this->colorInput = '';
}
public function saveDevicePalette(): void
{
$rules = [
'name' => 'required|string|max:255',
'description' => 'nullable|string|max:255',
'grays' => 'required|integer|min:1|max:256',
'colors' => 'nullable|array',
'colors.*' => 'string|regex:/^#[0-9A-Fa-f]{6}$/',
'framework_class' => 'nullable|string|max:255',
];
if ($this->editingDevicePaletteId) {
$rules['name'] = 'required|string|max:255|unique:device_palettes,name,'.$this->editingDevicePaletteId;
} else {
$rules['name'] = 'required|string|max:255|unique:device_palettes,name';
}
$this->validate($rules);
if ($this->editingDevicePaletteId) {
$devicePalette = DevicePalette::findOrFail($this->editingDevicePaletteId);
$devicePalette->update([
'name' => $this->name,
'description' => $this->description,
'grays' => $this->grays,
'colors' => ! empty($this->colors) ? $this->colors : null,
'framework_class' => $this->framework_class,
]);
$message = 'Device palette updated successfully.';
} else {
DevicePalette::create([
'name' => $this->name,
'description' => $this->description,
'grays' => $this->grays,
'colors' => ! empty($this->colors) ? $this->colors : null,
'framework_class' => $this->framework_class,
'source' => 'manual',
]);
$message = 'Device palette created successfully.';
}
$this->reset(['name', 'description', 'grays', 'colors', 'framework_class', 'colorInput', 'editingDevicePaletteId', 'viewingDevicePaletteId']);
Flux::modal('device-palette-modal')->close();
$this->devicePalettes = DevicePalette::all();
session()->flash('message', $message);
}
public function deleteDevicePalette(string $devicePaletteId): void
{
$devicePalette = DevicePalette::findOrFail($devicePaletteId);
$devicePalette->delete();
$this->devicePalettes = DevicePalette::all();
session()->flash('message', 'Device palette deleted successfully.');
}
public function duplicateDevicePalette(string $devicePaletteId): void
{
$devicePalette = DevicePalette::findOrFail($devicePaletteId);
$this->editingDevicePaletteId = null;
$this->viewingDevicePaletteId = null;
$this->name = $devicePalette->name.' (Copy)';
$this->description = $devicePalette->description;
$this->grays = $devicePalette->grays;
$colors = $devicePalette->getAttribute('colors');
if ($colors === null) {
$this->colors = [];
} elseif (is_string($colors)) {
$decoded = json_decode($colors, true);
$this->colors = is_array($decoded) ? array_values($decoded) : [];
} elseif (is_array($colors)) {
$this->colors = array_values($colors);
} else {
$this->colors = [];
}
$this->framework_class = $devicePalette->framework_class;
$this->colorInput = '';
$this->js('Flux.modal("device-palette-modal").show()');
}
}
?>
<div>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center space-x-2">
<h2 class="text-2xl font-semibold dark:text-gray-100">Device Palettes</h2>
<flux:dropdown>
<flux:button icon="chevron-down" variant="ghost"></flux:button>
<flux:menu>
<flux:menu.item href="{{ route('devices') }}">Devices</flux:menu.item>
<flux:menu.item href="{{ route('device-models.index') }}">Device Models</flux:menu.item>
</flux:menu>
</flux:dropdown>
</div>
<flux:modal.trigger name="device-palette-modal">
<flux:button wire:click="openDevicePaletteModal()" icon="plus" variant="primary">Add Device Palette</flux:button>
</flux:modal.trigger>
</div>
@if (session()->has('message'))
<div class="mb-4">
<flux:callout variant="success" icon="check-circle" heading=" {{ session('message') }}">
<x-slot name="controls">
<flux:button icon="x-mark" variant="ghost"
x-on:click="$el.closest('[data-flux-callout]').remove()"/>
</x-slot>
</flux:callout>
</div>
@endif
<flux:modal name="device-palette-modal" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">
@if ($viewingDevicePaletteId)
View Device Palette
@elseif ($editingDevicePaletteId)
Edit Device Palette
@else
Add Device Palette
@endif
</flux:heading>
</div>
<form wire:submit="saveDevicePalette">
<div class="mb-4">
<flux:input label="Name (Identifier)" wire:model="name" id="name" class="block mt-1 w-full" type="text"
name="name" autofocus :disabled="$viewingDevicePaletteId"/>
</div>
<div class="mb-4">
<flux:input label="Description" wire:model="description" id="description" class="block mt-1 w-full" type="text"
name="description" :disabled="$viewingDevicePaletteId"/>
</div>
<div class="mb-4">
<flux:input label="Grays" wire:model="grays" id="grays" class="block mt-1 w-full"
type="number"
name="grays" min="1" max="256" :disabled="$viewingDevicePaletteId"/>
</div>
<div class="mb-4">
<flux:input label="Framework Class" wire:model="framework_class" id="framework_class"
class="block mt-1 w-full" type="text"
name="framework_class" :disabled="$viewingDevicePaletteId"/>
</div>
<div class="mb-4">
<flux:label>Colors</flux:label>
@if (!$viewingDevicePaletteId)
<div class="flex gap-2 mb-2">
<flux:input wire:model="colorInput" placeholder="#FF0000" class="flex-1"/>
<flux:button type="button" wire:click="addColor" variant="ghost">Add</flux:button>
</div>
@endif
<div class="flex flex-wrap gap-2">
@if (!empty($colors) && is_array($colors) && count($colors) > 0)
@foreach ($colors as $index => $color)
@if (!empty($color))
<div wire:key="color-{{ $editingDevicePaletteId ?? $viewingDevicePaletteId ?? 'new' }}-{{ $index }}-{{ $color }}" class="flex items-center gap-2 px-3 py-1 bg-zinc-100 dark:bg-zinc-800 rounded">
<div class="w-4 h-4 rounded border border-zinc-300 dark:border-zinc-600" style="background-color: {{ $color }}"></div>
<span class="text-sm">{{ $color }}</span>
@if (!$viewingDevicePaletteId)
<flux:button type="button" wire:click="removeColor({{ $index }})" icon="x-mark" variant="ghost" size="sm"></flux:button>
@endif
</div>
@endif
@endforeach
@endif
</div>
@if (!$viewingDevicePaletteId)
<p class="mt-1 text-xs text-zinc-500">Leave empty for grayscale-only palette</p>
@endif
</div>
@if (!$viewingDevicePaletteId)
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">{{ $editingDevicePaletteId ? 'Update' : 'Create' }} Device Palette</flux:button>
</div>
@else
<div class="flex">
<flux:spacer/>
<flux:button type="button" wire:click="duplicateDevicePalette('{{ $viewingDevicePaletteId }}')" variant="primary">Duplicate</flux:button>
</div>
@endif
</form>
</div>
</flux:modal>
<table
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
data-flux-table>
<thead data-flux-columns>
<tr>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column>
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Description</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column>
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Grays</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column>
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Colors</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column>
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Actions</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows>
@foreach ($devicePalettes as $devicePalette)
<tr data-flux-row>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
<div>
<div class="font-medium text-zinc-800 dark:text-white">{{ $devicePalette->description ?? $devicePalette->name }}</div>
<div class="text-xs text-zinc-500">{{ $devicePalette->name }}</div>
</div>
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $devicePalette->grays }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
@if ($devicePalette->colors)
<div class="flex gap-1">
@foreach ($devicePalette->colors as $color)
<div class="w-4 h-4 rounded border border-zinc-300 dark:border-zinc-600" style="background-color: {{ $color }}"></div>
@endforeach
<span class="ml-2">({{ count($devicePalette->colors) }})</span>
</div>
@else
<span class="text-zinc-400">Grayscale only</span>
@endif
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap font-medium text-zinc-800 dark:text-white"
>
<div class="flex items-center gap-4">
<flux:button.group>
@if ($devicePalette->source === 'api')
<flux:modal.trigger name="device-palette-modal">
<flux:button wire:click="openDevicePaletteModal('{{ $devicePalette->id }}', true)" icon="eye"
iconVariant="outline">
</flux:button>
</flux:modal.trigger>
<flux:button wire:click="duplicateDevicePalette('{{ $devicePalette->id }}')" icon="document-duplicate"
iconVariant="outline">
</flux:button>
@else
<flux:modal.trigger name="device-palette-modal">
<flux:button wire:click="openDevicePaletteModal('{{ $devicePalette->id }}')" icon="pencil"
iconVariant="outline">
</flux:button>
</flux:modal.trigger>
<flux:button wire:click="deleteDevicePalette('{{ $devicePalette->id }}')" icon="trash"
iconVariant="outline">
</flux:button>
@endif
</flux:button.group>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>

View file

@ -121,7 +121,16 @@ new class extends Component {
{{--@dump($devices)--}}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
<div class="flex items-center space-x-2">
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
<flux:dropdown>
<flux:button icon="chevron-down" variant="ghost"></flux:button>
<flux:menu>
<flux:menu.item href="{{ route('device-models.index') }}">Device Models</flux:menu.item>
<flux:menu.item href="{{ route('device-palettes.index') }}">Device Palettes</flux:menu.item>
</flux:menu>
</flux:dropdown>
</div>
<flux:modal.trigger name="create-device">
<flux:button icon="plus" variant="primary">Add Device</flux:button>
</flux:modal.trigger>