Theme Addition

This commit is contained in:
Brian Fertig 2026-07-09 15:54:57 -06:00
parent 25d749cb29
commit 10bc170362
47 changed files with 767 additions and 122 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ db-data/
src/vendor/ src/vendor/
src/public/css/app.css src/public/css/app.css
src/public/vendor/swagger-ui/ src/public/vendor/swagger-ui/
src/public/uploads/
src/composer.lock src/composer.lock
src/node_modules/ src/node_modules/
src/storage/app/* src/storage/app/*

View File

@ -16,6 +16,9 @@ if [ -d /opt/swagger-ui ] && [ ! -f public/vendor/swagger-ui/swagger-ui-bundle.j
cp /opt/swagger-ui/* public/vendor/swagger-ui/ cp /opt/swagger-ui/* public/vendor/swagger-ui/
fi fi
# Directory for admin-uploaded theme assets (logo)
mkdir -p public/uploads/theme
# Compile Tailwind CSS # Compile Tailwind CSS
if [ -f resources/css/app.css ]; then if [ -f resources/css/app.css ]; then
echo "Building Tailwind CSS..." echo "Building Tailwind CSS..."

View File

@ -0,0 +1,37 @@
<?php
namespace App\Enums;
enum NavGradientDirection: string
{
case None = 'none';
case ToRight = 'to-r';
case ToBottom = 'to-b';
case ToBottomRight = 'to-br';
case ToTopRight = 'to-tr';
case Radial = 'radial';
public function label(): string
{
return match ($this) {
self::None => 'Solid (no gradient)',
self::ToRight => 'Left to right',
self::ToBottom => 'Top to bottom',
self::ToBottomRight => 'Upper-left to lower-right',
self::ToTopRight => 'Lower-left to upper-right',
self::Radial => 'Radial (from top center)',
};
}
/** Linear-gradient direction keyword; null for solid and radial. */
public function cssKeyword(): ?string
{
return match ($this) {
self::None, self::Radial => null,
self::ToRight => 'to right',
self::ToBottom => 'to bottom',
self::ToBottomRight => 'to bottom right',
self::ToTopRight => 'to top right',
};
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers;
use App\Enums\NavGradientDirection;
use App\Models\Theme;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class ThemeController extends Controller
{
private const HEX = 'regex:/^#[0-9a-fA-F]{6}$/';
public function edit(): View
{
return view('admin.theme.edit', ['theme' => Theme::current()]);
}
public function update(Request $request): RedirectResponse
{
$data = $request->validate([
'primary_color' => ['required', self::HEX],
'secondary_color' => ['required', self::HEX],
'tertiary_color' => ['required', self::HEX],
'nav_gradient_from' => ['required', self::HEX],
'nav_gradient_mid' => ['nullable', self::HEX],
'nav_gradient_to' => ['nullable', self::HEX, 'required_unless:nav_gradient_direction,none'],
'nav_gradient_direction' => ['required', Rule::enum(NavGradientDirection::class)],
'logo' => ['nullable', 'file', 'mimes:png,jpg,jpeg,webp', 'max:2048'],
'remove_logo' => ['nullable', 'boolean'],
], [
'nav_gradient_to.required_unless' => 'A gradient needs an end color — or choose "Solid (no gradient)".',
'*.regex' => 'Colors must be a 6-digit hex value like #2563eb.',
]);
$theme = Theme::current();
if ($request->boolean('remove_logo') || $request->hasFile('logo')) {
$this->deleteLogoFile($theme);
$theme->logo_path = null;
}
if ($request->hasFile('logo')) {
$file = $request->file('logo');
$name = 'logo-' . now()->format('YmdHis') . '.' . strtolower($file->getClientOriginalExtension());
$file->move(public_path('uploads/theme'), $name);
$theme->logo_path = 'uploads/theme/' . $name;
}
$theme->fill(Arr::except($data, ['logo', 'remove_logo']))->save();
return redirect()
->route('admin.theme.edit')
->with('status', 'Theme updated.');
}
public function reset(): RedirectResponse
{
$theme = Theme::current();
$this->deleteLogoFile($theme);
$theme->fill(Theme::DEFAULTS + ['logo_path' => null])->save();
return redirect()
->route('admin.theme.edit')
->with('status', 'Theme reset to defaults.');
}
private function deleteLogoFile(Theme $theme): void
{
if ($theme->logo_path && is_file(public_path($theme->logo_path))) {
@unlink(public_path($theme->logo_path));
}
}
}

158
src/app/Models/Theme.php Normal file
View File

@ -0,0 +1,158 @@
<?php
namespace App\Models;
use App\Enums\NavGradientDirection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class Theme extends Model
{
protected $table = 'site_themes';
protected $guarded = ['id'];
protected $casts = [
'nav_gradient_direction' => NavGradientDirection::class,
];
/** Stock values — must match the migration defaults and the @theme tokens in app.css. */
public const DEFAULTS = [
'primary_color' => '#2563eb',
'secondary_color' => '#3b82f6',
'tertiary_color' => '#1d4ed8',
'nav_gradient_from' => '#ffffff',
'nav_gradient_mid' => null,
'nav_gradient_to' => null,
'nav_gradient_direction' => 'none',
];
protected static function booted(): void
{
static::saved(fn () => Cache::forget('site-theme'));
static::deleted(fn () => Cache::forget('site-theme'));
}
/** The single site-wide theme row, cached until the next save. */
public static function current(): self
{
// refresh() hydrates the DB column defaults into the fresh model.
return Cache::rememberForever(
'site-theme',
fn () => static::query()->first() ?? static::query()->create([])->refresh()
);
}
/** True when the theme matches the stock look exactly (no override needed). */
public function isDefault(): bool
{
if ($this->logo_path !== null) {
return false;
}
foreach (self::DEFAULTS as $key => $default) {
$value = $this->{$key};
if ($value instanceof \BackedEnum) {
$value = $value->value;
}
if ($value === null || $default === null) {
if ($value !== $default) {
return false;
}
continue;
}
if (strtolower((string) $value) !== strtolower((string) $default)) {
return false;
}
}
return true;
}
public function primaryHover(): string
{
return self::darken($this->primary_color, 0.15);
}
/** CSS background value for the top bar: solid color or gradient. */
public function navBackgroundCss(): string
{
$stops = $this->activeGradientStops();
if ($this->nav_gradient_direction === NavGradientDirection::None || count($stops) < 2) {
return $this->nav_gradient_from;
}
$list = implode(', ', $stops);
return $this->nav_gradient_direction === NavGradientDirection::Radial
? "radial-gradient(ellipse at top, {$list})"
: "linear-gradient({$this->nav_gradient_direction->cssKeyword()}, {$list})";
}
/** Whether the nav background is dark enough to need white text (WCAG contrast). */
public function navIsDark(): bool
{
$stops = $this->nav_gradient_direction === NavGradientDirection::None
? [$this->nav_gradient_from]
: $this->activeGradientStops();
$l = array_sum(array_map(self::luminance(...), $stops)) / count($stops);
// White text wins when its contrast beats gray-900's (luminance ~0.0106).
return (1.05 / ($l + 0.05)) >= (($l + 0.05) / 0.0606);
}
public function navLinkColor(): string
{
return $this->navIsDark() ? '#ffffff' : $this->primary_color;
}
public function navTextColor(): string
{
return $this->navIsDark() ? '#ffffff' : '#111827';
}
public function navMutedColor(): string
{
return $this->navIsDark() ? 'rgba(255, 255, 255, 0.75)' : '#6b7280';
}
/** @return list<string> */
private function activeGradientStops(): array
{
return array_values(array_filter([
$this->nav_gradient_from,
$this->nav_gradient_mid,
$this->nav_gradient_to,
]));
}
/** WCAG relative luminance of a #rrggbb color. */
private static function luminance(string $hex): float
{
[$r, $g, $b] = sscanf($hex, '#%02x%02x%02x');
$lin = function (int $c): float {
$c = $c / 255;
return $c <= 0.03928 ? $c / 12.92 : (($c + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * $lin($r) + 0.7152 * $lin($g) + 0.0722 * $lin($b);
}
private static function darken(string $hex, float $amount): string
{
[$r, $g, $b] = sscanf($hex, '#%02x%02x%02x');
return sprintf(
'#%02x%02x%02x',
(int) round($r * (1 - $amount)),
(int) round($g * (1 - $amount)),
(int) round($b * (1 - $amount))
);
}
}

View File

@ -2,6 +2,8 @@
namespace App\Providers; namespace App\Providers;
use App\Models\Theme;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -13,6 +15,8 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
// View::composer('layouts.app', function ($view) {
$view->with('theme', Theme::current());
});
} }
} }

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Single-row site theme: logo, brand colors, and top-bar gradient.
* Defaults mirror the stock look (white header, Tailwind blues) so a
* fresh install renders identically until an admin customizes it.
*/
public function up(): void
{
Schema::create('site_themes', function (Blueprint $table) {
$table->id();
$table->string('logo_path')->nullable();
$table->string('primary_color', 7)->default('#2563eb');
$table->string('secondary_color', 7)->default('#3b82f6');
$table->string('tertiary_color', 7)->default('#1d4ed8');
$table->string('nav_gradient_from', 7)->default('#ffffff');
$table->string('nav_gradient_mid', 7)->nullable();
$table->string('nav_gradient_to', 7)->nullable();
$table->string('nav_gradient_direction', 10)->default('none');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('site_themes');
}
};

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,30 @@
/* Scan Blade templates for utility classes */ /* Scan Blade templates for utility classes */
@source "../views"; @source "../views";
/* --- Theme tokens ------------------------------------------------------ */
/* Semantic brand colors. Defaults are the stock blues; a customized theme
overrides these :root variables via an inline <style> in the layout head
(values come from the site_themes table), so no CSS rebuild is needed.
Must stay in sync with Theme::DEFAULTS. */
@theme {
--color-primary: #2563eb; /* blue-600 */
--color-primary-hover: #1d4ed8; /* blue-700 */
--color-secondary: #3b82f6; /* blue-500 */
--color-tertiary: #1d4ed8; /* blue-700 */
--color-nav-link: #2563eb;
--color-nav-text: #111827; /* gray-900 */
--color-nav-muted: #6b7280; /* gray-500 */
}
:root {
--theme-nav-bg: #ffffff;
--theme-chart-line: #2a78d6;
}
.site-nav {
background: var(--theme-nav-bg);
}
/* --- Destructive-action confirm modal ---------------------------------- */ /* --- Destructive-action confirm modal ---------------------------------- */
#confirm-delete-modal::backdrop { #confirm-delete-modal::backdrop {
background: rgba(17, 24, 39, 0.5); background: rgba(17, 24, 39, 0.5);

View File

@ -12,7 +12,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -21,14 +21,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description') }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description') }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create group</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create group</button>
<a href="{{ route('admin.groups.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('admin.groups.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -13,7 +13,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $group->name) }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name', $group->name) }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -22,14 +22,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description', $group->description) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description', $group->description) }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save changes</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save changes</button>
<a href="{{ route('admin.groups.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('admin.groups.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,12 +5,12 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">Groups</h1> <h1 class="text-2xl font-bold">Groups</h1>
<a href="{{ route('admin.groups.create') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">New group</a> <a href="{{ route('admin.groups.create') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">New group</a>
</div> </div>
@if ($groups->isEmpty()) @if ($groups->isEmpty())
<div class="rounded-lg border border-gray-200 bg-white px-6 py-10 text-center text-gray-500"> <div class="rounded-lg border border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
No groups yet. <a href="{{ route('admin.groups.create') }}" class="text-blue-600 hover:underline">Create the first one.</a> No groups yet. <a href="{{ route('admin.groups.create') }}" class="text-primary hover:underline">Create the first one.</a>
</div> </div>
@else @else
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white"> <div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">
@ -30,7 +30,7 @@
<td class="px-4 py-3 font-medium">{{ $group->name }}</td> <td class="px-4 py-3 font-medium">{{ $group->name }}</td>
<td class="px-4 py-3 text-gray-600">{{ $group->description ?? '—' }}</td> <td class="px-4 py-3 text-gray-600">{{ $group->description ?? '—' }}</td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<a href="{{ route('admin.groups.members', $group) }}" class="text-blue-600 hover:underline"> <a href="{{ route('admin.groups.members', $group) }}" class="text-primary hover:underline">
{{ $group->users_count }} {{ Str::plural('member', $group->users_count) }} {{ $group->users_count }} {{ Str::plural('member', $group->users_count) }}
</a> </a>
</td> </td>

