Deploy Ladill QR Plus / deploy (push) Successful in 28s
Full control center for ticketed events, contributions, attendees, badges, and programmes — not a QR utility clone. Includes SSO shell, import command, and platform cutover runbook. Co-authored-by: Cursor <cursoragent@cursor.com>
531 lines
34 KiB
PHP
531 lines
34 KiB
PHP
@php
|
||
$style = \App\Support\Qr\QrStyleDefaults::merge($style ?? null);
|
||
$moduleStyles = $moduleStyles ?? \App\Support\Qr\QrModuleStyleCatalog::visible();
|
||
$cornerOuterStyles = $cornerOuterStyles ?? \App\Support\Qr\QrCornerStyleCatalog::outerStyles();
|
||
$cornerInnerStyles = $cornerInnerStyles ?? \App\Support\Qr\QrCornerStyleCatalog::innerStyles();
|
||
$frameStyles = $frameStyles ?? \App\Support\Qr\QrFrameStyleCatalog::visible();
|
||
$qrBits = [1,0,1,0,1, 0,1,1,0,0, 1,1,0,1,1, 0,0,1,0,1, 1,0,1,1,0];
|
||
@endphp
|
||
|
||
@include('qr-codes.partials.qr-customizer-script')
|
||
|
||
{{-- ── Quick style presets ──────────────────────────────────────────── --}}
|
||
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||
<div class="border-b border-slate-100 bg-slate-50/70 px-5 py-3.5">
|
||
<p class="text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Quick styles</p>
|
||
</div>
|
||
<div class="grid grid-cols-4 gap-px bg-slate-100/80">
|
||
@foreach([
|
||
[
|
||
'key' => 'classic',
|
||
'label' => 'Classic',
|
||
'fg' => '#000000',
|
||
'bg' => '#ffffff',
|
||
'outer' => 'square',
|
||
'inner' => 'square',
|
||
'dot' => false,
|
||
],
|
||
[
|
||
'key' => 'rounded',
|
||
'label' => 'Rounded',
|
||
'fg' => '#111827',
|
||
'bg' => '#ffffff',
|
||
'outer' => 'rounded',
|
||
'inner' => 'square',
|
||
'dot' => false,
|
||
],
|
||
[
|
||
'key' => 'branded',
|
||
'label' => 'Branded',
|
||
'fg' => '#1d4ed8',
|
||
'bg' => '#ffffff',
|
||
'outer' => 'rounded',
|
||
'inner' => 'square',
|
||
'dot' => false,
|
||
],
|
||
[
|
||
'key' => 'dots',
|
||
'label' => 'Dots',
|
||
'fg' => '#0f172a',
|
||
'bg' => '#ffffff',
|
||
'outer' => 'square',
|
||
'inner' => 'dot',
|
||
'dot' => true,
|
||
],
|
||
] as $preset)
|
||
@php
|
||
$outerRadius = match($preset['outer']) { 'rounded' => '3px', 'circle' => '50%', default => '1px' };
|
||
$innerRadius = $preset['inner'] === 'dot' ? '50%' : '1px';
|
||
$dotRadius = $preset['dot'] ? '50%' : '1px';
|
||
@endphp
|
||
<button type="button"
|
||
@click="setStylePreset('{{ $preset['key'] }}')"
|
||
class="group flex flex-col items-center gap-2.5 bg-white px-3 py-4 text-center transition hover:bg-slate-50/80 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-400">
|
||
|
||
{{-- Mini QR illustration --}}
|
||
<span class="relative flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg"
|
||
style="background-color: {{ $preset['bg'] }}">
|
||
|
||
{{-- Top-left finder --}}
|
||
<span class="absolute left-[4px] top-[4px] flex h-[14px] w-[14px] items-center justify-center"
|
||
style="border: 2.5px solid {{ $preset['fg'] }}; border-radius: {{ $outerRadius }}; background: transparent">
|
||
<span class="block h-[5px] w-[5px]"
|
||
style="background: {{ $preset['fg'] }}; border-radius: {{ $innerRadius }}"></span>
|
||
</span>
|
||
|
||
{{-- Top-right finder --}}
|
||
<span class="absolute right-[4px] top-[4px] flex h-[14px] w-[14px] items-center justify-center"
|
||
style="border: 2.5px solid {{ $preset['fg'] }}; border-radius: {{ $outerRadius }}; background: transparent">
|
||
<span class="block h-[5px] w-[5px]"
|
||
style="background: {{ $preset['fg'] }}; border-radius: {{ $innerRadius }}"></span>
|
||
</span>
|
||
|
||
{{-- Bottom-left finder --}}
|
||
<span class="absolute bottom-[4px] left-[4px] flex h-[14px] w-[14px] items-center justify-center"
|
||
style="border: 2.5px solid {{ $preset['fg'] }}; border-radius: {{ $outerRadius }}; background: transparent">
|
||
<span class="block h-[5px] w-[5px]"
|
||
style="background: {{ $preset['fg'] }}; border-radius: {{ $innerRadius }}"></span>
|
||
</span>
|
||
|
||
{{-- Data dots (5×3 grid, avoiding finder corners) --}}
|
||
<span class="absolute inset-0 flex items-center justify-center">
|
||
<span class="grid grid-cols-3 gap-[2px] translate-x-[5px]">
|
||
@foreach([1,0,1,0,1,0,1,1,0] as $bit)
|
||
<span class="h-[3px] w-[3px]"
|
||
style="{{ $bit ? 'background:' . $preset['fg'] . ';border-radius:' . $dotRadius : '' }}"></span>
|
||
@endforeach
|
||
</span>
|
||
</span>
|
||
</span>
|
||
|
||
<span class="text-[11px] font-semibold text-slate-600 group-hover:text-slate-900">{{ $preset['label'] }}</span>
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||
|
||
{{-- Pill tab nav --}}
|
||
<div class="border-b border-slate-100 bg-slate-50/70 px-4 py-3">
|
||
<div class="flex gap-1 rounded-xl bg-slate-200/50 p-1">
|
||
@foreach([
|
||
['id' => 'body', 'label' => 'Body'],
|
||
['id' => 'colors', 'label' => 'Colors'],
|
||
['id' => 'eyes', 'label' => 'Eyes'],
|
||
['id' => 'logo', 'label' => 'Logo'],
|
||
['id' => 'frame', 'label' => 'Frame'],
|
||
] as $tab)
|
||
<button type="button"
|
||
@click="openSection = '{{ $tab['id'] }}'"
|
||
:class="openSection === '{{ $tab['id'] }}'
|
||
? 'bg-white shadow-sm text-slate-900'
|
||
: 'text-slate-500 hover:text-slate-700'"
|
||
class="flex-1 rounded-lg py-1.5 text-[11px] font-semibold transition-all duration-150">
|
||
{{ $tab['label'] }}
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="p-5">
|
||
|
||
{{-- ── Body ─────────────────────────────────────────────────────── --}}
|
||
<div x-show="openSection === 'body'" x-cloak
|
||
x-transition:enter="transition-opacity duration-150"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100">
|
||
<p class="mb-3 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Dot shape</p>
|
||
<div class="grid grid-cols-2 gap-2.5">
|
||
@foreach($moduleStyles as $key => $meta)
|
||
<label class="group cursor-pointer">
|
||
<input type="radio" name="style[module_style]" value="{{ $key }}" x-model="moduleStyle" class="sr-only">
|
||
<span class="flex items-center gap-3 rounded-xl border-2 bg-white px-3.5 py-3 transition-all"
|
||
:class="moduleStyle === '{{ $key }}'
|
||
? 'border-indigo-500 bg-indigo-50/60'
|
||
: 'border-slate-200 group-hover:border-slate-300'">
|
||
<span class="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-slate-50 ring-1 ring-slate-200/60">
|
||
@if ($key === 'square')
|
||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-slate-800" fill="currentColor">
|
||
<path d="M0,0V11H11V0H0ZM7,7h-3v-3h3v3ZM13,0V11h11V0H13Zm7,7h-3v-3h3v3ZM0,13v11H11V13H0Zm7,7h-3v-3h3v3Zm10-3h-4v-4h4v4Zm3,3h-3v-3h3v3Zm-3,4h-4v-4h4v4Zm7-7h-4v-4h4v4Z"/>
|
||
</svg>
|
||
@elseif ($key === 'dots')
|
||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-slate-800" fill="currentColor">
|
||
<path d="M7,0h-3C1.794,0,0,1.794,0,4v3c0,2.206,1.794,4,4,4h3c2.206,0,4-1.794,4-4V4C11,1.794,9.206,0,7,0Zm2,7c0,1.103-.897,2-2,2h-3c-1.103,0-2-.897-2-2V4c0-1.103,.897-2,2-2h3c1.103,0,2,.897,2,2v3Zm-2-2v1c0,.552-.448,1-1,1h-1c-.552,0-1-.448-1-1v-1c0-.552,.448-1,1-1h1c.552,0,1,.448,1,1Zm10,6h3c2.206,0,4-1.794,4-4V4C24,1.794,22.206,0,20,0h-3C14.794,0,13,1.794,13,4v3c0,2.206,1.794,4,4,4Zm-2-7c0-1.103,.897-2,2-2h3c1.103,0,2,.897,2,2v3c0,1.103-.897,2-2,2h-3c-1.103,0-2-.897-2-2V4Zm2,2v-1c0-.552,.448-1,1-1h1c.552,0,1,.448,1,1v1c0,.552-.448,1-1,1h-1c-.552,0-1-.448-1-1ZM7,13h-3c-2.206,0-4,1.794-4,4v3c0,2.206,1.794,4,4,4h3c2.206,0,4-1.794,4-4v-3c0-2.206-1.794-4-4-4Zm2,7c0,1.103-.897,2-2,2h-3c-1.103,0-2-.897-2-2v-3c0-1.103,.897-2,2-2h3c1.103,0,2,.897,2,2v3Zm-2-2v1c0,.552-.448,1-1,1h-1c-.552,0-1-.448-1-1v-1c0-.552,.448-1,1-1h1c.552,0,1,.448,1,1Zm10-3.5v1c0,.828-.672,1.5-1.5,1.5h-1c-.828,0-1.5-.672-1.5-1.5v-1c0-.828,.672-1.5,1.5-1.5h1c.828,0,1.5,.672,1.5,1.5Zm3,4h0c0,.828-.672,1.5-1.5,1.5h0c-.828,0-1.5-.672-1.5-1.5h0c0-.828,.672-1.5,1.5-1.5h0c.828,0,1.5,.672,1.5,1.5Zm-3,3v1c0,.828-.672,1.5-1.5,1.5h-1c-.828,0-1.5-.672-1.5-1.5v-1c0-.828,.672-1.5,1.5-1.5h1c.828,0,1.5,.672,1.5,1.5Zm7-7v1c0,.828-.672,1.5-1.5,1.5h-1c-.828,0-1.5-.672-1.5-1.5v-1c0-.828,.672-1.5,1.5-1.5h1c.828,0,1.5,.672,1.5,1.5Z"/>
|
||
</svg>
|
||
@else
|
||
<span class="grid grid-cols-4 gap-[2px] p-1.5">
|
||
@foreach($qrBits as $bit)
|
||
<span class="block aspect-square {{ $bit ? ('bg-slate-800 ' . ($meta['circular'] ? 'rounded-full' : '')) : 'bg-transparent' }}"></span>
|
||
@endforeach
|
||
</span>
|
||
@endif
|
||
</span>
|
||
<span class="text-xs font-semibold text-slate-700">{{ $meta['label'] }}</span>
|
||
</span>
|
||
</label>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Colors ───────────────────────────────────────────────────── --}}
|
||
<div x-show="openSection === 'colors'" x-cloak
|
||
x-transition:enter="transition-opacity duration-150"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
class="space-y-5">
|
||
<div>
|
||
<p class="mb-3 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Quick presets</p>
|
||
<div class="flex flex-wrap gap-2.5">
|
||
@foreach([
|
||
['fg' => '#000000', 'bg' => '#ffffff', 'label' => 'Classic'],
|
||
['fg' => '#1d4ed8', 'bg' => '#ffffff', 'label' => 'Blue'],
|
||
['fg' => '#047857', 'bg' => '#ffffff', 'label' => 'Green'],
|
||
['fg' => '#7c3aed', 'bg' => '#ffffff', 'label' => 'Purple'],
|
||
['fg' => '#c2410c', 'bg' => '#fff7ed', 'label' => 'Orange'],
|
||
['fg' => '#be185d', 'bg' => '#fdf4ff', 'label' => 'Pink'],
|
||
['fg' => '#0f172a', 'bg' => '#ecfdf5', 'label' => 'Dark teal'],
|
||
] as $preset)
|
||
<button type="button"
|
||
@click="fg = '{{ $preset['fg'] }}'; bg = '{{ $preset['bg'] }}'; schedulePreview()"
|
||
title="{{ $preset['label'] }}"
|
||
class="h-8 w-8 rounded-full border-2 border-white shadow ring-1 ring-slate-300/60 transition hover:scale-110 hover:ring-indigo-400"
|
||
style="background-color: {{ $preset['fg'] }}"></button>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid gap-3 sm:grid-cols-2">
|
||
<div>
|
||
<label class="mb-1.5 block text-xs font-semibold text-slate-600">Foreground</label>
|
||
<div class="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/80 p-2">
|
||
<input type="color" name="style[foreground]" x-model="fg" @input="schedulePreview()"
|
||
class="h-9 w-10 shrink-0 cursor-pointer rounded-lg border-0 bg-transparent p-0.5">
|
||
<input type="text" x-model="fg" @change="schedulePreview()"
|
||
class="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 font-mono text-sm focus:border-indigo-400 focus:outline-none">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1.5 block text-xs font-semibold text-slate-600">Background</label>
|
||
<div class="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/80 p-2">
|
||
<input type="color" name="style[background]" x-model="bg" @input="schedulePreview()"
|
||
class="h-9 w-10 shrink-0 cursor-pointer rounded-lg border-0 bg-transparent p-0.5">
|
||
<input type="text" x-model="bg" @change="schedulePreview()"
|
||
class="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 font-mono text-sm focus:border-indigo-400 focus:outline-none">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<p class="mb-3 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Gradient</p>
|
||
<input type="hidden" name="style[gradient_type]" :value="gradientType">
|
||
<div class="flex gap-1 rounded-xl bg-slate-200/50 p-1">
|
||
@foreach([['none', 'None'], ['linear', 'Linear'], ['radial', 'Radial']] as [$val, $lbl])
|
||
<button type="button"
|
||
@click="gradientType = '{{ $val }}'"
|
||
:class="gradientType === '{{ $val }}' ? 'bg-white shadow-sm text-slate-900' : 'text-slate-500 hover:text-slate-700'"
|
||
class="flex-1 rounded-lg py-1.5 text-[11px] font-semibold transition-all duration-150">
|
||
{{ $lbl }}
|
||
</button>
|
||
@endforeach
|
||
</div>
|
||
<div x-show="gradientType !== 'none'" x-cloak class="mt-3 space-y-3">
|
||
<div class="grid gap-3 sm:grid-cols-2">
|
||
<div>
|
||
<label class="mb-1.5 block text-xs font-semibold text-slate-600">Start color</label>
|
||
<div class="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/80 p-2">
|
||
<input type="color" name="style[gradient_color1]" x-model="gradientColor1" @input="schedulePreview()"
|
||
class="h-9 w-10 shrink-0 cursor-pointer rounded-lg border-0 bg-transparent p-0.5">
|
||
<input type="text" x-model="gradientColor1" @change="schedulePreview()"
|
||
class="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 font-mono text-sm focus:border-indigo-400 focus:outline-none">
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="mb-1.5 block text-xs font-semibold text-slate-600">End color</label>
|
||
<div class="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/80 p-2">
|
||
<input type="color" name="style[gradient_color2]" x-model="gradientColor2" @input="schedulePreview()"
|
||
class="h-9 w-10 shrink-0 cursor-pointer rounded-lg border-0 bg-transparent p-0.5">
|
||
<input type="text" x-model="gradientColor2" @change="schedulePreview()"
|
||
class="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 font-mono text-sm focus:border-indigo-400 focus:outline-none">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div x-show="gradientType === 'linear'" class="space-y-1">
|
||
<label class="flex items-center justify-between text-xs font-semibold text-slate-600">
|
||
<span>Rotation</span>
|
||
<span x-text="gradientRotation + '°'" class="font-normal text-slate-400"></span>
|
||
</label>
|
||
<input type="range" name="style[gradient_rotation]" min="0" max="360" x-model.number="gradientRotation"
|
||
class="w-full accent-indigo-600">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Eyes ─────────────────────────────────────────────────────── --}}
|
||
<div x-show="openSection === 'eyes'" x-cloak
|
||
x-transition:enter="transition-opacity duration-150"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
class="space-y-5">
|
||
<div>
|
||
<p class="mb-3 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Outer frame</p>
|
||
<div class="grid grid-cols-3 gap-2.5">
|
||
@foreach([
|
||
['value' => 'square', 'label' => 'Square', 'shape' => 'h-7 w-7 border-[3px] border-slate-800'],
|
||
['value' => 'rounded', 'label' => 'Rounded', 'shape' => 'h-7 w-7 rounded-[5px] border-[3px] border-slate-800'],
|
||
['value' => 'circle', 'label' => 'Circle', 'shape' => 'h-7 w-7 rounded-full border-[3px] border-slate-800'],
|
||
] as $opt)
|
||
<label class="group cursor-pointer">
|
||
<input type="radio" name="style[finder_outer]" value="{{ $opt['value'] }}" x-model="finderOuter" class="sr-only">
|
||
<span class="flex flex-col items-center gap-2 rounded-xl border-2 bg-white p-3 transition-all"
|
||
:class="finderOuter === '{{ $opt['value'] }}'
|
||
? 'border-indigo-500 bg-indigo-50/60'
|
||
: 'border-slate-200 group-hover:border-slate-300'">
|
||
<span class="flex h-10 w-10 items-center justify-center rounded-lg bg-slate-50 ring-1 ring-slate-200/60">
|
||
<span class="{{ $opt['shape'] }}"></span>
|
||
</span>
|
||
<span class="text-[11px] font-semibold text-slate-600">{{ $opt['label'] }}</span>
|
||
</span>
|
||
</label>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<p class="mb-3 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Inner dot</p>
|
||
<div class="grid grid-cols-3 gap-2.5">
|
||
@foreach([
|
||
['value' => 'square', 'label' => 'Square', 'shape' => 'h-5 w-5 rounded-[2px] bg-slate-800'],
|
||
['value' => 'rounded', 'label' => 'Rounded', 'shape' => 'h-5 w-5 rounded-[4px] bg-slate-800'],
|
||
['value' => 'dot', 'label' => 'Dot', 'shape' => 'h-5 w-5 rounded-full bg-slate-800'],
|
||
] as $opt)
|
||
<label class="group cursor-pointer">
|
||
<input type="radio" name="style[finder_inner]" value="{{ $opt['value'] }}" x-model="finderInner" class="sr-only">
|
||
<span class="flex flex-col items-center gap-2 rounded-xl border-2 bg-white p-3 transition-all"
|
||
:class="finderInner === '{{ $opt['value'] }}'
|
||
? 'border-indigo-500 bg-indigo-50/60'
|
||
: 'border-slate-200 group-hover:border-slate-300'">
|
||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-50 ring-1 ring-slate-200/60">
|
||
<span class="{{ $opt['shape'] }}"></span>
|
||
</span>
|
||
<span class="text-[11px] font-semibold text-slate-600">{{ $opt['label'] }}</span>
|
||
</span>
|
||
</label>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Logo ─────────────────────────────────────────────────────── --}}
|
||
<div x-show="openSection === 'logo'" x-cloak
|
||
x-transition:enter="transition-opacity duration-150"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
class="space-y-4">
|
||
<div>
|
||
<p class="text-xs font-semibold text-slate-700">Center logo</p>
|
||
<p class="mt-0.5 text-xs leading-5 text-slate-400">Placed in the center. Error correction is raised automatically when a logo is present.</p>
|
||
</div>
|
||
<label class="flex cursor-pointer flex-col items-center gap-3 rounded-xl border-2 border-dashed border-slate-200 bg-slate-50/60 py-8 text-center transition hover:border-indigo-400 hover:bg-indigo-50/20">
|
||
<span class="flex h-12 w-12 items-center justify-center rounded-full bg-white shadow-sm ring-1 ring-slate-200/60">
|
||
<svg class="h-6 w-6 text-slate-400" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"/>
|
||
</svg>
|
||
</span>
|
||
<div>
|
||
<p class="text-sm font-semibold text-slate-700">Click to upload</p>
|
||
<p class="text-xs text-slate-400">PNG, JPG, GIF, WebP · max 2 MB</p>
|
||
</div>
|
||
<input type="file" name="logo" accept="image/png,image/jpeg,image/gif,image/webp" x-ref="logoInput" @change="onLogoChange()" class="sr-only">
|
||
</label>
|
||
|
||
{{-- Logo options: shown when a logo is present (uploaded or existing) --}}
|
||
<div x-show="hasExistingLogo || _rawLogoFile" x-cloak class="space-y-4 border-t border-slate-100 pt-4">
|
||
|
||
{{-- Size --}}
|
||
<div>
|
||
<label class="flex items-center justify-between text-xs font-semibold text-slate-600">
|
||
<span>Logo size</span>
|
||
<span x-text="Math.round(logoSize * 100) + '%'" class="font-normal text-slate-400"></span>
|
||
</label>
|
||
<input type="range" min="10" max="40" step="1"
|
||
:value="Math.round(logoSize * 100)"
|
||
@input="logoSize = parseFloat($event.target.value) / 100"
|
||
class="mt-2 w-full accent-indigo-600">
|
||
<input type="hidden" name="style[logo_size]" :value="logoSize">
|
||
</div>
|
||
|
||
{{-- Padding --}}
|
||
<div>
|
||
<label class="flex items-center justify-between text-xs font-semibold text-slate-600">
|
||
<span>Padding</span>
|
||
<span x-text="logoMargin" class="font-normal text-slate-400"></span>
|
||
</label>
|
||
<input type="range" name="style[logo_margin]" min="0" max="15" x-model.number="logoMargin"
|
||
class="mt-2 w-full accent-indigo-600">
|
||
</div>
|
||
|
||
{{-- White background toggle --}}
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-xs font-semibold text-slate-600">White background</span>
|
||
<button type="button" role="switch" :aria-checked="logoWhiteBg"
|
||
@click="logoWhiteBg = !logoWhiteBg"
|
||
:class="logoWhiteBg ? 'bg-indigo-500' : 'bg-slate-200'"
|
||
class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none">
|
||
<span :class="logoWhiteBg ? 'translate-x-5' : 'translate-x-1'"
|
||
class="inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform"></span>
|
||
</button>
|
||
<input type="hidden" name="style[logo_white_bg]" :value="logoWhiteBg ? '1' : '0'">
|
||
</div>
|
||
|
||
{{-- Shape --}}
|
||
<div>
|
||
<p class="mb-2 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Shape</p>
|
||
<div class="grid grid-cols-3 gap-2">
|
||
{{-- Original: landscape rectangle — logo keeps its natural aspect ratio --}}
|
||
<button type="button"
|
||
@click="logoShape = 'none'"
|
||
:class="logoShape === 'none' ? 'border-indigo-500 bg-indigo-50/60' : 'border-slate-200 hover:border-slate-300'"
|
||
class="flex flex-col items-center gap-2 rounded-xl border-2 bg-white p-3 transition-all">
|
||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-50 ring-1 ring-slate-200/60">
|
||
<span class="h-4 w-7 rounded-[2px] bg-slate-800"></span>
|
||
</span>
|
||
<span class="text-[11px] font-semibold text-slate-600">Original</span>
|
||
</button>
|
||
{{-- Rounded: square crop with rounded corners --}}
|
||
<button type="button"
|
||
@click="logoShape = 'rounded'"
|
||
:class="logoShape === 'rounded' ? 'border-indigo-500 bg-indigo-50/60' : 'border-slate-200 hover:border-slate-300'"
|
||
class="flex flex-col items-center gap-2 rounded-xl border-2 bg-white p-3 transition-all">
|
||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-50 ring-1 ring-slate-200/60">
|
||
<span class="h-7 w-7 rounded-xl bg-slate-800"></span>
|
||
</span>
|
||
<span class="text-[11px] font-semibold text-slate-600">Rounded</span>
|
||
</button>
|
||
{{-- Circle: square crop clipped to circle --}}
|
||
<button type="button"
|
||
@click="logoShape = 'circle'"
|
||
:class="logoShape === 'circle' ? 'border-indigo-500 bg-indigo-50/60' : 'border-slate-200 hover:border-slate-300'"
|
||
class="flex flex-col items-center gap-2 rounded-xl border-2 bg-white p-3 transition-all">
|
||
<span class="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-50 ring-1 ring-slate-200/60">
|
||
<span class="h-7 w-7 rounded-full bg-slate-800"></span>
|
||
</span>
|
||
<span class="text-[11px] font-semibold text-slate-600">Circle</span>
|
||
</button>
|
||
</div>
|
||
<input type="hidden" name="style[logo_shape]" :value="logoShape">
|
||
</div>
|
||
|
||
</div>
|
||
|
||
@if(!empty($showRemoveLogo))
|
||
<label class="flex items-center gap-2.5 text-sm text-slate-600">
|
||
<input type="checkbox" name="remove_logo" value="1" x-model="removeLogo" @change="schedulePreview()"
|
||
class="h-4 w-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
|
||
Remove current logo
|
||
</label>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- ── Frame ────────────────────────────────────────────────────── --}}
|
||
<div x-show="openSection === 'frame'" x-cloak
|
||
x-transition:enter="transition-opacity duration-150"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
class="space-y-5">
|
||
<div>
|
||
<p class="mb-3 text-[10px] font-semibold uppercase tracking-[0.1em] text-slate-400">Frame style</p>
|
||
<div class="grid grid-cols-2 gap-2.5">
|
||
@foreach($frameStyles as $key => $meta)
|
||
<label class="group cursor-pointer">
|
||
<input type="radio" name="style[frame_style]" value="{{ $key }}" x-model="frameStyle" class="sr-only">
|
||
<span class="flex items-center gap-3 rounded-xl border-2 bg-white p-3 transition-all"
|
||
:class="frameStyle === '{{ $key }}'
|
||
? 'border-indigo-500 bg-indigo-50/60'
|
||
: 'border-slate-200 group-hover:border-slate-300'">
|
||
<span class="flex h-10 w-9 shrink-0 items-end justify-center overflow-hidden rounded-lg border border-slate-200 bg-white p-1 shadow-sm">
|
||
@if($key === 'none')
|
||
<span class="h-8 w-8 rounded-sm bg-slate-200 opacity-40"></span>
|
||
@elseif($meta['mode'] === 'border')
|
||
<span class="h-8 w-8 rounded-sm border-2 border-slate-500 bg-slate-100"></span>
|
||
@elseif($meta['mode'] === 'label')
|
||
<span class="flex h-9 w-8 flex-col items-stretch gap-0.5">
|
||
<span class="flex-1 rounded-sm border border-slate-300 bg-slate-100"></span>
|
||
<span class="rounded-sm bg-slate-700 py-0.5 text-center text-[5px] font-bold leading-none text-white">SCAN</span>
|
||
</span>
|
||
@else
|
||
<span class="flex h-9 w-8 flex-col items-stretch gap-0.5">
|
||
<span class="flex-1 rounded-sm border border-slate-300 bg-slate-100"></span>
|
||
<span class="rounded-full bg-slate-700 py-0.5 text-center text-[5px] font-bold leading-none text-white">TAP</span>
|
||
</span>
|
||
@endif
|
||
</span>
|
||
<span class="min-w-0">
|
||
<span class="block text-xs font-semibold text-slate-800">{{ $meta['label'] }}</span>
|
||
<span class="block text-[10px] leading-4 text-slate-400">{{ $meta['description'] }}</span>
|
||
</span>
|
||
</span>
|
||
</label>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Frame colour --}}
|
||
<div x-show="frameStyle !== 'none'" x-cloak class="space-y-1">
|
||
<label class="block text-xs font-semibold text-slate-600">Frame colour</label>
|
||
<div class="flex gap-2">
|
||
<input type="color" name="style[frame_color]" x-model="frameColor" @input="schedulePreview()"
|
||
class="h-9 w-9 shrink-0 cursor-pointer rounded-lg border border-slate-200 bg-white p-0.5">
|
||
<input type="text" x-model="frameColor" @change="schedulePreview()" maxlength="7"
|
||
class="flex-1 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Frame text -- only for frames that show a CTA label --}}
|
||
<div x-show="frameStyle === 'scan_me' || frameStyle === 'tap_to_scan'" x-cloak class="space-y-1">
|
||
<label class="flex items-center justify-between text-xs font-semibold text-slate-600">
|
||
<span>Label text</span>
|
||
<span class="font-normal text-slate-400" x-text="frameStyle === 'tap_to_scan' ? 'Default: TAP TO SCAN' : 'Default: SCAN ME'"></span>
|
||
</label>
|
||
<input type="text" name="style[frame_text]" x-model="frameText"
|
||
maxlength="100"
|
||
:placeholder="frameStyle === 'tap_to_scan' ? 'TAP TO SCAN' : 'SCAN ME'"
|
||
class="w-full rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||
</div>
|
||
|
||
<div class="space-y-4 border-t border-slate-100 pt-4">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-600">Error correction</label>
|
||
<select name="style[error_correction]" x-model="ecc"
|
||
class="mt-1.5 w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none">
|
||
<option value="L">Low — 7% recovery</option>
|
||
<option value="M">Medium — 15% recovery</option>
|
||
<option value="Q">Quartile — 25% recovery</option>
|
||
<option value="H">High — 30% recovery</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="flex items-center justify-between text-xs font-semibold text-slate-600">
|
||
<span>Output scale</span>
|
||
<span x-text="scale + '×'" class="font-normal text-slate-400"></span>
|
||
</label>
|
||
<input type="range" name="style[scale]" min="4" max="16" x-model.number="scale"
|
||
class="mt-2 w-full accent-indigo-600">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="flex items-center justify-between text-xs font-semibold text-slate-600">
|
||
<span>Quiet zone</span>
|
||
<span x-text="margin + ' modules'" class="font-normal text-slate-400"></span>
|
||
</label>
|
||
<input type="range" name="style[margin]" min="0" max="10" x-model.number="margin"
|
||
class="mt-2 w-full accent-indigo-600">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|