we now got websockets baby
This commit is contained in:
110
internal/view/component/popup/toast.templ
Normal file
110
internal/view/component/popup/toast.templ
Normal file
@@ -0,0 +1,110 @@
|
||||
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) {
|
||||
{{
|
||||
// Determine classes server-side based on notifType
|
||||
color := map[notify.Level]string{
|
||||
notify.LevelSuccess: "green",
|
||||
notify.LevelWarn: "yellow",
|
||||
notify.LevelInfo: "blue",
|
||||
hws.LevelShutdown: "yellow",
|
||||
}
|
||||
containerClass := fmt.Sprintf("bg-dark-%s border-2 border-%s", color[nt.Level], color[nt.Level])
|
||||
textClass := fmt.Sprintf("text-%s", color[nt.Level])
|
||||
progressClass := fmt.Sprintf("bg-%s", color[nt.Level])
|
||||
}}
|
||||
<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 %s", containerClass) }
|
||||
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 (rendered server-side based on notifType) -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class={ fmt.Sprintf("size-5 shrink-0 %s", textClass) }
|
||||
>
|
||||
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 (rendered server-side) -->
|
||||
<div class="flex-1 min-w-0 max-w-48 text-wrap">
|
||||
<p class={ fmt.Sprintf("font-semibold %s", textClass) }>
|
||||
{ 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 (vanilla JS manages width) -->
|
||||
<div class="h-1 bg-surface0">
|
||||
<div
|
||||
id={ fmt.Sprintf("ws-toast-%d-progress", id) }
|
||||
class={ fmt.Sprintf("h-full %s", progressClass) }
|
||||
style="width: 0"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package popup
|
||||
|
||||
// ToastContainer displays stacked toast notifications (success, warning, info)
|
||||
// Supports both regular toasts (AlpineJS) and WebSocket toasts (HTMX OOB swaps)
|
||||
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 id="toast-ws-container"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
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>
|
||||
}
|
||||
Reference in New Issue
Block a user