View File

@ -35,7 +35,7 @@
@csrf @csrf
@method('PUT') @method('PUT')
<select name="role" onchange="this.form.submit()" <select name="role" onchange="this.form.submit()"
class="cursor-pointer rounded border border-gray-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="cursor-pointer rounded border border-gray-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-secondary/40">
<option value="member" {{ $member->pivot->role === 'member' ? 'selected' : '' }}>Member</option> <option value="member" {{ $member->pivot->role === 'member' ? 'selected' : '' }}>Member</option>
<option value="owner" {{ $member->pivot->role === 'owner' ? 'selected' : '' }}>Owner</option> <option value="owner" {{ $member->pivot->role === 'owner' ? 'selected' : '' }}>Owner</option>
</select> </select>
@ -68,7 +68,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="user_id" class="mb-1 block font-semibold">User</label> <label for="user_id" class="mb-1 block font-semibold">User</label>
<select id="user_id" name="user_id" required <select id="user_id" name="user_id" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
<option value="">Select a user…</option> <option value="">Select a user…</option>
@foreach ($available as $user) @foreach ($available as $user)
<option value="{{ $user->id }}">{{ $user->name }} {{ $user->email }}</option> <option value="{{ $user->id }}">{{ $user->name }} {{ $user->email }}</option>
@ -81,12 +81,12 @@
<div class="mb-4"> <div class="mb-4">
<label for="role" class="mb-1 block font-semibold">Role</label> <label for="role" class="mb-1 block font-semibold">Role</label>
<select id="role" name="role" <select id="role" name="role"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
<option value="member">Member</option> <option value="member">Member</option>
<option value="owner">Owner</option> <option value="owner">Owner</option>
</select> </select>
</div> </div>
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Add to group</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Add to group</button>
</form> </form>
@endif @endif
</div> </div>

View File

