162 lines
10 KiB
PHP
162 lines
10 KiB
PHP
{{--
|
|
Thirteen answers on one screen.
|
|
|
|
The first version was one column: name, subject, two clamped lines of body
|
|
and two full-width buttons per entry. Thirteen of those is four screens of
|
|
scrolling to find the one you want, and the body preview was the least
|
|
useful part of it — the whole text is one click away in the modal anyway.
|
|
|
|
So: two columns of one-line entries, actions as icons that do not compete
|
|
with the name, the create form folded away behind a button, and the
|
|
placeholder list as a compact grid rather than a tall sidebar that pushed the
|
|
entries into half the window.
|
|
--}}
|
|
<div class="mx-auto max-w-[1120px] space-y-5" x-data="{ creating: false }">
|
|
<div class="flex flex-wrap items-start gap-4 animate-rise">
|
|
<div class="min-w-0">
|
|
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('templates.title') }}</h1>
|
|
<p class="mt-1 max-w-[75ch] text-sm text-muted">{{ __('templates.subtitle') }}</p>
|
|
</div>
|
|
<x-ui.button variant="primary" class="ml-auto" x-on:click="creating = !creating">
|
|
<x-ui.icon name="plus" class="size-4" />{{ __('templates.new') }}
|
|
</x-ui.button>
|
|
</div>
|
|
|
|
{{-- Folded away by default. A form that is always open is a screen of form
|
|
between the operator and the thing they came for — and creating a
|
|
template is the rarer of the two things done here. It stays a form that
|
|
IS the page (R20 exempts exactly that); only its visibility changed. --}}
|
|
<form wire:submit="create" x-show="creating" x-cloak x-transition
|
|
class="space-y-4 rounded-lg border border-accent-border bg-surface p-6 shadow-xs">
|
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
<x-ui.input name="name" wire:model="name" :label="__('templates.name')" :placeholder="__('templates.name_hint')" />
|
|
<x-ui.input name="subject" wire:model="subject" :label="__('templates.subject')" :placeholder="__('templates.subject_hint')" />
|
|
</div>
|
|
|
|
<div>
|
|
<label for="body" class="block text-sm font-medium text-body">{{ __('templates.body') }}</label>
|
|
<textarea id="body" rows="8" wire:model="body"
|
|
class="mt-1.5 block w-full rounded border border-line bg-surface px-3.5 py-2.5 text-sm text-ink placeholder:text-faint"
|
|
placeholder="{{ __('templates.body_hint') }}"></textarea>
|
|
@error('body')<p class="mt-1.5 text-xs text-danger">{{ $message }}</p>@enderror
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<x-ui.button type="submit" variant="primary" wire:loading.attr="disabled" wire:target="create">
|
|
{{ __('templates.create') }}
|
|
</x-ui.button>
|
|
<x-ui.button variant="secondary" x-on:click="creating = false">{{ __('templates.cancel') }}</x-ui.button>
|
|
</div>
|
|
</form>
|
|
|
|
{{-- The placeholders, as a grid across the page rather than a column beside
|
|
it. Rendered from the renderer's own list, so a token added there
|
|
appears here without being written down twice. --}}
|
|
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
|
|
<div class="flex flex-wrap items-baseline gap-x-4 gap-y-1">
|
|
<h2 class="font-semibold text-ink">{{ __('templates.placeholders') }}</h2>
|
|
<p class="text-xs text-muted">{{ __('templates.placeholders_note') }}</p>
|
|
</div>
|
|
|
|
<dl class="mt-4 grid gap-x-6 gap-y-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
@foreach ($placeholders as $token => $explanation)
|
|
{{-- Assembled in PHP, not inline: Blade's echo tag ends at the
|
|
first }} it finds, so a literal one inside the expression
|
|
cuts the tag in half and compiles to nonsense. --}}
|
|
@php $literal = '{{'.$token.'}}'; @endphp
|
|
<div class="min-w-0">
|
|
<dt class="select-all font-mono text-xs font-semibold text-accent-text">{{ $literal }}</dt>
|
|
<dd class="mt-0.5 text-xs leading-snug text-muted">{{ __($explanation) }}</dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
|
|
<p class="mt-4 border-t border-line pt-3 text-xs leading-relaxed text-muted">{{ __('templates.unknown_note') }}</p>
|
|
</div>
|
|
|
|
{{-- The entries. Two columns, one line each: what an operator scans for is
|
|
the name, and the subject is the only other thing that tells two similar
|
|
ones apart. The text itself is in the modal. --}}
|
|
@if ($templates->isEmpty())
|
|
<div class="rounded-lg border border-line bg-surface p-10 text-center shadow-xs animate-rise">
|
|
<p class="text-sm text-muted">{{ __('templates.empty') }}</p>
|
|
</div>
|
|
@else
|
|
<div class="grid gap-3 lg:grid-cols-2 animate-rise [animation-delay:120ms]">
|
|
@foreach ($templates as $template)
|
|
{{-- `opening` is local to the row: fetching the modal is a round
|
|
trip, and a click with nothing to show for it reads as a
|
|
click that missed. It clears when the modal is up (the
|
|
package announces that) and on a timer as well, so a request
|
|
that never answers cannot leave the row spinning for ever. --}}
|
|
<div wire:key="tpl-{{ $template->uuid }}"
|
|
x-data="{ opening: false }"
|
|
x-on:modal-opened.window="opening = false"
|
|
x-on:modalclosed.window="opening = false"
|
|
wire:loading.class="opacity-60"
|
|
wire:target="move('{{ $template->uuid }}', -1), move('{{ $template->uuid }}', 1), toggleActive('{{ $template->uuid }}')"
|
|
@class([
|
|
'group flex items-center gap-3 rounded-lg border bg-surface px-4 py-3 shadow-xs transition-colors',
|
|
'border-line hover:border-accent-border' => $template->active,
|
|
// A retired one stays visible and stays quiet.
|
|
'border-line bg-surface-2' => ! $template->active,
|
|
])>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex flex-wrap items-baseline gap-x-2">
|
|
<span @class([
|
|
'truncate text-sm font-medium',
|
|
'text-ink' => $template->active,
|
|
'text-muted line-through' => ! $template->active,
|
|
])>{{ $template->name }}</span>
|
|
@if (! $template->active)
|
|
<span class="text-[10px] font-semibold uppercase tracking-wider text-warning">{{ __('templates.retired') }}</span>
|
|
@endif
|
|
</div>
|
|
<p class="truncate text-xs text-muted">{{ $template->subject }}</p>
|
|
</div>
|
|
|
|
{{-- Every button reports its own click: pressed state on
|
|
the way down, disabled while the request is out. The
|
|
global bar at the top of the window says that SOMETHING
|
|
is happening; these say which thing. --}}
|
|
<div class="flex shrink-0 items-center gap-0.5">
|
|
<button type="button" wire:click="move('{{ $template->uuid }}', -1)"
|
|
wire:loading.attr="disabled" wire:target="move('{{ $template->uuid }}', -1)"
|
|
class="rounded p-1.5 text-faint transition-colors hover:bg-surface-hover hover:text-ink active:bg-accent-subtle active:text-accent-text disabled:opacity-40"
|
|
aria-label="{{ __('templates.up') }}" title="{{ __('templates.up') }}">
|
|
<x-ui.icon name="chevron-down" class="size-4 rotate-180" />
|
|
</button>
|
|
<button type="button" wire:click="move('{{ $template->uuid }}', 1)"
|
|
wire:loading.attr="disabled" wire:target="move('{{ $template->uuid }}', 1)"
|
|
class="rounded p-1.5 text-faint transition-colors hover:bg-surface-hover hover:text-ink active:bg-accent-subtle active:text-accent-text disabled:opacity-40"
|
|
aria-label="{{ __('templates.down') }}" title="{{ __('templates.down') }}">
|
|
<x-ui.icon name="chevron-down" class="size-4" />
|
|
</button>
|
|
<button type="button" wire:click="toggleActive('{{ $template->uuid }}')"
|
|
wire:loading.attr="disabled" wire:target="toggleActive('{{ $template->uuid }}')"
|
|
class="rounded p-1.5 text-faint transition-colors hover:bg-surface-hover hover:text-warning active:bg-warning-bg disabled:opacity-40"
|
|
aria-label="{{ $template->active ? __('templates.retire') : __('templates.restore') }}"
|
|
title="{{ $template->active ? __('templates.retire') : __('templates.restore') }}">
|
|
<x-ui.icon :name="$template->active ? 'x' : 'refresh'" class="size-4" />
|
|
</button>
|
|
{{-- R20: editing opens a modal. A five-line textarea in a
|
|
list row is the height jump the rule exists to stop.
|
|
|
|
The spinner is this button's own: opening the modal
|
|
is a Livewire request made by the MODAL component,
|
|
so wire:loading on this page never sees it. --}}
|
|
<button type="button"
|
|
x-bind:disabled="opening"
|
|
x-on:click="opening = true; setTimeout(() => opening = false, 6000); $dispatch('openModal', { component: 'admin.edit-mail-template', arguments: { uuid: '{{ $template->uuid }}' } })"
|
|
class="rounded p-1.5 text-muted transition-colors hover:bg-surface-hover hover:text-ink active:bg-accent-subtle active:text-accent-text disabled:opacity-60"
|
|
aria-label="{{ __('templates.edit') }}" title="{{ __('templates.edit') }}">
|
|
<x-ui.icon name="pencil" class="size-4" x-show="!opening" />
|
|
<x-ui.icon name="refresh" class="size-4 animate-spin text-accent-text" x-show="opening" x-cloak />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|