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" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"time"
"git.haelnorr.com/h/oslstats/internal/config" "git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/db" "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", 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) 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 := 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()) conn = bun.NewDB(sqldb, pgdialect.New())
close = sqldb.Close close = sqldb.Close
// Simple table creation for backward compatibility
err = loadModels(ctx, conn) err = loadModels(ctx, conn)
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err, "loadModels") return nil, nil, errors.Wrap(err, "loadModels")

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ package main
import ( import (
"context" "context"
"io" "fmt"
"os" "os"
"os/signal" "os/signal"
"sync" "sync"
@@ -18,16 +18,10 @@ import (
) )
// Initializes and runs the server // 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) ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel() 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 // Setup the database connection
logger.Debug().Msg("Config loaded and logger started") logger.Debug().Msg("Config loaded and logger started")
logger.Debug().Msg("Connecting to database") 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...") logger.Info().Msg("Shut down requested, waiting 60 seconds...")
err := httpServer.Shutdown(shutdownCtx) err := httpServer.Shutdown(shutdownCtx)
if err != nil { 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() 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/env v0.9.1
git.haelnorr.com/h/golib/ezconf v0.1.1 git.haelnorr.com/h/golib/ezconf v0.1.1
git.haelnorr.com/h/golib/hlog v0.10.4 git.haelnorr.com/h/golib/hlog v0.10.4
git.haelnorr.com/h/golib/hws v0.4.0 git.haelnorr.com/h/golib/hws v0.4.3
git.haelnorr.com/h/golib/hwsauth v0.5.2 git.haelnorr.com/h/golib/hwsauth v0.5.3
git.haelnorr.com/h/golib/notify v0.1.0 git.haelnorr.com/h/golib/notify v0.1.0
github.com/a-h/templ v0.3.977 github.com/a-h/templ v0.3.977
github.com/coder/websocket v1.8.14 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/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 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ=
git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc= 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.3 h1:rpqe0Dcbm3b5XZ/Bfy0LUhph6RR7+bmANrSA/W81l0A=
git.haelnorr.com/h/golib/hws v0.4.0/go.mod h1:UqB83p9lGjidDkk0pWRqxxOFrCkg8t+9J6uGtBOjNLo= 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 h1:K4McXMEHtI5o4fAL3AZrmaMkwORNqSTV3MM6BExNKag=
git.haelnorr.com/h/golib/hwsauth v0.5.2/go.mod h1:NOonrVU/lX8lzuV77eDEiTwBjn7RrzYVcSdXUJWeHmQ= 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 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
git.haelnorr.com/h/golib/jwt v0.10.1/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4= 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= git.haelnorr.com/h/golib/notify v0.1.0 h1:xdf6zd21F6n+SuGTeJiuLNMf6zFXMvwpKD0gmNq8N10=

View File

@@ -10,26 +10,6 @@ import (
"github.com/pkg/errors" "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 // APIClient is an HTTP client wrapper that handles Discord API rate limits
type APIClient struct { type APIClient struct {
cfg *Config cfg *Config
@@ -38,6 +18,7 @@ type APIClient struct {
mu sync.RWMutex mu sync.RWMutex
buckets map[string]*RateLimitState buckets map[string]*RateLimitState
trustedHost string trustedHost string
bot *BotSession
} }
// NewAPIClient creates a new Discord API client with rate limit handling // 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 == "" { if trustedhost == "" {
return nil, errors.New("trustedhost cannot be empty") return nil, errors.New("trustedhost cannot be empty")
} }
bot, err := newBotSession(cfg)
if err != nil {
return nil, errors.Wrap(err, "newBotSession")
}
return &APIClient{ return &APIClient{
client: &http.Client{Timeout: 30 * time.Second}, client: &http.Client{Timeout: 30 * time.Second},
logger: logger, logger: logger,
buckets: make(map[string]*RateLimitState), buckets: make(map[string]*RateLimitState),
cfg: cfg, cfg: cfg,
trustedHost: trustedhost, trustedHost: trustedhost,
bot: bot,
}, nil }, 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) ClientSecret string // ENV DISCORD_CLIENT_SECRET: Discord application client secret (required)
OAuthScopes string // Authorisation scopes for OAuth OAuthScopes string // Authorisation scopes for OAuth
RedirectPath string // ENV DISCORD_REDIRECT_PATH: Path for the OAuth redirect handler (required) 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) { func ConfigFromEnv() (any, error) {
@@ -20,6 +21,7 @@ func ConfigFromEnv() (any, error) {
ClientSecret: env.String("DISCORD_CLIENT_SECRET", ""), ClientSecret: env.String("DISCORD_CLIENT_SECRET", ""),
OAuthScopes: getOAuthScopes(), OAuthScopes: getOAuthScopes(),
RedirectPath: env.String("DISCORD_REDIRECT_PATH", ""), RedirectPath: env.String("DISCORD_REDIRECT_PATH", ""),
BotToken: env.String("DISCORD_BOT_TOKEN", ""),
} }
// Check required fields // Check required fields
@@ -32,6 +34,9 @@ func ConfigFromEnv() (any, error) {
if cfg.RedirectPath == "" { if cfg.RedirectPath == "" {
return nil, errors.New("Envar not set: DISCORD_REDIRECT_PATH") 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 return cfg, nil
} }

View File

@@ -8,9 +8,30 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/bwmarrin/discordgo"
"github.com/pkg/errors" "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 // Token represents a response from the Discord OAuth API after a successful authorization request
type Token struct { type Token struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`

View File

@@ -42,6 +42,17 @@ func throwInternalServiceError(
throwError(s, w, r, http.StatusInternalServerError, msg, err, "error") 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) // throwBadRequest handles 400 errors (malformed requests)
func throwBadRequest( func throwBadRequest(
s *hws.Server, s *hws.Server,

View File

@@ -1,11 +1,13 @@
package handlers package handlers
import ( import (
stderrors "errors"
"net/http" "net/http"
"git.haelnorr.com/h/golib/cookies" "git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/golib/hws"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/uptrace/bun"
"git.haelnorr.com/h/oslstats/internal/config" "git.haelnorr.com/h/oslstats/internal/config"
"git.haelnorr.com/h/oslstats/internal/discord" "git.haelnorr.com/h/oslstats/internal/discord"
@@ -13,16 +15,34 @@ import (
"git.haelnorr.com/h/oslstats/pkg/oauth" "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( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
// TODO: check DB is connected errDB := conn.Ping()
// check discord API is working _, 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 r.Method == "POST" {
// if either fail, notify the client that login is unavailable right now if err != nil {
// otherwise proceed redirect to GET method 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) cookies.SetPageFrom(w, r, cfg.HWSAuth.TrustedHost)
attempts, exceeded, track := st.TrackRedirect(r, "/login", 5) 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") st.ClearRedirectTrack(r, "/login")
throwError( throwError(
server, s,
w, w,
r, r,
http.StatusBadRequest, 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") state, uak, err := oauth.GenerateState(cfg.OAuth, "login")
if err != nil { if err != nil {
throwInternalServiceError(server, w, r, "Failed to generate state token", err) throwInternalServiceError(s, w, r, "Failed to generate state token", err)
return return
} }
oauth.SetStateCookie(w, uak, cfg.HWSAuth.SSL) oauth.SetStateCookie(w, uak, cfg.HWSAuth.SSL)
link, err := discordAPI.GetOAuthLink(state) link, err := discordAPI.GetOAuthLink(state)
if err != nil { 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 return
} }
st.ClearRedirectTrack(r, "/login") 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) 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 { func notifyWarn(s *hws.Server, r *http.Request, title, msg string, action any) error {
return notifyClient(s, r, notify.LevelWarn, title, msg, "", action) return notifyClient(s, r, notify.LevelWarn, title, msg, "", action)
} }

View File

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

View File

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