A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

begin large refactor of UI to use tailwind and daisy

+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 + }