added notification toasts (error modals still broken)
This commit is contained in:
@@ -56,6 +56,11 @@ func addRoutes(
|
||||
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
|
||||
Handler: auth.LoginReq(handlers.Logout(server, auth, conn, discordAPI)),
|
||||
},
|
||||
{
|
||||
Path: "/test",
|
||||
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
|
||||
Handler: handlers.Test(server),
|
||||
},
|
||||
}
|
||||
|
||||
htmxRoutes := []hws.Route{
|
||||
|
||||
141
internal/handlers/notifications.go
Normal file
141
internal/handlers/notifications.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
)
|
||||
|
||||
// NotificationType defines the type of notification
|
||||
type NotificationType string
|
||||
|
||||
const (
|
||||
NotificationSuccess NotificationType = "success"
|
||||
NotificationWarning NotificationType = "warning"
|
||||
NotificationInfo NotificationType = "info"
|
||||
)
|
||||
|
||||
// Notification represents a toast notification (success, warning, info)
|
||||
type Notification struct {
|
||||
Type NotificationType `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ErrorModal represents a full-screen error modal (500, 503)
|
||||
type ErrorModal struct {
|
||||
Code int `json:"code"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details"`
|
||||
}
|
||||
|
||||
// setHXTrigger sets the HX-Trigger header with JSON-encoded data
|
||||
func setHXTrigger(w http.ResponseWriter, event string, data any) {
|
||||
payload := map[string]any{
|
||||
event: data,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
// Fallback if JSON encoding fails
|
||||
w.Header().Set("HX-Trigger", event)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Trigger", string(jsonData))
|
||||
}
|
||||
|
||||
// formatErrorDetails extracts and formats error details from wrapped errors
|
||||
func formatErrorDetails(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Use %+v format to get stack trace from github.com/pkg/errors
|
||||
return fmt.Sprintf("%+v", err)
|
||||
}
|
||||
|
||||
// notifyToast sends a toast notification via HX-Trigger header
|
||||
func notifyToast(
|
||||
w http.ResponseWriter,
|
||||
notifType NotificationType,
|
||||
title string,
|
||||
message string,
|
||||
) {
|
||||
notification := Notification{
|
||||
Type: notifType,
|
||||
Title: title,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
setHXTrigger(w, "showNotification", notification)
|
||||
}
|
||||
|
||||
// notifyErrorModal sends a full-screen error modal via HX-Trigger header
|
||||
func notifyErrorModal(
|
||||
s *hws.Server,
|
||||
w http.ResponseWriter,
|
||||
statusCode int,
|
||||
title string,
|
||||
message string,
|
||||
err error,
|
||||
) {
|
||||
modal := ErrorModal{
|
||||
Code: statusCode,
|
||||
Title: title,
|
||||
Message: message,
|
||||
Details: formatErrorDetails(err),
|
||||
}
|
||||
|
||||
// Log the error
|
||||
s.LogError(hws.HWSError{
|
||||
StatusCode: statusCode,
|
||||
Message: message,
|
||||
Error: err,
|
||||
Level: hws.ErrorERROR,
|
||||
})
|
||||
|
||||
// Set response status
|
||||
w.WriteHeader(statusCode)
|
||||
|
||||
// Send notification via HX-Trigger
|
||||
setHXTrigger(w, "showErrorModal", modal)
|
||||
}
|
||||
|
||||
// notifySuccess sends a success toast notification
|
||||
func notifySuccess(w http.ResponseWriter, title string, message string) {
|
||||
notifyToast(w, NotificationSuccess, title, message)
|
||||
}
|
||||
|
||||
// notifyWarning sends a warning toast notification
|
||||
func notifyWarning(w http.ResponseWriter, title string, message string) {
|
||||
notifyToast(w, NotificationWarning, title, message)
|
||||
}
|
||||
|
||||
// notifyInfo sends an info toast notification
|
||||
func notifyInfo(w http.ResponseWriter, title string, message string) {
|
||||
notifyToast(w, NotificationInfo, title, message)
|
||||
}
|
||||
|
||||
// notifyInternalServiceError sends a 500 error modal
|
||||
func notifyInternalServiceError(
|
||||
s *hws.Server,
|
||||
w http.ResponseWriter,
|
||||
message string,
|
||||
err error,
|
||||
) {
|
||||
notifyErrorModal(s, w, 500, "Internal Server Error", message, err)
|
||||
}
|
||||
|
||||
// notifyServiceUnavailable sends a 503 error modal
|
||||
func notifyServiceUnavailable(
|
||||
s *hws.Server,
|
||||
w http.ResponseWriter,
|
||||
message string,
|
||||
err error,
|
||||
) {
|
||||
notifyErrorModal(s, w, 503, "Service Unavailable", message, err)
|
||||
}
|
||||
42
internal/handlers/test.go
Normal file
42
internal/handlers/test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.haelnorr.com/h/oslstats/internal/view/page"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"git.haelnorr.com/h/golib/hws"
|
||||
)
|
||||
|
||||
// Handles responses to the / path. Also serves a 404 Page for paths that
|
||||
// don't have explicit handlers
|
||||
func Test(server *hws.Server) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
page.Test().Render(r.Context(), w)
|
||||
} else {
|
||||
r.ParseForm()
|
||||
notifytype := r.Form.Get("type")
|
||||
title := r.Form.Get("title")
|
||||
message := r.Form.Get("message")
|
||||
switch notifytype {
|
||||
case "error":
|
||||
err := errors.New(message)
|
||||
notifyInternalServiceError(server, w, title, err)
|
||||
return
|
||||
case "warn":
|
||||
notifyWarning(w, title, message)
|
||||
return
|
||||
case "info":
|
||||
notifyInfo(w, title, message)
|
||||
return
|
||||
case "success":
|
||||
notifySuccess(w, title, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
126
internal/view/component/popup/errorModal.templ
Normal file
126
internal/view/component/popup/errorModal.templ
Normal 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>
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package popup
|
||||
|
||||
templ Error500Popup() {
|
||||
templ ErrorPopup() {
|
||||
<div
|
||||
x-cloak
|
||||
x-show="showError500"
|
||||
10
internal/view/component/popup/toastContainer.templ
Normal file
10
internal/view/component/popup/toastContainer.templ
Normal 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>
|
||||
}
|
||||
114
internal/view/component/popup/toastNotification.templ
Normal file
114
internal/view/component/popup/toastNotification.templ
Normal 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>
|
||||
}
|
||||
@@ -34,35 +34,139 @@ templ Global(title string) {
|
||||
htmx.logAll();
|
||||
</script>
|
||||
}
|
||||
<script>
|
||||
const bodyData = {
|
||||
showError500: false,
|
||||
showError503: false,
|
||||
// handle errors from the server on HTMX requests
|
||||
handleHtmxError(event) {
|
||||
const errorCode = event.detail.errorInfo.error;
|
||||
|
||||
// internal server error
|
||||
if (errorCode.includes("Code 500")) {
|
||||
this.showError500 = true;
|
||||
setTimeout(() => (this.showError500 = false), 6000);
|
||||
<script>
|
||||
const bodyData = {
|
||||
// Error modal (blocking, full-screen) - for 500/503 errors
|
||||
errorModal: {
|
||||
show: false,
|
||||
code: 0,
|
||||
title: '',
|
||||
message: '',
|
||||
details: ''
|
||||
},
|
||||
|
||||
// Toast notifications (non-blocking, stacked) - for success/warning/info
|
||||
toasts: [],
|
||||
toastIdCounter: 0,
|
||||
|
||||
// Handle HTMX beforeSwap event - intercept both errors and successes
|
||||
handleBeforeSwap(event) {
|
||||
const xhr = event.detail.xhr;
|
||||
if (!xhr) return;
|
||||
|
||||
const status = xhr.status;
|
||||
const trigger = xhr.getResponseHeader('HX-Trigger');
|
||||
if (!trigger) return;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(trigger);
|
||||
|
||||
// Handle 500/503 error modals
|
||||
if ((status === 500 || status === 503) && data.showErrorModal) {
|
||||
this.errorModal = {
|
||||
show: true,
|
||||
code: data.showErrorModal.code,
|
||||
title: data.showErrorModal.title,
|
||||
message: data.showErrorModal.message,
|
||||
details: data.showErrorModal.details
|
||||
};
|
||||
// Prevent swap but allow error logging
|
||||
event.detail.shouldSwap = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle success/warning/info toasts (200-299)
|
||||
if (status >= 200 && status < 300 && data.showNotification) {
|
||||
this.addToast(data.showNotification);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse HX-Trigger:', e);
|
||||
}
|
||||
},
|
||||
|
||||
// Add toast to stack
|
||||
addToast(notification) {
|
||||
const toast = {
|
||||
id: ++this.toastIdCounter,
|
||||
type: notification.type,
|
||||
title: notification.title,
|
||||
message: notification.message,
|
||||
paused: false,
|
||||
progress: 0
|
||||
};
|
||||
|
||||
this.toasts.push(toast);
|
||||
|
||||
// Determine timeout based on type
|
||||
const timeout = notification.type === 'warning' ? 5000 : 3000;
|
||||
|
||||
// Start progress animation
|
||||
this.animateToastProgress(toast.id, timeout);
|
||||
},
|
||||
|
||||
// Animate toast progress bar and auto-dismiss
|
||||
animateToastProgress(toastId, duration) {
|
||||
const toast = this.toasts.find(t => t.id === toastId);
|
||||
if (!toast) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
let totalPausedTime = 0;
|
||||
let pauseStartTime = null;
|
||||
|
||||
const animate = (currentTime) => {
|
||||
const toast = this.toasts.find(t => t.id === toastId);
|
||||
if (!toast) return; // Toast was manually removed
|
||||
|
||||
if (toast.paused) {
|
||||
// Track when pause started
|
||||
if (pauseStartTime === null) {
|
||||
pauseStartTime = currentTime;
|
||||
}
|
||||
// service not available error
|
||||
if (errorCode.includes("Code 503")) {
|
||||
this.showError503 = true;
|
||||
setTimeout(() => (this.showError503 = false), 6000);
|
||||
// Keep animating while paused
|
||||
requestAnimationFrame(animate);
|
||||
return;
|
||||
} else {
|
||||
// If we were paused, accumulate the paused time
|
||||
if (pauseStartTime !== null) {
|
||||
totalPausedTime += currentTime - pauseStartTime;
|
||||
pauseStartTime = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Calculate actual elapsed time (excluding paused time)
|
||||
const elapsed = currentTime - startTime - totalPausedTime;
|
||||
toast.progress = Math.min((elapsed / duration) * 100, 100);
|
||||
|
||||
if (elapsed >= duration) {
|
||||
this.removeToast(toastId);
|
||||
} else {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
// Remove toast from stack
|
||||
removeToast(id) {
|
||||
this.toasts = this.toasts.filter(t => t.id !== id);
|
||||
},
|
||||
|
||||
// Close error modal
|
||||
closeErrorModal() {
|
||||
this.errorModal.show = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body
|
||||
class="bg-base text-text ubuntu-mono-regular overflow-x-hidden"
|
||||
x-data="bodyData"
|
||||
x-on:htmx:error="handleHtmxError($event)"
|
||||
>
|
||||
@popup.Error500Popup()
|
||||
@popup.Error503Popup()
|
||||
<body
|
||||
class="bg-base text-text ubuntu-mono-regular overflow-x-hidden"
|
||||
x-data="bodyData"
|
||||
x-on:htmx:before-swap="handleBeforeSwap($event)"
|
||||
x-on:keydown.escape.window="closeErrorModal()"
|
||||
>
|
||||
@popup.ErrorModal()
|
||||
@popup.ToastContainer()
|
||||
<div
|
||||
id="main-content"
|
||||
class="flex flex-col h-screen justify-between"
|
||||
|
||||
71
internal/view/page/test.templ
Normal file
71
internal/view/page/test.templ
Normal file
@@ -0,0 +1,71 @@
|
||||
package page
|
||||
|
||||
import "git.haelnorr.com/h/oslstats/internal/view/layout"
|
||||
|
||||
// Page content for the test notification page
|
||||
templ Test() {
|
||||
@layout.Global("Notification Test") {
|
||||
<div class="flex items-center justify-center min-h-[calc(100vh-200px)]">
|
||||
<div class="w-full max-w-md px-4">
|
||||
<!-- Title -->
|
||||
<h1 class="text-4xl font-bold text-center text-text mb-8">
|
||||
Notification Test
|
||||
</h1>
|
||||
<!-- Form Card -->
|
||||
<div class="bg-surface0 rounded-lg shadow-lg p-6 border border-overlay0">
|
||||
<form hx-post="/test" hx-swap="none" class="flex flex-col gap-4">
|
||||
<!-- Notification Type -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="type" class="text-sm font-semibold text-subtext1">
|
||||
Notification Type
|
||||
</label>
|
||||
<select
|
||||
name="type"
|
||||
id="type"
|
||||
class="bg-base text-text border border-overlay0 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-mauve"
|
||||
>
|
||||
<option value="error">Error Modal</option>
|
||||
<option value="warn">Warning Toast</option>
|
||||
<option value="info">Info Toast</option>
|
||||
<option value="success">Success Toast</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Title Input -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="title" class="text-sm font-semibold text-subtext1">
|
||||
Title
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Enter notification title"
|
||||
class="bg-base text-text border border-overlay0 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-mauve"
|
||||
/>
|
||||
</div>
|
||||
<!-- Message Input -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="message" class="text-sm font-semibold text-subtext1">
|
||||
Message
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="message"
|
||||
id="message"
|
||||
placeholder="Enter notification message"
|
||||
class="bg-base text-text border border-overlay0 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-mauve"
|
||||
/>
|
||||
</div>
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="mt-2 bg-mauve text-crust font-semibold px-6 py-3 rounded-lg hover:bg-mauve/75 transition"
|
||||
>
|
||||
Send Notification
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,14 @@
|
||||
--color-maroon: var(--maroon);
|
||||
--color-peach: var(--peach);
|
||||
--color-yellow: var(--yellow);
|
||||
--color-dark-yellow: var(--dark-yellow);
|
||||
--color-green: var(--green);
|
||||
--color-dark-green: var(--dark-green);
|
||||
--color-teal: var(--teal);
|
||||
--color-sky: var(--sky);
|
||||
--color-sapphire: var(--sapphire);
|
||||
--color-blue: var(--blue);
|
||||
--color-dark-blue: var(--dark-blue);
|
||||
--color-lavender: var(--lavender);
|
||||
--color-text: var(--text);
|
||||
--color-subtext1: var(--subtext1);
|
||||
@@ -45,11 +48,14 @@
|
||||
--maroon: hsl(355, 76%, 59%);
|
||||
--peach: hsl(22, 99%, 52%);
|
||||
--yellow: hsl(35, 77%, 49%);
|
||||
--dark-yellow: hsl(35, 50%, 85%);
|
||||
--green: hsl(109, 58%, 40%);
|
||||
--dark-green: hsl(109, 35%, 85%);
|
||||
--teal: hsl(183, 74%, 35%);
|
||||
--sky: hsl(197, 97%, 46%);
|
||||
--sapphire: hsl(189, 70%, 42%);
|
||||
--blue: hsl(220, 91%, 54%);
|
||||
--dark-blue: hsl(220, 50%, 85%);
|
||||
--lavender: hsl(231, 97%, 72%);
|
||||
--text: hsl(234, 16%, 35%);
|
||||
--subtext1: hsl(233, 13%, 41%);
|
||||
@@ -75,11 +81,14 @@
|
||||
--maroon: hsl(350, 65%, 77%);
|
||||
--peach: hsl(23, 92%, 75%);
|
||||
--yellow: hsl(41, 86%, 83%);
|
||||
--dark-yellow: hsl(41, 30%, 25%);
|
||||
--green: hsl(115, 54%, 76%);
|
||||
--dark-green: hsl(115, 25%, 22%);
|
||||
--teal: hsl(170, 57%, 73%);
|
||||
--sky: hsl(189, 71%, 73%);
|
||||
--sapphire: hsl(199, 76%, 69%);
|
||||
--blue: hsl(217, 92%, 76%);
|
||||
--dark-blue: hsl(217, 30%, 25%);
|
||||
--lavender: hsl(232, 97%, 85%);
|
||||
--text: hsl(226, 64%, 88%);
|
||||
--subtext1: hsl(227, 35%, 80%);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
monospace;
|
||||
--spacing: 0.25rem;
|
||||
--breakpoint-xl: 80rem;
|
||||
--container-sm: 24rem;
|
||||
--container-md: 28rem;
|
||||
--container-2xl: 42rem;
|
||||
--container-7xl: 80rem;
|
||||
@@ -39,10 +40,42 @@
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--default-transition-duration: 150ms;
|
||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--default-font-family: var(--font-sans);
|
||||
--default-mono-font-family: var(--font-mono);
|
||||
--color-rosewater: var(--rosewater);
|
||||
--color-flamingo: var(--flamingo);
|
||||
--color-pink: var(--pink);
|
||||
--color-mauve: var(--mauve);
|
||||
--color-red: var(--red);
|
||||
--color-dark-red: var(--dark-red);
|
||||
--color-maroon: var(--maroon);
|
||||
--color-peach: var(--peach);
|
||||
--color-yellow: var(--yellow);
|
||||
--color-dark-yellow: var(--dark-yellow);
|
||||
--color-green: var(--green);
|
||||
--color-dark-green: var(--dark-green);
|
||||
--color-teal: var(--teal);
|
||||
--color-sky: var(--sky);
|
||||
--color-sapphire: var(--sapphire);
|
||||
--color-blue: var(--blue);
|
||||
--color-dark-blue: var(--dark-blue);
|
||||
--color-lavender: var(--lavender);
|
||||
--color-text: var(--text);
|
||||
--color-subtext1: var(--subtext1);
|
||||
--color-subtext0: var(--subtext0);
|
||||
--color-overlay2: var(--overlay2);
|
||||
--color-overlay1: var(--overlay1);
|
||||
--color-overlay0: var(--overlay0);
|
||||
--color-surface2: var(--surface2);
|
||||
--color-surface1: var(--surface1);
|
||||
--color-surface0: var(--surface0);
|
||||
--color-base: var(--base);
|
||||
--color-mantle: var(--mantle);
|
||||
--color-crust: var(--crust);
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
@@ -194,6 +227,15 @@
|
||||
}
|
||||
}
|
||||
@layer utilities {
|
||||
.pointer-events-auto {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
.collapse {
|
||||
visibility: collapse;
|
||||
}
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -211,12 +253,18 @@
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
.inset-0 {
|
||||
inset: calc(var(--spacing) * 0);
|
||||
}
|
||||
.end-0 {
|
||||
inset-inline-end: calc(var(--spacing) * 0);
|
||||
}
|
||||
@@ -229,21 +277,57 @@
|
||||
.top-4 {
|
||||
top: calc(var(--spacing) * 4);
|
||||
}
|
||||
.top-20 {
|
||||
top: calc(var(--spacing) * 20);
|
||||
}
|
||||
.right-0 {
|
||||
right: calc(var(--spacing) * 0);
|
||||
}
|
||||
.right-5 {
|
||||
right: calc(var(--spacing) * 5);
|
||||
}
|
||||
.bottom-0 {
|
||||
bottom: calc(var(--spacing) * 0);
|
||||
}
|
||||
.left-0 {
|
||||
left: calc(var(--spacing) * 0);
|
||||
}
|
||||
.z-3 {
|
||||
z-index: 3;
|
||||
}
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
.z-40 {
|
||||
z-index: 40;
|
||||
}
|
||||
.z-50 {
|
||||
z-index: 50;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
@media (width >= 40rem) {
|
||||
max-width: 40rem;
|
||||
}
|
||||
@media (width >= 48rem) {
|
||||
max-width: 48rem;
|
||||
}
|
||||
@media (width >= 64rem) {
|
||||
max-width: 64rem;
|
||||
}
|
||||
@media (width >= 80rem) {
|
||||
max-width: 80rem;
|
||||
}
|
||||
@media (width >= 96rem) {
|
||||
max-width: 96rem;
|
||||
}
|
||||
}
|
||||
.mx-auto {
|
||||
margin-inline: auto;
|
||||
}
|
||||
.mt-1 {
|
||||
margin-top: calc(var(--spacing) * 1);
|
||||
}
|
||||
.mt-1\.5 {
|
||||
margin-top: calc(var(--spacing) * 1.5);
|
||||
}
|
||||
@@ -280,6 +364,9 @@
|
||||
.mr-5 {
|
||||
margin-right: calc(var(--spacing) * 5);
|
||||
}
|
||||
.mb-8 {
|
||||
margin-bottom: calc(var(--spacing) * 8);
|
||||
}
|
||||
.mb-auto {
|
||||
margin-bottom: auto;
|
||||
}
|
||||
@@ -321,6 +408,13 @@
|
||||
width: calc(var(--spacing) * 6);
|
||||
height: calc(var(--spacing) * 6);
|
||||
}
|
||||
.size-8 {
|
||||
width: calc(var(--spacing) * 8);
|
||||
height: calc(var(--spacing) * 8);
|
||||
}
|
||||
.h-1 {
|
||||
height: calc(var(--spacing) * 1);
|
||||
}
|
||||
.h-16 {
|
||||
height: calc(var(--spacing) * 16);
|
||||
}
|
||||
@@ -330,6 +424,15 @@
|
||||
.h-screen {
|
||||
height: 100vh;
|
||||
}
|
||||
.max-h-96 {
|
||||
max-height: calc(var(--spacing) * 96);
|
||||
}
|
||||
.max-h-\[90vh\] {
|
||||
max-height: 90vh;
|
||||
}
|
||||
.min-h-\[calc\(100vh-200px\)\] {
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
.w-26 {
|
||||
width: calc(var(--spacing) * 26);
|
||||
}
|
||||
@@ -360,9 +463,30 @@
|
||||
.max-w-screen-xl {
|
||||
max-width: var(--breakpoint-xl);
|
||||
}
|
||||
.max-w-sm {
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
.min-w-0 {
|
||||
min-width: calc(var(--spacing) * 0);
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
.flex-shrink {
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.translate-x-0 {
|
||||
--tw-translate-x: calc(var(--spacing) * 0);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
@@ -371,12 +495,27 @@
|
||||
--tw-translate-x: 100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-x-full {
|
||||
--tw-translate-x: 100%;
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-y-0 {
|
||||
--tw-translate-y: calc(var(--spacing) * 0);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.translate-y-4 {
|
||||
--tw-translate-y: calc(var(--spacing) * 4);
|
||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||
}
|
||||
.transform {
|
||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||
}
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.resize {
|
||||
resize: both;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -389,6 +528,9 @@
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -401,6 +543,12 @@
|
||||
.gap-2 {
|
||||
gap: calc(var(--spacing) * 2);
|
||||
}
|
||||
.gap-3 {
|
||||
gap: calc(var(--spacing) * 3);
|
||||
}
|
||||
.gap-4 {
|
||||
gap: calc(var(--spacing) * 4);
|
||||
}
|
||||
.gap-6 {
|
||||
gap: calc(var(--spacing) * 6);
|
||||
}
|
||||
@@ -434,6 +582,11 @@
|
||||
border-color: var(--surface2);
|
||||
}
|
||||
}
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -443,6 +596,9 @@
|
||||
.overflow-x-hidden {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.overflow-y-auto {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
@@ -466,6 +622,16 @@
|
||||
border-style: var(--tw-border-style);
|
||||
border-width: 2px;
|
||||
}
|
||||
.border-b {
|
||||
border-bottom-style: var(--tw-border-style);
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.border-blue {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
.border-dark-red {
|
||||
border-color: var(--dark-red);
|
||||
}
|
||||
.border-green {
|
||||
border-color: var(--green);
|
||||
}
|
||||
@@ -481,15 +647,36 @@
|
||||
.border-transparent {
|
||||
border-color: transparent;
|
||||
}
|
||||
.border-yellow {
|
||||
border-color: var(--yellow);
|
||||
}
|
||||
.bg-base {
|
||||
background-color: var(--base);
|
||||
}
|
||||
.bg-blue {
|
||||
background-color: var(--blue);
|
||||
}
|
||||
.bg-crust {
|
||||
background-color: var(--crust);
|
||||
}
|
||||
.bg-crust\/80 {
|
||||
background-color: var(--crust);
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
background-color: color-mix(in oklab, var(--crust) 80%, transparent);
|
||||
}
|
||||
}
|
||||
.bg-dark-blue {
|
||||
background-color: var(--dark-blue);
|
||||
}
|
||||
.bg-dark-green {
|
||||
background-color: var(--dark-green);
|
||||
}
|
||||
.bg-dark-red {
|
||||
background-color: var(--dark-red);
|
||||
}
|
||||
.bg-dark-yellow {
|
||||
background-color: var(--dark-yellow);
|
||||
}
|
||||
.bg-green {
|
||||
background-color: var(--green);
|
||||
}
|
||||
@@ -508,6 +695,9 @@
|
||||
.bg-teal {
|
||||
background-color: var(--teal);
|
||||
}
|
||||
.bg-yellow {
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
.p-2 {
|
||||
padding: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -517,6 +707,9 @@
|
||||
.p-4 {
|
||||
padding: calc(var(--spacing) * 4);
|
||||
}
|
||||
.p-6 {
|
||||
padding: calc(var(--spacing) * 6);
|
||||
}
|
||||
.px-2 {
|
||||
padding-inline: calc(var(--spacing) * 2);
|
||||
}
|
||||
@@ -529,6 +722,9 @@
|
||||
.px-5 {
|
||||
padding-inline: calc(var(--spacing) * 5);
|
||||
}
|
||||
.px-6 {
|
||||
padding-inline: calc(var(--spacing) * 6);
|
||||
}
|
||||
.py-1 {
|
||||
padding-block: calc(var(--spacing) * 1);
|
||||
}
|
||||
@@ -617,9 +813,15 @@
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.text-blue {
|
||||
color: var(--blue);
|
||||
}
|
||||
.text-crust {
|
||||
color: var(--crust);
|
||||
}
|
||||
.text-green {
|
||||
color: var(--green);
|
||||
}
|
||||
.text-mantle {
|
||||
color: var(--mantle);
|
||||
}
|
||||
@@ -638,6 +840,15 @@
|
||||
.text-text {
|
||||
color: var(--text);
|
||||
}
|
||||
.text-yellow {
|
||||
color: var(--yellow);
|
||||
}
|
||||
.lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
.opacity-0 {
|
||||
opacity: 0%;
|
||||
}
|
||||
@@ -652,15 +863,46 @@
|
||||
--tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.shadow-xl {
|
||||
--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
.outline {
|
||||
outline-style: var(--tw-outline-style);
|
||||
outline-width: 1px;
|
||||
}
|
||||
.filter {
|
||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||
}
|
||||
.transition {
|
||||
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||
}
|
||||
.delay-100 {
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
.duration-200 {
|
||||
--tw-duration: 200ms;
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
.duration-300 {
|
||||
--tw-duration: 300ms;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
.ease-in {
|
||||
--tw-ease: var(--ease-in);
|
||||
transition-timing-function: var(--ease-in);
|
||||
}
|
||||
.ease-out {
|
||||
--tw-ease: var(--ease-out);
|
||||
transition-timing-function: var(--ease-out);
|
||||
}
|
||||
.outline-none {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
@@ -786,6 +1028,23 @@
|
||||
border-color: var(--red);
|
||||
}
|
||||
}
|
||||
.focus\:ring-2 {
|
||||
&:focus {
|
||||
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||
}
|
||||
}
|
||||
.focus\:ring-mauve {
|
||||
&:focus {
|
||||
--tw-ring-color: var(--mauve);
|
||||
}
|
||||
}
|
||||
.focus\:outline-none {
|
||||
&:focus {
|
||||
--tw-outline-style: none;
|
||||
outline-style: none;
|
||||
}
|
||||
}
|
||||
.disabled\:pointer-events-none {
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
@@ -940,11 +1199,14 @@
|
||||
--maroon: hsl(355, 76%, 59%);
|
||||
--peach: hsl(22, 99%, 52%);
|
||||
--yellow: hsl(35, 77%, 49%);
|
||||
--dark-yellow: hsl(35, 50%, 85%);
|
||||
--green: hsl(109, 58%, 40%);
|
||||
--dark-green: hsl(109, 35%, 85%);
|
||||
--teal: hsl(183, 74%, 35%);
|
||||
--sky: hsl(197, 97%, 46%);
|
||||
--sapphire: hsl(189, 70%, 42%);
|
||||
--blue: hsl(220, 91%, 54%);
|
||||
--dark-blue: hsl(220, 50%, 85%);
|
||||
--lavender: hsl(231, 97%, 72%);
|
||||
--text: hsl(234, 16%, 35%);
|
||||
--subtext1: hsl(233, 13%, 41%);
|
||||
@@ -969,11 +1231,14 @@
|
||||
--maroon: hsl(350, 65%, 77%);
|
||||
--peach: hsl(23, 92%, 75%);
|
||||
--yellow: hsl(41, 86%, 83%);
|
||||
--dark-yellow: hsl(41, 30%, 25%);
|
||||
--green: hsl(115, 54%, 76%);
|
||||
--dark-green: hsl(115, 25%, 22%);
|
||||
--teal: hsl(170, 57%, 73%);
|
||||
--sky: hsl(189, 71%, 73%);
|
||||
--sapphire: hsl(199, 76%, 69%);
|
||||
--blue: hsl(217, 92%, 76%);
|
||||
--dark-blue: hsl(217, 30%, 25%);
|
||||
--lavender: hsl(232, 97%, 85%);
|
||||
--text: hsl(226, 64%, 88%);
|
||||
--subtext1: hsl(227, 35%, 80%);
|
||||
@@ -1135,10 +1400,72 @@
|
||||
inherits: false;
|
||||
initial-value: 0 0 #0000;
|
||||
}
|
||||
@property --tw-outline-style {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
initial-value: solid;
|
||||
}
|
||||
@property --tw-blur {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-brightness {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-contrast {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-grayscale {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-hue-rotate {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-invert {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-opacity {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-saturate {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-sepia {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-drop-shadow {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-drop-shadow-color {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-drop-shadow-alpha {
|
||||
syntax: "<percentage>";
|
||||
inherits: false;
|
||||
initial-value: 100%;
|
||||
}
|
||||
@property --tw-drop-shadow-size {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-duration {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@property --tw-ease {
|
||||
syntax: "*";
|
||||
inherits: false;
|
||||
}
|
||||
@layer properties {
|
||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||
*, ::before, ::after, ::backdrop {
|
||||
@@ -1170,7 +1497,22 @@
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-outline-style: solid;
|
||||
--tw-blur: initial;
|
||||
--tw-brightness: initial;
|
||||
--tw-contrast: initial;
|
||||
--tw-grayscale: initial;
|
||||
--tw-hue-rotate: initial;
|
||||
--tw-invert: initial;
|
||||
--tw-opacity: initial;
|
||||
--tw-saturate: initial;
|
||||
--tw-sepia: initial;
|
||||
--tw-drop-shadow: initial;
|
||||
--tw-drop-shadow-color: initial;
|
||||
--tw-drop-shadow-alpha: 100%;
|
||||
--tw-drop-shadow-size: initial;
|
||||
--tw-duration: initial;
|
||||
--tw-ease: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user