A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

begin large refactor of UI to use tailwind and daisy

evan.jarrett.net 4c0f20a3 b1767cfb

verified
+4437 -4899
+3 -3
.air.hold.toml
··· 5 5 cmd = "go build -buildvcs=false -o ./tmp/atcr-hold ./cmd/hold" 6 6 entrypoint = ["./tmp/atcr-hold"] 7 7 include_ext = ["go"] 8 - exclude_dir = ["bin", "tmp", "vendor", "deploy", "docs", ".git", "dist", "pkg/appview"] 9 - exclude_regex = ["_test\\.go$"] 10 - delay = 1000 8 + exclude_dir = ["bin", "tmp", "vendor", "deploy", "docs", ".git", "dist", "pkg/appview", "node_modules"] 9 + exclude_regex = ["_test\\.go$", "cbor_gen\\.go$", "\\.min\\.js$"] 10 + delay = 3000 11 11 stop_on_error = true 12 12 send_interrupt = true 13 13 kill_delay = 500
+5 -5
.air.toml
··· 3 3 4 4 [build] 5 5 # Pre-build: generate assets if missing (each string is a shell command) 6 - pre_cmd = ["[ -f pkg/appview/static/js/htmx.min.js ] || go generate ./..."] 6 + pre_cmd = ["go generate ./..."] 7 7 cmd = "go build -buildvcs=false -o ./tmp/atcr-appview ./cmd/appview" 8 8 entrypoint = ["./tmp/atcr-appview", "serve"] 9 9 include_ext = ["go", "html", "css", "js"] 10 - exclude_dir = ["bin", "tmp", "vendor", "deploy", "docs", ".git", "dist"] 11 - exclude_regex = ["_test\\.go$"] 12 - delay = 1000 10 + exclude_dir = ["bin", "tmp", "vendor", "deploy", "docs", ".git", "dist", "node_modules"] 11 + exclude_regex = ["_test\\.go$", "cbor_gen\\.go$", "\\.min\\.js$"] 12 + delay = 3000 13 13 stop_on_error = true 14 14 send_interrupt = true 15 - kill_delay = 500 15 + kill_delay = 2000 16 16 17 17 [log] 18 18 time = false
+2
.claudeignore
··· 1 + # Generated files 2 + pkg/appview/public/css/style.css
+3 -2
.gitignore
··· 17 17 18 18 # Generated assets (run go generate to rebuild) 19 19 pkg/appview/licenses/spdx-licenses.json 20 - pkg/appview/static/js/htmx.min.js 21 - pkg/appview/static/js/lucide.min.js 20 + pkg/appview/public/js/htmx.min.js 21 + pkg/appview/public/js/lucide.min.js 22 22 23 23 # IDE 24 24 .zed/ ··· 31 31 # OS 32 32 .DS_Store 33 33 Thumbs.db 34 + node_modules
+2 -2
CLAUDE.md
··· 455 455 - `settings.go` - User settings management 456 456 - `api.go` - JSON API endpoints 457 457 458 - **Static Assets** (`pkg/appview/static/`, `pkg/appview/templates/`): 458 + **Static Assets** (`pkg/appview/public/`, `pkg/appview/templates/`): 459 459 - Templates use Go html/template 460 - - JavaScript in `static/js/app.js` 460 + - JavaScript in `public/js/app.js` 461 461 - Minimal CSS for clean UI 462 462 463 463 #### Hold Service (`cmd/hold/`)
+1 -1
Dockerfile.dev
··· 9 9 ENV AIR_CONFIG=${AIR_CONFIG} 10 10 11 11 RUN apt-get update && \ 12 - apt-get install -y --no-install-recommends sqlite3 libsqlite3-dev curl && \ 12 + apt-get install -y --no-install-recommends sqlite3 libsqlite3-dev curl nodejs npm && \ 13 13 rm -rf /var/lib/apt/lists/* && \ 14 14 go install github.com/air-verse/air@latest 15 15
+4 -4
Makefile
··· 16 16 17 17 # Generated asset files 18 18 GENERATED_ASSETS = \ 19 - pkg/appview/static/js/htmx.min.js \ 20 - pkg/appview/static/js/lucide.min.js \ 19 + pkg/appview/public/js/htmx.min.js \ 20 + pkg/appview/public/js/lucide.min.js \ 21 21 pkg/appview/licenses/spdx-licenses.json 22 22 23 23 generate: $(GENERATED_ASSETS) ## Run go generate to download vendor assets ··· 113 113 clean: ## Remove built binaries and generated assets 114 114 @echo "→ Cleaning build artifacts..." 115 115 rm -rf bin/ 116 - rm -f pkg/appview/static/js/htmx.min.js 117 - rm -f pkg/appview/static/js/lucide.min.js 116 + rm -f pkg/appview/public/js/htmx.min.js 117 + rm -f pkg/appview/public/js/lucide.min.js 118 118 rm -f pkg/appview/licenses/spdx-licenses.json 119 119 @echo "✓ Clean complete"
+1 -1
README.md
··· 131 131 │ ├── jetstream/ # ATProto Jetstream consumer 132 132 │ ├── middleware/ # Auth & registry middleware 133 133 │ ├── storage/ # Storage routing (hold cache, blob proxy, repository) 134 - │ ├── static/ # Static assets (JS, CSS, install scripts) 134 + │ ├── public/ # Static assets (JS, CSS, install scripts) 135 135 │ └── templates/ # HTML templates 136 136 ├── atproto/ # ATProto client, records, manifest/tag stores 137 137 ├── auth/
appview

This is a binary file and will not be displayed.

+6 -6
cmd/appview/serve.go
··· 347 347 // Mount static files if UI is enabled 348 348 if uiSessionStore != nil && uiTemplates != nil { 349 349 // Register dynamic routes for root-level files (favicons, manifests, etc.) 350 - staticHandler := appview.StaticHandler() 351 - rootFiles, err := appview.StaticRootFiles() 350 + publicHandler := appview.PublicHandler() 351 + rootFiles, err := appview.PublicRootFiles() 352 352 if err != nil { 353 353 slog.Warn("Failed to scan static root files", "error", err) 354 354 } else { ··· 358 358 mainRouter.Get("/"+file, func(w http.ResponseWriter, r *http.Request) { 359 359 // Serve the specific file from static root 360 360 r.URL.Path = "/" + file 361 - staticHandler.ServeHTTP(w, r) 361 + publicHandler.ServeHTTP(w, r) 362 362 }) 363 363 } 364 364 slog.Info("Registered dynamic root file routes", "count", len(rootFiles), "files", rootFiles) 365 365 } 366 366 367 367 // Mount subdirectory routes with clean paths 368 - mainRouter.Handle("/css/*", http.StripPrefix("/css/", appview.StaticSubdir("css"))) 369 - mainRouter.Handle("/js/*", http.StripPrefix("/js/", appview.StaticSubdir("js"))) 370 - mainRouter.Handle("/static/*", http.StripPrefix("/static/", appview.StaticSubdir("static"))) 368 + mainRouter.Handle("/css/*", http.StripPrefix("/css/", appview.PublicSubdir("css"))) 369 + mainRouter.Handle("/js/*", http.StripPrefix("/js/", appview.PublicSubdir("js"))) 370 + mainRouter.Handle("/static/*", http.StripPrefix("/static/", appview.PublicSubdir("static"))) 371 371 372 372 slog.Info("UI enabled", "home", "/", "settings", "/settings") 373 373 }
+4 -4
docs/ADMIN_PANEL.md
··· 135 135 │ ├── crew_row.html # Single crew row (for HTMX updates) 136 136 │ ├── usage_stats.html # Usage stats partial 137 137 │ └── top_users.html # Top users table partial 138 - └── static/ 138 + └── public/ 139 139 ├── css/ 140 140 │ └── admin.css # Admin-specific styles 141 141 └── js/ ··· 406 406 | `/admin/auth/oauth/authorize` | GET | Public | OAuth authorize | Start OAuth flow | 407 407 | `/admin/auth/oauth/callback` | GET | Public | `CallbackHandler` | OAuth callback | 408 408 | `/admin/auth/logout` | GET | Owner | `LogoutHandler` | Logout and clear session | 409 - | `/admin/static/*` | GET | Public | Static files | CSS, JS assets | 409 + | `/admin/public/*` | GET | Public | Static files | CSS, JS assets | 410 410 411 411 ### Route Registration 412 412 ··· 418 418 r.Get("/admin/auth/oauth/callback", ui.handleCallback) 419 419 420 420 // Static files (public) 421 - r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", ui.staticHandler())) 421 + r.Handle("/admin/public/*", http.StripPrefix("/admin/public/", ui.staticHandler())) 422 422 423 423 // Protected routes (require owner) 424 424 r.Group(func(r chi.Router) { ··· 899 899 900 900 ```html 901 901 {{ define "head" }} 902 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 902 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 903 903 <script src="https://unpkg.com/htmx.org@1.9.10"></script> 904 904 <script src="https://unpkg.com/lucide@latest"></script> 905 905 {{ end }}
+9 -9
docs/DEVELOPMENT.md
··· 65 65 │ │ ui.go checks DEV_MODE: │ │ 66 66 │ │ if DEV_MODE: │ │ 67 67 │ │ templatesFS = os.DirFS("...") │ │ 68 - │ │ staticFS = os.DirFS("...") │ │ 68 + │ │ publicFS = os.DirFS("...") │ │ 69 69 │ │ else: │ │ 70 70 │ │ use embed.FS (production) │ │ 71 71 │ │ │ │ ··· 78 78 79 79 #### Scenario 1: Edit CSS/JS/Templates 80 80 ``` 81 - 1. Edit pkg/appview/static/css/style.css in VSCode 81 + 1. Edit pkg/appview/public/css/style.css in VSCode 82 82 2. Save file 83 83 3. Change appears in container via volume mount (instant) 84 84 4. App uses os.DirFS → reads new file from disk (instant) ··· 313 313 var embeddedTemplatesFS embed.FS 314 314 315 315 //go:embed static 316 - var embeddedStaticFS embed.FS 316 + var embeddedpublicFS embed.FS 317 317 318 318 // Actual filesystems used at runtime (conditional) 319 319 var templatesFS fs.FS 320 - var staticFS fs.FS 320 + var publicFS fs.FS 321 321 322 322 func init() { 323 323 // Development mode: read from filesystem for instant updates 324 324 if os.Getenv("ATCR_DEV_MODE") == "true" { 325 325 log.Println("🔧 DEV MODE: Using filesystem for templates and static assets") 326 326 templatesFS = os.DirFS("pkg/appview/templates") 327 - staticFS = os.DirFS("pkg/appview/static") 327 + publicFS = os.DirFS("pkg/appview/static") 328 328 } else { 329 329 // Production mode: use embedded assets 330 330 log.Println("📦 PRODUCTION MODE: Using embedded assets") 331 331 templatesFS = embeddedTemplatesFS 332 - staticFS = embeddedStaticFS 332 + publicFS = embeddedpublicFS 333 333 } 334 334 } 335 335 ··· 344 344 345 345 // StaticHandler returns a handler for static files 346 346 func StaticHandler() http.Handler { 347 - sub, err := fs.Sub(staticFS, "static") 347 + sub, err := fs.Sub(publicFS, "static") 348 348 if err != nil { 349 349 log.Fatalf("Failed to create static sub-filesystem: %v", err) 350 350 } ··· 442 442 ```bash 443 443 # Edit any template, CSS, or JS file 444 444 vim pkg/appview/templates/pages/home.html 445 - vim pkg/appview/static/css/style.css 446 - vim pkg/appview/static/js/app.js 445 + vim pkg/appview/public/css/style.css 446 + vim pkg/appview/public/js/app.js 447 447 448 448 # Save file → changes appear instantly 449 449 # Just refresh browser (Cmd+R / Ctrl+R)
+27 -27
docs/MINIFY.md
··· 4 4 5 5 ATCR embeds static assets (CSS, JavaScript) directly into the binary using Go's `embed` directive. Currently: 6 6 7 - - **CSS Size:** 40KB (`pkg/appview/static/css/style.css`, 2,210 lines) 7 + - **CSS Size:** 40KB (`pkg/appview/public/css/style.css`, 2,210 lines) 8 8 - **Embedded:** All static files compiled into binary at build time 9 9 - **No Minification:** Source files embedded as-is 10 10 ··· 37 37 38 38 ### Step 2: Create Minification Script 39 39 40 - Create `pkg/appview/static/minify_assets.go`: 40 + Create `pkg/appview/public/minify_assets.go`: 41 41 42 42 ```go 43 43 //go:build ignore ··· 68 68 69 69 // Minify CSS 70 70 if err := minifyFile(m, "text/css", 71 - filepath.Join(dir, "pkg/appview/static/css/style.css"), 72 - filepath.Join(dir, "pkg/appview/static/css/style.min.css"), 71 + filepath.Join(dir, "pkg/appview/public/css/style.css"), 72 + filepath.Join(dir, "pkg/appview/public/css/style.min.css"), 73 73 ); err != nil { 74 74 log.Fatalf("Failed to minify CSS: %v", err) 75 75 } 76 76 77 77 // Minify JavaScript 78 78 if err := minifyFile(m, "text/javascript", 79 - filepath.Join(dir, "pkg/appview/static/js/app.js"), 80 - filepath.Join(dir, "pkg/appview/static/js/app.min.js"), 79 + filepath.Join(dir, "pkg/appview/public/js/app.js"), 80 + filepath.Join(dir, "pkg/appview/public/js/app.min.js"), 81 81 ); err != nil { 82 82 log.Fatalf("Failed to minify JS: %v", err) 83 83 } ··· 120 120 Add to `pkg/appview/ui.go` (before the `//go:embed` directive): 121 121 122 122 ```go 123 - //go:generate go run ./static/minify_assets.go 123 + //go:generate go run ./public/minify_assets.go 124 124 125 - //go:embed static 126 - var staticFS embed.FS 125 + //go:embed public 126 + var publicFS embed.FS 127 127 ``` 128 128 129 129 ### Step 4: Update HTML Templates ··· 132 132 133 133 **Before:** 134 134 ```html 135 - <link rel="stylesheet" href="/static/css/style.css"> 136 - <script src="/static/js/app.js"></script> 135 + <link rel="stylesheet" href="/public/css/style.css"> 136 + <script src="/public/js/app.js"></script> 137 137 ``` 138 138 139 139 **After:** 140 140 ```html 141 - <link rel="stylesheet" href="/static/css/style.min.css"> 142 - <script src="/static/js/app.min.js"></script> 141 + <link rel="stylesheet" href="/public/css/style.min.css"> 142 + <script src="/public/js/app.min.js"></script> 143 143 ``` 144 144 145 145 **Files to update:** ··· 167 167 168 168 ``` 169 169 # Generated minified assets 170 - pkg/appview/static/css/*.min.css 171 - pkg/appview/static/js/*.min.js 170 + pkg/appview/public/css/*.min.css 171 + pkg/appview/public/js/*.min.js 172 172 ``` 173 173 174 174 **Alternative:** Commit minified files if you want reproducible builds without running `go generate`. ··· 194 194 //go:build !production 195 195 196 196 //go:embed static 197 - var staticFS embed.FS 197 + var publicFS embed.FS 198 198 199 - func StylePath() string { return "/static/css/style.css" } 200 - func ScriptPath() string { return "/static/js/app.js" } 199 + func StylePath() string { return "/public/css/style.css" } 200 + func ScriptPath() string { return "/public/js/app.js" } 201 201 ``` 202 202 203 203 **pkg/appview/ui_production.go** (production): 204 204 ```go 205 205 //go:build production 206 206 207 - //go:generate go run ./static/minify_assets.go 207 + //go:generate go run ./public/minify_assets.go 208 208 209 209 //go:embed static 210 - var staticFS embed.FS 210 + var publicFS embed.FS 211 211 212 - func StylePath() string { return "/static/css/style.min.css" } 213 - func ScriptPath() string { return "/static/js/app.min.js" } 212 + func StylePath() string { return "/public/css/style.min.css" } 213 + func ScriptPath() string { return "/public/js/app.min.js" } 214 214 ``` 215 215 216 216 **Usage:** ··· 230 230 Use Node.js-based minifiers via `go:generate`: 231 231 232 232 ```go 233 - //go:generate sh -c "npx cssnano static/css/style.css static/css/style.min.css" 234 - //go:generate sh -c "npx esbuild static/js/app.js --minify --outfile=static/js/app.min.js" 233 + //go:generate sh -c "npx cssnano public/css/style.css public/css/style.min.css" 234 + //go:generate sh -c "npx esbuild public/js/app.js --minify --outfile=public/js/app.min.js" 235 235 ``` 236 236 237 237 **Pros:** ··· 251 251 import "github.com/NYTimes/gziphandler" 252 252 253 253 // Wrap static handler 254 - mux.Handle("/static/", gziphandler.GzipHandler(appview.StaticHandler())) 254 + mux.Handle("/public/", gziphandler.GzipHandler(appview.StaticHandler())) 255 255 ``` 256 256 257 257 **Pros:** ··· 316 316 ### Development Workflow 317 317 318 318 1. **Edit source files:** 319 - - Modify `pkg/appview/static/css/style.css` 320 - - Modify `pkg/appview/static/js/app.js` 319 + - Modify `pkg/appview/public/css/style.css` 320 + - Modify `pkg/appview/public/js/app.js` 321 321 322 322 2. **Test locally:** 323 323 ```bash
+558
docs/REBRAND.md
··· 1 + # Website Visual Improvement Plan 2 + 3 + ## Goal 4 + Create a fun, personality-driven container registry that embraces its nautical theme while being clearly functional. Think GitHub's Octocat or DigitalOcean's Sammy - playful but professional. 5 + 6 + ## Brand Identity (from seahorse logo) 7 + - **Primary Teal**: #4ECDC4 (body color) - the "ocean" feel 8 + - **Dark Teal**: #2E8B8B (mane/fins) - depth and contrast 9 + - **Mint Background**: #C8F0E7 - light, airy, underwater 10 + - **Coral Accent**: #FF6B6B (eye) - warmth, CTAs, highlights 11 + - **Nautical theme to embrace:** 12 + - "Ship" containers (not just push) 13 + - "Holds" for storage (like a ship's cargo hold) 14 + - "Sailors" are users, "Captains" own holds 15 + - Seahorse mascot as the friendly guide 16 + 17 + ## Design Direction: Fun but Functional 18 + - Softer, more rounded corners 19 + - Playful color combinations (teal + coral) 20 + - Mascot appearances in empty states, loading, errors 21 + - Ocean-inspired subtle backgrounds (gradients, waves) 22 + - Friendly copy and microcopy throughout 23 + - Still clearly a container registry with all the technical info 24 + 25 + ## Current State 26 + - Pure CSS with custom properties for theming 27 + - Basic card designs for repositories 28 + - Simple hero section with terminal mockup 29 + - Existing badges: Helm charts, multi-arch, attestations 30 + - Existing stats: stars, pull counts 31 + 32 + ## Layout Wireframes 33 + 34 + ### Current Homepage Layout 35 + ``` 36 + ┌─────────────────────────────────────────────────────────────────┐ 37 + │ [Logo] [Search] [Theme] [User] │ Navbar 38 + ├─────────────────────────────────────────────────────────────────┤ 39 + │ │ 40 + │ ship containers on the open web. │ Hero 41 + │ ┌─────────────────────────┐ │ 42 + │ │ $ docker login atcr.io │ │ 43 + │ └─────────────────────────┘ │ 44 + │ [Get Started] [Learn More] │ 45 + │ │ 46 + │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Benefits 47 + │ │ Docker │ │ Your Data │ │ Discover │ │ 48 + │ └─────────────┘ └─────────────┘ └─────────────┘ │ 49 + ├─────────────────────────────────────────────────────────────────┤ 50 + │ │ 51 + │ Featured │ 52 + │ ┌─────────────────────────────────────────────────────────────┐│ 53 + │ │ [icon] user/repo ★ 12 ↓ 340 ││ WIDE cards 54 + │ │ Description text here... ││ (current) 55 + │ └─────────────────────────────────────────────────────────────┘│ 56 + │ ┌─────────────────────────────────────────────────────────────┐│ 57 + │ │ [icon] user/repo2 ★ 5 ↓ 120 ││ 58 + │ └─────────────────────────────────────────────────────────────┘│ 59 + │ │ 60 + │ What's New │ 61 + │ (similar wide cards) │ 62 + └─────────────────────────────────────────────────────────────────┘ 63 + ``` 64 + 65 + ### Proposed Layout: Tile Grid 66 + ``` 67 + ┌─────────────────────────────────────────────────────────────────┐ 68 + │ [Logo] [Search] [Theme] [User] │ 69 + ├─────────────────────────────────────────────────────────────────┤ 70 + │ │ 71 + │ ship containers on the open web. │ 72 + │ ┌─────────────────────────┐ │ 73 + │ │ $ docker login atcr.io │ │ 74 + │ └─────────────────────────┘ │ 75 + │ [Get Started] [Learn More] │ 76 + │ │ 77 + │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ 78 + │ │ Docker │ │ Your Data │ │ Discover │ │ 79 + │ └────────────┘ └────────────┘ └────────────┘ │ 80 + ├─────────────────────────────────────────────────────────────────┤ 81 + │ │ 82 + │ Featured [View All] │ 83 + │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐│ 84 + │ │ [icon] │ │ [icon] │ │ [icon] ││ 3 columns 85 + │ │ user/repo │ │ user/repo2 │ │ user/repo3 ││ ~300px each 86 + │ │ Description... │ │ Description... │ │ Description... ││ 87 + │ │ ────────────────││ │ ────────────────││ │ ────────────────│││ 88 + │ │ ★ 12 ↓ 340 │ │ ★ 5 ↓ 120 │ │ ★ 8 ↓ 89 ││ 89 + │ └──────────────────┘ └──────────────────┘ └──────────────────┘│ 90 + │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐│ 91 + │ │ ... │ │ ... │ │ ... ││ 92 + │ └──────────────────┘ └──────────────────┘ └──────────────────┘│ 93 + │ │ 94 + ├─────────────────────────────────────────────────────────────────┤ 95 + │ │ 96 + │ What's New │ 97 + │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐│ 98 + │ │ ... │ │ ... │ │ ... ││ Same tile 99 + │ └──────────────────┘ └──────────────────┘ └──────────────────┘│ layout 100 + └─────────────────────────────────────────────────────────────────┘ 101 + ``` 102 + 103 + ### Unified Tile Card (Same for Featured & What's New) 104 + ``` 105 + ┌─────────────────────────────┐ 106 + │ ┌────┐ user/repo [Helm] │ Icon + name + type badge 107 + │ │icon│ :latest │ Tag (if applicable) 108 + │ └────┘ │ 109 + │ │ 110 + │ Description text that │ Description (2-3 lines max) 111 + │ wraps nicely here... │ 112 + │ │ 113 + │ sha256:abcdef12 │ Digest (truncated) 114 + │ ───────────────────────────│ Divider 115 + │ ★ 12 ↓ 340 1 day ago │ Stats + timestamp 116 + └─────────────────────────────┘ 117 + 118 + Card anatomy: 119 + ┌─────────────────────────────┐ 120 + │ HEADER │ - Icon (48x48) 121 + │ - icon + name + badge │ - user/repo 122 + │ - tag (optional) │ - :tag or :latest 123 + ├─────────────────────────────┤ 124 + │ BODY │ - Description (clamp 2-3 lines) 125 + │ - description │ - sha256:abc... (monospace) 126 + │ - digest │ 127 + ├─────────────────────────────┤ 128 + │ FOOTER │ - ★ star count 129 + │ - stats + time │ - ↓ pull count 130 + │ │ - "2 hours ago" 131 + └─────────────────────────────┘ 132 + ``` 133 + 134 + ### Both Sections Use Same Card (Different Sort) 135 + ``` 136 + Featured (by stars/curated): What's New (by last_push): 137 + ┌─────────────────────────┐ ┌─────────────────────────┐ 138 + │ user/repo │ │ user/repo │ 139 + │ :latest │ │ :v1.2.3 │ ← latest tag 140 + │ Description... │ │ Description... │ 141 + │ │ │ │ 142 + │ sha256:abc123 │ │ sha256:def456 │ ← latest digest 143 + │ ───────────────────────│ │ ───────────────────────│ 144 + │ ★ 12 ↓ 340 1 day ago │ │ ★ 5 ↓ 89 2 hrs ago │ ← last_push time 145 + └─────────────────────────┘ └─────────────────────────┘ 146 + 147 + Same card component, different data source: 148 + - Featured: GetFeaturedRepos() (curated or by stars) 149 + - What's New: GetRecentlyUpdatedRepos() (ORDER BY last_push DESC) 150 + ``` 151 + 152 + ### Card Dimensions Comparison 153 + ``` 154 + Current: █████████████████████████████████████████ (~800px+ wide) 155 + Proposed: ████████████ ████████████ ████████████ (~280-320px each) 156 + Card 1 Card 2 Card 3 157 + ``` 158 + 159 + ### Mobile Responsive Behavior 160 + ``` 161 + Desktop (>1024px): [Card] [Card] [Card] 3 columns 162 + Tablet (768-1024px): [Card] [Card] 2 columns 163 + Mobile (<768px): [Card] 1 column (full width) 164 + ``` 165 + 166 + ### Playful Elements 167 + ``` 168 + Empty State (no repos): 169 + ┌─────────────────────────────────────────┐ 170 + │ │ 171 + │ 🐴 (seahorse) │ 172 + │ "Nothing here yet!" │ 173 + │ │ 174 + │ Ship your first container to get │ 175 + │ started on your voyage. │ 176 + │ │ 177 + │ [Start Shipping] │ 178 + └─────────────────────────────────────────┘ 179 + 180 + Error/404: 181 + ┌─────────────────────────────────────────┐ 182 + │ │ 183 + │ 🐴 (confused seahorse) │ 184 + │ "Lost at sea!" │ 185 + │ │ 186 + │ We couldn't find that container. │ 187 + │ Maybe it drifted away? │ 188 + │ │ 189 + │ [Back to Shore] │ 190 + └─────────────────────────────────────────┘ 191 + 192 + Hero with subtle ocean feel: 193 + ┌─────────────────────────────────────────┐ 194 + │ ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋ │ Subtle wave pattern bg 195 + │ │ 196 + │ ship containers on the │ 197 + │ open web. 🐴 │ Mascot appears! 198 + │ │ 199 + │ ┌─────────────────────┐ │ 200 + │ │ $ docker login ... │ │ 201 + │ └─────────────────────┘ │ 202 + │ │ 203 + │ ≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋≋ │ 204 + └─────────────────────────────────────────┘ 205 + ``` 206 + 207 + ### Card with Personality 208 + ``` 209 + ┌───────────────────────────────────┐ 210 + │ ┌──────┐ │ 211 + │ │ icon │ user/repo │ 212 + │ │ │ :latest [⚓ Helm] │ Anchor icon for Helm 213 + │ └──────┘ │ 214 + │ │ 215 + │ A container that does amazing │ 216 + │ things for your app... │ 217 + │ │ 218 + │ sha256:abcdef12 │ 219 + │ ─────────────────────────────────│ 220 + │ ★ 12 ↓ 340 1 day ago │ 221 + │ │ 222 + │ 🐴 Shipped by alice.bsky.social │ Playful "shipped by" line 223 + └───────────────────────────────────┘ 224 + 225 + (optional: "Shipped by" could be subtle or only on hover) 226 + ``` 227 + 228 + ## Design Improvements 229 + 230 + ### 1. Enhanced Card Design (Priority: High) 231 + **Files:** `pkg/appview/public/css/style.css`, `pkg/appview/templates/components/repo-card.html` 232 + 233 + - Add subtle gradient backgrounds on hover 234 + - Improve shadow depth (layered shadows for modern look) 235 + - Add smooth transitions (transform, box-shadow) 236 + - Better icon styling with ring/border accent 237 + - Enhanced badge visibility with better contrast 238 + - Add "Updated X ago" timestamp to cards 239 + - Improve stat icon/count alignment and spacing 240 + 241 + ### 2. Hero Section Polish (Priority: High) 242 + **Files:** `pkg/appview/public/css/style.css`, `pkg/appview/templates/pages/home.html` 243 + 244 + - Add subtle background pattern or gradient mesh 245 + - Improve terminal mockup styling (better shadows, glow effect) 246 + - Enhance benefit cards with icons and better spacing 247 + - Add visual separation between hero and content 248 + - Improve CTA button styling with better hover states 249 + 250 + ### 3. Typography & Spacing (Priority: High) 251 + **Files:** `pkg/appview/public/css/style.css` 252 + 253 + - Increase visual hierarchy with better font weights 254 + - Add more breathing room (padding/margins) 255 + - Improve heading styles with subtle underlines or accents 256 + - Better link styling with hover states 257 + - Add letter-spacing to badges for readability 258 + 259 + ### 4. Badge System Enhancement (Priority: Medium) 260 + **Files:** `pkg/appview/public/css/style.css`, templates 261 + 262 + - Create unified badge design language 263 + - Add subtle icons inside badges (already using Lucide) 264 + - Improve color coding: Helm (blue), Attestation (green), Multi-arch (purple) 265 + - Add "Official" or "Verified" badge styling (for future use) 266 + - Better hover states on interactive badges 267 + 268 + ### 5. Featured Section Improvements (Priority: Medium) 269 + **Files:** `pkg/appview/templates/pages/home.html`, `pkg/appview/public/css/style.css` 270 + 271 + - Add section header with subtle styling 272 + - Improve grid responsiveness 273 + - Add "View All" link styling 274 + - Better visual distinction from "What's New" section 275 + 276 + ### 6. Navigation Polish (Priority: Medium) 277 + **Files:** `pkg/appview/public/css/style.css`, nav templates 278 + 279 + - Improve search bar visibility and styling 280 + - Better user menu dropdown aesthetics 281 + - Add subtle border or shadow to navbar 282 + - Improve mobile responsiveness 283 + 284 + ### 7. Loading & Empty States (Priority: Low) 285 + **Files:** `pkg/appview/public/css/style.css` 286 + 287 + - Add skeleton loading animations 288 + - Improve empty state illustrations/styling 289 + - Better transition when content loads 290 + 291 + ### 8. Micro-interactions (Priority: Low) 292 + **Files:** `pkg/appview/public/css/style.css`, `pkg/appview/public/js/app.js` 293 + 294 + - Add subtle hover animations throughout 295 + - Improve button press feedback 296 + - Star button animation on click 297 + - Copy button success animation 298 + 299 + ## Implementation Order 300 + 301 + 1. **Phase 1: Core Card Styling** 302 + - Update `.featured-card` with modern shadows and transitions 303 + - Enhance badge styling in `style.css` 304 + - Add hover effects and transforms 305 + 306 + 2. **Phase 2: Hero & Featured Section** 307 + - Improve hero section gradient/background 308 + - Polish benefit cards 309 + - Add section separators 310 + 311 + 3. **Phase 3: Typography & Spacing** 312 + - Update font weights and sizes 313 + - Improve padding throughout 314 + - Better visual rhythm 315 + 316 + 4. **Phase 4: Navigation & Polish** 317 + - Navbar improvements 318 + - Loading states 319 + - Final micro-interactions 320 + 321 + ## Key CSS Changes 322 + 323 + ### Tile Grid Layout 324 + ```css 325 + .featured-grid { 326 + display: grid; 327 + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 328 + gap: 1.5rem; 329 + } 330 + 331 + /* Already exists but updating min-width */ 332 + .featured-card { 333 + min-height: 200px; 334 + display: flex; 335 + flex-direction: column; 336 + justify-content: space-between; 337 + } 338 + ``` 339 + 340 + ### Enhanced Shadow System (Multi-layer for depth) 341 + ```css 342 + --shadow-card: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.05); 343 + --shadow-card-hover: 0 8px 25px rgba(78,205,196,0.15), 0 4px 12px rgba(0,0,0,0.1); 344 + --shadow-nav: 0 2px 8px rgba(0,0,0,0.1); 345 + ``` 346 + 347 + ### Card Design Enhancement 348 + ```css 349 + .featured-card { 350 + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; 351 + border: 1px solid var(--border); 352 + } 353 + .featured-card:hover { 354 + transform: translateY(-4px); 355 + box-shadow: var(--shadow-card-hover); 356 + border-color: var(--primary); /* teal accent on hover */ 357 + } 358 + ``` 359 + 360 + ### Icon Container Styling 361 + ```css 362 + .featured-icon-placeholder { 363 + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); 364 + box-shadow: 0 2px 8px rgba(78,205,196,0.3); 365 + } 366 + ``` 367 + 368 + ### Badge System (Consistent, Accessible) 369 + ```css 370 + .badge-helm { 371 + background: #0d6cbf; 372 + color: #fff; 373 + } 374 + .badge-multi { 375 + background: #7c3aed; 376 + color: #fff; 377 + } 378 + .badge-attestation { 379 + background: #059669; 380 + color: #fff; 381 + } 382 + /* All badges: */ 383 + font-weight: 600; 384 + letter-spacing: 0.02em; 385 + text-transform: uppercase; 386 + font-size: 0.7rem; 387 + padding: 0.25rem 0.5rem; 388 + border-radius: 4px; 389 + ``` 390 + 391 + ### Hero Section Enhancement 392 + ```css 393 + .hero-section { 394 + background: 395 + linear-gradient(135deg, var(--hero-bg-start) 0%, var(--hero-bg-end) 50%, rgba(78,205,196,0.1) 100%), 396 + url('/static/wave-pattern.svg'); /* subtle wave pattern */ 397 + background-size: cover, 100% 50px; 398 + background-position: center, bottom; 399 + background-repeat: no-repeat, repeat-x; 400 + } 401 + .benefit-card { 402 + border: 1px solid transparent; 403 + border-radius: 12px; /* softer corners */ 404 + transition: all 0.2s ease; 405 + } 406 + .benefit-card:hover { 407 + border-color: var(--primary); 408 + transform: translateY(-4px); 409 + } 410 + ``` 411 + 412 + ### Playful Border Radius (Softer Feel) 413 + ```css 414 + :root { 415 + --radius-sm: 6px; /* was 4px */ 416 + --radius-md: 12px; /* was 8px */ 417 + --radius-lg: 16px; /* new */ 418 + } 419 + 420 + .featured-card { border-radius: var(--radius-md); } 421 + .benefit-card { border-radius: var(--radius-md); } 422 + .btn { border-radius: var(--radius-sm); } 423 + .hero-terminal { border-radius: var(--radius-lg); } 424 + ``` 425 + 426 + ### Fun Empty States 427 + ```css 428 + .empty-state { 429 + text-align: center; 430 + padding: 3rem; 431 + } 432 + .empty-state-mascot { 433 + width: 120px; 434 + height: auto; 435 + margin-bottom: 1.5rem; 436 + animation: float 3s ease-in-out infinite; 437 + } 438 + @keyframes float { 439 + 0%, 100% { transform: translateY(0); } 440 + 50% { transform: translateY(-10px); } 441 + } 442 + .empty-state-title { 443 + font-size: 1.5rem; 444 + font-weight: 600; 445 + color: var(--fg); 446 + } 447 + .empty-state-text { 448 + color: var(--secondary); 449 + margin-bottom: 1.5rem; 450 + } 451 + ``` 452 + 453 + ### Typography Refinements 454 + ```css 455 + .featured-title { 456 + font-weight: 600; 457 + letter-spacing: -0.01em; 458 + } 459 + .featured-description { 460 + line-height: 1.5; 461 + opacity: 0.85; 462 + } 463 + ``` 464 + 465 + ## Data Model Change 466 + 467 + **Current "What's New":** Shows individual pushes (each tag push is a separate card) 468 + 469 + **Proposed "What's New":** Shows repos ordered by last update time (same as Featured, different sort) 470 + 471 + **Tracking:** `repository_stats` table already has `last_push` timestamp! 472 + ```sql 473 + SELECT * FROM repository_stats ORDER BY last_push DESC LIMIT 9; 474 + ``` 475 + 476 + **Unified Card Data:** 477 + | Field | Source | 478 + |-------|--------| 479 + | Handle, Repository | users + manifests | 480 + | Tag | Latest tag from `tags` table | 481 + | Digest | From latest tag or manifest | 482 + | Description, IconURL | repo_pages or annotations | 483 + | StarCount, PullCount | stars count + repository_stats | 484 + | LastUpdated | `repository_stats.last_push` | 485 + | ArtifactType | manifests.artifact_type | 486 + 487 + ## Files to Modify 488 + 489 + | File | Changes | 490 + |------|---------| 491 + | `pkg/appview/public/css/style.css` | Rounded corners, shadows, hover, badges, ocean theme | 492 + | `pkg/appview/public/wave-pattern.svg` | NEW: Subtle wave pattern for hero background | 493 + | `pkg/appview/templates/components/repo-card.html` | Add Tag, Digest, LastUpdated fields | 494 + | `pkg/appview/templates/components/empty-state.html` | NEW: Reusable fun empty state with mascot | 495 + | `pkg/appview/templates/pages/home.html` | Both sections use repo-card grid | 496 + | `pkg/appview/templates/pages/404.html` | Fun "Lost at sea" error page | 497 + | `pkg/appview/db/queries.go` | New `GetRecentlyUpdatedRepos()` query; add fields to `RepoCardData` | 498 + | `pkg/appview/handlers/home.go` | Replace `GetRecentPushes` with `GetRecentlyUpdatedRepos` | 499 + | `pkg/appview/templates/partials/push-list.html` | Delete or repurpose (no longer needed) | 500 + 501 + ## Dependencies 502 + 503 + **Mascot Art Needed:** 504 + - `seahorse-empty.svg` - Friendly pose for "nothing here yet" empty states 505 + - `seahorse-confused.svg` - Lost/confused pose for 404 errors 506 + - `seahorse-waving.svg` (optional) - For hero section accent 507 + 508 + **Can proceed without art:** 509 + - CSS changes (colors, shadows, rounded corners, gradients) 510 + - Card layout and grid changes 511 + - Data layer changes (queries, handlers) 512 + - Wave pattern background (simple SVG) 513 + 514 + **Blocked until art is ready:** 515 + - Empty state component with mascot 516 + - 404 page redesign with mascot 517 + - Hero mascot integration (optional) 518 + 519 + ## Implementation Phases 520 + 521 + ### Phase 1: CSS & Layout (No art needed) 522 + 1. Update border-radius variables (softer corners) 523 + 2. New shadow system 524 + 3. Card hover effects with teal accent 525 + 4. Tile grid layout (`minmax(280px, 1fr)`) 526 + 5. Wave pattern SVG for hero background 527 + 528 + ### Phase 2: Card Component & Data 529 + 1. Update `repo-card.html` with new structure 530 + 2. Add `Digest`, `Tag`, `CreatedAt` fields 531 + 3. Update queries for latest manifest info 532 + 4. Replace push list with card grid 533 + 534 + ### Phase 3: Hero & Section Polish 535 + 1. Hero gradient + wave pattern 536 + 2. Benefit card improvements 537 + 3. Section headers and spacing 538 + 4. Mobile responsive breakpoints 539 + 540 + ### Phase 4: Mascot Integration (BLOCKED - needs art) 541 + 1. Empty state component with mascot 542 + 2. 404 page with confused seahorse 543 + 3. Hero mascot (optional) 544 + 545 + ### Phase 5: Testing 546 + 1. Dark mode verification 547 + 2. Mobile responsive check 548 + 3. All functionality works (stars, links, copy) 549 + 550 + ## Verification 551 + 552 + 1. **Visual check on homepage** - cards have depth and polish 553 + 2. **Hover states** - smooth transitions on cards, buttons, badges 554 + 3. **Dark mode** - all changes work in both themes 555 + 4. **Mobile** - responsive at all breakpoints 556 + 5. **Functionality** - stars, search, navigation all work 557 + 6. **Performance** - no jank from CSS transitions 558 + 7. **Accessibility** - badge text readable (contrast check)
+1573
package-lock.json
··· 1 + { 2 + "name": "atcr-styles", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "atcr-styles", 9 + "version": "1.0.0", 10 + "dependencies": { 11 + "actor-typeahead": "^0.1.2", 12 + "htmx.org": "^2.0.8", 13 + "lucide": "^0.562.0" 14 + }, 15 + "devDependencies": { 16 + "@tailwindcss/cli": "^4.1.18", 17 + "daisyui": "^5.5.14", 18 + "esbuild": "^0.27.2", 19 + "tailwindcss": "^4.1" 20 + } 21 + }, 22 + "node_modules/@esbuild/aix-ppc64": { 23 + "version": "0.27.2", 24 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", 25 + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", 26 + "cpu": [ 27 + "ppc64" 28 + ], 29 + "dev": true, 30 + "license": "MIT", 31 + "optional": true, 32 + "os": [ 33 + "aix" 34 + ], 35 + "engines": { 36 + "node": ">=18" 37 + } 38 + }, 39 + "node_modules/@esbuild/android-arm": { 40 + "version": "0.27.2", 41 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", 42 + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", 43 + "cpu": [ 44 + "arm" 45 + ], 46 + "dev": true, 47 + "license": "MIT", 48 + "optional": true, 49 + "os": [ 50 + "android" 51 + ], 52 + "engines": { 53 + "node": ">=18" 54 + } 55 + }, 56 + "node_modules/@esbuild/android-arm64": { 57 + "version": "0.27.2", 58 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", 59 + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", 60 + "cpu": [ 61 + "arm64" 62 + ], 63 + "dev": true, 64 + "license": "MIT", 65 + "optional": true, 66 + "os": [ 67 + "android" 68 + ], 69 + "engines": { 70 + "node": ">=18" 71 + } 72 + }, 73 + "node_modules/@esbuild/android-x64": { 74 + "version": "0.27.2", 75 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", 76 + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", 77 + "cpu": [ 78 + "x64" 79 + ], 80 + "dev": true, 81 + "license": "MIT", 82 + "optional": true, 83 + "os": [ 84 + "android" 85 + ], 86 + "engines": { 87 + "node": ">=18" 88 + } 89 + }, 90 + "node_modules/@esbuild/darwin-arm64": { 91 + "version": "0.27.2", 92 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", 93 + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", 94 + "cpu": [ 95 + "arm64" 96 + ], 97 + "dev": true, 98 + "license": "MIT", 99 + "optional": true, 100 + "os": [ 101 + "darwin" 102 + ], 103 + "engines": { 104 + "node": ">=18" 105 + } 106 + }, 107 + "node_modules/@esbuild/darwin-x64": { 108 + "version": "0.27.2", 109 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", 110 + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", 111 + "cpu": [ 112 + "x64" 113 + ], 114 + "dev": true, 115 + "license": "MIT", 116 + "optional": true, 117 + "os": [ 118 + "darwin" 119 + ], 120 + "engines": { 121 + "node": ">=18" 122 + } 123 + }, 124 + "node_modules/@esbuild/freebsd-arm64": { 125 + "version": "0.27.2", 126 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", 127 + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", 128 + "cpu": [ 129 + "arm64" 130 + ], 131 + "dev": true, 132 + "license": "MIT", 133 + "optional": true, 134 + "os": [ 135 + "freebsd" 136 + ], 137 + "engines": { 138 + "node": ">=18" 139 + } 140 + }, 141 + "node_modules/@esbuild/freebsd-x64": { 142 + "version": "0.27.2", 143 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", 144 + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", 145 + "cpu": [ 146 + "x64" 147 + ], 148 + "dev": true, 149 + "license": "MIT", 150 + "optional": true, 151 + "os": [ 152 + "freebsd" 153 + ], 154 + "engines": { 155 + "node": ">=18" 156 + } 157 + }, 158 + "node_modules/@esbuild/linux-arm": { 159 + "version": "0.27.2", 160 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", 161 + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", 162 + "cpu": [ 163 + "arm" 164 + ], 165 + "dev": true, 166 + "license": "MIT", 167 + "optional": true, 168 + "os": [ 169 + "linux" 170 + ], 171 + "engines": { 172 + "node": ">=18" 173 + } 174 + }, 175 + "node_modules/@esbuild/linux-arm64": { 176 + "version": "0.27.2", 177 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", 178 + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", 179 + "cpu": [ 180 + "arm64" 181 + ], 182 + "dev": true, 183 + "license": "MIT", 184 + "optional": true, 185 + "os": [ 186 + "linux" 187 + ], 188 + "engines": { 189 + "node": ">=18" 190 + } 191 + }, 192 + "node_modules/@esbuild/linux-ia32": { 193 + "version": "0.27.2", 194 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", 195 + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", 196 + "cpu": [ 197 + "ia32" 198 + ], 199 + "dev": true, 200 + "license": "MIT", 201 + "optional": true, 202 + "os": [ 203 + "linux" 204 + ], 205 + "engines": { 206 + "node": ">=18" 207 + } 208 + }, 209 + "node_modules/@esbuild/linux-loong64": { 210 + "version": "0.27.2", 211 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", 212 + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", 213 + "cpu": [ 214 + "loong64" 215 + ], 216 + "dev": true, 217 + "license": "MIT", 218 + "optional": true, 219 + "os": [ 220 + "linux" 221 + ], 222 + "engines": { 223 + "node": ">=18" 224 + } 225 + }, 226 + "node_modules/@esbuild/linux-mips64el": { 227 + "version": "0.27.2", 228 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", 229 + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", 230 + "cpu": [ 231 + "mips64el" 232 + ], 233 + "dev": true, 234 + "license": "MIT", 235 + "optional": true, 236 + "os": [ 237 + "linux" 238 + ], 239 + "engines": { 240 + "node": ">=18" 241 + } 242 + }, 243 + "node_modules/@esbuild/linux-ppc64": { 244 + "version": "0.27.2", 245 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", 246 + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", 247 + "cpu": [ 248 + "ppc64" 249 + ], 250 + "dev": true, 251 + "license": "MIT", 252 + "optional": true, 253 + "os": [ 254 + "linux" 255 + ], 256 + "engines": { 257 + "node": ">=18" 258 + } 259 + }, 260 + "node_modules/@esbuild/linux-riscv64": { 261 + "version": "0.27.2", 262 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", 263 + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", 264 + "cpu": [ 265 + "riscv64" 266 + ], 267 + "dev": true, 268 + "license": "MIT", 269 + "optional": true, 270 + "os": [ 271 + "linux" 272 + ], 273 + "engines": { 274 + "node": ">=18" 275 + } 276 + }, 277 + "node_modules/@esbuild/linux-s390x": { 278 + "version": "0.27.2", 279 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", 280 + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", 281 + "cpu": [ 282 + "s390x" 283 + ], 284 + "dev": true, 285 + "license": "MIT", 286 + "optional": true, 287 + "os": [ 288 + "linux" 289 + ], 290 + "engines": { 291 + "node": ">=18" 292 + } 293 + }, 294 + "node_modules/@esbuild/linux-x64": { 295 + "version": "0.27.2", 296 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", 297 + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", 298 + "cpu": [ 299 + "x64" 300 + ], 301 + "dev": true, 302 + "license": "MIT", 303 + "optional": true, 304 + "os": [ 305 + "linux" 306 + ], 307 + "engines": { 308 + "node": ">=18" 309 + } 310 + }, 311 + "node_modules/@esbuild/netbsd-arm64": { 312 + "version": "0.27.2", 313 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", 314 + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", 315 + "cpu": [ 316 + "arm64" 317 + ], 318 + "dev": true, 319 + "license": "MIT", 320 + "optional": true, 321 + "os": [ 322 + "netbsd" 323 + ], 324 + "engines": { 325 + "node": ">=18" 326 + } 327 + }, 328 + "node_modules/@esbuild/netbsd-x64": { 329 + "version": "0.27.2", 330 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", 331 + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", 332 + "cpu": [ 333 + "x64" 334 + ], 335 + "dev": true, 336 + "license": "MIT", 337 + "optional": true, 338 + "os": [ 339 + "netbsd" 340 + ], 341 + "engines": { 342 + "node": ">=18" 343 + } 344 + }, 345 + "node_modules/@esbuild/openbsd-arm64": { 346 + "version": "0.27.2", 347 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", 348 + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", 349 + "cpu": [ 350 + "arm64" 351 + ], 352 + "dev": true, 353 + "license": "MIT", 354 + "optional": true, 355 + "os": [ 356 + "openbsd" 357 + ], 358 + "engines": { 359 + "node": ">=18" 360 + } 361 + }, 362 + "node_modules/@esbuild/openbsd-x64": { 363 + "version": "0.27.2", 364 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", 365 + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", 366 + "cpu": [ 367 + "x64" 368 + ], 369 + "dev": true, 370 + "license": "MIT", 371 + "optional": true, 372 + "os": [ 373 + "openbsd" 374 + ], 375 + "engines": { 376 + "node": ">=18" 377 + } 378 + }, 379 + "node_modules/@esbuild/openharmony-arm64": { 380 + "version": "0.27.2", 381 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", 382 + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", 383 + "cpu": [ 384 + "arm64" 385 + ], 386 + "dev": true, 387 + "license": "MIT", 388 + "optional": true, 389 + "os": [ 390 + "openharmony" 391 + ], 392 + "engines": { 393 + "node": ">=18" 394 + } 395 + }, 396 + "node_modules/@esbuild/sunos-x64": { 397 + "version": "0.27.2", 398 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", 399 + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", 400 + "cpu": [ 401 + "x64" 402 + ], 403 + "dev": true, 404 + "license": "MIT", 405 + "optional": true, 406 + "os": [ 407 + "sunos" 408 + ], 409 + "engines": { 410 + "node": ">=18" 411 + } 412 + }, 413 + "node_modules/@esbuild/win32-arm64": { 414 + "version": "0.27.2", 415 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", 416 + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", 417 + "cpu": [ 418 + "arm64" 419 + ], 420 + "dev": true, 421 + "license": "MIT", 422 + "optional": true, 423 + "os": [ 424 + "win32" 425 + ], 426 + "engines": { 427 + "node": ">=18" 428 + } 429 + }, 430 + "node_modules/@esbuild/win32-ia32": { 431 + "version": "0.27.2", 432 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", 433 + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", 434 + "cpu": [ 435 + "ia32" 436 + ], 437 + "dev": true, 438 + "license": "MIT", 439 + "optional": true, 440 + "os": [ 441 + "win32" 442 + ], 443 + "engines": { 444 + "node": ">=18" 445 + } 446 + }, 447 + "node_modules/@esbuild/win32-x64": { 448 + "version": "0.27.2", 449 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", 450 + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", 451 + "cpu": [ 452 + "x64" 453 + ], 454 + "dev": true, 455 + "license": "MIT", 456 + "optional": true, 457 + "os": [ 458 + "win32" 459 + ], 460 + "engines": { 461 + "node": ">=18" 462 + } 463 + }, 464 + "node_modules/@jridgewell/gen-mapping": { 465 + "version": "0.3.13", 466 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", 467 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", 468 + "dev": true, 469 + "license": "MIT", 470 + "dependencies": { 471 + "@jridgewell/sourcemap-codec": "^1.5.0", 472 + "@jridgewell/trace-mapping": "^0.3.24" 473 + } 474 + }, 475 + "node_modules/@jridgewell/remapping": { 476 + "version": "2.3.5", 477 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", 478 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", 479 + "dev": true, 480 + "license": "MIT", 481 + "dependencies": { 482 + "@jridgewell/gen-mapping": "^0.3.5", 483 + "@jridgewell/trace-mapping": "^0.3.24" 484 + } 485 + }, 486 + "node_modules/@jridgewell/resolve-uri": { 487 + "version": "3.1.2", 488 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 489 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 490 + "dev": true, 491 + "license": "MIT", 492 + "engines": { 493 + "node": ">=6.0.0" 494 + } 495 + }, 496 + "node_modules/@jridgewell/sourcemap-codec": { 497 + "version": "1.5.5", 498 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 499 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 500 + "dev": true, 501 + "license": "MIT" 502 + }, 503 + "node_modules/@jridgewell/trace-mapping": { 504 + "version": "0.3.31", 505 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", 506 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", 507 + "dev": true, 508 + "license": "MIT", 509 + "dependencies": { 510 + "@jridgewell/resolve-uri": "^3.1.0", 511 + "@jridgewell/sourcemap-codec": "^1.4.14" 512 + } 513 + }, 514 + "node_modules/@parcel/watcher": { 515 + "version": "2.5.4", 516 + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.4.tgz", 517 + "integrity": "sha512-WYa2tUVV5HiArWPB3ydlOc4R2ivq0IDrlqhMi3l7mVsFEXNcTfxYFPIHXHXIh/ca/y/V5N4E1zecyxdIBjYnkQ==", 518 + "dev": true, 519 + "hasInstallScript": true, 520 + "license": "MIT", 521 + "dependencies": { 522 + "detect-libc": "^2.0.3", 523 + "is-glob": "^4.0.3", 524 + "node-addon-api": "^7.0.0", 525 + "picomatch": "^4.0.3" 526 + }, 527 + "engines": { 528 + "node": ">= 10.0.0" 529 + }, 530 + "funding": { 531 + "type": "opencollective", 532 + "url": "https://opencollective.com/parcel" 533 + }, 534 + "optionalDependencies": { 535 + "@parcel/watcher-android-arm64": "2.5.4", 536 + "@parcel/watcher-darwin-arm64": "2.5.4", 537 + "@parcel/watcher-darwin-x64": "2.5.4", 538 + "@parcel/watcher-freebsd-x64": "2.5.4", 539 + "@parcel/watcher-linux-arm-glibc": "2.5.4", 540 + "@parcel/watcher-linux-arm-musl": "2.5.4", 541 + "@parcel/watcher-linux-arm64-glibc": "2.5.4", 542 + "@parcel/watcher-linux-arm64-musl": "2.5.4", 543 + "@parcel/watcher-linux-x64-glibc": "2.5.4", 544 + "@parcel/watcher-linux-x64-musl": "2.5.4", 545 + "@parcel/watcher-win32-arm64": "2.5.4", 546 + "@parcel/watcher-win32-ia32": "2.5.4", 547 + "@parcel/watcher-win32-x64": "2.5.4" 548 + } 549 + }, 550 + "node_modules/@parcel/watcher-android-arm64": { 551 + "version": "2.5.4", 552 + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.4.tgz", 553 + "integrity": "sha512-hoh0vx4v+b3BNI7Cjoy2/B0ARqcwVNrzN/n7DLq9ZB4I3lrsvhrkCViJyfTj/Qi5xM9YFiH4AmHGK6pgH1ss7g==", 554 + "cpu": [ 555 + "arm64" 556 + ], 557 + "dev": true, 558 + "license": "MIT", 559 + "optional": true, 560 + "os": [ 561 + "android" 562 + ], 563 + "engines": { 564 + "node": ">= 10.0.0" 565 + }, 566 + "funding": { 567 + "type": "opencollective", 568 + "url": "https://opencollective.com/parcel" 569 + } 570 + }, 571 + "node_modules/@parcel/watcher-darwin-arm64": { 572 + "version": "2.5.4", 573 + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.4.tgz", 574 + "integrity": "sha512-kphKy377pZiWpAOyTgQYPE5/XEKVMaj6VUjKT5VkNyUJlr2qZAn8gIc7CPzx+kbhvqHDT9d7EqdOqRXT6vk0zw==", 575 + "cpu": [ 576 + "arm64" 577 + ], 578 + "dev": true, 579 + "license": "MIT", 580 + "optional": true, 581 + "os": [ 582 + "darwin" 583 + ], 584 + "engines": { 585 + "node": ">= 10.0.0" 586 + }, 587 + "funding": { 588 + "type": "opencollective", 589 + "url": "https://opencollective.com/parcel" 590 + } 591 + }, 592 + "node_modules/@parcel/watcher-darwin-x64": { 593 + "version": "2.5.4", 594 + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.4.tgz", 595 + "integrity": "sha512-UKaQFhCtNJW1A9YyVz3Ju7ydf6QgrpNQfRZ35wNKUhTQ3dxJ/3MULXN5JN/0Z80V/KUBDGa3RZaKq1EQT2a2gg==", 596 + "cpu": [ 597 + "x64" 598 + ], 599 + "dev": true, 600 + "license": "MIT", 601 + "optional": true, 602 + "os": [ 603 + "darwin" 604 + ], 605 + "engines": { 606 + "node": ">= 10.0.0" 607 + }, 608 + "funding": { 609 + "type": "opencollective", 610 + "url": "https://opencollective.com/parcel" 611 + } 612 + }, 613 + "node_modules/@parcel/watcher-freebsd-x64": { 614 + "version": "2.5.4", 615 + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.4.tgz", 616 + "integrity": "sha512-Dib0Wv3Ow/m2/ttvLdeI2DBXloO7t3Z0oCp4bAb2aqyqOjKPPGrg10pMJJAQ7tt8P4V2rwYwywkDhUia/FgS+Q==", 617 + "cpu": [ 618 + "x64" 619 + ], 620 + "dev": true, 621 + "license": "MIT", 622 + "optional": true, 623 + "os": [ 624 + "freebsd" 625 + ], 626 + "engines": { 627 + "node": ">= 10.0.0" 628 + }, 629 + "funding": { 630 + "type": "opencollective", 631 + "url": "https://opencollective.com/parcel" 632 + } 633 + }, 634 + "node_modules/@parcel/watcher-linux-arm-glibc": { 635 + "version": "2.5.4", 636 + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.4.tgz", 637 + "integrity": "sha512-I5Vb769pdf7Q7Sf4KNy8Pogl/URRCKu9ImMmnVKYayhynuyGYMzuI4UOWnegQNa2sGpsPSbzDsqbHNMyeyPCgw==", 638 + "cpu": [ 639 + "arm" 640 + ], 641 + "dev": true, 642 + "license": "MIT", 643 + "optional": true, 644 + "os": [ 645 + "linux" 646 + ], 647 + "engines": { 648 + "node": ">= 10.0.0" 649 + }, 650 + "funding": { 651 + "type": "opencollective", 652 + "url": "https://opencollective.com/parcel" 653 + } 654 + }, 655 + "node_modules/@parcel/watcher-linux-arm-musl": { 656 + "version": "2.5.4", 657 + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.4.tgz", 658 + "integrity": "sha512-kGO8RPvVrcAotV4QcWh8kZuHr9bXi9a3bSZw7kFarYR0+fGliU7hd/zevhjw8fnvIKG3J9EO5G6sXNGCSNMYPQ==", 659 + "cpu": [ 660 + "arm" 661 + ], 662 + "dev": true, 663 + "license": "MIT", 664 + "optional": true, 665 + "os": [ 666 + "linux" 667 + ], 668 + "engines": { 669 + "node": ">= 10.0.0" 670 + }, 671 + "funding": { 672 + "type": "opencollective", 673 + "url": "https://opencollective.com/parcel" 674 + } 675 + }, 676 + "node_modules/@parcel/watcher-linux-arm64-glibc": { 677 + "version": "2.5.4", 678 + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.4.tgz", 679 + "integrity": "sha512-KU75aooXhqGFY2W5/p8DYYHt4hrjHZod8AhcGAmhzPn/etTa+lYCDB2b1sJy3sWJ8ahFVTdy+EbqSBvMx3iFlw==", 680 + "cpu": [ 681 + "arm64" 682 + ], 683 + "dev": true, 684 + "license": "MIT", 685 + "optional": true, 686 + "os": [ 687 + "linux" 688 + ], 689 + "engines": { 690 + "node": ">= 10.0.0" 691 + }, 692 + "funding": { 693 + "type": "opencollective", 694 + "url": "https://opencollective.com/parcel" 695 + } 696 + }, 697 + "node_modules/@parcel/watcher-linux-arm64-musl": { 698 + "version": "2.5.4", 699 + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.4.tgz", 700 + "integrity": "sha512-Qx8uNiIekVutnzbVdrgSanM+cbpDD3boB1f8vMtnuG5Zau4/bdDbXyKwIn0ToqFhIuob73bcxV9NwRm04/hzHQ==", 701 + "cpu": [ 702 + "arm64" 703 + ], 704 + "dev": true, 705 + "license": "MIT", 706 + "optional": true, 707 + "os": [ 708 + "linux" 709 + ], 710 + "engines": { 711 + "node": ">= 10.0.0" 712 + }, 713 + "funding": { 714 + "type": "opencollective", 715 + "url": "https://opencollective.com/parcel" 716 + } 717 + }, 718 + "node_modules/@parcel/watcher-linux-x64-glibc": { 719 + "version": "2.5.4", 720 + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.4.tgz", 721 + "integrity": "sha512-UYBQvhYmgAv61LNUn24qGQdjtycFBKSK3EXr72DbJqX9aaLbtCOO8+1SkKhD/GNiJ97ExgcHBrukcYhVjrnogA==", 722 + "cpu": [ 723 + "x64" 724 + ], 725 + "dev": true, 726 + "license": "MIT", 727 + "optional": true, 728 + "os": [ 729 + "linux" 730 + ], 731 + "engines": { 732 + "node": ">= 10.0.0" 733 + }, 734 + "funding": { 735 + "type": "opencollective", 736 + "url": "https://opencollective.com/parcel" 737 + } 738 + }, 739 + "node_modules/@parcel/watcher-linux-x64-musl": { 740 + "version": "2.5.4", 741 + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz", 742 + "integrity": "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==", 743 + "cpu": [ 744 + "x64" 745 + ], 746 + "dev": true, 747 + "license": "MIT", 748 + "optional": true, 749 + "os": [ 750 + "linux" 751 + ], 752 + "engines": { 753 + "node": ">= 10.0.0" 754 + }, 755 + "funding": { 756 + "type": "opencollective", 757 + "url": "https://opencollective.com/parcel" 758 + } 759 + }, 760 + "node_modules/@parcel/watcher-win32-arm64": { 761 + "version": "2.5.4", 762 + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.4.tgz", 763 + "integrity": "sha512-iby+D/YNXWkiQNYcIhg8P5hSjzXEHaQrk2SLrWOUD7VeC4Ohu0WQvmV+HDJokZVJ2UjJ4AGXW3bx7Lls9Ln4TQ==", 764 + "cpu": [ 765 + "arm64" 766 + ], 767 + "dev": true, 768 + "license": "MIT", 769 + "optional": true, 770 + "os": [ 771 + "win32" 772 + ], 773 + "engines": { 774 + "node": ">= 10.0.0" 775 + }, 776 + "funding": { 777 + "type": "opencollective", 778 + "url": "https://opencollective.com/parcel" 779 + } 780 + }, 781 + "node_modules/@parcel/watcher-win32-ia32": { 782 + "version": "2.5.4", 783 + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.4.tgz", 784 + "integrity": "sha512-vQN+KIReG0a2ZDpVv8cgddlf67J8hk1WfZMMP7sMeZmJRSmEax5xNDNWKdgqSe2brOKTQQAs3aCCUal2qBHAyg==", 785 + "cpu": [ 786 + "ia32" 787 + ], 788 + "dev": true, 789 + "license": "MIT", 790 + "optional": true, 791 + "os": [ 792 + "win32" 793 + ], 794 + "engines": { 795 + "node": ">= 10.0.0" 796 + }, 797 + "funding": { 798 + "type": "opencollective", 799 + "url": "https://opencollective.com/parcel" 800 + } 801 + }, 802 + "node_modules/@parcel/watcher-win32-x64": { 803 + "version": "2.5.4", 804 + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.4.tgz", 805 + "integrity": "sha512-3A6efb6BOKwyw7yk9ro2vus2YTt2nvcd56AuzxdMiVOxL9umDyN5PKkKfZ/gZ9row41SjVmTVQNWQhaRRGpOKw==", 806 + "cpu": [ 807 + "x64" 808 + ], 809 + "dev": true, 810 + "license": "MIT", 811 + "optional": true, 812 + "os": [ 813 + "win32" 814 + ], 815 + "engines": { 816 + "node": ">= 10.0.0" 817 + }, 818 + "funding": { 819 + "type": "opencollective", 820 + "url": "https://opencollective.com/parcel" 821 + } 822 + }, 823 + "node_modules/@tailwindcss/cli": { 824 + "version": "4.1.18", 825 + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz", 826 + "integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==", 827 + "dev": true, 828 + "license": "MIT", 829 + "dependencies": { 830 + "@parcel/watcher": "^2.5.1", 831 + "@tailwindcss/node": "4.1.18", 832 + "@tailwindcss/oxide": "4.1.18", 833 + "enhanced-resolve": "^5.18.3", 834 + "mri": "^1.2.0", 835 + "picocolors": "^1.1.1", 836 + "tailwindcss": "4.1.18" 837 + }, 838 + "bin": { 839 + "tailwindcss": "dist/index.mjs" 840 + } 841 + }, 842 + "node_modules/@tailwindcss/node": { 843 + "version": "4.1.18", 844 + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", 845 + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", 846 + "dev": true, 847 + "license": "MIT", 848 + "dependencies": { 849 + "@jridgewell/remapping": "^2.3.4", 850 + "enhanced-resolve": "^5.18.3", 851 + "jiti": "^2.6.1", 852 + "lightningcss": "1.30.2", 853 + "magic-string": "^0.30.21", 854 + "source-map-js": "^1.2.1", 855 + "tailwindcss": "4.1.18" 856 + } 857 + }, 858 + "node_modules/@tailwindcss/oxide": { 859 + "version": "4.1.18", 860 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", 861 + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", 862 + "dev": true, 863 + "license": "MIT", 864 + "engines": { 865 + "node": ">= 10" 866 + }, 867 + "optionalDependencies": { 868 + "@tailwindcss/oxide-android-arm64": "4.1.18", 869 + "@tailwindcss/oxide-darwin-arm64": "4.1.18", 870 + "@tailwindcss/oxide-darwin-x64": "4.1.18", 871 + "@tailwindcss/oxide-freebsd-x64": "4.1.18", 872 + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", 873 + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", 874 + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", 875 + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", 876 + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", 877 + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", 878 + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", 879 + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" 880 + } 881 + }, 882 + "node_modules/@tailwindcss/oxide-android-arm64": { 883 + "version": "4.1.18", 884 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", 885 + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", 886 + "cpu": [ 887 + "arm64" 888 + ], 889 + "dev": true, 890 + "license": "MIT", 891 + "optional": true, 892 + "os": [ 893 + "android" 894 + ], 895 + "engines": { 896 + "node": ">= 10" 897 + } 898 + }, 899 + "node_modules/@tailwindcss/oxide-darwin-arm64": { 900 + "version": "4.1.18", 901 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", 902 + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", 903 + "cpu": [ 904 + "arm64" 905 + ], 906 + "dev": true, 907 + "license": "MIT", 908 + "optional": true, 909 + "os": [ 910 + "darwin" 911 + ], 912 + "engines": { 913 + "node": ">= 10" 914 + } 915 + }, 916 + "node_modules/@tailwindcss/oxide-darwin-x64": { 917 + "version": "4.1.18", 918 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", 919 + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", 920 + "cpu": [ 921 + "x64" 922 + ], 923 + "dev": true, 924 + "license": "MIT", 925 + "optional": true, 926 + "os": [ 927 + "darwin" 928 + ], 929 + "engines": { 930 + "node": ">= 10" 931 + } 932 + }, 933 + "node_modules/@tailwindcss/oxide-freebsd-x64": { 934 + "version": "4.1.18", 935 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", 936 + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", 937 + "cpu": [ 938 + "x64" 939 + ], 940 + "dev": true, 941 + "license": "MIT", 942 + "optional": true, 943 + "os": [ 944 + "freebsd" 945 + ], 946 + "engines": { 947 + "node": ">= 10" 948 + } 949 + }, 950 + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { 951 + "version": "4.1.18", 952 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", 953 + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", 954 + "cpu": [ 955 + "arm" 956 + ], 957 + "dev": true, 958 + "license": "MIT", 959 + "optional": true, 960 + "os": [ 961 + "linux" 962 + ], 963 + "engines": { 964 + "node": ">= 10" 965 + } 966 + }, 967 + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { 968 + "version": "4.1.18", 969 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", 970 + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", 971 + "cpu": [ 972 + "arm64" 973 + ], 974 + "dev": true, 975 + "license": "MIT", 976 + "optional": true, 977 + "os": [ 978 + "linux" 979 + ], 980 + "engines": { 981 + "node": ">= 10" 982 + } 983 + }, 984 + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { 985 + "version": "4.1.18", 986 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", 987 + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", 988 + "cpu": [ 989 + "arm64" 990 + ], 991 + "dev": true, 992 + "license": "MIT", 993 + "optional": true, 994 + "os": [ 995 + "linux" 996 + ], 997 + "engines": { 998 + "node": ">= 10" 999 + } 1000 + }, 1001 + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { 1002 + "version": "4.1.18", 1003 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", 1004 + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", 1005 + "cpu": [ 1006 + "x64" 1007 + ], 1008 + "dev": true, 1009 + "license": "MIT", 1010 + "optional": true, 1011 + "os": [ 1012 + "linux" 1013 + ], 1014 + "engines": { 1015 + "node": ">= 10" 1016 + } 1017 + }, 1018 + "node_modules/@tailwindcss/oxide-linux-x64-musl": { 1019 + "version": "4.1.18", 1020 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", 1021 + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", 1022 + "cpu": [ 1023 + "x64" 1024 + ], 1025 + "dev": true, 1026 + "license": "MIT", 1027 + "optional": true, 1028 + "os": [ 1029 + "linux" 1030 + ], 1031 + "engines": { 1032 + "node": ">= 10" 1033 + } 1034 + }, 1035 + "node_modules/@tailwindcss/oxide-wasm32-wasi": { 1036 + "version": "4.1.18", 1037 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", 1038 + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", 1039 + "bundleDependencies": [ 1040 + "@napi-rs/wasm-runtime", 1041 + "@emnapi/core", 1042 + "@emnapi/runtime", 1043 + "@tybys/wasm-util", 1044 + "@emnapi/wasi-threads", 1045 + "tslib" 1046 + ], 1047 + "cpu": [ 1048 + "wasm32" 1049 + ], 1050 + "dev": true, 1051 + "license": "MIT", 1052 + "optional": true, 1053 + "dependencies": { 1054 + "@emnapi/core": "^1.7.1", 1055 + "@emnapi/runtime": "^1.7.1", 1056 + "@emnapi/wasi-threads": "^1.1.0", 1057 + "@napi-rs/wasm-runtime": "^1.1.0", 1058 + "@tybys/wasm-util": "^0.10.1", 1059 + "tslib": "^2.4.0" 1060 + }, 1061 + "engines": { 1062 + "node": ">=14.0.0" 1063 + } 1064 + }, 1065 + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { 1066 + "version": "4.1.18", 1067 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", 1068 + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", 1069 + "cpu": [ 1070 + "arm64" 1071 + ], 1072 + "dev": true, 1073 + "license": "MIT", 1074 + "optional": true, 1075 + "os": [ 1076 + "win32" 1077 + ], 1078 + "engines": { 1079 + "node": ">= 10" 1080 + } 1081 + }, 1082 + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { 1083 + "version": "4.1.18", 1084 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", 1085 + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", 1086 + "cpu": [ 1087 + "x64" 1088 + ], 1089 + "dev": true, 1090 + "license": "MIT", 1091 + "optional": true, 1092 + "os": [ 1093 + "win32" 1094 + ], 1095 + "engines": { 1096 + "node": ">= 10" 1097 + } 1098 + }, 1099 + "node_modules/actor-typeahead": { 1100 + "version": "0.1.2", 1101 + "resolved": "https://registry.npmjs.org/actor-typeahead/-/actor-typeahead-0.1.2.tgz", 1102 + "integrity": "sha512-I97YqqNl7Kar0J/bIJvgY/KmHpssHcDElhfwVTLP7wRFlkxso2ZLBqiS2zol5A8UVUJbQK2JXYaqNpZXz8Uk2A==", 1103 + "license": "MPL-2.0" 1104 + }, 1105 + "node_modules/daisyui": { 1106 + "version": "5.5.14", 1107 + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.14.tgz", 1108 + "integrity": "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg==", 1109 + "dev": true, 1110 + "license": "MIT", 1111 + "funding": { 1112 + "url": "https://github.com/saadeghi/daisyui?sponsor=1" 1113 + } 1114 + }, 1115 + "node_modules/detect-libc": { 1116 + "version": "2.1.2", 1117 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 1118 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 1119 + "dev": true, 1120 + "license": "Apache-2.0", 1121 + "engines": { 1122 + "node": ">=8" 1123 + } 1124 + }, 1125 + "node_modules/enhanced-resolve": { 1126 + "version": "5.18.4", 1127 + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", 1128 + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", 1129 + "dev": true, 1130 + "license": "MIT", 1131 + "dependencies": { 1132 + "graceful-fs": "^4.2.4", 1133 + "tapable": "^2.2.0" 1134 + }, 1135 + "engines": { 1136 + "node": ">=10.13.0" 1137 + } 1138 + }, 1139 + "node_modules/esbuild": { 1140 + "version": "0.27.2", 1141 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", 1142 + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", 1143 + "dev": true, 1144 + "hasInstallScript": true, 1145 + "license": "MIT", 1146 + "bin": { 1147 + "esbuild": "bin/esbuild" 1148 + }, 1149 + "engines": { 1150 + "node": ">=18" 1151 + }, 1152 + "optionalDependencies": { 1153 + "@esbuild/aix-ppc64": "0.27.2", 1154 + "@esbuild/android-arm": "0.27.2", 1155 + "@esbuild/android-arm64": "0.27.2", 1156 + "@esbuild/android-x64": "0.27.2", 1157 + "@esbuild/darwin-arm64": "0.27.2", 1158 + "@esbuild/darwin-x64": "0.27.2", 1159 + "@esbuild/freebsd-arm64": "0.27.2", 1160 + "@esbuild/freebsd-x64": "0.27.2", 1161 + "@esbuild/linux-arm": "0.27.2", 1162 + "@esbuild/linux-arm64": "0.27.2", 1163 + "@esbuild/linux-ia32": "0.27.2", 1164 + "@esbuild/linux-loong64": "0.27.2", 1165 + "@esbuild/linux-mips64el": "0.27.2", 1166 + "@esbuild/linux-ppc64": "0.27.2", 1167 + "@esbuild/linux-riscv64": "0.27.2", 1168 + "@esbuild/linux-s390x": "0.27.2", 1169 + "@esbuild/linux-x64": "0.27.2", 1170 + "@esbuild/netbsd-arm64": "0.27.2", 1171 + "@esbuild/netbsd-x64": "0.27.2", 1172 + "@esbuild/openbsd-arm64": "0.27.2", 1173 + "@esbuild/openbsd-x64": "0.27.2", 1174 + "@esbuild/openharmony-arm64": "0.27.2", 1175 + "@esbuild/sunos-x64": "0.27.2", 1176 + "@esbuild/win32-arm64": "0.27.2", 1177 + "@esbuild/win32-ia32": "0.27.2", 1178 + "@esbuild/win32-x64": "0.27.2" 1179 + } 1180 + }, 1181 + "node_modules/graceful-fs": { 1182 + "version": "4.2.11", 1183 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 1184 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 1185 + "dev": true, 1186 + "license": "ISC" 1187 + }, 1188 + "node_modules/htmx.org": { 1189 + "version": "2.0.8", 1190 + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz", 1191 + "integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==", 1192 + "license": "0BSD" 1193 + }, 1194 + "node_modules/is-extglob": { 1195 + "version": "2.1.1", 1196 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1197 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1198 + "dev": true, 1199 + "license": "MIT", 1200 + "engines": { 1201 + "node": ">=0.10.0" 1202 + } 1203 + }, 1204 + "node_modules/is-glob": { 1205 + "version": "4.0.3", 1206 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1207 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1208 + "dev": true, 1209 + "license": "MIT", 1210 + "dependencies": { 1211 + "is-extglob": "^2.1.1" 1212 + }, 1213 + "engines": { 1214 + "node": ">=0.10.0" 1215 + } 1216 + }, 1217 + "node_modules/jiti": { 1218 + "version": "2.6.1", 1219 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", 1220 + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", 1221 + "dev": true, 1222 + "license": "MIT", 1223 + "bin": { 1224 + "jiti": "lib/jiti-cli.mjs" 1225 + } 1226 + }, 1227 + "node_modules/lightningcss": { 1228 + "version": "1.30.2", 1229 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", 1230 + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", 1231 + "dev": true, 1232 + "license": "MPL-2.0", 1233 + "dependencies": { 1234 + "detect-libc": "^2.0.3" 1235 + }, 1236 + "engines": { 1237 + "node": ">= 12.0.0" 1238 + }, 1239 + "funding": { 1240 + "type": "opencollective", 1241 + "url": "https://opencollective.com/parcel" 1242 + }, 1243 + "optionalDependencies": { 1244 + "lightningcss-android-arm64": "1.30.2", 1245 + "lightningcss-darwin-arm64": "1.30.2", 1246 + "lightningcss-darwin-x64": "1.30.2", 1247 + "lightningcss-freebsd-x64": "1.30.2", 1248 + "lightningcss-linux-arm-gnueabihf": "1.30.2", 1249 + "lightningcss-linux-arm64-gnu": "1.30.2", 1250 + "lightningcss-linux-arm64-musl": "1.30.2", 1251 + "lightningcss-linux-x64-gnu": "1.30.2", 1252 + "lightningcss-linux-x64-musl": "1.30.2", 1253 + "lightningcss-win32-arm64-msvc": "1.30.2", 1254 + "lightningcss-win32-x64-msvc": "1.30.2" 1255 + } 1256 + }, 1257 + "node_modules/lightningcss-android-arm64": { 1258 + "version": "1.30.2", 1259 + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", 1260 + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", 1261 + "cpu": [ 1262 + "arm64" 1263 + ], 1264 + "dev": true, 1265 + "license": "MPL-2.0", 1266 + "optional": true, 1267 + "os": [ 1268 + "android" 1269 + ], 1270 + "engines": { 1271 + "node": ">= 12.0.0" 1272 + }, 1273 + "funding": { 1274 + "type": "opencollective", 1275 + "url": "https://opencollective.com/parcel" 1276 + } 1277 + }, 1278 + "node_modules/lightningcss-darwin-arm64": { 1279 + "version": "1.30.2", 1280 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", 1281 + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", 1282 + "cpu": [ 1283 + "arm64" 1284 + ], 1285 + "dev": true, 1286 + "license": "MPL-2.0", 1287 + "optional": true, 1288 + "os": [ 1289 + "darwin" 1290 + ], 1291 + "engines": { 1292 + "node": ">= 12.0.0" 1293 + }, 1294 + "funding": { 1295 + "type": "opencollective", 1296 + "url": "https://opencollective.com/parcel" 1297 + } 1298 + }, 1299 + "node_modules/lightningcss-darwin-x64": { 1300 + "version": "1.30.2", 1301 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", 1302 + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", 1303 + "cpu": [ 1304 + "x64" 1305 + ], 1306 + "dev": true, 1307 + "license": "MPL-2.0", 1308 + "optional": true, 1309 + "os": [ 1310 + "darwin" 1311 + ], 1312 + "engines": { 1313 + "node": ">= 12.0.0" 1314 + }, 1315 + "funding": { 1316 + "type": "opencollective", 1317 + "url": "https://opencollective.com/parcel" 1318 + } 1319 + }, 1320 + "node_modules/lightningcss-freebsd-x64": { 1321 + "version": "1.30.2", 1322 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", 1323 + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", 1324 + "cpu": [ 1325 + "x64" 1326 + ], 1327 + "dev": true, 1328 + "license": "MPL-2.0", 1329 + "optional": true, 1330 + "os": [ 1331 + "freebsd" 1332 + ], 1333 + "engines": { 1334 + "node": ">= 12.0.0" 1335 + }, 1336 + "funding": { 1337 + "type": "opencollective", 1338 + "url": "https://opencollective.com/parcel" 1339 + } 1340 + }, 1341 + "node_modules/lightningcss-linux-arm-gnueabihf": { 1342 + "version": "1.30.2", 1343 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", 1344 + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", 1345 + "cpu": [ 1346 + "arm" 1347 + ], 1348 + "dev": true, 1349 + "license": "MPL-2.0", 1350 + "optional": true, 1351 + "os": [ 1352 + "linux" 1353 + ], 1354 + "engines": { 1355 + "node": ">= 12.0.0" 1356 + }, 1357 + "funding": { 1358 + "type": "opencollective", 1359 + "url": "https://opencollective.com/parcel" 1360 + } 1361 + }, 1362 + "node_modules/lightningcss-linux-arm64-gnu": { 1363 + "version": "1.30.2", 1364 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", 1365 + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", 1366 + "cpu": [ 1367 + "arm64" 1368 + ], 1369 + "dev": true, 1370 + "license": "MPL-2.0", 1371 + "optional": true, 1372 + "os": [ 1373 + "linux" 1374 + ], 1375 + "engines": { 1376 + "node": ">= 12.0.0" 1377 + }, 1378 + "funding": { 1379 + "type": "opencollective", 1380 + "url": "https://opencollective.com/parcel" 1381 + } 1382 + }, 1383 + "node_modules/lightningcss-linux-arm64-musl": { 1384 + "version": "1.30.2", 1385 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", 1386 + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", 1387 + "cpu": [ 1388 + "arm64" 1389 + ], 1390 + "dev": true, 1391 + "license": "MPL-2.0", 1392 + "optional": true, 1393 + "os": [ 1394 + "linux" 1395 + ], 1396 + "engines": { 1397 + "node": ">= 12.0.0" 1398 + }, 1399 + "funding": { 1400 + "type": "opencollective", 1401 + "url": "https://opencollective.com/parcel" 1402 + } 1403 + }, 1404 + "node_modules/lightningcss-linux-x64-gnu": { 1405 + "version": "1.30.2", 1406 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", 1407 + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", 1408 + "cpu": [ 1409 + "x64" 1410 + ], 1411 + "dev": true, 1412 + "license": "MPL-2.0", 1413 + "optional": true, 1414 + "os": [ 1415 + "linux" 1416 + ], 1417 + "engines": { 1418 + "node": ">= 12.0.0" 1419 + }, 1420 + "funding": { 1421 + "type": "opencollective", 1422 + "url": "https://opencollective.com/parcel" 1423 + } 1424 + }, 1425 + "node_modules/lightningcss-linux-x64-musl": { 1426 + "version": "1.30.2", 1427 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", 1428 + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", 1429 + "cpu": [ 1430 + "x64" 1431 + ], 1432 + "dev": true, 1433 + "license": "MPL-2.0", 1434 + "optional": true, 1435 + "os": [ 1436 + "linux" 1437 + ], 1438 + "engines": { 1439 + "node": ">= 12.0.0" 1440 + }, 1441 + "funding": { 1442 + "type": "opencollective", 1443 + "url": "https://opencollective.com/parcel" 1444 + } 1445 + }, 1446 + "node_modules/lightningcss-win32-arm64-msvc": { 1447 + "version": "1.30.2", 1448 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", 1449 + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", 1450 + "cpu": [ 1451 + "arm64" 1452 + ], 1453 + "dev": true, 1454 + "license": "MPL-2.0", 1455 + "optional": true, 1456 + "os": [ 1457 + "win32" 1458 + ], 1459 + "engines": { 1460 + "node": ">= 12.0.0" 1461 + }, 1462 + "funding": { 1463 + "type": "opencollective", 1464 + "url": "https://opencollective.com/parcel" 1465 + } 1466 + }, 1467 + "node_modules/lightningcss-win32-x64-msvc": { 1468 + "version": "1.30.2", 1469 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", 1470 + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", 1471 + "cpu": [ 1472 + "x64" 1473 + ], 1474 + "dev": true, 1475 + "license": "MPL-2.0", 1476 + "optional": true, 1477 + "os": [ 1478 + "win32" 1479 + ], 1480 + "engines": { 1481 + "node": ">= 12.0.0" 1482 + }, 1483 + "funding": { 1484 + "type": "opencollective", 1485 + "url": "https://opencollective.com/parcel" 1486 + } 1487 + }, 1488 + "node_modules/lucide": { 1489 + "version": "0.562.0", 1490 + "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.562.0.tgz", 1491 + "integrity": "sha512-k1Fb8ZMnRQovWRlea7Jr0b9UKA29IM7/cu79+mJrhVohvA2YC/Ti3Sk+G+h/SIu3IlrKT4RAbWMHUBBQd1O6XA==", 1492 + "license": "ISC" 1493 + }, 1494 + "node_modules/magic-string": { 1495 + "version": "0.30.21", 1496 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 1497 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 1498 + "dev": true, 1499 + "license": "MIT", 1500 + "dependencies": { 1501 + "@jridgewell/sourcemap-codec": "^1.5.5" 1502 + } 1503 + }, 1504 + "node_modules/mri": { 1505 + "version": "1.2.0", 1506 + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", 1507 + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", 1508 + "dev": true, 1509 + "license": "MIT", 1510 + "engines": { 1511 + "node": ">=4" 1512 + } 1513 + }, 1514 + "node_modules/node-addon-api": { 1515 + "version": "7.1.1", 1516 + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", 1517 + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", 1518 + "dev": true, 1519 + "license": "MIT" 1520 + }, 1521 + "node_modules/picocolors": { 1522 + "version": "1.1.1", 1523 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1524 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1525 + "dev": true, 1526 + "license": "ISC" 1527 + }, 1528 + "node_modules/picomatch": { 1529 + "version": "4.0.3", 1530 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 1531 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 1532 + "dev": true, 1533 + "license": "MIT", 1534 + "engines": { 1535 + "node": ">=12" 1536 + }, 1537 + "funding": { 1538 + "url": "https://github.com/sponsors/jonschlinkert" 1539 + } 1540 + }, 1541 + "node_modules/source-map-js": { 1542 + "version": "1.2.1", 1543 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1544 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1545 + "dev": true, 1546 + "license": "BSD-3-Clause", 1547 + "engines": { 1548 + "node": ">=0.10.0" 1549 + } 1550 + }, 1551 + "node_modules/tailwindcss": { 1552 + "version": "4.1.18", 1553 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", 1554 + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", 1555 + "dev": true, 1556 + "license": "MIT" 1557 + }, 1558 + "node_modules/tapable": { 1559 + "version": "2.3.0", 1560 + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", 1561 + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", 1562 + "dev": true, 1563 + "license": "MIT", 1564 + "engines": { 1565 + "node": ">=6" 1566 + }, 1567 + "funding": { 1568 + "type": "opencollective", 1569 + "url": "https://opencollective.com/webpack" 1570 + } 1571 + } 1572 + } 1573 + }
+24
package.json
··· 1 + { 2 + "name": "atcr-styles", 3 + "version": "1.0.0", 4 + "private": true, 5 + "scripts": { 6 + "css:build": "BROWSERSLIST_IGNORE_OLD_DATA=1 npx tailwindcss -i ./pkg/appview/src/css/main.css -o ./pkg/appview/public/css/style.css --minify", 7 + "css:watch": "BROWSERSLIST_IGNORE_OLD_DATA=1 npx tailwindcss -i ./pkg/appview/src/css/main.css -o ./pkg/appview/public/css/style.css --watch", 8 + "js:build": "esbuild pkg/appview/src/js/main.js --bundle --minify --format=esm --outfile=pkg/appview/public/js/bundle.min.js", 9 + "js:watch": "esbuild pkg/appview/src/js/main.js --bundle --watch --format=esm --outfile=pkg/appview/public/js/bundle.min.js", 10 + "build": "npm run css:build && npm run js:build", 11 + "watch": "npm run css:watch & npm run js:watch" 12 + }, 13 + "devDependencies": { 14 + "@tailwindcss/cli": "^4.1.18", 15 + "daisyui": "^5.5.14", 16 + "esbuild": "^0.27.2", 17 + "tailwindcss": "^4.1" 18 + }, 19 + "dependencies": { 20 + "actor-typeahead": "^0.1.2", 21 + "htmx.org": "^2.0.8", 22 + "lucide": "^0.562.0" 23 + } 24 + }
+5 -2
pkg/appview/db/models.go
··· 137 137 IconURL string 138 138 StarCount int 139 139 PullCount int 140 - IsStarred bool // Whether the current user has starred this repository 141 - ArtifactType string // container-image, helm-chart, unknown 140 + IsStarred bool // Whether the current user has starred this repository 141 + ArtifactType string // container-image, helm-chart, unknown 142 + Tag string // Latest tag name (e.g., "latest", "v1.0.0") 143 + Digest string // Latest manifest digest (sha256:...) 144 + LastUpdated time.Time // When the repository was last pushed to 142 145 } 143 146 144 147 // PlatformInfo represents platform information (OS/Architecture)
+182
pkg/appview/db/queries.go
··· 1736 1736 return featured, nil 1737 1737 } 1738 1738 1739 + // RepoCardSortOrder specifies how repo cards should be sorted 1740 + type RepoCardSortOrder string 1741 + 1742 + const ( 1743 + // SortByScore sorts by combined stars and pulls (for Featured) 1744 + SortByScore RepoCardSortOrder = "score" 1745 + // SortByLastUpdate sorts by most recent push (for What's New) 1746 + SortByLastUpdate RepoCardSortOrder = "last_update" 1747 + ) 1748 + 1749 + // GetRepoCards fetches repository cards with full data including Tag, Digest, and LastUpdated 1750 + func GetRepoCards(db *sql.DB, limit int, currentUserDID string, sortOrder RepoCardSortOrder) ([]RepoCardData, error) { 1751 + // Build ORDER BY clause based on sort order 1752 + var orderBy string 1753 + switch sortOrder { 1754 + case SortByLastUpdate: 1755 + orderBy = "COALESCE(rs.last_push, m.created_at) DESC" 1756 + default: // SortByScore 1757 + orderBy = "repo_stats.score DESC, repo_stats.star_count DESC, repo_stats.pull_count DESC, m.created_at DESC" 1758 + } 1759 + 1760 + query := ` 1761 + WITH latest_manifests AS ( 1762 + SELECT did, repository, MAX(id) as latest_id 1763 + FROM manifests 1764 + GROUP BY did, repository 1765 + ), 1766 + repo_stats AS ( 1767 + SELECT 1768 + lm.did, 1769 + lm.repository, 1770 + COALESCE(rs.pull_count, 0) as pull_count, 1771 + COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = lm.did AND repository = lm.repository), 0) as star_count, 1772 + (COALESCE(rs.pull_count, 0) + COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = lm.did AND repository = lm.repository), 0) * 10) as score 1773 + FROM latest_manifests lm 1774 + LEFT JOIN repository_stats rs ON lm.did = rs.did AND lm.repository = rs.repository 1775 + ) 1776 + SELECT 1777 + m.did, 1778 + u.handle, 1779 + m.repository, 1780 + COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'org.opencontainers.image.title'), ''), 1781 + COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'org.opencontainers.image.description'), ''), 1782 + COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'io.atcr.icon'), ''), 1783 + repo_stats.star_count, 1784 + repo_stats.pull_count, 1785 + COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = m.did AND repository = m.repository), 0), 1786 + COALESCE(m.artifact_type, 'container-image'), 1787 + COALESCE((SELECT tag FROM tags WHERE did = m.did AND repository = m.repository ORDER BY created_at DESC LIMIT 1), ''), 1788 + COALESCE(m.digest, ''), 1789 + COALESCE(rs.last_push, m.created_at), 1790 + COALESCE(rp.avatar_cid, '') 1791 + FROM latest_manifests lm 1792 + JOIN manifests m ON lm.latest_id = m.id 1793 + JOIN users u ON m.did = u.did 1794 + JOIN repo_stats ON m.did = repo_stats.did AND m.repository = repo_stats.repository 1795 + LEFT JOIN repository_stats rs ON m.did = rs.did AND m.repository = rs.repository 1796 + LEFT JOIN repo_pages rp ON m.did = rp.did AND m.repository = rp.repository 1797 + ORDER BY ` + orderBy + ` 1798 + LIMIT ? 1799 + ` 1800 + 1801 + rows, err := db.Query(query, currentUserDID, limit) 1802 + if err != nil { 1803 + return nil, err 1804 + } 1805 + defer rows.Close() 1806 + 1807 + var cards []RepoCardData 1808 + for rows.Next() { 1809 + var c RepoCardData 1810 + var ownerDID string 1811 + var isStarredInt int 1812 + var avatarCID string 1813 + var lastUpdatedStr sql.NullString 1814 + 1815 + if err := rows.Scan(&ownerDID, &c.OwnerHandle, &c.Repository, &c.Title, &c.Description, &c.IconURL, 1816 + &c.StarCount, &c.PullCount, &isStarredInt, &c.ArtifactType, &c.Tag, &c.Digest, &lastUpdatedStr, &avatarCID); err != nil { 1817 + return nil, err 1818 + } 1819 + c.IsStarred = isStarredInt > 0 1820 + if lastUpdatedStr.Valid { 1821 + if t, err := parseTimestamp(lastUpdatedStr.String); err == nil { 1822 + c.LastUpdated = t 1823 + } 1824 + } 1825 + // Prefer repo page avatar over annotation icon 1826 + if avatarCID != "" { 1827 + c.IconURL = BlobCDNURL(ownerDID, avatarCID) 1828 + } 1829 + 1830 + cards = append(cards, c) 1831 + } 1832 + 1833 + if err := rows.Err(); err != nil { 1834 + return nil, err 1835 + } 1836 + 1837 + return cards, nil 1838 + } 1839 + 1840 + // GetUserRepoCards fetches repository cards for a specific user with full data 1841 + func GetUserRepoCards(db *sql.DB, userDID string, currentUserDID string) ([]RepoCardData, error) { 1842 + query := ` 1843 + WITH latest_manifests AS ( 1844 + SELECT did, repository, MAX(id) as latest_id 1845 + FROM manifests 1846 + WHERE did = ? 1847 + GROUP BY did, repository 1848 + ), 1849 + repo_stats AS ( 1850 + SELECT 1851 + lm.did, 1852 + lm.repository, 1853 + COALESCE(rs.pull_count, 0) as pull_count, 1854 + COALESCE((SELECT COUNT(*) FROM stars WHERE owner_did = lm.did AND repository = lm.repository), 0) as star_count 1855 + FROM latest_manifests lm 1856 + LEFT JOIN repository_stats rs ON lm.did = rs.did AND lm.repository = rs.repository 1857 + ) 1858 + SELECT 1859 + m.did, 1860 + u.handle, 1861 + m.repository, 1862 + COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'org.opencontainers.image.title'), ''), 1863 + COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'org.opencontainers.image.description'), ''), 1864 + COALESCE((SELECT value FROM repository_annotations WHERE did = m.did AND repository = m.repository AND key = 'io.atcr.icon'), ''), 1865 + repo_stats.star_count, 1866 + repo_stats.pull_count, 1867 + COALESCE((SELECT COUNT(*) FROM stars WHERE starrer_did = ? AND owner_did = m.did AND repository = m.repository), 0), 1868 + COALESCE(m.artifact_type, 'container-image'), 1869 + COALESCE((SELECT tag FROM tags WHERE did = m.did AND repository = m.repository ORDER BY created_at DESC LIMIT 1), ''), 1870 + COALESCE(m.digest, ''), 1871 + COALESCE(rs.last_push, m.created_at), 1872 + COALESCE(rp.avatar_cid, '') 1873 + FROM latest_manifests lm 1874 + JOIN manifests m ON lm.latest_id = m.id 1875 + JOIN users u ON m.did = u.did 1876 + JOIN repo_stats ON m.did = repo_stats.did AND m.repository = repo_stats.repository 1877 + LEFT JOIN repository_stats rs ON m.did = rs.did AND m.repository = rs.repository 1878 + LEFT JOIN repo_pages rp ON m.did = rp.did AND m.repository = rp.repository 1879 + ORDER BY COALESCE(rs.last_push, m.created_at) DESC 1880 + ` 1881 + 1882 + rows, err := db.Query(query, userDID, currentUserDID) 1883 + if err != nil { 1884 + return nil, err 1885 + } 1886 + defer rows.Close() 1887 + 1888 + var cards []RepoCardData 1889 + for rows.Next() { 1890 + var c RepoCardData 1891 + var ownerDID string 1892 + var isStarredInt int 1893 + var avatarCID string 1894 + var lastUpdatedStr sql.NullString 1895 + 1896 + if err := rows.Scan(&ownerDID, &c.OwnerHandle, &c.Repository, &c.Title, &c.Description, &c.IconURL, 1897 + &c.StarCount, &c.PullCount, &isStarredInt, &c.ArtifactType, &c.Tag, &c.Digest, &lastUpdatedStr, &avatarCID); err != nil { 1898 + return nil, err 1899 + } 1900 + c.IsStarred = isStarredInt > 0 1901 + if lastUpdatedStr.Valid { 1902 + if t, err := parseTimestamp(lastUpdatedStr.String); err == nil { 1903 + c.LastUpdated = t 1904 + } 1905 + } 1906 + // Prefer repo page avatar over annotation icon 1907 + if avatarCID != "" { 1908 + c.IconURL = BlobCDNURL(ownerDID, avatarCID) 1909 + } 1910 + 1911 + cards = append(cards, c) 1912 + } 1913 + 1914 + if err := rows.Err(); err != nil { 1915 + return nil, err 1916 + } 1917 + 1918 + return cards, nil 1919 + } 1920 + 1739 1921 // RepoPage represents a repository page record cached from PDS 1740 1922 type RepoPage struct { 1741 1923 DID string
+57 -2
pkg/appview/handlers/api.go
··· 1 1 package handlers 2 2 3 3 import ( 4 + "bytes" 4 5 "database/sql" 5 6 "errors" 6 7 "fmt" 8 + "html/template" 7 9 "log/slog" 8 10 "net/http" 9 11 ··· 21 23 DB *sql.DB 22 24 Directory identity.Directory 23 25 Refresher *oauth.Refresher 26 + Templates *template.Template 24 27 } 25 28 26 29 func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 64 67 return 65 68 } 66 69 67 - // Return success 70 + // Check if HTMX request - return HTML component 71 + if r.Header.Get("HX-Request") == "true" && h.Templates != nil { 72 + // Get current star count and do optimistic increment 73 + stats, _ := db.GetRepositoryStats(h.DB, ownerDID, repository) 74 + starCount := 0 75 + if stats != nil { 76 + starCount = stats.StarCount 77 + } 78 + starCount++ // Optimistic increment 79 + 80 + renderStarComponent(w, h.Templates, handle, repository, true, starCount) 81 + return 82 + } 83 + 84 + // Return JSON for API clients 68 85 w.WriteHeader(http.StatusCreated) 69 86 render.JSON(w, r, map[string]bool{"starred": true}) 70 87 } ··· 74 91 DB *sql.DB 75 92 Directory identity.Directory 76 93 Refresher *oauth.Refresher 94 + Templates *template.Template 77 95 } 78 96 79 97 func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ··· 119 137 slog.Debug("Star record not found, already unstarred") 120 138 } 121 139 122 - // Return success 140 + // Check if HTMX request - return HTML component 141 + if r.Header.Get("HX-Request") == "true" && h.Templates != nil { 142 + // Get current star count and do optimistic decrement 143 + stats, _ := db.GetRepositoryStats(h.DB, ownerDID, repository) 144 + starCount := 0 145 + if stats != nil { 146 + starCount = stats.StarCount 147 + } 148 + if starCount > 0 { 149 + starCount-- // Optimistic decrement 150 + } 151 + 152 + renderStarComponent(w, h.Templates, handle, repository, false, starCount) 153 + return 154 + } 155 + 156 + // Return JSON for API clients 123 157 render.JSON(w, r, map[string]bool{"starred": false}) 124 158 } 125 159 ··· 264 298 w.Header().Set("Cache-Control", "public, max-age=300") // Cache for 5 minutes 265 299 render.JSON(w, r, response) 266 300 } 301 + 302 + // renderStarComponent renders the star component HTML for HTMX responses 303 + func renderStarComponent(w http.ResponseWriter, tmpl *template.Template, handle, repository string, isStarred bool, starCount int) { 304 + data := map[string]any{ 305 + "Interactive": true, 306 + "Handle": handle, 307 + "Repository": repository, 308 + "IsStarred": isStarred, 309 + "StarCount": starCount, 310 + } 311 + 312 + var buf bytes.Buffer 313 + if err := tmpl.ExecuteTemplate(&buf, "star", data); err != nil { 314 + slog.Error("Failed to render star component", "error", err) 315 + http.Error(w, "Failed to render component", http.StatusInternalServerError) 316 + return 317 + } 318 + 319 + w.Header().Set("Content-Type", "text/html; charset=utf-8") 320 + _, _ = w.Write(buf.Bytes()) 321 + }
+28 -19
pkg/appview/handlers/home.go
··· 6 6 import ( 7 7 "database/sql" 8 8 "html/template" 9 + "log" 9 10 "net/http" 10 11 "strconv" 11 12 ··· 14 15 "atcr.io/pkg/appview/middleware" 15 16 ) 16 17 18 + // BenefitCard represents a feature benefit card on the home page 19 + type BenefitCard struct { 20 + Icon string 21 + Title string 22 + Description string 23 + } 24 + 17 25 // HomeHandler handles the home page 18 26 type HomeHandler struct { 19 27 DB *sql.DB ··· 28 36 currentUserDID = user.DID 29 37 } 30 38 31 - // Fetch featured repositories (top 6) 32 - featured, err := db.GetFeaturedRepositories(h.DB, 6, currentUserDID) 39 + // Fetch featured repositories (top 6 by score - carousel cycles through them) 40 + featuredCards, err := db.GetRepoCards(h.DB, 6, currentUserDID, db.SortByScore) 33 41 if err != nil { 34 - // Log error but continue - featured section will be empty 35 - featured = []db.FeaturedRepository{} 42 + log.Printf("Error fetching featured repos: %v", err) 43 + featuredCards = []db.RepoCardData{} 36 44 } 37 45 38 - // Convert to RepoCardData for template 39 - cards := make([]db.RepoCardData, len(featured)) 40 - for i, repo := range featured { 41 - cards[i] = db.RepoCardData{ 42 - OwnerHandle: repo.OwnerHandle, 43 - Repository: repo.Repository, 44 - Title: repo.Title, 45 - Description: repo.Description, 46 - IconURL: repo.IconURL, 47 - StarCount: repo.StarCount, 48 - PullCount: repo.PullCount, 49 - IsStarred: repo.IsStarred, 50 - ArtifactType: repo.ArtifactType, 51 - } 46 + // Fetch recently updated repositories (top 18 by last push - 6 rows) 47 + recentCards, err := db.GetRepoCards(h.DB, 18, currentUserDID, db.SortByLastUpdate) 48 + if err != nil { 49 + log.Printf("Error fetching recent repos: %v", err) 50 + recentCards = []db.RepoCardData{} 51 + } 52 + 53 + benefits := []BenefitCard{ 54 + {Icon: "ship", Title: "Works with Docker", Description: "Use docker push & pull. No new tools to learn."}, 55 + {Icon: "anchor", Title: "Your Data", Description: "Join shared holds or captain your own storage."}, 56 + {Icon: "compass", Title: "Discover Images", Description: "Browse and star public container registries."}, 52 57 } 53 58 54 59 data := struct { 55 60 PageData 56 61 FeaturedRepos []db.RepoCardData 62 + RecentRepos []db.RepoCardData 63 + Benefits []BenefitCard 57 64 }{ 58 65 PageData: NewPageData(r, h.RegistryURL), 59 - FeaturedRepos: cards, 66 + FeaturedRepos: featuredCards, 67 + RecentRepos: recentCards, 68 + Benefits: benefits, 60 69 } 61 70 62 71 if err := h.Templates.ExecuteTemplate(w, "home", data); err != nil {
+2
pkg/appview/handlers/repository.go
··· 245 245 Tags []db.TagWithPlatforms // Tags with platform info 246 246 Manifests []db.ManifestWithMetadata // Top-level manifests only 247 247 StarCount int 248 + PullCount int 248 249 IsStarred bool 249 250 IsOwner bool // Whether current user owns this repository 250 251 ReadmeHTML template.HTML ··· 256 257 Tags: tagsWithPlatforms, 257 258 Manifests: manifests, 258 259 StarCount: stats.StarCount, 260 + PullCount: stats.PullCount, 259 261 IsStarred: isStarred, 260 262 IsOwner: isOwner, 261 263 ReadmeHTML: readmeHTML,
+11 -25
pkg/appview/handlers/user.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "html/template" 6 + "log" 6 7 "net/http" 7 8 8 9 "atcr.io/pkg/appview/db" 10 + "atcr.io/pkg/appview/middleware" 9 11 "atcr.io/pkg/atproto" 10 12 "github.com/go-chi/chi/v5" 11 13 ) ··· 50 52 viewedUser.Handle = resolvedHandle 51 53 } 52 54 53 - // Fetch repositories for this user 54 - repos, err := db.GetUserRepositories(h.DB, viewedUser.DID) 55 - if err != nil { 56 - http.Error(w, err.Error(), http.StatusInternalServerError) 57 - return 55 + // Get current user DID for star state (empty string if not logged in) 56 + var currentUserDID string 57 + if user := middleware.GetUser(r); user != nil { 58 + currentUserDID = user.DID 58 59 } 59 60 60 - // Convert to RepoCardData for template 61 - cards := make([]db.RepoCardData, 0, len(repos)) 62 - for _, repo := range repos { 63 - stats, err := db.GetRepositoryStats(h.DB, viewedUser.DID, repo.Name) 64 - if err != nil { 65 - // Continue with zero stats on error 66 - stats = &db.RepositoryStats{ 67 - DID: viewedUser.DID, 68 - Repository: repo.Name, 69 - } 70 - } 71 - cards = append(cards, db.RepoCardData{ 72 - OwnerHandle: viewedUser.Handle, 73 - Repository: repo.Name, 74 - Title: repo.Title, 75 - Description: repo.Description, 76 - IconURL: repo.IconURL, 77 - StarCount: stats.StarCount, 78 - PullCount: stats.PullCount, 79 - }) 61 + // Fetch repository cards for this user 62 + cards, err := db.GetUserRepoCards(h.DB, viewedUser.DID, currentUserDID) 63 + if err != nil { 64 + log.Printf("Error fetching repo cards for user %s: %v", viewedUser.DID, err) 65 + cards = []db.RepoCardData{} 80 66 } 81 67 82 68 data := struct {
+2
pkg/appview/public/css/style.css
··· 1 + /*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */ 2 + @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-amber-400:oklch(82.8% .189 84.429);--color-gray-500:oklch(55.1% .027 264.364);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-xs:.125rem;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--shadow-sm:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--shadow-md:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--shadow-lg:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;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");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}@media (prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(58% .233 277.117);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:light;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(45% .24 277.023);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(77% .152 181.912);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root{--fx-noise:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E");scrollbar-color:currentColor #0000}@supports (color:color-mix(in lab, red, red)){:root{scrollbar-color:color-mix(in oklch,currentColor 35%,#0000)#0000}}@property --radialprogress{syntax: "<percentage>"; inherits: true; initial-value: 0%;}:root:not(span){overflow:var(--page-overflow)}:root{background:var(--page-scroll-bg,var(--root-bg));--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000),var(--root-bg,#0000))var(--root-bg,#0000)}@supports (color:color-mix(in lab, red, red)){:root{--page-scroll-bg-on:linear-gradient(var(--root-bg,#0000),var(--root-bg,#0000))color-mix(in srgb,var(--root-bg,#0000),oklch(0% 0 0) calc(var(--page-has-backdrop,0)*40%))}}:root{--page-scroll-transition-on:background-color .3s ease-out;transition:var(--page-scroll-transition);scrollbar-gutter:var(--page-scroll-gutter,unset);scrollbar-gutter:if(style(--page-has-scroll: 1): var(--page-scroll-gutter,unset); else: unset)}@keyframes set-page-has-scroll{0%,to{--page-has-scroll:1}}:root,[data-theme]{background:var(--page-scroll-bg,var(--root-bg));color:var(--color-base-content)}:where(:root,[data-theme]){--root-bg:var(--color-base-100)}:where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light]{color-scheme:normal;--color-base-100:oklch(100% 0 0);--color-base-200:oklch(98% 0 0);--color-base-300:oklch(95% 0 0);--color-base-content:oklch(21% .006 285.885);--color-primary:oklch(75% .12 175);--color-primary-content:oklch(93% .034 272.788);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(68% .18 25);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}:root:has(input.theme-controller[value=dark]:checked),[data-theme=dark]{color-scheme:normal;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);--color-base-300:oklch(21.15% .012 254.09);--color-base-content:oklch(97.807% .029 256.847);--color-primary:oklch(78% .12 175);--color-primary-content:oklch(96% .018 272.314);--color-secondary:oklch(65% .241 354.308);--color-secondary-content:oklch(94% .028 342.258);--color-accent:oklch(72% .16 25);--color-accent-content:oklch(38% .063 188.416);--color-neutral:oklch(14% .005 285.823);--color-neutral-content:oklch(92% .004 286.32);--color-info:oklch(74% .16 232.661);--color-info-content:oklch(29% .066 243.157);--color-success:oklch(76% .177 163.223);--color-success-content:oklch(37% .077 168.94);--color-warning:oklch(82% .189 84.429);--color-warning-content:oklch(41% .112 45.904);--color-error:oklch(71% .194 13.428);--color-error-content:oklch(27% .105 12.094);--radius-selector:.5rem;--radius-field:.25rem;--radius-box:.5rem;--size-selector:.25rem;--size-field:.25rem;--border:1px;--depth:1;--noise:0}}@layer components{.cmd{align-items:center;gap:calc(var(--spacing)*2);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-base-300);background-color:var(--color-base-200);width:100%;padding-inline:calc(var(--spacing)*3);padding-block:calc(var(--spacing)*2);display:flex;position:relative;overflow:hidden}.cmd code{text-overflow:ellipsis;white-space:nowrap;font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));overflow:hidden}.nav-search-wrapper{align-items:center;display:flex;position:relative}.nav-search-form{margin-right:calc(var(--spacing)*2);width:calc(var(--spacing)*0);opacity:0;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;transition-duration:.3s;position:absolute;right:100%;overflow:hidden}.nav-search-wrapper.expanded .nav-search-form{width:calc(var(--spacing)*62);opacity:1}.card-interactive{cursor:pointer;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.5s;transition-duration:.5s}.card-interactive:hover{box-shadow:var(--shadow-card-hover);transform:translateY(-2px)}actor-typeahead{--color-background:var(--color-base-100);--color-border:var(--color-base-300);--color-shadow:var(--color-base-content);--color-hover:var(--color-base-200);--color-avatar-fallback:var(--color-base-300);--radius:.5rem;--padding-menu:.25rem;z-index:50}actor-typeahead::part(handle){color:var(--color-base-content)}actor-typeahead::part(menu){--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);margin-top:.25rem}.recent-accounts-dropdown{top:100%;right:calc(var(--spacing)*0);left:calc(var(--spacing)*0);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-base-300);background-color:var(--color-base-100);border-radius:var(--radius-lg);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);z-index:50;max-height:calc(var(--spacing)*60);margin-top:.25rem;position:absolute;overflow-y:auto}.recent-accounts-header{padding-inline:calc(var(--spacing)*3);padding-block:calc(var(--spacing)*2);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);text-transform:uppercase;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-base-300);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.recent-accounts-header{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.recent-accounts-item{padding-inline:calc(var(--spacing)*3);padding-block:calc(var(--spacing)*2.5);cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;color:var(--color-base-content);transition-duration:.15s}.recent-accounts-item:hover,.recent-accounts-item.focused{background-color:var(--color-base-200)}}@layer utilities{@layer daisyui.l1.l2.l3{.diff{webkit-user-select:none;-webkit-user-select:none;user-select:none;direction:ltr;grid-template-rows:1fr 1.8rem 1fr;grid-template-columns:auto 1fr;width:100%;display:grid;position:relative;overflow:hidden;container-type:inline-size}.diff:focus-visible,.diff:has(.diff-item-1:focus-visible),.diff:focus-visible{outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px;outline-color:var(--color-base-content)}.diff:focus-visible .diff-resizer{min-width:95cqi;max-width:95cqi}.diff:has(.diff-item-1:focus-visible){outline-style:var(--tw-outline-style);outline-offset:1px;outline-width:2px}.diff:has(.diff-item-1:focus-visible) .diff-resizer{min-width:5cqi;max-width:5cqi}@supports (-webkit-overflow-scrolling:touch) and (overflow:-webkit-paged-x){.diff:focus .diff-resizer{min-width:5cqi;max-width:5cqi}.diff:has(.diff-item-1:focus) .diff-resizer{min-width:95cqi;max-width:95cqi}}.modal{pointer-events:none;visibility:hidden;width:100%;max-width:none;height:100%;max-height:none;color:inherit;transition:visibility .3s allow-discrete,background-color .3s ease-out,opacity .1s ease-out;overscroll-behavior:contain;z-index:999;scrollbar-gutter:auto;background-color:#0000;place-items:center;margin:0;padding:0;display:grid;position:fixed;inset:0;overflow:clip}.modal::backdrop{display:none}.fab{pointer-events:none;z-index:999;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:nowrap;inset-inline-end:1rem;flex-direction:column-reverse;align-items:flex-end;gap:.5rem;display:flex;position:fixed;bottom:1rem}.fab>*{pointer-events:auto;align-items:center;gap:.5rem;display:flex}.fab>:hover,.fab>:has(:focus-visible){z-index:1}.fab>[tabindex]:first-child{transition-property:opacity,visibility,rotate;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:grid;position:relative}.fab .fab-close,.fab .fab-main-action{inset-inline-end:0;position:absolute;bottom:0}:is(.fab:focus-within:has(.fab-close),.fab:focus-within:has(.fab-main-action))>[tabindex]{opacity:0;rotate:90deg}.fab:focus-within>[tabindex]:first-child{pointer-events:none}.fab:focus-within>:nth-child(n+2){visibility:visible;--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y);opacity:1}.fab>:nth-child(n+2){visibility:hidden;--tw-scale-x:80%;--tw-scale-y:80%;--tw-scale-z:80%;scale:var(--tw-scale-x)var(--tw-scale-y);opacity:0;transition-property:opacity,scale,visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.fab>:nth-child(n+2).fab-main-action,.fab>:nth-child(n+2).fab-close{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.fab>:nth-child(3){transition-delay:30ms}.fab>:nth-child(4){transition-delay:60ms}.fab>:nth-child(5){transition-delay:90ms}.fab>:nth-child(6){transition-delay:.12s}.tooltip{--tt-bg:var(--color-neutral);--tt-off:calc(100% + .5rem);--tt-tail:calc(100% + 1px + .25rem);display:inline-block;position:relative}.tooltip>.tooltip-content,.tooltip[data-tip]:before{border-radius:var(--radius-field);text-align:center;white-space:normal;max-width:20rem;color:var(--color-neutral-content);opacity:0;background-color:var(--tt-bg);pointer-events:none;z-index:2;--tw-content:attr(data-tip);content:var(--tw-content);width:max-content;padding-block:.25rem;padding-inline:.5rem;font-size:.875rem;line-height:1.25;position:absolute}.tooltip:after{opacity:0;background-color:var(--tt-bg);content:"";pointer-events:none;--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");width:.625rem;height:.25rem;-webkit-mask-position:-1px 0;mask-position:-1px 0;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-image:var(--mask-tooltip);-webkit-mask-image:var(--mask-tooltip);mask-image:var(--mask-tooltip);display:block;position:absolute}@media (prefers-reduced-motion:no-preference){.tooltip>.tooltip-content,.tooltip[data-tip]:before,.tooltip:after{transition:opacity .2s cubic-bezier(.4,0,.2,1) 75ms,transform .2s cubic-bezier(.4,0,.2,1) 75ms}}:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{opacity:1;--tt-pos:0rem}@media (prefers-reduced-motion:no-preference){:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))>.tooltip-content,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible))[data-tip]:before,:is(.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))).tooltip-open,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):hover,.tooltip:is([data-tip]:not([data-tip=""]),:has(.tooltip-content:not(:empty))):has(:focus-visible)):after{transition:opacity .2s cubic-bezier(.4,0,.2,1),transform .2s cubic-bezier(.4,0,.2,1)}}.tab{cursor:pointer;appearance:none;text-align:center;webkit-user-select:none;-webkit-user-select:none;user-select:none;flex-wrap:wrap;justify-content:center;align-items:center;display:inline-flex;position:relative}@media (hover:hover){.tab:hover{color:var(--color-base-content)}}.tab{--tab-p:.75rem;--tab-bg:var(--color-base-100);--tab-border-color:var(--color-base-300);--tab-radius-ss:0;--tab-radius-se:0;--tab-radius-es:0;--tab-radius-ee:0;--tab-order:0;--tab-radius-min:calc(.75rem - var(--border));--tab-radius-limit:min(var(--radius-field),var(--tab-radius-min));--tab-radius-grad:#0000 calc(69% - var(--border)),var(--tab-border-color)calc(69% - var(--border) + .25px),var(--tab-border-color)69%,var(--tab-bg)calc(69% + .25px);order:var(--tab-order);height:var(--tab-height);padding-inline:var(--tab-p);border-color:#0000;font-size:.875rem}.tab:is(input[type=radio]){min-width:fit-content}.tab:is(input[type=radio]):after{--tw-content:attr(aria-label);content:var(--tw-content)}.tab:is(label){position:relative}.tab:is(label) input{cursor:pointer;appearance:none;opacity:0;position:absolute;inset:0}:is(.tab:checked,.tab:is(label:has(:checked)),.tab:is(.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]))+.tab-content{display:block}.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.tab:not(:checked,label:has(:checked),:hover,.tab-active,[aria-selected=true],[aria-current=true],[aria-current=page]){color:color-mix(in oklab,var(--color-base-content)50%,transparent)}}.tab:not(input):empty{cursor:default;flex-grow:1}.tab:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.tab:focus{outline-offset:2px;outline:2px solid #0000}}.tab:focus-visible,.tab:is(label:has(:checked:focus-visible)){outline-offset:-5px;outline:2px solid}.tab[disabled]{pointer-events:none;opacity:.4}.menu{--menu-active-fg:var(--color-neutral-content);--menu-active-bg:var(--color-neutral);flex-flow:column wrap;width:fit-content;padding:.5rem;font-size:.875rem;display:flex}.menu :where(li ul){white-space:nowrap;margin-inline-start:1rem;padding-inline-start:.5rem;position:relative}.menu :where(li ul):before{background-color:var(--color-base-content);opacity:.1;width:var(--border);content:"";inset-inline-start:0;position:absolute;top:.75rem;bottom:.75rem}.menu :where(li>.menu-dropdown:not(.menu-dropdown-show)){display:none}.menu :where(li:not(.menu-title)>:not(ul,details,.menu-title,.btn)),.menu :where(li:not(.menu-title)>details>summary:not(.menu-title)){border-radius:var(--radius-field);text-align:start;text-wrap:balance;-webkit-user-select:none;user-select:none;grid-auto-columns:minmax(auto,max-content) auto max-content;grid-auto-flow:column;align-content:flex-start;align-items:center;gap:.5rem;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:grid}.menu :where(li>details>summary){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li>details>summary){outline-offset:2px;outline:2px solid #0000}}.menu :where(li>details>summary)::-webkit-details-marker{display:none}:is(.menu :where(li>details>summary),.menu :where(li>.menu-dropdown-toggle)):after{content:"";transform-origin:50%;pointer-events:none;justify-self:flex-end;width:.375rem;height:.375rem;transition-property:rotate,translate;transition-duration:.2s;display:block;translate:0 -1px;rotate:-135deg;box-shadow:inset 2px 2px}.menu details{interpolate-size:allow-keywords;overflow:hidden}.menu details::details-content{block-size:0}@media (prefers-reduced-motion:no-preference){.menu details::details-content{transition-behavior:allow-discrete;transition-property:block-size,content-visibility;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.menu details[open]::details-content{block-size:auto}.menu :where(li>details[open]>summary):after,.menu :where(li>.menu-dropdown-toggle.menu-dropdown-show):after{translate:0 1px;rotate:45deg}.menu :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).menu-focus,.menu :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):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :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).menu-focus,.menu :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):focus-visible{background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.menu :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).menu-focus,.menu :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):focus-visible{color:var(--color-base-content);--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :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).menu-focus,.menu :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):focus-visible{outline-offset:2px;outline:2px solid #0000}}.menu :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){cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :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){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.menu :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){--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :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){outline-offset:2px;outline:2px solid #0000}}.menu :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){box-shadow:inset 0 1px oklch(0% 0 0/.01),inset 0 -1px oklch(100% 0 0/.01)}.menu :where(li:empty){background-color:var(--color-base-content);opacity:.1;height:1px;margin:.5rem 1rem}.menu :where(li){flex-flow:column wrap;flex-shrink:0;align-items:stretch;display:flex;position:relative}.menu :where(li) .badge{justify-self:flex-end}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{outline-offset:2px;outline:2px solid #0000}}.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active{color:var(--menu-active-fg);background-color:var(--menu-active-bg);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise)}:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):not(:is(.menu :where(li)>:not(ul,.menu-title,details,.btn):active,.menu :where(li)>:not(ul,.menu-title,details,.btn).menu-active,.menu :where(li)>details>summary:active):active){box-shadow:0 2px calc(var(--depth)*3px)-2px var(--menu-active-bg)}.menu :where(li).menu-disabled{pointer-events:none;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.menu :where(li).menu-disabled{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.menu .dropdown:focus-within .menu-dropdown-toggle:after{translate:0 1px;rotate:45deg}.menu .dropdown-content{margin-top:.5rem;padding:.5rem}.menu .dropdown-content:before{display:none}.dropdown{position-area:var(--anchor-v,bottom)var(--anchor-h,span-right);display:inline-block;position:relative}.dropdown>:not(:has(~[class*=dropdown-content])):focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.dropdown>:not(:has(~[class*=dropdown-content])):focus{outline-offset:2px;outline:2px solid #0000}}.dropdown .dropdown-content{position:absolute}.dropdown.dropdown-close .dropdown-content,.dropdown:not(details,.dropdown-open,.dropdown-hover:hover,:focus-within) .dropdown-content,.dropdown.dropdown-hover:not(:hover) [tabindex]:first-child:focus:not(:focus-visible)~.dropdown-content{transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover],.dropdown .dropdown-content{z-index:999}@media (prefers-reduced-motion:no-preference){.dropdown[popover],.dropdown .dropdown-content{transition-behavior:allow-discrete;transition-property:opacity,scale,display;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);animation:.2s dropdown}}@starting-style{.dropdown[popover],.dropdown .dropdown-content{opacity:0;scale:95%}}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within)>[tabindex]:first-child{pointer-events:none}:is(.dropdown:not(.dropdown-close).dropdown-open,.dropdown:not(.dropdown-close):not(.dropdown-hover):focus,.dropdown:not(.dropdown-close):focus-within) .dropdown-content,.dropdown:not(.dropdown-close).dropdown-hover:hover .dropdown-content{opacity:1;scale:100%}.dropdown:is(details) summary::-webkit-details-marker{display:none}.dropdown:where([popover]){background:0 0}.dropdown[popover]{color:inherit;position:fixed}@supports not (position-area:bottom){.dropdown[popover]{margin:auto}.dropdown[popover].dropdown-close{transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover].dropdown-open:not(:popover-open){transform-origin:top;opacity:0;display:none;scale:95%}.dropdown[popover]::backdrop{background-color:oklab(0% none none/.3)}}:is(.dropdown[popover].dropdown-close,.dropdown[popover]:not(.dropdown-open,:popover-open)){transform-origin:top;opacity:0;display:none;scale:95%}:where(.btn){width:unset}.btn{cursor:pointer;text-align:center;vertical-align:middle;outline-offset:2px;webkit-user-select:none;-webkit-user-select:none;user-select:none;padding-inline:var(--btn-p);color:var(--btn-fg);--tw-prose-links:var(--btn-fg);height:var(--size);font-size:var(--fontsize,.875rem);outline-color:var(--btn-color,var(--color-base-content));background-color:var(--btn-bg);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--btn-noise);border-width:var(--border);border-style:solid;border-color:var(--btn-border);text-shadow:0 .5px oklch(100% 0 0/calc(var(--depth)*.15));touch-action:manipulation;box-shadow:0 .5px 0 .5px oklch(100% 0 0/calc(var(--depth)*6%))inset,var(--btn-shadow);--size:calc(var(--size-field,.25rem)*10);--btn-bg:var(--btn-color,var(--color-base-200));--btn-fg:var(--color-base-content);--btn-p:1rem;--btn-border:var(--btn-bg);border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-wrap:nowrap;flex-shrink:0;justify-content:center;align-items:center;gap:.375rem;font-weight:600;transition-property:color,background-color,border-color,box-shadow;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1);display:inline-flex}@supports (color:color-mix(in lab, red, red)){.btn{--btn-border:color-mix(in oklab,var(--btn-bg),#000 calc(var(--depth)*5%))}}.btn{--btn-shadow:0 3px 2px -2px var(--btn-bg),0 4px 3px -2px var(--btn-bg)}@supports (color:color-mix(in lab, red, red)){.btn{--btn-shadow:0 3px 2px -2px color-mix(in oklab,var(--btn-bg)calc(var(--depth)*30%),#0000),0 4px 3px -2px color-mix(in oklab,var(--btn-bg)calc(var(--depth)*30%),#0000)}}.btn{--btn-noise:var(--fx-noise)}@media (hover:hover){.btn:hover{--btn-bg:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:hover{--btn-bg:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 7%)}}}.btn:focus-visible,.btn:has(:focus-visible){isolation:isolate;outline-width:2px;outline-style:solid}.btn:active:not(.btn-active){--btn-bg:var(--btn-color,var(--color-base-200));translate:0 .5px}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-bg:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 5%)}}.btn:active:not(.btn-active){--btn-border:var(--btn-color,var(--color-base-200))}@supports (color:color-mix(in lab, red, red)){.btn:active:not(.btn-active){--btn-border:color-mix(in oklab,var(--btn-color,var(--color-base-200)),#000 7%)}}.btn:active:not(.btn-active){--btn-shadow:0 0 0 0 oklch(0% 0 0/0),0 0 0 0 oklch(0% 0 0/0)}.btn:is(input[type=checkbox],input[type=radio]){appearance:none}.btn:is(input[type=checkbox],input[type=radio])[aria-label]:after{--tw-content:attr(aria-label);content:var(--tw-content)}.btn:where(input:checked:not(.filter .btn)){--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content);isolation:isolate}.loading{pointer-events:none;aspect-ratio:1;vertical-align:middle;width:calc(var(--size-selector,.25rem)*6);background-color:currentColor;display:inline-block;-webkit-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");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");-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:100%;mask-size:100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.collapse{border-radius:var(--radius-box,1rem);isolation:isolate;grid-template-rows:max-content 0fr;grid-template-columns:minmax(0,1fr);width:100%;display:grid;position:relative;overflow:hidden}@media (prefers-reduced-motion:no-preference){.collapse{transition:grid-template-rows .2s}}.collapse>input:is([type=checkbox],[type=radio]){appearance:none;opacity:0;z-index:1;grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close)),.collapse:not(.collapse-close):has(>input:is([type=checkbox],[type=radio]):checked){grid-template-rows:max-content 1fr}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){content-visibility:visible;min-height:fit-content}@supports not (content-visibility:visible){.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>.collapse-content,.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){visibility:visible}}.collapse:focus-visible,.collapse:has(>input:is([type=checkbox],[type=radio]):focus-visible),.collapse:has(summary:focus-visible){outline-color:var(--color-base-content);outline-offset:2px;outline-width:2px;outline-style:solid}.collapse:not(.collapse-close)>input[type=checkbox],.collapse:not(.collapse-close)>input[type=radio]:not(:checked),.collapse:not(.collapse-close)>.collapse-title{cursor:pointer}:is(.collapse[tabindex]:focus:not(.collapse-close,.collapse[open]),.collapse[tabindex]:focus-within:not(.collapse-close,.collapse[open]))>.collapse-title{cursor:unset}.collapse:is([open],[tabindex]:focus:not(.collapse-close),[tabindex]:focus-within:not(.collapse-close))>:where(.collapse-content),.collapse:not(.collapse-close)>:where(input:is([type=checkbox],[type=radio]):checked~.collapse-content){padding-bottom:1rem}.collapse:is(details){width:100%}@media (prefers-reduced-motion:no-preference){.collapse:is(details)::details-content{transition:content-visibility .2s allow-discrete,visibility .2s allow-discrete,min-height .2s ease-out allow-discrete,padding .1s ease-out 20ms,background-color .2s ease-out,height .2s;interpolate-size:allow-keywords;height:0}.collapse:is(details):where([open])::details-content{height:auto}}.collapse:is(details) summary{display:block;position:relative}.collapse:is(details) summary::-webkit-details-marker{display:none}.collapse:is(details)>.collapse-content{content-visibility:visible}.collapse:is(details) summary{outline:none}.collapse-content{content-visibility:hidden;min-height:0;cursor:unset;grid-row-start:2;grid-column-start:1;padding-left:1rem;padding-right:1rem}@supports not (content-visibility:hidden){.collapse-content{visibility:hidden}}@media (prefers-reduced-motion:no-preference){.collapse-content{transition:content-visibility .2s allow-discrete,visibility .2s allow-discrete,min-height .2s ease-out allow-discrete,padding .1s ease-out 20ms,background-color .2s ease-out}}.validator-hint{visibility:hidden;margin-top:.5rem;font-size:.75rem}.validator:user-valid{--input-color:var(--color-success)}.validator:user-valid:focus{--input-color:var(--color-success)}.validator:user-valid:checked{--input-color:var(--color-success)}.validator:user-valid[aria-checked=true]{--input-color:var(--color-success)}.validator:user-valid:focus-within{--input-color:var(--color-success)}.validator:has(:user-valid){--input-color:var(--color-success)}.validator:has(:user-valid):focus{--input-color:var(--color-success)}.validator:has(:user-valid):checked{--input-color:var(--color-success)}.validator:has(:user-valid)[aria-checked=true]{--input-color:var(--color-success)}.validator:has(:user-valid):focus-within{--input-color:var(--color-success)}.validator:user-invalid{--input-color:var(--color-error)}.validator:user-invalid:focus{--input-color:var(--color-error)}.validator:user-invalid:checked{--input-color:var(--color-error)}.validator:user-invalid[aria-checked=true]{--input-color:var(--color-error)}.validator:user-invalid:focus-within{--input-color:var(--color-error)}.validator:user-invalid~.validator-hint{visibility:visible;color:var(--color-error)}.validator:has(:user-invalid){--input-color:var(--color-error)}.validator:has(:user-invalid):focus{--input-color:var(--color-error)}.validator:has(:user-invalid):checked{--input-color:var(--color-error)}.validator:has(:user-invalid)[aria-checked=true]{--input-color:var(--color-error)}.validator:has(:user-invalid):focus-within{--input-color:var(--color-error)}.validator:has(:user-invalid)~.validator-hint{visibility:visible;color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))),:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):checked,:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))[aria-checked=true],:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false]))):focus-within{--input-color:var(--color-error)}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{visibility:visible;color:var(--color-error)}.list{flex-direction:column;font-size:.875rem;display:flex}.list .list-row{--list-grid-cols:minmax(0,auto)1fr;border-radius:var(--radius-box);word-break:break-word;grid-auto-flow:column;grid-template-columns:var(--list-grid-cols);gap:1rem;padding:1rem;display:grid;position:relative}:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{content:"";border-bottom:var(--border)solid;inset-inline:var(--radius-box);border-color:var(--color-base-content);position:absolute;bottom:0}@supports (color:color-mix(in lab, red, red)){:is(.list>:not(:last-child).list-row,.list>:not(:last-child) .list-row):after{border-color:color-mix(in oklab,var(--color-base-content)5%,transparent)}}.toast{translate:var(--toast-x,0)var(--toast-y,0);inset-inline:auto 1rem;background-color:#0000;flex-direction:column;gap:.5rem;width:max-content;max-width:calc(100vw - 2rem);display:flex;position:fixed;top:auto;bottom:1rem}@media (prefers-reduced-motion:no-preference){.toast>*{animation:.25s ease-out toast}}.toggle{border:var(--border)solid currentColor;color:var(--input-color);cursor:pointer;appearance:none;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--toggle-p),var(--radius-selector-max)) + min(var(--border),var(--radius-selector-max)));padding:var(--toggle-p);flex-shrink:0;grid-template-columns:0fr 1fr 1fr;place-content:center;display:inline-grid;position:relative;box-shadow:inset 0 1px}@supports (color:color-mix(in lab, red, red)){.toggle{box-shadow:0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000)inset}}.toggle{--input-color:var(--color-base-content);transition:color .3s,grid-template-columns .2s}@supports (color:color-mix(in lab, red, red)){.toggle{--input-color:color-mix(in oklab,var(--color-base-content)50%,#0000)}}.toggle{--toggle-p:calc(var(--size)*.125);--size:calc(var(--size-selector,.25rem)*6);width:calc((var(--size)*2) - (var(--border) + var(--toggle-p))*2);height:var(--size)}.toggle>*{z-index:1;cursor:pointer;appearance:none;background-color:#0000;border:none;grid-column:2/span 1;grid-row-start:1;height:100%;padding:.125rem;transition:opacity .2s,rotate .4s}.toggle>:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.toggle>:focus{outline-offset:2px;outline:2px solid #0000}}.toggle>:nth-child(2){color:var(--color-base-100);rotate:none}.toggle>:nth-child(3){color:var(--color-base-100);opacity:0;rotate:-15deg}.toggle:has(:checked)>:nth-child(2){opacity:0;rotate:15deg}.toggle:has(:checked)>:nth-child(3){opacity:1;rotate:none}.toggle:before{aspect-ratio:1;border-radius:var(--radius-selector);--tw-content:"";content:var(--tw-content);height:100%;box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor;background-color:currentColor;grid-row-start:1;grid-column-start:2;transition:background-color .1s,translate .2s,inset-inline-start .2s;position:relative;inset-inline-start:0;translate:0}@supports (color:color-mix(in lab, red, red)){.toggle:before{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px color-mix(in oklab,currentColor calc(var(--depth)*10%),#0000)}}.toggle:before{background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise)}@media (forced-colors:active){.toggle:before{outline-style:var(--tw-outline-style);outline-offset:calc(1px*-1);outline-width:1px}}@media print{.toggle:before{outline-offset:-1rem;outline:.25rem solid}}.toggle:focus-visible,.toggle:has(:focus-visible){outline-offset:2px;outline:2px solid}.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked){background-color:var(--color-base-100);--input-color:var(--color-base-content);grid-template-columns:1fr 1fr 0fr}:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{background-color:currentColor}@starting-style{:is(.toggle:checked,.toggle[aria-checked=true],.toggle:has(>input:checked)):before{opacity:0}}.toggle:indeterminate{grid-template-columns:.5fr 1fr .5fr}.toggle:disabled{cursor:not-allowed;opacity:.3}.toggle:disabled:before{border:var(--border)solid currentColor;background-color:#0000}.input{cursor:text;border:var(--border)solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;white-space:nowrap;width:clamp(3rem,20rem,100%);height:var(--size);font-size:max(var(--font-size,.875rem),.875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.5rem;padding-inline:.75rem;display:inline-flex;position:relative}@supports (color:color-mix(in lab, red, red)){.input{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)*.1))inset}}.input{--size:calc(var(--size-field,.25rem)*10);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.input:where(input){display:inline-flex}.input :where(input){appearance:none;background-color:#0000;border:none;width:100%;height:100%;display:inline-flex}.input :where(input):focus,.input :where(input):focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.input :where(input):focus,.input :where(input):focus-within{outline-offset:2px;outline:2px solid #0000}}.input :where(input[type=url]),.input :where(input[type=email]){direction:ltr}.input :where(input[type=date]){display:inline-flex}.input:focus,.input:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.input:focus,.input:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.input:focus,.input:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.input:focus,.input:focus-within{--font-size:1rem}}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input)::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.input:has(>input[disabled]),.input:is(:disabled,[disabled]),fieldset:disabled .input{box-shadow:none}.input:has(>input[disabled])>input[disabled]{cursor:not-allowed}.input::-webkit-date-and-time-value{text-align:inherit}.input[type=number]::-webkit-inner-spin-button{margin-block:-.75rem;margin-inline-end:-.75rem}.input::-webkit-calendar-picker-indicator{position:absolute;inset-inline-end:.75em}.input:has(>input[type=date]) :where(input[type=date]){webkit-appearance:none;appearance:none;display:inline-flex}.input:has(>input[type=date]) input[type=date]::-webkit-calendar-picker-indicator{cursor:pointer;width:1em;height:1em;position:absolute;inset-inline-end:.75em}.indicator{width:max-content;display:inline-flex;position:relative}.indicator :where(.indicator-item){z-index:1;white-space:nowrap;top:var(--indicator-t,0);bottom:var(--indicator-b,auto);left:var(--indicator-s,auto);right:var(--indicator-e,0);translate:var(--indicator-x,50%)var(--indicator-y,-50%);position:absolute}.table{border-collapse:separate;--tw-border-spacing-x:calc(.25rem*0);--tw-border-spacing-y:calc(.25rem*0);width:100%;border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y);border-radius:var(--radius-box);text-align:left;font-size:.875rem;position:relative}.table:where(:dir(rtl),[dir=rtl],[dir=rtl] *){text-align:right}@media (hover:hover){:is(.table tr.row-hover,.table tr.row-hover:nth-child(2n)):hover{background-color:var(--color-base-200)}}.table :where(th,td){vertical-align:middle;padding-block:.75rem;padding-inline:1rem}.table :where(thead,tfoot){white-space:nowrap;color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead,tfoot){color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.table :where(thead,tfoot){font-size:.875rem;font-weight:600}.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(tfoot tr:first-child :is(td,th)){border-top:var(--border)solid color-mix(in oklch,var(--color-base-content)5%,#0000)}}.table :where(.table-pin-rows thead tr){z-index:1;background-color:var(--color-base-100);position:sticky;top:0}.table :where(.table-pin-rows tfoot tr){z-index:1;background-color:var(--color-base-100);position:sticky;bottom:0}.table :where(.table-pin-cols tr th){background-color:var(--color-base-100);position:sticky;left:0;right:0}.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.table :where(thead tr :is(td,th),tbody tr:not(:last-child) :is(td,th)){border-bottom:var(--border)solid color-mix(in oklch,var(--color-base-content)5%,#0000)}}.steps{counter-reset:step;grid-auto-columns:1fr;grid-auto-flow:column;display:inline-grid;overflow:auto hidden}.steps .step{text-align:center;--step-bg:var(--color-base-300);--step-fg:var(--color-base-content);grid-template-rows:40px 1fr;grid-template-columns:auto;place-items:center;min-width:4rem;display:grid}.steps .step:before{width:100%;height:.5rem;color:var(--step-bg);background-color:var(--step-bg);content:"";border:1px solid;grid-row-start:1;grid-column-start:1;margin-inline-start:-100%;top:0}.steps .step>.step-icon,.steps .step:not(:has(.step-icon)):after{--tw-content:counter(step);content:var(--tw-content);counter-increment:step;z-index:1;color:var(--step-fg);background-color:var(--step-bg);border:1px solid var(--step-bg);border-radius:3.40282e38px;grid-row-start:1;grid-column-start:1;place-self:center;place-items:center;width:2rem;height:2rem;display:grid;position:relative}.steps .step:first-child:before{--tw-content:none;content:var(--tw-content)}.steps .step[data-content]:after{--tw-content:attr(data-content);content:var(--tw-content)}.range{appearance:none;webkit-appearance:none;--range-thumb:var(--color-base-100);--range-thumb-size:calc(var(--size-selector,.25rem)*6);--range-progress:currentColor;--range-fill:1;--range-p:.25rem;--range-bg:currentColor}@supports (color:color-mix(in lab, red, red)){.range{--range-bg:color-mix(in oklab,currentColor 10%,#0000)}}.range{cursor:pointer;vertical-align:middle;--radius-selector-max:calc(var(--radius-selector) + var(--radius-selector) + var(--radius-selector));border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));width:clamp(3rem,20rem,100%);height:var(--range-thumb-size);background-color:#0000;border:none;overflow:hidden}[dir=rtl] .range{--range-dir:-1}.range:focus{outline:none}.range:focus-visible{outline-offset:2px;outline:2px solid}.range::-webkit-slider-runnable-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size)*.5)}@media (forced-colors:active){.range::-webkit-slider-runnable-track{border:1px solid}.range::-moz-range-track{border:1px solid}}.range::-webkit-slider-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));background-color:var(--range-thumb);height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p)solid;appearance:none;webkit-appearance:none;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor,0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*var(--range-fill));position:relative;top:50%;transform:translateY(-50%)}@supports (color:color-mix(in lab, red, red)){.range::-webkit-slider-thumb{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*var(--range-fill))}}.range::-moz-range-track{background-color:var(--range-bg);border-radius:var(--radius-selector);width:100%;height:calc(var(--range-thumb-size)*.5)}.range::-moz-range-thumb{box-sizing:border-box;border-radius:calc(var(--radius-selector) + min(var(--range-p),var(--radius-selector-max)));height:var(--range-thumb-size);width:var(--range-thumb-size);border:var(--range-p)solid;color:var(--range-progress);box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px currentColor,0 0 0 2rem var(--range-thumb)inset,calc((var(--range-dir,1)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*var(--range-fill));background-color:currentColor;position:relative;top:50%}@supports (color:color-mix(in lab, red, red)){.range::-moz-range-thumb{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.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)*-100cqw) - (var(--range-dir,1)*var(--range-thumb-size)/2))0 0 calc(100cqw*var(--range-fill))}}.range:disabled{cursor:not-allowed;opacity:.3}.diff-resizer{isolation:isolate;z-index:2;resize:horizontal;opacity:0;cursor:ew-resize;transform-origin:100% 100%;clip-path:inset(calc(100% - .75rem) 0 0 calc(100% - .75rem));grid-row-start:2;grid-column-start:1;width:50cqi;min-width:1rem;max-width:calc(100cqi - 1rem);height:.75rem;transition:min-width .3s ease-out,max-width .3s ease-out;position:relative;overflow:hidden;transform:scaleY(5)translate(.32rem,50%)}.select{border:var(--border)solid #0000;appearance:none;background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);height:var(--size);touch-action:manipulation;white-space:nowrap;text-overflow:ellipsis;box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;background-image:linear-gradient(45deg,#0000 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,#0000 50%);background-position:calc(100% - 20px) calc(1px + 50%),calc(100% - 16.1px) calc(1px + 50%);background-repeat:no-repeat;background-size:4px 4px,4px 4px;border-start-start-radius:var(--join-ss,var(--radius-field));border-start-end-radius:var(--join-se,var(--radius-field));border-end-end-radius:var(--join-ee,var(--radius-field));border-end-start-radius:var(--join-es,var(--radius-field));flex-shrink:1;align-items:center;gap:.375rem;padding-inline:.75rem 1.75rem;font-size:.875rem;display:inline-flex;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.select{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)*.1))inset}}.select{border-color:var(--input-color);--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.select{--size:calc(var(--size-field,.25rem)*10)}[dir=rtl] .select{background-position:12px calc(1px + 50%),16px calc(1px + 50%)}[dir=rtl] .select::picker(select){translate:.5rem}[dir=rtl] .select select::picker(select){translate:.5rem}.select[multiple]{background-image:none;height:auto;padding-block:.75rem;padding-inline-end:.75rem;overflow:auto}.select select{appearance:none;width:calc(100% + 2.75rem);height:calc(100% - calc(var(--border)*2));background:inherit;border-radius:inherit;border-style:none;align-items:center;margin-inline:-.75rem -1.75rem;padding-inline:.75rem 1.75rem}.select select:focus,.select select:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.select select:focus,.select select:focus-within{outline-offset:2px;outline:2px solid #0000}}.select select:not(:last-child){background-image:none;margin-inline-end:-1.375rem}.select:focus,.select:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.select:focus,.select:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.select:focus,.select:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select{color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select:has(>select[disabled]),.select:is(:disabled,[disabled]),fieldset:disabled .select)::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.select:has(>select[disabled])>select[disabled]{cursor:not-allowed}@supports (appearance:base-select){.select,.select select{appearance:base-select}:is(.select,.select select)::picker(select){appearance:base-select}}:is(.select,.select select)::picker(select){color:inherit;border:var(--border)solid var(--color-base-200);border-radius:var(--radius-box);background-color:inherit;max-height:min(24rem,70dvh);box-shadow:0 2px calc(var(--depth)*3px)-2px oklch(0% 0 0/.2);box-shadow:0 20px 25px -5px rgb(0 0 0/calc(var(--depth)*.1)),0 8px 10px -6px rgb(0 0 0/calc(var(--depth)*.1));margin-block:.5rem;margin-inline:.5rem;padding:.5rem;translate:-.5rem}:is(.select,.select select)::picker-icon{display:none}:is(.select,.select select) optgroup{padding-top:.5em}:is(.select,.select select) optgroup option:first-child{margin-top:.5em}:is(.select,.select select) option{border-radius:var(--radius-field);white-space:normal;padding-block:.375rem;padding-inline:.75rem;transition-property:color,background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{cursor:pointer;background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){:is(.select,.select select) option:not(:disabled):hover,:is(.select,.select select) option:not(:disabled):focus-visible{outline-offset:2px;outline:2px solid #0000}}:is(.select,.select select) option:not(:disabled):active{background-color:var(--color-neutral);color:var(--color-neutral-content);box-shadow:0 2px calc(var(--depth)*3px)-2px var(--color-neutral)}.timeline{display:flex;position:relative}.timeline>li{grid-template-rows:var(--timeline-row-start,minmax(0,1fr))auto var(--timeline-row-end,minmax(0,1fr));grid-template-columns:var(--timeline-col-start,minmax(0,1fr))auto var(--timeline-col-end,minmax(0,1fr));flex-shrink:0;align-items:center;display:grid;position:relative}.timeline>li>hr{border:none;width:100%}.timeline>li>hr:first-child{grid-row-start:2;grid-column-start:1}.timeline>li>hr:last-child{grid-area:2/3/auto/none}@media print{.timeline>li>hr{border:.1px solid var(--color-base-300)}}.timeline :where(hr){background-color:var(--color-base-300);height:.25rem}.timeline:has(.timeline-middle hr):first-child{border-start-start-radius:0;border-start-end-radius:var(--radius-selector);border-end-end-radius:var(--radius-selector);border-end-start-radius:0}.timeline:has(.timeline-middle hr):last-child,.timeline:not(:has(.timeline-middle)) :first-child hr:last-child{border-start-start-radius:var(--radius-selector);border-start-end-radius:0;border-end-end-radius:0;border-end-start-radius:var(--radius-selector)}.timeline:not(:has(.timeline-middle)) :last-child hr:first-child{border-start-start-radius:0;border-start-end-radius:var(--radius-selector);border-end-end-radius:var(--radius-selector);border-end-start-radius:0}.swap{cursor:pointer;vertical-align:middle;webkit-user-select:none;-webkit-user-select:none;user-select:none;place-content:center;display:inline-grid;position:relative}.swap input{appearance:none;border:none}.swap>*{grid-row-start:1;grid-column-start:1}@media (prefers-reduced-motion:no-preference){.swap>*{transition-property:transform,rotate,opacity;transition-duration:.2s;transition-timing-function:cubic-bezier(0,0,.2,1)}}.swap .swap-on,.swap .swap-indeterminate,.swap input:indeterminate~.swap-on,.swap input:is(:checked,:indeterminate)~.swap-off{opacity:0}.swap input:checked~.swap-on,.swap input:indeterminate~.swap-indeterminate{opacity:1;backface-visibility:visible}.collapse-title{grid-row-start:1;grid-column-start:1;width:100%;min-height:1lh;padding:1rem;padding-inline-end:3rem;transition:background-color .2s ease-out;position:relative}.mockup-code{border-radius:var(--radius-box);background-color:var(--color-neutral);color:var(--color-neutral-content);direction:ltr;padding-block:1.25rem;font-size:.875rem;position:relative;overflow:auto hidden}.mockup-code:before{content:"";opacity:.3;border-radius:3.40282e38px;width:.75rem;height:.75rem;margin-bottom:1rem;display:block;box-shadow:1.4em 0,2.8em 0,4.2em 0}.mockup-code pre{padding-right:1.25rem}.mockup-code pre:before{content:"";margin-right:2ch}.mockup-code pre[data-prefix]:before{--tw-content:attr(data-prefix);content:var(--tw-content);text-align:right;opacity:.5;width:2rem;display:inline-block}.avatar{vertical-align:middle;display:inline-flex;position:relative}.avatar>div{aspect-ratio:1;display:block;overflow:hidden}.avatar img{object-fit:cover;width:100%;height:100%}.checkbox{border:var(--border)solid var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox{border:var(--border)solid var(--input-color,color-mix(in oklab,var(--color-base-content)20%,#0000))}}.checkbox{cursor:pointer;appearance:none;border-radius:var(--radius-selector);vertical-align:middle;color:var(--color-base-content);box-shadow:0 1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 0 #0000 inset,0 0 #0000;--size:calc(var(--size-selector,.25rem)*6);width:var(--size);height:var(--size);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);flex-shrink:0;padding:.25rem;transition:background-color .2s,box-shadow .2s;display:inline-block;position:relative}.checkbox:before{--tw-content:"";content:var(--tw-content);opacity:0;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,70% 80%,70% 100%);width:100%;height:100%;box-shadow:0px 3px 0 0px oklch(100% 0 0/calc(var(--depth)*.1))inset;background-color:currentColor;font-size:1rem;line-height:.75;transition:clip-path .3s .1s,opacity .1s .1s,rotate .3s .1s,translate .3s .1s;display:block;rotate:45deg}.checkbox:focus-visible{outline:2px solid var(--input-color,currentColor);outline-offset:2px}.checkbox:checked,.checkbox[aria-checked=true]{background-color:var(--input-color,#0000);box-shadow:0 0 #0000 inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px oklch(0% 0 0/calc(var(--depth)*.1))}:is(.checkbox:checked,.checkbox[aria-checked=true]):before{clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 0%,70% 0%,70% 100%);opacity:1}@media (forced-colors:active){:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}@media print{:is(.checkbox:checked,.checkbox[aria-checked=true]):before{--tw-content:"✔︎";clip-path:none;background-color:#0000;rotate:none}}.checkbox:indeterminate{background-color:var(--input-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.checkbox:indeterminate{background-color:var(--input-color,color-mix(in oklab,var(--color-base-content)20%,#0000))}}.checkbox:indeterminate:before{opacity:1;clip-path:polygon(20% 100%,20% 80%,50% 80%,50% 80%,80% 80%,80% 100%);translate:0 -35%;rotate:none}.radio{cursor:pointer;appearance:none;vertical-align:middle;border:var(--border)solid var(--input-color,currentColor);border-radius:3.40282e38px;flex-shrink:0;padding:.25rem;display:inline-block;position:relative}@supports (color:color-mix(in lab, red, red)){.radio{border:var(--border)solid var(--input-color,color-mix(in srgb,currentColor 20%,#0000))}}.radio{box-shadow:0 1px oklch(0% 0 0/calc(var(--depth)*.1))inset;--size:calc(var(--size-selector,.25rem)*6);width:var(--size);height:var(--size);color:var(--input-color,currentColor)}.radio:before{--tw-content:"";content:var(--tw-content);background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);border-radius:3.40282e38px;width:100%;height:100%;display:block}.radio:focus-visible{outline:2px solid}.radio:checked,.radio[aria-checked=true]{background-color:var(--color-base-100);border-color:currentColor}@media (prefers-reduced-motion:no-preference){.radio:checked,.radio[aria-checked=true]{animation:.2s ease-out radio}}:is(.radio:checked,.radio[aria-checked=true]):before{box-shadow:0 -1px oklch(0% 0 0/calc(var(--depth)*.1))inset,0 8px 0 -4px oklch(100% 0 0/calc(var(--depth)*.1))inset,0 1px oklch(0% 0 0/calc(var(--depth)*.1));background-color:currentColor}@media (forced-colors:active){:is(.radio:checked,.radio[aria-checked=true]):before{outline-style:var(--tw-outline-style);outline-offset:calc(1px*-1);outline-width:1px}}@media print{:is(.radio:checked,.radio[aria-checked=true]):before{outline-offset:-1rem;outline:.25rem solid}}.rating{vertical-align:middle;display:inline-flex;position:relative}.rating input{appearance:none;border:none}.rating :where(*){background-color:var(--color-base-content);opacity:.2;border-radius:0;width:1.5rem;height:1.5rem}@media (prefers-reduced-motion:no-preference){.rating :where(*){animation:.25s ease-out rating}}.rating :where(*):is(input){cursor:pointer}.rating .rating-hidden{background-color:#0000;width:.5rem}.rating input[type=radio]:checked{background-image:none}.rating :checked,.rating [aria-checked=true],.rating [aria-current=true],.rating :has(~:checked,~[aria-checked=true],~[aria-current=true]){opacity:1}.rating :focus-visible{scale:1.1}@media (prefers-reduced-motion:no-preference){.rating :focus-visible{transition:scale .2s ease-out}}.rating :active:focus{animation:none;scale:1.1}.navbar{align-items:center;width:100%;min-height:4rem;padding:.5rem;display:flex}.card{border-radius:var(--radius-box);outline-offset:2px;outline:0 solid #0000;flex-direction:column;transition:outline .2s ease-in-out;display:flex;position:relative}.card:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.card:focus{outline-offset:2px;outline:2px solid #0000}}.card:focus-visible{outline-color:currentColor}.card :where(figure:first-child){border-start-start-radius:inherit;border-start-end-radius:inherit;border-end-end-radius:unset;border-end-start-radius:unset;overflow:hidden}.card :where(figure:last-child){border-start-start-radius:unset;border-start-end-radius:unset;border-end-end-radius:inherit;border-end-start-radius:inherit;overflow:hidden}.card figure{justify-content:center;align-items:center;display:flex}.card:has(>input:is(input[type=checkbox],input[type=radio])){cursor:pointer;-webkit-user-select:none;user-select:none}.card:has(>:checked){outline:2px solid}.stats{border-radius:var(--radius-box);grid-auto-flow:column;display:inline-grid;position:relative;overflow-x:auto}.progress{appearance:none;border-radius:var(--radius-box);background-color:currentColor;width:100%;height:.5rem;position:relative;overflow:hidden}@supports (color:color-mix(in lab, red, red)){.progress{background-color:color-mix(in oklab,currentcolor 20%,transparent)}}.progress{color:var(--color-base-content)}.progress:indeterminate{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%}@media (prefers-reduced-motion:no-preference){.progress:indeterminate{animation:5s ease-in-out infinite progress}}@supports ((-moz-appearance:none)){.progress:indeterminate::-moz-progress-bar{background-color:#0000}@media (prefers-reduced-motion:no-preference){.progress:indeterminate::-moz-progress-bar{background-image:repeating-linear-gradient(90deg,currentColor -1% 10%,#0000 10% 90%);background-position-x:15%;background-size:200%;animation:5s ease-in-out infinite progress}}.progress::-moz-progress-bar{border-radius:var(--radius-box);background-color:currentColor}}@supports ((-webkit-appearance:none)){.progress::-webkit-progress-bar{border-radius:var(--radius-box);background-color:#0000}.progress::-webkit-progress-value{border-radius:var(--radius-box);background-color:currentColor}}.hero-content{isolation:isolate;justify-content:center;align-items:center;gap:1rem;max-width:80rem;padding:1rem;display:flex}.textarea{border:var(--border)solid #0000;appearance:none;border-radius:var(--radius-field);background-color:var(--color-base-100);vertical-align:middle;width:clamp(3rem,20rem,100%);min-height:5rem;font-size:max(var(--font-size,.875rem),.875rem);touch-action:manipulation;border-color:var(--input-color);box-shadow:0 1px var(--input-color)inset,0 -1px oklch(100% 0 0/calc(var(--depth)*.1))inset;flex-shrink:1;padding-block:.5rem;padding-inline:.75rem}@supports (color:color-mix(in lab, red, red)){.textarea{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)*.1))inset}}.textarea{--input-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea{--input-color:color-mix(in oklab,var(--color-base-content)20%,#0000)}}.textarea textarea{appearance:none;background-color:#0000;border:none}.textarea textarea:focus,.textarea textarea:focus-within{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.textarea textarea:focus,.textarea textarea:focus-within{outline-offset:2px;outline:2px solid #0000}}.textarea:focus,.textarea:focus-within{--input-color:var(--color-base-content);box-shadow:0 1px var(--input-color)}@supports (color:color-mix(in lab, red, red)){.textarea:focus,.textarea:focus-within{box-shadow:0 1px color-mix(in oklab,var(--input-color)calc(var(--depth)*10%),#0000)}}.textarea:focus,.textarea:focus-within{outline:2px solid var(--input-color);outline-offset:2px;isolation:isolate}@media (pointer:coarse){@supports (-webkit-touch-callout:none){.textarea:focus,.textarea:focus-within{--font-size:1rem}}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){cursor:not-allowed;border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){color:color-mix(in oklab,var(--color-base-content)40%,transparent)}}:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:is(.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]))::placeholder{color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.textarea:has(>textarea[disabled]),.textarea:is(:disabled,[disabled]){box-shadow:none}.textarea:has(>textarea[disabled])>textarea[disabled]{cursor:not-allowed}.stack{grid-template-rows:3px 4px 1fr 4px 3px;grid-template-columns:3px 4px 1fr 4px 3px;display:inline-grid}.stack>*{width:100%;height:100%}.stack>:nth-child(n+2){opacity:.7;width:100%}.stack>:nth-child(2){z-index:2;opacity:.9}.stack>:first-child{z-index:3;width:100%}.modal-backdrop{color:#0000;z-index:-1;grid-row-start:1;grid-column-start:1;place-self:stretch stretch;display:grid}.modal-backdrop button{cursor:pointer}.tab-content{order:var(--tabcontent-order);--tabcontent-radius-ss:var(--radius-box);--tabcontent-radius-se:var(--radius-box);--tabcontent-radius-es:var(--radius-box);--tabcontent-radius-ee:var(--radius-box);--tabcontent-order:1;width:100%;height:calc(100% - var(--tab-height) + var(--border));margin:var(--tabcontent-margin);border-color:#0000;border-width:var(--border);border-start-start-radius:var(--tabcontent-radius-ss);border-start-end-radius:var(--tabcontent-radius-se);border-end-end-radius:var(--tabcontent-radius-ee);border-end-start-radius:var(--tabcontent-radius-es);display:none}.hero{background-position:50%;background-size:cover;place-items:center;width:100%;display:grid}.hero>*{grid-row-start:1;grid-column-start:1}.modal-box{background-color:var(--color-base-100);border-top-left-radius:var(--modal-tl,var(--radius-box));border-top-right-radius:var(--modal-tr,var(--radius-box));border-bottom-left-radius:var(--modal-bl,var(--radius-box));border-bottom-right-radius:var(--modal-br,var(--radius-box));opacity:0;overscroll-behavior:contain;grid-row-start:1;grid-column-start:1;width:91.6667%;max-width:32rem;max-height:100vh;padding:1.5rem;transition:translate .3s ease-out,scale .3s ease-out,opacity .2s ease-out 50ms,box-shadow .3s ease-out;overflow-y:auto;scale:95%;box-shadow:0 25px 50px -12px oklch(0% 0 0/.25)}.timeline-middle{grid-row-start:2;grid-column-start:2}.stat-value{white-space:nowrap;grid-column-start:1;font-size:2rem;font-weight:800}.divider{white-space:nowrap;height:1rem;margin:var(--divider-m,1rem 0);--divider-color:var(--color-base-content);flex-direction:row;align-self:stretch;align-items:center;display:flex}@supports (color:color-mix(in lab, red, red)){.divider{--divider-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.divider:before,.divider:after{content:"";background-color:var(--divider-color);flex-grow:1;width:100%;height:.125rem}@media print{.divider:before,.divider:after{border:.5px solid}}.divider:not(:empty){gap:1rem}.filter{flex-wrap:wrap;display:flex}.filter input[type=radio]{width:auto}.filter input{opacity:1;transition:margin .1s,opacity .3s,padding .3s,border-width .1s;overflow:hidden;scale:1}.filter input:not(:last-child){margin-inline-end:.25rem}.filter input.filter-reset{aspect-ratio:1}.filter input.filter-reset:after{--tw-content:"×";content:var(--tw-content)}.filter:not(:has(input:checked:not(.filter-reset))) .filter-reset,.filter:not(:has(input:checked:not(.filter-reset))) input[type=reset],.filter:has(input:checked:not(.filter-reset)) input:not(:checked,.filter-reset,input[type=reset]){opacity:0;border-width:0;width:0;margin-inline:0;padding-inline:0;scale:0}.label{white-space:nowrap;color:currentColor;align-items:center;gap:.375rem;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.label{color:color-mix(in oklab,currentcolor 60%,transparent)}}.label:has(input){cursor:pointer}.label:is(.input>*,.select>*){white-space:nowrap;height:calc(100% - .5rem);font-size:inherit;align-items:center;padding-inline:.75rem;display:flex}.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border)solid currentColor;margin-inline:-.75rem .75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):first-child{border-inline-end:var(--border)solid color-mix(in oklab,currentColor 10%,#0000)}}.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border)solid currentColor;margin-inline:.75rem -.75rem}@supports (color:color-mix(in lab, red, red)){.label:is(.input>*,.select>*):last-child{border-inline-start:var(--border)solid color-mix(in oklab,currentColor 10%,#0000)}}.modal-action{justify-content:flex-end;gap:.5rem;margin-top:1.5rem;display:flex}.carousel-item{box-sizing:content-box;scroll-snap-align:start;flex:none;display:flex}.status{aspect-ratio:1;border-radius:var(--radius-selector);background-color:var(--color-base-content);width:.5rem;height:.5rem;display:inline-block}@supports (color:color-mix(in lab, red, red)){.status{background-color:color-mix(in oklab,var(--color-base-content)20%,transparent)}}.status{vertical-align:middle;color:#0000004d;background-position:50%;background-repeat:no-repeat}@supports (color:color-mix(in lab, red, red)){.status{color:color-mix(in oklab,var(--color-black)30%,transparent)}}.status{background-image:radial-gradient(circle at 35% 30%,oklch(1 0 0/calc(var(--depth)*.5)),#0000);box-shadow:0 2px 3px -1px}@supports (color:color-mix(in lab, red, red)){.status{box-shadow:0 2px 3px -1px color-mix(in oklab,currentColor calc(var(--depth)*100%),#0000)}}.badge{border-radius:var(--radius-selector);vertical-align:middle;color:var(--badge-fg);border:var(--border)solid var(--badge-color,var(--color-base-200));background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);background-color:var(--badge-bg);--badge-bg:var(--badge-color,var(--color-base-100));--badge-fg:var(--color-base-content);--size:calc(var(--size-selector,.25rem)*6);width:fit-content;height:var(--size);padding-inline:calc(var(--size)/2 - var(--border));justify-content:center;align-items:center;gap:.5rem;font-size:.875rem;display:inline-flex}.kbd{border-radius:var(--radius-field);background-color:var(--color-base-200);vertical-align:middle;border:var(--border)solid var(--color-base-content);justify-content:center;align-items:center;padding-inline:.5em;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.kbd{border:var(--border)solid color-mix(in srgb,var(--color-base-content)20%,#0000)}}.kbd{border-bottom:calc(var(--border) + 1px)solid var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.kbd{border-bottom:calc(var(--border) + 1px)solid color-mix(in srgb,var(--color-base-content)20%,#0000)}}.kbd{--size:calc(var(--size-selector,.25rem)*6);height:var(--size);min-width:var(--size);font-size:.875rem}.tabs{--tabs-height:auto;--tabs-direction:row;--tab-height:calc(var(--size-field,.25rem)*10);height:var(--tabs-height);flex-wrap:wrap;flex-direction:var(--tabs-direction);display:flex}.footer{grid-auto-flow:row;place-items:start;gap:2.5rem 1rem;width:100%;font-size:.875rem;line-height:1.25rem;display:grid}.footer>*{place-items:start;gap:.5rem;display:grid}.footer.footer-center{text-align:center;grid-auto-flow:column dense;place-items:center}.footer.footer-center>*{place-items:center}.stat{grid-template-columns:repeat(1,1fr);column-gap:1rem;width:100%;padding-block:1rem;padding-inline:1.5rem;display:inline-grid}.stat:not(:last-child){border-inline-end:var(--border)dashed currentColor}@supports (color:color-mix(in lab, red, red)){.stat:not(:last-child){border-inline-end:var(--border)dashed color-mix(in oklab,currentColor 10%,#0000)}}.stat:not(:last-child){border-block-end:none}.navbar-end{justify-content:flex-end;align-items:center;width:50%;display:inline-flex}.navbar-start{justify-content:flex-start;align-items:center;width:50%;display:inline-flex}.card-body{padding:var(--card-p,1.5rem);font-size:var(--card-fs,.875rem);flex-direction:column;flex:auto;gap:.5rem;display:flex}.card-body :where(p){flex-grow:1}.carousel{scroll-snap-type:x mandatory;scrollbar-width:none;display:inline-flex;overflow-x:scroll}@media (prefers-reduced-motion:no-preference){.carousel{scroll-behavior:smooth}}.carousel::-webkit-scrollbar{display:none}.alert{--alert-border-color:var(--color-base-200);border-radius:var(--radius-box);color:var(--color-base-content);background-color:var(--alert-color,var(--color-base-200));text-align:start;background-size:auto,calc(var(--noise)*100%);background-image:none,var(--fx-noise);box-shadow:0 3px 0 -2px oklch(100% 0 0/calc(var(--depth)*.08))inset,0 1px #000,0 4px 3px -2px oklch(0% 0 0/calc(var(--depth)*.08));border-style:solid;grid-template-columns:auto;grid-auto-flow:column;justify-content:start;place-items:center start;gap:1rem;padding-block:.75rem;padding-inline:1rem;font-size:.875rem;line-height:1.25rem;display:grid}@supports (color:color-mix(in lab, red, red)){.alert{box-shadow:0 3px 0 -2px oklch(100% 0 0/calc(var(--depth)*.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)*.08))}}.alert:has(:nth-child(2)){grid-template-columns:auto minmax(auto,1fr)}.fieldset{grid-template-columns:1fr;grid-auto-rows:max-content;gap:.375rem;padding-block:.25rem;font-size:.75rem;display:grid}.chat{--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");grid-auto-rows:min-content;column-gap:.75rem;padding-block:.25rem;display:grid}.card-title{font-size:var(--cardtitle-fs,1.125rem);align-items:center;gap:.5rem;font-weight:600;display:flex}.mask{vertical-align:middle;display:inline-block;-webkit-mask-position:50%;mask-position:50%;-webkit-mask-size:contain;mask-size:contain;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.skeleton{border-radius:var(--radius-box);background-color:var(--color-base-300)}@media (prefers-reduced-motion:reduce){.skeleton{transition-duration:15s}}.skeleton{will-change:background-position;background-image:linear-gradient(105deg,#0000 0% 40%,var(--color-base-100)50%,#0000 60% 100%);background-position-x:-50%;background-size:200%}@media (prefers-reduced-motion:no-preference){.skeleton{animation:1.8s ease-in-out infinite skeleton}}.link{cursor:pointer;text-decoration-line:underline}.link:focus{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.link:focus{outline-offset:2px;outline:2px solid #0000}}.link:focus-visible{outline-offset:2px;outline:2px solid}.btn-error{--btn-color:var(--color-error);--btn-fg:var(--color-error-content)}.btn-primary{--btn-color:var(--color-primary);--btn-fg:var(--color-primary-content)}.btn-secondary{--btn-color:var(--color-secondary);--btn-fg:var(--color-secondary-content)}}@layer daisyui.l1.l2{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{pointer-events:auto;visibility:visible;opacity:1;transition:visibility 0s allow-discrete,background-color .3s ease-out,opacity .1s ease-out;background-color:oklch(0% 0 0/.4)}:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal) .modal-box{opacity:1;translate:0;scale:1}:root:has(:is(.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal)){--page-has-backdrop:1;--page-overflow:hidden;--page-scroll-bg:var(--page-scroll-bg-on);--page-scroll-gutter:stable;--page-scroll-transition:var(--page-scroll-transition-on);animation:forwards set-page-has-scroll;animation-timeline:scroll()}@starting-style{.modal.modal-open,.modal[open],.modal:target,.modal-toggle:checked+.modal{opacity:0}}.tooltip>.tooltip-content,.tooltip[data-tip]:before{transform:translateX(-50%)translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-off)50%}.tooltip:after{transform:translateX(-50%)translateY(var(--tt-pos,.25rem));inset:auto auto var(--tt-tail)50%}.btn:disabled:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.btn:disabled:not(.btn-link,.btn-ghost){box-shadow:none}.btn:disabled{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn:disabled{--btn-fg:color-mix(in oklch,var(--color-base-content)20%,#0000)}}.btn[disabled]:not(.btn-link,.btn-ghost){background-color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]:not(.btn-link,.btn-ghost){background-color:color-mix(in oklab,var(--color-base-content)10%,transparent)}}.btn[disabled]:not(.btn-link,.btn-ghost){box-shadow:none}.btn[disabled]{pointer-events:none;--btn-border:#0000;--btn-noise:none;--btn-fg:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.btn[disabled]{--btn-fg:color-mix(in oklch,var(--color-base-content)20%,#0000)}}@media (prefers-reduced-motion:no-preference){.collapse[open].collapse-arrow>.collapse-title:after,.collapse.collapse-open.collapse-arrow>.collapse-title:after{transform:translateY(-50%)rotate(225deg)}}.collapse.collapse-open.collapse-plus>.collapse-title:after{--tw-content:"−";content:var(--tw-content)}:is(.collapse[tabindex].collapse-arrow:focus:not(.collapse-close),.collapse.collapse-arrow[tabindex]:focus-within:not(.collapse-close))>.collapse-title:after,.collapse.collapse-arrow:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{transform:translateY(-50%)rotate(225deg)}.collapse[open].collapse-plus>.collapse-title:after,.collapse[tabindex].collapse-plus:focus:not(.collapse-close)>.collapse-title:after,.collapse.collapse-plus:not(.collapse-close)>input:is([type=checkbox],[type=radio]):checked~.collapse-title:after{--tw-content:"−";content:var(--tw-content)}.list .list-row:has(.list-col-grow:first-child){--list-grid-cols:1fr}.list .list-row:has(.list-col-grow:nth-child(2)){--list-grid-cols:minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(3)){--list-grid-cols:minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(4)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(5)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row:has(.list-col-grow:nth-child(6)){--list-grid-cols:minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)minmax(0,auto)1fr}.list .list-row>*{grid-row-start:1}.steps .step-neutral+.step-neutral:before,.steps .step-neutral:after,.steps .step-neutral>.step-icon{--step-bg:var(--color-neutral);--step-fg:var(--color-neutral-content)}.steps .step-primary+.step-primary:before,.steps .step-primary:after,.steps .step-primary>.step-icon{--step-bg:var(--color-primary);--step-fg:var(--color-primary-content)}.steps .step-secondary+.step-secondary:before,.steps .step-secondary:after,.steps .step-secondary>.step-icon{--step-bg:var(--color-secondary);--step-fg:var(--color-secondary-content)}.steps .step-accent+.step-accent:before,.steps .step-accent:after,.steps .step-accent>.step-icon{--step-bg:var(--color-accent);--step-fg:var(--color-accent-content)}.steps .step-info+.step-info:before,.steps .step-info:after,.steps .step-info>.step-icon{--step-bg:var(--color-info);--step-fg:var(--color-info-content)}.steps .step-success+.step-success:before,.steps .step-success:after,.steps .step-success>.step-icon{--step-bg:var(--color-success);--step-fg:var(--color-success-content)}.steps .step-warning+.step-warning:before,.steps .step-warning:after,.steps .step-warning>.step-icon{--step-bg:var(--color-warning);--step-fg:var(--color-warning-content)}.steps .step-error+.step-error:before,.steps .step-error:after,.steps .step-error>.step-icon{--step-bg:var(--color-error);--step-fg:var(--color-error-content)}.checkbox:disabled,.radio:disabled{cursor:not-allowed;opacity:.2}.rating.rating-xs :where(:not(.rating-hidden)){width:1rem;height:1rem}.rating.rating-sm :where(:not(.rating-hidden)){width:1.25rem;height:1.25rem}.rating.rating-md :where(:not(.rating-hidden)){width:1.5rem;height:1.5rem}.rating.rating-lg :where(:not(.rating-hidden)){width:1.75rem;height:1.75rem}.rating.rating-xl :where(:not(.rating-hidden)){width:2rem;height:2rem}:where(.navbar){position:relative}.dropdown-right{--anchor-h:right;--anchor-v:span-bottom}.dropdown-right .dropdown-content{transform-origin:0;inset-inline-start:100%;top:0;bottom:auto}.dropdown-left{--anchor-h:left;--anchor-v:span-bottom}.dropdown-left .dropdown-content{transform-origin:100%;inset-inline-end:100%;top:0;bottom:auto}.dropdown-end{--anchor-h:span-left}.dropdown-end :where(.dropdown-content){inset-inline-end:0;translate:0}[dir=rtl] :is(.dropdown-end :where(.dropdown-content)){translate:0}.dropdown-end.dropdown-left{--anchor-h:left;--anchor-v:span-top}.dropdown-end.dropdown-left .dropdown-content{top:auto;bottom:0}.dropdown-end.dropdown-right{--anchor-h:right;--anchor-v:span-top}.dropdown-end.dropdown-right .dropdown-content{top:auto;bottom:0}:is(.stack,.stack.stack-bottom)>*{grid-area:3/3/6/4}:is(.stack,.stack.stack-bottom)>:nth-child(2){grid-area:2/2/5/5}:is(.stack,.stack.stack-bottom)>:first-child{grid-area:1/1/4/6}.stack.stack-top>*{grid-area:1/3/4/4}.stack.stack-top>:nth-child(2){grid-area:2/2/5/5}.stack.stack-top>:first-child{grid-area:3/1/6/6}.stack.stack-start>*{grid-area:3/1/4/4}.stack.stack-start>:nth-child(2){grid-area:2/2/5/5}.stack.stack-start>:first-child{grid-area:1/3/6/6}.stack.stack-end>*{grid-area:3/3/4/6}.stack.stack-end>:nth-child(2){grid-area:2/2/5/5}.stack.stack-end>:first-child{grid-area:1/1/6/4}.input-sm{--size:calc(var(--size-field,.25rem)*8);font-size:max(var(--font-size,.75rem),.75rem)}.input-sm[type=number]::-webkit-inner-spin-button{margin-block:-.5rem;margin-inline-end:-.75rem}.avatar-placeholder>div{justify-content:center;align-items:center;display:flex}.btn-circle{width:var(--size);height:var(--size);border-radius:3.40282e38px;padding-inline:0}.btn-block{width:100%}.badge-ghost{border-color:var(--color-base-200);background-color:var(--color-base-200);color:var(--color-base-content);background-image:none}.badge-soft{color:var(--badge-color,var(--color-base-content));background-color:var(--badge-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.badge-soft{background-color:color-mix(in oklab,var(--badge-color,var(--color-base-content))8%,var(--color-base-100))}}.badge-soft{border-color:var(--badge-color,var(--color-base-content))}@supports (color:color-mix(in lab, red, red)){.badge-soft{border-color:color-mix(in oklab,var(--badge-color,var(--color-base-content))10%,var(--color-base-100))}}.badge-soft{background-image:none}.badge-outline{color:var(--badge-color);--badge-bg:#0000;background-image:none;border-color:currentColor}.table-zebra tbody tr:where(:nth-child(2n)),.table-zebra tbody tr:where(:nth-child(2n)) :where(.table-pin-cols tr th){background-color:var(--color-base-200)}@media (hover:hover){:is(.table-zebra tbody tr.row-hover,.table-zebra tbody tr.row-hover:where(:nth-child(2n))):hover{background-color:var(--color-base-300)}}.checkbox-sm{--size:calc(var(--size-selector,.25rem)*5);padding:.1875rem}.badge-lg{--size:calc(var(--size-selector,.25rem)*7);font-size:1rem}.badge-md{--size:calc(var(--size-selector,.25rem)*6);font-size:.875rem}.badge-sm{--size:calc(var(--size-selector,.25rem)*5);font-size:.75rem}.badge-xs{--size:calc(var(--size-selector,.25rem)*4);font-size:.625rem}.alert-error{color:var(--color-error-content);--alert-border-color:var(--color-error);--alert-color:var(--color-error)}.alert-info{color:var(--color-info-content);--alert-border-color:var(--color-info);--alert-color:var(--color-info)}.alert-success{color:var(--color-success-content);--alert-border-color:var(--color-success);--alert-color:var(--color-success)}.alert-warning{color:var(--color-warning-content);--alert-border-color:var(--color-warning);--alert-color:var(--color-warning)}.link-primary{color:var(--color-primary)}@media (hover:hover){.link-primary:hover{color:var(--color-primary)}@supports (color:color-mix(in lab, red, red)){.link-primary:hover{color:color-mix(in oklab,var(--color-primary)80%,#000)}}}.progress-error{color:var(--color-error)}.progress-success{color:var(--color-success)}.progress-warning{color:var(--color-warning)}.btn-lg{--fontsize:1.125rem;--btn-p:1.25rem;--size:calc(var(--size-field,.25rem)*12)}.btn-sm{--fontsize:.75rem;--btn-p:.75rem;--size:calc(var(--size-field,.25rem)*8)}.btn-xs{--fontsize:.6875rem;--btn-p:.5rem;--size:calc(var(--size-field,.25rem)*6)}.card-lg .card-body{--card-p:2rem;--card-fs:1rem}.card-lg .card-title{--cardtitle-fs:1.25rem}.card-sm .card-body{--card-p:1rem;--card-fs:.75rem}.card-sm .card-title{--cardtitle-fs:1rem}.badge-accent{--badge-color:var(--color-accent);--badge-fg:var(--color-accent-content)}.badge-info{--badge-color:var(--color-info);--badge-fg:var(--color-info-content)}.badge-primary{--badge-color:var(--color-primary);--badge-fg:var(--color-primary-content)}.badge-secondary{--badge-color:var(--color-secondary);--badge-fg:var(--color-secondary-content)}.badge-success{--badge-color:var(--color-success);--badge-fg:var(--color-success-content)}.badge-warning{--badge-color:var(--color-warning);--badge-fg:var(--color-warning-content)}.card-border{border:var(--border)solid var(--color-base-200)}}.pointer-events-none{pointer-events:none}.collapse:not(td,tr,colgroup){visibility:revert-layer}.validator:user-invalid~.validator-hint{display:revert-layer}.validator:has(:user-invalid)~.validator-hint{display:revert-layer}:is(.validator[aria-invalid]:not([aria-invalid=false]),.validator:has([aria-invalid]:not([aria-invalid=false])))~.validator-hint{display:revert-layer}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.top-1{top:calc(var(--spacing)*1)}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing)*2)}.top-full{top:100%}.right-2{right:calc(var(--spacing)*2)}.right-full{right:100%}.bottom-0{bottom:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.join{--join-ss:0;--join-se:0;--join-es:0;--join-ee:0;align-items:stretch;display:inline-flex}.join :where(.join-item){border-start-start-radius:var(--join-ss,0);border-start-end-radius:var(--join-se,0);border-end-end-radius:var(--join-ee,0);border-end-start-radius:var(--join-es,0)}.join :where(.join-item) *{--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>.join-item:where(:first-child),.join :first-child:not(:last-child) :where(.join-item){--join-ss:var(--radius-field);--join-se:0;--join-es:var(--radius-field);--join-ee:0}.join>.join-item:where(:last-child),.join :last-child:not(:first-child) :where(.join-item){--join-ss:0;--join-se:var(--radius-field);--join-es:0;--join-ee:var(--radius-field)}.join>.join-item:where(:only-child),.join :only-child :where(.join-item){--join-ss:var(--radius-field);--join-se:var(--radius-field);--join-es:var(--radius-field);--join-ee:var(--radius-field)}.join>:where(:focus,:has(:focus)){z-index:1}@media (hover:hover){.join>:where(.btn:hover,:has(.btn:hover)){isolation:isolate}}.z-8{z-index:8}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing)*0)}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-auto{margin-inline:auto}.my-2{margin-block:calc(var(--spacing)*2)}.my-4{margin-block:calc(var(--spacing)*4)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-12{margin-top:calc(var(--spacing)*12)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.ml-7{margin-left:calc(var(--spacing)*7)}.ml-auto{margin-left:auto}.kbd{box-shadow:none}.alert{border-width:var(--border);border-color:var(--alert-border-color,var(--color-base-200))}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}:root .prose{--tw-prose-body:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-body:color-mix(in oklab,var(--color-base-content)80%,#0000)}}:root .prose{--tw-prose-headings:var(--color-base-content);--tw-prose-lead:var(--color-base-content);--tw-prose-links:var(--color-base-content);--tw-prose-bold:var(--color-base-content);--tw-prose-counters:var(--color-base-content);--tw-prose-bullets:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-bullets:color-mix(in oklab,var(--color-base-content)50%,#0000)}}:root .prose{--tw-prose-hr:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-hr:color-mix(in oklab,var(--color-base-content)20%,#0000)}}:root .prose{--tw-prose-quotes:var(--color-base-content);--tw-prose-quote-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-quote-borders:color-mix(in oklab,var(--color-base-content)20%,#0000)}}:root .prose{--tw-prose-captions:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-captions:color-mix(in oklab,var(--color-base-content)50%,#0000)}}:root .prose{--tw-prose-code:var(--color-base-content);--tw-prose-pre-code:var(--color-neutral-content);--tw-prose-pre-bg:var(--color-neutral);--tw-prose-th-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-th-borders:color-mix(in oklab,var(--color-base-content)50%,#0000)}}:root .prose{--tw-prose-td-borders:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-td-borders:color-mix(in oklab,var(--color-base-content)20%,#0000)}}:root .prose{--tw-prose-kbd:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){:root .prose{--tw-prose-kbd:color-mix(in oklab,var(--color-base-content)80%,#0000)}}:root .prose :where(code):not(pre>code){background-color:var(--color-base-200);border-radius:var(--radius-selector);border:var(--border)solid var(--color-base-300);font-weight:inherit;padding-block:.2em;padding-inline:.5em}:root .prose :where(code):not(pre>code):before,:root .prose :where(code):not(pre>code):after{display:none}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.size-3{width:calc(var(--spacing)*3);height:calc(var(--spacing)*3)}.size-3\.5{width:calc(var(--spacing)*3.5);height:calc(var(--spacing)*3.5)}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.size-5{width:calc(var(--spacing)*5);height:calc(var(--spacing)*5)}.size-6{width:calc(var(--spacing)*6);height:calc(var(--spacing)*6)}.size-8{width:calc(var(--spacing)*8);height:calc(var(--spacing)*8)}.size-12{width:calc(var(--spacing)*12);height:calc(var(--spacing)*12)}.size-16{width:calc(var(--spacing)*16);height:calc(var(--spacing)*16)}.size-20{width:calc(var(--spacing)*20);height:calc(var(--spacing)*20)}.size-24{width:calc(var(--spacing)*24);height:calc(var(--spacing)*24)}.size-\[1\.1rem\]{width:1.1rem;height:1.1rem}.h-7{height:calc(var(--spacing)*7)}.h-9{height:calc(var(--spacing)*9)}.h-16{height:calc(var(--spacing)*16)}.max-h-75{max-height:calc(var(--spacing)*75)}.min-h-60{min-height:calc(var(--spacing)*60)}.min-h-\[60vh\]{min-height:60vh}.min-h-\[calc\(100vh-4rem\)\]{min-height:calc(100vh - 4rem)}.w-0{width:calc(var(--spacing)*0)}.w-7{width:calc(var(--spacing)*7)}.w-12{width:calc(var(--spacing)*12)}.w-20{width:calc(var(--spacing)*20)}.w-40{width:calc(var(--spacing)*40)}.w-52{width:calc(var(--spacing)*52)}.w-62{width:calc(var(--spacing)*62)}.w-full{width:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-40{max-width:calc(var(--spacing)*40)}.max-w-\[200px\]{max-width:200px}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-none{max-width:none}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-\[150px\]{min-width:150px}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.flex-col{flex-direction:column}.flex-row{flex-direction:row}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-12>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*12)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*12)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-y-2{row-gap:calc(var(--spacing)*2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-box{border-radius:var(--radius-box);border-radius:var(--radius-box)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-3{border-left-style:var(--tw-border-style);border-left-width:3px}.border-base-300{border-color:var(--color-base-300)}.border-error{border-color:var(--color-error)}.bg-base-100{background-color:var(--color-base-100)}.bg-base-200{background-color:var(--color-base-200)}.bg-base-300{background-color:var(--color-base-300)}.bg-black{background-color:var(--color-black)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-neutral{background-color:var(--color-neutral)}.bg-primary{background-color:var(--color-primary)}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-primary{--tw-gradient-from:var(--color-primary);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.fill-amber-400{fill:var(--color-amber-400)}.fill-none{fill:none}.stroke-amber-400{stroke:var(--color-amber-400)}.object-cover{object-fit:cover}.p-0{padding:calc(var(--spacing)*0)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.py-2{padding-block:calc(var(--spacing)*2)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pb-24{padding-bottom:calc(var(--spacing)*24)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-amber-400{color:var(--color-amber-400)}.text-base-content,.text-base-content\/50{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/50{color:color-mix(in oklab,var(--color-base-content)50%,transparent)}}.text-base-content\/60{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/60{color:color-mix(in oklab,var(--color-base-content)60%,transparent)}}.text-base-content\/70{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/70{color:color-mix(in oklab,var(--color-base-content)70%,transparent)}}.text-base-content\/80{color:var(--color-base-content)}@supports (color:color-mix(in lab, red, red)){.text-base-content\/80{color:color-mix(in oklab,var(--color-base-content)80%,transparent)}}.text-error{color:var(--color-error)}.text-gray-500{color:var(--color-gray-500)}.text-neutral{color:var(--color-neutral)}.text-neutral-content{color:var(--color-neutral-content)}.text-primary{color:var(--color-primary)}.text-primary-content{color:var(--color-primary-content)}.text-success{color:var(--color-success)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.prose :where(.btn-link):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}@layer daisyui.l1{.btn-link{--btn-border:#0000;--btn-bg:#0000;--btn-noise:none;--btn-shadow:"";outline-color:currentColor;text-decoration-line:underline}.btn-link:not(.btn-disabled,.btn:disabled,.btn[disabled]){--btn-fg:var(--btn-color,var(--color-primary))}.btn-link:is(.btn-active,:hover,:active:focus,:focus-visible){--btn-border:#0000;--btn-bg:#0000}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)){--btn-shadow:"";--btn-bg:#0000;--btn-border:#0000;--btn-noise:none}.btn-ghost:not(.btn-active,:hover,:active:focus,:focus-visible,input:checked:not(.filter .btn)):not(:disabled,[disabled],.btn-disabled){--btn-fg:var(--btn-color,currentColor);outline-color:currentColor}@media (hover:none){.btn-ghost:not(.btn-active,:active,:focus-visible,input:checked:not(.filter .btn)):hover{--btn-shadow:"";--btn-bg:#0000;--btn-fg:var(--btn-color,currentColor);--btn-border:#0000;--btn-noise:none;outline-color:currentColor}}}.no-underline,.prose :where(.btn-link):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);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,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a))drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a)drop-shadow(0 1px 1px #0000000f);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,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.\[rows\:\%v\]{rows:%v}@media (hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:48rem){.md\:w-\[calc\(50\%-0\.75rem\)\]{width:calc(50% - .75rem)}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}}@media (min-width:64rem){.lg\:w-\[calc\(33\.333\%-1rem\)\]{width:calc(33.333% - 1rem)}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-\[3fr_2fr\]{grid-template-columns:3fr 2fr}}@media (min-width:80rem){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}}:root{--shadow-card-hover:0 8px 25px oklch(75% .12 175/.15),0 4px 12px #0000001a}[data-theme=dark]{--shadow-card-hover:0 8px 25px oklch(78% .12 175/.1),0 4px 12px #0003}@keyframes rating{0%,40%{filter:brightness(1.05)contrast(1.05);scale:1.1}}@keyframes dropdown{0%{opacity:0}}@keyframes radio{0%{padding:5px}50%{padding:3px}}@keyframes toast{0%{opacity:0;scale:.9}to{opacity:1;scale:1}}@keyframes rotator{89.9999%,to{--first-item-position:0 0%}90%,99.9999%{--first-item-position:0 calc(var(--items)*100%)}to{translate:0 -100%}}@keyframes skeleton{0%{background-position:150%}to{background-position:-50%}}@keyframes menu{0%{opacity:0}}@keyframes progress{50%{background-position-x:-115%}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
+139
pkg/appview/public/js/bundle.min.js
··· 1 + var qe=(function(){"use strict";let htmx={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){return getInputValues(e,t||"post").values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:!0,historyCacheSize:10,refreshOnHistoryMiss:!1,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:!0,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:!0,allowScriptTags:!0,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:!1,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:!1,getCacheBusterParam:!1,globalViewTransitions:!1,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:!0,ignoreTitle:!1,scrollIntoViewOnBoost:!0,triggerSpecsCache:null,disableInheritance:!1,responseHandling:[{code:"204",swap:!1},{code:"[23]..",swap:!0},{code:"[45]..",swap:!1,error:!0}],allowNestedOobSwaps:!0,historyRestoreAsHxRequest:!0,reportValidityOfForms:!1},parseInterval:null,location,_:null,version:"2.0.8"};htmx.onLoad=onLoadHelper,htmx.process=processNode,htmx.on=addEventListenerImpl,htmx.off=removeEventListenerImpl,htmx.trigger=triggerEvent,htmx.ajax=ajaxHelper,htmx.find=find,htmx.findAll=findAll,htmx.closest=closest,htmx.remove=removeElement,htmx.addClass=addClassToElement,htmx.removeClass=removeClassFromElement,htmx.toggleClass=toggleClassOnElement,htmx.takeClass=takeClassForElement,htmx.swap=swap,htmx.defineExtension=defineExtension,htmx.removeExtension=removeExtension,htmx.logAll=logAll,htmx.logNone=logNone,htmx.parseInterval=parseInterval,htmx._=internalEval;let internalAPI={addTriggerHandler,bodyContains,canAccessLocalStorage,findThisElement,filterValues,swap,hasAttribute,getAttributeValue,getClosestAttributeValue,getClosestMatch,getExpressionVars,getHeaders,getInputValues,getInternalData,getSwapSpecification,getTriggerSpecs,getTarget,makeFragment,mergeObjects,makeSettleInfo,oobSwap,querySelectorExt,settleImmediately,shouldCancel,triggerEvent,triggerErrorEvent,withExtensions},VERBS=["get","post","put","delete","patch"],VERB_SELECTOR=VERBS.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function parseInterval(e){if(e==null)return;let t=NaN;return e.slice(-2)=="ms"?t=parseFloat(e.slice(0,-2)):e.slice(-1)=="s"?t=parseFloat(e.slice(0,-1))*1e3:e.slice(-1)=="m"?t=parseFloat(e.slice(0,-1))*1e3*60:t=parseFloat(e),isNaN(t)?void 0:t}function getRawAttribute(e,t){return e instanceof Element&&e.getAttribute(t)}function hasAttribute(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function getAttributeValue(e,t){return getRawAttribute(e,t)||getRawAttribute(e,"data-"+t)}function parentElt(e){let t=e.parentElement;return!t&&e.parentNode instanceof ShadowRoot?e.parentNode:t}function getDocument(){return document}function getRootNode(e,t){return e.getRootNode?e.getRootNode({composed:t}):getDocument()}function getClosestMatch(e,t){for(;e&&!t(e);)e=parentElt(e);return e||null}function getAttributeValueWithDisinheritance(e,t,r){let a=getAttributeValue(t,r),o=getAttributeValue(t,"hx-disinherit");var s=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return s&&(s==="*"||s.split(" ").indexOf(r)>=0)?a:null;if(o&&(o==="*"||o.split(" ").indexOf(r)>=0))return"unset"}return a}function getClosestAttributeValue(e,t){let r=null;if(getClosestMatch(e,function(a){return!!(r=getAttributeValueWithDisinheritance(e,asElement(a),t))}),r!=="unset")return r}function matches(e,t){return e instanceof Element&&e.matches(t)}function getStartTag(e){let r=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return r?r[1].toLowerCase():""}function parseHTML(e){return"parseHTMLUnsafe"in Document?Document.parseHTMLUnsafe(e):new DOMParser().parseFromString(e,"text/html")}function takeChildrenFor(e,t){for(;t.childNodes.length>0;)e.append(t.childNodes[0])}function duplicateScript(e){let t=getDocument().createElement("script");return forEach(e.attributes,function(r){t.setAttribute(r.name,r.value)}),t.textContent=e.textContent,t.async=!1,htmx.config.inlineScriptNonce&&(t.nonce=htmx.config.inlineScriptNonce),t}function isJavaScriptScriptNode(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function normalizeScriptTags(e){Array.from(e.querySelectorAll("script")).forEach(t=>{if(isJavaScriptScriptNode(t)){let r=duplicateScript(t),a=t.parentNode;try{a.insertBefore(r,t)}catch(o){logError(o)}finally{t.remove()}}})}function makeFragment(e){let t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,""),r=getStartTag(t),a;if(r==="html"){a=new DocumentFragment;let s=parseHTML(e);takeChildrenFor(a,s.body),a.title=s.title}else if(r==="body"){a=new DocumentFragment;let s=parseHTML(t);takeChildrenFor(a,s.body),a.title=s.title}else{let s=parseHTML('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");a=s.querySelector("template").content,a.title=s.title;var o=a.querySelector("title");o&&o.parentNode===a&&(o.remove(),a.title=o.innerText)}return a&&(htmx.config.allowScriptTags?normalizeScriptTags(a):a.querySelectorAll("script").forEach(s=>s.remove())),a}function maybeCall(e){e&&e()}function isType(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function isFunction(e){return typeof e=="function"}function isRawObject(e){return isType(e,"Object")}function getInternalData(e){let t="htmx-internal-data",r=e[t];return r||(r=e[t]={}),r}function toArray(e){let t=[];if(e)for(let r=0;r<e.length;r++)t.push(e[r]);return t}function forEach(e,t){if(e)for(let r=0;r<e.length;r++)t(e[r])}function isScrolledIntoView(e){let t=e.getBoundingClientRect(),r=t.top,a=t.bottom;return r<window.innerHeight&&a>=0}function bodyContains(e){return e.getRootNode({composed:!0})===document}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let r in t)t.hasOwnProperty(r)&&(e[r]=t[r]);return e}function parseJSON(e){try{return JSON.parse(e)}catch(t){return logError(t),null}}function canAccessLocalStorage(){let e="htmx:sessionStorageTest";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch{return!1}}function normalizePath(e){let t=new URL(e,"http://x");return t&&(e=t.pathname+t.search),e!="/"&&(e=e.replace(/\/+$/,"")),e}function internalEval(str){return maybeEval(getDocument().body,function(){return eval(str)})}function onLoadHelper(e){return htmx.on("htmx:load",function(r){e(r.detail.elt)})}function logAll(){htmx.logger=function(e,t,r){console&&console.log(t,e,r)}}function logNone(){htmx.logger=null}function find(e,t){return typeof e!="string"?e.querySelector(t):find(getDocument(),e)}function findAll(e,t){return typeof e!="string"?e.querySelectorAll(t):findAll(getDocument(),e)}function getWindow(){return window}function removeElement(e,t){e=resolveTarget(e),t?getWindow().setTimeout(function(){removeElement(e),e=null},t):parentElt(e).removeChild(e)}function asElement(e){return e instanceof Element?e:null}function asHtmlElement(e){return e instanceof HTMLElement?e:null}function asString(e){return typeof e=="string"?e:null}function asParentNode(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function addClassToElement(e,t,r){e=asElement(resolveTarget(e)),e&&(r?getWindow().setTimeout(function(){addClassToElement(e,t),e=null},r):e.classList&&e.classList.add(t))}function removeClassFromElement(e,t,r){let a=asElement(resolveTarget(e));a&&(r?getWindow().setTimeout(function(){removeClassFromElement(a,t),a=null},r):a.classList&&(a.classList.remove(t),a.classList.length===0&&a.removeAttribute("class")))}function toggleClassOnElement(e,t){e=resolveTarget(e),e.classList.toggle(t)}function takeClassForElement(e,t){e=resolveTarget(e),forEach(e.parentElement.children,function(r){removeClassFromElement(r,t)}),addClassToElement(asElement(e),t)}function closest(e,t){return e=asElement(resolveTarget(e)),e?e.closest(t):null}function startsWith(e,t){return e.substring(0,t.length)===t}function endsWith(e,t){return e.substring(e.length-t.length)===t}function normalizeSelector(e){let t=e.trim();return startsWith(t,"<")&&endsWith(t,"/>")?t.substring(1,t.length-2):t}function querySelectorAllExt(e,t,r){if(t.indexOf("global ")===0)return querySelectorAllExt(e,t.slice(7),!0);e=resolveTarget(e);let a=[];{let l=0,f=0;for(let n=0;n<t.length;n++){let u=t[n];if(u===","&&l===0){a.push(t.substring(f,n)),f=n+1;continue}u==="<"?l++:u==="/"&&n<t.length-1&&t[n+1]===">"&&l--}f<t.length&&a.push(t.substring(f))}let o=[],s=[];for(;a.length>0;){let l=normalizeSelector(a.shift()),f;l.indexOf("closest ")===0?f=closest(asElement(e),normalizeSelector(l.slice(8))):l.indexOf("find ")===0?f=find(asParentNode(e),normalizeSelector(l.slice(5))):l==="next"||l==="nextElementSibling"?f=asElement(e).nextElementSibling:l.indexOf("next ")===0?f=scanForwardQuery(e,normalizeSelector(l.slice(5)),!!r):l==="previous"||l==="previousElementSibling"?f=asElement(e).previousElementSibling:l.indexOf("previous ")===0?f=scanBackwardsQuery(e,normalizeSelector(l.slice(9)),!!r):l==="document"?f=document:l==="window"?f=window:l==="body"?f=document.body:l==="root"?f=getRootNode(e,!!r):l==="host"?f=e.getRootNode().host:s.push(l),f&&o.push(f)}if(s.length>0){let l=s.join(","),f=asParentNode(getRootNode(e,!!r));o.push(...toArray(f.querySelectorAll(l)))}return o}var scanForwardQuery=function(e,t,r){let a=asParentNode(getRootNode(e,r)).querySelectorAll(t);for(let o=0;o<a.length;o++){let s=a[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return s}},scanBackwardsQuery=function(e,t,r){let a=asParentNode(getRootNode(e,r)).querySelectorAll(t);for(let o=a.length-1;o>=0;o--){let s=a[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING)return s}};function querySelectorExt(e,t){return typeof e!="string"?querySelectorAllExt(e,t)[0]:querySelectorAllExt(getDocument().body,e)[0]}function resolveTarget(e,t){return typeof e=="string"?find(asParentNode(t)||document,e):e}function processEventArgs(e,t,r,a){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t,options:r}:{target:resolveTarget(e),event:asString(t),listener:r,options:a}}function addEventListenerImpl(e,t,r,a){return ready(function(){let s=processEventArgs(e,t,r,a);s.target.addEventListener(s.event,s.listener,s.options)}),isFunction(t)?t:r}function removeEventListenerImpl(e,t,r){return ready(function(){let a=processEventArgs(e,t,r);a.target.removeEventListener(a.event,a.listener)}),isFunction(t)?t:r}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let r=getClosestAttributeValue(e,t);if(r){if(r==="this")return[findThisElement(e,t)];{let a=querySelectorAllExt(e,r);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(r)){let s=asElement(getClosestMatch(e,function(l){return l!==e&&hasAttribute(asElement(l),t)}));s&&a.push(...findAttributeTargets(s,t))}return a.length===0?(logError('The selector "'+r+'" on '+t+" returned no matches!"),[DUMMY_ELT]):a}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(r){return getAttributeValue(asElement(r),t)!=null}))}function getTarget(e){let t=getClosestAttributeValue(e,"hx-target");return t?t==="this"?findThisElement(e,"hx-target"):querySelectorExt(e,t):getInternalData(e).boosted?getDocument().body:e}function shouldSettleAttribute(e){return htmx.config.attributesToSettle.includes(e)}function cloneAttributes(e,t){forEach(Array.from(e.attributes),function(r){!t.hasAttribute(r.name)&&shouldSettleAttribute(r.name)&&e.removeAttribute(r.name)}),forEach(t.attributes,function(r){shouldSettleAttribute(r.name)&&e.setAttribute(r.name,r.value)})}function isInlineSwap(e,t){let r=getExtensions(t);for(let a=0;a<r.length;a++){let o=r[a];try{if(o.isInlineSwap(e))return!0}catch(s){logError(s)}}return e==="outerHTML"}function oobSwap(e,t,r,a){a=a||getDocument();let o="#"+CSS.escape(getRawAttribute(t,"id")),s="outerHTML";e==="true"||(e.indexOf(":")>0?(s=e.substring(0,e.indexOf(":")),o=e.substring(e.indexOf(":")+1)):s=e),t.removeAttribute("hx-swap-oob"),t.removeAttribute("data-hx-swap-oob");let l=querySelectorAllExt(a,o,!1);return l.length?(forEach(l,function(f){let n,u=t.cloneNode(!0);n=getDocument().createDocumentFragment(),n.appendChild(u),isInlineSwap(s,f)||(n=asParentNode(u));let m={shouldSwap:!0,target:f,fragment:n};triggerEvent(f,"htmx:oobBeforeSwap",m)&&(f=m.target,m.shouldSwap&&(handlePreservedElements(n),swapWithStyle(s,f,f,n,r),restorePreservedElements()),forEach(r.elts,function(i){triggerEvent(i,"htmx:oobAfterSwap",m)}))}),t.parentNode.removeChild(t)):(t.parentNode.removeChild(t),triggerErrorEvent(getDocument().body,"htmx:oobErrorNoTarget",{content:t})),e}function restorePreservedElements(){let e=find("#--htmx-preserve-pantry--");if(e){for(let t of[...e.children]){let r=find("#"+t.id);r.parentNode.moveBefore(t,r),r.remove()}e.remove()}}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let r=getAttributeValue(t,"id"),a=getDocument().getElementById(r);if(a!=null)if(t.moveBefore){let o=find("#--htmx-preserve-pantry--");o==null&&(getDocument().body.insertAdjacentHTML("afterend","<div id='--htmx-preserve-pantry--'></div>"),o=find("#--htmx-preserve-pantry--")),o.moveBefore(a,null)}else t.parentNode.replaceChild(a,t)})}function handleAttributes(e,t,r){forEach(t.querySelectorAll("[id]"),function(a){let o=getRawAttribute(a,"id");if(o&&o.length>0){let s=o.replace("'","\\'"),l=a.tagName.replace(":","\\:"),f=asParentNode(e),n=f&&f.querySelector(l+"[id='"+s+"']");if(n&&n!==f){let u=a.cloneNode();cloneAttributes(a,n),r.tasks.push(function(){cloneAttributes(a,u)})}}})}function makeAjaxLoadTask(e){return function(){removeClassFromElement(e,htmx.config.addedClass),processNode(asElement(e)),processFocus(asParentNode(e)),triggerEvent(e,"htmx:load")}}function processFocus(e){let t="[autofocus]",r=asHtmlElement(matches(e,t)?e:e.querySelector(t));r?.focus()}function insertNodesBefore(e,t,r,a){for(handleAttributes(e,r,a);r.childNodes.length>0;){let o=r.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&a.tasks.push(makeAjaxLoadTask(o))}}function stringHash(e,t){let r=0;for(;r<e.length;)t=(t<<5)-t+e.charCodeAt(r++)|0;return t}function attributeHash(e){let t=0;for(let r=0;r<e.attributes.length;r++){let a=e.attributes[r];a.value&&(t=stringHash(a.name,t),t=stringHash(a.value,t))}return t}function deInitOnHandlers(e){let t=getInternalData(e);if(t.onHandlers){for(let r=0;r<t.onHandlers.length;r++){let a=t.onHandlers[r];removeEventListenerImpl(e,a.event,a.listener)}delete t.onHandlers}}function deInitNode(e){let t=getInternalData(e);t.timeout&&clearTimeout(t.timeout),t.listenerInfos&&forEach(t.listenerInfos,function(r){r.on&&removeEventListenerImpl(r.on,r.trigger,r.listener)}),deInitOnHandlers(e),forEach(Object.keys(t),function(r){r!=="firstInitCompleted"&&delete t[r]})}function cleanUpElement(e){triggerEvent(e,"htmx:beforeCleanupElement"),deInitNode(e),forEach(e.children,function(t){cleanUpElement(t)})}function swapOuterHTML(e,t,r){if(e.tagName==="BODY")return swapInnerHTML(e,t,r);let a,o=e.previousSibling,s=parentElt(e);if(s){for(insertNodesBefore(s,e,t,r),o==null?a=s.firstChild:a=o.nextSibling,r.elts=r.elts.filter(function(l){return l!==e});a&&a!==e;)a instanceof Element&&r.elts.push(a),a=a.nextSibling;cleanUpElement(e),e.remove()}}function swapAfterBegin(e,t,r){return insertNodesBefore(e,e.firstChild,t,r)}function swapBeforeBegin(e,t,r){return insertNodesBefore(parentElt(e),e,t,r)}function swapBeforeEnd(e,t,r){return insertNodesBefore(e,null,t,r)}function swapAfterEnd(e,t,r){return insertNodesBefore(parentElt(e),e.nextSibling,t,r)}function swapDelete(e){cleanUpElement(e);let t=parentElt(e);if(t)return t.removeChild(e)}function swapInnerHTML(e,t,r){let a=e.firstChild;if(insertNodesBefore(e,a,t,r),a){for(;a.nextSibling;)cleanUpElement(a.nextSibling),e.removeChild(a.nextSibling);cleanUpElement(a),e.removeChild(a)}}function swapWithStyle(e,t,r,a,o){switch(e){case"none":return;case"outerHTML":swapOuterHTML(r,a,o);return;case"afterbegin":swapAfterBegin(r,a,o);return;case"beforebegin":swapBeforeBegin(r,a,o);return;case"beforeend":swapBeforeEnd(r,a,o);return;case"afterend":swapAfterEnd(r,a,o);return;case"delete":swapDelete(r);return;default:var s=getExtensions(t);for(let l=0;l<s.length;l++){let f=s[l];try{let n=f.handleSwap(e,r,a,o);if(n){if(Array.isArray(n))for(let u=0;u<n.length;u++){let m=n[u];m.nodeType!==Node.TEXT_NODE&&m.nodeType!==Node.COMMENT_NODE&&o.tasks.push(makeAjaxLoadTask(m))}return}}catch(n){logError(n)}}e==="innerHTML"?swapInnerHTML(r,a,o):swapWithStyle(htmx.config.defaultSwapStyle,t,r,a,o)}}function findAndSwapOobElements(e,t,r){var a=findAll(e,"[hx-swap-oob], [data-hx-swap-oob]");return forEach(a,function(o){if(htmx.config.allowNestedOobSwaps||o.parentElement===null){let s=getAttributeValue(o,"hx-swap-oob");s!=null&&oobSwap(s,o,t,r)}else o.removeAttribute("hx-swap-oob"),o.removeAttribute("data-hx-swap-oob")}),a.length>0}function swap(e,t,r,a){a||(a={});let o=null,s=null,l=function(){maybeCall(a.beforeSwapCallback),e=resolveTarget(e);let u=a.contextElement?getRootNode(a.contextElement,!1):getDocument(),m=document.activeElement,i={};i={elt:m,start:m?m.selectionStart:null,end:m?m.selectionEnd:null};let c=makeSettleInfo(e);if(r.swapStyle==="textContent")e.textContent=t;else{let d=makeFragment(t);if(c.title=a.title||d.title,a.historyRequest&&(d=d.querySelector("[hx-history-elt],[data-hx-history-elt]")||d),a.selectOOB){let p=a.selectOOB.split(",");for(let g=0;g<p.length;g++){let E=p[g].split(":",2),b=E[0].trim();b.indexOf("#")===0&&(b=b.substring(1));let S=E[1]||"true",C=d.querySelector("#"+b);C&&oobSwap(S,C,c,u)}}if(findAndSwapOobElements(d,c,u),forEach(findAll(d,"template"),function(p){p.content&&findAndSwapOobElements(p.content,c,u)&&p.remove()}),a.select){let p=getDocument().createDocumentFragment();forEach(d.querySelectorAll(a.select),function(g){p.appendChild(g)}),d=p}handlePreservedElements(d),swapWithStyle(r.swapStyle,a.contextElement,e,d,c),restorePreservedElements()}if(i.elt&&!bodyContains(i.elt)&&getRawAttribute(i.elt,"id")){let d=document.getElementById(getRawAttribute(i.elt,"id")),p={preventScroll:r.focusScroll!==void 0?!r.focusScroll:!htmx.config.defaultFocusScroll};if(d){if(i.start&&d.setSelectionRange)try{d.setSelectionRange(i.start,i.end)}catch{}d.focus(p)}}e.classList.remove(htmx.config.swappingClass),forEach(c.elts,function(d){d.classList&&d.classList.add(htmx.config.settlingClass),triggerEvent(d,"htmx:afterSwap",a.eventInfo)}),maybeCall(a.afterSwapCallback),r.ignoreTitle||handleTitle(c.title);let x=function(){if(forEach(c.tasks,function(d){d.call()}),forEach(c.elts,function(d){d.classList&&d.classList.remove(htmx.config.settlingClass),triggerEvent(d,"htmx:afterSettle",a.eventInfo)}),a.anchor){let d=asElement(resolveTarget("#"+a.anchor));d&&d.scrollIntoView({block:"start",behavior:"auto"})}updateScrollState(c.elts,r),maybeCall(a.afterSettleCallback),maybeCall(o)};r.settleDelay>0?getWindow().setTimeout(x,r.settleDelay):x()},f=htmx.config.globalViewTransitions;r.hasOwnProperty("transition")&&(f=r.transition);let n=a.contextElement||getDocument();if(f&&triggerEvent(n,"htmx:beforeTransition",a.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let u=new Promise(function(i,c){o=i,s=c}),m=l;l=function(){document.startViewTransition(function(){return m(),u})}}try{r?.swapDelay&&r.swapDelay>0?getWindow().setTimeout(l,r.swapDelay):l()}catch(u){throw triggerErrorEvent(n,"htmx:swapError",a.eventInfo),maybeCall(s),u}}function handleTriggerHeader(e,t,r){let a=e.getResponseHeader(t);if(a.indexOf("{")===0){let o=parseJSON(a);for(let s in o)if(o.hasOwnProperty(s)){let l=o[s];isRawObject(l)?r=l.target!==void 0?l.target:r:l={value:l},triggerEvent(r,s,l)}}else{let o=a.split(",");for(let s=0;s<o.length;s++)triggerEvent(r,o[s].trim(),[])}}let WHITESPACE=/\s/,WHITESPACE_OR_COMMA=/[\s,]/,SYMBOL_START=/[_$a-zA-Z]/,SYMBOL_CONT=/[_$a-zA-Z0-9]/,STRINGISH_START=['"',"'","/"],NOT_WHITESPACE=/[^\s]/,COMBINED_SELECTOR_START=/[{(]/,COMBINED_SELECTOR_END=/[})]/;function tokenizeString(e){let t=[],r=0;for(;r<e.length;){if(SYMBOL_START.exec(e.charAt(r))){for(var a=r;SYMBOL_CONT.exec(e.charAt(r+1));)r++;t.push(e.substring(a,r+1))}else if(STRINGISH_START.indexOf(e.charAt(r))!==-1){let o=e.charAt(r);var a=r;for(r++;r<e.length&&e.charAt(r)!==o;)e.charAt(r)==="\\"&&r++,r++;t.push(e.substring(a,r+1))}else{let o=e.charAt(r);t.push(o)}r++}return t}function isPossibleRelativeReference(e,t,r){return SYMBOL_START.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function maybeGenerateConditional(e,t,r){if(t[0]==="["){t.shift();let a=1,o=" return (function("+r+"){ return (",s=null;for(;t.length>0;){let l=t[0];if(l==="]"){if(a--,a===0){s===null&&(o=o+"true"),t.shift(),o+=")})";try{let f=maybeEval(e,function(){return Function(o)()},function(){return!0});return f.source=o,f}catch(f){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:f,source:o}),null}}}else l==="["&&a++;isPossibleRelativeReference(l,s,r)?o+="(("+r+"."+l+") ? ("+r+"."+l+") : (window."+l+"))":o=o+l,s=t.shift()}}}function consumeUntil(e,t){let r="";for(;e.length>0&&!t.test(e[0]);)r+=e.shift();return r}function consumeCSSSelector(e){let t;return e.length>0&&COMBINED_SELECTOR_START.test(e[0])?(e.shift(),t=consumeUntil(e,COMBINED_SELECTOR_END).trim(),e.shift()):t=consumeUntil(e,WHITESPACE_OR_COMMA),t}let INPUT_SELECTOR="input, textarea, select";function parseAndCacheTrigger(e,t,r){let a=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let f=o.length,n=consumeUntil(o,/[,\[\s]/);if(n!=="")if(n==="every"){let u={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),u.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var s=maybeGenerateConditional(e,o,"event");s&&(u.eventFilter=s),a.push(u)}else{let u={trigger:n};var s=maybeGenerateConditional(e,o,"event");for(s&&(u.eventFilter=s),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let i=o.shift();if(i==="changed")u.changed=!0;else if(i==="once")u.once=!0;else if(i==="consume")u.consume=!0;else if(i==="delay"&&o[0]===":")o.shift(),u.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(i==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var l=consumeCSSSelector(o);else{var l=consumeUntil(o,WHITESPACE_OR_COMMA);if(l==="closest"||l==="find"||l==="next"||l==="previous"){o.shift();let x=consumeCSSSelector(o);x.length>0&&(l+=" "+x)}}u.from=l}else i==="target"&&o[0]===":"?(o.shift(),u.target=consumeCSSSelector(o)):i==="throttle"&&o[0]===":"?(o.shift(),u.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):i==="queue"&&o[0]===":"?(o.shift(),u.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):i==="root"&&o[0]===":"?(o.shift(),u[i]=consumeCSSSelector(o)):i==="threshold"&&o[0]===":"?(o.shift(),u[i]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}a.push(u)}o.length===f&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return r&&(r[t]=a),a}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),r=[];if(t){let a=htmx.config.triggerSpecsCache;r=a&&a[t]||parseAndCacheTrigger(e,t,a)}return r.length>0?r:matches(e,"form")?[{trigger:"submit"}]:matches(e,'input[type="button"], input[type="submit"]')?[{trigger:"click"}]:matches(e,INPUT_SELECTOR)?[{trigger:"change"}]:[{trigger:"click"}]}function cancelPolling(e){getInternalData(e).cancelled=!0}function processPolling(e,t,r){let a=getInternalData(e);a.timeout=getWindow().setTimeout(function(){bodyContains(e)&&a.cancelled!==!0&&(maybeFilterEvent(r,e,makeEvent("hx:poll:trigger",{triggerSpec:r,target:e}))||t(e),processPolling(e,t,r))},r.pollInterval)}function isLocalLink(e){return location.hostname===e.hostname&&getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")!==0}function eltIsDisabled(e){return closest(e,htmx.config.disableSelector)}function boostElement(e,t,r){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let a,o;if(e.tagName==="A")a="get",o=getRawAttribute(e,"href");else{let s=getRawAttribute(e,"method");a=s?s.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),a==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}r.forEach(function(s){addEventListener(e,function(l,f){let n=asElement(l);if(eltIsDisabled(n)){cleanUpElement(n);return}issueAjaxRequest(a,o,n,f)},t,s,!0)})}}function shouldCancel(e,t){if(e.type==="submit"&&t.tagName==="FORM")return!0;if(e.type==="click"){let r=t.closest('input[type="submit"], button');if(r&&r.form&&r.type==="submit")return!0;let a=t.closest("a"),o=/^#.+/;if(a&&a.href&&!o.test(a.getAttribute("href")))return!0}return!1}function ignoreBoostedAnchorCtrlClick(e,t){return getInternalData(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function maybeFilterEvent(e,t,r){let a=e.eventFilter;if(a)try{return a.call(t,r)!==!0}catch(o){let s=a.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:s}),!0}return!1}function addEventListener(e,t,r,a,o){let s=getInternalData(e),l;a.from?l=querySelectorAllExt(e,a.from):l=[e],a.changed&&("lastValue"in s||(s.lastValue=new WeakMap),l.forEach(function(f){s.lastValue.has(a)||s.lastValue.set(a,new WeakMap),s.lastValue.get(a).set(f,f.value)})),forEach(l,function(f){let n=function(u){if(!bodyContains(e)){f.removeEventListener(a.trigger,n);return}if(ignoreBoostedAnchorCtrlClick(e,u)||((o||shouldCancel(u,f))&&u.preventDefault(),maybeFilterEvent(a,e,u)))return;let m=getInternalData(u);if(m.triggerSpec=a,m.handledFor==null&&(m.handledFor=[]),m.handledFor.indexOf(e)<0){if(m.handledFor.push(e),a.consume&&u.stopPropagation(),a.target&&u.target&&!matches(asElement(u.target),a.target))return;if(a.once){if(s.triggeredOnce)return;s.triggeredOnce=!0}if(a.changed){let i=u.target,c=i.value,x=s.lastValue.get(a);if(x.has(i)&&x.get(i)===c)return;x.set(i,c)}if(s.delayed&&clearTimeout(s.delayed),s.throttle)return;a.throttle>0?s.throttle||(triggerEvent(e,"htmx:trigger"),t(e,u),s.throttle=getWindow().setTimeout(function(){s.throttle=null},a.throttle)):a.delay>0?s.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,u)},a.delay):(triggerEvent(e,"htmx:trigger"),t(e,u))}};r.listenerInfos==null&&(r.listenerInfos=[]),r.listenerInfos.push({trigger:a.trigger,listener:n,on:f}),f.addEventListener(a.trigger,n)})}let windowIsScrolling=!1,scrollHandler=null;function initScrollHandler(){scrollHandler||(scrollHandler=function(){windowIsScrolling=!0},window.addEventListener("scroll",scrollHandler),window.addEventListener("resize",scrollHandler),setInterval(function(){windowIsScrolling&&(windowIsScrolling=!1,forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){maybeReveal(e)}))},200))}function maybeReveal(e){!hasAttribute(e,"data-hx-revealed")&&isScrolledIntoView(e)&&(e.setAttribute("data-hx-revealed","true"),getInternalData(e).initHash?triggerEvent(e,"revealed"):e.addEventListener("htmx:afterProcessNode",function(){triggerEvent(e,"revealed")},{once:!0}))}function loadImmediately(e,t,r,a){let o=function(){r.loaded||(r.loaded=!0,triggerEvent(e,"htmx:trigger"),t(e))};a>0?getWindow().setTimeout(o,a):o()}function processVerbs(e,t,r){let a=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let s=getAttributeValue(e,"hx-"+o);a=!0,t.path=s,t.verb=o,r.forEach(function(l){addTriggerHandler(e,l,t,function(f,n){let u=asElement(f);if(eltIsDisabled(u)){cleanUpElement(u);return}issueAjaxRequest(o,s,u,n)})})}}),a}function addTriggerHandler(e,t,r,a){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,a,r,t),maybeReveal(asElement(e));else if(t.trigger==="intersect"){let o={};t.root&&(o.root=querySelectorExt(e,t.root)),t.threshold&&(o.threshold=parseFloat(t.threshold)),new IntersectionObserver(function(l){for(let f=0;f<l.length;f++)if(l[f].isIntersecting){triggerEvent(e,"intersect");break}},o).observe(asElement(e)),addEventListener(asElement(e),a,r,t)}else!r.firstInitCompleted&&t.trigger==="load"?maybeFilterEvent(t,e,makeEvent("load",{elt:e}))||loadImmediately(asElement(e),a,r,t.delay):t.pollInterval>0?(r.polling=!0,processPolling(asElement(e),a,t)):addEventListener(e,a,r,t)}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return!1;let r=t.attributes;for(let a=0;a<r.length;a++){let o=r[a].name;if(startsWith(o,"hx-on:")||startsWith(o,"data-hx-on:")||startsWith(o,"hx-on-")||startsWith(o,"data-hx-on-"))return!0}return!1}let HX_ON_QUERY=new XPathEvaluator().createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]');function processHXOnRoot(e,t){shouldProcessHxOn(e)&&t.push(asElement(e));let r=HX_ON_QUERY.evaluate(e),a=null;for(;a=r.iterateNext();)t.push(asElement(a))}function findHxOnWildcardElements(e){let t=[];if(e instanceof DocumentFragment)for(let r of e.childNodes)processHXOnRoot(r,t);else processHXOnRoot(e,t);return t}function findElementsToProcess(e){if(e.querySelectorAll){let r=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]",a=[];for(let s in extensions){let l=extensions[s];if(l.getSelectors){var t=l.getSelectors();t&&a.push(t)}}return e.querySelectorAll(VERB_SELECTOR+r+", form, [type='submit'], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+a.flat().map(s=>", "+s).join(""))}else return[]}function maybeSetLastButtonClicked(e){let t=getTargetButton(e.target),r=getRelatedFormData(e);r&&(r.lastButtonClicked=t)}function maybeUnsetLastButtonClicked(e){let t=getRelatedFormData(e);t&&(t.lastButtonClicked=null)}function getTargetButton(e){return closest(asElement(e),"button, input[type='submit']")}function getRelatedForm(e){return e.form||closest(e,"form")}function getRelatedFormData(e){let t=getTargetButton(e.target);if(!t)return;let r=getRelatedForm(t);if(r)return getInternalData(r)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(e,t,r){let a=getInternalData(e);Array.isArray(a.onHandlers)||(a.onHandlers=[]);let o,s=function(l){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",r)),o.call(e,l))})};e.addEventListener(t,s),a.onHandlers.push({event:t,listener:s})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;t<e.attributes.length;t++){let r=e.attributes[t].name,a=e.attributes[t].value;if(startsWith(r,"hx-on")||startsWith(r,"data-hx-on")){let o=r.indexOf("-on")+3,s=r.slice(o,o+1);if(s==="-"||s===":"){let l=r.slice(o+1);startsWith(l,":")?l="htmx"+l:startsWith(l,"-")?l="htmx:"+l.slice(1):startsWith(l,"htmx-")&&(l="htmx:"+l.slice(5)),addHxOnEventHandler(e,l,a)}}}}function initNode(e){triggerEvent(e,"htmx:beforeProcessNode");let t=getInternalData(e),r=getTriggerSpecs(e);processVerbs(e,t,r)||(getClosestAttributeValue(e,"hx-boost")==="true"?boostElement(e,t,r):hasAttribute(e,"hx-trigger")&&r.forEach(function(o){addTriggerHandler(e,o,t,function(){})})),(e.tagName==="FORM"||getRawAttribute(e,"type")==="submit"&&hasAttribute(e,"form"))&&initButtonTracking(e),t.firstInitCompleted=!0,triggerEvent(e,"htmx:afterProcessNode")}function maybeDeInitAndHash(e){if(!(e instanceof Element))return!1;let t=getInternalData(e),r=attributeHash(e);return t.initHash!==r?(deInitNode(e),t.initHash=r,!0):!1}function processNode(e){if(e=resolveTarget(e),eltIsDisabled(e)){cleanUpElement(e);return}let t=[];maybeDeInitAndHash(e)&&t.push(e),forEach(findElementsToProcess(e),function(r){if(eltIsDisabled(r)){cleanUpElement(r);return}maybeDeInitAndHash(r)&&t.push(r)}),forEach(findHxOnWildcardElements(e),processHxOnWildcard),forEach(t,initNode)}function kebabEventName(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function makeEvent(e,t){return new CustomEvent(e,{bubbles:!0,cancelable:!0,composed:!0,detail:t})}function triggerErrorEvent(e,t,r){triggerEvent(e,t,mergeObjects({error:t},r))}function ignoreEventForLogging(e){return e==="htmx:afterProcessNode"}function withExtensions(e,t,r){forEach(getExtensions(e,[],r),function(a){try{t(a)}catch(o){logError(o)}})}function logError(e){console.error(e)}function triggerEvent(e,t,r){e=resolveTarget(e),r==null&&(r={}),r.elt=e;let a=makeEvent(t,r);htmx.logger&&!ignoreEventForLogging(t)&&htmx.logger(e,t,r),r.error&&(logError(r.error),triggerEvent(e,"htmx:error",{errorInfo:r}));let o=e.dispatchEvent(a),s=kebabEventName(t);if(o&&s!==t){let l=makeEvent(s,a.detail);o=o&&e.dispatchEvent(l)}return withExtensions(asElement(e),function(l){o=o&&l.onEvent(t,a)!==!1&&!a.defaultPrevented}),o}let currentPathForHistory;function setCurrentPathForHistory(e){currentPathForHistory=e,canAccessLocalStorage()&&sessionStorage.setItem("htmx-current-path-for-history",e)}setCurrentPathForHistory(location.pathname+location.search);function getHistoryElement(){return getDocument().querySelector("[hx-history-elt],[data-hx-history-elt]")||getDocument().body}function saveToHistoryCache(e,t){if(!canAccessLocalStorage())return;let r=cleanInnerHtmlForHistory(t),a=getDocument().title,o=window.scrollY;if(htmx.config.historyCacheSize<=0){sessionStorage.removeItem("htmx-history-cache");return}e=normalizePath(e);let s=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let f=0;f<s.length;f++)if(s[f].url===e){s.splice(f,1);break}let l={url:e,content:r,title:a,scroll:o};for(triggerEvent(getDocument().body,"htmx:historyItemCreated",{item:l,cache:s}),s.push(l);s.length>htmx.config.historyCacheSize;)s.shift();for(;s.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(s));break}catch(f){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:f,cache:s}),s.shift()}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let r=0;r<t.length;r++)if(t[r].url===e)return t[r];return null}function cleanInnerHtmlForHistory(e){let t=htmx.config.requestClass,r=e.cloneNode(!0);return forEach(findAll(r,"."+t),function(a){removeClassFromElement(a,t)}),forEach(findAll(r,"[data-disabled-by-htmx]"),function(a){a.removeAttribute("disabled")}),r.innerHTML}function saveCurrentPageToHistory(){let e=getHistoryElement(),t=currentPathForHistory;canAccessLocalStorage()&&(t=sessionStorage.getItem("htmx-current-path-for-history")),t=t||location.pathname+location.search,getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')||(triggerEvent(getDocument().body,"htmx:beforeHistorySave",{path:t,historyElt:e}),saveToHistoryCache(t,e)),htmx.config.historyEnabled&&history.replaceState({htmx:!0},getDocument().title,location.href)}function pushUrlIntoHistory(e){htmx.config.getCacheBusterParam&&(e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,""),(endsWith(e,"&")||endsWith(e,"?"))&&(e=e.slice(0,-1))),htmx.config.historyEnabled&&history.pushState({htmx:!0},"",e),setCurrentPathForHistory(e)}function replaceUrlInHistory(e){htmx.config.historyEnabled&&history.replaceState({htmx:!0},"",e),setCurrentPathForHistory(e)}function settleImmediately(e){forEach(e,function(t){t.call(void 0)})}function loadHistoryFromServer(e){let t=new XMLHttpRequest,r={swapStyle:"innerHTML",swapDelay:0,settleDelay:0},a={path:e,xhr:t,historyElt:getHistoryElement(),swapSpec:r};t.open("GET",e,!0),htmx.config.historyRestoreAsHxRequest&&t.setRequestHeader("HX-Request","true"),t.setRequestHeader("HX-History-Restore-Request","true"),t.setRequestHeader("HX-Current-URL",location.href),t.onload=function(){this.status>=200&&this.status<400?(a.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",a),swap(a.historyElt,a.response,r,{contextElement:a.historyElt,historyRequest:!0}),setCurrentPathForHistory(a.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:a.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",a)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",a)&&t.send()}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let r={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll},a={path:e,item:t,historyElt:getHistoryElement(),swapSpec:r};triggerEvent(getDocument().body,"htmx:historyCacheHit",a)&&(swap(a.historyElt,t.content,r,{contextElement:a.historyElt,title:t.title}),setCurrentPathForHistory(a.path),triggerEvent(getDocument().body,"htmx:historyRestore",a))}else htmx.config.refreshOnHistoryMiss?htmx.location.reload(!0):loadHistoryFromServer(e)}function addRequestIndicatorClasses(e){let t=findAttributeTargets(e,"hx-indicator");return t==null&&(t=[e]),forEach(t,function(r){let a=getInternalData(r);a.requestCount=(a.requestCount||0)+1,r.classList.add.call(r.classList,htmx.config.requestClass)}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(r){let a=getInternalData(r);a.requestCount=(a.requestCount||0)+1,r.setAttribute("disabled",""),r.setAttribute("data-disabled-by-htmx","")}),t}function removeRequestIndicators(e,t){forEach(e.concat(t),function(r){let a=getInternalData(r);a.requestCount=(a.requestCount||1)-1}),forEach(e,function(r){getInternalData(r).requestCount===0&&r.classList.remove.call(r.classList,htmx.config.requestClass)}),forEach(t,function(r){getInternalData(r).requestCount===0&&(r.removeAttribute("disabled"),r.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(e,t){for(let r=0;r<e.length;r++)if(e[r].isSameNode(t))return!0;return!1}function shouldInclude(e){let t=e;return t.name===""||t.name==null||t.disabled||closest(t,"fieldset[disabled]")||t.type==="button"||t.type==="submit"||t.tagName==="image"||t.tagName==="reset"||t.tagName==="file"?!1:t.type==="checkbox"||t.type==="radio"?t.checked:!0}function addValueToFormData(e,t,r){e!=null&&t!=null&&(Array.isArray(t)?t.forEach(function(a){r.append(e,a)}):r.append(e,t))}function removeValueFromFormData(e,t,r){if(e!=null&&t!=null){let a=r.getAll(e);Array.isArray(t)?a=a.filter(o=>t.indexOf(o)<0):a=a.filter(o=>o!==t),r.delete(e),forEach(a,o=>r.append(e,o))}}function getValueFromInput(e){return e instanceof HTMLSelectElement&&e.multiple?toArray(e.querySelectorAll("option:checked")).map(function(t){return t.value}):e instanceof HTMLInputElement&&e.files?toArray(e.files):e.value}function processInputValue(e,t,r,a,o){if(!(a==null||haveSeenNode(e,a))){if(e.push(a),shouldInclude(a)){let s=getRawAttribute(a,"name");addValueToFormData(s,getValueFromInput(a),t),o&&validateElement(a,r)}a instanceof HTMLFormElement&&(forEach(a.elements,function(s){e.indexOf(s)>=0?removeValueFromFormData(s.name,getValueFromInput(s),t):e.push(s),o&&validateElement(s,r)}),new FormData(a).forEach(function(s,l){s instanceof File&&s.name===""||addValueToFormData(l,s,t)}))}}function validateElement(e,t){let r=e;r.willValidate&&(triggerEvent(r,"htmx:validation:validate"),r.checkValidity()||(triggerEvent(r,"htmx:validation:failed",{message:r.validationMessage,validity:r.validity})&&!t.length&&htmx.config.reportValidityOfForms&&r.reportValidity(),t.push({elt:r,message:r.validationMessage,validity:r.validity})))}function overrideFormData(e,t){for(let r of t.keys())e.delete(r);return t.forEach(function(r,a){e.append(a,r)}),e}function getInputValues(e,t){let r=[],a=new FormData,o=new FormData,s=[],l=getInternalData(e);l.lastButtonClicked&&!bodyContains(l.lastButtonClicked)&&(l.lastButtonClicked=null);let f=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(l.lastButtonClicked&&(f=f&&l.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(r,o,s,getRelatedForm(e),f),processInputValue(r,a,s,e,f),l.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let u=l.lastButtonClicked||e,m=getRawAttribute(u,"name");addValueToFormData(m,u.value,o)}let n=findAttributeTargets(e,"hx-include");return forEach(n,function(u){processInputValue(r,a,s,asElement(u),f),matches(u,"form")||forEach(asParentNode(u).querySelectorAll(INPUT_SELECTOR),function(m){processInputValue(r,a,s,m,f)})}),overrideFormData(a,o),{errors:s,formData:a,values:formDataProxy(a)}}function appendParam(e,t,r){e!==""&&(e+="&"),String(r)==="[object Object]"&&(r=JSON.stringify(r));let a=encodeURIComponent(r);return e+=encodeURIComponent(t)+"="+a,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(r,a){t=appendParam(t,a,r)}),t}function getHeaders(e,t,r){let a={"HX-Request":"true","HX-Trigger":getRawAttribute(e,"id"),"HX-Trigger-Name":getRawAttribute(e,"name"),"HX-Target":getAttributeValue(t,"id"),"HX-Current-URL":location.href};return getValuesForElement(e,"hx-headers",!1,a),r!==void 0&&(a["HX-Prompt"]=r),getInternalData(e).boosted&&(a["HX-Boosted"]="true"),a}function filterValues(e,t){let r=getClosestAttributeValue(t,"hx-params");if(r){if(r==="none")return new FormData;if(r==="*")return e;if(r.indexOf("not ")===0)return forEach(r.slice(4).split(","),function(a){a=a.trim(),e.delete(a)}),e;{let a=new FormData;return forEach(r.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(s){a.append(o,s)})}),a}}else return e}function isAnchorLink(e){return!!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let r=t||getClosestAttributeValue(e,"hx-swap"),a={swapStyle:getInternalData(e).boosted?"innerHTML":htmx.config.defaultSwapStyle,swapDelay:htmx.config.defaultSwapDelay,settleDelay:htmx.config.defaultSettleDelay};if(htmx.config.scrollIntoViewOnBoost&&getInternalData(e).boosted&&!isAnchorLink(e)&&(a.show="top"),r){let l=splitOnWhitespace(r);if(l.length>0)for(let f=0;f<l.length;f++){let n=l[f];if(n.indexOf("swap:")===0)a.swapDelay=parseInterval(n.slice(5));else if(n.indexOf("settle:")===0)a.settleDelay=parseInterval(n.slice(7));else if(n.indexOf("transition:")===0)a.transition=n.slice(11)==="true";else if(n.indexOf("ignoreTitle:")===0)a.ignoreTitle=n.slice(12)==="true";else if(n.indexOf("scroll:")===0){var o=n.slice(7).split(":");let m=o.pop();var s=o.length>0?o.join(":"):null;a.scroll=m,a.scrollTarget=s}else if(n.indexOf("show:")===0){var o=n.slice(5).split(":");let i=o.pop();var s=o.length>0?o.join(":"):null;a.show=i,a.showTarget=s}else if(n.indexOf("focus-scroll:")===0){let u=n.slice(13);a.focusScroll=u=="true"}else f==0?a.swapStyle=n:logError("Unknown modifier in hx-swap: "+n)}}return a}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,r){let a=null;return withExtensions(t,function(o){a==null&&(a=o.encodeParameters(e,r,t))}),a??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(r)):urlEncode(r))}function makeSettleInfo(e){return{tasks:[],elts:[e]}}function updateScrollState(e,t){let r=e[0],a=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(r,t.scrollTarget))),t.scroll==="top"&&(r||o)&&(o=o||r,o.scrollTop=0),t.scroll==="bottom"&&(a||o)&&(o=o||a,o.scrollTop=o.scrollHeight),typeof t.scroll=="number"&&getWindow().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}if(t.show){var o=null;if(t.showTarget){let l=t.showTarget;t.showTarget==="window"&&(l="body"),o=asElement(querySelectorExt(r,l))}t.show==="top"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(a||o)&&(o=o||a,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(e,t,r,a,o){if(a==null&&(a={}),e==null)return a;let s=getAttributeValue(e,t);if(s){let l=s.trim(),f=r;if(l==="unset")return null;l.indexOf("javascript:")===0?(l=l.slice(11),f=!0):l.indexOf("js:")===0&&(l=l.slice(3),f=!0),l.indexOf("{")!==0&&(l="{"+l+"}");let n;f?n=maybeEval(e,function(){return o?Function("event","return ("+l+")").call(e,o):Function("return ("+l+")").call(e)},{}):n=parseJSON(l);for(let u in n)n.hasOwnProperty(u)&&a[u]==null&&(a[u]=n[u])}return getValuesForElement(asElement(parentElt(e)),t,r,a,o)}function maybeEval(e,t,r){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),r)}function getHXVarsForElement(e,t,r){return getValuesForElement(e,"hx-vars",!0,r,t)}function getHXValsForElement(e,t,r){return getValuesForElement(e,"hx-vals",!1,r,t)}function getExpressionVars(e,t){return mergeObjects(getHXVarsForElement(e,t),getHXValsForElement(e,t))}function safelySetHeaderValue(e,t,r){if(r!==null)try{e.setRequestHeader(t,r)}catch{e.setRequestHeader(t,encodeURIComponent(r)),e.setRequestHeader(t+"-URI-AutoEncoded","true")}}function getPathFromResponse(e){if(e.responseURL)try{let t=new URL(e.responseURL);return t.pathname+t.search}catch{triggerErrorEvent(getDocument().body,"htmx:badResponseUrl",{url:e.responseURL})}}function hasHeader(e,t){return t.test(e.getAllResponseHeaders())}function ajaxHelper(e,t,r){if(e=e.toLowerCase(),r){if(r instanceof Element||typeof r=="string")return issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(r)||DUMMY_ELT,returnPromise:!0});{let a=resolveTarget(r.target);return(r.target&&!a||r.source&&!a&&!resolveTarget(r.source))&&(a=DUMMY_ELT),issueAjaxRequest(e,t,resolveTarget(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:a,swapOverride:r.swap,select:r.select,returnPromise:!0,push:r.push,replace:r.replace,selectOOB:r.selectOOB})}}else return issueAjaxRequest(e,t,null,null,{returnPromise:!0})}function hierarchyForElt(e){let t=[];for(;e;)t.push(e),e=e.parentElement;return t}function verifyPath(e,t,r){let a=new URL(t,location.protocol!=="about:"?location.href:window.origin),s=(location.protocol!=="about:"?location.origin:window.origin)===a.origin;return htmx.config.selfRequestsOnly&&!s?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:a,sameHost:s},r))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let r in e)e.hasOwnProperty(r)&&(e[r]&&typeof e[r].forEach=="function"?e[r].forEach(function(a){t.append(r,a)}):typeof e[r]=="object"&&!(e[r]instanceof Blob)?t.append(r,JSON.stringify(e[r])):t.append(r,e[r]));return t}function formDataArrayProxy(e,t,r){return new Proxy(r,{get:function(a,o){return typeof o=="number"?a[o]:o==="length"?a.length:o==="push"?function(s){a.push(s),e.append(t,s)}:typeof a[o]=="function"?function(){a[o].apply(a,arguments),e.delete(t),a.forEach(function(s){e.append(t,s)})}:a[o]&&a[o].length===1?a[o][0]:a[o]},set:function(a,o,s){return a[o]=s,e.delete(t),a.forEach(function(l){e.append(t,l)}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,r){if(typeof r=="symbol"){let o=Reflect.get(t,r);return typeof o=="function"?function(){return o.apply(e,arguments)}:o}if(r==="toJSON")return()=>Object.fromEntries(e);if(r in t&&typeof t[r]=="function")return function(){return e[r].apply(e,arguments)};let a=e.getAll(r);if(a.length!==0)return a.length===1?a[0]:formDataArrayProxy(t,r,a)},set:function(t,r,a){return typeof r!="string"?!1:(t.delete(r),a&&typeof a.forEach=="function"?a.forEach(function(o){t.append(r,o)}):typeof a=="object"&&!(a instanceof Blob)?t.append(r,JSON.stringify(a)):t.append(r,a),!0)},deleteProperty:function(t,r){return typeof r=="string"&&t.delete(r),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,r){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),r)}})}function issueAjaxRequest(e,t,r,a,o,s){let l=null,f=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var n=new Promise(function(h,y){l=h,f=y});r==null&&(r=getDocument().body);let u=o.handler||handleAjaxResponse,m=o.select||null;if(!bodyContains(r))return maybeCall(l),n;let i=o.targetOverride||asElement(getTarget(r));if(i==null||i==DUMMY_ELT)return triggerErrorEvent(r,"htmx:targetError",{target:getClosestAttributeValue(r,"hx-target")}),maybeCall(f),n;let c=getInternalData(r),x=c.lastButtonClicked;if(x){let h=getRawAttribute(x,"formaction");h!=null&&(t=h);let y=getRawAttribute(x,"formmethod");if(y!=null)if(VERBS.includes(y.toLowerCase()))e=y;else return maybeCall(l),n}let d=getClosestAttributeValue(r,"hx-confirm");if(s===void 0&&triggerEvent(r,"htmx:confirm",{target:i,elt:r,path:t,verb:e,triggeringEvent:a,etc:o,issueRequest:function(T){return issueAjaxRequest(e,t,r,a,o,!!T)},question:d})===!1)return maybeCall(l),n;let p=r,g=getClosestAttributeValue(r,"hx-sync"),E=null,b=!1;if(g){let h=g.split(":"),y=h[0].trim();if(y==="this"?p=findThisElement(r,"hx-sync"):p=asElement(querySelectorExt(r,y)),g=(h[1]||"drop").trim(),c=getInternalData(p),g==="drop"&&c.xhr&&c.abortable!==!0)return maybeCall(l),n;if(g==="abort"){if(c.xhr)return maybeCall(l),n;b=!0}else g==="replace"?triggerEvent(p,"htmx:abort"):g.indexOf("queue")===0&&(E=(g.split(" ")[1]||"last").trim())}if(c.xhr)if(c.abortable)triggerEvent(p,"htmx:abort");else{if(E==null){if(a){let h=getInternalData(a);h&&h.triggerSpec&&h.triggerSpec.queue&&(E=h.triggerSpec.queue)}E==null&&(E="last")}return c.queuedRequests==null&&(c.queuedRequests=[]),E==="first"&&c.queuedRequests.length===0?c.queuedRequests.push(function(){issueAjaxRequest(e,t,r,a,o)}):E==="all"?c.queuedRequests.push(function(){issueAjaxRequest(e,t,r,a,o)}):E==="last"&&(c.queuedRequests=[],c.queuedRequests.push(function(){issueAjaxRequest(e,t,r,a,o)})),maybeCall(l),n}let S=new XMLHttpRequest;c.xhr=S,c.abortable=b;let C=function(){c.xhr=null,c.abortable=!1,c.queuedRequests!=null&&c.queuedRequests.length>0&&c.queuedRequests.shift()()},ye=getClosestAttributeValue(r,"hx-prompt");if(ye){var U=prompt(ye);if(U===null||!triggerEvent(r,"htmx:prompt",{prompt:U,target:i}))return maybeCall(l),C(),n}if(d&&!s&&!confirm(d))return maybeCall(l),C(),n;let D=getHeaders(r,i,U);e!=="get"&&!usesFormData(r)&&(D["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(D=mergeObjects(D,o.headers));let we=getInputValues(r,e),R=we.errors,Ee=we.formData;o.values&&overrideFormData(Ee,formDataFromObject(o.values));let He=formDataFromObject(getExpressionVars(r,a)),N=overrideFormData(Ee,He),k=filterValues(N,r);htmx.config.getCacheBusterParam&&e==="get"&&k.set("org.htmx.cache-buster",getRawAttribute(i,"id")||"true"),(t==null||t==="")&&(t=location.href);let V=getValuesForElement(r,"hx-request"),be=getInternalData(r).boosted,M=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,v={boosted:be,useUrlParams:M,formData:k,parameters:formDataProxy(k),unfilteredFormData:N,unfilteredParameters:formDataProxy(N),headers:D,elt:r,target:i,verb:e,errors:R,withCredentials:o.credentials||V.credentials||htmx.config.withCredentials,timeout:o.timeout||V.timeout||htmx.config.timeout,path:t,triggeringEvent:a};if(!triggerEvent(r,"htmx:configRequest",v))return maybeCall(l),C(),n;if(t=v.path,e=v.verb,D=v.headers,k=formDataFromObject(v.parameters),R=v.errors,M=v.useUrlParams,R&&R.length>0)return triggerEvent(r,"htmx:validation:halted",v),maybeCall(l),C(),n;let ve=t.split("#"),Be=ve[0],W=ve[1],A=t;if(M&&(A=Be,!k.keys().next().done&&(A.indexOf("?")<0?A+="?":A+="&",A+=urlEncode(k),W&&(A+="#"+W))),!verifyPath(r,A,v))return triggerErrorEvent(r,"htmx:invalidPath",v),maybeCall(f),C(),n;if(S.open(e.toUpperCase(),A,!0),S.overrideMimeType("text/html"),S.withCredentials=v.withCredentials,S.timeout=v.timeout,!V.noHeaders){for(let h in D)if(D.hasOwnProperty(h)){let y=D[h];safelySetHeaderValue(S,h,y)}}let w={xhr:S,target:i,requestConfig:v,etc:o,boosted:be,select:m,pathInfo:{requestPath:t,finalRequestPath:A,responsePath:null,anchor:W}};if(S.onload=function(){try{let h=hierarchyForElt(r);if(w.pathInfo.responsePath=getPathFromResponse(S),u(r,w),w.keepIndicators!==!0&&removeRequestIndicators(F,H),triggerEvent(r,"htmx:afterRequest",w),triggerEvent(r,"htmx:afterOnLoad",w),!bodyContains(r)){let y=null;for(;h.length>0&&y==null;){let T=h.shift();bodyContains(T)&&(y=T)}y&&(triggerEvent(y,"htmx:afterRequest",w),triggerEvent(y,"htmx:afterOnLoad",w))}maybeCall(l)}catch(h){throw triggerErrorEvent(r,"htmx:onLoadError",mergeObjects({error:h},w)),h}finally{C()}},S.onerror=function(){removeRequestIndicators(F,H),triggerErrorEvent(r,"htmx:afterRequest",w),triggerErrorEvent(r,"htmx:sendError",w),maybeCall(f),C()},S.onabort=function(){removeRequestIndicators(F,H),triggerErrorEvent(r,"htmx:afterRequest",w),triggerErrorEvent(r,"htmx:sendAbort",w),maybeCall(f),C()},S.ontimeout=function(){removeRequestIndicators(F,H),triggerErrorEvent(r,"htmx:afterRequest",w),triggerErrorEvent(r,"htmx:timeout",w),maybeCall(f),C()},!triggerEvent(r,"htmx:beforeRequest",w))return maybeCall(l),C(),n;var F=addRequestIndicatorClasses(r),H=disableElements(r);forEach(["loadstart","loadend","progress","abort"],function(h){forEach([S,S.upload],function(y){y.addEventListener(h,function(T){triggerEvent(r,"htmx:xhr:"+h,{lengthComputable:T.lengthComputable,loaded:T.loaded,total:T.total})})})}),triggerEvent(r,"htmx:beforeSend",w);let Oe=M?null:encodeParamsForBody(S,r,k);return S.send(Oe),n}function determineHistoryUpdates(e,t){let r=t.xhr,a=null,o=null;if(hasHeader(r,/HX-Push:/i)?(a=r.getResponseHeader("HX-Push"),o="push"):hasHeader(r,/HX-Push-Url:/i)?(a=r.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(r,/HX-Replace-Url:/i)&&(a=r.getResponseHeader("HX-Replace-Url"),o="replace"),a)return a==="false"?{}:{type:o,path:a};let s=t.pathInfo.finalRequestPath,l=t.pathInfo.responsePath,f=t.etc.push||getClosestAttributeValue(e,"hx-push-url"),n=t.etc.replace||getClosestAttributeValue(e,"hx-replace-url"),u=getInternalData(e).boosted,m=null,i=null;return f?(m="push",i=f):n?(m="replace",i=n):u&&(m="push",i=l||s),i?i==="false"?{}:(i==="true"&&(i=l||s),t.pathInfo.anchor&&i.indexOf("#")===-1&&(i=i+"#"+t.pathInfo.anchor),{type:m,path:i}):{}}function codeMatches(e,t){var r=new RegExp(e.code);return r.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t<htmx.config.responseHandling.length;t++){var r=htmx.config.responseHandling[t];if(codeMatches(r,e.status))return r}return{swap:!1}}function handleTitle(e){if(e){let t=find("title");t?t.textContent=e:window.document.title=e}}function resolveRetarget(e,t){if(t==="this")return e;let r=asElement(querySelectorExt(e,t));if(r==null)throw triggerErrorEvent(e,"htmx:targetError",{target:t}),new Error(`Invalid re-target ${t}`);return r}function handleAjaxResponse(e,t){let r=t.xhr,a=t.target,o=t.etc,s=t.select;if(!triggerEvent(e,"htmx:beforeOnLoad",t))return;if(hasHeader(r,/HX-Trigger:/i)&&handleTriggerHeader(r,"HX-Trigger",e),hasHeader(r,/HX-Location:/i)){let b=r.getResponseHeader("HX-Location");var l={};b.indexOf("{")===0&&(l=parseJSON(b),b=l.path,delete l.path),l.push=l.push||"true",ajaxHelper("get",b,l);return}let f=hasHeader(r,/HX-Refresh:/i)&&r.getResponseHeader("HX-Refresh")==="true";if(hasHeader(r,/HX-Redirect:/i)){t.keepIndicators=!0,htmx.location.href=r.getResponseHeader("HX-Redirect"),f&&htmx.location.reload();return}if(f){t.keepIndicators=!0,htmx.location.reload();return}let n=determineHistoryUpdates(e,t),u=resolveResponseHandling(r),m=u.swap,i=!!u.error,c=htmx.config.ignoreTitle||u.ignoreTitle,x=u.select;u.target&&(t.target=resolveRetarget(e,u.target));var d=o.swapOverride;d==null&&u.swapOverride&&(d=u.swapOverride),hasHeader(r,/HX-Retarget:/i)&&(t.target=resolveRetarget(e,r.getResponseHeader("HX-Retarget"))),hasHeader(r,/HX-Reswap:/i)&&(d=r.getResponseHeader("HX-Reswap"));var p=r.response,g=mergeObjects({shouldSwap:m,serverResponse:p,isError:i,ignoreTitle:c,selectOverride:x,swapOverride:d},t);if(!(u.event&&!triggerEvent(a,u.event,g))&&triggerEvent(a,"htmx:beforeSwap",g)){if(a=g.target,p=g.serverResponse,i=g.isError,c=g.ignoreTitle,x=g.selectOverride,d=g.swapOverride,t.target=a,t.failed=i,t.successful=!i,g.shouldSwap){r.status===286&&cancelPolling(e),withExtensions(e,function(C){p=C.transformResponse(p,r,e)}),n.type&&saveCurrentPageToHistory();var E=getSwapSpecification(e,d);E.hasOwnProperty("ignoreTitle")||(E.ignoreTitle=c),a.classList.add(htmx.config.swappingClass),s&&(x=s),hasHeader(r,/HX-Reselect:/i)&&(x=r.getResponseHeader("HX-Reselect"));let b=o.selectOOB||getClosestAttributeValue(e,"hx-select-oob"),S=getClosestAttributeValue(e,"hx-select");swap(a,p,E,{select:x==="unset"?null:x||S,selectOOB:b,eventInfo:t,anchor:t.pathInfo.anchor,contextElement:e,afterSwapCallback:function(){if(hasHeader(r,/HX-Trigger-After-Swap:/i)){let C=e;bodyContains(e)||(C=getDocument().body),handleTriggerHeader(r,"HX-Trigger-After-Swap",C)}},afterSettleCallback:function(){if(hasHeader(r,/HX-Trigger-After-Settle:/i)){let C=e;bodyContains(e)||(C=getDocument().body),handleTriggerHeader(r,"HX-Trigger-After-Settle",C)}},beforeSwapCallback:function(){n.type&&(triggerEvent(getDocument().body,"htmx:beforeHistoryUpdate",mergeObjects({history:n},t)),n.type==="push"?(pushUrlIntoHistory(n.path),triggerEvent(getDocument().body,"htmx:pushedIntoHistory",{path:n.path})):(replaceUrlInHistory(n.path),triggerEvent(getDocument().body,"htmx:replacedInHistory",{path:n.path})))}})}i&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+r.status+" from "+t.pathInfo.requestPath},t))}}let extensions={};function extensionBase(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return!0},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return!1},handleSwap:function(e,t,r,a){return!1},encodeParameters:function(e,t,r){return null}}}function defineExtension(e,t){t.init&&t.init(internalAPI),extensions[e]=mergeObjects(extensionBase(),t)}function removeExtension(e){delete extensions[e]}function getExtensions(e,t,r){if(t==null&&(t=[]),e==null)return t;r==null&&(r=[]);let a=getAttributeValue(e,"hx-ext");return a&&forEach(a.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){r.push(o.slice(7));return}if(r.indexOf(o)<0){let s=extensions[o];s&&t.indexOf(s)<0&&t.push(s)}}),getExtensions(asElement(parentElt(e)),t,r)}var isReady=!1;getDocument().addEventListener("DOMContentLoaded",function(){isReady=!0});function ready(e){isReady||getDocument().readyState==="complete"?e():getDocument().addEventListener("DOMContentLoaded",e)}function insertIndicatorStyles(){if(htmx.config.includeIndicatorStyles!==!1){let e=htmx.config.inlineStyleNonce?` nonce="${htmx.config.inlineStyleNonce}"`:"",t=htmx.config.indicatorClass,r=htmx.config.requestClass;getDocument().head.insertAdjacentHTML("beforeend",`<style${e}>.${t}{opacity:0;visibility: hidden} .${r} .${t}, .${r}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}</style>`)}}function getMetaConfig(){let e=getDocument().querySelector('meta[name="htmx-config"]');return e?parseJSON(e.content):null}function mergeMetaConfig(){let e=getMetaConfig();e&&(htmx.config=mergeObjects(htmx.config,e))}return ready(function(){mergeMetaConfig(),insertIndicatorStyles();let e=getDocument().body;processNode(e);let t=getDocument().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(a){let o=a.detail.elt||a.target,s=getInternalData(o);s&&s.xhr&&s.xhr.abort()});let r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(a){a.state&&a.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent})})):r&&r(a)},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null},0)}),htmx})(),Ae=qe;var De=document.createElement("template");De.innerHTML=` 2 + <slot></slot> 3 + 4 + <ul class="menu" part="menu"></ul> 5 + 6 + <style> 7 + :host { 8 + --color-background-inherited: var(--color-background, #ffffff); 9 + --color-border-inherited: var(--color-border, #00000022); 10 + --color-shadow-inherited: var(--color-shadow, #000000); 11 + --color-hover-inherited: var(--color-hover, #00000011); 12 + --color-avatar-fallback-inherited: var(--color-avatar-fallback, #00000022); 13 + --radius-inherited: var(--radius, 8px); 14 + --padding-menu-inherited: var(--padding-menu, 4px); 15 + display: block; 16 + position: relative; 17 + font-family: system-ui; 18 + } 19 + 20 + *, *::before, *::after { 21 + margin: 0; 22 + padding: 0; 23 + box-sizing: border-box; 24 + } 25 + 26 + .menu { 27 + display: flex; 28 + flex-direction: column; 29 + position: absolute; 30 + left: 0; 31 + margin-top: 4px; 32 + width: 100%; 33 + list-style: none; 34 + overflow: hidden; 35 + background-color: var(--color-background-inherited); 36 + background-clip: padding-box; 37 + border: 1px solid var(--color-border-inherited); 38 + border-radius: var(--radius-inherited); 39 + box-shadow: 0 6px 6px -4px rgb(from var(--color-shadow-inherited) r g b / 20%); 40 + padding: var(--padding-menu-inherited); 41 + } 42 + 43 + .menu:empty { 44 + display: none; 45 + } 46 + 47 + .user { 48 + all: unset; 49 + box-sizing: border-box; 50 + display: flex; 51 + align-items: center; 52 + gap: 8px; 53 + padding: 6px 8px; 54 + width: 100%; 55 + height: calc(1.5rem + 6px * 2); 56 + border-radius: calc(var(--radius-inherited) - var(--padding-menu-inherited)); 57 + cursor: default; 58 + } 59 + 60 + .user:hover, 61 + .user[data-active="true"] { 62 + background-color: var(--color-hover-inherited); 63 + } 64 + 65 + .avatar { 66 + width: 1.5rem; 67 + height: 1.5rem; 68 + border-radius: 50%; 69 + background-color: var(--color-avatar-fallback-inherited); 70 + overflow: hidden; 71 + flex-shrink: 0; 72 + } 73 + 74 + .img { 75 + display: block; 76 + width: 100%; 77 + height: 100%; 78 + } 79 + 80 + .handle { 81 + white-space: nowrap; 82 + overflow: hidden; 83 + text-overflow: ellipsis; 84 + } 85 + </style> 86 + `;var ke=document.createElement("template");ke.innerHTML=` 87 + <li> 88 + <button class="user" part="user"> 89 + <div class="avatar" part="avatar"> 90 + <img class="img" part="img"> 91 + </div> 92 + <span class="handle" part="handle"></span> 93 + </button> 94 + </li> 95 + `;function Te(e){return e.cloneNode(!0)}var X=class extends HTMLElement{static tag="actor-typeahead";static define(t=this.tag){this.tag=t;let r=customElements.getName(this);if(r&&r!==t)return console.warn(`${this.name} already defined as <${r}>!`);let a=customElements.get(t);if(a&&a!==this)return console.warn(`<${t}> already defined as ${a.name}!`);customElements.define(t,this)}static{let t=new URL(import.meta.url).searchParams.get("tag")||this.tag;t!=="none"&&this.define(t)}#r=this.attachShadow({mode:"closed"});#a=[];#e=-1;#o=!1;constructor(){super(),this.#r.append(Te(De).content),this.#t(),this.addEventListener("input",this),this.addEventListener("focusout",this),this.addEventListener("keydown",this),this.#r.addEventListener("pointerdown",this),this.#r.addEventListener("pointerup",this),this.#r.addEventListener("click",this)}get#s(){let t=Number.parseInt(this.getAttribute("rows")??"");return Number.isNaN(t)?5:t}handleEvent(t){switch(t.type){case"input":this.#f(t);break;case"keydown":this.#l(t);break;case"focusout":this.#n(t);break;case"pointerdown":this.#u(t);break;case"pointerup":this.#i(t);break}}#l(t){switch(t.key){case"ArrowDown":t.preventDefault(),this.#e=Math.min(this.#e+1,this.#s-1),this.#t();break;case"PageDown":t.preventDefault(),this.#e=this.#s-1,this.#t();break;case"ArrowUp":t.preventDefault(),this.#e=Math.max(this.#e-1,0),this.#t();break;case"PageUp":t.preventDefault(),this.#e=0,this.#t();break;case"Escape":t.preventDefault(),this.#a=[],this.#e=-1,this.#t();break;case"Enter":t.preventDefault(),this.#r.querySelectorAll("button")[this.#e]?.dispatchEvent(new PointerEvent("pointerup",{bubbles:!0}));break}}async#f(t){let r=t.target?.value;if(!r){this.#a=[],this.#t();return}let a=this.getAttribute("host")??"https://public.api.bsky.app",o=new URL("xrpc/app.bsky.actor.searchActorsTypeahead",a);o.searchParams.set("q",r),o.searchParams.set("limit",`${this.#s}`);let l=await(await fetch(o)).json();this.#a=l.actors,this.#e=-1,this.#t()}async#n(t){this.#o||(this.#a=[],this.#e=-1,this.#t())}#t(){let t=document.createDocumentFragment(),r=-1;for(let a of this.#a){let o=Te(ke).content,s=o.querySelector("button");s&&(s.dataset.handle=a.handle,++r===this.#e&&(s.dataset.active="true"));let l=o.querySelector("img");l&&a.avatar&&(l.src=a.avatar);let f=o.querySelector(".handle");f&&(f.textContent=a.handle),t.append(o)}this.#r.querySelector(".menu")?.replaceChildren(...t.children)}#u(t){this.#o=!0}#i(t){this.#o=!1,this.querySelector("input")?.focus();let r=t.target?.closest("button"),a=this.querySelector("input");!a||!r||(a.value=r.dataset.handle||"",this.#a=[],this.#t())}};var B={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"};var Pe=([e,t,r])=>{let a=document.createElementNS("http://www.w3.org/2000/svg",e);return Object.keys(t).forEach(o=>{a.setAttribute(o,String(t[o]))}),r?.length&&r.forEach(o=>{let s=Pe(o);a.appendChild(s)}),a},Le=(e,t={})=>{let a={...B,...t};return Pe(["svg",a,e])};var Ie=e=>Array.from(e.attributes).reduce((t,r)=>(t[r.name]=r.value,t),{}),Ue=e=>typeof e=="string"?e:!e||!e.class?"":e.class&&typeof e.class=="string"?e.class.split(" "):e.class&&Array.isArray(e.class)?e.class:"",Ne=e=>e.flatMap(Ue).map(r=>r.trim()).filter(Boolean).filter((r,a,o)=>o.indexOf(r)===a).join(" "),Ve=e=>e.replace(/(\w)(\w*)(_|-|\s*)/g,(t,r,a)=>r.toUpperCase()+a.toLowerCase()),G=(e,{nameAttr:t,icons:r,attrs:a})=>{let o=e.getAttribute(t);if(o==null)return;let s=Ve(o),l=r[s];if(!l)return console.warn(`${e.outerHTML} icon name was not found in the provided icons object.`);let f=Ie(e),n={...B,"data-lucide":o,...a,...f},u=Ne(["lucide",`lucide-${o}`,f,a]);u&&Object.assign(n,{class:u});let m=Le(l,n);return e.parentNode?.replaceChild(m,e)};var _=[["path",{d:"M12 6v16"}],["path",{d:"m19 13 2-1a9 9 0 0 1-18 0l2 1"}],["path",{d:"M9 11h6"}],["circle",{cx:"12",cy:"4",r:"2"}]];var z=[["path",{d:"M12 17V3"}],["path",{d:"m6 11 6 6 6-6"}],["path",{d:"M19 21H5"}]];var j=[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"}],["path",{d:"m3.3 7 8.7 5 8.7-5"}],["path",{d:"M12 22V12"}]];var $=[["path",{d:"M20 6 9 17l-5-5"}]];var J=[["path",{d:"m6 9 6 6 6-6"}]];var K=[["path",{d:"m15 18-6-6 6-6"}]];var Q=[["path",{d:"m9 18 6-6-6-6"}]];var O=[["circle",{cx:"12",cy:"12",r:"10"}],["line",{x1:"12",x2:"12",y1:"8",y2:"12"}],["line",{x1:"12",x2:"12.01",y1:"16",y2:"16"}]];var q=[["path",{d:"M21.801 10A10 10 0 1 1 17 3.335"}],["path",{d:"m9 11 3 3L22 4"}]];var P=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"m15 9-6 6"}],["path",{d:"m9 9 6 6"}]];var Z=[["path",{d:"m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"}],["circle",{cx:"12",cy:"12",r:"10"}]];var Y=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"}]];var ee=[["path",{d:"M12 15V3"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"}],["path",{d:"m7 10 5 5 5-5"}]];var te=[["circle",{cx:"12",cy:"12",r:"10"}],["path",{d:"M12 16v-4"}],["path",{d:"M12 8h.01"}]];var I=[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56"}]];var re=[["path",{d:"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"}]];var ae=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z"}],["path",{d:"M12 22V12"}],["polyline",{points:"3.29 7 12 12 20.71 7"}],["path",{d:"m7.5 4.27 9 5.15"}]];var oe=[["path",{d:"M5 12h14"}],["path",{d:"M12 5v14"}]];var se=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"}],["path",{d:"M3 3v5h5"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"}],["path",{d:"M16 16h5v5"}]];var le=[["path",{d:"m21 21-4.34-4.34"}],["circle",{cx:"11",cy:"11",r:"8"}]];var fe=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"}],["path",{d:"m9 12 2 2 4-4"}]];var ne=[["path",{d:"M12 10.189V14"}],["path",{d:"M12 2v3"}],["path",{d:"M19 13V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v6"}],["path",{d:"M19.38 20A11.6 11.6 0 0 0 21 14l-8.188-3.639a2 2 0 0 0-1.624 0L3 14a11.6 11.6 0 0 0 2.81 7.76"}],["path",{d:"M2 21c.6.5 1.2 1 2.5 1 2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1s1.2 1 2.5 1c2.5 0 2.5-2 5-2 1.3 0 1.9.5 2.5 1"}]];var ue=[["path",{d:"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"}]];var ie=[["path",{d:"M12 2v2"}],["path",{d:"M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715"}],["path",{d:"M16 12a4 4 0 0 0-4-4"}],["path",{d:"m19 5-1.256 1.256"}],["path",{d:"M20 12h2"}]];var de=[["circle",{cx:"12",cy:"12",r:"4"}],["path",{d:"M12 2v2"}],["path",{d:"M12 20v2"}],["path",{d:"m4.93 4.93 1.41 1.41"}],["path",{d:"m17.66 17.66 1.41 1.41"}],["path",{d:"M2 12h2"}],["path",{d:"M20 12h2"}],["path",{d:"m6.34 17.66-1.41 1.41"}],["path",{d:"m19.07 4.93-1.41 1.41"}]];var me=[["path",{d:"M12 19h8"}],["path",{d:"m4 17 6-6-6-6"}]];var ce=[["path",{d:"M10 11v6"}],["path",{d:"M14 11v6"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"}],["path",{d:"M3 6h18"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}]];var L=[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"}],["path",{d:"M12 9v4"}],["path",{d:"M12 17h.01"}]];var pe=({icons:e={},nameAttr:t="data-lucide",attrs:r={},root:a=document,inTemplates:o}={})=>{if(!Object.values(e).length)throw new Error(`Please provide an icons object. 96 + If you want to use all the icons you can import it like: 97 + \`import { createIcons, icons } from 'lucide'; 98 + lucide.createIcons({icons});\``);if(typeof a>"u")throw new Error("`createIcons()` only works in a browser environment.");if(Array.from(a.querySelectorAll(`[${t}]`)).forEach(l=>G(l,{nameAttr:t,icons:e,attrs:r})),o&&Array.from(a.querySelectorAll("template")).forEach(f=>pe({icons:e,nameAttr:t,attrs:r,root:f.content,inTemplates:o})),t==="data-lucide"){let l=a.querySelectorAll("[icon-name]");l.length>0&&(console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide"),Array.from(l).forEach(f=>G(f,{nameAttr:"icon-name",icons:e,attrs:r})))}};function Re(){return localStorage.getItem("theme")||"system"}function We(e){return e==="dark"?"dark":e==="light"?"light":window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function ge(){let e=Re(),t=We(e);document.documentElement.classList.toggle("dark",t==="dark"),document.documentElement.setAttribute("data-theme",t),Xe(e)}function Me(e){localStorage.setItem("theme",e),ge(),Ge()}function Xe(e){let t={system:"sun-moon",light:"sun",dark:"moon"},r=document.getElementById("theme-icon");r&&(r.setAttribute("data-lucide",t[e]||"sun-moon"),typeof window.lucide<"u"&&window.lucide.createIcons()),document.querySelectorAll(".theme-option").forEach(a=>{let o=a.dataset.value===e,s=a.querySelector(".theme-check");s&&(s.style.visibility=o?"visible":"hidden")})}function Ge(){let t=document.getElementById("theme-toggle-btn")?.closest("details");t&&t.removeAttribute("open")}window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{Re()==="system"&&ge()});function _e(){let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(e.classList.toggle("expanded"),e.classList.contains("expanded")&&t.focus())}function xe(){let e=document.querySelector(".nav-search-wrapper");e&&e.classList.remove("expanded")}document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(document.addEventListener("keydown",r=>{r.key==="Escape"&&e.classList.contains("expanded")&&xe()}),document.addEventListener("click",r=>{e.classList.contains("expanded")&&!e.contains(r.target)&&xe()}))});function ze(e){navigator.clipboard.writeText(e).then(()=>{let t=event.target.closest("button"),r=t.innerHTML;t.innerHTML='<i data-lucide="check"></i> Copied!',typeof window.lucide<"u"&&window.lucide.createIcons(),setTimeout(()=>{t.innerHTML=r,typeof window.lucide<"u"&&window.lucide.createIcons()},2e3)}).catch(t=>{console.error("Failed to copy:",t)})}function je(e){let t=Math.floor((new Date-new Date(e))/1e3),r={year:31536e3,month:2592e3,week:604800,day:86400,hour:3600,minute:60,second:1};for(let[a,o]of Object.entries(r)){let s=Math.floor(t/o);if(s>=1)return s===1?`1 ${a} ago`:`${s} ${a}s ago`}return"just now"}function Ce(){document.querySelectorAll("time[datetime]").forEach(e=>{let t=e.getAttribute("datetime");if(t&&!e.dataset.noUpdate){let r=je(t);e.textContent!==r&&(e.textContent=r)}})}document.addEventListener("DOMContentLoaded",()=>{Ce(),ge();let e=document.getElementById("theme-dropdown-menu");e&&e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{Me(t.dataset.value)})})});document.addEventListener("htmx:afterSwap",Ce);setInterval(Ce,6e4);function $e(){let e=document.getElementById("show-offline-toggle"),t=document.querySelector(".manifests-list");!e||!t||(localStorage.setItem("showOfflineManifests",e.checked),e.checked?t.classList.add("show-offline"):t.classList.remove("show-offline"))}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("show-offline-toggle");if(!e)return;let t=localStorage.getItem("showOfflineManifests")==="true";e.checked=t;let r=document.querySelector(".manifests-list");r&&(t?r.classList.add("show-offline"):r.classList.remove("show-offline"))});async function Je(e,t,r){try{let a=await fetch(`/api/images/${e}/manifests/${t}`,{method:"DELETE",credentials:"include"});if(a.status===409){let o=await a.json();Ke(e,t,r,o.tags)}else if(a.ok)Fe(r);else{let o=await a.text();alert(`Failed to delete manifest: ${o}`)}}catch(a){console.error("Error deleting manifest:",a),alert(`Error deleting manifest: ${a.message}`)}}function Ke(e,t,r,a){let o=document.getElementById("manifest-delete-modal"),s=document.getElementById("manifest-delete-tags"),l=document.getElementById("confirm-manifest-delete-btn");s.innerHTML="",a.forEach(f=>{let n=document.createElement("li");n.textContent=f,s.appendChild(n)}),l.onclick=()=>Qe(e,t,r),o.style.display="flex"}function Se(){let e=document.getElementById("manifest-delete-modal");e.style.display="none"}async function Qe(e,t,r){let a=document.getElementById("confirm-manifest-delete-btn"),o=a.textContent;try{a.disabled=!0,a.textContent="Deleting...";let s=await fetch(`/api/images/${e}/manifests/${t}?confirm=true`,{method:"DELETE",credentials:"include"});if(s.ok)Se(),Fe(r),location.reload();else{let l=await s.text();alert(`Failed to delete manifest: ${l}`),a.disabled=!1,a.textContent=o}}catch(s){console.error("Error deleting manifest:",s),alert(`Error deleting manifest: ${s.message}`),a.disabled=!1,a.textContent=o}}function Fe(e){let t=document.getElementById(`manifest-${e}`);t&&t.remove()}async function Ze(e,t){let r=e.files[0];if(!r)return;if(!["image/png","image/jpeg","image/webp"].includes(r.type)){alert("Please select a PNG, JPEG, or WebP image");return}if(r.size>3*1024*1024){alert("Image must be less than 3MB");return}let o=new FormData;o.append("avatar",r);try{let s=await fetch(`/api/images/${t}/avatar`,{method:"POST",credentials:"include",body:o});if(s.status===401){window.location.href="/auth/oauth/login";return}if(!s.ok){let m=await s.text();throw new Error(m)}let l=await s.json(),f=document.querySelector(".repo-hero-icon-wrapper");if(!f)return;let n=f.querySelector(".repo-hero-icon"),u=f.querySelector(".repo-hero-icon-placeholder");if(n)n.src=l.avatarURL;else if(u){let m=document.createElement("img");m.src=l.avatarURL,m.alt=t,m.className="repo-hero-icon",u.replaceWith(m)}}catch(s){console.error("Error uploading avatar:",s),alert("Failed to upload avatar: "+s.message)}e.value=""}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("manifest-delete-modal");e&&e.addEventListener("click",t=>{t.target===e&&Se()})});var he=class{constructor(t){this.input=t,this.typeahead=t.closest("actor-typeahead"),this.dropdown=null,this.currentFocus=-1,this.init()}init(){this.createDropdown(),this.input.addEventListener("focus",()=>this.handleFocus()),this.input.addEventListener("input",()=>this.handleInput()),this.input.addEventListener("keydown",t=>this.handleKeydown(t)),document.addEventListener("click",t=>{!this.input.contains(t.target)&&!this.dropdown.contains(t.target)&&this.hideDropdown()})}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="recent-accounts-dropdown",this.dropdown.style.display="none",this.typeahead?this.typeahead.insertAdjacentElement("afterend",this.dropdown):this.input.insertAdjacentElement("afterend",this.dropdown)}handleFocus(){this.input.value.trim().length<1&&this.showRecentAccounts()}handleInput(){this.input.value.trim().length>=1&&this.hideDropdown()}showRecentAccounts(){let t=this.getRecentAccounts();if(t.length===0){this.hideDropdown();return}this.dropdown.innerHTML="",this.currentFocus=-1;let r=document.createElement("div");r.className="recent-accounts-header",r.textContent="Recent accounts",this.dropdown.appendChild(r),t.forEach((a,o)=>{let s=document.createElement("div");s.className="recent-accounts-item",s.dataset.index=o,s.dataset.handle=a,s.textContent=a,s.addEventListener("click",()=>this.selectItem(a)),this.dropdown.appendChild(s)}),this.dropdown.style.display="block"}selectItem(t){this.input.value=t,this.hideDropdown(),this.input.focus()}hideDropdown(){this.dropdown.style.display="none",this.currentFocus=-1}handleKeydown(t){if(this.dropdown.style.display==="none")return;let r=this.dropdown.querySelectorAll(".recent-accounts-item");t.key==="ArrowDown"?(t.preventDefault(),this.currentFocus++,this.currentFocus>=r.length&&(this.currentFocus=0),this.updateFocus(r)):t.key==="ArrowUp"?(t.preventDefault(),this.currentFocus--,this.currentFocus<0&&(this.currentFocus=r.length-1),this.updateFocus(r)):t.key==="Enter"&&this.currentFocus>-1&&r[this.currentFocus]?(t.preventDefault(),this.selectItem(r[this.currentFocus].dataset.handle)):t.key==="Escape"&&this.hideDropdown()}updateFocus(t){t.forEach((r,a)=>{r.classList.toggle("focused",a===this.currentFocus)})}getRecentAccounts(){try{let t=localStorage.getItem("atcr_recent_handles");return t?JSON.parse(t):[]}catch{return[]}}saveRecentAccount(t){if(t)try{let r=this.getRecentAccounts();r=r.filter(a=>a!==t),r.unshift(t),r=r.slice(0,5),localStorage.setItem("atcr_recent_handles",JSON.stringify(r))}catch(r){console.error("Failed to save recent account:",r)}}};document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("login-form"),t=document.getElementById("handle");e&&t&&new he(t)});document.addEventListener("DOMContentLoaded",()=>{let e=document.cookie.split("; ").find(r=>r.startsWith("atcr_login_handle="));if(!e)return;let t=decodeURIComponent(e.split("=")[1]);if(t){try{let r="atcr_recent_handles",a=JSON.parse(localStorage.getItem(r)||"[]");a=a.filter(o=>o!==t),a.unshift(t),a=a.slice(0,5),localStorage.setItem(r,JSON.stringify(a))}catch(r){console.error("Failed to save recent account:",r)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),r=document.getElementById("carousel-next");if(!e)return;let a=Array.from(e.querySelectorAll(".carousel-item"));if(a.length===0)return;let o=null,s=5e3;function l(){let x=a[0];if(!x)return 0;let d=getComputedStyle(e),p=parseFloat(d.gap)||24;return x.offsetWidth+p}function f(){let x=e.offsetWidth,d=l();return d===0?1:Math.round(x/d)}function n(){return e.scrollWidth-e.offsetWidth}function u(){let x=l(),d=n(),p=e.scrollLeft;p>=d-10?e.scrollTo({left:0,behavior:"smooth"}):e.scrollTo({left:p+x,behavior:"smooth"})}function m(){let x=l(),d=n(),p=e.scrollLeft;p<=10?e.scrollTo({left:d,behavior:"smooth"}):e.scrollTo({left:p-x,behavior:"smooth"})}t&&t.addEventListener("click",()=>{c(),m(),i()}),r&&r.addEventListener("click",()=>{c(),u(),i()});function i(){o||a.length<=f()||(o=setInterval(u,s))}function c(){o&&(clearInterval(o),o=null)}i(),e.addEventListener("mouseenter",c),e.addEventListener("mouseleave",i)});window.setTheme=Me;window.toggleSearch=_e;window.closeSearch=xe;window.copyToClipboard=ze;window.toggleOfflineManifests=$e;window.deleteManifest=Je;window.closeManifestDeleteModal=Se;window.uploadAvatar=Ze;window.htmx=Ae;var Ye={Anchor:_,AlertCircle:O,AlertTriangle:L,ArrowDownToLine:z,Box:j,Check:$,CheckCircle:q,ChevronDown:J,ChevronLeft:K,ChevronRight:Q,CircleX:P,Compass:Z,Copy:Y,Download:ee,Info:te,Loader2:I,Moon:re,Package:ae,Plus:oe,RefreshCcw:se,Search:le,ShieldCheck:fe,Ship:ne,Star:ue,Sun:de,SunMoon:ie,Terminal:me,Trash2:ce,TriangleAlert:L,XCircle:P};window.lucide={createIcons:(e={})=>pe({icons:Ye,...e})};document.addEventListener("DOMContentLoaded",()=>{window.lucide.createIcons(),document.body.addEventListener("htmx:afterSwap",()=>{window.lucide.createIcons()})}); 99 + /*! Bundled license information: 100 + 101 + lucide/dist/esm/defaultAttributes.js: 102 + lucide/dist/esm/createElement.js: 103 + lucide/dist/esm/replaceElement.js: 104 + lucide/dist/esm/icons/anchor.js: 105 + lucide/dist/esm/icons/arrow-down-to-line.js: 106 + lucide/dist/esm/icons/box.js: 107 + lucide/dist/esm/icons/check.js: 108 + lucide/dist/esm/icons/chevron-down.js: 109 + lucide/dist/esm/icons/chevron-left.js: 110 + lucide/dist/esm/icons/chevron-right.js: 111 + lucide/dist/esm/icons/circle-alert.js: 112 + lucide/dist/esm/icons/circle-check-big.js: 113 + lucide/dist/esm/icons/circle-x.js: 114 + lucide/dist/esm/icons/compass.js: 115 + lucide/dist/esm/icons/copy.js: 116 + lucide/dist/esm/icons/download.js: 117 + lucide/dist/esm/icons/info.js: 118 + lucide/dist/esm/icons/loader-circle.js: 119 + lucide/dist/esm/icons/moon.js: 120 + lucide/dist/esm/icons/package.js: 121 + lucide/dist/esm/icons/plus.js: 122 + lucide/dist/esm/icons/refresh-ccw.js: 123 + lucide/dist/esm/icons/search.js: 124 + lucide/dist/esm/icons/shield-check.js: 125 + lucide/dist/esm/icons/ship.js: 126 + lucide/dist/esm/icons/star.js: 127 + lucide/dist/esm/icons/sun-moon.js: 128 + lucide/dist/esm/icons/sun.js: 129 + lucide/dist/esm/icons/terminal.js: 130 + lucide/dist/esm/icons/trash-2.js: 131 + lucide/dist/esm/icons/triangle-alert.js: 132 + lucide/dist/esm/lucide.js: 133 + (** 134 + * @license lucide v0.562.0 - ISC 135 + * 136 + * This source code is licensed under the ISC license. 137 + * See the LICENSE file in the root directory of this source tree. 138 + *) 139 + */
+10
pkg/appview/public/static/wave-pattern.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 60" preserveAspectRatio="none"> 2 + <path 3 + fill="rgba(78, 205, 196, 0.25)" 4 + d="M0,30 C120,50 240,10 360,30 C480,50 600,10 720,30 C840,50 960,10 1080,30 C1200,50 1320,10 1440,30 L1440,60 L0,60 Z" 5 + /> 6 + <path 7 + fill="rgba(78, 205, 196, 0.15)" 8 + d="M0,35 C180,55 360,15 540,35 C720,55 900,15 1080,35 C1260,55 1440,35 1440,35 L1440,60 L0,60 Z" 9 + /> 10 + </svg>
+3
pkg/appview/routes/routes.go
··· 112 112 ).ServeHTTP) 113 113 114 114 // API routes for stars (require authentication) 115 + // Returns HTML for HTMX requests, JSON for API clients 115 116 router.Post("/api/stars/{handle}/{repository}", middleware.RequireAuth(deps.SessionStore, deps.Database)( 116 117 &uihandlers.StarRepositoryHandler{ 117 118 DB: deps.Database, // Needs write access 118 119 Directory: deps.OAuthClientApp.Dir, 119 120 Refresher: deps.Refresher, 121 + Templates: deps.Templates, 120 122 }, 121 123 ).ServeHTTP) 122 124 ··· 125 127 DB: deps.Database, // Needs write access 126 128 Directory: deps.OAuthClientApp.Dir, 127 129 Refresher: deps.Refresher, 130 + Templates: deps.Templates, 128 131 }, 129 132 ).ServeHTTP) 130 133
+143
pkg/appview/src/css/main.css
··· 1 + /* ======================================== 2 + TAILWIND + DAISYUI 3 + ======================================== */ 4 + @import "tailwindcss"; 5 + 6 + /*@layer base { 7 + .container { 8 + max-width: 1920px; 9 + } 10 + }*/ 11 + 12 + @plugin "daisyui" { 13 + themes: 14 + light --default, 15 + dark --prefersdark; 16 + } 17 + 18 + /* ======================================== 19 + BRAND COLOR OVERRIDES 20 + ======================================== */ 21 + @plugin "daisyui/theme" { 22 + name: "light"; 23 + default: true; 24 + --color-primary: oklch(75% 0.12 175); /* #4ECDC4 teal */ 25 + --color-accent: oklch(68% 0.18 25); /* #FF6B6B coral */ 26 + } 27 + 28 + @plugin "daisyui/theme" { 29 + name: "dark"; 30 + --color-primary: oklch(78% 0.12 175); /* #5ED4CB slightly brighter */ 31 + --color-accent: oklch(72% 0.16 25); /* #FF8080 */ 32 + } 33 + 34 + /* ======================================== 35 + ADDITIONAL CSS VARIABLES 36 + ======================================== */ 37 + :root { 38 + --shadow-card-hover: 39 + 0 8px 25px oklch(75% 0.12 175 / 0.15), 0 4px 12px rgba(0, 0, 0, 0.1); 40 + } 41 + 42 + [data-theme="dark"] { 43 + --shadow-card-hover: 44 + 0 8px 25px oklch(78% 0.12 175 / 0.1), 0 4px 12px rgba(0, 0, 0, 0.2); 45 + } 46 + 47 + /* ======================================== 48 + CUSTOM COMPONENTS (Not in DaisyUI) 49 + ======================================== */ 50 + @layer components { 51 + /* ---------------------------------------- 52 + COMMAND / CODE DISPLAY 53 + ---------------------------------------- */ 54 + .cmd { 55 + @apply flex items-center gap-2 relative w-full overflow-hidden; 56 + @apply bg-base-200 border border-base-300 rounded-md; 57 + @apply px-3 py-2; 58 + } 59 + 60 + .cmd code { 61 + @apply font-mono text-sm truncate; 62 + } 63 + 64 + /* ---------------------------------------- 65 + EXPANDABLE SEARCH (nav-specific) 66 + ---------------------------------------- */ 67 + .nav-search-wrapper { 68 + @apply relative flex items-center; 69 + } 70 + 71 + .nav-search-form { 72 + @apply absolute right-full mr-2; 73 + @apply w-0 opacity-0 overflow-hidden; 74 + @apply transition-all duration-300; 75 + } 76 + 77 + .nav-search-wrapper.expanded .nav-search-form { 78 + @apply w-62 opacity-100; 79 + } 80 + 81 + /* ---------------------------------------- 82 + CARD EXTENSIONS 83 + ---------------------------------------- */ 84 + .card-interactive { 85 + @apply cursor-pointer transition-all duration-500; 86 + } 87 + 88 + .card-interactive:hover { 89 + box-shadow: var(--shadow-card-hover); 90 + transform: translateY(-2px); 91 + } 92 + 93 + /* ---------------------------------------- 94 + ACTOR-TYPEAHEAD COMPONENT STYLING 95 + ---------------------------------------- */ 96 + actor-typeahead { 97 + /* Use DaisyUI CSS variables - they auto-switch with theme */ 98 + --color-background: var(--color-base-100); 99 + --color-border: var(--color-base-300); 100 + --color-shadow: var(--color-base-content); 101 + --color-hover: var(--color-base-200); 102 + --color-avatar-fallback: var(--color-base-300); 103 + --radius: 0.5rem; 104 + --padding-menu: 0.25rem; 105 + z-index: 50; 106 + } 107 + 108 + actor-typeahead::part(handle) { 109 + @apply text-base-content; 110 + } 111 + 112 + actor-typeahead::part(menu) { 113 + @apply shadow-lg; 114 + margin-top: 0.25rem; 115 + } 116 + 117 + /* ---------------------------------------- 118 + RECENT ACCOUNTS DROPDOWN 119 + ---------------------------------------- */ 120 + .recent-accounts-dropdown { 121 + @apply absolute top-full left-0 right-0; 122 + @apply bg-base-100 border border-base-300; 123 + @apply rounded-lg shadow-lg; 124 + @apply max-h-60 overflow-y-auto z-50; 125 + margin-top: 0.25rem; 126 + } 127 + 128 + .recent-accounts-header { 129 + @apply px-3 py-2 text-xs font-semibold uppercase; 130 + @apply text-base-content/60 border-b border-base-300; 131 + } 132 + 133 + .recent-accounts-item { 134 + @apply px-3 py-2.5; 135 + @apply cursor-pointer transition-colors duration-150; 136 + @apply text-base-content; 137 + } 138 + 139 + .recent-accounts-item:hover, 140 + .recent-accounts-item.focused { 141 + @apply bg-base-200; 142 + } 143 + }
+666
pkg/appview/src/js/app.js
··· 1 + // Theme management (system / light / dark) 2 + function getThemePreference() { 3 + return localStorage.getItem('theme') || 'system'; 4 + } 5 + 6 + function getEffectiveTheme(pref) { 7 + if (pref === 'dark') return 'dark'; 8 + if (pref === 'light') return 'light'; 9 + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 10 + } 11 + 12 + function applyTheme() { 13 + const pref = getThemePreference(); 14 + const effective = getEffectiveTheme(pref); 15 + 16 + document.documentElement.classList.toggle('dark', effective === 'dark'); 17 + document.documentElement.setAttribute('data-theme', effective); 18 + 19 + updateThemeUI(pref); 20 + } 21 + 22 + function setTheme(theme) { 23 + localStorage.setItem('theme', theme); 24 + applyTheme(); 25 + closeThemeDropdown(); 26 + } 27 + 28 + function updateThemeUI(pref) { 29 + // Update nav button icon to show selected preference 30 + const iconMap = { system: 'sun-moon', light: 'sun', dark: 'moon' }; 31 + const icon = document.getElementById('theme-icon'); 32 + if (icon) { 33 + icon.setAttribute('data-lucide', iconMap[pref] || 'sun-moon'); 34 + if (typeof window.lucide !== 'undefined') { 35 + window.lucide.createIcons(); 36 + } 37 + } 38 + 39 + // Update checkmarks in dropdown 40 + document.querySelectorAll('.theme-option').forEach(option => { 41 + const isSelected = option.dataset.value === pref; 42 + const check = option.querySelector('.theme-check'); 43 + if (check) { 44 + check.style.visibility = isSelected ? 'visible' : 'hidden'; 45 + } 46 + }); 47 + } 48 + 49 + function closeThemeDropdown() { 50 + const btn = document.getElementById('theme-toggle-btn'); 51 + const details = btn?.closest('details'); 52 + if (details) details.removeAttribute('open'); 53 + } 54 + 55 + // Listen for system theme changes 56 + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { 57 + if (getThemePreference() === 'system') { 58 + applyTheme(); 59 + } 60 + }); 61 + 62 + // Expandable search 63 + function toggleSearch() { 64 + const wrapper = document.querySelector('.nav-search-wrapper'); 65 + const input = document.getElementById('nav-search-input'); 66 + 67 + if (!wrapper || !input) return; 68 + 69 + wrapper.classList.toggle('expanded'); 70 + 71 + if (wrapper.classList.contains('expanded')) { 72 + input.focus(); 73 + } 74 + } 75 + 76 + function closeSearch() { 77 + const wrapper = document.querySelector('.nav-search-wrapper'); 78 + if (wrapper) { 79 + wrapper.classList.remove('expanded'); 80 + } 81 + } 82 + 83 + // Close search on Escape key and click outside 84 + document.addEventListener('DOMContentLoaded', () => { 85 + const wrapper = document.querySelector('.nav-search-wrapper'); 86 + const input = document.getElementById('nav-search-input'); 87 + 88 + if (!wrapper || !input) return; 89 + 90 + // Close on Escape key 91 + document.addEventListener('keydown', (e) => { 92 + if (e.key === 'Escape' && wrapper.classList.contains('expanded')) { 93 + closeSearch(); 94 + } 95 + }); 96 + 97 + // Close on click outside 98 + document.addEventListener('click', (e) => { 99 + if (wrapper.classList.contains('expanded') && 100 + !wrapper.contains(e.target)) { 101 + closeSearch(); 102 + } 103 + }); 104 + }); 105 + 106 + // Copy to clipboard 107 + function copyToClipboard(text) { 108 + navigator.clipboard.writeText(text).then(() => { 109 + // Show success feedback 110 + const btn = event.target.closest('button'); 111 + const originalHTML = btn.innerHTML; 112 + btn.innerHTML = '<i data-lucide="check"></i> Copied!'; 113 + // Re-initialize Lucide icons for the new icon 114 + if (typeof window.lucide !== 'undefined') { 115 + window.lucide.createIcons(); 116 + } 117 + setTimeout(() => { 118 + btn.innerHTML = originalHTML; 119 + // Re-initialize Lucide icons to restore original icon 120 + if (typeof window.lucide !== 'undefined') { 121 + window.lucide.createIcons(); 122 + } 123 + }, 2000); 124 + }).catch(err => { 125 + console.error('Failed to copy:', err); 126 + }); 127 + } 128 + 129 + // Time ago helper (for client-side rendering) 130 + function timeAgo(date) { 131 + const seconds = Math.floor((new Date() - new Date(date)) / 1000); 132 + 133 + const intervals = { 134 + year: 31536000, 135 + month: 2592000, 136 + week: 604800, 137 + day: 86400, 138 + hour: 3600, 139 + minute: 60, 140 + second: 1 141 + }; 142 + 143 + for (const [name, secondsInInterval] of Object.entries(intervals)) { 144 + const interval = Math.floor(seconds / secondsInInterval); 145 + if (interval >= 1) { 146 + return interval === 1 ? `1 ${name} ago` : `${interval} ${name}s ago`; 147 + } 148 + } 149 + 150 + return 'just now'; 151 + } 152 + 153 + // Update timestamps on page load and HTMX swaps 154 + function updateTimestamps() { 155 + document.querySelectorAll('time[datetime]').forEach(el => { 156 + const date = el.getAttribute('datetime'); 157 + if (date && !el.dataset.noUpdate) { 158 + const ago = timeAgo(date); 159 + if (el.textContent !== ago) { 160 + el.textContent = ago; 161 + } 162 + } 163 + }); 164 + } 165 + 166 + // Initial timestamp update and theme setup 167 + document.addEventListener('DOMContentLoaded', () => { 168 + updateTimestamps(); 169 + applyTheme(); 170 + 171 + // Theme dropdown setup - DaisyUI details handles open/close natively 172 + const themeMenu = document.getElementById('theme-dropdown-menu'); 173 + 174 + if (themeMenu) { 175 + // Handle theme option clicks 176 + themeMenu.querySelectorAll('.theme-option').forEach(option => { 177 + option.addEventListener('click', () => { 178 + setTheme(option.dataset.value); 179 + }); 180 + }); 181 + } 182 + }); 183 + 184 + // Update timestamps after HTMX swaps 185 + document.addEventListener('htmx:afterSwap', updateTimestamps); 186 + 187 + // Update timestamps periodically 188 + setInterval(updateTimestamps, 60000); // Every minute 189 + 190 + // Toggle offline manifests visibility 191 + function toggleOfflineManifests() { 192 + const checkbox = document.getElementById('show-offline-toggle'); 193 + const manifestsList = document.querySelector('.manifests-list'); 194 + 195 + if (!checkbox || !manifestsList) return; 196 + 197 + // Store preference in localStorage 198 + localStorage.setItem('showOfflineManifests', checkbox.checked); 199 + 200 + // Toggle visibility of offline manifests 201 + if (checkbox.checked) { 202 + manifestsList.classList.add('show-offline'); 203 + } else { 204 + manifestsList.classList.remove('show-offline'); 205 + } 206 + } 207 + 208 + // Restore offline manifests toggle state on page load 209 + document.addEventListener('DOMContentLoaded', () => { 210 + const checkbox = document.getElementById('show-offline-toggle'); 211 + if (!checkbox) return; 212 + 213 + // Restore state from localStorage 214 + const showOffline = localStorage.getItem('showOfflineManifests') === 'true'; 215 + checkbox.checked = showOffline; 216 + 217 + // Apply initial state 218 + const manifestsList = document.querySelector('.manifests-list'); 219 + if (manifestsList) { 220 + if (showOffline) { 221 + manifestsList.classList.add('show-offline'); 222 + } else { 223 + manifestsList.classList.remove('show-offline'); 224 + } 225 + } 226 + }); 227 + 228 + // Delete manifest with confirmation for tagged manifests 229 + async function deleteManifest(repository, digest, sanitizedId) { 230 + try { 231 + // First, try to delete without confirmation 232 + const response = await fetch(`/api/images/${repository}/manifests/${digest}`, { 233 + method: 'DELETE', 234 + credentials: 'include', 235 + }); 236 + 237 + if (response.status === 409) { 238 + // Manifest has tags, need confirmation 239 + const data = await response.json(); 240 + showManifestDeleteModal(repository, digest, sanitizedId, data.tags); 241 + } else if (response.ok) { 242 + // Successfully deleted 243 + removeManifestElement(sanitizedId); 244 + } else { 245 + // Other error 246 + const errorText = await response.text(); 247 + alert(`Failed to delete manifest: ${errorText}`); 248 + } 249 + } catch (err) { 250 + console.error('Error deleting manifest:', err); 251 + alert(`Error deleting manifest: ${err.message}`); 252 + } 253 + } 254 + 255 + // Show the confirmation modal for deleting a tagged manifest 256 + function showManifestDeleteModal(repository, digest, sanitizedId, tags) { 257 + const modal = document.getElementById('manifest-delete-modal'); 258 + const tagsList = document.getElementById('manifest-delete-tags'); 259 + const confirmBtn = document.getElementById('confirm-manifest-delete-btn'); 260 + 261 + // Clear and populate tags list 262 + tagsList.innerHTML = ''; 263 + tags.forEach(tag => { 264 + const li = document.createElement('li'); 265 + li.textContent = tag; 266 + tagsList.appendChild(li); 267 + }); 268 + 269 + // Set up confirm button click handler 270 + confirmBtn.onclick = () => confirmManifestDelete(repository, digest, sanitizedId); 271 + 272 + // Show modal 273 + modal.style.display = 'flex'; 274 + } 275 + 276 + // Close the manifest delete confirmation modal 277 + function closeManifestDeleteModal() { 278 + const modal = document.getElementById('manifest-delete-modal'); 279 + modal.style.display = 'none'; 280 + } 281 + 282 + // Confirm and execute manifest deletion with all tags 283 + async function confirmManifestDelete(repository, digest, sanitizedId) { 284 + const confirmBtn = document.getElementById('confirm-manifest-delete-btn'); 285 + const originalText = confirmBtn.textContent; 286 + 287 + try { 288 + // Disable button and show loading state 289 + confirmBtn.disabled = true; 290 + confirmBtn.textContent = 'Deleting...'; 291 + 292 + // Delete with confirmation 293 + const response = await fetch(`/api/images/${repository}/manifests/${digest}?confirm=true`, { 294 + method: 'DELETE', 295 + credentials: 'include', 296 + }); 297 + 298 + if (response.ok) { 299 + // Successfully deleted 300 + closeManifestDeleteModal(); 301 + removeManifestElement(sanitizedId); 302 + // Also remove any tag elements that were deleted 303 + location.reload(); // Reload to refresh the tags list 304 + } else { 305 + // Error 306 + const errorText = await response.text(); 307 + alert(`Failed to delete manifest: ${errorText}`); 308 + confirmBtn.disabled = false; 309 + confirmBtn.textContent = originalText; 310 + } 311 + } catch (err) { 312 + console.error('Error deleting manifest:', err); 313 + alert(`Error deleting manifest: ${err.message}`); 314 + confirmBtn.disabled = false; 315 + confirmBtn.textContent = originalText; 316 + } 317 + } 318 + 319 + // Remove a manifest element from the DOM 320 + function removeManifestElement(sanitizedId) { 321 + const element = document.getElementById(`manifest-${sanitizedId}`); 322 + if (element) { 323 + element.remove(); 324 + } 325 + } 326 + 327 + // Upload repository avatar 328 + async function uploadAvatar(input, repository) { 329 + const file = input.files[0]; 330 + if (!file) return; 331 + 332 + // Client-side validation 333 + const validTypes = ['image/png', 'image/jpeg', 'image/webp']; 334 + if (!validTypes.includes(file.type)) { 335 + alert('Please select a PNG, JPEG, or WebP image'); 336 + return; 337 + } 338 + if (file.size > 3 * 1024 * 1024) { 339 + alert('Image must be less than 3MB'); 340 + return; 341 + } 342 + 343 + const formData = new FormData(); 344 + formData.append('avatar', file); 345 + 346 + try { 347 + const response = await fetch(`/api/images/${repository}/avatar`, { 348 + method: 'POST', 349 + credentials: 'include', 350 + body: formData 351 + }); 352 + 353 + if (response.status === 401) { 354 + window.location.href = '/auth/oauth/login'; 355 + return; 356 + } 357 + 358 + if (!response.ok) { 359 + const error = await response.text(); 360 + throw new Error(error); 361 + } 362 + 363 + const data = await response.json(); 364 + 365 + // Update the avatar image on the page 366 + const wrapper = document.querySelector('.repo-hero-icon-wrapper'); 367 + if (!wrapper) return; 368 + 369 + const existingImg = wrapper.querySelector('.repo-hero-icon'); 370 + const placeholder = wrapper.querySelector('.repo-hero-icon-placeholder'); 371 + 372 + if (existingImg) { 373 + existingImg.src = data.avatarURL; 374 + } else if (placeholder) { 375 + const newImg = document.createElement('img'); 376 + newImg.src = data.avatarURL; 377 + newImg.alt = repository; 378 + newImg.className = 'repo-hero-icon'; 379 + placeholder.replaceWith(newImg); 380 + } 381 + } catch (err) { 382 + console.error('Error uploading avatar:', err); 383 + alert('Failed to upload avatar: ' + err.message); 384 + } 385 + 386 + // Clear input so same file can be selected again 387 + input.value = ''; 388 + } 389 + 390 + // Close modal when clicking outside 391 + document.addEventListener('DOMContentLoaded', () => { 392 + const modal = document.getElementById('manifest-delete-modal'); 393 + if (modal) { 394 + modal.addEventListener('click', (e) => { 395 + if (e.target === modal) { 396 + closeManifestDeleteModal(); 397 + } 398 + }); 399 + } 400 + }); 401 + 402 + // Login page recent accounts helper (works alongside actor-typeahead web component) 403 + class RecentAccountsHelper { 404 + constructor(inputElement) { 405 + this.input = inputElement; 406 + this.typeahead = inputElement.closest('actor-typeahead'); 407 + this.dropdown = null; 408 + this.currentFocus = -1; 409 + this.init(); 410 + } 411 + 412 + init() { 413 + this.createDropdown(); 414 + 415 + // Show recent accounts on focus when input is empty 416 + this.input.addEventListener('focus', () => this.handleFocus()); 417 + 418 + // Hide recent accounts when user starts typing (actor-typeahead takes over) 419 + this.input.addEventListener('input', () => this.handleInput()); 420 + 421 + // Keyboard navigation for recent accounts dropdown 422 + this.input.addEventListener('keydown', (e) => this.handleKeydown(e)); 423 + 424 + // Close dropdown when clicking outside 425 + document.addEventListener('click', (e) => { 426 + if (!this.input.contains(e.target) && !this.dropdown.contains(e.target)) { 427 + this.hideDropdown(); 428 + } 429 + }); 430 + } 431 + 432 + createDropdown() { 433 + this.dropdown = document.createElement('div'); 434 + this.dropdown.className = 'recent-accounts-dropdown'; 435 + this.dropdown.style.display = 'none'; 436 + // Insert after the actor-typeahead element 437 + if (this.typeahead) { 438 + this.typeahead.insertAdjacentElement('afterend', this.dropdown); 439 + } else { 440 + this.input.insertAdjacentElement('afterend', this.dropdown); 441 + } 442 + } 443 + 444 + handleFocus() { 445 + const value = this.input.value.trim(); 446 + if (value.length < 1) { 447 + this.showRecentAccounts(); 448 + } 449 + } 450 + 451 + handleInput() { 452 + const value = this.input.value.trim(); 453 + // Hide recent accounts once user starts typing (actor-typeahead shows its menu at 2+ chars) 454 + if (value.length >= 1) { 455 + this.hideDropdown(); 456 + } 457 + } 458 + 459 + showRecentAccounts() { 460 + const recent = this.getRecentAccounts(); 461 + if (recent.length === 0) { 462 + this.hideDropdown(); 463 + return; 464 + } 465 + 466 + this.dropdown.innerHTML = ''; 467 + this.currentFocus = -1; 468 + 469 + const header = document.createElement('div'); 470 + header.className = 'recent-accounts-header'; 471 + header.textContent = 'Recent accounts'; 472 + this.dropdown.appendChild(header); 473 + 474 + recent.forEach((handle, index) => { 475 + const item = document.createElement('div'); 476 + item.className = 'recent-accounts-item'; 477 + item.dataset.index = index; 478 + item.dataset.handle = handle; 479 + item.textContent = handle; 480 + item.addEventListener('click', () => this.selectItem(handle)); 481 + this.dropdown.appendChild(item); 482 + }); 483 + 484 + this.dropdown.style.display = 'block'; 485 + } 486 + 487 + selectItem(handle) { 488 + this.input.value = handle; 489 + this.hideDropdown(); 490 + this.input.focus(); 491 + } 492 + 493 + hideDropdown() { 494 + this.dropdown.style.display = 'none'; 495 + this.currentFocus = -1; 496 + } 497 + 498 + handleKeydown(e) { 499 + if (this.dropdown.style.display === 'none') return; 500 + 501 + const items = this.dropdown.querySelectorAll('.recent-accounts-item'); 502 + 503 + if (e.key === 'ArrowDown') { 504 + e.preventDefault(); 505 + this.currentFocus++; 506 + if (this.currentFocus >= items.length) this.currentFocus = 0; 507 + this.updateFocus(items); 508 + } else if (e.key === 'ArrowUp') { 509 + e.preventDefault(); 510 + this.currentFocus--; 511 + if (this.currentFocus < 0) this.currentFocus = items.length - 1; 512 + this.updateFocus(items); 513 + } else if (e.key === 'Enter' && this.currentFocus > -1 && items[this.currentFocus]) { 514 + e.preventDefault(); 515 + this.selectItem(items[this.currentFocus].dataset.handle); 516 + } else if (e.key === 'Escape') { 517 + this.hideDropdown(); 518 + } 519 + } 520 + 521 + updateFocus(items) { 522 + items.forEach((item, index) => { 523 + item.classList.toggle('focused', index === this.currentFocus); 524 + }); 525 + } 526 + 527 + getRecentAccounts() { 528 + try { 529 + const recent = localStorage.getItem('atcr_recent_handles'); 530 + return recent ? JSON.parse(recent) : []; 531 + } catch { 532 + return []; 533 + } 534 + } 535 + 536 + saveRecentAccount(handle) { 537 + if (!handle) return; 538 + try { 539 + let recent = this.getRecentAccounts(); 540 + recent = recent.filter(h => h !== handle); 541 + recent.unshift(handle); 542 + recent = recent.slice(0, 5); 543 + localStorage.setItem('atcr_recent_handles', JSON.stringify(recent)); 544 + } catch (err) { 545 + console.error('Failed to save recent account:', err); 546 + } 547 + } 548 + } 549 + 550 + // Initialize recent accounts helper on login page 551 + document.addEventListener('DOMContentLoaded', () => { 552 + const loginForm = document.getElementById('login-form'); 553 + const handleInput = document.getElementById('handle'); 554 + if (loginForm && handleInput) { 555 + new RecentAccountsHelper(handleInput); 556 + } 557 + }); 558 + 559 + // Save successful login handle from cookie (set by server after OAuth success) 560 + document.addEventListener('DOMContentLoaded', () => { 561 + const cookie = document.cookie.split('; ').find(c => c.startsWith('atcr_login_handle=')); 562 + if (!cookie) return; 563 + 564 + const handle = decodeURIComponent(cookie.split('=')[1]); 565 + if (handle) { 566 + // Save to recent accounts 567 + try { 568 + const key = 'atcr_recent_handles'; 569 + let recent = JSON.parse(localStorage.getItem(key) || '[]'); 570 + recent = recent.filter(h => h !== handle); 571 + recent.unshift(handle); 572 + recent = recent.slice(0, 5); 573 + localStorage.setItem(key, JSON.stringify(recent)); 574 + } catch (err) { 575 + console.error('Failed to save recent account:', err); 576 + } 577 + 578 + // Delete the cookie 579 + document.cookie = 'atcr_login_handle=; path=/; max-age=0'; 580 + } 581 + }); 582 + 583 + // Featured carousel - scroll-based with proper wrap-around 584 + document.addEventListener('DOMContentLoaded', () => { 585 + const carousel = document.getElementById('featured-carousel'); 586 + const prevBtn = document.getElementById('carousel-prev'); 587 + const nextBtn = document.getElementById('carousel-next'); 588 + if (!carousel) return; 589 + 590 + const items = Array.from(carousel.querySelectorAll('.carousel-item')); 591 + if (items.length === 0) return; 592 + 593 + let intervalId = null; 594 + const intervalMs = 5000; 595 + 596 + function getItemWidth() { 597 + const item = items[0]; 598 + if (!item) return 0; 599 + const style = getComputedStyle(carousel); 600 + const gap = parseFloat(style.gap) || 24; 601 + return item.offsetWidth + gap; 602 + } 603 + 604 + function getVisibleCount() { 605 + const containerWidth = carousel.offsetWidth; 606 + const itemWidth = getItemWidth(); 607 + if (itemWidth === 0) return 1; 608 + return Math.round(containerWidth / itemWidth); 609 + } 610 + 611 + function getMaxScroll() { 612 + return carousel.scrollWidth - carousel.offsetWidth; 613 + } 614 + 615 + function advance() { 616 + const itemWidth = getItemWidth(); 617 + const maxScroll = getMaxScroll(); 618 + const currentScroll = carousel.scrollLeft; 619 + 620 + // If we're at or near the end, wrap to start 621 + if (currentScroll >= maxScroll - 10) { 622 + carousel.scrollTo({ left: 0, behavior: 'smooth' }); 623 + } else { 624 + carousel.scrollTo({ left: currentScroll + itemWidth, behavior: 'smooth' }); 625 + } 626 + } 627 + 628 + function retreat() { 629 + const itemWidth = getItemWidth(); 630 + const maxScroll = getMaxScroll(); 631 + const currentScroll = carousel.scrollLeft; 632 + 633 + // If we're at or near the start, wrap to end 634 + if (currentScroll <= 10) { 635 + carousel.scrollTo({ left: maxScroll, behavior: 'smooth' }); 636 + } else { 637 + carousel.scrollTo({ left: currentScroll - itemWidth, behavior: 'smooth' }); 638 + } 639 + } 640 + 641 + if (prevBtn) prevBtn.addEventListener('click', () => { stopInterval(); retreat(); startInterval(); }); 642 + if (nextBtn) nextBtn.addEventListener('click', () => { stopInterval(); advance(); startInterval(); }); 643 + 644 + function startInterval() { 645 + if (intervalId || items.length <= getVisibleCount()) return; 646 + intervalId = setInterval(advance, intervalMs); 647 + } 648 + 649 + function stopInterval() { 650 + if (intervalId) { clearInterval(intervalId); intervalId = null; } 651 + } 652 + 653 + startInterval(); 654 + carousel.addEventListener('mouseenter', stopInterval); 655 + carousel.addEventListener('mouseleave', startInterval); 656 + }); 657 + 658 + // Export functions that are called from templates via onclick handlers 659 + window.setTheme = setTheme; 660 + window.toggleSearch = toggleSearch; 661 + window.closeSearch = closeSearch; 662 + window.copyToClipboard = copyToClipboard; 663 + window.toggleOfflineManifests = toggleOfflineManifests; 664 + window.deleteManifest = deleteManifest; 665 + window.closeManifestDeleteModal = closeManifestDeleteModal; 666 + window.uploadAvatar = uploadAvatar;
+93
pkg/appview/src/js/main.js
··· 1 + // HTMX 2 + import htmx from 'htmx.org'; 3 + window.htmx = htmx; 4 + 5 + // Actor Typeahead (web component, auto-registers on import) 6 + import 'actor-typeahead'; 7 + 8 + // Lucide Icons (tree-shaken - only icons actually used in templates) 9 + import { createIcons } from 'lucide'; 10 + import { 11 + Anchor, 12 + AlertCircle, 13 + AlertTriangle, 14 + ArrowDownToLine, 15 + Box, 16 + Check, 17 + CheckCircle, 18 + ChevronDown, 19 + ChevronLeft, 20 + ChevronRight, 21 + CircleX, 22 + Compass, 23 + Copy, 24 + Download, 25 + Info, 26 + Loader2, 27 + Moon, 28 + Package, 29 + Plus, 30 + RefreshCcw, 31 + Search, 32 + ShieldCheck, 33 + Ship, 34 + Star, 35 + Sun, 36 + SunMoon, 37 + Terminal, 38 + Trash2, 39 + TriangleAlert, 40 + XCircle, 41 + } from 'lucide'; 42 + 43 + // Create icons map for createIcons function 44 + const icons = { 45 + Anchor, 46 + AlertCircle, 47 + AlertTriangle, 48 + ArrowDownToLine, 49 + Box, 50 + Check, 51 + CheckCircle, 52 + ChevronDown, 53 + ChevronLeft, 54 + ChevronRight, 55 + CircleX, 56 + Compass, 57 + Copy, 58 + Download, 59 + Info, 60 + Loader2, 61 + Moon, 62 + Package, 63 + Plus, 64 + RefreshCcw, 65 + Search, 66 + ShieldCheck, 67 + Ship, 68 + Star, 69 + Sun, 70 + SunMoon, 71 + Terminal, 72 + Trash2, 73 + TriangleAlert, 74 + XCircle, 75 + }; 76 + 77 + // Export lucide to window for templates that use lucide.createIcons() 78 + window.lucide = { 79 + createIcons: (opts = {}) => createIcons({ icons, ...opts }), 80 + }; 81 + 82 + // Import app functionality 83 + import './app.js'; 84 + 85 + // Initialize icons on DOM load 86 + document.addEventListener('DOMContentLoaded', () => { 87 + window.lucide.createIcons(); 88 + 89 + // Re-initialize icons after HTMX swaps content 90 + document.body.addEventListener('htmx:afterSwap', () => { 91 + window.lucide.createIcons(); 92 + }); 93 + });
-2653
pkg/appview/static/css/style.css
··· 1 - :root { 2 - --primary: #0066cc; 3 - --button-primary: #0066cc; 4 - --primary-dark: #0052a3; 5 - --secondary: #6c757d; 6 - --success: #28a745; 7 - --success-bg: #d4edda; 8 - --warning: #ffc107; 9 - --warning-bg: #fff3cd; 10 - --danger: #dc3545; 11 - --danger-bg: #f8d7da; 12 - --bg: #ffffff; 13 - --fg: #1a1a1a; 14 - --border-dark: #666; 15 - --border: #e0e0e0; 16 - --code-bg: #f5f5f5; 17 - --hover-bg: #f9f9f9; 18 - --star: #fbbf24; 19 - 20 - /* Navbar colors - stay consistent in dark mode */ 21 - --navbar-bg: #1a1a1a; 22 - --navbar-fg: #ffffff; 23 - 24 - /* Button text color */ 25 - --btn-text: #ffffff; 26 - 27 - /* Shadows */ 28 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); 29 - --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.1); 30 - --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15); 31 - 32 - /* Metadata badge */ 33 - --metadata-badge-bg: #f0f0f0; 34 - --metadata-badge-text: var(--fg); 35 - 36 - /* Version badge */ 37 - --version-badge-bg: #f3e5f5; 38 - --version-badge-text: #7b1fa2; 39 - --version-badge-border: #ba68c8; 40 - 41 - /* Attestation badge */ 42 - --attestation-badge-bg: #d1fae5; 43 - --attestation-badge-text: #065f46; 44 - 45 - /* Hero section colors */ 46 - --hero-bg-start: #f8f9fa; 47 - --hero-bg-end: #e9ecef; 48 - 49 - /* Terminal colors */ 50 - --terminal-bg: var(--fg); 51 - --terminal-header-bg: #2d2d2d; 52 - --terminal-text: var(--border); 53 - --terminal-prompt: #4ec9b0; 54 - --terminal-comment: #6a9955; 55 - } 56 - 57 - [data-theme="dark"] { 58 - --primary: #60a5fa; 59 - --button-primary: #1d4ed8; 60 - --primary-dark: #1e40af; 61 - --secondary: #9ca3af; 62 - --success: #34d399; 63 - --success-bg: #064e3b; 64 - --warning: #fbbf24; 65 - --warning-bg: #422006; 66 - --danger: #dc3545; 67 - --danger-bg: #7f1d1d; 68 - --bg: #2a2a2a; 69 - --fg: #e0e0e0; 70 - --border-dark: #9ca3af; 71 - --border: #404040; 72 - --code-bg: #1e1e1e; 73 - --hover-bg: #333333; 74 - --star: #fbbf24; 75 - 76 - /* Navbar colors - stay consistent (always black) */ 77 - --navbar-bg: #1a1a1a; 78 - --navbar-fg: #ffffff; 79 - 80 - /* Button text color */ 81 - --btn-text: #ffffff; 82 - 83 - /* Shadows - lighter for dark backgrounds */ 84 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 85 - --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.4); 86 - --shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.5); 87 - 88 - /* Metadata badge - darker in dark mode */ 89 - --metadata-badge-bg: #1e1e1e; 90 - --metadata-badge-text: #ffffff; 91 - 92 - /* Version badge - swapped colors with softer purple background */ 93 - --version-badge-bg: #9b59b6; 94 - --version-badge-text: #ffffff; 95 - --version-badge-border: #ba68c8; 96 - 97 - /* Attestation badge */ 98 - --attestation-badge-bg: #065f46; 99 - --attestation-badge-text: #6ee7b7; 100 - 101 - /* Hero section colors */ 102 - --hero-bg-start: #2d2d2d; 103 - --hero-bg-end: #1a1a1a; 104 - 105 - /* Terminal colors - keep similar since already dark */ 106 - --terminal-bg: #0d0d0d; 107 - --terminal-header-bg: #1a1a1a; 108 - --terminal-text: #d0d0d0; 109 - --terminal-prompt: #4ec9b0; 110 - --terminal-comment: #6a9955; 111 - } 112 - 113 - * { 114 - margin: 0; 115 - padding: 0; 116 - box-sizing: border-box; 117 - } 118 - 119 - body { 120 - font-family: 121 - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", 122 - Arial, sans-serif; 123 - background: var(--bg); 124 - color: var(--fg); 125 - line-height: 1.6; 126 - } 127 - 128 - .container { 129 - max-width: 1920px; 130 - margin: 0 auto; 131 - padding: 20px; 132 - } 133 - 134 - /* Navigation */ 135 - .navbar { 136 - background: var(--navbar-bg); 137 - color: var(--navbar-fg); 138 - padding: 1rem 2rem; 139 - display: flex; 140 - justify-content: space-between; 141 - align-items: center; 142 - box-shadow: var(--shadow-md); 143 - } 144 - 145 - .nav-brand a { 146 - color: var(--navbar-fg); 147 - text-decoration: none; 148 - font-size: 1.5rem; 149 - font-weight: bold; 150 - } 151 - 152 - .nav-brand .at-protocol { 153 - color: var(--primary); 154 - } 155 - 156 - /* Expandable search */ 157 - .nav-search-wrapper { 158 - position: relative; 159 - display: flex; 160 - align-items: center; 161 - } 162 - 163 - .search-toggle-btn { 164 - display: inline-flex; 165 - align-items: center; 166 - justify-content: center; 167 - background: transparent; 168 - border: none; 169 - color: var(--navbar-fg); 170 - cursor: pointer; 171 - padding: 0.5rem; 172 - border-radius: 4px; 173 - } 174 - 175 - .search-toggle-btn:hover { 176 - background: var(--secondary); 177 - } 178 - 179 - .search-toggle-btn .search-icon { 180 - width: 1.25rem; 181 - height: 1.25rem; 182 - } 183 - 184 - .nav-search-form { 185 - position: absolute; 186 - right: 100%; 187 - width: 0; 188 - opacity: 0; 189 - overflow: hidden; 190 - transition: width 0.3s ease, opacity 0.3s ease; 191 - margin-right: 0.5rem; 192 - } 193 - 194 - .nav-search-wrapper.expanded .nav-search-form { 195 - width: 250px; 196 - opacity: 1; 197 - } 198 - 199 - .nav-search-form input { 200 - width: 100%; 201 - padding: 0.5rem 1rem; 202 - border: none; 203 - border-radius: 4px; 204 - font-size: 0.95rem; 205 - background: var(--bg); 206 - color: var(--fg); 207 - } 208 - 209 - .nav-search-form input:focus { 210 - outline: 2px solid var(--primary); 211 - outline-offset: -2px; 212 - } 213 - 214 - .nav-links { 215 - display: flex; 216 - gap: 1rem; 217 - align-items: center; 218 - } 219 - 220 - .nav-links a { 221 - color: var(--navbar-fg); 222 - text-decoration: none; 223 - padding: 0.5rem 1rem; 224 - } 225 - 226 - .nav-links a:hover { 227 - background: var(--secondary); 228 - border-radius: 4px; 229 - } 230 - 231 - /* User dropdown */ 232 - .user-dropdown { 233 - position: relative; 234 - } 235 - 236 - .user-menu-btn { 237 - display: flex; 238 - align-items: center; 239 - gap: 0.5rem; 240 - background: transparent; 241 - color: var(--navbar-fg); 242 - border: none; 243 - padding: 0.5rem; 244 - cursor: pointer; 245 - border-radius: 4px; 246 - transition: background 0.2s; 247 - } 248 - 249 - .user-menu-btn:hover { 250 - background: var(--secondary); 251 - } 252 - 253 - .user-avatar { 254 - width: 32px; 255 - height: 32px; 256 - border-radius: 50%; 257 - object-fit: cover; 258 - } 259 - 260 - .user-avatar-placeholder { 261 - width: 32px; 262 - height: 32px; 263 - border-radius: 50%; 264 - background: var(--button-primary); 265 - display: flex; 266 - align-items: center; 267 - justify-content: center; 268 - font-weight: bold; 269 - text-transform: uppercase; 270 - } 271 - 272 - /* Profile page avatars */ 273 - .profile-avatar { 274 - width: 80px; 275 - height: 80px; 276 - border-radius: 50%; 277 - object-fit: cover; 278 - } 279 - 280 - .profile-avatar-placeholder { 281 - width: 80px; 282 - height: 80px; 283 - border-radius: 50%; 284 - background: var(--button-primary); 285 - display: flex; 286 - align-items: center; 287 - justify-content: center; 288 - font-weight: bold; 289 - font-size: 2rem; 290 - text-transform: uppercase; 291 - color: var(--btn-text); 292 - } 293 - 294 - .user-profile { 295 - display: flex; 296 - align-items: center; 297 - gap: 1rem; 298 - margin-bottom: 2rem; 299 - } 300 - 301 - .user-profile h1 { 302 - font-size: 1.8rem; 303 - margin: 0; 304 - } 305 - 306 - .user-handle { 307 - color: var(--navbar-fg); 308 - font-size: 0.95rem; 309 - } 310 - 311 - .dropdown-arrow { 312 - transition: transform 0.2s; 313 - } 314 - 315 - .user-menu-btn[aria-expanded="true"] .dropdown-arrow { 316 - transform: rotate(180deg); 317 - } 318 - 319 - .dropdown-menu { 320 - position: absolute; 321 - top: calc(100% + 0.5rem); 322 - right: 0; 323 - background: var(--bg); 324 - border: 1px solid var(--border); 325 - border-radius: 8px; 326 - box-shadow: var(--shadow-lg); 327 - min-width: 200px; 328 - overflow: hidden; 329 - z-index: 1000; 330 - } 331 - 332 - .dropdown-menu[hidden] { 333 - display: none; 334 - } 335 - 336 - .dropdown-menu .dropdown-item { 337 - display: block; 338 - width: 100%; 339 - padding: 0.75rem 1rem; 340 - text-align: left; 341 - color: var(--fg); 342 - text-decoration: none; 343 - border: none; 344 - background: var(--bg); 345 - cursor: pointer; 346 - transition: background 0.2s; 347 - font-size: 0.95rem; 348 - } 349 - 350 - .dropdown-menu .dropdown-item:hover { 351 - background: var(--hover-bg); 352 - } 353 - 354 - .dropdown-divider { 355 - margin: 0; 356 - border: none; 357 - border-top: 1px solid var(--border); 358 - } 359 - 360 - .dropdown-menu .logout-btn { 361 - color: var(--danger); 362 - font-weight: 500; 363 - } 364 - 365 - /* Buttons */ 366 - button, 367 - .btn, 368 - .btn-primary, 369 - .btn-secondary { 370 - padding: 0.5rem 1rem; 371 - background: var(--button-primary); 372 - color: var(--btn-text); 373 - border: none; 374 - border-radius: 4px; 375 - cursor: pointer; 376 - text-decoration: none; 377 - display: inline-block; 378 - font-size: 0.95rem; 379 - transition: opacity 0.2s; 380 - } 381 - 382 - button:hover, 383 - .btn:hover, 384 - .btn-primary:hover, 385 - .btn-secondary:hover { 386 - opacity: 0.9; 387 - } 388 - 389 - /* Override nav-links color for primary button */ 390 - .nav-links .btn-primary { 391 - color: var(--btn-text); 392 - } 393 - 394 - .btn-secondary { 395 - background: var(--secondary); 396 - } 397 - 398 - .btn-link { 399 - background: transparent; 400 - color: var(--navbar-fg); 401 - text-decoration: none; 402 - } 403 - 404 - .theme-toggle-btn { 405 - display: inline-flex; 406 - align-items: center; 407 - justify-content: center; 408 - } 409 - 410 - .theme-toggle-btn .theme-icon { 411 - width: 1.25rem; 412 - height: 1.25rem; 413 - } 414 - 415 - .delete-btn { 416 - background: transparent; 417 - border: none; 418 - color: var(--danger); 419 - padding: 0.25rem 0.5rem; 420 - font-size: 0.85rem; 421 - cursor: pointer; 422 - transition: all 0.2s ease; 423 - display: inline-flex; 424 - align-items: center; 425 - } 426 - 427 - .delete-btn:hover { 428 - color: var(--danger); 429 - } 430 - 431 - .delete-btn:hover .lucide { 432 - transform: scale(1.2); 433 - } 434 - 435 - .copy-btn { 436 - padding: 0.25rem 0.5rem; 437 - background: transparent; 438 - color: var(--secondary); 439 - border: none; 440 - font-size: 0.85rem; 441 - cursor: pointer; 442 - transition: all 0.2s ease; 443 - display: inline-flex; 444 - align-items: center; 445 - } 446 - 447 - .copy-btn:hover { 448 - color: var(--primary); 449 - } 450 - 451 - .copy-btn:hover .lucide { 452 - transform: scale(1.2); 453 - } 454 - 455 - /* Cards */ 456 - .push-card, 457 - .repository-card { 458 - border: 1px solid var(--border); 459 - border-radius: 8px; 460 - padding: 1rem; 461 - margin-bottom: 1rem; 462 - background: var(--bg); 463 - box-shadow: var(--shadow-sm); 464 - } 465 - 466 - .push-header { 467 - display: flex; 468 - gap: 1rem; 469 - align-items: flex-start; 470 - margin-bottom: 0.75rem; 471 - } 472 - 473 - .push-user { 474 - color: var(--primary); 475 - text-decoration: none; 476 - font-weight: 500; 477 - } 478 - 479 - .push-user:hover { 480 - text-decoration: underline; 481 - } 482 - 483 - .push-separator { 484 - color: var(--border-dark); 485 - margin: 0 0.25rem; 486 - } 487 - 488 - .push-repo { 489 - font-weight: 500; 490 - color: var(--primary); 491 - text-decoration: none; 492 - } 493 - 494 - .push-repo:hover { 495 - color: var(--primary); 496 - text-decoration: underline; 497 - } 498 - 499 - .push-tag { 500 - color: var(--secondary); 501 - } 502 - 503 - .push-details { 504 - display: flex; 505 - align-items: center; 506 - gap: 1rem; 507 - color: var(--border-dark); 508 - font-size: 0.9rem; 509 - margin-bottom: 0.75rem; 510 - } 511 - 512 - .digest { 513 - font-family: "Monaco", "Courier New", monospace; 514 - font-size: 0.85rem; 515 - background: var(--code-bg); 516 - padding: 0.1rem 0.3rem; 517 - border-radius: 3px; 518 - max-width: 200px; 519 - overflow: hidden; 520 - text-overflow: ellipsis; 521 - white-space: nowrap; 522 - display: inline-block; 523 - vertical-align: middle; 524 - position: relative; 525 - } 526 - 527 - /* Digest with copy button container */ 528 - .digest-container { 529 - display: inline-flex; 530 - align-items: center; 531 - gap: 0.5rem; 532 - } 533 - 534 - /* Docker command component */ 535 - .docker-command { 536 - display: inline-flex; 537 - position: relative; 538 - align-items: center; 539 - gap: 0.5rem; 540 - background: var(--code-bg); 541 - border: 1px solid var(--border); 542 - border-radius: 6px; 543 - padding: 0.75rem; 544 - margin: 0.5rem 0; 545 - max-width: 100%; 546 - } 547 - 548 - .docker-command-icon { 549 - width: 1.25rem; 550 - height: 1.25rem; 551 - color: var(--secondary); 552 - flex-shrink: 0; 553 - } 554 - 555 - .docker-command-text { 556 - font-family: "Monaco", "Courier New", monospace; 557 - font-size: 0.85rem; 558 - color: var(--fg); 559 - flex: 0 1 auto; 560 - word-break: break-all; 561 - } 562 - 563 - .docker-command .copy-btn { 564 - position: absolute; 565 - right: 0.5rem; 566 - top: 50%; 567 - transform: translateY(-50%); 568 - background: linear-gradient(to right, transparent, var(--code-bg) 30%); 569 - padding: 0.5rem; 570 - padding-left: 1.5rem; 571 - border-radius: 4px; 572 - opacity: 0; 573 - visibility: hidden; 574 - transition: 575 - opacity 0.2s, 576 - visibility 0.2s; 577 - } 578 - 579 - .docker-command:hover .copy-btn { 580 - opacity: 1; 581 - visibility: visible; 582 - } 583 - 584 - /* Digest tooltip on hover - using title attribute for native browser tooltip */ 585 - .digest { 586 - cursor: default; 587 - } 588 - 589 - /* Digest copy button */ 590 - .digest-copy-btn { 591 - background: transparent; 592 - border: none; 593 - color: var(--secondary); 594 - padding: 0.1rem 0.4rem; 595 - cursor: pointer; 596 - transition: all 0.2s ease; 597 - display: inline-flex; 598 - align-items: center; 599 - } 600 - 601 - .digest-copy-btn:hover { 602 - color: var(--primary); 603 - } 604 - 605 - .digest-copy-btn:hover .lucide { 606 - transform: scale(1.2); 607 - } 608 - 609 - .digest-copy-btn .lucide { 610 - width: 0.875rem; 611 - height: 0.875rem; 612 - transition: transform 0.2s ease; 613 - } 614 - 615 - .delete-btn .lucide { 616 - width: 1rem; 617 - height: 1rem; 618 - transition: transform 0.2s ease; 619 - } 620 - 621 - .copy-btn .lucide { 622 - width: 1rem; 623 - height: 1rem; 624 - transition: transform 0.2s ease; 625 - } 626 - 627 - .separator { 628 - color: var(--border); 629 - } 630 - 631 - /* Push card icon and layout */ 632 - .push-icon { 633 - width: 48px; 634 - height: 48px; 635 - border-radius: 8px; 636 - object-fit: cover; 637 - flex-shrink: 0; 638 - } 639 - 640 - .push-icon-placeholder { 641 - width: 48px; 642 - height: 48px; 643 - border-radius: 8px; 644 - background: var(--button-primary); 645 - display: flex; 646 - align-items: center; 647 - justify-content: center; 648 - font-weight: bold; 649 - font-size: 1.5rem; 650 - text-transform: uppercase; 651 - color: var(--btn-text); 652 - flex-shrink: 0; 653 - } 654 - 655 - .push-info { 656 - flex: 1; 657 - min-width: 0; 658 - } 659 - 660 - .push-title-row { 661 - display: flex; 662 - justify-content: space-between; 663 - align-items: center; 664 - gap: 1rem; 665 - margin-bottom: 0.25rem; 666 - } 667 - 668 - .push-title { 669 - font-size: 1.1rem; 670 - flex: 1; 671 - } 672 - 673 - .push-description { 674 - color: var(--border-dark); 675 - font-size: 0.9rem; 676 - line-height: 1.4; 677 - margin: 0.25rem 0 0 0; 678 - } 679 - 680 - /* Push stats */ 681 - .push-stats { 682 - display: flex; 683 - gap: 1rem; 684 - align-items: center; 685 - flex-shrink: 0; 686 - } 687 - 688 - .push-stat { 689 - display: flex; 690 - align-items: center; 691 - gap: 0.35rem; 692 - color: var(--border-dark); 693 - font-size: 0.9rem; 694 - } 695 - 696 - .push-stat .star-icon { 697 - color: var(--star); 698 - font-size: 1rem; 699 - width: 1rem; 700 - height: 1rem; 701 - stroke: var(--star); 702 - fill: none; 703 - } 704 - 705 - .push-stat .star-icon.star-filled { 706 - fill: var(--star); 707 - } 708 - 709 - .push-stat .pull-icon { 710 - color: var(--primary); 711 - font-size: 1rem; 712 - width: 1rem; 713 - height: 1rem; 714 - stroke: var(--primary); 715 - } 716 - 717 - .push-stat .stat-count { 718 - font-weight: 600; 719 - color: var(--fg); 720 - } 721 - 722 - /* Repository Cards */ 723 - .repo-header { 724 - padding: 1rem; 725 - cursor: pointer; 726 - display: flex; 727 - gap: 1rem; 728 - align-items: flex-start; 729 - background: var(--hover-bg); 730 - border-radius: 8px 8px 0 0; 731 - margin: -1rem -1rem 0 -1rem; 732 - } 733 - 734 - .repo-header:hover { 735 - background: var(--hover-bg); 736 - } 737 - 738 - .repo-icon { 739 - width: 48px; 740 - height: 48px; 741 - border-radius: 8px; 742 - object-fit: cover; 743 - flex-shrink: 0; 744 - } 745 - 746 - .repo-info { 747 - flex: 1; 748 - min-width: 0; 749 - } 750 - 751 - .repo-title-row { 752 - display: flex; 753 - align-items: center; 754 - gap: 0.75rem; 755 - margin-bottom: 0.25rem; 756 - } 757 - 758 - .repo-header h2 { 759 - font-size: 1.3rem; 760 - margin: 0; 761 - } 762 - 763 - .repo-title-link { 764 - color: var(--fg); 765 - text-decoration: none; 766 - } 767 - 768 - .repo-title-link:hover { 769 - color: var(--primary); 770 - text-decoration: underline; 771 - } 772 - 773 - .repo-badge { 774 - display: inline-flex; 775 - align-items: center; 776 - padding: 0.2rem 0.6rem; 777 - font-size: 0.75rem; 778 - font-weight: 500; 779 - border-radius: 12px; 780 - white-space: nowrap; 781 - } 782 - 783 - .license-badge { 784 - background: var(--code-bg); 785 - color: var(--primary); 786 - border: 1px solid #90caf9; 787 - } 788 - 789 - /* Clickable license badges */ 790 - a.license-badge { 791 - text-decoration: none; 792 - cursor: pointer; 793 - transition: all 0.2s ease; 794 - } 795 - 796 - a.license-badge:hover { 797 - background: var(--button-primary); 798 - color: var(--btn-text); 799 - border-color: var(--button-primary); 800 - transform: translateY(-1px); 801 - box-shadow: var(--shadow-md); 802 - } 803 - 804 - .version-badge { 805 - background: var(--version-badge-bg); 806 - color: var(--version-badge-text); 807 - border: 1px solid var(--version-badge-border); 808 - } 809 - 810 - .repo-description { 811 - color: var(--border-dark); 812 - font-size: 0.95rem; 813 - margin: 0.25rem 0 0.5rem 0; 814 - line-height: 1.4; 815 - } 816 - 817 - .repo-stats { 818 - color: var(--border-dark); 819 - font-size: 0.9rem; 820 - display: flex; 821 - gap: 0.5rem; 822 - align-items: center; 823 - flex-wrap: wrap; 824 - } 825 - 826 - .repo-link { 827 - color: var(--primary); 828 - text-decoration: none; 829 - font-weight: 500; 830 - } 831 - 832 - .repo-link:hover { 833 - text-decoration: underline; 834 - } 835 - 836 - .expand-btn { 837 - background: transparent; 838 - color: var(--fg); 839 - padding: 0.25rem 0.5rem; 840 - font-size: 1.2rem; 841 - } 842 - 843 - .repo-details { 844 - padding-top: 1rem; 845 - } 846 - 847 - .tags-section, 848 - .manifests-section { 849 - margin-bottom: 1.5rem; 850 - } 851 - 852 - .tags-section h3, 853 - .manifests-section h3 { 854 - font-size: 1.1rem; 855 - margin-bottom: 0.5rem; 856 - color: var(--secondary); 857 - } 858 - 859 - .tag-row, 860 - .manifest-row { 861 - display: flex; 862 - gap: 1rem; 863 - align-items: center; 864 - padding: 0.5rem; 865 - border-bottom: 1px solid var(--border); 866 - } 867 - 868 - .tag-row:last-child, 869 - .manifest-row:last-child { 870 - border-bottom: none; 871 - } 872 - 873 - .tag-name { 874 - font-weight: 500; 875 - min-width: 100px; 876 - } 877 - 878 - .tag-arrow { 879 - color: var(--border-dark); 880 - } 881 - 882 - /* Note: .tag-digest and .manifest-digest styling now handled by .digest class above */ 883 - 884 - /* Settings Page */ 885 - .settings-page { 886 - max-width: 800px; 887 - margin: 0 auto; 888 - } 889 - 890 - .settings-section { 891 - background: var(--bg); 892 - border: 1px solid var(--border); 893 - border-radius: 8px; 894 - padding: 1.5rem; 895 - margin-bottom: 1.5rem; 896 - box-shadow: var(--shadow-sm); 897 - } 898 - 899 - .settings-section h2 { 900 - font-size: 1.3rem; 901 - margin-bottom: 1rem; 902 - padding-bottom: 0.5rem; 903 - border-bottom: 2px solid var(--border); 904 - } 905 - 906 - .form-group { 907 - margin-bottom: 1rem; 908 - } 909 - 910 - .form-group label { 911 - display: block; 912 - margin-bottom: 0.5rem; 913 - font-weight: 500; 914 - color: var(--secondary); 915 - } 916 - 917 - .form-group input, 918 - .form-group select { 919 - width: 100%; 920 - padding: 0.5rem; 921 - border: 1px solid var(--border); 922 - border-radius: 4px; 923 - font-size: 1rem; 924 - } 925 - 926 - .form-group small { 927 - display: block; 928 - margin-top: 0.25rem; 929 - color: var(--border-dark); 930 - font-size: 0.85rem; 931 - } 932 - 933 - .info-row { 934 - margin-bottom: 0.75rem; 935 - } 936 - 937 - .info-row strong { 938 - display: inline-block; 939 - min-width: 150px; 940 - color: var(--secondary); 941 - } 942 - 943 - /* Modal */ 944 - .modal-overlay { 945 - position: fixed; 946 - top: 0; 947 - left: 0; 948 - right: 0; 949 - bottom: 0; 950 - background: rgba(0, 0, 0, 0.6); 951 - display: flex; 952 - justify-content: center; 953 - align-items: center; 954 - z-index: 1000; 955 - } 956 - 957 - .modal-content { 958 - background: var(--bg); 959 - padding: 2rem; 960 - border-radius: 8px; 961 - max-width: 800px; 962 - max-height: 80vh; 963 - overflow-y: auto; 964 - position: relative; 965 - box-shadow: var(--shadow-lg); 966 - } 967 - 968 - .modal-close { 969 - position: absolute; 970 - top: 1rem; 971 - right: 1rem; 972 - background: none; 973 - border: none; 974 - font-size: 1.5rem; 975 - cursor: pointer; 976 - color: var(--secondary); 977 - } 978 - 979 - .modal-close:hover { 980 - color: var(--fg); 981 - } 982 - 983 - .manifest-json { 984 - background: var(--code-bg); 985 - padding: 1rem; 986 - border-radius: 4px; 987 - overflow-x: auto; 988 - font-family: "Monaco", "Courier New", monospace; 989 - font-size: 0.85rem; 990 - border: 1px solid var(--border); 991 - } 992 - 993 - /* Loading and Empty States */ 994 - .loading { 995 - text-align: center; 996 - padding: 2rem; 997 - color: var(--border-dark); 998 - } 999 - 1000 - .empty-state { 1001 - text-align: center; 1002 - padding: 3rem 2rem; 1003 - background: var(--hover-bg); 1004 - border-radius: 8px; 1005 - border: 1px solid var(--border); 1006 - } 1007 - 1008 - .empty-state p { 1009 - margin-bottom: 1rem; 1010 - font-size: 1.1rem; 1011 - color: var(--secondary); 1012 - } 1013 - 1014 - .empty-state pre { 1015 - background: var(--code-bg); 1016 - padding: 1rem; 1017 - border-radius: 4px; 1018 - display: inline-block; 1019 - } 1020 - 1021 - .empty-message { 1022 - color: var(--border-dark); 1023 - font-style: italic; 1024 - padding: 1rem; 1025 - } 1026 - 1027 - /* Status Messages / Callouts */ 1028 - .note { 1029 - background: var(--warning-bg); 1030 - border-left: 4px solid var(--warning); 1031 - padding: 1rem; 1032 - margin: 1rem 0; 1033 - } 1034 - 1035 - .note a { 1036 - color: var(--warning); 1037 - text-decoration: underline; 1038 - font-weight: 500; 1039 - } 1040 - 1041 - .note a:hover { 1042 - color: var(--primary); 1043 - } 1044 - 1045 - .note a:visited { 1046 - color: var(--warning); 1047 - } 1048 - 1049 - .success { 1050 - background: var(--success-bg); 1051 - border-left: 4px solid var(--success); 1052 - padding: 1rem; 1053 - margin: 1rem 0; 1054 - display: flex; 1055 - align-items: center; 1056 - gap: 0.5rem; 1057 - } 1058 - 1059 - .success .lucide { 1060 - width: 1.25rem; 1061 - height: 1.25rem; 1062 - color: var(--success); 1063 - stroke: var(--success); 1064 - flex-shrink: 0; 1065 - } 1066 - 1067 - .error { 1068 - background: var(--danger-bg); 1069 - border-left: 4px solid var(--danger); 1070 - padding: 1rem; 1071 - margin: 1rem 0; 1072 - } 1073 - 1074 - /* Login Page */ 1075 - .login-page { 1076 - max-width: 450px; 1077 - margin: 4rem auto; 1078 - padding: 2rem; 1079 - } 1080 - 1081 - .login-page h1 { 1082 - font-size: 2rem; 1083 - margin-bottom: 0.5rem; 1084 - text-align: center; 1085 - } 1086 - 1087 - .login-page > p { 1088 - text-align: center; 1089 - color: var(--secondary); 1090 - margin-bottom: 2rem; 1091 - } 1092 - 1093 - .login-form { 1094 - background: var(--bg); 1095 - padding: 2rem; 1096 - border-radius: 8px; 1097 - border: 1px solid var(--border); 1098 - box-shadow: var(--shadow-sm); 1099 - } 1100 - 1101 - .login-form .form-group { 1102 - margin-bottom: 1.5rem; 1103 - } 1104 - 1105 - .login-form label { 1106 - display: block; 1107 - margin-bottom: 0.5rem; 1108 - font-weight: 500; 1109 - } 1110 - 1111 - .login-form input[type="text"] { 1112 - width: 100%; 1113 - padding: 0.75rem; 1114 - border: 1px solid var(--border); 1115 - border-radius: 4px; 1116 - font-size: 1rem; 1117 - } 1118 - 1119 - .login-form input[type="text"]:focus { 1120 - outline: none; 1121 - border-color: var(--primary); 1122 - } 1123 - 1124 - .btn-large { 1125 - width: 100%; 1126 - padding: 0.75rem 1.5rem; 1127 - font-size: 1rem; 1128 - font-weight: 500; 1129 - } 1130 - 1131 - .login-help { 1132 - text-align: center; 1133 - margin-top: 2rem; 1134 - color: var(--secondary); 1135 - } 1136 - 1137 - .login-help a { 1138 - color: var(--primary); 1139 - text-decoration: none; 1140 - } 1141 - 1142 - .login-help a:hover { 1143 - text-decoration: underline; 1144 - } 1145 - 1146 - /* Login Typeahead */ 1147 - .login-form .form-group { 1148 - position: relative; 1149 - } 1150 - 1151 - .typeahead-dropdown { 1152 - position: absolute; 1153 - top: 100%; 1154 - left: 0; 1155 - right: 0; 1156 - background: var(--bg); 1157 - border: 1px solid var(--border); 1158 - border-top: none; 1159 - border-radius: 0 0 4px 4px; 1160 - box-shadow: var(--shadow-md); 1161 - max-height: 300px; 1162 - overflow-y: auto; 1163 - z-index: 1000; 1164 - margin-top: -1px; 1165 - } 1166 - 1167 - .typeahead-header { 1168 - padding: 0.5rem 0.75rem; 1169 - font-size: 0.75rem; 1170 - font-weight: 600; 1171 - text-transform: uppercase; 1172 - color: var(--secondary); 1173 - border-bottom: 1px solid var(--border); 1174 - } 1175 - 1176 - .typeahead-item { 1177 - display: flex; 1178 - align-items: center; 1179 - gap: 0.75rem; 1180 - padding: 0.75rem; 1181 - cursor: pointer; 1182 - transition: background-color 0.15s ease; 1183 - border-bottom: 1px solid var(--border); 1184 - } 1185 - 1186 - .typeahead-item:last-child { 1187 - border-bottom: none; 1188 - } 1189 - 1190 - .typeahead-item:hover, 1191 - .typeahead-item.typeahead-focused { 1192 - background: var(--hover-bg); 1193 - border-left: 3px solid var(--primary); 1194 - padding-left: calc(0.75rem - 3px); 1195 - } 1196 - 1197 - .typeahead-avatar { 1198 - width: 32px; 1199 - height: 32px; 1200 - border-radius: 50%; 1201 - object-fit: cover; 1202 - flex-shrink: 0; 1203 - } 1204 - 1205 - .typeahead-text { 1206 - flex: 1; 1207 - min-width: 0; 1208 - } 1209 - 1210 - .typeahead-displayname { 1211 - font-weight: 500; 1212 - color: var(--text); 1213 - overflow: hidden; 1214 - text-overflow: ellipsis; 1215 - white-space: nowrap; 1216 - } 1217 - 1218 - .typeahead-handle { 1219 - font-size: 0.875rem; 1220 - color: var(--secondary); 1221 - overflow: hidden; 1222 - text-overflow: ellipsis; 1223 - white-space: nowrap; 1224 - } 1225 - 1226 - .typeahead-recent .typeahead-handle { 1227 - font-size: 1rem; 1228 - color: var(--text); 1229 - } 1230 - 1231 - .typeahead-loading { 1232 - padding: 0.75rem; 1233 - text-align: center; 1234 - color: var(--secondary); 1235 - font-size: 0.875rem; 1236 - } 1237 - 1238 - /* Repository Page */ 1239 - .repository-page { 1240 - /* Let container's max-width (1200px) control page width */ 1241 - margin: 0 auto; 1242 - } 1243 - 1244 - .repository-header { 1245 - background: var(--bg); 1246 - border: 1px solid var(--border); 1247 - border-radius: 8px; 1248 - padding: 2rem; 1249 - margin-bottom: 2rem; 1250 - box-shadow: var(--shadow-sm); 1251 - } 1252 - 1253 - .repo-hero { 1254 - display: flex; 1255 - gap: 1.5rem; 1256 - align-items: flex-start; 1257 - margin-bottom: 1.5rem; 1258 - } 1259 - 1260 - .repo-hero-icon { 1261 - width: 80px; 1262 - height: 80px; 1263 - border-radius: 12px; 1264 - object-fit: cover; 1265 - flex-shrink: 0; 1266 - } 1267 - 1268 - .repo-hero-icon-placeholder { 1269 - width: 80px; 1270 - height: 80px; 1271 - border-radius: 12px; 1272 - background: var(--button-primary); 1273 - display: flex; 1274 - align-items: center; 1275 - justify-content: center; 1276 - font-weight: bold; 1277 - font-size: 2.5rem; 1278 - text-transform: uppercase; 1279 - color: var(--btn-text); 1280 - flex-shrink: 0; 1281 - } 1282 - 1283 - .repo-hero-icon-wrapper { 1284 - position: relative; 1285 - display: inline-block; 1286 - flex-shrink: 0; 1287 - } 1288 - 1289 - .avatar-upload-overlay { 1290 - position: absolute; 1291 - inset: 0; 1292 - display: flex; 1293 - align-items: center; 1294 - justify-content: center; 1295 - background: rgba(0, 0, 0, 0.5); 1296 - border-radius: 12px; 1297 - opacity: 0; 1298 - cursor: pointer; 1299 - transition: opacity 0.2s ease; 1300 - } 1301 - 1302 - .avatar-upload-overlay i { 1303 - color: white; 1304 - width: 24px; 1305 - height: 24px; 1306 - } 1307 - 1308 - .repo-hero-icon-wrapper:hover .avatar-upload-overlay { 1309 - opacity: 1; 1310 - } 1311 - 1312 - .repo-hero-info { 1313 - flex: 1; 1314 - } 1315 - 1316 - .repo-hero-info h1 { 1317 - font-size: 2rem; 1318 - margin: 0 0 0.5rem 0; 1319 - } 1320 - 1321 - .owner-link { 1322 - color: var(--primary); 1323 - text-decoration: none; 1324 - } 1325 - 1326 - .owner-link:hover { 1327 - text-decoration: underline; 1328 - } 1329 - 1330 - .repo-separator { 1331 - color: var(--border-dark); 1332 - margin: 0 0.25rem; 1333 - } 1334 - 1335 - .repo-name { 1336 - color: var(--fg); 1337 - } 1338 - 1339 - .repo-hero-description { 1340 - color: var(--border-dark); 1341 - font-size: 1.1rem; 1342 - line-height: 1.5; 1343 - margin: 0.5rem 0 0 0; 1344 - } 1345 - 1346 - .repo-info-row { 1347 - display: flex; 1348 - gap: 2rem; 1349 - align-items: center; 1350 - margin-top: 1.5rem; 1351 - } 1352 - 1353 - .repo-actions { 1354 - flex: 0 0 auto; 1355 - } 1356 - 1357 - .star-btn { 1358 - display: inline-flex; 1359 - align-items: center; 1360 - gap: 0.5rem; 1361 - padding: 0.5rem 1rem; 1362 - background: var(--bg); 1363 - border: 2px solid var(--border); 1364 - border-radius: 6px; 1365 - font-size: 1rem; 1366 - cursor: pointer; 1367 - transition: all 0.2s ease; 1368 - color: var(--fg); 1369 - } 1370 - 1371 - .star-btn:hover:not(:disabled) { 1372 - border-color: var(--primary); 1373 - background: var(--hover-bg); 1374 - } 1375 - 1376 - .star-btn:disabled { 1377 - opacity: 0.6; 1378 - cursor: not-allowed; 1379 - } 1380 - 1381 - .star-btn.starred { 1382 - border-color: var(--star); 1383 - background: var(--code-bg); 1384 - } 1385 - 1386 - .star-btn.starred:hover:not(:disabled) { 1387 - background: var(--hover-bg); 1388 - } 1389 - 1390 - /* Lucide icon base styles */ 1391 - .lucide { 1392 - display: inline-block; 1393 - width: 1em; 1394 - height: 1em; 1395 - vertical-align: middle; 1396 - stroke-width: 2; 1397 - transition: transform 0.2s ease; 1398 - } 1399 - 1400 - /* Star icon styles */ 1401 - .star-icon { 1402 - font-size: 1.25rem; 1403 - line-height: 1; 1404 - transition: transform 0.2s ease; 1405 - color: var(--star); 1406 - width: 1.25rem; 1407 - height: 1.25rem; 1408 - stroke: var(--star); 1409 - fill: none; 1410 - } 1411 - 1412 - .star-icon.star-filled { 1413 - fill: var(--star); 1414 - } 1415 - 1416 - .star-btn:hover:not(:disabled) .star-icon { 1417 - transform: scale(1.1); 1418 - } 1419 - 1420 - .star-count { 1421 - font-weight: 600; 1422 - color: var(--fg); 1423 - } 1424 - 1425 - .repo-metadata { 1426 - display: flex; 1427 - gap: 1rem; 1428 - align-items: center; 1429 - flex-wrap: wrap; 1430 - flex: 1; 1431 - justify-content: flex-end; 1432 - } 1433 - 1434 - .metadata-badge { 1435 - display: inline-flex; 1436 - align-items: center; 1437 - padding: 0.3rem 0.75rem; 1438 - font-size: 0.85rem; 1439 - font-weight: 500; 1440 - border-radius: 16px; 1441 - white-space: nowrap; 1442 - } 1443 - 1444 - .metadata-link { 1445 - color: var(--primary); 1446 - text-decoration: none; 1447 - font-weight: 500; 1448 - } 1449 - 1450 - .metadata-link:hover { 1451 - text-decoration: underline; 1452 - } 1453 - 1454 - .pull-command-section { 1455 - padding-top: 1rem; 1456 - border-top: 1px solid var(--border); 1457 - } 1458 - 1459 - .pull-command-section h3 { 1460 - font-size: 1rem; 1461 - margin-bottom: 0.75rem; 1462 - color: var(--secondary); 1463 - } 1464 - 1465 - .repo-section { 1466 - background: var(--bg); 1467 - border: 1px solid var(--border); 1468 - border-radius: 8px; 1469 - padding: 1.5rem; 1470 - margin-bottom: 2rem; 1471 - box-shadow: var(--shadow-sm); 1472 - } 1473 - 1474 - .repo-section h2 { 1475 - font-size: 1.5rem; 1476 - margin-bottom: 1rem; 1477 - padding-bottom: 0.5rem; 1478 - border-bottom: 2px solid var(--border); 1479 - } 1480 - 1481 - .tags-list, 1482 - .manifests-list { 1483 - display: flex; 1484 - flex-direction: column; 1485 - gap: 1rem; 1486 - } 1487 - 1488 - .tag-item, 1489 - .manifest-item { 1490 - border: 1px solid var(--border); 1491 - border-radius: 6px; 1492 - padding: 1rem; 1493 - background: var(--hover-bg); 1494 - } 1495 - 1496 - .tag-item-header, 1497 - .manifest-item-header { 1498 - display: flex; 1499 - justify-content: space-between; 1500 - align-items: center; 1501 - margin-bottom: 0.5rem; 1502 - } 1503 - 1504 - .tag-name-large { 1505 - font-size: 1.2rem; 1506 - font-weight: 600; 1507 - color: var(--fg); 1508 - } 1509 - 1510 - .tag-timestamp { 1511 - color: var(--border-dark); 1512 - font-size: 0.9rem; 1513 - } 1514 - 1515 - .tag-item-details { 1516 - margin-bottom: 0.75rem; 1517 - } 1518 - 1519 - .manifest-item-details { 1520 - display: flex; 1521 - gap: 0.5rem; 1522 - align-items: center; 1523 - color: var(--border-dark); 1524 - font-size: 0.9rem; 1525 - margin-top: 0.5rem; 1526 - } 1527 - 1528 - /* Offline manifest badge */ 1529 - .offline-badge { 1530 - display: inline-flex; 1531 - align-items: center; 1532 - gap: 0.35rem; 1533 - padding: 0.25rem 0.5rem; 1534 - background: var(--warning-bg); 1535 - color: var(--warning); 1536 - border: 1px solid var(--warning); 1537 - border-radius: 4px; 1538 - font-size: 0.85rem; 1539 - font-weight: 600; 1540 - margin-left: 0.5rem; 1541 - } 1542 - 1543 - .offline-badge .lucide { 1544 - width: 0.875rem; 1545 - height: 0.875rem; 1546 - } 1547 - 1548 - /* Checking manifest badge (health check in progress) */ 1549 - .checking-badge { 1550 - display: inline-flex; 1551 - align-items: center; 1552 - gap: 0.35rem; 1553 - padding: 0.25rem 0.5rem; 1554 - background: #e3f2fd; 1555 - color: #1976d2; 1556 - border: 1px solid #1976d2; 1557 - border-radius: 4px; 1558 - font-size: 0.85rem; 1559 - font-weight: 600; 1560 - margin-left: 0.5rem; 1561 - } 1562 - 1563 - .checking-badge .lucide { 1564 - width: 0.875rem; 1565 - height: 0.875rem; 1566 - } 1567 - 1568 - /* Hide offline manifests by default */ 1569 - .manifest-item[data-reachable="false"] { 1570 - display: none; 1571 - } 1572 - 1573 - /* Show offline manifests when toggle is checked */ 1574 - .manifests-list.show-offline .manifest-item[data-reachable="false"] { 1575 - display: block; 1576 - opacity: 0.6; 1577 - } 1578 - 1579 - /* Show offline images toggle styling */ 1580 - .show-offline-toggle { 1581 - display: flex; 1582 - align-items: center; 1583 - gap: 0.5rem; 1584 - cursor: pointer; 1585 - user-select: none; 1586 - } 1587 - 1588 - .show-offline-toggle input[type="checkbox"] { 1589 - cursor: pointer; 1590 - } 1591 - 1592 - .show-offline-toggle span { 1593 - font-size: 0.9rem; 1594 - color: var(--border-dark); 1595 - } 1596 - 1597 - .manifest-detail-label { 1598 - font-weight: 500; 1599 - color: var(--secondary); 1600 - } 1601 - 1602 - /* Multi-architecture badges */ 1603 - .badge-multi { 1604 - display: inline-flex; 1605 - align-items: center; 1606 - padding: 0.25rem 0.6rem; 1607 - font-size: 0.75rem; 1608 - font-weight: 600; 1609 - border-radius: 12px; 1610 - background: var(--button-primary); 1611 - color: var(--btn-text); 1612 - white-space: nowrap; 1613 - margin-left: 0.5rem; 1614 - } 1615 - 1616 - /* Helm chart badge */ 1617 - .badge-helm { 1618 - display: inline-flex; 1619 - align-items: center; 1620 - gap: 0.25rem; 1621 - padding: 0.25rem 0.6rem; 1622 - font-size: 0.75rem; 1623 - font-weight: 600; 1624 - border-radius: 12px; 1625 - background: #0f1689; 1626 - color: #fff; 1627 - white-space: nowrap; 1628 - margin-left: 0.5rem; 1629 - } 1630 - 1631 - .badge-helm svg { 1632 - width: 12px; 1633 - height: 12px; 1634 - } 1635 - 1636 - .platform-badge { 1637 - display: inline-flex; 1638 - align-items: center; 1639 - padding: 0.2rem 0.5rem; 1640 - font-size: 0.75rem; 1641 - font-weight: 500; 1642 - border-radius: 4px; 1643 - background: var(--code-bg); 1644 - color: var(--fg); 1645 - border: 1px solid var(--border); 1646 - white-space: nowrap; 1647 - font-family: "Monaco", "Courier New", monospace; 1648 - } 1649 - 1650 - .platforms-inline { 1651 - display: flex; 1652 - flex-wrap: wrap; 1653 - gap: 0.5rem; 1654 - align-items: center; 1655 - } 1656 - 1657 - .manifest-type { 1658 - display: inline-flex; 1659 - align-items: center; 1660 - gap: 0.35rem; 1661 - font-size: 0.9rem; 1662 - font-weight: 500; 1663 - color: var(--secondary); 1664 - } 1665 - 1666 - .manifest-type .lucide { 1667 - width: 0.95rem; 1668 - height: 0.95rem; 1669 - } 1670 - 1671 - .platform-count { 1672 - color: var(--border-dark); 1673 - font-size: 0.85rem; 1674 - font-style: italic; 1675 - } 1676 - 1677 - .text-muted { 1678 - color: var(--border-dark); 1679 - font-style: italic; 1680 - } 1681 - 1682 - .badge-attestation { 1683 - display: inline-flex; 1684 - align-items: center; 1685 - gap: 0.3rem; 1686 - padding: 0.25rem 0.6rem; 1687 - background: var(--attestation-badge-bg); 1688 - color: var(--attestation-badge-text); 1689 - border-radius: 12px; 1690 - font-size: 0.75rem; 1691 - font-weight: 600; 1692 - margin-left: 0.5rem; 1693 - vertical-align: middle; 1694 - white-space: nowrap; 1695 - } 1696 - 1697 - .badge-attestation .lucide { 1698 - width: 0.75rem; 1699 - height: 0.75rem; 1700 - } 1701 - 1702 - /* Featured Repositories Section */ 1703 - .featured-section { 1704 - margin-bottom: 3rem; 1705 - } 1706 - 1707 - .featured-section h1 { 1708 - font-size: 1.8rem; 1709 - margin-bottom: 1.5rem; 1710 - } 1711 - 1712 - .featured-grid { 1713 - display: grid; 1714 - grid-template-columns: repeat(3, 1fr); 1715 - gap: 1.5rem; 1716 - margin-bottom: 2rem; 1717 - } 1718 - 1719 - .featured-card { 1720 - border: 1px solid var(--border); 1721 - border-radius: 8px; 1722 - padding: 1.5rem; 1723 - background: var(--bg); 1724 - box-shadow: var(--shadow-sm); 1725 - transition: all 0.2s ease; 1726 - text-decoration: none; 1727 - color: var(--fg); 1728 - display: flex; 1729 - flex-direction: column; 1730 - justify-content: space-between; 1731 - min-height: 180px; 1732 - } 1733 - 1734 - .featured-card:hover { 1735 - box-shadow: var(--shadow-md); 1736 - border-color: var(--primary); 1737 - transform: translateY(-2px); 1738 - } 1739 - 1740 - .featured-header { 1741 - display: flex; 1742 - gap: 1rem; 1743 - align-items: flex-start; 1744 - margin-bottom: 1rem; 1745 - } 1746 - 1747 - .featured-icon { 1748 - width: 48px; 1749 - height: 48px; 1750 - border-radius: 8px; 1751 - object-fit: cover; 1752 - flex-shrink: 0; 1753 - } 1754 - 1755 - .featured-icon-placeholder { 1756 - width: 48px; 1757 - height: 48px; 1758 - border-radius: 8px; 1759 - background: var(--button-primary); 1760 - display: flex; 1761 - align-items: center; 1762 - justify-content: center; 1763 - font-weight: bold; 1764 - font-size: 1.5rem; 1765 - text-transform: uppercase; 1766 - color: var(--btn-text); 1767 - flex-shrink: 0; 1768 - } 1769 - 1770 - .featured-info { 1771 - flex: 1; 1772 - min-width: 0; 1773 - } 1774 - 1775 - .featured-title { 1776 - font-size: 1.1rem; 1777 - font-weight: 600; 1778 - margin-bottom: 0.5rem; 1779 - line-height: 1.3; 1780 - } 1781 - 1782 - .featured-owner { 1783 - color: var(--primary); 1784 - } 1785 - 1786 - .featured-separator { 1787 - color: var(--border-dark); 1788 - margin: 0 0.25rem; 1789 - } 1790 - 1791 - .featured-name { 1792 - color: var(--fg); 1793 - } 1794 - 1795 - .featured-description { 1796 - color: var(--border-dark); 1797 - font-size: 0.9rem; 1798 - line-height: 1.4; 1799 - margin: 0; 1800 - overflow: hidden; 1801 - text-overflow: ellipsis; 1802 - display: -webkit-box; 1803 - -webkit-line-clamp: 2; 1804 - -webkit-box-orient: vertical; 1805 - line-clamp: 2; 1806 - } 1807 - 1808 - .featured-stats { 1809 - display: flex; 1810 - gap: 1.5rem; 1811 - align-items: center; 1812 - padding-top: 0.75rem; 1813 - border-top: 1px solid var(--border); 1814 - } 1815 - 1816 - .featured-stat { 1817 - display: flex; 1818 - align-items: center; 1819 - gap: 0.5rem; 1820 - color: var(--border-dark); 1821 - font-size: 0.95rem; 1822 - } 1823 - 1824 - .featured-stat .star-icon { 1825 - color: var(--star); 1826 - font-size: 1.1rem; 1827 - width: 1.1rem; 1828 - height: 1.1rem; 1829 - stroke: var(--star); 1830 - fill: none; 1831 - } 1832 - 1833 - .featured-stat .star-icon.star-filled { 1834 - fill: var(--star); 1835 - } 1836 - 1837 - .featured-stat .pull-icon { 1838 - color: var(--primary); 1839 - font-size: 1.1rem; 1840 - width: 1.1rem; 1841 - height: 1.1rem; 1842 - stroke: var(--primary); 1843 - } 1844 - 1845 - .featured-stat .stat-count { 1846 - font-weight: 600; 1847 - color: var(--fg); 1848 - } 1849 - 1850 - /* Hero Section */ 1851 - .hero-section { 1852 - background: linear-gradient( 1853 - 135deg, 1854 - var(--hero-bg-start) 0%, 1855 - var(--hero-bg-end) 100% 1856 - ); 1857 - padding: 4rem 2rem; 1858 - border-bottom: 1px solid var(--border); 1859 - } 1860 - 1861 - .hero-content { 1862 - max-width: 900px; 1863 - margin: 0 auto; 1864 - text-align: center; 1865 - } 1866 - 1867 - .hero-title { 1868 - font-size: 3rem; 1869 - font-weight: 700; 1870 - margin-bottom: 1.5rem; 1871 - color: var(--fg); 1872 - line-height: 1.2; 1873 - } 1874 - 1875 - .hero-subtitle { 1876 - font-size: 1.2rem; 1877 - color: var(--border-dark); 1878 - margin-bottom: 3rem; 1879 - line-height: 1.6; 1880 - } 1881 - 1882 - .hero-terminal { 1883 - max-width: 600px; 1884 - margin: 0 auto 2.5rem; 1885 - background: var(--terminal-bg); 1886 - border-radius: 8px; 1887 - box-shadow: var(--shadow-lg); 1888 - overflow: hidden; 1889 - } 1890 - 1891 - .terminal-header { 1892 - background: var(--terminal-header-bg); 1893 - padding: 0.75rem 1rem; 1894 - display: flex; 1895 - gap: 0.5rem; 1896 - align-items: center; 1897 - } 1898 - 1899 - .terminal-dot { 1900 - width: 12px; 1901 - height: 12px; 1902 - border-radius: 50%; 1903 - background: var(--border-dark); 1904 - } 1905 - 1906 - .terminal-dot:nth-child(1) { 1907 - background: #ff5f56; 1908 - } 1909 - 1910 - .terminal-dot:nth-child(2) { 1911 - background: #ffbd2e; 1912 - } 1913 - 1914 - .terminal-dot:nth-child(3) { 1915 - background: #27c93f; 1916 - } 1917 - 1918 - .terminal-content { 1919 - padding: 1.5rem; 1920 - margin: 0; 1921 - font-family: "Monaco", "Courier New", monospace; 1922 - font-size: 0.95rem; 1923 - line-height: 1.8; 1924 - color: var(--terminal-text); 1925 - overflow-x: auto; 1926 - } 1927 - 1928 - .terminal-prompt { 1929 - color: var(--terminal-prompt); 1930 - font-weight: bold; 1931 - } 1932 - 1933 - .terminal-comment { 1934 - color: var(--terminal-comment); 1935 - font-style: italic; 1936 - } 1937 - 1938 - .hero-actions { 1939 - display: flex; 1940 - gap: 1rem; 1941 - justify-content: center; 1942 - margin-bottom: 4rem; 1943 - } 1944 - 1945 - .btn-hero-primary, 1946 - .btn-hero-secondary { 1947 - padding: 0.9rem 2rem; 1948 - font-size: 1.1rem; 1949 - font-weight: 600; 1950 - border-radius: 6px; 1951 - text-decoration: none; 1952 - transition: all 0.2s ease; 1953 - display: inline-block; 1954 - } 1955 - 1956 - .btn-hero-primary { 1957 - background: var(--button-primary); 1958 - color: var(--btn-text); 1959 - border: 2px solid var(--button-primary); 1960 - } 1961 - 1962 - .btn-hero-primary:hover { 1963 - background: var(--primary-dark); 1964 - border-color: var(--primary-dark); 1965 - transform: translateY(-2px); 1966 - box-shadow: 0 4px 12px rgba(0, 102, 204, 0.3); 1967 - } 1968 - 1969 - .btn-hero-secondary { 1970 - background: transparent; 1971 - color: var(--primary); 1972 - border: 2px solid var(--button-primary); 1973 - } 1974 - 1975 - .btn-hero-secondary:hover { 1976 - background: var(--button-primary); 1977 - color: var(--btn-text); 1978 - transform: translateY(-2px); 1979 - } 1980 - 1981 - .hero-benefits { 1982 - max-width: 1000px; 1983 - margin: 0 auto; 1984 - display: grid; 1985 - grid-template-columns: repeat(3, 1fr); 1986 - gap: 2rem; 1987 - } 1988 - 1989 - .benefit-card { 1990 - background: var(--bg); 1991 - border: 1px solid var(--border); 1992 - border-radius: 8px; 1993 - padding: 2rem 1.5rem; 1994 - text-align: center; 1995 - transition: all 0.2s ease; 1996 - } 1997 - 1998 - .benefit-card:hover { 1999 - border-color: var(--primary); 2000 - box-shadow: var(--shadow-md); 2001 - transform: translateY(-4px); 2002 - } 2003 - 2004 - .benefit-icon { 2005 - font-size: 3rem; 2006 - margin-bottom: 1rem; 2007 - line-height: 1; 2008 - } 2009 - 2010 - .benefit-icon .lucide { 2011 - width: 3rem; 2012 - height: 3rem; 2013 - stroke-width: 1.5; 2014 - color: var(--primary); 2015 - stroke: var(--primary); 2016 - } 2017 - 2018 - .benefit-card h3 { 2019 - font-size: 1.2rem; 2020 - margin-bottom: 0.75rem; 2021 - color: var(--fg); 2022 - } 2023 - 2024 - .benefit-card p { 2025 - color: var(--border-dark); 2026 - font-size: 0.95rem; 2027 - line-height: 1.5; 2028 - margin: 0; 2029 - } 2030 - 2031 - /* Install Page */ 2032 - .install-page { 2033 - max-width: 800px; 2034 - margin: 0 auto; 2035 - padding: 2rem 1rem; 2036 - } 2037 - 2038 - .install-section { 2039 - margin: 2rem 0; 2040 - } 2041 - 2042 - .install-section h2 { 2043 - margin-bottom: 1rem; 2044 - color: var(--fg); 2045 - } 2046 - 2047 - .install-section h3 { 2048 - margin: 1.5rem 0 0.5rem; 2049 - color: var(--border-dark); 2050 - font-size: 1.1rem; 2051 - } 2052 - 2053 - .install-section a { 2054 - color: var(--primary); 2055 - text-decoration: underline; 2056 - font-weight: 500; 2057 - } 2058 - 2059 - .install-section a:hover { 2060 - color: var(--primary-dark); 2061 - } 2062 - 2063 - .install-section a:visited { 2064 - color: var(--primary); 2065 - } 2066 - 2067 - .code-block { 2068 - background: var(--code-bg); 2069 - border: 1px solid var(--border); 2070 - border-radius: 4px; 2071 - padding: 1rem; 2072 - margin: 0.5rem 0 1rem; 2073 - overflow-x: auto; 2074 - } 2075 - 2076 - .code-block code { 2077 - font-family: "Monaco", "Menlo", monospace; 2078 - font-size: 0.9rem; 2079 - line-height: 1.5; 2080 - white-space: pre-wrap; 2081 - } 2082 - 2083 - .platform-tabs { 2084 - display: flex; 2085 - gap: 0.5rem; 2086 - border-bottom: 2px solid var(--border); 2087 - margin-bottom: 1rem; 2088 - } 2089 - 2090 - .platform-tab { 2091 - padding: 0.5rem 1rem; 2092 - cursor: pointer; 2093 - border: none; 2094 - background: none; 2095 - font-size: 1rem; 2096 - color: var(--border-dark); 2097 - transition: all 0.2s; 2098 - } 2099 - 2100 - .platform-tab:hover { 2101 - color: var(--fg); 2102 - } 2103 - 2104 - .platform-tab.active { 2105 - color: var(--primary); 2106 - border-bottom: 2px solid var(--primary); 2107 - margin-bottom: -2px; 2108 - } 2109 - 2110 - .platform-content { 2111 - display: none; 2112 - } 2113 - 2114 - .platform-content.active { 2115 - display: block; 2116 - } 2117 - 2118 - /* Responsive */ 2119 - @media (max-width: 768px) { 2120 - .navbar { 2121 - flex-direction: column; 2122 - gap: 1rem; 2123 - } 2124 - 2125 - .nav-search-wrapper.expanded .nav-search-form { 2126 - width: 200px; 2127 - } 2128 - 2129 - .push-details { 2130 - flex-wrap: wrap; 2131 - } 2132 - 2133 - .tag-row, 2134 - .manifest-row { 2135 - flex-wrap: wrap; 2136 - } 2137 - 2138 - .login-page { 2139 - margin: 2rem auto; 2140 - padding: 1rem; 2141 - } 2142 - 2143 - .repo-hero { 2144 - flex-direction: column; 2145 - } 2146 - 2147 - .repo-hero-info h1 { 2148 - font-size: 1.5rem; 2149 - } 2150 - 2151 - .tag-item-header { 2152 - flex-direction: column; 2153 - align-items: flex-start; 2154 - gap: 0.5rem; 2155 - } 2156 - 2157 - .manifest-item-details { 2158 - flex-direction: column; 2159 - align-items: flex-start; 2160 - } 2161 - 2162 - .featured-grid { 2163 - grid-template-columns: 1fr; 2164 - gap: 1rem; 2165 - } 2166 - 2167 - .featured-card { 2168 - min-height: auto; 2169 - } 2170 - 2171 - .hero-section { 2172 - padding: 3rem 1.5rem; 2173 - } 2174 - 2175 - .hero-title { 2176 - font-size: 2rem; 2177 - } 2178 - 2179 - .hero-subtitle { 2180 - font-size: 1rem; 2181 - margin-bottom: 2rem; 2182 - } 2183 - 2184 - .hero-terminal { 2185 - margin-bottom: 2rem; 2186 - } 2187 - 2188 - .terminal-content { 2189 - font-size: 0.85rem; 2190 - padding: 1rem; 2191 - } 2192 - 2193 - .hero-actions { 2194 - flex-direction: column; 2195 - margin-bottom: 3rem; 2196 - } 2197 - 2198 - .btn-hero-primary, 2199 - .btn-hero-secondary { 2200 - width: 100%; 2201 - text-align: center; 2202 - } 2203 - 2204 - .hero-benefits { 2205 - grid-template-columns: 1fr; 2206 - gap: 1.5rem; 2207 - } 2208 - } 2209 - 2210 - @media (max-width: 1024px) and (min-width: 769px) { 2211 - .featured-grid { 2212 - grid-template-columns: repeat(2, 1fr); 2213 - } 2214 - 2215 - .hero-benefits { 2216 - grid-template-columns: repeat(3, 1fr); 2217 - } 2218 - } 2219 - 2220 - /* README and Repository Layout */ 2221 - .repo-content-layout { 2222 - display: grid; 2223 - grid-template-columns: 6fr 4fr; 2224 - gap: 2rem; 2225 - margin-top: 2rem; 2226 - } 2227 - 2228 - .readme-section { 2229 - background: var(--bg); 2230 - border: 1px solid var(--border); 2231 - border-radius: 8px; 2232 - padding: 2rem; 2233 - min-width: 0; 2234 - box-sizing: border-box; 2235 - } 2236 - 2237 - .readme-section h2 { 2238 - margin-bottom: 1.5rem; 2239 - padding-bottom: 0.5rem; 2240 - border-bottom: 2px solid var(--border); 2241 - } 2242 - 2243 - .readme-content { 2244 - overflow-wrap: break-word; 2245 - max-width: 100%; 2246 - box-sizing: border-box; 2247 - } 2248 - 2249 - .repo-sidebar { 2250 - display: flex; 2251 - flex-direction: column; 2252 - gap: 1.5rem; 2253 - } 2254 - 2255 - /* Markdown Styling */ 2256 - .markdown-body { 2257 - font-size: 1rem; 2258 - line-height: 1.6; 2259 - word-wrap: break-word; 2260 - } 2261 - 2262 - .markdown-body h1, 2263 - .markdown-body h2, 2264 - .markdown-body h3, 2265 - .markdown-body h4, 2266 - .markdown-body h5, 2267 - .markdown-body h6 { 2268 - margin-top: 1.5rem; 2269 - margin-bottom: 1rem; 2270 - font-weight: 600; 2271 - line-height: 1.25; 2272 - } 2273 - 2274 - .markdown-body h1 { 2275 - font-size: 2rem; 2276 - border-bottom: 1px solid var(--border); 2277 - padding-bottom: 0.3rem; 2278 - } 2279 - 2280 - .markdown-body h2 { 2281 - font-size: 1.5rem; 2282 - border-bottom: 1px solid var(--border); 2283 - padding-bottom: 0.3rem; 2284 - } 2285 - 2286 - .markdown-body h3 { 2287 - font-size: 1.25rem; 2288 - } 2289 - 2290 - .markdown-body h4 { 2291 - font-size: 1rem; 2292 - } 2293 - 2294 - .markdown-body h5 { 2295 - font-size: 0.875rem; 2296 - } 2297 - 2298 - .markdown-body h6 { 2299 - font-size: 0.85rem; 2300 - color: var(--secondary); 2301 - } 2302 - 2303 - .markdown-body p { 2304 - margin-bottom: 1rem; 2305 - } 2306 - 2307 - .markdown-body ul, 2308 - .markdown-body ol { 2309 - margin-bottom: 1rem; 2310 - padding-left: 2rem; 2311 - } 2312 - 2313 - .markdown-body li { 2314 - margin-bottom: 0.25rem; 2315 - } 2316 - 2317 - .markdown-body li > p { 2318 - margin-bottom: 0.5rem; 2319 - } 2320 - 2321 - .markdown-body a { 2322 - color: var(--primary); 2323 - text-decoration: none; 2324 - } 2325 - 2326 - .markdown-body a:hover { 2327 - text-decoration: underline; 2328 - } 2329 - 2330 - .markdown-body code { 2331 - background: var(--code-bg); 2332 - padding: 0.2rem 0.4rem; 2333 - border-radius: 3px; 2334 - font-family: 2335 - "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; 2336 - font-size: 0.9em; 2337 - } 2338 - 2339 - .markdown-body pre { 2340 - background: var(--code-bg); 2341 - padding: 1rem; 2342 - border-radius: 6px; 2343 - overflow-x: auto; 2344 - margin-bottom: 1rem; 2345 - max-width: 100%; 2346 - box-sizing: border-box; 2347 - } 2348 - 2349 - .markdown-body pre code { 2350 - background: none; 2351 - padding: 0; 2352 - font-size: 0.875rem; 2353 - } 2354 - 2355 - .markdown-body blockquote { 2356 - padding: 0 1rem; 2357 - margin-bottom: 1rem; 2358 - color: var(--secondary); 2359 - border-left: 4px solid var(--border); 2360 - } 2361 - 2362 - .markdown-body table { 2363 - border-collapse: collapse; 2364 - width: 100%; 2365 - margin-bottom: 1rem; 2366 - } 2367 - 2368 - .markdown-body table th, 2369 - .markdown-body table td { 2370 - padding: 0.5rem 1rem; 2371 - border: 1px solid var(--border); 2372 - text-align: left; 2373 - } 2374 - 2375 - .markdown-body table th { 2376 - background: var(--code-bg); 2377 - font-weight: 600; 2378 - } 2379 - 2380 - .markdown-body table tr:nth-child(even) { 2381 - background: var(--hover-bg); 2382 - } 2383 - 2384 - .markdown-body img { 2385 - max-width: 100%; 2386 - height: auto; 2387 - margin: 1rem 0; 2388 - } 2389 - 2390 - .markdown-body hr { 2391 - height: 0.25rem; 2392 - margin: 1.5rem 0; 2393 - background: var(--border); 2394 - border: 0; 2395 - } 2396 - 2397 - /* Task lists */ 2398 - .markdown-body input[type="checkbox"] { 2399 - margin-right: 0.5rem; 2400 - } 2401 - 2402 - .markdown-body .task-list-item { 2403 - list-style-type: none; 2404 - } 2405 - 2406 - .markdown-body .task-list-item input { 2407 - margin: 0 0.2rem 0.25rem -1.6rem; 2408 - vertical-align: middle; 2409 - } 2410 - 2411 - /* Responsive Layout */ 2412 - @media (max-width: 1024px) { 2413 - .repo-content-layout { 2414 - grid-template-columns: 1fr; 2415 - } 2416 - 2417 - .repo-sidebar { 2418 - order: -1; /* Show sidebar first on mobile */ 2419 - } 2420 - } 2421 - 2422 - @media (max-width: 768px) { 2423 - .readme-section { 2424 - padding: 1rem; 2425 - } 2426 - 2427 - .markdown-body h1 { 2428 - font-size: 1.5rem; 2429 - } 2430 - 2431 - .markdown-body h2 { 2432 - font-size: 1.25rem; 2433 - } 2434 - 2435 - .markdown-body pre { 2436 - padding: 0.75rem; 2437 - } 2438 - } 2439 - 2440 - /* 404 Error Page */ 2441 - .error-page { 2442 - display: flex; 2443 - align-items: center; 2444 - justify-content: center; 2445 - min-height: calc(100vh - 60px); 2446 - text-align: center; 2447 - padding: 2rem; 2448 - } 2449 - 2450 - .error-content { 2451 - max-width: 480px; 2452 - } 2453 - 2454 - .error-icon { 2455 - width: 80px; 2456 - height: 80px; 2457 - color: var(--secondary); 2458 - margin-bottom: 1.5rem; 2459 - } 2460 - 2461 - .error-code { 2462 - font-size: 8rem; 2463 - font-weight: 700; 2464 - color: var(--primary); 2465 - line-height: 1; 2466 - margin-bottom: 0.5rem; 2467 - } 2468 - 2469 - .error-content h1 { 2470 - font-size: 2rem; 2471 - margin-bottom: 0.75rem; 2472 - color: var(--fg); 2473 - } 2474 - 2475 - .error-content p { 2476 - font-size: 1.125rem; 2477 - color: var(--secondary); 2478 - margin-bottom: 2rem; 2479 - } 2480 - 2481 - @media (max-width: 768px) { 2482 - .error-code { 2483 - font-size: 5rem; 2484 - } 2485 - 2486 - .error-icon { 2487 - width: 60px; 2488 - height: 60px; 2489 - } 2490 - 2491 - .error-content h1 { 2492 - font-size: 1.5rem; 2493 - } 2494 - } 2495 - 2496 - /* Artifact type badges */ 2497 - .artifact-badge { 2498 - display: inline-flex; 2499 - align-items: center; 2500 - justify-content: center; 2501 - padding: 0.15rem 0.35rem; 2502 - border-radius: 4px; 2503 - font-size: 0.7rem; 2504 - font-weight: 500; 2505 - margin-left: 0.5rem; 2506 - vertical-align: middle; 2507 - } 2508 - 2509 - .artifact-badge.helm { 2510 - background-color: rgba(13, 108, 191, 0.15); 2511 - color: #0d6cbf; 2512 - } 2513 - 2514 - .artifact-badge i { 2515 - width: 12px; 2516 - height: 12px; 2517 - } 2518 - 2519 - .manifest-type.helm { 2520 - background-color: rgba(13, 108, 191, 0.15); 2521 - color: #0d6cbf; 2522 - } 2523 - 2524 - /* Legal Pages (Privacy Policy, Terms of Service) */ 2525 - .legal-page { 2526 - max-width: 800px; 2527 - margin: 0 auto; 2528 - padding: 2rem 1rem; 2529 - } 2530 - 2531 - .legal-page h1 { 2532 - font-size: 2rem; 2533 - margin-bottom: 0.5rem; 2534 - color: var(--fg); 2535 - } 2536 - 2537 - .legal-updated { 2538 - color: var(--secondary); 2539 - margin-bottom: 2rem; 2540 - } 2541 - 2542 - .legal-section { 2543 - margin: 2rem 0; 2544 - padding-bottom: 1.5rem; 2545 - border-bottom: 1px solid var(--border); 2546 - } 2547 - 2548 - .legal-section:last-child { 2549 - border-bottom: none; 2550 - } 2551 - 2552 - .legal-section h2 { 2553 - font-size: 1.5rem; 2554 - margin-bottom: 1rem; 2555 - color: var(--fg); 2556 - } 2557 - 2558 - .legal-section h3 { 2559 - font-size: 1.15rem; 2560 - margin: 1.5rem 0 0.75rem; 2561 - color: var(--fg); 2562 - } 2563 - 2564 - .legal-section p { 2565 - margin-bottom: 1rem; 2566 - line-height: 1.7; 2567 - } 2568 - 2569 - .legal-section ul, 2570 - .legal-section ol { 2571 - margin-bottom: 1rem; 2572 - padding-left: 2rem; 2573 - } 2574 - 2575 - .legal-section li { 2576 - margin-bottom: 0.5rem; 2577 - line-height: 1.6; 2578 - } 2579 - 2580 - .legal-section ul ul { 2581 - margin-top: 0.5rem; 2582 - margin-bottom: 0.5rem; 2583 - } 2584 - 2585 - .legal-section code { 2586 - background: var(--code-bg); 2587 - padding: 0.2rem 0.4rem; 2588 - border-radius: 3px; 2589 - font-family: "Monaco", "Menlo", monospace; 2590 - font-size: 0.9em; 2591 - } 2592 - 2593 - .legal-section a { 2594 - color: var(--primary); 2595 - text-decoration: underline; 2596 - } 2597 - 2598 - .legal-section a:hover { 2599 - color: var(--primary-dark); 2600 - } 2601 - 2602 - .legal-section table { 2603 - width: 100%; 2604 - border-collapse: collapse; 2605 - margin: 1rem 0; 2606 - } 2607 - 2608 - .legal-section table th, 2609 - .legal-section table td { 2610 - padding: 0.75rem 1rem; 2611 - border: 1px solid var(--border); 2612 - text-align: left; 2613 - } 2614 - 2615 - .legal-section table th { 2616 - background: var(--code-bg); 2617 - font-weight: 600; 2618 - } 2619 - 2620 - .legal-section table tr:nth-child(even) { 2621 - background: var(--hover-bg); 2622 - } 2623 - 2624 - .legal-disclaimer { 2625 - background: var(--code-bg); 2626 - padding: 1rem; 2627 - border-radius: 4px; 2628 - font-size: 0.95rem; 2629 - margin: 1rem 0; 2630 - } 2631 - 2632 - @media (max-width: 768px) { 2633 - .legal-page { 2634 - padding: 1rem 0.5rem; 2635 - } 2636 - 2637 - .legal-page h1 { 2638 - font-size: 1.5rem; 2639 - } 2640 - 2641 - .legal-section h2 { 2642 - font-size: 1.25rem; 2643 - } 2644 - 2645 - .legal-section table { 2646 - font-size: 0.85rem; 2647 - } 2648 - 2649 - .legal-section table th, 2650 - .legal-section table td { 2651 - padding: 0.5rem; 2652 - } 2653 - }
-834
pkg/appview/static/js/app.js
··· 1 - // Theme management 2 - // Load theme immediately to avoid flash 3 - (function() { 4 - const theme = localStorage.getItem('theme') || 'light'; 5 - document.documentElement.setAttribute('data-theme', theme); 6 - })(); 7 - 8 - function toggleTheme() { 9 - const html = document.documentElement; 10 - const currentTheme = html.getAttribute('data-theme') || 'light'; 11 - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 12 - html.setAttribute('data-theme', newTheme); 13 - localStorage.setItem('theme', newTheme); 14 - updateThemeIcon(); 15 - } 16 - 17 - function updateThemeIcon() { 18 - const themeBtn = document.getElementById('theme-toggle'); 19 - if (!themeBtn) return; 20 - 21 - const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; 22 - const icon = themeBtn.querySelector('.theme-icon'); 23 - 24 - if (icon) { 25 - // In dark mode, show sun icon (to switch to light) 26 - // In light mode, show moon icon (to switch to dark) 27 - icon.setAttribute('data-lucide', currentTheme === 'dark' ? 'sun' : 'moon'); 28 - 29 - // Re-initialize Lucide icons 30 - if (typeof lucide !== 'undefined') { 31 - lucide.createIcons(); 32 - } 33 - } 34 - 35 - themeBtn.setAttribute('aria-label', currentTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'); 36 - } 37 - 38 - // Expandable search 39 - function toggleSearch() { 40 - const wrapper = document.querySelector('.nav-search-wrapper'); 41 - const input = document.getElementById('nav-search-input'); 42 - 43 - if (!wrapper || !input) return; 44 - 45 - wrapper.classList.toggle('expanded'); 46 - 47 - if (wrapper.classList.contains('expanded')) { 48 - input.focus(); 49 - } 50 - } 51 - 52 - function closeSearch() { 53 - const wrapper = document.querySelector('.nav-search-wrapper'); 54 - if (wrapper) { 55 - wrapper.classList.remove('expanded'); 56 - } 57 - } 58 - 59 - // Close search on Escape key and click outside 60 - document.addEventListener('DOMContentLoaded', () => { 61 - const wrapper = document.querySelector('.nav-search-wrapper'); 62 - const input = document.getElementById('nav-search-input'); 63 - 64 - if (!wrapper || !input) return; 65 - 66 - // Close on Escape key 67 - document.addEventListener('keydown', (e) => { 68 - if (e.key === 'Escape' && wrapper.classList.contains('expanded')) { 69 - closeSearch(); 70 - } 71 - }); 72 - 73 - // Close on click outside 74 - document.addEventListener('click', (e) => { 75 - if (wrapper.classList.contains('expanded') && 76 - !wrapper.contains(e.target)) { 77 - closeSearch(); 78 - } 79 - }); 80 - }); 81 - 82 - // Copy to clipboard 83 - function copyToClipboard(text) { 84 - navigator.clipboard.writeText(text).then(() => { 85 - // Show success feedback 86 - const btn = event.target.closest('button'); 87 - const originalHTML = btn.innerHTML; 88 - btn.innerHTML = '<i data-lucide="check"></i> Copied!'; 89 - // Re-initialize Lucide icons for the new icon 90 - if (typeof lucide !== 'undefined') { 91 - lucide.createIcons(); 92 - } 93 - setTimeout(() => { 94 - btn.innerHTML = originalHTML; 95 - // Re-initialize Lucide icons to restore original icon 96 - if (typeof lucide !== 'undefined') { 97 - lucide.createIcons(); 98 - } 99 - }, 2000); 100 - }).catch(err => { 101 - console.error('Failed to copy:', err); 102 - }); 103 - } 104 - 105 - // Time ago helper (for client-side rendering) 106 - function timeAgo(date) { 107 - const seconds = Math.floor((new Date() - new Date(date)) / 1000); 108 - 109 - const intervals = { 110 - year: 31536000, 111 - month: 2592000, 112 - week: 604800, 113 - day: 86400, 114 - hour: 3600, 115 - minute: 60, 116 - second: 1 117 - }; 118 - 119 - for (const [name, secondsInInterval] of Object.entries(intervals)) { 120 - const interval = Math.floor(seconds / secondsInInterval); 121 - if (interval >= 1) { 122 - return interval === 1 ? `1 ${name} ago` : `${interval} ${name}s ago`; 123 - } 124 - } 125 - 126 - return 'just now'; 127 - } 128 - 129 - // Update timestamps on page load and HTMX swaps 130 - function updateTimestamps() { 131 - document.querySelectorAll('time[datetime]').forEach(el => { 132 - const date = el.getAttribute('datetime'); 133 - if (date && !el.dataset.noUpdate) { 134 - const ago = timeAgo(date); 135 - if (el.textContent !== ago) { 136 - el.textContent = ago; 137 - } 138 - } 139 - }); 140 - } 141 - 142 - // Initial timestamp update 143 - document.addEventListener('DOMContentLoaded', () => { 144 - updateTimestamps(); 145 - updateThemeIcon(); 146 - }); 147 - 148 - // Update timestamps after HTMX swaps 149 - document.addEventListener('htmx:afterSwap', updateTimestamps); 150 - 151 - // Update timestamps periodically 152 - setInterval(updateTimestamps, 60000); // Every minute 153 - 154 - // Toggle repository details (for images page) 155 - function toggleRepo(name) { 156 - const details = document.getElementById('repo-' + name); 157 - const btn = document.getElementById('btn-' + name); 158 - 159 - if (details.style.display === 'none') { 160 - details.style.display = 'block'; 161 - btn.innerHTML = '<i data-lucide="chevron-up"></i>'; 162 - } else { 163 - details.style.display = 'none'; 164 - btn.innerHTML = '<i data-lucide="chevron-down"></i>'; 165 - } 166 - 167 - // Re-initialize Lucide icons 168 - if (typeof lucide !== 'undefined') { 169 - lucide.createIcons(); 170 - } 171 - } 172 - 173 - // User dropdown menu 174 - document.addEventListener('DOMContentLoaded', () => { 175 - const menuBtn = document.getElementById('user-menu-btn'); 176 - const dropdownMenu = document.getElementById('user-dropdown-menu'); 177 - 178 - if (menuBtn && dropdownMenu) { 179 - // Toggle dropdown on button click 180 - menuBtn.addEventListener('click', (e) => { 181 - e.stopPropagation(); 182 - const isExpanded = menuBtn.getAttribute('aria-expanded') === 'true'; 183 - 184 - if (isExpanded) { 185 - closeDropdown(); 186 - } else { 187 - openDropdown(); 188 - } 189 - }); 190 - 191 - // Close dropdown when clicking outside 192 - document.addEventListener('click', (e) => { 193 - if (!menuBtn.contains(e.target) && !dropdownMenu.contains(e.target)) { 194 - closeDropdown(); 195 - } 196 - }); 197 - 198 - // Close dropdown on Escape key 199 - document.addEventListener('keydown', (e) => { 200 - if (e.key === 'Escape') { 201 - closeDropdown(); 202 - } 203 - }); 204 - 205 - function openDropdown() { 206 - menuBtn.setAttribute('aria-expanded', 'true'); 207 - dropdownMenu.removeAttribute('hidden'); 208 - } 209 - 210 - function closeDropdown() { 211 - menuBtn.setAttribute('aria-expanded', 'false'); 212 - dropdownMenu.setAttribute('hidden', ''); 213 - } 214 - } 215 - }); 216 - 217 - // Toggle star on a repository 218 - async function toggleStar(handle, repository) { 219 - const starBtn = document.getElementById('star-btn'); 220 - const starIcon = document.getElementById('star-icon'); 221 - const starCountEl = document.getElementById('star-count'); 222 - 223 - if (!starBtn || !starIcon || !starCountEl) return; 224 - 225 - // Disable button during request 226 - starBtn.disabled = true; 227 - 228 - try { 229 - // Check current state 230 - const isStarred = starIcon.classList.contains('star-filled'); 231 - const method = isStarred ? 'DELETE' : 'POST'; 232 - const url = `/api/stars/${handle}/${repository}`; 233 - 234 - const response = await fetch(url, { 235 - method: method, 236 - credentials: 'include', 237 - }); 238 - 239 - if (response.status === 401) { 240 - console.log('Not authenticated, redirecting to login'); 241 - // Not authenticated, redirect to login 242 - window.location.href = '/auth/oauth/login'; 243 - return; 244 - } 245 - 246 - if (!response.ok) { 247 - const errorText = await response.text(); 248 - console.error(`Toggle star failed: ${response.status} ${response.statusText}`, errorText); 249 - throw new Error(`Failed to toggle star: ${errorText}`); 250 - } 251 - 252 - const data = await response.json(); 253 - 254 - // Update UI optimistically 255 - if (data.starred) { 256 - starIcon.classList.add('star-filled'); 257 - starBtn.classList.add('starred'); 258 - // Optimistically increment count 259 - const currentCount = parseInt(starCountEl.textContent) || 0; 260 - starCountEl.textContent = currentCount + 1; 261 - } else { 262 - starIcon.classList.remove('star-filled'); 263 - starBtn.classList.remove('starred'); 264 - // Optimistically decrement count 265 - const currentCount = parseInt(starCountEl.textContent) || 0; 266 - starCountEl.textContent = Math.max(0, currentCount - 1); 267 - } 268 - 269 - // Don't fetch count immediately - trust the optimistic update 270 - // The actual count will be correct on next page load 271 - 272 - } catch (err) { 273 - console.error('Error toggling star:', err); 274 - alert(`Failed to toggle star: ${err.message}`); 275 - } finally { 276 - starBtn.disabled = false; 277 - } 278 - } 279 - 280 - // Load star status and count for current repository 281 - async function loadStarStatus() { 282 - const starBtn = document.getElementById('star-btn'); 283 - const starIcon = document.getElementById('star-icon'); 284 - 285 - if (!starBtn || !starIcon) return; // Not on repository page 286 - 287 - // Extract handle and repository from button onclick attribute 288 - const onclick = starBtn.getAttribute('onclick'); 289 - const match = onclick.match(/toggleStar\('([^']+)',\s*'([^']+)'\)/); 290 - if (!match) return; 291 - 292 - const handle = match[1]; 293 - const repository = match[2]; 294 - 295 - try { 296 - // Check if user has starred this repo 297 - const starResponse = await fetch(`/api/stars/${handle}/${repository}`, { 298 - credentials: 'include', 299 - }); 300 - 301 - if (starResponse.ok) { 302 - const starData = await starResponse.json(); 303 - console.log('Star status data:', starData); 304 - if (starData.starred) { 305 - starIcon.classList.add('star-filled'); 306 - starBtn.classList.add('starred'); 307 - } 308 - } else { 309 - const errorText = await starResponse.text(); 310 - console.error('Failed to load star status:', errorText); 311 - } 312 - 313 - // Load star count 314 - await loadStarCount(handle, repository); 315 - 316 - } catch (err) { 317 - console.error('Error loading star status:', err); 318 - } 319 - } 320 - 321 - // Load star count for a repository 322 - async function loadStarCount(handle, repository) { 323 - const starCountEl = document.getElementById('star-count'); 324 - if (!starCountEl) return; 325 - 326 - try { 327 - const statsResponse = await fetch(`/api/stats/${handle}/${repository}`, { 328 - credentials: 'include', 329 - }); 330 - 331 - if (statsResponse.ok) { 332 - const stats = await statsResponse.json(); 333 - console.log('Stats data:', stats); 334 - starCountEl.textContent = stats.star_count || 0; 335 - } else { 336 - const errorText = await statsResponse.text(); 337 - console.error('Failed to load stats:', errorText); 338 - } 339 - } catch (err) { 340 - console.error('Error loading star count:', err); 341 - } 342 - } 343 - 344 - // Toggle offline manifests visibility 345 - function toggleOfflineManifests() { 346 - const checkbox = document.getElementById('show-offline-toggle'); 347 - const manifestsList = document.querySelector('.manifests-list'); 348 - 349 - if (!checkbox || !manifestsList) return; 350 - 351 - // Store preference in localStorage 352 - localStorage.setItem('showOfflineManifests', checkbox.checked); 353 - 354 - // Toggle visibility of offline manifests 355 - if (checkbox.checked) { 356 - manifestsList.classList.add('show-offline'); 357 - } else { 358 - manifestsList.classList.remove('show-offline'); 359 - } 360 - } 361 - 362 - // Restore offline manifests toggle state on page load 363 - document.addEventListener('DOMContentLoaded', () => { 364 - const checkbox = document.getElementById('show-offline-toggle'); 365 - if (!checkbox) return; 366 - 367 - // Restore state from localStorage 368 - const showOffline = localStorage.getItem('showOfflineManifests') === 'true'; 369 - checkbox.checked = showOffline; 370 - 371 - // Apply initial state 372 - const manifestsList = document.querySelector('.manifests-list'); 373 - if (manifestsList) { 374 - if (showOffline) { 375 - manifestsList.classList.add('show-offline'); 376 - } else { 377 - manifestsList.classList.remove('show-offline'); 378 - } 379 - } 380 - }); 381 - 382 - // Delete manifest with confirmation for tagged manifests 383 - async function deleteManifest(repository, digest, sanitizedId) { 384 - try { 385 - // First, try to delete without confirmation 386 - const response = await fetch(`/api/images/${repository}/manifests/${digest}`, { 387 - method: 'DELETE', 388 - credentials: 'include', 389 - }); 390 - 391 - if (response.status === 409) { 392 - // Manifest has tags, need confirmation 393 - const data = await response.json(); 394 - showManifestDeleteModal(repository, digest, sanitizedId, data.tags); 395 - } else if (response.ok) { 396 - // Successfully deleted 397 - removeManifestElement(sanitizedId); 398 - } else { 399 - // Other error 400 - const errorText = await response.text(); 401 - alert(`Failed to delete manifest: ${errorText}`); 402 - } 403 - } catch (err) { 404 - console.error('Error deleting manifest:', err); 405 - alert(`Error deleting manifest: ${err.message}`); 406 - } 407 - } 408 - 409 - // Show the confirmation modal for deleting a tagged manifest 410 - function showManifestDeleteModal(repository, digest, sanitizedId, tags) { 411 - const modal = document.getElementById('manifest-delete-modal'); 412 - const tagsList = document.getElementById('manifest-delete-tags'); 413 - const confirmBtn = document.getElementById('confirm-manifest-delete-btn'); 414 - 415 - // Clear and populate tags list 416 - tagsList.innerHTML = ''; 417 - tags.forEach(tag => { 418 - const li = document.createElement('li'); 419 - li.textContent = tag; 420 - tagsList.appendChild(li); 421 - }); 422 - 423 - // Set up confirm button click handler 424 - confirmBtn.onclick = () => confirmManifestDelete(repository, digest, sanitizedId); 425 - 426 - // Show modal 427 - modal.style.display = 'flex'; 428 - } 429 - 430 - // Close the manifest delete confirmation modal 431 - function closeManifestDeleteModal() { 432 - const modal = document.getElementById('manifest-delete-modal'); 433 - modal.style.display = 'none'; 434 - } 435 - 436 - // Confirm and execute manifest deletion with all tags 437 - async function confirmManifestDelete(repository, digest, sanitizedId) { 438 - const confirmBtn = document.getElementById('confirm-manifest-delete-btn'); 439 - const originalText = confirmBtn.textContent; 440 - 441 - try { 442 - // Disable button and show loading state 443 - confirmBtn.disabled = true; 444 - confirmBtn.textContent = 'Deleting...'; 445 - 446 - // Delete with confirmation 447 - const response = await fetch(`/api/images/${repository}/manifests/${digest}?confirm=true`, { 448 - method: 'DELETE', 449 - credentials: 'include', 450 - }); 451 - 452 - if (response.ok) { 453 - // Successfully deleted 454 - closeManifestDeleteModal(); 455 - removeManifestElement(sanitizedId); 456 - // Also remove any tag elements that were deleted 457 - location.reload(); // Reload to refresh the tags list 458 - } else { 459 - // Error 460 - const errorText = await response.text(); 461 - alert(`Failed to delete manifest: ${errorText}`); 462 - confirmBtn.disabled = false; 463 - confirmBtn.textContent = originalText; 464 - } 465 - } catch (err) { 466 - console.error('Error deleting manifest:', err); 467 - alert(`Error deleting manifest: ${err.message}`); 468 - confirmBtn.disabled = false; 469 - confirmBtn.textContent = originalText; 470 - } 471 - } 472 - 473 - // Remove a manifest element from the DOM 474 - function removeManifestElement(sanitizedId) { 475 - const element = document.getElementById(`manifest-${sanitizedId}`); 476 - if (element) { 477 - element.remove(); 478 - } 479 - } 480 - 481 - // Upload repository avatar 482 - async function uploadAvatar(input, repository) { 483 - const file = input.files[0]; 484 - if (!file) return; 485 - 486 - // Client-side validation 487 - const validTypes = ['image/png', 'image/jpeg', 'image/webp']; 488 - if (!validTypes.includes(file.type)) { 489 - alert('Please select a PNG, JPEG, or WebP image'); 490 - return; 491 - } 492 - if (file.size > 3 * 1024 * 1024) { 493 - alert('Image must be less than 3MB'); 494 - return; 495 - } 496 - 497 - const formData = new FormData(); 498 - formData.append('avatar', file); 499 - 500 - try { 501 - const response = await fetch(`/api/images/${repository}/avatar`, { 502 - method: 'POST', 503 - credentials: 'include', 504 - body: formData 505 - }); 506 - 507 - if (response.status === 401) { 508 - window.location.href = '/auth/oauth/login'; 509 - return; 510 - } 511 - 512 - if (!response.ok) { 513 - const error = await response.text(); 514 - throw new Error(error); 515 - } 516 - 517 - const data = await response.json(); 518 - 519 - // Update the avatar image on the page 520 - const wrapper = document.querySelector('.repo-hero-icon-wrapper'); 521 - if (!wrapper) return; 522 - 523 - const existingImg = wrapper.querySelector('.repo-hero-icon'); 524 - const placeholder = wrapper.querySelector('.repo-hero-icon-placeholder'); 525 - 526 - if (existingImg) { 527 - existingImg.src = data.avatarURL; 528 - } else if (placeholder) { 529 - const newImg = document.createElement('img'); 530 - newImg.src = data.avatarURL; 531 - newImg.alt = repository; 532 - newImg.className = 'repo-hero-icon'; 533 - placeholder.replaceWith(newImg); 534 - } 535 - } catch (err) { 536 - console.error('Error uploading avatar:', err); 537 - alert('Failed to upload avatar: ' + err.message); 538 - } 539 - 540 - // Clear input so same file can be selected again 541 - input.value = ''; 542 - } 543 - 544 - // Close modal when clicking outside 545 - document.addEventListener('DOMContentLoaded', () => { 546 - const modal = document.getElementById('manifest-delete-modal'); 547 - if (modal) { 548 - modal.addEventListener('click', (e) => { 549 - if (e.target === modal) { 550 - closeManifestDeleteModal(); 551 - } 552 - }); 553 - } 554 - }); 555 - 556 - // Login page typeahead functionality 557 - class LoginTypeahead { 558 - constructor(inputElement) { 559 - this.input = inputElement; 560 - this.dropdown = null; 561 - this.debounceTimer = null; 562 - this.currentFocus = -1; 563 - this.results = []; 564 - this.isLoading = false; 565 - 566 - this.init(); 567 - } 568 - 569 - init() { 570 - // Create dropdown element 571 - this.createDropdown(); 572 - 573 - // Event listeners 574 - this.input.addEventListener('input', (e) => this.handleInput(e)); 575 - this.input.addEventListener('keydown', (e) => this.handleKeydown(e)); 576 - this.input.addEventListener('focus', () => this.handleFocus()); 577 - 578 - // Close dropdown when clicking outside 579 - document.addEventListener('click', (e) => { 580 - if (!this.input.contains(e.target) && !this.dropdown.contains(e.target)) { 581 - this.hideDropdown(); 582 - } 583 - }); 584 - } 585 - 586 - createDropdown() { 587 - this.dropdown = document.createElement('div'); 588 - this.dropdown.className = 'typeahead-dropdown'; 589 - this.dropdown.style.display = 'none'; 590 - this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling); 591 - } 592 - 593 - handleInput(e) { 594 - const value = e.target.value.trim(); 595 - 596 - // Clear debounce timer 597 - clearTimeout(this.debounceTimer); 598 - 599 - if (value.length < 2) { 600 - this.showRecentAccounts(); 601 - return; 602 - } 603 - 604 - // Debounce API call (200ms) 605 - this.debounceTimer = setTimeout(() => { 606 - this.searchActors(value); 607 - }, 200); 608 - } 609 - 610 - handleFocus() { 611 - const value = this.input.value.trim(); 612 - if (value.length < 2) { 613 - this.showRecentAccounts(); 614 - } 615 - } 616 - 617 - async searchActors(query) { 618 - this.isLoading = true; 619 - this.showLoading(); 620 - 621 - try { 622 - const url = `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=3`; 623 - const response = await fetch(url); 624 - 625 - if (!response.ok) { 626 - throw new Error('Failed to fetch suggestions'); 627 - } 628 - 629 - const data = await response.json(); 630 - this.results = data.actors || []; 631 - this.renderResults(); 632 - } catch (err) { 633 - console.error('Typeahead error:', err); 634 - this.hideDropdown(); 635 - } finally { 636 - this.isLoading = false; 637 - } 638 - } 639 - 640 - showLoading() { 641 - this.dropdown.innerHTML = '<div class="typeahead-loading">Searching...</div>'; 642 - this.dropdown.style.display = 'block'; 643 - } 644 - 645 - renderResults() { 646 - if (this.results.length === 0) { 647 - this.hideDropdown(); 648 - return; 649 - } 650 - 651 - this.dropdown.innerHTML = ''; 652 - this.currentFocus = -1; 653 - 654 - this.results.slice(0, 3).forEach((actor, index) => { 655 - const item = this.createResultItem(actor, index); 656 - this.dropdown.appendChild(item); 657 - }); 658 - 659 - this.dropdown.style.display = 'block'; 660 - } 661 - 662 - createResultItem(actor, index) { 663 - const item = document.createElement('div'); 664 - item.className = 'typeahead-item'; 665 - item.dataset.index = index; 666 - item.dataset.handle = actor.handle; 667 - 668 - // Avatar 669 - const avatar = document.createElement('img'); 670 - avatar.className = 'typeahead-avatar'; 671 - avatar.src = actor.avatar || '/static/images/default-avatar.png'; 672 - avatar.alt = actor.handle; 673 - avatar.onerror = () => { 674 - avatar.src = '/static/images/default-avatar.png'; 675 - }; 676 - 677 - // Text container 678 - const textContainer = document.createElement('div'); 679 - textContainer.className = 'typeahead-text'; 680 - 681 - // Display name 682 - const displayName = document.createElement('div'); 683 - displayName.className = 'typeahead-displayname'; 684 - displayName.textContent = actor.displayName || actor.handle; 685 - 686 - // Handle 687 - const handle = document.createElement('div'); 688 - handle.className = 'typeahead-handle'; 689 - handle.textContent = `@${actor.handle}`; 690 - 691 - textContainer.appendChild(displayName); 692 - textContainer.appendChild(handle); 693 - 694 - item.appendChild(avatar); 695 - item.appendChild(textContainer); 696 - 697 - // Click handler 698 - item.addEventListener('click', () => this.selectItem(actor.handle)); 699 - 700 - return item; 701 - } 702 - 703 - showRecentAccounts() { 704 - const recent = this.getRecentAccounts(); 705 - if (recent.length === 0) { 706 - this.hideDropdown(); 707 - return; 708 - } 709 - 710 - this.dropdown.innerHTML = ''; 711 - this.currentFocus = -1; 712 - 713 - const header = document.createElement('div'); 714 - header.className = 'typeahead-header'; 715 - header.textContent = 'Recent accounts'; 716 - this.dropdown.appendChild(header); 717 - 718 - recent.forEach((handle, index) => { 719 - const item = document.createElement('div'); 720 - item.className = 'typeahead-item typeahead-recent'; 721 - item.dataset.index = index; 722 - item.dataset.handle = handle; 723 - 724 - const textContainer = document.createElement('div'); 725 - textContainer.className = 'typeahead-text'; 726 - 727 - const handleDiv = document.createElement('div'); 728 - handleDiv.className = 'typeahead-handle'; 729 - handleDiv.textContent = handle; 730 - 731 - textContainer.appendChild(handleDiv); 732 - item.appendChild(textContainer); 733 - 734 - item.addEventListener('click', () => this.selectItem(handle)); 735 - 736 - this.dropdown.appendChild(item); 737 - }); 738 - 739 - this.dropdown.style.display = 'block'; 740 - } 741 - 742 - selectItem(handle) { 743 - this.input.value = handle; 744 - this.hideDropdown(); 745 - this.saveRecentAccount(handle); 746 - // Optionally submit the form automatically 747 - // this.input.form.submit(); 748 - } 749 - 750 - hideDropdown() { 751 - this.dropdown.style.display = 'none'; 752 - this.currentFocus = -1; 753 - } 754 - 755 - handleKeydown(e) { 756 - // If dropdown is hidden, only respond to ArrowDown to show it 757 - if (this.dropdown.style.display === 'none') { 758 - if (e.key === 'ArrowDown') { 759 - e.preventDefault(); 760 - const value = this.input.value.trim(); 761 - if (value.length >= 2) { 762 - this.searchActors(value); 763 - } else { 764 - this.showRecentAccounts(); 765 - } 766 - } 767 - return; 768 - } 769 - 770 - const items = this.dropdown.querySelectorAll('.typeahead-item'); 771 - 772 - if (e.key === 'ArrowDown') { 773 - e.preventDefault(); 774 - this.currentFocus++; 775 - if (this.currentFocus >= items.length) this.currentFocus = 0; 776 - this.updateFocus(items); 777 - } else if (e.key === 'ArrowUp') { 778 - e.preventDefault(); 779 - this.currentFocus--; 780 - if (this.currentFocus < 0) this.currentFocus = items.length - 1; 781 - this.updateFocus(items); 782 - } else if (e.key === 'Enter') { 783 - if (this.currentFocus > -1 && items[this.currentFocus]) { 784 - e.preventDefault(); 785 - const handle = items[this.currentFocus].dataset.handle; 786 - this.selectItem(handle); 787 - } 788 - } else if (e.key === 'Escape') { 789 - this.hideDropdown(); 790 - } 791 - } 792 - 793 - updateFocus(items) { 794 - items.forEach((item, index) => { 795 - if (index === this.currentFocus) { 796 - item.classList.add('typeahead-focused'); 797 - } else { 798 - item.classList.remove('typeahead-focused'); 799 - } 800 - }); 801 - } 802 - 803 - getRecentAccounts() { 804 - try { 805 - const recent = localStorage.getItem('atcr_recent_handles'); 806 - return recent ? JSON.parse(recent) : []; 807 - } catch { 808 - return []; 809 - } 810 - } 811 - 812 - saveRecentAccount(handle) { 813 - try { 814 - let recent = this.getRecentAccounts(); 815 - // Remove if already exists 816 - recent = recent.filter(h => h !== handle); 817 - // Add to front 818 - recent.unshift(handle); 819 - // Keep only last 5 820 - recent = recent.slice(0, 5); 821 - localStorage.setItem('atcr_recent_handles', JSON.stringify(recent)); 822 - } catch (err) { 823 - console.error('Failed to save recent account:', err); 824 - } 825 - } 826 - } 827 - 828 - // Initialize typeahead on login page 829 - document.addEventListener('DOMContentLoaded', () => { 830 - const handleInput = document.getElementById('handle'); 831 - if (handleInput && handleInput.closest('.login-form')) { 832 - new LoginTypeahead(handleInput); 833 - } 834 - });
pkg/appview/static/static/install.ps1 pkg/appview/public/static/install.ps1
pkg/appview/static/static/install.sh pkg/appview/public/static/install.sh
+5 -5
pkg/appview/templates/components/docker-command.html
··· 5 5 Expects: string - the docker command to display 6 6 Usage: {{ template "docker-command" "docker pull atcr.io/alice/myapp:latest" }} 7 7 */}} 8 - <div class="docker-command"> 9 - <i data-lucide="terminal" class="docker-command-icon"></i> 10 - <code class="docker-command-text">{{ . }}</code> 11 - <button class="copy-btn" onclick="copyToClipboard(this.getAttribute('data-cmd'))" data-cmd="{{ . }}"> 12 - <i data-lucide="copy"></i> 8 + <div class="cmd group" onclick="event.stopPropagation()"> 9 + <i data-lucide="terminal" class="size-4 shrink-0 text-base-content/60"></i> 10 + <code>{{ . }}</code> 11 + <button class="btn btn-ghost btn-xs absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" onclick="event.stopPropagation(); copyToClipboard(this.getAttribute('data-cmd'))" data-cmd="{{ . }}"> 12 + <i data-lucide="copy" class="size-4"></i> 13 13 </button> 14 14 </div> 15 15 {{ end }}
+18 -20
pkg/appview/templates/components/head.html
··· 9 9 <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> 10 10 <link rel="manifest" href="/site.webmanifest" /> 11 11 12 - <!-- Stylesheets --> 13 - <link rel="stylesheet" href="/css/style.css"> 14 - 15 - <!-- HTMX (vendored) --> 16 - <script src="/js/htmx.min.js"></script> 17 - 18 - <!-- Lucide Icons (vendored) --> 19 - <script src="/js/lucide.min.js"></script> 20 - 21 - <!-- App Scripts --> 22 - <script src="/js/app.js"></script> 12 + <!-- Theme: apply early to prevent flash --> 23 13 <script> 24 - // Initialize Lucide icons after DOM is loaded 25 - document.addEventListener('DOMContentLoaded', () => { 26 - lucide.createIcons(); 27 - 28 - // Re-initialize icons after HTMX swaps content 29 - document.body.addEventListener('htmx:afterSwap', () => { 30 - lucide.createIcons(); 31 - }); 32 - }); 14 + (function() { 15 + function getEffectiveTheme(pref) { 16 + if (pref === 'dark') return 'dark'; 17 + if (pref === 'light') return 'light'; 18 + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 19 + } 20 + var pref = localStorage.getItem('theme') || 'system'; 21 + var effective = getEffectiveTheme(pref); 22 + document.documentElement.classList.toggle('dark', effective === 'dark'); 23 + document.documentElement.setAttribute('data-theme', effective); 24 + })(); 33 25 </script> 26 + 27 + <!-- Tailwind CSS (built via npm run css:build) --> 28 + <link rel="stylesheet" href="/css/style.css"> 29 + 30 + <!-- Bundled JS: HTMX + Lucide (tree-shaken) + Actor Typeahead + App --> 31 + <script type="module" src="/js/bundle.min.js"></script> 34 32 {{ end }}
+40
pkg/appview/templates/components/hero.html
··· 1 + {{ define "hero" }} 2 + {{/* 3 + Hero section component - displays landing page hero for non-authenticated users 4 + Required: .Benefits ([]Benefit with Icon, Title, Description fields) 5 + */}} 6 + <section class="hero bg-base-200 min-h-[60vh] py-16 pb-24 relative"> 7 + <div class="hero-content text-center flex-col"> 8 + <h1 class="text-4xl md:text-5xl font-bold">ship containers on the open web.</h1> 9 + <p class="text-lg text-base-content/70 max-w-lg mt-4"> 10 + Push and pull Docker images on the AT Protocol.<br> 11 + Browse public registries or control your data. 12 + </p> 13 + 14 + <div class="mockup-code bg-base-300 text-left w-full max-w-lg text-base mt-8"> 15 + <pre data-prefix="$"><code>docker login atcr.io</code></pre> 16 + <pre data-prefix="$"><code>docker push atcr.io/you/app</code></pre> 17 + <pre data-prefix="#" class="text-base-content/50"><code>same docker, decentralized</code></pre> 18 + </div> 19 + 20 + <div class="flex items-center justify-center gap-4 mt-8"> 21 + <a href="/auth/oauth/login?return_to=/" class="btn btn-primary btn-lg">Get Started</a> 22 + <a href="/install" class="btn btn-ghost btn-lg">Learn More</a> 23 + </div> 24 + 25 + <!-- Benefit Cards --> 26 + <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12 w-full max-w-4xl"> 27 + {{ range .Benefits }} 28 + <div class="card bg-base-100 shadow-sm p-6 text-center"> 29 + <div class="text-primary mb-4 flex justify-center"> 30 + <i data-lucide="{{ .Icon }}" class="size-8"></i> 31 + </div> 32 + <h3 class="font-semibold text-lg">{{ .Title }}</h3> 33 + <p class="text-base-content/70 mt-2">{{ .Description }}</p> 34 + </div> 35 + {{ end }} 36 + </div> 37 + </div> 38 + <img src="/static/wave-pattern.svg" alt="" class="absolute bottom-0 left-0 w-full h-16 pointer-events-none" aria-hidden="true"> 39 + </section> 40 + {{ end }}
+18 -15
pkg/appview/templates/components/modal.html
··· 1 1 {{ define "manifest-modal" }} 2 - <div class="modal-overlay" onclick="this.remove()"> 3 - <div class="modal-content" onclick="event.stopPropagation()"> 4 - <button class="modal-close" onclick="this.closest('.modal-overlay').remove()">✕</button> 2 + <dialog class="modal modal-open" onclick="if(event.target===this)this.remove()"> 3 + <div class="modal-box"> 4 + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="this.closest('dialog').remove()">✕</button> 5 5 6 - <h2>Manifest Details</h2> 6 + <h2 class="text-xl font-semibold mb-4">Manifest Details</h2> 7 7 8 - <div class="manifest-info"> 9 - <div class="info-row"> 10 - <strong>Digest:</strong> 11 - <code>{{ .Digest }}</code> 8 + <div class="space-y-3"> 9 + <div class="flex justify-between items-center"> 10 + <strong class="text-base-content/60 min-w-[150px]">Digest:</strong> 11 + <code class="font-mono text-sm">{{ .Digest }}</code> 12 12 </div> 13 - <div class="info-row"> 14 - <strong>Media Type:</strong> 13 + <div class="flex justify-between items-center"> 14 + <strong class="text-base-content/60 min-w-[150px]">Media Type:</strong> 15 15 <span>{{ .MediaType }}</span> 16 16 </div> 17 - <div class="info-row"> 18 - <strong>Hold Endpoint:</strong> 17 + <div class="flex justify-between items-center"> 18 + <strong class="text-base-content/60 min-w-[150px]">Hold Endpoint:</strong> 19 19 <span>{{ .HoldEndpoint }}</span> 20 20 </div> 21 - <div class="info-row"> 22 - <strong>Created:</strong> 21 + <div class="flex justify-between items-center"> 22 + <strong class="text-base-content/60 min-w-[150px]">Created:</strong> 23 23 <time datetime="{{ .CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 24 24 {{ .CreatedAt.Format "2006-01-02 15:04:05 MST" }} 25 25 </time> 26 26 </div> 27 27 </div> 28 28 </div> 29 - </div> 29 + <form method="dialog" class="modal-backdrop"> 30 + <button onclick="this.closest('dialog').remove()">close</button> 31 + </form> 32 + </dialog> 30 33 {{ end }}
+1 -3
pkg/appview/templates/components/nav-brand.html
··· 1 1 {{ define "nav-brand" }} 2 - <div class="nav-brand"> 3 - <a href="/"><span class="at-protocol">at://</span>Container Registry</a> 4 - </div> 2 + <a href="/" class="text-2xl font-bold text-neutral-content no-underline"><span class="text-primary">at://</span>Container Registry</a> 5 3 {{ end }}
+3 -3
pkg/appview/templates/components/nav-search.html
··· 1 1 {{ define "nav-search" }} 2 2 <div class="nav-search-wrapper"> 3 - <button id="search-toggle" onclick="toggleSearch()" class="btn-link search-toggle-btn" aria-label="Search"> 4 - <i data-lucide="search" class="search-icon"></i> 3 + <button onclick="toggleSearch()" class="btn btn-ghost btn-circle" aria-label="Search"> 4 + <i data-lucide="search" class="size-5"></i> 5 5 </button> 6 6 <form action="/search" method="get" class="nav-search-form"> 7 - <input type="text" id="nav-search-input" name="q" placeholder="Search images..." value="{{ .Query }}" /> 7 + <input type="text" id="nav-search-input" name="q" placeholder="Search images..." value="{{ .Query }}" class="input input-sm input-bordered" /> 8 8 </form> 9 9 </div> 10 10 {{ end }}
+28 -3
pkg/appview/templates/components/nav-theme-toggle.html
··· 1 1 {{ define "nav-theme-toggle" }} 2 - <button id="theme-toggle" onclick="toggleTheme()" class="btn-link theme-toggle-btn" aria-label="Toggle theme"> 3 - <i data-lucide="moon" class="theme-icon"></i> 4 - </button> 2 + <details class="dropdown dropdown-end"> 3 + <summary id="theme-toggle-btn" class="btn btn-ghost btn-circle list-none" aria-label="Theme settings"> 4 + <i data-lucide="sun" id="theme-icon" class="size-5"></i> 5 + </summary> 6 + <ul id="theme-dropdown-menu" class="dropdown-content menu bg-base-100 text-base-content rounded-box z-50 w-40 p-2 shadow-lg"> 7 + <li> 8 + <button type="button" class="theme-option" data-value="system"> 9 + <i data-lucide="sun-moon" class="size-4"></i> 10 + <span>System</span> 11 + <i data-lucide="check" class="size-4 ml-auto text-primary theme-check invisible"></i> 12 + </button> 13 + </li> 14 + <li> 15 + <button type="button" class="theme-option" data-value="light"> 16 + <i data-lucide="sun" class="size-4"></i> 17 + <span>Light</span> 18 + <i data-lucide="check" class="size-4 ml-auto text-primary theme-check invisible"></i> 19 + </button> 20 + </li> 21 + <li> 22 + <button type="button" class="theme-option" data-value="dark"> 23 + <i data-lucide="moon" class="size-4"></i> 24 + <span>Dark</span> 25 + <i data-lucide="check" class="size-4 ml-auto text-primary theme-check invisible"></i> 26 + </button> 27 + </li> 28 + </ul> 29 + </details> 5 30 {{ end }}
+21 -18
pkg/appview/templates/components/nav-user.html
··· 1 1 {{ define "nav-user" }} 2 2 {{ if .User }} 3 - <div class="user-dropdown"> 4 - <button class="user-menu-btn" id="user-menu-btn" aria-expanded="false" aria-haspopup="true"> 3 + <details class="dropdown dropdown-end"> 4 + <summary class="btn btn-ghost gap-2 list-none" aria-label="User menu"> 5 + <div class="avatar{{ if not .User.Avatar }} avatar-placeholder{{ end }}"> 5 6 {{ if .User.Avatar }} 6 - <img src="{{ .User.Avatar }}" alt="{{ .User.Handle }}" class="user-avatar"> 7 + <div class="w-7 rounded-full"> 8 + <img src="{{ .User.Avatar }}" alt="{{ .User.Handle }}" /> 9 + </div> 7 10 {{ else }} 8 - <div class="user-avatar-placeholder">{{ firstChar .User.Handle }}</div> 11 + <div class="bg-neutral text-neutral-content w-7 rounded-full"> 12 + <span class="text-xs">{{ firstChar .User.Handle }}</span> 13 + </div> 9 14 {{ end }} 10 - <span class="user-handle">@{{ .User.Handle }}</span> 11 - <svg class="dropdown-arrow" width="12" height="12" viewBox="0 0 12 12" fill="currentColor"> 12 - <path d="M6 9L1 4h10z"/> 13 - </svg> 14 - </button> 15 - <div class="dropdown-menu" id="user-dropdown-menu" hidden> 16 - <a href="/u/{{ .User.Handle }}" class="dropdown-item">Your Repositories</a> 17 - <a href="/settings" class="dropdown-item">Settings</a> 18 - <hr class="dropdown-divider"> 19 - <form action="/auth/logout" method="POST"> 20 - <button type="submit" class="dropdown-item logout-btn">Logout</button> 21 - </form> 22 15 </div> 23 - </div> 16 + <span class="hidden sm:inline">@{{ .User.Handle }}</span> 17 + <i data-lucide="chevron-down" class="size-3.5"></i> 18 + </summary> 19 + <ul class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg"> 20 + <li><a href="/u/{{ .User.Handle }}">Your Repositories</a></li> 21 + <li><a href="/settings">Settings</a></li> 22 + <li class="border-t border-base-300 mt-2 pt-2"> 23 + <a href="/auth/logout" class="text-error" onclick="event.preventDefault(); fetch('/auth/logout', {method: 'POST', credentials: 'same-origin'}).then(() => window.location.href = '/');">Logout</a> 24 + </li> 25 + </ul> 26 + </details> 24 27 {{ else }} 25 - <a href="/auth/oauth/login?return_to=/" class="btn-primary">Login</a> 28 + <button type="button" onclick="window.location='/auth/oauth/login?return_to=/'" class="btn btn-primary btn-sm">Login</button> 26 29 {{ end }} 27 30 {{ end }}
+10 -6
pkg/appview/templates/components/nav.html
··· 1 1 {{ define "nav" }} 2 - <nav class="navbar"> 3 - {{ template "nav-brand" }} 4 - <div class="nav-links"> 2 + <nav class="navbar bg-neutral text-neutral-content px-4"> 3 + <div class="navbar-start"> 4 + {{ template "nav-brand" }} 5 + </div> 6 + <div class="navbar-end flex items-center gap-2"> 5 7 {{ template "nav-search" . }} 6 8 {{ template "nav-theme-toggle" }} 7 9 {{ template "nav-user" . }} ··· 10 12 {{ end }} 11 13 12 14 {{ define "nav-simple" }} 13 - <nav class="navbar"> 14 - {{ template "nav-brand" }} 15 - <div class="nav-links"> 15 + <nav class="navbar bg-neutral text-neutral-content px-4"> 16 + <div class="navbar-start"> 17 + {{ template "nav-brand" }} 18 + </div> 19 + <div class="navbar-end flex items-center gap-2"> 16 20 {{ template "nav-theme-toggle" }} 17 21 </div> 18 22 </nav>
+10
pkg/appview/templates/components/pull-count.html
··· 1 + {{ define "pull-count" }} 2 + {{/* 3 + Pull count component - displays download icon with count 4 + Required: .PullCount (int) 5 + */}} 6 + <span class="flex items-center gap-2 text-base-content/60"> 7 + <i data-lucide="arrow-down-to-line" class="size-[1.1rem] text-primary"></i> 8 + <span class="font-semibold text-base-content">{{ .PullCount }}</span> 9 + </span> 10 + {{ end }}
+48 -24
pkg/appview/templates/components/repo-card.html
··· 11 11 - StarCount: int - Number of stars 12 12 - PullCount: int - Number of pulls 13 13 - ArtifactType: string - container-image, helm-chart, unknown 14 + - Tag: string (optional) - Latest tag name 15 + - Digest: string (optional) - Latest manifest digest 16 + - LastUpdated: time.Time (optional) - Last push time 14 17 */}} 15 - <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="featured-card"> 16 - <div class="featured-header"> 18 + <div class="card card-border card-interactive bg-base-100 p-6 flex flex-col justify-between min-h-60 w-full" onclick="window.location='/r/{{ .OwnerHandle }}/{{ .Repository }}'"> 19 + <div class="flex gap-4 items-start"> 17 20 {{ if .IconURL }} 18 - <img src="{{ .IconURL }}" alt="{{ .Repository }}" class="featured-icon"> 21 + <img src="{{ .IconURL }}" alt="{{ .Repository }}" class="w-12 rounded-lg object-cover shrink-0"> 19 22 {{ else }} 20 - <div class="featured-icon-placeholder">{{ firstChar .Repository }}</div> 23 + <div class="avatar avatar-placeholder"> 24 + <div class="bg-neutral text-neutral-content w-12 rounded-lg shadow-sm uppercase"> 25 + <span class="text-lg">{{ firstChar .Repository }}</span> 26 + </div> 27 + </div> 21 28 {{ end }} 22 - <div class="featured-info"> 23 - <div class="featured-title"> 24 - <span class="featured-owner">{{ .OwnerHandle }}</span> 25 - <span class="featured-separator">/</span> 26 - <span class="featured-name">{{ .Repository }}</span> 27 - {{ if eq .ArtifactType "helm-chart" }} 28 - <span class="artifact-badge helm"><i data-lucide="anchor"></i></span> 29 - {{ end }} 29 + <div class="flex-1 min-w-0"> 30 + <div class="font-semibold text-sm truncate"> 31 + <a href="/u/{{ .OwnerHandle }}" class="link link-primary" onclick="event.stopPropagation()">{{ .OwnerHandle }}</a> 32 + <span class="text-base-content/60">/</span> 33 + <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="link text-base-content hover:underline" onclick="event.stopPropagation()">{{ .Repository }}</a> 30 34 </div> 31 - {{ if .Description }} 32 - <p class="featured-description">{{ .Description }}</p> 35 + {{ if .Tag }} 36 + <span class="block text-base-content/60 text-sm truncate">Tag: {{ .Tag }}</span> 33 37 {{ end }} 34 38 </div> 35 39 </div> 36 - <div class="featured-stats"> 37 - <span class="featured-stat"> 38 - <i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}"></i> 39 - <span class="stat-count">{{ .StarCount }}</span> 40 - </span> 41 - <span class="featured-stat"> 42 - <i data-lucide="arrow-down-to-line" class="pull-icon"></i> 43 - <span class="stat-count">{{ .PullCount }}</span> 44 - </span> 40 + {{ if .Description }} 41 + <p class="text-base-content/60 text-sm line-clamp-3 m-0 my-4">{{ .Description }}</p> 42 + {{ end }} 43 + <div class="flex-1 flex flex-col justify-end py-2 min-w-0"> 44 + {{ if eq .ArtifactType "helm-chart" }} 45 + {{ if .Tag }} 46 + {{ template "docker-command" (printf "helm pull oci://atcr.io/%s/%s --version %s" .OwnerHandle .Repository .Tag) }} 47 + {{ else }} 48 + {{ template "docker-command" (printf "helm pull oci://atcr.io/%s/%s" .OwnerHandle .Repository) }} 49 + {{ end }} 50 + {{ else }} 51 + {{ if .Tag }} 52 + {{ template "docker-command" (printf "docker pull atcr.io/%s/%s:%s" .OwnerHandle .Repository .Tag) }} 53 + {{ else }} 54 + {{ template "docker-command" (printf "docker pull atcr.io/%s/%s" .OwnerHandle .Repository) }} 55 + {{ end }} 56 + {{ end }} 45 57 </div> 46 - </a> 58 + <div class="flex justify-between items-center pt-3 border-t border-base-300"> 59 + <div class="flex gap-6 items-center"> 60 + {{ template "star" (dict "IsStarred" .IsStarred "StarCount" .StarCount) }} 61 + {{ template "pull-count" (dict "PullCount" .PullCount) }} 62 + {{ if eq .ArtifactType "helm-chart" }} 63 + <span class="badge badge-sm badge-soft badge-primary" title="Helm chart"><i data-lucide="anchor"></i></span> 64 + {{ end }} 65 + </div> 66 + {{ if not .LastUpdated.IsZero }} 67 + <span class="text-base-content/60 text-sm">{{ timeAgo .LastUpdated }}</span> 68 + {{ end }} 69 + </div> 70 + </div> 47 71 {{ end }}
+30
pkg/appview/templates/components/star.html
··· 1 + {{ define "star" }} 2 + {{/* 3 + Star component - displays star icon with count 4 + Required: .IsStarred (bool), .StarCount (int) 5 + Optional: .Interactive (bool), .Handle (string), .Repository (string) 6 + 7 + Interactive mode: renders as button with HTMX toggle 8 + Display mode: renders as span (default) 9 + */}} 10 + {{ if .Interactive }} 11 + <button class="btn btn-sm gap-2{{ if .IsStarred }} btn-primary{{ else }} btn-ghost{{ end }}" 12 + id="star-btn" 13 + {{ if .IsStarred }} 14 + hx-delete="/api/stars/{{ .Handle }}/{{ .Repository }}" 15 + {{ else }} 16 + hx-post="/api/stars/{{ .Handle }}/{{ .Repository }}" 17 + {{ end }} 18 + hx-swap="outerHTML" 19 + hx-on::before-request="this.disabled=true" 20 + hx-on::after-request="if(event.detail.xhr.status===401) window.location='/auth/oauth/login'"> 21 + <i data-lucide="star" class="size-4 text-amber-400 stroke-amber-400{{ if .IsStarred }} fill-amber-400{{ end }}" id="star-icon"></i> 22 + <span id="star-count">{{ .StarCount }}</span> 23 + </button> 24 + {{ else }} 25 + <span class="flex items-center gap-2 text-base-content/60"> 26 + <i data-lucide="star" class="size-[1.1rem] text-amber-400 stroke-amber-400{{ if .IsStarred }} fill-amber-400{{ end }}"></i> 27 + <span class="font-semibold text-base-content">{{ .StarCount }}</span> 28 + </span> 29 + {{ end }} 30 + {{ end }}
+9 -7
pkg/appview/templates/pages/404.html
··· 7 7 </head> 8 8 <body> 9 9 {{ template "nav-simple" . }} 10 - <main class="error-page"> 11 - <div class="error-content"> 12 - <i data-lucide="anchor" class="error-icon"></i> 13 - <div class="error-code">404</div> 14 - <h1>Lost at Sea</h1> 15 - <p>The page you're looking for has drifted into uncharted waters.</p> 16 - <a href="/" class="btn btn-primary">Return to Port</a> 10 + <main class="hero min-h-[60vh]"> 11 + <div class="hero-content text-center"> 12 + <div class="flex flex-col items-center"> 13 + <i data-lucide="anchor" class="size-16 text-neutral mb-4"></i> 14 + <div class="font-bold text-primary" style="font-size: 150px; line-height: 1;">404</div> 15 + <h1 class="text-2xl font-semibold mt-4">Lost at Sea</h1> 16 + <p class="text-base-content/60 mt-2 max-w-md">The page you're looking for has drifted into uncharted waters.</p> 17 + <a href="/" class="btn btn-primary mt-6">Return to Port</a> 18 + </div> 17 19 </div> 18 20 </main> 19 21 <script>lucide.createIcons();</script>
+32 -61
pkg/appview/templates/pages/home.html
··· 23 23 {{ template "nav" . }} 24 24 25 25 {{ if not .User }} 26 - <!-- Hero Section for Non-Logged-In Users --> 27 - <section class="hero-section"> 28 - <div class="hero-content"> 29 - <h1 class="hero-title">ship containers on the open web.</h1> 30 - <p class="hero-subtitle"> 31 - Push and pull Docker images on the AT Protocol.<br> 32 - Browse public registries or control your data. 33 - </p> 34 - 35 - <div class="hero-terminal"> 36 - <div class="terminal-header"> 37 - <span class="terminal-dot"></span> 38 - <span class="terminal-dot"></span> 39 - <span class="terminal-dot"></span> 40 - </div> 41 - <pre class="terminal-content"><span class="terminal-prompt">$</span> docker login atcr.io 42 - <span class="terminal-prompt">$</span> docker push atcr.io/you/app 43 - 44 - <span class="terminal-comment"># same docker, decentralized</span></pre> 45 - </div> 46 - 47 - <div class="hero-actions"> 48 - <a href="/auth/oauth/login?return_to=/" class="btn-hero-primary">Get Started</a> 49 - <a href="/install" class="btn-hero-secondary">Learn More</a> 50 - </div> 51 - </div> 52 - 53 - <!-- Benefit Cards --> 54 - <div class="hero-benefits"> 55 - <div class="benefit-card"> 56 - <div class="benefit-icon"><i data-lucide="ship"></i></div> 57 - <h3>Works with Docker</h3> 58 - <p>Use docker push & pull. No new tools to learn.</p> 59 - </div> 60 - <div class="benefit-card"> 61 - <div class="benefit-icon"><i data-lucide="anchor"></i></div> 62 - <h3>Your Data</h3> 63 - <p>Join shared holds or captain your own storage.</p> 64 - </div> 65 - <div class="benefit-card"> 66 - <div class="benefit-icon"><i data-lucide="compass"></i></div> 67 - <h3>Discover Images</h3> 68 - <p>Browse and star public container registries.</p> 69 - </div> 70 - </div> 71 - </section> 26 + {{ template "hero" . }} 72 27 {{ end }} 73 28 74 - <main class="container"> 75 - <div class="home-page"> 29 + <main class="container mx-auto px-4 py-8"> 30 + <div class="space-y-12"> 76 31 <!-- Featured Repositories Section --> 77 32 {{ if .FeaturedRepos }} 78 - <div class="featured-section"> 79 - <h1>Featured</h1> 80 - <div class="featured-grid"> 81 - {{ range .FeaturedRepos }} 82 - {{ template "repo-card" . }} 33 + <section> 34 + <div class="flex justify-between items-center mb-6"> 35 + <h2 class="text-2xl font-bold">Featured</h2> 36 + <div class="flex gap-2"> 37 + <button id="carousel-prev" class="btn btn-circle btn-ghost btn-sm"> 38 + <i data-lucide="chevron-left" class="size-5"></i> 39 + </button> 40 + <button id="carousel-next" class="btn btn-circle btn-ghost btn-sm"> 41 + <i data-lucide="chevron-right" class="size-5"></i> 42 + </button> 43 + </div> 44 + </div> 45 + <div id="featured-carousel" class="carousel w-full gap-6 scroll-smooth"> 46 + {{ range $i, $repo := .FeaturedRepos }} 47 + <div id="featured-{{ $i }}" class="carousel-item overflow-hidden min-w-0 w-full md:w-[calc(50%-0.75rem)] lg:w-[calc(33.333%-1rem)] shrink-0"> 48 + {{ template "repo-card" $repo }} 49 + </div> 83 50 {{ end }} 84 51 </div> 85 - </div> 52 + </section> 86 53 {{ end }} 87 54 88 - <!-- Recent Pushes Section --> 89 - <h1>What's New</h1> 90 - 91 - <div id="push-list" hx-get="/api/recent-pushes" hx-trigger="load" hx-swap="innerHTML"> 92 - <!-- Initial loading state --> 93 - <div class="loading">Loading recent pushes...</div> 94 - </div> 55 + <!-- Recently Updated Section --> 56 + {{ if .RecentRepos }} 57 + <section> 58 + <h2 class="text-2xl font-bold mb-6">What's New</h2> 59 + <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> 60 + {{ range .RecentRepos }} 61 + {{ template "repo-card" . }} 62 + {{ end }} 63 + </div> 64 + </section> 65 + {{ end }} 95 66 </div> 96 67 </main> 97 68
+41 -28
pkg/appview/templates/pages/login.html
··· 8 8 <body> 9 9 {{ template "nav-simple" . }} 10 10 11 - <main class="container"> 12 - <div class="login-page"> 13 - <h1>Sign in to ATCR</h1> 14 - <p>Use your ATProto handle to sign in</p> 11 + <main class="min-h-[calc(100vh-4rem)] flex items-center justify-center px-4"> 12 + <div class="w-full max-w-md"> 13 + <h1 class="text-2xl font-semibold text-center mb-2">Sign in to ATCR</h1> 14 + <p class="text-center text-base-content/60 mb-6">Use your ATProto handle to sign in</p> 15 15 16 16 {{ if .Error }} 17 - <div class="error"> 18 - {{ if eq .Error "handle_required" }} 19 - Please enter your handle 20 - {{ else if eq .Error "auth_failed" }} 21 - Authentication failed. Please try again. 22 - {{ else }} 23 - An error occurred. Please try again. 24 - {{ end }} 17 + <div class="alert alert-error mb-6"> 18 + <i data-lucide="circle-x" class="size-5"></i> 19 + <span> 20 + {{ if eq .Error "handle_required" }} 21 + Please enter your handle 22 + {{ else if eq .Error "auth_failed" }} 23 + Authentication failed. Please try again. 24 + {{ else }} 25 + An error occurred. Please try again. 26 + {{ end }} 27 + </span> 25 28 </div> 26 29 {{ end }} 27 30 28 - <form action="/auth/oauth/login" method="POST" class="login-form"> 31 + <form action="/auth/oauth/login" method="POST" id="login-form" class="card bg-base-100 p-6"> 29 32 <input type="hidden" name="return_to" value="{{ .ReturnTo }}" /> 30 33 31 - <div class="form-group"> 32 - <label for="handle">Your ATProto Handle</label> 33 - <input type="text" 34 - id="handle" 35 - name="handle" 36 - placeholder="alice.bsky.social" 37 - autocomplete="off" 38 - required 39 - autofocus /> 40 - <small>Enter your Bluesky or ATProto handle</small> 41 - </div> 34 + <fieldset class="fieldset relative"> 35 + <label class="label" for="handle"> 36 + <span class="label-text">Your ATProto Handle</span> 37 + </label> 38 + <actor-typeahead rows="5" class="block"> 39 + <input type="text" 40 + id="handle" 41 + name="handle" 42 + class="input input-bordered w-full" 43 + placeholder="alice.bsky.social" 44 + autocomplete="off" 45 + required 46 + autofocus /> 47 + </actor-typeahead> 48 + <p class="label"> 49 + <span class="label-text-alt text-base-content/60">Enter your Bluesky or ATProto handle</span> 50 + </p> 51 + </fieldset> 42 52 43 - <button type="submit" class="btn-primary btn-large">Continue with ATProto</button> 53 + <button type="submit" class="btn btn-primary w-full mt-4"> 54 + Continue with ATProto 55 + </button> 44 56 </form> 45 57 46 - <div class="login-help"> 47 - <p>Don't have an account? Create one at <a href="https://bsky.app" target="_blank">bsky.app</a></p> 48 - </div> 58 + <p class="text-center text-base-content/60 mt-6"> 59 + Don't have an account? Create one at 60 + <a href="https://bsky.app" target="_blank" class="link link-primary">bsky.app</a> 61 + </p> 49 62 </div> 50 63 </main> 51 64 </body>
+122 -217
pkg/appview/templates/pages/repository.html
··· 22 22 <body> 23 23 {{ template "nav" . }} 24 24 25 - <main class="container"> 26 - <div class="repository-page"> 25 + <main class="container mx-auto px-4 py-8"> 26 + <div class="space-y-8"> 27 27 <!-- Repository Header --> 28 - <div class="repository-header"> 29 - <div class="repo-hero"> 30 - <div class="repo-hero-icon-wrapper"> 28 + <div class="card bg-base-100 shadow-sm p-6 space-y-6 w-full"> 29 + <div class="flex gap-4 items-start"> 30 + <div class="relative shrink-0"> 31 31 {{ if .Repository.IconURL }} 32 - <img src="{{ .Repository.IconURL }}" alt="{{ .Repository.Name }}" class="repo-hero-icon"> 32 + <img src="{{ .Repository.IconURL }}" alt="{{ .Repository.Name }}" class="w-20 rounded-lg object-cover"> 33 33 {{ else }} 34 - <div class="repo-hero-icon-placeholder">{{ firstChar .Repository.Name }}</div> 34 + <div class="avatar avatar-placeholder"> 35 + <div class="bg-neutral text-neutral-content w-20 rounded-lg shadow-sm uppercase"> 36 + <span class="text-4xl">{{ firstChar .Repository.Name }}</span> 37 + </div> 38 + </div> 35 39 {{ end }} 36 40 {{ if $.IsOwner }} 37 - <label class="avatar-upload-overlay" for="avatar-upload"> 38 - <i data-lucide="plus"></i> 41 + <label class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 hover:opacity-100 transition-opacity cursor-pointer rounded-lg" for="avatar-upload"> 42 + <i data-lucide="plus" class="size-8 text-white"></i> 39 43 </label> 40 44 <input type="file" id="avatar-upload" accept="image/png,image/jpeg,image/webp" 41 45 onchange="uploadAvatar(this, '{{ .Repository.Name }}')" hidden> 42 46 {{ end }} 43 47 </div> 44 - <div class="repo-hero-info"> 45 - <h1> 46 - <a href="/u/{{ .Owner.Handle }}" class="owner-link">{{ .Owner.Handle }}</a> 47 - <span class="repo-separator">/</span> 48 - <span class="repo-name">{{ .Repository.Name }}</span> 48 + <div class="flex-1 min-w-0"> 49 + <h1 class="text-2xl font-bold"> 50 + <a href="/u/{{ .Owner.Handle }}" class="link link-primary">{{ .Owner.Handle }}</a> 51 + <span class="text-base-content/60">/</span> 52 + <span>{{ .Repository.Name }}</span> 49 53 </h1> 50 54 {{ if .Repository.Description }} 51 - <p class="repo-hero-description">{{ .Repository.Description }}</p> 55 + <p class="text-base-content/70 mt-2">{{ .Repository.Description }}</p> 52 56 {{ end }} 53 57 </div> 54 58 </div> 55 59 56 - <!-- Star Button and Metadata Row --> 57 - <div class="repo-info-row"> 58 - <div class="repo-actions"> 59 - <button class="star-btn{{ if .IsStarred }} starred{{ end }}" id="star-btn" onclick="toggleStar('{{ .Owner.Handle }}', '{{ .Repository.Name }}')"> 60 - <i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}" id="star-icon"></i> 61 - <span class="star-count" id="star-count">{{ .StarCount }}</span> 62 - </button> 60 + <!-- Star Button, Pull Count and Metadata Row --> 61 + <div class="flex flex-wrap items-center justify-between gap-4"> 62 + <div class="flex items-center gap-4"> 63 + {{ template "star" (dict "IsStarred" .IsStarred "StarCount" .StarCount "Interactive" true "Handle" .Owner.Handle "Repository" .Repository.Name) }} 64 + {{ template "pull-count" (dict "PullCount" .PullCount) }} 63 65 </div> 64 66 65 67 <!-- Metadata Section --> 66 68 {{ if or .Repository.Licenses .Repository.SourceURL .Repository.DocumentationURL .Repository.Version }} 67 - <div class="repo-metadata"> 69 + <div class="flex flex-wrap items-center gap-2"> 68 70 {{ if .Repository.Version }} 69 - <span class="metadata-badge version-badge" title="Version"> 71 + <span class="badge badge-md badge-primary badge-outline" title="Version"> 70 72 {{ .Repository.Version }} 71 73 </span> 72 74 {{ end }} 73 75 {{ if .Repository.Licenses }} 74 76 {{ range parseLicenses .Repository.Licenses }} 75 77 {{ if .IsValid }} 76 - <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" class="metadata-badge license-badge" title="{{ .Name }}"> 78 + <a href="{{ .URL }}" target="_blank" rel="noopener noreferrer" class="badge badge-md badge-secondary" title="{{ .Name }}"> 77 79 {{ .SPDXID }} 78 80 </a> 79 81 {{ else }} 80 - <span class="metadata-badge license-badge" title="Custom license: {{ .Name }}"> 82 + <span class="badge badge-md badge-secondary" title="Custom license: {{ .Name }}"> 81 83 {{ .Name }} 82 84 </span> 83 85 {{ end }} 84 86 {{ end }} 85 87 {{ end }} 86 88 {{ if .Repository.SourceURL }} 87 - <a href="{{ .Repository.SourceURL }}" target="_blank" class="metadata-link"> 89 + <a href="{{ .Repository.SourceURL }}" target="_blank" class="link link-primary text-sm"> 88 90 Source 89 91 </a> 90 92 {{ end }} 91 93 {{ if .Repository.DocumentationURL }} 92 - <a href="{{ .Repository.DocumentationURL }}" target="_blank" class="metadata-link"> 94 + <a href="{{ .Repository.DocumentationURL }}" target="_blank" class="link link-primary text-sm"> 93 95 Documentation 94 96 </a> 95 97 {{ end }} 96 98 </div> 97 - {{ else }} 98 - <div class="repo-metadata"></div> 99 99 {{ end }} 100 100 </div> 101 + 102 + <div class="divider my-2"></div> 101 103 102 104 <!-- Pull Command --> 103 - <div class="pull-command-section"> 105 + <div class="space-y-2"> 104 106 {{ if eq .ArtifactType "helm-chart" }} 105 - <h3>Pull this chart</h3> 107 + <h3 class="font-semibold">Pull this chart</h3> 106 108 {{ if .Tags }} 107 109 {{ $firstTag := index .Tags 0 }} 108 110 {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " $firstTag.Tag.Tag) }} ··· 110 112 {{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name) }} 111 113 {{ end }} 112 114 {{ else }} 113 - <h3>Pull this image</h3> 115 + <h3 class="font-semibold">Pull this image</h3> 114 116 {{ if .Tags }} 115 117 {{ $firstTag := index .Tags 0 }} 116 118 {{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }} ··· 123 125 124 126 <!-- README and Tags/Manifests Layout --> 125 127 {{ if .ReadmeHTML }} 126 - <div class="repo-content-layout"> 128 + <div class="grid grid-cols-1 lg:grid-cols-[3fr_2fr] gap-8"> 127 129 <!-- README Section (Left) --> 128 - <div class="readme-section"> 129 - <h2>Overview</h2> 130 - <div class="readme-content markdown-body"> 130 + <div class="card bg-base-100 shadow-sm p-6 space-y-4 min-w-0"> 131 + <h2 class="text-xl font-semibold">Overview</h2> 132 + <div class="prose prose-sm max-w-none"> 131 133 {{ .ReadmeHTML }} 132 134 </div> 133 135 </div> 134 136 135 137 <!-- Tags and Manifests (Right) --> 136 - <div class="repo-sidebar"> 138 + <div class="space-y-8 min-w-0"> 137 139 {{ end }} 138 140 139 141 <!-- Tags Section --> 140 - <div class="repo-section"> 141 - <h2>Tags</h2> 142 + <div class="card bg-base-100 shadow-sm p-6 space-y-4"> 143 + <h2 class="text-xl font-semibold">Tags</h2> 142 144 {{ if .Tags }} 143 - <div class="tags-list"> 145 + <div class="space-y-4"> 144 146 {{ range .Tags }} 145 - <div class="tag-item" id="tag-{{ sanitizeID .Tag.Tag }}"> 146 - <div class="tag-item-header"> 147 - <div> 148 - <span class="tag-name-large">{{ .Tag.Tag }}</span> 147 + <div class="bg-base-200 rounded-lg p-4 space-y-3" id="tag-{{ sanitizeID .Tag.Tag }}"> 148 + <div class="flex flex-wrap items-center justify-between gap-2"> 149 + <div class="flex flex-wrap items-center gap-2"> 150 + <span class="font-mono font-semibold text-lg">{{ .Tag.Tag }}</span> 149 151 {{ if eq .ArtifactType "helm-chart" }} 150 - <span class="badge-helm"><i data-lucide="anchor"></i> Helm</span> 152 + <span class="badge badge-md badge-soft badge-primary"><i data-lucide="anchor" class="size-3"></i> Helm</span> 151 153 {{ else if .IsMultiArch }} 152 - <span class="badge-multi">Multi-arch</span> 154 + <span class="badge badge-md badge-primary">Multi-arch</span> 153 155 {{ end }} 154 156 {{ if .HasAttestations }} 155 - <span class="badge-attestation"><i data-lucide="shield-check"></i> Attestations</span> 157 + <span class="badge badge-md badge-success"><i data-lucide="shield-check" class="size-3"></i> Attestations</span> 156 158 {{ end }} 157 159 </div> 158 - <div style="display: flex; gap: 1rem; align-items: center;"> 159 - <time class="tag-timestamp" datetime="{{ .Tag.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 160 + <div class="flex items-center gap-2"> 161 + <time class="text-sm text-base-content/60" datetime="{{ .Tag.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 160 162 {{ timeAgo .Tag.CreatedAt }} 161 163 </time> 162 164 {{ if $.IsOwner }} 163 - <button class="delete-btn" 165 + <button class="btn btn-ghost btn-sm text-error" 164 166 hx-delete="/api/images/{{ $.Repository.Name }}/tags/{{ .Tag.Tag }}" 165 167 hx-confirm="Delete tag {{ .Tag.Tag }}?" 166 168 hx-target="#tag-{{ sanitizeID .Tag.Tag }}" 167 169 hx-swap="outerHTML"> 168 - <i data-lucide="trash-2"></i> 170 + <i data-lucide="trash-2" class="size-4"></i> 169 171 </button> 170 172 {{ end }} 171 173 </div> 172 174 </div> 173 - <div class="tag-item-details"> 174 - <div style="display: flex; justify-content: space-between; align-items: center;"> 175 - <div class="digest-container"> 176 - <code class="digest" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code> 177 - <button class="digest-copy-btn" onclick="copyToClipboard('{{ .Tag.Digest }}')"><i data-lucide="copy"></i></button> 175 + <div class="text-sm"> 176 + <div class="flex flex-wrap justify-between items-center gap-2"> 177 + <div class="flex items-center gap-2"> 178 + <code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code> 179 + <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')"><i data-lucide="copy" class="size-3"></i></button> 178 180 </div> 179 181 {{ if .Platforms }} 180 - <div class="platforms-inline"> 182 + <div class="flex flex-wrap gap-1"> 181 183 {{ range .Platforms }} 182 - <span class="platform-badge">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 184 + <span class="badge badge-sm badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 183 185 {{ end }} 184 186 </div> 185 187 {{ end }} ··· 194 196 {{ end }} 195 197 </div> 196 198 {{ else }} 197 - <p class="empty-message">No tags available</p> 199 + <p class="text-base-content/60">No tags available</p> 198 200 {{ end }} 199 201 </div> 200 202 201 203 <!-- Manifests Section --> 202 - <div class="repo-section"> 203 - <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> 204 - <h2>Manifests</h2> 205 - <label class="show-offline-toggle"> 206 - <input type="checkbox" id="show-offline-toggle" onchange="toggleOfflineManifests()"> 204 + <div class="card bg-base-100 shadow-sm p-6 space-y-4"> 205 + <div class="flex flex-wrap justify-between items-center gap-4"> 206 + <h2 class="text-xl font-semibold">Manifests</h2> 207 + <label class="flex items-center gap-2 text-sm cursor-pointer"> 208 + <input type="checkbox" class="checkbox checkbox-sm" id="show-offline-toggle" onchange="toggleOfflineManifests()"> 207 209 <span>Show offline images</span> 208 210 </label> 209 211 </div> 210 212 {{ if .Manifests }} 211 - <div class="manifests-list"> 213 + <div class="space-y-4"> 212 214 {{ range .Manifests }} 213 - <div class="manifest-item" id="manifest-{{ sanitizeID .Manifest.Digest }}" data-reachable="{{ .Reachable }}"> 214 - <div class="manifest-item-header"> 215 - <div> 216 - {{ if .IsManifestList }} 217 - <span class="manifest-type"><i data-lucide="package"></i> Multi-arch</span> 218 - {{ else if eq .ArtifactType "helm-chart" }} 219 - <span class="manifest-type helm"><i data-lucide="anchor"></i> Helm Chart</span> 220 - {{ else }} 221 - <span class="manifest-type"><i data-lucide="box"></i> Image</span> 222 - {{ end }} 223 - {{ if .HasAttestations }} 224 - <span class="badge-attestation"><i data-lucide="shield-check"></i> Attestations</span> 225 - {{ end }} 226 - {{ if .Pending }} 227 - <span class="checking-badge" 228 - hx-get="/api/manifest-health?endpoint={{ .Manifest.HoldEndpoint | urlquery }}" 229 - hx-trigger="load delay:2s" 230 - hx-swap="outerHTML"> 231 - <i data-lucide="refresh-ccw"></i> Checking... 232 - </span> 233 - {{ else if not .Reachable }} 234 - <span class="offline-badge"><i data-lucide="alert-triangle"></i> Offline</span> 235 - {{ end }} 236 - <div class="digest-container"> 237 - <code class="digest manifest-digest" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code> 238 - <button class="digest-copy-btn" onclick="copyToClipboard('{{ .Manifest.Digest }}')"><i data-lucide="copy"></i></button> 215 + <div class="bg-base-200 rounded-lg p-4 space-y-3" id="manifest-{{ sanitizeID .Manifest.Digest }}" data-reachable="{{ .Reachable }}"> 216 + <div class="flex flex-wrap items-start justify-between gap-2"> 217 + <div class="space-y-2"> 218 + <div class="flex flex-wrap items-center gap-2"> 219 + {{ if .IsManifestList }} 220 + <span class="flex items-center gap-1 font-medium"><i data-lucide="package" class="size-4"></i> Multi-arch</span> 221 + {{ else if eq .ArtifactType "helm-chart" }} 222 + <span class="flex items-center gap-1 font-medium text-primary"><i data-lucide="anchor" class="size-4"></i> Helm Chart</span> 223 + {{ else }} 224 + <span class="flex items-center gap-1 font-medium"><i data-lucide="box" class="size-4"></i> Image</span> 225 + {{ end }} 226 + {{ if .HasAttestations }} 227 + <span class="badge badge-md badge-success"><i data-lucide="shield-check" class="size-3"></i> Attestations</span> 228 + {{ end }} 229 + {{ if .Pending }} 230 + <span class="badge badge-sm badge-info" 231 + hx-get="/api/manifest-health?endpoint={{ .Manifest.HoldEndpoint | urlquery }}" 232 + hx-trigger="load delay:2s" 233 + hx-swap="outerHTML"> 234 + <i data-lucide="refresh-ccw" class="size-3"></i> Checking... 235 + </span> 236 + {{ else if not .Reachable }} 237 + <span class="badge badge-sm badge-warning"><i data-lucide="alert-triangle" class="size-3"></i> Offline</span> 238 + {{ end }} 239 + </div> 240 + <div class="flex items-center gap-2"> 241 + <code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code> 242 + <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')"><i data-lucide="copy" class="size-3"></i></button> 239 243 </div> 240 244 </div> 241 - <div style="display: flex; gap: 1rem; align-items: center;"> 242 - <time datetime="{{ .Manifest.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 245 + <div class="flex items-center gap-2"> 246 + <time class="text-sm text-base-content/60" datetime="{{ .Manifest.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 243 247 {{ timeAgo .Manifest.CreatedAt }} 244 248 </time> 245 249 {{ if $.IsOwner }} 246 - <button class="delete-btn" 250 + <button class="btn btn-ghost btn-sm text-error" 247 251 onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')"> 248 - <i data-lucide="trash-2"></i> 252 + <i data-lucide="trash-2" class="size-4"></i> 249 253 </button> 250 254 {{ end }} 251 255 </div> 252 256 </div> 253 - <div class="manifest-item-details"> 254 - <div style="display: flex; justify-content: space-between; align-items: center;"> 257 + <div class="text-sm"> 258 + <div class="flex flex-wrap justify-between items-center gap-2"> 255 259 <div> 256 260 {{ if .Tags }} 257 - <span class="manifest-detail-label">Tags:</span> 261 + <span class="text-base-content/60">Tags:</span> 258 262 {{ range $index, $tag := .Tags }}{{ if $index }}, {{ end }}{{ $tag }}{{ end }} 259 263 {{ else }} 260 - <span class="text-muted">(untagged)</span> 264 + <span class="text-base-content/50">(untagged)</span> 261 265 {{ end }} 262 266 </div> 263 267 {{ if .IsManifestList }} 264 268 {{ if .Platforms }} 265 - <div class="platforms-inline"> 269 + <div class="flex flex-wrap gap-1"> 266 270 {{ range .Platforms }} 267 - <span class="platform-badge">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 271 + <span class="badge badge-sm badge-secondary">{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</span> 268 272 {{ end }} 269 273 </div> 270 274 {{ end }} ··· 275 279 {{ end }} 276 280 </div> 277 281 {{ else }} 278 - <p class="empty-message">No manifests available</p> 282 + <p class="text-base-content/60">No manifests available</p> 279 283 {{ end }} 280 284 </div> 281 285 282 286 {{ if .ReadmeHTML }} 283 - </div><!-- Close repo-sidebar --> 284 - </div><!-- Close repo-content-layout --> 287 + </div><!-- Close sidebar --> 288 + </div><!-- Close grid layout --> 285 289 {{ end }} 286 290 </div> 287 291 </main> ··· 290 294 <div id="modal"></div> 291 295 292 296 <!-- Manifest Delete Confirmation Modal --> 293 - <div id="manifest-delete-modal" class="modal-overlay" style="display: none;"> 294 - <div class="modal-dialog"> 295 - <div class="modal-header"> 296 - <h3>Confirm Deletion</h3> 297 - <button class="modal-close" onclick="closeManifestDeleteModal()">&times;</button> 298 - </div> 299 - <div class="modal-body"> 300 - <p id="manifest-delete-message">This manifest has associated tags that will also be deleted:</p> 301 - <ul id="manifest-delete-tags" class="tag-list"></ul> 302 - <p><strong>This action cannot be undone.</strong></p> 303 - </div> 304 - <div class="modal-footer"> 305 - <button class="btn btn-secondary" onclick="closeManifestDeleteModal()">Cancel</button> 306 - <button class="btn btn-danger" id="confirm-manifest-delete-btn">Delete All</button> 297 + <dialog id="manifest-delete-modal" class="modal"> 298 + <div class="modal-box"> 299 + <h3 class="text-lg font-bold">Confirm Deletion</h3> 300 + <p id="manifest-delete-message" class="py-2">This manifest has associated tags that will also be deleted:</p> 301 + <ul id="manifest-delete-tags" class="list-disc list-inside text-sm space-y-1"></ul> 302 + <p class="font-bold py-2 text-error">This action cannot be undone.</p> 303 + <div class="modal-action"> 304 + <button class="btn" onclick="closeManifestDeleteModal()">Cancel</button> 305 + <button class="btn btn-error" id="confirm-manifest-delete-btn">Delete All</button> 307 306 </div> 308 307 </div> 309 - </div> 308 + <form method="dialog" class="modal-backdrop"> 309 + <button onclick="closeManifestDeleteModal()">close</button> 310 + </form> 311 + </dialog> 310 312 311 - <style> 312 - .modal-overlay { 313 - position: fixed; 314 - top: 0; 315 - left: 0; 316 - width: 100%; 317 - height: 100%; 318 - background: rgba(0, 0, 0, 0.5); 319 - display: flex; 320 - align-items: center; 321 - justify-content: center; 322 - z-index: 1000; 323 - } 324 - 325 - .modal-dialog { 326 - background: var(--bg-secondary, #1a1a1a); 327 - border: 1px solid var(--border-color, #333); 328 - border-radius: 8px; 329 - max-width: 500px; 330 - width: 90%; 331 - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); 332 - } 333 - 334 - .modal-header { 335 - padding: 1rem 1.5rem; 336 - border-bottom: 1px solid var(--border-color, #333); 337 - display: flex; 338 - justify-content: space-between; 339 - align-items: center; 340 - } 341 - 342 - .modal-header h3 { 343 - margin: 0; 344 - font-size: 1.25rem; 345 - } 346 - 347 - .modal-close { 348 - background: none; 349 - border: none; 350 - font-size: 1.5rem; 351 - cursor: pointer; 352 - color: var(--text-color, #fff); 353 - padding: 0; 354 - width: 2rem; 355 - height: 2rem; 356 - line-height: 1; 357 - } 358 - 359 - .modal-body { 360 - padding: 1.5rem; 361 - } 362 - 363 - .modal-body .tag-list { 364 - margin: 1rem 0; 365 - padding-left: 1.5rem; 366 - } 367 - 368 - .modal-body .tag-list li { 369 - margin: 0.5rem 0; 370 - font-family: monospace; 371 - } 372 - 373 - .modal-footer { 374 - padding: 1rem 1.5rem; 375 - border-top: 1px solid var(--border-color, #333); 376 - display: flex; 377 - justify-content: flex-end; 378 - gap: 0.5rem; 379 - } 380 - 381 - .btn { 382 - padding: 0.5rem 1rem; 383 - border: none; 384 - border-radius: 4px; 385 - cursor: pointer; 386 - font-size: 0.875rem; 387 - font-weight: 500; 388 - } 389 - 390 - .btn-secondary { 391 - background: var(--bg-tertiary, #2a2a2a); 392 - color: var(--text-color, #fff); 393 - } 394 - 395 - .btn-secondary:hover { 396 - background: var(--bg-hover, #3a3a3a); 397 - } 398 - 399 - .btn-danger { 400 - background: #dc3545; 401 - color: white; 402 - } 403 - 404 - .btn-danger:hover { 405 - background: #c82333; 406 - } 407 - </style> 408 313 </body> 409 314 </html> 410 315 {{ end }}
+279 -787
pkg/appview/templates/pages/settings.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container"> 12 - <div class="settings-page"> 13 - <h1>Settings</h1> 11 + <main class="container mx-auto px-4 py-8"> 12 + <div class="max-w-4xl mx-auto space-y-8"> 13 + <h1 class="text-3xl font-bold">Settings</h1> 14 14 15 - <!-- Identity Section --> 16 - <section class="settings-section"> 17 - <h2>Identity</h2> 18 - <div class="form-group"> 19 - <label>Handle:</label> 20 - <span>{{ .Profile.Handle }}</span> 21 - </div> 22 - <div class="form-group"> 23 - <label>DID:</label> 24 - <code>{{ .Profile.DID }}</code> 25 - </div> 26 - <div class="form-group"> 27 - <label>PDS:</label> 28 - <span>{{ .Profile.PDSEndpoint }}</span> 29 - </div> 30 - </section> 15 + <!-- Identity Section --> 16 + <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 17 + <h2 class="text-xl font-semibold">Identity</h2> 18 + <div class="grid gap-3"> 19 + <div class="flex flex-col gap-1"> 20 + <span class="text-sm font-medium text-base-content/70">Handle</span> 21 + <span>{{ .Profile.Handle }}</span> 22 + </div> 23 + <div class="flex flex-col gap-1"> 24 + <span class="text-sm font-medium text-base-content/70">DID</span> 25 + <code class="cmd">{{ .Profile.DID }}</code> 26 + </div> 27 + <div class="flex flex-col gap-1"> 28 + <span class="text-sm font-medium text-base-content/70">PDS</span> 29 + <span>{{ .Profile.PDSEndpoint }}</span> 30 + </div> 31 + </div> 32 + </section> 31 33 32 - <!-- Storage Usage Section --> 33 - <section class="settings-section storage-section"> 34 - <h2>Stowage</h2> 35 - <p>Estimated storage usage on your default hold.</p> 36 - <div id="storage-stats" hx-get="/api/storage" hx-trigger="load" hx-swap="innerHTML"> 37 - <p><i data-lucide="loader-2" class="spin"></i> Loading...</p> 38 - </div> 39 - </section> 34 + <!-- Storage Usage Section --> 35 + <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 36 + <h2 class="text-xl font-semibold">Stowage</h2> 37 + <p class="text-base-content/70">Estimated storage usage on your default hold.</p> 38 + <div id="storage-stats" hx-get="/api/storage" hx-trigger="load" hx-swap="innerHTML"> 39 + <p class="flex items-center gap-2"><i data-lucide="loader-2" class="size-4 animate-spin"></i> Loading...</p> 40 + </div> 41 + </section> 40 42 41 - <!-- Default Hold Section --> 42 - <section class="settings-section hold-section"> 43 - <h2>Default Hold</h2> 44 - <p class="help-text">Select where your container images will be stored.</p> 43 + <!-- Default Hold Section --> 44 + <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 45 + <h2 class="text-xl font-semibold">Default Hold</h2> 46 + <p class="text-base-content/70">Select where your container images will be stored.</p> 45 47 46 - <form hx-post="/api/profile/default-hold" 47 - hx-target="#hold-status" 48 - hx-swap="innerHTML" 49 - id="hold-form"> 48 + <form hx-post="/api/profile/default-hold" 49 + hx-target="#hold-status" 50 + hx-swap="innerHTML" 51 + id="hold-form" 52 + class="space-y-4"> 50 53 51 - <div class="form-group"> 52 - <label for="default-hold">Storage Hold:</label> 53 - <div class="select-wrapper"> 54 - <select id="default-hold" name="hold_did" class="form-select"> 55 - <option value=""{{ if eq .CurrentHoldDID "" }} selected{{ end }}>AppView Default ({{ .AppViewDefaultHoldDisplay }}{{ if .AppViewDefaultRegion }}, {{ .AppViewDefaultRegion }}{{ end }})</option> 54 + <fieldset class="fieldset"> 55 + <label class="label" for="default-hold"> 56 + <span class="label-text">Storage Hold</span> 57 + </label> 58 + <select id="default-hold" name="hold_did" class="select select-bordered w-full"> 59 + <option value=""{{ if eq .CurrentHoldDID "" }} selected{{ end }}>AppView Default ({{ .AppViewDefaultHoldDisplay }}{{ if .AppViewDefaultRegion }}, {{ .AppViewDefaultRegion }}{{ end }})</option> 56 60 57 - {{ if .ShowCurrentHold }} 58 - <option value="{{ .CurrentHoldDID }}" selected>Current ({{ .CurrentHoldDisplay }})</option> 59 - {{ end }} 61 + {{ if .ShowCurrentHold }} 62 + <option value="{{ .CurrentHoldDID }}" selected>Current ({{ .CurrentHoldDisplay }})</option> 63 + {{ end }} 60 64 61 - {{ if .OwnedHolds }} 62 - <optgroup label="Your Holds"> 63 - {{ range .OwnedHolds }} 64 - <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 65 - {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 66 - </option> 65 + {{ if .OwnedHolds }} 66 + <optgroup label="Your Holds"> 67 + {{ range .OwnedHolds }} 68 + <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 69 + {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 70 + </option> 71 + {{ end }} 72 + </optgroup> 67 73 {{ end }} 68 - </optgroup> 69 - {{ end }} 70 74 71 - {{ if .CrewHolds }} 72 - <optgroup label="Crew Member"> 73 - {{ range .CrewHolds }} 74 - <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 75 - {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 76 - </option> 75 + {{ if .CrewHolds }} 76 + <optgroup label="Crew Member"> 77 + {{ range .CrewHolds }} 78 + <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 79 + {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 80 + </option> 81 + {{ end }} 82 + </optgroup> 77 83 {{ end }} 78 - </optgroup> 79 - {{ end }} 80 84 81 - {{ if .EligibleHolds }} 82 - <optgroup label="Open Registration"> 83 - {{ range .EligibleHolds }} 84 - <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 85 - {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 86 - </option> 85 + {{ if .EligibleHolds }} 86 + <optgroup label="Open Registration"> 87 + {{ range .EligibleHolds }} 88 + <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 89 + {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 90 + </option> 91 + {{ end }} 92 + </optgroup> 87 93 {{ end }} 88 - </optgroup> 89 - {{ end }} 90 94 91 - {{ if .PublicHolds }} 92 - <optgroup label="Public Holds"> 93 - {{ range .PublicHolds }} 94 - <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 95 - {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 96 - </option> 95 + {{ if .PublicHolds }} 96 + <optgroup label="Public Holds"> 97 + {{ range .PublicHolds }} 98 + <option value="{{ .DID }}" {{ if eq $.CurrentHoldDID .DID }}selected{{ end }}> 99 + {{ .DisplayName }}{{ if .Region }} ({{ .Region }}){{ end }} 100 + </option> 101 + {{ end }} 102 + </optgroup> 97 103 {{ end }} 98 - </optgroup> 99 - {{ end }} 100 - </select> 101 - <i data-lucide="chevron-down" class="select-icon"></i> 102 - </div> 103 - <small>Your images will be stored on the selected hold</small> 104 - </div> 104 + </select> 105 + <p class="text-sm text-base-content/60 mt-1">Your images will be stored on the selected hold</p> 106 + </fieldset> 105 107 106 - <button type="submit" class="btn-primary">Save</button> 107 - </form> 108 + <button type="submit" class="btn btn-primary">Save</button> 109 + </form> 108 110 109 - <div id="hold-status"></div> 111 + <div id="hold-status"></div> 110 112 111 - <!-- Hold details panel (shows when hold selected) --> 112 - <div id="hold-details" class="hold-details" style="display: none;"> 113 - <h3>Hold Details</h3> 114 - <dl> 115 - <dt>DID:</dt> 116 - <dd id="hold-did"></dd> 117 - <dt>Region:</dt> 118 - <dd id="hold-region"></dd> 119 - <dt>Your Access:</dt> 120 - <dd id="hold-access"></dd> 121 - </dl> 122 - </div> 113 + <!-- Hold details panel (shows when hold selected) --> 114 + <div id="hold-details" class="hidden mt-4 p-4 bg-base-200 rounded-lg"> 115 + <h3 class="font-semibold mb-3">Hold Details</h3> 116 + <dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm"> 117 + <dt class="text-base-content/70">DID:</dt> 118 + <dd id="hold-did" class="font-mono"></dd> 119 + <dt class="text-base-content/70">Region:</dt> 120 + <dd id="hold-region"></dd> 121 + <dt class="text-base-content/70">Your Access:</dt> 122 + <dd id="hold-access"></dd> 123 + </dl> 124 + </div> 125 + </section> 123 126 124 - </section> 127 + <!-- Authorized Devices Section --> 128 + <section class="card bg-base-100 shadow-sm p-6 space-y-6"> 129 + <div> 130 + <h2 class="text-xl font-semibold">Authorized Devices</h2> 131 + <p class="text-base-content/70 mt-1">Devices authorized via <code class="cmd">docker-credential-atcr</code> credential helper.</p> 132 + </div> 125 133 126 - <!-- Authorized Devices Section --> 127 - <section class="settings-section devices-section"> 128 - <h2>Authorized Devices</h2> 129 - <p>Devices authorized via <code>docker-credential-atcr</code> credential helper.</p> 130 - 131 - <!-- Setup Instructions --> 132 - <div class="setup-instructions"> 133 - <h3>First Time Setup</h3> 134 - <ol> 135 - <li>Install credential helper: 136 - <pre><code>curl -fsSL atcr.io/static/install.sh | bash</code></pre> 137 - </li> 138 - <li>Configure Docker to use the helper. Add to <code>~/.docker/config.json</code>: 139 - <pre><code>{ 134 + <!-- Setup Instructions --> 135 + <div class="bg-base-200 rounded-lg p-4 space-y-4"> 136 + <h3 class="font-semibold">First Time Setup</h3> 137 + <ol class="list-decimal list-inside space-y-4 text-sm"> 138 + <li>Install credential helper: 139 + <pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto"><code>curl -fsSL atcr.io/static/install.sh | bash</code></pre> 140 + </li> 141 + <li>Configure Docker to use the helper. Add to <code class="cmd">~/.docker/config.json</code>: 142 + <pre class="mt-2 p-3 bg-base-300 rounded-lg overflow-x-auto"><code>{ 140 143 "credHelpers": { 141 144 "{{ .RegistryURL }}": "atcr" 142 145 } 143 146 }</code></pre> 144 - </li> 145 - <li>Run any Docker command: 146 - {{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }} 147 - </li> 148 - <li>Browser will open for authorization - click Approve</li> 149 - <li>Done! Device is automatically authorized</li> 150 - </ol> 147 + </li> 148 + <li>Run any Docker command: 149 + <div class="mt-2">{{ template "docker-command" (print "docker pull " .RegistryURL "/" .Profile.Handle "/myimage") }}</div> 150 + </li> 151 + <li>Browser will open for authorization - click Approve</li> 152 + <li>Done! Device is automatically authorized</li> 153 + </ol> 151 154 152 - <div class="fallback-note"> 153 - <strong>Fallback:</strong> Use <a href="https://bsky.app/settings/app-passwords" target="_blank">app password</a> with <code>docker login {{ .RegistryURL }}</code> for quick start (no device tracking) 154 - </div> 155 - </div> 155 + <div class="pt-3 border-t border-base-300 text-sm"> 156 + <strong>Fallback:</strong> Use <a href="https://bsky.app/settings/app-passwords" target="_blank" class="link link-primary">app password</a> with <code class="cmd">docker login {{ .RegistryURL }}</code> for quick start (no device tracking) 157 + </div> 158 + </div> 159 + 160 + <!-- Devices List --> 161 + <div class="space-y-3"> 162 + <h3 class="font-semibold">Your Authorized Devices</h3> 163 + <div class="overflow-x-auto"> 164 + <table class="table table-zebra"> 165 + <thead> 166 + <tr> 167 + <th>Device Name</th> 168 + <th>IP Address</th> 169 + <th>Created</th> 170 + <th>Last Used</th> 171 + <th>Actions</th> 172 + </tr> 173 + </thead> 174 + <tbody id="devices-table"> 175 + <tr><td colspan="5" class="text-center text-base-content/60">Loading...</td></tr> 176 + </tbody> 177 + </table> 178 + </div> 179 + </div> 180 + </section> 156 181 157 - <!-- Devices List --> 158 - <div class="devices-list"> 159 - <h3>Your Authorized Devices</h3> 160 - <table> 161 - <thead> 162 - <tr> 163 - <th>Device Name</th> 164 - <th>IP Address</th> 165 - <th>Created</th> 166 - <th>Last Used</th> 167 - <th>Actions</th> 168 - </tr> 169 - </thead> 170 - <tbody id="devices-table"> 171 - <tr><td colspan="5">Loading...</td></tr> 172 - </tbody> 173 - </table> 174 - </div> 175 - </section> 182 + <!-- Data Privacy Section --> 183 + <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 184 + <h2 class="text-xl font-semibold">Data Privacy</h2> 185 + <p class="text-base-content/70">Download a copy of all data we store about you.</p> 176 186 177 - <!-- Data Privacy Section --> 178 - <section class="settings-section privacy-section"> 179 - <h2>Data Privacy</h2> 180 - <p>Download a copy of all data we store about you.</p> 187 + <div> 188 + <a href="/api/export-data" class="btn btn-secondary gap-2" download> 189 + <i data-lucide="download" class="size-4"></i> 190 + Export All My Data 191 + </a> 192 + </div> 181 193 182 - <div class="privacy-actions"> 183 - <a href="/api/export-data" class="btn-secondary" download> 184 - <i data-lucide="download"></i> 185 - Export All My Data 186 - </a> 187 - </div> 194 + <p class="text-sm text-base-content/60"> 195 + This includes your authorized devices, sessions, and hold memberships. 196 + Data stored on your PDS is already under your control. 197 + See our <a href="/privacy" class="link link-primary">Privacy Policy</a> for details. 198 + </p> 199 + </section> 188 200 189 - <p class="privacy-note"> 190 - <small> 191 - This includes your authorized devices, sessions, and hold memberships. 192 - Data stored on your PDS is already under your control. 193 - See our <a href="/privacy">Privacy Policy</a> for details. 194 - </small> 195 - </p> 196 - </section> 201 + <!-- Danger Zone Section --> 202 + <section class="border-2 border-error rounded-lg p-6 space-y-4"> 203 + <h2 class="text-xl font-semibold text-error flex items-center gap-2"> 204 + <i data-lucide="alert-triangle" class="size-5"></i> 205 + Danger Zone 206 + </h2> 197 207 198 - <!-- Danger Zone Section --> 199 - <section class="settings-section danger-zone"> 200 - <h2><i data-lucide="alert-triangle"></i> Danger Zone</h2> 208 + <div class="space-y-4"> 209 + <div> 210 + <h3 class="font-semibold">Delete ATCR Data</h3> 211 + <p class="text-base-content/70 mt-1">Remove your data from ATCR. This action cannot be undone.</p> 212 + </div> 201 213 202 - <div class="danger-card"> 203 - <h3>Delete ATCR Data</h3> 204 - <p>Remove your data from ATCR. This action cannot be undone.</p> 205 - <div class="info-notice"> 206 - <i data-lucide="info"></i> 207 - <span><strong>This does not delete your ATProto (Bluesky, Blacksky, Tangled) account.</strong><br>Only ATCR-specific data (authorized devices, hold memberships, settings) will be removed.</span> 208 - </div> 214 + <div class="alert bg-base-200"> 215 + <i data-lucide="info" class="size-5 shrink-0"></i> 216 + <span><strong>This does not delete your ATProto (Bluesky, Blacksky, Tangled) account.</strong><br>Only ATCR-specific data (authorized devices, hold memberships, settings) will be removed.</span> 217 + </div> 209 218 210 - <div class="delete-options"> 211 - <label class="checkbox-label"> 212 - <input type="checkbox" id="delete-pds-records"> 213 - <span>Also delete all <code>io.atcr.*</code> records from my ATProto PDS</span> 214 - </label> 215 - <small class="option-help"> 216 - This removes ATCR records (manifests, tags, stars, profile) stored in your PDS. 217 - Other records in your account are not impacted. 218 - </small> 219 - </div> 219 + <div class="space-y-2"> 220 + <label class="flex items-start gap-3 cursor-pointer"> 221 + <input type="checkbox" id="delete-pds-records" class="checkbox checkbox-sm mt-0.5"> 222 + <span class="text-sm">Also delete all <code class="cmd">io.atcr.*</code> records from my ATProto PDS</span> 223 + </label> 224 + <p class="text-xs text-base-content/60 ml-7"> 225 + This removes ATCR records (manifests, tags, stars, profile) stored in your PDS. 226 + Other records in your account are not impacted. 227 + </p> 228 + </div> 220 229 221 - <button type="button" id="delete-account-btn" class="btn-danger-large"> 222 - <i data-lucide="trash-2"></i> 223 - Delete My ATCR Data 224 - </button> 230 + <button type="button" id="delete-account-btn" class="btn btn-error btn-lg gap-2"> 231 + <i data-lucide="trash-2" class="size-5"></i> 232 + Delete My ATCR Data 233 + </button> 234 + </div> 235 + </section> 225 236 </div> 226 - </section> 227 - </div> 228 237 </main> 229 238 230 239 <script> ··· 260 269 'public': 'Public Access' 261 270 }[hold.membership] || hold.membership; 262 271 263 - const accessClass = 'access-' + hold.membership; 264 - accessEl.innerHTML = '<span class="access-badge ' + accessClass + '">' + accessLabel + '</span>'; 272 + const badgeColor = { 273 + 'owner': 'badge-primary', 274 + 'crew': 'badge-secondary', 275 + 'eligible': 'badge-accent', 276 + 'public': 'badge-ghost' 277 + }[hold.membership] || ''; 278 + 279 + accessEl.innerHTML = '<span class="badge badge-sm ' + badgeColor + '">' + accessLabel + '</span>'; 265 280 266 281 // Show permissions for crew members 267 282 if (hold.membership === 'crew' && hold.permissions && hold.permissions.length > 0) { 268 - accessEl.innerHTML += '<br><small>Permissions: ' + hold.permissions.join(', ') + '</small>'; 283 + accessEl.innerHTML += '<br><span class="text-xs text-base-content/60">Permissions: ' + hold.permissions.join(', ') + '</span>'; 269 284 } 270 285 271 286 holdDetails.style.display = 'block'; ··· 304 319 const tbody = document.getElementById('devices-table'); 305 320 306 321 if (devices.length === 0) { 307 - tbody.innerHTML = '<tr><td colspan="5">No authorized devices yet. Follow the setup instructions above!</td></tr>'; 322 + tbody.innerHTML = '<tr><td colspan="5" class="text-center text-base-content/60">No authorized devices yet. Follow the setup instructions above!</td></tr>'; 308 323 return; 309 324 } 310 325 ··· 317 332 return ` 318 333 <tr> 319 334 <td>${escapeHtml(device.name)}</td> 320 - <td>${escapeHtml(device.ip_address || 'Unknown')}</td> 335 + <td class="font-mono text-sm">${escapeHtml(device.ip_address || 'Unknown')}</td> 321 336 <td>${createdDate}</td> 322 337 <td>${lastUsed}</td> 323 - <td><button class="btn-danger" onclick="revokeDevice('${device.id}')">Revoke</button></td> 338 + <td><button class="btn btn-error btn-xs" onclick="revokeDevice('${device.id}')">Revoke</button></td> 324 339 </tr> 325 340 `; 326 341 }).join(''); 327 342 } catch (err) { 328 343 console.error('Error loading devices:', err); 329 344 document.getElementById('devices-table').innerHTML = 330 - '<tr><td colspan="5">Error loading devices</td></tr>'; 345 + '<tr><td colspan="5" class="text-center text-error">Error loading devices</td></tr>'; 331 346 } 332 347 } 333 348 ··· 372 387 }); 373 388 374 389 function showDeleteConfirmationModal() { 375 - // Create modal backdrop 390 + // Create modal using DaisyUI structure 376 391 const modal = document.createElement('div'); 377 - modal.className = 'delete-modal-backdrop'; 392 + modal.className = 'modal modal-open'; 378 393 modal.innerHTML = ` 379 - <div class="delete-modal"> 380 - <h2><i data-lucide="alert-triangle"></i> Delete ATCR Data</h2> 381 - <p class="reassurance-text"> 382 - <i data-lucide="check-circle"></i> 383 - Your ATProto account will <strong>NOT</strong> be affected. 384 - </p> 385 - <p class="warning-text"> 386 - This action <strong>cannot be undone</strong>. This will permanently delete: 387 - </p> 388 - <ul class="delete-list"> 389 - <li>Your ATCR account and all settings</li> 390 - <li>All authorized devices</li> 391 - <li>Your data from all holds you're a member of</li> 392 - ${document.getElementById('delete-pds-records').checked ? 393 - '<li>All io.atcr.* records from your ATProto PDS</li>' : ''} 394 - </ul> 395 - <p class="confirm-text">Type <strong>DELETE {{ .Profile.Handle }}</strong> to confirm:</p> 396 - <input type="text" id="confirm-delete-input" class="confirm-input" placeholder="DELETE {{ .Profile.Handle }}" autocomplete="off"> 397 - <div class="modal-actions"> 398 - <button type="button" class="btn-cancel" id="cancel-delete">Cancel</button> 399 - <button type="button" class="btn-confirm-delete" id="confirm-delete" disabled> 400 - <i data-lucide="trash-2"></i> 394 + <div class="modal-box max-w-lg"> 395 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 396 + <i data-lucide="alert-triangle" class="size-6"></i> 397 + Delete ATCR Data 398 + </h2> 399 + 400 + <div class="py-4 space-y-4"> 401 + <div class="alert alert-success"> 402 + <i data-lucide="check-circle" class="size-5"></i> 403 + <span>Your ATProto account will <strong>NOT</strong> be affected.</span> 404 + </div> 405 + 406 + <p class="text-base-content/80"> 407 + This action <strong>cannot be undone</strong>. This will permanently delete: 408 + </p> 409 + 410 + <ul class="list-disc list-inside text-sm space-y-1 text-base-content/70"> 411 + <li>Your ATCR account and all settings</li> 412 + <li>All authorized devices</li> 413 + <li>Your data from all holds you're a member of</li> 414 + ${document.getElementById('delete-pds-records').checked ? 415 + '<li>All io.atcr.* records from your ATProto PDS</li>' : ''} 416 + </ul> 417 + 418 + <div class="space-y-2"> 419 + <p class="text-sm">Type <strong class="font-mono">DELETE {{ .Profile.Handle }}</strong> to confirm:</p> 420 + <input type="text" id="confirm-delete-input" class="input input-bordered w-full font-mono" placeholder="DELETE {{ .Profile.Handle }}" autocomplete="off"> 421 + </div> 422 + </div> 423 + 424 + <div class="modal-action"> 425 + <button type="button" class="btn" id="cancel-delete">Cancel</button> 426 + <button type="button" class="btn btn-error gap-2" id="confirm-delete" disabled> 427 + <i data-lucide="trash-2" class="size-4"></i> 401 428 Delete My ATCR Data 402 429 </button> 403 430 </div> 404 431 </div> 432 + <div class="modal-backdrop bg-black/50" id="modal-backdrop"></div> 405 433 `; 406 434 document.body.appendChild(modal); 407 435 ··· 437 465 modal.remove(); 438 466 }); 439 467 440 - // Click outside to close 441 - modal.addEventListener('click', function(e) { 442 - if (e.target === modal) { 443 - modal.remove(); 444 - } 468 + // Click outside to close (on backdrop) 469 + document.getElementById('modal-backdrop').addEventListener('click', function() { 470 + modal.remove(); 445 471 }); 446 472 447 473 // Escape key to close ··· 460 486 461 487 // Show loading state 462 488 confirmBtn.disabled = true; 463 - confirmBtn.innerHTML = '<i data-lucide="loader-2" class="spin"></i> Deleting...'; 489 + confirmBtn.innerHTML = '<i data-lucide="loader-2" class="size-4 animate-spin"></i> Deleting...'; 464 490 if (typeof lucide !== 'undefined') { 465 491 lucide.createIcons(); 466 492 } ··· 480 506 481 507 if (response.ok && result.success) { 482 508 // Show success and redirect 483 - modal.querySelector('.delete-modal').innerHTML = ` 484 - <h2><i data-lucide="check-circle"></i> Account Deleted</h2> 485 - <p>Your account has been successfully deleted.</p> 486 - <p>Redirecting to home page...</p> 509 + modal.querySelector('.modal-box').innerHTML = ` 510 + <h2 class="text-xl font-bold flex items-center gap-2 text-success"> 511 + <i data-lucide="check-circle" class="size-6"></i> 512 + Account Deleted 513 + </h2> 514 + <div class="py-4 space-y-2"> 515 + <p>Your account has been successfully deleted.</p> 516 + <p class="text-base-content/70">Redirecting to home page...</p> 517 + </div> 487 518 `; 488 519 if (typeof lucide !== 'undefined') { 489 520 lucide.createIcons(); ··· 494 525 } else { 495 526 // Show error 496 527 const errors = result.errors || ['An unknown error occurred']; 497 - modal.querySelector('.delete-modal').innerHTML = ` 498 - <h2><i data-lucide="x-circle"></i> Deletion Failed</h2> 499 - <p>There were errors during account deletion:</p> 500 - <ul class="error-list"> 501 - ${errors.map(e => '<li>' + escapeHtml(e) + '</li>').join('')} 502 - </ul> 503 - <div class="modal-actions"> 504 - <button type="button" class="btn-cancel" onclick="this.closest('.delete-modal-backdrop').remove()">Close</button> 528 + modal.querySelector('.modal-box').innerHTML = ` 529 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 530 + <i data-lucide="x-circle" class="size-6"></i> 531 + Deletion Failed 532 + </h2> 533 + <div class="py-4 space-y-4"> 534 + <p>There were errors during account deletion:</p> 535 + <ul class="list-disc list-inside text-sm space-y-1 text-error"> 536 + ${errors.map(e => '<li>' + escapeHtml(e) + '</li>').join('')} 537 + </ul> 538 + </div> 539 + <div class="modal-action"> 540 + <button type="button" class="btn" onclick="this.closest('.modal').remove()">Close</button> 505 541 </div> 506 542 `; 507 543 if (typeof lucide !== 'undefined') { ··· 510 546 } 511 547 } catch (err) { 512 548 console.error('Delete account error:', err); 513 - modal.querySelector('.delete-modal').innerHTML = ` 514 - <h2><i data-lucide="x-circle"></i> Error</h2> 515 - <p>Failed to delete account: ${escapeHtml(err.message)}</p> 516 - <div class="modal-actions"> 517 - <button type="button" class="btn-cancel" onclick="this.closest('.delete-modal-backdrop').remove()">Close</button> 549 + modal.querySelector('.modal-box').innerHTML = ` 550 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 551 + <i data-lucide="x-circle" class="size-6"></i> 552 + Error 553 + </h2> 554 + <div class="py-4"> 555 + <p>Failed to delete account: ${escapeHtml(err.message)}</p> 556 + </div> 557 + <div class="modal-action"> 558 + <button type="button" class="btn" onclick="this.closest('.modal').remove()">Close</button> 518 559 </div> 519 560 `; 520 561 if (typeof lucide !== 'undefined') { ··· 531 572 } 532 573 })(); 533 574 </script> 534 - 535 - <style> 536 - /* Storage Section Styles */ 537 - .storage-section .storage-stats { 538 - background: var(--code-bg); 539 - padding: 1rem; 540 - border-radius: 4px; 541 - margin-top: 0.5rem; 542 - } 543 - .storage-section .stat-row { 544 - display: flex; 545 - justify-content: space-between; 546 - padding: 0.5rem 0; 547 - border-bottom: 1px solid var(--border); 548 - } 549 - .storage-section .stat-row:last-child { 550 - border-bottom: none; 551 - } 552 - .storage-section .stat-label { 553 - color: var(--fg-muted); 554 - } 555 - .storage-section .stat-value { 556 - font-weight: bold; 557 - font-family: monospace; 558 - } 559 - .storage-section .storage-error, 560 - .storage-section .storage-info { 561 - padding: 1rem; 562 - border-radius: 4px; 563 - margin-top: 0.5rem; 564 - display: flex; 565 - align-items: center; 566 - gap: 0.5rem; 567 - } 568 - .storage-section .storage-error { 569 - background: var(--error-bg, #fef2f2); 570 - color: var(--error, #dc2626); 571 - border: 1px solid var(--error, #dc2626); 572 - } 573 - .storage-section .storage-info { 574 - background: var(--info-bg, #eff6ff); 575 - color: var(--info, #2563eb); 576 - border: 1px solid var(--info, #2563eb); 577 - } 578 - .spin { 579 - animation: spin 1s linear infinite; 580 - } 581 - @keyframes spin { 582 - from { transform: rotate(0deg); } 583 - to { transform: rotate(360deg); } 584 - } 585 - 586 - /* Quota Progress Bar */ 587 - .storage-section .quota-progress { 588 - display: flex; 589 - align-items: center; 590 - gap: 0.75rem; 591 - padding: 0.75rem 0; 592 - } 593 - .storage-section .progress-bar { 594 - flex: 1; 595 - height: 8px; 596 - background: var(--border); 597 - border-radius: 4px; 598 - overflow: hidden; 599 - } 600 - .storage-section .progress-fill { 601 - height: 100%; 602 - border-radius: 4px; 603 - transition: width 0.3s ease; 604 - } 605 - .storage-section .progress-ok { 606 - background: #22c55e; 607 - } 608 - .storage-section .progress-warning { 609 - background: #eab308; 610 - } 611 - .storage-section .progress-danger { 612 - background: #ef4444; 613 - } 614 - .storage-section .progress-text { 615 - font-size: 0.85rem; 616 - color: var(--fg-muted); 617 - white-space: nowrap; 618 - } 619 - 620 - /* Tier Badge */ 621 - .storage-section .tier-badge { 622 - text-transform: capitalize; 623 - padding: 0.125rem 0.5rem; 624 - border-radius: 4px; 625 - font-size: 0.85rem; 626 - background: var(--accent-bg, #e0f2fe); 627 - color: var(--accent, #0369a1); 628 - } 629 - .storage-section .tier-owner { 630 - background: #fef3c7; 631 - color: #92400e; 632 - } 633 - .storage-section .tier-quartermaster { 634 - background: #dcfce7; 635 - color: #166534; 636 - } 637 - .storage-section .tier-bosun { 638 - background: #e0e7ff; 639 - color: #3730a3; 640 - } 641 - .storage-section .unlimited-badge { 642 - font-size: 0.75rem; 643 - padding: 0.125rem 0.375rem; 644 - background: #22c55e; 645 - color: #fff; 646 - border-radius: 3px; 647 - margin-left: 0.25rem; 648 - font-weight: 500; 649 - } 650 - 651 - /* Devices Section Styles */ 652 - .devices-section .setup-instructions { 653 - margin: 1rem 0; 654 - padding: 1.5rem; 655 - background: var(--code-bg); 656 - border-radius: 4px; 657 - } 658 - .devices-section .setup-instructions h3 { 659 - margin-top: 0; 660 - } 661 - .devices-section .setup-instructions ol { 662 - margin-left: 1.5rem; 663 - } 664 - .devices-section .setup-instructions li { 665 - margin-bottom: 1rem; 666 - } 667 - .devices-section .setup-instructions pre { 668 - background: var(--bg); 669 - color: var(--fg); 670 - border: 1px solid var(--border); 671 - padding: 0.75rem; 672 - border-radius: 4px; 673 - overflow-x: auto; 674 - margin: 0.5rem 0; 675 - } 676 - .devices-section .setup-instructions code { 677 - font-family: monospace; 678 - } 679 - .devices-section .fallback-note { 680 - margin-top: 1rem; 681 - padding: 1rem; 682 - background: var(--warning-bg); 683 - border: 1px solid var(--warning); 684 - border-radius: 4px; 685 - } 686 - .devices-section .fallback-note a { 687 - color: var(--warning); 688 - text-decoration: underline; 689 - font-weight: 500; 690 - } 691 - .devices-section .fallback-note a:hover { 692 - color: var(--primary); 693 - } 694 - .devices-section .fallback-note a:visited { 695 - color: var(--warning); 696 - } 697 - .devices-section table { 698 - width: 100%; 699 - border-collapse: collapse; 700 - margin-top: 1rem; 701 - } 702 - .devices-section th, 703 - .devices-section td { 704 - padding: 0.75rem; 705 - text-align: left; 706 - border-bottom: 1px solid var(--border); 707 - } 708 - .devices-section th { 709 - background: var(--code-bg); 710 - font-weight: bold; 711 - } 712 - .devices-section .btn-danger { 713 - background: #dc3545; 714 - color: white; 715 - border: none; 716 - padding: 0.5rem 1rem; 717 - border-radius: 4px; 718 - cursor: pointer; 719 - } 720 - .devices-section .btn-danger:hover { 721 - background: #c82333; 722 - } 723 - .devices-list { 724 - margin-top: 2rem; 725 - } 726 - 727 - /* Hold Selection Styles */ 728 - .hold-section .select-wrapper { 729 - position: relative; 730 - display: block; 731 - } 732 - .hold-section .form-select { 733 - width: 100%; 734 - padding: 0.75rem 2.5rem 0.75rem 0.75rem; 735 - font-size: 1rem; 736 - border: 1px solid var(--border); 737 - border-radius: 4px; 738 - background: var(--bg); 739 - color: var(--fg); 740 - cursor: pointer; 741 - appearance: none; 742 - -webkit-appearance: none; 743 - -moz-appearance: none; 744 - } 745 - .hold-section .select-icon { 746 - position: absolute; 747 - right: 0.75rem; 748 - top: 50%; 749 - transform: translateY(-50%); 750 - width: 1.25rem; 751 - height: 1.25rem; 752 - color: var(--fg-muted); 753 - pointer-events: none; 754 - } 755 - .hold-section .form-select:focus { 756 - outline: none; 757 - border-color: var(--primary); 758 - box-shadow: 0 0 0 2px var(--primary-bg, rgba(59, 130, 246, 0.1)); 759 - } 760 - .hold-section .form-select:focus + .select-icon { 761 - color: var(--primary); 762 - } 763 - .hold-section .form-select optgroup { 764 - font-weight: bold; 765 - color: var(--fg-muted); 766 - padding-top: 0.5rem; 767 - } 768 - .hold-section .form-select option { 769 - padding: 0.5rem; 770 - font-weight: normal; 771 - color: var(--fg); 772 - } 773 - 774 - /* Hold Details Panel */ 775 - .hold-details { 776 - margin-top: 1rem; 777 - padding: 1rem; 778 - background: var(--code-bg); 779 - border-radius: 4px; 780 - border: 1px solid var(--border); 781 - } 782 - .hold-details h3 { 783 - margin-top: 0; 784 - margin-bottom: 0.75rem; 785 - font-size: 0.9rem; 786 - color: var(--fg-muted); 787 - text-transform: uppercase; 788 - letter-spacing: 0.05em; 789 - } 790 - .hold-details dl { 791 - display: grid; 792 - grid-template-columns: auto 1fr; 793 - gap: 0.5rem 1rem; 794 - margin: 0; 795 - } 796 - .hold-details dt { 797 - color: var(--fg-muted); 798 - font-weight: 500; 799 - } 800 - .hold-details dd { 801 - margin: 0; 802 - font-family: monospace; 803 - word-break: break-all; 804 - } 805 - 806 - /* Access Level Badges */ 807 - .access-badge { 808 - display: inline-block; 809 - padding: 0.125rem 0.5rem; 810 - border-radius: 4px; 811 - font-size: 0.85rem; 812 - font-weight: 500; 813 - } 814 - .access-owner { 815 - background: #fef3c7; 816 - color: #92400e; 817 - } 818 - .access-crew { 819 - background: #dcfce7; 820 - color: #166534; 821 - } 822 - .access-eligible { 823 - background: #e0e7ff; 824 - color: #3730a3; 825 - } 826 - .access-public { 827 - background: #f3f4f6; 828 - color: #374151; 829 - } 830 - 831 - /* Privacy Section Styles */ 832 - .privacy-section .privacy-actions { 833 - margin: 1rem 0; 834 - } 835 - .privacy-section .btn-secondary { 836 - display: inline-flex; 837 - align-items: center; 838 - gap: 0.5rem; 839 - padding: 0.75rem 1.5rem; 840 - background: var(--code-bg); 841 - color: var(--fg); 842 - border: 1px solid var(--border); 843 - border-radius: 4px; 844 - text-decoration: none; 845 - font-weight: 500; 846 - transition: background 0.2s, border-color 0.2s; 847 - } 848 - .privacy-section .btn-secondary:hover { 849 - background: var(--border); 850 - border-color: var(--fg-muted); 851 - } 852 - .privacy-section .privacy-note { 853 - color: var(--fg-muted); 854 - margin-top: 1rem; 855 - } 856 - .privacy-section .privacy-note a { 857 - color: var(--primary); 858 - text-decoration: underline; 859 - } 860 - 861 - /* Danger Zone Styles */ 862 - .danger-zone { 863 - margin-top: 3rem; 864 - border: 2px solid #dc3545; 865 - border-radius: 8px; 866 - background: rgba(220, 53, 69, 0.03); 867 - } 868 - .danger-zone h2 { 869 - color: #dc3545; 870 - display: flex; 871 - align-items: center; 872 - gap: 0.5rem; 873 - } 874 - .danger-zone h2 svg { 875 - width: 1.25rem; 876 - height: 1.25rem; 877 - } 878 - .danger-card { 879 - padding: 1rem; 880 - background: var(--bg); 881 - border-radius: 4px; 882 - border: 1px solid var(--border); 883 - } 884 - .danger-card h3 { 885 - margin-top: 0; 886 - margin-bottom: 0.5rem; 887 - } 888 - .delete-options { 889 - margin: 1.5rem 0; 890 - padding: 1rem; 891 - background: var(--code-bg); 892 - border-radius: 4px; 893 - } 894 - .checkbox-label { 895 - display: flex; 896 - align-items: flex-start; 897 - gap: 0.5rem; 898 - cursor: pointer; 899 - } 900 - .checkbox-label input[type="checkbox"] { 901 - margin-top: 0.2rem; 902 - width: 1rem; 903 - height: 1rem; 904 - cursor: pointer; 905 - } 906 - .checkbox-label span { 907 - flex: 1; 908 - } 909 - .option-help { 910 - display: block; 911 - margin-top: 0.5rem; 912 - margin-left: 1.5rem; 913 - color: var(--fg-muted); 914 - } 915 - /* Info Notice in Danger Zone */ 916 - .danger-card .info-notice { 917 - display: flex; 918 - align-items: flex-start; 919 - gap: 0.5rem; 920 - padding: 0.75rem 1rem; 921 - margin: 1rem 0; 922 - background: #eff6ff; 923 - border: 1px solid #3b82f6; 924 - border-radius: 4px; 925 - color: #1e40af; 926 - } 927 - .danger-card .info-notice svg { 928 - width: 1rem; 929 - height: 1rem; 930 - flex-shrink: 0; 931 - margin-top: 0.125rem; 932 - } 933 - .btn-danger-large { 934 - display: inline-flex; 935 - align-items: center; 936 - gap: 0.5rem; 937 - padding: 0.75rem 1.5rem; 938 - background: #dc3545; 939 - color: white; 940 - border: none; 941 - border-radius: 4px; 942 - font-size: 1rem; 943 - font-weight: 500; 944 - cursor: pointer; 945 - transition: background 0.2s; 946 - } 947 - .btn-danger-large:hover { 948 - background: #c82333; 949 - } 950 - .btn-danger-large svg { 951 - width: 1rem; 952 - height: 1rem; 953 - } 954 - 955 - /* Delete Account Modal */ 956 - .delete-modal-backdrop { 957 - position: fixed; 958 - top: 0; 959 - left: 0; 960 - width: 100%; 961 - height: 100%; 962 - background: rgba(0, 0, 0, 0.6); 963 - display: flex; 964 - align-items: center; 965 - justify-content: center; 966 - z-index: 1000; 967 - padding: 1rem; 968 - } 969 - .delete-modal { 970 - background: var(--bg); 971 - padding: 2rem; 972 - border-radius: 8px; 973 - max-width: 480px; 974 - width: 100%; 975 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); 976 - } 977 - .delete-modal h2 { 978 - margin-top: 0; 979 - color: #dc3545; 980 - display: flex; 981 - align-items: center; 982 - gap: 0.5rem; 983 - } 984 - .delete-modal h2 svg { 985 - width: 1.5rem; 986 - height: 1.5rem; 987 - } 988 - .delete-modal .reassurance-text { 989 - display: flex; 990 - align-items: center; 991 - gap: 0.5rem; 992 - padding: 0.5rem 0.75rem; 993 - margin-bottom: 1rem; 994 - background: #dcfce7; 995 - border: 1px solid #22c55e; 996 - border-radius: 4px; 997 - color: #166534; 998 - font-size: 0.9rem; 999 - } 1000 - .delete-modal .reassurance-text svg { 1001 - width: 1rem; 1002 - height: 1rem; 1003 - flex-shrink: 0; 1004 - } 1005 - .delete-modal .warning-text { 1006 - margin-bottom: 0.5rem; 1007 - } 1008 - .delete-modal .delete-list { 1009 - margin: 1rem 0 1.5rem; 1010 - padding-left: 1.5rem; 1011 - } 1012 - .delete-modal .delete-list li { 1013 - margin-bottom: 0.5rem; 1014 - color: var(--fg-muted); 1015 - } 1016 - .delete-modal .confirm-text { 1017 - margin-bottom: 0.5rem; 1018 - } 1019 - .delete-modal .confirm-input { 1020 - width: 100%; 1021 - padding: 0.75rem; 1022 - font-size: 1rem; 1023 - border: 2px solid var(--border); 1024 - border-radius: 4px; 1025 - background: var(--bg); 1026 - color: var(--fg); 1027 - margin-bottom: 1.5rem; 1028 - } 1029 - .delete-modal .confirm-input:focus { 1030 - outline: none; 1031 - border-color: #dc3545; 1032 - } 1033 - .delete-modal .modal-actions { 1034 - display: flex; 1035 - gap: 1rem; 1036 - justify-content: flex-end; 1037 - } 1038 - .delete-modal .btn-cancel { 1039 - padding: 0.75rem 1.5rem; 1040 - background: var(--code-bg); 1041 - color: var(--fg); 1042 - border: 1px solid var(--border); 1043 - border-radius: 4px; 1044 - cursor: pointer; 1045 - font-size: 1rem; 1046 - } 1047 - .delete-modal .btn-cancel:hover { 1048 - background: var(--border); 1049 - } 1050 - .delete-modal .btn-cancel:disabled { 1051 - opacity: 0.5; 1052 - cursor: not-allowed; 1053 - } 1054 - .delete-modal .btn-confirm-delete { 1055 - display: inline-flex; 1056 - align-items: center; 1057 - gap: 0.5rem; 1058 - padding: 0.75rem 1.5rem; 1059 - background: #dc3545; 1060 - color: white; 1061 - border: none; 1062 - border-radius: 4px; 1063 - font-size: 1rem; 1064 - cursor: pointer; 1065 - } 1066 - .delete-modal .btn-confirm-delete:hover:not(:disabled) { 1067 - background: #c82333; 1068 - } 1069 - .delete-modal .btn-confirm-delete:disabled { 1070 - background: #6c757d; 1071 - cursor: not-allowed; 1072 - } 1073 - .delete-modal .btn-confirm-delete svg { 1074 - width: 1rem; 1075 - height: 1rem; 1076 - } 1077 - .delete-modal .error-list { 1078 - margin: 1rem 0; 1079 - padding-left: 1.5rem; 1080 - color: #dc3545; 1081 - } 1082 - </style> 1083 575 </body> 1084 576 </html> 1085 577 {{ end }}
+24 -10
pkg/appview/templates/pages/user.html
··· 22 22 <body> 23 23 {{ template "nav" . }} 24 24 25 - <main class="container"> 26 - <div class="home-page"> 27 - <div class="user-profile"> 25 + <main class="container mx-auto px-4 py-8"> 26 + <div class="flex flex-col items-center gap-8"> 27 + <!-- User Profile Header --> 28 + <div class="flex flex-col items-center gap-4"> 28 29 {{ if .ViewedUser.Avatar }} 29 - <img src="{{ .ViewedUser.Avatar }}" alt="{{ .ViewedUser.Handle }}" class="profile-avatar"> 30 + <div class="avatar"> 31 + <div class="w-20 rounded-full shadow"> 32 + <img src="{{ .ViewedUser.Avatar }}" alt="{{ .ViewedUser.Handle }}" /> 33 + </div> 34 + </div> 30 35 {{ else if .HasProfile }} 31 - <div class="profile-avatar-placeholder">{{ firstChar .ViewedUser.Handle }}</div> 36 + <div class="avatar avatar-placeholder"> 37 + <div class="bg-neutral text-neutral-content w-20 rounded-full shadow"> 38 + <span class="text-3xl">{{ firstChar .ViewedUser.Handle }}</span> 39 + </div> 40 + </div> 32 41 {{ else }} 33 - <div class="profile-avatar-placeholder">?</div> 42 + <div class="avatar avatar-placeholder"> 43 + <div class="bg-base-300 text-base-content/60 w-20 rounded-full shadow"> 44 + <span class="text-3xl">?</span> 45 + </div> 46 + </div> 34 47 {{ end }} 35 - <h1>{{ .ViewedUser.Handle }}</h1> 48 + <h1 class="text-2xl font-bold">{{ .ViewedUser.Handle }}</h1> 36 49 </div> 37 50 51 + <!-- Content --> 38 52 {{ if not .HasProfile }} 39 - <div class="empty-state"> 53 + <div class="text-center text-base-content/60 py-12"> 40 54 <p>This user hasn't set up their ATCR profile yet.</p> 41 55 </div> 42 56 {{ else if .Repositories }} 43 - <div class="featured-grid"> 57 + <div class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> 44 58 {{ range .Repositories }} 45 59 {{ template "repo-card" . }} 46 60 {{ end }} 47 61 </div> 48 62 {{ else }} 49 - <div class="empty-state"> 63 + <div class="text-center text-base-content/60 py-12"> 50 64 <p>No images yet.</p> 51 65 </div> 52 66 {{ end }}
+2 -2
pkg/appview/templates/partials/health-badge.html
··· 1 1 {{ define "health-badge" }} 2 2 {{ if .Pending }} 3 - <span class="checking-badge" 3 + <span class="badge badge-sm badge-info" 4 4 hx-get="/api/manifest-health?endpoint={{ .RetryURL }}" 5 5 hx-trigger="load delay:3s" 6 6 hx-swap="outerHTML"><i data-lucide="refresh-ccw"></i> Checking...</span> 7 7 {{ else if not .Reachable }} 8 - <span class="offline-badge"><i data-lucide="triangle-alert"></i> Offline</span> 8 + <span class="badge badge-sm badge-warning"><i data-lucide="triangle-alert"></i> Offline</span> 9 9 {{ end }} 10 10 {{ end }}
+29 -31
pkg/appview/templates/partials/push-list.html
··· 1 1 {{ range .Pushes }} 2 - <div class="push-card"> 3 - <div class="push-header"> 2 + <div class="card p-4"> 3 + <div class="flex items-start gap-4"> 4 4 {{ if .IconURL }} 5 - <img src="{{ .IconURL }}" alt="{{ .Repository }}" class="push-icon"> 5 + <img src="{{ .IconURL }}" alt="{{ .Repository }}" class="size-12 rounded-lg object-cover shrink-0"> 6 6 {{ else }} 7 - <div class="push-icon-placeholder">{{ firstChar .Repository }}</div> 7 + <div class="avatar avatar-placeholder"> 8 + <div class="bg-neutral text-neutral-content size-12 rounded-lg shadow-sm"> 9 + <span class="text-lg">{{ firstChar .Repository }}</span> 10 + </div> 11 + </div> 8 12 {{ end }} 9 - <div class="push-info"> 10 - <div class="push-title-row"> 11 - <div class="push-title"> 12 - <a href="/u/{{ .Handle }}" class="push-user">{{ .Handle }}</a> 13 - <span class="push-separator">/</span> 14 - <a href="/r/{{ .Handle }}/{{ .Repository }}" class="push-repo">{{ .Repository }}</a> 15 - <span class="push-separator">:</span> 16 - <span class="push-tag">{{ .Tag }}</span> 13 + <div class="flex-1 min-w-0"> 14 + <div class="flex justify-between items-center gap-4"> 15 + <div class="truncate"> 16 + <a href="/u/{{ .Handle }}" class="link link-primary font-medium">{{ .Handle }}</a> 17 + <span class="text-base-content/60">/</span> 18 + <a href="/r/{{ .Handle }}/{{ .Repository }}" class="link text-base-content font-medium hover:underline">{{ .Repository }}</a> 19 + <span class="text-base-content/60 mx-1">:</span> 20 + <span class="text-base-content/60">{{ .Tag }}</span> 17 21 {{ if eq .ArtifactType "helm-chart" }} 18 - <span class="artifact-badge helm"><i data-lucide="anchor"></i></span> 22 + <span class="badge badge-xs badge-soft badge-primary"><i data-lucide="anchor"></i></span> 19 23 {{ end }} 20 24 </div> 21 - <div class="push-stats"> 22 - <span class="push-stat"> 23 - <i data-lucide="star" class="star-icon{{ if .IsStarred }} star-filled{{ end }}"></i> 24 - <span class="stat-count">{{ .StarCount }}</span> 25 - </span> 26 - <span class="push-stat"> 27 - <i data-lucide="arrow-down-to-line" class="pull-icon"></i> 28 - <span class="stat-count">{{ .PullCount }}</span> 29 - </span> 25 + <div class="flex items-center gap-4 shrink-0"> 26 + {{ template "star" (dict "IsStarred" .IsStarred "StarCount" .StarCount) }} 27 + {{ template "pull-count" (dict "PullCount" .PullCount) }} 30 28 </div> 31 29 </div> 32 30 {{ if .Description }} 33 - <p class="push-description">{{ .Description }}</p> 31 + <p class="text-base-content/60 text-sm mt-1 m-0">{{ .Description }}</p> 34 32 {{ end }} 35 33 </div> 36 34 </div> 37 35 38 - <div class="push-details"> 39 - <div class="digest-container"> 40 - <code class="digest" title="{{ .Digest }}">{{ .Digest }}</code> 41 - <button class="digest-copy-btn" onclick="copyToClipboard('{{ .Digest }}')"><i data-lucide="copy"></i></button> 36 + <div class="flex items-center gap-4 mt-3 pt-3 border-t border-base-300 text-base-content/60"> 37 + <div class="flex items-center gap-2"> 38 + <code class="font-mono text-sm truncate max-w-[200px]" title="{{ .Digest }}">{{ .Digest }}</code> 39 + <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Digest }}')"><i data-lucide="copy" class="size-4"></i></button> 42 40 </div> 43 - <time class="timestamp" datetime="{{ .CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 41 + <time datetime="{{ .CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}" class="text-sm"> 44 42 {{ timeAgo .CreatedAt }} 45 43 </time> 46 44 </div> ··· 48 46 {{ end }} 49 47 50 48 {{ if eq (len .Pushes) 0 }} 51 - <div class="empty-state"> 52 - <p>No pushes yet. Start using ATCR by pushing your first image!</p> 53 - <pre><code>docker push {{ .RegistryURL }}/yourhandle/myapp:latest</code></pre> 49 + <div class="py-8 text-center"> 50 + <p class="text-base-content/60">No pushes yet. Start using ATCR by pushing your first image!</p> 51 + <pre class="mt-4"><code class="font-mono text-sm">docker push {{ .RegistryURL }}/yourhandle/myapp:latest</code></pre> 54 52 </div> 55 53 {{ end }}
+14 -16
pkg/appview/templates/partials/storage_stats.html
··· 1 1 {{ define "storage_stats" }} 2 - <div class="storage-stats"> 2 + <div class="space-y-2"> 3 3 {{ if .Tier }} 4 - <div class="stat-row"> 5 - <span class="stat-label">Tier:</span> 6 - <span class="stat-value tier-badge tier-{{ .Tier }}">{{ .Tier }}</span> 4 + <div class="flex justify-between items-center"> 5 + <span class="text-base-content/60">Tier:</span> 6 + <span class="badge badge-xs badge-{{ .Tier }} font-semibold">{{ .Tier }}</span> 7 7 </div> 8 8 {{ end }} 9 - <div class="stat-row"> 10 - <span class="stat-label">Storage:</span> 11 - <span class="stat-value"> 9 + <div class="flex justify-between items-center"> 10 + <span class="text-base-content/60">Storage:</span> 11 + <span class="font-semibold font-mono"> 12 12 {{ if .HasLimit }} 13 13 {{ .HumanSize }} / {{ .HumanLimit }} 14 14 {{ else }} 15 - {{ .HumanSize }} <span class="unlimited-badge">Unlimited</span> 15 + {{ .HumanSize }} <span class="badge badge-xs badge-success">Unlimited</span> 16 16 {{ end }} 17 17 </span> 18 18 </div> 19 19 {{ if .HasLimit }} 20 - <div class="quota-progress"> 21 - <div class="progress-bar"> 22 - <div class="progress-fill {{ if ge .UsagePercent 95 }}progress-danger{{ else if ge .UsagePercent 80 }}progress-warning{{ else }}progress-ok{{ end }}" style="width: {{ .UsagePercent }}%"></div> 23 - </div> 24 - <span class="progress-text">{{ .UsagePercent }}% used</span> 20 + <div class="flex items-center gap-2 py-2"> 21 + <progress class="progress {{ if ge .UsagePercent 95 }}progress-error{{ else if ge .UsagePercent 80 }}progress-warning{{ else }}progress-success{{ end }} w-full" value="{{ .UsagePercent }}" max="100"></progress> 22 + <span class="text-sm text-base-content/60 whitespace-nowrap">{{ .UsagePercent }}% used</span> 25 23 </div> 26 24 {{ end }} 27 - <div class="stat-row"> 28 - <span class="stat-label">Unique Blobs:</span> 29 - <span class="stat-value">{{ .UniqueBlobs }}</span> 25 + <div class="flex justify-between items-center"> 26 + <span class="text-base-content/60">Unique Blobs:</span> 27 + <span class="font-semibold font-mono">{{ .UniqueBlobs }}</span> 30 28 </div> 31 29 </div> 32 30 {{ end }}
+21 -13
pkg/appview/ui.go
··· 12 12 "atcr.io/pkg/appview/licenses" 13 13 ) 14 14 15 - //go:generate curl -fsSL -o static/js/htmx.min.js https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js 16 - //go:generate curl -fsSL -o static/js/lucide.min.js https://unpkg.com/lucide@latest/dist/umd/lucide.min.js 15 + //go:generate sh -c "cd ../.. && npm run build" 17 16 18 17 //go:embed templates/**/*.html 19 18 var templatesFS embed.FS 20 19 21 - //go:embed static 22 - var staticFS embed.FS 20 + //go:embed public 21 + var publicFS embed.FS 23 22 24 23 // Templates returns parsed templates with helper functions 25 24 func Templates() (*template.Template, error) { ··· 96 95 "parseLicenses": func(licensesStr string) []licenses.LicenseInfo { 97 96 return licenses.ParseLicenses(licensesStr) 98 97 }, 98 + 99 + "dict": func(values ...any) map[string]any { 100 + dict := make(map[string]any, len(values)/2) 101 + for i := 0; i < len(values); i += 2 { 102 + key, _ := values[i].(string) 103 + dict[key] = values[i+1] 104 + } 105 + return dict 106 + }, 99 107 } 100 108 101 109 tmpl := template.New("").Funcs(funcMap) ··· 107 115 return tmpl, nil 108 116 } 109 117 110 - // StaticHandler returns HTTP handler for static files 111 - func StaticHandler() http.Handler { 112 - sub, err := fs.Sub(staticFS, "static") 118 + // PublicHandler returns HTTP handler for static files 119 + func PublicHandler() http.Handler { 120 + sub, err := fs.Sub(publicFS, "public") 113 121 if err != nil { 114 122 panic(err) 115 123 } 116 124 return http.FileServer(http.FS(sub)) 117 125 } 118 126 119 - // StaticRootFiles returns list of root-level files in static directory (not subdirectories) 120 - func StaticRootFiles() ([]string, error) { 121 - entries, err := staticFS.ReadDir("static") 127 + // PublicRootFiles returns list of root-level files in static directory (not subdirectories) 128 + func PublicRootFiles() ([]string, error) { 129 + entries, err := publicFS.ReadDir("public") 122 130 if err != nil { 123 131 return nil, err 124 132 } ··· 133 141 return files, nil 134 142 } 135 143 136 - // StaticSubdir returns an fs.FS for a subdirectory within static/ 137 - func StaticSubdir(name string) http.Handler { 138 - sub, err := fs.Sub(staticFS, "static/"+name) 144 + // PublicSubdir returns an fs.FS for a subdirectory within static/ 145 + func PublicSubdir(name string) http.Handler { 146 + sub, err := fs.Sub(publicFS, "public/"+name) 139 147 if err != nil { 140 148 panic(err) 141 149 }
+15 -9
pkg/appview/ui_test.go
··· 581 581 PullCount int 582 582 IsStarred bool 583 583 ArtifactType string 584 + Tag string 585 + Digest string 586 + LastUpdated time.Time 584 587 }{ 585 588 OwnerHandle: "alice.bsky.social", 586 589 Repository: "myapp", ··· 590 593 PullCount: 1337, 591 594 IsStarred: true, 592 595 ArtifactType: "container-image", 596 + Tag: "latest", 597 + Digest: "sha256:abc123def456", 598 + LastUpdated: time.Now().Add(-24 * time.Hour), 593 599 } 594 600 595 601 buf := new(bytes.Buffer) ··· 605 611 "alice.bsky.social", 606 612 "myapp", 607 613 "A cool container image", 608 - "42", // star count 609 - "1337", // pull count 610 - "featured-icon-placeholder", // no icon URL provided 614 + "42", // star count 615 + "1337", // pull count 616 + "avatar-placeholder", // DaisyUI avatar placeholder when no icon URL 611 617 } 612 618 613 619 for _, expected := range expectedContent { ··· 704 710 "Reachable": false, 705 711 "RetryURL": "http%3A%2F%2Fexample.com", 706 712 }, 707 - expectInOutput: "checking-badge", 708 - expectMissing: "offline-badge", 713 + expectInOutput: "badge-info", 714 + expectMissing: "badge-warning", 709 715 }, 710 716 { 711 717 name: "offline state", ··· 714 720 "Reachable": false, 715 721 "RetryURL": "", 716 722 }, 717 - expectInOutput: "offline-badge", 718 - expectMissing: "checking-badge", 723 + expectInOutput: "badge-warning", 724 + expectMissing: "badge-info", 719 725 }, 720 726 { 721 727 name: "online state - empty output", ··· 774 780 } 775 781 } 776 782 777 - func TestStaticHandler(t *testing.T) { 778 - handler := StaticHandler() 783 + func TestPublicHandler(t *testing.T) { 784 + handler := PublicHandler() 779 785 if handler == nil { 780 786 t.Fatal("StaticHandler() returned nil") 781 787 }
+12
pkg/auth/oauth/server.go
··· 256 256 HttpOnly: true, 257 257 }) 258 258 259 + // Set a JS-readable cookie with the handle for "recent accounts" feature 260 + // Frontend will read this, save to localStorage, and delete the cookie 261 + http.SetCookie(w, &http.Cookie{ 262 + Name: "atcr_login_handle", 263 + Value: handle, 264 + Path: "/", 265 + MaxAge: 60, // Short-lived, just for the redirect 266 + HttpOnly: false, 267 + Secure: r.URL.Scheme == "https" || r.Header.Get("X-Forwarded-Proto") == "https", 268 + SameSite: http.SameSiteLaxMode, 269 + }) 270 + 259 271 // Redirect to return URL 260 272 returnTo := cookie.Value 261 273 if returnTo == "" {
+6 -6
pkg/hold/admin/admin.go
··· 3 3 // and usage metrics. The admin panel is embedded directly in the hold service binary. 4 4 package admin 5 5 6 - //go:generate curl -fsSL -o static/js/htmx.min.js https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js 7 - //go:generate curl -fsSL -o static/js/lucide.min.js https://unpkg.com/lucide@latest/dist/umd/lucide.min.js 6 + //go:generate curl -fsSL -o public/js/htmx.min.js https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js 7 + //go:generate curl -fsSL -o public/js/lucide.min.js https://unpkg.com/lucide@latest/dist/umd/lucide.min.js 8 8 9 9 import ( 10 10 "context" ··· 33 33 //go:embed templates/* 34 34 var templatesFS embed.FS 35 35 36 - //go:embed static/* 37 - var staticFS embed.FS 36 + //go:embed public/* 37 + var publicFS embed.FS 38 38 39 39 // AdminConfig holds admin panel configuration 40 40 type AdminConfig struct { ··· 291 291 // RegisterRoutes registers all admin routes with the router 292 292 func (ui *AdminUI) RegisterRoutes(r chi.Router) { 293 293 // Static files (public) 294 - staticSub, _ := fs.Sub(staticFS, "static") 295 - r.Handle("/admin/static/*", http.StripPrefix("/admin/static/", http.FileServer(http.FS(staticSub)))) 294 + staticSub, _ := fs.Sub(publicFS, "public") 295 + r.Handle("/admin/public/*", http.StripPrefix("/admin/public/", http.FileServer(http.FS(staticSub)))) 296 296 297 297 // OAuth client metadata endpoint (required for production OAuth) 298 298 r.Get("/admin/oauth-client-metadata.json", ui.handleClientMetadata)
pkg/hold/admin/static/css/admin.css pkg/hold/admin/public/css/admin.css
pkg/hold/admin/static/js/htmx.min.js pkg/hold/admin/public/js/htmx.min.js
pkg/hold/admin/static/js/lucide.min.js pkg/hold/admin/public/js/lucide.min.js
+3 -3
pkg/hold/admin/templates/pages/crew.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>{{.Title}} - Hold Admin</title> 8 - <script src="/admin/static/js/htmx.min.js"></script> 9 - <script src="/admin/static/js/lucide.min.js"></script> 10 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <script src="/admin/public/js/htmx.min.js"></script> 9 + <script src="/admin/public/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 11 11 </head> 12 12 <body> 13 13 {{template "nav" .}}
+3 -3
pkg/hold/admin/templates/pages/crew_add.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>{{.Title}} - Hold Admin</title> 8 - <script src="/admin/static/js/htmx.min.js"></script> 9 - <script src="/admin/static/js/lucide.min.js"></script> 10 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <script src="/admin/public/js/htmx.min.js"></script> 9 + <script src="/admin/public/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 11 11 </head> 12 12 <body> 13 13 {{template "nav" .}}
+3 -3
pkg/hold/admin/templates/pages/crew_edit.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>{{.Title}} - Hold Admin</title> 8 - <script src="/admin/static/js/htmx.min.js"></script> 9 - <script src="/admin/static/js/lucide.min.js"></script> 10 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <script src="/admin/public/js/htmx.min.js"></script> 9 + <script src="/admin/public/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 11 11 </head> 12 12 <body> 13 13 {{template "nav" .}}
+2 -2
pkg/hold/admin/templates/pages/dashboard.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>{{.Title}} - Hold Admin</title> 8 - <script src="/admin/static/js/htmx.min.js"></script> 9 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <script src="/admin/public/js/htmx.min.js"></script> 9 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 10 10 </head> 11 11 <body> 12 12 {{template "nav" .}}
+1 -1
pkg/hold/admin/templates/pages/error.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>Error - Hold Admin</title> 8 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 9 9 </head> 10 10 <body> 11 11 {{template "nav" .}}
+1 -1
pkg/hold/admin/templates/pages/login.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>Login - Hold Admin</title> 8 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 9 9 </head> 10 10 <body class="login-page"> 11 11 <div class="login-container">
+3 -3
pkg/hold/admin/templates/pages/settings.html
··· 5 5 <meta charset="UTF-8"> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 7 <title>{{.Title}} - Hold Admin</title> 8 - <script src="/admin/static/js/htmx.min.js"></script> 9 - <script src="/admin/static/js/lucide.min.js"></script> 10 - <link rel="stylesheet" href="/admin/static/css/admin.css"> 8 + <script src="/admin/public/js/htmx.min.js"></script> 9 + <script src="/admin/public/js/lucide.min.js"></script> 10 + <link rel="stylesheet" href="/admin/public/css/admin.css"> 11 11 </head> 12 12 <body> 13 13 {{template "nav" .}}
+20
tailwind.config.js
··· 1 + /** @type {import('tailwindcss').Config} */ 2 + module.exports = { 3 + content: [ 4 + "./pkg/appview/templates/**/*.html", 5 + "./pkg/appview/public/js/**/*.js", 6 + ], 7 + // DaisyUI handles dark mode via data-theme 8 + theme: { 9 + extend: { 10 + // Only keep custom extensions not covered by DaisyUI 11 + colors: { 12 + star: 'var(--star)', 13 + }, 14 + fontFamily: { 15 + mono: ['Monaco', 'Menlo', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace'], 16 + }, 17 + }, 18 + }, 19 + // DaisyUI is added via @plugin in CSS 20 + }