0489028486
CI / test (push) Has been cancelled
v2 tokens were duplicated: in design-v2.scss for the SPA, inlined in login.html.twig for Twig. Two places to keep in sync. Now: one shared /public/css/wevisto-design.css loaded by every Twig standalone template AND linked from the SPA index.html. It contains: - Brand constants (yellow / navy / fonts) - v2 tokens with per-theme dusk overrides - v2 base body bg + editorial typography defaults - v2 overrides for the .card / .btn / .field-error / .logo-badge patterns used across all Twig templates The SPA's design-v2.scss now holds only SPA-specific composition: side rail at desktop, frame card, theme swatch harbor preview, settings polish. No token duplication. Result: changing a v2 color in one file flows to every surface in both worlds. Adding v2 to another Twig template only requires the existing shared CSS link (already wired up to all 11 standalone templates). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
8.4 KiB
Twig
144 lines
8.4 KiB
Twig
<!DOCTYPE html>
|
|
<html lang="en" data-design="{{ app.request.cookies.get('wevisto_design')|default('v1') }}">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="icon" type="image/svg+xml" href="/build/favicon.svg?v=20260515-vviewfinder">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/build/icons/favicon-32.png?v=20260515-vviewfinder">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/build/icons/favicon-16.png?v=20260515-vviewfinder">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/build/icons/apple-touch-icon.png?v=20260515-vviewfinder">
|
|
<link rel="stylesheet" href="/css/wevisto-design.css">
|
|
<title>Name your frame — WeVisto</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: system-ui, sans-serif; min-height: 100dvh; background: #fdf6ee; color: #3a2e22;
|
|
display: flex; align-items: flex-start; justify-content: center; padding: 2rem 1rem; }
|
|
.card { width: 100%; max-width: 400px; }
|
|
h1 { font-size: 1.4rem; font-weight: 700; margin-bottom: .25rem; }
|
|
.subtitle { font-size: .875rem; color: #8a7060; margin-bottom: 1.5rem; }
|
|
.field { margin-bottom: 1.25rem; }
|
|
label { display: block; font-size: .8125rem; font-weight: 600; color: #8a7060; margin-bottom: .375rem; }
|
|
input[type="text"], select {
|
|
width: 100%; min-height: 44px; padding: 0 .875rem; border: 1px solid #e8d9c4;
|
|
border-radius: 10px; background: #fff; font-size: 1rem; color: #3a2e22; }
|
|
.orientation-picker { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
|
|
.orientation-opt { display: flex; flex-direction: column; align-items: center; gap: .25rem;
|
|
padding: .75rem .5rem; background: #fff9f2; border: 2px solid #e8d9c4;
|
|
border-radius: 12px; cursor: pointer; transition: border-color .15s; }
|
|
.orientation-opt--active { border-color: #c97c3a; }
|
|
.orientation-opt__diagram { width: 48px; height: 48px; color: #8a7060; }
|
|
.orientation-opt .opt-body { stroke: #8a7060; fill: #f5ede0; }
|
|
.orientation-opt--active .opt-body { stroke: #c97c3a; fill: rgba(201,124,58,.12); }
|
|
.orientation-opt .opt-ribbon { fill: #8a7060; }
|
|
.orientation-opt--active .opt-ribbon { fill: #c97c3a; }
|
|
.orientation-opt__label { font-size: .75rem; font-weight: 700; color: #3a2e22; }
|
|
.orientation-opt__sub { font-size: .6875rem; color: #8a7060; text-align: center; line-height: 1.2; }
|
|
.field-error { margin-top: .375rem; font-size: .8125rem; color: #c0392b; }
|
|
.hint { margin-top: .375rem; font-size: .8125rem; color: #8a7060; }
|
|
.btn { display: flex; align-items: center; justify-content: center; width: 100%; min-height: 44px;
|
|
margin-top: 1.5rem; background: #c97c3a; color: #fff; border: none; border-radius: 9999px;
|
|
font-size: 1rem; font-weight: 700; cursor: pointer; }
|
|
.logo-badge { display: block; width: 80px; height: 80px; margin: 0 auto 1rem; border-radius: 14px; overflow: hidden; border: 1px solid #e8d9c4; box-shadow: 0 2px 10px rgba(0,0,0,.06); }
|
|
.logo-badge img { display: block; width: 100%; height: 100%; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<a href="/" class="logo-badge" aria-label="WeVisto"><img src="/build/logo.svg" alt=""></a>
|
|
<h1>Name your frame</h1>
|
|
<p class="subtitle">You can always change these settings later.</p>
|
|
|
|
{% if error %}
|
|
<p class="field-error" role="alert" style="margin-bottom:1rem">{{ error }}</p>
|
|
{% endif %}
|
|
|
|
<form method="post" novalidate>
|
|
<div class="field">
|
|
<label for="name">Frame name</label>
|
|
<input type="text" id="name" name="name"
|
|
value="{{ device.name }}"
|
|
placeholder="e.g. Living room frame"
|
|
maxlength="100" required autofocus>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Display orientation</label>
|
|
<input type="hidden" id="orientation" name="orientation" value="{{ device.orientation.value }}">
|
|
<div class="orientation-picker" role="radiogroup" aria-label="Display orientation">
|
|
<button type="button" role="radio"
|
|
class="orientation-opt{% if device.orientation.value == 'landscape' %} orientation-opt--active{% endif %}"
|
|
aria-checked="{{ device.orientation.value == 'landscape' ? 'true' : 'false' }}"
|
|
aria-label="Landscape"
|
|
data-value="landscape">
|
|
<svg class="orientation-opt__diagram" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
|
<rect x="4" y="12" width="40" height="24" rx="2" stroke-width="1.5"
|
|
class="opt-body"/>
|
|
<rect x="20" y="36" width="8" height="5" rx="1"
|
|
class="opt-ribbon"/>
|
|
</svg>
|
|
<span class="orientation-opt__label">Landscape</span>
|
|
<span class="orientation-opt__sub">Ribbon at bottom</span>
|
|
</button>
|
|
<button type="button" role="radio"
|
|
class="orientation-opt{% if device.orientation.value == 'portrait' %} orientation-opt--active{% endif %}"
|
|
aria-checked="{{ device.orientation.value == 'portrait' ? 'true' : 'false' }}"
|
|
aria-label="Portrait"
|
|
data-value="portrait">
|
|
<svg class="orientation-opt__diagram" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
|
<rect x="12" y="4" width="24" height="40" rx="2" stroke-width="1.5"
|
|
class="opt-body"/>
|
|
<rect x="7" y="20" width="5" height="8" rx="1"
|
|
class="opt-ribbon"/>
|
|
</svg>
|
|
<span class="orientation-opt__label">Portrait</span>
|
|
<span class="orientation-opt__sub">Ribbon on left</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="rotation_interval_hours">Rotation frequency</label>
|
|
{% set currentHours = device.rotationIntervalMinutes / 60 %}
|
|
<select id="rotation_interval_hours" name="rotation_interval_hours">
|
|
<option value="6" {% if currentHours == 6 %}selected{% endif %}>Every 6 hours</option>
|
|
<option value="12" {% if currentHours == 12 %}selected{% endif %}>Every 12 hours</option>
|
|
<option value="24" {% if currentHours == 24 %}selected{% endif %}>Daily (every 24 hours)</option>
|
|
<option value="48" {% if currentHours == 48 %}selected{% endif %}>Every 2 days</option>
|
|
<option value="168" {% if currentHours == 168 %}selected{% endif %}>Weekly</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="uniqueness_window">Uniqueness window</label>
|
|
<select id="uniqueness_window" name="uniqueness_window">
|
|
<option value="5" {% if device.uniquenessWindow == 5 %}selected{% endif %}>5 cycles</option>
|
|
<option value="10" {% if device.uniquenessWindow == 10 %}selected{% endif %}>10 cycles (default)</option>
|
|
<option value="20" {% if device.uniquenessWindow == 20 %}selected{% endif %}>20 cycles</option>
|
|
<option value="50" {% if device.uniquenessWindow == 50 %}selected{% endif %}>50 cycles</option>
|
|
</select>
|
|
<p class="hint">Images won't repeat until this many other photos have been shown.</p>
|
|
</div>
|
|
|
|
<button type="submit" class="btn">Save & finish setup</button>
|
|
</form>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var hidden = document.getElementById('orientation');
|
|
var btns = document.querySelectorAll('.orientation-opt');
|
|
btns.forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
btns.forEach(function (b) {
|
|
b.classList.remove('orientation-opt--active');
|
|
b.setAttribute('aria-checked', 'false');
|
|
});
|
|
btn.classList.add('orientation-opt--active');
|
|
btn.setAttribute('aria-checked', 'true');
|
|
hidden.value = btn.dataset.value;
|
|
});
|
|
});
|
|
}());
|
|
</script>
|
|
</body>
|
|
</html>
|