@ -0,0 +1,280 @@
@extends('layouts.app')
@section('title', 'Theme — ' . config('app.name'))
@section('content')
@php
$brandColors = [
['name' => 'primary_color', 'label' => 'Primary', 'hint' => 'Buttons, links, and the chart value line.'],
['name' => 'secondary_color', 'label' => 'Secondary', 'hint' => 'Focus rings and input highlights.'],
['name' => 'tertiary_color', 'label' => 'Tertiary', 'hint' => 'Chips, badges, and info panels.'],
];
$gradientStops = [
['name' => 'nav_gradient_from', 'label' => 'From', 'optional' => false],
['name' => 'nav_gradient_mid', 'label' => 'Middle (optional)', 'optional' => true],
['name' => 'nav_gradient_to', 'label' => 'To', 'optional' => true],
];
$presets = [
'Stock' => ['primary_color' => '#2563eb', 'secondary_color' => '#3b82f6', 'tertiary_color' => '#1d4ed8', 'nav_gradient_from' => '#ffffff', 'nav_gradient_mid' => '', 'nav_gradient_to' => '', 'nav_gradient_direction' => 'none'],
'Ocean' => ['primary_color' => '#0369a1', 'secondary_color' => '#0ea5e9', 'tertiary_color' => '#075985', 'nav_gradient_from' => '#0c4a6e', 'nav_gradient_mid' => '', 'nav_gradient_to' => '#0369a1', 'nav_gradient_direction' => 'to-r'],
'Forest' => ['primary_color' => '#15803d', 'secondary_color' => '#22c55e', 'tertiary_color' => '#166534', 'nav_gradient_from' => '#14532d', 'nav_gradient_mid' => '', 'nav_gradient_to' => '#15803d', 'nav_gradient_direction' => 'to-br'],
'Plum' => ['primary_color' => '#7e22ce', 'secondary_color' => '#a855f7', 'tertiary_color' => '#6b21a8', 'nav_gradient_from' => '#581c87', 'nav_gradient_mid' => '', 'nav_gradient_to' => '#7e22ce', 'nav_gradient_direction' => 'to-tr'],
'Slate' => ['primary_color' => '#334155', 'secondary_color' => '#64748b', 'tertiary_color' => '#1e293b', 'nav_gradient_from' => '#0f172a', 'nav_gradient_mid' => '', 'nav_gradient_to' => '#334155', 'nav_gradient_direction' => 'to-b'],
];
$inputClasses = 'rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40';
@endphp
<div class="mx-auto max-w-3xl rounded-lg border border-gray-200 bg-white p-8">
<h1 class="mb-1 text-2xl font-bold">Theme</h1>
<p class="mb-6 text-sm text-gray-500">Brand the site with your logo, colors, and a menu bar gradient. Changes apply to everyone.</p>
{{-- Live preview --}}
<div id="theme-preview" class="mb-8 rounded-lg border border-gray-200 bg-gray-50 p-4">
<p class="mb-3 text-xs font-semibold uppercase tracking-wide text-gray-500">Preview</p>
<div class="site-nav mb-3 flex items-center justify-between rounded-md border border-gray-200 px-4 py-3">
<span class="flex items-center text-lg font-bold text-nav-text" data-preview-brand>
<img src="{{ $theme->logo_path ? asset($theme->logo_path) : '' }}" alt="" class="h-8 w-auto {{ $theme->logo_path ? '' : 'hidden' }}" data-preview-logo>
<span class="{{ $theme->logo_path ? 'hidden' : '' }}" data-preview-name>{{ config('app.name', 'Bowler') }}</span>
</span>
<span class="flex items-center gap-4 text-sm">
<span class="text-nav-link">Charts</span>
<span class="text-nav-link">Data</span>
<span class="text-nav-muted">{{ auth()->user()->name }}</span>
</span>
</div>
<div class="flex flex-wrap items-center gap-3">
<span class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-white">Primary button</span>
<span class="text-sm text-primary underline">A link</span>
<span class="rounded-md border border-secondary px-3 py-2 text-sm text-gray-500 ring-2 ring-secondary/40">Focused input</span>
<span class="rounded-full border border-tertiary/25 bg-tertiary/5 px-3 py-1 text-sm text-tertiary">Chip</span>
</div>
</div>
{{-- Presets --}}
<div class="mb-8">
<p class="mb-2 font-semibold">Presets</p>
<div class="flex flex-wrap gap-2">
@foreach ($presets as $label => $values)
<button type="button" data-preset="{{ json_encode($values) }}"
class="cursor-pointer rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm hover:bg-gray-50">
<span class="mr-1.5 inline-block h-3 w-3 rounded-full align-middle" style="background: {{ $values['primary_color'] }}"></span>{{ $label }}
</button>
@endforeach
</div>
</div>
<form method="POST" action="{{ route('admin.theme.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
{{-- Logo --}}
<div class="mb-8">
<p class="mb-2 font-semibold">Logo</p>
@if ($theme->logo_path)
<div class="mb-3 flex items-center gap-4">
<img src="{{ asset($theme->logo_path) }}" alt="Current logo" class="h-9 w-auto rounded border border-gray-200 bg-white p-1">
<label class="flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" name="remove_logo" value="1" data-remove-logo class="rounded border-gray-300 text-primary">
Remove current logo
</label>
</div>
@endif
<input type="file" name="logo" accept="image/png,image/jpeg,image/webp"
class="block w-full cursor-pointer text-sm text-gray-700 file:mr-3 file:cursor-pointer file:rounded-md file:border-0 file:bg-gray-100 file:px-3 file:py-1.5 file:text-sm file:font-medium hover:file:bg-gray-200">
<p class="mt-1 text-sm text-gray-500">PNG, JPEG, or WebP up to 2 MB. Shown at 36px tall in place of the site name.</p>
@error('logo')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror
</div>
{{-- Brand colors --}}
<div class="mb-8">
<p class="mb-2 font-semibold">Brand colors</p>
<div class="space-y-3">
@foreach ($brandColors as $color)
<div class="flex items-center gap-3">
<input type="color" value="{{ old($color['name'], $theme->{$color['name']}) }}"
data-color-for="{{ $color['name'] }}" aria-label="{{ $color['label'] }} color picker"
class="h-9 w-12 cursor-pointer rounded border border-gray-300">
<input type="text" name="{{ $color['name'] }}" value="{{ old($color['name'], $theme->{$color['name']}) }}"
required pattern="#[0-9a-fA-F]{6}" maxlength="7" data-theme-input
class="w-28 {{ $inputClasses }}">
<div>
<span class="block text-sm font-medium">{{ $color['label'] }}</span>
<span class="block text-xs text-gray-500">{{ $color['hint'] }}</span>
</div>
</div>
@error($color['name'])
<p class="text-sm text-red-700">{{ $message }}</p>
@enderror
@endforeach
</div>
</div>
{{-- Menu bar --}}
<div class="mb-8">
<p class="mb-2 font-semibold">Menu bar</p>
<div class="mb-3">
<label for="nav_gradient_direction" class="mb-1 block text-sm font-medium">Style</label>
<select id="nav_gradient_direction" name="nav_gradient_direction" data-theme-input
class="w-64 {{ $inputClasses }}">
@foreach (\App\Enums\NavGradientDirection::cases() as $direction)
<option value="{{ $direction->value }}" @selected(old('nav_gradient_direction', $theme->nav_gradient_direction->value) === $direction->value)>
{{ $direction->label() }}
</option>
@endforeach
</select>
@error('nav_gradient_direction')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror
</div>
<div class="space-y-3">
@foreach ($gradientStops as $stop)
<div class="flex items-center gap-3">
<input type="color" value="{{ old($stop['name'], $theme->{$stop['name']}) ?: '#ffffff' }}"
data-color-for="{{ $stop['name'] }}" aria-label="{{ $stop['label'] }} color picker"
class="h-9 w-12 cursor-pointer rounded border border-gray-300">
<input type="text" name="{{ $stop['name'] }}" value="{{ old($stop['name'], $theme->{$stop['name']}) }}"
pattern="#[0-9a-fA-F]{6}" maxlength="7" data-theme-input
@unless ($stop['optional']) required @endunless
class="w-28 {{ $inputClasses }}">
<span class="text-sm font-medium">{{ $stop['label'] }}</span>
@if ($stop['optional'])
<button type="button" data-clear-stop="{{ $stop['name'] }}"
class="cursor-pointer text-sm text-gray-500 hover:text-gray-700 hover:underline">Clear</button>
@endif
</div>
@error($stop['name'])
<p class="text-sm text-red-700">{{ $message }}</p>
@enderror
@endforeach
</div>
<p class="mt-2 text-sm text-gray-500">With "Solid", only the From color is used. Menu text switches between light and dark automatically for readability.</p>
</div>
<div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save theme</button>
</div>
</form>
<form method="POST" action="{{ route('admin.theme.reset') }}" class="mt-3"
onsubmit="return confirm('Reset the theme to stock defaults and delete the uploaded logo?')">
@csrf
<button type="submit" class="cursor-pointer text-sm text-red-700 hover:underline">Reset to defaults</button>
</form>
</div>
<script>
(function () {
var panel = document.getElementById('theme-preview');
var HEX = /^#[0-9a-fA-F]{6}$/;
function field(name) {
return document.querySelector('[name="' + name + '"]');
}
function val(name) {
return field(name).value.trim();
}
// Same math as App\Models\Theme — keep in sync.
function luminance(hex) {
var parts = [1, 3, 5].map(function (i) {
var c = parseInt(hex.slice(i, i + 2), 16) / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * parts[0] + 0.7152 * parts[1] + 0.0722 * parts[2];
}
function darken(hex, amount) {
return '#' + [1, 3, 5].map(function (i) {
var c = Math.round(parseInt(hex.slice(i, i + 2), 16) * (1 - amount));
return c.toString(16).padStart(2, '0');
}).join('');
}
var directionCss = { 'to-r': 'to right', 'to-b': 'to bottom', 'to-br': 'to bottom right', 'to-tr': 'to top right' };
function render() {
var from = val('nav_gradient_from');
if (!HEX.test(from)) return;
var direction = val('nav_gradient_direction');
var stops = [from];
if (direction !== 'none') {
[val('nav_gradient_mid'), val('nav_gradient_to')].forEach(function (s) {
if (HEX.test(s)) stops.push(s);
});
}
var background = (direction === 'none' || stops.length < 2) ? from
: direction === 'radial' ? 'radial-gradient(ellipse at top, ' + stops.join(', ') + ')'
: 'linear-gradient(' + directionCss[direction] + ', ' + stops.join(', ') + ')';
var l = stops.reduce(function (sum, s) { return sum + luminance(s); }, 0) / stops.length;
var dark = (1.05 / (l + 0.05)) >= ((l + 0.05) / 0.0606);
var primary = HEX.test(val('primary_color')) ? val('primary_color') : '#2563eb';
var secondary = HEX.test(val('secondary_color')) ? val('secondary_color') : '#3b82f6';
var tertiary = HEX.test(val('tertiary_color')) ? val('tertiary_color') : '#1d4ed8';
panel.style.setProperty('--theme-nav-bg', background);
panel.style.setProperty('--color-primary', primary);
panel.style.setProperty('--color-primary-hover', darken(primary, 0.15));
panel.style.setProperty('--color-secondary', secondary);
panel.style.setProperty('--color-tertiary', tertiary);
panel.style.setProperty('--color-nav-link', dark ? '#ffffff' : primary);
panel.style.setProperty('--color-nav-text', dark ? '#ffffff' : '#111827');
panel.style.setProperty('--color-nav-muted', dark ? 'rgba(255, 255, 255, 0.75)' : '#6b7280');
}
// Two-way sync between each color picker and its hex text field.
document.querySelectorAll('[data-color-for]').forEach(function (picker) {
var text = field(picker.getAttribute('data-color-for'));
picker.addEventListener('input', function () {
text.value = picker.value;
render();
});
text.addEventListener('input', function () {
if (HEX.test(text.value.trim())) picker.value = text.value.trim();
});
});
document.querySelectorAll('[data-theme-input]').forEach(function (el) {
el.addEventListener('input', render);
});
document.querySelectorAll('[data-clear-stop]').forEach(function (button) {
button.addEventListener('click', function () {
field(button.getAttribute('data-clear-stop')).value = '';
render();
});
});
document.querySelectorAll('[data-preset]').forEach(function (button) {
button.addEventListener('click', function () {
var values = JSON.parse(button.getAttribute('data-preset'));
Object.keys(values).forEach(function (name) {
var input = field(name);
if (!input) return;
input.value = values[name];
var picker = document.querySelector('[data-color-for="' + name + '"]');
if (picker && HEX.test(values[name])) picker.value = values[name];
});
render();
});
});
// Toggle the preview brand between logo and site name with the remove checkbox.
var removeLogo = document.querySelector('[data-remove-logo]');
if (removeLogo) {
removeLogo.addEventListener('change', function () {
document.querySelector('[data-preview-logo]').classList.toggle('hidden', removeLogo.checked);
document.querySelector('[data-preview-name]').classList.toggle('hidden', !removeLogo.checked);
});
}
render();
})();
</script>
@endsection

View File

@ -12,7 +12,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -21,7 +21,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="email" class="mb-1 block font-semibold">Email</label> <label for="email" class="mb-1 block font-semibold">Email</label>
<input id="email" type="email" name="email" value="{{ old('email') }}" required <input id="email" type="email" name="email" value="{{ old('email') }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('email') @error('email')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -30,7 +30,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="password" class="mb-1 block font-semibold">Password</label> <label for="password" class="mb-1 block font-semibold">Password</label>
<input id="password" type="password" name="password" required autocomplete="new-password" <input id="password" type="password" name="password" required autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('password') @error('password')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -39,7 +39,7 @@
<div class="mb-6"> <div class="mb-6">
<label for="type" class="mb-1 block font-semibold">User type</label> <label for="type" class="mb-1 block font-semibold">User type</label>
<select id="type" name="type" <select id="type" name="type"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@foreach (\App\Enums\UserType::cases() as $type) @foreach (\App\Enums\UserType::cases() as $type)
<option value="{{ $type->value }}" {{ old('type', 'standard') === $type->value ? 'selected' : '' }}>{{ $type->label() }}</option> <option value="{{ $type->value }}" {{ old('type', 'standard') === $type->value ? 'selected' : '' }}>{{ $type->label() }}</option>
@endforeach @endforeach
@ -51,7 +51,7 @@
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create user</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create user</button>
<a href="{{ route('admin.users.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('admin.users.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -13,7 +13,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $user->name) }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name', $user->name) }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -22,7 +22,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="email" class="mb-1 block font-semibold">Email</label> <label for="email" class="mb-1 block font-semibold">Email</label>
<input id="email" type="email" name="email" value="{{ old('email', $user->email) }}" required <input id="email" type="email" name="email" value="{{ old('email', $user->email) }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('email') @error('email')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -31,7 +31,7 @@
<div class="mb-6"> <div class="mb-6">
<label for="type" class="mb-1 block font-semibold">User type</label> <label for="type" class="mb-1 block font-semibold">User type</label>
<select id="type" name="type" {{ $user->id === auth()->id() ? 'disabled' : '' }} <select id="type" name="type" {{ $user->id === auth()->id() ? 'disabled' : '' }}
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500">
@foreach (\App\Enums\UserType::cases() as $type) @foreach (\App\Enums\UserType::cases() as $type)
<option value="{{ $type->value }}" {{ old('type', $user->type->value) === $type->value ? 'selected' : '' }}>{{ $type->label() }}</option> <option value="{{ $type->value }}" {{ old('type', $user->type->value) === $type->value ? 'selected' : '' }}>{{ $type->label() }}</option>
@endforeach @endforeach
@ -49,9 +49,9 @@
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save changes</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save changes</button>
<a href="{{ route('admin.users.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('admin.users.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
<a href="{{ route('admin.users.password.edit', $user) }}" class="ml-auto text-sm text-blue-600 hover:underline">Reset password </a> <a href="{{ route('admin.users.password.edit', $user) }}" class="ml-auto text-sm text-primary hover:underline">Reset password </a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -5,7 +5,7 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">User Management</h1> <h1 class="text-2xl font-bold">User Management</h1>
<a href="{{ route('admin.users.create') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">New user</a> <a href="{{ route('admin.users.create') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">New user</a>
</div> </div>
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white"> <div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">
@ -35,7 +35,7 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
@forelse ($user->groups as $group) @forelse ($user->groups as $group)
<a href="{{ route('admin.groups.members', $group) }}" <a href="{{ route('admin.groups.members', $group) }}"
class="mr-1 inline-block rounded-full bg-blue-50 px-2 py-0.5 text-xs text-blue-700 hover:bg-blue-100">{{ $group->name }}</a> class="mr-1 inline-block rounded-full bg-tertiary/5 px-2 py-0.5 text-xs text-tertiary hover:bg-tertiary/10">{{ $group->name }}</a>
@empty @empty
<span class="text-gray-400"></span> <span class="text-gray-400"></span>
@endforelse @endforelse

View File

@ -14,7 +14,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="password" class="mb-1 block font-semibold">New password</label> <label for="password" class="mb-1 block font-semibold">New password</label>
<input id="password" type="password" name="password" required autofocus autocomplete="new-password" <input id="password" type="password" name="password" required autofocus autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('password') @error('password')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -23,11 +23,11 @@
<div class="mb-6"> <div class="mb-6">
<label for="password_confirmation" class="mb-1 block font-semibold">Confirm new password</label> <label for="password_confirmation" class="mb-1 block font-semibold">Confirm new password</label>
<input id="password_confirmation" type="password" name="password_confirmation" required autocomplete="new-password" <input id="password_confirmation" type="password" name="password_confirmation" required autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Reset password</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Reset password</button>
<a href="{{ route('admin.users.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('admin.users.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">API Keys</h1> <h1 class="text-2xl font-bold">API Keys</h1>
<a href="{{ route('api.docs') }}" class="text-blue-600 hover:underline">API documentation </a> <a href="{{ route('api.docs') }}" class="text-primary hover:underline">API documentation </a>
</div> </div>
@if ($errors->has('view')) @if ($errors->has('view'))
@ -37,9 +37,9 @@
<div class="flex-1"> <div class="flex-1">
<label for="name" class="mb-1 block font-semibold">Key name</label> <label for="name" class="mb-1 block font-semibold">Key name</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required placeholder="e.g. CI pipeline, Power BI, my-script" <input id="name" type="text" name="name" value="{{ old('name') }}" required placeholder="e.g. CI pipeline, Power BI, my-script"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
</div> </div>
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create key</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create key</button>
</form> </form>
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>

View File

@ -12,7 +12,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="email" class="mb-1 block font-semibold">Email</label> <label for="email" class="mb-1 block font-semibold">Email</label>
<input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus autocomplete="username" <input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus autocomplete="username"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('email') @error('email')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -21,7 +21,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="password" class="mb-1 block font-semibold">Password</label> <label for="password" class="mb-1 block font-semibold">Password</label>
<input id="password" type="password" name="password" required autocomplete="current-password" <input id="password" type="password" name="password" required autocomplete="current-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('password') @error('password')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -34,7 +34,7 @@
</label> </label>
</div> </div>
<button type="submit" class="w-full cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Log in</button> <button type="submit" class="w-full cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Log in</button>
</form> </form>
</div> </div>
@endsection @endsection

View File

@ -13,14 +13,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $category->name) }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name', $category->name) }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save</button>
<a href="{{ route('categories.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('categories.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">My Categories</h1> <h1 class="text-2xl font-bold">My Categories</h1>
<a href="{{ route('teams.index') }}" class="text-blue-600 hover:underline">Manage teams </a> <a href="{{ route('teams.index') }}" class="text-primary hover:underline">Manage teams </a>
</div> </div>
<p class="mb-4 text-sm text-gray-500">Categories sub-group items within each team on bowler charts e.g. "Performance", "General", or "Security".</p> <p class="mb-4 text-sm text-gray-500">Categories sub-group items within each team on bowler charts e.g. "Performance", "General", or "Security".</p>
@ -19,9 +19,9 @@
<div class="flex-1"> <div class="flex-1">
<label for="name" class="mb-1 block font-semibold">New category</label> <label for="name" class="mb-1 block font-semibold">New category</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required placeholder="e.g. Performance" <input id="name" type="text" name="name" value="{{ old('name') }}" required placeholder="e.g. Performance"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
</div> </div>
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Add category</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Add category</button>
</form> </form>
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>

