penguinui
This commit is contained in:
@@ -1,13 +1,47 @@
|
||||
{#
|
||||
Inline status blocks. Imported wherever a form reports its outcome:
|
||||
Inline status blocks — Penguin UI's "alert" component
|
||||
(https://www.penguinui.com/components/alert, modern style, with icon), minus
|
||||
the dark variants because the app has no dark theme. The colours come from
|
||||
the @theme block in ui/base.html.
|
||||
|
||||
Imported wherever a form reports its outcome:
|
||||
|
||||
{% import "ui/alert.html" as alert %}
|
||||
{% call alert::error("Could not create the table", message) %}{% endcall %}
|
||||
|
||||
An error is normally paired with ui/dialog.html, which is what actually puts
|
||||
the message in front of someone who has scrolled away from the top of a form.
|
||||
#}
|
||||
{% macro error(title, message) %}
|
||||
<div class="form-error"><strong>{{ title }}</strong><p>{{ message }}</p></div>
|
||||
<div class="relative mt-4 w-full overflow-hidden rounded-md border border-danger bg-surface text-onSurface" role="alert">
|
||||
<div class="flex items-center gap-2 bg-danger/10 p-4">
|
||||
<div class="shrink-0 rounded-full bg-danger/15 p-1 text-danger">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6" aria-hidden="true">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<h3 class="text-sm font-semibold text-danger">{{ title }}</h3>
|
||||
<p class="text-sm font-medium whitespace-pre-wrap">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro success(title, message) %}
|
||||
<div class="form-success"><strong>{{ title }}</strong><p>{{ message }}</p></div>
|
||||
<div class="relative mt-4 w-full overflow-hidden rounded-md border border-success bg-surface text-onSurface" role="alert">
|
||||
<div class="flex items-center gap-2 bg-success/10 p-4">
|
||||
<div class="shrink-0 rounded-full bg-success/15 p-1 text-success">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-6" aria-hidden="true">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-2">
|
||||
<h3 class="text-sm font-semibold text-success">{{ title }}</h3>
|
||||
<p class="text-sm font-medium whitespace-pre-wrap">{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
{#
|
||||
Standalone swap target for every form POST — crate::ui::Alert.
|
||||
Rendered into the page's #submission-status / #login-status div.
|
||||
|
||||
A success is an inline alert and nothing more. A failure also gets the
|
||||
dialog: these divs sit at the bottom of their form, so on any page long
|
||||
enough to scroll — the validation forms, the import form, the table builder
|
||||
— the alert alone lands off screen and the page looks like it did nothing.
|
||||
#}
|
||||
{% import "ui/alert.html" as alert %}
|
||||
{%- if success %}{% call alert::success(title, message) %}{% endcall %}{% else %}{% call alert::error(title, message) %}{% endcall %}{% endif -%}
|
||||
{% import "ui/dialog.html" as dialog %}
|
||||
{%- if success -%}
|
||||
{% call alert::success(title, message) %}{% endcall %}
|
||||
{%- else -%}
|
||||
{% call alert::error(title, message) %}{% endcall %}
|
||||
{% call dialog::error(title, message) %}{% endcall %}
|
||||
{%- endif -%}
|
||||
|
||||
@@ -11,8 +11,57 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Komp Accounting{% endblock %} · Komp Accounting</title>
|
||||
{#
|
||||
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 its
|
||||
components are what this app uses for anything the user has to be told:
|
||||
ui/alert.html and ui/dialog.html. The focus plugin is what x-trap in the
|
||||
dialog 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 "modern" theme setup: its semantic names, this app's palette. */
|
||||
@theme {
|
||||
--color-surface: #ffffff;
|
||||
--color-surfaceAlt: #f3f5f7;
|
||||
--color-surfaceDark: #152238;
|
||||
--color-onSurface: #465267;
|
||||
--color-onSurfaceStrong: #17202a;
|
||||
--color-outline: #d9dfe7;
|
||||
--color-primary: #2563eb;
|
||||
--color-onPrimary: #ffffff;
|
||||
--color-danger: #9d342d;
|
||||
--color-success: #21643a;
|
||||
}
|
||||
</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>
|
||||
|
||||
54
web/templates/ui/dialog.html
Normal file
54
web/templates/ui/dialog.html
Normal file
@@ -0,0 +1,54 @@
|
||||
{#
|
||||
Blocking modal dialog — Penguin UI's "modal" component
|
||||
(https://www.penguinui.com/components/modal, modern style), with the dark
|
||||
variants dropped because the app has no dark theme.
|
||||
|
||||
It is Tailwind + Alpine markup, so a page that calls this macro must load
|
||||
both in its `head` block; `pages/add_table/add_table.html` shows the set.
|
||||
The `@theme` block there maps Penguin UI's semantic colour names onto the
|
||||
palette in `static/app.css`.
|
||||
|
||||
Rendered already open (`modalIsOpen: true`): the server only sends the
|
||||
dialog when there is something the user has to be told, so there is no
|
||||
trigger button — the response itself is the trigger.
|
||||
|
||||
{% import "ui/dialog.html" as dialog %}
|
||||
{% call dialog::error("Could not add the column", message) %}{% endcall %}
|
||||
#}
|
||||
{% macro error(title, message) %}
|
||||
<div x-data="{ modalIsOpen: true }">
|
||||
<div x-cloak x-show="modalIsOpen" x-transition.opacity.duration.200ms
|
||||
x-trap.inert.noscroll="modalIsOpen"
|
||||
x-on:keydown.esc.window="modalIsOpen = false" x-on:click.self="modalIsOpen = false"
|
||||
class="fixed inset-0 z-30 flex items-end justify-center bg-surfaceDark/20 p-4 pb-8 backdrop-blur-xs sm:items-center lg:p-8"
|
||||
role="dialog" aria-modal="true" aria-labelledby="dialogTitle">
|
||||
<!-- Modal Dialog -->
|
||||
<div x-show="modalIsOpen"
|
||||
x-transition:enter="transition delay-100 duration-200 ease-out motion-reduce:transition-opacity"
|
||||
x-transition:enter-start="scale-50 opacity-0" x-transition:enter-end="scale-100 opacity-100"
|
||||
class="flex max-w-lg flex-col gap-4 overflow-hidden rounded-lg border border-outline bg-surface text-onSurface">
|
||||
<!-- Dialog Header -->
|
||||
<div class="flex items-center justify-between border-b border-outline bg-surfaceAlt/60 p-4">
|
||||
<h3 id="dialogTitle" class="font-semibold tracking-wide text-danger">{{ title }}</h3>
|
||||
<button type="button" x-on:click="modalIsOpen = false" class="cursor-pointer text-onSurface" aria-label="close dialog">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor"
|
||||
fill="none" stroke-width="1.4" class="h-5 w-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Dialog Body -->
|
||||
<div class="px-4 py-4">
|
||||
<p class="text-pretty whitespace-pre-wrap">{{ message }}</p>
|
||||
</div>
|
||||
<!-- Dialog Footer -->
|
||||
<div class="flex flex-col-reverse justify-between gap-2 border-t border-outline bg-surfaceAlt/60 p-4 sm:flex-row sm:items-center md:justify-end">
|
||||
<button type="button" x-on:click="modalIsOpen = false"
|
||||
class="cursor-pointer rounded-md whitespace-nowrap bg-primary px-4 py-2 text-center text-sm font-medium tracking-wide text-onPrimary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0">
|
||||
Back to the form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user