added notification toasts (error modals still broken)

This commit is contained in:
2026-01-25 12:59:27 +11:00
parent 25461143ba
commit 959ca96b68
13 changed files with 990 additions and 89 deletions

View File

@@ -1,63 +0,0 @@
package popup
templ Error503Popup() {
<div
x-cloak
x-show="showError503"
class="absolute w-82 left-0 right-0 mt-20 mr-5 ml-auto"
x-transition:enter="transform translate-x-[100%] opacity-0 duration-200"
x-transition:enter-start="opacity-0 translate-x-[100%]"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="opacity-0 duration-200"
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 translate-x-[100%]"
>
<div
role="alert"
class="rounded-sm bg-dark-red p-4"
>
<div class="flex justify-between">
<div class="flex items-center gap-2 text-red w-fit">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355
12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309
0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75
0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0
01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
clip-rule="evenodd"
></path>
</svg>
<strong class="block font-medium">Service Unavailable</strong>
</div>
<div class="flex">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6 text-subtext0 hover:cursor-pointer"
@click="showError503=false"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</div>
</div>
<p class="mt-2 text-sm text-red">
The service is currently available. It could be down for maintenance.
Please try again later.
</p>
</div>
</div>
}

View File

@@ -0,0 +1,126 @@
package popup
// ErrorModal displays a full-screen modal for critical errors (500, 503)
templ ErrorModal() {
<div
x-show="errorModal.show"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center p-4"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<!-- Backdrop (not clickable) -->
<div class="absolute inset-0 bg-crust/80"></div>
<!-- Modal Card -->
<div
class="relative bg-surface0 border-2 border-dark-red rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
x-transition:enter="transition ease-out duration-300 delay-100"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
role="dialog"
aria-modal="true"
aria-labelledby="error-modal-title"
>
<!-- Header -->
<div class="flex items-start justify-between p-6 border-b border-dark-red">
<div class="flex items-center gap-3">
<!-- Warning Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-8 text-red flex-shrink-0"
>
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
clip-rule="evenodd"
></path>
</svg>
<!-- Title -->
<h2
id="error-modal-title"
class="text-2xl font-bold text-red"
x-text="`${errorModal.code} - ${errorModal.title}`"
></h2>
</div>
<!-- Close Button -->
<button
@click="closeErrorModal()"
class="text-subtext0 hover:text-text transition"
aria-label="Close modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
<!-- Body -->
<div class="p-6">
<!-- User Message -->
<p class="text-lg text-subtext0" x-text="errorModal.message"></p>
<!-- Details Dropdown -->
<div class="mt-6">
<details class="bg-mantle rounded-lg p-4">
<summary class="cursor-pointer text-subtext1 font-semibold select-none hover:text-text transition">
Details
<span class="text-xs text-subtext0 ml-2">(click to expand)</span>
</summary>
<div class="mt-4 relative">
<pre
id="error-modal-details"
class="text-xs text-subtext0 font-mono whitespace-pre-wrap break-all bg-surface0 p-4 rounded overflow-x-auto max-h-96"
x-text="errorModal.details"
></pre>
</div>
<button
onclick="copyToClipboard('error-modal-details', this)"
class="mt-2 bg-mauve text-crust px-3 py-1 rounded text-xs hover:bg-mauve/75 transition"
title="Copy to clipboard"
>
Copy
</button>
</details>
</div>
</div>
</div>
</div>
<!-- Copy to Clipboard Script -->
<script>
function copyToClipboard(elementId, button) {
const element = document.getElementById(elementId);
const text = element.innerText;
navigator.clipboard.writeText(text)
.then(() => {
const originalText = button.innerText;
button.innerText = 'Copied!';
setTimeout(() => {
button.innerText = originalText;
}, 2000);
})
.catch(err => {
console.error('Failed to copy:', err);
button.innerText = 'Failed';
});
}
</script>
}

View File

@@ -1,6 +1,6 @@
package popup
templ Error500Popup() {
templ ErrorPopup() {
<div
x-cloak
x-show="showError500"

View File

@@ -0,0 +1,10 @@
package popup
// ToastContainer displays stacked toast notifications (success, warning, info)
templ ToastContainer() {
<div class="fixed top-20 right-5 z-40 flex flex-col gap-3 max-w-sm pointer-events-none">
<template x-for="toast in toasts" :key="toast.id">
@ToastNotification()
</template>
</div>
}

View File

@@ -0,0 +1,114 @@
package popup
// ToastNotification displays an individual toast notification
templ ToastNotification() {
<div
class="pointer-events-auto rounded-lg shadow-lg overflow-hidden w-full"
:class="{
'bg-dark-green border-2 border-green': toast.type === 'success',
'bg-dark-yellow border-2 border-yellow': toast.type === 'warning',
'bg-dark-blue border-2 border-blue': toast.type === 'info'
}"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-x-full"
x-transition:enter-end="opacity-100 translate-x-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 translate-x-full"
@mouseenter="toast.paused = true"
@mouseleave="toast.paused = false"
role="alert"
>
<!-- Toast Content -->
<div class="p-4">
<div class="flex justify-between items-start gap-3">
<!-- Icon + Content -->
<div class="flex items-start gap-3 flex-1">
<!-- Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-5 flex-shrink-0"
:class="{
'text-green': toast.type === 'success',
'text-yellow': toast.type === 'warning',
'text-blue': toast.type === 'info'
}"
>
<!-- Success Icon (checkmark) -->
<template x-if="toast.type === 'success'">
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z"
clip-rule="evenodd"
></path>
</template>
<!-- Warning Icon (exclamation) -->
<template x-if="toast.type === 'warning'">
<path
fill-rule="evenodd"
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
clip-rule="evenodd"
></path>
</template>
<!-- Info Icon (information) -->
<template x-if="toast.type === 'info'">
<path
fill-rule="evenodd"
d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm8.706-1.442c1.146-.573 2.437.463 2.126 1.706l-.709 2.836.042-.02a.75.75 0 01.67 1.34l-.04.022c-1.147.573-2.438-.463-2.127-1.706l.71-2.836-.042.02a.75.75 0 11-.671-1.34l.041-.022zM12 9a.75.75 0 100-1.5.75.75 0 000 1.5z"
clip-rule="evenodd"
></path>
</template>
</svg>
<!-- Text Content -->
<div class="flex-1 min-w-0">
<p
class="font-semibold"
:class="{
'text-green': toast.type === 'success',
'text-yellow': toast.type === 'warning',
'text-blue': toast.type === 'info'
}"
x-text="toast.title"
></p>
<p class="text-sm text-subtext0 mt-1" x-text="toast.message"></p>
</div>
</div>
<!-- Close Button -->
<button
@click="removeToast(toast.id)"
class="text-subtext0 hover:text-text transition flex-shrink-0"
aria-label="Close notification"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
</div>
</div>
<!-- Progress Bar -->
<div class="h-1 bg-surface0">
<div
class="h-full"
:class="{
'bg-green': toast.type === 'success',
'bg-yellow': toast.type === 'warning',
'bg-blue': toast.type === 'info'
}"
:style="`width: ${toast.progress}%`"
></div>
</div>
</div>
}