Files
komp_ac/web/templates/ui/base.html
2026-08-21 10:03:36 +02:00

119 lines
5.1 KiB
HTML

{#
The shell every full page extends. A page template overrides `title`,
optionally `head` (extra scripts) and `body_class`, and always `content`.
Every page struct must carry a `nav: crate::ui::Nav` field, because the
navbar component below reads it.
#}
<!doctype html>
<html lang="{{ nav.language() }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Komp Accounting{% endblock %} · Komp Accounting</title>
<script>
// Runs before any stylesheet so the `dark` class is already on <html> at
// first paint — otherwise the page would flash light before this fires.
// Dark is the default: only an explicit "light" choice in localStorage
// (set by the toggle in ui/navbar.html) turns it off.
document.documentElement.classList.toggle('dark', localStorage.getItem('theme') !== 'light');
</script>
{#
Cascade order, declared before anything ships CSS so that it is this line
that decides it. `app` is static/app.css, which is wrapped in that layer:
it sits above Tailwind's preflight — so the reset cannot touch the site's
own look — and below Tailwind's utilities, so a utility class put on an
element still wins. That is what makes Tailwind usable here one element at
a time, without converting the stylesheet.
#}
<style>@layer theme, base, app, components, utilities;</style>
<link rel="stylesheet" href="/static/app.css">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
{#
Penguin UI (https://www.penguinui.com) is Tailwind + Alpine, and every
component on this site is one of its components: the navbar, the failure
alert and modal, and the toast, which is included straight out of
penguinui-components. The focus plugin is what x-trap in the modal needs,
and it has to load before Alpine itself.
#}
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
/* Penguin UI's names stay as aliases; app.css owns the actual palette. */
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* All aliases resolve through app.css, including the dark aliases. */
--color-surface: var(--surface);
--color-surface-alt: var(--surface-alt);
--color-on-surface: var(--label);
--color-on-surface-strong: var(--text-strong);
--color-primary: var(--primary);
--color-on-primary: var(--on-primary);
--color-secondary: var(--secondary);
--color-on-secondary: var(--on-secondary);
--color-outline: var(--border);
--color-outline-strong: var(--outline-strong);
--color-surface-dark: var(--surface);
--color-surface-dark-alt: var(--surface-alt);
--color-on-surface-dark: var(--text);
--color-on-surface-dark-strong: var(--text-strong);
--color-primary-dark: var(--primary);
--color-on-primary-dark: var(--on-primary);
--color-secondary-dark: var(--secondary);
--color-on-secondary-dark: var(--on-secondary);
--color-outline-dark: var(--border);
--color-outline-dark-strong: var(--outline-strong);
--color-info: var(--info);
--color-on-info: var(--on-info);
--color-success: var(--success);
--color-on-success: var(--on-success);
--color-warning: var(--warning);
--color-on-warning: var(--on-warning);
--color-danger: var(--danger);
--color-on-danger: var(--on-danger);
/* Border radius. */
--radius-radius: 6px;
}
</style>
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<script>
// htmx only swaps 2xx responses. Every form in this app answers a failure
// with a status that says so — 422 for a rejected draft, 403 for a lost
// session, 502 for an unreachable backend — so the markup explaining the
// failure was rendered, returned, and then thrown away, and the page did
// nothing at all. Swap it instead; the response body is the explanation.
document.addEventListener('htmx:beforeSwap', function (event) {
if (event.detail.xhr.status >= 400) {
event.detail.shouldSwap = true;
event.detail.isError = false;
}
});
</script>
{% block head %}{% endblock %}
</head>
<body>
{% include "ui/navbar.html" %}
{% block content %}{% endblock %}
{#
Penguin UI's stacking toast notification, straight out of the library and
unmodified — askama.toml puts penguinui-components on the template path, so
this is the library's file, not a copy of it.
Mounted once per page and deliberately outside the block every fragment
swaps into: a success is announced by a fragment dispatching `notify` (see
ui/toast.html), and the toast showing it has to outlive the swap that
follows. The component file also ships the demo buttons that trigger it in
the library's own preview; `.penguin-toast > button` in static/app.css is
what keeps those off the page.
#}
<div class="penguin-toast">{% include "toast-notification/stacking-toast-notification.html" %}</div>
</body>
</html>