Included
* Standardised all six theme.json files to manifest version 2:
* admin
* common
* corporate
* saasland
* swiss
* welcome
* Every manifest now uses the same fields and structure.
* Separate active frontend and backend themes.
* One-click theme activation.
* Theme preview in a new browser tab without activating it.
* Theme cards showing:
* Preview
* Name
* Version
* Author
* Description
* Theme type
* Active status
* Dynamic theme-specific settings.
* Support for:
* Text
* URL
* Colour
* Textarea
* Checkbox fields
* Variants
* Appearance
* Accent palettes
* Theme ZIP installation.
* Theme ZIP export.
* Asset optimisation.
* Accessibility validation.
* Safe ZIP path validation.
* Theme inheritance.
* Active themes cannot be uninstalled.
* Parent themes cannot be uninstalled while child themes depend on them.
* Stylist administration routes now require an authenticated, verified user.
* Manifest validation now restricts theme types to:
* frontend
* backend
* common
* Added activation tests for independent frontend and backend theme selection.
The supplied corporate theme was incomplete, so I added the required backend entry view and placeholder asset files while preserving its supplied layout.
79 lines
8.6 KiB
PHP
79 lines
8.6 KiB
PHP
<x-app-layout>
|
|
<x-wms.page-hero eyebrow="Administration" title="Stylist" subtitle="Install, configure, validate and export self-contained themes." />
|
|
|
|
@if (session('status'))
|
|
<div class="mb-6 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-emerald-800">{{ session('status') }}</div>
|
|
@endif
|
|
@if ($errors->any())
|
|
<div class="mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-red-800">{{ $errors->first() }}</div>
|
|
@endif
|
|
|
|
<form method="POST" action="{{ route('stylist.install') }}" enctype="multipart/form-data" class="mb-8 flex flex-wrap items-end gap-3 rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
@csrf
|
|
<label class="min-w-64 flex-1"><span class="mb-2 block text-sm font-medium text-slate-700">Install theme ZIP</span><input type="file" name="theme" accept=".zip" required class="block w-full rounded-lg border border-slate-300 p-2"></label>
|
|
<button class="rounded-lg bg-slate-900 px-4 py-2.5 font-semibold text-white">Install theme</button>
|
|
</form>
|
|
|
|
<div class="grid gap-6 xl:grid-cols-2">
|
|
@foreach ($themes as $item)
|
|
@php($saved = $settingsService->get($item))
|
|
@php($variant = $settingsService->variant($item))
|
|
@php($issues = $accessibility->validate($item))
|
|
@php($appearance = $settingsService->appearance($item))
|
|
@php($accent = $settingsService->accent($item))
|
|
<article class="overflow-hidden rounded-2xl border {{ (($item->type() === 'frontend' && $item->key() === $activeFrontendTheme) || ($item->type() === 'backend' && $item->key() === $activeBackendTheme)) ? 'border-blue-400 ring-2 ring-blue-100' : 'border-slate-200' }} bg-white shadow-sm">
|
|
<div class="aspect-[16/7] bg-slate-100">
|
|
@if ($item->preview() && $item->has($item->preview()))
|
|
<img src="{{ $urls->forTheme($item, $item->preview()) }}" alt="{{ $item->name() }} theme preview" class="h-full w-full object-cover">
|
|
@else
|
|
<div class="flex h-full items-center justify-center text-sm text-slate-500">Add preview.webp to this theme</div>
|
|
@endif
|
|
</div>
|
|
<div class="p-5">
|
|
<div class="flex items-start justify-between gap-4"><div><h2 class="text-xl font-bold text-slate-950">{{ $item->name() }}</h2><p class="text-sm text-slate-500">v{{ $item->version() }}@if($item->author()) · {{ $item->author() }}@endif</p></div>@if(($item->type() === 'frontend' && $item->key() === $activeFrontendTheme) || ($item->type() === 'backend' && $item->key() === $activeBackendTheme))<span class="rounded-full bg-blue-100 px-3 py-1 text-xs font-semibold text-blue-700">Active {{ ucfirst($item->type()) }}</span>@endif</div>
|
|
@if($item->description())<p class="mt-3 text-sm text-slate-600">{{ $item->description() }}</p>@endif
|
|
@if($item->parent())<p class="mt-2 text-xs font-medium text-violet-700">Child of {{ $item->parent() }}</p>@endif
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
@if(in_array($item->type(), ['frontend', 'backend'], true))
|
|
<form method="POST" action="{{ route('stylist.activate', $item->key()) }}">@csrf<button class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white">Activate {{ ucfirst($item->type()) }}</button></form>
|
|
<a href="{{ route('stylist.preview', $item->key()) }}" target="_blank" rel="noopener" class="rounded-lg border border-blue-300 px-3 py-2 text-sm font-medium text-blue-700">Preview</a>
|
|
@endif
|
|
<a href="{{ route('stylist.export', $item->key()) }}" class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium">Export ZIP</a>
|
|
<form method="POST" action="{{ route('stylist.optimize', $item->key()) }}">@csrf<button class="rounded-lg border border-slate-300 px-3 py-2 text-sm font-medium">Optimise assets</button></form>
|
|
@if(str_starts_with(realpath($item->path()) ?: '', realpath(config('stylist.themes_path')) ?: '/__never__'))
|
|
<form method="POST" action="{{ route('stylist.uninstall', $item->key()) }}" onsubmit="return confirm('Uninstall this theme?')">@csrf @method('DELETE')<button class="rounded-lg border border-red-300 px-3 py-2 text-sm font-medium text-red-700">Uninstall</button></form>
|
|
@endif
|
|
</div>
|
|
<details class="mt-5 rounded-xl border border-slate-200 p-4">
|
|
<summary class="cursor-pointer font-semibold">Theme settings</summary>
|
|
<form method="POST" action="{{ route('stylist.settings', $item->key()) }}" class="mt-4 space-y-4">@csrf @method('PUT')
|
|
@if($item->variants())
|
|
<label class="block"><span class="mb-1 block text-sm font-medium">Variant</span><select name="variant" class="w-full rounded-lg border-slate-300">@foreach(array_keys($item->variants()) as $name)<option value="{{ $name }}" @selected($variant === $name)>{{ ucfirst($name) }}</option>@endforeach</select></label>
|
|
@endif
|
|
@if($item->supports('dark_mode') || $item->appearances())
|
|
<label class="block"><span class="mb-1 block text-sm font-medium">Appearance</span><select name="appearance" class="w-full rounded-lg border-slate-300">@foreach(array_keys($item->appearances()) ?: ['light', 'dark', 'auto'] as $name)<option value="{{ $name }}" @selected($appearance === $name)>{{ ucfirst($name) }}</option>@endforeach</select></label>
|
|
@endif
|
|
@if($item->accents())
|
|
<div><span class="mb-2 block text-sm font-medium">Accent colour</span><div class="flex flex-wrap gap-2">@foreach($item->accents() as $name => $definition)<label class="cursor-pointer"><input type="radio" name="accent" value="{{ $name }}" class="peer sr-only" @checked($accent === $name)><span class="inline-flex items-center gap-2 rounded-lg border border-slate-300 px-3 py-2 text-sm peer-checked:border-blue-500 peer-checked:ring-2 peer-checked:ring-blue-100"><span class="h-4 w-4 rounded-full border border-black/10" style="background: {{ data_get($definition, 'swatch', data_get($definition, 'variables.primary', '#64748b')) }}"></span>{{ data_get($definition, 'label', ucfirst($name)) }}</span></label>@endforeach</div></div>
|
|
@endif
|
|
@foreach($item->settingsSchema() as $key => $definition)
|
|
@php($definition = is_array($definition) ? $definition : ['default' => $definition])
|
|
<label class="block"><span class="mb-1 block text-sm font-medium">{{ $definition['label'] ?? str($key)->headline() }}</span>
|
|
@switch($definition['type'] ?? 'text')
|
|
@case('color')<input type="color" name="settings[{{ $key }}]" value="{{ $saved[$key] ?? ($definition['default'] ?? '#000000') }}" class="h-10 w-full rounded-lg border-slate-300">@break
|
|
@case('textarea')<textarea name="settings[{{ $key }}]" rows="4" class="w-full rounded-lg border-slate-300">{{ $saved[$key] ?? ($definition['default'] ?? '') }}</textarea>@break
|
|
@case('checkbox')<input type="hidden" name="settings[{{ $key }}]" value="0"><label class="inline-flex items-center gap-2"><input type="checkbox" name="settings[{{ $key }}]" value="1" @checked((bool)($saved[$key] ?? ($definition['default'] ?? false))) class="rounded border-slate-300"><span>Enabled</span></label>@break
|
|
@default<input type="{{ ($definition['type'] ?? 'text') === 'url' ? 'url' : 'text' }}" name="settings[{{ $key }}]" value="{{ $saved[$key] ?? ($definition['default'] ?? '') }}" class="w-full rounded-lg border-slate-300">
|
|
@endswitch
|
|
</label>
|
|
@endforeach
|
|
<button class="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white">Save settings</button>
|
|
</form>
|
|
</details>
|
|
<div class="mt-4 text-sm {{ collect($issues)->contains('severity', 'error') ? 'text-red-700' : 'text-emerald-700' }}">Accessibility: {{ count($issues) ? count($issues).' issue(s)' : 'passed static checks' }}</div>
|
|
</div>
|
|
</article>
|
|
@endforeach
|
|
</div>
|
|
</x-app-layout>
|