This commit is contained in:
2026-01-27 19:14:12 +11:00
parent fb701bf205
commit 96d534f045
15 changed files with 138 additions and 314 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"time"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/db"
@@ -17,10 +18,15 @@ func setupBun(ctx context.Context, cfg *config.Config) (conn *bun.DB, close func
dsn := fmt.Sprintf("postgres://%s:%s@%s:%v/%s?sslmode=%s",
cfg.DB.User, cfg.DB.Password, cfg.DB.Host, cfg.DB.Port, cfg.DB.DB, cfg.DB.SSL)
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
sqldb.SetMaxOpenConns(25)
sqldb.SetMaxIdleConns(10)
sqldb.SetConnMaxLifetime(5 * time.Minute)
sqldb.SetConnMaxIdleTime(5 * time.Minute)
conn = bun.NewDB(sqldb, pgdialect.New())
close = sqldb.Close
// Simple table creation for backward compatibility
err = loadModels(ctx, conn)
if err != nil {
return nil, nil, errors.Wrap(err, "loadModels")

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/oslstats/internal/config"
"github.com/pkg/errors"
)
@@ -23,6 +24,12 @@ func main() {
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "Failed to load config"))
os.Exit(1)
}
// Setup the logger
logger, err := hlog.NewLogger(cfg.HLOG, os.Stdout)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", errors.Wrap(err, "Failed to init logger"))
os.Exit(1)
}
// Handle utility flags
if flags.EnvDoc || flags.ShowEnv {
@@ -38,8 +45,7 @@ func main() {
// Handle migration file creation (doesn't need DB connection)
if flags.MigrateCreate != "" {
if err := createMigration(flags.MigrateCreate); err != nil {
fmt.Fprintf(os.Stderr, "Error creating migration: %v\n", err)
os.Exit(1)
logger.Fatal().Err(err).Str("stacktrace", fmt.Sprintf("%+v", errors.Wrap(err, "createMigration"))).Msg("Error creating migration")
}
return
}
@@ -52,8 +58,7 @@ func main() {
// Setup database connection
conn, close, err := setupBun(ctx, cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Error setting up database: %v\n", err)
os.Exit(1)
logger.Fatal().Err(err).Str("stacktrace", fmt.Sprintf("%+v", errors.Wrap(err, "setupBun"))).Msg("Error setting up database")
}
defer close()
@@ -71,15 +76,13 @@ func main() {
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
logger.Fatal().Err(err).Str("stacktrace", fmt.Sprintf("%+v", errors.Wrap(err, "dbFlags"))).Msg("Error migrating database")
}
return
}
// Normal server startup
if err := run(ctx, os.Stdout, cfg); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
if err := run(ctx, logger, cfg); err != nil {
logger.Fatal().Err(err).Str("stacktrace", fmt.Sprintf("%+v", errors.Wrap(err, "run"))).Msg("Error starting server")
}
}

View File

@@ -38,8 +38,8 @@ func addRoutes(
},
{
Path: "/login",
Method: hws.MethodGET,
Handler: auth.LogoutReq(handlers.Login(s, cfg, store, discordAPI)),
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST},
Handler: auth.LogoutReq(handlers.Login(s, conn, cfg, store, discordAPI)),
},
{
Path: "/auth/callback",

View File

@@ -2,7 +2,7 @@ package main
import (
"context"
"io"
"fmt"
"os"
"os/signal"
"sync"
@@ -18,16 +18,10 @@ import (
)
// Initializes and runs the server
func run(ctx context.Context, w io.Writer, cfg *config.Config) error {
func run(ctx context.Context, logger *hlog.Logger, cfg *config.Config) error {
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
// Setup the logger
logger, err := hlog.NewLogger(cfg.HLOG, w)
if err != nil {
return errors.Wrap(err, "hlog.NewLogger")
}
// Setup the database connection
logger.Debug().Msg("Config loaded and logger started")
logger.Debug().Msg("Connecting to database")
@@ -78,7 +72,7 @@ func run(ctx context.Context, w io.Writer, cfg *config.Config) error {
logger.Info().Msg("Shut down requested, waiting 60 seconds...")
err := httpServer.Shutdown(shutdownCtx)
if err != nil {
logger.Error().Err(err).Msg("Graceful shutdown failed")
logger.Error().Err(err).Str("stacktrace", fmt.Sprintf("%+v", errors.Wrap(err, "httpServer.Shutdown"))).Msg("Graceful shutdown failed")
}
})
wg.Wait()

4
go.mod
View File

@@ -6,8 +6,8 @@ require (
git.haelnorr.com/h/golib/env v0.9.1
git.haelnorr.com/h/golib/ezconf v0.1.1
git.haelnorr.com/h/golib/hlog v0.10.4
git.haelnorr.com/h/golib/hws v0.4.0
git.haelnorr.com/h/golib/hwsauth v0.5.2
git.haelnorr.com/h/golib/hws v0.4.3
git.haelnorr.com/h/golib/hwsauth v0.5.3
git.haelnorr.com/h/golib/notify v0.1.0
github.com/a-h/templ v0.3.977
github.com/coder/websocket v1.8.14

6
go.sum
View File

@@ -6,10 +6,12 @@ git.haelnorr.com/h/golib/ezconf v0.1.1 h1:4euTSDb9jvuQQkVq+x5gHoYPYyUZPWxoOSlWCI
git.haelnorr.com/h/golib/ezconf v0.1.1/go.mod h1:rETDcjpcEyyeBgCiZSU617wc0XycwZSC5+IAOtXmwP8=
git.haelnorr.com/h/golib/hlog v0.10.4 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ=
git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc=
git.haelnorr.com/h/golib/hws v0.4.0 h1:T2JfRz4zpgsNXj0Vyfzxdf/60Tee/7H30osFmr5jDh0=
git.haelnorr.com/h/golib/hws v0.4.0/go.mod h1:UqB83p9lGjidDkk0pWRqxxOFrCkg8t+9J6uGtBOjNLo=
git.haelnorr.com/h/golib/hws v0.4.3 h1:rpqe0Dcbm3b5XZ/Bfy0LUhph6RR7+bmANrSA/W81l0A=
git.haelnorr.com/h/golib/hws v0.4.3/go.mod h1:UqB83p9lGjidDkk0pWRqxxOFrCkg8t+9J6uGtBOjNLo=
git.haelnorr.com/h/golib/hwsauth v0.5.2 h1:K4McXMEHtI5o4fAL3AZrmaMkwORNqSTV3MM6BExNKag=
git.haelnorr.com/h/golib/hwsauth v0.5.2/go.mod h1:NOonrVU/lX8lzuV77eDEiTwBjn7RrzYVcSdXUJWeHmQ=
git.haelnorr.com/h/golib/hwsauth v0.5.3 h1:Vgw8khDQZJRCc3m7z9QlbL9CYPyFB9JXUC3+omKRZPc=
git.haelnorr.com/h/golib/hwsauth v0.5.3/go.mod h1:NOonrVU/lX8lzuV77eDEiTwBjn7RrzYVcSdXUJWeHmQ=
git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
git.haelnorr.com/h/golib/jwt v0.10.1/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
git.haelnorr.com/h/golib/notify v0.1.0 h1:xdf6zd21F6n+SuGTeJiuLNMf6zFXMvwpKD0gmNq8N10=

View File

@@ -10,26 +10,6 @@ import (
"github.com/pkg/errors"
)
type OAuthSession struct {
*discordgo.Session
}
func NewOAuthSession(token *Token) (*OAuthSession, error) {
session, err := discordgo.New("Bearer " + token.AccessToken)
if err != nil {
return nil, errors.Wrap(err, "discordgo.New")
}
return &OAuthSession{Session: session}, nil
}
func (s *OAuthSession) GetUser() (*discordgo.User, error) {
user, err := s.User("@me")
if err != nil {
return nil, errors.Wrap(err, "s.User")
}
return user, nil
}
// APIClient is an HTTP client wrapper that handles Discord API rate limits
type APIClient struct {
cfg *Config
@@ -38,6 +18,7 @@ type APIClient struct {
mu sync.RWMutex
buckets map[string]*RateLimitState
trustedHost string
bot *BotSession
}
// NewAPIClient creates a new Discord API client with rate limit handling
@@ -51,11 +32,20 @@ func NewAPIClient(cfg *Config, logger *hlog.Logger, trustedhost string) (*APICli
if trustedhost == "" {
return nil, errors.New("trustedhost cannot be empty")
}
bot, err := newBotSession(cfg)
if err != nil {
return nil, errors.Wrap(err, "newBotSession")
}
return &APIClient{
client: &http.Client{Timeout: 30 * time.Second},
logger: logger,
buckets: make(map[string]*RateLimitState),
cfg: cfg,
trustedHost: trustedhost,
bot: bot,
}, nil
}
func (api *APIClient) Ping() (*discordgo.Application, error) {
return api.bot.Application("@me")
}

22
internal/discord/bot.go Normal file
View File

@@ -0,0 +1,22 @@
package discord
import (
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
)
type BotSession struct {
*discordgo.Session
}
func newBotSession(cfg *Config) (*BotSession, error) {
session, err := discordgo.New("Bot " + cfg.BotToken)
if err != nil {
return nil, errors.Wrap(err, "discordgo.New")
}
return &BotSession{Session: session}, nil
}
func (api *APIClient) Bot() *BotSession {
return api.bot
}

View File

@@ -12,6 +12,7 @@ type Config struct {
ClientSecret string // ENV DISCORD_CLIENT_SECRET: Discord application client secret (required)
OAuthScopes string // Authorisation scopes for OAuth
RedirectPath string // ENV DISCORD_REDIRECT_PATH: Path for the OAuth redirect handler (required)
BotToken string // ENV DISCORD_BOT_TOKEN: Token for the discord bot (required)
}
func ConfigFromEnv() (any, error) {
@@ -20,6 +21,7 @@ func ConfigFromEnv() (any, error) {
ClientSecret: env.String("DISCORD_CLIENT_SECRET", ""),
OAuthScopes: getOAuthScopes(),
RedirectPath: env.String("DISCORD_REDIRECT_PATH", ""),
BotToken: env.String("DISCORD_BOT_TOKEN", ""),
}
// Check required fields
@@ -32,6 +34,9 @@ func ConfigFromEnv() (any, error) {
if cfg.RedirectPath == "" {
return nil, errors.New("Envar not set: DISCORD_REDIRECT_PATH")
}
if cfg.BotToken == "" {
return nil, errors.New("Envar not set: DISCORD_BOT_TOKEN")
}
return cfg, nil
}

View File

@@ -8,9 +8,30 @@ import (
"net/url"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
)
type OAuthSession struct {
*discordgo.Session
}
func NewOAuthSession(token *Token) (*OAuthSession, error) {
session, err := discordgo.New("Bearer " + token.AccessToken)
if err != nil {
return nil, errors.Wrap(err, "discordgo.New")
}
return &OAuthSession{Session: session}, nil
}
func (s *OAuthSession) GetUser() (*discordgo.User, error) {
user, err := s.User("@me")
if err != nil {
return nil, errors.Wrap(err, "s.User")
}
return user, nil
}
// Token represents a response from the Discord OAuth API after a successful authorization request
type Token struct {
AccessToken string `json:"access_token"`

View File

@@ -42,6 +42,17 @@ func throwInternalServiceError(
throwError(s, w, r, http.StatusInternalServerError, msg, err, "error")
}
// throwServiceUnavailable handles 503 errors
func throwServiceUnavailable(
s *hws.Server,
w http.ResponseWriter,
r *http.Request,
msg string,
err error,
) {
throwError(s, w, r, http.StatusServiceUnavailable, msg, err, "error")
}
// throwBadRequest handles 400 errors (malformed requests)
func throwBadRequest(
s *hws.Server,

View File

@@ -1,11 +1,13 @@
package handlers
import (
stderrors "errors"
"net/http"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hws"
"github.com/pkg/errors"
"github.com/uptrace/bun"
"git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/discord"
@@ -13,16 +15,34 @@ import (
"git.haelnorr.com/h/oslstats/pkg/oauth"
)
func Login(server *hws.Server, cfg *config.Config, st *store.Store, discordAPI *discord.APIClient) http.Handler {
func Login(
s *hws.Server,
conn *bun.DB,
cfg *config.Config,
st *store.Store,
discordAPI *discord.APIClient,
) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// TODO: check DB is connected
// check discord API is working
errDB := conn.Ping()
_, errDisc := discordAPI.Ping()
err := stderrors.Join(errors.Wrap(errDB, "conn.Ping"), errors.Wrap(errDisc, "discordAPI.Ping"))
err = errors.Wrap(err, "login error")
if r.Method == "POST" {
// if either fail, notify the client that login is unavailable right now
// otherwise proceed redirect to GET method
if err != nil {
notifyServiceUnavailable(s, r, "Login currently unavailable", err)
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("HX-Redirect", "/login")
return
}
if err != nil {
throwServiceUnavailable(s, w, r, "Login currently unavailable", err)
return
}
// if either fail and method is GET, show service not available page
cookies.SetPageFrom(w, r, cfg.HWSAuth.TrustedHost)
attempts, exceeded, track := st.TrackRedirect(r, "/login", 5)
@@ -39,7 +59,7 @@ func Login(server *hws.Server, cfg *config.Config, st *store.Store, discordAPI *
st.ClearRedirectTrack(r, "/login")
throwError(
server,
s,
w,
r,
http.StatusBadRequest,
@@ -52,14 +72,14 @@ func Login(server *hws.Server, cfg *config.Config, st *store.Store, discordAPI *
state, uak, err := oauth.GenerateState(cfg.OAuth, "login")
if err != nil {
throwInternalServiceError(server, w, r, "Failed to generate state token", err)
throwInternalServiceError(s, w, r, "Failed to generate state token", err)
return
}
oauth.SetStateCookie(w, uak, cfg.HWSAuth.SSL)
link, err := discordAPI.GetOAuthLink(state)
if err != nil {
throwInternalServiceError(server, w, r, "An error occurred trying to generate the login link", err)
throwInternalServiceError(s, w, r, "An error occurred trying to generate the login link", err)
return
}
st.ClearRedirectTrack(r, "/login")

View File

@@ -37,6 +37,11 @@ func notifyInternalServiceError(s *hws.Server, r *http.Request, msg string, err
SerializeErrorDetails(http.StatusInternalServerError, err), nil)
}
func notifyServiceUnavailable(s *hws.Server, r *http.Request, msg string, err error) error {
return notifyClient(s, r, notify.LevelError, "Service Unavailable", msg,
SerializeErrorDetails(http.StatusServiceUnavailable, err), nil)
}
func notifyWarn(s *hws.Server, r *http.Request, title, msg string, action any) error {
return notifyClient(s, r, notify.LevelWarn, title, msg, "", action)
}

View File

@@ -81,13 +81,14 @@ templ navRight() {
</div>
</div>
} else {
<a
class="hidden rounded-lg px-4 py-2 sm:block
<button
class="hidden rounded-lg px-4 py-2 sm:block hover:cursor-pointer
bg-green hover:bg-green/75 text-mantle transition"
href="/login"
hx-post="/login"
hx-swap="none"
>
Login
</a>
</button>
}
</div>
<button

View File

@@ -32,50 +32,16 @@
--text-6xl--line-height: 1;
--text-9xl: 8rem;
--text-9xl--line-height: 1;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--tracking-tight: -0.025em;
--leading-relaxed: 1.625;
--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 {
@@ -301,24 +267,6 @@
.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;
}
@@ -352,15 +300,9 @@
.mt-12 {
margin-top: calc(var(--spacing) * 12);
}
.mt-20 {
margin-top: calc(var(--spacing) * 20);
}
.mt-24 {
margin-top: calc(var(--spacing) * 24);
}
.mr-5 {
margin-right: calc(var(--spacing) * 5);
}
.mb-2 {
margin-bottom: calc(var(--spacing) * 2);
}
@@ -373,9 +315,6 @@
.ml-2 {
margin-left: calc(var(--spacing) * 2);
}
.ml-auto {
margin-left: auto;
}
.block {
display: block;
}
@@ -404,14 +343,6 @@
width: calc(var(--spacing) * 5);
height: calc(var(--spacing) * 5);
}
.size-6 {
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);
}
@@ -439,9 +370,6 @@
.w-36 {
width: calc(var(--spacing) * 36);
}
.w-82 {
width: calc(var(--spacing) * 82);
}
.w-fit {
width: fit-content;
}
@@ -478,33 +406,9 @@
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.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);
}
.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,);
}
@@ -580,11 +484,6 @@
border-color: var(--surface2);
}
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
@@ -606,9 +505,6 @@
.rounded-lg {
border-radius: var(--radius-lg);
}
.rounded-sm {
border-radius: var(--radius-sm);
}
.rounded-xl {
border-radius: var(--radius-xl);
}
@@ -620,16 +516,9 @@
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);
}
@@ -669,9 +558,6 @@
.bg-dark-green {
background-color: var(--dark-green);
}
.bg-dark-red {
background-color: var(--dark-red);
}
.bg-dark-yellow {
background-color: var(--dark-yellow);
}
@@ -796,10 +682,6 @@
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.font-semibold {
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
@@ -847,18 +729,6 @@
.text-yellow {
color: var(--yellow);
}
.lowercase {
text-transform: lowercase;
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
.opacity-100 {
opacity: 100%;
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px 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);
@@ -871,37 +741,11 @@
--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));
}
.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;
@@ -1272,21 +1116,6 @@
font-weight: 700;
font-style: italic;
}
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
@@ -1399,78 +1228,9 @@
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 {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
@@ -1496,22 +1256,6 @@
--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;
}
}
}