View File

@ -15,7 +15,7 @@
<th class="min-w-28 px-2 py-3">Owner</th> <th class="min-w-28 px-2 py-3">Owner</th>
@endif @endif
@foreach ($months as $month) @foreach ($months as $month)
<th class="px-2 py-3 text-center {{ $loop->last ? 'bg-blue-50' : '' }}">{{ $month['label'] }}</th> <th class="px-2 py-3 text-center {{ $loop->last ? 'bg-tertiary/5' : '' }}">{{ $month['label'] }}</th>
@endforeach @endforeach
</tr> </tr>
</thead> </thead>
@ -47,12 +47,12 @@
@endif @endif
@foreach ($months as $month) @foreach ($months as $month)
@php($cell = $row['cells'][$month['key']] ?? null) @php($cell = $row['cells'][$month['key']] ?? null)
<td class="px-1 py-1 text-center {{ $loop->last ? 'bg-blue-50/50' : '' }}"> <td class="px-1 py-1 text-center {{ $loop->last ? 'bg-tertiary/3' : '' }}">
@if ($cell && $row['type'] === 'project') @if ($cell && $row['type'] === 'project')
<button type="button" title="{{ $cell['tooltip'] }}" <button type="button" title="{{ $cell['tooltip'] }}"
data-month="{{ $month['key'] }}" data-month="{{ $month['key'] }}"
data-details="{{ json_encode($cell['details']) }}" data-details="{{ json_encode($cell['details']) }}"
class="js-project-cell inline-block w-full cursor-pointer rounded px-1 py-1.5 text-xs font-semibold hover:ring-2 hover:ring-blue-400 {{ $cell['status']->badgeClasses() }}"> class="js-project-cell inline-block w-full cursor-pointer rounded px-1 py-1.5 text-xs font-semibold hover:ring-2 hover:ring-secondary {{ $cell['status']->badgeClasses() }}">
{{ $cell['display'] }} {{ $cell['display'] }}
</button> </button>
@elseif ($cell) @elseif ($cell)
@ -68,13 +68,13 @@
</tr> </tr>
@if ($row['type'] === 'project') @if ($row['type'] === 'project')
<tr class="js-detail-row hidden"> <tr class="js-detail-row hidden">
<td colspan="{{ $totalCols }}" class="bg-blue-50/60 p-0"> <td colspan="{{ $totalCols }}" class="bg-tertiary/3 p-0">
<div class="detail-outer"><div class="detail-inner js-detail-content px-6 py-4"></div></div> <div class="detail-outer"><div class="detail-inner js-detail-content px-6 py-4"></div></div>
</td> </td>
</tr> </tr>
@else @else
<tr class="js-detail-row hidden"> <tr class="js-detail-row hidden">
<td colspan="{{ $totalCols }}" class="bg-blue-50/60 p-0"> <td colspan="{{ $totalCols }}" class="bg-tertiary/3 p-0">
<div class="detail-outer"><div class="detail-inner js-detail-content px-6 py-4"> <div class="detail-outer"><div class="detail-inner js-detail-content px-6 py-4">
@include('charts._kpi-chart', ['kpi' => $item, 'chart' => $row['chart']]) @include('charts._kpi-chart', ['kpi' => $item, 'chart' => $row['chart']])
</div></div> </div></div>
@ -120,7 +120,7 @@
function collapseAll() { function collapseAll() {
document.querySelectorAll('.js-detail-row.is-open').forEach(collapseRow); document.querySelectorAll('.js-detail-row.is-open').forEach(collapseRow);
document.querySelectorAll('.js-project-cell.ring-2').forEach(function (c) { c.classList.remove('ring-2', 'ring-blue-500'); }); document.querySelectorAll('.js-project-cell.ring-2').forEach(function (c) { c.classList.remove('ring-2', 'ring-secondary'); });
} }
function section(label, text) { function section(label, text) {
@ -195,7 +195,7 @@
populate(detailRow.querySelector('.js-detail-content'), JSON.parse(cell.dataset.details)); populate(detailRow.querySelector('.js-detail-content'), JSON.parse(cell.dataset.details));
detailRow.dataset.month = cell.dataset.month; detailRow.dataset.month = cell.dataset.month;
expandRow(detailRow); expandRow(detailRow);
cell.classList.add('ring-2', 'ring-blue-500'); cell.classList.add('ring-2', 'ring-secondary');
} }
return; return;
} }

