refactored view package

This commit is contained in:
2026-02-09 19:30:47 +11:00
parent fa3b8e3982
commit 0b3301f921
47 changed files with 653 additions and 490 deletions

View File

@@ -0,0 +1,6 @@
package popup
// ErrorModalContainer provides the target div for WebSocket error modal OOB swaps
templ ErrorModalContainer() {
<div id="error-modal-ws-container"></div>
}

View File

@@ -0,0 +1,64 @@
package popup
import "strconv"
import "fmt"
import "git.haelnorr.com/h/golib/notify"
// ErrorModalWS displays a WebSocket-triggered error modal
// Matches design of error page (page.ErrorWithDetails)
templ ErrorModalWS(code int, stacktrace string, nt notify.Notification, id int) {
<div
id="error-modal-ws-container"
hx-swap-oob="morph:#error-modal-ws-container"
>
<!-- Fullscreen backdrop overlay -->
<div class="fixed inset-0 z-50 bg-crust/80 flex items-center justify-center p-4">
<!-- Modal card -->
<div class="bg-base rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto p-8 text-center">
<!-- Error code -->
<h1 class="text-9xl text-text">{ strconv.Itoa(code) }</h1>
<!-- Error title -->
<p class="text-2xl font-bold tracking-tight text-subtext1 sm:text-4xl">
{ nt.Title }
</p>
<!-- User message -->
<p class="mt-4 text-subtext0">{ nt.Message }</p>
<!-- Details dropdown -->
if stacktrace != "" {
<div class="mt-8 text-left">
<details class="bg-surface0 rounded-lg p-4 text-right">
<summary class="text-left cursor-pointer text-subtext1 font-semibold select-none hover:text-text">
Details
<span class="text-xs text-subtext0 ml-2">(click to expand)</span>
</summary>
<div class="text-left mt-4 relative">
<pre
id={ fmt.Sprintf("error-modal-ws-details-%d", id) }
class="text-xs text-subtext0 font-mono whitespace-pre-wrap break-all bg-mantle p-4 rounded overflow-x-auto max-h-96"
>{ stacktrace }</pre>
</div>
<button
onclick={ templ.JSFuncCall("copyToClipboard", fmt.Sprintf("error-modal-ws-details-%v", id), "copyButton") }
id="copyButton"
class="mt-2 bg-mauve text-crust px-3 py-1 rounded text-xs hover:bg-mauve/75 transition hover:cursor-pointer"
title="Copy to clipboard"
>
Copy
</button>
</details>
</div>
}
<!-- Close button -->
<div class="mt-6">
<button
onclick="document.getElementById('error-modal-ws-container').innerHTML = ''"
class="inline-block rounded-lg bg-mauve px-5 py-3 text-sm text-crust transition hover:bg-mauve/75 hover:cursor-pointer"
>
Close Modal
</button>
</div>
</div>
</div>
<script src="/static/js/copytoclipboard.js"></script>
</div>
}

View File

@@ -0,0 +1,116 @@
package popup
import "fmt"
import "git.haelnorr.com/h/golib/notify"
import "git.haelnorr.com/h/golib/hws"
// ToastNotificationWS creates a toast notification sent via WebSocket with OOB swap
// Backend should pass: notifType ("success"|"warning"|"info"), title, message
templ Toast(nt notify.Notification, id, duration int) {
{{
type style struct {
bg string
fg string
border string
bgdark string
}
// Determine classes server-side based on notifType
var colors style
switch nt.Level {
case notify.LevelSuccess:
colors = style{bg: "bg-green", fg: "text-green", border: "border-green", bgdark: "bg-dark-green"}
case notify.LevelWarn, hws.LevelShutdown:
colors = style{bg: "bg-yellow", fg: "text-yellow", border: "border-yellow", bgdark: "bg-dark-yellow"}
case notify.LevelInfo:
colors = style{bg: "bg-blue", fg: "text-blue", border: "border-blue", bgdark: "bg-dark-blue"}
}
}}
<div
id="toast-ws-container"
hx-swap-oob="beforeend:#toast-ws-container"
>
<div
id={ fmt.Sprintf("ws-toast-%d", id) }
class={ fmt.Sprintf("pointer-events-auto rounded-lg shadow-lg overflow-hidden w-full mb-2 border-2 %s %s", colors.bgdark, colors.border) }
role="alert"
x-data={ templ.JSFuncCall("progressBar", fmt.Sprintf("ws-toast-%d", id), duration).CallInline }
@mouseenter="paused = true"
@mouseleave="paused = false"
>
<!-- Toast Content -->
<div class="p-4 max-w-80">
<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={ fmt.Sprintf("size-5 shrink-0 %s", colors.fg) }
>
switch nt.Level {
case notify.LevelSuccess:
<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>
case notify.LevelWarn:
<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>
default:
<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>
}
</svg>
<!-- Text Content -->
<div class="flex-1 min-w-0 max-w-48 text-wrap">
<p class={ fmt.Sprintf("font-semibold %s", colors.fg) }>
{ nt.Title }
</p>
<p class="text-sm text-subtext0 mt-1 text-wrap">
{ nt.Message }
</p>
</div>
</div>
<!-- Close Button -->
<button
onclick="this.closest('[id^=ws-toast-]').remove()"
class="text-subtext0 hover:text-text transition shrink-0 hover:cursor-pointer"
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">
<div
id={ fmt.Sprintf("ws-toast-%d-progress", id) }
class={ fmt.Sprintf("h-full %s", colors.bg) }
style="width: 0"
></div>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,10 @@
package popup
// ToastContainer displays stacked toast notifications (success, warning, info)
// Supports both regular toasts (AlpineJS) and WebSocket toasts (HTMX OOB swaps)
templ ToastContainer() {
<script src="/static/js/toasts.js" defer></script>
<div class="fixed top-20 right-5 z-40 flex flex-col gap-3 max-w-sm pointer-events-none">
<div id="toast-ws-container"></div>
</div>
}