{% extends "vantage/base.html" %} {% load static i18n %} {% block head %} {% endblock %} {% block content %} {% comment %} One form for the whole screen. The per-section "Reset" buttons are submit buttons in it rather than forms of their own: a form inside a form is not valid HTML, and the browser silently drops the inner one — which is how a reset button ends up saving. {% endcomment %}
{% csrf_token %}

{{ page_title }}

{{ page_description }}

{% if config.updated_at %} {% if config.updated_by %} {% blocktranslate with when=config.updated_at|date:"j M Y, H:i" who=config.updated_by %}Last changed {{ when }} by {{ who }}{% endblocktranslate %} {% else %} {% blocktranslate with when=config.updated_at|date:"j M Y, H:i" %}Last changed {{ when }}{% endblocktranslate %} {% endif %} {% endif %}
{% if form.errors %} {% endif %} {% if not media_configured %}

{% translate "Uploads have nowhere to go." %}

{% blocktranslate trimmed %} This project has no MEDIA_ROOT and MEDIA_URL, so an uploaded logo cannot be stored or served. Everything else on this page works. {% endblocktranslate %}

{% endif %} {% for section in sections %}

{{ section.title }}

{{ section.description }}

{% for entry in section.fields %}
{% if entry.setting == "BRAND_COLOR" %}
{{ entry.field }}
{% else %} {{ entry.field }} {% endif %} {% if entry.file %}

{% if entry.file_url %} {{ entry.file.name }} {% else %} {{ entry.file.name }} {% endif %}

{% endif %} {% for error in entry.field.errors %} {% endfor %} {% if entry.field.help_text %}

{{ entry.field.help_text }}

{% endif %} {% if not entry.is_file %}

{% translate "nothing" as inherited_default %} {% blocktranslate trimmed with inherited=entry.inherited|default_if_none:inherited_default %} Empty means {{ inherited }}, from settings.py. {% endblocktranslate %}

{% endif %}
{% endfor %}
{% if section.key == "appearance" %}

{% translate "Derived palette" %}

{% comment %} A live region: this number changes under the user's fingers as they type a brand colour, and it is the one thing on the page that says whether the colour they have chosen is legible. Announcing it is the difference between choosing a palette and guessing at one. Polite rather than assertive — it updates on every keystroke, and interrupting that often would make the field unusable. {% endcomment %} {% if brand %}{% blocktranslate with ratio=brand.ratio|floatformat:1 %}{{ ratio }}:1 on button text{% endblocktranslate %}{% endif %}
{% if brand %}
{% translate "Light" %} {% for swatch in brand.light %} {{ swatch.name }} {% endfor %}
{% translate "Dark" %} {% for swatch in brand.dark %} {{ swatch.name }} {% endfor %}
{% endif %}
{% translate "Save changes" %} {% translate "A link, a focus ring, a selected row" %}

{{ brand.warning|default:"" }}

{% endif %}
{% endfor %}
{% comment %} The setting names are joined into one placeholder rather than looped inside the sentence: a {% templatetag openblock %} for {% templatetag closeblock %} cannot live inside a blocktranslate, and splitting the lead-in from the list would hand the translator a fragment ending in a colon. One wraps the whole list instead of each name — the styling is identical and the sentence stays whole. {% endcomment %}

{% blocktranslate with names=code_only|join:", " %}These settings live in code and are not editable here, because they decide what exists and who may reach it rather than how it looks: {{ names }}.{% endblocktranslate %}

{% blocktranslate with kilobytes=max_upload_kb %}Logos and favicons may be SVG, PNG, JPEG or WebP, up to {{ kilobytes }} KB. An SVG containing script or event handlers is refused: it would run on every page of this panel.{% endblocktranslate %}

{% comment %} Outside the form above, not inside it. The sidebar editor is a form of its own, and a form nested in a form is not valid HTML — the browser drops the inner one, which is how a Save button ends up saving somebody else's fields. {% endcomment %} {% include "vantage/settings_nav.html" %} {% endblock %} {% block scripts %} /* Live palette preview. The colour maths is not reimplemented here — the page asks the * same view that will render the panel, so the swatches cannot drift from the result. * Debounced, because a colour input fires continuously while dragging. */ (function () { var root = document.querySelector('[data-vantage-preview]'); var input = document.querySelector('[data-vantage-brand]'); var picker = document.querySelector('[data-vantage-color]'); if (!root || !input) return; var ratio = root.querySelector('[data-preview-ratio]'); var warning = root.querySelector('[data-preview-warning]'); var rows = root.querySelectorAll('.v-settings-swatches'); var sample = root.querySelector('.v-settings-sample'); var timer = null; var inflight = null; function paint(row, swatches) { var cells = row.querySelectorAll('.v-settings-swatch'); for (var i = 0; i < cells.length && i < swatches.length; i++) { cells[i].style.background = swatches[i].value; cells[i].title = '--' + swatches[i].name + ': ' + swatches[i].value; } } function apply(data) { if (rows.length > 1) { paint(rows[0], data.light); paint(rows[1], data.dark); } if (ratio) ratio.textContent = data.ratio.toFixed(1) + ':1 on button text'; if (sample) { sample.style.setProperty('--preview-primary', data.light[0].value); sample.style.setProperty('--preview-on-primary', data.light[2].value); } if (warning) { warning.hidden = !data.warning; warning.textContent = data.warning || ''; } } function request(value) { if (inflight) inflight.abort(); inflight = new AbortController(); fetch(window.location.pathname + '?preview=' + encodeURIComponent(value), { signal: inflight.signal, headers: { 'Accept': 'application/json' } }) .then(function (response) { return response.ok ? response.json() : null; }) .then(function (data) { if (data && data.ok) apply(data); }) .catch(function () { /* an aborted or failed preview is not worth reporting */ }); } function schedule() { var value = input.value.trim(); if (!/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value)) return; window.clearTimeout(timer); timer = window.setTimeout(function () { request(value); }, 120); } input.addEventListener('input', schedule); if (picker) { picker.addEventListener('input', function () { input.value = picker.value; schedule(); }); input.addEventListener('change', function () { if (/^#[0-9a-fA-F]{6}$/.test(input.value.trim())) picker.value = input.value.trim(); }); } })(); {% endblock %}