View File

@ -33,7 +33,7 @@
{{-- Value line (gaps split the path; pathLength normalizes the draw-in animation) --}} {{-- Value line (gaps split the path; pathLength normalizes the draw-in animation) --}}
@foreach ($chart['paths'] as $path) @foreach ($chart['paths'] as $path)
<path d="{{ $path }}" pathLength="1" class="kpi-line" <path d="{{ $path }}" pathLength="1" class="kpi-line"
fill="none" stroke="{{ $chart['lineColor'] }}" stroke-width="2" fill="none" style="stroke: var(--theme-chart-line)" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" /> stroke-linecap="round" stroke-linejoin="round" />
@endforeach @endforeach

View File

@ -10,7 +10,7 @@
<p class="text-gray-600"> <p class="text-gray-600">
You are not an owner of any group, so you can't create group charts. You are not an owner of any group, so you can't create group charts.
Ask a group owner (or an administrator) to make you an owner, or use Ask a group owner (or an administrator) to make you an owner, or use
<a href="{{ route('charts.personal') }}" class="text-blue-600 hover:underline">your personal bowler</a>. <a href="{{ route('charts.personal') }}" class="text-primary hover:underline">your personal bowler</a>.
</p> </p>
@else @else
<form method="POST" action="{{ route('charts.store') }}"> <form method="POST" action="{{ route('charts.store') }}">
@ -19,7 +19,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="group_id" class="mb-1 block font-semibold">Group</label> <label for="group_id" class="mb-1 block font-semibold">Group</label>
<select id="group_id" name="group_id" required <select id="group_id" name="group_id" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@foreach ($groups as $group) @foreach ($groups as $group)
<option value="{{ $group->id }}" {{ (int) old('group_id') === $group->id ? 'selected' : '' }}>{{ $group->name }}</option> <option value="{{ $group->id }}" {{ (int) old('group_id') === $group->id ? 'selected' : '' }}>{{ $group->name }}</option>
@endforeach @endforeach
@ -32,7 +32,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required <input id="name" type="text" name="name" value="{{ old('name') }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -41,14 +41,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="2" <textarea id="description" name="description" rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description') }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description') }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create chart</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create chart</button>
<a href="{{ route('charts.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('charts.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -14,7 +14,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $chart->name) }}" required <input id="name" type="text" name="name" value="{{ old('name', $chart->name) }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -23,14 +23,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="2" <textarea id="description" name="description" rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description', $chart->description) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description', $chart->description) }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save changes</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save changes</button>
<a href="{{ route('charts.show', $chart) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('charts.show', $chart) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,16 +5,16 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">Bowler Charts</h1> <h1 class="text-2xl font-bold">Bowler Charts</h1>
<a href="{{ route('charts.create') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">New chart</a> <a href="{{ route('charts.create') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">New chart</a>
</div> </div>
{{-- Personal chart --}} {{-- Personal chart --}}
<div class="mb-6 flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 px-5 py-4"> <div class="mb-6 flex items-center justify-between rounded-lg border border-tertiary/25 bg-tertiary/5 px-5 py-4">
<div> <div>
<a href="{{ route('charts.personal') }}" class="font-semibold text-blue-800 hover:underline">My Bowler</a> <a href="{{ route('charts.personal') }}" class="font-semibold text-tertiary hover:underline">My Bowler</a>
<p class="text-sm text-blue-700/80">Your personal chart with all of your own KPIs and projects.</p> <p class="text-sm text-tertiary/80">Your personal chart with all of your own KPIs and projects.</p>
</div> </div>
<a href="{{ route('charts.personal') }}" class="rounded-md border border-blue-600 px-3 py-1.5 text-sm text-blue-700 hover:bg-blue-100">Open</a> <a href="{{ route('charts.personal') }}" class="rounded-md border border-tertiary px-3 py-1.5 text-sm text-tertiary hover:bg-tertiary/10">Open</a>
</div> </div>
@if ($chartsByGroup->isEmpty()) @if ($chartsByGroup->isEmpty())

View File

@ -9,7 +9,7 @@
@guest @guest
<p class="mt-6"> <p class="mt-6">
<a href="{{ route('login') }}" class="inline-block rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Log in to get started</a> <a href="{{ route('login') }}" class="inline-block rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Log in to get started</a>
</p> </p>
@endguest @endguest
@ -17,7 +17,7 @@
<p class="mt-6">You are logged in as <strong>{{ auth()->user()->name }}</strong>.</p> <p class="mt-6">You are logged in as <strong>{{ auth()->user()->name }}</strong>.</p>
@if (auth()->user()->isAdmin()) @if (auth()->user()->isAdmin())
<p class="mt-4"> <p class="mt-4">
<a href="{{ route('admin.users.index') }}" class="inline-block rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Manage users</a> <a href="{{ route('admin.users.index') }}" class="inline-block rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Manage users</a>
</p> </p>
@endif @endif
@endauth @endauth

