Files
pictureFrame-webApp/templates/security/register.html.twig
T
football2801 0489028486
CI / test (push) Has been cancelled
refactor(design): single source of truth — wevisto-design.css
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>
2026-05-15 15:24:45 -04:00

211 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>Create account — WeVisto</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100dvh;
background: #fdf6ee;
color: #3a2e22;
}
.card {
width: 100%;
max-width: 380px;
margin: 1rem;
padding: 2rem;
background: #fff9f2;
border-radius: 16px;
border: 1px solid #e8d9c4;
}
h1 { font-size: 1.4rem; font-weight: 700; margin-bottom: 1.5rem; }
.field { margin-bottom: 1rem; }
label { display: block; font-size: 0.8125rem; font-weight: 600; color: #8a7060; margin-bottom: 0.375rem; }
input[type="email"],
input[type="password"] {
width: 100%;
min-height: 44px;
padding: 0 0.875rem;
border: 1px solid #e8d9c4;
border-radius: 10px;
background: #fff;
font-size: 1rem;
color: #3a2e22;
transition: border-color 0.15s;
}
input:focus { outline: none; border-color: #c97c3a; }
input[aria-invalid="true"] { border-color: #c0392b; }
.field-error {
margin-top: 0.25rem;
font-size: 0.8125rem;
color: #c0392b;
min-height: 1.2em;
}
.field-error:empty { display: none; }
.btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 44px;
margin-top: 1.25rem;
padding: 0 1.25rem;
background: #c97c3a;
color: #fff;
border: none;
border-radius: 9999px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: opacity 0.15s;
}
.btn:hover { opacity: 0.9; }
.login-link { display: block; text-align: center; margin-top: 1rem; font-size: 0.875rem; color: #8a7060; }
.login-link a { color: #c97c3a; text-decoration: none; font-weight: 600; }
.logo-badge { display: block; width: 88px; height: 88px; margin: 0 auto 1.25rem; 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>Create account</h1>
{{ form_start(form, {attr: {novalidate: 'novalidate', id: 'reg-form'}}) }}
<div class="field">
{{ form_label(form.email) }}
{{ form_widget(form.email, {attr: {
id: 'reg-email',
autocomplete: 'email',
'aria-describedby': 'reg-email-error',
'aria-invalid': form.email.vars.errors|length > 0 ? 'true' : 'false'
}}) }}
<p id="reg-email-error" class="field-error" role="alert">
{% for error in form.email.vars.errors %}{{ error.message }}{% endfor %}
</p>
</div>
<div class="field">
{{ form_label(form.plainPassword.first) }}
{{ form_widget(form.plainPassword.first, {attr: {
id: 'reg-password',
autocomplete: 'new-password',
'aria-describedby': 'reg-password-error',
'aria-invalid': form.plainPassword.first.vars.errors|length > 0 ? 'true' : 'false'
}}) }}
<p id="reg-password-error" class="field-error" role="alert">
{% for error in form.plainPassword.first.vars.errors %}{{ error.message }}{% endfor %}
</p>
</div>
<div class="field">
{{ form_label(form.plainPassword.second) }}
{{ form_widget(form.plainPassword.second, {attr: {
id: 'reg-password-confirm',
autocomplete: 'new-password',
'aria-describedby': 'reg-password-confirm-error',
'aria-invalid': form.plainPassword.vars.errors|length > 0 ? 'true' : 'false'
}}) }}
<p id="reg-password-confirm-error" class="field-error" role="alert">
{% for error in form.plainPassword.vars.errors %}{{ error.message }}{% endfor %}
</p>
</div>
<button type="submit" class="btn">Create account</button>
{{ form_end(form) }}
<p class="login-link">Already have an account? <a href="/login">Sign in</a></p>
</div>
<script>
(function () {
var form = document.getElementById('reg-form');
var emailInput = document.getElementById('reg-email');
var emailError = document.getElementById('reg-email-error');
var pwInput = document.getElementById('reg-password');
var pwError = document.getElementById('reg-password-error');
var confirmInput = document.getElementById('reg-password-confirm');
var confirmError = document.getElementById('reg-password-confirm-error');
function validateEmail() {
if (emailError.dataset.serverError) return;
var val = emailInput.value.trim();
if (!val) {
setError(emailInput, emailError, 'Please enter your email address');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
setError(emailInput, emailError, 'Please enter a valid email address');
} else {
clearError(emailInput, emailError);
}
}
function validatePassword() {
if (pwError.dataset.serverError) return;
var val = pwInput.value;
if (!val) {
setError(pwInput, pwError, 'Please enter a password');
} else if (val.length < 8) {
setError(pwInput, pwError, 'Your password must be at least 8 characters');
} else {
clearError(pwInput, pwError);
}
}
function validateConfirm() {
if (confirmError.dataset.serverError) return;
if (confirmInput.value && confirmInput.value !== pwInput.value) {
setError(confirmInput, confirmError, 'Passwords do not match.');
} else {
clearError(confirmInput, confirmError);
}
}
function setError(input, errorEl, message) {
input.setAttribute('aria-invalid', 'true');
errorEl.textContent = message;
}
function clearError(input, errorEl) {
input.setAttribute('aria-invalid', 'false');
errorEl.textContent = '';
}
// Mark existing server errors so client-side blur doesn't clobber them
if (emailError.textContent.trim()) emailError.dataset.serverError = '1';
if (pwError.textContent.trim()) pwError.dataset.serverError = '1';
if (confirmError.textContent.trim()) confirmError.dataset.serverError = '1';
// Clear server-error flag once user starts typing
emailInput.addEventListener('input', function () { delete emailError.dataset.serverError; });
pwInput.addEventListener('input', function () { delete pwError.dataset.serverError; });
confirmInput.addEventListener('input', function () { delete confirmError.dataset.serverError; });
emailInput.addEventListener('blur', validateEmail);
pwInput.addEventListener('blur', validatePassword);
confirmInput.addEventListener('blur', validateConfirm);
// Re-validate confirm whenever the password field changes
pwInput.addEventListener('input', function () { if (confirmInput.value) validateConfirm(); });
form.addEventListener('submit', function (e) {
validateEmail();
validatePassword();
validateConfirm();
});
}());
</script>
</body>
</html>