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},
|
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
|
||||||
Handler: auth.LoginReq(handlers.Logout(server, auth, conn, discordAPI)),
|
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{
|
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
|
package popup
|
||||||
|
|
||||||
templ Error500Popup() {
|
templ ErrorPopup() {
|
||||||
<div
|
<div
|
||||||
x-cloak
|
x-cloak
|
||||||
x-show="showError500"
|
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();
|
htmx.logAll();
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
const bodyData = {
|
const bodyData = {
|
||||||
showError500: false,
|
// Error modal (blocking, full-screen) - for 500/503 errors
|
||||||
showError503: false,
|
errorModal: {
|
||||||
// handle errors from the server on HTMX requests
|
show: false,
|
||||||
handleHtmxError(event) {
|
code: 0,
|
||||||
const errorCode = event.detail.errorInfo.error;
|
title: '',
|
||||||
|
message: '',
|
||||||
// internal server error
|
details: ''
|
||||||
if (errorCode.includes("Code 500")) {
|
},
|
||||||
this.showError500 = true;
|
|
||||||
setTimeout(() => (this.showError500 = false), 6000);
|
// 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
|
// Keep animating while paused
|
||||||
if (errorCode.includes("Code 503")) {
|
requestAnimationFrame(animate);
|
||||||
this.showError503 = true;
|
return;
|
||||||
setTimeout(() => (this.showError503 = false), 6000);
|
} 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>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="bg-base text-text ubuntu-mono-regular overflow-x-hidden"
|
class="bg-base text-text ubuntu-mono-regular overflow-x-hidden"
|
||||||
x-data="bodyData"
|
x-data="bodyData"
|
||||||
x-on:htmx:error="handleHtmxError($event)"
|
x-on:htmx:before-swap="handleBeforeSwap($event)"
|
||||||
>
|
x-on:keydown.escape.window="closeErrorModal()"
|
||||||
@popup.Error500Popup()
|
>
|
||||||
@popup.Error503Popup()
|
@popup.ErrorModal()
|
||||||
|
@popup.ToastContainer()
|
||||||
<div
|
<div
|
||||||
id="main-content"
|
id="main-content"
|
||||||
class="flex flex-col h-screen justify-between"
|
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-maroon: var(--maroon);
|
||||||
--color-peach: var(--peach);
|
--color-peach: var(--peach);
|
||||||
--color-yellow: var(--yellow);
|
--color-yellow: var(--yellow);
|
||||||
|
--color-dark-yellow: var(--dark-yellow);
|
||||||
--color-green: var(--green);
|
--color-green: var(--green);
|
||||||
|
--color-dark-green: var(--dark-green);
|
||||||
--color-teal: var(--teal);
|
--color-teal: var(--teal);
|
||||||
--color-sky: var(--sky);
|
--color-sky: var(--sky);
|
||||||
--color-sapphire: var(--sapphire);
|
--color-sapphire: var(--sapphire);
|
||||||
--color-blue: var(--blue);
|
--color-blue: var(--blue);
|
||||||
|
--color-dark-blue: var(--dark-blue);
|
||||||
--color-lavender: var(--lavender);
|
--color-lavender: var(--lavender);
|
||||||
--color-text: var(--text);
|
--color-text: var(--text);
|
||||||
--color-subtext1: var(--subtext1);
|
--color-subtext1: var(--subtext1);
|
||||||
@@ -45,11 +48,14 @@
|
|||||||
--maroon: hsl(355, 76%, 59%);
|
--maroon: hsl(355, 76%, 59%);
|
||||||
--peach: hsl(22, 99%, 52%);
|
--peach: hsl(22, 99%, 52%);
|
||||||
--yellow: hsl(35, 77%, 49%);
|
--yellow: hsl(35, 77%, 49%);
|
||||||
|
--dark-yellow: hsl(35, 50%, 85%);
|
||||||
--green: hsl(109, 58%, 40%);
|
--green: hsl(109, 58%, 40%);
|
||||||
|
--dark-green: hsl(109, 35%, 85%);
|
||||||
--teal: hsl(183, 74%, 35%);
|
--teal: hsl(183, 74%, 35%);
|
||||||
--sky: hsl(197, 97%, 46%);
|
--sky: hsl(197, 97%, 46%);
|
||||||
--sapphire: hsl(189, 70%, 42%);
|
--sapphire: hsl(189, 70%, 42%);
|
||||||
--blue: hsl(220, 91%, 54%);
|
--blue: hsl(220, 91%, 54%);
|
||||||
|
--dark-blue: hsl(220, 50%, 85%);
|
||||||
--lavender: hsl(231, 97%, 72%);
|
--lavender: hsl(231, 97%, 72%);
|
||||||
--text: hsl(234, 16%, 35%);
|
--text: hsl(234, 16%, 35%);
|
||||||
--subtext1: hsl(233, 13%, 41%);
|
--subtext1: hsl(233, 13%, 41%);
|
||||||
@@ -75,11 +81,14 @@
|
|||||||
--maroon: hsl(350, 65%, 77%);
|
--maroon: hsl(350, 65%, 77%);
|
||||||
--peach: hsl(23, 92%, 75%);
|
--peach: hsl(23, 92%, 75%);
|
||||||
--yellow: hsl(41, 86%, 83%);
|
--yellow: hsl(41, 86%, 83%);
|
||||||
|
--dark-yellow: hsl(41, 30%, 25%);
|
||||||
--green: hsl(115, 54%, 76%);
|
--green: hsl(115, 54%, 76%);
|
||||||
|
--dark-green: hsl(115, 25%, 22%);
|
||||||
--teal: hsl(170, 57%, 73%);
|
--teal: hsl(170, 57%, 73%);
|
||||||
--sky: hsl(189, 71%, 73%);
|
--sky: hsl(189, 71%, 73%);
|
||||||
--sapphire: hsl(199, 76%, 69%);
|
--sapphire: hsl(199, 76%, 69%);
|
||||||
--blue: hsl(217, 92%, 76%);
|
--blue: hsl(217, 92%, 76%);
|
||||||
|
--dark-blue: hsl(217, 30%, 25%);
|
||||||
--lavender: hsl(232, 97%, 85%);
|
--lavender: hsl(232, 97%, 85%);
|
||||||
--text: hsl(226, 64%, 88%);
|
--text: hsl(226, 64%, 88%);
|
||||||
--subtext1: hsl(227, 35%, 80%);
|
--subtext1: hsl(227, 35%, 80%);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
monospace;
|
monospace;
|
||||||
--spacing: 0.25rem;
|
--spacing: 0.25rem;
|
||||||
--breakpoint-xl: 80rem;
|
--breakpoint-xl: 80rem;
|
||||||
|
--container-sm: 24rem;
|
||||||
--container-md: 28rem;
|
--container-md: 28rem;
|
||||||
--container-2xl: 42rem;
|
--container-2xl: 42rem;
|
||||||
--container-7xl: 80rem;
|
--container-7xl: 80rem;
|
||||||
@@ -39,10 +40,42 @@
|
|||||||
--radius-sm: 0.25rem;
|
--radius-sm: 0.25rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
--radius-xl: 0.75rem;
|
--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-duration: 150ms;
|
||||||
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
--default-font-family: var(--font-sans);
|
--default-font-family: var(--font-sans);
|
||||||
--default-mono-font-family: var(--font-mono);
|
--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 {
|
@layer base {
|
||||||
@@ -194,6 +227,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.pointer-events-auto {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.collapse {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -211,12 +253,18 @@
|
|||||||
.absolute {
|
.absolute {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
.relative {
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.static {
|
.static {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
.inset-0 {
|
||||||
|
inset: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.end-0 {
|
.end-0 {
|
||||||
inset-inline-end: calc(var(--spacing) * 0);
|
inset-inline-end: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
@@ -229,21 +277,57 @@
|
|||||||
.top-4 {
|
.top-4 {
|
||||||
top: calc(var(--spacing) * 4);
|
top: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.top-20 {
|
||||||
|
top: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
.right-0 {
|
.right-0 {
|
||||||
right: calc(var(--spacing) * 0);
|
right: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.right-5 {
|
||||||
|
right: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
.bottom-0 {
|
.bottom-0 {
|
||||||
bottom: calc(var(--spacing) * 0);
|
bottom: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.left-0 {
|
.left-0 {
|
||||||
left: calc(var(--spacing) * 0);
|
left: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.z-3 {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
.z-10 {
|
.z-10 {
|
||||||
z-index: 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 {
|
.mx-auto {
|
||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
.mt-1\.5 {
|
.mt-1\.5 {
|
||||||
margin-top: calc(var(--spacing) * 1.5);
|
margin-top: calc(var(--spacing) * 1.5);
|
||||||
}
|
}
|
||||||
@@ -280,6 +364,9 @@
|
|||||||
.mr-5 {
|
.mr-5 {
|
||||||
margin-right: calc(var(--spacing) * 5);
|
margin-right: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
.mb-auto {
|
.mb-auto {
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
@@ -321,6 +408,13 @@
|
|||||||
width: calc(var(--spacing) * 6);
|
width: calc(var(--spacing) * 6);
|
||||||
height: 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 {
|
.h-16 {
|
||||||
height: calc(var(--spacing) * 16);
|
height: calc(var(--spacing) * 16);
|
||||||
}
|
}
|
||||||
@@ -330,6 +424,15 @@
|
|||||||
.h-screen {
|
.h-screen {
|
||||||
height: 100vh;
|
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 {
|
.w-26 {
|
||||||
width: calc(var(--spacing) * 26);
|
width: calc(var(--spacing) * 26);
|
||||||
}
|
}
|
||||||
@@ -360,9 +463,30 @@
|
|||||||
.max-w-screen-xl {
|
.max-w-screen-xl {
|
||||||
max-width: var(--breakpoint-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: 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 {
|
.translate-x-0 {
|
||||||
--tw-translate-x: calc(var(--spacing) * 0);
|
--tw-translate-x: calc(var(--spacing) * 0);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -371,12 +495,27 @@
|
|||||||
--tw-translate-x: 100%;
|
--tw-translate-x: 100%;
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
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 {
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
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 {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.resize {
|
||||||
|
resize: both;
|
||||||
|
}
|
||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -389,6 +528,9 @@
|
|||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.items-start {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
.justify-between {
|
.justify-between {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@@ -401,6 +543,12 @@
|
|||||||
.gap-2 {
|
.gap-2 {
|
||||||
gap: calc(var(--spacing) * 2);
|
gap: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
|
.gap-3 {
|
||||||
|
gap: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
.gap-6 {
|
.gap-6 {
|
||||||
gap: calc(var(--spacing) * 6);
|
gap: calc(var(--spacing) * 6);
|
||||||
}
|
}
|
||||||
@@ -434,6 +582,11 @@
|
|||||||
border-color: var(--surface2);
|
border-color: var(--surface2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -443,6 +596,9 @@
|
|||||||
.overflow-x-hidden {
|
.overflow-x-hidden {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
.overflow-y-auto {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
.rounded {
|
.rounded {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
@@ -466,6 +622,16 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 2px;
|
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-green {
|
||||||
border-color: var(--green);
|
border-color: var(--green);
|
||||||
}
|
}
|
||||||
@@ -481,15 +647,36 @@
|
|||||||
.border-transparent {
|
.border-transparent {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
.border-yellow {
|
||||||
|
border-color: var(--yellow);
|
||||||
|
}
|
||||||
.bg-base {
|
.bg-base {
|
||||||
background-color: var(--base);
|
background-color: var(--base);
|
||||||
}
|
}
|
||||||
|
.bg-blue {
|
||||||
|
background-color: var(--blue);
|
||||||
|
}
|
||||||
.bg-crust {
|
.bg-crust {
|
||||||
background-color: var(--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 {
|
.bg-dark-red {
|
||||||
background-color: var(--dark-red);
|
background-color: var(--dark-red);
|
||||||
}
|
}
|
||||||
|
.bg-dark-yellow {
|
||||||
|
background-color: var(--dark-yellow);
|
||||||
|
}
|
||||||
.bg-green {
|
.bg-green {
|
||||||
background-color: var(--green);
|
background-color: var(--green);
|
||||||
}
|
}
|
||||||
@@ -508,6 +695,9 @@
|
|||||||
.bg-teal {
|
.bg-teal {
|
||||||
background-color: var(--teal);
|
background-color: var(--teal);
|
||||||
}
|
}
|
||||||
|
.bg-yellow {
|
||||||
|
background-color: var(--yellow);
|
||||||
|
}
|
||||||
.p-2 {
|
.p-2 {
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -517,6 +707,9 @@
|
|||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.p-6 {
|
||||||
|
padding: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.px-2 {
|
.px-2 {
|
||||||
padding-inline: calc(var(--spacing) * 2);
|
padding-inline: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -529,6 +722,9 @@
|
|||||||
.px-5 {
|
.px-5 {
|
||||||
padding-inline: calc(var(--spacing) * 5);
|
padding-inline: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.px-6 {
|
||||||
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.py-1 {
|
.py-1 {
|
||||||
padding-block: calc(var(--spacing) * 1);
|
padding-block: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -617,9 +813,15 @@
|
|||||||
.whitespace-pre-wrap {
|
.whitespace-pre-wrap {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
.text-blue {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
.text-crust {
|
.text-crust {
|
||||||
color: var(--crust);
|
color: var(--crust);
|
||||||
}
|
}
|
||||||
|
.text-green {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
.text-mantle {
|
.text-mantle {
|
||||||
color: var(--mantle);
|
color: var(--mantle);
|
||||||
}
|
}
|
||||||
@@ -638,6 +840,15 @@
|
|||||||
.text-text {
|
.text-text {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
.text-yellow {
|
||||||
|
color: var(--yellow);
|
||||||
|
}
|
||||||
|
.lowercase {
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
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));
|
--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);
|
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 {
|
||||||
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-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-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
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 {
|
.duration-200 {
|
||||||
--tw-duration: 200ms;
|
--tw-duration: 200ms;
|
||||||
transition-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 {
|
.outline-none {
|
||||||
--tw-outline-style: none;
|
--tw-outline-style: none;
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
@@ -786,6 +1028,23 @@
|
|||||||
border-color: var(--red);
|
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 {
|
||||||
&:disabled {
|
&:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -940,11 +1199,14 @@
|
|||||||
--maroon: hsl(355, 76%, 59%);
|
--maroon: hsl(355, 76%, 59%);
|
||||||
--peach: hsl(22, 99%, 52%);
|
--peach: hsl(22, 99%, 52%);
|
||||||
--yellow: hsl(35, 77%, 49%);
|
--yellow: hsl(35, 77%, 49%);
|
||||||
|
--dark-yellow: hsl(35, 50%, 85%);
|
||||||
--green: hsl(109, 58%, 40%);
|
--green: hsl(109, 58%, 40%);
|
||||||
|
--dark-green: hsl(109, 35%, 85%);
|
||||||
--teal: hsl(183, 74%, 35%);
|
--teal: hsl(183, 74%, 35%);
|
||||||
--sky: hsl(197, 97%, 46%);
|
--sky: hsl(197, 97%, 46%);
|
||||||
--sapphire: hsl(189, 70%, 42%);
|
--sapphire: hsl(189, 70%, 42%);
|
||||||
--blue: hsl(220, 91%, 54%);
|
--blue: hsl(220, 91%, 54%);
|
||||||
|
--dark-blue: hsl(220, 50%, 85%);
|
||||||
--lavender: hsl(231, 97%, 72%);
|
--lavender: hsl(231, 97%, 72%);
|
||||||
--text: hsl(234, 16%, 35%);
|
--text: hsl(234, 16%, 35%);
|
||||||
--subtext1: hsl(233, 13%, 41%);
|
--subtext1: hsl(233, 13%, 41%);
|
||||||
@@ -969,11 +1231,14 @@
|
|||||||
--maroon: hsl(350, 65%, 77%);
|
--maroon: hsl(350, 65%, 77%);
|
||||||
--peach: hsl(23, 92%, 75%);
|
--peach: hsl(23, 92%, 75%);
|
||||||
--yellow: hsl(41, 86%, 83%);
|
--yellow: hsl(41, 86%, 83%);
|
||||||
|
--dark-yellow: hsl(41, 30%, 25%);
|
||||||
--green: hsl(115, 54%, 76%);
|
--green: hsl(115, 54%, 76%);
|
||||||
|
--dark-green: hsl(115, 25%, 22%);
|
||||||
--teal: hsl(170, 57%, 73%);
|
--teal: hsl(170, 57%, 73%);
|
||||||
--sky: hsl(189, 71%, 73%);
|
--sky: hsl(189, 71%, 73%);
|
||||||
--sapphire: hsl(199, 76%, 69%);
|
--sapphire: hsl(199, 76%, 69%);
|
||||||
--blue: hsl(217, 92%, 76%);
|
--blue: hsl(217, 92%, 76%);
|
||||||
|
--dark-blue: hsl(217, 30%, 25%);
|
||||||
--lavender: hsl(232, 97%, 85%);
|
--lavender: hsl(232, 97%, 85%);
|
||||||
--text: hsl(226, 64%, 88%);
|
--text: hsl(226, 64%, 88%);
|
||||||
--subtext1: hsl(227, 35%, 80%);
|
--subtext1: hsl(227, 35%, 80%);
|
||||||
@@ -1135,10 +1400,72 @@
|
|||||||
inherits: false;
|
inherits: false;
|
||||||
initial-value: 0 0 #0000;
|
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 {
|
@property --tw-duration {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-ease {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
@layer properties {
|
@layer properties {
|
||||||
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
@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 {
|
*, ::before, ::after, ::backdrop {
|
||||||
@@ -1170,7 +1497,22 @@
|
|||||||
--tw-ring-offset-width: 0px;
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
--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-duration: initial;
|
||||||
|
--tw-ease: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user