View File

@ -29,7 +29,7 @@
@endif @endif
</label> </label>
<input id="value" type="number" step="any" name="value" value="{{ old('value', $entry?->value) }}" required <input id="value" type="number" step="any" name="value" value="{{ old('value', $entry?->value) }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('value') @error('value')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror

View File

@ -4,7 +4,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $kpi?->name) }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name', $kpi?->name) }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -13,7 +13,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="2" <textarea id="description" name="description" rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description', $kpi?->description) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description', $kpi?->description) }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -25,7 +25,7 @@
<div> <div>
<label for="value_type" class="mb-1 block font-semibold">Measurement type</label> <label for="value_type" class="mb-1 block font-semibold">Measurement type</label>
<select id="value_type" name="value_type" <select id="value_type" name="value_type"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@foreach (\App\Enums\KpiValueType::cases() as $type) @foreach (\App\Enums\KpiValueType::cases() as $type)
<option value="{{ $type->value }}" {{ old('value_type', $kpi?->value_type?->value ?? 'number') === $type->value ? 'selected' : '' }}> <option value="{{ $type->value }}" {{ old('value_type', $kpi?->value_type?->value ?? 'number') === $type->value ? 'selected' : '' }}>
{{ $type->label() }} {{ $type->label() }}
@ -36,7 +36,7 @@
<div> <div>
<label for="unit_label" class="mb-1 block font-semibold">Unit label <span class="font-normal text-gray-500">(optional)</span></label> <label for="unit_label" class="mb-1 block font-semibold">Unit label <span class="font-normal text-gray-500">(optional)</span></label>
<input id="unit_label" type="text" name="unit_label" value="{{ old('unit_label', $kpi?->unit_label) }}" placeholder="e.g. days, units, $" <input id="unit_label" type="text" name="unit_label" value="{{ old('unit_label', $kpi?->unit_label) }}" placeholder="e.g. days, units, $"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('unit_label') @error('unit_label')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -46,7 +46,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="evaluation" class="mb-1 block font-semibold">Evaluation</label> <label for="evaluation" class="mb-1 block font-semibold">Evaluation</label>
<select id="evaluation" name="evaluation" <select id="evaluation" name="evaluation"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@foreach (\App\Enums\KpiEvaluation::cases() as $mode) @foreach (\App\Enums\KpiEvaluation::cases() as $mode)
<option value="{{ $mode->value }}" {{ old('evaluation', $kpi?->evaluation?->value ?? 'higher_is_better') === $mode->value ? 'selected' : '' }}> <option value="{{ $mode->value }}" {{ old('evaluation', $kpi?->evaluation?->value ?? 'higher_is_better') === $mode->value ? 'selected' : '' }}>
{{ $mode->label() }} {{ $mode->label() }}
@ -63,7 +63,7 @@
<div> <div>
<label for="green_threshold" class="mb-1 block font-semibold">Green threshold</label> <label for="green_threshold" class="mb-1 block font-semibold">Green threshold</label>
<input id="green_threshold" type="number" step="any" name="green_threshold" value="{{ old('green_threshold', $kpi?->green_threshold) }}" <input id="green_threshold" type="number" step="any" name="green_threshold" value="{{ old('green_threshold', $kpi?->green_threshold) }}"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
<p class="mt-1 text-sm text-gray-500" data-hint-higher>Green when value this</p> <p class="mt-1 text-sm text-gray-500" data-hint-higher>Green when value this</p>
<p class="mt-1 hidden text-sm text-gray-500" data-hint-lower>Green when value this</p> <p class="mt-1 hidden text-sm text-gray-500" data-hint-lower>Green when value this</p>
@error('green_threshold') @error('green_threshold')
@ -73,7 +73,7 @@
<div> <div>
<label for="yellow_threshold" class="mb-1 block font-semibold">Yellow threshold</label> <label for="yellow_threshold" class="mb-1 block font-semibold">Yellow threshold</label>
<input id="yellow_threshold" type="number" step="any" name="yellow_threshold" value="{{ old('yellow_threshold', $kpi?->yellow_threshold) }}" <input id="yellow_threshold" type="number" step="any" name="yellow_threshold" value="{{ old('yellow_threshold', $kpi?->yellow_threshold) }}"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
<p class="mt-1 text-sm text-gray-500">Beyond yellow is red</p> <p class="mt-1 text-sm text-gray-500">Beyond yellow is red</p>
@error('yellow_threshold') @error('yellow_threshold')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@ -86,7 +86,7 @@
<div> <div>
<label for="target" class="mb-1 block font-semibold">Target</label> <label for="target" class="mb-1 block font-semibold">Target</label>
<input id="target" type="number" step="any" name="target" value="{{ old('target', $kpi?->target) }}" <input id="target" type="number" step="any" name="target" value="{{ old('target', $kpi?->target) }}"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('target') @error('target')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -94,7 +94,7 @@
<div> <div>
<label for="green_tolerance" class="mb-1 block font-semibold">Green ±</label> <label for="green_tolerance" class="mb-1 block font-semibold">Green ±</label>
<input id="green_tolerance" type="number" step="any" min="0" name="green_tolerance" value="{{ old('green_tolerance', $kpi?->green_tolerance) }}" <input id="green_tolerance" type="number" step="any" min="0" name="green_tolerance" value="{{ old('green_tolerance', $kpi?->green_tolerance) }}"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('green_tolerance') @error('green_tolerance')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -102,7 +102,7 @@
<div> <div>
<label for="yellow_tolerance" class="mb-1 block font-semibold">Yellow ±</label> <label for="yellow_tolerance" class="mb-1 block font-semibold">Yellow ±</label>
<input id="yellow_tolerance" type="number" step="any" min="0" name="yellow_tolerance" value="{{ old('yellow_tolerance', $kpi?->yellow_tolerance) }}" <input id="yellow_tolerance" type="number" step="any" min="0" name="yellow_tolerance" value="{{ old('yellow_tolerance', $kpi?->yellow_tolerance) }}"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('yellow_tolerance') @error('yellow_tolerance')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror

View File

@ -13,7 +13,7 @@
@csrf @csrf
@include('kpis._form') @include('kpis._form')
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create KPI</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create KPI</button>
<a href="{{ route('kpis.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('kpis.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -11,7 +11,7 @@
@method('PUT') @method('PUT')
@include('kpis._form') @include('kpis._form')
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save changes</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save changes</button>
<a href="{{ route('kpis.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('kpis.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -34,7 +34,7 @@
<div> <div>
<label for="entry_date" class="mb-1 block font-semibold">Date</label> <label for="entry_date" class="mb-1 block font-semibold">Date</label>
<input id="entry_date" type="date" name="entry_date" value="{{ old('entry_date', now()->format('Y-m-d')) }}" required <input id="entry_date" type="date" name="entry_date" value="{{ old('entry_date', now()->format('Y-m-d')) }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('entry_date') @error('entry_date')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -43,13 +43,13 @@
<div> <div>
<label for="notes" class="mb-1 block font-semibold">Notes <span class="font-normal text-gray-500">(optional)</span></label> <label for="notes" class="mb-1 block font-semibold">Notes <span class="font-normal text-gray-500">(optional)</span></label>
<input id="notes" type="text" name="notes" value="{{ old('notes') }}" <input id="notes" type="text" name="notes" value="{{ old('notes') }}"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('notes') @error('notes')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="self-end"> <div class="self-end">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Add</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Add</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -14,7 +14,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="entry_date" class="mb-1 block font-semibold">Date</label> <label for="entry_date" class="mb-1 block font-semibold">Date</label>
<input id="entry_date" type="date" name="entry_date" value="{{ old('entry_date', $entry->entry_date->format('Y-m-d')) }}" required <input id="entry_date" type="date" name="entry_date" value="{{ old('entry_date', $entry->entry_date->format('Y-m-d')) }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('entry_date') @error('entry_date')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -27,14 +27,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="notes" class="mb-1 block font-semibold">Notes <span class="font-normal text-gray-500">(optional)</span></label> <label for="notes" class="mb-1 block font-semibold">Notes <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="notes" name="notes" rows="3" <textarea id="notes" name="notes" rows="3"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('notes', $entry->notes) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('notes', $entry->notes) }}</textarea>
@error('notes') @error('notes')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save entry</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save entry</button>
<a href="{{ route('kpis.entries.index', $kpi) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('kpis.entries.index', $kpi) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,12 +5,12 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">My KPIs</h1> <h1 class="text-2xl font-bold">My KPIs</h1>
<a href="{{ route('kpis.create') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">New KPI</a> <a href="{{ route('kpis.create') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">New KPI</a>
</div> </div>
@if ($kpis->isEmpty()) @if ($kpis->isEmpty())
<div class="rounded-lg border border-gray-200 bg-white px-6 py-10 text-center text-gray-500"> <div class="rounded-lg border border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
No KPIs yet. <a href="{{ route('kpis.create') }}" class="text-blue-600 hover:underline">Create your first KPI.</a> No KPIs yet. <a href="{{ route('kpis.create') }}" class="text-primary hover:underline">Create your first KPI.</a>
</div> </div>
@else @else
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white"> <div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">

