detached 619fddb

Merge branch 'feat/templ-integration' into dev

commit
619fddb839e76a292aacb94f071cbea709d04689
author
bamen · · 2025-03-25 06:37 UTC
merge of
441dfbb + 8a8ae4c · diff against the first parent
signature
unsigned
90 files · +14194 -776

this diff is incomplete: 2 files are too large to show. the totals above count everything; clone the repository to read the rest.

M .goreleaser.yaml +2 -0
@@ -3,6 +3,8 @@ version: 2
33 before:
44 hooks:
55 - go mod tidy
6+ - templ generate
7+ - bun run build:css
68
79 release:
810 draft: true
A .templ.config.json +4 -0
@@ -0,0 +1,4 @@
1+{
2+ "root": "internal/templating/models/templ",
3+ "paths": ["internal/templating/models/templ"]
4+}⏎̸
M Makefile +8 -2
@@ -20,13 +20,13 @@ LDFLAGS := -s -w \
2020 ARCHS := amd64 arm64
2121
2222 # Phony targets
23-.PHONY: all build build-push clean build-css
23+.PHONY: all build build-push clean build-css build-templ
2424
2525 # Default target
2626 all: build
2727
2828 # Build binaries
29-build: build-css
29+build: build-css build-templ
3030 @echo "Building Go binaries..."
3131 @mkdir -p $(DIST_DIR)
3232 @rm -f $(DIST_DIR)/*
@@ -41,6 +41,12 @@ build-css:
4141 @bun run build:css
4242 @echo "CSS built successfully"
4343
44+# Generate Go code from templ templates
45+build-templ:
46+ @echo "Generating Go code from templ templates..."
47+ @templ generate
48+ @echo "templ generation completed successfully"
49+
4450 # Build and push Docker images
4551 build-push: build-css build
4652 @echo "Cleaning up dangling images..."
M docker-compose.yml.example +6 -1
@@ -44,9 +44,11 @@ services:
4444 # Reverse Proxy Configuration
4545 - GORDON_PROXY_PORT=443 # HTTPS port for reverse proxy
4646 - GORDON_PROXY_HTTP_PORT=80 # HTTP port for reverse proxy
47+ - GORDON_PROXY_ENABLED=true # Set to false to disable the reverse proxy (Note: proxy is automatically disabled when not running in a container)
4748 - GORDON_PROXY_CERT_DIR=/certs
4849 - GORDON_PROXY_EMAIL=admin@example.com
49- - GORDON_PROXY_ENABLE_LOGS=true # Enable HTTP request logging (set to false to disable)
50+ - GORDON_PROXY_ENABLE_HTTP_LOGS=true # Enable HTTP request logging (set to false to disable)
51+ - GORDON_PROXY_ENABLE_RATE_LIMIT=false # Enable rate limiting middleware (set to true to enable)
5052
5153 # Session Secret for security
5254 - SESSION_SECRET=replace_with_secure_random_string
@@ -97,8 +99,11 @@ networks:
9799 # # Reverse Proxy Configuration
98100 # - GORDON_PROXY_PORT=443 # HTTPS port for reverse proxy
99101 # - GORDON_PROXY_HTTP_PORT=80 # HTTP port for reverse proxy
102+# - GORDON_PROXY_ENABLED=true # Set to false to disable the reverse proxy (Note: proxy is automatically disabled when not running in a container)
100103 # - GORDON_PROXY_CERT_DIR=/certs
101104 # - GORDON_PROXY_EMAIL=admin@example.com
105+# - GORDON_PROXY_ENABLE_HTTP_LOGS=true # Enable HTTP request logging (set to false to disable)
106+# - GORDON_PROXY_ENABLE_RATE_LIMIT=false # Enable rate limiting middleware (set to true to enable)
102107 #
103108 # # Session Secret for security
104109 # - SESSION_SECRET=replace_with_secure_random_string
M go.mod +9 -0
@@ -6,6 +6,7 @@ require (
66 github.com/AlecAivazis/survey/v2 v2.3.7
77 github.com/Masterminds/semver/v3 v3.3.1
88 github.com/PuerkitoBio/goquery v1.10.2
9+ github.com/a-h/templ v0.3.833
910 github.com/charmbracelet/bubbles v0.20.0
1011 github.com/charmbracelet/lipgloss v1.0.0
1112 github.com/charmbracelet/log v0.4.0
@@ -24,12 +25,20 @@ require (
2425 )
2526
2627 require (
28+ github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
29+ github.com/andybalholm/brotli v1.1.0 // indirect
30+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
2731 github.com/cespare/xxhash/v2 v2.3.0 // indirect
32+ github.com/cli/browser v1.3.0 // indirect
33+ github.com/fsnotify/fsnotify v1.7.0 // indirect
2834 github.com/klauspost/compress v1.18.0 // indirect
2935 github.com/klauspost/cpuid/v2 v2.2.10 // indirect
36+ github.com/natefinch/atomic v1.0.1 // indirect
3037 github.com/zeebo/xxh3 v1.0.2 // indirect
3138 go.mongodb.org/mongo-driver v1.17.3 // indirect
3239 go.opentelemetry.io/auto/sdk v1.1.0 // indirect
40+ golang.org/x/mod v0.24.0 // indirect
41+ golang.org/x/tools v0.31.0 // indirect
3342 google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect
3443 )
3544
M go.sum +14 -2 generated file: collapsed
M internal/common/init_config.go +170 -61
@@ -47,14 +47,15 @@ type BuildConfig struct {
4747 }
4848
4949 type AdminConfig struct {
50- Path string `yaml:"path"`
50+ Path string `yaml:"path"`
51+ AdminWebUI bool `yaml:"-"`
5152 }
5253
5354 type GeneralConfig struct {
5455 StorageDir string `yaml:"storageDir"`
5556 Token string `yaml:"token"`
5657 LogLevel string `yaml:"logLevel"`
57- GordonContainerID string `yaml:"gordonContainerID"`
58+ GordonContainerID string `yaml:"-"`
5859 }
5960
6061 type HttpConfig struct {
@@ -73,33 +74,39 @@ type ContainerEngineConfig struct {
7374 }
7475
7576 type ReverseProxyConfig struct {
76- Port string `yaml:"port"` // Port for the reverse proxy to listen on
77- HttpPort string `yaml:"httpPort"` // HTTP port (usually 80) for redirecting to HTTPS
78- CertDir string `yaml:"certDir"` // Directory to store Let's Encrypt certificates
79- AutoRenew bool `yaml:"autoRenew"` // Whether to automatically renew certificates
80- RenewBefore int `yaml:"renewBefore"` // Days before expiry to renew certificates
81- LetsEncryptMode string `yaml:"letsEncryptMode"` // "staging" or "production"
82- Email string `yaml:"email"` // Email for Let's Encrypt
83- CacheSize int `yaml:"cacheSize"` // Size of the certificate cache
84- GracePeriod int `yaml:"gracePeriod"` // Shutdown grace period in seconds
85- EnableLogs bool `yaml:"enableLogs"` // Whether to enable HTTP request logging (default: true)
86- DisableRateLimit bool `yaml:"disableRateLimit"` // Whether to disable rate limiting middleware (default: false)
77+ Enabled bool `yaml:"enabled"` // Whether the reverse proxy is enabled
78+ Port string `yaml:"port"` // Port for the reverse proxy to listen on
79+ HttpPort string `yaml:"httpPort"` // HTTP port (usually 80) for redirecting to HTTPS
80+ CertDir string `yaml:"certDir"` // Directory to store Let's Encrypt certificates
81+ AutoRenew bool `yaml:"autoRenew"` // Whether to automatically renew certificates
82+ RenewBefore int `yaml:"renewBefore"` // Days before expiry to renew certificates
83+ LetsEncryptMode string `yaml:"letsEncryptMode"` // "staging" or "production"
84+ Email string `yaml:"email"` // Email for Let's Encrypt
85+ CacheSize int `yaml:"cacheSize"` // Size of the certificate cache
86+ GracePeriod int `yaml:"gracePeriod"` // Shutdown grace period in seconds
87+ EnableHttpLogs bool `yaml:"enableHttpLogs"` // Whether to enable HTTP request logging (default: true)
88+ EnableRateLimit bool `yaml:"enableRateLimit"` // Whether to enable rate limiting middleware (default: false)
89+ DetectUpstreamProxy bool `yaml:"detectUpstreamProxy"` // Whether to detect and handle upstream TLS termination proxies (default: false)
90+ SkipCertificates bool `yaml:"skipCertificates"` // Skip Let's Encrypt certificate acquisition when behind a TLS terminating proxy (default: false)
8791 }
8892
8993 // Default values
9094 var (
91- sock = "/var/run/docker.sock"
92- podmansock = "/run/podman/podman.sock"
93- reverseProxyPort = "443" // Changed to 443 for standard HTTPS
94- httpPort = "80" // Standard HTTP port
95- certDir = "/certs"
96- letsEncryptMode = "staging"
97- autoRenew = true
98- renewBefore = 30 // days
99- cacheSize = 1000 // entries
100- gracePeriod = 30 // seconds
101- defaultLogLevel = "info" // Default log level
102- disableRateLimit = false
95+ sock = "/var/run/docker.sock"
96+ podmansock = "/run/podman/podman.sock"
97+ reverseProxyPort = "443" // Changed to 443 for standard HTTPS
98+ httpPort = "80" // Standard HTTP port
99+ certDir = "/certs"
100+ letsEncryptMode = "staging"
101+ autoRenew = true
102+ proxyEnabled = true // Default to enabled for backward compatibility
103+ renewBefore = 30 // days
104+ cacheSize = 1000 // entries
105+ gracePeriod = 30 // seconds
106+ defaultLogLevel = "info" // Default log level
107+ disableRateLimit = false
108+ detectUpstreamProxy = false // Default to disabled
109+ skipCertificates = false // Default to disabled
103110 )
104111
105112 // applyDefaultsToConfig applies default values to any fields that have zero values
@@ -154,6 +161,12 @@ func applyDefaultsToConfig(config *Config) bool {
154161 }
155162
156163 // Apply defaults to ReverseProxy config
164+ // Set Enabled to true by default if not specified
165+ if !config.ReverseProxy.Enabled {
166+ config.ReverseProxy.Enabled = proxyEnabled
167+ logger.Debug("Applied default value for ReverseProxy.Enabled", "value", proxyEnabled)
168+ defaultsApplied = true
169+ }
157170 if config.ReverseProxy.Port == "" {
158171 config.ReverseProxy.Port = reverseProxyPort
159172 logger.Debug("Applied default value for ReverseProxy.Port", "value", reverseProxyPort)
@@ -189,16 +202,30 @@ func applyDefaultsToConfig(config *Config) bool {
189202 logger.Debug("Applied default value for ReverseProxy.GracePeriod", "value", gracePeriod)
190203 defaultsApplied = true
191204 }
192- // Set EnableLogs to true by default if not specified
193- if !config.ReverseProxy.EnableLogs {
194- config.ReverseProxy.EnableLogs = true
195- logger.Debug("Applied default value for ReverseProxy.EnableLogs", "value", true)
205+ // Set EnableHttpLogs to true by default if not specified
206+ if !config.ReverseProxy.EnableHttpLogs {
207+ config.ReverseProxy.EnableHttpLogs = true
208+ logger.Debug("Applied default value for ReverseProxy.EnableHttpLogs", "value", true)
209+ defaultsApplied = true
210+ }
211+ // Set EnableRateLimit to false by default if not specified
212+ if !config.ReverseProxy.EnableRateLimit {
213+ config.ReverseProxy.EnableRateLimit = false
214+ logger.Debug("Applied default value for ReverseProxy.EnableRateLimit", "value", false)
215+ defaultsApplied = true
216+ }
217+
218+ // Set DetectUpstreamProxy to false by default if not specified
219+ if !config.ReverseProxy.DetectUpstreamProxy {
220+ config.ReverseProxy.DetectUpstreamProxy = detectUpstreamProxy
221+ logger.Debug("Applied default value for ReverseProxy.DetectUpstreamProxy", "value", detectUpstreamProxy)
196222 defaultsApplied = true
197223 }
198- // Set DisableRateLimit to false by default if not specified
199- if !config.ReverseProxy.DisableRateLimit {
200- config.ReverseProxy.DisableRateLimit = false
201- logger.Debug("Applied default value for ReverseProxy.DisableRateLimit", "value", false)
224+
225+ // Set SkipCertificates to false by default if not specified
226+ if !config.ReverseProxy.SkipCertificates {
227+ config.ReverseProxy.SkipCertificates = skipCertificates
228+ logger.Debug("Applied default value for ReverseProxy.SkipCertificates", "value", skipCertificates)
202229 defaultsApplied = true
203230 }
204231
@@ -552,6 +579,25 @@ func loadConfigFromEnv(config *Config, printLogs bool) {
552579 logger.Info("Using environment variable GORDON_PROXY_HTTP_PORT", "value", val)
553580 }
554581 }
582+
583+ // New environment variables for reverse proxy port control
584+ if val := os.Getenv("GORDON_USE_FALLBACK_BINDING"); val != "" {
585+ // This is handled at runtime in the proxy server but we log it here
586+ if printLogs {
587+ logger.Info("Found environment variable GORDON_USE_FALLBACK_BINDING", "value", val)
588+ }
589+ }
590+
591+ // Handle GORDON_PROXY_ENABLED environment variable
592+ if val := os.Getenv("GORDON_PROXY_ENABLED"); val != "" {
593+ enabled, err := strconv.ParseBool(val)
594+ if err == nil {
595+ config.ReverseProxy.Enabled = enabled
596+ }
597+ if printLogs {
598+ logger.Info("Using environment variable GORDON_PROXY_ENABLED", "value", config.ReverseProxy.Enabled)
599+ }
600+ }
555601 if val := os.Getenv("GORDON_PROXY_CERT_DIR"); val != "" {
556602 config.ReverseProxy.CertDir = val
557603 if printLogs {
@@ -600,22 +646,59 @@ func loadConfigFromEnv(config *Config, printLogs bool) {
600646 logger.Info("Using environment variable GORDON_PROXY_GRACE_PERIOD", "value", i)
601647 }
602648
603- // Handle GORDON_PROXY_ENABLE_LOGS environment variable
604- if val := os.Getenv("GORDON_PROXY_ENABLE_LOGS"); val != "" {
649+ // Handle GORDON_PROXY_ENABLE_HTTP_LOGS environment variable
650+ if val := os.Getenv("GORDON_PROXY_ENABLE_HTTP_LOGS"); val != "" {
605651 enableLogs, err := strconv.ParseBool(val)
606652 if err == nil {
607- config.ReverseProxy.EnableLogs = enableLogs
653+ config.ReverseProxy.EnableHttpLogs = enableLogs
608654 }
609- logger.Info("Using environment variable GORDON_PROXY_ENABLE_LOGS", "value", config.ReverseProxy.EnableLogs)
655+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_HTTP_LOGS", "value", config.ReverseProxy.EnableHttpLogs)
656+ } else if val := os.Getenv("GORDON_PROXY_ENABLE_LOGS"); val != "" {
657+ // For backward compatibility
658+ enableLogs, err := strconv.ParseBool(val)
659+ if err == nil {
660+ config.ReverseProxy.EnableHttpLogs = enableLogs
661+ }
662+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_LOGS (deprecated, use GORDON_PROXY_ENABLE_HTTP_LOGS)", "value", config.ReverseProxy.EnableHttpLogs)
610663 }
611664
612- // Handle GORDON_PROXY_DISABLE_RATE_LIMIT environment variable
613- if val := os.Getenv("GORDON_PROXY_DISABLE_RATE_LIMIT"); val != "" {
665+ // Handle GORDON_PROXY_ENABLE_RATE_LIMIT environment variable
666+ if val := os.Getenv("GORDON_PROXY_ENABLE_RATE_LIMIT"); val != "" {
667+ enableRateLimit, err := strconv.ParseBool(val)
668+ if err == nil {
669+ config.ReverseProxy.EnableRateLimit = enableRateLimit
670+ }
671+ logger.Info("Using environment variable GORDON_PROXY_ENABLE_RATE_LIMIT", "value", config.ReverseProxy.EnableRateLimit)
672+ } else if val := os.Getenv("GORDON_PROXY_DISABLE_RATE_LIMIT"); val != "" {
673+ // For backward compatibility
614674 disableRateLimit, err := strconv.ParseBool(val)
615675 if err == nil {
616- config.ReverseProxy.DisableRateLimit = disableRateLimit
676+ // Invert the logic
677+ config.ReverseProxy.EnableRateLimit = !disableRateLimit
678+ }
679+ logger.Info("Using environment variable GORDON_PROXY_DISABLE_RATE_LIMIT (deprecated, use GORDON_PROXY_ENABLE_RATE_LIMIT)", "value", !config.ReverseProxy.EnableRateLimit)
680+ }
681+
682+ // Handle GORDON_PROXY_DETECT_UPSTREAM environment variable
683+ if val := os.Getenv("GORDON_PROXY_DETECT_UPSTREAM"); val != "" {
684+ detectUpstream, err := strconv.ParseBool(val)
685+ if err == nil {
686+ config.ReverseProxy.DetectUpstreamProxy = detectUpstream
687+ }
688+ if printLogs {
689+ logger.Info("Using environment variable GORDON_PROXY_DETECT_UPSTREAM", "value", config.ReverseProxy.DetectUpstreamProxy)
690+ }
691+ }
692+
693+ // Handle GORDON_PROXY_SKIP_CERTIFICATES environment variable
694+ if val := os.Getenv("GORDON_PROXY_SKIP_CERTIFICATES"); val != "" {
695+ skipCerts, err := strconv.ParseBool(val)
696+ if err == nil {
697+ config.ReverseProxy.SkipCertificates = skipCerts
698+ }
699+ if printLogs {
700+ logger.Info("Using environment variable GORDON_PROXY_SKIP_CERTIFICATES", "value", config.ReverseProxy.SkipCertificates)
617701 }
618- logger.Info("Using environment variable GORDON_PROXY_DISABLE_RATE_LIMIT", "value", config.ReverseProxy.DisableRateLimit)
619702 }
620703 }
621704
@@ -663,16 +746,16 @@ func (config *Config) LoadConfig() (*Config, error) {
663746 Network: "gordon",
664747 },
665748 ReverseProxy: ReverseProxyConfig{
666- Port: reverseProxyPort,
667- HttpPort: httpPort,
668- CertDir: certDir,
669- AutoRenew: autoRenew,
670- RenewBefore: renewBefore,
671- LetsEncryptMode: letsEncryptMode,
672- CacheSize: cacheSize,
673- GracePeriod: gracePeriod,
674- EnableLogs: true,
675- DisableRateLimit: false,
749+ Port: reverseProxyPort,
750+ HttpPort: httpPort,
751+ CertDir: certDir,
752+ AutoRenew: autoRenew,
753+ RenewBefore: renewBefore,
754+ LetsEncryptMode: letsEncryptMode,
755+ CacheSize: cacheSize,
756+ GracePeriod: gracePeriod,
757+ EnableHttpLogs: true,
758+ EnableRateLimit: false,
676759 },
677760 }
678761
@@ -802,16 +885,16 @@ func (config *Config) LoadConfig() (*Config, error) {
802885 Network: "gordon",
803886 },
804887 ReverseProxy: ReverseProxyConfig{
805- Port: reverseProxyPort,
806- HttpPort: httpPort,
807- CertDir: certDir,
808- AutoRenew: autoRenew,
809- RenewBefore: renewBefore,
810- LetsEncryptMode: letsEncryptMode,
811- CacheSize: cacheSize,
812- GracePeriod: gracePeriod,
813- EnableLogs: true,
814- DisableRateLimit: false,
888+ Port: reverseProxyPort,
889+ HttpPort: httpPort,
890+ CertDir: certDir,
891+ AutoRenew: autoRenew,
892+ RenewBefore: renewBefore,
893+ LetsEncryptMode: letsEncryptMode,
894+ CacheSize: cacheSize,
895+ GracePeriod: gracePeriod,
896+ EnableHttpLogs: true,
897+ EnableRateLimit: false,
815898 },
816899 }
817900
@@ -884,6 +967,27 @@ func (config *Config) LoadConfig() (*Config, error) {
884967 logger.Info("Loaded configuration from", "path", configFilePath)
885968 }
886969
970+ // After loading from file or environment, check if Admin.Path is empty
971+ // This should be placed before returning the config
972+ if config.Admin.Path == "" {
973+ logger.Warn("Admin path is empty or not set, admin dashboard webui will be disabled")
974+ config.Admin.AdminWebUI = false
975+ } else {
976+ config.Admin.AdminWebUI = true
977+ }
978+
979+ // Save config file if running in container and env vars were applied
980+ // This ensures config.yml always reflects the environment variables
981+ if docker.IsRunningInContainer() && configLogsPrinted {
982+ logger.Info("Environment variables were applied, saving configuration to reflect current values...")
983+ err = config.SaveConfig()
984+ if err != nil {
985+ logger.Error("Failed to save configuration with environment variables", "error", err)
986+ } else {
987+ logger.Info("Successfully updated config.yml with environment variable values")
988+ }
989+ }
990+
887991 // Apply the log level from configuration
888992 if config.General.LogLevel != "" {
889993 // Set the log level using our centralized logger
@@ -1006,3 +1110,8 @@ func (c *HttpConfig) FullDomain() string {
10061110 func (c *Config) GetVersion() string {
10071111 return c.Build.BuildVersion
10081112 }
1113+
1114+// IsAdminWebUIEnabled returns whether the admin dashboard is enabled
1115+func (c *AdminConfig) IsAdminWebUIEnabled() bool {
1116+ return c.AdminWebUI && c.Path != ""
1117+}
M internal/common/init_dockerclient.go +155 -41
@@ -19,62 +19,176 @@ func DockerInit(cc *ContainerEngineConfig) error {
1919 cc.PodmanSock = cc.Sock
2020 }
2121
22- config := NewDockerConfig(cc)
23- err := docker.InitializeClient(config)
24- if err != nil {
25- // Check if it's a "socket doesn't exist" error
26- if strings.Contains(err.Error(), "no such file or directory") {
27- // Log the error
28- log.Printf("Error initializing Docker client: %s", err)
29-
30- // Try alternative socket paths
31- socketPaths := []string{
32- "/run/podman/podman.sock", // Standard path
33- "/var/run/podman/podman.sock", // Alternative standard path
34- os.Getenv("HOME") + "/.local/podman/podman.sock", // Some distros
35- "/run/user/1000/podman/podman.sock", // User-specific socket
36- }
22+ // If socket is empty, try to detect it automatically
23+ if cc.Sock == "" {
24+ // Try standard paths first
25+ standardPaths := []string{
26+ "/var/run/docker.sock", // Standard Docker socket in containers
27+ "/run/podman/podman.sock", // Standard Podman path
28+ "/var/run/podman/podman.sock", // Alternative standard path
29+ }
3730
38- // Add Docker socket path as a fallback
39- if !isPodmanSocket {
40- socketPaths = append(socketPaths, "/var/run/docker.sock")
41- }
31+ // Add home directory based path
32+ homePath := os.Getenv("HOME")
33+ if homePath != "" {
34+ standardPaths = append(standardPaths, homePath+"/.local/podman/podman.sock")
35+ }
36+
37+ // Check standard paths first
38+ for _, path := range standardPaths {
39+ if _, err := os.Stat(path); err == nil {
40+ log.Printf("Auto-detected container engine socket at %s", path)
4241
43- for _, path := range socketPaths {
44- // Skip if it's the path we already tried
45- if path == cc.Sock {
46- continue
42+ // Detect if this is a Podman socket
43+ isPodmanSocket = detectPodmanSocket(path)
44+ if isPodmanSocket {
45+ cc.Podman = true
46+ cc.PodmanSock = path
4747 }
4848
49- // Check if this socket exists
50- if _, err := os.Stat(path); err == nil {
51- log.Printf("Found alternative socket at %s", path)
49+ cc.Sock = path
50+ break
51+ } else {
52+ log.Printf("Socket not found at: %s", path)
53+ }
54+ }
5255
53- // Detect if this is a Podman socket
54- isPodmanSocket = detectPodmanSocket(path)
55- if isPodmanSocket {
56+ // If not found in standard paths, try common user IDs
57+ if cc.Sock == "" {
58+ log.Printf("Scanning for user-specific Podman sockets in ID range 1000-1100...")
59+
60+ // Try common user IDs from 1000 to 1100 (typical range for first users on Linux)
61+ for uid := 1000; uid <= 1100; uid++ {
62+ uidStr := fmt.Sprintf("%d", uid)
63+ podmanSocketPath := fmt.Sprintf("/run/user/%s/podman/podman.sock", uidStr)
64+ dockerSocketPath := fmt.Sprintf("/run/user/%s/docker/docker.sock", uidStr)
65+
66+ // Check the Podman socket exists
67+ if _, err := os.Stat(podmanSocketPath); err == nil {
68+ // Check if the socket is accessible for reading/writing
69+ if file, err := os.OpenFile(podmanSocketPath, os.O_RDWR, 0); err == nil {
70+ file.Close()
71+ log.Printf("Found accessible Podman socket for user ID %s at: %s", uidStr, podmanSocketPath)
5672 cc.Podman = true
57- cc.PodmanSock = path
73+ cc.PodmanSock = podmanSocketPath
74+ cc.Sock = podmanSocketPath
75+ break
76+ } else {
77+ log.Printf("Found Podman socket for user ID %s at %s but it's not accessible: %v", uidStr, podmanSocketPath, err)
5878 }
79+ }
5980
60- cc.Sock = path
61-
62- // Try again with the new path
63- newConfig := NewDockerConfig(cc)
64- if err := docker.InitializeClient(newConfig); err == nil {
65- return nil
81+ // Check Docker socket for this UID
82+ if _, err := os.Stat(dockerSocketPath); err == nil {
83+ if file, err := os.OpenFile(dockerSocketPath, os.O_RDWR, 0); err == nil {
84+ file.Close()
85+ log.Printf("Found accessible Docker socket for user ID %s at: %s", uidStr, dockerSocketPath)
86+ cc.Sock = dockerSocketPath
87+ break
6688 }
6789 }
6890 }
91+ }
92+ }
6993
70- // Return without an error, as we want to continue even if Docker is not available
71- log.Printf("Docker/Podman functionality will be limited or unavailable")
72- return fmt.Errorf("could not connect to container engine: %w", err)
94+ // If we found a socket, try to initialize Docker client
95+ if cc.Sock != "" {
96+ config := NewDockerConfig(cc)
97+ err := docker.InitializeClient(config)
98+ if err == nil {
99+ log.Printf("Successfully initialized Docker client with socket: %s", cc.Sock)
100+ return nil
73101 }
74102
75- return err
103+ // Log the error but continue with fallback strategy
104+ log.Printf("Error initializing Docker client: %s", err)
105+ }
106+
107+ // If we failed to find or initialize a socket, try a comprehensive fallback scan
108+ // across all common socket locations and user IDs
109+ socketPaths := []string{
110+ "/var/run/docker.sock",
111+ "/run/podman/podman.sock",
112+ "/var/run/podman/podman.sock",
113+ }
114+
115+ // Add home directory based path
116+ homePath := os.Getenv("HOME")
117+ if homePath != "" {
118+ socketPaths = append(socketPaths, homePath+"/.local/podman/podman.sock")
119+ }
120+
121+ // Skip if we already tried this path
122+ if cc.Sock != "" {
123+ skipPath := cc.Sock
124+ for i, path := range socketPaths {
125+ if path == skipPath {
126+ socketPaths = append(socketPaths[:i], socketPaths[i+1:]...)
127+ break
128+ }
129+ }
76130 }
77- return nil
131+
132+ // Try common user IDs from 1000 to 1100
133+ for uid := 1000; uid <= 1100; uid++ {
134+ uidStr := fmt.Sprintf("%d", uid)
135+ podmanSocketPath := fmt.Sprintf("/run/user/%s/podman/podman.sock", uidStr)
136+ dockerSocketPath := fmt.Sprintf("/run/user/%s/docker/docker.sock", uidStr)
137+
138+ // Check the Podman socket exists
139+ if _, err := os.Stat(podmanSocketPath); err == nil {
140+ // Check if the socket is accessible for reading/writing
141+ if file, err := os.OpenFile(podmanSocketPath, os.O_RDWR, 0); err == nil {
142+ file.Close()
143+ log.Printf("Found accessible Podman socket for user ID %s at: %s", uidStr, podmanSocketPath)
144+ socketPaths = append([]string{podmanSocketPath}, socketPaths...) // Prioritize this path
145+ } else {
146+ log.Printf("Found Podman socket for user ID %s at %s but it's not accessible: %v", uidStr, podmanSocketPath, err)
147+ }
148+ }
149+
150+ // Also check Docker socket for this UID
151+ if _, err := os.Stat(dockerSocketPath); err == nil {
152+ socketPaths = append(socketPaths, dockerSocketPath)
153+ }
154+ }
155+
156+ log.Printf("Comprehensive socket scan across %d potential paths...", len(socketPaths))
157+
158+ for _, path := range socketPaths {
159+ // Skip if it's the path we already tried
160+ if path == cc.Sock {
161+ continue
162+ }
163+
164+ // Check if this socket exists
165+ if _, err := os.Stat(path); err == nil {
166+ log.Printf("Found alternative socket at %s", path)
167+
168+ // Detect if this is a Podman socket
169+ isPodmanSocket = detectPodmanSocket(path)
170+ if isPodmanSocket {
171+ cc.Podman = true
172+ cc.PodmanSock = path
173+ }
174+
175+ cc.Sock = path
176+
177+ // Try again with the new path
178+ newConfig := NewDockerConfig(cc)
179+ if err := docker.InitializeClient(newConfig); err == nil {
180+ log.Printf("Successfully initialized Docker client with socket: %s", path)
181+ return nil
182+ } else {
183+ log.Printf("Socket found but client initialization failed: %s", err)
184+ }
185+ }
186+ }
187+
188+ // If we still haven't found a valid socket, give up but don't fail the application
189+ log.Printf("No working container engine socket found after comprehensive scan")
190+ log.Printf("Docker/Podman functionality will be limited or unavailable")
191+ return fmt.Errorf("could not find or connect to any container engine socket")
78192 }
79193
80194 // detectPodmanSocket checks if a socket path is likely a Podman socket
M internal/httpserve/handlers/container_crud.go +35 -37
@@ -7,6 +7,8 @@ import (
77
88 "github.com/bnema/gordon/internal/server"
99 "github.com/bnema/gordon/internal/templating/cmdparams"
10+ templcomponents "github.com/bnema/gordon/internal/templating/models/templ/components"
11+ templadmin "github.com/bnema/gordon/internal/templating/models/templ/pages/admin"
1012 "github.com/bnema/gordon/internal/templating/render"
1113 "github.com/bnema/gordon/pkg/docker"
1214 "github.com/bnema/gordon/pkg/sanitize"
@@ -34,7 +36,7 @@ func CreateContainerGET(c echo.Context, a *server.App) error {
3436 fullID, exists := safelyInteractWithIDMap(Fetch, shortID)
3537 if !exists {
3638 log.Debug("No mapping found for short ID: %s", shortID)
37- return c.String(http.StatusNotFound, "Image ID not found")
39+ return RenderNotFoundPage(c, a)
3840 }
3941
4042 log.Debug("Found mapping: %s -> %s", shortID, fullID)
@@ -49,30 +51,23 @@ func CreateContainerGET(c echo.Context, a *server.App) error {
4951 var imageName string
5052 if len(imageInfo.RepoTags) > 0 {
5153 imageName = imageInfo.RepoTags[0]
54+ } else {
55+ imageName = imageInfo.ID
5256 }
5357
54- data := map[string]interface{}{
55- "Title": "Create a new container",
56- "ShortID": shortID,
57- "ImageID": fullID,
58- "ImageName": imageName,
58+ // Setup form data
59+ formData := templcomponents.ContainerFormData{
60+ ImageName: imageName,
61+ ImageID: fullID,
62+ ShortID: shortID,
63+ ErrorMessage: "",
5964 }
6065
61- // Render the create container page
62- rendererData, err := render.GetHTMLRenderer("html/fragments", "createcontainer.gohtml", a.TemplateFS, a)
63-
64- if err != nil {
65- return sendError(c, err)
66- }
67-
68- renderedHTML, err := rendererData.Render(data, a)
69-
70- if err != nil {
71- return sendError(c, err)
72- }
73-
74- return c.HTML(200, renderedHTML)
66+ // Get the renderer
67+ renderer := render.NewTemplRenderer(a)
7568
69+ // Render the component
70+ return renderer.RenderTempl(c, templcomponents.CreateContainerForm(formData))
7671 }
7772
7873 // CreateContainerFullGET handles the full HTML page for creating a new container
@@ -84,7 +79,7 @@ func CreateContainerFullGET(c echo.Context, a *server.App) error {
8479 fullID, exists := safelyInteractWithIDMap(Fetch, shortID)
8580 if !exists {
8681 log.Debug("No mapping found for short ID: %s", shortID)
87- return c.String(http.StatusNotFound, "Image ID not found")
82+ return RenderNotFoundPage(c, a)
8883 }
8984
9085 log.Debug("Found mapping: %s -> %s", shortID, fullID)
@@ -99,29 +94,32 @@ func CreateContainerFullGET(c echo.Context, a *server.App) error {
9994 var imageName string
10095 if len(imageInfo.RepoTags) > 0 {
10196 imageName = imageInfo.RepoTags[0]
97+ } else {
98+ imageName = imageInfo.ID
10299 }
103100
104- data := map[string]interface{}{
105- "Title": "Create a new container",
106- "ShortID": shortID,
107- "ImageID": fullID,
108- "ImageName": imageName,
109- "AdminPath": a.Config.Admin.Path,
101+ // Setup form data
102+ formData := templcomponents.ContainerFormData{
103+ ImageName: imageName,
104+ ImageID: fullID,
105+ ShortID: shortID,
106+ ErrorMessage: "",
110107 }
111108
112- // Specify both admin and fragments directories
113- rendererData, err := render.GetHTMLRenderer("html/admin", "createcontainerfull.gohtml", a.TemplateFS, a, "html/fragments")
114- if err != nil {
115- return sendError(c, err)
116- }
109+ // Get the renderer
110+ renderer := render.NewTemplRenderer(a)
117111
118- renderedHTML, err := rendererData.Render(data, a)
119- if err != nil {
120- log.Debug("Error rendering template: %v", err)
121- return sendError(c, err)
112+ // Render the createcontainerfull template
113+ pageData := templadmin.CreateContainerFullData{
114+ Title: "Create Container | Gordon",
115+ BuildVersion: a.Config.Build.BuildVersion,
116+ AdminPath: a.Config.Admin.Path,
117+ UserSettings: "/user",
118+ LogoutURL: a.Config.Admin.Path + "/logout",
119+ ContainerData: formData,
122120 }
123121
124- return c.HTML(200, renderedHTML)
122+ return renderer.RenderTempl(c, templadmin.CreateContainerFullPage(pageData))
125123 }
126124
127125 // CreateContainerPOST handles the create container form submission
M internal/httpserve/handlers/container_management.go +58 -2
@@ -12,6 +12,7 @@ import (
1212 "github.com/bnema/gordon/internal/templating/render"
1313 "github.com/bnema/gordon/pkg/docker"
1414 "github.com/bnema/gordon/pkg/humanize"
15+ "github.com/bnema/gordon/pkg/logger"
1516 "github.com/docker/docker/api/types"
1617 "github.com/labstack/echo/v4"
1718 )
@@ -58,6 +59,10 @@ type HumanReadableContainerImage struct {
5859 SizeStr string
5960 RepoDigests []string
6061 RepoTags []string
62+ Repository string
63+ Tag string
64+ Size string
65+ Created string
6166 }
6267
6368 type HumanReadableContainer struct {
@@ -85,17 +90,33 @@ type ContainerDisplay struct {
8590
8691 // renderHTML is a generalized function to render HTML
8792 func renderHTML(c echo.Context, a *server.App, path, templateName string, data map[string]interface{}) error {
93+ logger.Debug("Rendering HTML", "path", path, "template", templateName, "data_keys", getKeysFromMap(data))
94+
8895 rendererData, err := render.GetHTMLRenderer(path, templateName, a.TemplateFS, a)
8996 if err != nil {
97+ logger.Error("Error getting HTML renderer", "error", err)
9098 return err
9199 }
100+
92101 renderedHTML, err := rendererData.Render(data, a)
93102 if err != nil {
103+ logger.Error("Error rendering HTML", "error", err)
94104 return err
95105 }
106+
107+ logger.Debug("HTML rendered successfully", "content_length", len(renderedHTML))
96108 return c.HTML(http.StatusOK, renderedHTML)
97109 }
98110
111+// getKeysFromMap returns a slice of keys from a map
112+func getKeysFromMap(data map[string]interface{}) []string {
113+ keys := make([]string, 0, len(data))
114+ for k := range data {
115+ keys = append(keys, k)
116+ }
117+ return keys
118+}
119+
99120 // ActionSuccess returns the success HTML fragment
100121 func ActionSuccess(a *server.App) string {
101122 // Create a data structure to pass to the template
@@ -114,11 +135,19 @@ func ActionSuccess(a *server.App) string {
114135
115136 // ImageManagerComponent handles the /image-manager route (HTMX route)
116137 func ImageManagerComponent(c echo.Context, a *server.App) error {
138+ logger.Debug("ImageManagerComponent called", "url", c.Request().URL.String())
139+
117140 images, err := docker.ListContainerImages()
118141 if err != nil {
142+ logger.Error("Error listing container images:", "error", err)
119143 return sendError(c, err)
120144 }
121145
146+ logger.Debug("Images found", "count", len(images))
147+ for i, img := range images {
148+ logger.Debug("Image details", "index", i, "id", img.ID, "repoTags", img.RepoTags)
149+ }
150+
122151 var humanReadableImages []HumanReadableContainerImage
123152
124153 for _, img := range images {
@@ -129,6 +158,14 @@ func ImageManagerComponent(c echo.Context, a *server.App) error {
129158 sizeStr := humanize.BytesToReadableSize(img.Size)
130159
131160 for _, repoTag := range img.RepoTags {
161+ // Split the repo tag into repository and tag
162+ parts := strings.Split(repoTag, ":")
163+ repository := parts[0]
164+ tag := "latest"
165+ if len(parts) > 1 {
166+ tag = parts[1]
167+ }
168+
132169 humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
133170 ID: img.ID,
134171 ShortID: shortID,
@@ -137,6 +174,11 @@ func ImageManagerComponent(c echo.Context, a *server.App) error {
137174 Name: repoTag,
138175 RepoDigests: img.RepoDigests,
139176 RepoTags: img.RepoTags,
177+ // Add fields that match template expectations
178+ Repository: repository,
179+ Tag: tag,
180+ Size: sizeStr,
181+ Created: createdStr,
140182 })
141183 }
142184 }
@@ -145,6 +187,7 @@ func ImageManagerComponent(c echo.Context, a *server.App) error {
145187 "Images": humanReadableImages,
146188 }
147189
190+ logger.Debug("Rendering imagelist template with", len(humanReadableImages), "images")
148191 return renderHTML(c, a, "html/fragments", "imagelist.gohtml", data)
149192 }
150193
@@ -170,11 +213,19 @@ func ImageManagerDelete(c echo.Context, a *server.App) error {
170213
171214 // ContainerManagerComponent handles the /container-manager route
172215 func ContainerManagerComponent(c echo.Context, a *server.App) error {
216+ logger.Debug("ContainerManagerComponent called", "url", c.Request().URL.String())
217+
173218 containers, err := docker.ListRunningContainers()
174219 if err != nil {
220+ logger.Error("Error listing containers", "error", err)
175221 return sendError(c, err)
176222 }
177223
224+ logger.Debug("Containers found", "count", len(containers))
225+ for i, container := range containers {
226+ logger.Debug("Container details", "index", i, "id", container.ID, "names", container.Names, "state", container.State)
227+ }
228+
178229 var humanReadableContainers []HumanReadableContainer
179230
180231 for _, container := range containers {
@@ -212,10 +263,15 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error {
212263 return err
213264 }
214265 data := map[string]interface{}{
215- "Lang": yamlData["Lang"],
216- "containers": humanReadableContainers,
266+ "Lang": yamlData["Lang"],
267+ "Containers": humanReadableContainers,
268+ "NoContainer": len(humanReadableContainers) == 0,
217269 }
218270
271+ logger.Debug("Rendering containerlist template",
272+ "humanReadableContainers", len(humanReadableContainers),
273+ "noContainer", len(humanReadableContainers) == 0)
274+
219275 return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data)
220276 }
221277
M internal/httpserve/handlers/container_transactions.go +3 -3
@@ -2,13 +2,13 @@ package handlers
22
33 import (
44 "fmt"
5- "log"
65 "strings"
76
87 "github.com/bnema/gordon/internal/server"
98 "github.com/bnema/gordon/internal/templating/cmdparams"
109 "github.com/bnema/gordon/internal/templating/render"
1110 "github.com/bnema/gordon/pkg/docker"
11+ "github.com/bnema/gordon/pkg/logger"
1212 "github.com/labstack/echo/v4"
1313 "gopkg.in/yaml.v3"
1414 )
@@ -132,13 +132,13 @@ func (tq *TransactionQueue) Execute() error {
132132 for i, step := range tq.steps {
133133 err := step()
134134 if err != nil {
135- log.Printf("Error occurred at step index %d: %v", i, err)
135+ logger.Error(fmt.Sprintf("Error occurred at step index %d: %v", i, err))
136136 transactionErrors = append(transactionErrors, err)
137137 for j := i - 1; j >= 0; j-- {
138138 rollback := tq.rollbacks[j]
139139 if rollback != nil {
140140 if rollbackErr := rollback(); rollbackErr != nil {
141- log.Printf("Failed to execute rollback at index %d: %v", j, rollbackErr)
141+ logger.Error(fmt.Sprintf("Failed to execute rollback at index %d: %v", j, rollbackErr))
142142 rollbackErrors = append(rollbackErrors, rollbackErr)
143143 }
144144 }
M internal/httpserve/handlers/endpoint_deploy.go +34 -34
@@ -17,7 +17,7 @@ import (
1717 "github.com/bnema/gordon/internal/proxy"
1818 "github.com/bnema/gordon/internal/server"
1919 "github.com/bnema/gordon/pkg/docker"
20- "github.com/charmbracelet/log"
20+ "github.com/bnema/gordon/pkg/logger"
2121 "github.com/labstack/echo/v4"
2222 )
2323
@@ -26,7 +26,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
2626 payload, err := validateAndPrepareDeployPayload(c)
2727 if err != nil {
2828 // Debug
29- log.Error("Failed to validate and prepare payload", "error", err)
29+ logger.Error("Failed to validate and prepare payload", "error", err)
3030 return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
3131 Success: false,
3232 Message: fmt.Sprintf("Invalid payload: %v", err),
@@ -37,7 +37,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
3737 _, err = saveAndImportImage(c, a, payload)
3838 if err != nil {
3939 // Debug
40- log.Error("Failed to save and import image", "error", err)
40+ logger.Error("Failed to save and import image", "error", err)
4141 return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
4242 Success: false,
4343 Message: fmt.Sprintf("Failed to save or import image: %v", err),
@@ -48,14 +48,14 @@ func PostDeploy(c echo.Context, a *server.App) error {
4848 containerID, containerName, err := createAndStartContainer(a, payload)
4949 if err != nil {
5050 if strings.Contains(err.Error(), "is already in use") {
51- log.Warn("Container already exists", "error", err)
51+ logger.Warn("Container already exists", "error", err)
5252 existingContainerID := extractContainerID(err.Error())
5353 existingContainerName, _ := docker.GetContainerName(existingContainerID)
5454 // Remove the / from the container name
5555 existingContainerName = strings.TrimLeft(existingContainerName, "/")
5656
5757 if existingContainerID == "" || existingContainerName == "" {
58- log.Error("Failed to extract container ID or Name from error message")
58+ logger.Error("Failed to extract container ID or Name from error message")
5959 return sendJSONResponse(c, http.StatusConflict, DeployResponse{
6060 Success: false,
6161 Message: "A container for this deploy already exists but could not be identified.",
@@ -71,7 +71,7 @@ func PostDeploy(c echo.Context, a *server.App) error {
7171 StatusCode: http.StatusConflict,
7272 })
7373 }
74- log.Error("Failed to create or start container", "error", err)
74+ logger.Error("Failed to create or start container", "error", err)
7575 return sendJSONResponse(c, http.StatusInternalServerError, DeployResponse{
7676 Success: false,
7777 Message: fmt.Sprintf("Failed to create or start container: %v", err),
@@ -90,11 +90,11 @@ func PostDeploy(c echo.Context, a *server.App) error {
9090 containerIP := GetContainerIP(a, containerID, containerName)
9191 err = AddProxyRoute(a, containerID, containerIP, payload.Port, payload.TargetDomain)
9292 if err != nil {
93- log.Error("Failed to add proxy route", "error", err)
93+ logger.Error("Failed to add proxy route", "error", err)
9494 return sendDeployErrorResponse(c, "Failed to add proxy route", err)
9595 }
9696 } else {
97- log.Info("Skipping proxy setup as requested", "container_id", containerID, "container_name", containerName)
97+ logger.Info("Skipping proxy setup as requested", "container_id", containerID, "container_name", containerName)
9898 }
9999
100100 // Arrived here means deployment was successful
@@ -124,7 +124,7 @@ func handleChunkedDeploy(c echo.Context, a *server.App, metadataStr string) erro
124124
125125 payload, err := validateAndPrepareDeployPayload(c)
126126 if err != nil {
127- log.Error("Failed to validate and prepare payload", "error", err)
127+ logger.Error("Failed to validate and prepare payload", "error", err)
128128 return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
129129 Success: false,
130130 Message: fmt.Sprintf("Invalid payload: %v", err),
@@ -196,7 +196,7 @@ func handleChunkedDeploy(c echo.Context, a *server.App, metadataStr string) erro
196196 // Chunks validation steps
197197 //
198198 if len(chunkData) == 0 {
199- log.Error("Received empty chunk")
199+ logger.Error("Received empty chunk")
200200 return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
201201 Success: false,
202202 Message: "Received empty chunk",
@@ -206,7 +206,7 @@ func handleChunkedDeploy(c echo.Context, a *server.App, metadataStr string) erro
206206
207207 // Verify chunk size matches metadata
208208 if int64(len(chunkData)) != metadata.ChunkSize {
209- log.Error("Chunk size mismatch",
209+ logger.Error("Chunk size mismatch",
210210 "expected", metadata.ChunkSize,
211211 "received", len(chunkData))
212212 return sendJSONResponse(c, http.StatusBadRequest, DeployResponse{
@@ -236,7 +236,7 @@ func handleChunkedDeploy(c echo.Context, a *server.App, metadataStr string) erro
236236 }
237237
238238 func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transferID string) error {
239- log.Info("Starting complete deploy transfer processing",
239+ logger.Info("Starting complete deploy transfer processing",
240240 "transferID", transferID)
241241
242242 // Get transfer data
@@ -296,21 +296,21 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
296296 // Check for existing container
297297 existingContainerID := docker.GetContainerIDByName(containerName)
298298 if existingContainerID != "" {
299- log.Warn("Container already exists",
299+ logger.Warn("Container already exists",
300300 "name", containerName,
301301 "id", existingContainerID)
302302 return sendDeployErrorResponse(c, "Container already exists", nil)
303303 }
304304
305305 // Import the image
306- log.Info("Importing image from file",
306+ logger.Info("Importing image from file",
307307 "path", tmpFile.Name(),
308308 "imageName", imageName)
309309
310310 imageID, err := docker.ImportImageToEngine(tmpFile.Name())
311311 if err != nil {
312312 if strings.Contains(err.Error(), "platform compatibility check failed") {
313- log.Error("Platform compatibility check failed",
313+ logger.Error("Platform compatibility check failed",
314314 "error", err,
315315 "imageName", imageName)
316316 return sendDeployErrorResponse(c, "Platform compatibility check failed", err)
@@ -343,7 +343,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
343343 // Cleanup transfer data
344344 cleanupTransfer(transferID)
345345
346- log.Info("Deployment completed successfully",
346+ logger.Info("Deployment completed successfully",
347347 "containerID", containerID,
348348 "containerName", containerName,
349349 "domain", targetDomain)
@@ -352,7 +352,7 @@ func processCompleteChunkedDeployTansfert(c echo.Context, a *server.App, transfe
352352 containerIP := GetContainerIP(a, containerID, containerName)
353353 err = AddProxyRoute(a, containerID, containerIP, port, targetDomain)
354354 if err != nil {
355- log.Error("Failed to add proxy route", "error", err)
355+ logger.Error("Failed to add proxy route", "error", err)
356356 return sendDeployErrorResponse(c, "Failed to add proxy route", err)
357357 }
358358
@@ -422,7 +422,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
422422
423423 // Double check that we have a valid container IP - critical for recreated containers
424424 if containerIP == "" || containerIP == containerID || containerIP == "localhost" {
425- log.Warn("Invalid container IP detected, attempting to get correct IP",
425+ logger.Warn("Invalid container IP detected, attempting to get correct IP",
426426 "containerID", containerID,
427427 "invalid_ip", containerIP)
428428
@@ -437,7 +437,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
437437 // Get the correct container IP with retry logic
438438 newContainerIP := GetContainerIP(a, containerID, containerName)
439439 if newContainerIP != containerIP && newContainerIP != containerName {
440- log.Info("Updated container IP for proxy route",
440+ logger.Info("Updated container IP for proxy route",
441441 "containerID", containerID,
442442 "old_ip", containerIP,
443443 "new_ip", newContainerIP)
@@ -448,12 +448,12 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
448448 // Get container info to check labels
449449 containerInfo, err := docker.GetContainerInfo(containerID)
450450 if err != nil {
451- log.Warn("Failed to get container info", "error", err)
451+ logger.Warn("Failed to get container info", "error", err)
452452 } else {
453453 // Check if the container has a gordon.proxy.port label that might override containerPort
454454 if portLabel, exists := containerInfo.Config.Labels["gordon.proxy.port"]; exists && portLabel != "" {
455455 if portLabel != containerPort {
456- log.Info("Using container label port instead of provided port",
456+ logger.Info("Using container label port instead of provided port",
457457 "label_port", portLabel,
458458 "provided_port", containerPort)
459459 containerPort = portLabel
@@ -467,7 +467,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
467467 // Check if the target domain specifies HTTPS
468468 if strings.HasPrefix(strings.ToLower(targetDomain), "https://") {
469469 protocol = "https"
470- log.Debug("Using HTTPS protocol for proxy route based on target domain",
470+ logger.Debug("Using HTTPS protocol for proxy route based on target domain",
471471 "domain", targetDomain,
472472 "protocol", protocol)
473473 } else {
@@ -477,7 +477,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
477477 if sslValue, exists := containerInfo.Config.Labels["gordon.proxy.ssl"]; exists &&
478478 (sslValue == "true" || sslValue == "1" || sslValue == "yes") {
479479 protocol = "https"
480- log.Debug("Using HTTPS protocol for proxy route based on container labels",
480+ logger.Debug("Using HTTPS protocol for proxy route based on container labels",
481481 "domain", targetDomain,
482482 "containerID", containerID,
483483 "label_value", sslValue,
@@ -496,16 +496,16 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
496496 cleanDomain = strings.TrimPrefix(cleanDomain, ".")
497497
498498 // Log that we're adding the domain for Let's Encrypt
499- log.Debug("Adding domain for Let's Encrypt verification",
499+ logger.Debug("Adding domain for Let's Encrypt verification",
500500 "domain", cleanDomain)
501501
502502 // Get container name for better logging
503503 containerName, err := docker.GetContainerName(containerID)
504504 if err != nil {
505- log.Warn("Unable to get container name", "containerID", containerID, "error", err)
505+ logger.Warn("Unable to get container name", "containerID", containerID, "error", err)
506506 } else {
507507 containerName = strings.TrimPrefix(containerName, "/")
508- log.Debug("Container name for proxy route", "name", containerName)
508+ logger.Debug("Container name for proxy route", "name", containerName)
509509 }
510510
511511 // Check the database for any existing routes with this domain, regardless of container ID
@@ -526,7 +526,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
526526
527527 if err == nil {
528528 // Domain exists in database, check if this is a recreated container
529- log.Info("Found existing proxy route for domain",
529+ logger.Info("Found existing proxy route for domain",
530530 "domain", cleanDomain,
531531 "old_container_id", existingContainerID,
532532 "new_container_id", containerID)
@@ -538,7 +538,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
538538 networkName := a.Config.ContainerEngine.Network
539539 if networkSettings, exists := containerNetworkInfo.NetworkSettings.Networks[networkName]; exists &&
540540 networkSettings.IPAddress != "" && networkSettings.IPAddress != containerIP {
541- log.Info("Updating container IP before database update",
541+ logger.Info("Updating container IP before database update",
542542 "domain", cleanDomain,
543543 "old_ip", containerIP,
544544 "new_ip", networkSettings.IPAddress)
@@ -559,7 +559,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
559559 return fmt.Errorf("failed to update proxy route: %w", err)
560560 }
561561
562- log.Info("Updated proxy route for recreated container",
562+ logger.Info("Updated proxy route for recreated container",
563563 "domain", cleanDomain,
564564 "old_container_id", existingContainerID,
565565 "new_container_id", containerID,
@@ -579,11 +579,11 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
579579 WHERE id = ?`, existingRouteID).Scan(&updatedContainerIP)
580580
581581 if err == nil && updatedContainerIP != containerIP {
582- log.Warn("Database verification failed: container IP mismatch",
582+ logger.Warn("Database verification failed: container IP mismatch",
583583 "expected", containerIP,
584584 "actual", updatedContainerIP)
585585 } else {
586- log.Debug("Database verification passed: container IP updated correctly",
586+ logger.Debug("Database verification passed: container IP updated correctly",
587587 "container_ip", containerIP)
588588 }
589589
@@ -596,12 +596,12 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
596596 routes := p.GetRoutes()
597597 if route, exists := routes[cleanDomain]; exists {
598598 if route.ContainerIP != containerIP {
599- log.Warn("Route reload verification failed: container IP mismatch",
599+ logger.Warn("Route reload verification failed: container IP mismatch",
600600 "domain", cleanDomain,
601601 "expected", containerIP,
602602 "actual", route.ContainerIP)
603603 } else {
604- log.Debug("Route reload verification passed: container IP loaded correctly",
604+ logger.Debug("Route reload verification passed: container IP loaded correctly",
605605 "domain", cleanDomain,
606606 "container_ip", containerIP)
607607 }
@@ -623,7 +623,7 @@ func AddProxyRoute(a *server.App, containerID, containerIP, containerPort, targe
623623 // and avoid database locking conflicts
624624 p.RegisterNewlyCreatedContainer(containerID)
625625
626- log.Debug("Added proxy route",
626+ logger.Debug("Added proxy route",
627627 "domain", targetDomain,
628628 "containerIP", containerIP,
629629 "containerPort", containerPort,
M internal/httpserve/handlers/endpoint_helpers.go +24 -24
@@ -18,7 +18,7 @@ import (
1818 "github.com/bnema/gordon/internal/templating/cmdparams"
1919 "github.com/bnema/gordon/pkg/docker"
2020 "github.com/bnema/gordon/pkg/filestore"
21- "github.com/charmbracelet/log"
21+ "github.com/bnema/gordon/pkg/logger"
2222 "github.com/labstack/echo/v4"
2323 )
2424
@@ -71,7 +71,7 @@ func (info *InfoResponse) Populate(a *server.App) {
7171 func sendJSONResponse(c echo.Context, statusCode int, response interface{}) error {
7272 err := c.JSON(statusCode, response)
7373 if err != nil {
74- log.Error("Failed to send JSON response",
74+ logger.Error("Failed to send JSON response",
7575 "error", err,
7676 "statusCode", statusCode)
7777 }
@@ -152,7 +152,7 @@ func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (str
152152 imageID, err = docker.ImportImageToEngine(imagePath)
153153 if err != nil {
154154 if removeErr := filestore.RemoveFromStorage(imagePath); removeErr != nil {
155- log.Error("Failed to remove temporary image file after import failure",
155+ logger.Error("Failed to remove temporary image file after import failure",
156156 "error", removeErr,
157157 "path", imagePath)
158158 }
@@ -161,7 +161,7 @@ func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (str
161161
162162 if imageID == "" {
163163 if removeErr := filestore.RemoveFromStorage(imagePath); removeErr != nil {
164- log.Error("Failed to remove temporary image file after empty ID",
164+ logger.Error("Failed to remove temporary image file after empty ID",
165165 "error", removeErr,
166166 "path", imagePath)
167167 }
@@ -170,7 +170,7 @@ func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (str
170170
171171 err = filestore.RemoveFromStorage(imagePath)
172172 if err != nil {
173- log.Error("Failed to remove temporary image file after successful import",
173+ logger.Error("Failed to remove temporary image file after successful import",
174174 "error", err,
175175 "path", imagePath)
176176 return "", fmt.Errorf("failed to remove temporary image file: %v", err)
@@ -181,7 +181,7 @@ func saveAndImportImage(c echo.Context, a *server.App, payload interface{}) (str
181181
182182 // createAndStartContainer creates a container from the image, starts it and returns the container ID
183183 func createAndStartContainer(a *server.App, payload *common.DeployPayload) (string, string, error) {
184- log.Info("Starting container creation process",
184+ logger.Info("Starting container creation process",
185185 "imageName", payload.ImageName,
186186 "imageID", payload.ImageID,
187187 "port", payload.Port)
@@ -191,21 +191,21 @@ func createAndStartContainer(a *server.App, payload *common.DeployPayload) (stri
191191 return "", "", fmt.Errorf("failed to create command parameters: %v", err)
192192 }
193193
194- log.Debug("Container parameters prepared", "params", params)
194+ logger.Debug("Container parameters prepared", "params", params)
195195
196196 containerID, err := docker.CreateContainer(params)
197197 if err != nil {
198198 return "", "", fmt.Errorf("failed to create container: %v", err)
199199 }
200200
201- log.Info("Container created successfully", "containerID", containerID)
201+ logger.Info("Container created successfully", "containerID", containerID)
202202
203203 containerName, err := docker.GetContainerName(containerID)
204204 if err != nil {
205205 return "", "", fmt.Errorf("failed to get container name: %v", err)
206206 }
207207
208- log.Info("Starting container",
208+ logger.Info("Starting container",
209209 "containerID", containerID,
210210 "name", containerName)
211211
@@ -215,7 +215,7 @@ func createAndStartContainer(a *server.App, payload *common.DeployPayload) (stri
215215 // Get container logs if start failed
216216 logs, logErr := docker.GetContainerLogs(containerID)
217217 if logErr == nil {
218- log.Error("Container start failed - container logs",
218+ logger.Error("Container start failed - container logs",
219219 "logs", logs,
220220 "containerID", containerID)
221221 }
@@ -223,14 +223,14 @@ func createAndStartContainer(a *server.App, payload *common.DeployPayload) (stri
223223 // Cleanup the failed container
224224 cleanupErr := docker.RemoveContainer(containerID)
225225 if cleanupErr != nil {
226- log.Error("Failed to cleanup container after start failure",
226+ logger.Error("Failed to cleanup container after start failure",
227227 "containerID", containerID,
228228 "error", cleanupErr)
229229 }
230230 return "", "", fmt.Errorf("failed to start container: %v", err)
231231 }
232232
233- log.Info("Container started successfully",
233+ logger.Info("Container started successfully",
234234 "containerID", containerID,
235235 "name", containerName)
236236
@@ -292,14 +292,14 @@ func isTransferComplete(transferID string) bool {
292292
293293 metadata := chunkStore.metadata[transferID]
294294 if metadata == nil {
295- log.Warn("No metadata found for transfer", "transferID", transferID)
295+ logger.Warn("No metadata found for transfer", "transferID", transferID)
296296 return false
297297 }
298298
299299 chunksReceived := len(chunkStore.chunks[transferID])
300300 isComplete := chunksReceived == metadata.TotalChunks
301301
302- log.Debug("Transfer completion check",
302+ logger.Debug("Transfer completion check",
303303 "transferID", transferID,
304304 "chunksReceived", chunksReceived,
305305 "totalExpected", metadata.TotalChunks,
@@ -328,7 +328,7 @@ func cleanupOldTransfers() {
328328 chunkStore.mu.Lock()
329329 for id, startTime := range chunkStore.started {
330330 if now.Sub(startTime) > transferTimeout {
331- log.Debug("Cleaning up stale transfer",
331+ logger.Debug("Cleaning up stale transfer",
332332 "transferID", id,
333333 "age", now.Sub(startTime))
334334 cleanupTransfer(id)
@@ -376,7 +376,7 @@ func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) er
376376 totalSize += int64(len(chunk))
377377 }
378378
379- log.Debug("Starting chunk assembly",
379+ logger.Debug("Starting chunk assembly",
380380 "totalChunks", totalChunks,
381381 "totalSize", fmt.Sprintf("%.2f MB", float64(totalSize)/(1024*1024)))
382382
@@ -435,7 +435,7 @@ func writeChunksToFile(file *os.File, chunks map[int][]byte, totalChunks int) er
435435 return fmt.Errorf("failed to reset file pointer: %w", err)
436436 }
437437
438- log.Debug("Chunk assembly completed successfully",
438+ logger.Debug("Chunk assembly completed successfully",
439439 "totalBytesWritten", written,
440440 "sizeInMB", fmt.Sprintf("%.2f MB", float64(written)/(1024*1024)))
441441
@@ -464,7 +464,7 @@ func GetContainerIP(a *server.App, containerID, containerName string) string {
464464 // If this is a retry, add an increasing delay
465465 if retry > 0 {
466466 sleepTime := time.Duration(500*retry) * time.Millisecond
467- log.Debug("Retrying to get container IP",
467+ logger.Debug("Retrying to get container IP",
468468 "attempt", retry+1,
469469 "containerID", containerID,
470470 "delay", sleepTime)
@@ -486,7 +486,7 @@ func GetContainerIP(a *server.App, containerID, containerName string) string {
486486 refreshedInfo, err := docker.GetContainerInfo(containerID)
487487 if err == nil {
488488 containerInfo = refreshedInfo
489- log.Debug("Refreshed container info for IP retrieval",
489+ logger.Debug("Refreshed container info for IP retrieval",
490490 "containerName", containerName,
491491 "containerID", containerID)
492492 } else {
@@ -501,7 +501,7 @@ func GetContainerIP(a *server.App, containerID, containerName string) string {
501501 networkName := a.Config.ContainerEngine.Network
502502 if networkSettings, exists := containerInfo.NetworkSettings.Networks[networkName]; exists && networkSettings.IPAddress != "" {
503503 containerIP = networkSettings.IPAddress
504- log.Debug("Found container IP",
504+ logger.Debug("Found container IP",
505505 "containerName", containerName,
506506 "containerID", containerID,
507507 "containerIP", containerIP,
@@ -513,7 +513,7 @@ func GetContainerIP(a *server.App, containerID, containerName string) string {
513513 for networkName, networkSettings := range containerInfo.NetworkSettings.Networks {
514514 if networkSettings.IPAddress != "" {
515515 containerIP = networkSettings.IPAddress
516- log.Debug("Found container IP in alternative network",
516+ logger.Debug("Found container IP in alternative network",
517517 "containerName", containerName,
518518 "containerID", containerID,
519519 "containerIP", containerIP,
@@ -533,7 +533,7 @@ func GetContainerIP(a *server.App, containerID, containerName string) string {
533533 // Extract just the IP address from the CIDR format
534534 ipCIDR := endpoint.IPv4Address
535535 containerIP = strings.Split(ipCIDR, "/")[0]
536- log.Debug("Found container IP via network inspection",
536+ logger.Debug("Found container IP via network inspection",
537537 "containerName", containerName,
538538 "containerID", containerID,
539539 "containerIP", containerIP,
@@ -546,12 +546,12 @@ func GetContainerIP(a *server.App, containerID, containerName string) string {
546546
547547 // If we've tried multiple times and couldn't get an IP
548548 if lastErr != nil {
549- log.Warn("Failed to get container info after multiple attempts",
549+ logger.Warn("Failed to get container info after multiple attempts",
550550 "error", lastErr,
551551 "containerID", containerID)
552552 }
553553
554- log.Warn("Could not find container IP, using container name as fallback",
554+ logger.Warn("Could not find container IP, using container name as fallback",
555555 "containerName", containerName,
556556 "containerID", containerID)
557557
A internal/httpserve/handlers/error_handlers.go +36 -0
@@ -0,0 +1,36 @@
1+package handlers
2+
3+import (
4+ "net/http"
5+
6+ "github.com/bnema/gordon/internal/server"
7+ "github.com/bnema/gordon/internal/templating/models/templ/pages/errors"
8+ "github.com/bnema/gordon/internal/templating/render"
9+ "github.com/labstack/echo/v4"
10+)
11+
12+// RenderNotFoundPage renders a 404 error page using templ
13+func RenderNotFoundPage(c echo.Context, a *server.App) error {
14+ data := errors.ErrorPageData{
15+ Title: "Page Not Found | Gordon",
16+ AdminPath: a.Config.Admin.Path,
17+ StatusCode: http.StatusNotFound,
18+ Message: "Sorry, the page you are looking for does not exist.",
19+ }
20+
21+ renderer := render.NewTemplRenderer(a)
22+ return renderer.RenderTempl(c, errors.NotFoundPage(data))
23+}
24+
25+// RenderForbiddenPage renders a 403 error page using templ
26+func RenderForbiddenPage(c echo.Context, a *server.App) error {
27+ data := errors.ErrorPageData{
28+ Title: "Access Forbidden | Gordon",
29+ AdminPath: a.Config.Admin.Path,
30+ StatusCode: http.StatusForbidden,
31+ Message: "You don't have permission to access this resource.",
32+ }
33+
34+ renderer := render.NewTemplRenderer(a)
35+ return renderer.RenderTempl(c, errors.ForbiddenPage(data))
36+} ⏎̸
A internal/httpserve/handlers/templ_handlers.go +212 -0
@@ -0,0 +1,212 @@
1+package handlers
2+
3+import (
4+ "github.com/bnema/gordon/internal/server"
5+ "github.com/bnema/gordon/internal/templating/models/templ/components"
6+ "github.com/bnema/gordon/internal/templating/models/templ/pages/admin"
7+ "github.com/bnema/gordon/internal/templating/models/templ/pages/login"
8+ "github.com/bnema/gordon/internal/templating/render"
9+ "github.com/bnema/gordon/pkg/docker"
10+ "github.com/docker/docker/api/types"
11+ "github.com/labstack/echo/v4"
12+ "strings"
13+ "time"
14+ "github.com/bnema/gordon/pkg/humanize"
15+ "net/http"
16+)
17+
18+// RenderTemplLoginPage renders the login page using templ
19+func RenderTemplLoginPage(c echo.Context, a *server.App) error {
20+ // Check for token in query param for backward compatibility
21+ urlToken := c.QueryParam("token")
22+ _ = urlToken // Not using this yet, but kept for compatibility
23+
24+ // Check for error message
25+ errorMsg := c.QueryParam("error")
26+
27+ // Create a templ renderer
28+ renderer := render.NewTemplRenderer(a)
29+
30+ // Render the login page
31+ return renderer.RenderTempl(c, login.LoginPage("Login", a.Config.Build.BuildVersion, errorMsg))
32+}
33+
34+// RenderTemplDashboardPage renders the dashboard page using templ
35+func RenderTemplDashboardPage(c echo.Context, a *server.App) error {
36+ // Create a templ renderer
37+ renderer := render.NewTemplRenderer(a)
38+
39+ // Get user-specific URLs
40+ adminPath := a.Config.Admin.Path
41+ userSettingsURL := adminPath + "/settings"
42+ logoutURL := adminPath + "/logout"
43+
44+ // Render the dashboard page
45+ return renderer.RenderTempl(c, admin.DashboardPage(
46+ "Dashboard",
47+ a.Config.Build.BuildVersion,
48+ adminPath,
49+ userSettingsURL,
50+ logoutURL,
51+ ))
52+}
53+
54+// RenderTemplManagerPage renders the manager page using templ
55+func RenderTemplManagerPage(c echo.Context, a *server.App) error {
56+ // Create a templ renderer
57+ renderer := render.NewTemplRenderer(a)
58+
59+ // Get container and image counts
60+ containerCount, _ := GetContainerCount(a)
61+ imageCount, _ := GetImageCount(a)
62+
63+ // Get user-specific URLs
64+ adminPath := a.Config.Admin.Path
65+ userSettingsURL := adminPath + "/settings"
66+ logoutURL := adminPath + "/logout"
67+
68+ // Render the manager page
69+ return renderer.RenderTempl(c, admin.ManagerPage(
70+ "Container Manager",
71+ a.Config.Build.BuildVersion,
72+ adminPath,
73+ userSettingsURL,
74+ logoutURL,
75+ containerCount,
76+ imageCount,
77+ ))
78+}
79+
80+// RenderTemplContainerList renders the container list component
81+func RenderTemplContainerList(c echo.Context, a *server.App) error {
82+ // Create a templ renderer
83+ renderer := render.NewTemplRenderer(a)
84+
85+ // Get the containers
86+ containers, noContainer, err := GetContainers(a)
87+ if err != nil {
88+ return err
89+ }
90+
91+ // Render the container list component
92+ return renderer.RenderTempl(c, components.ContainerList(containers, noContainer))
93+}
94+
95+// RenderTemplImageList renders the image list component
96+func RenderTemplImageList(c echo.Context, a *server.App) error {
97+ // Create a templ renderer
98+ renderer := render.NewTemplRenderer(a)
99+
100+ // Get the images
101+ images, err := docker.ListContainerImages()
102+ if err != nil {
103+ return err
104+ }
105+
106+ // Convert to human-readable format
107+ var humanReadableImages []components.HumanReadableImage
108+ for _, img := range images {
109+ shortID := img.ID[7:19]
110+
111+ for _, repoTag := range img.RepoTags {
112+ // Split the repo tag into repository and tag
113+ parts := strings.Split(repoTag, ":")
114+ repository := parts[0]
115+ tag := "latest"
116+ if len(parts) > 1 {
117+ tag = parts[1]
118+ }
119+
120+ humanReadableImages = append(humanReadableImages, components.HumanReadableImage{
121+ ID: img.ID,
122+ ShortID: shortID,
123+ Name: repoTag,
124+ Repository: repository,
125+ Tag: tag,
126+ Size: humanize.BytesToReadableSize(img.Size),
127+ Created: humanize.TimeAgo(time.Unix(img.Created, 0)),
128+ RepoDigests: img.RepoDigests,
129+ RepoTags: img.RepoTags,
130+ })
131+ }
132+ }
133+
134+ // Render the image list component
135+ return renderer.RenderTempl(c, components.ImageList(humanReadableImages))
136+}
137+
138+// RenderTemplUploadImageForm renders the upload image form component
139+func RenderTemplUploadImageForm(c echo.Context, a *server.App) error {
140+ // Create a templ renderer
141+ renderer := render.NewTemplRenderer(a)
142+
143+ // Render the upload image form component
144+ return renderer.RenderTempl(c, components.UploadImageForm())
145+}
146+
147+// RenderTemplCreateContainerForm renders the create container form component
148+func RenderTemplCreateContainerForm(c echo.Context, a *server.App) error {
149+ // Create a templ renderer
150+ renderer := render.NewTemplRenderer(a)
151+
152+ // Get the image ID from the URL parameter
153+ shortID := c.Param("ID")
154+
155+ // Try to fetch the image info
156+ imageID, exists := safelyInteractWithIDMap(Fetch, shortID)
157+ if !exists {
158+ return c.String(http.StatusBadRequest, "Invalid image ID")
159+ }
160+
161+ // Get image info
162+ image, err := docker.GetImageInfo(imageID)
163+ if err != nil {
164+ return err
165+ }
166+
167+ // Prepare image name from repo tags
168+ imageName := "unknown"
169+ if len(image.RepoTags) > 0 {
170+ imageName = image.RepoTags[0]
171+ }
172+
173+ // Prepare form data
174+ formData := components.ContainerFormData{
175+ ImageName: imageName,
176+ ImageID: imageID,
177+ ShortID: shortID,
178+ // Default values can be set here if needed
179+ ProxyPort: "80", // Default proxy port
180+ }
181+
182+ // Render the create container form component
183+ return renderer.RenderTempl(c, components.CreateContainerForm(formData))
184+}
185+
186+// GetContainerCount returns the count of containers
187+func GetContainerCount(a *server.App) (int, error) {
188+ containers, err := docker.ListRunningContainers()
189+ if err != nil {
190+ return 0, err
191+ }
192+ return len(containers), nil
193+}
194+
195+// GetImageCount returns the count of images
196+func GetImageCount(a *server.App) (int, error) {
197+ images, err := docker.ListContainerImages()
198+ if err != nil {
199+ return 0, err
200+ }
201+ return len(images), nil
202+}
203+
204+// GetContainers returns the list of containers
205+func GetContainers(a *server.App) ([]types.Container, bool, error) {
206+ containers, err := docker.ListRunningContainers()
207+ if err != nil {
208+ return nil, false, err
209+ }
210+ noContainer := len(containers) == 0
211+ return containers, noContainer, nil
212+}⏎̸
A internal/httpserve/middleware/error_middleware.go +36 -0
@@ -0,0 +1,36 @@
1+package middleware
2+
3+import (
4+ "net/http"
5+
6+ "github.com/bnema/gordon/internal/httpserve/handlers"
7+ "github.com/bnema/gordon/internal/server"
8+ "github.com/labstack/echo/v4"
9+)
10+
11+// CustomHTTPErrorHandler is a custom error handler that uses templ to render error pages
12+func CustomHTTPErrorHandler(a *server.App) echo.HTTPErrorHandler {
13+ return func(err error, c echo.Context) {
14+ code := http.StatusInternalServerError
15+ if he, ok := err.(*echo.HTTPError); ok {
16+ code = he.Code
17+ }
18+
19+ // Handle specific error codes with our templates
20+ switch code {
21+ case http.StatusNotFound:
22+ if err := handlers.RenderNotFoundPage(c, a); err != nil {
23+ c.Logger().Error(err)
24+ }
25+ return
26+ case http.StatusForbidden:
27+ if err := handlers.RenderForbiddenPage(c, a); err != nil {
28+ c.Logger().Error(err)
29+ }
30+ return
31+ default:
32+ // For other errors, use Echo's default error handler
33+ c.Echo().DefaultHTTPErrorHandler(err, c)
34+ }
35+ }
36+} ⏎̸
M internal/httpserve/router.go +47 -7
@@ -12,8 +12,13 @@ import (
1212
1313 // RegisterRoutes registers all routes and middlewares
1414 func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
15-
1615 e.Use(echomid.Recover())
16+
17+ // Log admin status
18+ log.Info("Admin WebUI status",
19+ "enabled", a.Config.Admin.IsAdminWebUIEnabled(),
20+ "path", a.Config.Admin.Path)
21+
1722 // Initiate the session middleware
1823 e.Use(middleware.InitSessionMiddleware(a))
1924
@@ -21,7 +26,10 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
2126 e.Use(middleware.LanguageDetection)
2227
2328 AdminPath := a.Config.Admin.Path
29+ log.Debug("Binding login routes with AdminPath", "path", AdminPath)
2430 bindLoginRoute(e, a, AdminPath)
31+
32+ log.Debug("Binding API endpoints")
2533 bindAPIEndpoints(e, a)
2634
2735 // SetCommonDataMiddleware will pass data to the renderer
@@ -36,15 +44,29 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
3644 // Color scheme detection for dark/light mode
3745 e.Use(middleware.ColorSchemeDetection)
3846
47+ // Set custom error handler
48+ e.HTTPErrorHandler = middleware.CustomHTTPErrorHandler(a)
49+
50+ log.Debug("Binding admin routes with AdminPath", "path", AdminPath)
3951 bindAdminRoute(e, a, AdminPath)
52+
53+ log.Debug("Binding static routes")
4054 bindStaticRoute(e, a, "/*")
41- bindHTMXEndpoints(e, a)
55+
56+ // Only bind HTMX endpoints if Admin WebUI is enabled
57+ if a.Config.Admin.IsAdminWebUIEnabled() {
58+ log.Info("Admin WebUI is enabled, binding HTMX endpoints")
59+ bindHTMXEndpoints(e, a)
60+ } else {
61+ log.Info("Admin WebUI is disabled, skipping HTMX endpoints")
62+ }
4263
4364 // Protect the root path with a 403
4465 e.GET("/", func(c echo.Context) error {
45- return c.String(403, "403 Forbidden")
66+ return handlers.RenderForbiddenPage(c, a)
4667 })
4768
69+ log.Info("All routes registered successfully")
4870 return e
4971 }
5072
@@ -100,8 +122,14 @@ func bindStaticRoute(e *echo.Echo, a *server.App, path string) {
100122
101123 // bindLoginRoute binds all login routes
102124 func bindLoginRoute(e *echo.Echo, a *server.App, adminPath string) {
125+ // Skip binding admin login routes if admin webUI is disabled
126+ if !a.Config.Admin.IsAdminWebUIEnabled() {
127+ log.Debug("Admin WebUI is disabled, skipping admin login routes")
128+ return
129+ }
130+
103131 e.GET(adminPath+"/login", func(c echo.Context) error {
104- return handlers.RenderLoginPage(c, a)
132+ return handlers.RenderTemplLoginPage(c, a)
105133 })
106134 e.POST(adminPath+"/login/submit-token", func(c echo.Context) error {
107135 return handlers.HandleTokenSubmission(c, a)
@@ -119,6 +147,12 @@ func bindLoginRoute(e *echo.Echo, a *server.App, adminPath string) {
119147
120148 // bindAdminRoute binds all admin routes
121149 func bindAdminRoute(e *echo.Echo, a *server.App, adminPath string) {
150+ // Skip binding admin routes if admin webUI is disabled
151+ if !a.Config.Admin.IsAdminWebUIEnabled() {
152+ log.Debug("Admin WebUI is disabled, skipping admin routes")
153+ return
154+ }
155+
122156 adminGroup := e.Group(adminPath)
123157 // Since login is behind the admin path, we cannot use group middleware
124158 adminGroup.GET("", func(c echo.Context) error {
@@ -126,7 +160,7 @@ func bindAdminRoute(e *echo.Echo, a *server.App, adminPath string) {
126160 }, middleware.RequireLogin(a))
127161
128162 adminGroup.GET("/manager", func(c echo.Context) error {
129- return handlers.AdminManagerRoute(c, a)
163+ return handlers.RenderTemplManagerPage(c, a)
130164 }, middleware.RequireLogin(a))
131165 adminGroup.GET("/cc/:ID", func(c echo.Context) error {
132166 return handlers.CreateContainerFullGET(c, a)
@@ -145,7 +179,13 @@ func bindAdminRoute(e *echo.Echo, a *server.App, adminPath string) {
145179
146180 // bindHTMXEndpoints binds all HTMX endpoints
147181 func bindHTMXEndpoints(e *echo.Echo, a *server.App) {
148- // Create a group for /htmx endpoints
182+ // Skip if AdminWebUI is disabled
183+ if !a.Config.Admin.IsAdminWebUIEnabled() {
184+ log.Debug("Admin WebUI is disabled, skipping HTMX endpoints")
185+ return
186+ }
187+
188+ // Create a group for /htmx endpoints
149189 htmxGroup := e.Group("/htmx")
150190
151191 // Apply middleware to the group, so all endpoints are protected
@@ -162,7 +202,7 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) {
162202
163203 // List all containers
164204 htmxGroup.GET("/container-manager", func(c echo.Context) error {
165- return handlers.ContainerManagerComponent(c, a)
205+ return handlers.RenderTemplContainerList(c, a)
166206 })
167207 // Stop a container
168208 htmxGroup.POST("/container-manager/stop/:ID", func(c echo.Context) error {
M internal/proxy/certificates.go +200 -74
@@ -31,8 +31,116 @@ const (
3131 LetsEncryptProdPattern = "%s_letsencrypt-production"
3232 )
3333
34+// detectUpstreamProxy checks for signs of an upstream TLS-terminating proxy
35+func (p *Proxy) detectUpstreamProxy() bool {
36+ // Check if detection is explicitly disabled
37+ if !p.config.DetectUpstreamProxy {
38+ return false
39+ }
40+
41+ // First check our cached detection status - if we already detected a proxy previously
42+ p.upstreamProxyMu.RLock()
43+ alreadyDetected := p.upstreamProxyDetected
44+ p.upstreamProxyMu.RUnlock()
45+
46+ if alreadyDetected {
47+ return true
48+ }
49+
50+ // Check for known HTTP headers that indicate a TLS termination proxy
51+ // This is a best-effort approach since we're analyzing HTTP headers from
52+ // previous requests stored in our context (if available)
53+
54+ // Look for common proxy headers in our recent requests
55+ // This is a static check at startup; ideally we would check incoming
56+ // requests dynamically, but for this implementation we're using
57+ // known proxy environment indicators
58+
59+ // Most common proxy headers: X-Forwarded-Proto, X-Forwarded-SSL, X-Forwarded-For
60+ // Environment variables: HTTPS=on, HTTP_X_FORWARDED_PROTO=https
61+
62+ // Check for common environment variables set by upstream proxies
63+ if os.Getenv("HTTPS") == "on" ||
64+ os.Getenv("HTTP_X_FORWARDED_PROTO") == "https" ||
65+ os.Getenv("HTTP_X_FORWARDED_SSL") == "on" {
66+ logger.Info("Upstream TLS-terminating proxy detected via environment variables",
67+ "detection_method", "environment_variables",
68+ "https", os.Getenv("HTTPS"),
69+ "x_forwarded_proto", os.Getenv("HTTP_X_FORWARDED_PROTO"),
70+ "x_forwarded_ssl", os.Getenv("HTTP_X_FORWARDED_SSL"))
71+
72+ // Cache this result
73+ p.upstreamProxyMu.Lock()
74+ p.upstreamProxyDetected = true
75+ p.upstreamProxyMu.Unlock()
76+
77+ return true
78+ }
79+
80+ // Check if Cloudflare headers are present in environment
81+ // Cloudflare sets environment variables like HTTP_CF_VISITOR={"scheme":"https"}
82+ if cfVisitor := os.Getenv("HTTP_CF_VISITOR"); cfVisitor != "" {
83+ if strings.Contains(cfVisitor, "\"scheme\":\"https\"") {
84+ logger.Info("Cloudflare proxy detected via environment variables",
85+ "detection_method", "cloudflare_headers",
86+ "cf_visitor", cfVisitor)
87+
88+ // Cache this result
89+ p.upstreamProxyMu.Lock()
90+ p.upstreamProxyDetected = true
91+ p.upstreamProxyMu.Unlock()
92+
93+ return true
94+ }
95+ }
96+
97+ // Not detected, but log if explicit configuration is set
98+ if p.config.SkipCertificates {
99+ logger.Info("No upstream proxy detected automatically, but certificate acquisition will be skipped due to configuration",
100+ "detection_result", "false",
101+ "skip_certificates", "true (forced via config)")
102+ }
103+
104+ return false
105+}
106+
34107 // setupCertManager configures the autocert manager for Let's Encrypt
35108 func (p *Proxy) setupCertManager() {
109+ // First check for upstream TLS-terminating proxies if detection is enabled
110+ isProxied := p.detectUpstreamProxy()
111+
112+ // Skip certificate acquisition if we're behind a TLS-terminating proxy
113+ // and the skip certificates option is enabled
114+ if isProxied && p.config.SkipCertificates {
115+ logger.Info("Upstream TLS-terminating proxy detected, skipping Let's Encrypt certificate acquisition",
116+ "upstream_proxy", "detected",
117+ "certificate_acquisition", "disabled")
118+ // We still setup a fallback cert for the admin domain
119+ adminDomain := p.app.GetConfig().Http.FullDomain()
120+ rootDomain := p.app.GetConfig().Http.Domain
121+
122+ var fallbackDomains []string
123+ fallbackDomains = append(fallbackDomains, adminDomain)
124+
125+ // Include root domain in fallback certificate if it's different from admin domain
126+ if rootDomain != "" && rootDomain != adminDomain {
127+ fallbackDomains = append(fallbackDomains, rootDomain)
128+ }
129+
130+ fallbackCert, err := generateFallbackCertificates(fallbackDomains)
131+ if err != nil {
132+ logger.Warn("Failed to generate fallback certificate", "error", err)
133+ } else {
134+ logger.Info("Generated fallback self-signed certificate for admin domain behind proxy",
135+ "domain", adminDomain,
136+ "valid_until", time.Now().Add(24*time.Hour).Format("2006-01-02 15:04:05"))
137+ // Store the fallback certificate
138+ p.fallbackCert = fallbackCert
139+ }
140+
141+ return
142+ }
143+
36144 // Create a cache directory if it doesn't exist
37145 dir := p.config.CertDir
38146 if dir == "" {
@@ -169,11 +277,11 @@ func (p *Proxy) checkCertificateInCache(domain string) (bool, string) {
169277 "expires_in", expiresInDays,
170278 "days",
171279 "type", certType)
172-
280+
173281 // Check if certificate matches current mode
174282 if p.config.LetsEncryptMode == "production" && certType == "staging" {
175283 logger.Info("Found staging certificate but production mode is enabled",
176- "domain", domain,
284+ "domain", domain,
177285 "action", "will request new production certificate")
178286 return false, certType
179287 } else if p.config.LetsEncryptMode == "staging" && certType == "production" {
@@ -183,7 +291,7 @@ func (p *Proxy) checkCertificateInCache(domain string) (bool, string) {
183291 "domain", domain,
184292 "action", "using existing production certificate")
185293 }
186-
294+
187295 return true, certType
188296 }
189297
@@ -206,27 +314,27 @@ func getCertificateFileName(domain string, certType string) string {
206314 func getCertificateType(cert *x509.Certificate) string {
207315 // Check issuer to determine the certificate type
208316 issuer := cert.Issuer.CommonName
209-
317+
210318 // Check for self-signed certificate
211319 if cert.Issuer.CommonName == cert.Subject.CommonName {
212320 return "self-signed"
213321 }
214-
322+
215323 // Check for Let's Encrypt staging certificate
216- if strings.Contains(issuer, "STAGING") ||
217- strings.Contains(issuer, "Fake LE") ||
218- strings.Contains(issuer, "Counterfeit") ||
219- strings.Contains(issuer, "False Fennel") {
324+ if strings.Contains(issuer, "STAGING") ||
325+ strings.Contains(issuer, "Fake LE") ||
326+ strings.Contains(issuer, "Counterfeit") ||
327+ strings.Contains(issuer, "False Fennel") {
220328 return "staging"
221329 }
222-
330+
223331 // Check for Let's Encrypt production certificate
224- if strings.Contains(issuer, "Let's Encrypt") ||
225- strings.Contains(issuer, "R3") ||
226- strings.Contains(issuer, "E1") {
332+ if strings.Contains(issuer, "Let's Encrypt") ||
333+ strings.Contains(issuer, "R3") ||
334+ strings.Contains(issuer, "E1") {
227335 return "production"
228336 }
229-
337+
230338 // Unknown issuer
231339 return "unknown"
232340 }
@@ -252,7 +360,7 @@ func isCertificateValid(certData []byte, domain string) (bool, float64, string,
252360
253361 // Determine the certificate type
254362 certType := getCertificateType(cert)
255-
363+
256364 // Check if the certificate is still valid
257365 now := time.Now()
258366 if now.After(cert.NotAfter) || now.Before(cert.NotBefore) {
@@ -303,21 +411,21 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
303411 getCertificateFileName(domain, "staging"),
304412 getCertificateFileName(domain, "production"),
305413 }
306-
414+
307415 // Check each possible certificate file using known patterns
308416 for _, baseName := range certFiles {
309417 certFile := filepath.Join(certDir, baseName)
310418 keyFile := filepath.Join(certDir, baseName+"+rsa")
311-
419+
312420 // Skip if either file doesn't exist
313421 if _, err := os.Stat(certFile); os.IsNotExist(err) {
314422 continue
315423 }
316-
424+
317425 if _, err := os.Stat(keyFile); os.IsNotExist(err) {
318426 continue
319427 }
320-
428+
321429 // If we found both files, validate the certificate
322430 certData, err := os.ReadFile(certFile)
323431 if err != nil {
@@ -327,24 +435,24 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
327435 "error", err)
328436 continue
329437 }
330-
438+
331439 // Validate the certificate data
332440 validCert, expiresInDays, certType, _ := isCertificateValid(certData, domain)
333441 if !validCert {
334442 continue
335443 }
336-
444+
337445 logger.Info("Valid certificate found in filesystem with standard naming pattern",
338446 "domain", domain,
339447 "expires_in", expiresInDays,
340448 "days",
341449 "type", certType,
342450 "cert_file", certFile)
343-
451+
344452 // Check if certificate matches current mode
345453 if p.config.LetsEncryptMode == "production" && certType == "staging" {
346454 logger.Info("Found staging certificate but production mode is enabled",
347- "domain", domain,
455+ "domain", domain,
348456 "action", "will request new production certificate")
349457 // Delete the staging certificate
350458 deleteCertificateFiles(certDir, baseName)
@@ -356,7 +464,7 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
356464 "domain", domain,
357465 "action", "using existing production certificate")
358466 }
359-
467+
360468 // Log some debug information about the certificate files
361469 certFileInfo, _ := os.Stat(certFile)
362470 keyFileInfo, _ := os.Stat(keyFile)
@@ -368,30 +476,30 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
368476 "key_file", keyFile,
369477 "key_size", keyFileInfo.Size(),
370478 "key_modified", keyFileInfo.ModTime())
371-
479+
372480 // Attempt to add to autocert cache if it's not already there
373481 if certType == "production" || (certType == "staging" && p.config.LetsEncryptMode == "staging") {
374482 // Add to autocert cache for future use
375483 p.addCertificateToCache(domain, certFile, keyFile)
376484 }
377-
485+
378486 return true, certType
379487 }
380488
381489 // If no matches with standard patterns, try a glob search for any files starting with the domain name
382- // This will catch files with any suffix like domain_letsencrypt-production
490+ // This will catch files with any suffix like domain_letsencrypt-production
383491 // and domain_letsencrypt-staging
384492 logger.Debug("No standard certificate files found, searching for any matching certificates",
385493 "domain", domain)
386-
494+
387495 files, err := filepath.Glob(filepath.Join(certDir, domain+"*"))
388496 if err != nil {
389- logger.Warn("Error searching for certificate files",
390- "domain", domain,
497+ logger.Warn("Error searching for certificate files",
498+ "domain", domain,
391499 "error", err)
392500 return false, ""
393501 }
394-
502+
395503 // Extract base names without the path
396504 var candidates []string
397505 for _, file := range files {
@@ -399,34 +507,34 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
399507 if strings.HasSuffix(file, "+rsa") {
400508 continue
401509 }
402-
510+
403511 baseFile := filepath.Base(file)
404512 // Check if the corresponding key file exists
405513 keyFile := file + "+rsa"
406514 if _, err := os.Stat(keyFile); os.IsNotExist(err) {
407515 continue
408516 }
409-
517+
410518 // Add this candidate
411519 candidates = append(candidates, baseFile)
412520 }
413-
521+
414522 // No certificate files found
415523 if len(candidates) == 0 {
416- logger.Debug("No certificate files found in filesystem glob search",
524+ logger.Debug("No certificate files found in filesystem glob search",
417525 "domain", domain)
418526 return false, ""
419527 }
420-
528+
421529 logger.Debug("Found certificate candidates in glob search",
422530 "domain", domain,
423531 "candidates", candidates)
424-
532+
425533 // Process each candidate (if there are multiple matches)
426534 for _, baseName := range candidates {
427535 certFile := filepath.Join(certDir, baseName)
428536 keyFile := filepath.Join(certDir, baseName+"+rsa")
429-
537+
430538 // Read and validate the certificate
431539 certData, err := os.ReadFile(certFile)
432540 if err != nil {
@@ -436,27 +544,27 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
436544 "error", err)
437545 continue
438546 }
439-
547+
440548 // Validate the certificate data
441549 validCert, expiresInDays, certType, _ := isCertificateValid(certData, domain)
442550 if !validCert {
443551 continue
444552 }
445-
553+
446554 logger.Info("Valid certificate found in filesystem with non-standard naming",
447555 "domain", domain,
448556 "expires_in", expiresInDays,
449557 "days",
450558 "type", certType,
451559 "cert_file", certFile)
452-
560+
453561 // If we're in production mode and this is a production certificate, use it
454562 if p.config.LetsEncryptMode == "production" && certType == "production" {
455563 // Attempt to add to autocert cache
456564 p.addCertificateToCache(domain, certFile, keyFile)
457565 return true, certType
458566 }
459-
567+
460568 // If we're in staging mode, prefer production certificates if available
461569 if p.config.LetsEncryptMode == "staging" {
462570 if certType == "production" {
@@ -470,7 +578,7 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
470578 return true, certType
471579 }
472580 }
473-
581+
474582 // If this is a staging certificate but we're in production mode, ignore it
475583 // and continue checking other candidates
476584 if p.config.LetsEncryptMode == "production" && certType == "staging" {
@@ -479,7 +587,7 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
479587 "cert_file", certFile)
480588 continue
481589 }
482-
590+
483591 // Fallback: use the certificate if it's valid
484592 // Attempt to add to autocert cache
485593 p.addCertificateToCache(domain, certFile, keyFile)
@@ -495,7 +603,7 @@ func (p *Proxy) checkCertificateInFilesystem(domain string) (bool, string) {
495603 func deleteCertificateFiles(certDir string, baseName string) {
496604 certFile := filepath.Join(certDir, baseName)
497605 keyFile := filepath.Join(certDir, baseName+"+rsa")
498-
606+
499607 // Delete certificate file
500608 if err := os.Remove(certFile); err != nil && !os.IsNotExist(err) {
501609 logger.Warn("Failed to delete certificate file",
@@ -504,7 +612,7 @@ func deleteCertificateFiles(certDir string, baseName string) {
504612 } else if !os.IsNotExist(err) {
505613 logger.Info("Deleted certificate file", "file", certFile)
506614 }
507-
615+
508616 // Delete key file
509617 if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) {
510618 logger.Warn("Failed to delete certificate key file",
@@ -665,8 +773,8 @@ func (p *Proxy) checkCertificateExists(domain string) (bool, string) {
665773 // First check in the cache
666774 validInCache, cacheType := p.checkCertificateInCache(domain)
667775 if validInCache {
668- logger.Debug("Certificate exists in cache",
669- "domain", domain,
776+ logger.Debug("Certificate exists in cache",
777+ "domain", domain,
670778 "type", cacheType)
671779 return true, cacheType
672780 }
@@ -674,8 +782,8 @@ func (p *Proxy) checkCertificateExists(domain string) (bool, string) {
674782 // If not in cache, check the filesystem
675783 validInFS, fsType := p.checkCertificateInFilesystem(domain)
676784 if validInFS {
677- logger.Debug("Certificate exists in filesystem but not in cache",
678- "domain", domain,
785+ logger.Debug("Certificate exists in filesystem but not in cache",
786+ "domain", domain,
679787 "type", fsType)
680788 return true, fsType
681789 }
@@ -699,6 +807,15 @@ func (p *Proxy) requestAdminCertificate() {
699807 return
700808 }
701809
810+ // Check for upstream proxy with certificate skipping enabled
811+ isProxied := p.detectUpstreamProxy()
812+ if isProxied && p.config.SkipCertificates {
813+ logger.Info("Upstream TLS-terminating proxy detected, skipping admin certificate request",
814+ "domain", adminDomain,
815+ "skip_reason", "behind TLS-terminating proxy")
816+ return
817+ }
818+
702819 // Extract hostname from admin domain, resolving it to see if it's publicly accessible
703820 host := adminDomain
704821 ips, err := net.LookupIP(host)
@@ -721,7 +838,7 @@ func (p *Proxy) requestAdminCertificate() {
721838
722839 // Check if we already have a valid certificate (cache or filesystem)
723840 hasValidCert, certType := p.checkCertificateExists(adminDomain)
724-
841+
725842 // If we have a valid cert that matches our mode, use it
726843 if hasValidCert {
727844 // Check for mode mismatch
@@ -765,7 +882,7 @@ func (p *Proxy) requestAdminCertificate() {
765882 if certDir == "" {
766883 certDir = p.app.GetConfig().General.StorageDir + "/certs"
767884 }
768-
885+
769886 // Ensure certificate directory exists
770887 if err := os.MkdirAll(certDir, 0755); err != nil {
771888 logger.Warn("Failed to create certificate directory",
@@ -863,15 +980,15 @@ func (p *Proxy) requestAdminCertificate() {
863980 if p.config.LetsEncryptMode == "staging" {
864981 certType = "staging"
865982 }
866-
983+
867984 // Try to rename the certificate files
868985 oldCertPath := filepath.Join(certDir, adminDomain)
869986 oldKeyPath := filepath.Join(certDir, adminDomain+"+rsa")
870-
987+
871988 newName := getCertificateFileName(adminDomain, certType)
872989 newCertPath := filepath.Join(certDir, newName)
873990 newKeyPath := filepath.Join(certDir, newName+"+rsa")
874-
991+
875992 // Only rename if the files exist and the new names are different
876993 if oldCertPath != newCertPath {
877994 // Check if old cert exists
@@ -890,7 +1007,7 @@ func (p *Proxy) requestAdminCertificate() {
8901007 "from", oldCertPath,
8911008 "to", newCertPath)
8921009 }
893-
1010+
8941011 // Rename key
8951012 if _, err := os.Stat(oldKeyPath); err == nil {
8961013 if err := os.Rename(oldKeyPath, newKeyPath); err != nil {
@@ -909,7 +1026,7 @@ func (p *Proxy) requestAdminCertificate() {
9091026 }
9101027 }
9111028 }
912-
1029+
9131030 logger.Info("Successfully obtained Let's Encrypt certificate",
9141031 "domain", adminDomain,
9151032 "type", certType)
@@ -930,6 +1047,15 @@ func (p *Proxy) requestDomainCertificate(domain string) {
9301047 return
9311048 }
9321049
1050+ // Check for upstream proxy with certificate skipping enabled
1051+ isProxied := p.detectUpstreamProxy()
1052+ if isProxied && p.config.SkipCertificates {
1053+ logger.Info("Upstream TLS-terminating proxy detected, skipping domain certificate request",
1054+ "domain", domain,
1055+ "skip_reason", "behind TLS-terminating proxy")
1056+ return
1057+ }
1058+
9331059 // Extract hostname from domain, resolving it to see if it's publicly accessible
9341060 ips, err := net.LookupIP(domain)
9351061 if err != nil {
@@ -951,7 +1077,7 @@ func (p *Proxy) requestDomainCertificate(domain string) {
9511077
9521078 // Check if we already have a valid certificate (cache or filesystem)
9531079 hasValidCert, certType := p.checkCertificateExists(domain)
954-
1080+
9551081 // If we have a valid cert that matches our mode, use it
9561082 if hasValidCert {
9571083 // Check for mode mismatch
@@ -995,7 +1121,7 @@ func (p *Proxy) requestDomainCertificate(domain string) {
9951121 if certDir == "" {
9961122 certDir = p.app.GetConfig().General.StorageDir + "/certs"
9971123 }
998-
1124+
9991125 // Ensure certificate directory exists
10001126 if err := os.MkdirAll(certDir, 0755); err != nil {
10011127 logger.Warn("Failed to create certificate directory",
@@ -1093,15 +1219,15 @@ func (p *Proxy) requestDomainCertificate(domain string) {
10931219 if p.config.LetsEncryptMode == "staging" {
10941220 certType = "staging"
10951221 }
1096-
1222+
10971223 // Try to rename the certificate files
10981224 oldCertPath := filepath.Join(certDir, domain)
10991225 oldKeyPath := filepath.Join(certDir, domain+"+rsa")
1100-
1226+
11011227 newName := getCertificateFileName(domain, certType)
11021228 newCertPath := filepath.Join(certDir, newName)
11031229 newKeyPath := filepath.Join(certDir, newName+"+rsa")
1104-
1230+
11051231 // Only rename if the files exist and the new names are different
11061232 if oldCertPath != newCertPath {
11071233 // Check if old cert exists
@@ -1120,7 +1246,7 @@ func (p *Proxy) requestDomainCertificate(domain string) {
11201246 "from", oldCertPath,
11211247 "to", newCertPath)
11221248 }
1123-
1249+
11241250 // Rename key
11251251 if _, err := os.Stat(oldKeyPath); err == nil {
11261252 if err := os.Rename(oldKeyPath, newKeyPath); err != nil {
@@ -1139,7 +1265,7 @@ func (p *Proxy) requestDomainCertificate(domain string) {
11391265 }
11401266 }
11411267 }
1142-
1268+
11431269 logger.Info("Successfully obtained Let's Encrypt certificate",
11441270 "domain", domain,
11451271 "type", certType)
@@ -1224,15 +1350,15 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
12241350 if p.config.LetsEncryptMode == "staging" {
12251351 certType = "staging"
12261352 }
1227-
1353+
12281354 // Rename the certificate files to include the type
12291355 oldCertPath := filepath.Join(certDir, domain)
12301356 oldKeyPath := filepath.Join(certDir, domain+"+rsa")
1231-
1357+
12321358 newName := getCertificateFileName(domain, certType)
12331359 newCertPath := filepath.Join(certDir, newName)
12341360 newKeyPath := filepath.Join(certDir, newName+"+rsa")
1235-
1361+
12361362 // Only rename if the files exist and the new names are different
12371363 if oldCertPath != newCertPath {
12381364 // Check if old cert exists
@@ -1251,7 +1377,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
12511377 "from", oldCertPath,
12521378 "to", newCertPath)
12531379 }
1254-
1380+
12551381 // Rename key
12561382 if _, err := os.Stat(oldKeyPath); err == nil {
12571383 if err := os.Rename(oldKeyPath, newKeyPath); err != nil {
@@ -1270,7 +1396,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
12701396 }
12711397 }
12721398 }
1273-
1399+
12741400 logger.Info("Successfully obtained Let's Encrypt certificate on retry",
12751401 "domain", domain,
12761402 "attempt", attempt,
@@ -1326,7 +1452,7 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
13261452 "domain", domain,
13271453 "max_retries", maxRetries,
13281454 "fallback", "Generating self-signed certificate")
1329-
1455+
13301456 // Generate a self-signed certificate for this domain
13311457 fallbackCert, err := generateFallbackCertificates([]string{domain})
13321458 if err != nil {
@@ -1338,13 +1464,13 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
13381464 selfSignedName := getCertificateFileName(domain, "self-signed")
13391465 certPath := filepath.Join(certDir, selfSignedName)
13401466 keyPath := filepath.Join(certDir, selfSignedName+"+rsa")
1341-
1467+
13421468 // Save the certificate and key
13431469 certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: fallbackCert.Certificate[0]})
13441470 keyBytes, err := x509.MarshalPKCS8PrivateKey(fallbackCert.PrivateKey)
13451471 if err == nil {
13461472 keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
1347-
1473+
13481474 // Write files
13491475 if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
13501476 logger.Error("Failed to write self-signed certificate to disk",
@@ -1352,14 +1478,14 @@ func (p *Proxy) retryCertificateRequest(domain string, maxRetries int, initialBa
13521478 "path", certPath,
13531479 "error", err)
13541480 }
1355-
1481+
13561482 if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
13571483 logger.Error("Failed to write self-signed key to disk",
13581484 "domain", domain,
13591485 "path", keyPath,
13601486 "error", err)
13611487 }
1362-
1488+
13631489 logger.Info("Generated and saved self-signed fallback certificate",
13641490 "domain", domain,
13651491 "cert_path", certPath,
M internal/proxy/containers.go +7 -5
@@ -1,7 +1,6 @@
11 package proxy
22
33 import (
4- "fmt"
54 "os"
65 "strings"
76
@@ -12,11 +11,11 @@ import (
1211 // scanContainersAndCheckCertificates scans all containers from the Gordon network
1312 // and checks if we have certificates for their domains and subdomains
1413 func (p *Proxy) scanContainersAndCheckCertificates() error {
15- // Get the network name from app config
1614 networkName := p.app.GetConfig().ContainerEngine.Network
17-
1815 if networkName == "" {
19- return fmt.Errorf("container network name is not configured")
16+ // If no network is defined, skip container scanning
17+ logger.Info("No container network defined, skipping certificate checks")
18+ return nil
2019 }
2120
2221 logger.Info("Scanning containers in network for certificate checks",
@@ -25,7 +24,10 @@ func (p *Proxy) scanContainersAndCheckCertificates() error {
2524 // Try to get network info using the docker client
2625 networkInfo, err := docker.GetNetworkInfo(networkName)
2726 if err != nil {
28- return fmt.Errorf("failed to get network info: %w", err)
27+ logger.Warn("Failed to get network info, skipping certificate checks",
28+ "error", err,
29+ "network", networkName)
30+ return nil // Return nil instead of error to continue execution
2931 }
3032
3133 // Skip admin cert check if environment variable is set
M internal/proxy/core.go +34 -6
@@ -22,7 +22,6 @@ import (
2222 // - certificates.go - Certificate management functions
2323 // - routes.go - Route management functions
2424 // - middleware.go - HTTP middleware functions
25-// - blacklist.go - IP blacklisting functionality
2625 // - server.go - Server startup and shutdown
2726 // - utils.go - Utility functions
2827
@@ -58,6 +57,10 @@ type Proxy struct {
5857 recentContainersMu sync.RWMutex
5958 containerCooldownPeriod time.Duration
6059
60+ // Upstream proxy detection
61+ upstreamProxyDetected bool
62+ upstreamProxyMu sync.RWMutex
63+
6164 // SQL queries
6265 queries *queries.ProxyQueries
6366 }
@@ -66,8 +69,36 @@ type Proxy struct {
6669 func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
6770 logger.Debug("Initializing reverse proxy")
6871
69- // Log important configuration information
70- logger.Debug("All internal connections to containers will use HTTP protocol regardless of external protocol")
72+ // Set up the config
73+ config := app.GetConfig().ReverseProxy
74+
75+ // Check if proxy is disabled through configuration
76+ if !config.Enabled {
77+ logger.Info("Reverse proxy is disabled via configuration")
78+ // Return a non-active proxy instance
79+ return &Proxy{
80+ config: config,
81+ app: app,
82+ serverStarted: false,
83+ shutdown: make(chan struct{}),
84+ }, nil
85+ }
86+
87+ // Check if running inside a container
88+ isContainer := docker.IsRunningInContainer()
89+ logger.Debug("Container environment check", "is_running_in_container", isContainer)
90+
91+ if !isContainer {
92+ logger.Info("Gordon is not running in a container, proxy will be disabled automatically")
93+ // Override the enabled setting to false
94+ config.Enabled = false
95+ return &Proxy{
96+ config: config,
97+ app: app,
98+ serverStarted: false,
99+ shutdown: make(chan struct{}),
100+ }, nil
101+ }
71102
72103 // Set up the echo server for HTTPS traffic
73104 httpsServer := echo.New()
@@ -77,9 +108,6 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
77108 httpServer := echo.New()
78109 httpServer.HideBanner = true
79110
80- // Set up the config
81- config := app.GetConfig().ReverseProxy
82-
83111 // Detect and store our container ID (if we're running in a container)
84112 var ourContainerID string
85113 if docker.IsRunningInContainer() {
D internal/proxy/utils.go +0 -26
@@ -1,26 +0,0 @@
1-package proxy
2-
3-import (
4- "crypto/rand"
5- "fmt"
6- "time"
7-
8- "github.com/google/uuid"
9-)
10-
11-// proxyGenerateUUID generates a random UUID for use in various identifiers
12-// This is a renamed version to avoid conflicts until we clean up proxy.go
13-func proxyGenerateUUID() string {
14- id, err := uuid.NewRandom()
15- if err != nil {
16- // If we can't generate a UUID using the standard library,
17- // fall back to our own implementation
18- b := make([]byte, 16)
19- _, err := rand.Read(b)
20- if err != nil {
21- return fmt.Sprintf("%d", time.Now().UnixNano())
22- }
23- return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
24- }
25- return id.String()
26-}
M internal/templating/models/html/admin/createcontainerfull.gohtml +3 -3
@@ -2,13 +2,13 @@
22 <html lang="en">
33 {{template "header.gohtml" .}}
44
5- <body class="bg-zinc-200 flex flex-col lg:flex-row p-0">
5+ <body class="bg-app flex flex-col lg:flex-row p-0">
66 {{template "menu.gohtml" .}}
77 <!-- Main content here -->
88 <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
99 <div class="flex flex-col md:flex-row w-full flex-grow">
10- <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
11- <h1 class="text-2xl font-bold mb-4">Edit Container</h1>
10+ <div class="card flex-grow">
11+ <h1 class="card-header mb-4">Edit Container</h1>
1212 <!-- Include the existing edit container fragment here -->
1313 {{template "createcontainer.gohtml" .}}
1414 </div>
M internal/templating/models/html/admin/dashboard.gohtml +1 -1
@@ -1,6 +1,6 @@
11 <div id="dashboard">
22 <h1
3- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
3+ class="text-5xl fw100 text-indigo-600 animate-bounce-alt animate-count-infinite animate-duration-1s"
44 >
55 Hello World from dashboard.html
66 </h1>
M internal/templating/models/html/admin/index.gohtml +9 -9
@@ -2,23 +2,23 @@
22 <html lang="en">
33 {{template "header.gohtml" .}}
44
5- <body class="bg-zinc-200 flex flex-col md:flex-row p-4 md:p-0">
5+ <body class="bg-app flex flex-col md:flex-row p-4 md:p-0">
66 {{template "menu.gohtml" .}}
77 <div class="flex-1 flex flex-col md:pt-10 space-y-4 p-4 md:ml-4">
88 <div
99 class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow">
10- <div class="flex-grow bg-white p-4 rounded-md shadow mb-4">
11- <h2>{{index .Lang "dashboard" "content" "title"}}</h2>
10+ <div class="card flex-grow">
11+ <h2 class="card-header">{{index .Lang "dashboard" "content" "title"}}</h2>
1212 </div>
1313 <div class="w-full md:w-2/6 flex flex-col space-y-4">
14- <div class="bg-white p-4 rounded-md shadow mb-4">
15- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
14+ <div class="card">
15+ <div class="w-full h-44 rounded-sm bg-indigo-100"></div>
1616 </div>
17- <div class="bg-white p-4 rounded-md shadow mb-4">
18- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
17+ <div class="card">
18+ <div class="w-full h-44 rounded-sm bg-indigo-100"></div>
1919 </div>
20- <div class="bg-white p-4 rounded-md shadow mb-4">
21- <div class="w-full h-44 rounded-sm bg-gray-300"></div>
20+ <div class="card">
21+ <div class="w-full h-44 rounded-sm bg-indigo-100"></div>
2222 </div>
2323 </div>
2424 </div>
M internal/templating/models/html/admin/install.gohtml +7 -7
@@ -21,7 +21,7 @@
2121 </head>
2222
2323 <body
24- class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center"
24+ class="IBMPlexSans bg-app md:p-8 flex flex-col items-center"
2525 hx-boost="true"
2626 >
2727 <!-- Logo and Title -->
@@ -38,28 +38,28 @@
3838 </div>
3939 <div class="flex justify-center mt-4">
4040 <h1
41- class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter"
41+ class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-indigo-800 tracking-tighter"
4242 >
4343 Gordon
4444 </h1>
45- <span class="gordon-v-subtitle text-gordonmascot-mblue text-sm ml-2"
45+ <span class="gordon-v-subtitle text-indigo-600 text-sm ml-2"
4646 >v0.0.1</span
4747 >
4848 </div>
4949 </div>
5050
5151 <!-- Main Content -->
52- <div class="bg-white p-4 rounded-md shadow mb-4 w-2/3">
52+ <div class="card w-2/3">
5353 <!-- Built-in Proxy -->
5454 <div id="built-in-proxy" class="mt-8">
5555 <div class="flex items-center mb-4">
56- <div class="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
56+ <div class="w-8 h-8 bg-indigo-500 rounded-full flex items-center justify-center">
5757 <span class="text-white font-bold">✓</span>
5858 </div>
59- <h2 class="text-lg ml-2">Reverse Proxy</h2>
59+ <h2 class="text-lg ml-2 text-title">Reverse Proxy</h2>
6060 </div>
6161
62- <div class="bg-gray-100 p-4 rounded-md">
62+ <div class="bg-indigo-50 p-4 rounded-md">
6363 <p class="mb-4">Gordon now uses a built-in reverse proxy to handle all domain routing and TLS certificates.</p>
6464 <p>The proxy automatically:</p>
6565 <ul class="list-disc ml-6 mb-4">
M internal/templating/models/html/admin/manager.gohtml +65 -23
@@ -1,34 +1,76 @@
11 <!-- manager.gohtml -->
22 <!DOCTYPE html>
3-<html lang="en">
3+<html lang="en" data-theme="gordon">
44 {{template "header.gohtml" .}}
55
6- <body class="bg-zinc-200 flex flex-col lg:flex-row p-0">
6+ <body class="min-h-screen bg-base-200">
77 {{template "menu.gohtml" .}}
8- <!-- Main content here -->
9- <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6">
10- <div class="flex flex-col md:flex-row w-full flex-grow">
11- <div class="flex-grow bg-white p-4 md:rounded-md md:shadow mb-4">
12- <!-- List all the containers -->
13- <div
14- id="container-manager"
15- hx-get="/htmx/container-manager"
16- hx-trigger="load"
17- hx-swap="innerHTML"
18- hx-swap-oob="true"
19- ></div>
20- <!-- List all the images -->
21- <div
22- id="image-manager"
23- hx-get="/htmx/image-manager"
24- hx-trigger="load"
25- hx-swap="innerHTML"
26- hx-swap-oob="true"
27- ></div>
8+
9+ <!-- Main content -->
10+ <div class="container mx-auto py-6 px-4 mt-16">
11+ <div class="text-sm breadcrumbs mb-4">
12+ <ul>
13+ <li><a href="{{ .AdminPath }}">Gordon</a></li>
14+ <li>Manager</li>
15+ </ul>
2816 </div>
17+
18+ <!-- Stats overview -->
19+ <div class="stats shadow mb-6 w-full">
20+ <div class="stat">
21+ <div class="stat-figure text-primary">
22+ <span class="iconf text-3xl">󰡨</span>
23+ </div>
24+ <div class="stat-title">Containers</div>
25+ <div class="stat-value" id="container-count">{{.ContainerCount}}</div>
26+ <div class="stat-desc">Docker containers managed by Gordon</div>
27+ </div>
28+
29+ <div class="stat">
30+ <div class="stat-figure text-secondary">
31+ <span class="iconf text-3xl">󰋊</span>
32+ </div>
33+ <div class="stat-title">Images</div>
34+ <div class="stat-value" id="image-count">{{.ImageCount}}</div>
35+ <div class="stat-desc">Docker images available</div>
36+ </div>
37+ </div>
38+
39+ <!-- Tabs for containers and images -->
40+ <div role="tablist" class="tabs tabs-boxed mb-4">
41+ <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked />
42+ <div role="tabpanel" class="tab-content p-0">
43+ <!-- Container manager section -->
44+ <div id="container-manager"
45+ hx-get="/htmx/container-manager"
46+ hx-trigger="load"
47+ hx-swap="innerHTML">
48+ <!-- Container content loaded via HTMX -->
49+ <div class="text-center p-4">
50+ <span class="iconf text-3xl animate-spin inline-block">󰑐</span>
51+ Loading containers...
52+ </div>
53+ </div>
54+ </div>
55+
56+ <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" />
57+ <div role="tabpanel" class="tab-content p-0">
58+ <!-- Image manager section -->
59+ <div id="image-manager"
60+ hx-get="/htmx/image-manager"
61+ hx-trigger="load"
62+ hx-swap="innerHTML">
63+ <!-- Image content loaded via HTMX -->
64+ <div class="text-center p-4">
65+ <span class="iconf text-3xl animate-spin inline-block">󰑐</span>
66+ Loading images...
67+ </div>
68+ </div>
69+ </div>
70+ </div>
71+
2972 </div>
3073
31- <!-- footer -->
3274 {{template "footer.gohtml" .}}
3375 </body>
3476 </html>
M internal/templating/models/html/fragments/components.gohtml +55 -12
@@ -1,20 +1,24 @@
11 <!-- @format -->
22
3-<div id="header">
4- <h1
5- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
6- >
7- {{ .body.header.helloworld }}
8- </h1>
3+<div id="header" class="hero bg-base-200">
4+ <div class="hero-content text-center">
5+ <div class="max-w-md">
6+ <h1 class="text-5xl font-bold gordon-title">
7+ {{ .body.header.helloworld }}
8+ </h1>
9+ <p class="py-6 IBMPlexSans">Welcome to Gordon Container Management system.</p>
10+ <button class="btn btn-primary">Get Started</button>
11+ </div>
12+ </div>
913 </div>
1014
11-<div id="hello">
12- <h1
13- class="text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s"
14- >
15- {{ .body.div.hello }}
16- </h1>
15+<div id="hello" class="alert alert-info shadow-lg mt-4 mb-4">
16+ <div>
17+ <span class="iconfb">󰋽</span>
18+ <span class="IBMPlexSans">{{ .body.div.hello }}</span>
19+ </div>
1720 </div>
21+
1822 <div
1923 id="header"
2024 hx-get="/htmx"
@@ -31,3 +35,42 @@
3135 hx-trigger="load"
3236 hx-swap="outerHTML"
3337 ></div>
38+
39+<!-- Stats Cards -->
40+<div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-4">
41+ <div class="stat bg-base-100 shadow">
42+ <div class="stat-figure text-primary">
43+ <span class="iconfb text-3xl">󰡨</span>
44+ </div>
45+ <div class="stat-title">Containers</div>
46+ <div class="stat-value">31</div>
47+ <div class="stat-desc">Running containers</div>
48+ </div>
49+
50+ <div class="stat bg-base-100 shadow">
51+ <div class="stat-figure text-secondary">
52+ <span class="iconfb text-3xl">󱧕</span>
53+ </div>
54+ <div class="stat-title">Images</div>
55+ <div class="stat-value">42</div>
56+ <div class="stat-desc">Available images</div>
57+ </div>
58+
59+ <div class="stat bg-base-100 shadow">
60+ <div class="stat-figure text-accent">
61+ <span class="iconfb text-3xl">󰙅</span>
62+ </div>
63+ <div class="stat-title">Networks</div>
64+ <div class="stat-value">12</div>
65+ <div class="stat-desc">Container networks</div>
66+ </div>
67+</div>
68+
69+<!-- Example of tabs -->
70+<div class="tabs tabs-boxed my-4">
71+ <a class="tab tab-active">Overview</a>
72+ <a class="tab">Containers</a>
73+ <a class="tab">Images</a>
74+ <a class="tab">Networks</a>
75+ <a class="tab">Volumes</a>
76+</div>
M internal/templating/models/html/fragments/containerlist.gohtml +120 -110
@@ -1,112 +1,122 @@
1-<div class="flex flex-col p-4" hx-ext="response-targets">
2- <div class="flex justify-between items-center mb-1">
3- <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">
4- {{index .Lang "manager" "content" "h1-containers"}}
5- </h1>
6- <button
7- id="refresh-containers-button"
8- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
9- title="Refresh container list"
10- >
11- <span class="iconf text-xl align-middle">󰑐 </span>
12- Refresh
13- </button>
14- </div>
15- <table class="p-4 border-t-2 table-auto">
16- <thead>
17- <tr class="text-center text-gray-400">
18- <th>{{index .Lang "manager" "content" "thName"}}</th>
19- <th>{{index .Lang "manager" "content" "thPorts"}}</th>
20- <th>{{index .Lang "manager" "content" "thEntryPoint"}}</th>
21- <th>{{index .Lang "manager" "content" "thStatus"}}</th>
22- <th>{{index .Lang "manager" "content" "thState"}}</th>
23- <th>{{index .Lang "manager" "content" "thActions"}}</th>
24- </tr>
25- </thead>
26- <tbody>
27- {{ $Lang := .Lang }}
28- {{
29- range.containers
30- }}
31- <tr id="container-{{.ID}}" class="text-center hover:bg-slate-100">
32- <td>
33- <span
34- title="{{.Container.ID}}"
35- class="font-bold"
36- >{{.Name}}</span
37- >
38- </td>
39- <td class="port-container">{{ .Ports }}</td>
40- <td class="entrypoint-container">{{.ProxyPort}}</td>
41- <td class="status-container">{{.Status}} (Up since {{.UpSince}})</td>
42- <td style="color: {{.StateColor}};">{{.State}}</td>
43- <td>
44- <button
45- id="start-button-{{.ID}}"
46- hx-post="/htmx/container-manager/start/{{.ID}}"
47- hx-target="#actionResponse-{{.ID}}"
48- hx-target-*="#error-message-{{.ID}}"
49- hx-swap="outerHTML"
50- class="py-2 px-4"
51- >
52- <span title="Start the container" class="iconf text-xl text-blue-600 hover:text-gordonmascot-hblue"></span>
53- </button>
54- <!-- EDIT -->
55- <button
56- id="edit-button-{{.ID}}"
57- hx-get="/htmx/container-manager/edit/{{.ID}}"
58- hx-target="#actionResponse-{{.ID}}"
59- hx-target-*="#error-message-{{.ID}}"
60- hx-swap="innerHTML"
61- class="py-2 px-4"
62- data-active="false"
63- >
64- <span title="Edit the container" class="iconf text-2xl text-blue-600 hover:text-gordonmascot-hblue">󱩽</span>
65- </button>
66- <!-- stop -->
67- <button
68- id="stop-button-{{.ID}}"
69- hx-post="/htmx/container-manager/stop/{{.ID}}"
70- hx-target="#actionResponse-{{.ID}}"
71- hx-target-*="#error-message-{{.ID}}"
72- hx-swap="innerHTML"
73- hx-confirm="{{index $Lang "manager" "content" "confirm-container-stop"}}"
74- class="py-2 px-4"
75- >
76- <span title="Stop the container" class="iconf text-xl text-red-600 hover:text-red-900"></span>
77- </button>
78- <!-- REMOVE -->
79- <button
80- id="remove-button-img-{{.ID}}"
81- hx-delete="/htmx/container-manager/delete/{{.ID}}"
82- hx-target="#actionResponse-{{.ID}}"
83- hx-target-*="#error-message-{{.ID}}"
84- hx-swap="innerHTML"
85- hx-confirm="{{index $Lang "manager" "content" "confirm-delete"}}"
86- class="py-2 px-4"
87- >
88- <!-- iconoir trash -->
89- <span title="Remove the container" class="iconf text-xl text-red-600 hover:text-red-900"></span>
90- </button>
91- </td>
1+<!-- Container list using daisyUI components -->
2+<div class="card w-full bg-base-300">
3+ <div class="card-body p-4">
4+
5+ <!-- Action buttons -->
6+ <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
7+ <h3 class="font-semibold text-base-content">Available Containers: {{ len .Containers }}</h3>
8+ <div class="btn-group">
9+ <button
10+ class="btn btn-sm btn-primary"
11+ hx-get="/htmx/container-manager"
12+ hx-target="#container-manager"
13+ hx-swap="innerHTML">
14+ <span class="iconf">󰑓</span> Refresh
15+ </button>
16+ </div>
17+ </div>
18+
19+ <!-- Container list -->
20+ <table class="table">
21+ <!-- head -->
22+ <thead>
23+ <tr>
24+ <th>Name</th>
25+ <th>Image</th>
26+ <th>Status</th>
27+ <th>Created</th>
28+ <th>Actions</th>
29+ </tr>
30+ </thead>
31+ <tbody>
32+ {{ if or .NoContainer (eq (len .Containers) 0) }}
33+ <tr>
34+ <td colspan="5" class="text-center py-4">
35+ <div class="alert alert-info">
36+ <span class="iconf">ℹ️</span>
37+ <span>No containers found. Upload an image first, then create a container.</span>
38+ </div>
39+ </td>
9240 </tr>
93- <tr>
94- <td colspan="7">
95- <div id="actionResponse-{{.ID}}"></div>
96- <div id="error-message-{{.ID}}" class="text-red-500"></div>
97- </td>
98- </tr>
99- {{ else }}
100- <tr>
101- <td colspan="7" class="mt-2">
102- <p class="text-center text-gray-400 text-xl">
103- {{index $Lang "manager" "content" "no-containers"}}
104- </p>
105- </td>
106- </tr>
107- {{
108- end
109- }}
110- </tbody>
111- </table>
41+ {{ else }}
42+ {{ range .Containers }}
43+ <tr class="hover">
44+ <td>
45+ <div class="flex items-center gap-3">
46+ <div>
47+ <div class="font-bold">{{ .Names }}</div>
48+ <div class="text-sm opacity-50">{{ .ID }}</div>
49+ </div>
50+ </div>
51+ </td>
52+ <td>{{ .Image }}</td>
53+ <td>
54+ {{ if eq .State "running" }}
55+ <div class="badge badge-success badge-sm">Running</div>
56+ {{ else if eq .State "exited" }}
57+ <div class="badge badge-warning badge-sm">Stopped</div>
58+ {{ else }}
59+ <div class="badge badge-ghost badge-sm">{{ .State }}</div>
60+ {{ end }}
61+ </td>
62+ <td>{{ .Created }}</td>
63+ <th>
64+ <div class="dropdown dropdown-end">
65+ <div tabindex="0" role="button" class="btn btn-ghost btn-xs">
66+ <span class="iconf">󰅀</span>
67+ </div>
68+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-40">
69+ {{ if eq .State "running" }}
70+ <li>
71+ <button
72+ class="text-warning flex items-center"
73+ hx-post="/htmx/container-manager/stop/{{ .ID }}"
74+ hx-target="#container-manager"
75+ hx-swap="innerHTML">
76+ <span class="iconf mr-2">󰓛</span> Stop
77+ </button>
78+ </li>
79+ {{ else if eq .State "exited" }}
80+ <li>
81+ <button
82+ class="text-success flex items-center"
83+ hx-post="/htmx/container-manager/start/{{ .ID }}"
84+ hx-target="#container-manager"
85+ hx-swap="innerHTML">
86+ <span class="iconf mr-2">󰐊</span> Start
87+ </button>
88+ </li>
89+ {{ end }}
90+ <li>
91+ <button
92+ class="text-info flex items-center"
93+ hx-get="/htmx/container-manager/edit/{{ .ID }}"
94+ hx-target="#container-manager"
95+ hx-swap="innerHTML">
96+ <span class="iconf mr-2">󰏫</span> Edit
97+ </button>
98+ </li>
99+ <li>
100+ <button
101+ class="text-error flex items-center"
102+ hx-delete="/htmx/container-manager/delete/{{ .ID }}"
103+ hx-target="#container-manager"
104+ hx-swap="innerHTML"
105+ hx-confirm="Are you sure you want to delete this container?">
106+ <span class="iconf mr-2">󰆴</span> Delete
107+ </button>
108+ </li>
109+ </ul>
110+ </div>
111+ </th>
112+ </tr>
113+ {{ end }}
114+ {{ end }}
115+ </tbody>
116+ </table>
117+ </div>
112118 </div>
119+
120+<script>
121+console.debug('Container list rendered');
122+</script>
M internal/templating/models/html/fragments/createcontainer.gohtml +79 -54
@@ -1,9 +1,12 @@
11 <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets">
22 <!-- Left Side: Docker Run Options -->
3- <div class="flex-grow bg-slate-100 p-4 rounded-md">
4- <h2 class="font-bold pb-4">Deploy a container based on {{.ImageName}}</h2>
3+ <div class="flex-grow card p-6">
4+ <h2 class="card-title pb-4">Deploy a container based on {{.ImageName}}</h2>
55 {{if .ErrorMessage}}
6- <div class="text-red-500">{{.ErrorMessage}}</div>
6+ <div class="alert alert-error">
7+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
8+ <span>{{.ErrorMessage}}</span>
9+ </div>
710 {{ end }}
811 <form id="create-container-form-{{.ShortID}}" action="/create-container/{{.ShortID}}" method="post"
912 hx-post="/htmx/create-container/{{.ShortID}}"
@@ -11,83 +14,104 @@
1114 hx-target="#create-container-{{.ShortID}}"
1215 hx-target-*="#error-message-create-container-{{.ShortID}}">
1316 <!-- Image Name -->
14- <div class="flex space-x-4 pb-4">
15- <div class="w-1/3">
16- <label class="block">Image name</label>
17- <input type="text" name="image_name" class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
17+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4">
18+ <div class="md:col-span-1">
19+ <label class="label">
20+ <span class="label-text">Image name</span>
21+ </label>
22+ <input type="text" name="image_name" class="input input-bordered input-disabled w-full"
1823 value="{{.ImageName}}" readonly />
1924 </div>
2025
21- <div class="w-2/3">
22- <label class="block">Image ID</label>
23- <input type="text" name="image_id" class="w-full p-4 rounded-md bg-gray-100 border-gray-300 border-2"
26+ <div class="md:col-span-2">
27+ <label class="label">
28+ <span class="label-text">Image ID</span>
29+ </label>
30+ <input type="text" name="image_id" class="input input-bordered input-disabled w-full"
2431 value="{{.ImageID}}" readonly />
2532 </div>
2633 </div>
2734
2835 <!-- Container Name -->
29- <div class="pb-4">
30- <label class="block">Container name</label>
31- <input type="text" name="container_name" class="w-full p-4 rounded-md" value="{{.ContainerName}}" />
36+ <div class="form-control pb-4">
37+ <label class="label">
38+ <span class="label-text">Container name</span>
39+ </label>
40+ <input type="text" name="container_name" class="input input-bordered w-full" value="{{.ContainerName}}" />
3241 </div>
42+
3343 <!-- Http(s) / Subdomain / Domain -->
34- <div class="flex pb-4 space-x-4">
44+ <div class="grid grid-cols-1 md:grid-cols-12 gap-4 pb-4">
3545 <!-- Protocol -->
36- <div class="w-1/4">
37- <label class="block">Protocol</label>
38- <select name="container_protocol" class="w-full p-4 rounded-md border-2 bg-slate-200 border-gray-300">
46+ <div class="md:col-span-3">
47+ <label class="label">
48+ <span class="label-text">Protocol</span>
49+ </label>
50+ <select name="container_protocol" class="select select-bordered w-full">
3951 <option value="https">https</option>
4052 <option value="http">http</option>
4153 </select>
4254 </div>
4355
4456 <!-- Subdomain -->
45- <div class="w-1/3">
46- <label class="block">Subdomain</label>
47- <input type="text" name="container_subdomain" class="w-full p-4 rounded-md" value="{{.ContainerSubdomain}}" />
57+ <div class="md:col-span-4">
58+ <label class="label">
59+ <span class="label-text">Subdomain</span>
60+ </label>
61+ <input type="text" name="container_subdomain" class="input input-bordered w-full" value="{{.ContainerSubdomain}}" />
4862 </div>
4963
5064 <!-- Domain -->
51- <div class="w-5/12">
52- <label class="block">Domain</label>
53- <input type="text" name="container_domain" class="w-full p-4 rounded-md" value="{{.ContainerDomain}}" />
65+ <div class="md:col-span-5">
66+ <label class="label">
67+ <span class="label-text">Domain</span>
68+ </label>
69+ <input type="text" name="container_domain" class="input input-bordered w-full" value="{{.ContainerDomain}}" />
5470 </div>
5571 </div>
72+
5673 <!-- Exposed Ports -->
57- <div class="pb-4">
58- <label class="block">Exposed Ports (Optional)</label>
59- <span class="text-gray-500">
60- eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)
61- </span>
62- <input type="text" name="ports" class="w-full p-4 rounded-md" value="{{.Ports}}" />
74+ <div class="form-control pb-4">
75+ <label class="label">
76+ <span class="label-text">Exposed Ports (Optional)</span>
77+ <span class="label-text-alt">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span>
78+ </label>
79+ <input type="text" name="ports" class="input input-bordered w-full" value="{{.Ports}}" />
6380 </div>
6481
6582 <!-- Proxy Port -->
66- <div class="mb-6">
67- <label class="block">Container Proxy Port (Required)</label>
68- <div class="text-xs text-gray-500 mb-2">
69- Specify the internal container port that the proxy will route traffic to
70- </div>
71- <input type="text" name="container_port" class="w-full p-4 rounded-md" value="{{.ProxyPort}}" required />
83+ <div class="form-control pb-4">
84+ <label class="label">
85+ <span class="label-text">Container Proxy Port (Required)</span>
86+ <span class="label-text-alt">Specify the internal container port that the proxy will route traffic to</span>
87+ </label>
88+ <input type="text" name="container_port" class="input input-bordered w-full" value="{{.ProxyPort}}" required />
7289 </div>
7390
7491 <!-- Volumes -->
75- <div class="pb-4">
76- <label class="block">Volumes</label>
77- <span class="text-gray-500">e.g. ./data:/container/data </span>
78- <input type="text" name="volumes" class="w-full p-4 rounded-md" value="{{.Volumes}}" />
92+ <div class="form-control pb-4">
93+ <label class="label">
94+ <span class="label-text">Volumes</span>
95+ <span class="label-text-alt">e.g. ./data:/container/data</span>
96+ </label>
97+ <input type="text" name="volumes" class="input input-bordered w-full" value="{{.Volumes}}" />
7998 </div>
8099
81- <div class="pb-4">
82- <label class="block">Environment variables</label>
83- <span class="text-gray-500">e.g. KEY=VALUE </span>
84- <textarea name="environment_variables" class="w-full p-4 rounded-md"
100+ <div class="form-control pb-4">
101+ <label class="label">
102+ <span class="label-text">Environment variables</span>
103+ <span class="label-text-alt">e.g. KEY=VALUE</span>
104+ </label>
105+ <textarea name="environment_variables" class="textarea textarea-bordered w-full"
85106 rows="4">{{.EnvironmentVariables}}</textarea>
86107 </div>
108+
87109 <!-- Restart: always/on-failure/only when stopped/never -->
88- <div class="pb-4">
89- <label class="block">Restart</label>
90- <select name="restart" class="w-full p-4 rounded-md border-2 bg-gray-100 border-gray-300">
110+ <div class="form-control pb-4">
111+ <label class="label">
112+ <span class="label-text">Restart</span>
113+ </label>
114+ <select name="restart" class="select select-bordered w-full">
91115 <option value="always">Always</option>
92116 <option value="on-failure">On failure</option>
93117 <option value="unless-stopped">Unless stopped</option>
@@ -96,20 +120,21 @@
96120 </div>
97121
98122 <!-- Skip Proxy Setup Option -->
99- <div class="pb-4">
100- <label class="flex items-center space-x-2">
101- <input type="checkbox" name="skip_proxy_setup" value="true" class="form-checkbox h-5 w-5 text-blue-600" />
123+ <div class="form-control pb-4">
124+ <label class="label cursor-pointer justify-start gap-4">
125+ <input type="checkbox" name="skip_proxy_setup" value="true" class="checkbox checkbox-primary" />
102126 <span>Skip proxy setup (create container only, don't set up domain/certificates)</span>
103127 </label>
104- <p class="text-sm text-gray-500 ml-7">
128+ <div class="text-sm opacity-70 ml-10">
105129 Use this option if you only want to create the container without triggering proxy setup,
106130 certificate generation, and other related processes.
107- </p>
131+ </div>
108132 </div>
109133
110- <input type="submit" value="Create" id="create-container-submit"
111- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
134+ <button type="submit" id="create-container-submit" class="btn btn-primary">
135+ Create Container
136+ </button>
112137 </form>
113- <div id="error-message-create-container-{{.ShortID}}" class="text-red-500"></div>
138+ <div id="error-message-create-container-{{.ShortID}}" class="text-error mt-4"></div>
114139 </div>
115140 </div>⏎̸
M internal/templating/models/html/fragments/editcontainer.gohtml +12 -10
@@ -1,9 +1,9 @@
11 <div
22 id="edit-container-{{.ID}}"
3- class="h-fit-content"
3+ class="card bg-base-100 shadow-xl"
44 hx-ext="response-targets"
55 >
6- <div id="editor">
6+ <div id="editor" class="card-body">
77 <form
88 method="post"
99 action="/htmx/container-manager/edit/{{.ID}}"
@@ -14,17 +14,19 @@
1414 <textarea
1515 name="container_config"
1616 id="container_config"
17- class="w-full p-4 rounded-md bg-slate-100 border-gray-300 border-2"
17+ class="textarea textarea-bordered w-full h-96 font-mono JetBrainsMonoNF"
1818 rows="20"
1919 >{{.ContainerInfo}}</textarea
2020 >
21- <input
22- type="submit"
23- value="Save"
24- id="save-button-{{.ID}}"
25- class="bg-gordonmascot-lblue text-white font-bold px-8 py-1 rounded-md hover:bg-gordonmascot-mblue cursor-pointer"
26- />
21+ <div class="card-actions justify-end mt-4">
22+ <input
23+ type="submit"
24+ value="Save"
25+ id="save-button-{{.ID}}"
26+ class="btn btn-primary"
27+ />
28+ </div>
2729 </form>
28- <div id="error-message-edit-container-{{.ID}}" class="text-red-500"></div>
30+ <div id="error-message-edit-container-{{.ID}}" class="text-error mt-2"></div>
2931 </div>
3032 </div>
M internal/templating/models/html/fragments/footer.gohtml +33 -0
@@ -0,0 +1,33 @@
1+<footer class="footer p-10 bg-base-200 text-base-content">
2+ <div>
3+ <span class="footer-title">Gordon</span>
4+ <a class="link link-hover">About us</a>
5+ <a class="link link-hover">Documentation</a>
6+ <a class="link link-hover">Github</a>
7+ </div>
8+ <div>
9+ <span class="footer-title">Container Management</span>
10+ <a class="link link-hover">Dashboard</a>
11+ <a class="link link-hover">Settings</a>
12+ <a class="link link-hover">Stats</a>
13+ </div>
14+ <div>
15+ <span class="footer-title">Legal</span>
16+ <a class="link link-hover">Terms of use</a>
17+ <a class="link link-hover">Privacy policy</a>
18+ <a class="link link-hover">License</a>
19+ </div>
20+</footer>
21+<footer class="footer px-10 py-4 border-t bg-base-300 text-base-content border-base-300">
22+ <div class="items-center grid-flow-col">
23+ <span class="iconfb text-2xl">󰊢</span>
24+ <p class="IBMPlexSans">Gordon Container Manager<br/>Providing reliable container management since 2023</p>
25+ </div>
26+ <div class="md:place-self-center md:justify-self-end">
27+ <div class="grid grid-flow-col gap-4">
28+ <a class="iconfb text-2xl">󰊤</a>
29+ <a class="iconfb text-2xl">󰗃</a>
30+ <a class="iconfb text-2xl">󰖟</a>
31+ </div>
32+ </div>
33+</footer>
M internal/templating/models/html/fragments/header.gohtml +10 -0
@@ -11,4 +11,14 @@
1111 <script src="/libs/htmx-sse.js"></script>
1212 <script defer src="/libs/hyperscript.min.js"></script>
1313 <script defer src="/assets/js/custom.js"></script>
14+
15+ <!-- daisyUI theme configuration -->
16+ <script>
17+ // Check for saved theme preference or use preferred color scheme
18+ const savedTheme = localStorage.getItem('theme') || 'gordon';
19+ document.documentElement.setAttribute('data-theme', savedTheme);
20+
21+ // Log theme loading for debugging
22+ console.debug('Theme loaded:', savedTheme);
23+ </script>
1424 </head>
M internal/templating/models/html/fragments/imagelist.gohtml +95 -79
@@ -1,82 +1,98 @@
1-<div class="flex flex-col p-4">
2- <!-- Upload button aligned right of Images -->
3- <div class="flex justify-between items-center mb-1">
4- <h1 class="text-lg md:text-xl font-bold text-gordonmascot-hblue">Images</h1>
5- <div class="flex space-x-2">
6- <button
7- id="refresh-images-button"
8- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
9- title="Refresh image list"
10- >
11- <span class="iconf text-xl align-middle">󰑐 </span>
12- Refresh
13- </button>
14- <button
15- class="py-1 px-2 bg-blue-500 hover:bg-blue-700 text-white font-bold rounded"
16- id="upload-button"
17- hx-get="/htmx/upload-image"
18- hx-target="#upload-image"
19- hx-swap="innerHTML"
20- title="Manually upload a .tar container image"
21- >
22- <span class="iconf text-xl align-middle">󰭾 </span>
23- Upload
24- </button>
1+<!-- Image list using daisyUI components -->
2+<div class="card w-full bg-base-300">
3+ <div class="card-body p-4">
4+
5+ <!-- Action buttons -->
6+ <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
7+ <h3 class="font-semibold text-base-content">Available Images: {{ len .Images }}</h3>
8+ <div class="flex gap-2">
9+ <button
10+ class="btn btn-sm btn-secondary"
11+ hx-get="/htmx/upload-image"
12+ hx-target="#image-manager"
13+ hx-swap="innerHTML">
14+ <span class="iconf">󰦒</span> Upload
15+ </button>
16+ <button
17+ class="btn btn-sm btn-primary"
18+ hx-get="/htmx/image-manager"
19+ hx-target="#image-manager"
20+ hx-swap="innerHTML">
21+ <span class="iconf">󰑓</span> Refresh
22+ </button>
23+ </div>
2524 </div>
26- </div>
27- <div id="upload-image"></div>
28- <table class="p-4 border-t-2">
29- <tbody>
30- {{
31- range.Images
32- }}
33- <tr id="img-{{.ShortID}}" hx-ext="response-targets" class=" hover:bg-slate-100">
34- <td>
35- <span title="{{.ShortID}}">{{.Name}}</span>
36- </td>
37- <td>
38- <span title="Image creation date">{{.CreatedStr}}</span>
39- </td>
40- <td>{{.SizeStr}}</td>
41- <td align="right">
42-
43- <button
44- id="add-button-img-{{.ShortID}}"
45- hx-get="/htmx/create-container/{{.ShortID}}"
46- hx-target="#create-container-{{.ShortID}}"
47- hx-target-*="#error-message-div-{{.ShortID}}"
48- hx-swap="innerHTML"
49- class="py-2 px-4"
50- >
51- <span title="Deploy a container using this image" class="iconf text-xl text-blue-600 hover:text-gordonmascot-hblue">󰆍</span>
52- </button>
5325
54- <button
55- id="remove-button-img-{{.ShortID}}"
56- hx-delete="/htmx/image-manager/delete/{{.ShortID}}"
57- hx-target="#img-{{.ShortID}}"
58- hx-target-*="#error-message-div-{{.ShortID}}"
59- hx-swap="innerHTML"
60- hx-confirm="Are you sure you want to delete this container image?"
61- class="py-2 px-4"
62- >
63- <span title="Remove this image" class="iconf text-xl text-red-600 hover:text-red-900"></span>
64- </button>
65- </td>
66- </tr>
67- <t>
68- <td colspan="4">
69- <div id="error-message-div-{{.ShortID}}" class="text-red-500"></div>
70- <div class="create-container" id="create-container-{{.ShortID}}"></div>
71- </td>
72- </tr>
73- {{ else }}
74- <tr>
75- <td colspan="4" class="mt-2">No images found.</td>
76- </tr>
77- {{
78- end
79- }}
80- </tbody>
81- </table>
26+ <!-- Image list -->
27+ <table class="table">
28+ <!-- head -->
29+ <thead>
30+ <tr>
31+ <th>Repository</th>
32+ <th>Tag</th>
33+ <th>Size</th>
34+ <th>Created</th>
35+ <th>Actions</th>
36+ </tr>
37+ </thead>
38+ <tbody>
39+ {{ if .NoImage }}
40+ <tr>
41+ <td colspan="5" class="text-center py-4">
42+ <div class="alert alert-info">
43+ <span class="iconf">ℹ️</span>
44+ <span>No images found. Upload an image first.</span>
45+ </div>
46+ </td>
47+ </tr>
48+ {{ else }}
49+ {{ range .Images }}
50+ <tr class="hover">
51+ <td>
52+ <div class="flex items-center gap-3">
53+ <div>
54+ <div class="font-bold">{{ .Repository }}</div>
55+ <div class="text-sm opacity-50">{{ .ID }}</div>
56+ </div>
57+ </div>
58+ </td>
59+ <td>{{ .Tag }}</td>
60+ <td>{{ .Size }}</td>
61+ <td>{{ .Created }}</td>
62+ <th>
63+ <div class="dropdown dropdown-end">
64+ <div tabindex="0" role="button" class="btn btn-ghost btn-xs">
65+ <span class="iconf">󰅀</span>
66+ </div>
67+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
68+ <li>
69+ <a
70+ href="/htmx/create-container/{{ if gt (len .ID) 12 }}{{ slice .ID 0 12 }}{{ else }}{{ .ID }}{{ end }}"
71+ class="text-success flex items-center">
72+ <span class="iconf mr-2">󰐕</span> Create Container
73+ </a>
74+ </li>
75+ <li>
76+ <button
77+ class="text-error flex items-center"
78+ hx-delete="/htmx/image-manager/delete/{{ .ID }}"
79+ hx-target="#image-manager"
80+ hx-swap="innerHTML"
81+ hx-confirm="Are you sure you want to delete this image?">
82+ <span class="iconf mr-2">󰆴</span> Delete
83+ </button>
84+ </li>
85+ </ul>
86+ </div>
87+ </th>
88+ </tr>
89+ {{ end }}
90+ {{ end }}
91+ </tbody>
92+ </table>
93+ </div>
8294 </div>
95+
96+<script>
97+console.debug('Image list rendered');
98+</script>
M internal/templating/models/html/fragments/menu.gohtml +61 -39
@@ -1,45 +1,67 @@
1-<div
2- id="menubar"
3- class="w-full h-12 md:h-16 fixed top-0 left-0 bg-gordonmascot-lblue flex flex-row justify-evenly items-center px-4 IBMPlexSans">
4- <div class="w-full flex justify-start">
5- <h1
6- class="gordon-title text-3xl md:text-4xl text-gordonmascot-hblue tracking-tighter">
7- Gordon
8- </h1>
9- <span
10- id="actual-version"
11- class="text-gordonmascot-mblue text-xs md:text-sm pr-1 JetBrainsMonoNF">
12- {{ .BuildVersion }}
13- </span>
14- <span
15- hidden
16- id="update-available"
17- class="text-green-300 text-xs md:text-sm JetBrainsMonoNF hidden"
18- title=""
19- ><a href="https://github.com/bnema/gordon/releases">󰨦</a>
20- </span>
1+<!-- DaisyUI Navbar -->
2+<div class="navbar bg-base-100 shadow-lg fixed top-0 z-50">
3+ <div class="navbar-start">
4+ <div class="flex items-center">
5+ <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4">
6+ Gordon
7+ </h1>
8+ <span id="actual-version" class="text-xs md:text-sm pr-1 JetBrainsMonoNF text-base-content/70">
9+ {{ .BuildVersion }}
10+ </span>
11+ <span hidden id="update-available" class="text-success text-xs md:text-sm JetBrainsMonoNF hidden" title="">
12+ <a href="https://github.com/bnema/gordon/releases" class="link">󰨦</a>
13+ </span>
14+ </div>
2115 </div>
22- <div class="w-full">
23- <ul
24- class="flex flex-row space-y-6 font-semibold text-gray-100 text-lg md:text-xl justify-center">
25- <li class="">
26- <span class="JetBrainsMonoBoldNF pr-1"> </span>
27- <a href="{{ .AdminPath }}/manager">Manager</a>
16+ <div class="navbar-center hidden lg:flex">
17+ <ul class="menu menu-horizontal px-1">
18+ <li>
19+ <a href="{{ .AdminPath }}/manager" class="font-semibold">
20+ <span class="JetBrainsMonoBoldNF pr-1">󰘦</span> Manager
21+ </a>
2822 </li>
2923 </ul>
3024 </div>
31- <div class="w-full flex space-x-4 justify-end">
32- <a
33- href="{{ .UserSettingsURL }}"
34- title="User settings"
35- class="iconf text-gordonmascot-mblue text-lg md:text-xl"
36- ></a
37- >
38- <a
39- href="{{ .LogoutURL }}"
40- title="Logout"
41- class="iconf text-gordonmascot-mblue text-lg md:text-xl"
42- >󰍃</a
43- >
25+ <div class="navbar-end">
26+ <!-- Theme toggle -->
27+ <div class="dropdown dropdown-end">
28+ <label tabindex="0" class="btn btn-ghost btn-circle">
29+ <span class="iconf text-lg md:text-xl">󰔎</span>
30+ </label>
31+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
32+ <li><button onclick="setTheme('gordon')">Gordon Light</button></li>
33+ <li><button onclick="setTheme('gordon-dark')">Gordon Dark</button></li>
34+ <li><button onclick="setTheme('light')">Light</button></li>
35+ <li><button onclick="setTheme('dark')">Dark</button></li>
36+ </ul>
37+ </div>
38+
39+ <!-- User menu -->
40+ <div class="dropdown dropdown-end">
41+ <label tabindex="0" class="btn btn-ghost btn-circle">
42+ <span class="iconf text-lg md:text-xl">󰀉</span>
43+ </label>
44+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
45+ <li>
46+ <a href="{{ .UserSettingsURL }}" title="User settings">
47+ <span class="iconf">󰒓</span> Settings
48+ </a>
49+ </li>
50+ <li>
51+ <a href="{{ .LogoutURL }}" title="Logout">
52+ <span class="iconf">󰍃</span> Logout
53+ </a>
54+ </li>
55+ </ul>
56+ </div>
4457 </div>
4558 </div>
59+
60+<!-- Theme toggle script -->
61+<script>
62+ function setTheme(theme) {
63+ document.documentElement.setAttribute('data-theme', theme);
64+ localStorage.setItem('theme', theme);
65+ console.debug('Theme changed to:', theme);
66+ }
67+</script>
M internal/templating/models/html/fragments/success.gohtml +1 -1
@@ -1,5 +1,5 @@
11 <div
22 id="success"
3- class="flex text-blue-800 content-center justify-center items-center w-full h-full">
3+ class="flex text-indigo-600 content-center justify-center items-center w-full h-full">
44 <p class="text-2xl text-center">{{if .Message}}{{.Message}}{{else}}Success!{{end}}</p>
55 </div>
M internal/templating/models/html/fragments/uploadimage.gohtml +53 -27
@@ -1,35 +1,61 @@
1-<!-- Form to upload a container image (.tar format) -->
2-<div
3- class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"
4- hx-ext="response-targets"
5- hx-boost="true">
6- <div class="flex-grow bg-slate-100 p-4 rounded-md">
7- <h2>Upload Image Containers</h2>
8- <!-- Upload Form -->
1+<!-- Upload image form using DaisyUI components -->
2+<div class="card bg-base-100 shadow">
3+ <div class="card-body">
4+ <div class="flex justify-between items-center">
5+ <h2 class="card-title text-primary">Upload Image</h2>
6+ <button
7+ class="btn btn-sm btn-circle btn-ghost"
8+ hx-get="/htmx/image-manager"
9+ hx-target="#image-manager"
10+ hx-swap="innerHTML">
11+ <span class="iconf text-lg">✕</span>
12+ </button>
13+ </div>
14+ <div class="divider"></div>
15+
916 <form
10- action="/upload"
11- method="post"
12- enctype="multipart/form-data"
17+ id="upload-form"
18+ hx-encoding="multipart/form-data"
1319 hx-post="/htmx/upload-image"
14- hx-swap="innerHTML"
1520 hx-target="#image-manager"
16- hx-target-*="#error-message-div">
17- <div
18- class="upload-zone w-full h-44 bg-slate-200 rounded-md p-4"
19- hx-drop="true">
20- Drag & Drop your file here or
21- <input
22- type="file"
23- name="file"
21+ hx-swap="innerHTML"
22+ class="space-y-4">
23+
24+ <div class="alert alert-info shadow-lg mb-4">
25+ <span class="iconf">ℹ️</span>
26+ <div>
27+ <p>This form allows you to upload a container image (.tar file) to Gordon.</p>
28+ <p>After uploading, you can create and manage containers from this image.</p>
29+ </div>
30+ </div>
31+
32+ <div class="form-control w-full">
33+ <label class="label">
34+ <span class="label-text">Select a container image file (.tar)</span>
35+ </label>
36+ <input
37+ type="file"
38+ name="imageFile"
39+ class="file-input file-input-bordered w-full"
2440 accept=".tar"
25- class="bg-transparent outline-none" />
41+ required />
42+ </div>
43+
44+ <div class="card-actions justify-end mt-4">
45+ <button
46+ class="btn btn-ghost"
47+ hx-get="/htmx/image-manager"
48+ hx-target="#image-manager"
49+ hx-swap="innerHTML"
50+ type="button">
51+ Cancel
52+ </button>
53+ <button
54+ class="btn btn-primary"
55+ type="submit">
56+ <span class="iconf mr-1">󰦒</span> Upload
57+ </button>
2658 </div>
27- <input
28- type="submit"
29- value="Upload"
30- id="upload-submit-button"
31- class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" />
3259 </form>
33- <div id="error-message-div" class="text-red-500"></div>
3460 </div>
3561 </div>
M internal/templating/models/html/login/index.gohtml +79 -63
@@ -1,5 +1,5 @@
11 <!doctype html>
2-<html lang="en">
2+<html lang="en" data-theme="corporate">
33 <head>
44 <meta charset="UTF-8" />
55 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,95 +12,111 @@
1212 <script src="/libs/htmx.min.js"></script>
1313 <!-- HTMX SSE -->
1414 <script src="/libs/htmx-sse.js"></script>
15+ <!-- DaisyUI -->
16+ <script src="/libs/daisyui.js"></script>
1517 <!-- Custom JS from assets/js -->
1618 <script src="/assets/js/custom.js"></script>
19+
20+ <!-- daisyUI theme configuration -->
21+ <script>
22+ // Check for saved theme preference or use preferred color scheme
23+ const savedTheme = localStorage.getItem('theme') || 'corporate';
24+ document.documentElement.setAttribute('data-theme', savedTheme);
25+ console.debug('Theme loaded:', savedTheme);
26+ </script>
1727 </head>
1828
19- <body class="IBMPlexSans bg-zinc-200 md:p-8 flex flex-col items-center">
29+ <body class="IBMPlexSans bg-base-200 min-h-screen flex flex-col items-center justify-center p-4">
2030 <!-- Logo and Title -->
2131 <div
2232 id="logo-mascot"
2333 class="flex flex-col justify-center items-center p-4 mb-4">
24- <div class="circle-shadow flex justify-center w-100">
34+ <div class="flex justify-center w-100">
2535 <img
2636 src="/assets/imgs/gordon-mascot-mq-trsp.png"
2737 alt="logo gordon"
28- class="sm:h-24 h-32 xl:h-48" />
38+ class="sm:h-24 h-32 xl:h-48 drop-shadow-xl" />
2939 </div>
3040 <div class="flex justify-center mt-4">
3141 <h1
32- class="gordon-title text-4xl lg:text-4xl xl:text-5xl text-gordonmascot-hblue tracking-tighter">
42+ class="gordon-title text-4xl lg:text-4xl xl:text-5xl text-primary tracking-tighter">
3343 Gordon
3444 </h1>
3545 <span
36- class="gordon-v-subtitle text-gordonmascot-mblue text-sm ml-2"
46+ class="gordon-v-subtitle text-secondary text-sm ml-2"
3747 >{{ .BuildVersion }}</span
3848 >
3949 </div>
4050 </div>
4151
4252 <!-- Main Content -->
43- <div class="bg-white p-4 rounded-md shadow mb-4 w-2/3">
44- <!-- Login Section -->
45- <div id="login-section">
46- <div class="flex flex-col items-center">
47- <h2 class="text-2xl font-semibold text-gray-800">Login</h2>
48-
49- <!-- Error Message Display -->
50- {{ if .Error }}
51- <div class="mt-4 w-full max-w-md bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
52- <span class="block sm:inline">
53- {{ if eq .Error "empty_token" }}
54- Please enter a token.
55- {{ else if eq .Error "invalid_token" }}
56- Invalid token. Please try again.
57- {{ else if eq .Error "server_error" }}
58- A server error occurred. Please try again later.
59- {{ else }}
60- An error occurred. Please try again.
61- {{ end }}
62- </span>
63- </div>
64- {{ end }}
65-
66- <!-- Token Input Form -->
67- <div class="mt-4 w-full max-w-md">
68- <form action="login/submit-token" method="POST" class="mb-4">
69- <div class="mb-2">
70- <label for="token" class="block text-sm font-medium text-gray-700">Admin Token</label>
71- <input
72- type="text"
73- id="token"
74- name="token"
75- placeholder="Enter your admin token"
76- class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-gordonmascot-hblue focus:border-gordonmascot-hblue"
77- required
78- />
79- </div>
80- <button
81- type="submit"
82- class="w-full bg-gordonmascot-mblue hover:bg-gordonmascot-hblue text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
53+ <div class="card bg-base-100 shadow-xl w-full max-w-md">
54+ <div class="card-body">
55+ <!-- Login Section -->
56+ <div id="login-section">
57+ <div class="flex flex-col items-center">
58+ <h2 class="card-title text-center mb-4">Login</h2>
59+
60+ <!-- Error Message Display -->
61+ {{ if .Error }}
62+ <div class="alert alert-error mb-4">
63+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
64+ <span>
65+ {{ if eq .Error "empty_token" }}
66+ Please enter a token.
67+ {{ else if eq .Error "invalid_token" }}
68+ Invalid token. Please try again.
69+ {{ else if eq .Error "server_error" }}
70+ A server error occurred. Please try again later.
71+ {{ else }}
72+ An error occurred. Please try again.
73+ {{ end }}
74+ </span>
75+ </div>
76+ {{ end }}
77+
78+ <!-- Token Input Form -->
79+ <div class="w-full">
80+ <form action="login/submit-token" method="POST" class="mb-4">
81+ <div class="form-control mb-4">
82+ <label for="token" class="label">
83+ <span class="label-text">Admin Token</span>
84+ </label>
85+ <input
86+ type="text"
87+ id="token"
88+ name="token"
89+ placeholder="Enter your admin token"
90+ class="input input-bordered w-full"
91+ required
92+ />
93+ </div>
94+ <button
95+ type="submit"
96+ class="btn btn-primary w-full"
97+ >
98+ Continue with Token
99+ </button>
100+ </form>
101+ <div class="divider">OR</div>
102+ </div>
103+
104+ <!-- GitHub OAuth -->
105+ <div class="w-full">
106+ <a
107+ href="login/oauth/github"
108+ class="btn btn-secondary w-full"
83109 >
84- Continue with Token
85- </button>
86- </form>
87- <div class="text-center text-sm text-gray-500 mb-4">- OR -</div>
88- </div>
89-
90- <!-- GitHub OAuth -->
91- <div
92- class="mt-2 p-2 bg-gordonmascot-hblue text-white rounded-md flex justify-left">
93- <a href="login/oauth/github" class="flex items-center">
94- <img
95- src="/assets/imgs/github/github-mark-white.png"
96- alt="logo github"
97- class="h-8 xl:h-12" />
98- <span class="w-full p-2 text-white">Login with GitHub</span>
99- </a>
110+ <svg class="mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
111+ <path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" />
112+ </svg>
113+ Sign in with GitHub
114+ </a>
115+ </div>
100116 </div>
101117 </div>
118+ <!-- End Login Section -->
102119 </div>
103- <!-- End Login Section -->
104120 </div>
105121 </body>
106122 </html>
A internal/templating/models/templ/README.md +48 -0
@@ -0,0 +1,48 @@
1+# Templ Templates for Gordon
2+
3+This directory contains [templ](https://github.com/a-h/templ) templates for the Gordon application. We're migrating from Go's html/template to templ for improved type safety and maintainability.
4+
5+## Directory Structure
6+
7+- `components/` - Reusable UI components
8+- `layouts/` - Page layouts and base templates
9+- `pages/` - Full page templates
10+ - `admin/` - Admin pages
11+ - `login/` - Login pages
12+
13+## Working with Templates
14+
15+To create or modify templates:
16+
17+1. Edit the `.templ` files in the appropriate directory
18+2. Run `templ generate` or `make build-templ` to generate Go code
19+3. Use the templates in your handlers with the `TemplRenderer`
20+
21+## Converting from gohtml
22+
23+When converting from gohtml:
24+
25+1. Create a new `.templ` file
26+2. Copy HTML structure from gohtml
27+3. Replace `{{ .varName }}` with `{ varName }`
28+4. Use templ's control flow syntax like `if`, `for`, etc.
29+5. Use `@` to invoke other components
30+
31+## Example:
32+
33+```templ
34+package components
35+
36+templ Button(label string, classes string) {
37+ <button class={classes}>{ label }</button>
38+}
39+```
40+
41+## Usage in Handlers
42+
43+```go
44+func RenderMyPage(c echo.Context, a *server.App) error {
45+ renderer := render.NewTemplRenderer(a)
46+ return renderer.RenderTempl(c, mypage.PageTemplate("Page Title", someData))
47+}
48+```⏎̸
A internal/templating/models/templ/components/containerlist.templ +132 -0
@@ -0,0 +1,132 @@
1+package components
2+
3+import (
4+ "github.com/docker/docker/api/types"
5+ "strconv"
6+)
7+
8+// ContainerList displays a list of containers
9+templ ContainerList(containers []types.Container, noContainer bool) {
10+ <!-- Container list using daisyUI components -->
11+ <div class="card w-full bg-base-300">
12+ <div class="card-body p-4">
13+
14+ <!-- Action buttons -->
15+ <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
16+ <h3 class="font-semibold text-base-content">Available Containers: { strconv.Itoa(len(containers)) }</h3>
17+ <div class="btn-group">
18+ <button
19+ class="btn btn-sm btn-primary"
20+ hx-get="/htmx/container-manager"
21+ hx-target="#container-manager"
22+ hx-swap="innerHTML">
23+ <span class="iconf">󰑓</span> Refresh
24+ </button>
25+ </div>
26+ </div>
27+
28+ <!-- Container list -->
29+ <table class="table">
30+ <!-- head -->
31+ <thead>
32+ <tr>
33+ <th>Name</th>
34+ <th>Image</th>
35+ <th>Status</th>
36+ <th>Created</th>
37+ <th>Actions</th>
38+ </tr>
39+ </thead>
40+ <tbody>
41+ if noContainer || len(containers) == 0 {
42+ <tr>
43+ <td colspan="5" class="text-center py-4">
44+ <div class="alert alert-info">
45+ <span class="iconf">ℹ️</span>
46+ <span>No containers found. Upload an image first, then create a container.</span>
47+ </div>
48+ </td>
49+ </tr>
50+ } else {
51+ for _, container := range containers {
52+ <tr class="hover">
53+ <td>
54+ <div class="flex items-center gap-3">
55+ <div>
56+ <div class="font-bold">{ container.Names[0] }</div>
57+ <div class="text-sm opacity-50">{ container.ID }</div>
58+ </div>
59+ </div>
60+ </td>
61+ <td>{ container.Image }</td>
62+ <td>
63+ if container.State == "running" {
64+ <div class="badge badge-success badge-sm">Running</div>
65+ } else if container.State == "exited" {
66+ <div class="badge badge-warning badge-sm">Stopped</div>
67+ } else {
68+ <div class="badge badge-ghost badge-sm">{ container.State }</div>
69+ }
70+ </td>
71+ <td>{ strconv.FormatInt(container.Created, 10) }</td>
72+ <th>
73+ <div class="dropdown dropdown-end">
74+ <div tabindex="0" role="button" class="btn btn-ghost btn-xs">
75+ <span class="iconf">󰅀</span>
76+ </div>
77+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-40">
78+ if container.State == "running" {
79+ <li>
80+ <button
81+ class="text-warning flex items-center"
82+ hx-post={ "/htmx/container-manager/stop/" + container.ID }
83+ hx-target="#container-manager"
84+ hx-swap="innerHTML">
85+ <span class="iconf mr-2">󰓛</span> Stop
86+ </button>
87+ </li>
88+ } else if container.State == "exited" {
89+ <li>
90+ <button
91+ class="text-success flex items-center"
92+ hx-post={ "/htmx/container-manager/start/" + container.ID }
93+ hx-target="#container-manager"
94+ hx-swap="innerHTML">
95+ <span class="iconf mr-2">󰐊</span> Start
96+ </button>
97+ </li>
98+ }
99+ <li>
100+ <button
101+ class="text-info flex items-center"
102+ hx-get={ "/htmx/container-manager/edit/" + container.ID }
103+ hx-target="#container-manager"
104+ hx-swap="innerHTML">
105+ <span class="iconf mr-2">󰏫</span> Edit
106+ </button>
107+ </li>
108+ <li>
109+ <button
110+ class="text-error flex items-center"
111+ hx-delete={ "/htmx/container-manager/delete/" + container.ID }
112+ hx-target="#container-manager"
113+ hx-swap="innerHTML"
114+ hx-confirm="Are you sure you want to delete this container?">
115+ <span class="iconf mr-2">󰆴</span> Delete
116+ </button>
117+ </li>
118+ </ul>
119+ </div>
120+ </th>
121+ </tr>
122+ }
123+ }
124+ </tbody>
125+ </table>
126+ </div>
127+ </div>
128+
129+ <script>
130+ console.debug('Container list rendered');
131+ </script>
132+}⏎̸
A internal/templating/models/templ/components/containerlist_templ.go +228 -0 generated file: collapsed
A internal/templating/models/templ/components/createcontainer.templ +160 -0
@@ -0,0 +1,160 @@
1+package components
2+
3+// ContainerFormData holds the data needed for the container creation form
4+type ContainerFormData struct {
5+ ImageName string
6+ ImageID string
7+ ShortID string
8+ ContainerName string
9+ ContainerSubdomain string
10+ ContainerDomain string
11+ Ports string
12+ ProxyPort string
13+ Volumes string
14+ EnvironmentVariables string
15+ ErrorMessage string
16+}
17+
18+// CreateContainerForm displays a form for creating a new container
19+templ CreateContainerForm(data ContainerFormData) {
20+ <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets">
21+ <!-- Left Side: Docker Run Options -->
22+ <div class="flex-grow card p-6">
23+ <h2 class="card-title pb-4">Deploy a container based on { data.ImageName }</h2>
24+ if data.ErrorMessage != "" {
25+ <div class="alert alert-error">
26+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
27+ <span>{ data.ErrorMessage }</span>
28+ </div>
29+ }
30+ <form id={ "create-container-form-" + data.ShortID } action={ templ.SafeURL("/create-container/" + data.ShortID) } method="post"
31+ hx-post={ "/htmx/create-container/" + data.ShortID }
32+ hx-swap="innerHTML"
33+ hx-target={ "#create-container-" + data.ShortID }
34+ hx-target-*={ "#error-message-create-container-" + data.ShortID }>
35+ <!-- Image Name -->
36+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4">
37+ <div class="md:col-span-1">
38+ <label class="label">
39+ <span class="label-text">Image name</span>
40+ </label>
41+ <input type="text" name="image_name" class="input input-bordered input-disabled w-full"
42+ value={ data.ImageName } readonly />
43+ </div>
44+
45+ <div class="md:col-span-2">
46+ <label class="label">
47+ <span class="label-text">Image ID</span>
48+ </label>
49+ <input type="text" name="image_id" class="input input-bordered input-disabled w-full"
50+ value={ data.ImageID } readonly />
51+ </div>
52+ </div>
53+
54+ <!-- Container Name -->
55+ <div class="form-control pb-4">
56+ <label class="label">
57+ <span class="label-text">Container name</span>
58+ </label>
59+ <input type="text" name="container_name" class="input input-bordered w-full" value={ data.ContainerName } />
60+ </div>
61+
62+ <!-- Http(s) / Subdomain / Domain -->
63+ <div class="grid grid-cols-1 md:grid-cols-12 gap-4 pb-4">
64+ <!-- Protocol -->
65+ <div class="md:col-span-3">
66+ <label class="label">
67+ <span class="label-text">Protocol</span>
68+ </label>
69+ <select name="container_protocol" class="select select-bordered w-full">
70+ <option value="https">https</option>
71+ <option value="http">http</option>
72+ </select>
73+ </div>
74+
75+ <!-- Subdomain -->
76+ <div class="md:col-span-4">
77+ <label class="label">
78+ <span class="label-text">Subdomain</span>
79+ </label>
80+ <input type="text" name="container_subdomain" class="input input-bordered w-full" value={ data.ContainerSubdomain } />
81+ </div>
82+
83+ <!-- Domain -->
84+ <div class="md:col-span-5">
85+ <label class="label">
86+ <span class="label-text">Domain</span>
87+ </label>
88+ <input type="text" name="container_domain" class="input input-bordered w-full" value={ data.ContainerDomain } />
89+ </div>
90+ </div>
91+
92+ <!-- Exposed Ports -->
93+ <div class="form-control pb-4">
94+ <label class="label">
95+ <span class="label-text">Exposed Ports (Optional)</span>
96+ <span class="label-text-alt">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span>
97+ </label>
98+ <input type="text" name="ports" class="input input-bordered w-full" value={ data.Ports } />
99+ </div>
100+
101+ <!-- Proxy Port -->
102+ <div class="form-control pb-4">
103+ <label class="label">
104+ <span class="label-text">Container Proxy Port (Required)</span>
105+ <span class="label-text-alt">Specify the internal container port that the proxy will route traffic to</span>
106+ </label>
107+ <input type="text" name="container_port" class="input input-bordered w-full" value={ data.ProxyPort } required />
108+ </div>
109+
110+ <!-- Volumes -->
111+ <div class="form-control pb-4">
112+ <label class="label">
113+ <span class="label-text">Volumes</span>
114+ <span class="label-text-alt">e.g. ./data:/container/data</span>
115+ </label>
116+ <input type="text" name="volumes" class="input input-bordered w-full" value={ data.Volumes } />
117+ </div>
118+
119+ <div class="form-control pb-4">
120+ <label class="label">
121+ <span class="label-text">Environment variables</span>
122+ <span class="label-text-alt">e.g. KEY=VALUE</span>
123+ </label>
124+ <textarea name="environment_variables" class="textarea textarea-bordered w-full"
125+ rows="4">{ data.EnvironmentVariables }</textarea>
126+ </div>
127+
128+ <!-- Restart: always/on-failure/only when stopped/never -->
129+ <div class="form-control pb-4">
130+ <label class="label">
131+ <span class="label-text">Restart</span>
132+ </label>
133+ <select name="restart" class="select select-bordered w-full">
134+ <option value="always">Always</option>
135+ <option value="on-failure">On failure</option>
136+ <option value="unless-stopped">Unless stopped</option>
137+ <option value="never">Never</option>
138+ </select>
139+ </div>
140+
141+ <!-- Skip Proxy Setup Option -->
142+ <div class="form-control pb-4">
143+ <label class="label cursor-pointer justify-start gap-4">
144+ <input type="checkbox" name="skip_proxy_setup" value="true" class="checkbox checkbox-primary" />
145+ <span>Skip proxy setup (create container only, don't set up domain/certificates)</span>
146+ </label>
147+ <div class="text-sm opacity-70 ml-10">
148+ Use this option if you only want to create the container without triggering proxy setup,
149+ certificate generation, and other related processes.
150+ </div>
151+ </div>
152+
153+ <button type="submit" id="create-container-submit" class="btn btn-primary">
154+ Create Container
155+ </button>
156+ </form>
157+ <div id={ "error-message-create-container-" + data.ShortID } class="text-error mt-4"></div>
158+ </div>
159+ </div>
160+} ⏎̸
A internal/templating/models/templ/components/createcontainer_templ.go +283 -0 generated file: collapsed
A internal/templating/models/templ/components/editcontainer.templ +39 -0
@@ -0,0 +1,39 @@
1+package components
2+
3+import t "github.com/a-h/templ"
4+
5+// EditContainer replaces editcontainer.gohtml
6+// It renders a form to edit a container's configuration
7+templ EditContainer(id string, containerInfo string) {
8+ <div
9+ id={ "edit-container-" + id }
10+ class="card bg-base-100 shadow-xl"
11+ hx-ext="response-targets"
12+ >
13+ <div id="editor" class="card-body">
14+ <form
15+ method="post"
16+ action={ t.URL("/htmx/container-manager/edit/" + id) }
17+ hx-post={ string(t.URL("/htmx/container-manager/edit/" + id)) }
18+ hx-swap="innerHTML"
19+ hx-target-*={ "#error-message-edit-container-" + id }
20+ >
21+ <textarea
22+ name="container_config"
23+ id="container_config"
24+ class="textarea textarea-bordered w-full h-96 font-mono JetBrainsMonoNF"
25+ rows="20"
26+ >{ containerInfo }</textarea>
27+ <div class="card-actions justify-end mt-4">
28+ <input
29+ type="submit"
30+ value="Save"
31+ id={ "save-button-" + id }
32+ class="btn btn-primary"
33+ />
34+ </div>
35+ </form>
36+ <div id={ "error-message-edit-container-" + id } class="text-error mt-2"></div>
37+ </div>
38+ </div>
39+} ⏎̸
A internal/templating/models/templ/components/editcontainer_templ.go +131 -0 generated file: collapsed
A internal/templating/models/templ/components/footer.templ +39 -0
@@ -0,0 +1,39 @@
1+package components
2+
3+// Footer is a templ component for the page footer
4+// It replaces footer.gohtml
5+templ Footer() {
6+ <footer class="footer p-10 bg-base-200 text-base-content">
7+ <div>
8+ <span class="footer-title">Gordon</span>
9+ <a class="link link-hover">About us</a>
10+ <a class="link link-hover">Documentation</a>
11+ <a class="link link-hover">Github</a>
12+ </div>
13+ <div>
14+ <span class="footer-title">Container Management</span>
15+ <a class="link link-hover">Dashboard</a>
16+ <a class="link link-hover">Settings</a>
17+ <a class="link link-hover">Stats</a>
18+ </div>
19+ <div>
20+ <span class="footer-title">Legal</span>
21+ <a class="link link-hover">Terms of use</a>
22+ <a class="link link-hover">Privacy policy</a>
23+ <a class="link link-hover">License</a>
24+ </div>
25+ </footer>
26+ <footer class="footer px-10 py-4 border-t bg-base-300 text-base-content border-base-300">
27+ <div class="items-center grid-flow-col">
28+ <span class="iconfb text-2xl">󰊢</span>
29+ <p class="IBMPlexSans">Gordon Container Manager<br/>Providing reliable container management since 2023</p>
30+ </div>
31+ <div class="md:place-self-center md:justify-self-end">
32+ <div class="grid grid-flow-col gap-4">
33+ <a class="iconfb text-2xl">󰊤</a>
34+ <a class="iconfb text-2xl">󰗃</a>
35+ <a class="iconfb text-2xl">󰖟</a>
36+ </div>
37+ </div>
38+ </footer>
39+}⏎̸
A internal/templating/models/templ/components/footer_templ.go +42 -0 generated file: collapsed
A internal/templating/models/templ/components/header.templ +30 -0
@@ -0,0 +1,30 @@
1+package components
2+
3+// Header is a templ component for the page header
4+// It replaces header.gohtml
5+templ Header(title string) {
6+ <head>
7+ <meta charset="UTF-8" />
8+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9+ <title>{ title }</title>
10+ <link rel="stylesheet" href="/assets/css/tailwind.css" />
11+
12+ <script src="/libs/jquery.min.js"></script>
13+ <script src="/libs/htmx.min.js"></script>
14+ <script src="/libs/htmx-response-targets.js"></script>
15+ <script src="/libs/htmx-debug.js"></script>
16+ <script src="/libs/htmx-sse.js"></script>
17+ <script defer src="/libs/hyperscript.min.js"></script>
18+ <script defer src="/assets/js/custom.js"></script>
19+
20+ <!-- daisyUI theme configuration -->
21+ <script>
22+ // Check for saved theme preference or use preferred color scheme
23+ const savedTheme = localStorage.getItem('theme') || 'gordon-dark';
24+ document.documentElement.setAttribute('data-theme', savedTheme);
25+
26+ // Log theme loading for debugging
27+ console.debug('Theme loaded:', savedTheme);
28+ </script>
29+ </head>
30+}⏎̸
A internal/templating/models/templ/components/header_templ.go +55 -0 generated file: collapsed
A internal/templating/models/templ/components/htmx_components.templ +80 -0
@@ -0,0 +1,80 @@
1+package components
2+
3+// HTMXHeader creates the hero header with HTMX attributes
4+templ HTMXHeader(title string, subtitle string) {
5+ <div id="header" class="hero bg-base-200">
6+ <div class="hero-content text-center">
7+ <div class="max-w-md">
8+ <h1 class="text-5xl font-bold gordon-title">
9+ { title }
10+ </h1>
11+ <p class="py-6 IBMPlexSans">{ subtitle }</p>
12+ <button class="btn btn-primary">Get Started</button>
13+ </div>
14+ </div>
15+ </div>
16+}
17+
18+// HTMXAlert creates an alert box that can be loaded with HTMX
19+templ HTMXAlert(message string) {
20+ <div id="hello" class="alert alert-info shadow-lg mt-4 mb-4">
21+ <div>
22+ <span class="iconfb">󰋽</span>
23+ <span class="IBMPlexSans">{ message }</span>
24+ </div>
25+ </div>
26+}
27+
28+// HTMXHeaderContainer creates a container to load the HTMX header
29+templ HTMXHeaderContainer() {
30+ <div
31+ id="header"
32+ hx-get="/htmx"
33+ hx-headers='{"HX-Fragment": "header"}'
34+ hx-trigger="load"
35+ hx-swap="outerHTML"
36+ ></div>
37+}
38+
39+// HTMXAppContainer creates a container to load the HTMX app
40+templ HTMXAppContainer() {
41+ <div
42+ id="app"
43+ hx-get="/htmx"
44+ hx-headers='{"HX-Fragment": "hello"}'
45+ hx-trigger="load"
46+ hx-swap="outerHTML"
47+ ></div>
48+}
49+
50+// StatsCard creates individual stat cards for the dashboard
51+templ StatsCard(title string, value string, description string, icon string) {
52+ <div class="stat bg-base-100 shadow">
53+ <div class="stat-figure text-primary">
54+ <span class="iconfb text-3xl">{ icon }</span>
55+ </div>
56+ <div class="stat-title">{ title }</div>
57+ <div class="stat-value">{ value }</div>
58+ <div class="stat-desc">{ description }</div>
59+ </div>
60+}
61+
62+// StatsCards creates a set of stats cards
63+templ StatsCards() {
64+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-4">
65+ @StatsCard("Containers", "31", "Running containers", "󰡨")
66+ @StatsCard("Images", "42", "Available images", "󱧕")
67+ @StatsCard("Networks", "12", "Container networks", "󰙅")
68+ </div>
69+}
70+
71+// TabsNav creates a navigation tabs component
72+templ TabsNav(activeTab string) {
73+ <div class="tabs tabs-boxed my-4">
74+ <a class={templ.KV("tab", true), templ.KV("tab-active", activeTab == "Overview")}>Overview</a>
75+ <a class={templ.KV("tab", true), templ.KV("tab-active", activeTab == "Containers")}>Containers</a>
76+ <a class={templ.KV("tab", true), templ.KV("tab-active", activeTab == "Images")}>Images</a>
77+ <a class={templ.KV("tab", true), templ.KV("tab-active", activeTab == "Networks")}>Networks</a>
78+ <a class={templ.KV("tab", true), templ.KV("tab-active", activeTab == "Volumes")}>Volumes</a>
79+ </div>
80+}⏎̸
A internal/templating/models/templ/components/htmx_components_templ.go +438 -0 generated file: collapsed
A internal/templating/models/templ/components/imagelist.templ +122 -0
@@ -0,0 +1,122 @@
1+package components
2+
3+import (
4+ "strconv"
5+)
6+
7+// HumanReadableImage represents an image with human-readable attributes
8+type HumanReadableImage struct {
9+ ID string
10+ ShortID string
11+ Name string
12+ Repository string
13+ Tag string
14+ Size string
15+ Created string
16+ RepoDigests []string
17+ RepoTags []string
18+}
19+
20+// ImageList displays a list of images
21+templ ImageList(images []HumanReadableImage) {
22+ <!-- Image list using daisyUI components -->
23+ <div class="card w-full bg-base-300">
24+ <div class="card-body p-4">
25+
26+ <!-- Action buttons -->
27+ <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
28+ <h3 class="font-semibold text-base-content">Available Images: { strconv.Itoa(len(images)) }</h3>
29+ <div class="flex gap-2">
30+ <button
31+ class="btn btn-sm btn-secondary"
32+ hx-get="/htmx/upload-image"
33+ hx-target="#image-manager"
34+ hx-swap="innerHTML">
35+ <span class="iconf">󰦒</span> Upload
36+ </button>
37+ <button
38+ class="btn btn-sm btn-primary"
39+ hx-get="/htmx/image-manager"
40+ hx-target="#image-manager"
41+ hx-swap="innerHTML">
42+ <span class="iconf">󰑓</span> Refresh
43+ </button>
44+ </div>
45+ </div>
46+
47+ <!-- Image list -->
48+ <table class="table">
49+ <!-- head -->
50+ <thead>
51+ <tr>
52+ <th>Repository</th>
53+ <th>Tag</th>
54+ <th>Size</th>
55+ <th>Created</th>
56+ <th>Actions</th>
57+ </tr>
58+ </thead>
59+ <tbody>
60+ /* Check if there are no images */
61+ if len(images) == 0 {
62+ <tr>
63+ <td colspan="5" class="text-center py-4">
64+ <div class="alert alert-info">
65+ <span class="iconf">ℹ️</span>
66+ <span>No images found. Upload an image first.</span>
67+ </div>
68+ </td>
69+ </tr>
70+ } else {
71+ /* List all available images */
72+ for _, img := range images {
73+ <tr class="hover">
74+ <td>
75+ <div class="flex items-center gap-3">
76+ <div>
77+ <div class="font-bold">{ img.Repository }</div>
78+ <div class="text-sm opacity-50">{ img.ID }</div>
79+ </div>
80+ </div>
81+ </td>
82+ <td>{ img.Tag }</td>
83+ <td>{ img.Size }</td>
84+ <td>{ img.Created }</td>
85+ <th>
86+ <div class="dropdown dropdown-end">
87+ <div tabindex="0" role="button" class="btn btn-ghost btn-xs">
88+ <span class="iconf">󰅀</span>
89+ </div>
90+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
91+ <li>
92+ <a
93+ href={ templ.SafeURL("/htmx/create-container/" + img.ShortID) }
94+ class="text-success flex items-center">
95+ <span class="iconf mr-2">󰐕</span> Create Container
96+ </a>
97+ </li>
98+ <li>
99+ <button
100+ class="text-error flex items-center"
101+ hx-delete={ "/htmx/image-manager/delete/" + img.ShortID }
102+ hx-target="#image-manager"
103+ hx-swap="innerHTML"
104+ hx-confirm="Are you sure you want to delete this image?">
105+ <span class="iconf mr-2">󰆴</span> Delete
106+ </button>
107+ </li>
108+ </ul>
109+ </div>
110+ </th>
111+ </tr>
112+ }
113+ }
114+ </tbody>
115+ </table>
116+ </div>
117+ </div>
118+
119+ <script>
120+ console.debug('Image list rendered');
121+ </script>
122+} ⏎̸
A internal/templating/models/templ/components/imagelist_templ.go +175 -0 generated file: collapsed
A internal/templating/models/templ/components/navbar.templ +72 -0
@@ -0,0 +1,72 @@
1+package components
2+
3+// Navbar is a menu component for navigation
4+templ Navbar(buildVersion string, adminPath string, userSettingsURL string, logoutURL string) {
5+ <!-- DaisyUI Navbar -->
6+ <div class="navbar bg-base-100 shadow-lg fixed top-0 z-50">
7+ <div class="navbar-start">
8+ <div class="flex items-center">
9+ <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4">
10+ Gordon
11+ </h1>
12+ <span id="actual-version" class="text-xs md:text-sm pr-1 JetBrainsMonoNF text-base-content/70">
13+ { buildVersion }
14+ </span>
15+ <span hidden id="update-available" class="text-success text-xs md:text-sm JetBrainsMonoNF hidden" title="">
16+ <a href="https://github.com/bnema/gordon/releases" class="link">󰨦</a>
17+ </span>
18+ </div>
19+ </div>
20+ <div class="navbar-center hidden lg:flex">
21+ <ul class="menu menu-horizontal px-1">
22+ <li>
23+ <a href={ templ.URL(adminPath + "/manager") } class="font-semibold">
24+ <span class="JetBrainsMonoBoldNF pr-1">󰘦</span> Manager
25+ </a>
26+ </li>
27+ </ul>
28+ </div>
29+ <div class="navbar-end">
30+ <!-- Theme toggle -->
31+ <div class="dropdown dropdown-end">
32+ <label tabindex="0" class="btn btn-ghost btn-circle">
33+ <span class="iconf text-lg md:text-xl">󰔎</span>
34+ </label>
35+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
36+ <li><button onclick="setTheme('gordon')">Gordon Light</button></li>
37+ <li><button onclick="setTheme('gordon-dark')">Gordon Dark</button></li>
38+ <li><button onclick="setTheme('light')">Light</button></li>
39+ <li><button onclick="setTheme('dark')">Dark</button></li>
40+ </ul>
41+ </div>
42+
43+ <!-- User menu -->
44+ <div class="dropdown dropdown-end">
45+ <label tabindex="0" class="btn btn-ghost btn-circle">
46+ <span class="iconf text-lg md:text-xl">󰀉</span>
47+ </label>
48+ <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
49+ <li>
50+ <a href={ templ.URL(userSettingsURL) } title="User settings">
51+ <span class="iconf">󰒓</span> Settings
52+ </a>
53+ </li>
54+ <li>
55+ <a href={ templ.URL(logoutURL) } title="Logout">
56+ <span class="iconf">󰍃</span> Logout
57+ </a>
58+ </li>
59+ </ul>
60+ </div>
61+ </div>
62+ </div>
63+
64+ <!-- Theme toggle script -->
65+ <script>
66+ function setTheme(theme) {
67+ document.documentElement.setAttribute('data-theme', theme);
68+ localStorage.setItem('theme', theme);
69+ console.debug('Theme changed to:', theme);
70+ }
71+ </script>
72+}⏎̸
A internal/templating/models/templ/components/navbar_templ.go +81 -0 generated file: collapsed
A internal/templating/models/templ/components/success.templ +16 -0
@@ -0,0 +1,16 @@
1+package components
2+
3+// Success displays a success message
4+templ Success(message string) {
5+ <div
6+ id="success"
7+ class="flex text-indigo-600 content-center justify-center items-center w-full h-full">
8+ <p class="text-2xl text-center">
9+ if message != "" {
10+ { message }
11+ } else {
12+ Success!
13+ }
14+ </p>
15+ </div>
16+}⏎̸
A internal/templating/models/templ/components/success_templ.go +61 -0 generated file: collapsed
A internal/templating/models/templ/components/uploadimage.templ +66 -0
@@ -0,0 +1,66 @@
1+package components
2+
3+// UploadImageForm displays a form for uploading Docker images
4+templ UploadImageForm() {
5+ <!-- Upload image form using DaisyUI components -->
6+ <div class="card bg-base-100 shadow">
7+ <div class="card-body">
8+ <div class="flex justify-between items-center">
9+ <h2 class="card-title text-primary">Upload Image</h2>
10+ <button
11+ class="btn btn-sm btn-circle btn-ghost"
12+ hx-get="/htmx/image-manager"
13+ hx-target="#image-manager"
14+ hx-swap="innerHTML">
15+ <span class="iconf text-lg">✕</span>
16+ </button>
17+ </div>
18+ <div class="divider"></div>
19+
20+ <form
21+ id="upload-form"
22+ hx-encoding="multipart/form-data"
23+ hx-post="/htmx/upload-image"
24+ hx-target="#image-manager"
25+ hx-swap="innerHTML"
26+ class="space-y-4">
27+
28+ <div class="alert alert-info shadow-lg mb-4">
29+ <span class="iconf">ℹ️</span>
30+ <div>
31+ <p>This form allows you to upload a container image (.tar file) to Gordon.</p>
32+ <p>After uploading, you can create and manage containers from this image.</p>
33+ </div>
34+ </div>
35+
36+ <div class="form-control w-full">
37+ <label class="label">
38+ <span class="label-text">Select a container image file (.tar)</span>
39+ </label>
40+ <input
41+ type="file"
42+ name="imageFile"
43+ class="file-input file-input-bordered w-full"
44+ accept=".tar"
45+ required />
46+ </div>
47+
48+ <div class="card-actions justify-end mt-4">
49+ <button
50+ class="btn btn-ghost"
51+ hx-get="/htmx/image-manager"
52+ hx-target="#image-manager"
53+ hx-swap="innerHTML"
54+ type="button">
55+ Cancel
56+ </button>
57+ <button
58+ class="btn btn-primary"
59+ type="submit">
60+ <span class="iconf mr-1">󰦒</span> Upload
61+ </button>
62+ </div>
63+ </form>
64+ </div>
65+ </div>
66+} ⏎̸
A internal/templating/models/templ/components/uploadimage_templ.go +41 -0 generated file: collapsed
A internal/templating/models/templ/layouts/admin_layout.templ +21 -0
@@ -0,0 +1,21 @@
1+package layouts
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/components"
4+
5+// AdminLayout provides the base layout for admin pages
6+templ AdminLayout(title string, buildVersion string, adminPath string, userSettingsURL string, logoutURL string) {
7+ <!DOCTYPE html>
8+ <html lang="en" data-theme="gordon-dark">
9+ @components.Header(title)
10+ <body class="min-h-screen bg-base-100">
11+ @components.Navbar(buildVersion, adminPath, userSettingsURL, logoutURL)
12+
13+ <!-- Main content starts after navbar height -->
14+ <div class="container mx-auto pt-20 px-4">
15+ { children... }
16+ </div>
17+
18+ @components.Footer()
19+ </body>
20+ </html>
21+}⏎̸
A internal/templating/models/templ/layouts/admin_layout_templ.go +75 -0 generated file: collapsed
A internal/templating/models/templ/layouts/base.templ +16 -0
@@ -0,0 +1,16 @@
1+package layouts
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/components"
4+
5+// Base layout template that provides the HTML structure
6+// All pages will use this layout template
7+templ Base(title string) {
8+ <!DOCTYPE html>
9+ <html lang="en">
10+ @components.Header(title)
11+ <body class="min-h-screen bg-base-100">
12+ { children... }
13+ @components.Footer()
14+ </body>
15+ </html>
16+}⏎̸
A internal/templating/models/templ/layouts/base_templ.go +64 -0 generated file: collapsed
A internal/templating/models/templ/pages/admin/createcontainerfull.templ +30 -0
@@ -0,0 +1,30 @@
1+package admin
2+
3+import (
4+ "github.com/bnema/gordon/internal/templating/models/templ/layouts"
5+ "github.com/bnema/gordon/internal/templating/models/templ/components"
6+)
7+
8+// CreateContainerFullData represents the data needed for the create container full page
9+type CreateContainerFullData struct {
10+ Title string
11+ BuildVersion string
12+ AdminPath string
13+ UserSettings string
14+ LogoutURL string
15+ ContainerData components.ContainerFormData
16+}
17+
18+// CreateContainerFullPage replaces createcontainerfull.gohtml
19+// It renders the full page for creating a container
20+templ CreateContainerFullPage(data CreateContainerFullData) {
21+ @layouts.AdminLayout(data.Title, data.BuildVersion, data.AdminPath, data.UserSettings, data.LogoutURL) {
22+ <div class="flex flex-col md:flex-row w-full flex-grow">
23+ <div class="card flex-grow">
24+ <h1 class="card-header mb-4">Create Container</h1>
25+ <!-- Include the container creation form component -->
26+ @components.CreateContainerForm(data.ContainerData)
27+ </div>
28+ </div>
29+ }
30+} ⏎̸
A internal/templating/models/templ/pages/admin/createcontainerfull_templ.go +83 -0 generated file: collapsed
A internal/templating/models/templ/pages/admin/dashboard.templ +18 -0
@@ -0,0 +1,18 @@
1+package admin
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/layouts"
4+
5+// DashboardPage implements the admin dashboard
6+templ DashboardPage(title string, buildVersion string, adminPath string, userSettingsURL string, logoutURL string) {
7+ @layouts.AdminLayout(title, buildVersion, adminPath, userSettingsURL, logoutURL) {
8+ <div id="dashboard">
9+ <h1
10+ class="text-5xl fw100 text-indigo-600 animate-bounce-alt animate-count-infinite animate-duration-1s"
11+ >
12+ Hello World from dashboard.html
13+ </h1>
14+ <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded -->
15+ <script src="libs/chart.js"></script>
16+ </div>
17+ }
18+}⏎̸
A internal/templating/models/templ/pages/admin/dashboard_templ.go +61 -0 generated file: collapsed
A internal/templating/models/templ/pages/admin/index.templ +36 -0
@@ -0,0 +1,36 @@
1+package admin
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/layouts"
4+
5+// IndexPageData represents data needed for the index page
6+type IndexPageData struct {
7+ Title string
8+ BuildVersion string
9+ AdminPath string
10+ UserSettings string
11+ LogoutURL string
12+ LangContent map[string]string // For storing language translations
13+}
14+
15+// IndexPage replaces admin/index.gohtml
16+// It renders the admin dashboard index page
17+templ IndexPage(data IndexPageData) {
18+ @layouts.AdminLayout(data.Title, data.BuildVersion, data.AdminPath, data.UserSettings, data.LogoutURL) {
19+ <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow">
20+ <div class="card flex-grow">
21+ <h2 class="card-header">{ data.LangContent["title"] }</h2>
22+ </div>
23+ <div class="w-full md:w-2/6 flex flex-col space-y-4">
24+ <div class="card">
25+ <div class="w-full h-44 rounded-sm bg-indigo-100"></div>
26+ </div>
27+ <div class="card">
28+ <div class="w-full h-44 rounded-sm bg-indigo-100"></div>
29+ </div>
30+ <div class="card">
31+ <div class="w-full h-44 rounded-sm bg-indigo-100"></div>
32+ </div>
33+ </div>
34+ </div>
35+ }
36+} ⏎̸
A internal/templating/models/templ/pages/admin/index_templ.go +85 -0 generated file: collapsed
A internal/templating/models/templ/pages/admin/install.templ +59 -0
@@ -0,0 +1,59 @@
1+package admin
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/layouts"
4+
5+// InstallPage replaces install.gohtml
6+// It renders the installation page
7+templ InstallPage(title string, version string) {
8+ @layouts.Base(title) {
9+ <!-- Logo and Title -->
10+ <div
11+ id="logo-mascot"
12+ class="flex flex-col justify-center items-center p-4 mb-4"
13+ >
14+ <div class="circle-shadow flex justify-center w-100">
15+ <img
16+ src="/assets/imgs/gordon-mascot-hq-trsp.png"
17+ alt="logo"
18+ class="sm:h-24 h-32 xl:h-48"
19+ />
20+ </div>
21+ <div class="flex justify-center mt-4">
22+ <h1
23+ class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-indigo-800 tracking-tighter"
24+ >
25+ Gordon
26+ </h1>
27+ <span class="gordon-v-subtitle text-indigo-600 text-sm ml-2">
28+ v{ version }
29+ </span>
30+ </div>
31+ </div>
32+
33+ <!-- Main Content -->
34+ <div class="card w-2/3">
35+ <!-- Built-in Proxy -->
36+ <div id="built-in-proxy" class="mt-8">
37+ <div class="flex items-center mb-4">
38+ <div class="w-8 h-8 bg-indigo-500 rounded-full flex items-center justify-center">
39+ <span class="text-white font-bold">✓</span>
40+ </div>
41+ <h2 class="text-lg ml-2 text-title">Reverse Proxy</h2>
42+ </div>
43+
44+ <div class="bg-indigo-50 p-4 rounded-md">
45+ <p class="mb-4">Gordon now uses a built-in reverse proxy to handle all domain routing and TLS certificates.</p>
46+ <p>The proxy automatically:</p>
47+ <ul class="list-disc ml-6 mb-4">
48+ <li>Manages Let's Encrypt certificates</li>
49+ <li>Routes domains to the appropriate containers</li>
50+ <li>Handles HTTP to HTTPS redirects</li>
51+ <li>Provides proper headers for forwarded requests</li>
52+ </ul>
53+ <p>No additional configuration is required.</p>
54+ </div>
55+ </div>
56+ <!-- End Built-in Proxy -->
57+ </div>
58+ }
59+} ⏎̸
A internal/templating/models/templ/pages/admin/install_templ.go +75 -0 generated file: collapsed
A internal/templating/models/templ/pages/admin/manager.templ +73 -0
@@ -0,0 +1,73 @@
1+package admin
2+
3+import (
4+ "github.com/bnema/gordon/internal/templating/models/templ/layouts"
5+ "strconv"
6+)
7+
8+// ManagerPage implements the container and image manager page
9+templ ManagerPage(title string, buildVersion string, adminPath string, userSettingsURL string, logoutURL string, containerCount int, imageCount int) {
10+ @layouts.AdminLayout(title, buildVersion, adminPath, userSettingsURL, logoutURL) {
11+ <!-- Main content -->
12+ <div class="text-sm breadcrumbs mb-4">
13+ <ul>
14+ <li><a href={ templ.URL(adminPath) }>Gordon</a></li>
15+ <li>Manager</li>
16+ </ul>
17+ </div>
18+
19+ <!-- Stats overview -->
20+ <div class="stats shadow mb-6 w-full">
21+ <div class="stat">
22+ <div class="stat-figure text-primary">
23+ <span class="iconf text-3xl">󰡨</span>
24+ </div>
25+ <div class="stat-title">Containers</div>
26+ <div class="stat-value" id="container-count">{ strconv.Itoa(containerCount) }</div>
27+ <div class="stat-desc">Docker containers managed by Gordon</div>
28+ </div>
29+
30+ <div class="stat">
31+ <div class="stat-figure text-secondary">
32+ <span class="iconf text-3xl">󰋊</span>
33+ </div>
34+ <div class="stat-title">Images</div>
35+ <div class="stat-value" id="image-count">{ strconv.Itoa(imageCount) }</div>
36+ <div class="stat-desc">Docker images available</div>
37+ </div>
38+ </div>
39+
40+ <!-- Tabs for containers and images -->
41+ <div role="tablist" class="tabs tabs-boxed mb-4">
42+ <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked />
43+ <div role="tabpanel" class="tab-content p-0">
44+ <!-- Container manager section -->
45+ <div id="container-manager"
46+ hx-get="/htmx/container-manager"
47+ hx-trigger="load"
48+ hx-swap="innerHTML">
49+ <!-- Container content loaded via HTMX -->
50+ <div class="text-center p-4">
51+ <span class="iconf text-3xl animate-spin inline-block">󰑐</span>
52+ Loading containers...
53+ </div>
54+ </div>
55+ </div>
56+
57+ <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" />
58+ <div role="tabpanel" class="tab-content p-0">
59+ <!-- Image manager section -->
60+ <div id="image-manager"
61+ hx-get="/htmx/image-manager"
62+ hx-trigger="load"
63+ hx-swap="innerHTML">
64+ <!-- Image content loaded via HTMX -->
65+ <div class="text-center p-4">
66+ <span class="iconf text-3xl animate-spin inline-block">󰑐</span>
67+ Loading images...
68+ </div>
69+ </div>
70+ </div>
71+ </div>
72+ }
73+}⏎̸
A internal/templating/models/templ/pages/admin/manager_templ.go +99 -0 generated file: collapsed
A internal/templating/models/templ/pages/admin/user.templ +58 -0
@@ -0,0 +1,58 @@
1+package admin
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/layouts"
4+
5+// UserPageData represents data needed for the user page
6+type UserPageData struct {
7+ Title string
8+ BuildVersion string
9+ AdminPath string
10+ UserSettings string
11+ LogoutURL string
12+ Username string
13+ Email string
14+ Role string
15+}
16+
17+// UserPage replaces admin/user.gohtml
18+// It renders the user profile page
19+templ UserPage(data UserPageData) {
20+ @layouts.AdminLayout(data.Title, data.BuildVersion, data.AdminPath, data.UserSettings, data.LogoutURL) {
21+ <div class="text-sm breadcrumbs mb-4">
22+ <ul>
23+ <li><a href={ templ.URL(data.AdminPath) }>Gordon</a></li>
24+ <li>User Profile</li>
25+ </ul>
26+ </div>
27+
28+ <div class="card bg-base-100 shadow-xl">
29+ <div class="card-body">
30+ <h2 class="card-title">User Profile</h2>
31+ <div class="divider"></div>
32+
33+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
34+ <div class="form-control">
35+ <label class="label">
36+ <span class="label-text">Username</span>
37+ </label>
38+ <input type="text" class="input input-bordered" value={ data.Username } readonly />
39+ </div>
40+
41+ <div class="form-control">
42+ <label class="label">
43+ <span class="label-text">Email</span>
44+ </label>
45+ <input type="email" class="input input-bordered" value={ data.Email } readonly />
46+ </div>
47+
48+ <div class="form-control">
49+ <label class="label">
50+ <span class="label-text">Role</span>
51+ </label>
52+ <input type="text" class="input input-bordered" value={ data.Role } readonly />
53+ </div>
54+ </div>
55+ </div>
56+ </div>
57+ }
58+} ⏎̸
A internal/templating/models/templ/pages/admin/user_templ.go +122 -0 generated file: collapsed
A internal/templating/models/templ/pages/errors/errors.templ +46 -0
@@ -0,0 +1,46 @@
1+package errors
2+
3+import "github.com/bnema/gordon/internal/templating/models/templ/layouts"
4+
5+// ErrorPageData represents data needed for error pages
6+type ErrorPageData struct {
7+ Title string
8+ BuildVersion string
9+ AdminPath string
10+ StatusCode int
11+ Message string
12+}
13+
14+// NotFoundPage renders a 404 error page
15+templ NotFoundPage(data ErrorPageData) {
16+ @layouts.Base(data.Title) {
17+ <div class="flex flex-col items-center justify-center min-h-screen p-4">
18+ <div class="flex flex-col items-center mb-8">
19+ <img src="/assets/imgs/gordon-mascot-hq-trsp.png" alt="Gordon Mascot" class="w-64 h-64 mb-4" />
20+ <h1 class="text-4xl font-bold text-error">404</h1>
21+ <h2 class="text-2xl mb-2">Page Not Found</h2>
22+ <p class="text-center text-base-content opacity-70 max-w-md mb-8">{ data.Message }</p>
23+ <a href={ templ.URL(data.AdminPath) } class="btn btn-primary">
24+ <span class="iconf mr-2">🏠</span> Go Home
25+ </a>
26+ </div>
27+ </div>
28+ }
29+}
30+
31+// ForbiddenPage renders a 403 error page
32+templ ForbiddenPage(data ErrorPageData) {
33+ @layouts.Base(data.Title) {
34+ <div class="flex flex-col items-center justify-center min-h-screen p-4">
35+ <div class="flex flex-col items-center mb-8">
36+ <img src="/assets/imgs/gordon-mascot-hq-trsp.png" alt="Gordon Mascot" class="w-64 h-64 mb-4" />
37+ <h1 class="text-4xl font-bold text-error">403</h1>
38+ <h2 class="text-2xl mb-2">Access Forbidden</h2>
39+ <p class="text-center text-base-content opacity-70 max-w-md mb-8">{ data.Message }</p>
40+ <a href={ templ.URL(data.AdminPath) } class="btn btn-primary">
41+ <span class="iconf mr-2">🏠</span> Go Home
42+ </a>
43+ </div>
44+ </div>
45+ }
46+} ⏎̸
A internal/templating/models/templ/pages/errors/errors_templ.go +162 -0 generated file: collapsed
A internal/templating/models/templ/pages/login/index.templ +126 -0
@@ -0,0 +1,126 @@
1+package login
2+
3+// LoginPage implements the login page with token and GitHub authentication
4+templ LoginPage(title string, buildVersion string, errorMsg string) {
5+ <!doctype html>
6+ <html lang="en" data-theme="gordon-dark">
7+ <head>
8+ <meta charset="UTF-8" />
9+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10+ <title>{ title }</title>
11+ <meta name="description" content="Gordon Container Management System" />
12+ <meta name="keywords" content="container, management, docker, podman" />
13+ <!-- Custom CSS + Tailwind -->
14+ <link rel="stylesheet" href="/assets/css/tailwind.css" />
15+ <!-- HTMX min js -->
16+ <script src="/libs/htmx.min.js"></script>
17+ <!-- HTMX SSE -->
18+ <script src="/libs/htmx-sse.js"></script>
19+ <!-- DaisyUI -->
20+ <script src="/libs/daisyui.js"></script>
21+ <!-- Custom JS from assets/js -->
22+ <script src="/assets/js/custom.js"></script>
23+
24+ <!-- daisyUI theme configuration -->
25+ <script>
26+ // Check for saved theme preference or use preferred color scheme
27+ const savedTheme = localStorage.getItem('theme') || 'gordon-dark';
28+ document.documentElement.setAttribute('data-theme', savedTheme);
29+ console.debug('Theme loaded:', savedTheme);
30+ </script>
31+ </head>
32+
33+ <body class="IBMPlexSans bg-base-200 min-h-screen flex flex-col items-center justify-center p-4">
34+ <!-- Logo and Title -->
35+ <div
36+ id="logo-mascot"
37+ class="flex flex-col justify-center items-center p-4 mb-4">
38+ <div class="flex justify-center w-100">
39+ <img
40+ src="/assets/imgs/gordon-mascot-mq-trsp.png"
41+ alt="logo gordon"
42+ class="sm:h-24 h-32 xl:h-48 drop-shadow-xl" />
43+ </div>
44+ <div class="flex justify-center mt-4">
45+ <h1
46+ class="gordon-title text-4xl lg:text-4xl xl:text-5xl text-primary tracking-tighter">
47+ Gordon
48+ </h1>
49+ <span
50+ class="gordon-v-subtitle text-secondary text-sm ml-2"
51+ >{ buildVersion }</span>
52+ </div>
53+ </div>
54+
55+ <!-- Main Content -->
56+ <div class="card bg-base-100 shadow-xl w-full max-w-md">
57+ <div class="card-body">
58+ <!-- Login Section -->
59+ <div id="login-section">
60+ <div class="flex flex-col items-center">
61+ <h2 class="card-title text-center mb-4">Login</h2>
62+
63+ <!-- Error Message Display -->
64+ if errorMsg != "" {
65+ <div class="alert alert-error mb-4">
66+ <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
67+ <span>
68+ if errorMsg == "empty_token" {
69+ Please enter a token.
70+ } else if errorMsg == "invalid_token" {
71+ Invalid token. Please try again.
72+ } else if errorMsg == "server_error" {
73+ A server error occurred. Please try again later.
74+ } else {
75+ An error occurred. Please try again.
76+ }
77+ </span>
78+ </div>
79+ }
80+
81+ <!-- Token Input Form -->
82+ <div class="w-full">
83+ <form action="login/submit-token" method="POST" class="mb-4">
84+ <div class="form-control mb-4">
85+ <label for="token" class="label">
86+ <span class="label-text">Admin Token</span>
87+ </label>
88+ <input
89+ type="text"
90+ id="token"
91+ name="token"
92+ placeholder="Enter your admin token"
93+ class="input input-bordered w-full"
94+ required
95+ />
96+ </div>
97+ <button
98+ type="submit"
99+ class="btn btn-primary w-full"
100+ >
101+ Continue with Token
102+ </button>
103+ </form>
104+ <div class="divider">OR</div>
105+ </div>
106+
107+ <!-- GitHub OAuth -->
108+ <div class="w-full">
109+ <a
110+ href="login/oauth/github"
111+ class="btn btn-secondary w-full"
112+ >
113+ <svg class="mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
114+ <path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" />
115+ </svg>
116+ Sign in with GitHub
117+ </a>
118+ </div>
119+ </div>
120+ </div>
121+ <!-- End Login Section -->
122+ </div>
123+ </div>
124+ </body>
125+ </html>
126+}⏎̸
A internal/templating/models/templ/pages/login/index_templ.go +102 -0 generated file: collapsed
A internal/templating/render/templ.go +42 -0
@@ -0,0 +1,42 @@
1+package render
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+
8+ "github.com/a-h/templ"
9+ "github.com/bnema/gordon/internal/server"
10+ "github.com/labstack/echo/v4"
11+)
12+
13+// Renderer for templ templates
14+type TemplRenderer struct {
15+ BuildVersion string
16+ LatestVersion string
17+}
18+
19+// RenderTempl renders a templ component
20+func (r *TemplRenderer) RenderTempl(ctx echo.Context, component templ.Component) error {
21+ // Set the content type and status code
22+ ctx.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTML)
23+ ctx.Response().WriteHeader(http.StatusOK)
24+
25+ // Create a render context
26+ renderCtx := context.WithValue(ctx.Request().Context(), "BuildVersion", r.BuildVersion)
27+
28+ // Render the component
29+ err := component.Render(renderCtx, ctx.Response().Writer)
30+ if err != nil {
31+ return fmt.Errorf("failed to render templ component: %w", err)
32+ }
33+
34+ return nil
35+}
36+
37+// NewTemplRenderer creates a new templ renderer
38+func NewTemplRenderer(a *server.App) *TemplRenderer {
39+ return &TemplRenderer{
40+ BuildVersion: a.Config.Build.BuildVersion,
41+ }
42+}⏎̸
A internal/templating/templ_embed.go +13 -0
@@ -0,0 +1,13 @@
1+package templating
2+
3+// This file contains templ components embeddings
4+// As we migrate from traditional Go templates to templ,
5+// we'll add the appropriate code here to handle templ templates.
6+
7+import (
8+ // Import templ generated templates, which will make them available to the application
9+ _ "github.com/bnema/gordon/internal/templating/models/templ/components"
10+ _ "github.com/bnema/gordon/internal/templating/models/templ/layouts"
11+ _ "github.com/bnema/gordon/internal/templating/models/templ/pages/admin"
12+ _ "github.com/bnema/gordon/internal/templating/models/templ/pages/login"
13+)⏎̸
M internal/webui/public/assets/css/custom.css +112 -3
@@ -6,9 +6,118 @@
66
77 /* Embeded path : assets/css/cutsom.css */
88
9-@tailwind base;
10-@tailwind components;
11-@tailwind utilities;
9+@import "tailwindcss";
10+@source "../../../templating/models/html/**/*.{gohtml,go}";
11+@plugin "daisyui";
12+
13+/* Custom DaisyUI theme */
14+@plugin "daisyui/theme" {
15+ name: "gordon-dark";
16+ default: true;
17+ prefersdark: true;
18+ color-scheme: "dark";
19+ --color-base-100: oklch(28% 0.091 267.935);
20+ --color-base-200: oklch(13% 0.028 261.692);
21+ --color-base-300: oklch(20% 0.042 265.755);
22+ --color-base-content: oklch(93% 0.034 272.788);
23+ --color-primary: oklch(87% 0 0);
24+ --color-primary-content: oklch(14% 0 0);
25+ --color-secondary: oklch(83% 0.145 321.434);
26+ --color-secondary-content: oklch(29% 0.136 325.661);
27+ --color-accent: oklch(80% 0.105 251.813);
28+ --color-accent-content: oklch(28% 0.091 267.935);
29+ --color-neutral: oklch(37% 0.034 259.733);
30+ --color-neutral-content: oklch(98% 0.002 247.839);
31+ --color-info: oklch(68% 0.169 237.323);
32+ --color-info-content: oklch(97% 0.013 236.62);
33+ --color-success: oklch(70% 0.14 182.503);
34+ --color-success-content: oklch(98% 0.014 180.72);
35+ --color-warning: oklch(76% 0.188 70.08);
36+ --color-warning-content: oklch(98% 0.022 95.277);
37+ --color-error: oklch(64% 0.246 16.439);
38+ --color-error-content: oklch(96% 0.015 12.422);
39+ --radius-selector: 1rem;
40+ --radius-field: 0rem;
41+ --radius-box: 0.25rem;
42+ --size-selector: 0.25rem;
43+ --size-field: 0.25rem;
44+ --border: 1px;
45+ --depth: 0;
46+ --noise: 0;
47+}
48+
49+
50+/* Custom component classes */
51+@layer components {
52+ /* Button styles */
53+ .btn-primary {
54+ @apply flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
55+ }
56+
57+ .btn-secondary {
58+ @apply flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2;
59+ }
60+
61+ .btn-danger {
62+ @apply flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2;
63+ }
64+
65+ /* Alert styles */
66+ .alert-error {
67+ @apply rounded-md bg-red-50 p-4 dark:bg-red-900/20;
68+ }
69+
70+ .alert-error-icon {
71+ @apply h-5 w-5 text-red-400;
72+ }
73+
74+ .alert-error-text {
75+ @apply text-sm font-medium text-red-800 dark:text-red-300;
76+ }
77+
78+ .alert-success {
79+ @apply rounded-md bg-green-50 p-4 dark:bg-green-900/20;
80+ }
81+
82+ .alert-success-icon {
83+ @apply h-5 w-5 text-green-400;
84+ }
85+
86+ .alert-success-text {
87+ @apply text-sm font-medium text-green-800 dark:text-green-300;
88+ }
89+
90+ /* Card styles */
91+ .card {
92+ @apply bg-white p-4 rounded-md shadow mb-4;
93+ }
94+
95+ .card-header {
96+ @apply text-lg font-semibold text-indigo-600 mb-2;
97+ }
98+
99+ /* Background styles */
100+ .bg-app {
101+ @apply bg-indigo-50;
102+ }
103+
104+ .bg-sidebar {
105+ @apply bg-indigo-900;
106+ }
107+
108+ .bg-card {
109+ @apply bg-white;
110+ }
111+
112+ /* Text styles */
113+ .text-title {
114+ @apply text-indigo-600 font-semibold;
115+ }
116+
117+ .text-subtitle {
118+ @apply text-indigo-500;
119+ }
120+}
12121
13122 /* Import IBM Plex Fonts */
14123 @font-face {
M internal/webui/public/assets/css/tailwind.css +8364 -0
@@ -1,4 +1,5 @@
11 /*! tailwindcss v4.0.14 | MIT License | https://tailwindcss.com */
2+<<<<<<< HEAD
23 .visible {
34 visibility: visible;
45 }
@@ -163,6 +164,7879 @@
163164 &:focus {
164165 --tw-outline-style: none;
165166 outline-style: none;
167+=======
168+@layer theme, base, components, utilities;
169+@layer theme {
170+ :root, :host {
171+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
172+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
173+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
174+ "Courier New", monospace;
175+ --color-red-50: oklch(0.971 0.013 17.38);
176+ --color-red-300: oklch(0.808 0.114 19.571);
177+ --color-red-400: oklch(0.704 0.191 22.216);
178+ --color-red-500: oklch(0.637 0.237 25.331);
179+ --color-red-600: oklch(0.577 0.245 27.325);
180+ --color-red-700: oklch(0.505 0.213 27.518);
181+ --color-red-800: oklch(0.444 0.177 26.899);
182+ --color-red-900: oklch(0.396 0.141 25.723);
183+ --color-green-50: oklch(0.982 0.018 155.826);
184+ --color-green-300: oklch(0.871 0.15 154.449);
185+ --color-green-400: oklch(0.792 0.209 151.711);
186+ --color-green-500: oklch(0.723 0.219 149.579);
187+ --color-green-800: oklch(0.448 0.119 151.328);
188+ --color-green-900: oklch(0.393 0.095 152.535);
189+ --color-blue-500: oklch(0.623 0.214 259.815);
190+ --color-indigo-50: oklch(0.962 0.018 272.314);
191+ --color-indigo-100: oklch(0.93 0.034 272.788);
192+ --color-indigo-500: oklch(0.585 0.233 277.117);
193+ --color-indigo-600: oklch(0.511 0.262 276.966);
194+ --color-indigo-700: oklch(0.457 0.24 277.023);
195+ --color-indigo-800: oklch(0.398 0.195 277.366);
196+ --color-indigo-900: oklch(0.359 0.144 278.697);
197+ --color-gray-50: oklch(0.985 0.002 247.839);
198+ --color-gray-300: oklch(0.872 0.01 258.338);
199+ --color-gray-700: oklch(0.373 0.034 259.733);
200+ --color-black: #000;
201+ --color-white: #fff;
202+ --spacing: 0.25rem;
203+ --container-md: 28rem;
204+ --text-xs: 0.75rem;
205+ --text-xs--line-height: calc(1 / 0.75);
206+ --text-sm: 0.875rem;
207+ --text-sm--line-height: calc(1.25 / 0.875);
208+ --text-lg: 1.125rem;
209+ --text-lg--line-height: calc(1.75 / 1.125);
210+ --text-xl: 1.25rem;
211+ --text-xl--line-height: calc(1.75 / 1.25);
212+ --text-2xl: 1.5rem;
213+ --text-2xl--line-height: calc(2 / 1.5);
214+ --text-3xl: 1.875rem;
215+ --text-3xl--line-height: calc(2.25 / 1.875);
216+ --text-4xl: 2.25rem;
217+ --text-4xl--line-height: calc(2.5 / 2.25);
218+ --text-5xl: 3rem;
219+ --text-5xl--line-height: 1;
220+ --font-weight-medium: 500;
221+ --font-weight-semibold: 600;
222+ --font-weight-bold: 700;
223+ --tracking-tighter: -0.05em;
224+ --radius-sm: 0.25rem;
225+ --radius-md: 0.375rem;
226+ --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);
227+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
228+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
229+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
230+ --animate-spin: spin 1s linear infinite;
231+ --default-transition-duration: 150ms;
232+ --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
233+ --default-font-family: var(--font-sans);
234+ --default-font-feature-settings: var(--font-sans--font-feature-settings);
235+ --default-font-variation-settings: var(
236+ --font-sans--font-variation-settings
237+ );
238+ --default-mono-font-family: var(--font-mono);
239+ --default-mono-font-feature-settings: var(
240+ --font-mono--font-feature-settings
241+ );
242+ --default-mono-font-variation-settings: var(
243+ --font-mono--font-variation-settings
244+ );
245+ }
246+}
247+@layer base {
248+ *, ::after, ::before, ::backdrop, ::file-selector-button {
249+ box-sizing: border-box;
250+ margin: 0;
251+ padding: 0;
252+ border: 0 solid;
253+ }
254+ html, :host {
255+ line-height: 1.5;
256+ -webkit-text-size-adjust: 100%;
257+ tab-size: 4;
258+ font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" );
259+ font-feature-settings: var(--default-font-feature-settings, normal);
260+ font-variation-settings: var( --default-font-variation-settings, normal );
261+ -webkit-tap-highlight-color: transparent;
262+ }
263+ body {
264+ line-height: inherit;
265+ }
266+ hr {
267+ height: 0;
268+ color: inherit;
269+ border-top-width: 1px;
270+ }
271+ abbr:where([title]) {
272+ -webkit-text-decoration: underline dotted;
273+ text-decoration: underline dotted;
274+ }
275+ h1, h2, h3, h4, h5, h6 {
276+ font-size: inherit;
277+ font-weight: inherit;
278+ }
279+ a {
280+ color: inherit;
281+ -webkit-text-decoration: inherit;
282+ text-decoration: inherit;
283+ }
284+ b, strong {
285+ font-weight: bolder;
286+ }
287+ code, kbd, samp, pre {
288+ font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace );
289+ font-feature-settings: var( --default-mono-font-feature-settings, normal );
290+ font-variation-settings: var( --default-mono-font-variation-settings, normal );
291+ font-size: 1em;
292+ }
293+ small {
294+ font-size: 80%;
295+ }
296+ sub, sup {
297+ font-size: 75%;
298+ line-height: 0;
299+ position: relative;
300+ vertical-align: baseline;
301+ }
302+ sub {
303+ bottom: -0.25em;
304+ }
305+ sup {
306+ top: -0.5em;
307+ }
308+ table {
309+ text-indent: 0;
310+ border-color: inherit;
311+ border-collapse: collapse;
312+ }
313+ :-moz-focusring {
314+ outline: auto;
315+ }
316+ progress {
317+ vertical-align: baseline;
318+ }
319+ summary {
320+ display: list-item;
321+ }
322+ ol, ul, menu {
323+ list-style: none;
324+ }
325+ img, svg, video, canvas, audio, iframe, embed, object {
326+ display: block;
327+ vertical-align: middle;
328+ }
329+ img, video {
330+ max-width: 100%;
331+ height: auto;
332+ }
333+ button, input, select, optgroup, textarea, ::file-selector-button {
334+ font: inherit;
335+ font-feature-settings: inherit;
336+ font-variation-settings: inherit;
337+ letter-spacing: inherit;
338+ color: inherit;
339+ border-radius: 0;
340+ background-color: transparent;
341+ opacity: 1;
342+ }
343+ :where(select:is([multiple], [size])) optgroup {
344+ font-weight: bolder;
345+ }
346+ :where(select:is([multiple], [size])) optgroup option {
347+ padding-inline-start: 20px;
348+ }
349+ ::file-selector-button {
350+ margin-inline-end: 4px;
351+ }
352+ ::placeholder {
353+ opacity: 1;
354+ color: color-mix(in oklab, currentColor 50%, transparent);
355+ }
356+ textarea {
357+ resize: vertical;
358+ }
359+ ::-webkit-search-decoration {
360+ -webkit-appearance: none;
361+ }
362+ ::-webkit-date-and-time-value {
363+ min-height: 1lh;
364+ text-align: inherit;
365+ }
366+ ::-webkit-datetime-edit {
367+ display: inline-flex;
368+ }
369+ ::-webkit-datetime-edit-fields-wrapper {
370+ padding: 0;
371+ }
372+ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
373+ padding-block: 0;
374+ }
375+ :-moz-ui-invalid {
376+ box-shadow: none;
377+ }
378+ button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
379+ appearance: button;
380+ }
381+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
382+ height: auto;
383+ }
384+ [hidden]:where(:not([hidden="until-found"])) {
385+ display: none !important;
386+ }
387+}
388+@layer utilities {
389+ .diff {
390+ position: relative;
391+ display: grid;
392+ width: 100%;
393+ overflow: hidden;
394+ webkit-user-select: none;
395+ user-select: none;
396+ direction: ltr;
397+ container-type: inline-size;
398+ grid-template-columns: auto 1fr;
399+ &:focus-visible, &:has(.diff-item-1:focus) {
400+ outline-style: var(--tw-outline-style);
401+ outline-width: 2px;
402+ outline-offset: 1px;
403+ outline-color: var(--color-base-content);
404+ }
405+ &:focus-visible {
406+ outline-style: var(--tw-outline-style);
407+ outline-width: 2px;
408+ outline-offset: 1px;
409+ outline-color: var(--color-base-content);
410+ .diff-resizer {
411+ min-width: 90cqi;
412+ max-width: 90cqi;
413+ }
414+ }
415+ &:has(.diff-item-2:focus-visible) {
416+ outline-style: var(--tw-outline-style);
417+ outline-width: 2px;
418+ outline-offset: 1px;
419+ .diff-resizer {
420+ min-width: 10cqi;
421+ max-width: 10cqi;
422+ }
423+ }
424+ @supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) {
425+ &:focus {
426+ .diff-resizer {
427+ min-width: 10cqi;
428+ max-width: 10cqi;
429+ }
430+ }
431+ &:has(.diff-item-1:focus) {
432+ .diff-resizer {
433+ min-width: 90cqi;
434+ max-width: 90cqi;
435+ }
436+ }
437+ }
438+ }
439+ .modal {
440+ pointer-events: none;
441+ visibility: hidden;
442+ position: fixed;
443+ inset: calc(0.25rem * 0);
444+ margin: calc(0.25rem * 0);
445+ display: grid;
446+ height: 100%;
447+ max-height: none;
448+ width: 100%;
449+ max-width: none;
450+ align-items: center;
451+ justify-items: center;
452+ background-color: transparent;
453+ padding: calc(0.25rem * 0);
454+ color: inherit;
455+ overflow-x: hidden;
456+ transition: transform 0.3s ease-out, visibility 0.3s allow-discrete, background-color 0.3s ease-out, opacity 0.1s ease-out;
457+ overflow-y: hidden;
458+ overscroll-behavior: contain;
459+ z-index: 999;
460+ &::backdrop {
461+ display: none;
462+ }
463+ &.modal-open, &[open], &:target {
464+ background-color: oklch(0% 0 0/ 0.4);
465+ transition: transform 0.3s ease-out, background-color 0.3s ease-out, opacity 0.1s ease-out;
466+ pointer-events: auto;
467+ visibility: visible;
468+ opacity: 100%;
469+ .modal-box {
470+ translate: 0 0;
471+ scale: 1;
472+ opacity: 1;
473+ }
474+ }
475+ @starting-style {
476+ &.modal-open, &[open], &:target {
477+ visibility: hidden;
478+ opacity: 0%;
479+ }
480+ }
481+ }
482+ .drawer-side {
483+ pointer-events: none;
484+ visibility: hidden;
485+ position: fixed;
486+ inset-inline-start: calc(0.25rem * 0);
487+ top: calc(0.25rem * 0);
488+ grid-column-start: 1;
489+ grid-row-start: 1;
490+ display: grid;
491+ width: 100%;
492+ grid-template-columns: repeat(1, minmax(0, 1fr));
493+ grid-template-rows: repeat(1, minmax(0, 1fr));
494+ align-items: flex-start;
495+ justify-items: start;
496+ overflow-x: hidden;
497+ overflow-y: hidden;
498+ overscroll-behavior: contain;
499+ opacity: 0%;
500+ transition: opacity 0.2s ease-out 0.1s allow-discrete, visibility 0.3s ease-out 0.1s allow-discrete;
501+ height: 100vh;
502+ height: 100dvh;
503+ > .drawer-overlay {
504+ position: sticky;
505+ top: calc(0.25rem * 0);
506+ cursor: pointer;
507+ place-self: stretch;
508+ background-color: oklch(0% 0 0 / 40%);
509+ }
510+ > * {
511+ grid-column-start: 1;
512+ grid-row-start: 1;
513+ }
514+ > *:not(.drawer-overlay) {
515+ will-change: transform;
516+ transition: translate 0.3s ease-out;
517+ translate: -100%;
518+ [dir="rtl"] & {
519+ translate: 100%;
520+ }
521+ }
522+ }
523+ .drawer-open {
524+ > .drawer-side {
525+ overflow-y: auto;
526+ }
527+ > .drawer-toggle {
528+ display: none;
529+ & ~ .drawer-side {
530+ pointer-events: auto;
531+ visibility: visible;
532+ position: sticky;
533+ display: block;
534+ width: auto;
535+ overscroll-behavior: auto;
536+ opacity: 100%;
537+ & > .drawer-overlay {
538+ cursor: default;
539+ background-color: transparent;
540+ }
541+ & > *:not(.drawer-overlay) {
542+ translate: 0%;
543+ [dir="rtl"] & {
544+ translate: 0%;
545+ }
546+ }
547+ }
548+ &:checked ~ .drawer-side {
549+ pointer-events: auto;
550+ visibility: visible;
551+ }
552+ }
553+ }
554+ .modal-toggle {
555+ position: fixed;
556+ height: calc(0.25rem * 0);
557+ width: calc(0.25rem * 0);
558+ appearance: none;
559+ opacity: 0%;
560+ &:checked + .modal {
561+ pointer-events: auto;
562+ visibility: visible;
563+ opacity: 100%;
564+ background-color: oklch(0% 0 0/ 0.4);
565+ .modal-box {
566+ translate: 0 0;
567+ scale: 1;
568+ opacity: 1;
569+ }
570+ }
571+ @starting-style {
572+ &:checked + .modal {
573+ visibility: hidden;
574+ opacity: 0%;
575+ }
576+ }
577+ }
578+ .drawer-toggle {
579+ position: fixed;
580+ height: calc(0.25rem * 0);
581+ width: calc(0.25rem * 0);
582+ appearance: none;
583+ opacity: 0%;
584+ &:checked {
585+ & ~ .drawer-side {
586+ pointer-events: auto;
587+ visibility: visible;
588+ overflow-y: auto;
589+ opacity: 100%;
590+ & > *:not(.drawer-overlay) {
591+ translate: 0%;
592+ }
593+ }
594+ }
595+ &:focus-visible ~ .drawer-content label.drawer-button {
596+ outline: 2px solid;
597+ outline-offset: 2px;
598+ }
599+ }
600+ .tooltip {
601+ position: relative;
602+ display: inline-block;
603+ --tt-bg: var(--color-neutral);
604+ --tt-off: calc(100% + 0.5rem);
605+ --tt-tail: calc(100% + 1px + 0.25rem);
606+ > :where(.tooltip-content), &[data-tip]:before {
607+ position: absolute;
608+ max-width: 20rem;
609+ border-radius: var(--radius-field);
610+ padding-inline: calc(0.25rem * 2);
611+ padding-block: calc(0.25rem * 1);
612+ text-align: center;
613+ white-space: normal;
614+ color: var(--color-neutral-content);
615+ opacity: 0%;
616+ font-size: 0.875rem;
617+ line-height: 1.25em;
618+ transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms;
619+ background-color: var(--tt-bg);
620+ width: max-content;
621+ pointer-events: none;
622+ z-index: 1;
623+ --tw-content: attr(data-tip);
624+ content: var(--tw-content);
625+ }
626+ &:after {
627+ position: absolute;
628+ position: absolute;
629+ opacity: 0%;
630+ background-color: var(--tt-bg);
631+ transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms;
632+ content: "";
633+ pointer-events: none;
634+ width: 0.625rem;
635+ height: 0.25rem;
636+ display: block;
637+ mask-repeat: no-repeat;
638+ mask-position: -1px 0;
639+ --mask-tooltip: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A");
640+ mask-image: var(--mask-tooltip);
641+ }
642+ &.tooltip-open, &[data-tip]:hover, &:hover, &:has(:focus-visible) {
643+ > .tooltip-content, &[data-tip]:before, &:after {
644+ opacity: 100%;
645+ --tt-pos: 0rem;
646+ transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0ms;
647+ }
648+ }
649+ > .tooltip-content, &[data-tip]:before {
650+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
651+ inset: auto auto var(--tt-off) 50%;
652+ }
653+ &:after {
654+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
655+ inset: auto auto var(--tt-tail) 50%;
656+ }
657+ }
658+ .tab {
659+ position: relative;
660+ display: inline-flex;
661+ cursor: pointer;
662+ appearance: none;
663+ flex-wrap: wrap;
664+ align-items: center;
665+ justify-content: center;
666+ text-align: center;
667+ webkit-user-select: none;
668+ user-select: none;
669+ &:hover {
670+ @media (hover: hover) {
671+ color: var(--color-base-content);
672+ }
673+ }
674+ --tab-p: 1rem;
675+ --tab-bg: var(--color-base-100);
676+ --tab-border-color: var(--color-base-300);
677+ --tab-radius-ss: 0;
678+ --tab-radius-se: 0;
679+ --tab-radius-es: 0;
680+ --tab-radius-ee: 0;
681+ --tab-order: 0;
682+ --tab-radius-min: calc(0.75rem - var(--border));
683+ border-color: #0000;
684+ order: var(--tab-order);
685+ height: calc(var(--size-field, 0.25rem) * 10);
686+ font-size: 0.875rem;
687+ padding-inline-start: var(--tab-p);
688+ padding-inline-end: var(--tab-p);
689+ &:is(input[type="radio"]) {
690+ min-width: fit-content;
691+ &:after {
692+ content: attr(aria-label);
693+ }
694+ }
695+ &:is(label) {
696+ position: relative;
697+ input {
698+ position: absolute;
699+ inset: calc(0.25rem * 0);
700+ cursor: pointer;
701+ appearance: none;
702+ opacity: 0%;
703+ }
704+ }
705+ &:checked, &:is(label:has(:checked)), &:is(.tab-active, [aria-selected="true"]) {
706+ & + .tab-content {
707+ display: block;
708+ height: 100%;
709+ }
710+ }
711+ &:not(:checked, label:has(:checked), :hover, .tab-active, [aria-selected="true"]) {
712+ color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
713+ }
714+ &:not(input):empty {
715+ flex-grow: 1;
716+ cursor: default;
717+ }
718+ &:focus {
719+ --tw-outline-style: none;
720+ outline-style: none;
721+ @media (forced-colors: active) {
722+ outline: 2px solid transparent;
723+ outline-offset: 2px;
724+ }
725+ }
726+ &:focus-visible, &:is(label:has(:checked:focus-visible)) {
727+ outline: 2px solid currentColor;
728+ outline-offset: -5px;
729+ }
730+ &[disabled] {
731+ pointer-events: none;
732+ opacity: 40%;
733+ }
734+ }
735+ .menu {
736+ display: flex;
737+ width: fit-content;
738+ flex-direction: column;
739+ flex-wrap: wrap;
740+ padding: calc(0.25rem * 2);
741+ --menu-active-fg: var(--color-neutral-content);
742+ --menu-active-bg: var(--color-neutral);
743+ font-size: 0.875rem;
744+ :where(li ul) {
745+ position: relative;
746+ margin-inline-start: calc(0.25rem * 4);
747+ padding-inline-start: calc(0.25rem * 2);
748+ white-space: nowrap;
749+ &:before {
750+ position: absolute;
751+ inset-inline-start: calc(0.25rem * 0);
752+ top: calc(0.25rem * 3);
753+ bottom: calc(0.25rem * 3);
754+ background-color: var(--color-base-content);
755+ opacity: 10%;
756+ width: var(--border);
757+ content: "";
758+ }
759+ }
760+ :where(li > .menu-dropdown:not(.menu-dropdown-show)) {
761+ display: none;
762+ }
763+ :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
764+ display: grid;
765+ grid-auto-flow: column;
766+ align-content: flex-start;
767+ align-items: center;
768+ gap: calc(0.25rem * 2);
769+ border-radius: var(--radius-field);
770+ padding-inline: calc(0.25rem * 3);
771+ padding-block: calc(0.25rem * 1.5);
772+ text-align: start;
773+ transition-property: color, background-color, box-shadow;
774+ transition-duration: 0.2s;
775+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
776+ grid-auto-columns: minmax(auto, max-content) auto max-content;
777+ text-wrap: balance;
778+ user-select: none;
779+ }
780+ :where(li > details > summary) {
781+ --tw-outline-style: none;
782+ outline-style: none;
783+ @media (forced-colors: active) {
784+ outline: 2px solid transparent;
785+ outline-offset: 2px;
786+ }
787+ &::-webkit-details-marker {
788+ display: none;
789+ }
790+ }
791+ :where(li > details > summary), :where(li > .menu-dropdown-toggle) {
792+ &:after {
793+ justify-self: flex-end;
794+ display: block;
795+ height: 0.375rem;
796+ width: 0.375rem;
797+ rotate: -135deg;
798+ translate: 0 -1px;
799+ transition-property: rotate, translate;
800+ transition-duration: 0.2s;
801+ content: "";
802+ transform-origin: 50% 50%;
803+ box-shadow: 2px 2px inset;
804+ pointer-events: none;
805+ }
806+ }
807+ :where(li > details[open] > summary):after, :where(li > .menu-dropdown-toggle.menu-dropdown-show):after {
808+ rotate: 45deg;
809+ translate: 0 1px;
810+ }
811+ :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title), li:not(.menu-title, .disabled) > details > summary:not(.menu-title) ):not(.menu-active, :active, .btn) {
812+ &.menu-focus, &:focus-visible {
813+ cursor: pointer;
814+ background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
815+ color: var(--color-base-content);
816+ --tw-outline-style: none;
817+ outline-style: none;
818+ @media (forced-colors: active) {
819+ outline: 2px solid transparent;
820+ outline-offset: 2px;
821+ }
822+ }
823+ }
824+ :where( li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title):not(.menu-active, :active, .btn):hover, li:not(.menu-title, .disabled) > details > summary:not(.menu-title):not(.menu-active, :active, .btn):hover ) {
825+ cursor: pointer;
826+ background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
827+ --tw-outline-style: none;
828+ outline-style: none;
829+ @media (forced-colors: active) {
830+ outline: 2px solid transparent;
831+ outline-offset: 2px;
832+ }
833+ box-shadow: 0 1px oklch(0% 0 0 / 0.01) inset, 0 -1px oklch(100% 0 0 / 0.01) inset;
834+ }
835+ :where(li:empty) {
836+ background-color: var(--color-base-content);
837+ opacity: 10%;
838+ margin: 0.5rem 1rem;
839+ height: 1px;
840+ }
841+ :where(li) {
842+ position: relative;
843+ display: flex;
844+ flex-shrink: 0;
845+ flex-direction: column;
846+ flex-wrap: wrap;
847+ align-items: stretch;
848+ .badge {
849+ justify-self: flex-end;
850+ }
851+ & > *:not(ul, .menu-title, details, .btn):active, & > *:not(ul, .menu-title, details, .btn).menu-active, & > details > summary:active {
852+ --tw-outline-style: none;
853+ outline-style: none;
854+ @media (forced-colors: active) {
855+ outline: 2px solid transparent;
856+ outline-offset: 2px;
857+ }
858+ color: var(--menu-active-fg);
859+ background-color: var(--menu-active-bg);
860+ background-size: auto, calc(var(--noise) * 100%);
861+ background-image: none, var(--fx-noise);
862+ &:not(&:active) {
863+ box-shadow: 0 2px calc(var(--depth) * 3px) -2px var(--menu-active-bg);
864+ }
865+ }
866+ &.menu-disabled {
867+ pointer-events: none;
868+ color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
869+ }
870+ }
871+ .dropdown:focus-within {
872+ .menu-dropdown-toggle:after {
873+ rotate: 45deg;
874+ translate: 0 1px;
875+ }
876+ }
877+ .dropdown-content {
878+ margin-top: calc(0.25rem * 2);
879+ padding: calc(0.25rem * 2);
880+ &:before {
881+ display: none;
882+ }
883+ }
884+ }
885+ .floating-label {
886+ position: relative;
887+ display: block;
888+ input {
889+ display: block;
890+ &::placeholder {
891+ transition: top 0.1s ease-out, translate 0.1s ease-out, scale 0.1s ease-out, opacity 0.1s ease-out;
892+ }
893+ }
894+ textarea {
895+ &::placeholder {
896+ transition: top 0.1s ease-out, translate 0.1s ease-out, scale 0.1s ease-out, opacity 0.1s ease-out;
897+ }
898+ }
899+ > span {
900+ position: absolute;
901+ inset-inline-start: calc(0.25rem * 3);
902+ z-index: 1;
903+ background-color: var(--color-base-100);
904+ padding-inline: calc(0.25rem * 1);
905+ opacity: 0%;
906+ font-size: 0.875rem;
907+ top: calc(var(--size-field, 0.25rem) * 10 / 2);
908+ line-height: 1;
909+ border-radius: 2px;
910+ pointer-events: none;
911+ translate: 0 -50%;
912+ transition: top 0.1s ease-out, translate 0.1s ease-out, scale 0.1s ease-out, opacity 0.1s ease-out;
913+ }
914+ &:focus-within, &:not(:has(input:placeholder-shown, textarea:placeholder-shown)) {
915+ ::placeholder {
916+ opacity: 0%;
917+ top: 0;
918+ translate: -12.5% calc(-50% - 0.125em);
919+ scale: 0.75;
920+ pointer-events: auto;
921+ }
922+ > span {
923+ opacity: 100%;
924+ top: 0;
925+ translate: -12.5% calc(-50% - 0.125em);
926+ scale: 0.75;
927+ pointer-events: auto;
928+ }
929+ }
930+ &:has(:disabled, [disabled]) {
931+ > span {
932+ opacity: 0%;
933+ }
934+ }
935+ &:has(.input-xs, .select-xs, .textarea-xs) span {
936+ font-size: 0.6875rem;
937+ top: calc(var(--size-field, 0.25rem) * 6 / 2);
938+ }
939+ &:has(.input-sm, .select-sm, .textarea-sm) span {
940+ font-size: 0.75rem;
941+ top: calc(var(--size-field, 0.25rem) * 8 / 2);
942+ }
943+ &:has(.input-md, .select-md, .textarea-md) span {
944+ font-size: 0.875rem;
945+ top: calc(var(--size-field, 0.25rem) * 10 / 2);
946+ }
947+ &:has(.input-lg, .select-lg, .textarea-lg) span {
948+ font-size: 1.125rem;
949+ top: calc(var(--size-field, 0.25rem) * 12 / 2);
950+ }
951+ &:has(.input-xl, .select-xl, .textarea-xl) span {
952+ font-size: 1.375rem;
953+ top: calc(var(--size-field, 0.25rem) * 14 / 2);
954+ }
955+ }
956+ .collapse-arrow {
957+ > .collapse-title:after {
958+ position: absolute;
959+ display: block;
960+ height: 0.5rem;
961+ width: 0.5rem;
962+ transform: translateY(-100%) rotate(45deg);
963+ transition-property: all;
964+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
965+ transition-duration: 0.2s;
966+ top: 1.9rem;
967+ inset-inline-end: 1.4rem;
968+ content: "";
969+ transform-origin: 75% 75%;
970+ box-shadow: 2px 2px;
971+ pointer-events: none;
972+ }
973+ }
974+ .collapse-plus {
975+ > .collapse-title:after {
976+ position: absolute;
977+ display: block;
978+ height: 0.5rem;
979+ width: 0.5rem;
980+ transition-property: all;
981+ transition-duration: 300ms;
982+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
983+ top: 0.9rem;
984+ inset-inline-end: 1.4rem;
985+ content: "+";
986+ pointer-events: none;
987+ }
988+ }
989+ .diff-item-2 {
990+ position: relative;
991+ grid-column-start: 1;
992+ grid-row-start: 1;
993+ &:after {
994+ pointer-events: none;
995+ position: absolute;
996+ top: calc(1/2 * 100%);
997+ right: 1px;
998+ bottom: calc(0.25rem * 0);
999+ z-index: 2;
1000+ border-radius: calc(infinity * 1px);
1001+ background-color: color-mix(in oklab, var(--color-base-100) 50%, transparent);
1002+ width: 1.2rem;
1003+ height: 1.8rem;
1004+ border: 2px solid var(--color-base-100);
1005+ content: "";
1006+ outline: 1px solid color-mix(in oklab, var(--color-base-content) 5%, #0000);
1007+ outline-offset: -3px;
1008+ backdrop-filter: blur(8px);
1009+ box-shadow: 0 1px 2px 0 oklch(0% 0 0 / 0.1);
1010+ translate: 50% -50%;
1011+ }
1012+ > * {
1013+ pointer-events: none;
1014+ position: absolute;
1015+ top: calc(0.25rem * 0);
1016+ bottom: calc(0.25rem * 0);
1017+ left: calc(0.25rem * 0);
1018+ height: 100%;
1019+ width: 100cqi;
1020+ max-width: none;
1021+ object-fit: cover;
1022+ object-position: center;
1023+ }
1024+ @supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) {
1025+ &:after {
1026+ content: none;
1027+ }
1028+ }
1029+ }
1030+ .pika-single {
1031+ &:is(div) {
1032+ user-select: none;
1033+ font-size: 0.75rem;
1034+ z-index: 999;
1035+ display: inline-block;
1036+ position: relative;
1037+ color: var(--color-base-content);
1038+ background-color: var(--color-base-100);
1039+ border-radius: var(--radius-box);
1040+ border: var(--border) solid var(--color-base-200);
1041+ padding: 0.5rem;
1042+ &:before,
1043+ &:after {
1044+ content: "";
1045+ display: table;
1046+ }
1047+ &:after {
1048+ clear: both;
1049+ }
1050+ &.is-hidden {
1051+ display: none;
1052+ }
1053+ &.is-bound {
1054+ position: absolute;
1055+ }
1056+ .pika-lendar {
1057+ css-float: left;
1058+ }
1059+ .pika-title {
1060+ position: relative;
1061+ text-align: center;
1062+ select {
1063+ cursor: pointer;
1064+ position: absolute;
1065+ z-index: 999;
1066+ margin: 0;
1067+ left: 0;
1068+ top: 5px;
1069+ opacity: 0;
1070+ }
1071+ }
1072+ .pika-label {
1073+ display: inline-block;
1074+ position: relative;
1075+ z-index: 999;
1076+ overflow: hidden;
1077+ margin: 0;
1078+ padding: 5px 3px;
1079+ background-color: var(--color-base-100);
1080+ }
1081+ .pika-prev,
1082+ .pika-next {
1083+ display: block;
1084+ cursor: pointer;
1085+ position: absolute;
1086+ top: 0;
1087+ outline: none;
1088+ border: 0;
1089+ width: 2.25rem;
1090+ height: 2.25rem;
1091+ color: #0000;
1092+ font-size: 1.2em;
1093+ border-radius: var(--radius-field);
1094+ &:hover {
1095+ background-color: var(--color-base-200);
1096+ }
1097+ &.is-disabled {
1098+ cursor: default;
1099+ opacity: 0.2;
1100+ }
1101+ &:before {
1102+ display: inline-block;
1103+ width: 2.25rem;
1104+ height: 2.25rem;
1105+ line-height: 2.25;
1106+ color: var(--color-base-content);
1107+ }
1108+ }
1109+ .pika-prev {
1110+ left: 0;
1111+ &:before {
1112+ content: "‹";
1113+ }
1114+ }
1115+ .pika-next {
1116+ right: 0;
1117+ &:before {
1118+ content: "›";
1119+ }
1120+ }
1121+ .pika-select {
1122+ display: inline-block;
1123+ }
1124+ .pika-table {
1125+ width: 100%;
1126+ border-collapse: collapse;
1127+ border-spacing: 0;
1128+ border: 0;
1129+ th,
1130+ td {
1131+ padding: 0;
1132+ }
1133+ th {
1134+ opacity: 0.6;
1135+ text-align: center;
1136+ width: 2.25rem;
1137+ height: 2.25rem;
1138+ }
1139+ }
1140+ .pika-button {
1141+ cursor: pointer;
1142+ display: block;
1143+ outline: none;
1144+ border: 0;
1145+ margin: 0;
1146+ width: 2.25rem;
1147+ height: 2.25rem;
1148+ padding: 5px;
1149+ text-align: right;
1150+ text-align: center;
1151+ }
1152+ .pika-week {
1153+ color: var(--color-base-content);
1154+ }
1155+ .is-today {
1156+ .pika-button {
1157+ background: var(--color-primary);
1158+ color: var(--color-primary-content);
1159+ }
1160+ }
1161+ .is-selected,
1162+ .has-event {
1163+ .pika-button {
1164+ &,
1165+ &:hover {
1166+ color: var(--color-base-100);
1167+ background-color: var(--color-base-content);
1168+ border-radius: var(--radius-field);
1169+ }
1170+ }
1171+ }
1172+ .has-event {
1173+ .pika-button {
1174+ background: var(--color-base-primary);
1175+ }
1176+ }
1177+ .is-disabled,
1178+ .is-inrange {
1179+ .pika-button {
1180+ background: var(--color-base-primary);
1181+ }
1182+ }
1183+ .is-startrange {
1184+ .pika-button {
1185+ color: var(--color-base-100);
1186+ background: var(--color-base-content);
1187+ border-radius: var(--radius-field);
1188+ }
1189+ }
1190+ .is-endrange {
1191+ .pika-button {
1192+ color: var(--color-base-100);
1193+ background: var(--color-base-content);
1194+ border-radius: var(--radius-field);
1195+ }
1196+ }
1197+ .is-disabled {
1198+ .pika-button {
1199+ pointer-events: none;
1200+ cursor: default;
1201+ color: var(--color-base-content);
1202+ opacity: 0.3;
1203+ }
1204+ }
1205+ .is-outside-current-month {
1206+ .pika-button {
1207+ color: var(--color-base-content);
1208+ opacity: 0.3;
1209+ }
1210+ }
1211+ .is-selection-disabled {
1212+ pointer-events: none;
1213+ cursor: default;
1214+ }
1215+ .pika-button:hover,
1216+ .pika-row.pick-whole-week:hover .pika-button {
1217+ color: var(--color-base-content);
1218+ background-color: var(--color-base-200);
1219+ border-radius: var(--radius-field);
1220+ }
1221+ .pika-table abbr {
1222+ text-decoration: none;
1223+ font-weight: normal;
1224+ }
1225+ }
1226+ }
1227+ .diff-item-1 {
1228+ position: relative;
1229+ z-index: 1;
1230+ grid-column-start: 1;
1231+ grid-row-start: 1;
1232+ overflow: hidden;
1233+ border-right: 2px solid var(--color-base-100);
1234+ > * {
1235+ pointer-events: none;
1236+ position: absolute;
1237+ top: calc(0.25rem * 0);
1238+ bottom: calc(0.25rem * 0);
1239+ left: calc(0.25rem * 0);
1240+ height: 100%;
1241+ width: 100cqi;
1242+ max-width: none;
1243+ object-fit: cover;
1244+ object-position: center;
1245+ }
1246+ }
1247+ .dock {
1248+ position: fixed;
1249+ right: calc(0.25rem * 0);
1250+ bottom: calc(0.25rem * 0);
1251+ left: calc(0.25rem * 0);
1252+ z-index: 1;
1253+ display: flex;
1254+ width: 100%;
1255+ flex-direction: row;
1256+ align-items: center;
1257+ justify-content: space-around;
1258+ background-color: var(--color-base-100);
1259+ padding: calc(0.25rem * 2);
1260+ color: currentColor;
1261+ border-top: 0.5px solid color-mix(in oklab, var(--color-base-content) 5%, #0000);
1262+ height: 4rem;
1263+ height: calc(4rem + env(safe-area-inset-bottom));
1264+ padding-bottom: env(safe-area-inset-bottom);
1265+ > * {
1266+ position: relative;
1267+ margin-bottom: calc(0.25rem * 2);
1268+ display: flex;
1269+ height: 100%;
1270+ max-width: calc(0.25rem * 32);
1271+ flex-shrink: 1;
1272+ flex-basis: 100%;
1273+ cursor: pointer;
1274+ flex-direction: column;
1275+ align-items: center;
1276+ justify-content: center;
1277+ gap: 1px;
1278+ border-radius: var(--radius-box);
1279+ background-color: transparent;
1280+ transition: opacity 0.2s ease-out;
1281+ &:where(.dock-active) {
1282+ &:after {
1283+ content: "";
1284+ position: absolute;
1285+ height: calc(0.25rem * 1);
1286+ width: calc(0.25rem * 10);
1287+ border-radius: calc(infinity * 1px);
1288+ background-color: currentColor;
1289+ bottom: 0.2rem;
1290+ border-top: 3px solid;
1291+ }
1292+ }
1293+ @media (hover: hover) {
1294+ &:hover {
1295+ opacity: 80%;
1296+ }
1297+ }
1298+ &[aria-disabled="true"], &[disabled] {
1299+ &, &:hover {
1300+ pointer-events: none;
1301+ color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
1302+ opacity: 100%;
1303+ }
1304+ }
1305+ .dock-label {
1306+ font-size: 0.6875rem;
1307+ }
1308+ }
1309+ }
1310+ .dropdown {
1311+ position: relative;
1312+ display: inline-block;
1313+ position-area: var(--anchor-v, bottom) var(--anchor-h, span-right);
1314+ & > *:not(summary):focus {
1315+ --tw-outline-style: none;
1316+ outline-style: none;
1317+ @media (forced-colors: active) {
1318+ outline: 2px solid transparent;
1319+ outline-offset: 2px;
1320+ }
1321+ }
1322+ .dropdown-content {
1323+ position: absolute;
1324+ }
1325+ &:not(details, .dropdown-open, .dropdown-hover:hover, :focus-within) {
1326+ .dropdown-content {
1327+ display: none;
1328+ transform-origin: top;
1329+ opacity: 0%;
1330+ scale: 95%;
1331+ }
1332+ }
1333+ &[popover], .dropdown-content {
1334+ z-index: 999;
1335+ animation: dropdown 0.2s;
1336+ transition-property: opacity, scale, display;
1337+ transition-behavior: allow-discrete;
1338+ transition-duration: 0.2s;
1339+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1340+ }
1341+ @starting-style {
1342+ &[popover], .dropdown-content {
1343+ scale: 95%;
1344+ opacity: 0;
1345+ }
1346+ }
1347+ &.dropdown-open, &:not(.dropdown-hover):focus, &:focus-within {
1348+ > [tabindex]:first-child {
1349+ pointer-events: none;
1350+ }
1351+ .dropdown-content {
1352+ opacity: 100%;
1353+ }
1354+ }
1355+ &.dropdown-hover:hover {
1356+ .dropdown-content {
1357+ opacity: 100%;
1358+ scale: 100%;
1359+ }
1360+ }
1361+ &:is(details) {
1362+ summary {
1363+ &::-webkit-details-marker {
1364+ display: none;
1365+ }
1366+ }
1367+ }
1368+ &.dropdown-open, &:focus, &:focus-within {
1369+ .dropdown-content {
1370+ scale: 100%;
1371+ }
1372+ }
1373+ &:where([popover]) {
1374+ background: #0000;
1375+ }
1376+ &[popover] {
1377+ position: fixed;
1378+ color: inherit;
1379+ @supports not (position-area: bottom) {
1380+ margin: auto;
1381+ &.dropdown-open:not(:popover-open) {
1382+ display: none;
1383+ transform-origin: top;
1384+ opacity: 0%;
1385+ scale: 95%;
1386+ }
1387+ &::backdrop {
1388+ background-color: color-mix(in oklab, #000 30%, #0000);
1389+ }
1390+ }
1391+ &:not(.dropdown-open, :popover-open) {
1392+ display: none;
1393+ transform-origin: top;
1394+ opacity: 0%;
1395+ scale: 95%;
1396+ }
1397+ }
1398+ }
1399+ .btn {
1400+ display: inline-flex;
1401+ flex-shrink: 0;
1402+ cursor: pointer;
1403+ flex-wrap: nowrap;
1404+ align-items: center;
1405+ justify-content: center;
1406+ gap: calc(0.25rem * 1.5);
1407+ text-align: center;
1408+ vertical-align: middle;
1409+ outline-offset: 2px;
1410+ webkit-user-select: none;
1411+ user-select: none;
1412+ padding-inline: var(--btn-p);
1413+ color: var(--btn-fg);
1414+ --tw-prose-links: var(--btn-fg);
1415+ height: var(--size);
1416+ font-size: var(--fontsize, 0.875rem);
1417+ font-weight: 600;
1418+ outline-color: var(--btn-color, var(--color-base-content));
1419+ transition-property: color, background-color, border-color, box-shadow;
1420+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1421+ transition-duration: 0.2s;
1422+ border-start-start-radius: var(--join-ss, var(--radius-field));
1423+ border-start-end-radius: var(--join-se, var(--radius-field));
1424+ border-end-start-radius: var(--join-es, var(--radius-field));
1425+ border-end-end-radius: var(--join-ee, var(--radius-field));
1426+ background-color: var(--btn-bg);
1427+ background-size: auto, calc(var(--noise) * 100%);
1428+ background-image: none, var(--btn-noise);
1429+ border-width: var(--border);
1430+ border-style: solid;
1431+ border-color: var(--btn-border);
1432+ text-shadow: 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 0.15));
1433+ box-shadow: 0 0.5px 0 0.5px oklch(100% 0 0 / calc(var(--depth) * 6%)) inset, var(--btn-shadow);
1434+ --size: calc(var(--size-field, 0.25rem) * 10);
1435+ --btn-bg: var(--btn-color, var(--color-base-200));
1436+ --btn-fg: var(--color-base-content);
1437+ --btn-p: 1rem;
1438+ --btn-border: color-mix(in oklab, var(--btn-bg), #000 calc(var(--depth) * 5%));
1439+ --btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000),
1440+ 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) calc(var(--depth) * 30%), #0000);
1441+ --btn-noise: var(--fx-noise);
1442+ .prose & {
1443+ text-decoration-line: none;
1444+ }
1445+ @media (hover: hover) {
1446+ &:hover {
1447+ --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%);
1448+ }
1449+ }
1450+ &:focus-visible {
1451+ outline-width: 2px;
1452+ outline-style: solid;
1453+ }
1454+ &:active:not(.btn-active) {
1455+ translate: 0 0.5px;
1456+ --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 5%);
1457+ --btn-border: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%);
1458+ --btn-shadow: 0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0);
1459+ }
1460+ &:is(:disabled, [disabled], .btn-disabled) {
1461+ &:not(.btn-link, .btn-ghost) {
1462+ background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
1463+ box-shadow: none;
1464+ }
1465+ pointer-events: none;
1466+ --btn-border: #0000;
1467+ --btn-noise: none;
1468+ --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000);
1469+ @media (hover: hover) {
1470+ &:hover {
1471+ pointer-events: none;
1472+ background-color: color-mix(in oklab, var(--color-neutral) 20%, transparent);
1473+ --btn-border: #0000;
1474+ --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000);
1475+ }
1476+ }
1477+ }
1478+ &:is(input[type="checkbox"], input[type="radio"]) {
1479+ appearance: none;
1480+ &::after {
1481+ content: attr(aria-label);
1482+ }
1483+ }
1484+ &:where(input:checked:not(.filter .btn)) {
1485+ --btn-color: var(--color-primary);
1486+ --btn-fg: var(--color-primary-content);
1487+ isolation: isolate;
1488+ }
1489+ }
1490+ .loading {
1491+ pointer-events: none;
1492+ display: inline-block;
1493+ aspect-ratio: 1 / 1;
1494+ background-color: currentColor;
1495+ vertical-align: middle;
1496+ width: calc(var(--size-selector, 0.25rem) * 6);
1497+ mask-size: 100%;
1498+ mask-repeat: no-repeat;
1499+ mask-position: center;
1500+ mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
1501+ }
1502+ .tab-disabled {
1503+ pointer-events: none;
1504+ opacity: 40%;
1505+ }
1506+ .react-day-picker {
1507+ user-select: none;
1508+ background-color: var(--color-base-100);
1509+ border-radius: var(--radius-box);
1510+ border: var(--border) solid var(--color-base-200);
1511+ font-size: 0.75rem;
1512+ display: inline-block;
1513+ position: relative;
1514+ overflow: clip;
1515+ &[dir="rtl"] {
1516+ .rdp-nav {
1517+ .rdp-chevron {
1518+ transform-origin: 50%;
1519+ transform: rotate(180deg);
1520+ }
1521+ }
1522+ }
1523+ * {
1524+ box-sizing: border-box;
1525+ }
1526+ .rdp-day {
1527+ width: 2.25rem;
1528+ height: 2.25rem;
1529+ text-align: center;
1530+ }
1531+ .rdp-day_button {
1532+ cursor: pointer;
1533+ font: inherit;
1534+ color: inherit;
1535+ width: 2.25rem;
1536+ height: 2.25rem;
1537+ border: 2px solid #0000;
1538+ border-radius: var(--radius-field);
1539+ background: 0 0;
1540+ justify-content: center;
1541+ align-items: center;
1542+ margin: 0;
1543+ padding: 0;
1544+ display: flex;
1545+ &:disabled {
1546+ cursor: revert;
1547+ }
1548+ &:hover {
1549+ background-color: var(--color-base-200);
1550+ }
1551+ }
1552+ .rdp-caption_label {
1553+ z-index: 1;
1554+ white-space: nowrap;
1555+ border: 0;
1556+ align-items: center;
1557+ display: inline-flex;
1558+ position: relative;
1559+ }
1560+ .rdp-button_next {
1561+ border-radius: var(--radius-field);
1562+ &:hover {
1563+ background-color: var(--color-base-200);
1564+ }
1565+ }
1566+ .rdp-button_previous {
1567+ border-radius: var(--radius-field);
1568+ &:hover {
1569+ background-color: var(--color-base-200);
1570+ }
1571+ }
1572+ .rdp-button_next,
1573+ .rdp-button_previous {
1574+ cursor: pointer;
1575+ font: inherit;
1576+ color: inherit;
1577+ appearance: none;
1578+ width: 2.25rem;
1579+ height: 2.25rem;
1580+ background: 0 0;
1581+ border: none;
1582+ justify-content: center;
1583+ align-items: center;
1584+ margin: 0;
1585+ padding: 0;
1586+ display: inline-flex;
1587+ position: relative;
1588+ &:disabled {
1589+ cursor: revert;
1590+ opacity: 0.5;
1591+ }
1592+ }
1593+ .rdp-chevron {
1594+ fill: var(--color-base-content);
1595+ width: 1rem;
1596+ height: 1rem;
1597+ display: inline-block;
1598+ }
1599+ .rdp-dropdowns {
1600+ align-items: center;
1601+ gap: 0.5rem;
1602+ display: inline-flex;
1603+ position: relative;
1604+ }
1605+ .rdp-dropdown {
1606+ z-index: 2;
1607+ opacity: 0;
1608+ appearance: none;
1609+ cursor: inherit;
1610+ line-height: inherit;
1611+ border: none;
1612+ width: 100%;
1613+ margin: 0;
1614+ padding: 0;
1615+ position: absolute;
1616+ inset-block: 0;
1617+ inset-inline-start: 0;
1618+ &:focus-visible {
1619+ ~ .rdp-caption_label {
1620+ outline: 5px auto highlight;
1621+ outline: 5px auto -webkit-focus-ring-color;
1622+ }
1623+ }
1624+ }
1625+ .rdp-dropdown_root {
1626+ align-items: center;
1627+ display: inline-flex;
1628+ position: relative;
1629+ &[data-disabled="true"] {
1630+ .rdp-chevron {
1631+ opacity: 0.5;
1632+ }
1633+ }
1634+ }
1635+ .rdp-month_caption {
1636+ height: 2.75rem;
1637+ font-size: 0.75rem;
1638+ font-weight: inherit;
1639+ place-content: center;
1640+ display: flex;
1641+ }
1642+ .rdp-months {
1643+ gap: 2rem;
1644+ flex-wrap: wrap;
1645+ max-width: fit-content;
1646+ padding: 0.5rem;
1647+ display: flex;
1648+ position: relative;
1649+ }
1650+ .rdp-month_grid {
1651+ border-collapse: collapse;
1652+ }
1653+ .rdp-nav {
1654+ height: 2.75rem;
1655+ inset-block-start: 0;
1656+ inset-inline-end: 0;
1657+ justify-content: space-between;
1658+ align-items: center;
1659+ width: 100%;
1660+ padding-inline: 0.5rem;
1661+ display: flex;
1662+ position: absolute;
1663+ top: 0.25rem;
1664+ }
1665+ .rdp-weekday {
1666+ opacity: 0.6;
1667+ padding: 0.5rem 0rem;
1668+ text-align: center;
1669+ font-size: smaller;
1670+ font-weight: 500;
1671+ }
1672+ .rdp-week_number {
1673+ opacity: 0.6;
1674+ height: 2.25rem;
1675+ width: 2.25rem;
1676+ border: none;
1677+ border-radius: 100%;
1678+ text-align: center;
1679+ font-size: small;
1680+ font-weight: 400;
1681+ }
1682+ .rdp-today:not(.rdp-outside) {
1683+ .rdp-day_button {
1684+ background: var(--color-primary);
1685+ color: var(--color-primary-content);
1686+ }
1687+ }
1688+ .rdp-selected {
1689+ font-weight: inherit;
1690+ font-size: 0.75rem;
1691+ .rdp-day_button {
1692+ color: var(--color-base-100);
1693+ background-color: var(--color-base-content);
1694+ border-radius: var(--radius-field);
1695+ border: none;
1696+ &:hover {
1697+ background-color: var(--color-base-content);
1698+ }
1699+ }
1700+ }
1701+ .rdp-outside {
1702+ opacity: 0.75;
1703+ }
1704+ .rdp-disabled {
1705+ opacity: 0.5;
1706+ }
1707+ .rdp-hidden {
1708+ visibility: hidden;
1709+ color: var(--color-base-content);
1710+ }
1711+ .rdp-range_start {
1712+ .rdp-day_button {
1713+ border-radius: var(--radius-field) 0 0 var(--radius-field);
1714+ }
1715+ }
1716+ .rdp-range_start .rdp-day_button {
1717+ background-color: var(--color-base-content);
1718+ color: var(--color-base-content);
1719+ }
1720+ .rdp-range_middle {
1721+ background-color: var(--color-base-200);
1722+ }
1723+ .rdp-range_middle .rdp-day_button {
1724+ border: unset;
1725+ border-radius: unset;
1726+ color: inherit;
1727+ }
1728+ .rdp-range_end {
1729+ color: var(--color-base-content);
1730+ .rdp-day_button {
1731+ border-radius: 0 var(--radius-field) var(--radius-field) 0;
1732+ }
1733+ }
1734+ .rdp-range_end .rdp-day_button {
1735+ color: var(--color-base-content);
1736+ background-color: var(--color-base-content);
1737+ }
1738+ .rdp-range_start.rdp-range_end {
1739+ background: revert;
1740+ }
1741+ .rdp-focusable {
1742+ cursor: pointer;
1743+ }
1744+ .rdp-footer {
1745+ border-top: var(--border) solid var(--color-base-200);
1746+ padding: 0.5rem;
1747+ }
1748+ }
1749+ .collapse {
1750+ &:not(td, tr, colgroup) {
1751+ visibility: visible;
1752+ }
1753+ position: relative;
1754+ display: grid;
1755+ overflow: hidden;
1756+ border-radius: var(--radius-box, 1rem);
1757+ width: 100%;
1758+ grid-template-rows: max-content 0fr;
1759+ transition: grid-template-rows 0.2s;
1760+ > input:is([type="checkbox"], [type="radio"]) {
1761+ grid-column-start: 1;
1762+ grid-row-start: 1;
1763+ appearance: none;
1764+ opacity: 0;
1765+ z-index: 1;
1766+ width: 100%;
1767+ padding: 1rem;
1768+ padding-inline-end: 3rem;
1769+ min-height: 3.75rem;
1770+ transition: background-color 0.2s ease-out;
1771+ }
1772+ &:is([open], :focus:not(.collapse-close)),
1773+ &:not(.collapse-close):has(> input:is([type="checkbox"], [type="radio"]):checked) {
1774+ grid-template-rows: max-content 1fr;
1775+ }
1776+ &:is([open], :focus:not(.collapse-close)) > .collapse-content,
1777+ &:not(.collapse-close)
1778+ > :where(input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-content) {
1779+ visibility: visible;
1780+ min-height: fit-content;
1781+ }
1782+ &:focus-visible,
1783+ &:has(> input:is([type="checkbox"], [type="radio"]):focus-visible) {
1784+ outline-color: var(--color-base-content);
1785+ outline-style: solid;
1786+ outline-width: 2px;
1787+ outline-offset: 2px;
1788+ }
1789+ &:not(.collapse-close) {
1790+ > input[type="checkbox"],
1791+ > input[type="radio"]:not(:checked),
1792+ > .collapse-title {
1793+ cursor: pointer;
1794+ }
1795+ }
1796+ &:focus:not(.collapse-close, .collapse[open]) > .collapse-title {
1797+ cursor: unset;
1798+ }
1799+ &:is([open], :focus:not(.collapse-close)) > :where(.collapse-content),
1800+ &:not(.collapse-close)
1801+ > :where(input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-content) {
1802+ padding-bottom: 1rem;
1803+ transition: padding 0.2s ease-out,
1804+ background-color 0.2s ease-out;
1805+ }
1806+ &:is([open]) {
1807+ &.collapse-arrow {
1808+ > .collapse-title:after {
1809+ transform: translateY(-50%) rotate(225deg);
1810+ }
1811+ }
1812+ }
1813+ &.collapse-open {
1814+ &.collapse-arrow {
1815+ > .collapse-title:after {
1816+ transform: translateY(-50%) rotate(225deg);
1817+ }
1818+ }
1819+ &.collapse-plus {
1820+ > .collapse-title:after {
1821+ content: "−";
1822+ }
1823+ }
1824+ }
1825+ &.collapse-arrow:focus:not(.collapse-close) {
1826+ > .collapse-title:after {
1827+ transform: translateY(-50%) rotate(225deg);
1828+ }
1829+ }
1830+ &.collapse-arrow:not(.collapse-close) {
1831+ > input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-title:after {
1832+ transform: translateY(-50%) rotate(225deg);
1833+ }
1834+ }
1835+ &[open] {
1836+ &.collapse-plus {
1837+ > .collapse-title:after {
1838+ content: "−";
1839+ }
1840+ }
1841+ }
1842+ &.collapse-plus:focus:not(.collapse-close) {
1843+ > .collapse-title:after {
1844+ content: "−";
1845+ }
1846+ }
1847+ &.collapse-plus:not(.collapse-close) {
1848+ > input:is([type="checkbox"], [type="radio"]):checked ~ .collapse-title:after {
1849+ content: "−";
1850+ }
1851+ }
1852+ &:is(details) {
1853+ width: 100%;
1854+ & summary {
1855+ position: relative;
1856+ display: block;
1857+ &::-webkit-details-marker {
1858+ display: none;
1859+ }
1860+ }
1861+ }
1862+ &:is(details) summary {
1863+ outline: none;
1864+ }
1865+ }
1866+ .collapse-content {
1867+ grid-column-start: 1;
1868+ grid-row-start: 1;
1869+ visibility: hidden;
1870+ grid-column-start: 1;
1871+ grid-row-start: 2;
1872+ min-height: 0;
1873+ transition: visibility 0.2s;
1874+ transition: padding 0.2s ease-out,
1875+ background-color 0.2s ease-out;
1876+ padding-left: 1rem;
1877+ padding-right: 1rem;
1878+ cursor: unset;
1879+ }
1880+ .validator {
1881+ &:user-valid, &:has(:user-valid) {
1882+ &, &:focus, &:checked, &[aria-checked="true"], &:focus-within {
1883+ --input-color: var(--color-success);
1884+ }
1885+ }
1886+ &:user-invalid, &:has(:user-invalid) {
1887+ &, &:focus, &:checked, &[aria-checked="true"], &:focus-within {
1888+ --input-color: var(--color-error);
1889+ }
1890+ & ~ .validator-hint {
1891+ visibility: visible;
1892+ display: block;
1893+ color: var(--color-error);
1894+ }
1895+ }
1896+ & ~ .validator-hint {
1897+ visibility: hidden;
1898+ }
1899+ }
1900+ .collapse-open {
1901+ grid-template-rows: max-content 1fr;
1902+ > .collapse-content {
1903+ visibility: visible;
1904+ min-height: fit-content;
1905+ padding-bottom: 1rem;
1906+ transition: padding 0.2sease-out,
1907+ background-color 0.2sease-out;
1908+ }
1909+ }
1910+ .collapse {
1911+ visibility: collapse;
1912+ }
1913+ .visible {
1914+ visibility: visible;
1915+ }
1916+ .tabs-lift {
1917+ --tabs-height: auto;
1918+ --tabs-direction: row;
1919+ > .tab {
1920+ --tab-border: 0 0 var(--border) 0;
1921+ --tab-radius-ss: min(var(--radius-field), var(--tab-radius-min));
1922+ --tab-radius-se: min(var(--radius-field), var(--tab-radius-min));
1923+ --tab-radius-es: 0;
1924+ --tab-radius-ee: 0;
1925+ --tab-paddings: var(--border) var(--tab-p) 0 var(--tab-p);
1926+ --tab-border-colors: #0000 #0000 var(--tab-border-color) #0000;
1927+ --tab-corner-width: calc(100% + min(var(--radius-field), var(--tab-radius-min)) * 2);
1928+ --tab-corner-height: min(var(--radius-field), var(--tab-radius-min));
1929+ --tab-corner-position: top left, top right;
1930+ border-width: var(--tab-border);
1931+ border-start-start-radius: var(--tab-radius-ss);
1932+ border-start-end-radius: var(--tab-radius-se);
1933+ border-end-start-radius: var(--tab-radius-es);
1934+ border-end-end-radius: var(--tab-radius-ee);
1935+ padding: var(--tab-paddings);
1936+ border-color: var(--tab-border-colors);
1937+ &:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked, label:has(:checked)) {
1938+ --tab-border: var(--border) var(--border) 0 var(--border);
1939+ --tab-border-colors: var(--tab-border-color) var(--tab-border-color) #0000
1940+ var(--tab-border-color);
1941+ --tab-paddings: 0 calc(var(--tab-p) - var(--border)) var(--border)
1942+ calc(var(--tab-p) - var(--border));
1943+ --tab-inset: auto auto 0 auto;
1944+ --tab-grad: calc(69% - var(--border));
1945+ --radius-start: radial-gradient(
1946+ circle at top left,
1947+ #0000 var(--tab-grad),
1948+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
1949+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
1950+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
1951+ );
1952+ --radius-end: radial-gradient(
1953+ circle at top right,
1954+ #0000 var(--tab-grad),
1955+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
1956+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
1957+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
1958+ );
1959+ background-color: var(--tab-bg);
1960+ &:before {
1961+ z-index: 1;
1962+ content: "";
1963+ display: block;
1964+ position: absolute;
1965+ width: var(--tab-corner-width);
1966+ height: var(--tab-corner-height);
1967+ background-position: var(--tab-corner-position);
1968+ background-image: var(--radius-start), var(--radius-end);
1969+ background-size: min(var(--radius-field), var(--tab-radius-min)) min(var(--radius-field), var(--tab-radius-min));
1970+ background-repeat: no-repeat;
1971+ inset: var(--tab-inset);
1972+ }
1973+ &:first-child:before {
1974+ --radius-start: none;
1975+ }
1976+ [dir="rtl"] &:first-child:before {
1977+ transform: rotateY(180deg);
1978+ }
1979+ &:last-child:before {
1980+ --radius-end: none;
1981+ }
1982+ [dir="rtl"] &:last-child:before {
1983+ transform: rotateY(180deg);
1984+ }
1985+ }
1986+ }
1987+ &:has(.tab-content) {
1988+ > .tab:first-child {
1989+ &:not(.tab-active, [aria-selected="true"]) {
1990+ --tab-border-colors: var(--tab-border-color) var(--tab-border-color) #0000
1991+ var(--tab-border-color);
1992+ }
1993+ }
1994+ }
1995+ .tab-content {
1996+ --tabcontent-margin: calc(-1 * var(--border)) 0 0 0;
1997+ --tabcontent-radius-ss: 0;
1998+ --tabcontent-radius-se: var(--radius-box);
1999+ --tabcontent-radius-es: var(--radius-box);
2000+ --tabcontent-radius-ee: var(--radius-box);
2001+ }
2002+ :checked, label:has(:checked), :is(.tab-active, [aria-selected="true"]) {
2003+ & + .tab-content {
2004+ &:nth-child(1), &:nth-child(n + 3) {
2005+ --tabcontent-radius-ss: var(--radius-box);
2006+ }
2007+ }
2008+ }
2009+ }
2010+ .radial-progress {
2011+ position: relative;
2012+ display: inline-grid;
2013+ height: var(--size);
2014+ width: var(--size);
2015+ place-content: center;
2016+ border-radius: calc(infinity * 1px);
2017+ background-color: transparent;
2018+ vertical-align: middle;
2019+ box-sizing: content-box;
2020+ --value: 0;
2021+ --size: 5rem;
2022+ --thickness: calc(var(--size) / 10);
2023+ --radialprogress: calc(var(--value) * 1%);
2024+ transition: --radialprogress 0.3s linear;
2025+ &:before {
2026+ position: absolute;
2027+ inset: calc(0.25rem * 0);
2028+ border-radius: calc(infinity * 1px);
2029+ content: "";
2030+ background: radial-gradient(farthest-side, currentColor 98%, #0000) top/var(--thickness) var(--thickness) no-repeat, conic-gradient(currentColor var(--radialprogress), #0000 0);
2031+ webkit-mask: radial-gradient( farthest-side, #0000 calc(100% - var(--thickness)), #000 calc(100% + 0.5px - var(--thickness)) );
2032+ mask: radial-gradient( farthest-side, #0000 calc(100% - var(--thickness)), #000 calc(100% + 0.5px - var(--thickness)) );
2033+ }
2034+ &:after {
2035+ position: absolute;
2036+ border-radius: calc(infinity * 1px);
2037+ background-color: currentColor;
2038+ transition: transform 0.3s linear;
2039+ content: "";
2040+ inset: calc(50% - var(--thickness) / 2);
2041+ transform: rotate(calc(var(--value) * 3.6deg - 90deg)) translate(calc(var(--size) / 2 - 50%));
2042+ }
2043+ }
2044+ .list {
2045+ display: flex;
2046+ flex-direction: column;
2047+ font-size: 0.875rem;
2048+ :where(.list-row) {
2049+ --list-grid-cols: minmax(0, auto) 1fr;
2050+ position: relative;
2051+ display: grid;
2052+ grid-auto-flow: column;
2053+ gap: calc(0.25rem * 4);
2054+ border-radius: var(--radius-box);
2055+ padding: calc(0.25rem * 4);
2056+ word-break: break-word;
2057+ grid-template-columns: var(--list-grid-cols);
2058+ &:has(.list-col-grow:nth-child(1)) {
2059+ --list-grid-cols: 1fr;
2060+ }
2061+ &:has(.list-col-grow:nth-child(2)) {
2062+ --list-grid-cols: minmax(0, auto) 1fr;
2063+ }
2064+ &:has(.list-col-grow:nth-child(3)) {
2065+ --list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr;
2066+ }
2067+ &:has(.list-col-grow:nth-child(4)) {
2068+ --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr;
2069+ }
2070+ &:has(.list-col-grow:nth-child(5)) {
2071+ --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr;
2072+ }
2073+ &:has(.list-col-grow:nth-child(6)) {
2074+ --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto)
2075+ minmax(0, auto) 1fr;
2076+ }
2077+ :not(.list-col-wrap) {
2078+ grid-row-start: 1;
2079+ }
2080+ }
2081+ & > :not(:last-child) {
2082+ &.list-row, .list-row {
2083+ &:after {
2084+ content: "";
2085+ border-bottom: var(--border) solid;
2086+ inset-inline: var(--radius-box);
2087+ position: absolute;
2088+ bottom: calc(0.25rem * 0);
2089+ border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent);
2090+ }
2091+ }
2092+ }
2093+ }
2094+ .toast {
2095+ position: fixed;
2096+ inset-inline-start: auto;
2097+ inset-inline-end: calc(0.25rem * 0);
2098+ top: auto;
2099+ bottom: calc(0.25rem * 0);
2100+ margin: calc(0.25rem * 4);
2101+ display: flex;
2102+ min-width: fit-content;
2103+ flex-direction: column;
2104+ gap: calc(0.25rem * 2);
2105+ background-color: transparent;
2106+ white-space: nowrap;
2107+ translate: var(--toast-x, 0) var(--toast-y, 0);
2108+ & > * {
2109+ animation: toast 0.25s ease-out;
2110+ }
2111+ &:where(.toast-start) {
2112+ inset-inline-start: calc(0.25rem * 0);
2113+ inset-inline-end: auto;
2114+ --toast-x: 0;
2115+ }
2116+ &:where(.toast-center) {
2117+ inset-inline-start: calc(1/2 * 100%);
2118+ inset-inline-end: calc(1/2 * 100%);
2119+ --toast-x: -50%;
2120+ }
2121+ &:where(.toast-end) {
2122+ inset-inline-start: auto;
2123+ inset-inline-end: calc(0.25rem * 0);
2124+ --toast-x: 0;
2125+ }
2126+ &:where(.toast-bottom) {
2127+ top: auto;
2128+ bottom: calc(0.25rem * 0);
2129+ --toast-y: 0;
2130+ }
2131+ &:where(.toast-middle) {
2132+ top: calc(1/2 * 100%);
2133+ bottom: auto;
2134+ --toast-y: -50%;
2135+ }
2136+ &:where(.toast-top) {
2137+ top: calc(0.25rem * 0);
2138+ bottom: auto;
2139+ --toast-y: 0;
2140+ }
2141+ }
2142+ .toggle {
2143+ border: var(--border) solid currentColor;
2144+ color: var(--input-color);
2145+ position: relative;
2146+ display: inline-grid;
2147+ flex-shrink: 0;
2148+ cursor: pointer;
2149+ appearance: none;
2150+ place-content: center;
2151+ vertical-align: middle;
2152+ webkit-user-select: none;
2153+ user-select: none;
2154+ grid-template-columns: 0fr 1fr 1fr;
2155+ --radius-selector-max: calc(
2156+ var(--radius-selector) + var(--radius-selector) + var(--radius-selector)
2157+ );
2158+ border-radius: calc( var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)) );
2159+ padding: var(--toggle-p);
2160+ box-shadow: 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset;
2161+ transition: color 0.3s, grid-template-columns 0.2s;
2162+ --input-color: color-mix(in oklab, var(--color-base-content) 50%, #0000);
2163+ --toggle-p: 0.1875rem;
2164+ --size: calc(var(--size-selector, 0.25rem) * 6);
2165+ width: calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2);
2166+ height: var(--size);
2167+ > * {
2168+ z-index: 1;
2169+ grid-column: span 1 / span 1;
2170+ grid-column-start: 2;
2171+ grid-row-start: 1;
2172+ height: 100%;
2173+ cursor: pointer;
2174+ appearance: none;
2175+ background-color: transparent;
2176+ padding: calc(0.25rem * 0.5);
2177+ transition: opacity 0.2s, rotate 0.4s;
2178+ border: none;
2179+ &:focus {
2180+ --tw-outline-style: none;
2181+ outline-style: none;
2182+ @media (forced-colors: active) {
2183+ outline: 2px solid transparent;
2184+ outline-offset: 2px;
2185+ }
2186+ }
2187+ &:nth-child(2) {
2188+ color: var(--color-base-100);
2189+ rotate: 0deg;
2190+ }
2191+ &:nth-child(3) {
2192+ color: var(--color-base-100);
2193+ opacity: 0%;
2194+ rotate: -15deg;
2195+ }
2196+ }
2197+ &:has(:checked) {
2198+ > :nth-child(2) {
2199+ opacity: 0%;
2200+ rotate: 15deg;
2201+ }
2202+ > :nth-child(3) {
2203+ opacity: 100%;
2204+ rotate: 0deg;
2205+ }
2206+ }
2207+ &:before {
2208+ position: relative;
2209+ inset-inline-start: calc(0.25rem * 0);
2210+ grid-column-start: 2;
2211+ grid-row-start: 1;
2212+ aspect-ratio: 1 / 1;
2213+ height: 100%;
2214+ border-radius: var(--radius-selector);
2215+ background-color: currentColor;
2216+ translate: 0;
2217+ --tw-content: "";
2218+ content: var(--tw-content);
2219+ transition: background-color 0.1s, translate 0.2s, inset-inline-start 0.2s;
2220+ box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000);
2221+ background-size: auto, calc(var(--noise) * 100%);
2222+ background-image: none, var(--fx-noise);
2223+ }
2224+ @media (forced-colors: active) {
2225+ &:before {
2226+ outline-style: var(--tw-outline-style);
2227+ outline-width: 1px;
2228+ outline-offset: calc(1px * -1);
2229+ }
2230+ }
2231+ @media print {
2232+ &:before {
2233+ outline: 0.25rem solid;
2234+ outline-offset: -1rem;
2235+ }
2236+ }
2237+ &:focus-visible, &:has(:focus-visible) {
2238+ outline: 2px solid currentColor;
2239+ outline-offset: 2px;
2240+ }
2241+ &:checked, &[aria-checked="true"], &:has(> input:checked) {
2242+ grid-template-columns: 1fr 1fr 0fr;
2243+ background-color: var(--color-base-100);
2244+ --input-color: var(--color-base-content);
2245+ &:before {
2246+ background-color: currentColor;
2247+ }
2248+ @starting-style {
2249+ &:before {
2250+ opacity: 0;
2251+ }
2252+ }
2253+ }
2254+ &:indeterminate {
2255+ grid-template-columns: 0.5fr 1fr 0.5fr;
2256+ }
2257+ &:disabled {
2258+ cursor: not-allowed;
2259+ opacity: 30%;
2260+ &:before {
2261+ background-color: transparent;
2262+ border: var(--border) solid currentColor;
2263+ }
2264+ }
2265+ }
2266+ .input {
2267+ cursor: text;
2268+ border: var(--border) solid #0000;
2269+ position: relative;
2270+ display: inline-flex;
2271+ flex-shrink: 1;
2272+ appearance: none;
2273+ align-items: center;
2274+ gap: calc(0.25rem * 2);
2275+ background-color: var(--color-base-100);
2276+ padding-inline: calc(0.25rem * 3);
2277+ vertical-align: middle;
2278+ white-space: nowrap;
2279+ width: clamp(3rem, 20rem, 100%);
2280+ height: var(--size);
2281+ font-size: 0.875rem;
2282+ border-start-start-radius: var(--join-ss, var(--radius-field));
2283+ border-start-end-radius: var(--join-se, var(--radius-field));
2284+ border-end-start-radius: var(--join-es, var(--radius-field));
2285+ border-end-end-radius: var(--join-ee, var(--radius-field));
2286+ border-color: var(--input-color);
2287+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
2288+ --size: calc(var(--size-field, 0.25rem) * 10);
2289+ --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
2290+ &:where(input) {
2291+ display: inline-flex;
2292+ }
2293+ :where(input) {
2294+ display: inline-flex;
2295+ height: 100%;
2296+ width: 100%;
2297+ appearance: none;
2298+ background-color: transparent;
2299+ border: none;
2300+ &:focus, &:focus-within {
2301+ --tw-outline-style: none;
2302+ outline-style: none;
2303+ @media (forced-colors: active) {
2304+ outline: 2px solid transparent;
2305+ outline-offset: 2px;
2306+ }
2307+ }
2308+ }
2309+ &:focus, &:focus-within {
2310+ --input-color: var(--color-base-content);
2311+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000);
2312+ outline: 2px solid var(--input-color);
2313+ outline-offset: 2px;
2314+ isolation: isolate;
2315+ }
2316+ &:has(> input[disabled]), &:is(:disabled, [disabled]) {
2317+ cursor: not-allowed;
2318+ border-color: var(--color-base-200);
2319+ background-color: var(--color-base-200);
2320+ color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
2321+ &::placeholder {
2322+ color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
2323+ }
2324+ box-shadow: none;
2325+ }
2326+ &:has(> input[disabled]) > input[disabled] {
2327+ cursor: not-allowed;
2328+ }
2329+ &::-webkit-date-and-time-value {
2330+ text-align: inherit;
2331+ }
2332+ &[type="number"] {
2333+ &::-webkit-inner-spin-button {
2334+ margin-block: calc(0.25rem * -3);
2335+ margin-inline-end: calc(0.25rem * -3);
2336+ }
2337+ }
2338+ &::-webkit-calendar-picker-indicator {
2339+ position: absolute;
2340+ inset-inline-end: 0.75em;
2341+ }
2342+ }
2343+ .indicator {
2344+ position: relative;
2345+ display: inline-flex;
2346+ width: max-content;
2347+ :where(.indicator-item) {
2348+ z-index: 1;
2349+ position: absolute;
2350+ white-space: nowrap;
2351+ top: var(--inidicator-t, 0);
2352+ bottom: var(--inidicator-b, auto);
2353+ left: var(--inidicator-s, auto);
2354+ right: var(--inidicator-e, 0);
2355+ translate: var(--inidicator-x, 50%) var(--indicator-y, -50%);
2356+ }
2357+ }
2358+ .table {
2359+ font-size: 0.875rem;
2360+ position: relative;
2361+ width: 100%;
2362+ border-radius: var(--radius-box);
2363+ text-align: left;
2364+ &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) {
2365+ text-align: right;
2366+ }
2367+ tr.row-hover {
2368+ &, &:nth-child(even) {
2369+ &:hover {
2370+ @media (hover: hover) {
2371+ background-color: var(--color-base-200);
2372+ }
2373+ }
2374+ }
2375+ }
2376+ :where(th, td) {
2377+ padding-inline: calc(0.25rem * 4);
2378+ padding-block: calc(0.25rem * 3);
2379+ vertical-align: middle;
2380+ }
2381+ :where(thead, tfoot) {
2382+ white-space: nowrap;
2383+ color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
2384+ font-size: 0.875rem;
2385+ font-weight: 600;
2386+ }
2387+ :where(tfoot) {
2388+ border-top: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000);
2389+ }
2390+ :where(.table-pin-rows thead tr) {
2391+ position: sticky;
2392+ top: calc(0.25rem * 0);
2393+ z-index: 1;
2394+ background-color: var(--color-base-100);
2395+ }
2396+ :where(.table-pin-rows tfoot tr) {
2397+ position: sticky;
2398+ bottom: calc(0.25rem * 0);
2399+ z-index: 1;
2400+ background-color: var(--color-base-100);
2401+ }
2402+ :where(.table-pin-cols tr th) {
2403+ position: sticky;
2404+ right: calc(0.25rem * 0);
2405+ left: calc(0.25rem * 0);
2406+ background-color: var(--color-base-100);
2407+ }
2408+ :where(thead tr, tbody tr:not(:last-child)) {
2409+ border-bottom: var(--border) solid color-mix(in oklch, var(--color-base-content) 5%, #0000);
2410+ }
2411+ }
2412+ .avatar-offline {
2413+ &:before {
2414+ content: "";
2415+ position: absolute;
2416+ z-index: 1;
2417+ display: block;
2418+ border-radius: calc(infinity * 1px);
2419+ background-color: var(--color-base-300);
2420+ outline: 2px solid var(--color-base-100);
2421+ width: 15%;
2422+ height: 15%;
2423+ top: 7%;
2424+ right: 7%;
2425+ }
2426+ }
2427+ .avatar-online {
2428+ &:before {
2429+ content: "";
2430+ position: absolute;
2431+ z-index: 1;
2432+ display: block;
2433+ border-radius: calc(infinity * 1px);
2434+ background-color: var(--color-success);
2435+ outline: 2px solid var(--color-base-100);
2436+ width: 15%;
2437+ height: 15%;
2438+ top: 7%;
2439+ right: 7%;
2440+ }
2441+ }
2442+ .steps {
2443+ display: inline-grid;
2444+ grid-auto-flow: column;
2445+ overflow: hidden;
2446+ overflow-x: auto;
2447+ counter-reset: step;
2448+ grid-auto-columns: 1fr;
2449+ .step {
2450+ display: grid;
2451+ grid-template-columns: repeat(1, minmax(0, 1fr));
2452+ grid-template-columns: auto;
2453+ grid-template-rows: repeat(2, minmax(0, 1fr));
2454+ grid-template-rows: 40px 1fr;
2455+ place-items: center;
2456+ text-align: center;
2457+ min-width: 4rem;
2458+ --step-bg: var(--color-base-300);
2459+ --step-fg: var(--color-base-content);
2460+ &:before {
2461+ top: calc(0.25rem * 0);
2462+ grid-column-start: 1;
2463+ grid-row-start: 1;
2464+ height: calc(0.25rem * 2);
2465+ width: 100%;
2466+ border: 1px solid;
2467+ color: var(--step-bg);
2468+ background-color: var(--step-bg);
2469+ --tw-content: "";
2470+ content: var(--tw-content);
2471+ margin-inline-start: -100%;
2472+ }
2473+ > .step-icon, &:not(:has(.step-icon)):after {
2474+ content: counter(step);
2475+ counter-increment: step;
2476+ z-index: 1;
2477+ color: var(--step-fg);
2478+ background-color: var(--step-bg);
2479+ border: 1px solid var(--step-bg);
2480+ position: relative;
2481+ grid-column-start: 1;
2482+ grid-row-start: 1;
2483+ display: grid;
2484+ height: calc(0.25rem * 8);
2485+ width: calc(0.25rem * 8);
2486+ place-items: center;
2487+ place-self: center;
2488+ border-radius: calc(infinity * 1px);
2489+ }
2490+ &:first-child:before {
2491+ content: none;
2492+ }
2493+ &[data-content]:after {
2494+ content: attr(data-content);
2495+ }
2496+ }
2497+ .step-neutral {
2498+ + .step-neutral:before, &:after, > .step-icon {
2499+ --step-bg: var(--color-neutral);
2500+ --step-fg: var(--color-neutral-content);
2501+ }
2502+ }
2503+ .step-primary {
2504+ + .step-primary:before, &:after, > .step-icon {
2505+ --step-bg: var(--color-primary);
2506+ --step-fg: var(--color-primary-content);
2507+ }
2508+ }
2509+ .step-secondary {
2510+ + .step-secondary:before, &:after, > .step-icon {
2511+ --step-bg: var(--color-secondary);
2512+ --step-fg: var(--color-secondary-content);
2513+ }
2514+ }
2515+ .step-accent {
2516+ + .step-accent:before, &:after, > .step-icon {
2517+ --step-bg: var(--color-accent);
2518+ --step-fg: var(--color-accent-content);
2519+ }
2520+ }
2521+ .step-info {
2522+ + .step-info:before, &:after, > .step-icon {
2523+ --step-bg: var(--color-info);
2524+ --step-fg: var(--color-info-content);
2525+ }
2526+ }
2527+ .step-success {
2528+ + .step-success:before, &:after, > .step-icon {
2529+ --step-bg: var(--color-success);
2530+ --step-fg: var(--color-success-content);
2531+ }
2532+ }
2533+ .step-warning {
2534+ + .step-warning:before, &:after, > .step-icon {
2535+ --step-bg: var(--color-warning);
2536+ --step-fg: var(--color-warning-content);
2537+ }
2538+ }
2539+ .step-error {
2540+ + .step-error:before, &:after, > .step-icon {
2541+ --step-bg: var(--color-error);
2542+ --step-fg: var(--color-error-content);
2543+ }
2544+ }
2545+ }
2546+ .diff-resizer {
2547+ position: relative;
2548+ top: calc(1/2 * 100%);
2549+ z-index: 1;
2550+ grid-column-start: 1;
2551+ grid-row-start: 1;
2552+ height: calc(0.25rem * 2);
2553+ width: 50cqi;
2554+ max-width: calc(100cqi - 1rem);
2555+ min-width: 1rem;
2556+ resize: horizontal;
2557+ overflow: hidden;
2558+ opacity: 0%;
2559+ transform: scaleY(3) translate(0.35rem, 0.08rem);
2560+ cursor: ew-resize;
2561+ transform-origin: 100% 100%;
2562+ clip-path: inset(calc(100% - 0.75rem) 0 0 calc(100% - 0.75rem));
2563+ transition: min-width 0.3s ease-out, max-width 0.3s ease-out;
2564+ }
2565+ .range {
2566+ appearance: none;
2567+ webkit-appearance: none;
2568+ --range-thumb: var(--color-base-100);
2569+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 6);
2570+ --range-progress: currentColor;
2571+ --range-fill: 1;
2572+ --range-p: 0.25rem;
2573+ --range-bg: color-mix(in oklab, currentColor 10%, #0000);
2574+ cursor: pointer;
2575+ overflow: hidden;
2576+ background-color: transparent;
2577+ vertical-align: middle;
2578+ width: clamp(3rem, 20rem, 100%);
2579+ --radius-selector-max: calc(
2580+ var(--radius-selector) + var(--radius-selector) + var(--radius-selector)
2581+ );
2582+ border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
2583+ border: none;
2584+ height: var(--range-thumb-size);
2585+ [dir="rtl"] & {
2586+ --range-dir: -1;
2587+ }
2588+ &:focus {
2589+ outline: none;
2590+ }
2591+ &:focus-visible {
2592+ outline: 2px solid;
2593+ outline-offset: 2px;
2594+ }
2595+ &::-webkit-slider-runnable-track {
2596+ width: 100%;
2597+ background-color: var(--range-bg);
2598+ border-radius: var(--radius-selector);
2599+ height: calc(var(--range-thumb-size) * 0.5);
2600+ }
2601+ @media (forced-colors: active) {
2602+ &::-webkit-slider-runnable-track {
2603+ border: 1px solid;
2604+ }
2605+ }
2606+ @media (forced-colors: active) {
2607+ &::-moz-range-track {
2608+ border: 1px solid;
2609+ }
2610+ }
2611+ &::-webkit-slider-thumb {
2612+ position: relative;
2613+ box-sizing: border-box;
2614+ border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
2615+ background-color: currentColor;
2616+ height: var(--range-thumb-size);
2617+ width: var(--range-thumb-size);
2618+ border: var(--range-p) solid;
2619+ appearance: none;
2620+ webkit-appearance: none;
2621+ top: 50%;
2622+ color: var(--range-progress);
2623+ transform: translateY(-50%);
2624+ box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill));
2625+ }
2626+ &::-moz-range-track {
2627+ width: 100%;
2628+ background-color: var(--range-bg);
2629+ border-radius: var(--radius-selector);
2630+ height: calc(var(--range-thumb-size) * 0.5);
2631+ }
2632+ &::-moz-range-thumb {
2633+ position: relative;
2634+ box-sizing: border-box;
2635+ border-radius: calc(var(--radius-selector) + min(var(--range-p), var(--radius-selector-max)));
2636+ background-color: currentColor;
2637+ height: var(--range-thumb-size);
2638+ width: var(--range-thumb-size);
2639+ border: var(--range-p) solid;
2640+ top: 50%;
2641+ color: var(--range-progress);
2642+ box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000), 0 0 0 2rem var(--range-thumb) inset, calc((var(--range-dir, 1) * -100rem) - (var(--range-dir, 1) * var(--range-thumb-size) / 2)) 0 0 calc(100rem * var(--range-fill));
2643+ }
2644+ &:disabled {
2645+ cursor: not-allowed;
2646+ opacity: 30%;
2647+ }
2648+ }
2649+ .countdown {
2650+ display: inline-flex;
2651+ &.countdown {
2652+ line-height: 1em;
2653+ }
2654+ & > * {
2655+ display: inline-block;
2656+ overflow-y: hidden;
2657+ height: 1em;
2658+ &:before {
2659+ position: relative;
2660+ content: "00\A 01\A 02\A 03\A 04\A 05\A 06\A 07\A 08\A 09\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20\A 21\A 22\A 23\A 24\A 25\A 26\A 27\A 28\A 29\A 30\A 31\A 32\A 33\A 34\A 35\A 36\A 37\A 38\A 39\A 40\A 41\A 42\A 43\A 44\A 45\A 46\A 47\A 48\A 49\A 50\A 51\A 52\A 53\A 54\A 55\A 56\A 57\A 58\A 59\A 60\A 61\A 62\A 63\A 64\A 65\A 66\A 67\A 68\A 69\A 70\A 71\A 72\A 73\A 74\A 75\A 76\A 77\A 78\A 79\A 80\A 81\A 82\A 83\A 84\A 85\A 86\A 87\A 88\A 89\A 90\A 91\A 92\A 93\A 94\A 95\A 96\A 97\A 98\A 99\A";
2661+ white-space: pre;
2662+ top: calc(var(--value) * -1em);
2663+ text-align: center;
2664+ transition: all 1s cubic-bezier(1, 0, 0, 1);
2665+ }
2666+ }
2667+ }
2668+ .tabs-border {
2669+ .tab {
2670+ --tab-border-color: #0000 #0000 var(--tab-border-color) #0000;
2671+ position: relative;
2672+ border-radius: var(--radius-field);
2673+ &:before {
2674+ --tw-content: "";
2675+ content: var(--tw-content);
2676+ background-color: var(--tab-border-color);
2677+ transition: background-color 0.2s ease;
2678+ width: 80%;
2679+ height: 3px;
2680+ border-radius: var(--radius-field);
2681+ bottom: 0;
2682+ left: 10%;
2683+ position: absolute;
2684+ }
2685+ &:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked), &:is(label:has(:checked)) {
2686+ &:before {
2687+ --tab-border-color: currentColor;
2688+ border-top: 3px solid;
2689+ }
2690+ }
2691+ }
2692+ }
2693+ .chat-bubble {
2694+ position: relative;
2695+ display: block;
2696+ width: fit-content;
2697+ border-radius: var(--radius-field);
2698+ background-color: var(--color-base-300);
2699+ padding-inline: calc(0.25rem * 4);
2700+ padding-block: calc(0.25rem * 2);
2701+ color: var(--color-base-content);
2702+ grid-row-end: 3;
2703+ min-height: 2rem;
2704+ min-width: 2.5rem;
2705+ max-width: 90%;
2706+ &:before {
2707+ position: absolute;
2708+ bottom: calc(0.25rem * 0);
2709+ height: calc(0.25rem * 3);
2710+ width: calc(0.25rem * 3);
2711+ background-color: inherit;
2712+ content: "";
2713+ mask-repeat: no-repeat;
2714+ mask-image: var(--mask-chat);
2715+ mask-position: 0px -1px;
2716+ mask-size: 13px;
2717+ }
2718+ }
2719+ .timeline {
2720+ position: relative;
2721+ display: flex;
2722+ > li {
2723+ position: relative;
2724+ display: grid;
2725+ flex-shrink: 0;
2726+ align-items: center;
2727+ grid-template-rows: var(--timeline-row-start, minmax(0, 1fr)) auto var( --timeline-row-end, minmax(0, 1fr) );
2728+ grid-template-columns: var(--timeline-col-start, minmax(0, 1fr)) auto var( --timeline-col-end, minmax(0, 1fr) );
2729+ > hr {
2730+ border: none;
2731+ width: 100%;
2732+ &:first-child {
2733+ grid-column-start: 1;
2734+ grid-row-start: 2;
2735+ }
2736+ &:last-child {
2737+ grid-column-start: 3;
2738+ grid-column-end: none;
2739+ grid-row-start: 2;
2740+ grid-row-end: auto;
2741+ }
2742+ @media print {
2743+ border: 0.1px solid var(--color-base-300);
2744+ }
2745+ }
2746+ }
2747+ :where(hr) {
2748+ height: calc(0.25rem * 1);
2749+ background-color: var(--color-base-300);
2750+ }
2751+ &:has(.timeline-middle hr) {
2752+ &:first-child {
2753+ border-start-start-radius: 0;
2754+ border-end-start-radius: 0;
2755+ border-start-end-radius: var(--radius-selector);
2756+ border-end-end-radius: var(--radius-selector);
2757+ }
2758+ &:last-child {
2759+ border-start-start-radius: var(--radius-selector);
2760+ border-end-start-radius: var(--radius-selector);
2761+ border-start-end-radius: 0;
2762+ border-end-end-radius: 0;
2763+ }
2764+ }
2765+ &:not(:has(.timeline-middle)) {
2766+ :first-child hr:last-child {
2767+ border-start-start-radius: var(--radius-selector);
2768+ border-end-start-radius: var(--radius-selector);
2769+ border-start-end-radius: 0;
2770+ border-end-end-radius: 0;
2771+ }
2772+ :last-child hr:first-child {
2773+ border-start-start-radius: 0;
2774+ border-end-start-radius: 0;
2775+ border-start-end-radius: var(--radius-selector);
2776+ border-end-end-radius: var(--radius-selector);
2777+ }
2778+ }
2779+ }
2780+ .card {
2781+ position: relative;
2782+ display: flex;
2783+ flex-direction: column;
2784+ border-radius: var(--radius-box);
2785+ outline-width: 2px;
2786+ transition: outline 0.2s ease-in-out;
2787+ outline: 0 solid #0000;
2788+ outline-offset: 2px;
2789+ &:focus {
2790+ --tw-outline-style: none;
2791+ outline-style: none;
2792+ @media (forced-colors: active) {
2793+ outline: 2px solid transparent;
2794+ outline-offset: 2px;
2795+ }
2796+ }
2797+ &:focus-visible {
2798+ outline-color: currentColor;
2799+ }
2800+ :where(figure:first-child) {
2801+ overflow: hidden;
2802+ border-start-start-radius: inherit;
2803+ border-start-end-radius: inherit;
2804+ border-end-start-radius: unset;
2805+ border-end-end-radius: unset;
2806+ }
2807+ :where(figure:last-child) {
2808+ overflow: hidden;
2809+ border-start-start-radius: unset;
2810+ border-start-end-radius: unset;
2811+ border-end-start-radius: inherit;
2812+ border-end-end-radius: inherit;
2813+ }
2814+ &:where(.card-border) {
2815+ border: var(--border) solid var(--color-base-200);
2816+ }
2817+ &:where(.card-dash) {
2818+ border: var(--border) dashed var(--color-base-200);
2819+ }
2820+ &.image-full {
2821+ display: grid;
2822+ &:before {
2823+ position: relative;
2824+ grid-column-start: 1;
2825+ grid-row-start: 1;
2826+ border-radius: var(--radius-box);
2827+ background-color: var(--color-neutral);
2828+ opacity: 75%;
2829+ content: "";
2830+ }
2831+ > * {
2832+ grid-column-start: 1;
2833+ grid-row-start: 1;
2834+ }
2835+ > .card-body {
2836+ position: relative;
2837+ color: var(--color-neutral-content);
2838+ }
2839+ :where(figure) {
2840+ overflow: hidden;
2841+ border-radius: inherit;
2842+ }
2843+ > figure img {
2844+ height: 100%;
2845+ object-fit: cover;
2846+ }
2847+ }
2848+ figure {
2849+ display: flex;
2850+ align-items: center;
2851+ justify-content: center;
2852+ }
2853+ &:has(> input:is(input[type="checkbox"], input[type="radio"])) {
2854+ cursor: pointer;
2855+ user-select: none;
2856+ }
2857+ &:has(> :checked) {
2858+ outline: 2px solid currentColor;
2859+ }
2860+ }
2861+ .swap {
2862+ position: relative;
2863+ display: inline-grid;
2864+ cursor: pointer;
2865+ place-content: center;
2866+ vertical-align: middle;
2867+ webkit-user-select: none;
2868+ user-select: none;
2869+ input {
2870+ appearance: none;
2871+ border: none;
2872+ }
2873+ > * {
2874+ grid-column-start: 1;
2875+ grid-row-start: 1;
2876+ transition-property: transform, rotate, opacity;
2877+ transition-duration: 0.2s;
2878+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
2879+ }
2880+ .swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on {
2881+ opacity: 0%;
2882+ }
2883+ input:is(:checked, :indeterminate) {
2884+ & ~ .swap-off {
2885+ opacity: 0%;
2886+ }
2887+ }
2888+ input:checked ~ .swap-on, input:indeterminate ~ .swap-indeterminate {
2889+ opacity: 100%;
2890+ }
2891+ }
2892+ .collapse-title {
2893+ grid-column-start: 1;
2894+ grid-row-start: 1;
2895+ position: relative;
2896+ width: 100%;
2897+ padding: 1rem;
2898+ padding-inline-end: 3rem;
2899+ min-height: 3.75rem;
2900+ transition: background-color 0.2s ease-out;
2901+ }
2902+ .mockup-browser {
2903+ position: relative;
2904+ overflow: hidden;
2905+ overflow-x: auto;
2906+ border-radius: var(--radius-box);
2907+ pre[data-prefix] {
2908+ &:before {
2909+ content: attr(data-prefix);
2910+ display: inline-block;
2911+ text-align: right;
2912+ }
2913+ }
2914+ .mockup-browser-toolbar {
2915+ margin-block: calc(0.25rem * 3);
2916+ display: inline-flex;
2917+ width: 100%;
2918+ align-items: center;
2919+ padding-right: 1.4em;
2920+ &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) {
2921+ flex-direction: row-reverse;
2922+ }
2923+ &:before {
2924+ content: "";
2925+ margin-right: 4.8rem;
2926+ display: inline-block;
2927+ aspect-ratio: 1 / 1;
2928+ height: calc(0.25rem * 3);
2929+ border-radius: calc(infinity * 1px);
2930+ opacity: 30%;
2931+ box-shadow: 1.4em 0, 2.8em 0, 4.2em 0;
2932+ }
2933+ .input {
2934+ margin-inline: auto;
2935+ display: flex;
2936+ height: 100%;
2937+ align-items: center;
2938+ gap: calc(0.25rem * 2);
2939+ overflow: hidden;
2940+ background-color: var(--color-base-200);
2941+ text-overflow: ellipsis;
2942+ white-space: nowrap;
2943+ font-size: 0.75rem;
2944+ direction: ltr;
2945+ &:before {
2946+ content: "";
2947+ width: calc(0.25rem * 4);
2948+ height: calc(0.25rem * 4);
2949+ opacity: 30%;
2950+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor' class='size-4'%3E%3Cpath fill-rule='evenodd' d='M9.965 11.026a5 5 0 1 1 1.06-1.06l2.755 2.754a.75.75 0 1 1-1.06 1.06l-2.755-2.754ZM10.5 7a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Z' clip-rule='evenodd' /%3E%3C/svg%3E%0A");
2951+ }
2952+ }
2953+ }
2954+ }
2955+ .select {
2956+ border: var(--border) solid #0000;
2957+ position: relative;
2958+ display: inline-flex;
2959+ flex-shrink: 1;
2960+ appearance: none;
2961+ align-items: center;
2962+ gap: calc(0.25rem * 1.5);
2963+ background-color: var(--color-base-100);
2964+ padding-inline-start: calc(0.25rem * 4);
2965+ padding-inline-end: calc(0.25rem * 7);
2966+ vertical-align: middle;
2967+ width: clamp(3rem, 20rem, 100%);
2968+ height: var(--size);
2969+ font-size: 0.875rem;
2970+ border-start-start-radius: var(--join-ss, var(--radius-field));
2971+ border-start-end-radius: var(--join-se, var(--radius-field));
2972+ border-end-start-radius: var(--join-es, var(--radius-field));
2973+ border-end-end-radius: var(--join-ee, var(--radius-field));
2974+ background-image: linear-gradient(45deg, #0000 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, #0000 50%);
2975+ background-position: calc(100% - 20px) calc(1px + 50%), calc(100% - 16.1px) calc(1px + 50%);
2976+ background-size: 4px 4px, 4px 4px;
2977+ background-repeat: no-repeat;
2978+ text-overflow: ellipsis;
2979+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
2980+ border-color: var(--input-color);
2981+ --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
2982+ --size: calc(var(--size-field, 0.25rem) * 10);
2983+ [dir="rtl"] & {
2984+ background-position: calc(0% + 12px) calc(1px + 50%), calc(0% + 16px) calc(1px + 50%);
2985+ }
2986+ select {
2987+ margin-inline-start: calc(0.25rem * -4);
2988+ margin-inline-end: calc(0.25rem * -7);
2989+ width: calc(100% + 2.75rem);
2990+ appearance: none;
2991+ padding-inline-start: calc(0.25rem * 4);
2992+ padding-inline-end: calc(0.25rem * 7);
2993+ height: calc(100% - 2px);
2994+ background: inherit;
2995+ border-radius: inherit;
2996+ border-style: none;
2997+ &:focus, &:focus-within {
2998+ --tw-outline-style: none;
2999+ outline-style: none;
3000+ @media (forced-colors: active) {
3001+ outline: 2px solid transparent;
3002+ outline-offset: 2px;
3003+ }
3004+ }
3005+ &:not(:last-child) {
3006+ margin-inline-end: calc(0.25rem * -5.5);
3007+ background-image: none;
3008+ }
3009+ }
3010+ &:focus, &:focus-within {
3011+ --input-color: var(--color-base-content);
3012+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000);
3013+ outline: 2px solid var(--input-color);
3014+ outline-offset: 2px;
3015+ }
3016+ &:has(> select[disabled]), &:is(:disabled, [disabled]) {
3017+ cursor: not-allowed;
3018+ border-color: var(--color-base-200);
3019+ background-color: var(--color-base-200);
3020+ color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
3021+ &::placeholder {
3022+ color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
3023+ }
3024+ }
3025+ &:has(> select[disabled]) > select[disabled] {
3026+ cursor: not-allowed;
3027+ }
3028+ }
3029+ .menu-horizontal {
3030+ display: inline-flex;
3031+ flex-direction: row;
3032+ & > li:not(.menu-title) > details > ul {
3033+ position: absolute;
3034+ margin-inline-start: calc(0.25rem * 0);
3035+ margin-top: calc(0.25rem * 4);
3036+ padding-block: calc(0.25rem * 2);
3037+ padding-inline-end: calc(0.25rem * 2);
3038+ }
3039+ & > li > details > ul {
3040+ &:before {
3041+ content: none;
3042+ }
3043+ }
3044+ :where(& > li:not(.menu-title) > details > ul) {
3045+ border-radius: var(--radius-box);
3046+ background-color: var(--color-base-100);
3047+ box-shadow: 0 1px 3px 0 oklch(0% 0 0/0.1), 0 1px 2px -1px oklch(0% 0 0/0.1);
3048+ }
3049+ }
3050+ .menu-vertical {
3051+ display: inline-flex;
3052+ flex-direction: column;
3053+ & > li:not(.menu-title) > details > ul {
3054+ position: relative;
3055+ margin-inline-start: calc(0.25rem * 4);
3056+ margin-top: calc(0.25rem * 0);
3057+ padding-block: calc(0.25rem * 0);
3058+ padding-inline-end: calc(0.25rem * 0);
3059+ }
3060+ }
3061+ .mockup-code {
3062+ position: relative;
3063+ overflow: hidden;
3064+ overflow-x: auto;
3065+ border-radius: var(--radius-box);
3066+ background-color: var(--color-neutral);
3067+ padding-block: calc(0.25rem * 5);
3068+ color: var(--color-neutral-content);
3069+ font-size: 0.875rem;
3070+ direction: ltr;
3071+ &:before {
3072+ content: "";
3073+ margin-bottom: calc(0.25rem * 4);
3074+ display: block;
3075+ height: calc(0.25rem * 3);
3076+ width: calc(0.25rem * 3);
3077+ border-radius: calc(infinity * 1px);
3078+ opacity: 30%;
3079+ box-shadow: 1.4em 0, 2.8em 0, 4.2em 0;
3080+ }
3081+ pre {
3082+ padding-right: calc(0.25rem * 5);
3083+ &:before {
3084+ content: "";
3085+ margin-right: 2ch;
3086+ }
3087+ &[data-prefix] {
3088+ &:before {
3089+ content: attr(data-prefix);
3090+ display: inline-block;
3091+ width: calc(0.25rem * 8);
3092+ text-align: right;
3093+ opacity: 50%;
3094+ }
3095+ }
3096+ }
3097+ }
3098+ .mockup-window {
3099+ position: relative;
3100+ display: flex;
3101+ flex-direction: column;
3102+ overflow: hidden;
3103+ overflow-x: auto;
3104+ border-radius: var(--radius-box);
3105+ padding-top: calc(0.25rem * 5);
3106+ &:before {
3107+ content: "";
3108+ margin-bottom: calc(0.25rem * 4);
3109+ display: block;
3110+ aspect-ratio: 1 / 1;
3111+ height: calc(0.25rem * 3);
3112+ flex-shrink: 0;
3113+ align-self: flex-start;
3114+ border-radius: calc(infinity * 1px);
3115+ opacity: 30%;
3116+ box-shadow: 1.4em 0, 2.8em 0, 4.2em 0;
3117+ }
3118+ [dir="rtl"] &:before {
3119+ align-self: flex-end;
3120+ }
3121+ pre[data-prefix] {
3122+ &:before {
3123+ content: attr(data-prefix);
3124+ display: inline-block;
3125+ text-align: right;
3126+ }
3127+ }
3128+ }
3129+ .avatar {
3130+ position: relative;
3131+ display: inline-flex;
3132+ vertical-align: middle;
3133+ & > div {
3134+ display: block;
3135+ aspect-ratio: 1 / 1;
3136+ overflow: hidden;
3137+ }
3138+ img {
3139+ height: 100%;
3140+ width: 100%;
3141+ object-fit: cover;
3142+ }
3143+ }
3144+ .checkbox {
3145+ border: var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 20%, #0000));
3146+ position: relative;
3147+ flex-shrink: 0;
3148+ cursor: pointer;
3149+ appearance: none;
3150+ border-radius: var(--radius-selector);
3151+ padding: calc(0.25rem * 1);
3152+ vertical-align: middle;
3153+ color: var(--color-base-content);
3154+ box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0 #0000 inset, 0 0 #0000;
3155+ transition: background-color 0.2s, box-shadow 0.2s;
3156+ --size: calc(var(--size-selector, 0.25rem) * 6);
3157+ width: var(--size);
3158+ height: var(--size);
3159+ background-size: auto, calc(var(--noise) * 100%);
3160+ background-image: none, var(--fx-noise);
3161+ &:before {
3162+ --tw-content: "";
3163+ content: var(--tw-content);
3164+ display: block;
3165+ width: 100%;
3166+ height: 100%;
3167+ rotate: 45deg;
3168+ background-color: currentColor;
3169+ opacity: 0%;
3170+ transition: clip-path 0.3s, opacity 0.1s, rotate 0.3s, translate 0.3s;
3171+ transition-delay: 0.1s;
3172+ clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 70% 80%, 70% 100%);
3173+ box-shadow: 0px 3px 0 0px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
3174+ font-size: 1rem;
3175+ line-height: 0.75;
3176+ }
3177+ &:focus-visible {
3178+ outline: 2px solid var(--input-color, currentColor);
3179+ outline-offset: 2px;
3180+ }
3181+ &:checked, &[aria-checked="true"] {
3182+ background-color: var(--input-color, #0000);
3183+ box-shadow: 0 0 #0000 inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1));
3184+ &:before {
3185+ clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 0%, 70% 0%, 70% 100%);
3186+ opacity: 100%;
3187+ }
3188+ @media (forced-colors: active) {
3189+ &:before {
3190+ rotate: 0deg;
3191+ background-color: transparent;
3192+ --tw-content: "✔︎";
3193+ clip-path: none;
3194+ }
3195+ }
3196+ @media print {
3197+ &:before {
3198+ rotate: 0deg;
3199+ background-color: transparent;
3200+ --tw-content: "✔︎";
3201+ clip-path: none;
3202+ }
3203+ }
3204+ }
3205+ &:indeterminate {
3206+ &:before {
3207+ rotate: 0deg;
3208+ opacity: 100%;
3209+ translate: 0 -35%;
3210+ clip-path: polygon(20% 100%, 20% 80%, 50% 80%, 50% 80%, 80% 80%, 80% 100%);
3211+ }
3212+ }
3213+ &:disabled {
3214+ cursor: not-allowed;
3215+ opacity: 20%;
3216+ }
3217+ }
3218+ .radio {
3219+ position: relative;
3220+ flex-shrink: 0;
3221+ cursor: pointer;
3222+ appearance: none;
3223+ border-radius: calc(infinity * 1px);
3224+ padding: calc(0.25rem * 1);
3225+ vertical-align: middle;
3226+ border: var(--border) solid var(--input-color, color-mix(in srgb, currentColor 20%, #0000));
3227+ box-shadow: 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset;
3228+ --size: calc(var(--size-selector, 0.25rem) * 6);
3229+ width: var(--size);
3230+ height: var(--size);
3231+ color: var(--input-color, currentColor);
3232+ &:before {
3233+ display: block;
3234+ width: 100%;
3235+ height: 100%;
3236+ border-radius: calc(infinity * 1px);
3237+ --tw-content: "";
3238+ content: var(--tw-content);
3239+ background-size: auto, calc(var(--noise) * 100%);
3240+ background-image: none, var(--fx-noise);
3241+ }
3242+ &:focus-visible {
3243+ outline: 2px solid currentColor;
3244+ }
3245+ &:checked, &[aria-checked="true"] {
3246+ animation: radio 0.2s ease-out;
3247+ border-color: currentColor;
3248+ background-color: var(--color-base-100);
3249+ &:before {
3250+ background-color: currentColor;
3251+ box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px oklch(0% 0 0 / calc(var(--depth) * 0.1));
3252+ }
3253+ @media (forced-colors: active) {
3254+ &:before {
3255+ outline-style: var(--tw-outline-style);
3256+ outline-width: 1px;
3257+ outline-offset: calc(1px * -1);
3258+ }
3259+ }
3260+ @media print {
3261+ &:before {
3262+ outline: 0.25rem solid;
3263+ outline-offset: -1rem;
3264+ }
3265+ }
3266+ }
3267+ &:disabled {
3268+ cursor: not-allowed;
3269+ opacity: 20%;
3270+ }
3271+ }
3272+ .rating {
3273+ position: relative;
3274+ display: inline-flex;
3275+ vertical-align: middle;
3276+ & input {
3277+ border: none;
3278+ appearance: none;
3279+ }
3280+ :where(*) {
3281+ animation: rating 0.25s ease-out;
3282+ height: calc(0.25rem * 6);
3283+ width: calc(0.25rem * 6);
3284+ border-radius: 0;
3285+ background-color: var(--color-base-content);
3286+ opacity: 20%;
3287+ &:is(input) {
3288+ cursor: pointer;
3289+ }
3290+ }
3291+ & .rating-hidden {
3292+ width: calc(0.25rem * 2);
3293+ background-color: transparent;
3294+ }
3295+ input[type="radio"]:checked {
3296+ background-image: none;
3297+ }
3298+ * {
3299+ &:checked, &[aria-checked="true"], &[aria-current="true"], &:has(~ *:checked, ~ *[aria-checked="true"], ~ *[aria-current="true"]) {
3300+ opacity: 100%;
3301+ }
3302+ &:focus-visible {
3303+ transition: scale 0.2s ease-out;
3304+ scale: 1.1;
3305+ }
3306+ }
3307+ & *:active:focus {
3308+ animation: none;
3309+ scale: 1.1;
3310+ }
3311+ &.rating-xs :where(*:not(.rating-hidden)) {
3312+ width: calc(0.25rem * 4);
3313+ height: calc(0.25rem * 4);
3314+ }
3315+ &.rating-sm :where(*:not(.rating-hidden)) {
3316+ width: calc(0.25rem * 5);
3317+ height: calc(0.25rem * 5);
3318+ }
3319+ &.rating-md :where(*:not(.rating-hidden)) {
3320+ width: calc(0.25rem * 6);
3321+ height: calc(0.25rem * 6);
3322+ }
3323+ &.rating-lg :where(*:not(.rating-hidden)) {
3324+ width: calc(0.25rem * 7);
3325+ height: calc(0.25rem * 7);
3326+ }
3327+ &.rating-xl :where(*:not(.rating-hidden)) {
3328+ width: calc(0.25rem * 8);
3329+ height: calc(0.25rem * 8);
3330+ }
3331+ }
3332+ .drawer {
3333+ position: relative;
3334+ display: grid;
3335+ width: 100%;
3336+ grid-auto-columns: max-content auto;
3337+ }
3338+ .stats {
3339+ position: relative;
3340+ display: inline-grid;
3341+ grid-auto-flow: column;
3342+ overflow-x: auto;
3343+ border-radius: var(--radius-box);
3344+ }
3345+ .progress {
3346+ position: relative;
3347+ height: calc(0.25rem * 2);
3348+ width: 100%;
3349+ appearance: none;
3350+ overflow: hidden;
3351+ border-radius: var(--radius-box);
3352+ background-color: color-mix(in oklab, currentColor 20%, transparent);
3353+ color: var(--color-base-content);
3354+ &:indeterminate {
3355+ background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% );
3356+ background-size: 200%;
3357+ background-position-x: 15%;
3358+ animation: progress 5s ease-in-out infinite;
3359+ @supports (-moz-appearance: none) {
3360+ &::-moz-progress-bar {
3361+ background-color: transparent;
3362+ background-image: repeating-linear-gradient( 90deg, currentColor -1%, currentColor 10%, #0000 10%, #0000 90% );
3363+ background-size: 200%;
3364+ background-position-x: 15%;
3365+ animation: progress 5s ease-in-out infinite;
3366+ }
3367+ }
3368+ }
3369+ @supports (-moz-appearance: none) {
3370+ &::-moz-progress-bar {
3371+ border-radius: var(--radius-box);
3372+ background-color: currentColor;
3373+ }
3374+ }
3375+ @supports (-webkit-appearance: none) {
3376+ &::-webkit-progress-bar {
3377+ border-radius: var(--radius-box);
3378+ background-color: transparent;
3379+ }
3380+ &::-webkit-progress-value {
3381+ border-radius: var(--radius-box);
3382+ background-color: currentColor;
3383+ }
3384+ }
3385+ }
3386+ .absolute {
3387+ position: absolute;
3388+ }
3389+ .fixed {
3390+ position: fixed;
3391+ }
3392+ .relative {
3393+ position: relative;
3394+ }
3395+ .static {
3396+ position: static;
3397+ }
3398+ .sticky {
3399+ position: sticky;
3400+ }
3401+ .tooltip-bottom {
3402+ > .tooltip-content, &[data-tip]:before {
3403+ transform: translateX(-50%) translateY(var(--tt-pos, -0.25rem));
3404+ inset: var(--tt-off) auto auto 50%;
3405+ }
3406+ &:after {
3407+ transform: translateX(-50%) translateY(var(--tt-pos, -0.25rem)) rotate(180deg);
3408+ inset: var(--tt-tail) auto auto 50%;
3409+ }
3410+ }
3411+ .tooltip-left {
3412+ > .tooltip-content, &[data-tip]:before {
3413+ transform: translateX(calc(var(--tt-pos, 0.25rem) - 0.25rem)) translateY(-50%);
3414+ inset: 50% var(--tt-off) auto auto;
3415+ }
3416+ &:after {
3417+ transform: translateX(var(--tt-pos, 0.25rem)) translateY(-50%) rotate(-90deg);
3418+ inset: 50% calc(var(--tt-tail) + 1px) auto auto;
3419+ }
3420+ }
3421+ .tooltip-right {
3422+ > .tooltip-content, &[data-tip]:before {
3423+ transform: translateX(calc(var(--tt-pos, -0.25rem) + 0.25rem)) translateY(-50%);
3424+ inset: 50% auto auto var(--tt-off);
3425+ }
3426+ &:after {
3427+ transform: translateX(var(--tt-pos, -0.25rem)) translateY(-50%) rotate(90deg);
3428+ inset: 50% auto auto calc(var(--tt-tail) + 1px);
3429+ }
3430+ }
3431+ .tooltip-top {
3432+ > .tooltip-content, &[data-tip]:before {
3433+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
3434+ inset: auto auto var(--tt-off) 50%;
3435+ }
3436+ &:after {
3437+ transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem));
3438+ inset: auto auto var(--tt-tail) 50%;
3439+ }
3440+ }
3441+ .dropdown-right {
3442+ --anchor-h: right;
3443+ --anchor-v: span-bottom;
3444+ .dropdown-content {
3445+ inset-inline-start: 100%;
3446+ top: calc(0.25rem * 0);
3447+ bottom: auto;
3448+ transform-origin: left;
3449+ }
3450+ }
3451+ .chat-end {
3452+ place-items: end;
3453+ grid-template-columns: 1fr auto;
3454+ .chat-header {
3455+ grid-column-start: 1;
3456+ }
3457+ .chat-footer {
3458+ grid-column-start: 1;
3459+ }
3460+ .chat-image {
3461+ grid-column-start: 2;
3462+ }
3463+ .chat-bubble {
3464+ grid-column-start: 1;
3465+ border-end-end-radius: 0;
3466+ &:before {
3467+ transform: rotateY(180deg);
3468+ inset-inline-start: 100%;
3469+ }
3470+ [dir="rtl"] &:before {
3471+ transform: rotateY(0deg);
3472+ }
3473+ }
3474+ }
3475+ .chat-start {
3476+ place-items: start;
3477+ grid-template-columns: auto 1fr;
3478+ .chat-header {
3479+ grid-column-start: 2;
3480+ }
3481+ .chat-footer {
3482+ grid-column-start: 2;
3483+ }
3484+ .chat-image {
3485+ grid-column-start: 1;
3486+ }
3487+ .chat-bubble {
3488+ grid-column-start: 2;
3489+ border-end-start-radius: 0;
3490+ &:before {
3491+ transform: rotateY(0deg);
3492+ inset-inline-start: -0.75rem;
3493+ }
3494+ [dir="rtl"] &:before {
3495+ transform: rotateY(180deg);
3496+ }
3497+ }
3498+ }
3499+ .dropdown-left {
3500+ --anchor-h: left;
3501+ --anchor-v: span-bottom;
3502+ .dropdown-content {
3503+ inset-inline-end: 100%;
3504+ top: calc(0.25rem * 0);
3505+ bottom: auto;
3506+ transform-origin: right;
3507+ }
3508+ }
3509+ .dropdown-center {
3510+ --anchor-h: center;
3511+ :where(.dropdown-content) {
3512+ inset-inline-end: calc(1/2 * 100%);
3513+ translate: 50% 0;
3514+ }
3515+ &.dropdown-left {
3516+ --anchor-h: left;
3517+ --anchor-v: center;
3518+ .dropdown-content {
3519+ top: auto;
3520+ bottom: calc(1/2 * 100%);
3521+ translate: 0 50%;
3522+ }
3523+ }
3524+ &.dropdown-right {
3525+ --anchor-h: right;
3526+ --anchor-v: center;
3527+ .dropdown-content {
3528+ top: auto;
3529+ bottom: calc(1/2 * 100%);
3530+ translate: 0 50%;
3531+ }
3532+ }
3533+ }
3534+ .dropdown-end {
3535+ --anchor-h: span-left;
3536+ :where(.dropdown-content) {
3537+ inset-inline-end: calc(0.25rem * 0);
3538+ translate: 0 0;
3539+ }
3540+ &.dropdown-left {
3541+ --anchor-h: left;
3542+ --anchor-v: span-top;
3543+ .dropdown-content {
3544+ top: auto;
3545+ bottom: calc(0.25rem * 0);
3546+ }
3547+ }
3548+ &.dropdown-right {
3549+ --anchor-h: right;
3550+ --anchor-v: span-top;
3551+ .dropdown-content {
3552+ top: auto;
3553+ bottom: calc(0.25rem * 0);
3554+ }
3555+ }
3556+ }
3557+ .dropdown-start {
3558+ --anchor-h: span-right;
3559+ :where(.dropdown-content) {
3560+ inset-inline-end: auto;
3561+ }
3562+ &.dropdown-left {
3563+ --anchor-h: left;
3564+ --anchor-v: span-bottom;
3565+ .dropdown-content {
3566+ top: calc(0.25rem * 0);
3567+ bottom: auto;
3568+ }
3569+ }
3570+ &.dropdown-right {
3571+ --anchor-h: right;
3572+ --anchor-v: span-bottom;
3573+ .dropdown-content {
3574+ top: calc(0.25rem * 0);
3575+ bottom: auto;
3576+ }
3577+ }
3578+ }
3579+ .dropdown-bottom {
3580+ --anchor-v: bottom;
3581+ .dropdown-content {
3582+ top: 100%;
3583+ bottom: auto;
3584+ transform-origin: top;
3585+ }
3586+ }
3587+ .dropdown-top {
3588+ --anchor-v: top;
3589+ .dropdown-content {
3590+ top: auto;
3591+ bottom: 100%;
3592+ transform-origin: bottom;
3593+ }
3594+ }
3595+ .top-0 {
3596+ top: calc(var(--spacing) * 0);
3597+ }
3598+ .top-4 {
3599+ top: calc(var(--spacing) * 4);
3600+ }
3601+ .right-4 {
3602+ right: calc(var(--spacing) * 4);
3603+ }
3604+ .dock-sm {
3605+ height: calc(0.25rem * 14);
3606+ height: 3.5rem;
3607+ height: calc(3.5rem + env(safe-area-inset-bottom));
3608+ .dock-active {
3609+ &:after {
3610+ bottom: -0.1rem;
3611+ }
3612+ }
3613+ .dock-label {
3614+ font-size: 0.625rem;
3615+ }
3616+ }
3617+ .dock-lg {
3618+ height: 4.5rem;
3619+ height: calc(4.5rem + env(safe-area-inset-bottom));
3620+ .dock-active {
3621+ &:after {
3622+ bottom: 0.4rem;
3623+ }
3624+ }
3625+ .dock-label {
3626+ font-size: 0.6875rem;
3627+ }
3628+ }
3629+ .dock-xl {
3630+ height: 5rem;
3631+ height: calc(5rem + env(safe-area-inset-bottom));
3632+ .dock-active {
3633+ &:after {
3634+ bottom: 0.4rem;
3635+ }
3636+ }
3637+ .dock-label {
3638+ font-size: 0.75rem;
3639+ }
3640+ }
3641+ .dock-xs {
3642+ height: 3rem;
3643+ height: calc(3rem + env(safe-area-inset-bottom));
3644+ .dock-active {
3645+ &:after {
3646+ bottom: -0.1rem;
3647+ }
3648+ }
3649+ .dock-label {
3650+ font-size: 0.625rem;
3651+ }
3652+ }
3653+ .file-input {
3654+ cursor: pointer;
3655+ cursor: pointer;
3656+ border: var(--border) solid #0000;
3657+ display: inline-flex;
3658+ appearance: none;
3659+ align-items: center;
3660+ background-color: var(--color-base-100);
3661+ vertical-align: middle;
3662+ webkit-user-select: none;
3663+ user-select: none;
3664+ width: clamp(3rem, 20rem, 100%);
3665+ height: var(--size);
3666+ padding-inline-end: 0.75rem;
3667+ font-size: 0.875rem;
3668+ line-height: 2;
3669+ border-start-start-radius: var(--join-ss, var(--radius-field));
3670+ border-start-end-radius: var(--join-se, var(--radius-field));
3671+ border-end-start-radius: var(--join-es, var(--radius-field));
3672+ border-end-end-radius: var(--join-ee, var(--radius-field));
3673+ border-color: var(--input-color);
3674+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
3675+ --size: calc(var(--size-field, 0.25rem) * 10);
3676+ --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
3677+ &::file-selector-button {
3678+ margin-inline-end: calc(0.25rem * 4);
3679+ cursor: pointer;
3680+ padding-inline: calc(0.25rem * 4);
3681+ webkit-user-select: none;
3682+ user-select: none;
3683+ height: calc(100% + var(--border) * 2);
3684+ margin-block: calc(var(--border) * -1);
3685+ margin-inline-start: calc(var(--border) * -1);
3686+ font-size: 0.875rem;
3687+ color: var(--btn-fg);
3688+ border-width: var(--border);
3689+ border-style: solid;
3690+ border-color: var(--btn-border);
3691+ border-start-start-radius: calc(var(--join-ss, var(--radius-field) - var(--border)));
3692+ border-end-start-radius: calc(var(--join-es, var(--radius-field) - var(--border)));
3693+ font-weight: 600;
3694+ background-color: var(--btn-bg);
3695+ background-size: calc(var(--noise) * 100%);
3696+ background-image: var(--btn-noise);
3697+ text-shadow: 0 0.5px oklch(1 0 0 / calc(var(--depth) * 0.15));
3698+ box-shadow: 0 0.5px 0 0.5px color-mix( in oklab, color-mix(in oklab, white 30%, var(--btn-bg)) calc(var(--depth) * 20%), #0000 ) inset, var(--btn-shadow);
3699+ --size: calc(var(--size-field, 0.25rem) * 10);
3700+ --btn-bg: var(--btn-color, var(--color-base-200));
3701+ --btn-fg: var(--color-base-content);
3702+ --btn-border: color-mix(in oklab, var(--btn-bg), #000 5%);
3703+ --btn-shadow: 0 3px 2px -2px color-mix(in oklab, var(--btn-bg) 30%, #0000),
3704+ 0 4px 3px -2px color-mix(in oklab, var(--btn-bg) 30%, #0000);
3705+ --btn-noise: var(--fx-noise);
3706+ }
3707+ &:focus {
3708+ --input-color: var(--color-base-content);
3709+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) 10%, #0000);
3710+ outline: 2px solid var(--input-color);
3711+ outline-offset: 2px;
3712+ isolation: isolate;
3713+ }
3714+ &:has(> input[disabled]), &:is(:disabled, [disabled]) {
3715+ cursor: not-allowed;
3716+ border-color: var(--color-base-200);
3717+ background-color: var(--color-base-200);
3718+ &::placeholder {
3719+ color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
3720+ }
3721+ box-shadow: none;
3722+ color: color-mix(in oklch, var(--color-base-content) 20%, #0000);
3723+ &::file-selector-button {
3724+ cursor: not-allowed;
3725+ border-color: var(--color-base-200);
3726+ background-color: var(--color-base-200);
3727+ --btn-border: #0000;
3728+ --btn-noise: none;
3729+ --btn-fg: color-mix(in oklch, var(--color-base-content) 20%, #0000);
3730+ }
3731+ }
3732+ }
3733+ .hero-content {
3734+ isolation: isolate;
3735+ display: flex;
3736+ max-width: 80rem;
3737+ align-items: center;
3738+ justify-content: center;
3739+ gap: calc(0.25rem * 4);
3740+ padding: calc(0.25rem * 4);
3741+ }
3742+ .textarea {
3743+ border: var(--border) solid #0000;
3744+ min-height: calc(0.25rem * 20);
3745+ flex-shrink: 1;
3746+ appearance: none;
3747+ border-radius: var(--radius-field);
3748+ background-color: var(--color-base-100);
3749+ padding-block: calc(0.25rem * 2);
3750+ vertical-align: middle;
3751+ width: clamp(3rem, 20rem, 100%);
3752+ padding-inline-start: 0.75rem;
3753+ padding-inline-end: 0.75rem;
3754+ font-size: 0.875rem;
3755+ border-color: var(--input-color);
3756+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset;
3757+ --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
3758+ textarea {
3759+ appearance: none;
3760+ background-color: transparent;
3761+ border: none;
3762+ &:focus, &:focus-within {
3763+ --tw-outline-style: none;
3764+ outline-style: none;
3765+ @media (forced-colors: active) {
3766+ outline: 2px solid transparent;
3767+ outline-offset: 2px;
3768+ }
3769+ }
3770+ }
3771+ &:focus, &:focus-within {
3772+ --input-color: var(--color-base-content);
3773+ box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000);
3774+ outline: 2px solid var(--input-color);
3775+ outline-offset: 2px;
3776+ isolation: isolate;
3777+ }
3778+ &:has(> textarea[disabled]), &:is(:disabled, [disabled]) {
3779+ cursor: not-allowed;
3780+ border-color: var(--color-base-200);
3781+ background-color: var(--color-base-200);
3782+ color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
3783+ &::placeholder {
3784+ color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
3785+ }
3786+ box-shadow: none;
3787+ }
3788+ &:has(> textarea[disabled]) > textarea[disabled] {
3789+ cursor: not-allowed;
3790+ }
3791+ }
3792+ .btn-active {
3793+ --btn-bg: color-mix(in oklab, var(--btn-color, var(--color-base-200)), #000 7%);
3794+ --btn-shadow: 0 0 0 0 oklch(0% 0 0/0), 0 0 0 0 oklch(0% 0 0/0);
3795+ isolation: isolate;
3796+ }
3797+ .isolate {
3798+ isolation: isolate;
3799+ }
3800+ .mockup-phone-camera {
3801+ grid-column: 1/1;
3802+ grid-row: 1/1;
3803+ background: #000;
3804+ height: 32px;
3805+ width: 126px;
3806+ border-radius: 17px;
3807+ z-index: 1;
3808+ margin-top: 6px;
3809+ }
3810+ .stack {
3811+ display: inline-grid;
3812+ grid-template-columns: 3px 4px 1fr 4px 3px;
3813+ grid-template-rows: 3px 4px 1fr 4px 3px;
3814+ & > * {
3815+ height: 100%;
3816+ width: 100%;
3817+ &:nth-child(n + 2) {
3818+ width: 100%;
3819+ opacity: 70%;
3820+ }
3821+ &:nth-child(2) {
3822+ z-index: 2;
3823+ opacity: 90%;
3824+ }
3825+ &:nth-child(1) {
3826+ z-index: 3;
3827+ width: 100%;
3828+ }
3829+ }
3830+ &, &.stack-bottom {
3831+ > * {
3832+ grid-column: 3 / 4;
3833+ grid-row: 3 / 6;
3834+ &:nth-child(2) {
3835+ grid-column: 2 / 5;
3836+ grid-row: 2 / 5;
3837+ }
3838+ &:nth-child(1) {
3839+ grid-column: 1 / 6;
3840+ grid-row: 1 / 4;
3841+ }
3842+ }
3843+ }
3844+ &.stack-top {
3845+ > * {
3846+ grid-column: 3 / 4;
3847+ grid-row: 1 / 4;
3848+ &:nth-child(2) {
3849+ grid-column: 2 / 5;
3850+ grid-row: 2 / 5;
3851+ }
3852+ &:nth-child(1) {
3853+ grid-column: 1 / 6;
3854+ grid-row: 3 / 6;
3855+ }
3856+ }
3857+ }
3858+ &.stack-start {
3859+ > * {
3860+ grid-column: 1 / 4;
3861+ grid-row: 3 / 4;
3862+ &:nth-child(2) {
3863+ grid-column: 2 / 5;
3864+ grid-row: 2 / 5;
3865+ }
3866+ &:nth-child(1) {
3867+ grid-column: 3 / 6;
3868+ grid-row: 1 / 6;
3869+ }
3870+ }
3871+ }
3872+ &.stack-end {
3873+ > * {
3874+ grid-column: 3 / 6;
3875+ grid-row: 3 / 4;
3876+ &:nth-child(2) {
3877+ grid-column: 2 / 5;
3878+ grid-row: 2 / 5;
3879+ }
3880+ &:nth-child(1) {
3881+ grid-column: 1 / 4;
3882+ grid-row: 1 / 6;
3883+ }
3884+ }
3885+ }
3886+ }
3887+ .stack\! {
3888+ display: inline-grid !important;
3889+ grid-template-columns: 3px 4px 1fr 4px 3px !important;
3890+ grid-template-rows: 3px 4px 1fr 4px 3px !important;
3891+ & > * {
3892+ height: 100% !important;
3893+ width: 100% !important;
3894+ &:nth-child(n + 2) {
3895+ width: 100% !important;
3896+ opacity: 70% !important;
3897+ }
3898+ &:nth-child(2) {
3899+ z-index: 2 !important;
3900+ opacity: 90% !important;
3901+ }
3902+ &:nth-child(1) {
3903+ z-index: 3 !important;
3904+ width: 100% !important;
3905+ }
3906+ }
3907+ &, &.stack-bottom {
3908+ > * {
3909+ grid-column: 3 / 4 !important;
3910+ grid-row: 3 / 6 !important;
3911+ &:nth-child(2) {
3912+ grid-column: 2 / 5 !important;
3913+ grid-row: 2 / 5 !important;
3914+ }
3915+ &:nth-child(1) {
3916+ grid-column: 1 / 6 !important;
3917+ grid-row: 1 / 4 !important;
3918+ }
3919+ }
3920+ }
3921+ &.stack-top {
3922+ > * {
3923+ grid-column: 3 / 4 !important;
3924+ grid-row: 1 / 4 !important;
3925+ &:nth-child(2) {
3926+ grid-column: 2 / 5 !important;
3927+ grid-row: 2 / 5 !important;
3928+ }
3929+ &:nth-child(1) {
3930+ grid-column: 1 / 6 !important;
3931+ grid-row: 3 / 6 !important;
3932+ }
3933+ }
3934+ }
3935+ &.stack-start {
3936+ > * {
3937+ grid-column: 1 / 4 !important;
3938+ grid-row: 3 / 4 !important;
3939+ &:nth-child(2) {
3940+ grid-column: 2 / 5 !important;
3941+ grid-row: 2 / 5 !important;
3942+ }
3943+ &:nth-child(1) {
3944+ grid-column: 3 / 6 !important;
3945+ grid-row: 1 / 6 !important;
3946+ }
3947+ }
3948+ }
3949+ &.stack-end {
3950+ > * {
3951+ grid-column: 3 / 6 !important;
3952+ grid-row: 3 / 4 !important;
3953+ &:nth-child(2) {
3954+ grid-column: 2 / 5 !important;
3955+ grid-row: 2 / 5 !important;
3956+ }
3957+ &:nth-child(1) {
3958+ grid-column: 1 / 4 !important;
3959+ grid-row: 1 / 6 !important;
3960+ }
3961+ }
3962+ }
3963+ }
3964+ .modal-backdrop {
3965+ grid-column-start: 1;
3966+ grid-row-start: 1;
3967+ display: grid;
3968+ align-self: stretch;
3969+ justify-self: stretch;
3970+ color: transparent;
3971+ z-index: -1;
3972+ button {
3973+ cursor: pointer;
3974+ }
3975+ }
3976+ .z-50 {
3977+ z-index: 50;
3978+ }
3979+ .z-\[1\] {
3980+ z-index: 1;
3981+ }
3982+ .tab-content {
3983+ order: var(--tabcontent-order);
3984+ display: none;
3985+ border-color: transparent;
3986+ --tabcontent-radius-ss: 0;
3987+ --tabcontent-radius-se: 0;
3988+ --tabcontent-radius-es: 0;
3989+ --tabcontent-radius-ee: 0;
3990+ --tabcontent-order: 1;
3991+ width: 100%;
3992+ margin: var(--tabcontent-margin);
3993+ border-width: var(--border);
3994+ border-start-start-radius: var(--tabcontent-radius-ss);
3995+ border-start-end-radius: var(--tabcontent-radius-se);
3996+ border-end-start-radius: var(--tabcontent-radius-es);
3997+ border-end-end-radius: var(--tabcontent-radius-ee);
3998+ }
3999+ .mockup-phone-display {
4000+ grid-column: 1/1;
4001+ grid-row: 1/1;
4002+ overflow: hidden;
4003+ border-radius: 49px;
4004+ width: 390px;
4005+ height: 845px;
4006+ }
4007+ .timeline-end {
4008+ grid-column-start: 1;
4009+ grid-column-end: 4;
4010+ grid-row-start: 3;
4011+ grid-row-end: 4;
4012+ margin: calc(0.25rem * 1);
4013+ align-self: flex-start;
4014+ justify-self: center;
4015+ }
4016+ .timeline-start {
4017+ grid-column-start: 1;
4018+ grid-column-end: 4;
4019+ grid-row-start: 1;
4020+ grid-row-end: 2;
4021+ margin: calc(0.25rem * 1);
4022+ align-self: flex-end;
4023+ justify-self: center;
4024+ }
4025+ .timeline-horizontal {
4026+ flex-direction: row;
4027+ > li {
4028+ align-items: center;
4029+ > hr {
4030+ height: calc(0.25rem * 1);
4031+ width: 100%;
4032+ &:first-child {
4033+ grid-column-start: 1;
4034+ grid-row-start: 2;
4035+ }
4036+ &:last-child {
4037+ grid-column-start: 3;
4038+ grid-column-end: none;
4039+ grid-row-start: 2;
4040+ grid-row-end: auto;
4041+ }
4042+ }
4043+ }
4044+ .timeline-start {
4045+ grid-column-start: 1;
4046+ grid-column-end: 4;
4047+ grid-row-start: 1;
4048+ grid-row-end: 2;
4049+ align-self: flex-end;
4050+ justify-self: center;
4051+ }
4052+ .timeline-end {
4053+ grid-column-start: 1;
4054+ grid-column-end: 4;
4055+ grid-row-start: 3;
4056+ grid-row-end: 4;
4057+ align-self: flex-start;
4058+ justify-self: center;
4059+ }
4060+ &:has(.timeline-middle) {
4061+ > li {
4062+ > hr {
4063+ &:first-child {
4064+ border-start-start-radius: 0;
4065+ border-end-start-radius: 0;
4066+ border-start-end-radius: var(--radius-selector);
4067+ border-end-end-radius: var(--radius-selector);
4068+ }
4069+ &:last-child {
4070+ border-start-start-radius: var(--radius-selector);
4071+ border-end-start-radius: var(--radius-selector);
4072+ border-start-end-radius: 0;
4073+ border-end-end-radius: 0;
4074+ }
4075+ }
4076+ }
4077+ }
4078+ &:not(:has(.timeline-middle)) {
4079+ :first-child {
4080+ > hr:last-child {
4081+ border-start-start-radius: var(--radius-selector);
4082+ border-end-start-radius: var(--radius-selector);
4083+ border-start-end-radius: 0;
4084+ border-end-end-radius: 0;
4085+ }
4086+ }
4087+ :last-child {
4088+ > hr:first-child {
4089+ border-start-start-radius: 0;
4090+ border-end-start-radius: 0;
4091+ border-start-end-radius: var(--radius-selector);
4092+ border-end-end-radius: var(--radius-selector);
4093+ }
4094+ }
4095+ }
4096+ }
4097+ .timeline-vertical {
4098+ flex-direction: column;
4099+ > li {
4100+ justify-items: center;
4101+ --timeline-row-start: minmax(0, 1fr);
4102+ --timeline-row-end: minmax(0, 1fr);
4103+ > hr {
4104+ height: 100%;
4105+ width: calc(0.25rem * 1);
4106+ &:first-child {
4107+ grid-column-start: 2;
4108+ grid-row-start: 1;
4109+ }
4110+ &:last-child {
4111+ grid-column-start: 2;
4112+ grid-column-end: auto;
4113+ grid-row-start: 3;
4114+ grid-row-end: none;
4115+ }
4116+ }
4117+ }
4118+ .timeline-start {
4119+ grid-column-start: 1;
4120+ grid-column-end: 2;
4121+ grid-row-start: 1;
4122+ grid-row-end: 4;
4123+ align-self: center;
4124+ justify-self: flex-end;
4125+ }
4126+ .timeline-end {
4127+ grid-column-start: 3;
4128+ grid-column-end: 4;
4129+ grid-row-start: 1;
4130+ grid-row-end: 4;
4131+ align-self: center;
4132+ justify-self: flex-start;
4133+ }
4134+ &:has(.timeline-middle) {
4135+ > li {
4136+ > hr {
4137+ &:first-child {
4138+ border-top-left-radius: 0;
4139+ border-top-right-radius: 0;
4140+ border-bottom-right-radius: var(--radius-selector);
4141+ border-bottom-left-radius: var(--radius-selector);
4142+ }
4143+ &:last-child {
4144+ border-top-left-radius: var(--radius-selector);
4145+ border-top-right-radius: var(--radius-selector);
4146+ border-bottom-right-radius: 0;
4147+ border-bottom-left-radius: 0;
4148+ }
4149+ }
4150+ }
4151+ }
4152+ &:not(:has(.timeline-middle)) {
4153+ :first-child {
4154+ > hr:last-child {
4155+ border-top-left-radius: var(--radius-selector);
4156+ border-top-right-radius: var(--radius-selector);
4157+ border-bottom-right-radius: 0;
4158+ border-bottom-left-radius: 0;
4159+ }
4160+ }
4161+ :last-child {
4162+ > hr:first-child {
4163+ border-top-left-radius: 0;
4164+ border-top-right-radius: 0;
4165+ border-bottom-right-radius: var(--radius-selector);
4166+ border-bottom-left-radius: var(--radius-selector);
4167+ }
4168+ }
4169+ }
4170+ &.timeline-snap-icon {
4171+ > li {
4172+ --timeline-col-start: minmax(0, 1fr);
4173+ --timeline-row-start: 0.5rem;
4174+ }
4175+ }
4176+ }
4177+ .timeline-compact {
4178+ --timeline-row-start: 0;
4179+ .timeline-start {
4180+ grid-column-start: 1;
4181+ grid-column-end: 4;
4182+ grid-row-start: 3;
4183+ grid-row-end: 4;
4184+ align-self: flex-start;
4185+ justify-self: center;
4186+ }
4187+ li:has(.timeline-start) {
4188+ .timeline-end {
4189+ grid-column-start: none;
4190+ grid-row-start: auto;
4191+ }
4192+ }
4193+ &.timeline-vertical {
4194+ > li {
4195+ --timeline-col-start: 0;
4196+ }
4197+ .timeline-start {
4198+ grid-column-start: 3;
4199+ grid-column-end: 4;
4200+ grid-row-start: 1;
4201+ grid-row-end: 4;
4202+ align-self: center;
4203+ justify-self: flex-start;
4204+ }
4205+ li:has(.timeline-start) {
4206+ .timeline-end {
4207+ grid-column-start: auto;
4208+ grid-row-start: none;
4209+ }
4210+ }
4211+ }
4212+ }
4213+ .stat-figure {
4214+ grid-column-start: 2;
4215+ grid-row: span 3 / span 3;
4216+ grid-row-start: 1;
4217+ place-self: center;
4218+ justify-self: flex-end;
4219+ }
4220+ .hero {
4221+ display: grid;
4222+ width: 100%;
4223+ place-items: center;
4224+ background-size: cover;
4225+ background-position: center;
4226+ & > * {
4227+ grid-column-start: 1;
4228+ grid-row-start: 1;
4229+ }
4230+ }
4231+ .hero-overlay {
4232+ grid-column-start: 1;
4233+ grid-row-start: 1;
4234+ height: 100%;
4235+ width: 100%;
4236+ background-color: color-mix(in oklab, var(--color-neutral) 50%, transparent);
4237+ }
4238+ .modal-box {
4239+ grid-column-start: 1;
4240+ grid-row-start: 1;
4241+ max-height: 100vh;
4242+ width: calc(11/12 * 100%);
4243+ max-width: 32rem;
4244+ background-color: var(--color-base-100);
4245+ padding: calc(0.25rem * 6);
4246+ transition: translate 0.3s ease-out, scale 0.3s ease-out, opacity 0.2s ease-out 0.05s, box-shadow 0.3s ease-out;
4247+ border-top-left-radius: var(--modal-tl, var(--radius-box));
4248+ border-top-right-radius: var(--modal-tr, var(--radius-box));
4249+ border-bottom-left-radius: var(--modal-bl, var(--radius-box));
4250+ border-bottom-right-radius: var(--modal-br, var(--radius-box));
4251+ scale: 95%;
4252+ opacity: 0;
4253+ box-shadow: oklch(0% 0 0/ 0.25) 0px 25px 50px -12px;
4254+ overflow-y: auto;
4255+ overscroll-behavior: contain;
4256+ }
4257+ .drawer-content {
4258+ grid-column-start: 2;
4259+ grid-row-start: 1;
4260+ min-width: calc(0.25rem * 0);
4261+ }
4262+ .timeline-middle {
4263+ grid-column-start: 2;
4264+ grid-row-start: 2;
4265+ }
4266+ .drawer-end {
4267+ grid-auto-columns: auto max-content;
4268+ > .drawer-toggle {
4269+ & ~ .drawer-content {
4270+ grid-column-start: 1;
4271+ }
4272+ & ~ .drawer-side {
4273+ grid-column-start: 2;
4274+ justify-items: end;
4275+ }
4276+ & ~ .drawer-side > *:not(.drawer-overlay) {
4277+ translate: 100%;
4278+ [dir="rtl"] & {
4279+ translate: -100%;
4280+ }
4281+ }
4282+ &:checked ~ .drawer-side > *:not(.drawer-overlay) {
4283+ translate: 0%;
4284+ }
4285+ }
4286+ }
4287+ .stat-value {
4288+ grid-column-start: 1;
4289+ white-space: nowrap;
4290+ font-size: 2rem;
4291+ font-weight: 800;
4292+ }
4293+ .stat-desc {
4294+ grid-column-start: 1;
4295+ white-space: nowrap;
4296+ color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
4297+ font-size: 0.75rem;
4298+ }
4299+ .stat-title {
4300+ grid-column-start: 1;
4301+ white-space: nowrap;
4302+ color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
4303+ font-size: 0.75rem;
4304+ }
4305+ .stat-actions {
4306+ grid-column-start: 1;
4307+ white-space: nowrap;
4308+ }
4309+ .chat-image {
4310+ grid-row: span 2 / span 2;
4311+ align-self: flex-end;
4312+ }
4313+ .chat-footer {
4314+ grid-row-start: 3;
4315+ display: flex;
4316+ gap: calc(0.25rem * 1);
4317+ font-size: 0.6875rem;
4318+ }
4319+ .chat-header {
4320+ grid-row-start: 1;
4321+ display: flex;
4322+ gap: calc(0.25rem * 1);
4323+ font-size: 0.6875rem;
4324+ }
4325+ .list-col-wrap {
4326+ grid-row-start: 2;
4327+ }
4328+ .container {
4329+ width: 100%;
4330+ @media (width >= 40rem) {
4331+ max-width: 40rem;
4332+ }
4333+ @media (width >= 48rem) {
4334+ max-width: 48rem;
4335+ }
4336+ @media (width >= 64rem) {
4337+ max-width: 64rem;
4338+ }
4339+ @media (width >= 80rem) {
4340+ max-width: 80rem;
4341+ }
4342+ @media (width >= 96rem) {
4343+ max-width: 96rem;
4344+ }
4345+ }
4346+ .divider {
4347+ display: flex;
4348+ height: calc(0.25rem * 4);
4349+ flex-direction: row;
4350+ align-items: center;
4351+ align-self: stretch;
4352+ white-space: nowrap;
4353+ margin: var(--divider-m, 1rem 0);
4354+ &:before, &:after {
4355+ content: "";
4356+ height: calc(0.25rem * 0.5);
4357+ width: 100%;
4358+ flex-grow: 1;
4359+ background-color: color-mix(in oklab, var(--color-base-content) 10%, transparent);
4360+ }
4361+ @media print {
4362+ &:before, &:after {
4363+ border: 0.5px solid;
4364+ }
4365+ }
4366+ &:not(:empty) {
4367+ gap: calc(0.25rem * 4);
4368+ }
4369+ }
4370+ .filter {
4371+ display: flex;
4372+ flex-wrap: wrap;
4373+ input[type="radio"] {
4374+ width: auto;
4375+ }
4376+ input {
4377+ overflow: hidden;
4378+ opacity: 100%;
4379+ scale: 1;
4380+ transition: margin 0.1s, opacity 0.3s, padding 0.3s, border-width 0.1s;
4381+ &:not(:last-child) {
4382+ margin-inline-end: calc(0.25rem * 1);
4383+ }
4384+ &.filter-reset {
4385+ aspect-ratio: 1 / 1;
4386+ &::after {
4387+ content: "×";
4388+ }
4389+ }
4390+ }
4391+ &:not(:has(input:checked:not(.filter-reset))) {
4392+ .filter-reset, input[type="reset"] {
4393+ scale: 0;
4394+ border-width: 0;
4395+ margin-inline: calc(0.25rem * 0);
4396+ width: calc(0.25rem * 0);
4397+ padding-inline: calc(0.25rem * 0);
4398+ opacity: 0%;
4399+ }
4400+ }
4401+ &:has(input:checked:not(.filter-reset)) {
4402+ input:not(:checked, .filter-reset, input[type="reset"]) {
4403+ scale: 0;
4404+ border-width: 0;
4405+ margin-inline: calc(0.25rem * 0);
4406+ width: calc(0.25rem * 0);
4407+ padding-inline: calc(0.25rem * 0);
4408+ opacity: 0%;
4409+ }
4410+ }
4411+ }
4412+ .mx-auto {
4413+ margin-inline: auto;
4414+ }
4415+ .file-input-ghost {
4416+ background-color: transparent;
4417+ transition: background-color 0.2s;
4418+ box-shadow: none;
4419+ border-color: #0000;
4420+ &::file-selector-button {
4421+ margin-inline-start: calc(0.25rem * 0);
4422+ margin-inline-end: calc(0.25rem * 4);
4423+ height: 100%;
4424+ cursor: pointer;
4425+ padding-inline: calc(0.25rem * 4);
4426+ webkit-user-select: none;
4427+ user-select: none;
4428+ margin-block: 0;
4429+ border-start-end-radius: calc(var(--join-ss, var(--radius-field) - var(--border)));
4430+ border-end-end-radius: calc(var(--join-es, var(--radius-field) - var(--border)));
4431+ }
4432+ &:focus, &:focus-within {
4433+ background-color: var(--color-base-100);
4434+ color: var(--color-base-content);
4435+ border-color: #0000;
4436+ box-shadow: none;
4437+ }
4438+ }
4439+ .input-lg {
4440+ --size: calc(var(--size-field, 0.25rem) * 12);
4441+ font-size: 1.125rem;
4442+ &[type="number"] {
4443+ &::-webkit-inner-spin-button {
4444+ margin-block: calc(0.25rem * -3);
4445+ margin-inline-end: calc(0.25rem * -3);
4446+ }
4447+ }
4448+ }
4449+ .input-md {
4450+ --size: calc(var(--size-field, 0.25rem) * 10);
4451+ font-size: 0.875rem;
4452+ &[type="number"] {
4453+ &::-webkit-inner-spin-button {
4454+ margin-block: calc(0.25rem * -3);
4455+ margin-inline-end: calc(0.25rem * -3);
4456+ }
4457+ }
4458+ }
4459+ .input-sm {
4460+ --size: calc(var(--size-field, 0.25rem) * 8);
4461+ font-size: 0.75rem;
4462+ &[type="number"] {
4463+ &::-webkit-inner-spin-button {
4464+ margin-block: calc(0.25rem * -2);
4465+ margin-inline-end: calc(0.25rem * -3);
4466+ }
4467+ }
4468+ }
4469+ .input-xl {
4470+ --size: calc(var(--size-field, 0.25rem) * 14);
4471+ font-size: 1.375rem;
4472+ &[type="number"] {
4473+ &::-webkit-inner-spin-button {
4474+ margin-block: calc(0.25rem * -4);
4475+ margin-inline-end: calc(0.25rem * -3);
4476+ }
4477+ }
4478+ }
4479+ .input-xs {
4480+ --size: calc(var(--size-field, 0.25rem) * 6);
4481+ font-size: 0.6875rem;
4482+ &[type="number"] {
4483+ &::-webkit-inner-spin-button {
4484+ margin-block: calc(0.25rem * -1);
4485+ margin-inline-end: calc(0.25rem * -3);
4486+ }
4487+ }
4488+ }
4489+ .my-4 {
4490+ margin-block: calc(var(--spacing) * 4);
4491+ }
4492+ .label {
4493+ display: inline-flex;
4494+ align-items: center;
4495+ gap: calc(0.25rem * 1.5);
4496+ white-space: nowrap;
4497+ color: color-mix(in oklab, currentColor 60%, transparent);
4498+ &:has(input) {
4499+ cursor: pointer;
4500+ }
4501+ &:is(.input > *, .select > *) {
4502+ display: flex;
4503+ height: calc(100% - 0.5rem);
4504+ align-items: center;
4505+ padding-inline: calc(0.25rem * 3);
4506+ white-space: nowrap;
4507+ font-size: inherit;
4508+ &:first-child {
4509+ margin-inline-start: calc(0.25rem * -3);
4510+ margin-inline-end: calc(0.25rem * 3);
4511+ border-inline-end: var(--border) solid color-mix(in oklab, currentColor 10%, #0000);
4512+ }
4513+ &:last-child {
4514+ margin-inline-start: calc(0.25rem * 3);
4515+ margin-inline-end: calc(0.25rem * -3);
4516+ border-inline-start: var(--border) solid color-mix(in oklab, currentColor 10%, #0000);
4517+ }
4518+ }
4519+ }
4520+ .steps-vertical {
4521+ grid-auto-rows: 1fr;
4522+ grid-auto-flow: row;
4523+ .step {
4524+ display: grid;
4525+ grid-template-columns: repeat(2, minmax(0, 1fr));
4526+ grid-template-columns: 40px 1fr;
4527+ grid-template-rows: repeat(1, minmax(0, 1fr));
4528+ grid-template-rows: auto;
4529+ gap: 0.5rem;
4530+ min-height: 4rem;
4531+ justify-items: start;
4532+ &:before {
4533+ height: 100%;
4534+ width: calc(0.25rem * 2);
4535+ translate: -50% -50%;
4536+ margin-inline-start: 50%;
4537+ }
4538+ [dir="rtl"] &:before {
4539+ translate: 50% -50%;
4540+ }
4541+ }
4542+ }
4543+ .steps-horizontal {
4544+ grid-auto-columns: 1fr;
4545+ display: inline-grid;
4546+ grid-auto-flow: column;
4547+ overflow: hidden;
4548+ overflow-x: auto;
4549+ .step {
4550+ display: grid;
4551+ grid-template-columns: repeat(1, minmax(0, 1fr));
4552+ grid-template-columns: auto;
4553+ grid-template-rows: repeat(2, minmax(0, 1fr));
4554+ grid-template-rows: 40px 1fr;
4555+ place-items: center;
4556+ text-align: center;
4557+ min-width: 4rem;
4558+ &:before {
4559+ height: calc(0.25rem * 2);
4560+ width: 100%;
4561+ translate: 0;
4562+ content: "";
4563+ margin-inline-start: -100%;
4564+ }
4565+ [dir="rtl"] &:before {
4566+ translate: 0;
4567+ }
4568+ }
4569+ }
4570+ .join-horizontal {
4571+ flex-direction: row;
4572+ > .join-item:first-child {
4573+ --join-ss: var(--radius-field);
4574+ --join-se: 0;
4575+ --join-es: var(--radius-field);
4576+ --join-ee: 0;
4577+ }
4578+ :first-child:not(:last-child) {
4579+ .join-item {
4580+ --join-ss: var(--radius-field);
4581+ --join-se: 0;
4582+ --join-es: var(--radius-field);
4583+ --join-ee: 0;
4584+ }
4585+ }
4586+ > .join-item:last-child {
4587+ --join-ss: 0;
4588+ --join-se: var(--radius-field);
4589+ --join-es: 0;
4590+ --join-ee: var(--radius-field);
4591+ }
4592+ :last-child:not(:first-child) {
4593+ .join-item {
4594+ --join-ss: 0;
4595+ --join-se: var(--radius-field);
4596+ --join-es: 0;
4597+ --join-ee: var(--radius-field);
4598+ }
4599+ }
4600+ > .join-item:only-child {
4601+ --join-ss: var(--radius-field);
4602+ --join-se: var(--radius-field);
4603+ --join-es: var(--radius-field);
4604+ --join-ee: var(--radius-field);
4605+ }
4606+ :only-child {
4607+ .join-item {
4608+ --join-ss: var(--radius-field);
4609+ --join-se: var(--radius-field);
4610+ --join-es: var(--radius-field);
4611+ --join-ee: var(--radius-field);
4612+ }
4613+ }
4614+ .join-item {
4615+ &:where(*:not(:first-child)) {
4616+ margin-inline-start: calc(var(--border, 1px) * -1);
4617+ margin-block-start: 0;
4618+ }
4619+ }
4620+ }
4621+ .join-vertical {
4622+ flex-direction: column;
4623+ > .join-item:first-child {
4624+ --join-ss: var(--radius-field);
4625+ --join-se: var(--radius-field);
4626+ --join-es: 0;
4627+ --join-ee: 0;
4628+ }
4629+ :first-child:not(:last-child) {
4630+ .join-item {
4631+ --join-ss: var(--radius-field);
4632+ --join-se: var(--radius-field);
4633+ --join-es: 0;
4634+ --join-ee: 0;
4635+ }
4636+ }
4637+ > .join-item:last-child {
4638+ --join-ss: 0;
4639+ --join-se: 0;
4640+ --join-es: var(--radius-field);
4641+ --join-ee: var(--radius-field);
4642+ }
4643+ :last-child:not(:first-child) {
4644+ .join-item {
4645+ --join-ss: 0;
4646+ --join-se: 0;
4647+ --join-es: var(--radius-field);
4648+ --join-ee: var(--radius-field);
4649+ }
4650+ }
4651+ > .join-item:only-child {
4652+ --join-ss: var(--radius-field);
4653+ --join-se: var(--radius-field);
4654+ --join-es: var(--radius-field);
4655+ --join-ee: var(--radius-field);
4656+ }
4657+ :only-child {
4658+ .join-item {
4659+ --join-ss: var(--radius-field);
4660+ --join-se: var(--radius-field);
4661+ --join-es: var(--radius-field);
4662+ --join-ee: var(--radius-field);
4663+ }
4664+ }
4665+ .join-item {
4666+ &:where(*:not(:first-child)) {
4667+ margin-inline-start: 0;
4668+ margin-block-start: calc(var(--border, 1px) * -1);
4669+ }
4670+ }
4671+ }
4672+ .join-item {
4673+ &:where(*:not(:first-child)) {
4674+ margin-inline-start: calc(var(--border, 1px) * -1);
4675+ margin-block-start: 0;
4676+ }
4677+ }
4678+ .modal-action {
4679+ margin-top: calc(0.25rem * 6);
4680+ display: flex;
4681+ justify-content: flex-end;
4682+ gap: calc(0.25rem * 2);
4683+ }
4684+ .validator-hint {
4685+ margin-top: calc(0.25rem * 2);
4686+ font-size: var(--text-xs);
4687+ line-height: var(--tw-leading, var(--text-xs--line-height));
4688+ }
4689+ .mt-2 {
4690+ margin-top: calc(var(--spacing) * 2);
4691+ }
4692+ .mt-4 {
4693+ margin-top: calc(var(--spacing) * 4);
4694+ }
4695+ .mt-8 {
4696+ margin-top: calc(var(--spacing) * 8);
4697+ }
4698+ .mt-16 {
4699+ margin-top: calc(var(--spacing) * 16);
4700+ }
4701+ .breadcrumbs {
4702+ max-width: 100%;
4703+ overflow-x: auto;
4704+ padding-block: calc(0.25rem * 2);
4705+ > menu, > ul, > ol {
4706+ display: flex;
4707+ min-height: min-content;
4708+ align-items: center;
4709+ white-space: nowrap;
4710+ > li {
4711+ display: flex;
4712+ align-items: center;
4713+ > * {
4714+ display: flex;
4715+ cursor: pointer;
4716+ align-items: center;
4717+ gap: calc(0.25rem * 2);
4718+ &:hover {
4719+ @media (hover: hover) {
4720+ text-decoration-line: underline;
4721+ }
4722+ }
4723+ &:focus {
4724+ --tw-outline-style: none;
4725+ outline-style: none;
4726+ @media (forced-colors: active) {
4727+ outline: 2px solid transparent;
4728+ outline-offset: 2px;
4729+ }
4730+ }
4731+ &:focus-visible {
4732+ outline: 2px solid currentColor;
4733+ outline-offset: 2px;
4734+ }
4735+ }
4736+ & + *:before {
4737+ content: "";
4738+ margin-right: calc(0.25rem * 3);
4739+ margin-left: calc(0.25rem * 2);
4740+ display: block;
4741+ height: calc(0.25rem * 1.5);
4742+ width: calc(0.25rem * 1.5);
4743+ opacity: 40%;
4744+ rotate: 45deg;
4745+ border-top: 1px solid;
4746+ border-right: 1px solid;
4747+ background-color: #0000;
4748+ }
4749+ [dir="rtl"] & + *:before {
4750+ rotate: -135deg;
4751+ }
4752+ }
4753+ }
4754+ }
4755+ .mr-1 {
4756+ margin-right: calc(var(--spacing) * 1);
4757+ }
4758+ .mr-2 {
4759+ margin-right: calc(var(--spacing) * 2);
4760+ }
4761+ .fieldset-legend {
4762+ margin-bottom: calc(0.25rem * -1);
4763+ display: flex;
4764+ align-items: center;
4765+ justify-content: space-between;
4766+ gap: calc(0.25rem * 2);
4767+ padding-block: calc(0.25rem * 2);
4768+ color: var(--color-base-content);
4769+ font-weight: 600;
4770+ }
4771+ .footer-title {
4772+ margin-bottom: calc(0.25rem * 2);
4773+ text-transform: uppercase;
4774+ opacity: 60%;
4775+ font-weight: 600;
4776+ }
4777+ .mb-2 {
4778+ margin-bottom: calc(var(--spacing) * 2);
4779+ }
4780+ .mb-4 {
4781+ margin-bottom: calc(var(--spacing) * 4);
4782+ }
4783+ .mb-6 {
4784+ margin-bottom: calc(var(--spacing) * 6);
4785+ }
4786+ .mb-8 {
4787+ margin-bottom: calc(var(--spacing) * 8);
4788+ }
4789+ .ml-2 {
4790+ margin-left: calc(var(--spacing) * 2);
4791+ }
4792+ .ml-6 {
4793+ margin-left: calc(var(--spacing) * 6);
4794+ }
4795+ .ml-10 {
4796+ margin-left: calc(var(--spacing) * 10);
4797+ }
4798+ .carousel-item {
4799+ box-sizing: content-box;
4800+ display: flex;
4801+ flex: none;
4802+ scroll-snap-align: start;
4803+ }
4804+ .status {
4805+ display: inline-block;
4806+ aspect-ratio: 1 / 1;
4807+ width: calc(0.25rem * 2);
4808+ height: calc(0.25rem * 2);
4809+ border-radius: var(--radius-selector);
4810+ background-color: color-mix(in oklab, var(--color-base-content) 20%, transparent);
4811+ background-position: center;
4812+ background-repeat: no-repeat;
4813+ vertical-align: middle;
4814+ color: color-mix(in oklab, var(--color-black) 30%, transparent);
4815+ background-image: radial-gradient( circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), #0000 );
4816+ box-shadow: 0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000);
4817+ }
4818+ .badge {
4819+ display: inline-flex;
4820+ align-items: center;
4821+ justify-content: center;
4822+ gap: calc(0.25rem * 2);
4823+ border-radius: var(--radius-selector);
4824+ vertical-align: middle;
4825+ color: var(--badge-fg);
4826+ border: var(--border) solid var(--badge-color, var(--color-base-200));
4827+ font-size: 0.875rem;
4828+ width: fit-content;
4829+ padding-inline: calc(0.25rem * 3 - var(--border));
4830+ background-size: auto, calc(var(--noise) * 100%);
4831+ background-image: none, var(--fx-noise);
4832+ background-color: var(--badge-bg);
4833+ --badge-bg: var(--badge-color, var(--color-base-100));
4834+ --badge-fg: var(--color-base-content);
4835+ --size: calc(var(--size-selector, 0.25rem) * 6);
4836+ height: var(--size);
4837+ &.badge-outline {
4838+ --badge-fg: var(--badge-color);
4839+ --badge-bg: #0000;
4840+ background-image: none;
4841+ }
4842+ &.badge-dash {
4843+ --badge-fg: var(--badge-color);
4844+ --badge-bg: #0000;
4845+ border-style: dashed;
4846+ background-image: none;
4847+ }
4848+ &.badge-soft {
4849+ color: var(--badge-color, var(--color-base-content));
4850+ background-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 8%, var(--color-base-100) );
4851+ border-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 10%, var(--color-base-100) );
4852+ background-image: none;
4853+ }
4854+ }
4855+ .kbd {
4856+ display: inline-flex;
4857+ align-items: center;
4858+ justify-content: center;
4859+ border-radius: var(--radius-field);
4860+ background-color: var(--color-base-200);
4861+ vertical-align: middle;
4862+ padding-left: 0.5em;
4863+ padding-right: 0.5em;
4864+ border: var(--border) solid color-mix(in srgb, var(--color-base-content) 20%, #0000);
4865+ border-bottom: calc(var(--border) + 1px) solid color-mix(in srgb, var(--color-base-content) 20%, #0000);
4866+ --size: calc(var(--size-selector, 0.25rem) * 6);
4867+ font-size: 0.875rem;
4868+ height: var(--size);
4869+ min-width: var(--size);
4870+ }
4871+ .tabs {
4872+ display: flex;
4873+ flex-wrap: wrap;
4874+ --tabs-height: auto;
4875+ --tabs-direction: row;
4876+ height: var(--tabs-height);
4877+ flex-direction: var(--tabs-direction);
4878+ }
4879+ .navbar {
4880+ display: flex;
4881+ width: 100%;
4882+ align-items: center;
4883+ padding: 0.5rem;
4884+ min-height: 4rem;
4885+ }
4886+ .footer {
4887+ display: grid;
4888+ width: 100%;
4889+ grid-auto-flow: row;
4890+ place-items: start;
4891+ column-gap: calc(0.25rem * 4);
4892+ row-gap: calc(0.25rem * 10);
4893+ font-size: 0.875rem;
4894+ line-height: 1.25rem;
4895+ & > * {
4896+ display: grid;
4897+ place-items: start;
4898+ gap: calc(0.25rem * 2);
4899+ }
4900+ &.footer-center {
4901+ grid-auto-flow: column dense;
4902+ place-items: center;
4903+ text-align: center;
4904+ & > * {
4905+ place-items: center;
4906+ }
4907+ }
4908+ }
4909+ .stat {
4910+ display: inline-grid;
4911+ width: 100%;
4912+ column-gap: calc(0.25rem * 4);
4913+ padding-inline: calc(0.25rem * 6);
4914+ padding-block: calc(0.25rem * 4);
4915+ grid-template-columns: repeat(1, 1fr);
4916+ &:not(:last-child) {
4917+ border-inline-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000);
4918+ border-block-end: none;
4919+ }
4920+ }
4921+ .navbar-end {
4922+ display: inline-flex;
4923+ align-items: center;
4924+ width: 50%;
4925+ justify-content: flex-end;
4926+ }
4927+ .navbar-start {
4928+ display: inline-flex;
4929+ align-items: center;
4930+ width: 50%;
4931+ justify-content: flex-start;
4932+ }
4933+ .card-body {
4934+ display: flex;
4935+ flex: auto;
4936+ flex-direction: column;
4937+ gap: calc(0.25rem * 2);
4938+ padding: var(--card-p, 1.5rem);
4939+ font-size: var(--card-fs, 0.875rem);
4940+ :where(p) {
4941+ flex-grow: 1;
4942+ }
4943+ }
4944+ .navbar-center {
4945+ display: inline-flex;
4946+ align-items: center;
4947+ flex-shrink: 0;
4948+ }
4949+ .fieldset-label {
4950+ display: flex;
4951+ align-items: center;
4952+ gap: calc(0.25rem * 1.5);
4953+ color: color-mix(in oklab, var(--color-base-content) 60%, transparent);
4954+ &:has(input) {
4955+ cursor: pointer;
4956+ }
4957+ }
4958+ .carousel {
4959+ display: inline-flex;
4960+ overflow-x: scroll;
4961+ scroll-snap-type: x mandatory;
4962+ scroll-behavior: smooth;
4963+ scrollbar-width: none;
4964+ &::-webkit-scrollbar {
4965+ display: none;
4966+ }
4967+ }
4968+ .alert {
4969+ display: grid;
4970+ align-items: center;
4971+ gap: calc(0.25rem * 4);
4972+ border-radius: var(--radius-box);
4973+ padding-inline: calc(0.25rem * 4);
4974+ padding-block: calc(0.25rem * 3);
4975+ color: var(--color-base-content);
4976+ background-color: var(--alert-color, var(--color-base-200));
4977+ justify-content: start;
4978+ justify-items: start;
4979+ grid-auto-flow: column;
4980+ grid-template-columns: auto minmax(auto, 1fr);
4981+ text-align: start;
4982+ border: var(--border) solid var(--color-base-200);
4983+ font-size: 0.875rem;
4984+ line-height: 1.25rem;
4985+ background-size: auto, calc(var(--noise) * 100%);
4986+ background-image: none, var(--fx-noise);
4987+ box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px color-mix( in oklab, color-mix(in oklab, #000 20%, var(--alert-color, var(--color-base-200))) calc(var(--depth) * 20%), #0000 ), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08));
4988+ &.alert-outline {
4989+ background-color: transparent;
4990+ color: var(--alert-color);
4991+ box-shadow: none;
4992+ background-image: none;
4993+ }
4994+ &.alert-dash {
4995+ background-color: transparent;
4996+ color: var(--alert-color);
4997+ border-style: dashed;
4998+ box-shadow: none;
4999+ background-image: none;
5000+ }
5001+ &.alert-soft {
5002+ color: var(--alert-color, var(--color-base-content));
5003+ background: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 8%, var(--color-base-100) );
5004+ border-color: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 10%, var(--color-base-100) );
5005+ box-shadow: none;
5006+ background-image: none;
5007+ }
5008+ }
5009+ .fieldset {
5010+ display: grid;
5011+ gap: calc(0.25rem * 1.5);
5012+ padding-block: calc(0.25rem * 1);
5013+ font-size: 0.75rem;
5014+ grid-template-columns: 1fr;
5015+ grid-auto-rows: max-content;
5016+ }
5017+ .card-actions {
5018+ display: flex;
5019+ flex-wrap: wrap;
5020+ align-items: flex-start;
5021+ gap: calc(0.25rem * 2);
5022+ }
5023+ .avatar-placeholder {
5024+ & > div {
5025+ display: flex;
5026+ align-items: center;
5027+ justify-content: center;
5028+ }
5029+ }
5030+ .card-title {
5031+ display: flex;
5032+ align-items: center;
5033+ gap: calc(0.25rem * 2);
5034+ font-size: var(--cardtitle-fs, 1.125rem);
5035+ font-weight: 600;
5036+ }
5037+ .join {
5038+ display: inline-flex;
5039+ align-items: stretch;
5040+ --join-ss: 0;
5041+ --join-se: 0;
5042+ --join-es: 0;
5043+ --join-ee: 0;
5044+ :where(.join-item) {
5045+ border-start-start-radius: var(--join-ss, 0);
5046+ border-start-end-radius: var(--join-se, 0);
5047+ border-end-start-radius: var(--join-es, 0);
5048+ border-end-end-radius: var(--join-ee, 0);
5049+ * {
5050+ --join-ss: var(--radius-field);
5051+ --join-se: var(--radius-field);
5052+ --join-es: var(--radius-field);
5053+ --join-ee: var(--radius-field);
5054+ }
5055+ }
5056+ > .join-item:where(:first-child) {
5057+ --join-ss: var(--radius-field);
5058+ --join-se: 0;
5059+ --join-es: var(--radius-field);
5060+ --join-ee: 0;
5061+ }
5062+ :first-child:not(:last-child) {
5063+ :where(.join-item) {
5064+ --join-ss: var(--radius-field);
5065+ --join-se: 0;
5066+ --join-es: var(--radius-field);
5067+ --join-ee: 0;
5068+ }
5069+ }
5070+ > .join-item:where(:last-child) {
5071+ --join-ss: 0;
5072+ --join-se: var(--radius-field);
5073+ --join-es: 0;
5074+ --join-ee: var(--radius-field);
5075+ }
5076+ :last-child:not(:first-child) {
5077+ :where(.join-item) {
5078+ --join-ss: 0;
5079+ --join-se: var(--radius-field);
5080+ --join-es: 0;
5081+ --join-ee: var(--radius-field);
5082+ }
5083+ }
5084+ > .join-item:where(:only-child) {
5085+ --join-ss: var(--radius-field);
5086+ --join-se: var(--radius-field);
5087+ --join-es: var(--radius-field);
5088+ --join-ee: var(--radius-field);
5089+ }
5090+ :only-child {
5091+ :where(.join-item) {
5092+ --join-ss: var(--radius-field);
5093+ --join-se: var(--radius-field);
5094+ --join-es: var(--radius-field);
5095+ --join-ee: var(--radius-field);
5096+ }
5097+ }
5098+ }
5099+ .mockup-phone {
5100+ display: inline-grid;
5101+ justify-items: center;
5102+ border: 6px solid #6b6b6b;
5103+ border-radius: 65px;
5104+ background-color: #000;
5105+ padding: 11px;
5106+ overflow: hidden;
5107+ }
5108+ .chat {
5109+ display: grid;
5110+ column-gap: calc(0.25rem * 3);
5111+ padding-block: calc(0.25rem * 1);
5112+ }
5113+ .avatar-group {
5114+ display: flex;
5115+ overflow: hidden;
5116+ :where(.avatar) {
5117+ overflow: hidden;
5118+ border-radius: calc(infinity * 1px);
5119+ border: 4px solid var(--color-base-100);
5120+ }
5121+ }
5122+ .prose {
5123+ :root & {
5124+ --tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, #0000);
5125+ --tw-prose-headings: var(--color-base-content);
5126+ --tw-prose-lead: var(--color-base-content);
5127+ --tw-prose-links: var(--color-base-content);
5128+ --tw-prose-bold: var(--color-base-content);
5129+ --tw-prose-counters: var(--color-base-content);
5130+ --tw-prose-bullets: color-mix(in oklab, var(--color-base-content) 50%, #0000);
5131+ --tw-prose-hr: color-mix(in oklab, var(--color-base-content) 20%, #0000);
5132+ --tw-prose-quotes: var(--color-base-content);
5133+ --tw-prose-quote-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000);
5134+ --tw-prose-captions: color-mix(in oklab, var(--color-base-content) 50%, #0000);
5135+ --tw-prose-code: var(--color-base-content);
5136+ --tw-prose-pre-code: var(--color-neutral-content);
5137+ --tw-prose-pre-bg: var(--color-neutral);
5138+ --tw-prose-th-borders: color-mix(in oklab, var(--color-base-content) 50%, #0000);
5139+ --tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000);
5140+ --tw-prose-kbd: color-mix(in oklab, var(--color-base-content) 80%, #0000);
5141+ :where(code):not(pre > code) {
5142+ background-color: var(--color-base-200);
5143+ border-radius: var(--radius-selector);
5144+ border: var(--border) solid var(--color-base-300);
5145+ padding-inline: 0.5em;
5146+ font-weight: inherit;
5147+ &:before,
5148+ &:after {
5149+ display: none;
5150+ }
5151+ }
5152+ }
5153+ }
5154+ .mask {
5155+ display: inline-block;
5156+ vertical-align: middle;
5157+ mask-size: contain;
5158+ mask-repeat: no-repeat;
5159+ mask-position: center;
5160+ }
5161+ .block {
5162+ display: block;
5163+ }
5164+ .contents {
5165+ display: contents;
5166+ }
5167+ .flex {
5168+ display: flex;
5169+ }
5170+ .grid {
5171+ display: grid;
5172+ }
5173+ .hidden {
5174+ display: none;
5175+ }
5176+ .inline {
5177+ display: inline;
5178+ }
5179+ .inline-block {
5180+ display: inline-block;
5181+ }
5182+ .inline-flex {
5183+ display: inline-flex;
5184+ }
5185+ .inline-grid {
5186+ display: inline-grid;
5187+ }
5188+ .table {
5189+ display: table;
5190+ }
5191+ .modal-bottom {
5192+ place-items: end;
5193+ :where(.modal-box) {
5194+ height: auto;
5195+ width: 100%;
5196+ max-width: none;
5197+ max-height: calc(100vh - 5em);
5198+ translate: 0 100%;
5199+ scale: 1;
5200+ --modal-tl: var(--radius-box);
5201+ --modal-tr: var(--radius-box);
5202+ --modal-bl: 0;
5203+ --modal-br: 0;
5204+ }
5205+ }
5206+ .modal-end {
5207+ place-items: end;
5208+ :where(.modal-box) {
5209+ height: 100vh;
5210+ max-height: none;
5211+ width: auto;
5212+ max-width: none;
5213+ translate: 100% 0;
5214+ scale: 1;
5215+ --modal-tl: var(--radius-box);
5216+ --modal-tr: 0;
5217+ --modal-bl: var(--radius-box);
5218+ --modal-br: 0;
5219+ }
5220+ }
5221+ .modal-middle {
5222+ place-items: center;
5223+ :where(.modal-box) {
5224+ height: auto;
5225+ width: calc(11/12 * 100%);
5226+ max-width: 32rem;
5227+ max-height: calc(100vh - 5em);
5228+ translate: 0 2%;
5229+ scale: 98%;
5230+ --modal-tl: var(--radius-box);
5231+ --modal-tr: var(--radius-box);
5232+ --modal-bl: var(--radius-box);
5233+ --modal-br: var(--radius-box);
5234+ }
5235+ }
5236+ .modal-start {
5237+ place-items: start;
5238+ :where(.modal-box) {
5239+ height: 100vh;
5240+ max-height: none;
5241+ width: auto;
5242+ max-width: none;
5243+ translate: -100% 0;
5244+ scale: 1;
5245+ --modal-tl: 0;
5246+ --modal-tr: var(--radius-box);
5247+ --modal-bl: 0;
5248+ --modal-br: var(--radius-box);
5249+ }
5250+ }
5251+ .modal-top {
5252+ place-items: start;
5253+ :where(.modal-box) {
5254+ height: auto;
5255+ width: 100%;
5256+ max-width: none;
5257+ max-height: calc(100vh - 5em);
5258+ translate: 0 -100%;
5259+ scale: 1;
5260+ --modal-tl: 0;
5261+ --modal-tr: 0;
5262+ --modal-bl: var(--radius-box);
5263+ --modal-br: var(--radius-box);
5264+ }
5265+ }
5266+ .card-side {
5267+ align-items: stretch;
5268+ flex-direction: row;
5269+ :where(figure:first-child) {
5270+ overflow: hidden;
5271+ border-start-start-radius: inherit;
5272+ border-start-end-radius: unset;
5273+ border-end-start-radius: inherit;
5274+ border-end-end-radius: unset;
5275+ }
5276+ :where(figure:last-child) {
5277+ overflow: hidden;
5278+ border-start-start-radius: unset;
5279+ border-start-end-radius: inherit;
5280+ border-end-start-radius: unset;
5281+ border-end-end-radius: inherit;
5282+ }
5283+ figure > * {
5284+ max-width: unset;
5285+ }
5286+ :where(figure > *) {
5287+ width: 100%;
5288+ height: 100%;
5289+ object-fit: cover;
5290+ }
5291+ }
5292+ .divider-horizontal {
5293+ --divider-m: 0 1rem;
5294+ &.divider {
5295+ height: auto;
5296+ width: calc(0.25rem * 4);
5297+ flex-direction: column;
5298+ &:before {
5299+ height: 100%;
5300+ width: calc(0.25rem * 0.5);
5301+ }
5302+ &:after {
5303+ height: 100%;
5304+ width: calc(0.25rem * 0.5);
5305+ }
5306+ }
5307+ }
5308+ .divider-vertical {
5309+ --divider-m: 1rem 0;
5310+ &.divider {
5311+ height: calc(0.25rem * 4);
5312+ width: auto;
5313+ flex-direction: row;
5314+ &:before {
5315+ height: calc(0.25rem * 0.5);
5316+ width: 100%;
5317+ }
5318+ &:after {
5319+ height: calc(0.25rem * 0.5);
5320+ width: 100%;
5321+ }
5322+ }
5323+ }
5324+ .btn-circle {
5325+ border-radius: calc(infinity * 1px);
5326+ padding-inline: calc(0.25rem * 0);
5327+ width: var(--size);
5328+ height: var(--size);
5329+ }
5330+ .btn-square {
5331+ padding-inline: calc(0.25rem * 0);
5332+ width: var(--size);
5333+ height: var(--size);
5334+ }
5335+ .size-4 {
5336+ width: calc(var(--spacing) * 4);
5337+ height: calc(var(--spacing) * 4);
5338+ }
5339+ .status-lg {
5340+ width: calc(0.25rem * 3);
5341+ height: calc(0.25rem * 3);
5342+ }
5343+ .status-md {
5344+ width: calc(0.25rem * 2);
5345+ height: calc(0.25rem * 2);
5346+ }
5347+ .status-sm {
5348+ width: calc(0.25rem * 1);
5349+ height: calc(0.25rem * 1);
5350+ }
5351+ .status-xl {
5352+ width: calc(0.25rem * 4);
5353+ height: calc(0.25rem * 4);
5354+ }
5355+ .status-xs {
5356+ width: calc(0.25rem * 0.5);
5357+ height: calc(0.25rem * 0.5);
5358+ }
5359+ .tabs-lg {
5360+ :where(.tab) {
5361+ height: calc(var(--size-field, 0.25rem) * 12);
5362+ font-size: 1.125rem;
5363+ --tab-p: 1rem;
5364+ --tab-radius-min: calc(1.5rem - var(--border));
5365+ }
5366+ }
5367+ .tabs-md {
5368+ :where(.tab) {
5369+ height: calc(var(--size-field, 0.25rem) * 10);
5370+ font-size: 0.875rem;
5371+ --tab-p: 0.75rem;
5372+ --tab-radius-min: calc(0.75rem - var(--border));
5373+ }
5374+ }
5375+ .tabs-sm {
5376+ :where(.tab) {
5377+ height: calc(var(--size-field, 0.25rem) * 8);
5378+ font-size: 0.875rem;
5379+ --tab-p: 0.5rem;
5380+ --tab-radius-min: calc(0.5rem - var(--border));
5381+ }
5382+ }
5383+ .tabs-xl {
5384+ :where(.tab) {
5385+ height: calc(var(--size-field, 0.25rem) * 14);
5386+ font-size: 1.125rem;
5387+ --tab-p: 1.25rem;
5388+ --tab-radius-min: calc(2rem - var(--border));
5389+ }
5390+ }
5391+ .tabs-xs {
5392+ :where(.tab) {
5393+ height: calc(var(--size-field, 0.25rem) * 6);
5394+ font-size: 0.75rem;
5395+ --tab-p: 0.375rem;
5396+ --tab-radius-min: calc(0.5rem - var(--border));
5397+ }
5398+ }
5399+ .dock-md {
5400+ height: 4rem;
5401+ height: calc(4rem + env(safe-area-inset-bottom));
5402+ .dock-label {
5403+ font-size: 0.6875rem;
5404+ }
5405+ }
5406+ .h-5 {
5407+ height: calc(var(--spacing) * 5);
5408+ }
5409+ .h-6 {
5410+ height: calc(var(--spacing) * 6);
5411+ }
5412+ .h-8 {
5413+ height: calc(var(--spacing) * 8);
5414+ }
5415+ .h-32 {
5416+ height: calc(var(--spacing) * 32);
5417+ }
5418+ .h-44 {
5419+ height: calc(var(--spacing) * 44);
5420+ }
5421+ .h-64 {
5422+ height: calc(var(--spacing) * 64);
5423+ }
5424+ .h-96 {
5425+ height: calc(var(--spacing) * 96);
5426+ }
5427+ .h-full {
5428+ height: 100%;
5429+ }
5430+ .min-h-screen {
5431+ min-height: 100vh;
5432+ }
5433+ .btn-wide {
5434+ width: 100%;
5435+ max-width: calc(0.25rem * 64);
5436+ }
5437+ .cally {
5438+ font-size: 0.7rem;
5439+ &::part(container) {
5440+ padding: 0.5rem 1rem;
5441+ user-select: none;
5442+ }
5443+ ::part(th) {
5444+ font-weight: normal;
5445+ block-size: auto;
5446+ }
5447+ &::part(header) {
5448+ direction: ltr;
5449+ }
5450+ ::part(head) {
5451+ opacity: 0.5;
5452+ font-size: 0.7rem;
5453+ }
5454+ &::part(button) {
5455+ border-radius: var(--radius-field);
5456+ border: none;
5457+ padding: 0.5rem;
5458+ background: #0000;
5459+ }
5460+ &::part(button):hover {
5461+ background: var(--color-base-200);
5462+ }
5463+ ::part(day) {
5464+ border-radius: var(--radius-field);
5465+ font-size: 0.7rem;
5466+ }
5467+ ::part(button day today) {
5468+ background: var(--color-primary);
5469+ color: var(--color-primary-content);
5470+ }
5471+ ::part(selected) {
5472+ color: var(--color-base-100);
5473+ background: var(--color-base-content);
5474+ border-radius: var(--radius-field);
5475+ }
5476+ ::part(range-inner) {
5477+ border-radius: 0;
5478+ }
5479+ ::part(range-start) {
5480+ border-start-end-radius: 0;
5481+ border-end-end-radius: 0;
5482+ }
5483+ ::part(range-end) {
5484+ border-start-start-radius: 0;
5485+ border-end-start-radius: 0;
5486+ }
5487+ ::part(range-start range-end) {
5488+ border-radius: var(--radius-field);
5489+ }
5490+ calendar-month {
5491+ width: 100%;
5492+ }
5493+ }
5494+ .rating-half {
5495+ :where(*:not(.rating-hidden)) {
5496+ width: calc(0.25rem * 3);
5497+ }
5498+ &.rating-xs *:not(.rating-hidden) {
5499+ width: calc(0.25rem * 2);
5500+ }
5501+ &.rating-sm *:not(.rating-hidden) {
5502+ width: calc(0.25rem * 2.5);
5503+ }
5504+ &.rating-md *:not(.rating-hidden) {
5505+ width: calc(0.25rem * 3);
5506+ }
5507+ &.rating-lg *:not(.rating-hidden) {
5508+ width: .875rem;
5509+ }
5510+ &.rating-xl *:not(.rating-hidden) {
5511+ width: calc(0.25rem * 4);
5512+ }
5513+ }
5514+ .btn-block {
5515+ width: 100%;
5516+ }
5517+ .loading-lg {
5518+ width: calc(var(--size-selector, 0.25rem) * 7);
5519+ }
5520+ .loading-md {
5521+ width: calc(var(--size-selector, 0.25rem) * 6);
5522+ }
5523+ .loading-sm {
5524+ width: calc(var(--size-selector, 0.25rem) * 5);
5525+ }
5526+ .loading-xl {
5527+ width: calc(var(--size-selector, 0.25rem) * 8);
5528+ }
5529+ .loading-xs {
5530+ width: calc(var(--size-selector, 0.25rem) * 4);
5531+ }
5532+ .w-2\/3 {
5533+ width: calc(2/3 * 100%);
5534+ }
5535+ .w-5 {
5536+ width: calc(var(--spacing) * 5);
5537+ }
5538+ .w-6 {
5539+ width: calc(var(--spacing) * 6);
5540+ }
5541+ .w-8 {
5542+ width: calc(var(--spacing) * 8);
5543+ }
5544+ .w-40 {
5545+ width: calc(var(--spacing) * 40);
5546+ }
5547+ .w-52 {
5548+ width: calc(var(--spacing) * 52);
5549+ }
5550+ .w-64 {
5551+ width: calc(var(--spacing) * 64);
5552+ }
5553+ .w-100 {
5554+ width: calc(var(--spacing) * 100);
5555+ }
5556+ .w-full {
5557+ width: 100%;
5558+ }
5559+ .max-w-md {
5560+ max-width: var(--container-md);
5561+ }
5562+ .flex-1 {
5563+ flex: 1;
5564+ }
5565+ .flex-shrink {
5566+ flex-shrink: 1;
5567+ }
5568+ .shrink-0 {
5569+ flex-shrink: 0;
5570+ }
5571+ .flex-grow {
5572+ flex-grow: 1;
5573+ }
5574+ .border-collapse {
5575+ border-collapse: collapse;
5576+ }
5577+ .swap-rotate {
5578+ .swap-on, input:indeterminate ~ .swap-on {
5579+ rotate: 45deg;
5580+ }
5581+ input:is(:checked, :indeterminate) ~ .swap-on, &.swap-active .swap-on {
5582+ rotate: 0deg;
5583+ }
5584+ input:is(:checked, :indeterminate) ~ .swap-off, &.swap-active .swap-off {
5585+ rotate: calc(45deg * -1);
5586+ }
5587+ }
5588+ .swap-flip {
5589+ transform-style: preserve-3d;
5590+ perspective: 20rem;
5591+ .swap-on, .swap-indeterminate, input:indeterminate ~ .swap-on {
5592+ transform: rotateY(180deg);
5593+ backface-visibility: hidden;
5594+ opacity: 100%;
5595+ }
5596+ input:is(:checked, :indeterminate) ~ .swap-on, &.swap-active .swap-on {
5597+ transform: rotateY(0deg);
5598+ }
5599+ input:is(:checked, :indeterminate) ~ .swap-off, &.swap-active .swap-off {
5600+ transform: rotateY(-180deg);
5601+ backface-visibility: hidden;
5602+ opacity: 100%;
5603+ }
5604+ }
5605+ .transform {
5606+ transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
5607+ }
5608+ .skeleton {
5609+ border-radius: var(--radius-box);
5610+ background-color: var(--color-base-300);
5611+ @media (prefers-reduced-motion: reduce) {
5612+ transition-duration: 15s;
5613+ }
5614+ will-change: background-position;
5615+ animation: skeleton 1.8s ease-in-out infinite;
5616+ background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% );
5617+ background-size: 200% auto;
5618+ background-repeat: no-repeat;
5619+ background-position-x: -50%;
5620+ }
5621+ .animate-spin {
5622+ animation: var(--animate-spin);
5623+ }
5624+ .link {
5625+ cursor: pointer;
5626+ text-decoration-line: underline;
5627+ &:focus {
5628+ --tw-outline-style: none;
5629+ outline-style: none;
5630+ @media (forced-colors: active) {
5631+ outline: 2px solid transparent;
5632+ outline-offset: 2px;
5633+ }
5634+ }
5635+ &:focus-visible {
5636+ outline: 2px solid currentColor;
5637+ outline-offset: 2px;
5638+ }
5639+ }
5640+ .cursor-pointer {
5641+ cursor: pointer;
5642+ }
5643+ .resize {
5644+ resize: both;
5645+ }
5646+ .carousel-horizontal {
5647+ flex-direction: row;
5648+ overflow-x: scroll;
5649+ scroll-snap-type: x mandatory;
5650+ }
5651+ .carousel-vertical {
5652+ flex-direction: column;
5653+ overflow-y: scroll;
5654+ scroll-snap-type: y mandatory;
5655+ }
5656+ .carousel-center {
5657+ .carousel-item {
5658+ scroll-snap-align: center;
5659+ }
5660+ }
5661+ .carousel-end {
5662+ .carousel-item {
5663+ scroll-snap-align: end;
5664+ }
5665+ }
5666+ .carousel-start {
5667+ .carousel-item {
5668+ scroll-snap-align: start;
5669+ }
5670+ }
5671+ .list-disc {
5672+ list-style-type: disc;
5673+ }
5674+ .alert-horizontal {
5675+ justify-content: start;
5676+ justify-items: start;
5677+ grid-auto-flow: column;
5678+ grid-template-columns: auto minmax(auto, 1fr);
5679+ text-align: start;
5680+ }
5681+ .alert-vertical {
5682+ justify-content: center;
5683+ justify-items: center;
5684+ grid-auto-flow: row;
5685+ grid-template-columns: auto;
5686+ text-align: center;
5687+ }
5688+ .stats-horizontal {
5689+ grid-auto-flow: column;
5690+ overflow-x: auto;
5691+ .stat:not(:last-child) {
5692+ border-inline-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000);
5693+ border-block-end: none;
5694+ }
5695+ }
5696+ .stats-vertical {
5697+ grid-auto-flow: row;
5698+ overflow-y: auto;
5699+ .stat:not(:last-child) {
5700+ border-inline-end: none;
5701+ border-block-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000);
5702+ }
5703+ }
5704+ .footer-horizontal {
5705+ grid-auto-flow: column;
5706+ &.footer-center {
5707+ grid-auto-flow: row dense;
5708+ }
5709+ }
5710+ .footer-vertical {
5711+ grid-auto-flow: row;
5712+ &.footer-center {
5713+ grid-auto-flow: column dense;
5714+ }
5715+ }
5716+ .grid-flow-col {
5717+ grid-auto-flow: column;
5718+ }
5719+ .grid-cols-1 {
5720+ grid-template-columns: repeat(1, minmax(0, 1fr));
5721+ }
5722+ .flex-col {
5723+ flex-direction: column;
5724+ }
5725+ .flex-wrap {
5726+ flex-wrap: wrap;
5727+ }
5728+ .content-center {
5729+ align-content: center;
5730+ }
5731+ .items-center {
5732+ align-items: center;
5733+ }
5734+ .items-start {
5735+ align-items: flex-start;
5736+ }
5737+ .justify-between {
5738+ justify-content: space-between;
5739+ }
5740+ .justify-center {
5741+ justify-content: center;
5742+ }
5743+ .justify-end {
5744+ justify-content: flex-end;
5745+ }
5746+ .justify-start {
5747+ justify-content: flex-start;
5748+ }
5749+ .gap-2 {
5750+ gap: calc(var(--spacing) * 2);
5751+ }
5752+ .gap-3 {
5753+ gap: calc(var(--spacing) * 3);
5754+ }
5755+ .gap-4 {
5756+ gap: calc(var(--spacing) * 4);
5757+ }
5758+ .space-y-2 {
5759+ :where(& > :not(:last-child)) {
5760+ --tw-space-y-reverse: 0;
5761+ margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));
5762+ margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)));
5763+ }
5764+ }
5765+ .space-y-4 {
5766+ :where(& > :not(:last-child)) {
5767+ --tw-space-y-reverse: 0;
5768+ margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));
5769+ margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)));
5770+ }
5771+ }
5772+ .tabs-box {
5773+ background-color: var(--color-base-200);
5774+ padding: calc(0.25rem * 1);
5775+ border-radius: calc( var(--radius-field) + min(0.25rem, calc(var(--radius-field) + var(--radius-field) + var(--radius-field))) );
5776+ box-shadow: 0 -0.5px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 0.5px oklch(0% 0 0 / calc(var(--depth) * 0.05)) inset;
5777+ .tab {
5778+ border-radius: var(--radius-field);
5779+ border-style: none;
5780+ &:focus-visible, &:is(label:has(:checked:focus-visible)) {
5781+ outline-offset: 2px;
5782+ }
5783+ }
5784+ :is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), :is(input:checked), :is(label:has(:checked)) {
5785+ background-color: var(--tab-bg, var(--color-base-100));
5786+ box-shadow: 0 1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px 1px -1px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 50%), #0000), 0 1px 6px -4px color-mix(in oklab, var(--color-neutral) calc(var(--depth) * 100%), #0000);
5787+ @media (forced-colors: active) {
5788+ border: 1px solid;
5789+ }
5790+ }
5791+ }
5792+ .timeline-box {
5793+ border: var(--border) solid;
5794+ border-radius: var(--radius-box);
5795+ border-color: var(--color-base-300);
5796+ background-color: var(--color-base-100);
5797+ padding-inline: calc(0.25rem * 4);
5798+ padding-block: calc(0.25rem * 2);
5799+ font-size: 0.75rem;
5800+ box-shadow: 0 1px 2px 0 oklch(0% 0 0/0.05);
5801+ }
5802+ .menu-lg {
5803+ :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
5804+ border-radius: var(--radius-field);
5805+ padding-inline: calc(0.25rem * 4);
5806+ padding-block: calc(0.25rem * 1.5);
5807+ font-size: 1.125rem;
5808+ }
5809+ .menu-title {
5810+ padding-inline: calc(0.25rem * 6);
5811+ padding-block: calc(0.25rem * 3);
5812+ }
5813+ }
5814+ .menu-md {
5815+ :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
5816+ border-radius: var(--radius-field);
5817+ padding-inline: calc(0.25rem * 3);
5818+ padding-block: calc(0.25rem * 1.5);
5819+ font-size: 0.875rem;
5820+ }
5821+ .menu-title {
5822+ padding-inline: calc(0.25rem * 3);
5823+ padding-block: calc(0.25rem * 2);
5824+ }
5825+ }
5826+ .menu-sm {
5827+ :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
5828+ border-radius: var(--radius-field);
5829+ padding-inline: calc(0.25rem * 2.5);
5830+ padding-block: calc(0.25rem * 1);
5831+ font-size: 0.75rem;
5832+ }
5833+ .menu-title {
5834+ padding-inline: calc(0.25rem * 3);
5835+ padding-block: calc(0.25rem * 2);
5836+ }
5837+ }
5838+ .menu-xl {
5839+ :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
5840+ border-radius: var(--radius-field);
5841+ padding-inline: calc(0.25rem * 5);
5842+ padding-block: calc(0.25rem * 1.5);
5843+ font-size: 1.375rem;
5844+ }
5845+ .menu-title {
5846+ padding-inline: calc(0.25rem * 6);
5847+ padding-block: calc(0.25rem * 3);
5848+ }
5849+ }
5850+ .menu-xs {
5851+ :where(li:not(.menu-title) > *:not(ul, details, .menu-title)), :where(li:not(.menu-title) > details > summary:not(.menu-title)) {
5852+ border-radius: var(--radius-field);
5853+ padding-inline: calc(0.25rem * 2);
5854+ padding-block: calc(0.25rem * 1);
5855+ font-size: 0.6875rem;
5856+ }
5857+ .menu-title {
5858+ padding-inline: calc(0.25rem * 2);
5859+ padding-block: calc(0.25rem * 1);
5860+ }
5861+ }
5862+ .rounded-box {
5863+ border-radius: var(--radius-box);
5864+ }
5865+ .rounded-box {
5866+ border-radius: var(--radius-box);
5867+ }
5868+ .rounded-field {
5869+ border-radius: var(--radius-field);
5870+ }
5871+ .rounded-field {
5872+ border-radius: var(--radius-field);
5873+ }
5874+ .rounded-full {
5875+ border-radius: calc(infinity * 1px);
5876+ }
5877+ .rounded-md {
5878+ border-radius: var(--radius-md);
5879+ }
5880+ .rounded-selector {
5881+ border-radius: var(--radius-selector);
5882+ }
5883+ .rounded-selector {
5884+ border-radius: var(--radius-selector);
5885+ }
5886+ .rounded-sm {
5887+ border-radius: var(--radius-sm);
5888+ }
5889+ .rounded-t-box {
5890+ border-top-left-radius: var(--radius-box);
5891+ border-top-right-radius: var(--radius-box);
5892+ }
5893+ .rounded-t-box {
5894+ border-top-left-radius: var(--radius-box);
5895+ border-top-right-radius: var(--radius-box);
5896+ }
5897+ .rounded-t-field {
5898+ border-top-left-radius: var(--radius-field);
5899+ border-top-right-radius: var(--radius-field);
5900+ }
5901+ .rounded-t-field {
5902+ border-top-left-radius: var(--radius-field);
5903+ border-top-right-radius: var(--radius-field);
5904+ }
5905+ .rounded-t-selector {
5906+ border-top-left-radius: var(--radius-selector);
5907+ border-top-right-radius: var(--radius-selector);
5908+ }
5909+ .rounded-t-selector {
5910+ border-top-left-radius: var(--radius-selector);
5911+ border-top-right-radius: var(--radius-selector);
5912+ }
5913+ .rounded-l-box {
5914+ border-top-left-radius: var(--radius-box);
5915+ border-bottom-left-radius: var(--radius-box);
5916+ }
5917+ .rounded-l-box {
5918+ border-top-left-radius: var(--radius-box);
5919+ border-bottom-left-radius: var(--radius-box);
5920+ }
5921+ .rounded-l-field {
5922+ border-top-left-radius: var(--radius-field);
5923+ border-bottom-left-radius: var(--radius-field);
5924+ }
5925+ .rounded-l-field {
5926+ border-top-left-radius: var(--radius-field);
5927+ border-bottom-left-radius: var(--radius-field);
5928+ }
5929+ .rounded-l-selector {
5930+ border-top-left-radius: var(--radius-selector);
5931+ border-bottom-left-radius: var(--radius-selector);
5932+ }
5933+ .rounded-l-selector {
5934+ border-top-left-radius: var(--radius-selector);
5935+ border-bottom-left-radius: var(--radius-selector);
5936+ }
5937+ .rounded-tl-box {
5938+ border-top-left-radius: var(--radius-box);
5939+ }
5940+ .rounded-tl-box {
5941+ border-top-left-radius: var(--radius-box);
5942+ }
5943+ .rounded-tl-field {
5944+ border-top-left-radius: var(--radius-field);
5945+ }
5946+ .rounded-tl-field {
5947+ border-top-left-radius: var(--radius-field);
5948+ }
5949+ .rounded-tl-selector {
5950+ border-top-left-radius: var(--radius-selector);
5951+ }
5952+ .rounded-tl-selector {
5953+ border-top-left-radius: var(--radius-selector);
5954+ }
5955+ .rounded-r-box {
5956+ border-top-right-radius: var(--radius-box);
5957+ border-bottom-right-radius: var(--radius-box);
5958+ }
5959+ .rounded-r-box {
5960+ border-top-right-radius: var(--radius-box);
5961+ border-bottom-right-radius: var(--radius-box);
5962+ }
5963+ .rounded-r-field {
5964+ border-top-right-radius: var(--radius-field);
5965+ border-bottom-right-radius: var(--radius-field);
5966+ }
5967+ .rounded-r-field {
5968+ border-top-right-radius: var(--radius-field);
5969+ border-bottom-right-radius: var(--radius-field);
5970+ }
5971+ .rounded-r-selector {
5972+ border-top-right-radius: var(--radius-selector);
5973+ border-bottom-right-radius: var(--radius-selector);
5974+ }
5975+ .rounded-r-selector {
5976+ border-top-right-radius: var(--radius-selector);
5977+ border-bottom-right-radius: var(--radius-selector);
5978+ }
5979+ .rounded-tr-box {
5980+ border-top-right-radius: var(--radius-box);
5981+ }
5982+ .rounded-tr-box {
5983+ border-top-right-radius: var(--radius-box);
5984+ }
5985+ .rounded-tr-field {
5986+ border-top-right-radius: var(--radius-field);
5987+ }
5988+ .rounded-tr-field {
5989+ border-top-right-radius: var(--radius-field);
5990+ }
5991+ .rounded-tr-selector {
5992+ border-top-right-radius: var(--radius-selector);
5993+ }
5994+ .rounded-tr-selector {
5995+ border-top-right-radius: var(--radius-selector);
5996+ }
5997+ .rounded-b-box {
5998+ border-bottom-left-radius: var(--radius-box);
5999+ border-bottom-right-radius: var(--radius-box);
6000+ }
6001+ .rounded-b-box {
6002+ border-bottom-right-radius: var(--radius-box);
6003+ border-bottom-left-radius: var(--radius-box);
6004+ }
6005+ .rounded-b-field {
6006+ border-bottom-left-radius: var(--radius-field);
6007+ border-bottom-right-radius: var(--radius-field);
6008+ }
6009+ .rounded-b-field {
6010+ border-bottom-right-radius: var(--radius-field);
6011+ border-bottom-left-radius: var(--radius-field);
6012+ }
6013+ .rounded-b-selector {
6014+ border-bottom-left-radius: var(--radius-selector);
6015+ border-bottom-right-radius: var(--radius-selector);
6016+ }
6017+ .rounded-b-selector {
6018+ border-bottom-right-radius: var(--radius-selector);
6019+ border-bottom-left-radius: var(--radius-selector);
6020+ }
6021+ .rounded-br-box {
6022+ border-bottom-right-radius: var(--radius-box);
6023+ }
6024+ .rounded-br-box {
6025+ border-bottom-right-radius: var(--radius-box);
6026+ }
6027+ .rounded-br-field {
6028+ border-bottom-right-radius: var(--radius-field);
6029+ }
6030+ .rounded-br-field {
6031+ border-bottom-right-radius: var(--radius-field);
6032+ }
6033+ .rounded-br-selector {
6034+ border-bottom-right-radius: var(--radius-selector);
6035+ }
6036+ .rounded-br-selector {
6037+ border-bottom-right-radius: var(--radius-selector);
6038+ }
6039+ .rounded-bl-box {
6040+ border-bottom-left-radius: var(--radius-box);
6041+ }
6042+ .rounded-bl-box {
6043+ border-bottom-left-radius: var(--radius-box);
6044+ }
6045+ .rounded-bl-field {
6046+ border-bottom-left-radius: var(--radius-field);
6047+ }
6048+ .rounded-bl-field {
6049+ border-bottom-left-radius: var(--radius-field);
6050+ }
6051+ .rounded-bl-selector {
6052+ border-bottom-left-radius: var(--radius-selector);
6053+ }
6054+ .rounded-bl-selector {
6055+ border-bottom-left-radius: var(--radius-selector);
6056+ }
6057+ .border {
6058+ border-style: var(--tw-border-style);
6059+ border-width: 1px;
6060+ }
6061+ .border-t {
6062+ border-top-style: var(--tw-border-style);
6063+ border-top-width: 1px;
6064+ }
6065+ .btn-dash {
6066+ &:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked ) {
6067+ --btn-shadow: "";
6068+ border-style: dashed;
6069+ --btn-bg: #0000;
6070+ --btn-fg: var(--btn-color);
6071+ --btn-border: var(--btn-color);
6072+ --btn-noise: none;
6073+ }
6074+ }
6075+ .badge-ghost {
6076+ border-color: var(--color-base-200);
6077+ background-color: var(--color-base-200);
6078+ color: var(--color-base-content);
6079+ background-image: none;
6080+ }
6081+ .select-ghost {
6082+ background-color: transparent;
6083+ transition: background-color 0.2s;
6084+ box-shadow: none;
6085+ border-color: #0000;
6086+ &:focus, &:focus-within {
6087+ background-color: var(--color-base-100);
6088+ color: var(--color-base-content);
6089+ border-color: #0000;
6090+ box-shadow: none;
6091+ }
6092+ }
6093+ .input-ghost {
6094+ background-color: transparent;
6095+ box-shadow: none;
6096+ border-color: #0000;
6097+ &:focus, &:focus-within {
6098+ background-color: var(--color-base-100);
6099+ color: var(--color-base-content);
6100+ border-color: #0000;
6101+ box-shadow: none;
6102+ }
6103+ }
6104+ .textarea-ghost {
6105+ background-color: transparent;
6106+ box-shadow: none;
6107+ border-color: #0000;
6108+ &:focus, &:focus-within {
6109+ background-color: var(--color-base-100);
6110+ color: var(--color-base-content);
6111+ border-color: #0000;
6112+ box-shadow: none;
6113+ }
6114+ }
6115+ .alert-error {
6116+ border-color: var(--color-error);
6117+ color: var(--color-error-content);
6118+ --alert-color: var(--color-error);
6119+ }
6120+ .alert-info {
6121+ border-color: var(--color-info);
6122+ color: var(--color-info-content);
6123+ --alert-color: var(--color-info);
6124+ }
6125+ .alert-success {
6126+ border-color: var(--color-success);
6127+ color: var(--color-success-content);
6128+ --alert-color: var(--color-success);
6129+ }
6130+ .alert-warning {
6131+ border-color: var(--color-warning);
6132+ color: var(--color-warning-content);
6133+ --alert-color: var(--color-warning);
6134+ }
6135+ .border-base-300 {
6136+ border-color: var(--color-base-300);
6137+ }
6138+ .glass {
6139+ border: none;
6140+ backdrop-filter: blur(var(--glass-blur, 40px));
6141+ background-color: #0000;
6142+ background-image: linear-gradient(
6143+ 135deg,
6144+ oklch(100% 0 0 / var(--glass-opacity, 30%)) 0%,
6145+ oklch(0% 0 0 / 0%) 100%
6146+ ),
6147+ linear-gradient(
6148+ var(--glass-reflect-degree, 100deg),
6149+ oklch(100% 0 0 / var(--glass-reflect-opacity, 5%)) 25%,
6150+ oklch(0% 0 0 / 0%) 25%
6151+ );
6152+ box-shadow: 0 0 0 1px oklch(100% 0 0 / var(--glass-border-opacity, 20%)) inset,
6153+ 0 0 0 2px oklch(0% 0 0 / 5%);
6154+ text-shadow: 0 1px oklch(0% 0 0 / var(--glass-text-shadow-opacity, 5%));
6155+ }
6156+ .chat-bubble-accent {
6157+ background-color: var(--color-accent);
6158+ color: var(--color-accent-content);
6159+ }
6160+ .chat-bubble-error {
6161+ background-color: var(--color-error);
6162+ color: var(--color-error-content);
6163+ }
6164+ .chat-bubble-info {
6165+ background-color: var(--color-info);
6166+ color: var(--color-info-content);
6167+ }
6168+ .chat-bubble-neutral {
6169+ background-color: var(--color-neutral);
6170+ color: var(--color-neutral-content);
6171+ }
6172+ .chat-bubble-primary {
6173+ background-color: var(--color-primary);
6174+ color: var(--color-primary-content);
6175+ }
6176+ .chat-bubble-secondary {
6177+ background-color: var(--color-secondary);
6178+ color: var(--color-secondary-content);
6179+ }
6180+ .chat-bubble-success {
6181+ background-color: var(--color-success);
6182+ color: var(--color-success-content);
6183+ }
6184+ .chat-bubble-warning {
6185+ background-color: var(--color-warning);
6186+ color: var(--color-warning-content);
6187+ }
6188+ .status-accent {
6189+ background-color: var(--color-accent);
6190+ color: var(--color-accent);
6191+ }
6192+ .status-error {
6193+ background-color: var(--color-error);
6194+ color: var(--color-error);
6195+ }
6196+ .status-info {
6197+ background-color: var(--color-info);
6198+ color: var(--color-info);
6199+ }
6200+ .status-neutral {
6201+ background-color: var(--color-neutral);
6202+ color: var(--color-neutral);
6203+ }
6204+ .status-primary {
6205+ background-color: var(--color-primary);
6206+ color: var(--color-primary);
6207+ }
6208+ .status-secondary {
6209+ background-color: var(--color-secondary);
6210+ color: var(--color-secondary);
6211+ }
6212+ .status-success {
6213+ background-color: var(--color-success);
6214+ color: var(--color-success);
6215+ }
6216+ .status-warning {
6217+ background-color: var(--color-warning);
6218+ color: var(--color-warning);
6219+ }
6220+ .table-zebra {
6221+ tbody {
6222+ tr {
6223+ &:nth-child(even) {
6224+ background-color: var(--color-base-200);
6225+ :where(.table-pin-cols tr th) {
6226+ background-color: var(--color-base-200);
6227+ }
6228+ }
6229+ &.row-hover {
6230+ &, &:nth-child(even) {
6231+ &:hover {
6232+ @media (hover: hover) {
6233+ background-color: var(--color-base-300);
6234+ }
6235+ }
6236+ }
6237+ }
6238+ }
6239+ }
6240+ }
6241+ .bg-base-100 {
6242+ background-color: var(--color-base-100);
6243+ }
6244+ .bg-base-200 {
6245+ background-color: var(--color-base-200);
6246+ }
6247+ .bg-base-300 {
6248+ background-color: var(--color-base-300);
6249+ }
6250+ .bg-blue-500 {
6251+ background-color: var(--color-blue-500);
6252+ }
6253+ .bg-green-500 {
6254+ background-color: var(--color-green-500);
6255+ }
6256+ .bg-indigo-50 {
6257+ background-color: var(--color-indigo-50);
6258+ }
6259+ .bg-indigo-100 {
6260+ background-color: var(--color-indigo-100);
6261+ }
6262+ .bg-indigo-500 {
6263+ background-color: var(--color-indigo-500);
6264+ }
6265+ .bg-red-500 {
6266+ background-color: var(--color-red-500);
6267+ }
6268+ .divider-accent {
6269+ &:before, &:after {
6270+ background-color: var(--color-accent);
6271+ }
6272+ }
6273+ .divider-error {
6274+ &:before, &:after {
6275+ background-color: var(--color-error);
6276+ }
6277+ }
6278+ .divider-info {
6279+ &:before, &:after {
6280+ background-color: var(--color-info);
6281+ }
6282+ }
6283+ .divider-neutral {
6284+ &:before, &:after {
6285+ background-color: var(--color-neutral);
6286+ }
6287+ }
6288+ .divider-primary {
6289+ &:before, &:after {
6290+ background-color: var(--color-primary);
6291+ }
6292+ }
6293+ .divider-secondary {
6294+ &:before, &:after {
6295+ background-color: var(--color-secondary);
6296+ }
6297+ }
6298+ .divider-success {
6299+ &:before, &:after {
6300+ background-color: var(--color-success);
6301+ }
6302+ }
6303+ .divider-warning {
6304+ &:before, &:after {
6305+ background-color: var(--color-warning);
6306+ }
6307+ }
6308+ .stroke-current {
6309+ stroke: currentColor;
6310+ }
6311+ .checkbox-lg {
6312+ padding: 0.3125rem;
6313+ --size: calc(var(--size-selector, 0.25rem) * 7);
6314+ }
6315+ .checkbox-md {
6316+ padding: 0.25rem;
6317+ --size: calc(var(--size-selector, 0.25rem) * 6);
6318+ }
6319+ .checkbox-sm {
6320+ padding: 0.1875rem;
6321+ --size: calc(var(--size-selector, 0.25rem) * 5);
6322+ }
6323+ .checkbox-xl {
6324+ padding: 0.375rem;
6325+ --size: calc(var(--size-selector, 0.25rem) * 8);
6326+ }
6327+ .checkbox-xs {
6328+ padding: 0.125rem;
6329+ --size: calc(var(--size-selector, 0.25rem) * 4);
6330+ }
6331+ .radio-lg {
6332+ padding: 0.3125rem;
6333+ &:is([type="radio"]) {
6334+ --size: calc(var(--size-selector, 0.25rem) * 7);
6335+ }
6336+ }
6337+ .radio-md {
6338+ padding: 0.25rem;
6339+ &:is([type="radio"]) {
6340+ --size: calc(var(--size-selector, 0.25rem) * 6);
6341+ }
6342+ }
6343+ .radio-sm {
6344+ padding: 0.1875rem;
6345+ &:is([type="radio"]) {
6346+ --size: calc(var(--size-selector, 0.25rem) * 5);
6347+ }
6348+ }
6349+ .radio-xl {
6350+ padding: 0.375rem;
6351+ &:is([type="radio"]) {
6352+ --size: calc(var(--size-selector, 0.25rem) * 8);
6353+ }
6354+ }
6355+ .radio-xs {
6356+ padding: 0.125rem;
6357+ &:is([type="radio"]) {
6358+ --size: calc(var(--size-selector, 0.25rem) * 4);
6359+ }
6360+ }
6361+ .p-0 {
6362+ padding: calc(var(--spacing) * 0);
6363+ }
6364+ .p-2 {
6365+ padding: calc(var(--spacing) * 2);
6366+ }
6367+ .p-3 {
6368+ padding: calc(var(--spacing) * 3);
6369+ }
6370+ .p-4 {
6371+ padding: calc(var(--spacing) * 4);
6372+ }
6373+ .p-6 {
6374+ padding: calc(var(--spacing) * 6);
6375+ }
6376+ .p-10 {
6377+ padding: calc(var(--spacing) * 10);
6378+ }
6379+ .menu-title {
6380+ padding-inline: calc(0.25rem * 3);
6381+ padding-block: calc(0.25rem * 2);
6382+ color: color-mix(in oklab, var(--color-base-content) 40%, transparent);
6383+ font-size: 0.875rem;
6384+ font-weight: 600;
6385+ }
6386+ .table-lg {
6387+ :not(thead, tfoot) tr {
6388+ font-size: 1.125rem;
6389+ }
6390+ :where(th, td) {
6391+ padding-inline: calc(0.25rem * 5);
6392+ padding-block: calc(0.25rem * 4);
6393+ }
6394+ }
6395+ .table-md {
6396+ :not(thead, tfoot) tr {
6397+ font-size: 0.875rem;
6398+ }
6399+ :where(th, td) {
6400+ padding-inline: calc(0.25rem * 4);
6401+ padding-block: calc(0.25rem * 3);
6402+ }
6403+ }
6404+ .table-sm {
6405+ :not(thead, tfoot) tr {
6406+ font-size: 0.75rem;
6407+ }
6408+ :where(th, td) {
6409+ padding-inline: calc(0.25rem * 3);
6410+ padding-block: calc(0.25rem * 2);
6411+ }
6412+ }
6413+ .table-xl {
6414+ :not(thead, tfoot) tr {
6415+ font-size: 1.375rem;
6416+ }
6417+ :where(th, td) {
6418+ padding-inline: calc(0.25rem * 6);
6419+ padding-block: calc(0.25rem * 5);
6420+ }
6421+ }
6422+ .table-xs {
6423+ :not(thead, tfoot) tr {
6424+ font-size: 0.6875rem;
6425+ }
6426+ :where(th, td) {
6427+ padding-inline: calc(0.25rem * 2);
6428+ padding-block: calc(0.25rem * 1);
6429+ }
6430+ }
6431+ .badge-lg {
6432+ --size: calc(var(--size-selector, 0.25rem) * 7);
6433+ font-size: 1rem;
6434+ padding-inline: calc(0.25rem * 3.5 - var(--border));
6435+ }
6436+ .badge-md {
6437+ --size: calc(var(--size-selector, 0.25rem) * 6);
6438+ font-size: 0.875rem;
6439+ padding-inline: calc(0.25rem * 3 - var(--border));
6440+ }
6441+ .badge-sm {
6442+ --size: calc(var(--size-selector, 0.25rem) * 5);
6443+ font-size: 0.75rem;
6444+ padding-inline: calc(0.25rem * 2.5 - var(--border));
6445+ }
6446+ .badge-xl {
6447+ --size: calc(var(--size-selector, 0.25rem) * 8);
6448+ font-size: 1.125rem;
6449+ padding-inline: calc(0.25rem * 4 - var(--border));
6450+ }
6451+ .badge-xs {
6452+ --size: calc(var(--size-selector, 0.25rem) * 4);
6453+ font-size: 0.625rem;
6454+ padding-inline: calc(0.25rem * 2 - var(--border));
6455+ }
6456+ .px-1 {
6457+ padding-inline: calc(var(--spacing) * 1);
6458+ }
6459+ .px-4 {
6460+ padding-inline: calc(var(--spacing) * 4);
6461+ }
6462+ .px-10 {
6463+ padding-inline: calc(var(--spacing) * 10);
6464+ }
6465+ .py-4 {
6466+ padding-block: calc(var(--spacing) * 4);
6467+ }
6468+ .py-6 {
6469+ padding-block: calc(var(--spacing) * 6);
6470+ }
6471+ .file-input-xl {
6472+ padding-inline-end: calc(0.25rem * 6);
6473+ --size: calc(var(--size-field, 0.25rem) * 14);
6474+ font-size: 1.125rem;
6475+ line-height: 3rem;
6476+ &::file-selector-button {
6477+ font-size: 1.375rem;
6478+ }
6479+ }
6480+ .pt-16 {
6481+ padding-top: calc(var(--spacing) * 16);
6482+ }
6483+ .pt-20 {
6484+ padding-top: calc(var(--spacing) * 20);
6485+ }
6486+ .pr-1 {
6487+ padding-right: calc(var(--spacing) * 1);
6488+ }
6489+ .pb-4 {
6490+ padding-bottom: calc(var(--spacing) * 4);
6491+ }
6492+ .text-center {
6493+ text-align: center;
6494+ }
6495+ .font-mono {
6496+ font-family: var(--font-mono);
6497+ }
6498+ .file-input-lg {
6499+ --size: calc(var(--size-field, 0.25rem) * 12);
6500+ font-size: 1.125rem;
6501+ line-height: 2.5rem;
6502+ &::file-selector-button {
6503+ font-size: 1.125rem;
6504+ }
6505+ }
6506+ .file-input-md {
6507+ --size: calc(var(--size-field, 0.25rem) * 10);
6508+ font-size: 0.875rem;
6509+ line-height: 2;
6510+ &::file-selector-button {
6511+ font-size: 0.875rem;
6512+ }
6513+ }
6514+ .file-input-sm {
6515+ --size: calc(var(--size-field, 0.25rem) * 8);
6516+ font-size: 0.75rem;
6517+ line-height: 1.5rem;
6518+ &::file-selector-button {
6519+ font-size: 0.75rem;
6520+ }
6521+ }
6522+ .file-input-xs {
6523+ --size: calc(var(--size-field, 0.25rem) * 6);
6524+ font-size: 0.6875rem;
6525+ line-height: 1rem;
6526+ &::file-selector-button {
6527+ font-size: 0.6875rem;
6528+ }
6529+ }
6530+ .text-2xl {
6531+ font-size: var(--text-2xl);
6532+ line-height: var(--tw-leading, var(--text-2xl--line-height));
6533+ }
6534+ .text-3xl {
6535+ font-size: var(--text-3xl);
6536+ line-height: var(--tw-leading, var(--text-3xl--line-height));
6537+ }
6538+ .text-4xl {
6539+ font-size: var(--text-4xl);
6540+ line-height: var(--tw-leading, var(--text-4xl--line-height));
6541+ }
6542+ .text-5xl {
6543+ font-size: var(--text-5xl);
6544+ line-height: var(--tw-leading, var(--text-5xl--line-height));
6545+ }
6546+ .text-lg {
6547+ font-size: var(--text-lg);
6548+ line-height: var(--tw-leading, var(--text-lg--line-height));
6549+ }
6550+ .text-sm {
6551+ font-size: var(--text-sm);
6552+ line-height: var(--tw-leading, var(--text-sm--line-height));
6553+ }
6554+ .text-xs {
6555+ font-size: var(--text-xs);
6556+ line-height: var(--tw-leading, var(--text-xs--line-height));
6557+ }
6558+ .kbd-lg {
6559+ --size: calc(var(--size-selector, 0.25rem) * 7);
6560+ font-size: 1rem;
6561+ }
6562+ .kbd-md {
6563+ --size: calc(var(--size-selector, 0.25rem) * 6);
6564+ font-size: 0.875rem;
6565+ }
6566+ .kbd-sm {
6567+ --size: calc(var(--size-selector, 0.25rem) * 5);
6568+ font-size: 0.75rem;
6569+ }
6570+ .kbd-xl {
6571+ --size: calc(var(--size-selector, 0.25rem) * 8);
6572+ font-size: 1.125rem;
6573+ }
6574+ .kbd-xs {
6575+ --size: calc(var(--size-selector, 0.25rem) * 4);
6576+ font-size: 0.625rem;
6577+ }
6578+ .select-lg {
6579+ --size: calc(var(--size-field, 0.25rem) * 12);
6580+ font-size: 1.125rem;
6581+ }
6582+ .select-md {
6583+ --size: calc(var(--size-field, 0.25rem) * 10);
6584+ font-size: 0.875rem;
6585+ }
6586+ .select-sm {
6587+ --size: calc(var(--size-field, 0.25rem) * 8);
6588+ font-size: 0.75rem;
6589+ }
6590+ .select-xl {
6591+ --size: calc(var(--size-field, 0.25rem) * 14);
6592+ font-size: 1.375rem;
6593+ }
6594+ .select-xs {
6595+ --size: calc(var(--size-field, 0.25rem) * 6);
6596+ font-size: 0.6875rem;
6597+ }
6598+ .textarea-lg {
6599+ font-size: 1.125rem;
6600+ }
6601+ .textarea-md {
6602+ font-size: 0.875rem;
6603+ }
6604+ .textarea-sm {
6605+ font-size: 0.75rem;
6606+ }
6607+ .textarea-xl {
6608+ font-size: 1.375rem;
6609+ }
6610+ .textarea-xs {
6611+ font-size: 0.6875rem;
6612+ }
6613+ .font-bold {
6614+ --tw-font-weight: var(--font-weight-bold);
6615+ font-weight: var(--font-weight-bold);
6616+ }
6617+ .font-semibold {
6618+ --tw-font-weight: var(--font-weight-semibold);
6619+ font-weight: var(--font-weight-semibold);
6620+ }
6621+ .tracking-tighter {
6622+ --tw-tracking: var(--tracking-tighter);
6623+ letter-spacing: var(--tracking-tighter);
6624+ }
6625+ .text-wrap {
6626+ text-wrap: wrap;
6627+ }
6628+ .file-input-accent {
6629+ --btn-color: var(--color-accent);
6630+ &::file-selector-button {
6631+ color: var(--color-accent-content);
6632+ }
6633+ &, &:focus, &:focus-within {
6634+ --input-color: var(--color-accent);
6635+ }
6636+ }
6637+ .file-input-error {
6638+ --btn-color: var(--color-error);
6639+ &::file-selector-button {
6640+ color: var(--color-error-content);
6641+ }
6642+ &, &:focus, &:focus-within {
6643+ --input-color: var(--color-error);
6644+ }
6645+ }
6646+ .file-input-info {
6647+ --btn-color: var(--color-info);
6648+ &::file-selector-button {
6649+ color: var(--color-info-content);
6650+ }
6651+ &, &:focus, &:focus-within {
6652+ --input-color: var(--color-info);
6653+ }
6654+ }
6655+ .file-input-neutral {
6656+ --btn-color: var(--color-neutral);
6657+ &::file-selector-button {
6658+ color: var(--color-neutral-content);
6659+ }
6660+ &, &:focus, &:focus-within {
6661+ --input-color: var(--color-neutral);
6662+ }
6663+ }
6664+ .file-input-primary {
6665+ --btn-color: var(--color-primary);
6666+ &::file-selector-button {
6667+ color: var(--color-primary-content);
6668+ }
6669+ &, &:focus, &:focus-within {
6670+ --input-color: var(--color-primary);
6671+ }
6672+ }
6673+ .file-input-secondary {
6674+ --btn-color: var(--color-secondary);
6675+ &::file-selector-button {
6676+ color: var(--color-secondary-content);
6677+ }
6678+ &, &:focus, &:focus-within {
6679+ --input-color: var(--color-secondary);
6680+ }
6681+ }
6682+ .file-input-success {
6683+ --btn-color: var(--color-success);
6684+ &::file-selector-button {
6685+ color: var(--color-success-content);
6686+ }
6687+ &, &:focus, &:focus-within {
6688+ --input-color: var(--color-success);
6689+ }
6690+ }
6691+ .file-input-warning {
6692+ --btn-color: var(--color-warning);
6693+ &::file-selector-button {
6694+ color: var(--color-warning-content);
6695+ }
6696+ &, &:focus, &:focus-within {
6697+ --input-color: var(--color-warning);
6698+ }
6699+ }
6700+ .checkbox-accent {
6701+ color: var(--color-accent-content);
6702+ --input-color: var(--color-accent);
6703+ }
6704+ .checkbox-error {
6705+ color: var(--color-error-content);
6706+ --input-color: var(--color-error);
6707+ }
6708+ .checkbox-info {
6709+ color: var(--color-info-content);
6710+ --input-color: var(--color-info);
6711+ }
6712+ .checkbox-neutral {
6713+ color: var(--color-neutral-content);
6714+ --input-color: var(--color-neutral);
6715+ }
6716+ .checkbox-primary {
6717+ color: var(--color-primary-content);
6718+ --input-color: var(--color-primary);
6719+ }
6720+ .checkbox-secondary {
6721+ color: var(--color-secondary-content);
6722+ --input-color: var(--color-secondary);
6723+ }
6724+ .checkbox-success {
6725+ color: var(--color-success-content);
6726+ --input-color: var(--color-success);
6727+ }
6728+ .checkbox-warning {
6729+ color: var(--color-warning-content);
6730+ --input-color: var(--color-warning);
6731+ }
6732+ .link-accent {
6733+ color: var(--color-accent);
6734+ @media (hover: hover) {
6735+ &:hover {
6736+ color: color-mix(in oklab, var(--color-accent) 80%, #000);
6737+ }
6738+ }
6739+ }
6740+ .link-error {
6741+ color: var(--color-error);
6742+ @media (hover: hover) {
6743+ &:hover {
6744+ color: color-mix(in oklab, var(--color-error) 80%, #000);
6745+ }
6746+ }
6747+ }
6748+ .link-info {
6749+ color: var(--color-info);
6750+ @media (hover: hover) {
6751+ &:hover {
6752+ color: color-mix(in oklab, var(--color-info) 80%, #000);
6753+ }
6754+ }
6755+ }
6756+ .link-neutral {
6757+ color: var(--color-neutral);
6758+ @media (hover: hover) {
6759+ &:hover {
6760+ color: color-mix(in oklab, var(--color-neutral) 80%, #000);
6761+ }
6762+ }
6763+ }
6764+ .link-primary {
6765+ color: var(--color-primary);
6766+ @media (hover: hover) {
6767+ &:hover {
6768+ color: color-mix(in oklab, var(--color-primary) 80%, #000);
6769+ }
6770+ }
6771+ }
6772+ .link-secondary {
6773+ color: var(--color-secondary);
6774+ @media (hover: hover) {
6775+ &:hover {
6776+ color: color-mix(in oklab, var(--color-secondary) 80%, #000);
6777+ }
6778+ }
6779+ }
6780+ .link-success {
6781+ color: var(--color-success);
6782+ @media (hover: hover) {
6783+ &:hover {
6784+ color: color-mix(in oklab, var(--color-success) 80%, #000);
6785+ }
6786+ }
6787+ }
6788+ .link-warning {
6789+ color: var(--color-warning);
6790+ @media (hover: hover) {
6791+ &:hover {
6792+ color: color-mix(in oklab, var(--color-warning) 80%, #000);
6793+ }
6794+ }
6795+ }
6796+ .range-accent {
6797+ color: var(--color-accent);
6798+ --range-thumb: var(--color-accent-content);
6799+ }
6800+ .range-error {
6801+ color: var(--color-error);
6802+ --range-thumb: var(--color-error-content);
6803+ }
6804+ .range-info {
6805+ color: var(--color-info);
6806+ --range-thumb: var(--color-info-content);
6807+ }
6808+ .range-neutral {
6809+ color: var(--color-neutral);
6810+ --range-thumb: var(--color-neutral-content);
6811+ }
6812+ .range-primary {
6813+ color: var(--color-primary);
6814+ --range-thumb: var(--color-primary-content);
6815+ }
6816+ .range-secondary {
6817+ color: var(--color-secondary);
6818+ --range-thumb: var(--color-secondary-content);
6819+ }
6820+ .range-success {
6821+ color: var(--color-success);
6822+ --range-thumb: var(--color-success-content);
6823+ }
6824+ .range-warning {
6825+ color: var(--color-warning);
6826+ --range-thumb: var(--color-warning-content);
6827+ }
6828+ .tooltip-accent {
6829+ --tt-bg: var(--color-accent);
6830+ > .tooltip-content, &[data-tip]:before {
6831+ color: var(--color-accent-content);
6832+ }
6833+ }
6834+ .tooltip-error {
6835+ --tt-bg: var(--color-error);
6836+ > .tooltip-content, &[data-tip]:before {
6837+ color: var(--color-error-content);
6838+ }
6839+ }
6840+ .tooltip-info {
6841+ --tt-bg: var(--color-info);
6842+ > .tooltip-content, &[data-tip]:before {
6843+ color: var(--color-info-content);
6844+ }
6845+ }
6846+ .tooltip-primary {
6847+ --tt-bg: var(--color-primary);
6848+ > .tooltip-content, &[data-tip]:before {
6849+ color: var(--color-primary-content);
6850+ }
6851+ }
6852+ .tooltip-secondary {
6853+ --tt-bg: var(--color-secondary);
6854+ > .tooltip-content, &[data-tip]:before {
6855+ color: var(--color-secondary-content);
6856+ }
6857+ }
6858+ .tooltip-success {
6859+ --tt-bg: var(--color-success);
6860+ > .tooltip-content, &[data-tip]:before {
6861+ color: var(--color-success-content);
6862+ }
6863+ }
6864+ .tooltip-warning {
6865+ --tt-bg: var(--color-warning);
6866+ > .tooltip-content, &[data-tip]:before {
6867+ color: var(--color-warning-content);
6868+ }
6869+ }
6870+ .progress-accent {
6871+ color: var(--color-accent);
6872+ }
6873+ .progress-error {
6874+ color: var(--color-error);
6875+ }
6876+ .progress-info {
6877+ color: var(--color-info);
6878+ }
6879+ .progress-neutral {
6880+ color: var(--color-neutral);
6881+ }
6882+ .progress-primary {
6883+ color: var(--color-primary);
6884+ }
6885+ .progress-secondary {
6886+ color: var(--color-secondary);
6887+ }
6888+ .progress-success {
6889+ color: var(--color-success);
6890+ }
6891+ .progress-warning {
6892+ color: var(--color-warning);
6893+ }
6894+ .text-accent {
6895+ color: var(--color-accent);
6896+ }
6897+ .text-base-content {
6898+ color: var(--color-base-content);
6899+ }
6900+ .text-base-content\/70 {
6901+ color: color-mix(in oklab, var(--color-base-content) 70%, transparent);
6902+ }
6903+ .text-error {
6904+ color: var(--color-error);
6905+ }
6906+ .text-indigo-600 {
6907+ color: var(--color-indigo-600);
6908+ }
6909+ .text-indigo-800 {
6910+ color: var(--color-indigo-800);
6911+ }
6912+ .text-info {
6913+ color: var(--color-info);
6914+ }
6915+ .text-primary {
6916+ color: var(--color-primary);
6917+ }
6918+ .text-secondary {
6919+ color: var(--color-secondary);
6920+ }
6921+ .text-success {
6922+ color: var(--color-success);
6923+ }
6924+ .text-warning {
6925+ color: var(--color-warning);
6926+ }
6927+ .text-white {
6928+ color: var(--color-white);
6929+ }
6930+ .lowercase {
6931+ text-transform: lowercase;
6932+ }
6933+ .uppercase {
6934+ text-transform: uppercase;
6935+ }
6936+ .btn-link {
6937+ text-decoration-line: underline;
6938+ outline-color: currentColor;
6939+ --btn-border: #0000;
6940+ --btn-bg: #0000;
6941+ --btn-fg: var(--color-primary);
6942+ --btn-noise: none;
6943+ --btn-shadow: "";
6944+ &:is(.btn-active, :hover, :active:focus, :focus-visible) {
6945+ text-decoration-line: underline;
6946+ --btn-border: #0000;
6947+ --btn-bg: #0000;
6948+ }
6949+ }
6950+ .link-hover {
6951+ text-decoration-line: none;
6952+ &:hover {
6953+ @media (hover: hover) {
6954+ text-decoration-line: underline;
6955+ }
6956+ }
6957+ }
6958+ .underline {
6959+ text-decoration-line: underline;
6960+ }
6961+ .swap-active {
6962+ .swap-off {
6963+ opacity: 0%;
6964+ }
6965+ .swap-on {
6966+ opacity: 100%;
6967+ }
6968+ }
6969+ .opacity-0 {
6970+ opacity: 0%;
6971+ }
6972+ .opacity-50 {
6973+ opacity: 50%;
6974+ }
6975+ .opacity-70 {
6976+ opacity: 70%;
6977+ }
6978+ .shadow {
6979+ --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));
6980+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
6981+ }
6982+ .shadow-lg {
6983+ --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));
6984+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
6985+ }
6986+ .shadow-md {
6987+ --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
6988+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
6989+ }
6990+ .shadow-xl {
6991+ --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));
6992+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
6993+ }
6994+ .outline {
6995+ outline-style: var(--tw-outline-style);
6996+ outline-width: 1px;
6997+ }
6998+ .btn-ghost {
6999+ &:not(.btn-active, :hover, :active:focus, :focus-visible) {
7000+ --btn-shadow: "";
7001+ --btn-bg: #0000;
7002+ --btn-border: #0000;
7003+ --btn-noise: none;
7004+ &:not(:disabled, [disabled], .btn-disabled) {
7005+ outline-color: currentColor;
7006+ --btn-fg: currentColor;
7007+ }
7008+ }
7009+ }
7010+ .blur {
7011+ --tw-blur: blur(8px);
7012+ 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,);
7013+ }
7014+ .drop-shadow-xl {
7015+ --tw-drop-shadow: drop-shadow(var(--drop-shadow-xl));
7016+ 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,);
7017+ }
7018+ .filter {
7019+ 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,);
7020+ }
7021+ .backdrop-filter {
7022+ -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
7023+ backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
7024+ }
7025+ .transition {
7026+ 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;
7027+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
7028+ transition-duration: var(--tw-duration, var(--default-transition-duration));
7029+ }
7030+ .transition-opacity {
7031+ transition-property: opacity;
7032+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
7033+ transition-duration: var(--tw-duration, var(--default-transition-duration));
7034+ }
7035+ .duration-300 {
7036+ --tw-duration: 300ms;
7037+ transition-duration: 300ms;
7038+ }
7039+ .ease-in {
7040+ --tw-ease: var(--ease-in);
7041+ transition-timing-function: var(--ease-in);
7042+ }
7043+ .ease-in-out {
7044+ --tw-ease: var(--ease-in-out);
7045+ transition-timing-function: var(--ease-in-out);
7046+ }
7047+ .ease-out {
7048+ --tw-ease: var(--ease-out);
7049+ transition-timing-function: var(--ease-out);
7050+ }
7051+ .tabs-bottom {
7052+ --tabs-height: auto;
7053+ --tabs-direction: row;
7054+ .tab {
7055+ --tab-order: 1;
7056+ --tab-border: var(--border) 0 0 0;
7057+ --tab-radius-ss: 0;
7058+ --tab-radius-se: 0;
7059+ --tab-radius-es: min(var(--radius-field), var(--tab-radius-min));
7060+ --tab-radius-ee: min(var(--radius-field), var(--tab-radius-min));
7061+ --tab-border-colors: var(--tab-border-color) #0000 #0000 #0000;
7062+ --tab-paddings: 0 var(--tab-p) var(--border) var(--tab-p);
7063+ --tab-corner-width: calc(100% + min(var(--radius-field), var(--tab-radius-min)) * 2);
7064+ --tab-corner-height: min(var(--radius-field), var(--tab-radius-min));
7065+ --tab-corner-position: top left, top right;
7066+ &:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked), &:is(label:has(:checked)) {
7067+ --tab-border: 0 var(--border) var(--border) var(--border);
7068+ --tab-border-colors: #0000 var(--tab-border-color) var(--tab-border-color)
7069+ var(--tab-border-color);
7070+ --tab-paddings: var(--border) calc(var(--tab-p) - var(--border)) 0
7071+ calc(var(--tab-p) - var(--border));
7072+ --tab-inset: 0 auto auto auto;
7073+ --radius-start: radial-gradient(
7074+ circle at bottom left,
7075+ #0000 var(--tab-grad),
7076+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
7077+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
7078+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
7079+ );
7080+ --radius-end: radial-gradient(
7081+ circle at bottom right,
7082+ #0000 var(--tab-grad),
7083+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
7084+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
7085+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
7086+ );
7087+ }
7088+ }
7089+ &:has(.tab-content) {
7090+ > .tab:first-child {
7091+ &:not(.tab-active, [aria-selected="true"]) {
7092+ --tab-border-colors: #0000 var(--tab-border-color) var(--tab-border-color)
7093+ var(--tab-border-color);
7094+ }
7095+ }
7096+ }
7097+ .tab-content {
7098+ --tabcontent-order: 0;
7099+ --tabcontent-margin: 0 0 calc(-1 * var(--border)) 0;
7100+ --tabcontent-radius-ss: var(--radius-box);
7101+ --tabcontent-radius-se: var(--radius-box);
7102+ --tabcontent-radius-es: 0;
7103+ --tabcontent-radius-ee: var(--radius-box);
7104+ }
7105+ :checked, :is(label:has(:checked)), :is(.tab-active, [aria-selected="true"]) {
7106+ & + .tab-content:not(:nth-child(2)) {
7107+ --tabcontent-radius-es: var(--radius-box);
7108+ }
7109+ }
7110+ }
7111+ .tabs-top {
7112+ --tabs-height: auto;
7113+ --tabs-direction: row;
7114+ .tab {
7115+ --tab-order: 0;
7116+ --tab-border: 0 0 var(--border) 0;
7117+ --tab-radius-ss: min(var(--radius-field), var(--tab-radius-min));
7118+ --tab-radius-se: min(var(--radius-field), var(--tab-radius-min));
7119+ --tab-radius-es: 0;
7120+ --tab-radius-ee: 0;
7121+ --tab-paddings: var(--border) var(--tab-p) 0 var(--tab-p);
7122+ --tab-border-colors: #0000 #0000 var(--tab-border-color) #0000;
7123+ --tab-corner-width: calc(100% + min(var(--radius-field), var(--tab-radius-min)) * 2);
7124+ --tab-corner-height: min(var(--radius-field), var(--tab-radius-min));
7125+ --tab-corner-position: top left, top right;
7126+ &:is(.tab-active, [aria-selected="true"]):not(.tab-disabled, [disabled]), &:is(input:checked), &:is(label:has(:checked)) {
7127+ --tab-border: var(--border) var(--border) 0 var(--border);
7128+ --tab-border-colors: var(--tab-border-color) var(--tab-border-color) #0000
7129+ var(--tab-border-color);
7130+ --tab-paddings: 0 calc(var(--tab-p) - var(--border)) var(--border)
7131+ calc(var(--tab-p) - var(--border));
7132+ --tab-inset: auto auto 0 auto;
7133+ --radius-start: radial-gradient(
7134+ circle at top left,
7135+ #0000 var(--tab-grad),
7136+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
7137+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
7138+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
7139+ );
7140+ --radius-end: radial-gradient(
7141+ circle at top right,
7142+ #0000 var(--tab-grad),
7143+ var(--tab-border-color) calc(var(--tab-grad) + 0.25px),
7144+ var(--tab-border-color) calc(var(--tab-grad) + var(--border)),
7145+ var(--tab-bg) calc(var(--tab-grad) + var(--border) + 0.25px)
7146+ );
7147+ }
7148+ }
7149+ &:has(.tab-content) {
7150+ > .tab:first-child {
7151+ &:not(.tab-active, [aria-selected="true"]) {
7152+ --tab-border-colors: var(--tab-border-color) var(--tab-border-color) #0000
7153+ var(--tab-border-color);
7154+ }
7155+ }
7156+ }
7157+ .tab-content {
7158+ --tabcontent-order: 1;
7159+ --tabcontent-margin: calc(-1 * var(--border)) 0 0 0;
7160+ --tabcontent-radius-ss: 0;
7161+ --tabcontent-radius-se: var(--radius-box);
7162+ --tabcontent-radius-es: var(--radius-box);
7163+ --tabcontent-radius-ee: var(--radius-box);
7164+ }
7165+ :checked, label:has(:checked), :is(.tab-active, [aria-selected="true"]) {
7166+ & + .tab-content {
7167+ &:nth-child(1), &:nth-child(n + 3) {
7168+ --tabcontent-radius-ss: var(--radius-box);
7169+ }
7170+ }
7171+ }
7172+ }
7173+ .btn-outline {
7174+ &:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked ) {
7175+ --btn-shadow: "";
7176+ --btn-bg: #0000;
7177+ --btn-fg: var(--btn-color);
7178+ --btn-border: var(--btn-color);
7179+ --btn-noise: none;
7180+ }
7181+ }
7182+ .btn-soft {
7183+ &:not(.btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled) {
7184+ --btn-shadow: "";
7185+ --btn-fg: var(--btn-color, var(--color-base-content));
7186+ --btn-bg: color-mix(
7187+ in oklab,
7188+ var(--btn-color, var(--color-base-content)) 8%,
7189+ var(--color-base-100)
7190+ );
7191+ --btn-border: color-mix(
7192+ in oklab,
7193+ var(--btn-color, var(--color-base-content)) 10%,
7194+ var(--color-base-100)
7195+ );
7196+ --btn-noise: none;
7197+ }
7198+ }
7199+ .indicator-center {
7200+ --inidicator-s: 50%;
7201+ --inidicator-e: 50%;
7202+ --inidicator-x: -50%;
7203+ [dir="rtl"] & {
7204+ --inidicator-x: 50%;
7205+ }
7206+ }
7207+ .mask-half-1 {
7208+ mask-size: 200%;
7209+ mask-position: left;
7210+ mask-position: left;
7211+ &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) {
7212+ mask-position: right;
7213+ }
7214+ }
7215+ .mask-half-2 {
7216+ mask-size: 200%;
7217+ mask-position: right;
7218+ mask-position: right;
7219+ &:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *) {
7220+ mask-position: left;
7221+ }
7222+ }
7223+ .btn-lg {
7224+ --fontsize: 1.125rem;
7225+ --btn-p: 1.25rem;
7226+ --size: calc(var(--size-field, 0.25rem) * 12);
7227+ }
7228+ .btn-md {
7229+ --fontsize: 0.875rem;
7230+ --btn-p: 1rem;
7231+ --size: calc(var(--size-field, 0.25rem) * 10);
7232+ }
7233+ .btn-sm {
7234+ --fontsize: 0.75rem;
7235+ --btn-p: 0.75rem;
7236+ --size: calc(var(--size-field, 0.25rem) * 8);
7237+ }
7238+ .btn-xl {
7239+ --fontsize: 1.375rem;
7240+ --btn-p: 1.5rem;
7241+ --size: calc(var(--size-field, 0.25rem) * 14);
7242+ }
7243+ .btn-xs {
7244+ --fontsize: 0.6875rem;
7245+ --btn-p: 0.5rem;
7246+ --size: calc(var(--size-field, 0.25rem) * 6);
7247+ }
7248+ .card-lg {
7249+ .card-body {
7250+ --card-p: 2rem;
7251+ --card-fs: 1rem;
7252+ }
7253+ .card-title {
7254+ --cardtitle-fs: 1.25rem;
7255+ }
7256+ }
7257+ .card-md {
7258+ .card-body {
7259+ --card-p: 1.5rem;
7260+ --card-fs: 0.875rem;
7261+ }
7262+ .card-title {
7263+ --cardtitle-fs: 1.125rem;
7264+ }
7265+ }
7266+ .card-sm {
7267+ .card-body {
7268+ --card-p: 1rem;
7269+ --card-fs: 0.75rem;
7270+ }
7271+ .card-title {
7272+ --cardtitle-fs: 1rem;
7273+ }
7274+ }
7275+ .card-xl {
7276+ .card-body {
7277+ --card-p: 2.5rem;
7278+ --card-fs: 1.125rem;
7279+ }
7280+ .card-title {
7281+ --cardtitle-fs: 1.375rem;
7282+ }
7283+ }
7284+ .card-xs {
7285+ .card-body {
7286+ --card-p: 0.5rem;
7287+ --card-fs: 0.6875rem;
7288+ }
7289+ .card-title {
7290+ --cardtitle-fs: 0.875rem;
7291+ }
7292+ }
7293+ .indicator-bottom {
7294+ --inidicator-t: auto;
7295+ --inidicator-b: 0;
7296+ --indicator-y: 50%;
7297+ }
7298+ .indicator-end {
7299+ --inidicator-s: auto;
7300+ --inidicator-e: 0;
7301+ --inidicator-x: 50%;
7302+ }
7303+ .indicator-middle {
7304+ --inidicator-t: 50%;
7305+ --inidicator-b: 50%;
7306+ --indicator-y: -50%;
7307+ }
7308+ .indicator-start {
7309+ --inidicator-s: 0;
7310+ --inidicator-e: auto;
7311+ --inidicator-x: -50%;
7312+ }
7313+ .indicator-top {
7314+ --inidicator-t: 0;
7315+ --inidicator-b: auto;
7316+ --indicator-y: -50%;
7317+ }
7318+ .badge-accent {
7319+ --badge-color: var(--color-accent);
7320+ --badge-fg: var(--color-accent-content);
7321+ }
7322+ .badge-error {
7323+ --badge-color: var(--color-error);
7324+ --badge-fg: var(--color-error-content);
7325+ }
7326+ .badge-info {
7327+ --badge-color: var(--color-info);
7328+ --badge-fg: var(--color-info-content);
7329+ }
7330+ .badge-neutral {
7331+ --badge-color: var(--color-neutral);
7332+ --badge-fg: var(--color-neutral-content);
7333+ }
7334+ .badge-primary {
7335+ --badge-color: var(--color-primary);
7336+ --badge-fg: var(--color-primary-content);
7337+ }
7338+ .badge-secondary {
7339+ --badge-color: var(--color-secondary);
7340+ --badge-fg: var(--color-secondary-content);
7341+ }
7342+ .badge-success {
7343+ --badge-color: var(--color-success);
7344+ --badge-fg: var(--color-success-content);
7345+ }
7346+ .badge-warning {
7347+ --badge-color: var(--color-warning);
7348+ --badge-fg: var(--color-warning-content);
7349+ }
7350+ .btn-accent {
7351+ --btn-color: var(--color-accent);
7352+ --btn-fg: var(--color-accent-content);
7353+ }
7354+ .btn-error {
7355+ --btn-color: var(--color-error);
7356+ --btn-fg: var(--color-error-content);
7357+ }
7358+ .btn-info {
7359+ --btn-color: var(--color-info);
7360+ --btn-fg: var(--color-info-content);
7361+ }
7362+ .btn-neutral {
7363+ --btn-color: var(--color-neutral);
7364+ --btn-fg: var(--color-neutral-content);
7365+ }
7366+ .btn-primary {
7367+ --btn-color: var(--color-primary);
7368+ --btn-fg: var(--color-primary-content);
7369+ }
7370+ .btn-secondary {
7371+ --btn-color: var(--color-secondary);
7372+ --btn-fg: var(--color-secondary-content);
7373+ }
7374+ .btn-success {
7375+ --btn-color: var(--color-success);
7376+ --btn-fg: var(--color-success-content);
7377+ }
7378+ .btn-warning {
7379+ --btn-color: var(--color-warning);
7380+ --btn-fg: var(--color-warning-content);
7381+ }
7382+ .timeline-snap-icon {
7383+ > li {
7384+ --timeline-col-start: 0.5rem;
7385+ --timeline-row-start: minmax(0, 1fr);
7386+ }
7387+ }
7388+ .toggle-lg {
7389+ &:is([type="checkbox"]), &:has([type="checkbox"]) {
7390+ --toggle-p: 0.25rem;
7391+ --size: calc(var(--size-selector, 0.25rem) * 7);
7392+ }
7393+ }
7394+ .toggle-md {
7395+ &:is([type="checkbox"]), &:has([type="checkbox"]) {
7396+ --toggle-p: 0.1875rem;
7397+ --size: calc(var(--size-selector, 0.25rem) * 6);
7398+ }
7399+ }
7400+ .toggle-sm {
7401+ &:is([type="checkbox"]), &:has([type="checkbox"]) {
7402+ --toggle-p: 0.125rem;
7403+ --size: calc(var(--size-selector, 0.25rem) * 5);
7404+ }
7405+ }
7406+ .toggle-xl {
7407+ &:is([type="checkbox"]), &:has([type="checkbox"]) {
7408+ --toggle-p: 0.3125rem;
7409+ --size: calc(var(--size-selector, 0.25rem) * 8);
7410+ }
7411+ }
7412+ .toggle-xs {
7413+ &:is([type="checkbox"]), &:has([type="checkbox"]) {
7414+ --toggle-p: 0.0625rem;
7415+ --size: calc(var(--size-selector, 0.25rem) * 4);
7416+ }
7417+ }
7418+ .\[image\:tag\] {
7419+ image: tag;
7420+ }
7421+ .input-accent {
7422+ &, &:focus, &:focus-within {
7423+ --input-color: var(--color-accent);
7424+ }
7425+ }
7426+ .input-error {
7427+ &, &:focus, &:focus-within {
7428+ --input-color: var(--color-error);
7429+ }
7430+ }
7431+ .input-info {
7432+ &, &:focus, &:focus-within {
7433+ --input-color: var(--color-info);
7434+ }
7435+ }
7436+ .input-neutral {
7437+ &, &:focus, &:focus-within {
7438+ --input-color: var(--color-neutral);
7439+ }
7440+ }
7441+ .input-primary {
7442+ &, &:focus, &:focus-within {
7443+ --input-color: var(--color-primary);
7444+ }
7445+ }
7446+ .input-secondary {
7447+ &, &:focus, &:focus-within {
7448+ --input-color: var(--color-secondary);
7449+ }
7450+ }
7451+ .input-success {
7452+ &, &:focus, &:focus-within {
7453+ --input-color: var(--color-success);
7454+ }
7455+ }
7456+ .input-warning {
7457+ &, &:focus, &:focus-within {
7458+ --input-color: var(--color-warning);
7459+ }
7460+ }
7461+ .loading-ball {
7462+ mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cellipse cx='12' cy='5' rx='4' ry='4'%3E%3Canimate attributeName='cy' values='5;20;20.5;20;5' keyTimes='0;0.469;0.5;0.531;1' dur='.8s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1'/%3E%3Canimate attributeName='rx' values='4;4;4.8;4;4' keyTimes='0;0.469;0.5;0.531;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='ry' values='4;4;3;4;4' keyTimes='0;0.469;0.5;0.531;1' dur='.8s' repeatCount='indefinite'/%3E%3C/ellipse%3E%3C/svg%3E");
7463+ }
7464+ .loading-bars {
7465+ mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite'/%3E%3C/rect%3E%3Crect x='9' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.65s'/%3E%3C/rect%3E%3Crect x='17' y='1' width='6' height='22'%3E%3Canimate attributeName='y' values='1;5;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3Canimate attributeName='height' values='22;14;22' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3Canimate attributeName='opacity' values='1;0.2;1' keyTimes='0;0.938;1' dur='.8s' repeatCount='indefinite' begin='-0.5s'/%3E%3C/rect%3E%3C/svg%3E");
7466+ }
7467+ .loading-dots {
7468+ mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='4' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1'/%3E%3C/circle%3E%3Ccircle cx='12' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1' begin='0.1s'/%3E%3C/circle%3E%3Ccircle cx='20' cy='12' r='3'%3E%3Canimate attributeName='cy' values='12;6;12;12' keyTimes='0;0.286;0.571;1' dur='1.05s' repeatCount='indefinite' keySplines='.33,0,.66,.33;.33,.66,.66,1' begin='0.2s'/%3E%3C/circle%3E%3C/svg%3E");
7469+ }
7470+ .loading-infinity {
7471+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='shape-rendering:auto;' width='200px' height='200px' viewBox='0 0 100 100' preserveAspectRatio='xMidYMid'%3E%3Cpath fill='none' stroke='black' stroke-width='10' stroke-dasharray='205.271 51.318' d='M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z' stroke-linecap='round' style='transform:scale(0.8);transform-origin:50px 50px'%3E%3Canimate attributeName='stroke-dashoffset' repeatCount='indefinite' dur='2s' keyTimes='0;1' values='0;256.589'/%3E%3C/path%3E%3C/svg%3E");
7472+ }
7473+ .loading-ring {
7474+ mask-image: url("data:image/svg+xml,%3Csvg width='44' height='44' viewBox='0 0 44 44' xmlns='http://www.w3.org/2000/svg' stroke='white'%3E%3Cg fill='none' fill-rule='evenodd' stroke-width='2'%3E%3Ccircle cx='22' cy='22' r='1'%3E%3Canimate attributeName='r' begin='0s' dur='1.8s' values='1;20' calcMode='spline' keyTimes='0;1' keySplines='0.165,0.84,0.44,1' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-opacity' begin='0s' dur='1.8s' values='1;0' calcMode='spline' keyTimes='0;1' keySplines='0.3,0.61,0.355,1' repeatCount='indefinite'/%3E%3C/circle%3E%3Ccircle cx='22' cy='22' r='1'%3E%3Canimate attributeName='r' begin='-0.9s' dur='1.8s' values='1;20' calcMode='spline' keyTimes='0;1' keySplines='0.165,0.84,0.44,1' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-opacity' begin='-0.9s' dur='1.8s' values='1;0' calcMode='spline' keyTimes='0;1' keySplines='0.3,0.61,0.355,1' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
7475+ }
7476+ .loading-spinner {
7477+ mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E");
7478+ }
7479+ .mask-circle {
7480+ mask-image: url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle fill='black' cx='100' cy='100' r='100' fill-rule='evenodd'/%3e%3c/svg%3e");
7481+ }
7482+ .mask-decagon {
7483+ mask-image: url("data:image/svg+xml,%3csvg width='192' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 0 58.779 19.098 36.327 50v61.804l-36.327 50L96 200l-58.779-19.098-36.327-50V69.098l36.327-50z' fill-rule='evenodd'/%3e%3c/svg%3e");
7484+ }
7485+ .mask-diamond {
7486+ mask-image: url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m100 0 100 100-100 100L0 100z' fill-rule='evenodd'/%3e%3c/svg%3e");
7487+ }
7488+ .mask-heart {
7489+ mask-image: url("data:image/svg+xml,%3csvg width='200' height='185' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 184.606a15.384 15.384 0 0 1-8.653-2.678C53.565 156.28 37.205 138.695 28.182 127.7 8.952 104.264-.254 80.202.005 54.146.308 24.287 24.264 0 53.406 0c21.192 0 35.869 11.937 44.416 21.879a2.884 2.884 0 0 0 4.356 0C110.725 11.927 125.402 0 146.594 0c29.142 0 53.098 24.287 53.4 54.151.26 26.061-8.956 50.122-28.176 73.554-9.023 10.994-25.383 28.58-63.165 54.228a15.384 15.384 0 0 1-8.653 2.673Z' fill='black' fill-rule='nonzero'/%3e%3c/svg%3e");
7490+ }
7491+ .mask-hexagon {
7492+ mask-image: url("data:image/svg+xml,%3csvg width='182' height='201' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M.3 65.486c0-9.196 6.687-20.063 14.211-25.078l61.86-35.946c8.36-5.016 20.899-5.016 29.258 0l61.86 35.946c8.36 5.015 14.211 15.882 14.211 25.078v71.055c0 9.196-6.687 20.063-14.211 25.079l-61.86 35.945c-8.36 4.18-20.899 4.18-29.258 0L14.51 161.62C6.151 157.44.3 145.737.3 136.54V65.486Z' fill='black' fill-rule='nonzero'/%3e%3c/svg%3e");
7493+ }
7494+ .mask-hexagon-2 {
7495+ mask-image: url("data:image/svg+xml,%3csvg width='200' height='182' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M64.786 181.4c-9.196 0-20.063-6.687-25.079-14.21L3.762 105.33c-5.016-8.36-5.016-20.9 0-29.259l35.945-61.86C44.723 5.851 55.59 0 64.786 0h71.055c9.196 0 20.063 6.688 25.079 14.211l35.945 61.86c4.18 8.36 4.18 20.899 0 29.258l-35.945 61.86c-4.18 8.36-15.883 14.211-25.079 14.211H64.786Z' fill='black' fill-rule='nonzero'/%3e%3c/svg%3e");
7496+ }
7497+ .mask-pentagon {
7498+ mask-image: url("data:image/svg+xml,%3csvg width='192' height='181' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 0 95.106 69.098-36.327 111.804H37.22L.894 69.098z' fill-rule='evenodd'/%3e%3c/svg%3e");
7499+ }
7500+ .mask-squircle {
7501+ mask-image: url("data:image/svg+xml,%3csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M100 0C20 0 0 20 0 100s20 100 100 100 100-20 100-100S180 0 100 0Z'/%3e%3c/svg%3e");
7502+ }
7503+ .mask-star {
7504+ mask-image: url("data:image/svg+xml,%3csvg width='192' height='180' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 137.263-58.779 42.024 22.163-68.389L.894 68.481l72.476-.243L96 0l22.63 68.238 72.476.243-58.49 42.417 22.163 68.389z' fill-rule='evenodd'/%3e%3c/svg%3e");
7505+ }
7506+ .mask-star-2 {
7507+ mask-image: url("data:image/svg+xml,%3csvg width='192' height='180' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m96 153.044-58.779 26.243 7.02-63.513L.894 68.481l63.117-13.01L96 0l31.989 55.472 63.117 13.01-43.347 47.292 7.02 63.513z' fill-rule='evenodd'/%3e%3c/svg%3e");
7508+ }
7509+ .mask-triangle {
7510+ mask-image: url("data:image/svg+xml,%3csvg width='174' height='149' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m87 148.476-86.603.185L43.86 74.423 87 0l43.14 74.423 43.463 74.238z' fill-rule='evenodd'/%3e%3c/svg%3e");
7511+ }
7512+ .mask-triangle-2 {
7513+ mask-image: url("data:image/svg+xml,%3csvg width='174' height='150' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m87 .738 86.603-.184-43.463 74.238L87 149.214 43.86 74.792.397.554z' fill-rule='evenodd'/%3e%3c/svg%3e");
7514+ }
7515+ .mask-triangle-3 {
7516+ mask-image: url("data:image/svg+xml,%3csvg width='150' height='174' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m149.369 87.107.185 86.603-74.239-43.463L.893 87.107l74.422-43.14L149.554.505z' fill-rule='evenodd'/%3e%3c/svg%3e");
7517+ }
7518+ .mask-triangle-4 {
7519+ mask-image: url("data:image/svg+xml,%3csvg width='150' height='174' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M.631 87.107.446.505l74.239 43.462 74.422 43.14-74.422 43.14L.446 173.71z' fill-rule='evenodd'/%3e%3c/svg%3e");
7520+ }
7521+ .radio-accent {
7522+ --input-color: var(--color-accent);
7523+ }
7524+ .radio-error {
7525+ --input-color: var(--color-error);
7526+ }
7527+ .radio-info {
7528+ --input-color: var(--color-info);
7529+ }
7530+ .radio-neutral {
7531+ --input-color: var(--color-neutral);
7532+ }
7533+ .radio-primary {
7534+ --input-color: var(--color-primary);
7535+ }
7536+ .radio-secondary {
7537+ --input-color: var(--color-secondary);
7538+ }
7539+ .radio-success {
7540+ --input-color: var(--color-success);
7541+ }
7542+ .radio-warning {
7543+ --input-color: var(--color-warning);
7544+ }
7545+ .range-lg {
7546+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 7);
7547+ }
7548+ .range-md {
7549+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 6);
7550+ }
7551+ .range-sm {
7552+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 5);
7553+ }
7554+ .range-xl {
7555+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 8);
7556+ }
7557+ .range-xs {
7558+ --range-thumb-size: calc(var(--size-selector, 0.25rem) * 4);
7559+ }
7560+ .select-accent {
7561+ &, &:focus, &:focus-within {
7562+ --input-color: var(--color-accent);
7563+ }
7564+ }
7565+ .select-error {
7566+ &, &:focus, &:focus-within {
7567+ --input-color: var(--color-error);
7568+ }
7569+ }
7570+ .select-info {
7571+ &, &:focus, &:focus-within {
7572+ --input-color: var(--color-info);
7573+ }
7574+ }
7575+ .select-neutral {
7576+ &, &:focus, &:focus-within {
7577+ --input-color: var(--color-neutral);
7578+ }
7579+ }
7580+ .select-primary {
7581+ &, &:focus, &:focus-within {
7582+ --input-color: var(--color-primary);
7583+ }
7584+ }
7585+ .select-secondary {
7586+ &, &:focus, &:focus-within {
7587+ --input-color: var(--color-secondary);
7588+ }
7589+ }
7590+ .select-success {
7591+ &, &:focus, &:focus-within {
7592+ --input-color: var(--color-success);
7593+ }
7594+ }
7595+ .select-warning {
7596+ &, &:focus, &:focus-within {
7597+ --input-color: var(--color-warning);
7598+ }
7599+ }
7600+ .textarea-accent {
7601+ &, &:focus, &:focus-within {
7602+ --input-color: var(--color-accent);
7603+ }
7604+ }
7605+ .textarea-error {
7606+ &, &:focus, &:focus-within {
7607+ --input-color: var(--color-error);
7608+ }
7609+ }
7610+ .textarea-info {
7611+ &, &:focus, &:focus-within {
7612+ --input-color: var(--color-info);
7613+ }
7614+ }
7615+ .textarea-neutral {
7616+ &, &:focus, &:focus-within {
7617+ --input-color: var(--color-neutral);
7618+ }
7619+ }
7620+ .textarea-primary {
7621+ &, &:focus, &:focus-within {
7622+ --input-color: var(--color-primary);
7623+ }
7624+ }
7625+ .textarea-secondary {
7626+ &, &:focus, &:focus-within {
7627+ --input-color: var(--color-secondary);
7628+ }
7629+ }
7630+ .textarea-success {
7631+ &, &:focus, &:focus-within {
7632+ --input-color: var(--color-success);
7633+ }
7634+ }
7635+ .textarea-warning {
7636+ &, &:focus, &:focus-within {
7637+ --input-color: var(--color-warning);
7638+ }
7639+ }
7640+ .toggle-accent {
7641+ &:checked, &[aria-checked="true"] {
7642+ --input-color: var(--color-accent);
7643+ }
7644+ }
7645+ .toggle-error {
7646+ &:checked, &[aria-checked="true"] {
7647+ --input-color: var(--color-error);
7648+ }
7649+ }
7650+ .toggle-info {
7651+ &:checked, &[aria-checked="true"] {
7652+ --input-color: var(--color-info);
7653+ }
7654+ }
7655+ .toggle-neutral {
7656+ &:checked, &[aria-checked="true"] {
7657+ --input-color: var(--color-neutral);
7658+ }
7659+ }
7660+ .toggle-primary {
7661+ &:checked, &[aria-checked="true"] {
7662+ --input-color: var(--color-primary);
7663+ }
7664+ }
7665+ .toggle-secondary {
7666+ &:checked, &[aria-checked="true"] {
7667+ --input-color: var(--color-secondary);
7668+ }
7669+ }
7670+ .toggle-success {
7671+ &:checked, &[aria-checked="true"] {
7672+ --input-color: var(--color-success);
7673+ }
7674+ }
7675+ .toggle-warning {
7676+ &:checked, &[aria-checked="true"] {
7677+ --input-color: var(--color-warning);
7678+ }
7679+ }
7680+ .sm\:h-24 {
7681+ @media (width >= 40rem) {
7682+ height: calc(var(--spacing) * 24);
7683+ }
7684+ }
7685+ .sm\:flex-row {
7686+ @media (width >= 40rem) {
7687+ flex-direction: row;
7688+ }
7689+ }
7690+ .sm\:items-center {
7691+ @media (width >= 40rem) {
7692+ align-items: center;
7693+ }
7694+ }
7695+ .md\:col-span-1 {
7696+ @media (width >= 48rem) {
7697+ grid-column: span 1 / span 1;
7698+ }
7699+ }
7700+ .md\:col-span-2 {
7701+ @media (width >= 48rem) {
7702+ grid-column: span 2 / span 2;
7703+ }
7704+ }
7705+ .md\:col-span-3 {
7706+ @media (width >= 48rem) {
7707+ grid-column: span 3 / span 3;
7708+ }
7709+ }
7710+ .md\:col-span-4 {
7711+ @media (width >= 48rem) {
7712+ grid-column: span 4 / span 4;
7713+ }
7714+ }
7715+ .md\:col-span-5 {
7716+ @media (width >= 48rem) {
7717+ grid-column: span 5 / span 5;
7718+ }
7719+ }
7720+ .md\:ml-4 {
7721+ @media (width >= 48rem) {
7722+ margin-left: calc(var(--spacing) * 4);
7723+ }
7724+ }
7725+ .md\:w-2\/6 {
7726+ @media (width >= 48rem) {
7727+ width: calc(2/6 * 100%);
7728+ }
7729+ }
7730+ .md\:grid-cols-2 {
7731+ @media (width >= 48rem) {
7732+ grid-template-columns: repeat(2, minmax(0, 1fr));
7733+ }
7734+ }
7735+ .md\:grid-cols-3 {
7736+ @media (width >= 48rem) {
7737+ grid-template-columns: repeat(3, minmax(0, 1fr));
7738+ }
7739+ }
7740+ .md\:grid-cols-12 {
7741+ @media (width >= 48rem) {
7742+ grid-template-columns: repeat(12, minmax(0, 1fr));
7743+ }
7744+ }
7745+ .md\:flex-row {
7746+ @media (width >= 48rem) {
7747+ flex-direction: row;
7748+ }
7749+ }
7750+ .md\:space-y-0 {
7751+ @media (width >= 48rem) {
7752+ :where(& > :not(:last-child)) {
7753+ --tw-space-y-reverse: 0;
7754+ margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
7755+ margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
7756+ }
7757+ }
7758+ }
7759+ .md\:space-x-4 {
7760+ @media (width >= 48rem) {
7761+ :where(& > :not(:last-child)) {
7762+ --tw-space-x-reverse: 0;
7763+ margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
7764+ margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
7765+ }
7766+ }
7767+ }
7768+ .md\:place-self-center {
7769+ @media (width >= 48rem) {
7770+ place-self: center;
7771+ }
7772+ }
7773+ .md\:justify-self-end {
7774+ @media (width >= 48rem) {
7775+ justify-self: flex-end;
7776+ }
7777+ }
7778+ .md\:p-0 {
7779+ @media (width >= 48rem) {
7780+ padding: calc(var(--spacing) * 0);
7781+ }
7782+ }
7783+ .md\:p-8 {
7784+ @media (width >= 48rem) {
7785+ padding: calc(var(--spacing) * 8);
7786+ }
7787+ }
7788+ .md\:px-6 {
7789+ @media (width >= 48rem) {
7790+ padding-inline: calc(var(--spacing) * 6);
7791+ }
7792+ }
7793+ .md\:pt-10 {
7794+ @media (width >= 48rem) {
7795+ padding-top: calc(var(--spacing) * 10);
7796+ }
7797+ }
7798+ .md\:pt-32 {
7799+ @media (width >= 48rem) {
7800+ padding-top: calc(var(--spacing) * 32);
7801+ }
7802+ }
7803+ .md\:text-3xl {
7804+ @media (width >= 48rem) {
7805+ font-size: var(--text-3xl);
7806+ line-height: var(--tw-leading, var(--text-3xl--line-height));
7807+ }
7808+ }
7809+ .md\:text-sm {
7810+ @media (width >= 48rem) {
7811+ font-size: var(--text-sm);
7812+ line-height: var(--tw-leading, var(--text-sm--line-height));
7813+ }
7814+ }
7815+ .md\:text-xl {
7816+ @media (width >= 48rem) {
7817+ font-size: var(--text-xl);
7818+ line-height: var(--tw-leading, var(--text-xl--line-height));
7819+ }
7820+ }
7821+ .lg\:flex {
7822+ @media (width >= 64rem) {
7823+ display: flex;
7824+ }
7825+ }
7826+ .lg\:flex-row {
7827+ @media (width >= 64rem) {
7828+ flex-direction: row;
7829+ }
7830+ }
7831+ .lg\:text-4xl {
7832+ @media (width >= 64rem) {
7833+ font-size: var(--text-4xl);
7834+ line-height: var(--tw-leading, var(--text-4xl--line-height));
7835+ }
7836+ }
7837+ .xl\:h-48 {
7838+ @media (width >= 80rem) {
7839+ height: calc(var(--spacing) * 48);
7840+ }
7841+ }
7842+ .xl\:text-5xl {
7843+ @media (width >= 80rem) {
7844+ font-size: var(--text-5xl);
7845+ line-height: var(--tw-leading, var(--text-5xl--line-height));
7846+ }
7847+ }
7848+}
7849+@layer components {
7850+ .btn-primary {
7851+ display: flex;
7852+ width: 100%;
7853+ justify-content: center;
7854+ border-radius: var(--radius-md);
7855+ border-style: var(--tw-border-style);
7856+ border-width: 1px;
7857+ border-color: transparent;
7858+ background-color: var(--color-indigo-600);
7859+ padding-inline: calc(var(--spacing) * 4);
7860+ padding-block: calc(var(--spacing) * 2);
7861+ font-size: var(--text-sm);
7862+ line-height: var(--tw-leading, var(--text-sm--line-height));
7863+ --tw-font-weight: var(--font-weight-medium);
7864+ font-weight: var(--font-weight-medium);
7865+ color: var(--color-white);
7866+ &:hover {
7867+ @media (hover: hover) {
7868+ background-color: var(--color-indigo-700);
7869+ }
7870+ }
7871+ &:focus {
7872+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
7873+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
7874+ }
7875+ &:focus {
7876+ --tw-ring-color: var(--color-indigo-500);
7877+ }
7878+ &:focus {
7879+ --tw-ring-offset-width: 2px;
7880+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
7881+ }
7882+ &:focus {
7883+ --tw-outline-style: none;
7884+ outline-style: none;
7885+ }
7886+ }
7887+ .btn-secondary {
7888+ display: flex;
7889+ width: 100%;
7890+ justify-content: center;
7891+ border-radius: var(--radius-md);
7892+ border-style: var(--tw-border-style);
7893+ border-width: 1px;
7894+ border-color: var(--color-gray-300);
7895+ background-color: var(--color-white);
7896+ padding-inline: calc(var(--spacing) * 4);
7897+ padding-block: calc(var(--spacing) * 2);
7898+ font-size: var(--text-sm);
7899+ line-height: var(--tw-leading, var(--text-sm--line-height));
7900+ --tw-font-weight: var(--font-weight-medium);
7901+ font-weight: var(--font-weight-medium);
7902+ color: var(--color-gray-700);
7903+ &:hover {
7904+ @media (hover: hover) {
7905+ background-color: var(--color-gray-50);
7906+ }
7907+ }
7908+ &:focus {
7909+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
7910+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
7911+ }
7912+ &:focus {
7913+ --tw-ring-color: var(--color-indigo-500);
7914+ }
7915+ &:focus {
7916+ --tw-ring-offset-width: 2px;
7917+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
7918+ }
7919+ &:focus {
7920+ --tw-outline-style: none;
7921+ outline-style: none;
7922+ }
7923+ }
7924+ .btn-danger {
7925+ display: flex;
7926+ width: 100%;
7927+ justify-content: center;
7928+ border-radius: var(--radius-md);
7929+ border-style: var(--tw-border-style);
7930+ border-width: 1px;
7931+ border-color: transparent;
7932+ background-color: var(--color-red-600);
7933+ padding-inline: calc(var(--spacing) * 4);
7934+ padding-block: calc(var(--spacing) * 2);
7935+ font-size: var(--text-sm);
7936+ line-height: var(--tw-leading, var(--text-sm--line-height));
7937+ --tw-font-weight: var(--font-weight-medium);
7938+ font-weight: var(--font-weight-medium);
7939+ color: var(--color-white);
7940+ &:hover {
7941+ @media (hover: hover) {
7942+ background-color: var(--color-red-700);
7943+ }
7944+ }
7945+ &:focus {
7946+ --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
7947+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
7948+ }
7949+ &:focus {
7950+ --tw-ring-color: var(--color-red-500);
7951+ }
7952+ &:focus {
7953+ --tw-ring-offset-width: 2px;
7954+ --tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
7955+ }
7956+ &:focus {
7957+ --tw-outline-style: none;
7958+ outline-style: none;
7959+ }
7960+ }
7961+ .alert-error {
7962+ border-radius: var(--radius-md);
7963+ background-color: var(--color-red-50);
7964+ padding: calc(var(--spacing) * 4);
7965+ @media (prefers-color-scheme: dark) {
7966+ background-color: color-mix(in oklab, var(--color-red-900) 20%, transparent);
7967+ }
7968+ }
7969+ .alert-error-icon {
7970+ height: calc(var(--spacing) * 5);
7971+ width: calc(var(--spacing) * 5);
7972+ color: var(--color-red-400);
7973+ }
7974+ .alert-error-text {
7975+ font-size: var(--text-sm);
7976+ line-height: var(--tw-leading, var(--text-sm--line-height));
7977+ --tw-font-weight: var(--font-weight-medium);
7978+ font-weight: var(--font-weight-medium);
7979+ color: var(--color-red-800);
7980+ @media (prefers-color-scheme: dark) {
7981+ color: var(--color-red-300);
7982+ }
7983+ }
7984+ .alert-success {
7985+ border-radius: var(--radius-md);
7986+ background-color: var(--color-green-50);
7987+ padding: calc(var(--spacing) * 4);
7988+ @media (prefers-color-scheme: dark) {
7989+ background-color: color-mix(in oklab, var(--color-green-900) 20%, transparent);
7990+ }
7991+ }
7992+ .alert-success-icon {
7993+ height: calc(var(--spacing) * 5);
7994+ width: calc(var(--spacing) * 5);
7995+ color: var(--color-green-400);
7996+ }
7997+ .alert-success-text {
7998+ font-size: var(--text-sm);
7999+ line-height: var(--tw-leading, var(--text-sm--line-height));
8000+ --tw-font-weight: var(--font-weight-medium);
8001+ font-weight: var(--font-weight-medium);
8002+ color: var(--color-green-800);
8003+ @media (prefers-color-scheme: dark) {
8004+ color: var(--color-green-300);
8005+ }
8006+ }
8007+ .card {
8008+ margin-bottom: calc(var(--spacing) * 4);
8009+ border-radius: var(--radius-md);
8010+ background-color: var(--color-white);
8011+ padding: calc(var(--spacing) * 4);
8012+ --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));
8013+ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
8014+ }
8015+ .card-header {
8016+ margin-bottom: calc(var(--spacing) * 2);
8017+ font-size: var(--text-lg);
8018+ line-height: var(--tw-leading, var(--text-lg--line-height));
8019+ --tw-font-weight: var(--font-weight-semibold);
8020+ font-weight: var(--font-weight-semibold);
8021+ color: var(--color-indigo-600);
8022+ }
8023+ .bg-app {
8024+ background-color: var(--color-indigo-50);
8025+ }
8026+ .bg-sidebar {
8027+ background-color: var(--color-indigo-900);
8028+ }
8029+ .bg-card {
8030+ background-color: var(--color-white);
8031+ }
8032+ .text-title {
8033+ --tw-font-weight: var(--font-weight-semibold);
8034+ font-weight: var(--font-weight-semibold);
8035+ color: var(--color-indigo-600);
8036+ }
8037+ .text-subtitle {
8038+ color: var(--color-indigo-500);
8039+>>>>>>> feat/templ-integration
1668040 }
1678041 }
1688042 @font-face {
@@ -251,11 +8125,450 @@
2518125 .JetBrainsMonoBoldNF, .iconfb {
2528126 font-family: "JetBrainsMono-Nerd-Font-Bold", sans-serif;
2538127 }
8128+<<<<<<< HEAD
8129+@property --tw-border-style {
8130+ syntax: "*";
8131+ inherits: false;
8132+ initial-value: solid;
8133+}
8134+@property --tw-blur {
8135+ syntax: "*";
8136+ inherits: false;
8137+}
8138+@property --tw-brightness {
8139+ syntax: "*";
8140+ inherits: false;
8141+}
8142+@property --tw-contrast {
8143+ syntax: "*";
8144+ inherits: false;
8145+}
8146+@property --tw-grayscale {
8147+ syntax: "*";
8148+ inherits: false;
8149+}
8150+@property --tw-hue-rotate {
8151+ syntax: "*";
8152+ inherits: false;
8153+}
8154+@property --tw-invert {
8155+ syntax: "*";
8156+ inherits: false;
8157+}
8158+@property --tw-opacity {
8159+ syntax: "*";
8160+ inherits: false;
8161+}
8162+@property --tw-saturate {
8163+ syntax: "*";
8164+ inherits: false;
8165+}
8166+@property --tw-sepia {
8167+ syntax: "*";
8168+ inherits: false;
8169+}
8170+@property --tw-drop-shadow {
8171+ syntax: "*";
8172+ inherits: false;
8173+}
8174+@property --tw-duration {
8175+ syntax: "*";
8176+ inherits: false;
8177+=======
8178+@layer base {
8179+ :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
8180+ color-scheme: light;
8181+ --color-base-100: oklch(100% 0 0);
8182+ --color-base-200: oklch(98% 0 0);
8183+ --color-base-300: oklch(95% 0 0);
8184+ --color-base-content: oklch(21% 0.006 285.885);
8185+ --color-primary: oklch(45% 0.24 277.023);
8186+ --color-primary-content: oklch(93% 0.034 272.788);
8187+ --color-secondary: oklch(65% 0.241 354.308);
8188+ --color-secondary-content: oklch(94% 0.028 342.258);
8189+ --color-accent: oklch(77% 0.152 181.912);
8190+ --color-accent-content: oklch(38% 0.063 188.416);
8191+ --color-neutral: oklch(14% 0.005 285.823);
8192+ --color-neutral-content: oklch(92% 0.004 286.32);
8193+ --color-info: oklch(74% 0.16 232.661);
8194+ --color-info-content: oklch(29% 0.066 243.157);
8195+ --color-success: oklch(76% 0.177 163.223);
8196+ --color-success-content: oklch(37% 0.077 168.94);
8197+ --color-warning: oklch(82% 0.189 84.429);
8198+ --color-warning-content: oklch(41% 0.112 45.904);
8199+ --color-error: oklch(71% 0.194 13.428);
8200+ --color-error-content: oklch(27% 0.105 12.094);
8201+ --radius-selector: 0.5rem;
8202+ --radius-field: 0.25rem;
8203+ --radius-box: 0.5rem;
8204+ --size-selector: 0.25rem;
8205+ --size-field: 0.25rem;
8206+ --border: 1px;
8207+ --depth: 1;
8208+ --noise: 0;
8209+ }
8210+}
8211+@layer base {
8212+ @media (prefers-color-scheme: dark) {
8213+ :root {
8214+ color-scheme: dark;
8215+ --color-base-100: oklch(25.33% 0.016 252.42);
8216+ --color-base-200: oklch(23.26% 0.014 253.1);
8217+ --color-base-300: oklch(21.15% 0.012 254.09);
8218+ --color-base-content: oklch(97.807% 0.029 256.847);
8219+ --color-primary: oklch(58% 0.233 277.117);
8220+ --color-primary-content: oklch(96% 0.018 272.314);
8221+ --color-secondary: oklch(65% 0.241 354.308);
8222+ --color-secondary-content: oklch(94% 0.028 342.258);
8223+ --color-accent: oklch(77% 0.152 181.912);
8224+ --color-accent-content: oklch(38% 0.063 188.416);
8225+ --color-neutral: oklch(14% 0.005 285.823);
8226+ --color-neutral-content: oklch(92% 0.004 286.32);
8227+ --color-info: oklch(74% 0.16 232.661);
8228+ --color-info-content: oklch(29% 0.066 243.157);
8229+ --color-success: oklch(76% 0.177 163.223);
8230+ --color-success-content: oklch(37% 0.077 168.94);
8231+ --color-warning: oklch(82% 0.189 84.429);
8232+ --color-warning-content: oklch(41% 0.112 45.904);
8233+ --color-error: oklch(71% 0.194 13.428);
8234+ --color-error-content: oklch(27% 0.105 12.094);
8235+ --radius-selector: 0.5rem;
8236+ --radius-field: 0.25rem;
8237+ --radius-box: 0.5rem;
8238+ --size-selector: 0.25rem;
8239+ --size-field: 0.25rem;
8240+ --border: 1px;
8241+ --depth: 1;
8242+ --noise: 0;
8243+ }
8244+ }
8245+}
8246+@layer base {
8247+ :root:has(input.theme-controller[value=light]:checked),[data-theme=light] {
8248+ color-scheme: light;
8249+ --color-base-100: oklch(100% 0 0);
8250+ --color-base-200: oklch(98% 0 0);
8251+ --color-base-300: oklch(95% 0 0);
8252+ --color-base-content: oklch(21% 0.006 285.885);
8253+ --color-primary: oklch(45% 0.24 277.023);
8254+ --color-primary-content: oklch(93% 0.034 272.788);
8255+ --color-secondary: oklch(65% 0.241 354.308);
8256+ --color-secondary-content: oklch(94% 0.028 342.258);
8257+ --color-accent: oklch(77% 0.152 181.912);
8258+ --color-accent-content: oklch(38% 0.063 188.416);
8259+ --color-neutral: oklch(14% 0.005 285.823);
8260+ --color-neutral-content: oklch(92% 0.004 286.32);
8261+ --color-info: oklch(74% 0.16 232.661);
8262+ --color-info-content: oklch(29% 0.066 243.157);
8263+ --color-success: oklch(76% 0.177 163.223);
8264+ --color-success-content: oklch(37% 0.077 168.94);
8265+ --color-warning: oklch(82% 0.189 84.429);
8266+ --color-warning-content: oklch(41% 0.112 45.904);
8267+ --color-error: oklch(71% 0.194 13.428);
8268+ --color-error-content: oklch(27% 0.105 12.094);
8269+ --radius-selector: 0.5rem;
8270+ --radius-field: 0.25rem;
8271+ --radius-box: 0.5rem;
8272+ --size-selector: 0.25rem;
8273+ --size-field: 0.25rem;
8274+ --border: 1px;
8275+ --depth: 1;
8276+ --noise: 0;
8277+ }
8278+}
8279+@layer base {
8280+ :root:has(input.theme-controller[value=dark]:checked),[data-theme=dark] {
8281+ color-scheme: dark;
8282+ --color-base-100: oklch(25.33% 0.016 252.42);
8283+ --color-base-200: oklch(23.26% 0.014 253.1);
8284+ --color-base-300: oklch(21.15% 0.012 254.09);
8285+ --color-base-content: oklch(97.807% 0.029 256.847);
8286+ --color-primary: oklch(58% 0.233 277.117);
8287+ --color-primary-content: oklch(96% 0.018 272.314);
8288+ --color-secondary: oklch(65% 0.241 354.308);
8289+ --color-secondary-content: oklch(94% 0.028 342.258);
8290+ --color-accent: oklch(77% 0.152 181.912);
8291+ --color-accent-content: oklch(38% 0.063 188.416);
8292+ --color-neutral: oklch(14% 0.005 285.823);
8293+ --color-neutral-content: oklch(92% 0.004 286.32);
8294+ --color-info: oklch(74% 0.16 232.661);
8295+ --color-info-content: oklch(29% 0.066 243.157);
8296+ --color-success: oklch(76% 0.177 163.223);
8297+ --color-success-content: oklch(37% 0.077 168.94);
8298+ --color-warning: oklch(82% 0.189 84.429);
8299+ --color-warning-content: oklch(41% 0.112 45.904);
8300+ --color-error: oklch(71% 0.194 13.428);
8301+ --color-error-content: oklch(27% 0.105 12.094);
8302+ --radius-selector: 0.5rem;
8303+ --radius-field: 0.25rem;
8304+ --radius-box: 0.5rem;
8305+ --size-selector: 0.25rem;
8306+ --size-field: 0.25rem;
8307+ --border: 1px;
8308+ --depth: 1;
8309+ --noise: 0;
8310+ }
8311+}
8312+@layer base {
8313+ @property --radialprogress {
8314+ syntax: "<percentage>";
8315+ inherits: true;
8316+ initial-value: 0%;
8317+ }
8318+}
8319+@layer base {
8320+ :root {
8321+ scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) #0000;
8322+ }
8323+}
8324+@layer base {
8325+ :root:has( .modal-open, .modal[open], .modal:target, .modal-toggle:checked, .drawer:not([class*="drawer-open"]) > .drawer-toggle:checked ) {
8326+ overflow: hidden;
8327+ }
8328+}
8329+@layer base {
8330+ :root,
8331+[data-theme] {
8332+ background-color: var(--root-bg, var(--color-base-100));
8333+ color: var(--color-base-content);
8334+ }
8335+}
8336+@layer base {
8337+ :root {
8338+ --fx-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");
8339+ }
8340+ .chat {
8341+ --mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e");
8342+ }
8343+}
8344+@layer base {
8345+ :where(
8346+ :root:has(
8347+ .modal-open,
8348+ .modal[open],
8349+ .modal:target,
8350+ .modal-toggle:checked,
8351+ .drawer:not(.drawer-open) > .drawer-toggle:checked
8352+ )
8353+) {
8354+ scrollbar-gutter: stable;
8355+ background-image: linear-gradient(var(--color-base-100), var(--color-base-100));
8356+ --root-bg: color-mix(in srgb, var(--color-base-100), oklch(0% 0 0) 40%);
8357+ }
8358+}
8359+@keyframes skeleton {
8360+ 0% {
8361+ background-position: 150%;
8362+ }
8363+ 100% {
8364+ background-position: -50%;
8365+ }
8366+}
8367+@keyframes progress {
8368+ 50% {
8369+ background-position-x: -115%;
8370+ }
8371+}
8372+@keyframes radio {
8373+ 0% {
8374+ padding: 5px;
8375+ }
8376+ 50% {
8377+ padding: 3px;
8378+ }
8379+}
8380+@keyframes dropdown {
8381+ 0% {
8382+ opacity: 0;
8383+ }
8384+}
8385+@keyframes rating {
8386+ 0%, 40% {
8387+ scale: 1.1;
8388+ filter: brightness(1.05) contrast(1.05);
8389+ }
8390+}
8391+@keyframes toast {
8392+ 0% {
8393+ scale: 0.9;
8394+ opacity: 0;
8395+ }
8396+ 100% {
8397+ scale: 1;
8398+ opacity: 1;
8399+ }
8400+}
8401+@layer base {
8402+ @media (prefers-color-scheme: dark) {
8403+ :root {
8404+ color-scheme: dark;
8405+ --color-base-100: oklch(28% 0.091 267.935);
8406+ --color-base-200: oklch(13% 0.028 261.692);
8407+ --color-base-300: oklch(20% 0.042 265.755);
8408+ --color-base-content: oklch(93% 0.034 272.788);
8409+ --color-primary: oklch(87% 0 0);
8410+ --color-primary-content: oklch(14% 0 0);
8411+ --color-secondary: oklch(83% 0.145 321.434);
8412+ --color-secondary-content: oklch(29% 0.136 325.661);
8413+ --color-accent: oklch(80% 0.105 251.813);
8414+ --color-accent-content: oklch(28% 0.091 267.935);
8415+ --color-neutral: oklch(37% 0.034 259.733);
8416+ --color-neutral-content: oklch(98% 0.002 247.839);
8417+ --color-info: oklch(68% 0.169 237.323);
8418+ --color-info-content: oklch(97% 0.013 236.62);
8419+ --color-success: oklch(70% 0.14 182.503);
8420+ --color-success-content: oklch(98% 0.014 180.72);
8421+ --color-warning: oklch(76% 0.188 70.08);
8422+ --color-warning-content: oklch(98% 0.022 95.277);
8423+ --color-error: oklch(64% 0.246 16.439);
8424+ --color-error-content: oklch(96% 0.015 12.422);
8425+ --radius-selector: 1rem;
8426+ --radius-field: 0rem;
8427+ --radius-box: 0.25rem;
8428+ --size-selector: 0.25rem;
8429+ --size-field: 0.25rem;
8430+ --border: 1px;
8431+ --depth: 0;
8432+ --noise: 0;
8433+ }
8434+ }
8435+}
8436+@layer base {
8437+ :where(:root),:root:has(input.theme-controller[value=gordon-dark]:checked),[data-theme="gordon-dark"] {
8438+ color-scheme: dark;
8439+ --color-base-100: oklch(28% 0.091 267.935);
8440+ --color-base-200: oklch(13% 0.028 261.692);
8441+ --color-base-300: oklch(20% 0.042 265.755);
8442+ --color-base-content: oklch(93% 0.034 272.788);
8443+ --color-primary: oklch(87% 0 0);
8444+ --color-primary-content: oklch(14% 0 0);
8445+ --color-secondary: oklch(83% 0.145 321.434);
8446+ --color-secondary-content: oklch(29% 0.136 325.661);
8447+ --color-accent: oklch(80% 0.105 251.813);
8448+ --color-accent-content: oklch(28% 0.091 267.935);
8449+ --color-neutral: oklch(37% 0.034 259.733);
8450+ --color-neutral-content: oklch(98% 0.002 247.839);
8451+ --color-info: oklch(68% 0.169 237.323);
8452+ --color-info-content: oklch(97% 0.013 236.62);
8453+ --color-success: oklch(70% 0.14 182.503);
8454+ --color-success-content: oklch(98% 0.014 180.72);
8455+ --color-warning: oklch(76% 0.188 70.08);
8456+ --color-warning-content: oklch(98% 0.022 95.277);
8457+ --color-error: oklch(64% 0.246 16.439);
8458+ --color-error-content: oklch(96% 0.015 12.422);
8459+ --radius-selector: 1rem;
8460+ --radius-field: 0rem;
8461+ --radius-box: 0.25rem;
8462+ --size-selector: 0.25rem;
8463+ --size-field: 0.25rem;
8464+ --border: 1px;
8465+ --depth: 0;
8466+ --noise: 0;
8467+ }
8468+}
8469+@property --tw-rotate-x {
8470+ syntax: "*";
8471+ inherits: false;
8472+ initial-value: rotateX(0);
8473+}
8474+@property --tw-rotate-y {
8475+ syntax: "*";
8476+ inherits: false;
8477+ initial-value: rotateY(0);
8478+}
8479+@property --tw-rotate-z {
8480+ syntax: "*";
8481+ inherits: false;
8482+ initial-value: rotateZ(0);
8483+}
8484+@property --tw-skew-x {
8485+ syntax: "*";
8486+ inherits: false;
8487+ initial-value: skewX(0);
8488+}
8489+@property --tw-skew-y {
8490+ syntax: "*";
8491+ inherits: false;
8492+ initial-value: skewY(0);
8493+}
8494+@property --tw-space-y-reverse {
8495+ syntax: "*";
8496+ inherits: false;
8497+ initial-value: 0;
8498+}
2548499 @property --tw-border-style {
2558500 syntax: "*";
2568501 inherits: false;
2578502 initial-value: solid;
2588503 }
8504+@property --tw-font-weight {
8505+ syntax: "*";
8506+ inherits: false;
8507+}
8508+@property --tw-tracking {
8509+ syntax: "*";
8510+ inherits: false;
8511+}
8512+@property --tw-shadow {
8513+ syntax: "*";
8514+ inherits: false;
8515+ initial-value: 0 0 #0000;
8516+}
8517+@property --tw-shadow-color {
8518+ syntax: "*";
8519+ inherits: false;
8520+}
8521+@property --tw-inset-shadow {
8522+ syntax: "*";
8523+ inherits: false;
8524+ initial-value: 0 0 #0000;
8525+}
8526+@property --tw-inset-shadow-color {
8527+ syntax: "*";
8528+ inherits: false;
8529+}
8530+@property --tw-ring-color {
8531+ syntax: "*";
8532+ inherits: false;
8533+}
8534+@property --tw-ring-shadow {
8535+ syntax: "*";
8536+ inherits: false;
8537+ initial-value: 0 0 #0000;
8538+}
8539+@property --tw-inset-ring-color {
8540+ syntax: "*";
8541+ inherits: false;
8542+}
8543+@property --tw-inset-ring-shadow {
8544+ syntax: "*";
8545+ inherits: false;
8546+ initial-value: 0 0 #0000;
8547+}
8548+@property --tw-ring-inset {
8549+ syntax: "*";
8550+ inherits: false;
8551+}
8552+@property --tw-ring-offset-width {
8553+ syntax: "<length>";
8554+ inherits: false;
8555+ initial-value: 0px;
8556+}
8557+@property --tw-ring-offset-color {
8558+ syntax: "*";
8559+ inherits: false;
8560+ initial-value: #fff;
8561+}
8562+@property --tw-ring-offset-shadow {
8563+ syntax: "*";
8564+ inherits: false;
8565+ initial-value: 0 0 #0000;
8566+}
8567+@property --tw-outline-style {
8568+ syntax: "*";
8569+ inherits: false;
8570+ initial-value: solid;
8571+}
2598572 @property --tw-blur {
2608573 syntax: "*";
2618574 inherits: false;
@@ -296,7 +8609,58 @@
2968609 syntax: "*";
2978610 inherits: false;
2988611 }
8612+@property --tw-backdrop-blur {
8613+ syntax: "*";
8614+ inherits: false;
8615+}
8616+@property --tw-backdrop-brightness {
8617+ syntax: "*";
8618+ inherits: false;
8619+}
8620+@property --tw-backdrop-contrast {
8621+ syntax: "*";
8622+ inherits: false;
8623+}
8624+@property --tw-backdrop-grayscale {
8625+ syntax: "*";
8626+ inherits: false;
8627+}
8628+@property --tw-backdrop-hue-rotate {
8629+ syntax: "*";
8630+ inherits: false;
8631+}
8632+@property --tw-backdrop-invert {
8633+ syntax: "*";
8634+ inherits: false;
8635+}
8636+@property --tw-backdrop-opacity {
8637+ syntax: "*";
8638+ inherits: false;
8639+}
8640+@property --tw-backdrop-saturate {
8641+ syntax: "*";
8642+ inherits: false;
8643+}
8644+@property --tw-backdrop-sepia {
8645+ syntax: "*";
8646+ inherits: false;
8647+}
2998648 @property --tw-duration {
3008649 syntax: "*";
3018650 inherits: false;
3028651 }
8652+@property --tw-ease {
8653+ syntax: "*";
8654+ inherits: false;
8655+}
8656+@property --tw-space-x-reverse {
8657+ syntax: "*";
8658+ inherits: false;
8659+ initial-value: 0;
8660+}
8661+@keyframes spin {
8662+ to {
8663+ transform: rotate(360deg);
8664+ }
8665+>>>>>>> feat/templ-integration
8666+}
M internal/webui/public/assets/js/custom.js +33 -1
@@ -187,18 +187,36 @@ function refreshContainerList() {
187187
188188 // Show loading indicator
189189 const containerManager = $('#container-manager');
190+ if (containerManager.length === 0) {
191+ console.error("Container manager element not found in DOM");
192+ return;
193+ }
194+
195+ console.debug("Container manager element found:", containerManager[0]);
196+ console.debug("Container manager classes:", containerManager.attr('class'));
197+
190198 const originalContent = containerManager.html();
191199 containerManager.html('<div class="text-center p-4"><span class="iconf text-3xl animate-spin inline-block">󰑐</span> Loading containers...</div>');
192200
201+ // Ensure the container is visible
202+ containerManager.removeClass('hidden');
203+
193204 $.ajax({
194205 url: '/htmx/container-manager',
195206 type: 'GET',
196207 success: function(data) {
208+ console.debug("Container data received, length:", data.length);
197209 $('#container-manager').html(data);
210+
211+ // Force visibility
212+ $('#container-manager').removeClass('hidden');
213+
198214 console.debug("Container list refreshed successfully");
199215 },
200216 error: function(xhr, status, error) {
201217 console.error("Error refreshing container list:", error);
218+ console.error("Status:", status);
219+ console.error("Response:", xhr.responseText);
202220 // Restore original content on error
203221 containerManager.html(originalContent);
204222 }
@@ -211,6 +229,14 @@ function refreshImageList() {
211229
212230 // Show loading indicator
213231 const imageManager = $('#image-manager');
232+ if (imageManager.length === 0) {
233+ console.error("Image manager element not found in DOM");
234+ return;
235+ }
236+
237+ console.debug("Image manager element found:", imageManager[0]);
238+ console.debug("Image manager classes:", imageManager.attr('class'));
239+
214240 const originalContent = imageManager.html();
215241 imageManager.html('<div class="text-center p-4"><span class="iconf text-3xl animate-spin inline-block">󰑐</span> Loading images...</div>');
216242
@@ -218,11 +244,14 @@ function refreshImageList() {
218244 url: '/htmx/image-manager',
219245 type: 'GET',
220246 success: function(data) {
247+ console.debug("Image data received, length:", data.length);
221248 $('#image-manager').html(data);
222249 console.debug("Image list refreshed successfully");
223250 },
224251 error: function(xhr, status, error) {
225252 console.error("Error refreshing image list:", error);
253+ console.error("Status:", status);
254+ console.error("Response:", xhr.responseText);
226255 // Restore original content on error
227256 imageManager.html(originalContent);
228257 }
@@ -236,6 +265,9 @@ function setupHtmxEventListeners() {
236265 const targetId = event.detail.target.id;
237266 console.debug(`HTMX afterOnLoad event for target: ${targetId}`);
238267
268+ // Remove the tab visibility fix which was interfering with DaisyUI
269+ // DaisyUI handles tab visibility automatically with the input[type="radio"] approach
270+
239271 // Check if the target is an action response
240272 if (targetId && targetId.startsWith('actionResponse-')) {
241273 console.debug(`Action completed for ${targetId}`);
@@ -257,7 +289,7 @@ function setupHtmxEventListeners() {
257289 console.debug('Image manager updated');
258290
259291 // Show notification for successful upload
260- if (event.detail.xhr.responseText.includes('Success')) {
292+ if (event.detail.xhr && event.detail.xhr.responseText && event.detail.xhr.responseText.includes('Success')) {
261293 showNotification('Image uploaded successfully', 'success');
262294 }
263295
A internal/webui/public/libs/chart.js +20 -0 too large to show in the browser

too large to show in the browser view raw

A internal/webui/public/libs/daisyui.js +8 -0 too large to show in the browser

too large to show in the browser view raw

M package.json +3 -1
@@ -1,5 +1,7 @@
11 {
2+ "type": "module",
23 "devDependencies": {
4+ "daisyui": "^5.0.6",
35 "prettier": "^3.5.3",
46 "prettier-plugin-tailwindcss": "^0.5.14",
57 "tailwindcss": "^4.0.14"
@@ -9,7 +11,7 @@
911 "build:css": "tailwindcss -i internal/webui/public/assets/css/custom.css -o internal/webui/public/assets/css/tailwind.css"
1012 },
1113 "dependencies": {
12- "caniuse-lite": "^1.0.30001705",
14+ "@tailwindcss/cli": "^4.0.14",
1315 "dotenv": "^16.4.7"
1416 }
1517 }
M pkg/docker/client.go +19 -3
@@ -28,14 +28,15 @@ func InitializeClient(config *Config) error {
2828 return fmt.Errorf("sock field in Config is empty")
2929 }
3030
31+ logger.Debug("Checking socket file existence", "socket_path", config.Sock)
3132 if _, err := os.Stat(config.Sock); os.IsNotExist(err) {
32- logger.Error("Failed to initialize Docker client", "error", err)
33+ logger.Error("Failed to initialize Docker client", "error", err, "socket_path", config.Sock)
3334 return err
3435 }
3536
3637 // For Podman compatibility, we need to ensure we're using the right connection approach
3738 host := "unix://" + config.Sock
38- logger.Debug("Setting up Docker client", "host", host)
39+ logger.Debug("Setting up Docker client", "host", host, "podman_enabled", config.PodmanEnable)
3940
4041 // Create the Docker client with appropriate options
4142 cli, err := client.NewClientWithOpts(
@@ -47,7 +48,18 @@ func InitializeClient(config *Config) error {
4748 return fmt.Errorf("error initializing Docker client: %s", err)
4849 }
4950
50- logger.Debug("Docker client initialized", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
51+ // Test the connection to make sure it works
52+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
53+ defer cancel()
54+
55+ logger.Debug("Testing Docker client connection", "socket", config.Sock)
56+ _, err = cli.Ping(ctx)
57+ if err != nil {
58+ logger.Error("Docker client ping failed", "error", err, "socket", config.Sock)
59+ return fmt.Errorf("docker daemon not responding: %s", err)
60+ }
61+
62+ logger.Debug("Docker client initialized successfully", "socket", config.Sock, "podman_enabled", config.PodmanEnable)
5163 dockerCli = cli
5264 currentConfig = config
5365 return nil
@@ -56,17 +68,20 @@ func InitializeClient(config *Config) error {
5668 func CheckIfInitialized() error {
5769 if currentConfig == nil || currentConfig.Sock == "" {
5870 logger.Error("Docker client check failed", "error", "client not initialized or configuration missing")
71+ logger.Debug("Docker client initialization failed: configuration is missing or empty")
5972 return fmt.Errorf("docker client is not initialized or configuration is missing")
6073 }
6174
6275 socketPath := currentConfig.Sock
6376 if _, err := os.Stat(socketPath); os.IsNotExist(err) {
6477 logger.Error("Docker client check failed", "error", "socket file not found", "path", socketPath)
78+ logger.Debug("Docker client check failed: socket file not found", "path", socketPath)
6579 return fmt.Errorf("sock file does not exist: %s", socketPath)
6680 }
6781
6882 if dockerCli == nil {
6983 logger.Error("Docker client check failed", "error", "client is nil")
84+ logger.Debug("Docker client check failed: client is nil")
7085 return fmt.Errorf("docker client is not initialized")
7186 }
7287
@@ -76,6 +91,7 @@ func CheckIfInitialized() error {
7691 _, err := dockerCli.ContainerList(ctx, container.ListOptions{})
7792 if err != nil {
7893 logger.Error("Docker client check failed", "error", err)
94+ logger.Debug("Docker client check failed: cannot connect to Docker daemon", "error", err)
7995 return fmt.Errorf("cannot connect to Docker daemon: %s", err)
8096 }
8197
M pkg/docker/containers.go +7 -5
@@ -100,10 +100,7 @@ func ListRunningContainers() ([]types.Container, error) {
100100 if err != nil {
101101 return nil, err
102102 }
103- // Check if the list is empty
104- if len(containers) == 0 {
105- return nil, fmt.Errorf("no containers found")
106- }
103+ // Just return the containers, even if it's an empty slice
107104 return containers, nil
108105 }
109106
@@ -420,6 +417,11 @@ func CheckIfNetworkExists(networkName string) (bool, error) {
420417
421418 // GetNetworkInfo returns information about a network
422419 func GetNetworkInfo(networkName string) (*network.Inspect, error) {
420+ // Check if Docker client is initialized
421+ if err := CheckIfInitialized(); err != nil {
422+ return nil, fmt.Errorf("cannot get network info: %w", err)
423+ }
424+
423425 networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, network.InspectOptions{})
424426 if err != nil {
425427 return nil, err
@@ -947,7 +949,7 @@ func ListenForContainerEvents(networkFilter string, startCallback func(string, s
947949 logger.Debug("Container stopped",
948950 "container_id", event.id,
949951 "container_name", event.name)
950-
952+
951953 // Call the stop callback if provided
952954 if stopCallback != nil {
953955 logger.Debug("Calling container stop callback",
M tailwind.config.js +39 -0
@@ -14,7 +14,46 @@ module.exports = {
1414 lbeige: "#FFDCB3",
1515 mbeige: "#D7B49F",
1616 },
17+ // Custom color scheme
18+ indigo: {
19+ 50: "#eef2ff",
20+ 100: "#e0e7ff",
21+ 200: "#c7d2fe",
22+ 300: "#a5b4fc",
23+ 400: "#818cf8",
24+ 500: "#6366f1",
25+ 600: "#4f46e5",
26+ 700: "#4338ca",
27+ 800: "#3730a3",
28+ 900: "#312e81",
29+ 950: "#1e1b4b",
30+ },
31+ red: {
32+ 50: "#fef2f2",
33+ 100: "#fee2e2",
34+ 200: "#fecaca",
35+ 300: "#fca5a5",
36+ 400: "#f87171",
37+ 500: "#ef4444",
38+ 600: "#dc2626",
39+ 700: "#b91c1c",
40+ 800: "#991b1b",
41+ 900: {
42+ DEFAULT: "#7f1d1d",
43+ 20: "rgba(127, 29, 29, 0.2)",
44+ },
45+ 950: "#450a0a",
46+ },
1747 },
1848 },
1949 },
50+ plugins: [require("daisyui")],
51+ daisyui: {
52+ themes: ["light", "dark", "gordon", "gordon-dark"],
53+ darkTheme: "gordon-dark",
54+ base: true,
55+ styled: true,
56+ utils: true,
57+ logs: true,
58+ },
2059 };
A tools.go +7 -0
@@ -0,0 +1,7 @@
1+// +build tools
2+
3+package tools
4+
5+import (
6+ _ "github.com/a-h/templ/cmd/templ"
7+)⏎̸

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close