View File

@ -5,17 +5,38 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', config('app.name', 'Bowler'))</title> <title>@yield('title', config('app.name', 'Bowler'))</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}"> <link rel="stylesheet" href="{{ asset('css/app.css') }}">
@if (! $theme->isDefault())
<style>
:root {
--color-primary: {{ $theme->primary_color }};
--color-primary-hover: {{ $theme->primaryHover() }};
--color-secondary: {{ $theme->secondary_color }};
--color-tertiary: {{ $theme->tertiary_color }};
--color-nav-link: {{ $theme->navLinkColor() }};
--color-nav-text: {{ $theme->navTextColor() }};
--color-nav-muted: {{ $theme->navMutedColor() }};
--theme-nav-bg: {{ $theme->navBackgroundCss() }};
--theme-chart-line: {{ $theme->primary_color }};
}
</style>
@endif
</head> </head>
<body class="min-h-screen bg-gray-50 text-gray-900 antialiased"> <body class="min-h-screen bg-gray-50 text-gray-900 antialiased">
<header class="border-b border-gray-200 bg-white"> <header class="site-nav border-b border-gray-200">
<div class="mx-auto flex h-14 max-w-5xl items-center justify-between px-4"> <div class="mx-auto flex h-14 max-w-5xl items-center justify-between px-4">
<a href="{{ route('home') }}" class="text-lg font-bold text-gray-900">{{ config('app.name', 'Bowler') }}</a> <a href="{{ route('home') }}" class="flex items-center text-lg font-bold text-nav-text">
@if ($theme->logo_path)
<img src="{{ asset($theme->logo_path) }}" alt="{{ config('app.name', 'Bowler') }}" class="h-9 w-auto">
@else
{{ config('app.name', 'Bowler') }}
@endif
</a>
<nav class="flex items-center gap-4"> <nav class="flex items-center gap-4">
@auth @auth
<a href="{{ route('charts.index') }}" class="text-blue-600 hover:underline">Charts</a> <a href="{{ route('charts.index') }}" class="text-nav-link hover:underline">Charts</a>
<div class="relative" data-dropdown> <div class="relative" data-dropdown>
<button type="button" data-dropdown-toggle aria-expanded="false" aria-haspopup="true" <button type="button" data-dropdown-toggle aria-expanded="false" aria-haspopup="true"
class="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"> class="flex cursor-pointer items-center gap-1 text-nav-link hover:underline">
Data Data
<svg data-dropdown-chevron class="h-3 w-3 transition-transform duration-150" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg data-dropdown-chevron class="h-3 w-3 transition-transform duration-150" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@ -29,7 +50,7 @@
</div> </div>
<div class="relative" data-dropdown> <div class="relative" data-dropdown>
<button type="button" data-dropdown-toggle aria-expanded="false" aria-haspopup="true" <button type="button" data-dropdown-toggle aria-expanded="false" aria-haspopup="true"
class="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"> class="flex cursor-pointer items-center gap-1 text-nav-link hover:underline">
Account Account
<svg data-dropdown-chevron class="h-3 w-3 transition-transform duration-150" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg data-dropdown-chevron class="h-3 w-3 transition-transform duration-150" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@ -46,7 +67,7 @@
@if (auth()->user()->isAdmin()) @if (auth()->user()->isAdmin())
<div class="relative" data-dropdown> <div class="relative" data-dropdown>
<button type="button" data-dropdown-toggle aria-expanded="false" aria-haspopup="true" <button type="button" data-dropdown-toggle aria-expanded="false" aria-haspopup="true"
class="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"> class="flex cursor-pointer items-center gap-1 text-nav-link hover:underline">
Admin Admin
<svg data-dropdown-chevron class="h-3 w-3 transition-transform duration-150" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg data-dropdown-chevron class="h-3 w-3 transition-transform duration-150" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@ -56,17 +77,18 @@
class="absolute right-0 top-full z-20 mt-2 hidden w-44 rounded-md border border-gray-200 bg-white py-1 shadow-lg"> class="absolute right-0 top-full z-20 mt-2 hidden w-44 rounded-md border border-gray-200 bg-white py-1 shadow-lg">
<a href="{{ route('admin.users.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Users</a> <a href="{{ route('admin.users.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Users</a>
<a href="{{ route('admin.groups.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Groups</a> <a href="{{ route('admin.groups.index') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Groups</a>
<a href="{{ route('admin.theme.edit') }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Theme</a>
</div> </div>
</div> </div>
@endif @endif
<span class="text-gray-500">{{ auth()->user()->name }}</span> <span class="text-nav-muted">{{ auth()->user()->name }}</span>
<form method="POST" action="{{ route('logout') }}" class="inline"> <form method="POST" action="{{ route('logout') }}" class="inline">
@csrf @csrf
<button type="submit" class="cursor-pointer text-blue-600 hover:underline">Log out</button> <button type="submit" class="cursor-pointer text-nav-link hover:underline">Log out</button>
</form> </form>
@else @else
<a href="{{ route('login') }}" class="text-blue-600 hover:underline">Log in</a> <a href="{{ route('login') }}" class="text-nav-link hover:underline">Log in</a>
@endauth @endauth
</nav> </nav>
</div> </div>

View File

@ -1,16 +1,16 @@
{{-- Shown instead of a create form when the user has no teams or categories yet. --}} {{-- Shown instead of a create form when the user has no teams or categories yet. --}}
<div class="rounded-lg border border-blue-200 bg-blue-50 p-6 text-center"> <div class="rounded-lg border border-tertiary/25 bg-tertiary/5 p-6 text-center">
<p class="mb-3 font-semibold text-blue-900">First, set up your teams and categories</p> <p class="mb-3 font-semibold text-tertiary">First, set up your teams and categories</p>
<p class="mb-4 text-sm text-blue-800/80"> <p class="mb-4 text-sm text-tertiary/80">
Every KPI and project belongs to a team (e.g. "Website Development") and a category Every KPI and project belongs to a team (e.g. "Website Development") and a category
(e.g. "Performance") so bowler charts can group them. Create at least one of each to continue. (e.g. "Performance") so bowler charts can group them. Create at least one of each to continue.
</p> </p>
<div class="flex justify-center gap-3"> <div class="flex justify-center gap-3">
@if (auth()->user()->teams->isEmpty()) @if (auth()->user()->teams->isEmpty())
<a href="{{ route('teams.index') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create a team</a> <a href="{{ route('teams.index') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create a team</a>
@endif @endif
@if (auth()->user()->categories->isEmpty()) @if (auth()->user()->categories->isEmpty())
<a href="{{ route('categories.index') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create a category</a> <a href="{{ route('categories.index') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create a category</a>
@endif @endif
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
<div> <div>
<label for="team_id" class="mb-1 block font-semibold">Team</label> <label for="team_id" class="mb-1 block font-semibold">Team</label>
<select id="team_id" name="team_id" required <select id="team_id" name="team_id" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
<option value="">Select a team…</option> <option value="">Select a team…</option>
@foreach ($ownerTeams as $team) @foreach ($ownerTeams as $team)
<option value="{{ $team->id }}" {{ (int) old('team_id', $item?->team_id) === $team->id ? 'selected' : '' }}>{{ $team->name }}</option> <option value="{{ $team->id }}" {{ (int) old('team_id', $item?->team_id) === $team->id ? 'selected' : '' }}>{{ $team->name }}</option>
@ -23,7 +23,7 @@
<div> <div>
<label for="category_id" class="mb-1 block font-semibold">Category</label> <label for="category_id" class="mb-1 block font-semibold">Category</label>
<select id="category_id" name="category_id" required <select id="category_id" name="category_id" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
<option value="">Select a category…</option> <option value="">Select a category…</option>
@foreach ($ownerCategories as $category) @foreach ($ownerCategories as $category)
<option value="{{ $category->id }}" {{ (int) old('category_id', $item?->category_id) === $category->id ? 'selected' : '' }}>{{ $category->name }}</option> <option value="{{ $category->id }}" {{ (int) old('category_id', $item?->category_id) === $category->id ? 'selected' : '' }}>{{ $category->name }}</option>
@ -36,6 +36,6 @@
</div> </div>
<p class="-mt-2 mb-4 text-sm text-gray-500"> <p class="-mt-2 mb-4 text-sm text-gray-500">
Used to group items on bowler charts. Used to group items on bowler charts.
<a href="{{ route('teams.index') }}" class="text-blue-600 hover:underline">Manage teams</a> · <a href="{{ route('teams.index') }}" class="text-primary hover:underline">Manage teams</a> ·
<a href="{{ route('categories.index') }}" class="text-blue-600 hover:underline">Manage categories</a> <a href="{{ route('categories.index') }}" class="text-primary hover:underline">Manage categories</a>
</p> </p>

View File

@ -6,7 +6,7 @@
<label for="entry_date" class="mb-1 block font-semibold">Date</label> <label for="entry_date" class="mb-1 block font-semibold">Date</label>
<input id="entry_date" type="date" name="entry_date" <input id="entry_date" type="date" name="entry_date"
value="{{ old('entry_date', $entry?->entry_date?->format('Y-m-d') ?? now()->format('Y-m-d')) }}" required value="{{ old('entry_date', $entry?->entry_date?->format('Y-m-d') ?? now()->format('Y-m-d')) }}" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('entry_date') @error('entry_date')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -14,7 +14,7 @@
<div> <div>
<label for="status" class="mb-1 block font-semibold">Status</label> <label for="status" class="mb-1 block font-semibold">Status</label>
<select id="status" name="status" required <select id="status" name="status" required
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@foreach (\App\Enums\RagStatus::cases() as $status) @foreach (\App\Enums\RagStatus::cases() as $status)
<option value="{{ $status->value }}" {{ old('status', $entry?->status?->value) === $status->value ? 'selected' : '' }}> <option value="{{ $status->value }}" {{ old('status', $entry?->status?->value) === $status->value ? 'selected' : '' }}>
{{ $status->label() }} {{ $status->label() }}
@ -30,7 +30,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="notes" class="mb-1 block font-semibold">Notes <span class="font-normal text-gray-500">(optional)</span></label> <label for="notes" class="mb-1 block font-semibold">Notes <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="notes" name="notes" rows="2" placeholder="Overall status description" <textarea id="notes" name="notes" rows="2" placeholder="Overall status description"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('notes', $entry?->notes) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('notes', $entry?->notes) }}</textarea>
@error('notes') @error('notes')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -41,7 +41,7 @@
<div> <div>
<label for="{{ $field }}" class="mb-1 block font-semibold">{{ $label }} <span class="font-normal text-gray-500">(optional)</span></label> <label for="{{ $field }}" class="mb-1 block font-semibold">{{ $label }} <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="{{ $field }}" name="{{ $field }}" rows="2" <textarea id="{{ $field }}" name="{{ $field }}" rows="2"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old($field, $entry?->{$field}) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old($field, $entry?->{$field}) }}</textarea>
@error($field) @error($field)
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror

View File

@ -15,7 +15,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name') }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -26,7 +26,7 @@
<div class="mb-6"> <div class="mb-6">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description') }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description') }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -35,7 +35,7 @@
@include('partials.chart-sharing') @include('partials.chart-sharing')
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Create project</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Create project</button>
<a href="{{ route('projects.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('projects.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -13,7 +13,7 @@
<div class="mb-4"> <div class="mb-4">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $project->name) }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name', $project->name) }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -24,7 +24,7 @@
<div class="mb-6"> <div class="mb-6">
<label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label> <label for="description" class="mb-1 block font-semibold">Description <span class="font-normal text-gray-500">(optional)</span></label>
<textarea id="description" name="description" rows="3" <textarea id="description" name="description" rows="3"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40">{{ old('description', $project->description) }}</textarea> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">{{ old('description', $project->description) }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
@ -33,7 +33,7 @@
@include('partials.chart-sharing', ['item' => $project]) @include('partials.chart-sharing', ['item' => $project])
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save changes</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save changes</button>
<a href="{{ route('projects.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('projects.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -30,7 +30,7 @@
<form method="POST" action="{{ route('projects.entries.store', $project) }}" class="border-t border-gray-200 p-4"> <form method="POST" action="{{ route('projects.entries.store', $project) }}" class="border-t border-gray-200 p-4">
@csrf @csrf
@include('projects._entry-fields') @include('projects._entry-fields')
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Add entry</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Add entry</button>
</form> </form>
</details> </details>
@endcan @endcan

View File

@ -12,7 +12,7 @@
@method('PUT') @method('PUT')
@include('projects._entry-fields', ['entry' => $entry]) @include('projects._entry-fields', ['entry' => $entry])
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save entry</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save entry</button>
<a href="{{ route('projects.entries.index', $project) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('projects.entries.index', $project) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,12 +5,12 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">My Projects</h1> <h1 class="text-2xl font-bold">My Projects</h1>
<a href="{{ route('projects.create') }}" class="rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">New project</a> <a href="{{ route('projects.create') }}" class="rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">New project</a>
</div> </div>
@if ($projects->isEmpty()) @if ($projects->isEmpty())
<div class="rounded-lg border border-gray-200 bg-white px-6 py-10 text-center text-gray-500"> <div class="rounded-lg border border-gray-200 bg-white px-6 py-10 text-center text-gray-500">
No projects yet. <a href="{{ route('projects.create') }}" class="text-blue-600 hover:underline">Create your first project.</a> No projects yet. <a href="{{ route('projects.create') }}" class="text-primary hover:underline">Create your first project.</a>
</div> </div>
@else @else
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white"> <div class="overflow-x-auto rounded-lg border border-gray-200 bg-white">

View File

@ -13,14 +13,14 @@
<div class="mb-6"> <div class="mb-6">
<label for="name" class="mb-1 block font-semibold">Name</label> <label for="name" class="mb-1 block font-semibold">Name</label>
<input id="name" type="text" name="name" value="{{ old('name', $team->name) }}" required autofocus <input id="name" type="text" name="name" value="{{ old('name', $team->name) }}" required autofocus
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Save</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Save</button>
<a href="{{ route('teams.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a> <a href="{{ route('teams.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-900 hover:bg-gray-50">Cancel</a>
</div> </div>
</form> </form>

View File

@ -5,7 +5,7 @@
@section('content') @section('content')
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h1 class="text-2xl font-bold">My Teams</h1> <h1 class="text-2xl font-bold">My Teams</h1>
<a href="{{ route('categories.index') }}" class="text-blue-600 hover:underline">Manage categories </a> <a href="{{ route('categories.index') }}" class="text-primary hover:underline">Manage categories </a>
</div> </div>
<p class="mb-4 text-sm text-gray-500">Teams group your KPIs and projects on bowler charts e.g. "Website Development" or "Integrations".</p> <p class="mb-4 text-sm text-gray-500">Teams group your KPIs and projects on bowler charts e.g. "Website Development" or "Integrations".</p>
@ -19,9 +19,9 @@
<div class="flex-1"> <div class="flex-1">
<label for="name" class="mb-1 block font-semibold">New team</label> <label for="name" class="mb-1 block font-semibold">New team</label>
<input id="name" type="text" name="name" value="{{ old('name') }}" required placeholder="e.g. Website Development" <input id="name" type="text" name="name" value="{{ old('name') }}" required placeholder="e.g. Website Development"
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/40"> class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-secondary focus:outline-none focus:ring-2 focus:ring-secondary/40">
</div> </div>
<button type="submit" class="cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700">Add team</button> <button type="submit" class="cursor-pointer rounded-md bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover">Add team</button>
</form> </form>
@error('name') @error('name')
<p class="mt-1 text-sm text-red-700">{{ $message }}</p> <p class="mt-1 text-sm text-red-700">{{ $message }}</p>

View File

@ -10,6 +10,7 @@ use App\Http\Controllers\KpiEntryController;
use App\Http\Controllers\ProjectController; use App\Http\Controllers\ProjectController;
use App\Http\Controllers\ProjectEntryController; use App\Http\Controllers\ProjectEntryController;
use App\Http\Controllers\TeamController; use App\Http\Controllers\TeamController;
use App\Http\Controllers\ThemeController;
use App\Http\Controllers\UserAdminController; use App\Http\Controllers\UserAdminController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@ -110,4 +111,8 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun
Route::post('/groups/{group}/members', [GroupAdminController::class, 'addMember'])->name('groups.members.add'); Route::post('/groups/{group}/members', [GroupAdminController::class, 'addMember'])->name('groups.members.add');
Route::put('/groups/{group}/members/{user}/role', [GroupAdminController::class, 'updateMemberRole'])->name('groups.members.role'); Route::put('/groups/{group}/members/{user}/role', [GroupAdminController::class, 'updateMemberRole'])->name('groups.members.role');
Route::delete('/groups/{group}/members/{user}', [GroupAdminController::class, 'removeMember'])->name('groups.members.remove'); Route::delete('/groups/{group}/members/{user}', [GroupAdminController::class, 'removeMember'])->name('groups.members.remove');
Route::get('/theme', [ThemeController::class, 'edit'])->name('theme.edit');
Route::put('/theme', [ThemeController::class, 'update'])->name('theme.update');
Route::post('/theme/reset', [ThemeController::class, 'reset'])->name('theme.reset');
}); });