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

Configure Feed

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

impeccable pass

+2050 -1553
+232 -232
package-lock.json
··· 10 10 "dependencies": { 11 11 "htmx-ext-json-enc": "^2.0.3", 12 12 "htmx.org": "^2.0.8", 13 - "lucide": "^0.577.0" 13 + "lucide": "^1.8.0" 14 14 }, 15 15 "devDependencies": { 16 - "@tailwindcss/cli": "^4.2.1", 16 + "@tailwindcss/cli": "^4.2.2", 17 17 "@tailwindcss/typography": "^0.5.19", 18 18 "daisyui": "^5.5.19", 19 - "esbuild": "^0.27.4", 19 + "esbuild": "^0.28.0", 20 20 "glob": "^13.0.6", 21 - "tailwindcss": "^4.2" 21 + "tailwindcss": "^4.2.2" 22 22 } 23 23 }, 24 24 "node_modules/@esbuild/aix-ppc64": { 25 - "version": "0.27.4", 26 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", 27 - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", 25 + "version": "0.28.0", 26 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", 27 + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", 28 28 "cpu": [ 29 29 "ppc64" 30 30 ], ··· 39 39 } 40 40 }, 41 41 "node_modules/@esbuild/android-arm": { 42 - "version": "0.27.4", 43 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", 44 - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", 42 + "version": "0.28.0", 43 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", 44 + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", 45 45 "cpu": [ 46 46 "arm" 47 47 ], ··· 56 56 } 57 57 }, 58 58 "node_modules/@esbuild/android-arm64": { 59 - "version": "0.27.4", 60 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", 61 - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", 59 + "version": "0.28.0", 60 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", 61 + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", 62 62 "cpu": [ 63 63 "arm64" 64 64 ], ··· 73 73 } 74 74 }, 75 75 "node_modules/@esbuild/android-x64": { 76 - "version": "0.27.4", 77 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", 78 - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", 76 + "version": "0.28.0", 77 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", 78 + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", 79 79 "cpu": [ 80 80 "x64" 81 81 ], ··· 90 90 } 91 91 }, 92 92 "node_modules/@esbuild/darwin-arm64": { 93 - "version": "0.27.4", 94 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", 95 - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", 93 + "version": "0.28.0", 94 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", 95 + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", 96 96 "cpu": [ 97 97 "arm64" 98 98 ], ··· 107 107 } 108 108 }, 109 109 "node_modules/@esbuild/darwin-x64": { 110 - "version": "0.27.4", 111 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", 112 - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", 110 + "version": "0.28.0", 111 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", 112 + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", 113 113 "cpu": [ 114 114 "x64" 115 115 ], ··· 124 124 } 125 125 }, 126 126 "node_modules/@esbuild/freebsd-arm64": { 127 - "version": "0.27.4", 128 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", 129 - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", 127 + "version": "0.28.0", 128 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", 129 + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", 130 130 "cpu": [ 131 131 "arm64" 132 132 ], ··· 141 141 } 142 142 }, 143 143 "node_modules/@esbuild/freebsd-x64": { 144 - "version": "0.27.4", 145 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", 146 - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", 144 + "version": "0.28.0", 145 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", 146 + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", 147 147 "cpu": [ 148 148 "x64" 149 149 ], ··· 158 158 } 159 159 }, 160 160 "node_modules/@esbuild/linux-arm": { 161 - "version": "0.27.4", 162 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", 163 - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", 161 + "version": "0.28.0", 162 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", 163 + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", 164 164 "cpu": [ 165 165 "arm" 166 166 ], ··· 175 175 } 176 176 }, 177 177 "node_modules/@esbuild/linux-arm64": { 178 - "version": "0.27.4", 179 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", 180 - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", 178 + "version": "0.28.0", 179 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", 180 + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", 181 181 "cpu": [ 182 182 "arm64" 183 183 ], ··· 192 192 } 193 193 }, 194 194 "node_modules/@esbuild/linux-ia32": { 195 - "version": "0.27.4", 196 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", 197 - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", 195 + "version": "0.28.0", 196 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", 197 + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", 198 198 "cpu": [ 199 199 "ia32" 200 200 ], ··· 209 209 } 210 210 }, 211 211 "node_modules/@esbuild/linux-loong64": { 212 - "version": "0.27.4", 213 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", 214 - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", 212 + "version": "0.28.0", 213 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", 214 + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", 215 215 "cpu": [ 216 216 "loong64" 217 217 ], ··· 226 226 } 227 227 }, 228 228 "node_modules/@esbuild/linux-mips64el": { 229 - "version": "0.27.4", 230 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", 231 - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", 229 + "version": "0.28.0", 230 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", 231 + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", 232 232 "cpu": [ 233 233 "mips64el" 234 234 ], ··· 243 243 } 244 244 }, 245 245 "node_modules/@esbuild/linux-ppc64": { 246 - "version": "0.27.4", 247 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", 248 - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", 246 + "version": "0.28.0", 247 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", 248 + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", 249 249 "cpu": [ 250 250 "ppc64" 251 251 ], ··· 260 260 } 261 261 }, 262 262 "node_modules/@esbuild/linux-riscv64": { 263 - "version": "0.27.4", 264 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", 265 - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", 263 + "version": "0.28.0", 264 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", 265 + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", 266 266 "cpu": [ 267 267 "riscv64" 268 268 ], ··· 277 277 } 278 278 }, 279 279 "node_modules/@esbuild/linux-s390x": { 280 - "version": "0.27.4", 281 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", 282 - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", 280 + "version": "0.28.0", 281 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", 282 + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", 283 283 "cpu": [ 284 284 "s390x" 285 285 ], ··· 294 294 } 295 295 }, 296 296 "node_modules/@esbuild/linux-x64": { 297 - "version": "0.27.4", 298 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", 299 - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", 297 + "version": "0.28.0", 298 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", 299 + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", 300 300 "cpu": [ 301 301 "x64" 302 302 ], ··· 311 311 } 312 312 }, 313 313 "node_modules/@esbuild/netbsd-arm64": { 314 - "version": "0.27.4", 315 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", 316 - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", 314 + "version": "0.28.0", 315 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", 316 + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", 317 317 "cpu": [ 318 318 "arm64" 319 319 ], ··· 328 328 } 329 329 }, 330 330 "node_modules/@esbuild/netbsd-x64": { 331 - "version": "0.27.4", 332 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", 333 - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", 331 + "version": "0.28.0", 332 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", 333 + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", 334 334 "cpu": [ 335 335 "x64" 336 336 ], ··· 345 345 } 346 346 }, 347 347 "node_modules/@esbuild/openbsd-arm64": { 348 - "version": "0.27.4", 349 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", 350 - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", 348 + "version": "0.28.0", 349 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", 350 + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", 351 351 "cpu": [ 352 352 "arm64" 353 353 ], ··· 362 362 } 363 363 }, 364 364 "node_modules/@esbuild/openbsd-x64": { 365 - "version": "0.27.4", 366 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", 367 - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", 365 + "version": "0.28.0", 366 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", 367 + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", 368 368 "cpu": [ 369 369 "x64" 370 370 ], ··· 379 379 } 380 380 }, 381 381 "node_modules/@esbuild/openharmony-arm64": { 382 - "version": "0.27.4", 383 - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", 384 - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", 382 + "version": "0.28.0", 383 + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", 384 + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", 385 385 "cpu": [ 386 386 "arm64" 387 387 ], ··· 396 396 } 397 397 }, 398 398 "node_modules/@esbuild/sunos-x64": { 399 - "version": "0.27.4", 400 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", 401 - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", 399 + "version": "0.28.0", 400 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", 401 + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", 402 402 "cpu": [ 403 403 "x64" 404 404 ], ··· 413 413 } 414 414 }, 415 415 "node_modules/@esbuild/win32-arm64": { 416 - "version": "0.27.4", 417 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", 418 - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", 416 + "version": "0.28.0", 417 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", 418 + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", 419 419 "cpu": [ 420 420 "arm64" 421 421 ], ··· 430 430 } 431 431 }, 432 432 "node_modules/@esbuild/win32-ia32": { 433 - "version": "0.27.4", 434 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", 435 - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", 433 + "version": "0.28.0", 434 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", 435 + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", 436 436 "cpu": [ 437 437 "ia32" 438 438 ], ··· 447 447 } 448 448 }, 449 449 "node_modules/@esbuild/win32-x64": { 450 - "version": "0.27.4", 451 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", 452 - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", 450 + "version": "0.28.0", 451 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", 452 + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", 453 453 "cpu": [ 454 454 "x64" 455 455 ], ··· 823 823 } 824 824 }, 825 825 "node_modules/@tailwindcss/cli": { 826 - "version": "4.2.1", 827 - "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz", 828 - "integrity": "sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==", 826 + "version": "4.2.2", 827 + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.2.tgz", 828 + "integrity": "sha512-iJS+8kAFZ8HPqnh0O5DHCLjo4L6dD97DBQEkrhfSO4V96xeefUus2jqsBs1dUMt3OU9Ks4qIkiY0mpL5UW+4LQ==", 829 829 "dev": true, 830 830 "license": "MIT", 831 831 "dependencies": { 832 832 "@parcel/watcher": "^2.5.1", 833 - "@tailwindcss/node": "4.2.1", 834 - "@tailwindcss/oxide": "4.2.1", 833 + "@tailwindcss/node": "4.2.2", 834 + "@tailwindcss/oxide": "4.2.2", 835 835 "enhanced-resolve": "^5.19.0", 836 836 "mri": "^1.2.0", 837 837 "picocolors": "^1.1.1", 838 - "tailwindcss": "4.2.1" 838 + "tailwindcss": "4.2.2" 839 839 }, 840 840 "bin": { 841 841 "tailwindcss": "dist/index.mjs" 842 842 } 843 843 }, 844 844 "node_modules/@tailwindcss/node": { 845 - "version": "4.2.1", 846 - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", 847 - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", 845 + "version": "4.2.2", 846 + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", 847 + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", 848 848 "dev": true, 849 849 "license": "MIT", 850 850 "dependencies": { 851 851 "@jridgewell/remapping": "^2.3.5", 852 852 "enhanced-resolve": "^5.19.0", 853 853 "jiti": "^2.6.1", 854 - "lightningcss": "1.31.1", 854 + "lightningcss": "1.32.0", 855 855 "magic-string": "^0.30.21", 856 856 "source-map-js": "^1.2.1", 857 - "tailwindcss": "4.2.1" 857 + "tailwindcss": "4.2.2" 858 858 } 859 859 }, 860 860 "node_modules/@tailwindcss/oxide": { 861 - "version": "4.2.1", 862 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", 863 - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", 861 + "version": "4.2.2", 862 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", 863 + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", 864 864 "dev": true, 865 865 "license": "MIT", 866 866 "engines": { 867 867 "node": ">= 20" 868 868 }, 869 869 "optionalDependencies": { 870 - "@tailwindcss/oxide-android-arm64": "4.2.1", 871 - "@tailwindcss/oxide-darwin-arm64": "4.2.1", 872 - "@tailwindcss/oxide-darwin-x64": "4.2.1", 873 - "@tailwindcss/oxide-freebsd-x64": "4.2.1", 874 - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", 875 - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", 876 - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", 877 - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", 878 - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", 879 - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", 880 - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", 881 - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" 870 + "@tailwindcss/oxide-android-arm64": "4.2.2", 871 + "@tailwindcss/oxide-darwin-arm64": "4.2.2", 872 + "@tailwindcss/oxide-darwin-x64": "4.2.2", 873 + "@tailwindcss/oxide-freebsd-x64": "4.2.2", 874 + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", 875 + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", 876 + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", 877 + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", 878 + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", 879 + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", 880 + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", 881 + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" 882 882 } 883 883 }, 884 884 "node_modules/@tailwindcss/oxide-android-arm64": { 885 - "version": "4.2.1", 886 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", 887 - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", 885 + "version": "4.2.2", 886 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", 887 + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", 888 888 "cpu": [ 889 889 "arm64" 890 890 ], ··· 899 899 } 900 900 }, 901 901 "node_modules/@tailwindcss/oxide-darwin-arm64": { 902 - "version": "4.2.1", 903 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", 904 - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", 902 + "version": "4.2.2", 903 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", 904 + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", 905 905 "cpu": [ 906 906 "arm64" 907 907 ], ··· 916 916 } 917 917 }, 918 918 "node_modules/@tailwindcss/oxide-darwin-x64": { 919 - "version": "4.2.1", 920 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", 921 - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", 919 + "version": "4.2.2", 920 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", 921 + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", 922 922 "cpu": [ 923 923 "x64" 924 924 ], ··· 933 933 } 934 934 }, 935 935 "node_modules/@tailwindcss/oxide-freebsd-x64": { 936 - "version": "4.2.1", 937 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", 938 - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", 936 + "version": "4.2.2", 937 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", 938 + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", 939 939 "cpu": [ 940 940 "x64" 941 941 ], ··· 950 950 } 951 951 }, 952 952 "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { 953 - "version": "4.2.1", 954 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", 955 - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", 953 + "version": "4.2.2", 954 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", 955 + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", 956 956 "cpu": [ 957 957 "arm" 958 958 ], ··· 967 967 } 968 968 }, 969 969 "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { 970 - "version": "4.2.1", 971 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", 972 - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", 970 + "version": "4.2.2", 971 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", 972 + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", 973 973 "cpu": [ 974 974 "arm64" 975 975 ], ··· 984 984 } 985 985 }, 986 986 "node_modules/@tailwindcss/oxide-linux-arm64-musl": { 987 - "version": "4.2.1", 988 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", 989 - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", 987 + "version": "4.2.2", 988 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", 989 + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", 990 990 "cpu": [ 991 991 "arm64" 992 992 ], ··· 1001 1001 } 1002 1002 }, 1003 1003 "node_modules/@tailwindcss/oxide-linux-x64-gnu": { 1004 - "version": "4.2.1", 1005 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", 1006 - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", 1004 + "version": "4.2.2", 1005 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", 1006 + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", 1007 1007 "cpu": [ 1008 1008 "x64" 1009 1009 ], ··· 1018 1018 } 1019 1019 }, 1020 1020 "node_modules/@tailwindcss/oxide-linux-x64-musl": { 1021 - "version": "4.2.1", 1022 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", 1023 - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", 1021 + "version": "4.2.2", 1022 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", 1023 + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", 1024 1024 "cpu": [ 1025 1025 "x64" 1026 1026 ], ··· 1035 1035 } 1036 1036 }, 1037 1037 "node_modules/@tailwindcss/oxide-wasm32-wasi": { 1038 - "version": "4.2.1", 1039 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", 1040 - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", 1038 + "version": "4.2.2", 1039 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", 1040 + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", 1041 1041 "bundleDependencies": [ 1042 1042 "@napi-rs/wasm-runtime", 1043 1043 "@emnapi/core", ··· 1065 1065 } 1066 1066 }, 1067 1067 "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { 1068 - "version": "4.2.1", 1069 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", 1070 - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", 1068 + "version": "4.2.2", 1069 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", 1070 + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", 1071 1071 "cpu": [ 1072 1072 "arm64" 1073 1073 ], ··· 1082 1082 } 1083 1083 }, 1084 1084 "node_modules/@tailwindcss/oxide-win32-x64-msvc": { 1085 - "version": "4.2.1", 1086 - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", 1087 - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", 1085 + "version": "4.2.2", 1086 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", 1087 + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", 1088 1088 "cpu": [ 1089 1089 "x64" 1090 1090 ], ··· 1168 1168 } 1169 1169 }, 1170 1170 "node_modules/enhanced-resolve": { 1171 - "version": "5.20.0", 1172 - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", 1173 - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", 1171 + "version": "5.20.1", 1172 + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", 1173 + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", 1174 1174 "dev": true, 1175 1175 "license": "MIT", 1176 1176 "dependencies": { ··· 1182 1182 } 1183 1183 }, 1184 1184 "node_modules/esbuild": { 1185 - "version": "0.27.4", 1186 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", 1187 - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", 1185 + "version": "0.28.0", 1186 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", 1187 + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", 1188 1188 "dev": true, 1189 1189 "hasInstallScript": true, 1190 1190 "license": "MIT", ··· 1195 1195 "node": ">=18" 1196 1196 }, 1197 1197 "optionalDependencies": { 1198 - "@esbuild/aix-ppc64": "0.27.4", 1199 - "@esbuild/android-arm": "0.27.4", 1200 - "@esbuild/android-arm64": "0.27.4", 1201 - "@esbuild/android-x64": "0.27.4", 1202 - "@esbuild/darwin-arm64": "0.27.4", 1203 - "@esbuild/darwin-x64": "0.27.4", 1204 - "@esbuild/freebsd-arm64": "0.27.4", 1205 - "@esbuild/freebsd-x64": "0.27.4", 1206 - "@esbuild/linux-arm": "0.27.4", 1207 - "@esbuild/linux-arm64": "0.27.4", 1208 - "@esbuild/linux-ia32": "0.27.4", 1209 - "@esbuild/linux-loong64": "0.27.4", 1210 - "@esbuild/linux-mips64el": "0.27.4", 1211 - "@esbuild/linux-ppc64": "0.27.4", 1212 - "@esbuild/linux-riscv64": "0.27.4", 1213 - "@esbuild/linux-s390x": "0.27.4", 1214 - "@esbuild/linux-x64": "0.27.4", 1215 - "@esbuild/netbsd-arm64": "0.27.4", 1216 - "@esbuild/netbsd-x64": "0.27.4", 1217 - "@esbuild/openbsd-arm64": "0.27.4", 1218 - "@esbuild/openbsd-x64": "0.27.4", 1219 - "@esbuild/openharmony-arm64": "0.27.4", 1220 - "@esbuild/sunos-x64": "0.27.4", 1221 - "@esbuild/win32-arm64": "0.27.4", 1222 - "@esbuild/win32-ia32": "0.27.4", 1223 - "@esbuild/win32-x64": "0.27.4" 1198 + "@esbuild/aix-ppc64": "0.28.0", 1199 + "@esbuild/android-arm": "0.28.0", 1200 + "@esbuild/android-arm64": "0.28.0", 1201 + "@esbuild/android-x64": "0.28.0", 1202 + "@esbuild/darwin-arm64": "0.28.0", 1203 + "@esbuild/darwin-x64": "0.28.0", 1204 + "@esbuild/freebsd-arm64": "0.28.0", 1205 + "@esbuild/freebsd-x64": "0.28.0", 1206 + "@esbuild/linux-arm": "0.28.0", 1207 + "@esbuild/linux-arm64": "0.28.0", 1208 + "@esbuild/linux-ia32": "0.28.0", 1209 + "@esbuild/linux-loong64": "0.28.0", 1210 + "@esbuild/linux-mips64el": "0.28.0", 1211 + "@esbuild/linux-ppc64": "0.28.0", 1212 + "@esbuild/linux-riscv64": "0.28.0", 1213 + "@esbuild/linux-s390x": "0.28.0", 1214 + "@esbuild/linux-x64": "0.28.0", 1215 + "@esbuild/netbsd-arm64": "0.28.0", 1216 + "@esbuild/netbsd-x64": "0.28.0", 1217 + "@esbuild/openbsd-arm64": "0.28.0", 1218 + "@esbuild/openbsd-x64": "0.28.0", 1219 + "@esbuild/openharmony-arm64": "0.28.0", 1220 + "@esbuild/sunos-x64": "0.28.0", 1221 + "@esbuild/win32-arm64": "0.28.0", 1222 + "@esbuild/win32-ia32": "0.28.0", 1223 + "@esbuild/win32-x64": "0.28.0" 1224 1224 } 1225 1225 }, 1226 1226 "node_modules/glob": { ··· 1296 1296 } 1297 1297 }, 1298 1298 "node_modules/lightningcss": { 1299 - "version": "1.31.1", 1300 - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", 1301 - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", 1299 + "version": "1.32.0", 1300 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", 1301 + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", 1302 1302 "dev": true, 1303 1303 "license": "MPL-2.0", 1304 1304 "dependencies": { ··· 1312 1312 "url": "https://opencollective.com/parcel" 1313 1313 }, 1314 1314 "optionalDependencies": { 1315 - "lightningcss-android-arm64": "1.31.1", 1316 - "lightningcss-darwin-arm64": "1.31.1", 1317 - "lightningcss-darwin-x64": "1.31.1", 1318 - "lightningcss-freebsd-x64": "1.31.1", 1319 - "lightningcss-linux-arm-gnueabihf": "1.31.1", 1320 - "lightningcss-linux-arm64-gnu": "1.31.1", 1321 - "lightningcss-linux-arm64-musl": "1.31.1", 1322 - "lightningcss-linux-x64-gnu": "1.31.1", 1323 - "lightningcss-linux-x64-musl": "1.31.1", 1324 - "lightningcss-win32-arm64-msvc": "1.31.1", 1325 - "lightningcss-win32-x64-msvc": "1.31.1" 1315 + "lightningcss-android-arm64": "1.32.0", 1316 + "lightningcss-darwin-arm64": "1.32.0", 1317 + "lightningcss-darwin-x64": "1.32.0", 1318 + "lightningcss-freebsd-x64": "1.32.0", 1319 + "lightningcss-linux-arm-gnueabihf": "1.32.0", 1320 + "lightningcss-linux-arm64-gnu": "1.32.0", 1321 + "lightningcss-linux-arm64-musl": "1.32.0", 1322 + "lightningcss-linux-x64-gnu": "1.32.0", 1323 + "lightningcss-linux-x64-musl": "1.32.0", 1324 + "lightningcss-win32-arm64-msvc": "1.32.0", 1325 + "lightningcss-win32-x64-msvc": "1.32.0" 1326 1326 } 1327 1327 }, 1328 1328 "node_modules/lightningcss-android-arm64": { 1329 - "version": "1.31.1", 1330 - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", 1331 - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", 1329 + "version": "1.32.0", 1330 + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", 1331 + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", 1332 1332 "cpu": [ 1333 1333 "arm64" 1334 1334 ], ··· 1347 1347 } 1348 1348 }, 1349 1349 "node_modules/lightningcss-darwin-arm64": { 1350 - "version": "1.31.1", 1351 - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", 1352 - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", 1350 + "version": "1.32.0", 1351 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", 1352 + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", 1353 1353 "cpu": [ 1354 1354 "arm64" 1355 1355 ], ··· 1368 1368 } 1369 1369 }, 1370 1370 "node_modules/lightningcss-darwin-x64": { 1371 - "version": "1.31.1", 1372 - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", 1373 - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", 1371 + "version": "1.32.0", 1372 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", 1373 + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", 1374 1374 "cpu": [ 1375 1375 "x64" 1376 1376 ], ··· 1389 1389 } 1390 1390 }, 1391 1391 "node_modules/lightningcss-freebsd-x64": { 1392 - "version": "1.31.1", 1393 - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", 1394 - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", 1392 + "version": "1.32.0", 1393 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", 1394 + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", 1395 1395 "cpu": [ 1396 1396 "x64" 1397 1397 ], ··· 1410 1410 } 1411 1411 }, 1412 1412 "node_modules/lightningcss-linux-arm-gnueabihf": { 1413 - "version": "1.31.1", 1414 - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", 1415 - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", 1413 + "version": "1.32.0", 1414 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", 1415 + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", 1416 1416 "cpu": [ 1417 1417 "arm" 1418 1418 ], ··· 1431 1431 } 1432 1432 }, 1433 1433 "node_modules/lightningcss-linux-arm64-gnu": { 1434 - "version": "1.31.1", 1435 - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", 1436 - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", 1434 + "version": "1.32.0", 1435 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", 1436 + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", 1437 1437 "cpu": [ 1438 1438 "arm64" 1439 1439 ], ··· 1452 1452 } 1453 1453 }, 1454 1454 "node_modules/lightningcss-linux-arm64-musl": { 1455 - "version": "1.31.1", 1456 - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", 1457 - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", 1455 + "version": "1.32.0", 1456 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", 1457 + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", 1458 1458 "cpu": [ 1459 1459 "arm64" 1460 1460 ], ··· 1473 1473 } 1474 1474 }, 1475 1475 "node_modules/lightningcss-linux-x64-gnu": { 1476 - "version": "1.31.1", 1477 - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", 1478 - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", 1476 + "version": "1.32.0", 1477 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", 1478 + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", 1479 1479 "cpu": [ 1480 1480 "x64" 1481 1481 ], ··· 1494 1494 } 1495 1495 }, 1496 1496 "node_modules/lightningcss-linux-x64-musl": { 1497 - "version": "1.31.1", 1498 - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", 1499 - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", 1497 + "version": "1.32.0", 1498 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", 1499 + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", 1500 1500 "cpu": [ 1501 1501 "x64" 1502 1502 ], ··· 1515 1515 } 1516 1516 }, 1517 1517 "node_modules/lightningcss-win32-arm64-msvc": { 1518 - "version": "1.31.1", 1519 - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", 1520 - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", 1518 + "version": "1.32.0", 1519 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", 1520 + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", 1521 1521 "cpu": [ 1522 1522 "arm64" 1523 1523 ], ··· 1536 1536 } 1537 1537 }, 1538 1538 "node_modules/lightningcss-win32-x64-msvc": { 1539 - "version": "1.31.1", 1540 - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", 1541 - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", 1539 + "version": "1.32.0", 1540 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", 1541 + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", 1542 1542 "cpu": [ 1543 1543 "x64" 1544 1544 ], ··· 1567 1567 } 1568 1568 }, 1569 1569 "node_modules/lucide": { 1570 - "version": "0.577.0", 1571 - "resolved": "https://registry.npmjs.org/lucide/-/lucide-0.577.0.tgz", 1572 - "integrity": "sha512-PpC/m5eOItp/WU/GlQPFBXDOhq6HibL73KzYP37OX3LM7VmzWQF8voEj8QRWUFvy9FIKfeDQkWYoyS1D/MdWFA==", 1570 + "version": "1.8.0", 1571 + "resolved": "https://registry.npmjs.org/lucide/-/lucide-1.8.0.tgz", 1572 + "integrity": "sha512-JjV/QnadgFLj1Pyu9IKl0lknrolFEzo04B64QcYLLeRzZl/iEHpdbSrRRKbyXcv45SZNv+WGjIUCT33e7xHO6Q==", 1573 1573 "license": "ISC" 1574 1574 }, 1575 1575 "node_modules/magic-string": { ··· 1687 1687 } 1688 1688 }, 1689 1689 "node_modules/tailwindcss": { 1690 - "version": "4.2.1", 1691 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", 1692 - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", 1690 + "version": "4.2.2", 1691 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", 1692 + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", 1693 1693 "dev": true, 1694 1694 "license": "MIT" 1695 1695 }, 1696 1696 "node_modules/tapable": { 1697 - "version": "2.3.0", 1698 - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", 1699 - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", 1697 + "version": "2.3.2", 1698 + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", 1699 + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", 1700 1700 "dev": true, 1701 1701 "license": "MIT", 1702 1702 "engines": {
+8 -8
package.json
··· 4 4 "private": true, 5 5 "scripts": { 6 6 "icons:build": "node scripts/generate-icons.js", 7 - "css:build": "BROWSERSLIST_IGNORE_OLD_DATA=1 npx tailwindcss -i ./pkg/appview/src/css/main.css -o ./pkg/appview/public/css/style.css --minify", 8 - "css:build:hold": "BROWSERSLIST_IGNORE_OLD_DATA=1 npx tailwindcss -i ./pkg/hold/admin/src/css/main.css -o ./pkg/hold/admin/public/css/style.css --minify", 9 - "css:watch": "BROWSERSLIST_IGNORE_OLD_DATA=1 npx tailwindcss -i ./pkg/appview/src/css/main.css -o ./pkg/appview/public/css/style.css --watch", 7 + "css:build": "npx tailwindcss -i ./pkg/appview/src/css/main.css -o ./pkg/appview/public/css/style.css --minify", 8 + "css:build:hold": "npx tailwindcss -i ./pkg/hold/admin/src/css/main.css -o ./pkg/hold/admin/public/css/style.css --minify", 9 + "css:watch": "npx tailwindcss -i ./pkg/appview/src/css/main.css -o ./pkg/appview/public/css/style.css --watch", 10 10 "js:build": "esbuild pkg/appview/src/js/main.js --bundle --minify --format=esm --outfile=pkg/appview/public/js/bundle.min.js", 11 11 "js:build:hold": "esbuild pkg/hold/admin/src/js/main.js --bundle --minify --format=esm --outfile=pkg/hold/admin/public/js/bundle.min.js", 12 12 "js:watch": "esbuild pkg/appview/src/js/main.js --bundle --watch --format=esm --outfile=pkg/appview/public/js/bundle.min.js", ··· 16 16 "watch": "npm run css:watch & npm run js:watch" 17 17 }, 18 18 "devDependencies": { 19 - "@tailwindcss/cli": "^4.2.1", 19 + "@tailwindcss/cli": "^4.2.2", 20 20 "@tailwindcss/typography": "^0.5.19", 21 21 "daisyui": "^5.5.19", 22 - "esbuild": "^0.27.4", 22 + "esbuild": "^0.28.0", 23 23 "glob": "^13.0.6", 24 - "tailwindcss": "^4.2" 24 + "lucide": "^1.8.0", 25 + "tailwindcss": "^4.2.2" 25 26 }, 26 27 "dependencies": { 27 28 "htmx-ext-json-enc": "^2.0.3", 28 - "htmx.org": "^2.0.8", 29 - "lucide": "^0.577.0" 29 + "htmx.org": "^2.0.8" 30 30 } 31 31 }
+7 -7
pkg/appview/handlers/scan_result_test.go
··· 95 95 if !strings.Contains(body, "vuln-box-critical") { 96 96 t.Error("Expected body to contain vuln-box-critical for critical vulnerabilities") 97 97 } 98 - if !strings.Contains(body, `data-tip="Critical">2<`) { 98 + if !strings.Contains(body, `aria-label="2 critical">2<`) { 99 99 t.Error("Expected critical count of 2") 100 100 } 101 101 if !strings.Contains(body, "vuln-box-high") { 102 102 t.Error("Expected body to contain vuln-box-high for high vulnerabilities") 103 103 } 104 - if !strings.Contains(body, `data-tip="High">5<`) { 104 + if !strings.Contains(body, `aria-label="5 high">5<`) { 105 105 t.Error("Expected high count of 5") 106 106 } 107 - if !strings.Contains(body, `data-tip="Medium">10<`) { 107 + if !strings.Contains(body, `aria-label="10 medium">10<`) { 108 108 t.Error("Expected medium count of 10") 109 109 } 110 - if !strings.Contains(body, `data-tip="Low">3<`) { 110 + if !strings.Contains(body, `aria-label="3 low">3<`) { 111 111 t.Error("Expected low count of 3") 112 112 } 113 113 // Should show vulnerability strip with tooltip ··· 274 274 275 275 body := rr.Body.String() 276 276 277 - if !strings.Contains(body, `data-tip="Critical">3<`) { 277 + if !strings.Contains(body, `aria-label="3 critical">3<`) { 278 278 t.Error("Expected critical count of 3") 279 279 } 280 280 // Zero-count badges should NOT appear ··· 353 353 } 354 354 355 355 // abc123 should have vulnerability badges 356 - if !strings.Contains(body, `data-tip="Critical">2<`) { 356 + if !strings.Contains(body, `aria-label="2 critical">2<`) { 357 357 t.Error("Expected critical count of 2 for abc123") 358 358 } 359 359 // def456 should have clean badge ··· 496 496 if !strings.Contains(body, `id="scan-badge-abc123"`) { 497 497 t.Error("Expected OOB span for abc123") 498 498 } 499 - if !strings.Contains(body, `data-tip="Critical">1<`) { 499 + if !strings.Contains(body, `aria-label="1 critical">1<`) { 500 500 t.Error("Expected critical count of 1") 501 501 } 502 502 }
+3 -3
pkg/appview/handlers/vuln_details_test.go
··· 223 223 } 224 224 225 225 // Should contain "-" placeholder for unfixed vuln 226 - if !strings.Contains(body, `opacity-40`) { 227 - t.Error("Expected body to contain opacity-40 placeholder for unfixed vulnerability") 226 + if !strings.Contains(body, `text-base-content/40`) { 227 + t.Error("Expected body to contain dimmed placeholder for unfixed vulnerability") 228 228 } 229 229 230 230 // Should contain a table ··· 256 256 if !strings.Contains(body, "vuln-box-critical") { 257 257 t.Error("Expected body to contain vuln-box-critical in summary") 258 258 } 259 - if !strings.Contains(body, `data-tip="Critical">2<`) { 259 + if !strings.Contains(body, `aria-label="2 critical">2<`) { 260 260 t.Error("Expected critical count of 2 in summary") 261 261 } 262 262
-1
pkg/appview/public/icons.svg
··· 30 30 <symbol id="fingerprint" viewBox="0 0 24 24"><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M2 12a10 10 0 0 1 18-6"/><path d="M2 16h.01"/><path d="M21.8 16c.2-2 .131-5.354 0-6"/><path d="M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2"/><path d="M8.65 22c.21-.66.45-1.32.57-2"/><path d="M9 6.8a6 6 0 0 1 9 5.2v2"/></symbol> 31 31 <symbol id="git-compare" viewBox="0 0 24 24"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/></symbol> 32 32 <symbol id="git-merge" viewBox="0 0 24 24"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></symbol> 33 - <symbol id="github" viewBox="0 0 24 24"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></symbol> 34 33 <symbol id="hard-drive" viewBox="0 0 24 24"><path d="M10 16h.01"/><path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><path d="M21.946 12.013H2.054"/><path d="M6 16h.01"/></symbol> 35 34 <symbol id="heading" viewBox="0 0 24 24"><path d="M6 12h12"/><path d="M6 20V4"/><path d="M18 20V4"/></symbol> 36 35 <symbol id="history" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></symbol>
+76 -2
pkg/appview/public/js/bundle.min.js
··· 1 - var ce=(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,n){let r=getAttributeValue(t,n),o=getAttributeValue(t,"hx-disinherit");var s=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return s&&(s==="*"||s.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return"unset"}return r}function getClosestAttributeValue(e,t){let n=null;if(getClosestMatch(e,function(r){return!!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}),n!=="unset")return n}function matches(e,t){return e instanceof Element&&e.matches(t)}function getStartTag(e){let n=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return n?n[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(n){t.setAttribute(n.name,n.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 n=duplicateScript(t),r=t.parentNode;try{r.insertBefore(n,t)}catch(o){logError(o)}finally{t.remove()}}})}function makeFragment(e){let t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,""),n=getStartTag(t),r;if(n==="html"){r=new DocumentFragment;let s=parseHTML(e);takeChildrenFor(r,s.body),r.title=s.title}else if(n==="body"){r=new DocumentFragment;let s=parseHTML(t);takeChildrenFor(r,s.body),r.title=s.title}else{let s=parseHTML('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=s.querySelector("template").content,r.title=s.title;var o=r.querySelector("title");o&&o.parentNode===r&&(o.remove(),r.title=o.innerText)}return r&&(htmx.config.allowScriptTags?normalizeScriptTags(r):r.querySelectorAll("script").forEach(s=>s.remove())),r}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",n=e[t];return n||(n=e[t]={}),n}function toArray(e){let t=[];if(e)for(let n=0;n<e.length;n++)t.push(e[n]);return t}function forEach(e,t){if(e)for(let n=0;n<e.length;n++)t(e[n])}function isScrolledIntoView(e){let t=e.getBoundingClientRect(),n=t.top,r=t.bottom;return n<window.innerHeight&&r>=0}function bodyContains(e){return e.getRootNode({composed:!0})===document}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);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(n){e(n.detail.elt)})}function logAll(){htmx.logger=function(e,t,n){console&&console.log(t,e,n)}}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,n){e=asElement(resolveTarget(e)),e&&(n?getWindow().setTimeout(function(){addClassToElement(e,t),e=null},n):e.classList&&e.classList.add(t))}function removeClassFromElement(e,t,n){let r=asElement(resolveTarget(e));r&&(n?getWindow().setTimeout(function(){removeClassFromElement(r,t),r=null},n):r.classList&&(r.classList.remove(t),r.classList.length===0&&r.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(n){removeClassFromElement(n,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,n){if(t.indexOf("global ")===0)return querySelectorAllExt(e,t.slice(7),!0);e=resolveTarget(e);let r=[];{let i=0,a=0;for(let l=0;l<t.length;l++){let c=t[l];if(c===","&&i===0){r.push(t.substring(a,l)),a=l+1;continue}c==="<"?i++:c==="/"&&l<t.length-1&&t[l+1]===">"&&i--}a<t.length&&r.push(t.substring(a))}let o=[],s=[];for(;r.length>0;){let i=normalizeSelector(r.shift()),a;i.indexOf("closest ")===0?a=closest(asElement(e),normalizeSelector(i.slice(8))):i.indexOf("find ")===0?a=find(asParentNode(e),normalizeSelector(i.slice(5))):i==="next"||i==="nextElementSibling"?a=asElement(e).nextElementSibling:i.indexOf("next ")===0?a=scanForwardQuery(e,normalizeSelector(i.slice(5)),!!n):i==="previous"||i==="previousElementSibling"?a=asElement(e).previousElementSibling:i.indexOf("previous ")===0?a=scanBackwardsQuery(e,normalizeSelector(i.slice(9)),!!n):i==="document"?a=document:i==="window"?a=window:i==="body"?a=document.body:i==="root"?a=getRootNode(e,!!n):i==="host"?a=e.getRootNode().host:s.push(i),a&&o.push(a)}if(s.length>0){let i=s.join(","),a=asParentNode(getRootNode(e,!!n));o.push(...toArray(a.querySelectorAll(i)))}return o}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o<r.length;o++){let s=r[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return s}},scanBackwardsQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=r.length-1;o>=0;o--){let s=r[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,n,r){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t,options:n}:{target:resolveTarget(e),event:asString(t),listener:n,options:r}}function addEventListenerImpl(e,t,n,r){return ready(function(){let s=processEventArgs(e,t,n,r);s.target.addEventListener(s.event,s.listener,s.options)}),isFunction(t)?t:n}function removeEventListenerImpl(e,t,n){return ready(function(){let r=processEventArgs(e,t,n);r.target.removeEventListener(r.event,r.listener)}),isFunction(t)?t:n}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let n=getClosestAttributeValue(e,t);if(n){if(n==="this")return[findThisElement(e,t)];{let r=querySelectorAllExt(e,n);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(n)){let s=asElement(getClosestMatch(e,function(i){return i!==e&&hasAttribute(asElement(i),t)}));s&&r.push(...findAttributeTargets(s,t))}return r.length===0?(logError('The selector "'+n+'" on '+t+" returned no matches!"),[DUMMY_ELT]):r}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(n){return getAttributeValue(asElement(n),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(n){!t.hasAttribute(n.name)&&shouldSettleAttribute(n.name)&&e.removeAttribute(n.name)}),forEach(t.attributes,function(n){shouldSettleAttribute(n.name)&&e.setAttribute(n.name,n.value)})}function isInlineSwap(e,t){let n=getExtensions(t);for(let r=0;r<n.length;r++){let o=n[r];try{if(o.isInlineSwap(e))return!0}catch(s){logError(s)}}return e==="outerHTML"}function oobSwap(e,t,n,r){r=r||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 i=querySelectorAllExt(r,o,!1);return i.length?(forEach(i,function(a){let l,c=t.cloneNode(!0);l=getDocument().createDocumentFragment(),l.appendChild(c),isInlineSwap(s,a)||(l=asParentNode(c));let d={shouldSwap:!0,target:a,fragment:l};triggerEvent(a,"htmx:oobBeforeSwap",d)&&(a=d.target,d.shouldSwap&&(handlePreservedElements(l),swapWithStyle(s,a,a,l,n),restorePreservedElements()),forEach(n.elts,function(u){triggerEvent(u,"htmx:oobAfterSwap",d)}))}),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 n=find("#"+t.id);n.parentNode.moveBefore(t,n),n.remove()}e.remove()}}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let n=getAttributeValue(t,"id"),r=getDocument().getElementById(n);if(r!=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(r,null)}else t.parentNode.replaceChild(r,t)})}function handleAttributes(e,t,n){forEach(t.querySelectorAll("[id]"),function(r){let o=getRawAttribute(r,"id");if(o&&o.length>0){let s=o.replace("'","\\'"),i=r.tagName.replace(":","\\:"),a=asParentNode(e),l=a&&a.querySelector(i+"[id='"+s+"']");if(l&&l!==a){let c=r.cloneNode();cloneAttributes(r,l),n.tasks.push(function(){cloneAttributes(r,c)})}}})}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]",n=asHtmlElement(matches(e,t)?e:e.querySelector(t));n?.focus()}function insertNodesBefore(e,t,n,r){for(handleAttributes(e,n,r);n.childNodes.length>0;){let o=n.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&r.tasks.push(makeAjaxLoadTask(o))}}function stringHash(e,t){let n=0;for(;n<e.length;)t=(t<<5)-t+e.charCodeAt(n++)|0;return t}function attributeHash(e){let t=0;for(let n=0;n<e.attributes.length;n++){let r=e.attributes[n];r.value&&(t=stringHash(r.name,t),t=stringHash(r.value,t))}return t}function deInitOnHandlers(e){let t=getInternalData(e);if(t.onHandlers){for(let n=0;n<t.onHandlers.length;n++){let r=t.onHandlers[n];removeEventListenerImpl(e,r.event,r.listener)}delete t.onHandlers}}function deInitNode(e){let t=getInternalData(e);t.timeout&&clearTimeout(t.timeout),t.listenerInfos&&forEach(t.listenerInfos,function(n){n.on&&removeEventListenerImpl(n.on,n.trigger,n.listener)}),deInitOnHandlers(e),forEach(Object.keys(t),function(n){n!=="firstInitCompleted"&&delete t[n]})}function cleanUpElement(e){triggerEvent(e,"htmx:beforeCleanupElement"),deInitNode(e),forEach(e.children,function(t){cleanUpElement(t)})}function swapOuterHTML(e,t,n){if(e.tagName==="BODY")return swapInnerHTML(e,t,n);let r,o=e.previousSibling,s=parentElt(e);if(s){for(insertNodesBefore(s,e,t,n),o==null?r=s.firstChild:r=o.nextSibling,n.elts=n.elts.filter(function(i){return i!==e});r&&r!==e;)r instanceof Element&&n.elts.push(r),r=r.nextSibling;cleanUpElement(e),e.remove()}}function swapAfterBegin(e,t,n){return insertNodesBefore(e,e.firstChild,t,n)}function swapBeforeBegin(e,t,n){return insertNodesBefore(parentElt(e),e,t,n)}function swapBeforeEnd(e,t,n){return insertNodesBefore(e,null,t,n)}function swapAfterEnd(e,t,n){return insertNodesBefore(parentElt(e),e.nextSibling,t,n)}function swapDelete(e){cleanUpElement(e);let t=parentElt(e);if(t)return t.removeChild(e)}function swapInnerHTML(e,t,n){let r=e.firstChild;if(insertNodesBefore(e,r,t,n),r){for(;r.nextSibling;)cleanUpElement(r.nextSibling),e.removeChild(r.nextSibling);cleanUpElement(r),e.removeChild(r)}}function swapWithStyle(e,t,n,r,o){switch(e){case"none":return;case"outerHTML":swapOuterHTML(n,r,o);return;case"afterbegin":swapAfterBegin(n,r,o);return;case"beforebegin":swapBeforeBegin(n,r,o);return;case"beforeend":swapBeforeEnd(n,r,o);return;case"afterend":swapAfterEnd(n,r,o);return;case"delete":swapDelete(n);return;default:var s=getExtensions(t);for(let i=0;i<s.length;i++){let a=s[i];try{let l=a.handleSwap(e,n,r,o);if(l){if(Array.isArray(l))for(let c=0;c<l.length;c++){let d=l[c];d.nodeType!==Node.TEXT_NODE&&d.nodeType!==Node.COMMENT_NODE&&o.tasks.push(makeAjaxLoadTask(d))}return}}catch(l){logError(l)}}e==="innerHTML"?swapInnerHTML(n,r,o):swapWithStyle(htmx.config.defaultSwapStyle,t,n,r,o)}}function findAndSwapOobElements(e,t,n){var r=findAll(e,"[hx-swap-oob], [data-hx-swap-oob]");return forEach(r,function(o){if(htmx.config.allowNestedOobSwaps||o.parentElement===null){let s=getAttributeValue(o,"hx-swap-oob");s!=null&&oobSwap(s,o,t,n)}else o.removeAttribute("hx-swap-oob"),o.removeAttribute("data-hx-swap-oob")}),r.length>0}function swap(e,t,n,r){r||(r={});let o=null,s=null,i=function(){maybeCall(r.beforeSwapCallback),e=resolveTarget(e);let c=r.contextElement?getRootNode(r.contextElement,!1):getDocument(),d=document.activeElement,u={};u={elt:d,start:d?d.selectionStart:null,end:d?d.selectionEnd:null};let f=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else{let h=makeFragment(t);if(f.title=r.title||h.title,r.historyRequest&&(h=h.querySelector("[hx-history-elt],[data-hx-history-elt]")||h),r.selectOOB){let p=r.selectOOB.split(",");for(let g=0;g<p.length;g++){let w=p[g].split(":",2),C=w[0].trim();C.indexOf("#")===0&&(C=C.substring(1));let x=w[1]||"true",E=h.querySelector("#"+C);E&&oobSwap(x,E,f,c)}}if(findAndSwapOobElements(h,f,c),forEach(findAll(h,"template"),function(p){p.content&&findAndSwapOobElements(p.content,f,c)&&p.remove()}),r.select){let p=getDocument().createDocumentFragment();forEach(h.querySelectorAll(r.select),function(g){p.appendChild(g)}),h=p}handlePreservedElements(h),swapWithStyle(n.swapStyle,r.contextElement,e,h,f),restorePreservedElements()}if(u.elt&&!bodyContains(u.elt)&&getRawAttribute(u.elt,"id")){let h=document.getElementById(getRawAttribute(u.elt,"id")),p={preventScroll:n.focusScroll!==void 0?!n.focusScroll:!htmx.config.defaultFocusScroll};if(h){if(u.start&&h.setSelectionRange)try{h.setSelectionRange(u.start,u.end)}catch{}h.focus(p)}}e.classList.remove(htmx.config.swappingClass),forEach(f.elts,function(h){h.classList&&h.classList.add(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSwap",r.eventInfo)}),maybeCall(r.afterSwapCallback),n.ignoreTitle||handleTitle(f.title);let y=function(){if(forEach(f.tasks,function(h){h.call()}),forEach(f.elts,function(h){h.classList&&h.classList.remove(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSettle",r.eventInfo)}),r.anchor){let h=asElement(resolveTarget("#"+r.anchor));h&&h.scrollIntoView({block:"start",behavior:"auto"})}updateScrollState(f.elts,n),maybeCall(r.afterSettleCallback),maybeCall(o)};n.settleDelay>0?getWindow().setTimeout(y,n.settleDelay):y()},a=htmx.config.globalViewTransitions;n.hasOwnProperty("transition")&&(a=n.transition);let l=r.contextElement||getDocument();if(a&&triggerEvent(l,"htmx:beforeTransition",r.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let c=new Promise(function(u,f){o=u,s=f}),d=i;i=function(){document.startViewTransition(function(){return d(),c})}}try{n?.swapDelay&&n.swapDelay>0?getWindow().setTimeout(i,n.swapDelay):i()}catch(c){throw triggerErrorEvent(l,"htmx:swapError",r.eventInfo),maybeCall(s),c}}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let s in o)if(o.hasOwnProperty(s)){let i=o[s];isRawObject(i)?n=i.target!==void 0?i.target:n:i={value:i},triggerEvent(n,s,i)}}else{let o=r.split(",");for(let s=0;s<o.length;s++)triggerEvent(n,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=[],n=0;for(;n<e.length;){if(SYMBOL_START.exec(e.charAt(n))){for(var r=n;SYMBOL_CONT.exec(e.charAt(n+1));)n++;t.push(e.substring(r,n+1))}else if(STRINGISH_START.indexOf(e.charAt(n))!==-1){let o=e.charAt(n);var r=n;for(n++;n<e.length&&e.charAt(n)!==o;)e.charAt(n)==="\\"&&n++,n++;t.push(e.substring(r,n+1))}else{let o=e.charAt(n);t.push(o)}n++}return t}function isPossibleRelativeReference(e,t,n){return SYMBOL_START.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function maybeGenerateConditional(e,t,n){if(t[0]==="["){t.shift();let r=1,o=" return (function("+n+"){ return (",s=null;for(;t.length>0;){let i=t[0];if(i==="]"){if(r--,r===0){s===null&&(o=o+"true"),t.shift(),o+=")})";try{let a=maybeEval(e,function(){return Function(o)()},function(){return!0});return a.source=o,a}catch(a){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:a,source:o}),null}}}else i==="["&&r++;isPossibleRelativeReference(i,s,n)?o+="(("+n+"."+i+") ? ("+n+"."+i+") : (window."+i+"))":o=o+i,s=t.shift()}}}function consumeUntil(e,t){let n="";for(;e.length>0&&!t.test(e[0]);)n+=e.shift();return n}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,n){let r=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let a=o.length,l=consumeUntil(o,/[,\[\s]/);if(l!=="")if(l==="every"){let c={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),c.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var s=maybeGenerateConditional(e,o,"event");s&&(c.eventFilter=s),r.push(c)}else{let c={trigger:l};var s=maybeGenerateConditional(e,o,"event");for(s&&(c.eventFilter=s),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let u=o.shift();if(u==="changed")c.changed=!0;else if(u==="once")c.once=!0;else if(u==="consume")c.consume=!0;else if(u==="delay"&&o[0]===":")o.shift(),c.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(u==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var i=consumeCSSSelector(o);else{var i=consumeUntil(o,WHITESPACE_OR_COMMA);if(i==="closest"||i==="find"||i==="next"||i==="previous"){o.shift();let y=consumeCSSSelector(o);y.length>0&&(i+=" "+y)}}c.from=i}else u==="target"&&o[0]===":"?(o.shift(),c.target=consumeCSSSelector(o)):u==="throttle"&&o[0]===":"?(o.shift(),c.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):u==="queue"&&o[0]===":"?(o.shift(),c.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):u==="root"&&o[0]===":"?(o.shift(),c[u]=consumeCSSSelector(o)):u==="threshold"&&o[0]===":"?(o.shift(),c[u]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}r.push(c)}o.length===a&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r)}return n.length>0?n: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,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n))},n.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,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else{let s=getRawAttribute(e,"method");r=s?s.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),r==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}n.forEach(function(s){addEventListener(e,function(i,a){let l=asElement(i);if(eltIsDisabled(l)){cleanUpElement(l);return}issueAjaxRequest(r,o,l,a)},t,s,!0)})}}function shouldCancel(e,t){if(e.type==="submit"&&t.tagName==="FORM")return!0;if(e.type==="click"){let n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit")return!0;let r=t.closest("a"),o=/^#.+/;if(r&&r.href&&!o.test(r.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,n){let r=e.eventFilter;if(r)try{return r.call(t,n)!==!0}catch(o){let s=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:s}),!0}return!1}function addEventListener(e,t,n,r,o){let s=getInternalData(e),i;r.from?i=querySelectorAllExt(e,r.from):i=[e],r.changed&&("lastValue"in s||(s.lastValue=new WeakMap),i.forEach(function(a){s.lastValue.has(r)||s.lastValue.set(r,new WeakMap),s.lastValue.get(r).set(a,a.value)})),forEach(i,function(a){let l=function(c){if(!bodyContains(e)){a.removeEventListener(r.trigger,l);return}if(ignoreBoostedAnchorCtrlClick(e,c)||((o||shouldCancel(c,a))&&c.preventDefault(),maybeFilterEvent(r,e,c)))return;let d=getInternalData(c);if(d.triggerSpec=r,d.handledFor==null&&(d.handledFor=[]),d.handledFor.indexOf(e)<0){if(d.handledFor.push(e),r.consume&&c.stopPropagation(),r.target&&c.target&&!matches(asElement(c.target),r.target))return;if(r.once){if(s.triggeredOnce)return;s.triggeredOnce=!0}if(r.changed){let u=c.target,f=u.value,y=s.lastValue.get(r);if(y.has(u)&&y.get(u)===f)return;y.set(u,f)}if(s.delayed&&clearTimeout(s.delayed),s.throttle)return;r.throttle>0?s.throttle||(triggerEvent(e,"htmx:trigger"),t(e,c),s.throttle=getWindow().setTimeout(function(){s.throttle=null},r.throttle)):r.delay>0?s.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,c)},r.delay):(triggerEvent(e,"htmx:trigger"),t(e,c))}};n.listenerInfos==null&&(n.listenerInfos=[]),n.listenerInfos.push({trigger:r.trigger,listener:l,on:a}),a.addEventListener(r.trigger,l)})}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,n,r){let o=function(){n.loaded||(n.loaded=!0,triggerEvent(e,"htmx:trigger"),t(e))};r>0?getWindow().setTimeout(o,r):o()}function processVerbs(e,t,n){let r=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let s=getAttributeValue(e,"hx-"+o);r=!0,t.path=s,t.verb=o,n.forEach(function(i){addTriggerHandler(e,i,t,function(a,l){let c=asElement(a);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(o,s,c,l)})})}}),r}function addTriggerHandler(e,t,n,r){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,r,n,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(i){for(let a=0;a<i.length;a++)if(i[a].isIntersecting){triggerEvent(e,"intersect");break}},o).observe(asElement(e)),addEventListener(asElement(e),r,n,t)}else!n.firstInitCompleted&&t.trigger==="load"?maybeFilterEvent(t,e,makeEvent("load",{elt:e}))||loadImmediately(asElement(e),r,n,t.delay):t.pollInterval>0?(n.polling=!0,processPolling(asElement(e),r,t)):addEventListener(e,r,n,t)}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return!1;let n=t.attributes;for(let r=0;r<n.length;r++){let o=n[r].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 n=HX_ON_QUERY.evaluate(e),r=null;for(;r=n.iterateNext();)t.push(asElement(r))}function findHxOnWildcardElements(e){let t=[];if(e instanceof DocumentFragment)for(let n of e.childNodes)processHXOnRoot(n,t);else processHXOnRoot(e,t);return t}function findElementsToProcess(e){if(e.querySelectorAll){let n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]",r=[];for(let s in extensions){let i=extensions[s];if(i.getSelectors){var t=i.getSelectors();t&&r.push(t)}}return e.querySelectorAll(VERB_SELECTOR+n+", form, [type='submit'], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(s=>", "+s).join(""))}else return[]}function maybeSetLastButtonClicked(e){let t=getTargetButton(e.target),n=getRelatedFormData(e);n&&(n.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 n=getRelatedForm(t);if(n)return getInternalData(n)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(e,t,n){let r=getInternalData(e);Array.isArray(r.onHandlers)||(r.onHandlers=[]);let o,s=function(i){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,i))})};e.addEventListener(t,s),r.onHandlers.push({event:t,listener:s})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;t<e.attributes.length;t++){let n=e.attributes[t].name,r=e.attributes[t].value;if(startsWith(n,"hx-on")||startsWith(n,"data-hx-on")){let o=n.indexOf("-on")+3,s=n.slice(o,o+1);if(s==="-"||s===":"){let i=n.slice(o+1);startsWith(i,":")?i="htmx"+i:startsWith(i,"-")?i="htmx:"+i.slice(1):startsWith(i,"htmx-")&&(i="htmx:"+i.slice(5)),addHxOnEventHandler(e,i,r)}}}}function initNode(e){triggerEvent(e,"htmx:beforeProcessNode");let t=getInternalData(e),n=getTriggerSpecs(e);processVerbs(e,t,n)||(getClosestAttributeValue(e,"hx-boost")==="true"?boostElement(e,t,n):hasAttribute(e,"hx-trigger")&&n.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),n=attributeHash(e);return t.initHash!==n?(deInitNode(e),t.initHash=n,!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(n){if(eltIsDisabled(n)){cleanUpElement(n);return}maybeDeInitAndHash(n)&&t.push(n)}),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,n){triggerEvent(e,t,mergeObjects({error:t},n))}function ignoreEventForLogging(e){return e==="htmx:afterProcessNode"}function withExtensions(e,t,n){forEach(getExtensions(e,[],n),function(r){try{t(r)}catch(o){logError(o)}})}function logError(e){console.error(e)}function triggerEvent(e,t,n){e=resolveTarget(e),n==null&&(n={}),n.elt=e;let r=makeEvent(t,n);htmx.logger&&!ignoreEventForLogging(t)&&htmx.logger(e,t,n),n.error&&(logError(n.error),triggerEvent(e,"htmx:error",{errorInfo:n}));let o=e.dispatchEvent(r),s=kebabEventName(t);if(o&&s!==t){let i=makeEvent(s,r.detail);o=o&&e.dispatchEvent(i)}return withExtensions(asElement(e),function(i){o=o&&i.onEvent(t,r)!==!1&&!r.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 n=cleanInnerHtmlForHistory(t),r=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 a=0;a<s.length;a++)if(s[a].url===e){s.splice(a,1);break}let i={url:e,content:n,title:r,scroll:o};for(triggerEvent(getDocument().body,"htmx:historyItemCreated",{item:i,cache:s}),s.push(i);s.length>htmx.config.historyCacheSize;)s.shift();for(;s.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(s));break}catch(a){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:a,cache:s}),s.shift()}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let n=0;n<t.length;n++)if(t[n].url===e)return t[n];return null}function cleanInnerHtmlForHistory(e){let t=htmx.config.requestClass,n=e.cloneNode(!0);return forEach(findAll(n,"."+t),function(r){removeClassFromElement(r,t)}),forEach(findAll(n,"[data-disabled-by-htmx]"),function(r){r.removeAttribute("disabled")}),n.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,n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0},r={path:e,xhr:t,historyElt:getHistoryElement(),swapSpec:n};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?(r.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",r),swap(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:!0}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:r.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",r)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",r)&&t.send()}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll},r={path:e,item:t,historyElt:getHistoryElement(),swapSpec:n};triggerEvent(getDocument().body,"htmx:historyCacheHit",r)&&(swap(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",r))}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(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.classList.add.call(n.classList,htmx.config.requestClass)}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.setAttribute("disabled",""),n.setAttribute("data-disabled-by-htmx","")}),t}function removeRequestIndicators(e,t){forEach(e.concat(t),function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||1)-1}),forEach(e,function(n){getInternalData(n).requestCount===0&&n.classList.remove.call(n.classList,htmx.config.requestClass)}),forEach(t,function(n){getInternalData(n).requestCount===0&&(n.removeAttribute("disabled"),n.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(e,t){for(let n=0;n<e.length;n++)if(e[n].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,n){e!=null&&t!=null&&(Array.isArray(t)?t.forEach(function(r){n.append(e,r)}):n.append(e,t))}function removeValueFromFormData(e,t,n){if(e!=null&&t!=null){let r=n.getAll(e);Array.isArray(t)?r=r.filter(o=>t.indexOf(o)<0):r=r.filter(o=>o!==t),n.delete(e),forEach(r,o=>n.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,n,r,o){if(!(r==null||haveSeenNode(e,r))){if(e.push(r),shouldInclude(r)){let s=getRawAttribute(r,"name");addValueToFormData(s,getValueFromInput(r),t),o&&validateElement(r,n)}r instanceof HTMLFormElement&&(forEach(r.elements,function(s){e.indexOf(s)>=0?removeValueFromFormData(s.name,getValueFromInput(s),t):e.push(s),o&&validateElement(s,n)}),new FormData(r).forEach(function(s,i){s instanceof File&&s.name===""||addValueToFormData(i,s,t)}))}}function validateElement(e,t){let n=e;n.willValidate&&(triggerEvent(n,"htmx:validation:validate"),n.checkValidity()||(triggerEvent(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&htmx.config.reportValidityOfForms&&n.reportValidity(),t.push({elt:n,message:n.validationMessage,validity:n.validity})))}function overrideFormData(e,t){for(let n of t.keys())e.delete(n);return t.forEach(function(n,r){e.append(r,n)}),e}function getInputValues(e,t){let n=[],r=new FormData,o=new FormData,s=[],i=getInternalData(e);i.lastButtonClicked&&!bodyContains(i.lastButtonClicked)&&(i.lastButtonClicked=null);let a=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(i.lastButtonClicked&&(a=a&&i.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,s,getRelatedForm(e),a),processInputValue(n,r,s,e,a),i.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let c=i.lastButtonClicked||e,d=getRawAttribute(c,"name");addValueToFormData(d,c.value,o)}let l=findAttributeTargets(e,"hx-include");return forEach(l,function(c){processInputValue(n,r,s,asElement(c),a),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(d){processInputValue(n,r,s,d,a)})}),overrideFormData(r,o),{errors:s,formData:r,values:formDataProxy(r)}}function appendParam(e,t,n){e!==""&&(e+="&"),String(n)==="[object Object]"&&(n=JSON.stringify(n));let r=encodeURIComponent(n);return e+=encodeURIComponent(t)+"="+r,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(n,r){t=appendParam(t,r,n)}),t}function getHeaders(e,t,n){let r={"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,r),n!==void 0&&(r["HX-Prompt"]=n),getInternalData(e).boosted&&(r["HX-Boosted"]="true"),r}function filterValues(e,t){let n=getClosestAttributeValue(t,"hx-params");if(n){if(n==="none")return new FormData;if(n==="*")return e;if(n.indexOf("not ")===0)return forEach(n.slice(4).split(","),function(r){r=r.trim(),e.delete(r)}),e;{let r=new FormData;return forEach(n.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(s){r.append(o,s)})}),r}}else return e}function isAnchorLink(e){return!!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let n=t||getClosestAttributeValue(e,"hx-swap"),r={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)&&(r.show="top"),n){let i=splitOnWhitespace(n);if(i.length>0)for(let a=0;a<i.length;a++){let l=i[a];if(l.indexOf("swap:")===0)r.swapDelay=parseInterval(l.slice(5));else if(l.indexOf("settle:")===0)r.settleDelay=parseInterval(l.slice(7));else if(l.indexOf("transition:")===0)r.transition=l.slice(11)==="true";else if(l.indexOf("ignoreTitle:")===0)r.ignoreTitle=l.slice(12)==="true";else if(l.indexOf("scroll:")===0){var o=l.slice(7).split(":");let d=o.pop();var s=o.length>0?o.join(":"):null;r.scroll=d,r.scrollTarget=s}else if(l.indexOf("show:")===0){var o=l.slice(5).split(":");let u=o.pop();var s=o.length>0?o.join(":"):null;r.show=u,r.showTarget=s}else if(l.indexOf("focus-scroll:")===0){let c=l.slice(13);r.focusScroll=c=="true"}else a==0?r.swapStyle=l:logError("Unknown modifier in hx-swap: "+l)}}return r}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,n){let r=null;return withExtensions(t,function(o){r==null&&(r=o.encodeParameters(e,n,t))}),r??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(n)):urlEncode(n))}function makeSettleInfo(e){return{tasks:[],elts:[e]}}function updateScrollState(e,t){let n=e[0],r=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(n,t.scrollTarget))),t.scroll==="top"&&(n||o)&&(o=o||n,o.scrollTop=0),t.scroll==="bottom"&&(r||o)&&(o=o||r,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 i=t.showTarget;t.showTarget==="window"&&(i="body"),o=asElement(querySelectorExt(n,i))}t.show==="top"&&(n||o)&&(o=o||n,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(e,t,n,r,o){if(r==null&&(r={}),e==null)return r;let s=getAttributeValue(e,t);if(s){let i=s.trim(),a=n;if(i==="unset")return null;i.indexOf("javascript:")===0?(i=i.slice(11),a=!0):i.indexOf("js:")===0&&(i=i.slice(3),a=!0),i.indexOf("{")!==0&&(i="{"+i+"}");let l;a?l=maybeEval(e,function(){return o?Function("event","return ("+i+")").call(e,o):Function("return ("+i+")").call(e)},{}):l=parseJSON(i);for(let c in l)l.hasOwnProperty(c)&&r[c]==null&&(r[c]=l[c])}return getValuesForElement(asElement(parentElt(e)),t,n,r,o)}function maybeEval(e,t,n){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),n)}function getHXVarsForElement(e,t,n){return getValuesForElement(e,"hx-vars",!0,n,t)}function getHXValsForElement(e,t,n){return getValuesForElement(e,"hx-vals",!1,n,t)}function getExpressionVars(e,t){return mergeObjects(getHXVarsForElement(e,t),getHXValsForElement(e,t))}function safelySetHeaderValue(e,t,n){if(n!==null)try{e.setRequestHeader(t,n)}catch{e.setRequestHeader(t,encodeURIComponent(n)),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,n){if(e=e.toLowerCase(),n){if(n instanceof Element||typeof n=="string")return issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(n)||DUMMY_ELT,returnPromise:!0});{let r=resolveTarget(n.target);return(n.target&&!r||n.source&&!r&&!resolveTarget(n.source))&&(r=DUMMY_ELT),issueAjaxRequest(e,t,resolveTarget(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:r,swapOverride:n.swap,select:n.select,returnPromise:!0,push:n.push,replace:n.replace,selectOOB:n.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,n){let r=new URL(t,location.protocol!=="about:"?location.href:window.origin),s=(location.protocol!=="about:"?location.origin:window.origin)===r.origin;return htmx.config.selfRequestsOnly&&!s?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:r,sameHost:s},n))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let n in e)e.hasOwnProperty(n)&&(e[n]&&typeof e[n].forEach=="function"?e[n].forEach(function(r){t.append(n,r)}):typeof e[n]=="object"&&!(e[n]instanceof Blob)?t.append(n,JSON.stringify(e[n])):t.append(n,e[n]));return t}function formDataArrayProxy(e,t,n){return new Proxy(n,{get:function(r,o){return typeof o=="number"?r[o]:o==="length"?r.length:o==="push"?function(s){r.push(s),e.append(t,s)}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(s){e.append(t,s)})}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,s){return r[o]=s,e.delete(t),r.forEach(function(i){e.append(t,i)}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,n){if(typeof n=="symbol"){let o=Reflect.get(t,n);return typeof o=="function"?function(){return o.apply(e,arguments)}:o}if(n==="toJSON")return()=>Object.fromEntries(e);if(n in t&&typeof t[n]=="function")return function(){return e[n].apply(e,arguments)};let r=e.getAll(n);if(r.length!==0)return r.length===1?r[0]:formDataArrayProxy(t,n,r)},set:function(t,n,r){return typeof n!="string"?!1:(t.delete(n),r&&typeof r.forEach=="function"?r.forEach(function(o){t.append(n,o)}):typeof r=="object"&&!(r instanceof Blob)?t.append(n,JSON.stringify(r)):t.append(n,r),!0)},deleteProperty:function(t,n){return typeof n=="string"&&t.delete(n),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,n){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),n)}})}function issueAjaxRequest(e,t,n,r,o,s){let i=null,a=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var l=new Promise(function(m,v){i=m,a=v});n==null&&(n=getDocument().body);let c=o.handler||handleAjaxResponse,d=o.select||null;if(!bodyContains(n))return maybeCall(i),l;let u=o.targetOverride||asElement(getTarget(n));if(u==null||u==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getClosestAttributeValue(n,"hx-target")}),maybeCall(a),l;let f=getInternalData(n),y=f.lastButtonClicked;if(y){let m=getRawAttribute(y,"formaction");m!=null&&(t=m);let v=getRawAttribute(y,"formmethod");if(v!=null)if(VERBS.includes(v.toLowerCase()))e=v;else return maybeCall(i),l}let h=getClosestAttributeValue(n,"hx-confirm");if(s===void 0&&triggerEvent(n,"htmx:confirm",{target:u,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(A){return issueAjaxRequest(e,t,n,r,o,!!A)},question:h})===!1)return maybeCall(i),l;let p=n,g=getClosestAttributeValue(n,"hx-sync"),w=null,C=!1;if(g){let m=g.split(":"),v=m[0].trim();if(v==="this"?p=findThisElement(n,"hx-sync"):p=asElement(querySelectorExt(n,v)),g=(m[1]||"drop").trim(),f=getInternalData(p),g==="drop"&&f.xhr&&f.abortable!==!0)return maybeCall(i),l;if(g==="abort"){if(f.xhr)return maybeCall(i),l;C=!0}else g==="replace"?triggerEvent(p,"htmx:abort"):g.indexOf("queue")===0&&(w=(g.split(" ")[1]||"last").trim())}if(f.xhr)if(f.abortable)triggerEvent(p,"htmx:abort");else{if(w==null){if(r){let m=getInternalData(r);m&&m.triggerSpec&&m.triggerSpec.queue&&(w=m.triggerSpec.queue)}w==null&&(w="last")}return f.queuedRequests==null&&(f.queuedRequests=[]),w==="first"&&f.queuedRequests.length===0?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):w==="all"?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):w==="last"&&(f.queuedRequests=[],f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)})),maybeCall(i),l}let x=new XMLHttpRequest;f.xhr=x,f.abortable=C;let E=function(){f.xhr=null,f.abortable=!1,f.queuedRequests!=null&&f.queuedRequests.length>0&&f.queuedRequests.shift()()},J=getClosestAttributeValue(n,"hx-prompt");if(J){var F=prompt(J);if(F===null||!triggerEvent(n,"htmx:prompt",{prompt:F,target:u}))return maybeCall(i),E(),l}if(h&&!s&&!confirm(h))return maybeCall(i),E(),l;let H=getHeaders(n,u,F);e!=="get"&&!usesFormData(n)&&(H["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(H=mergeObjects(H,o.headers));let Y=getInputValues(n,e),O=Y.errors,z=Y.formData;o.values&&overrideFormData(z,formDataFromObject(o.values));let ie=formDataFromObject(getExpressionVars(n,r)),q=overrideFormData(z,ie),I=filterValues(q,n);htmx.config.getCacheBusterParam&&e==="get"&&I.set("org.htmx.cache-buster",getRawAttribute(u,"id")||"true"),(t==null||t==="")&&(t=location.href);let k=getValuesForElement(n,"hx-request"),K=getInternalData(n).boosted,L=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,S={boosted:K,useUrlParams:L,formData:I,parameters:formDataProxy(I),unfilteredFormData:q,unfilteredParameters:formDataProxy(q),headers:H,elt:n,target:u,verb:e,errors:O,withCredentials:o.credentials||k.credentials||htmx.config.withCredentials,timeout:o.timeout||k.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",S))return maybeCall(i),E(),l;if(t=S.path,e=S.verb,H=S.headers,I=formDataFromObject(S.parameters),O=S.errors,L=S.useUrlParams,O&&O.length>0)return triggerEvent(n,"htmx:validation:halted",S),maybeCall(i),E(),l;let G=t.split("#"),ae=G[0],_=G[1],T=t;if(L&&(T=ae,!I.keys().next().done&&(T.indexOf("?")<0?T+="?":T+="&",T+=urlEncode(I),_&&(T+="#"+_))),!verifyPath(n,T,S))return triggerErrorEvent(n,"htmx:invalidPath",S),maybeCall(a),E(),l;if(x.open(e.toUpperCase(),T,!0),x.overrideMimeType("text/html"),x.withCredentials=S.withCredentials,x.timeout=S.timeout,!k.noHeaders){for(let m in H)if(H.hasOwnProperty(m)){let v=H[m];safelySetHeaderValue(x,m,v)}}let b={xhr:x,target:u,requestConfig:S,etc:o,boosted:K,select:d,pathInfo:{requestPath:t,finalRequestPath:T,responsePath:null,anchor:_}};if(x.onload=function(){try{let m=hierarchyForElt(n);if(b.pathInfo.responsePath=getPathFromResponse(x),c(n,b),b.keepIndicators!==!0&&removeRequestIndicators(P,N),triggerEvent(n,"htmx:afterRequest",b),triggerEvent(n,"htmx:afterOnLoad",b),!bodyContains(n)){let v=null;for(;m.length>0&&v==null;){let A=m.shift();bodyContains(A)&&(v=A)}v&&(triggerEvent(v,"htmx:afterRequest",b),triggerEvent(v,"htmx:afterOnLoad",b))}maybeCall(i)}catch(m){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:m},b)),m}finally{E()}},x.onerror=function(){removeRequestIndicators(P,N),triggerErrorEvent(n,"htmx:afterRequest",b),triggerErrorEvent(n,"htmx:sendError",b),maybeCall(a),E()},x.onabort=function(){removeRequestIndicators(P,N),triggerErrorEvent(n,"htmx:afterRequest",b),triggerErrorEvent(n,"htmx:sendAbort",b),maybeCall(a),E()},x.ontimeout=function(){removeRequestIndicators(P,N),triggerErrorEvent(n,"htmx:afterRequest",b),triggerErrorEvent(n,"htmx:timeout",b),maybeCall(a),E()},!triggerEvent(n,"htmx:beforeRequest",b))return maybeCall(i),E(),l;var P=addRequestIndicatorClasses(n),N=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(m){forEach([x,x.upload],function(v){v.addEventListener(m,function(A){triggerEvent(n,"htmx:xhr:"+m,{lengthComputable:A.lengthComputable,loaded:A.loaded,total:A.total})})})}),triggerEvent(n,"htmx:beforeSend",b);let le=L?null:encodeParamsForBody(x,n,I);return x.send(le),l}function determineHistoryUpdates(e,t){let n=t.xhr,r=null,o=null;if(hasHeader(n,/HX-Push:/i)?(r=n.getResponseHeader("HX-Push"),o="push"):hasHeader(n,/HX-Push-Url:/i)?(r=n.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(n,/HX-Replace-Url:/i)&&(r=n.getResponseHeader("HX-Replace-Url"),o="replace"),r)return r==="false"?{}:{type:o,path:r};let s=t.pathInfo.finalRequestPath,i=t.pathInfo.responsePath,a=t.etc.push||getClosestAttributeValue(e,"hx-push-url"),l=t.etc.replace||getClosestAttributeValue(e,"hx-replace-url"),c=getInternalData(e).boosted,d=null,u=null;return a?(d="push",u=a):l?(d="replace",u=l):c&&(d="push",u=i||s),u?u==="false"?{}:(u==="true"&&(u=i||s),t.pathInfo.anchor&&u.indexOf("#")===-1&&(u=u+"#"+t.pathInfo.anchor),{type:d,path:u}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t<htmx.config.responseHandling.length;t++){var n=htmx.config.responseHandling[t];if(codeMatches(n,e.status))return n}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 n=asElement(querySelectorExt(e,t));if(n==null)throw triggerErrorEvent(e,"htmx:targetError",{target:t}),new Error(`Invalid re-target ${t}`);return n}function handleAjaxResponse(e,t){let n=t.xhr,r=t.target,o=t.etc,s=t.select;if(!triggerEvent(e,"htmx:beforeOnLoad",t))return;if(hasHeader(n,/HX-Trigger:/i)&&handleTriggerHeader(n,"HX-Trigger",e),hasHeader(n,/HX-Location:/i)){let C=n.getResponseHeader("HX-Location");var i={};C.indexOf("{")===0&&(i=parseJSON(C),C=i.path,delete i.path),i.push=i.push||"true",ajaxHelper("get",C,i);return}let a=hasHeader(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(hasHeader(n,/HX-Redirect:/i)){t.keepIndicators=!0,htmx.location.href=n.getResponseHeader("HX-Redirect"),a&&htmx.location.reload();return}if(a){t.keepIndicators=!0,htmx.location.reload();return}let l=determineHistoryUpdates(e,t),c=resolveResponseHandling(n),d=c.swap,u=!!c.error,f=htmx.config.ignoreTitle||c.ignoreTitle,y=c.select;c.target&&(t.target=resolveRetarget(e,c.target));var h=o.swapOverride;h==null&&c.swapOverride&&(h=c.swapOverride),hasHeader(n,/HX-Retarget:/i)&&(t.target=resolveRetarget(e,n.getResponseHeader("HX-Retarget"))),hasHeader(n,/HX-Reswap:/i)&&(h=n.getResponseHeader("HX-Reswap"));var p=n.response,g=mergeObjects({shouldSwap:d,serverResponse:p,isError:u,ignoreTitle:f,selectOverride:y,swapOverride:h},t);if(!(c.event&&!triggerEvent(r,c.event,g))&&triggerEvent(r,"htmx:beforeSwap",g)){if(r=g.target,p=g.serverResponse,u=g.isError,f=g.ignoreTitle,y=g.selectOverride,h=g.swapOverride,t.target=r,t.failed=u,t.successful=!u,g.shouldSwap){n.status===286&&cancelPolling(e),withExtensions(e,function(E){p=E.transformResponse(p,n,e)}),l.type&&saveCurrentPageToHistory();var w=getSwapSpecification(e,h);w.hasOwnProperty("ignoreTitle")||(w.ignoreTitle=f),r.classList.add(htmx.config.swappingClass),s&&(y=s),hasHeader(n,/HX-Reselect:/i)&&(y=n.getResponseHeader("HX-Reselect"));let C=o.selectOOB||getClosestAttributeValue(e,"hx-select-oob"),x=getClosestAttributeValue(e,"hx-select");swap(r,p,w,{select:y==="unset"?null:y||x,selectOOB:C,eventInfo:t,anchor:t.pathInfo.anchor,contextElement:e,afterSwapCallback:function(){if(hasHeader(n,/HX-Trigger-After-Swap:/i)){let E=e;bodyContains(e)||(E=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Swap",E)}},afterSettleCallback:function(){if(hasHeader(n,/HX-Trigger-After-Settle:/i)){let E=e;bodyContains(e)||(E=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Settle",E)}},beforeSwapCallback:function(){l.type&&(triggerEvent(getDocument().body,"htmx:beforeHistoryUpdate",mergeObjects({history:l},t)),l.type==="push"?(pushUrlIntoHistory(l.path),triggerEvent(getDocument().body,"htmx:pushedIntoHistory",{path:l.path})):(replaceUrlInHistory(l.path),triggerEvent(getDocument().body,"htmx:replacedInHistory",{path:l.path})))}})}u&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+n.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,n){return e},isInlineSwap:function(e){return!1},handleSwap:function(e,t,n,r){return!1},encodeParameters:function(e,t,n){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,n){if(t==null&&(t=[]),e==null)return t;n==null&&(n=[]);let r=getAttributeValue(e,"hx-ext");return r&&forEach(r.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){n.push(o.slice(7));return}if(n.indexOf(o)<0){let s=extensions[o];s&&t.indexOf(s)<0&&t.push(s)}}),getExtensions(asElement(parentElt(e)),t,n)}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,n=htmx.config.requestClass;getDocument().head.insertAdjacentHTML("beforeend",`<style${e}>.${t}{opacity:0;visibility: hidden} .${n} .${t}, .${n}.${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(r){let o=r.detail.elt||r.target,s=getInternalData(o);s&&s.xhr&&s.xhr.abort()});let n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(r){r.state&&r.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent})})):n&&n(r)},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null},0)}),htmx})(),R=ce;(function(){let e;R.defineExtension("json-enc",{init:function(t){e=t},onEvent:function(t,n){t==="htmx:configRequest"&&(n.detail.headers["Content-Type"]="application/json")},encodeParameters:function(t,n,r){t.overrideMimeType("text/json");let o={};n.forEach(function(i,a){Object.hasOwn(o,a)?(Array.isArray(o[a])||(o[a]=[o[a]]),o[a].push(i)):o[a]=i});let s=e.getExpressionVars(r);return Object.keys(o).forEach(function(i){o[i]=Object.hasOwn(s,i)?s[i]:o[i]}),JSON.stringify(o)}})})();var Q="https://typeahead.waow.tech",ee="https://public.api.bsky.app",ue="/xrpc/app.bsky.actor.searchActorsTypeahead",fe="/xrpc/app.bsky.actor.getProfiles";var de="atcr_recent_handles",te="atcr_recent_profile_cache";var U=class{constructor(t){this.input=t,this.container=t.closest(".sailor-typeahead")||t.parentElement,this.dropdown=null,this.selectedCard=null,this.actors=[],this.currentItems=[],this.mode="hidden",this.focusIndex=-1,this.debounceTimer=null,this.requestSeq=0,this.primaryUnhealthyUntil=0,this.lastPrefetchPrefix="",this.lastPrefetchAt=0,this.createDropdown(),this.bindEvents(),this.input.value.trim().length===0&&this.showRecent()}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="sailor-typeahead-dropdown",this.dropdown.setAttribute("role","listbox"),this.dropdown.style.display="none",this.input.insertAdjacentElement("afterend",this.dropdown)}bindEvents(){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.hide()}),document.addEventListener("keydown",t=>{t.key==="Escape"&&this.selectedCard&&this.clearSelection()})}handleFocus(){this.input.value.trim().length===0&&this.showRecent()}handleInput(){let t=this.input.value.trim();if(t.length===0){this.showRecent();return}if(t.length>=2&&t.length<4){this.hide(),this.schedulePrefetch(t);return}if(t.length>=4){this.scheduleSearch(t);return}this.hide()}schedulePrefetch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runPrefetch(t),150)}scheduleSearch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runSearch(t),150)}async runPrefetch(t){let n=Date.now();if(!(t===this.lastPrefetchPrefix&&n-this.lastPrefetchAt<1e4)&&!(n<this.primaryUnhealthyUntil)){this.lastPrefetchPrefix=t,this.lastPrefetchAt=n;try{await B(Q,t,400)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}}}async runSearch(t){let n=++this.requestSeq,r=null;if(Date.now()>=this.primaryUnhealthyUntil)try{r=await B(Q,t,1500)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}if(r===null)try{r=await B(ee,t,1500)}catch{r=[]}n===this.requestSeq&&(this.actors=r||[],this.focusIndex=-1,this.renderResults())}renderResults(){if(this.mode="results",this.dropdown.innerHTML="",this.currentItems=[],this.actors.length===0){this.hide();return}this.actors.forEach((t,n)=>{this.currentItems.push(t),this.dropdown.appendChild(this.buildActorRow(t,n))}),this.dropdown.style.display="block"}buildActorRow(t,n){let r=document.createElement("div");r.className="sailor-typeahead-item",r.setAttribute("role","option"),r.dataset.index=String(n),r.dataset.handle=t.handle;let o=document.createElement("div");if(o.className="sailor-typeahead-avatar",t.avatar){let l=document.createElement("img");l.src=t.avatar,l.alt="",l.loading="lazy",o.appendChild(l)}let s=document.createElement("div");s.className="sailor-typeahead-text";let i=t.displayName&&t.displayName!==t.handle;if(i){let l=document.createElement("div");l.className="sailor-typeahead-name",l.textContent=t.displayName,s.appendChild(l)}let a=document.createElement("div");return a.className=i?"sailor-typeahead-handle":"sailor-typeahead-name",a.textContent="@"+t.handle,s.appendChild(a),r.append(o,s),r.addEventListener("mousedown",l=>{l.preventDefault(),this.select(t)}),r}showRecent(){let t=me();if(t.length===0){this.hide();return}this.mode="recent",this.focusIndex=-1,this.renderRecent(t),this.enrichRecent(t)}renderRecent(t){let n=M();this.dropdown.innerHTML="",this.currentItems=[];let r=document.createElement("div");r.className="sailor-typeahead-header",r.textContent="Recent accounts",this.dropdown.appendChild(r),t.forEach((o,s)=>{let i=n[o]?.profile||{handle:o};this.currentItems.push(i),this.dropdown.appendChild(this.buildActorRow(i,s))}),this.dropdown.style.display="block"}async enrichRecent(t){let n=M(),r=Date.now(),o=t.filter(a=>{let l=n[a];return!l||r-l.ts>864e5});if(o.length===0)return;let s=await he(o);if(s.length===0)return;let i=M();s.forEach(a=>{i[a.handle]={ts:r,profile:{handle:a.handle,displayName:a.displayName,avatar:a.avatar}}}),Z(i),this.mode==="recent"&&this.renderRecent(t)}hide(){this.mode="hidden",this.focusIndex=-1,this.dropdown.style.display="none"}select(t){if(typeof t=="string"&&(t={handle:t}),this.input.value=t.handle,this.hide(),this.showSelectedCard(t),t.handle){let n=M();n[t.handle]={ts:Date.now(),profile:{handle:t.handle,displayName:t.displayName,avatar:t.avatar}},Z(n)}}showSelectedCard(t){this.clearSelectedCard();let n=document.createElement("div");n.className="sailor-typeahead-selected";let r=document.createElement("div");if(r.className="sailor-typeahead-avatar",t.avatar){let l=document.createElement("img");l.src=t.avatar,l.alt="",r.appendChild(l)}let o=document.createElement("div");o.className="sailor-typeahead-text";let s=t.displayName&&t.displayName!==t.handle;if(s){let l=document.createElement("div");l.className="sailor-typeahead-name",l.textContent=t.displayName,o.appendChild(l)}let i=document.createElement("div");i.className=s?"sailor-typeahead-handle":"sailor-typeahead-name",i.textContent="@"+t.handle,o.appendChild(i);let a=document.createElement("button");a.type="button",a.className="sailor-typeahead-clear",a.tabIndex=-1,a.setAttribute("aria-label","Change account"),a.innerHTML="&times;",a.addEventListener("click",()=>this.clearSelection()),n.append(r,o,a),this.input.style.display="none",this.input.insertAdjacentElement("beforebegin",n),this.selectedCard=n}clearSelectedCard(){this.selectedCard&&(this.selectedCard.remove(),this.selectedCard=null)}clearSelection(){this.clearSelectedCard(),this.input.style.display="",this.input.value="",this.input.focus(),this.showRecent()}handleKeydown(t){if(this.mode==="hidden")return;let n=this.dropdown.querySelectorAll(".sailor-typeahead-item");n.length!==0&&(t.key==="ArrowDown"?(t.preventDefault(),this.focusIndex=(this.focusIndex+1)%n.length,this.updateFocus(n)):t.key==="ArrowUp"?(t.preventDefault(),this.focusIndex=this.focusIndex<=0?n.length-1:this.focusIndex-1,this.updateFocus(n)):t.key==="Enter"?this.focusIndex>=0&&this.currentItems[this.focusIndex]&&(t.preventDefault(),this.select(this.currentItems[this.focusIndex])):t.key==="Escape"?this.hide():t.key==="Tab"&&this.focusIndex===-1&&n.length>0&&(t.preventDefault(),this.focusIndex=0,this.updateFocus(n)))}updateFocus(t){t.forEach((n,r)=>{n.classList.toggle("focused",r===this.focusIndex),r===this.focusIndex&&n.scrollIntoView({block:"nearest"})})}};async function B(e,t,n){let r=new URL(ue,e);r.searchParams.set("q",t),r.searchParams.set("limit",String(8));let o=new AbortController,s=setTimeout(()=>o.abort(),n);try{let i=await fetch(r,{signal:o.signal});if(!i.ok)throw new Error("HTTP "+i.status);let a=await i.json();return Array.isArray(a.actors)?a.actors:[]}finally{clearTimeout(s)}}async function he(e){if(e.length===0)return[];let t=new URL(fe,ee);e.forEach(o=>t.searchParams.append("actors",o));let n=new AbortController,r=setTimeout(()=>n.abort(),3e3);try{let o=await fetch(t,{signal:n.signal});if(!o.ok)return[];let s=await o.json();return Array.isArray(s.profiles)?s.profiles:[]}catch{return[]}finally{clearTimeout(r)}}function M(){try{return JSON.parse(localStorage.getItem(te)||"{}")}catch{return{}}}function Z(e){try{localStorage.setItem(te,JSON.stringify(e))}catch{}}function me(){try{let e=localStorage.getItem(de);return e?JSON.parse(e):[]}catch{return[]}}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("handle");e&&new U(e)});function re(){return localStorage.getItem("theme")||"system"}function ge(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function W(){let e=re(),n=ge(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),pe(e)}function oe(e){localStorage.setItem("theme",e),W(),Ee()}function pe(e){let t={system:"sun-moon",light:"sun",dark:"moon"};document.querySelectorAll("[data-theme-icon] use").forEach(n=>{n.setAttribute("href",`/icons.svg#${t[e]||"sun-moon"}`)}),document.querySelectorAll(".theme-option").forEach(n=>{let r=n.dataset.value===e,o=n.querySelector(".theme-check");o&&(o.style.visibility=r?"visible":"hidden")})}function Ee(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{re()==="system"&&W()});function ye(){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 V(){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",n=>{if(n.key==="Escape"&&e.classList.contains("expanded")&&V(),n.key==="/"&&!e.classList.contains("expanded")){let r=n.target.tagName;if(r==="INPUT"||r==="TEXTAREA"||n.target.isContentEditable)return;n.preventDefault(),e.classList.add("expanded"),t.focus()}}),document.addEventListener("click",n=>{e.classList.contains("expanded")&&!e.contains(n.target)&&V()}))});function j(e,t){!t&&typeof event<"u"&&(t=event.target.closest("button")),navigator.clipboard.writeText(e).then(()=>{if(!t)return;let n=t.innerHTML;t.innerHTML='<svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#check"></use></svg> Copied!',setTimeout(()=>{t.innerHTML=n},2e3)}).catch(n=>{console.error("Failed to copy:",n)})}function xe(e){let t=s=>{let i=(s==null?"":String(s)).replace(/\s+/g," ").trim();return/[",\n\r]/.test(i)?'"'+i.replace(/"/g,'""')+'"':i},n=s=>Array.from(s).map(i=>t(i.textContent)).join(","),r=[],o=e.querySelector("thead tr");return o&&r.push(n(o.querySelectorAll("th,td"))),e.querySelectorAll("tbody tr").forEach(s=>{r.push(n(s.querySelectorAll("td,th")))}),r.join(` 2 - `)}document.addEventListener("DOMContentLoaded",()=>{document.addEventListener("click",e=>{let t=e.target.closest("button[data-copy-csv]");if(t){let o=t.closest("[data-csv-section]"),s=o&&o.querySelector("table");s&&j(xe(s),t);return}let n=e.target.closest("button[data-cmd]");if(n){j(n.getAttribute("data-cmd"),n);return}if(e.target.closest("a, button, input, .cmd"))return;let r=e.target.closest("[data-href]");r&&(window.location=r.getAttribute("data-href"))})});function ve(e){let t=Math.floor((new Date-new Date(e))/1e3),n={year:31536e3,month:2592e3,week:604800,day:86400,hour:3600,minute:60,second:1};for(let[r,o]of Object.entries(n)){let s=Math.floor(t/o);if(s>=1)return s===1?`1 ${r} ago`:`${s} ${r}s ago`}return"just now"}function X(){document.querySelectorAll("time[datetime]").forEach(e=>{let t=e.getAttribute("datetime");if(t&&!e.dataset.noUpdate){let n=ve(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{X(),W(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{oe(t.dataset.value)})})}),document.addEventListener("click",e=>{let t=e.target.closest("details.dropdown");document.querySelectorAll("details.dropdown[open]").forEach(n=>{n!==t&&n.removeAttribute("open")})})});document.addEventListener("htmx:afterSwap",X);setInterval(X,6e4);function be(){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 n=document.querySelector(".manifests-list");n&&(t?n.classList.add("show-offline"):n.classList.remove("show-offline"))});async function we(e,t,n){try{let r=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!1})});if(r.status===409){let o=await r.json();Ce(e,t,n,o.tags)}else if(r.ok)se(n);else{let o=await r.text();alert(`Failed to delete manifest: ${o}`)}}catch(r){console.error("Error deleting manifest:",r),alert(`Error deleting manifest: ${r.message}`)}}function Ce(e,t,n,r){let o=document.getElementById("manifest-delete-modal"),s=document.getElementById("manifest-delete-tags"),i=document.getElementById("confirm-manifest-delete-btn");s.innerHTML="",r.forEach(a=>{let l=document.createElement("li");l.textContent=a,s.appendChild(l)}),i.onclick=()=>Se(e,t,n),o.style.display="flex"}function $(){let e=document.getElementById("manifest-delete-modal");e.style.display="none"}async function Se(e,t,n){let r=document.getElementById("confirm-manifest-delete-btn"),o=r.textContent;try{r.disabled=!0,r.textContent="Deleting...";let s=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!0})});if(s.ok)$(),se(n),location.reload();else{let i=await s.text();alert(`Failed to delete manifest: ${i}`),r.disabled=!1,r.textContent=o}}catch(s){console.error("Error deleting manifest:",s),alert(`Error deleting manifest: ${s.message}`),r.disabled=!1,r.textContent=o}}async function Te(e){let t=document.getElementById("confirm-untagged-delete-btn"),n=t.textContent;try{t.disabled=!0,t.textContent="Deleting...";let r=await fetch("/api/manifests/untagged",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e})}),o=await r.json();r.ok?(document.getElementById("untagged-delete-modal").close(),D(`Deleted ${o.deleted} untagged manifest(s)`,"success"),o.deleted>0&&location.reload(),t.disabled=!1,t.textContent=n):(alert(`Failed to delete untagged manifests: ${o.error||"Unknown error"}`),t.disabled=!1,t.textContent=n)}catch(r){console.error("Error deleting untagged manifests:",r),alert(`Error: ${r.message}`),t.disabled=!1,t.textContent=n}}function se(e){let t=document.getElementById(`manifest-${e}`);t&&t.remove()}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("manifest-delete-modal");e&&e.addEventListener("click",t=>{t.target===e&&$()})});async function Ae(e,t){let n=document.getElementById("vuln-detail-modal"),r=document.getElementById("vuln-modal-body");if(!(!n||!r)){r.innerHTML='<div class="flex justify-center py-8"><span class="loading loading-spinner loading-lg"></span></div>',n.showModal();try{let o=await fetch(`/api/vuln-details?digest=${encodeURIComponent(e)}&holdEndpoint=${encodeURIComponent(t)}`);r.innerHTML=await o.text()}catch{r.innerHTML='<p class="text-error">Failed to load vulnerability details</p>'}}}document.addEventListener("DOMContentLoaded",()=>{let e=document.cookie.split("; ").find(n=>n.startsWith("atcr_login_handle="));if(!e)return;let t=decodeURIComponent(e.split("=")[1]);if(t){try{let n="atcr_recent_handles",r=JSON.parse(localStorage.getItem(n)||"[]");r=r.filter(o=>o!==t),r.unshift(t),r=r.slice(0,5),localStorage.setItem(n,JSON.stringify(r))}catch(n){console.error("Failed to save recent account:",n)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});function ne(){let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),n=document.getElementById("carousel-next");if(!e)return;let r=e.querySelectorAll(".carousel-item");if(r.length===0)return;let o=null,s=5e3;function i(){let u=r[0],f=parseFloat(getComputedStyle(e).gap)||24;return u.offsetWidth+f}function a(){let u=e.scrollWidth-e.clientWidth;e.scrollLeft>=u-10?e.scrollTo({left:0,behavior:"smooth"}):e.scrollBy({left:i(),behavior:"smooth"})}function l(){e.scrollLeft<=10?e.scrollTo({left:e.scrollWidth,behavior:"smooth"}):e.scrollBy({left:-i(),behavior:"smooth"})}function c(){o||e.scrollWidth<=e.clientWidth+10||(o=setInterval(a,s))}function d(){o&&(clearInterval(o),o=null)}t&&t.addEventListener("click",()=>{d(),l(),c()}),n&&n.addEventListener("click",()=>{d(),a(),c()}),e.addEventListener("mouseenter",d),e.addEventListener("mouseleave",c),c()}document.addEventListener("DOMContentLoaded",()=>{"requestIdleCallback"in window?requestIdleCallback(ne,{timeout:2e3}):setTimeout(ne,100)});function D(e,t){let n=document.getElementById("toast-container");n||(n=document.createElement("div"),n.id="toast-container",n.className="toast toast-end toast-bottom z-50",document.body.appendChild(n));let r=t==="error"?"alert-error":"alert-success",o=document.createElement("div");o.className=`alert ${r} shadow-lg transition-opacity duration-300`,o.innerHTML=`<span>${e}</span>`,n.appendChild(o),setTimeout(()=>{o.style.opacity="0",setTimeout(()=>o.remove(),300)},3e3)}async function He(e){try{let t=await fetch(`/api/webhooks/${e}/test`,{method:"POST",credentials:"include"}),n=await t.text();n.includes('class="success"')||t.ok&&!n.includes('class="error"')?D("Test webhook delivered successfully!","success"):D("Test delivery failed \u2014 check the webhook URL","error")}catch{D("Failed to reach server","error")}}window.setTheme=oe;window.toggleSearch=ye;window.closeSearch=V;window.copyToClipboard=j;window.toggleOfflineManifests=be;window.deleteManifest=we;window.deleteUntaggedManifests=Te;window.closeManifestDeleteModal=$;window.openVulnDetails=Ae;window.showToast=D;window.testWebhook=He;window.htmx=R;R.config.methodsThatUseUrlParams=["get"]; 1 + var xe=(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,n){let r=getAttributeValue(t,n),o=getAttributeValue(t,"hx-disinherit");var s=getAttributeValue(t,"hx-inherit");if(e!==t){if(htmx.config.disableInheritance)return s&&(s==="*"||s.split(" ").indexOf(n)>=0)?r:null;if(o&&(o==="*"||o.split(" ").indexOf(n)>=0))return"unset"}return r}function getClosestAttributeValue(e,t){let n=null;if(getClosestMatch(e,function(r){return!!(n=getAttributeValueWithDisinheritance(e,asElement(r),t))}),n!=="unset")return n}function matches(e,t){return e instanceof Element&&e.matches(t)}function getStartTag(e){let n=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i.exec(e);return n?n[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(n){t.setAttribute(n.name,n.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 n=duplicateScript(t),r=t.parentNode;try{r.insertBefore(n,t)}catch(o){logError(o)}finally{t.remove()}}})}function makeFragment(e){let t=e.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i,""),n=getStartTag(t),r;if(n==="html"){r=new DocumentFragment;let s=parseHTML(e);takeChildrenFor(r,s.body),r.title=s.title}else if(n==="body"){r=new DocumentFragment;let s=parseHTML(t);takeChildrenFor(r,s.body),r.title=s.title}else{let s=parseHTML('<body><template class="internal-htmx-wrapper">'+t+"</template></body>");r=s.querySelector("template").content,r.title=s.title;var o=r.querySelector("title");o&&o.parentNode===r&&(o.remove(),r.title=o.innerText)}return r&&(htmx.config.allowScriptTags?normalizeScriptTags(r):r.querySelectorAll("script").forEach(s=>s.remove())),r}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",n=e[t];return n||(n=e[t]={}),n}function toArray(e){let t=[];if(e)for(let n=0;n<e.length;n++)t.push(e[n]);return t}function forEach(e,t){if(e)for(let n=0;n<e.length;n++)t(e[n])}function isScrolledIntoView(e){let t=e.getBoundingClientRect(),n=t.top,r=t.bottom;return n<window.innerHeight&&r>=0}function bodyContains(e){return e.getRootNode({composed:!0})===document}function splitOnWhitespace(e){return e.trim().split(/\s+/)}function mergeObjects(e,t){for(let n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);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(n){e(n.detail.elt)})}function logAll(){htmx.logger=function(e,t,n){console&&console.log(t,e,n)}}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,n){e=asElement(resolveTarget(e)),e&&(n?getWindow().setTimeout(function(){addClassToElement(e,t),e=null},n):e.classList&&e.classList.add(t))}function removeClassFromElement(e,t,n){let r=asElement(resolveTarget(e));r&&(n?getWindow().setTimeout(function(){removeClassFromElement(r,t),r=null},n):r.classList&&(r.classList.remove(t),r.classList.length===0&&r.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(n){removeClassFromElement(n,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,n){if(t.indexOf("global ")===0)return querySelectorAllExt(e,t.slice(7),!0);e=resolveTarget(e);let r=[];{let i=0,l=0;for(let a=0;a<t.length;a++){let c=t[a];if(c===","&&i===0){r.push(t.substring(l,a)),l=a+1;continue}c==="<"?i++:c==="/"&&a<t.length-1&&t[a+1]===">"&&i--}l<t.length&&r.push(t.substring(l))}let o=[],s=[];for(;r.length>0;){let i=normalizeSelector(r.shift()),l;i.indexOf("closest ")===0?l=closest(asElement(e),normalizeSelector(i.slice(8))):i.indexOf("find ")===0?l=find(asParentNode(e),normalizeSelector(i.slice(5))):i==="next"||i==="nextElementSibling"?l=asElement(e).nextElementSibling:i.indexOf("next ")===0?l=scanForwardQuery(e,normalizeSelector(i.slice(5)),!!n):i==="previous"||i==="previousElementSibling"?l=asElement(e).previousElementSibling:i.indexOf("previous ")===0?l=scanBackwardsQuery(e,normalizeSelector(i.slice(9)),!!n):i==="document"?l=document:i==="window"?l=window:i==="body"?l=document.body:i==="root"?l=getRootNode(e,!!n):i==="host"?l=e.getRootNode().host:s.push(i),l&&o.push(l)}if(s.length>0){let i=s.join(","),l=asParentNode(getRootNode(e,!!n));o.push(...toArray(l.querySelectorAll(i)))}return o}var scanForwardQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=0;o<r.length;o++){let s=r[o];if(s.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING)return s}},scanBackwardsQuery=function(e,t,n){let r=asParentNode(getRootNode(e,n)).querySelectorAll(t);for(let o=r.length-1;o>=0;o--){let s=r[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,n,r){return isFunction(t)?{target:getDocument().body,event:asString(e),listener:t,options:n}:{target:resolveTarget(e),event:asString(t),listener:n,options:r}}function addEventListenerImpl(e,t,n,r){return ready(function(){let s=processEventArgs(e,t,n,r);s.target.addEventListener(s.event,s.listener,s.options)}),isFunction(t)?t:n}function removeEventListenerImpl(e,t,n){return ready(function(){let r=processEventArgs(e,t,n);r.target.removeEventListener(r.event,r.listener)}),isFunction(t)?t:n}let DUMMY_ELT=getDocument().createElement("output");function findAttributeTargets(e,t){let n=getClosestAttributeValue(e,t);if(n){if(n==="this")return[findThisElement(e,t)];{let r=querySelectorAllExt(e,n);if(/(^|,)(\s*)inherit(\s*)($|,)/.test(n)){let s=asElement(getClosestMatch(e,function(i){return i!==e&&hasAttribute(asElement(i),t)}));s&&r.push(...findAttributeTargets(s,t))}return r.length===0?(logError('The selector "'+n+'" on '+t+" returned no matches!"),[DUMMY_ELT]):r}}}function findThisElement(e,t){return asElement(getClosestMatch(e,function(n){return getAttributeValue(asElement(n),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(n){!t.hasAttribute(n.name)&&shouldSettleAttribute(n.name)&&e.removeAttribute(n.name)}),forEach(t.attributes,function(n){shouldSettleAttribute(n.name)&&e.setAttribute(n.name,n.value)})}function isInlineSwap(e,t){let n=getExtensions(t);for(let r=0;r<n.length;r++){let o=n[r];try{if(o.isInlineSwap(e))return!0}catch(s){logError(s)}}return e==="outerHTML"}function oobSwap(e,t,n,r){r=r||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 i=querySelectorAllExt(r,o,!1);return i.length?(forEach(i,function(l){let a,c=t.cloneNode(!0);a=getDocument().createDocumentFragment(),a.appendChild(c),isInlineSwap(s,l)||(a=asParentNode(c));let d={shouldSwap:!0,target:l,fragment:a};triggerEvent(l,"htmx:oobBeforeSwap",d)&&(l=d.target,d.shouldSwap&&(handlePreservedElements(a),swapWithStyle(s,l,l,a,n),restorePreservedElements()),forEach(n.elts,function(u){triggerEvent(u,"htmx:oobAfterSwap",d)}))}),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 n=find("#"+t.id);n.parentNode.moveBefore(t,n),n.remove()}e.remove()}}function handlePreservedElements(e){forEach(findAll(e,"[hx-preserve], [data-hx-preserve]"),function(t){let n=getAttributeValue(t,"id"),r=getDocument().getElementById(n);if(r!=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(r,null)}else t.parentNode.replaceChild(r,t)})}function handleAttributes(e,t,n){forEach(t.querySelectorAll("[id]"),function(r){let o=getRawAttribute(r,"id");if(o&&o.length>0){let s=o.replace("'","\\'"),i=r.tagName.replace(":","\\:"),l=asParentNode(e),a=l&&l.querySelector(i+"[id='"+s+"']");if(a&&a!==l){let c=r.cloneNode();cloneAttributes(r,a),n.tasks.push(function(){cloneAttributes(r,c)})}}})}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]",n=asHtmlElement(matches(e,t)?e:e.querySelector(t));n?.focus()}function insertNodesBefore(e,t,n,r){for(handleAttributes(e,n,r);n.childNodes.length>0;){let o=n.firstChild;addClassToElement(asElement(o),htmx.config.addedClass),e.insertBefore(o,t),o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE&&r.tasks.push(makeAjaxLoadTask(o))}}function stringHash(e,t){let n=0;for(;n<e.length;)t=(t<<5)-t+e.charCodeAt(n++)|0;return t}function attributeHash(e){let t=0;for(let n=0;n<e.attributes.length;n++){let r=e.attributes[n];r.value&&(t=stringHash(r.name,t),t=stringHash(r.value,t))}return t}function deInitOnHandlers(e){let t=getInternalData(e);if(t.onHandlers){for(let n=0;n<t.onHandlers.length;n++){let r=t.onHandlers[n];removeEventListenerImpl(e,r.event,r.listener)}delete t.onHandlers}}function deInitNode(e){let t=getInternalData(e);t.timeout&&clearTimeout(t.timeout),t.listenerInfos&&forEach(t.listenerInfos,function(n){n.on&&removeEventListenerImpl(n.on,n.trigger,n.listener)}),deInitOnHandlers(e),forEach(Object.keys(t),function(n){n!=="firstInitCompleted"&&delete t[n]})}function cleanUpElement(e){triggerEvent(e,"htmx:beforeCleanupElement"),deInitNode(e),forEach(e.children,function(t){cleanUpElement(t)})}function swapOuterHTML(e,t,n){if(e.tagName==="BODY")return swapInnerHTML(e,t,n);let r,o=e.previousSibling,s=parentElt(e);if(s){for(insertNodesBefore(s,e,t,n),o==null?r=s.firstChild:r=o.nextSibling,n.elts=n.elts.filter(function(i){return i!==e});r&&r!==e;)r instanceof Element&&n.elts.push(r),r=r.nextSibling;cleanUpElement(e),e.remove()}}function swapAfterBegin(e,t,n){return insertNodesBefore(e,e.firstChild,t,n)}function swapBeforeBegin(e,t,n){return insertNodesBefore(parentElt(e),e,t,n)}function swapBeforeEnd(e,t,n){return insertNodesBefore(e,null,t,n)}function swapAfterEnd(e,t,n){return insertNodesBefore(parentElt(e),e.nextSibling,t,n)}function swapDelete(e){cleanUpElement(e);let t=parentElt(e);if(t)return t.removeChild(e)}function swapInnerHTML(e,t,n){let r=e.firstChild;if(insertNodesBefore(e,r,t,n),r){for(;r.nextSibling;)cleanUpElement(r.nextSibling),e.removeChild(r.nextSibling);cleanUpElement(r),e.removeChild(r)}}function swapWithStyle(e,t,n,r,o){switch(e){case"none":return;case"outerHTML":swapOuterHTML(n,r,o);return;case"afterbegin":swapAfterBegin(n,r,o);return;case"beforebegin":swapBeforeBegin(n,r,o);return;case"beforeend":swapBeforeEnd(n,r,o);return;case"afterend":swapAfterEnd(n,r,o);return;case"delete":swapDelete(n);return;default:var s=getExtensions(t);for(let i=0;i<s.length;i++){let l=s[i];try{let a=l.handleSwap(e,n,r,o);if(a){if(Array.isArray(a))for(let c=0;c<a.length;c++){let d=a[c];d.nodeType!==Node.TEXT_NODE&&d.nodeType!==Node.COMMENT_NODE&&o.tasks.push(makeAjaxLoadTask(d))}return}}catch(a){logError(a)}}e==="innerHTML"?swapInnerHTML(n,r,o):swapWithStyle(htmx.config.defaultSwapStyle,t,n,r,o)}}function findAndSwapOobElements(e,t,n){var r=findAll(e,"[hx-swap-oob], [data-hx-swap-oob]");return forEach(r,function(o){if(htmx.config.allowNestedOobSwaps||o.parentElement===null){let s=getAttributeValue(o,"hx-swap-oob");s!=null&&oobSwap(s,o,t,n)}else o.removeAttribute("hx-swap-oob"),o.removeAttribute("data-hx-swap-oob")}),r.length>0}function swap(e,t,n,r){r||(r={});let o=null,s=null,i=function(){maybeCall(r.beforeSwapCallback),e=resolveTarget(e);let c=r.contextElement?getRootNode(r.contextElement,!1):getDocument(),d=document.activeElement,u={};u={elt:d,start:d?d.selectionStart:null,end:d?d.selectionEnd:null};let f=makeSettleInfo(e);if(n.swapStyle==="textContent")e.textContent=t;else{let h=makeFragment(t);if(f.title=r.title||h.title,r.historyRequest&&(h=h.querySelector("[hx-history-elt],[data-hx-history-elt]")||h),r.selectOOB){let y=r.selectOOB.split(",");for(let p=0;p<y.length;p++){let w=y[p].split(":",2),T=w[0].trim();T.indexOf("#")===0&&(T=T.substring(1));let v=w[1]||"true",E=h.querySelector("#"+T);E&&oobSwap(v,E,f,c)}}if(findAndSwapOobElements(h,f,c),forEach(findAll(h,"template"),function(y){y.content&&findAndSwapOobElements(y.content,f,c)&&y.remove()}),r.select){let y=getDocument().createDocumentFragment();forEach(h.querySelectorAll(r.select),function(p){y.appendChild(p)}),h=y}handlePreservedElements(h),swapWithStyle(n.swapStyle,r.contextElement,e,h,f),restorePreservedElements()}if(u.elt&&!bodyContains(u.elt)&&getRawAttribute(u.elt,"id")){let h=document.getElementById(getRawAttribute(u.elt,"id")),y={preventScroll:n.focusScroll!==void 0?!n.focusScroll:!htmx.config.defaultFocusScroll};if(h){if(u.start&&h.setSelectionRange)try{h.setSelectionRange(u.start,u.end)}catch{}h.focus(y)}}e.classList.remove(htmx.config.swappingClass),forEach(f.elts,function(h){h.classList&&h.classList.add(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSwap",r.eventInfo)}),maybeCall(r.afterSwapCallback),n.ignoreTitle||handleTitle(f.title);let m=function(){if(forEach(f.tasks,function(h){h.call()}),forEach(f.elts,function(h){h.classList&&h.classList.remove(htmx.config.settlingClass),triggerEvent(h,"htmx:afterSettle",r.eventInfo)}),r.anchor){let h=asElement(resolveTarget("#"+r.anchor));h&&h.scrollIntoView({block:"start",behavior:"auto"})}updateScrollState(f.elts,n),maybeCall(r.afterSettleCallback),maybeCall(o)};n.settleDelay>0?getWindow().setTimeout(m,n.settleDelay):m()},l=htmx.config.globalViewTransitions;n.hasOwnProperty("transition")&&(l=n.transition);let a=r.contextElement||getDocument();if(l&&triggerEvent(a,"htmx:beforeTransition",r.eventInfo)&&typeof Promise<"u"&&document.startViewTransition){let c=new Promise(function(u,f){o=u,s=f}),d=i;i=function(){document.startViewTransition(function(){return d(),c})}}try{n?.swapDelay&&n.swapDelay>0?getWindow().setTimeout(i,n.swapDelay):i()}catch(c){throw triggerErrorEvent(a,"htmx:swapError",r.eventInfo),maybeCall(s),c}}function handleTriggerHeader(e,t,n){let r=e.getResponseHeader(t);if(r.indexOf("{")===0){let o=parseJSON(r);for(let s in o)if(o.hasOwnProperty(s)){let i=o[s];isRawObject(i)?n=i.target!==void 0?i.target:n:i={value:i},triggerEvent(n,s,i)}}else{let o=r.split(",");for(let s=0;s<o.length;s++)triggerEvent(n,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=[],n=0;for(;n<e.length;){if(SYMBOL_START.exec(e.charAt(n))){for(var r=n;SYMBOL_CONT.exec(e.charAt(n+1));)n++;t.push(e.substring(r,n+1))}else if(STRINGISH_START.indexOf(e.charAt(n))!==-1){let o=e.charAt(n);var r=n;for(n++;n<e.length&&e.charAt(n)!==o;)e.charAt(n)==="\\"&&n++,n++;t.push(e.substring(r,n+1))}else{let o=e.charAt(n);t.push(o)}n++}return t}function isPossibleRelativeReference(e,t,n){return SYMBOL_START.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==n&&t!=="."}function maybeGenerateConditional(e,t,n){if(t[0]==="["){t.shift();let r=1,o=" return (function("+n+"){ return (",s=null;for(;t.length>0;){let i=t[0];if(i==="]"){if(r--,r===0){s===null&&(o=o+"true"),t.shift(),o+=")})";try{let l=maybeEval(e,function(){return Function(o)()},function(){return!0});return l.source=o,l}catch(l){return triggerErrorEvent(getDocument().body,"htmx:syntax:error",{error:l,source:o}),null}}}else i==="["&&r++;isPossibleRelativeReference(i,s,n)?o+="(("+n+"."+i+") ? ("+n+"."+i+") : (window."+i+"))":o=o+i,s=t.shift()}}}function consumeUntil(e,t){let n="";for(;e.length>0&&!t.test(e[0]);)n+=e.shift();return n}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,n){let r=[],o=tokenizeString(t);do{consumeUntil(o,NOT_WHITESPACE);let l=o.length,a=consumeUntil(o,/[,\[\s]/);if(a!=="")if(a==="every"){let c={trigger:"every"};consumeUntil(o,NOT_WHITESPACE),c.pollInterval=parseInterval(consumeUntil(o,/[,\[\s]/)),consumeUntil(o,NOT_WHITESPACE);var s=maybeGenerateConditional(e,o,"event");s&&(c.eventFilter=s),r.push(c)}else{let c={trigger:a};var s=maybeGenerateConditional(e,o,"event");for(s&&(c.eventFilter=s),consumeUntil(o,NOT_WHITESPACE);o.length>0&&o[0]!==",";){let u=o.shift();if(u==="changed")c.changed=!0;else if(u==="once")c.once=!0;else if(u==="consume")c.consume=!0;else if(u==="delay"&&o[0]===":")o.shift(),c.delay=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA));else if(u==="from"&&o[0]===":"){if(o.shift(),COMBINED_SELECTOR_START.test(o[0]))var i=consumeCSSSelector(o);else{var i=consumeUntil(o,WHITESPACE_OR_COMMA);if(i==="closest"||i==="find"||i==="next"||i==="previous"){o.shift();let m=consumeCSSSelector(o);m.length>0&&(i+=" "+m)}}c.from=i}else u==="target"&&o[0]===":"?(o.shift(),c.target=consumeCSSSelector(o)):u==="throttle"&&o[0]===":"?(o.shift(),c.throttle=parseInterval(consumeUntil(o,WHITESPACE_OR_COMMA))):u==="queue"&&o[0]===":"?(o.shift(),c.queue=consumeUntil(o,WHITESPACE_OR_COMMA)):u==="root"&&o[0]===":"?(o.shift(),c[u]=consumeCSSSelector(o)):u==="threshold"&&o[0]===":"?(o.shift(),c[u]=consumeUntil(o,WHITESPACE_OR_COMMA)):triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()});consumeUntil(o,NOT_WHITESPACE)}r.push(c)}o.length===l&&triggerErrorEvent(e,"htmx:syntax:error",{token:o.shift()}),consumeUntil(o,NOT_WHITESPACE)}while(o[0]===","&&o.shift());return n&&(n[t]=r),r}function getTriggerSpecs(e){let t=getAttributeValue(e,"hx-trigger"),n=[];if(t){let r=htmx.config.triggerSpecsCache;n=r&&r[t]||parseAndCacheTrigger(e,t,r)}return n.length>0?n: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,n){let r=getInternalData(e);r.timeout=getWindow().setTimeout(function(){bodyContains(e)&&r.cancelled!==!0&&(maybeFilterEvent(n,e,makeEvent("hx:poll:trigger",{triggerSpec:n,target:e}))||t(e),processPolling(e,t,n))},n.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,n){if(e instanceof HTMLAnchorElement&&isLocalLink(e)&&(e.target===""||e.target==="_self")||e.tagName==="FORM"&&String(getRawAttribute(e,"method")).toLowerCase()!=="dialog"){t.boosted=!0;let r,o;if(e.tagName==="A")r="get",o=getRawAttribute(e,"href");else{let s=getRawAttribute(e,"method");r=s?s.toLowerCase():"get",o=getRawAttribute(e,"action"),(o==null||o==="")&&(o=location.href),r==="get"&&o.includes("?")&&(o=o.replace(/\?[^#]+/,""))}n.forEach(function(s){addEventListener(e,function(i,l){let a=asElement(i);if(eltIsDisabled(a)){cleanUpElement(a);return}issueAjaxRequest(r,o,a,l)},t,s,!0)})}}function shouldCancel(e,t){if(e.type==="submit"&&t.tagName==="FORM")return!0;if(e.type==="click"){let n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit")return!0;let r=t.closest("a"),o=/^#.+/;if(r&&r.href&&!o.test(r.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,n){let r=e.eventFilter;if(r)try{return r.call(t,n)!==!0}catch(o){let s=r.source;return triggerErrorEvent(getDocument().body,"htmx:eventFilter:error",{error:o,source:s}),!0}return!1}function addEventListener(e,t,n,r,o){let s=getInternalData(e),i;r.from?i=querySelectorAllExt(e,r.from):i=[e],r.changed&&("lastValue"in s||(s.lastValue=new WeakMap),i.forEach(function(l){s.lastValue.has(r)||s.lastValue.set(r,new WeakMap),s.lastValue.get(r).set(l,l.value)})),forEach(i,function(l){let a=function(c){if(!bodyContains(e)){l.removeEventListener(r.trigger,a);return}if(ignoreBoostedAnchorCtrlClick(e,c)||((o||shouldCancel(c,l))&&c.preventDefault(),maybeFilterEvent(r,e,c)))return;let d=getInternalData(c);if(d.triggerSpec=r,d.handledFor==null&&(d.handledFor=[]),d.handledFor.indexOf(e)<0){if(d.handledFor.push(e),r.consume&&c.stopPropagation(),r.target&&c.target&&!matches(asElement(c.target),r.target))return;if(r.once){if(s.triggeredOnce)return;s.triggeredOnce=!0}if(r.changed){let u=c.target,f=u.value,m=s.lastValue.get(r);if(m.has(u)&&m.get(u)===f)return;m.set(u,f)}if(s.delayed&&clearTimeout(s.delayed),s.throttle)return;r.throttle>0?s.throttle||(triggerEvent(e,"htmx:trigger"),t(e,c),s.throttle=getWindow().setTimeout(function(){s.throttle=null},r.throttle)):r.delay>0?s.delayed=getWindow().setTimeout(function(){triggerEvent(e,"htmx:trigger"),t(e,c)},r.delay):(triggerEvent(e,"htmx:trigger"),t(e,c))}};n.listenerInfos==null&&(n.listenerInfos=[]),n.listenerInfos.push({trigger:r.trigger,listener:a,on:l}),l.addEventListener(r.trigger,a)})}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,n,r){let o=function(){n.loaded||(n.loaded=!0,triggerEvent(e,"htmx:trigger"),t(e))};r>0?getWindow().setTimeout(o,r):o()}function processVerbs(e,t,n){let r=!1;return forEach(VERBS,function(o){if(hasAttribute(e,"hx-"+o)){let s=getAttributeValue(e,"hx-"+o);r=!0,t.path=s,t.verb=o,n.forEach(function(i){addTriggerHandler(e,i,t,function(l,a){let c=asElement(l);if(eltIsDisabled(c)){cleanUpElement(c);return}issueAjaxRequest(o,s,c,a)})})}}),r}function addTriggerHandler(e,t,n,r){if(t.trigger==="revealed")initScrollHandler(),addEventListener(e,r,n,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(i){for(let l=0;l<i.length;l++)if(i[l].isIntersecting){triggerEvent(e,"intersect");break}},o).observe(asElement(e)),addEventListener(asElement(e),r,n,t)}else!n.firstInitCompleted&&t.trigger==="load"?maybeFilterEvent(t,e,makeEvent("load",{elt:e}))||loadImmediately(asElement(e),r,n,t.delay):t.pollInterval>0?(n.polling=!0,processPolling(asElement(e),r,t)):addEventListener(e,r,n,t)}function shouldProcessHxOn(e){let t=asElement(e);if(!t)return!1;let n=t.attributes;for(let r=0;r<n.length;r++){let o=n[r].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 n=HX_ON_QUERY.evaluate(e),r=null;for(;r=n.iterateNext();)t.push(asElement(r))}function findHxOnWildcardElements(e){let t=[];if(e instanceof DocumentFragment)for(let n of e.childNodes)processHXOnRoot(n,t);else processHXOnRoot(e,t);return t}function findElementsToProcess(e){if(e.querySelectorAll){let n=", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]",r=[];for(let s in extensions){let i=extensions[s];if(i.getSelectors){var t=i.getSelectors();t&&r.push(t)}}return e.querySelectorAll(VERB_SELECTOR+n+", form, [type='submit'], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"+r.flat().map(s=>", "+s).join(""))}else return[]}function maybeSetLastButtonClicked(e){let t=getTargetButton(e.target),n=getRelatedFormData(e);n&&(n.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 n=getRelatedForm(t);if(n)return getInternalData(n)}function initButtonTracking(e){e.addEventListener("click",maybeSetLastButtonClicked),e.addEventListener("focusin",maybeSetLastButtonClicked),e.addEventListener("focusout",maybeUnsetLastButtonClicked)}function addHxOnEventHandler(e,t,n){let r=getInternalData(e);Array.isArray(r.onHandlers)||(r.onHandlers=[]);let o,s=function(i){maybeEval(e,function(){eltIsDisabled(e)||(o||(o=new Function("event",n)),o.call(e,i))})};e.addEventListener(t,s),r.onHandlers.push({event:t,listener:s})}function processHxOnWildcard(e){deInitOnHandlers(e);for(let t=0;t<e.attributes.length;t++){let n=e.attributes[t].name,r=e.attributes[t].value;if(startsWith(n,"hx-on")||startsWith(n,"data-hx-on")){let o=n.indexOf("-on")+3,s=n.slice(o,o+1);if(s==="-"||s===":"){let i=n.slice(o+1);startsWith(i,":")?i="htmx"+i:startsWith(i,"-")?i="htmx:"+i.slice(1):startsWith(i,"htmx-")&&(i="htmx:"+i.slice(5)),addHxOnEventHandler(e,i,r)}}}}function initNode(e){triggerEvent(e,"htmx:beforeProcessNode");let t=getInternalData(e),n=getTriggerSpecs(e);processVerbs(e,t,n)||(getClosestAttributeValue(e,"hx-boost")==="true"?boostElement(e,t,n):hasAttribute(e,"hx-trigger")&&n.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),n=attributeHash(e);return t.initHash!==n?(deInitNode(e),t.initHash=n,!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(n){if(eltIsDisabled(n)){cleanUpElement(n);return}maybeDeInitAndHash(n)&&t.push(n)}),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,n){triggerEvent(e,t,mergeObjects({error:t},n))}function ignoreEventForLogging(e){return e==="htmx:afterProcessNode"}function withExtensions(e,t,n){forEach(getExtensions(e,[],n),function(r){try{t(r)}catch(o){logError(o)}})}function logError(e){console.error(e)}function triggerEvent(e,t,n){e=resolveTarget(e),n==null&&(n={}),n.elt=e;let r=makeEvent(t,n);htmx.logger&&!ignoreEventForLogging(t)&&htmx.logger(e,t,n),n.error&&(logError(n.error),triggerEvent(e,"htmx:error",{errorInfo:n}));let o=e.dispatchEvent(r),s=kebabEventName(t);if(o&&s!==t){let i=makeEvent(s,r.detail);o=o&&e.dispatchEvent(i)}return withExtensions(asElement(e),function(i){o=o&&i.onEvent(t,r)!==!1&&!r.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 n=cleanInnerHtmlForHistory(t),r=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 l=0;l<s.length;l++)if(s[l].url===e){s.splice(l,1);break}let i={url:e,content:n,title:r,scroll:o};for(triggerEvent(getDocument().body,"htmx:historyItemCreated",{item:i,cache:s}),s.push(i);s.length>htmx.config.historyCacheSize;)s.shift();for(;s.length>0;)try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(s));break}catch(l){triggerErrorEvent(getDocument().body,"htmx:historyCacheError",{cause:l,cache:s}),s.shift()}}function getCachedHistory(e){if(!canAccessLocalStorage())return null;e=normalizePath(e);let t=parseJSON(sessionStorage.getItem("htmx-history-cache"))||[];for(let n=0;n<t.length;n++)if(t[n].url===e)return t[n];return null}function cleanInnerHtmlForHistory(e){let t=htmx.config.requestClass,n=e.cloneNode(!0);return forEach(findAll(n,"."+t),function(r){removeClassFromElement(r,t)}),forEach(findAll(n,"[data-disabled-by-htmx]"),function(r){r.removeAttribute("disabled")}),n.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,n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0},r={path:e,xhr:t,historyElt:getHistoryElement(),swapSpec:n};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?(r.response=this.response,triggerEvent(getDocument().body,"htmx:historyCacheMissLoad",r),swap(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:!0}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",{path:e,cacheMiss:!0,serverResponse:r.response})):triggerErrorEvent(getDocument().body,"htmx:historyCacheMissLoadError",r)},triggerEvent(getDocument().body,"htmx:historyCacheMiss",r)&&t.send()}function restoreHistory(e){saveCurrentPageToHistory(),e=e||location.pathname+location.search;let t=getCachedHistory(e);if(t){let n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll},r={path:e,item:t,historyElt:getHistoryElement(),swapSpec:n};triggerEvent(getDocument().body,"htmx:historyCacheHit",r)&&(swap(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title}),setCurrentPathForHistory(r.path),triggerEvent(getDocument().body,"htmx:historyRestore",r))}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(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.classList.add.call(n.classList,htmx.config.requestClass)}),t}function disableElements(e){let t=findAttributeTargets(e,"hx-disabled-elt");return t==null&&(t=[]),forEach(t,function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||0)+1,n.setAttribute("disabled",""),n.setAttribute("data-disabled-by-htmx","")}),t}function removeRequestIndicators(e,t){forEach(e.concat(t),function(n){let r=getInternalData(n);r.requestCount=(r.requestCount||1)-1}),forEach(e,function(n){getInternalData(n).requestCount===0&&n.classList.remove.call(n.classList,htmx.config.requestClass)}),forEach(t,function(n){getInternalData(n).requestCount===0&&(n.removeAttribute("disabled"),n.removeAttribute("data-disabled-by-htmx"))})}function haveSeenNode(e,t){for(let n=0;n<e.length;n++)if(e[n].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,n){e!=null&&t!=null&&(Array.isArray(t)?t.forEach(function(r){n.append(e,r)}):n.append(e,t))}function removeValueFromFormData(e,t,n){if(e!=null&&t!=null){let r=n.getAll(e);Array.isArray(t)?r=r.filter(o=>t.indexOf(o)<0):r=r.filter(o=>o!==t),n.delete(e),forEach(r,o=>n.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,n,r,o){if(!(r==null||haveSeenNode(e,r))){if(e.push(r),shouldInclude(r)){let s=getRawAttribute(r,"name");addValueToFormData(s,getValueFromInput(r),t),o&&validateElement(r,n)}r instanceof HTMLFormElement&&(forEach(r.elements,function(s){e.indexOf(s)>=0?removeValueFromFormData(s.name,getValueFromInput(s),t):e.push(s),o&&validateElement(s,n)}),new FormData(r).forEach(function(s,i){s instanceof File&&s.name===""||addValueToFormData(i,s,t)}))}}function validateElement(e,t){let n=e;n.willValidate&&(triggerEvent(n,"htmx:validation:validate"),n.checkValidity()||(triggerEvent(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&htmx.config.reportValidityOfForms&&n.reportValidity(),t.push({elt:n,message:n.validationMessage,validity:n.validity})))}function overrideFormData(e,t){for(let n of t.keys())e.delete(n);return t.forEach(function(n,r){e.append(r,n)}),e}function getInputValues(e,t){let n=[],r=new FormData,o=new FormData,s=[],i=getInternalData(e);i.lastButtonClicked&&!bodyContains(i.lastButtonClicked)&&(i.lastButtonClicked=null);let l=e instanceof HTMLFormElement&&e.noValidate!==!0||getAttributeValue(e,"hx-validate")==="true";if(i.lastButtonClicked&&(l=l&&i.lastButtonClicked.formNoValidate!==!0),t!=="get"&&processInputValue(n,o,s,getRelatedForm(e),l),processInputValue(n,r,s,e,l),i.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&getRawAttribute(e,"type")==="submit"){let c=i.lastButtonClicked||e,d=getRawAttribute(c,"name");addValueToFormData(d,c.value,o)}let a=findAttributeTargets(e,"hx-include");return forEach(a,function(c){processInputValue(n,r,s,asElement(c),l),matches(c,"form")||forEach(asParentNode(c).querySelectorAll(INPUT_SELECTOR),function(d){processInputValue(n,r,s,d,l)})}),overrideFormData(r,o),{errors:s,formData:r,values:formDataProxy(r)}}function appendParam(e,t,n){e!==""&&(e+="&"),String(n)==="[object Object]"&&(n=JSON.stringify(n));let r=encodeURIComponent(n);return e+=encodeURIComponent(t)+"="+r,e}function urlEncode(e){e=formDataFromObject(e);let t="";return e.forEach(function(n,r){t=appendParam(t,r,n)}),t}function getHeaders(e,t,n){let r={"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,r),n!==void 0&&(r["HX-Prompt"]=n),getInternalData(e).boosted&&(r["HX-Boosted"]="true"),r}function filterValues(e,t){let n=getClosestAttributeValue(t,"hx-params");if(n){if(n==="none")return new FormData;if(n==="*")return e;if(n.indexOf("not ")===0)return forEach(n.slice(4).split(","),function(r){r=r.trim(),e.delete(r)}),e;{let r=new FormData;return forEach(n.split(","),function(o){o=o.trim(),e.has(o)&&e.getAll(o).forEach(function(s){r.append(o,s)})}),r}}else return e}function isAnchorLink(e){return!!getRawAttribute(e,"href")&&getRawAttribute(e,"href").indexOf("#")>=0}function getSwapSpecification(e,t){let n=t||getClosestAttributeValue(e,"hx-swap"),r={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)&&(r.show="top"),n){let i=splitOnWhitespace(n);if(i.length>0)for(let l=0;l<i.length;l++){let a=i[l];if(a.indexOf("swap:")===0)r.swapDelay=parseInterval(a.slice(5));else if(a.indexOf("settle:")===0)r.settleDelay=parseInterval(a.slice(7));else if(a.indexOf("transition:")===0)r.transition=a.slice(11)==="true";else if(a.indexOf("ignoreTitle:")===0)r.ignoreTitle=a.slice(12)==="true";else if(a.indexOf("scroll:")===0){var o=a.slice(7).split(":");let d=o.pop();var s=o.length>0?o.join(":"):null;r.scroll=d,r.scrollTarget=s}else if(a.indexOf("show:")===0){var o=a.slice(5).split(":");let u=o.pop();var s=o.length>0?o.join(":"):null;r.show=u,r.showTarget=s}else if(a.indexOf("focus-scroll:")===0){let c=a.slice(13);r.focusScroll=c=="true"}else l==0?r.swapStyle=a:logError("Unknown modifier in hx-swap: "+a)}}return r}function usesFormData(e){return getClosestAttributeValue(e,"hx-encoding")==="multipart/form-data"||matches(e,"form")&&getRawAttribute(e,"enctype")==="multipart/form-data"}function encodeParamsForBody(e,t,n){let r=null;return withExtensions(t,function(o){r==null&&(r=o.encodeParameters(e,n,t))}),r??(usesFormData(t)?overrideFormData(new FormData,formDataFromObject(n)):urlEncode(n))}function makeSettleInfo(e){return{tasks:[],elts:[e]}}function updateScrollState(e,t){let n=e[0],r=e[e.length-1];if(t.scroll){var o=null;t.scrollTarget&&(o=asElement(querySelectorExt(n,t.scrollTarget))),t.scroll==="top"&&(n||o)&&(o=o||n,o.scrollTop=0),t.scroll==="bottom"&&(r||o)&&(o=o||r,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 i=t.showTarget;t.showTarget==="window"&&(i="body"),o=asElement(querySelectorExt(n,i))}t.show==="top"&&(n||o)&&(o=o||n,o.scrollIntoView({block:"start",behavior:htmx.config.scrollBehavior})),t.show==="bottom"&&(r||o)&&(o=o||r,o.scrollIntoView({block:"end",behavior:htmx.config.scrollBehavior}))}}function getValuesForElement(e,t,n,r,o){if(r==null&&(r={}),e==null)return r;let s=getAttributeValue(e,t);if(s){let i=s.trim(),l=n;if(i==="unset")return null;i.indexOf("javascript:")===0?(i=i.slice(11),l=!0):i.indexOf("js:")===0&&(i=i.slice(3),l=!0),i.indexOf("{")!==0&&(i="{"+i+"}");let a;l?a=maybeEval(e,function(){return o?Function("event","return ("+i+")").call(e,o):Function("return ("+i+")").call(e)},{}):a=parseJSON(i);for(let c in a)a.hasOwnProperty(c)&&r[c]==null&&(r[c]=a[c])}return getValuesForElement(asElement(parentElt(e)),t,n,r,o)}function maybeEval(e,t,n){return htmx.config.allowEval?t():(triggerErrorEvent(e,"htmx:evalDisallowedError"),n)}function getHXVarsForElement(e,t,n){return getValuesForElement(e,"hx-vars",!0,n,t)}function getHXValsForElement(e,t,n){return getValuesForElement(e,"hx-vals",!1,n,t)}function getExpressionVars(e,t){return mergeObjects(getHXVarsForElement(e,t),getHXValsForElement(e,t))}function safelySetHeaderValue(e,t,n){if(n!==null)try{e.setRequestHeader(t,n)}catch{e.setRequestHeader(t,encodeURIComponent(n)),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,n){if(e=e.toLowerCase(),n){if(n instanceof Element||typeof n=="string")return issueAjaxRequest(e,t,null,null,{targetOverride:resolveTarget(n)||DUMMY_ELT,returnPromise:!0});{let r=resolveTarget(n.target);return(n.target&&!r||n.source&&!r&&!resolveTarget(n.source))&&(r=DUMMY_ELT),issueAjaxRequest(e,t,resolveTarget(n.source),n.event,{handler:n.handler,headers:n.headers,values:n.values,targetOverride:r,swapOverride:n.swap,select:n.select,returnPromise:!0,push:n.push,replace:n.replace,selectOOB:n.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,n){let r=new URL(t,location.protocol!=="about:"?location.href:window.origin),s=(location.protocol!=="about:"?location.origin:window.origin)===r.origin;return htmx.config.selfRequestsOnly&&!s?!1:triggerEvent(e,"htmx:validateUrl",mergeObjects({url:r,sameHost:s},n))}function formDataFromObject(e){if(e instanceof FormData)return e;let t=new FormData;for(let n in e)e.hasOwnProperty(n)&&(e[n]&&typeof e[n].forEach=="function"?e[n].forEach(function(r){t.append(n,r)}):typeof e[n]=="object"&&!(e[n]instanceof Blob)?t.append(n,JSON.stringify(e[n])):t.append(n,e[n]));return t}function formDataArrayProxy(e,t,n){return new Proxy(n,{get:function(r,o){return typeof o=="number"?r[o]:o==="length"?r.length:o==="push"?function(s){r.push(s),e.append(t,s)}:typeof r[o]=="function"?function(){r[o].apply(r,arguments),e.delete(t),r.forEach(function(s){e.append(t,s)})}:r[o]&&r[o].length===1?r[o][0]:r[o]},set:function(r,o,s){return r[o]=s,e.delete(t),r.forEach(function(i){e.append(t,i)}),!0}})}function formDataProxy(e){return new Proxy(e,{get:function(t,n){if(typeof n=="symbol"){let o=Reflect.get(t,n);return typeof o=="function"?function(){return o.apply(e,arguments)}:o}if(n==="toJSON")return()=>Object.fromEntries(e);if(n in t&&typeof t[n]=="function")return function(){return e[n].apply(e,arguments)};let r=e.getAll(n);if(r.length!==0)return r.length===1?r[0]:formDataArrayProxy(t,n,r)},set:function(t,n,r){return typeof n!="string"?!1:(t.delete(n),r&&typeof r.forEach=="function"?r.forEach(function(o){t.append(n,o)}):typeof r=="object"&&!(r instanceof Blob)?t.append(n,JSON.stringify(r)):t.append(n,r),!0)},deleteProperty:function(t,n){return typeof n=="string"&&t.delete(n),!0},ownKeys:function(t){return Reflect.ownKeys(Object.fromEntries(t))},getOwnPropertyDescriptor:function(t,n){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(t),n)}})}function issueAjaxRequest(e,t,n,r,o,s){let i=null,l=null;if(o=o??{},o.returnPromise&&typeof Promise<"u")var a=new Promise(function(g,b){i=g,l=b});n==null&&(n=getDocument().body);let c=o.handler||handleAjaxResponse,d=o.select||null;if(!bodyContains(n))return maybeCall(i),a;let u=o.targetOverride||asElement(getTarget(n));if(u==null||u==DUMMY_ELT)return triggerErrorEvent(n,"htmx:targetError",{target:getClosestAttributeValue(n,"hx-target")}),maybeCall(l),a;let f=getInternalData(n),m=f.lastButtonClicked;if(m){let g=getRawAttribute(m,"formaction");g!=null&&(t=g);let b=getRawAttribute(m,"formmethod");if(b!=null)if(VERBS.includes(b.toLowerCase()))e=b;else return maybeCall(i),a}let h=getClosestAttributeValue(n,"hx-confirm");if(s===void 0&&triggerEvent(n,"htmx:confirm",{target:u,elt:n,path:t,verb:e,triggeringEvent:r,etc:o,issueRequest:function(L){return issueAjaxRequest(e,t,n,r,o,!!L)},question:h})===!1)return maybeCall(i),a;let y=n,p=getClosestAttributeValue(n,"hx-sync"),w=null,T=!1;if(p){let g=p.split(":"),b=g[0].trim();if(b==="this"?y=findThisElement(n,"hx-sync"):y=asElement(querySelectorExt(n,b)),p=(g[1]||"drop").trim(),f=getInternalData(y),p==="drop"&&f.xhr&&f.abortable!==!0)return maybeCall(i),a;if(p==="abort"){if(f.xhr)return maybeCall(i),a;T=!0}else p==="replace"?triggerEvent(y,"htmx:abort"):p.indexOf("queue")===0&&(w=(p.split(" ")[1]||"last").trim())}if(f.xhr)if(f.abortable)triggerEvent(y,"htmx:abort");else{if(w==null){if(r){let g=getInternalData(r);g&&g.triggerSpec&&g.triggerSpec.queue&&(w=g.triggerSpec.queue)}w==null&&(w="last")}return f.queuedRequests==null&&(f.queuedRequests=[]),w==="first"&&f.queuedRequests.length===0?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):w==="all"?f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)}):w==="last"&&(f.queuedRequests=[],f.queuedRequests.push(function(){issueAjaxRequest(e,t,n,r,o)})),maybeCall(i),a}let v=new XMLHttpRequest;f.xhr=v,f.abortable=T;let E=function(){f.xhr=null,f.abortable=!1,f.queuedRequests!=null&&f.queuedRequests.length>0&&f.queuedRequests.shift()()},Q=getClosestAttributeValue(n,"hx-prompt");if(Q){var U=prompt(Q);if(U===null||!triggerEvent(n,"htmx:prompt",{prompt:U,target:u}))return maybeCall(i),E(),a}if(h&&!s&&!confirm(h))return maybeCall(i),E(),a;let I=getHeaders(n,u,U);e!=="get"&&!usesFormData(n)&&(I["Content-Type"]="application/x-www-form-urlencoded"),o.headers&&(I=mergeObjects(I,o.headers));let Z=getInputValues(n,e),q=Z.errors,ee=Z.formData;o.values&&overrideFormData(ee,formDataFromObject(o.values));let ve=formDataFromObject(getExpressionVars(n,r)),V=overrideFormData(ee,ve),H=filterValues(V,n);htmx.config.getCacheBusterParam&&e==="get"&&H.set("org.htmx.cache-buster",getRawAttribute(u,"id")||"true"),(t==null||t==="")&&(t=location.href);let j=getValuesForElement(n,"hx-request"),te=getInternalData(n).boosted,k=htmx.config.methodsThatUseUrlParams.indexOf(e)>=0,S={boosted:te,useUrlParams:k,formData:H,parameters:formDataProxy(H),unfilteredFormData:V,unfilteredParameters:formDataProxy(V),headers:I,elt:n,target:u,verb:e,errors:q,withCredentials:o.credentials||j.credentials||htmx.config.withCredentials,timeout:o.timeout||j.timeout||htmx.config.timeout,path:t,triggeringEvent:r};if(!triggerEvent(n,"htmx:configRequest",S))return maybeCall(i),E(),a;if(t=S.path,e=S.verb,I=S.headers,H=formDataFromObject(S.parameters),q=S.errors,k=S.useUrlParams,q&&q.length>0)return triggerEvent(n,"htmx:validation:halted",S),maybeCall(i),E(),a;let ne=t.split("#"),be=ne[0],W=ne[1],A=t;if(k&&(A=be,!H.keys().next().done&&(A.indexOf("?")<0?A+="?":A+="&",A+=urlEncode(H),W&&(A+="#"+W))),!verifyPath(n,A,S))return triggerErrorEvent(n,"htmx:invalidPath",S),maybeCall(l),E(),a;if(v.open(e.toUpperCase(),A,!0),v.overrideMimeType("text/html"),v.withCredentials=S.withCredentials,v.timeout=S.timeout,!j.noHeaders){for(let g in I)if(I.hasOwnProperty(g)){let b=I[g];safelySetHeaderValue(v,g,b)}}let x={xhr:v,target:u,requestConfig:S,etc:o,boosted:te,select:d,pathInfo:{requestPath:t,finalRequestPath:A,responsePath:null,anchor:W}};if(v.onload=function(){try{let g=hierarchyForElt(n);if(x.pathInfo.responsePath=getPathFromResponse(v),c(n,x),x.keepIndicators!==!0&&removeRequestIndicators(N,P),triggerEvent(n,"htmx:afterRequest",x),triggerEvent(n,"htmx:afterOnLoad",x),!bodyContains(n)){let b=null;for(;g.length>0&&b==null;){let L=g.shift();bodyContains(L)&&(b=L)}b&&(triggerEvent(b,"htmx:afterRequest",x),triggerEvent(b,"htmx:afterOnLoad",x))}maybeCall(i)}catch(g){throw triggerErrorEvent(n,"htmx:onLoadError",mergeObjects({error:g},x)),g}finally{E()}},v.onerror=function(){removeRequestIndicators(N,P),triggerErrorEvent(n,"htmx:afterRequest",x),triggerErrorEvent(n,"htmx:sendError",x),maybeCall(l),E()},v.onabort=function(){removeRequestIndicators(N,P),triggerErrorEvent(n,"htmx:afterRequest",x),triggerErrorEvent(n,"htmx:sendAbort",x),maybeCall(l),E()},v.ontimeout=function(){removeRequestIndicators(N,P),triggerErrorEvent(n,"htmx:afterRequest",x),triggerErrorEvent(n,"htmx:timeout",x),maybeCall(l),E()},!triggerEvent(n,"htmx:beforeRequest",x))return maybeCall(i),E(),a;var N=addRequestIndicatorClasses(n),P=disableElements(n);forEach(["loadstart","loadend","progress","abort"],function(g){forEach([v,v.upload],function(b){b.addEventListener(g,function(L){triggerEvent(n,"htmx:xhr:"+g,{lengthComputable:L.lengthComputable,loaded:L.loaded,total:L.total})})})}),triggerEvent(n,"htmx:beforeSend",x);let we=k?null:encodeParamsForBody(v,n,H);return v.send(we),a}function determineHistoryUpdates(e,t){let n=t.xhr,r=null,o=null;if(hasHeader(n,/HX-Push:/i)?(r=n.getResponseHeader("HX-Push"),o="push"):hasHeader(n,/HX-Push-Url:/i)?(r=n.getResponseHeader("HX-Push-Url"),o="push"):hasHeader(n,/HX-Replace-Url:/i)&&(r=n.getResponseHeader("HX-Replace-Url"),o="replace"),r)return r==="false"?{}:{type:o,path:r};let s=t.pathInfo.finalRequestPath,i=t.pathInfo.responsePath,l=t.etc.push||getClosestAttributeValue(e,"hx-push-url"),a=t.etc.replace||getClosestAttributeValue(e,"hx-replace-url"),c=getInternalData(e).boosted,d=null,u=null;return l?(d="push",u=l):a?(d="replace",u=a):c&&(d="push",u=i||s),u?u==="false"?{}:(u==="true"&&(u=i||s),t.pathInfo.anchor&&u.indexOf("#")===-1&&(u=u+"#"+t.pathInfo.anchor),{type:d,path:u}):{}}function codeMatches(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function resolveResponseHandling(e){for(var t=0;t<htmx.config.responseHandling.length;t++){var n=htmx.config.responseHandling[t];if(codeMatches(n,e.status))return n}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 n=asElement(querySelectorExt(e,t));if(n==null)throw triggerErrorEvent(e,"htmx:targetError",{target:t}),new Error(`Invalid re-target ${t}`);return n}function handleAjaxResponse(e,t){let n=t.xhr,r=t.target,o=t.etc,s=t.select;if(!triggerEvent(e,"htmx:beforeOnLoad",t))return;if(hasHeader(n,/HX-Trigger:/i)&&handleTriggerHeader(n,"HX-Trigger",e),hasHeader(n,/HX-Location:/i)){let T=n.getResponseHeader("HX-Location");var i={};T.indexOf("{")===0&&(i=parseJSON(T),T=i.path,delete i.path),i.push=i.push||"true",ajaxHelper("get",T,i);return}let l=hasHeader(n,/HX-Refresh:/i)&&n.getResponseHeader("HX-Refresh")==="true";if(hasHeader(n,/HX-Redirect:/i)){t.keepIndicators=!0,htmx.location.href=n.getResponseHeader("HX-Redirect"),l&&htmx.location.reload();return}if(l){t.keepIndicators=!0,htmx.location.reload();return}let a=determineHistoryUpdates(e,t),c=resolveResponseHandling(n),d=c.swap,u=!!c.error,f=htmx.config.ignoreTitle||c.ignoreTitle,m=c.select;c.target&&(t.target=resolveRetarget(e,c.target));var h=o.swapOverride;h==null&&c.swapOverride&&(h=c.swapOverride),hasHeader(n,/HX-Retarget:/i)&&(t.target=resolveRetarget(e,n.getResponseHeader("HX-Retarget"))),hasHeader(n,/HX-Reswap:/i)&&(h=n.getResponseHeader("HX-Reswap"));var y=n.response,p=mergeObjects({shouldSwap:d,serverResponse:y,isError:u,ignoreTitle:f,selectOverride:m,swapOverride:h},t);if(!(c.event&&!triggerEvent(r,c.event,p))&&triggerEvent(r,"htmx:beforeSwap",p)){if(r=p.target,y=p.serverResponse,u=p.isError,f=p.ignoreTitle,m=p.selectOverride,h=p.swapOverride,t.target=r,t.failed=u,t.successful=!u,p.shouldSwap){n.status===286&&cancelPolling(e),withExtensions(e,function(E){y=E.transformResponse(y,n,e)}),a.type&&saveCurrentPageToHistory();var w=getSwapSpecification(e,h);w.hasOwnProperty("ignoreTitle")||(w.ignoreTitle=f),r.classList.add(htmx.config.swappingClass),s&&(m=s),hasHeader(n,/HX-Reselect:/i)&&(m=n.getResponseHeader("HX-Reselect"));let T=o.selectOOB||getClosestAttributeValue(e,"hx-select-oob"),v=getClosestAttributeValue(e,"hx-select");swap(r,y,w,{select:m==="unset"?null:m||v,selectOOB:T,eventInfo:t,anchor:t.pathInfo.anchor,contextElement:e,afterSwapCallback:function(){if(hasHeader(n,/HX-Trigger-After-Swap:/i)){let E=e;bodyContains(e)||(E=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Swap",E)}},afterSettleCallback:function(){if(hasHeader(n,/HX-Trigger-After-Settle:/i)){let E=e;bodyContains(e)||(E=getDocument().body),handleTriggerHeader(n,"HX-Trigger-After-Settle",E)}},beforeSwapCallback:function(){a.type&&(triggerEvent(getDocument().body,"htmx:beforeHistoryUpdate",mergeObjects({history:a},t)),a.type==="push"?(pushUrlIntoHistory(a.path),triggerEvent(getDocument().body,"htmx:pushedIntoHistory",{path:a.path})):(replaceUrlInHistory(a.path),triggerEvent(getDocument().body,"htmx:replacedInHistory",{path:a.path})))}})}u&&triggerErrorEvent(e,"htmx:responseError",mergeObjects({error:"Response Status Error Code "+n.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,n){return e},isInlineSwap:function(e){return!1},handleSwap:function(e,t,n,r){return!1},encodeParameters:function(e,t,n){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,n){if(t==null&&(t=[]),e==null)return t;n==null&&(n=[]);let r=getAttributeValue(e,"hx-ext");return r&&forEach(r.split(","),function(o){if(o=o.replace(/ /g,""),o.slice(0,7)=="ignore:"){n.push(o.slice(7));return}if(n.indexOf(o)<0){let s=extensions[o];s&&t.indexOf(s)<0&&t.push(s)}}),getExtensions(asElement(parentElt(e)),t,n)}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,n=htmx.config.requestClass;getDocument().head.insertAdjacentHTML("beforeend",`<style${e}>.${t}{opacity:0;visibility: hidden} .${n} .${t}, .${n}.${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(r){let o=r.detail.elt||r.target,s=getInternalData(o);s&&s.xhr&&s.xhr.abort()});let n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(r){r.state&&r.state.htmx?(restoreHistory(),forEach(t,function(o){triggerEvent(o,"htmx:restored",{document:getDocument(),triggerEvent})})):n&&n(r)},getWindow().setTimeout(function(){triggerEvent(e,"htmx:load",{}),e=null},0)}),htmx})(),D=xe;(function(){let e;D.defineExtension("json-enc",{init:function(t){e=t},onEvent:function(t,n){t==="htmx:configRequest"&&(n.detail.headers["Content-Type"]="application/json")},encodeParameters:function(t,n,r){t.overrideMimeType("text/json");let o={};n.forEach(function(i,l){Object.hasOwn(o,l)?(Array.isArray(o[l])||(o[l]=[o[l]]),o[l].push(i)):o[l]=i});let s=e.getExpressionVars(r);return Object.keys(o).forEach(function(i){o[i]=Object.hasOwn(s,i)?s[i]:o[i]}),JSON.stringify(o)}})})();var re="https://typeahead.waow.tech",se="https://public.api.bsky.app",Te="/xrpc/app.bsky.actor.searchActorsTypeahead",Se="/xrpc/app.bsky.actor.getProfiles";var Ce="atcr_recent_handles",ie="atcr_recent_profile_cache";var z=class{constructor(t){this.input=t,this.container=t.closest(".sailor-typeahead")||t.parentElement,this.dropdown=null,this.selectedCard=null,this.actors=[],this.currentItems=[],this.mode="hidden",this.focusIndex=-1,this.debounceTimer=null,this.requestSeq=0,this.primaryUnhealthyUntil=0,this.lastPrefetchPrefix="",this.lastPrefetchAt=0,this.createDropdown(),this.bindEvents(),this.input.value.trim().length===0&&this.showRecent()}createDropdown(){this.dropdown=document.createElement("div"),this.dropdown.className="sailor-typeahead-dropdown",this.dropdown.setAttribute("role","listbox"),this.dropdown.style.display="none",this.input.insertAdjacentElement("afterend",this.dropdown)}bindEvents(){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.hide()}),document.addEventListener("keydown",t=>{t.key==="Escape"&&this.selectedCard&&this.clearSelection()})}handleFocus(){this.input.value.trim().length===0&&this.showRecent()}handleInput(){let t=this.input.value.trim();if(t.length===0){this.showRecent();return}if(t.length>=2&&t.length<4){this.hide(),this.schedulePrefetch(t);return}if(t.length>=4){this.scheduleSearch(t);return}this.hide()}schedulePrefetch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runPrefetch(t),150)}scheduleSearch(t){clearTimeout(this.debounceTimer),this.debounceTimer=setTimeout(()=>this.runSearch(t),150)}async runPrefetch(t){let n=Date.now();if(!(t===this.lastPrefetchPrefix&&n-this.lastPrefetchAt<1e4)&&!(n<this.primaryUnhealthyUntil)){this.lastPrefetchPrefix=t,this.lastPrefetchAt=n;try{await X(re,t,400)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}}}async runSearch(t){let n=++this.requestSeq,r=null;if(Date.now()>=this.primaryUnhealthyUntil)try{r=await X(re,t,1500)}catch{this.primaryUnhealthyUntil=Date.now()+6e4}if(r===null)try{r=await X(se,t,1500)}catch{r=[]}n===this.requestSeq&&(this.actors=r||[],this.focusIndex=-1,this.renderResults())}renderResults(){if(this.mode="results",this.dropdown.innerHTML="",this.currentItems=[],this.actors.length===0){this.hide();return}this.actors.forEach((t,n)=>{this.currentItems.push(t),this.dropdown.appendChild(this.buildActorRow(t,n))}),this.dropdown.style.display="block"}buildActorRow(t,n){let r=document.createElement("div");r.className="sailor-typeahead-item",r.setAttribute("role","option"),r.dataset.index=String(n),r.dataset.handle=t.handle;let o=document.createElement("div");if(o.className="sailor-typeahead-avatar",t.avatar){let a=document.createElement("img");a.src=t.avatar,a.alt="",a.loading="lazy",o.appendChild(a)}let s=document.createElement("div");s.className="sailor-typeahead-text";let i=t.displayName&&t.displayName!==t.handle;if(i){let a=document.createElement("div");a.className="sailor-typeahead-name",a.textContent=t.displayName,s.appendChild(a)}let l=document.createElement("div");return l.className=i?"sailor-typeahead-handle":"sailor-typeahead-name",l.textContent="@"+t.handle,s.appendChild(l),r.append(o,s),r.addEventListener("mousedown",a=>{a.preventDefault(),this.select(t)}),r}showRecent(){let t=Le();if(t.length===0){this.hide();return}this.mode="recent",this.focusIndex=-1,this.renderRecent(t),this.enrichRecent(t)}renderRecent(t){let n=F();this.dropdown.innerHTML="",this.currentItems=[];let r=document.createElement("div");r.className="sailor-typeahead-header",r.textContent="Recent accounts",this.dropdown.appendChild(r),t.forEach((o,s)=>{let i=n[o]?.profile||{handle:o};this.currentItems.push(i),this.dropdown.appendChild(this.buildActorRow(i,s))}),this.dropdown.style.display="block"}async enrichRecent(t){let n=F(),r=Date.now(),o=t.filter(l=>{let a=n[l];return!a||r-a.ts>864e5});if(o.length===0)return;let s=await Ae(o);if(s.length===0)return;let i=F();s.forEach(l=>{i[l.handle]={ts:r,profile:{handle:l.handle,displayName:l.displayName,avatar:l.avatar}}}),oe(i),this.mode==="recent"&&this.renderRecent(t)}hide(){this.mode="hidden",this.focusIndex=-1,this.dropdown.style.display="none"}select(t){if(typeof t=="string"&&(t={handle:t}),this.input.value=t.handle,this.hide(),this.showSelectedCard(t),t.handle){let n=F();n[t.handle]={ts:Date.now(),profile:{handle:t.handle,displayName:t.displayName,avatar:t.avatar}},oe(n)}}showSelectedCard(t){this.clearSelectedCard();let n=document.createElement("div");n.className="sailor-typeahead-selected";let r=document.createElement("div");if(r.className="sailor-typeahead-avatar",t.avatar){let a=document.createElement("img");a.src=t.avatar,a.alt="",r.appendChild(a)}let o=document.createElement("div");o.className="sailor-typeahead-text";let s=t.displayName&&t.displayName!==t.handle;if(s){let a=document.createElement("div");a.className="sailor-typeahead-name",a.textContent=t.displayName,o.appendChild(a)}let i=document.createElement("div");i.className=s?"sailor-typeahead-handle":"sailor-typeahead-name",i.textContent="@"+t.handle,o.appendChild(i);let l=document.createElement("button");l.type="button",l.className="sailor-typeahead-clear",l.tabIndex=-1,l.setAttribute("aria-label","Change account"),l.innerHTML="&times;",l.addEventListener("click",()=>this.clearSelection()),n.append(r,o,l),this.input.style.display="none",this.input.insertAdjacentElement("beforebegin",n),this.selectedCard=n}clearSelectedCard(){this.selectedCard&&(this.selectedCard.remove(),this.selectedCard=null)}clearSelection(){this.clearSelectedCard(),this.input.style.display="",this.input.value="",this.input.focus(),this.showRecent()}handleKeydown(t){if(this.mode==="hidden")return;let n=this.dropdown.querySelectorAll(".sailor-typeahead-item");n.length!==0&&(t.key==="ArrowDown"?(t.preventDefault(),this.focusIndex=(this.focusIndex+1)%n.length,this.updateFocus(n)):t.key==="ArrowUp"?(t.preventDefault(),this.focusIndex=this.focusIndex<=0?n.length-1:this.focusIndex-1,this.updateFocus(n)):t.key==="Enter"?this.focusIndex>=0&&this.currentItems[this.focusIndex]&&(t.preventDefault(),this.select(this.currentItems[this.focusIndex])):t.key==="Escape"?this.hide():t.key==="Tab"&&this.focusIndex===-1&&n.length>0&&(t.preventDefault(),this.focusIndex=0,this.updateFocus(n)))}updateFocus(t){t.forEach((n,r)=>{n.classList.toggle("focused",r===this.focusIndex),r===this.focusIndex&&n.scrollIntoView({block:"nearest"})})}};async function X(e,t,n){let r=new URL(Te,e);r.searchParams.set("q",t),r.searchParams.set("limit",String(8));let o=new AbortController,s=setTimeout(()=>o.abort(),n);try{let i=await fetch(r,{signal:o.signal});if(!i.ok)throw new Error("HTTP "+i.status);let l=await i.json();return Array.isArray(l.actors)?l.actors:[]}finally{clearTimeout(s)}}async function Ae(e){if(e.length===0)return[];let t=new URL(Se,se);e.forEach(o=>t.searchParams.append("actors",o));let n=new AbortController,r=setTimeout(()=>n.abort(),3e3);try{let o=await fetch(t,{signal:n.signal});if(!o.ok)return[];let s=await o.json();return Array.isArray(s.profiles)?s.profiles:[]}catch{return[]}finally{clearTimeout(r)}}function F(){try{return JSON.parse(localStorage.getItem(ie)||"{}")}catch{return{}}}function oe(e){try{localStorage.setItem(ie,JSON.stringify(e))}catch{}}function Le(){try{let e=localStorage.getItem(Ce);return e?JSON.parse(e):[]}catch{return[]}}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("handle");e&&new z(e)});function ue(){return localStorage.getItem("theme")||"system"}function Ie(e){return e==="dark"||e==="light"?e:window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function J(){let e=ue(),n=Ie(e)==="dark";document.documentElement.classList.toggle("dark",n),document.documentElement.setAttribute("data-theme",n?"dark":"light"),He(e)}function de(e){localStorage.setItem("theme",e),J(),De()}function He(e){let t={system:"sun-moon",light:"sun",dark:"moon"};document.querySelectorAll("[data-theme-icon] use").forEach(n=>{n.setAttribute("href",`/icons.svg#${t[e]||"sun-moon"}`)}),document.querySelectorAll(".theme-option").forEach(n=>{let r=n.dataset.value===e;n.setAttribute("aria-checked",r?"true":"false");let o=n.querySelector(".theme-check");o&&(o.style.visibility=r?"visible":"hidden")})}function De(){document.querySelectorAll("[data-theme-toggle]").forEach(e=>{let t=e.closest("details");t&&t.removeAttribute("open")})}window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{ue()==="system"&&J()});function fe(e,t){if(!e)return;let n=e.querySelector(".nav-search-form"),r=e.querySelector('button[aria-controls="nav-search-form"]');e.classList.toggle("expanded",t),n&&(t?n.removeAttribute("inert"):n.setAttribute("inert","")),r&&r.setAttribute("aria-expanded",t?"true":"false")}function Re(){let e=document.querySelector(".nav-search-wrapper");if(!e)return;let t=!e.classList.contains("expanded");if(fe(e,t),t){let n=document.getElementById("nav-search-input");n&&n.focus()}}function ae(){fe(document.querySelector(".nav-search-wrapper"),!1)}document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelector(".nav-search-wrapper"),t=document.getElementById("nav-search-input");!e||!t||(document.addEventListener("keydown",n=>{if(n.key==="Escape"&&e.classList.contains("expanded")&&ae(),n.key==="/"&&!e.classList.contains("expanded")){let r=n.target.tagName;if(r==="INPUT"||r==="TEXTAREA"||n.target.isContentEditable)return;n.preventDefault(),e.classList.add("expanded"),t.focus()}}),document.addEventListener("click",n=>{e.classList.contains("expanded")&&!e.contains(n.target)&&ae()}))});function $(e,t){let n=()=>{if(!t)return;let r=t.innerHTML;t.innerHTML='<svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#check"></use></svg> Copied!',setTimeout(()=>{t.innerHTML=r},2e3)};if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(e).then(n).catch(r=>{console.error("Clipboard API failed, falling back:",r),le(e)?n():C("Copy failed \u2014 check browser permissions","error")});return}le(e)?n():C("Copy failed \u2014 select the text and copy manually","error")}function le(e){let t=document.createElement("textarea");t.value=e,t.setAttribute("readonly",""),t.setAttribute("aria-hidden","true"),t.style.position="fixed",t.style.top="0",t.style.left="0",t.style.width="1px",t.style.height="1px",t.style.opacity="0",t.style.pointerEvents="none",document.body.appendChild(t);let n=!1;try{t.focus(),t.select(),t.setSelectionRange(0,e.length),n=document.execCommand&&document.execCommand("copy")}catch{n=!1}return document.body.removeChild(t),!!n}function Oe(e){let t=s=>{let i=(s==null?"":String(s)).replace(/\s+/g," ").trim();return/[",\n\r]/.test(i)?'"'+i.replace(/"/g,'""')+'"':i},n=s=>Array.from(s).map(i=>t(i.textContent)).join(","),r=[],o=e.querySelector("thead tr");return o&&r.push(n(o.querySelectorAll("th,td"))),e.querySelectorAll("tbody tr").forEach(s=>{r.push(n(s.querySelectorAll("td,th")))}),r.join(` 2 + `)}document.addEventListener("DOMContentLoaded",()=>{document.addEventListener("click",e=>{let t=e.target.closest("button[data-copy-csv]");if(t){let r=t.closest("[data-csv-section]"),o=r&&r.querySelector("table");o&&$(Oe(o),t);return}let n=e.target.closest("button[data-cmd]");if(n){$(n.getAttribute("data-cmd"),n);return}})});function Me(e){let t=Math.floor((new Date-new Date(e))/1e3),n={year:31536e3,month:2592e3,week:604800,day:86400,hour:3600,minute:60,second:1};for(let[r,o]of Object.entries(n)){let s=Math.floor(t/o);if(s>=1)return s===1?`1 ${r} ago`:`${s} ${r}s ago`}return"just now"}function B(){document.querySelectorAll("time[datetime]").forEach(e=>{let t=e.getAttribute("datetime");if(t&&!e.dataset.noUpdate){let n=Me(t);e.textContent!==n&&(e.textContent=n)}})}document.addEventListener("DOMContentLoaded",()=>{B(),J(),document.querySelectorAll("[data-theme-menu]").forEach(e=>{e.querySelectorAll(".theme-option").forEach(t=>{t.addEventListener("click",()=>{de(t.dataset.value)})})}),document.addEventListener("click",e=>{let t=e.target.closest("details.dropdown");document.querySelectorAll("details.dropdown[open]").forEach(n=>{n!==t&&n.removeAttribute("open")})})});document.addEventListener("htmx:afterSwap",B);var R=null;function he(){R===null&&(R=setInterval(B,6e4))}function qe(){R!==null&&(clearInterval(R),R=null)}document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?qe():(B(),he())});he();async function ke(e,t,n){try{let r=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!1})});if(r.status===409){let o=await r.json();Ne(e,t,n,o.tags)}else if(r.ok)me(n);else{let o=await r.text();C(`Failed to delete manifest: ${o||r.status}`,"error")}}catch(r){console.error("Error deleting manifest:",r),C(`Error deleting manifest: ${r.message}`,"error")}}function Ne(e,t,n,r){let o=document.getElementById("manifest-delete-modal"),s=document.getElementById("manifest-delete-tags"),i=document.getElementById("confirm-manifest-delete-btn");s.innerHTML="",r.forEach(l=>{let a=document.createElement("li");a.textContent=l,s.appendChild(a)}),i.onclick=()=>Pe(e,t,n),K(o)}function Y(){O(document.getElementById("manifest-delete-modal"))}async function Pe(e,t,n){let r=document.getElementById("confirm-manifest-delete-btn"),o=r.textContent;try{r.disabled=!0,r.textContent="Deleting...";let s=await fetch("/api/manifests",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e,digest:t,confirm:!0})});if(s.ok)Y(),me(n),location.reload();else{let i=await s.text();C(`Failed to delete manifest: ${i||s.status}`,"error"),r.disabled=!1,r.textContent=o}}catch(s){console.error("Error deleting manifest:",s),C(`Error deleting manifest: ${s.message}`,"error"),r.disabled=!1,r.textContent=o}}async function Fe(e){let t=document.getElementById("confirm-untagged-delete-btn"),n=t.textContent;try{t.disabled=!0,t.textContent="Deleting...";let r=await fetch("/api/manifests/untagged",{method:"DELETE",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:e})}),o=await r.json();r.ok?(O(document.getElementById("untagged-delete-modal")),C(`Deleted ${o.deleted} untagged manifest(s)`,"success"),o.deleted>0&&location.reload(),t.disabled=!1,t.textContent=n):(C(`Failed to delete untagged manifests: ${o.error||"Unknown error"}`,"error"),t.disabled=!1,t.textContent=n)}catch(r){console.error("Error deleting untagged manifests:",r),C(`Error: ${r.message}`,"error"),t.disabled=!1,t.textContent=n}}function me(e){let t=document.getElementById(`manifest-${e}`);t&&t.remove()}document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("manifest-delete-modal");e&&e.addEventListener("click",t=>{t.target===e&&Y()})});var M=new WeakMap;function K(e,t){if(e&&(M.set(e,t||document.activeElement),typeof e.showModal=="function")){e.open&&(e.open=!1);try{e.showModal()}catch{}}}function O(e,{remove:t=!1}={}){if(!e)return;let n=M.get(e);if(M.delete(e),typeof e.close=="function"&&e.open)try{e.close()}catch{}t&&e.remove(),ge(n)}function ge(e){e&&typeof e.focus=="function"&&document.contains(e)&&e.focus()}document.addEventListener("close",e=>{let t=e.target;if(!(t instanceof HTMLDialogElement))return;let n=M.get(t);M.delete(t),ge(n)},!0);document.body.addEventListener("htmx:afterSettle",()=>{document.querySelectorAll("dialog.modal-open:not([data-modal-promoted]), dialog[open]:not([data-modal-promoted])").forEach(t=>{t.dataset.modalPromoted="1",K(t)})});document.addEventListener("change",e=>{let t=e.target.closest("select[data-diff-url]");if(!t)return;let n=t.dataset.diffUrl;n&&(window.location.href=n.replace("__VALUE__",encodeURIComponent(t.value)))});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("pull-cmd-container");if(!e)return;let t=e.dataset.registryUrl,n=e.dataset.ownerHandle,r=e.dataset.repoName,o=e.dataset.tag||"latest",s=e.dataset.isLoggedIn==="true";function i(a){let d=(a==="none"?"":a+" pull ")+t+"/"+n+"/"+r+":"+o,u=document.getElementById("pull-cmd-display");if(!u)return;let f=u.querySelector("code");f&&(f.textContent=d);let m=u.querySelector("[data-cmd]");m&&(m.dataset.cmd=d),s&&window.htmx?window.htmx.ajax("POST","/api/profile/oci-client",{values:{oci_client:a},swap:"none"}):s||localStorage.setItem("oci-client",a)}if(!s){let a=localStorage.getItem("oci-client");if(a){let c=document.getElementById("oci-client-switcher");c&&(c.value=a,i(a))}}let l=document.getElementById("oci-client-switcher");l&&l.addEventListener("change",()=>i(l.value))});document.addEventListener("DOMContentLoaded",()=>{let e=document.querySelectorAll(".platform-tab[data-platform]");e.length&&e.forEach(t=>{t.addEventListener("click",()=>{e.forEach(r=>{let o=r===t;r.classList.toggle("btn-primary",o),r.classList.toggle("btn-ghost",!o)}),document.querySelectorAll(".platform-content").forEach(r=>r.classList.add("hidden"));let n=document.getElementById(t.dataset.platform+"-content");n&&n.classList.remove("hidden")})})});document.addEventListener("DOMContentLoaded",()=>{let e=document.getElementById("login-form");e&&e.addEventListener("submit",()=>{let t=e.querySelector('button[type="submit"]');!t||t.disabled||(t.disabled=!0,t.innerHTML='<span class="loading loading-spinner loading-sm align-middle"></span> Navigating&hellip;')})});document.addEventListener("DOMContentLoaded",()=>{let e=document.cookie.split("; ").find(n=>n.startsWith("atcr_login_handle="));if(!e)return;let t=decodeURIComponent(e.split("=")[1]);if(t){try{let n="atcr_recent_handles",r=JSON.parse(localStorage.getItem(n)||"[]");r=r.filter(o=>o!==t),r.unshift(t),r=r.slice(0,5),localStorage.setItem(n,JSON.stringify(r))}catch(n){console.error("Failed to save recent account:",n)}document.cookie="atcr_login_handle=; path=/; max-age=0"}});function ce(){let e=document.getElementById("featured-carousel"),t=document.getElementById("carousel-prev"),n=document.getElementById("carousel-next");if(!e)return;let r=e.querySelectorAll(".carousel-item");if(r.length===0)return;let o=null,s=5e3,i=0,l=0;function a(){let m=parseFloat(getComputedStyle(e).gap)||24;i=r[0].offsetWidth+m}a(),window.addEventListener("resize",()=>{cancelAnimationFrame(l),l=requestAnimationFrame(a)}),document.body.addEventListener("htmx:afterSettle",m=>{m.target&&m.target.contains&&m.target.contains(e)&&a()});function c(){let m=e.scrollWidth-e.clientWidth;e.scrollLeft>=m-10?e.scrollTo({left:0,behavior:"smooth"}):e.scrollBy({left:i,behavior:"smooth"})}function d(){e.scrollLeft<=10?e.scrollTo({left:e.scrollWidth,behavior:"smooth"}):e.scrollBy({left:-i,behavior:"smooth"})}function u(){o||document.visibilityState!=="hidden"&&(e.scrollWidth<=e.clientWidth+10||(o=setInterval(c,s)))}function f(){o&&(clearInterval(o),o=null)}t&&t.addEventListener("click",()=>{f(),d(),u()}),n&&n.addEventListener("click",()=>{f(),c(),u()}),e.addEventListener("mouseenter",f),e.addEventListener("mouseleave",u),document.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?f():u()}),u()}document.addEventListener("DOMContentLoaded",()=>{"requestIdleCallback"in window?requestIdleCallback(ce,{timeout:2e3}):setTimeout(ce,100)});var Be=4,_e=1500;function C(e,t){let n=document.getElementById("toast-container");n||(n=document.createElement("div"),n.id="toast-container",n.className="toast toast-end toast-bottom z-50",n.setAttribute("aria-live","polite"),n.setAttribute("aria-atomic","false"),document.body.appendChild(n));let r=(t||"info")+"|"+e,o=Date.now(),s=n.querySelector(`[data-toast-key="${Ve(r)}"]`);if(s&&o-Number(s.dataset.toastAt)<_e){Ue(s);return}let i=t==="error",l=i?"alert-error":"alert-success",a=document.createElement("div");a.className=`alert ${l} shadow-lg transition-opacity duration-300`,a.style.willChange="opacity",a.setAttribute("role",i?"alert":"status"),a.dataset.toastKey=r,a.dataset.toastAt=String(o);let c=document.createElement("span");for(c.textContent=e,a.appendChild(c),n.appendChild(a);n.children.length>Be;)n.firstElementChild.remove();pe(a)}function pe(e){e._dismissTimer=setTimeout(()=>{e.style.opacity="0",e._removeTimer=setTimeout(()=>e.remove(),300)},3e3)}function Ue(e){clearTimeout(e._dismissTimer),clearTimeout(e._removeTimer),e.style.opacity="",e.dataset.toastAt=String(Date.now()),pe(e)}function Ve(e){return window.CSS&&CSS.escape?CSS.escape(e):String(e).replace(/[^a-zA-Z0-9_-]/g,t=>"\\"+t)}async function je(e){try{let t=await fetch(`/api/webhooks/${e}/test`,{method:"POST",credentials:"include"}),n=await t.text();n.includes('class="success"')||t.ok&&!n.includes('class="error"')?C("Test webhook delivered successfully!","success"):C("Test delivery failed \u2014 check the webhook URL","error")}catch{C("Failed to reach server","error")}}(function(){let t={"switch-repo-tab":s=>window.switchRepoTab&&window.switchRepoTab(s.dataset.tab),"switch-editor-tab":s=>window.switchEditorTab&&window.switchEditorTab(s.dataset.tab),"insert-md":s=>window.insertMd&&window.insertMd(s.dataset.mdType),"toggle-editor":s=>window.toggleOverviewEditor&&window.toggleOverviewEditor(s.dataset.show==="true"),"show-modal":s=>K(document.getElementById(s.dataset.modalId),s),"close-dialog":s=>O(s.closest("dialog")),"remove-closest-dialog":s=>O(s.closest("dialog"),{remove:!0}),"close-manifest-delete-modal":()=>window.closeManifestDeleteModal&&window.closeManifestDeleteModal(),"save-overview":()=>window.saveOverview&&window.saveOverview(),"delete-manifest":s=>window.deleteManifest&&window.deleteManifest(s.dataset.repo,s.dataset.digest,s.dataset.manifestId||""),"delete-untagged":s=>window.deleteUntaggedManifests&&window.deleteUntaggedManifests(s.dataset.repo),copy:s=>window.copyToClipboard&&window.copyToClipboard(s.dataset.copy,s),"toggle-search":()=>window.toggleSearch&&window.toggleSearch(),"switch-settings-tab":s=>window.switchSettingsTab&&window.switchSettingsTab(s.dataset.tab),"test-webhook":s=>window.testWebhook&&window.testWebhook(s.dataset.webhookId),"diff-to":(s,i)=>window.diffToTag&&window.diffToTag(i,s),"modal-backdrop-close":(s,i)=>{i.target===s&&O(s,{remove:!0})}},n={"sort-tags":s=>window.sortTags&&window.sortTags(s.value),"submit-form":s=>s.form&&s.form.requestSubmit()},r={"filter-tags":s=>window.filterTags&&window.filterTags(s.value)};function o(s,i){let l=i.target.closest("[data-action]");if(!l)return;let a=s[l.dataset.action];a&&a(l,i)}document.addEventListener("click",s=>o(t,s)),document.addEventListener("change",s=>o(n,s)),document.addEventListener("input",s=>o(r,s))})();window.setTheme=de;window.toggleSearch=Re;window.copyToClipboard=$;window.deleteManifest=ke;window.deleteUntaggedManifests=Fe;window.closeManifestDeleteModal=Y;window.showToast=C;window.testWebhook=je;function We(){let e=document.getElementById("md-editor");if(!e)return;let t=e.dataset.ownerDid,n=e.dataset.repository;window.toggleOverviewEditor=function(r){document.getElementById("overview-view").classList.toggle("hidden",r),document.getElementById("overview-edit").classList.toggle("hidden",!r),r&&e.focus()},window.switchEditorTab=function(r){if(document.querySelectorAll(".editor-panel").forEach(o=>o.classList.add("hidden")),document.getElementById(r==="write"?"editor-write":"editor-preview").classList.remove("hidden"),document.querySelectorAll(".editor-tab").forEach(o=>{let s=o.dataset.tab===r;o.classList.toggle("border-primary",s),o.classList.toggle("text-primary",s),o.classList.toggle("border-transparent",!s),o.classList.toggle("text-base-content/60",!s)}),r==="preview"){let o=e.value,s=document.getElementById("preview-content");if(!o.trim()){s.innerHTML='<p class="text-base-content/60">Nothing to preview</p>';return}s.innerHTML='<p class="text-base-content/60"><span class="loading loading-spinner loading-xs align-middle"></span> Rendering preview&hellip;</p>';let i=new FormData;i.append("markdown",o),fetch("/api/repo-page/preview",{method:"POST",body:i}).then(l=>{if(!l.ok)throw new Error("HTTP "+l.status);return l.text()}).then(l=>{s.innerHTML=l}).catch(()=>{s.innerHTML='<p class="text-error">Preview failed. Check your connection and try again.</p>'})}},window.insertMd=function(r){let o=e.selectionStart,s=e.selectionEnd,i=e.value.substring(o,s),l=e.value.substring(0,o),a=e.value.substring(s),c,d,u;switch(r){case"heading":c="## "+(i||"Heading"),d=o+3,u=o+c.length;break;case"bold":c="**"+(i||"bold text")+"**",d=o+2,u=o+c.length-2;break;case"italic":c="_"+(i||"italic text")+"_",d=o+1,u=o+c.length-1;break;case"link":c="["+(i||"link text")+"](url)",d=o+c.length-4,u=o+c.length-1;break;case"image":c="!["+(i||"alt text")+"](url)",d=o+c.length-4,u=o+c.length-1;break;case"ul":c="- "+(i||"list item"),d=o+2,u=o+c.length;break;case"ol":c="1. "+(i||"list item"),d=o+3,u=o+c.length;break;case"code":i&&i.indexOf(` 3 + `)!==-1?(c="```\n"+i+"\n```",d=o+4,u=o+4+i.length):(c="`"+(i||"code")+"`",d=o+1,u=o+c.length-1);break;default:return}e.value=l+c+a,e.focus(),e.selectionStart=d,e.selectionEnd=u},window.saveOverview=function(){let r=document.getElementById("save-overview-btn");r.classList.add("btn-disabled"),r.innerHTML='<span class="loading loading-spinner loading-xs"></span> Saving...';let o=new FormData;o.append("did",t),o.append("repository",n),o.append("description",e.value),fetch("/api/repo-page",{method:"POST",body:o,headers:{"HX-Request":"true"}}).then(s=>s.ok?s.text():s.text().then(i=>{throw new Error(i)})).then(s=>{document.getElementById("overview-rendered").innerHTML=s,window.toggleOverviewEditor(!1),typeof window.showToast=="function"&&window.showToast("Overview saved","success")}).catch(s=>{typeof window.showToast=="function"&&window.showToast(s.message||"Failed to save","error")}).finally(()=>{r.classList.remove("btn-disabled"),r.innerHTML="Save"})},e.addEventListener("keydown",r=>{(r.ctrlKey||r.metaKey)&&r.key==="s"&&(r.preventDefault(),window.saveOverview())})}window.sortTags=function(e){let t=document.getElementById("tags-list");if(!t)return;let n=Array.from(t.querySelectorAll(".artifact-entry"));n.sort((r,o)=>{switch(e){case"oldest":return parseInt(r.dataset.created)-parseInt(o.dataset.created);case"az":return r.dataset.tag.localeCompare(o.dataset.tag);case"za":return o.dataset.tag.localeCompare(r.dataset.tag);default:return parseInt(o.dataset.created)-parseInt(r.dataset.created)}}),n.forEach(r=>t.appendChild(r))};var _=0;window.filterTags=function(e){_&&cancelAnimationFrame(_),_=requestAnimationFrame(()=>{_=0;let t=e.toLowerCase();document.querySelectorAll("#tags-list .artifact-entry").forEach(n=>{n.style.display=!t||n.dataset.tag.toLowerCase().includes(t)?"":"none"})})};function Xe(){if(!document.getElementById("tag-content"))return;let e=["overview","layers","vulns","sbom","artifacts"],t={};function n(i,l){if(t[i])return;t[i]=!0;let a=document.getElementById(i);if(!a)return;let c=new AbortController,d=setTimeout(()=>c.abort(),1e4);fetch(l,{signal:c.signal}).then(u=>{if(!u.ok)throw new Error("HTTP "+u.status);return u.text()}).then(u=>{a.innerHTML=u,a.querySelectorAll("script").forEach(f=>{let m=document.createElement("script");m.textContent=f.textContent,f.parentNode.replaceChild(m,f)}),typeof window.htmx<"u"&&window.htmx.process(a)}).catch(u=>{t[i]=!1;let m=u&&u.name==="AbortError"?"This section took too long to load.":"Couldn't load this section.";a.innerHTML='<div class="py-6 text-sm text-base-content/70"><p>'+m+'</p><button type="button" class="btn btn-sm btn-ghost mt-2" data-retry-section="'+i+'">Try again</button></div>'}).finally(()=>clearTimeout(d))}document.body.addEventListener("click",i=>{let l=i.target.closest("[data-retry-section]");if(!l)return;let a=l.getAttribute("data-retry-section"),d={"artifacts-content":o,"layers-content":()=>r("layers"),"vulns-content":()=>r("vulns"),"sbom-content":()=>r("sbom")}[a];if(d){let u=d();u&&n(a,u)}});function r(i){let l=document.getElementById("tag-content");if(!l)return null;let a=l.dataset.digest;return a?"/api/digest-content/"+l.dataset.owner+"/"+l.dataset.repo+"?digest="+encodeURIComponent(a)+"&section="+i:null}function o(){let i=document.getElementById("tag-content");return i?"/api/repo-tags/"+i.dataset.owner+"/"+i.dataset.repo:null}window.diffToTag=function(i,l){i.preventDefault();let a=l.dataset.diffTo,c=document.getElementById("tag-content"),d=document.getElementById("tag-selector");if(!c||!d||!a)return;let u=c.dataset.digest,f=d.value;!u||a===f||(window.location.href="/diff/"+c.dataset.owner+"/"+c.dataset.repo+"?from="+encodeURIComponent(u)+"&to="+encodeURIComponent(a))},window.switchRepoTab=function(i){window._activeRepoTab=i;let l=document.getElementById("tag-content");if(!l)return;l.querySelectorAll(".repo-panel").forEach(d=>d.classList.add("hidden"));let a=document.getElementById("tab-"+i);a&&a.classList.remove("hidden"),l.querySelectorAll(".repo-tab").forEach(d=>{let u=d.dataset.tab===i;d.classList.toggle("border-primary",u),d.classList.toggle("text-primary",u),d.classList.toggle("border-transparent",!u),d.classList.toggle("text-base-content/60",!u),d.setAttribute("aria-selected",u?"true":"false"),d.setAttribute("tabindex",u?"0":"-1")});let c=new URL(window.location);if(c.hash=i,history.replaceState(null,"",c.toString()),i==="artifacts"){let d=o();d&&n("artifacts-content",d)}if(i==="layers"){let d=r("layers");d&&n("layers-content",d)}if(i==="vulns"){let d=r("vulns");d&&n("vulns-content",d)}if(i==="sbom"){let d=r("sbom");d&&n("sbom-content",d)}};function s(){t={},[["artifacts-tab-btn","artifacts-content",o],["layers-tab-btn","layers-content",()=>r("layers")],["vulns-tab-btn","vulns-content",()=>r("vulns")],["sbom-tab-btn","sbom-content",()=>r("sbom")]].forEach(([c,d,u])=>{let f=document.getElementById(c);f&&f.addEventListener("mouseenter",()=>{let m=u();m&&n(d,m)},{once:!0})});let l=document.querySelector('[role="tablist"][aria-label="Repository sections"]');l&&!l.dataset.keyboardBound&&(l.dataset.keyboardBound="1",l.addEventListener("keydown",c=>{let d=Array.from(l.querySelectorAll(".repo-tab")),u=d.indexOf(document.activeElement);if(u===-1)return;let f=-1;switch(c.key){case"ArrowRight":f=(u+1)%d.length;break;case"ArrowLeft":f=(u-1+d.length)%d.length;break;case"Home":f=0;break;case"End":f=d.length-1;break;case"Enter":case" ":c.preventDefault(),window.switchRepoTab(d[u].dataset.tab);return;default:return}c.preventDefault(),d[f].focus()}));let a=window._activeRepoTab||window.location.hash.replace("#","")||"overview";e.indexOf(a)===-1&&(a="overview"),window.switchRepoTab(a)}s(),document.addEventListener("keydown",i=>{if(i.target.tagName==="INPUT"||i.target.tagName==="TEXTAREA"||i.target.tagName==="SELECT"||i.target.isContentEditable||i.ctrlKey||i.metaKey||i.altKey)return;let a={o:"overview",l:"layers",v:"vulns",s:"sbom",a:"artifacts"}[i.key.toLowerCase()];a&&e.indexOf(a)!==-1&&window.switchRepoTab(a)}),document.body.addEventListener("htmx:afterSettle",i=>{i.detail.target&&i.detail.target.id==="tag-content"&&s()})}document.addEventListener("DOMContentLoaded",()=>{We(),Xe()});function ze(){let e=["user","billing","storage","devices","webhooks","advanced"];if(!document.querySelector(".settings-tab-mobile, .menu li[data-tab]"))return;function t(a){document.querySelectorAll(".settings-panel").forEach(d=>d.classList.add("hidden"));let c=document.getElementById("tab-"+a);c&&c.classList.remove("hidden"),document.querySelectorAll(".menu li[data-tab]").forEach(d=>{let u=d.dataset.tab===a;d.classList.toggle("menu-active",u);let f=d.querySelector('a[role="tab"]');f&&(f.setAttribute("aria-selected",u?"true":"false"),f.setAttribute("tabindex",u?"0":"-1"))}),document.querySelectorAll(".settings-tab-mobile").forEach(d=>{let u=d.dataset.tab===a;d.classList.toggle("btn-ghost",!u),d.classList.toggle("btn-secondary",u),d.setAttribute("aria-selected",u?"true":"false"),d.setAttribute("tabindex",u?"0":"-1")}),history.replaceState(null,"","#"+a),document.body.dispatchEvent(new CustomEvent("tab:"+a))}window.isTabActive=function(a){let c=document.getElementById("tab-"+a);return c&&!c.classList.contains("hidden")},window.switchSettingsTab=t;function n(a,c){let d=c==="vertical"?"ArrowUp":"ArrowLeft",u=c==="vertical"?"ArrowDown":"ArrowRight";return function(f){let m=a.indexOf(f.currentTarget);if(m===-1)return;let h=null;f.key===d?h=a[(m-1+a.length)%a.length]:f.key===u?h=a[(m+1)%a.length]:f.key==="Home"?h=a[0]:f.key==="End"&&(h=a[a.length-1]),h&&(f.preventDefault(),t(h.dataset.tab||h.parentElement.dataset.tab),h.focus())}}let r=Array.from(document.querySelectorAll(".settings-tab-mobile")),o=n(r,"horizontal");r.forEach(a=>{a.addEventListener("click",c=>{c.preventDefault(),t(a.dataset.tab)}),a.addEventListener("keydown",o)});let s=Array.from(document.querySelectorAll('.menu li[data-tab] a[role="tab"]')),i=n(s,"vertical");s.forEach(a=>{a.addEventListener("click",c=>{c.preventDefault(),t(a.parentElement.dataset.tab)}),a.addEventListener("keydown",i)});let l=window.location.hash.replace("#","")||"user";e.indexOf(l)===-1&&(l="user"),t(l),window.addEventListener("hashchange",()=>{let a=window.location.hash.replace("#","")||"user";e.indexOf(a)!==-1&&t(a)})}function $e(){let e=document.getElementById("delete-account-btn");if(!e)return;let t=e.dataset.clientShortName||"this account",r="DELETE "+(e.dataset.profileHandle||"");function o(i){let l=document.createElement("div");return l.textContent=i,l.innerHTML}e.addEventListener("click",s);function s(){let i=document.getElementById("delete-pds-records").checked,l=document.createElement("div");l.className="modal modal-open",l.innerHTML=` 4 + <div class="modal-box bg-base-200 max-w-lg"> 5 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 6 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#alert-triangle"></use></svg> 7 + Delete ${o(t)} Data 8 + </h2> 9 + 10 + <div class="py-4 space-y-4"> 11 + <div class="alert alert-success"> 12 + <svg class="icon size-5" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 13 + <span>Your ATProto account will <strong>NOT</strong> be affected.</span> 14 + </div> 15 + 16 + <p class="text-base-content/80"> 17 + This action <strong>cannot be undone</strong>. This will permanently delete: 18 + </p> 19 + 20 + <ul class="list-disc list-inside text-sm space-y-1 text-base-content/70"> 21 + <li>Your ${o(t)} account and all settings</li> 22 + <li>All authorized devices</li> 23 + <li>Your data from all holds you're a member of</li> 24 + ${i?"<li>All io.atcr.* records from your ATProto PDS</li>":""} 25 + </ul> 26 + 27 + <div class="space-y-2"> 28 + <label for="confirm-delete-input" class="text-sm">Type <strong class="font-mono">${o(r)}</strong> to confirm:</label> 29 + <input type="text" id="confirm-delete-input" class="input input-bordered w-full font-mono" placeholder="${o(r)}" autocomplete="off"> 30 + </div> 31 + </div> 32 + 33 + <div class="modal-action"> 34 + <button type="button" class="btn" id="cancel-delete">Cancel</button> 35 + <button type="button" class="btn btn-error gap-2" id="confirm-delete" disabled> 36 + <svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#trash-2"></use></svg> 37 + Delete My ${o(t)} Data 38 + </button> 39 + </div> 40 + </div> 41 + <div class="modal-backdrop bg-black/50" id="modal-backdrop"></div> 42 + `,document.body.appendChild(l);let a=document.getElementById("confirm-delete-input"),c=document.getElementById("confirm-delete"),d=document.getElementById("cancel-delete");setTimeout(()=>a.focus(),100),a.addEventListener("input",function(){c.disabled=this.value!==r}),a.addEventListener("keydown",function(m){m.key==="Enter"&&this.value===r&&f()}),d.addEventListener("click",()=>l.remove()),document.getElementById("modal-backdrop").addEventListener("click",()=>l.remove());function u(m){m.key==="Escape"&&(l.remove(),document.removeEventListener("keydown",u))}document.addEventListener("keydown",u),c.addEventListener("click",f);async function f(){let m=document.getElementById("delete-pds-records").checked;c.disabled=!0,c.innerHTML='<svg class="icon size-4 animate-spin" aria-hidden="true"><use href="/icons.svg#loader-2"></use></svg> Deleting...',d.disabled=!0;try{let h=await fetch("/api/account",{method:"DELETE",headers:{"Content-Type":"application/json"},body:JSON.stringify({delete_pds_records:m,confirmation:r})}),y=await h.json();if(h.ok&&y.success)l.querySelector(".modal-box").innerHTML=` 43 + <h2 class="text-xl font-bold flex items-center gap-2 text-success"> 44 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 45 + Account Deleted 46 + </h2> 47 + <div class="py-4 space-y-2"> 48 + <p>Your account has been successfully deleted.</p> 49 + <p class="text-base-content/70">Redirecting to home page...</p> 50 + </div> 51 + `,setTimeout(()=>{window.location.href="/?deleted=true"},2e3);else{let p=y.errors||["An unknown error occurred"];l.querySelector(".modal-box").innerHTML=` 52 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 53 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg> 54 + Deletion Failed 55 + </h2> 56 + <div class="py-4 space-y-4"> 57 + <p>There were errors during account deletion:</p> 58 + <ul class="list-disc list-inside text-sm space-y-1 text-error"> 59 + ${p.map(w=>"<li>"+o(w)+"</li>").join("")} 60 + </ul> 61 + </div> 62 + <div class="modal-action"> 63 + <button type="button" class="btn" data-dismiss-modal>Close</button> 64 + </div> 65 + `,l.querySelector("[data-dismiss-modal]").addEventListener("click",()=>l.remove())}}catch(h){console.error("Delete account error:",h),l.querySelector(".modal-box").innerHTML=` 66 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 67 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg> 68 + Error 69 + </h2> 70 + <div class="py-4"> 71 + <p>Failed to delete account: ${o(h.message)}</p> 72 + </div> 73 + <div class="modal-action"> 74 + <button type="button" class="btn" data-dismiss-modal>Close</button> 75 + </div> 76 + `,l.querySelector("[data-dismiss-modal]").addEventListener("click",()=>l.remove())}}}}document.addEventListener("DOMContentLoaded",()=>{ze(),$e()});var G="showEmptyLayers";function Je(e,t,n){let r=0;for(let o=t;o<t+n;o++){let s=e[o].querySelector("td[data-bytes]");if(!s)continue;let i=Number(s.dataset.bytes);Number.isFinite(i)&&(r+=i)}return r}function Ye(e){return e<1024?e+" B":e<1048576?(e/1024).toFixed(1)+" KB":e<1073741824?(e/1048576).toFixed(1)+" MB":(e/1073741824).toFixed(1)+" GB"}function Ke(e){let t=e.querySelector("tbody");if(!t)return;let n=Array.from(t.querySelectorAll("tr")),r=0;for(;r<n.length;){if(n[r].dataset.noCommand!=="true"){r++;continue}let o=r;for(;r<n.length&&n[r].dataset.noCommand==="true";)n[r].classList.add("no-history-row","hidden"),r++;let s=r-o;if(s<=1){n[o].classList.remove("hidden");continue}let i=n[o].querySelector("td").textContent.trim(),l=n[r-1].querySelector("td").textContent.trim(),a=Ye(Je(n,o,s)),c=document.createElement("tr");c.className="no-history-summary cursor-pointer hover:bg-base-300",c.innerHTML='<td colspan="2" class="text-sm py-2">Layers '+i+"-"+l+' contain no history <span class="text-xs ml-2">('+s+' layers, click to expand)</span></td><td class="text-right text-sm whitespace-nowrap">'+a+"</td>",c.addEventListener("click",()=>{c.remove();for(let d=o;d<o+s;d++)n[d].classList.remove("hidden")}),t.insertBefore(c,n[o])}}function ye(e){let t=localStorage.getItem(G)==="true";e.querySelectorAll('tr[data-empty="true"]').forEach(n=>{n.style.display=t?"":"none"})}function Ee(e){let t=e||document;(t.querySelectorAll?t.querySelectorAll(".layers-table:not([data-layers-processed])"):[]).forEach(r=>{r.setAttribute("data-layers-processed","1"),Ke(r),ye(r)})}function Ge(e){localStorage.setItem(G,e),document.querySelectorAll(".show-empty-layers-cb").forEach(t=>{t.checked=e}),document.querySelectorAll(".layers-table").forEach(ye)}document.addEventListener("DOMContentLoaded",()=>{let e=localStorage.getItem(G)==="true";document.querySelectorAll(".show-empty-layers-cb").forEach(t=>{t.checked=e}),Ee()});document.addEventListener("change",e=>{e.target.matches("[data-toggle-empty-layers]")&&Ge(e.target.checked)});document.body.addEventListener("htmx:afterSettle",e=>{e.target&&e.target.querySelectorAll&&Ee(e.target)});window.htmx=D;D.config.methodsThatUseUrlParams=["get"];
+228 -40
pkg/appview/src/css/main.css
··· 99 99 default: false; 100 100 prefersdark: true; 101 101 color-scheme: "dark"; 102 - --color-base-100: oklch(19.5% 0.036 257.7); 103 - --color-base-200: oklch(23.2% 0.041 253.9); 104 - --color-base-300: oklch(28% 0.049 252); 102 + --color-base-100: oklch(20% 0.05 250); 103 + --color-base-200: oklch(25% 0.05 250); 104 + --color-base-300: oklch(30% 0.05 250); 105 105 --color-base-content: oklch(95.4% 0.022 211); 106 106 --color-primary: oklch(60% 0.126 221.723); 107 107 --color-primary-content: oklch(10% 0.126 221.723); ··· 134 134 ============================================ */ 135 135 @plugin "daisyui/theme" { 136 136 name: "light"; 137 - default: true 137 + default: true; 138 138 prefersdark: false; 139 139 color-scheme: "light"; 140 - --color-base-100: oklch(99.4% 0.004 214.3); 141 - --color-base-200: oklch(97.3% 0.01 212.5); 142 - --color-base-300: oklch(93.7% 0.02 212.5); 140 + --color-base-100: oklch(98% 0.01 225); 141 + --color-base-200: oklch(95% 0.02 225); 142 + --color-base-300: oklch(92% 0.03 225); 143 143 --color-base-content: oklch(21.1% 0.037 254.4); 144 144 --color-primary: oklch(60% 0.126 221.723); 145 145 --color-primary-content: oklch(10% 0.126 221.723); ··· 171 171 ADDITIONAL CSS VARIABLES 172 172 ======================================== */ 173 173 :root { 174 - --shadow-card-hover: 175 - 0 8px 25px oklch(67.1% 0.05 145 / 0.25), 0 4px 12px oklch(0% 0 0 / 0.1); 176 174 /* Dedicated star/favorite color — semantically distinct from `warning` 177 175 even if values currently coincide. Used by .text-star/.fill-star/etc. */ 178 176 --color-star: oklch(82% 0.189 84.429); 177 + 178 + /* Vulnerability severity scale. Held constant across themes on purpose: 179 + CVE severity is a product-semantic signal that needs to read the same 180 + way regardless of surface. Content-pair colors come from the same hue 181 + family (lighter for dark surfaces, darker for light surfaces) to avoid 182 + gray-on-color contrast problems. */ 183 + --color-severity-critical: oklch(45% 0.19 25); 184 + --color-severity-critical-content: oklch(97% 0.01 25); 185 + --color-severity-high: oklch(56% 0.19 50); 186 + --color-severity-high-content: oklch(97% 0.01 50); 187 + --color-severity-medium: oklch(72% 0.15 70); 188 + --color-severity-medium-content: oklch(25% 0.05 70); 189 + --color-severity-low: oklch(80% 0.1 85); 190 + --color-severity-low-content: oklch(25% 0.05 85); 179 191 } 180 192 181 193 /* ======================================== ··· 209 221 font-variant-numeric: tabular-nums; 210 222 } 211 223 224 + /* Card elevation. In dark mode, shadows disappear against a dark surface — 225 + depth is communicated primarily by the base-100 → base-200 surface ramp 226 + plus a white-alpha edge ring (see `.card-interactive` rule below). On 227 + hover we add a neutral-black amplified shadow (~4–5× the light-mode 228 + alpha). Tinted or glow shadows read as highlights, not elevation. */ 212 229 [data-theme="dark"] { 213 230 --shadow-card-hover: 214 - 0 8px 25px oklch(67.1% 0.05 145 / 0.2), 0 4px 12px oklch(0% 0 0 / 0.2); 231 + 0 2px 4px -1px oklch(0% 0 0 / 0.55), 232 + 0 12px 24px -6px oklch(0% 0 0 / 0.45); 215 233 } 216 234 217 235 [data-theme="light"] { 218 236 --shadow-card-hover: 219 - 0 8px 25px oklch(53.1% 0.1 144.8 / 0.25), 0 4px 12px oklch(0% 0 0 / 0.1); 220 - } 221 - 222 - [data-theme="dark"] { 223 - --shadow-card-hover: 224 - 0 8px 25px oklch(63.1% 0.07 144.7 / 0.2), 0 4px 12px oklch(0% 0 0 / 0.2); 237 + 0 1px 2px oklch(0% 0 0 / 0.06), 238 + 0 8px 24px -6px oklch(0% 0 0 / 0.12); 225 239 } 226 240 227 241 /* ======================================== ··· 348 362 white-space: nowrap; 349 363 border-width: 0; 350 364 } 365 + 366 + /* Skip-to-content link. Hidden until keyboard-focused, then anchors at 367 + the top-left so the user can press Enter to jump past the nav. */ 368 + .skip-link { 369 + @apply absolute left-2 z-50 px-3 py-2 rounded-md; 370 + @apply bg-primary text-primary-content font-medium text-sm; 371 + top: -10rem; 372 + transition: top 0.15s ease-out; 373 + } 374 + .skip-link:focus, 375 + .skip-link:focus-visible { 376 + top: 0.5rem; 377 + } 351 378 } 352 379 353 380 /* ======================================== ··· 361 388 } 362 389 363 390 /* ======================================== 391 + TOUCH TARGET SIZING 392 + Small buttons and compact form controls meet the keyboard minimum on 393 + desktop but fall below the 44×44 recommended touch target on touch 394 + devices (WCAG 2.5.5). Grow them only on coarse-pointer devices so 395 + pointer-primary layouts stay dense. 396 + ======================================== */ 397 + @media (pointer: coarse) { 398 + /* Icon-only buttons grow both axes — daisyUI's circle/square variants 399 + are the marker for these. */ 400 + :is(.btn-circle, .btn-square):is(.btn-xs, .btn-sm) { 401 + min-width: 2.75rem; 402 + min-height: 2.75rem; 403 + } 404 + 405 + /* Text buttons only need vertical clearance — padding handles width. */ 406 + .btn-xs, .btn-sm { 407 + min-height: 2.75rem; 408 + } 409 + 410 + /* Small checkbox/radio: expand the clickable region without distorting 411 + the control itself — negative margin on the parent <label> would be 412 + cleaner but requires markup changes; here we just grow the box. */ 413 + :is(.checkbox, .radio):is(.checkbox-xs, .radio-xs, .checkbox-sm, .radio-sm) { 414 + min-width: 1.5rem; 415 + min-height: 1.5rem; 416 + } 417 + 418 + /* daisyUI menu items (used in dropdowns, settings sidebar) are small 419 + link-like elements. Ensure they meet the tap threshold. */ 420 + .menu li > a, 421 + .menu li > button { 422 + min-height: 2.75rem; 423 + } 424 + 425 + /* Custom tablist buttons (repo-tab, editor-tab) should match. Padding 426 + already puts them near the threshold on desktop; lock in the floor. */ 427 + .repo-tab, 428 + .editor-tab { 429 + min-height: 2.75rem; 430 + } 431 + 432 + .sailor-typeahead-selected .sailor-typeahead-clear { 433 + width: 2.75rem; 434 + height: 2.75rem; 435 + } 436 + } 437 + 438 + /* ======================================== 439 + COMMAND BLOCK ON NARROW VIEWPORTS 440 + On desktop .cmd uses `truncate` for a tidy inline look. On mobile, 441 + truncation hides the very thing the user wants to copy ("atcr.io/…"), 442 + so switch to horizontal scrolling — the copy button still copies the 443 + full text, but users can also see what they're about to paste. 444 + ======================================== */ 445 + @media (max-width: 40rem) { 446 + .cmd code { 447 + overflow-x: auto; 448 + overflow-y: hidden; 449 + text-overflow: clip; 450 + /* Touch-friendly scroll momentum */ 451 + -webkit-overflow-scrolling: touch; 452 + } 453 + } 454 + 455 + /* ======================================== 456 + REDUCED MOTION 457 + Honor users who opt out of animation. Collapse all durations to a 458 + near-instant value rather than removing transitions entirely, so 459 + state changes still fire (transitionend listeners, etc.). 460 + ======================================== */ 461 + @media (prefers-reduced-motion: reduce) { 462 + *, 463 + *::before, 464 + *::after { 465 + animation-duration: 0.01ms !important; 466 + animation-iteration-count: 1 !important; 467 + transition-duration: 0.01ms !important; 468 + scroll-behavior: auto !important; 469 + } 470 + } 471 + 472 + /* ======================================== 473 + FORCED COLORS (Windows High Contrast) 474 + The OS strips box-shadow entirely, so any UI that communicates a 475 + "separate surface" purely through shadow becomes invisible. Paint 476 + explicit borders using system colors so edges survive. Hover/active 477 + states use the Highlight system color which the OS also guarantees 478 + is perceptually distinct. 479 + ======================================== */ 480 + @media (forced-colors: active) { 481 + .card-interactive { 482 + border: 1px solid CanvasText; 483 + } 484 + .card-interactive:hover { 485 + outline: 2px solid Highlight; 486 + outline-offset: 2px; 487 + } 488 + /* Severity badges lose their distinguishing fill in forced-colors; 489 + fall back to a visible border so the severity strip still reads 490 + as a series of distinct cells. */ 491 + .vuln-strip > span { 492 + border: 1px solid CanvasText; 493 + } 494 + } 495 + 496 + /* ======================================== 364 497 CUSTOM COMPONENTS (Not in DaisyUI) 365 498 ======================================== */ 366 499 @layer components { ··· 368 501 COMMAND / CODE DISPLAY 369 502 ---------------------------------------- */ 370 503 .cmd { 371 - @apply flex items-center gap-2 relative w-fit max-w-full overflow-hidden; 372 - @apply bg-base-200 border border-base-300 rounded-md; 504 + @apply flex items-center gap-2 relative w-fit max-w-full; 505 + @apply bg-base-300 rounded-md; 373 506 @apply px-3 py-2; 374 507 } 375 508 509 + /* `min-w-0` + `flex-1` let the code shrink below its intrinsic width so 510 + `truncate` can actually produce an ellipsis inside a flex container. 511 + Without them, long commands overflow silently. */ 376 512 .cmd code { 377 - @apply font-mono text-sm truncate; 513 + @apply font-mono text-sm truncate min-w-0 flex-1; 378 514 } 379 515 380 516 /* ---------------------------------------- ··· 432 568 /* ---------------------------------------- 433 569 CARD EXTENSIONS 434 570 ---------------------------------------- */ 571 + /* Resting cards rely on surface lightness for elevation. In dark mode 572 + we also paint a 1px inner white-alpha ring so the edge reads against 573 + the slightly-lighter card surface — borders do more perceptual work 574 + than shadows in dark. Hover then adds a real (neutral-black) shadow. */ 435 575 .card-interactive { 436 - @apply cursor-pointer duration-500; 437 - transition-property: box-shadow, transform; 576 + position: relative; 577 + transition: transform 250ms cubic-bezier(0.16, 1, 0.3, 1), 578 + box-shadow 250ms cubic-bezier(0.16, 1, 0.3, 1); 579 + } 580 + 581 + [data-theme="dark"] .card-interactive { 582 + box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.06); 438 583 } 439 584 440 585 .card-interactive:hover { ··· 442 587 transform: translateY(-2px); 443 588 } 444 589 590 + [data-theme="dark"] .card-interactive:hover { 591 + box-shadow: 592 + inset 0 0 0 1px oklch(100% 0 0 / 0.1), 593 + var(--shadow-card-hover); 594 + } 595 + 596 + /* Stretched-link pattern: the whole card is one accessible link. 597 + The anchor fills the card via absolute positioning; real interactive 598 + descendants (owner/repo links, copy buttons, inline commands) raise 599 + themselves above it with z-index so they remain clickable. */ 600 + .card-stretched-link { 601 + position: absolute; 602 + inset: 0; 603 + z-index: 1; 604 + border-radius: inherit; 605 + text-decoration: none; 606 + } 607 + .card-stretched-link:focus-visible { 608 + outline: 2px solid var(--color-primary); 609 + outline-offset: 2px; 610 + } 611 + .card-interactive :is(a:not(.card-stretched-link), button, .cmd, [role="button"]) { 612 + position: relative; 613 + z-index: 2; 614 + } 615 + 445 616 /* ---------------------------------------- 446 617 SAILOR TYPEAHEAD 447 618 ---------------------------------------- */ ··· 455 626 @apply rounded-lg shadow-lg; 456 627 @apply max-h-80 overflow-y-auto z-50; 457 628 margin-top: 0.25rem; 629 + /* Don't escape the viewport on narrow screens — the dropdown is 630 + anchored to its input, but if the input sits near a viewport 631 + edge the right: 0 anchor can still overflow. */ 632 + max-width: calc(100vw - 1rem); 458 633 } 459 634 460 635 .sailor-typeahead-header { ··· 470 645 471 646 .sailor-typeahead-item:hover, 472 647 .sailor-typeahead-item.focused { 473 - @apply bg-base-200; 648 + @apply bg-base-300; 474 649 } 475 650 476 651 .sailor-typeahead-item-compact { ··· 514 689 @apply flex-shrink-0 w-8 h-8 rounded-full; 515 690 @apply flex items-center justify-center; 516 691 @apply text-xl leading-none text-base-content/60; 517 - @apply hover:bg-base-200 hover:text-base-content; 692 + @apply hover:bg-base-300 hover:text-base-content; 518 693 @apply transition-colors cursor-pointer; 519 694 } 520 695 ··· 558 733 } 559 734 560 735 /* ---------------------------------------- 561 - OFFLINE MANIFEST FILTERING 562 - Hide offline manifests by default; 563 - show when "Show offline images" is checked 564 - ---------------------------------------- */ 565 - .manifests-list > [data-reachable="false"] { 566 - display: none; 567 - } 568 - 569 - .manifests-list.show-offline > [data-reachable="false"] { 570 - display: block; 571 - } 572 - 573 - /* ---------------------------------------- 574 736 RESPONSIVE TABLE — STACK ON MOBILE 575 737 Below md, each row becomes a labeled block. 576 738 Add data-label="..." to each <td>. ··· 607 769 } 608 770 609 771 /* ---------------------------------------- 772 + MODAL — MOBILE GUTTER 773 + DaisyUI's .modal-box is ~512px wide by default with no mobile clamp; 774 + on viewports under that it presses against the screen edges. Force 775 + a 1rem gutter on each side so the dialog reads as a card. Only 776 + applies under the sm breakpoint so desktop max-w-* utilities win. 777 + ---------------------------------------- */ 778 + @media (max-width: 39.999rem) { 779 + .modal .modal-box { 780 + max-width: calc(100vw - 2rem); 781 + } 782 + } 783 + 784 + /* ---------------------------------------- 610 785 VULNERABILITY SEVERITY BOX STRIP 611 786 Docker Hub-style connected severity boxes 612 787 ---------------------------------------- */ 613 788 .vuln-strip { 614 789 @apply inline-flex items-stretch text-xs font-semibold leading-none; 790 + /* Never spill out of a narrow column. The strip's natural width 791 + (~115px on mobile) almost always fits; this is a safety net for 792 + ultra-cramped table cells and side panels. */ 793 + max-width: 100%; 615 794 } 616 795 .vuln-strip > span { 617 796 @apply px-1 py-1 min-w-5 text-center cursor-pointer sm:px-2 sm:min-w-7; 618 797 } 619 798 .vuln-strip > span:first-child { @apply rounded-l-sm; } 620 799 .vuln-strip > span:last-child { @apply rounded-r-sm; } 621 - .vuln-box-critical { background-color: oklch(45% 0.19 25); color: oklch(97% 0.01 25); } 622 - .vuln-box-high { background-color: oklch(56% 0.19 50); color: oklch(97% 0.01 50); } 623 - .vuln-box-medium { background-color: oklch(72% 0.15 70); color: oklch(25% 0.05 70); } 624 - .vuln-box-low { background-color: oklch(80% 0.1 85); color: oklch(25% 0.05 85); } 800 + .vuln-box-critical { background-color: var(--color-severity-critical); color: var(--color-severity-critical-content); } 801 + .vuln-box-high { background-color: var(--color-severity-high); color: var(--color-severity-high-content); } 802 + .vuln-box-medium { background-color: var(--color-severity-medium); color: var(--color-severity-medium-content); } 803 + .vuln-box-low { background-color: var(--color-severity-low); color: var(--color-severity-low-content); } 625 804 } 805 + 806 + /* ======================================== 807 + HTMX REQUEST INDICATOR 808 + Opt-in: elements with class `htmx-indicator` are hidden except during 809 + an in-flight request. Keeps request UX declarative — no JS required. 810 + ======================================== */ 811 + .htmx-indicator { display: none; } 812 + .htmx-request .htmx-indicator, 813 + .htmx-request.htmx-indicator { display: inline-block; } 626 814 627 815 /* ======================================== 628 816 TOAST CONTAINER
+392 -115
pkg/appview/src/js/app.js
··· 32 32 use.setAttribute('href', `/icons.svg#${iconMap[pref] || 'sun-moon'}`); 33 33 }); 34 34 35 - // Update checkmarks in dropdown 35 + // Update checkmarks + ARIA state in dropdown 36 36 document.querySelectorAll('.theme-option').forEach(option => { 37 37 const isSelected = option.dataset.value === pref; 38 + option.setAttribute('aria-checked', isSelected ? 'true' : 'false'); 38 39 const check = option.querySelector('.theme-check'); 39 40 if (check) { 40 41 check.style.visibility = isSelected ? 'visible' : 'hidden'; ··· 58 59 }); 59 60 60 61 // Expandable search 62 + function setSearchExpanded(wrapper, expanded) { 63 + if (!wrapper) return; 64 + const form = wrapper.querySelector('.nav-search-form'); 65 + const toggleBtn = wrapper.querySelector('button[aria-controls="nav-search-form"]'); 66 + wrapper.classList.toggle('expanded', expanded); 67 + if (form) { 68 + if (expanded) { 69 + form.removeAttribute('inert'); 70 + } else { 71 + form.setAttribute('inert', ''); 72 + } 73 + } 74 + if (toggleBtn) { 75 + toggleBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false'); 76 + } 77 + } 78 + 61 79 function toggleSearch() { 62 80 const wrapper = document.querySelector('.nav-search-wrapper'); 63 - const input = document.getElementById('nav-search-input'); 64 - 65 - if (!wrapper || !input) return; 66 - 67 - wrapper.classList.toggle('expanded'); 68 - 69 - if (wrapper.classList.contains('expanded')) { 70 - input.focus(); 81 + if (!wrapper) return; 82 + const willExpand = !wrapper.classList.contains('expanded'); 83 + setSearchExpanded(wrapper, willExpand); 84 + if (willExpand) { 85 + const input = document.getElementById('nav-search-input'); 86 + if (input) input.focus(); 71 87 } 72 88 } 73 89 74 90 function closeSearch() { 75 - const wrapper = document.querySelector('.nav-search-wrapper'); 76 - if (wrapper) { 77 - wrapper.classList.remove('expanded'); 78 - } 91 + setSearchExpanded(document.querySelector('.nav-search-wrapper'), false); 79 92 } 80 93 81 94 // Close search on Escape key and click outside ··· 110 123 }); 111 124 }); 112 125 113 - // Copy to clipboard 114 - // text: the text to copy 115 - // btn: optional button element for feedback (uses event.target if not provided) 126 + // Copy to clipboard. Always called with an explicit btn from the delegated 127 + // dispatcher or direct callers — no implicit global `event` fallback. 116 128 function copyToClipboard(text, btn) { 117 - if (!btn && typeof event !== 'undefined') { 118 - btn = event.target.closest('button'); 119 - } 120 - navigator.clipboard.writeText(text).then(() => { 129 + const onSuccess = () => { 121 130 if (!btn) return; 122 - // Show success feedback 123 131 const originalHTML = btn.innerHTML; 124 132 btn.innerHTML = '<svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#check"></use></svg> Copied!'; 125 - setTimeout(() => { 126 - btn.innerHTML = originalHTML; 127 - }, 2000); 128 - }).catch(err => { 129 - console.error('Failed to copy:', err); 130 - }); 133 + setTimeout(() => { btn.innerHTML = originalHTML; }, 2000); 134 + }; 135 + 136 + if (navigator.clipboard && window.isSecureContext) { 137 + navigator.clipboard.writeText(text).then(onSuccess).catch(err => { 138 + console.error('Clipboard API failed, falling back:', err); 139 + if (!legacyCopy(text)) { 140 + showToast('Copy failed — check browser permissions', 'error'); 141 + } else { 142 + onSuccess(); 143 + } 144 + }); 145 + return; 146 + } 147 + 148 + // Insecure context (http://) or unsupported browser — fall back to the 149 + // legacy textarea + execCommand technique. Still works in every browser 150 + // released in the last decade. 151 + if (legacyCopy(text)) { 152 + onSuccess(); 153 + } else { 154 + showToast('Copy failed — select the text and copy manually', 'error'); 155 + } 156 + } 157 + 158 + // Hidden-textarea fallback for clipboard copy. Returns true on success. 159 + function legacyCopy(text) { 160 + const ta = document.createElement('textarea'); 161 + ta.value = text; 162 + ta.setAttribute('readonly', ''); 163 + ta.setAttribute('aria-hidden', 'true'); 164 + // Position off-screen but inside viewport so iOS Safari doesn't reject 165 + // the selection. Avoid display:none — the textarea must be focusable. 166 + ta.style.position = 'fixed'; 167 + ta.style.top = '0'; 168 + ta.style.left = '0'; 169 + ta.style.width = '1px'; 170 + ta.style.height = '1px'; 171 + ta.style.opacity = '0'; 172 + ta.style.pointerEvents = 'none'; 173 + document.body.appendChild(ta); 174 + let ok = false; 175 + try { 176 + ta.focus(); 177 + ta.select(); 178 + ta.setSelectionRange(0, text.length); 179 + ok = document.execCommand && document.execCommand('copy'); 180 + } catch (_) { 181 + ok = false; 182 + } 183 + document.body.removeChild(ta); 184 + return !!ok; 131 185 } 132 186 133 187 // Serialize a <table> (thead + tbody) as CSV (RFC 4180 quoting) ··· 146 200 return lines.join('\n'); 147 201 } 148 202 149 - // Initialize copy buttons with data-cmd attribute and clickable cards with data-href 203 + // Initialize copy buttons with data-cmd attribute and CSV copy buttons. 150 204 document.addEventListener('DOMContentLoaded', () => { 151 205 document.addEventListener('click', (e) => { 152 206 // Handle copy-as-CSV buttons (for vulnerability / SBOM tables) ··· 163 217 if (btn) { 164 218 copyToClipboard(btn.getAttribute('data-cmd'), btn); 165 219 return; 166 - } 167 - 168 - // Handle clickable cards (skip if clicking on interactive elements) 169 - if (e.target.closest('a, button, input, .cmd')) return; 170 - const card = e.target.closest('[data-href]'); 171 - if (card) { 172 - window.location = card.getAttribute('data-href'); 173 220 } 174 221 }); 175 222 }); ··· 241 288 // Update timestamps after HTMX swaps 242 289 document.addEventListener('htmx:afterSwap', updateTimestamps); 243 290 244 - // Update timestamps periodically 245 - setInterval(updateTimestamps, 60000); // Every minute 246 - 247 - // Toggle offline manifests visibility 248 - function toggleOfflineManifests() { 249 - const checkbox = document.getElementById('show-offline-toggle'); 250 - const manifestsList = document.querySelector('.manifests-list'); 251 - 252 - if (!checkbox || !manifestsList) return; 253 - 254 - // Store preference in localStorage 255 - localStorage.setItem('showOfflineManifests', checkbox.checked); 256 - 257 - // Toggle visibility of offline manifests 258 - if (checkbox.checked) { 259 - manifestsList.classList.add('show-offline'); 260 - } else { 261 - manifestsList.classList.remove('show-offline'); 262 - } 291 + // Update timestamps periodically — pause while the tab is hidden so we don't 292 + // walk the DOM every minute for an audience that isn't looking. On return, 293 + // refresh immediately so stamps reflect real time elapsed. 294 + let timestampInterval = null; 295 + function startTimestampUpdates() { 296 + if (timestampInterval === null) timestampInterval = setInterval(updateTimestamps, 60000); 263 297 } 264 - 265 - // Restore offline manifests toggle state on page load 266 - document.addEventListener('DOMContentLoaded', () => { 267 - const checkbox = document.getElementById('show-offline-toggle'); 268 - if (!checkbox) return; 269 - 270 - // Restore state from localStorage 271 - const showOffline = localStorage.getItem('showOfflineManifests') === 'true'; 272 - checkbox.checked = showOffline; 273 - 274 - // Apply initial state 275 - const manifestsList = document.querySelector('.manifests-list'); 276 - if (manifestsList) { 277 - if (showOffline) { 278 - manifestsList.classList.add('show-offline'); 279 - } else { 280 - manifestsList.classList.remove('show-offline'); 281 - } 298 + function stopTimestampUpdates() { 299 + if (timestampInterval !== null) { clearInterval(timestampInterval); timestampInterval = null; } 300 + } 301 + document.addEventListener('visibilitychange', () => { 302 + if (document.visibilityState === 'hidden') { 303 + stopTimestampUpdates(); 304 + } else { 305 + updateTimestamps(); 306 + startTimestampUpdates(); 282 307 } 283 308 }); 309 + startTimestampUpdates(); 284 310 285 311 // Delete manifest with confirmation for tagged manifests 286 312 async function deleteManifest(repository, digest, sanitizedId) { ··· 303 329 } else { 304 330 // Other error 305 331 const errorText = await response.text(); 306 - alert(`Failed to delete manifest: ${errorText}`); 332 + showToast(`Failed to delete manifest: ${errorText || response.status}`, 'error'); 307 333 } 308 334 } catch (err) { 309 335 console.error('Error deleting manifest:', err); 310 - alert(`Error deleting manifest: ${err.message}`); 336 + showToast(`Error deleting manifest: ${err.message}`, 'error'); 311 337 } 312 338 } 313 339 ··· 328 354 // Set up confirm button click handler 329 355 confirmBtn.onclick = () => confirmManifestDelete(repository, digest, sanitizedId); 330 356 331 - // Show modal 332 - modal.style.display = 'flex'; 357 + openDialog(modal); 333 358 } 334 359 335 360 // Close the manifest delete confirmation modal 336 361 function closeManifestDeleteModal() { 337 - const modal = document.getElementById('manifest-delete-modal'); 338 - modal.style.display = 'none'; 362 + closeDialog(document.getElementById('manifest-delete-modal')); 339 363 } 340 364 341 365 // Confirm and execute manifest deletion with all tags ··· 357 381 }); 358 382 359 383 if (response.ok) { 360 - // Successfully deleted 361 384 closeManifestDeleteModal(); 362 385 removeManifestElement(sanitizedId); 363 - // Also remove any tag elements that were deleted 364 - location.reload(); // Reload to refresh the tags list 386 + location.reload(); 365 387 } else { 366 388 // Error 367 389 const errorText = await response.text(); 368 - alert(`Failed to delete manifest: ${errorText}`); 390 + showToast(`Failed to delete manifest: ${errorText || response.status}`, 'error'); 369 391 confirmBtn.disabled = false; 370 392 confirmBtn.textContent = originalText; 371 393 } 372 394 } catch (err) { 373 395 console.error('Error deleting manifest:', err); 374 - alert(`Error deleting manifest: ${err.message}`); 396 + showToast(`Error deleting manifest: ${err.message}`, 'error'); 375 397 confirmBtn.disabled = false; 376 398 confirmBtn.textContent = originalText; 377 399 } ··· 396 418 const data = await response.json(); 397 419 398 420 if (response.ok) { 399 - document.getElementById('untagged-delete-modal').close(); 421 + closeDialog(document.getElementById('untagged-delete-modal')); 400 422 showToast(`Deleted ${data.deleted} untagged manifest(s)`, 'success'); 401 423 if (data.deleted > 0) { 402 424 location.reload(); ··· 404 426 confirmBtn.disabled = false; 405 427 confirmBtn.textContent = originalText; 406 428 } else { 407 - alert(`Failed to delete untagged manifests: ${data.error || 'Unknown error'}`); 429 + showToast(`Failed to delete untagged manifests: ${data.error || 'Unknown error'}`, 'error'); 408 430 confirmBtn.disabled = false; 409 431 confirmBtn.textContent = originalText; 410 432 } 411 433 } catch (err) { 412 434 console.error('Error deleting untagged manifests:', err); 413 - alert(`Error: ${err.message}`); 435 + showToast(`Error: ${err.message}`, 'error'); 414 436 confirmBtn.disabled = false; 415 437 confirmBtn.textContent = originalText; 416 438 } ··· 424 446 } 425 447 } 426 448 427 - // Close modal when clicking outside 449 + // Close manifest-delete-modal on backdrop click. With native showModal() 450 + // the dialog itself receives clicks that land on its ::backdrop pseudo, 451 + // so e.target === modal correctly identifies a backdrop click. 428 452 document.addEventListener('DOMContentLoaded', () => { 429 453 const modal = document.getElementById('manifest-delete-modal'); 430 454 if (modal) { ··· 436 460 } 437 461 }); 438 462 439 - // Vulnerability details modal 440 - async function openVulnDetails(digest, holdEndpoint) { 441 - const modal = document.getElementById('vuln-detail-modal'); 442 - const body = document.getElementById('vuln-modal-body'); 443 - if (!modal || !body) return; 463 + // ---------------------------------------- 464 + // Modal focus management 465 + // Native <dialog>.showModal() already traps Tab; we layer on: 466 + // 1. Restoring focus to the trigger when the dialog closes. 467 + // 2. Promoting HTMX-injected <dialog.modal-open> elements (which arrive 468 + // without showModal() ever being called) so the focus trap engages. 469 + // ---------------------------------------- 470 + const _dialogOpeners = new WeakMap(); 471 + 472 + function openDialog(dialog, opener) { 473 + if (!dialog) return; 474 + _dialogOpeners.set(dialog, opener || document.activeElement); 475 + if (typeof dialog.showModal !== 'function') return; 476 + // If the dialog is already in the non-modal open state (e.g. injected 477 + // with the [open] attribute), force it back into modal mode so Tab is 478 + // trapped. Setting open=false before showModal() avoids firing the 479 + // close event (which would steal focus restoration). 480 + if (dialog.open) dialog.open = false; 481 + try { dialog.showModal(); } catch (_) { /* detached from DOM */ } 482 + } 444 483 445 - // Show modal with loading spinner 446 - body.innerHTML = '<div class="flex justify-center py-8"><span class="loading loading-spinner loading-lg"></span></div>'; 447 - modal.showModal(); 484 + function closeDialog(dialog, { remove = false } = {}) { 485 + if (!dialog) return; 486 + const opener = _dialogOpeners.get(dialog); 487 + _dialogOpeners.delete(dialog); 488 + if (typeof dialog.close === 'function' && dialog.open) { 489 + try { dialog.close(); } catch (_) { /* ignore */ } 490 + } 491 + if (remove) dialog.remove(); 492 + restoreFocus(opener); 493 + } 448 494 449 - try { 450 - const resp = await fetch(`/api/vuln-details?digest=${encodeURIComponent(digest)}&holdEndpoint=${encodeURIComponent(holdEndpoint)}`); 451 - body.innerHTML = await resp.text(); 452 - } catch { 453 - body.innerHTML = '<p class="text-error">Failed to load vulnerability details</p>'; 495 + function restoreFocus(opener) { 496 + if (opener && typeof opener.focus === 'function' && document.contains(opener)) { 497 + opener.focus(); 454 498 } 455 499 } 456 500 501 + // Native close events fire from Escape and <form method="dialog"> submits. 502 + // Capture phase because close doesn't bubble. 503 + document.addEventListener('close', e => { 504 + const t = e.target; 505 + if (!(t instanceof HTMLDialogElement)) return; 506 + const opener = _dialogOpeners.get(t); 507 + _dialogOpeners.delete(t); 508 + restoreFocus(opener); 509 + }, true); 510 + 511 + // HTMX-injected dialogs were never passed through showModal(), so Tab can 512 + // escape them and the close event won't fire. Promote any settled <dialog> 513 + // that is currently visible — either via the .modal-open CSS class or via 514 + // the native [open] attribute. Skipping already-promoted ones keeps the 515 + // pass idempotent across nested swaps. 516 + document.body.addEventListener('htmx:afterSettle', () => { 517 + const sel = 'dialog.modal-open:not([data-modal-promoted]), dialog[open]:not([data-modal-promoted])'; 518 + document.querySelectorAll(sel).forEach(d => { 519 + d.dataset.modalPromoted = '1'; 520 + openDialog(d); 521 + }); 522 + }); 523 + 524 + // Diff page: full-page navigate when the platform select changes. 525 + // Select carries data-diff-url with __VALUE__ as the platform placeholder. 526 + document.addEventListener('change', e => { 527 + const sel = e.target.closest('select[data-diff-url]'); 528 + if (!sel) return; 529 + const template = sel.dataset.diffUrl; 530 + if (template) { 531 + window.location.href = template.replace('__VALUE__', encodeURIComponent(sel.value)); 532 + } 533 + }); 534 + 535 + // Pull-command switcher: updates the displayed pull command in place and 536 + // persists the client preference (profile for signed-in users, localStorage 537 + // for anonymous). Server values flow through data-attributes on the 538 + // container element rather than inline string interpolation. 539 + document.addEventListener('DOMContentLoaded', () => { 540 + const container = document.getElementById('pull-cmd-container'); 541 + if (!container) return; 542 + 543 + const registryURL = container.dataset.registryUrl; 544 + const ownerHandle = container.dataset.ownerHandle; 545 + const repoName = container.dataset.repoName; 546 + const tag = container.dataset.tag || 'latest'; 547 + const isLoggedIn = container.dataset.isLoggedIn === 'true'; 548 + 549 + function updatePullCommand(client) { 550 + const prefix = client === 'none' ? '' : client + ' pull '; 551 + const cmd = prefix + registryURL + '/' + ownerHandle + '/' + repoName + ':' + tag; 552 + const display = document.getElementById('pull-cmd-display'); 553 + if (!display) return; 554 + const code = display.querySelector('code'); 555 + if (code) code.textContent = cmd; 556 + const btn = display.querySelector('[data-cmd]'); 557 + if (btn) btn.dataset.cmd = cmd; 558 + 559 + if (isLoggedIn && window.htmx) { 560 + window.htmx.ajax('POST', '/api/profile/oci-client', { values: { oci_client: client }, swap: 'none' }); 561 + } else if (!isLoggedIn) { 562 + localStorage.setItem('oci-client', client); 563 + } 564 + } 565 + 566 + // Restore preference for anonymous users. 567 + if (!isLoggedIn) { 568 + const saved = localStorage.getItem('oci-client'); 569 + if (saved) { 570 + const sel = document.getElementById('oci-client-switcher'); 571 + if (sel) { 572 + sel.value = saved; 573 + updatePullCommand(saved); 574 + } 575 + } 576 + } 577 + 578 + const sel = document.getElementById('oci-client-switcher'); 579 + if (sel) sel.addEventListener('change', () => updatePullCommand(sel.value)); 580 + }); 581 + 582 + // Install page: platform tab switcher. Each .platform-tab has data-platform 583 + // pointing at a sibling panel (#<platform>-content). No-op off the install page. 584 + document.addEventListener('DOMContentLoaded', () => { 585 + const tabs = document.querySelectorAll('.platform-tab[data-platform]'); 586 + if (!tabs.length) return; 587 + 588 + tabs.forEach(tab => { 589 + tab.addEventListener('click', () => { 590 + tabs.forEach(t => { 591 + const active = t === tab; 592 + t.classList.toggle('btn-primary', active); 593 + t.classList.toggle('btn-ghost', !active); 594 + }); 595 + document.querySelectorAll('.platform-content').forEach(p => p.classList.add('hidden')); 596 + const panel = document.getElementById(tab.dataset.platform + '-content'); 597 + if (panel) panel.classList.remove('hidden'); 598 + }); 599 + }); 600 + }); 601 + 602 + // Prevent double-submit on the login form. The OAuth redirect can take a few 603 + // seconds during which a second click would fire another request. 604 + document.addEventListener('DOMContentLoaded', () => { 605 + const loginForm = document.getElementById('login-form'); 606 + if (loginForm) { 607 + loginForm.addEventListener('submit', () => { 608 + const submit = loginForm.querySelector('button[type="submit"]'); 609 + if (!submit || submit.disabled) return; 610 + submit.disabled = true; 611 + submit.innerHTML = '<span class="loading loading-spinner loading-sm align-middle"></span> Navigating&hellip;'; 612 + }); 613 + } 614 + }); 615 + 457 616 // Save successful login handle from cookie (set by server after OAuth success) 458 617 document.addEventListener('DOMContentLoaded', () => { 459 618 const cookie = document.cookie.split('; ').find(c => c.startsWith('atcr_login_handle=')); ··· 494 653 let intervalId = null; 495 654 const intervalMs = 5000; 496 655 497 - // Read on demand — at most once per click or per 5s autoplay tick. 498 - function step() { 499 - const first = items[0]; 656 + // Cache the per-step scroll distance; offsetWidth forces layout, so 657 + // measuring once per resize beats once per autoplay tick. rAF-coalesces 658 + // bursty resize events. 659 + let stepPx = 0; 660 + let resizeRaf = 0; 661 + function measureStep() { 500 662 const gap = parseFloat(getComputedStyle(carousel).gap) || 24; 501 - return first.offsetWidth + gap; 663 + stepPx = items[0].offsetWidth + gap; 502 664 } 665 + measureStep(); 666 + window.addEventListener('resize', () => { 667 + cancelAnimationFrame(resizeRaf); 668 + resizeRaf = requestAnimationFrame(measureStep); 669 + }); 670 + document.body.addEventListener('htmx:afterSettle', e => { 671 + if (e.target && e.target.contains && e.target.contains(carousel)) measureStep(); 672 + }); 503 673 504 674 function advance() { 505 675 const max = carousel.scrollWidth - carousel.clientWidth; 506 676 if (carousel.scrollLeft >= max - 10) { 507 677 carousel.scrollTo({ left: 0, behavior: 'smooth' }); 508 678 } else { 509 - carousel.scrollBy({ left: step(), behavior: 'smooth' }); 679 + carousel.scrollBy({ left: stepPx, behavior: 'smooth' }); 510 680 } 511 681 } 512 682 ··· 514 684 if (carousel.scrollLeft <= 10) { 515 685 carousel.scrollTo({ left: carousel.scrollWidth, behavior: 'smooth' }); 516 686 } else { 517 - carousel.scrollBy({ left: -step(), behavior: 'smooth' }); 687 + carousel.scrollBy({ left: -stepPx, behavior: 'smooth' }); 518 688 } 519 689 } 520 690 521 691 function startInterval() { 522 692 if (intervalId) return; 693 + if (document.visibilityState === 'hidden') return; 523 694 if (carousel.scrollWidth <= carousel.clientWidth + 10) return; 524 695 intervalId = setInterval(advance, intervalMs); 525 696 } ··· 534 705 carousel.addEventListener('mouseenter', stopInterval); 535 706 carousel.addEventListener('mouseleave', startInterval); 536 707 708 + // Pause autoplay while the tab is hidden — a scroll-snap animation on an 709 + // invisible carousel still eats compositor time on the other tab. 710 + document.addEventListener('visibilitychange', () => { 711 + if (document.visibilityState === 'hidden') stopInterval(); 712 + else startInterval(); 713 + }); 714 + 537 715 startInterval(); 538 716 } 539 717 ··· 546 724 } 547 725 }); 548 726 549 - // Toast notifications (auto-dismiss after 3s) 727 + // Toast notifications (auto-dismiss after 3s). 728 + // - Uses textContent, never innerHTML — error text sometimes relays server 729 + // response bodies that could contain markup. 730 + // - Container has aria-live for screen reader announcements; errors use 731 + // role="alert" (assertive) and success uses role="status" (polite). 732 + // - Dedupes identical toasts fired within 1.5s and caps the visible stack 733 + // so a burst of failures (bulk delete, retry storm) can't pile up. 734 + const TOAST_MAX = 4; 735 + const TOAST_DEDUPE_MS = 1500; 736 + 550 737 function showToast(message, type) { 551 738 let container = document.getElementById('toast-container'); 552 739 if (!container) { 553 740 container = document.createElement('div'); 554 741 container.id = 'toast-container'; 555 742 container.className = 'toast toast-end toast-bottom z-50'; 743 + container.setAttribute('aria-live', 'polite'); 744 + container.setAttribute('aria-atomic', 'false'); 556 745 document.body.appendChild(container); 557 746 } 558 747 559 - const alertClass = type === 'error' ? 'alert-error' : 'alert-success'; 748 + // Dedupe: if an identical toast is already on screen and was added 749 + // within the dedupe window, reset its dismiss timer instead of adding 750 + // a duplicate. Keeps the stack readable during error storms. 751 + const key = (type || 'info') + '|' + message; 752 + const now = Date.now(); 753 + const existing = container.querySelector(`[data-toast-key="${cssEscape(key)}"]`); 754 + if (existing && now - Number(existing.dataset.toastAt) < TOAST_DEDUPE_MS) { 755 + resetToastDismiss(existing); 756 + return; 757 + } 758 + 759 + const isError = type === 'error'; 760 + const alertClass = isError ? 'alert-error' : 'alert-success'; 560 761 const toast = document.createElement('div'); 561 762 toast.className = `alert ${alertClass} shadow-lg transition-opacity duration-300`; 562 - toast.innerHTML = `<span>${message}</span>`; 763 + toast.style.willChange = 'opacity'; 764 + toast.setAttribute('role', isError ? 'alert' : 'status'); 765 + toast.dataset.toastKey = key; 766 + toast.dataset.toastAt = String(now); 767 + const span = document.createElement('span'); 768 + span.textContent = message; 769 + toast.appendChild(span); 563 770 container.appendChild(toast); 564 771 565 - setTimeout(() => { 772 + // Cap the visible stack — drop the oldest if we exceed the cap. 773 + while (container.children.length > TOAST_MAX) { 774 + container.firstElementChild.remove(); 775 + } 776 + 777 + scheduleToastDismiss(toast); 778 + } 779 + 780 + function scheduleToastDismiss(toast) { 781 + toast._dismissTimer = setTimeout(() => { 566 782 toast.style.opacity = '0'; 567 - setTimeout(() => toast.remove(), 300); 783 + toast._removeTimer = setTimeout(() => toast.remove(), 300); 568 784 }, 3000); 569 785 } 570 786 787 + function resetToastDismiss(toast) { 788 + clearTimeout(toast._dismissTimer); 789 + clearTimeout(toast._removeTimer); 790 + toast.style.opacity = ''; 791 + toast.dataset.toastAt = String(Date.now()); 792 + scheduleToastDismiss(toast); 793 + } 794 + 795 + // Minimal CSS.escape polyfill for the dedupe attribute selector. 796 + function cssEscape(s) { 797 + return (window.CSS && CSS.escape) ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g, c => '\\' + c); 798 + } 799 + 571 800 // Test webhook via fetch + toast 572 801 async function testWebhook(id) { 573 802 try { ··· 586 815 } 587 816 } 588 817 589 - // Export functions that are called from templates via onclick handlers 818 + // ---------------------------------------- 819 + // Delegated action dispatcher 820 + // Templates use data-action="name" + data-* attributes for args instead 821 + // of inline onclick=/onchange=/oninput= handlers. Handlers resolve off 822 + // window.* (already registered below) so HTMX-swapped content keeps 823 + // working via natural event bubbling — no re-binding needed. 824 + // ---------------------------------------- 825 + (function initActionDispatcher() { 826 + const click = { 827 + 'switch-repo-tab': (t) => window.switchRepoTab && window.switchRepoTab(t.dataset.tab), 828 + 'switch-editor-tab': (t) => window.switchEditorTab && window.switchEditorTab(t.dataset.tab), 829 + 'insert-md': (t) => window.insertMd && window.insertMd(t.dataset.mdType), 830 + 'toggle-editor': (t) => window.toggleOverviewEditor && window.toggleOverviewEditor(t.dataset.show === 'true'), 831 + 'show-modal': (t) => openDialog(document.getElementById(t.dataset.modalId), t), 832 + 'close-dialog': (t) => closeDialog(t.closest('dialog')), 833 + 'remove-closest-dialog': (t) => closeDialog(t.closest('dialog'), { remove: true }), 834 + 'close-manifest-delete-modal': () => window.closeManifestDeleteModal && window.closeManifestDeleteModal(), 835 + 'save-overview': () => window.saveOverview && window.saveOverview(), 836 + 'delete-manifest': (t) => window.deleteManifest && window.deleteManifest(t.dataset.repo, t.dataset.digest, t.dataset.manifestId || ''), 837 + 'delete-untagged': (t) => window.deleteUntaggedManifests && window.deleteUntaggedManifests(t.dataset.repo), 838 + 'copy': (t) => window.copyToClipboard && window.copyToClipboard(t.dataset.copy, t), 839 + 'toggle-search': () => window.toggleSearch && window.toggleSearch(), 840 + 'switch-settings-tab': (t) => window.switchSettingsTab && window.switchSettingsTab(t.dataset.tab), 841 + 'test-webhook': (t) => window.testWebhook && window.testWebhook(t.dataset.webhookId), 842 + 'diff-to': (t, e) => window.diffToTag && window.diffToTag(e, t), 843 + 'modal-backdrop-close': (t, e) => { if (e.target === t) closeDialog(t, { remove: true }); }, 844 + }; 845 + 846 + const change = { 847 + 'sort-tags': (t) => window.sortTags && window.sortTags(t.value), 848 + 'submit-form': (t) => t.form && t.form.requestSubmit(), 849 + }; 850 + 851 + const input = { 852 + 'filter-tags': (t) => window.filterTags && window.filterTags(t.value), 853 + }; 854 + 855 + function dispatch(map, e) { 856 + const t = e.target.closest('[data-action]'); 857 + if (!t) return; 858 + const fn = map[t.dataset.action]; 859 + if (fn) fn(t, e); 860 + } 861 + 862 + document.addEventListener('click', e => dispatch(click, e)); 863 + document.addEventListener('change', e => dispatch(change, e)); 864 + document.addEventListener('input', e => dispatch(input, e)); 865 + })(); 866 + 867 + // Expose the minimum surface the delegated dispatcher and other modules 868 + // need. Keep this list tight — anything added here is implicitly part of 869 + // the public API that templates can reach for. 590 870 window.setTheme = setTheme; 591 871 window.toggleSearch = toggleSearch; 592 - window.closeSearch = closeSearch; 593 872 window.copyToClipboard = copyToClipboard; 594 - window.toggleOfflineManifests = toggleOfflineManifests; 595 873 window.deleteManifest = deleteManifest; 596 874 window.deleteUntaggedManifests = deleteUntaggedManifests; 597 875 window.closeManifestDeleteModal = closeManifestDeleteModal; 598 - window.openVulnDetails = openVulnDetails; 599 876 window.showToast = showToast; 600 877 window.testWebhook = testWebhook;
+102
pkg/appview/src/js/layers.js
··· 1 + // Layer table enhancements: collapse runs of no-history layers into a 2 + // single summary row, and toggle empty-layer visibility via localStorage. 3 + // Shared by repository.html (Layers tab) and digest.html (side-by-side view). 4 + 5 + const STORAGE_KEY = 'showEmptyLayers'; 6 + 7 + function sumBytesFromTable(rows, start, count) { 8 + let total = 0; 9 + for (let k = start; k < start + count; k++) { 10 + const sizeCell = rows[k].querySelector('td[data-bytes]'); 11 + if (!sizeCell) continue; 12 + const bytes = Number(sizeCell.dataset.bytes); 13 + if (Number.isFinite(bytes)) total += bytes; 14 + } 15 + return total; 16 + } 17 + 18 + function humanize(bytes) { 19 + if (bytes < 1024) return bytes + ' B'; 20 + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; 21 + if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'; 22 + return (bytes / 1073741824).toFixed(1) + ' GB'; 23 + } 24 + 25 + function collapseNoHistoryLayers(table) { 26 + const tbody = table.querySelector('tbody'); 27 + if (!tbody) return; 28 + 29 + const rows = Array.from(tbody.querySelectorAll('tr')); 30 + let i = 0; 31 + while (i < rows.length) { 32 + if (rows[i].dataset.noCommand !== 'true') { i++; continue; } 33 + 34 + const start = i; 35 + while (i < rows.length && rows[i].dataset.noCommand === 'true') { 36 + rows[i].classList.add('no-history-row', 'hidden'); 37 + i++; 38 + } 39 + const count = i - start; 40 + if (count <= 1) { rows[start].classList.remove('hidden'); continue; } 41 + 42 + const startIdx = rows[start].querySelector('td').textContent.trim(); 43 + const endIdx = rows[i - 1].querySelector('td').textContent.trim(); 44 + const sizeStr = humanize(sumBytesFromTable(rows, start, count)); 45 + 46 + const summary = document.createElement('tr'); 47 + summary.className = 'no-history-summary cursor-pointer hover:bg-base-300'; 48 + summary.innerHTML = 49 + '<td colspan="2" class="text-sm py-2">Layers ' + startIdx + '-' + endIdx + 50 + ' contain no history <span class="text-xs ml-2">(' + count + 51 + ' layers, click to expand)</span></td>' + 52 + '<td class="text-right text-sm whitespace-nowrap">' + sizeStr + '</td>'; 53 + summary.addEventListener('click', () => { 54 + summary.remove(); 55 + for (let j = start; j < start + count; j++) rows[j].classList.remove('hidden'); 56 + }); 57 + tbody.insertBefore(summary, rows[start]); 58 + } 59 + } 60 + 61 + function applyLayerVisibility(table) { 62 + const show = localStorage.getItem(STORAGE_KEY) === 'true'; 63 + table.querySelectorAll('tr[data-empty="true"]').forEach(row => { 64 + row.style.display = show ? '' : 'none'; 65 + }); 66 + } 67 + 68 + function initLayersTables(root) { 69 + const scope = root || document; 70 + const tables = scope.querySelectorAll ? 71 + scope.querySelectorAll('.layers-table:not([data-layers-processed])') : 72 + []; 73 + tables.forEach(table => { 74 + table.setAttribute('data-layers-processed', '1'); 75 + collapseNoHistoryLayers(table); 76 + applyLayerVisibility(table); 77 + }); 78 + } 79 + 80 + function toggleEmptyLayers(show) { 81 + localStorage.setItem(STORAGE_KEY, show); 82 + document.querySelectorAll('.show-empty-layers-cb').forEach(cb => { cb.checked = show; }); 83 + document.querySelectorAll('.layers-table').forEach(applyLayerVisibility); 84 + } 85 + 86 + // Seed checkbox state from localStorage on load, and delegate changes. 87 + document.addEventListener('DOMContentLoaded', () => { 88 + const show = localStorage.getItem(STORAGE_KEY) === 'true'; 89 + document.querySelectorAll('.show-empty-layers-cb').forEach(cb => { cb.checked = show; }); 90 + initLayersTables(); 91 + }); 92 + 93 + document.addEventListener('change', e => { 94 + if (e.target.matches('[data-toggle-empty-layers]')) { 95 + toggleEmptyLayers(e.target.checked); 96 + } 97 + }); 98 + 99 + // Process tables that arrive via HTMX swap (layers tab lazy-loads). 100 + document.body.addEventListener('htmx:afterSettle', e => { 101 + if (e.target && e.target.querySelectorAll) initLayersTables(e.target); 102 + });
+10
pkg/appview/src/js/main.js
··· 13 13 14 14 // Import app functionality 15 15 import './app.js'; 16 + 17 + // Repository page module (owner editor, tag sort/filter, tab controller). 18 + // Safe to include globally — all initializers no-op when their target DOM is absent. 19 + import './repository.js'; 20 + 21 + // Settings page module (tab controller + account deletion modal). 22 + import './settings.js'; 23 + 24 + // Layer table enhancements (shared by repository + digest views). 25 + import './layers.js';
+385
pkg/appview/src/js/repository.js
··· 1 + // Repository page: owner markdown editor, tag sort/filter, tab controller. 2 + // Extracted from repository.html to keep the page shell light. 3 + 4 + // ---------------------------------------- 5 + // Owner markdown editor (no-op if not owner / not on page) 6 + // ---------------------------------------- 7 + function initOverviewEditor() { 8 + const textarea = document.getElementById('md-editor'); 9 + if (!textarea) return; 10 + 11 + const ownerDID = textarea.dataset.ownerDid; 12 + const repoName = textarea.dataset.repository; 13 + 14 + window.toggleOverviewEditor = function(show) { 15 + document.getElementById('overview-view').classList.toggle('hidden', show); 16 + document.getElementById('overview-edit').classList.toggle('hidden', !show); 17 + if (show) textarea.focus(); 18 + }; 19 + 20 + window.switchEditorTab = function(tab) { 21 + document.querySelectorAll('.editor-panel').forEach(p => p.classList.add('hidden')); 22 + document.getElementById(tab === 'write' ? 'editor-write' : 'editor-preview').classList.remove('hidden'); 23 + 24 + document.querySelectorAll('.editor-tab').forEach(t => { 25 + const active = t.dataset.tab === tab; 26 + t.classList.toggle('border-primary', active); 27 + t.classList.toggle('text-primary', active); 28 + t.classList.toggle('border-transparent', !active); 29 + t.classList.toggle('text-base-content/60', !active); 30 + }); 31 + 32 + if (tab === 'preview') { 33 + const content = textarea.value; 34 + const previewEl = document.getElementById('preview-content'); 35 + if (!content.trim()) { 36 + previewEl.innerHTML = '<p class="text-base-content/60">Nothing to preview</p>'; 37 + return; 38 + } 39 + previewEl.innerHTML = '<p class="text-base-content/60"><span class="loading loading-spinner loading-xs align-middle"></span> Rendering preview&hellip;</p>'; 40 + const form = new FormData(); 41 + form.append('markdown', content); 42 + fetch('/api/repo-page/preview', { method: 'POST', body: form }) 43 + .then(r => { 44 + if (!r.ok) throw new Error('HTTP ' + r.status); 45 + return r.text(); 46 + }) 47 + .then(html => { previewEl.innerHTML = html; }) 48 + .catch(() => { 49 + previewEl.innerHTML = '<p class="text-error">Preview failed. Check your connection and try again.</p>'; 50 + }); 51 + } 52 + }; 53 + 54 + window.insertMd = function(type) { 55 + const start = textarea.selectionStart; 56 + const end = textarea.selectionEnd; 57 + const selected = textarea.value.substring(start, end); 58 + const before = textarea.value.substring(0, start); 59 + const after = textarea.value.substring(end); 60 + let insert, cursorStart, cursorEnd; 61 + 62 + switch (type) { 63 + case 'heading': 64 + insert = '## ' + (selected || 'Heading'); 65 + cursorStart = start + 3; 66 + cursorEnd = start + insert.length; 67 + break; 68 + case 'bold': 69 + insert = '**' + (selected || 'bold text') + '**'; 70 + cursorStart = start + 2; 71 + cursorEnd = start + insert.length - 2; 72 + break; 73 + case 'italic': 74 + insert = '_' + (selected || 'italic text') + '_'; 75 + cursorStart = start + 1; 76 + cursorEnd = start + insert.length - 1; 77 + break; 78 + case 'link': 79 + insert = '[' + (selected || 'link text') + '](url)'; 80 + cursorStart = start + insert.length - 4; 81 + cursorEnd = start + insert.length - 1; 82 + break; 83 + case 'image': 84 + insert = '![' + (selected || 'alt text') + '](url)'; 85 + cursorStart = start + insert.length - 4; 86 + cursorEnd = start + insert.length - 1; 87 + break; 88 + case 'ul': 89 + insert = '- ' + (selected || 'list item'); 90 + cursorStart = start + 2; 91 + cursorEnd = start + insert.length; 92 + break; 93 + case 'ol': 94 + insert = '1. ' + (selected || 'list item'); 95 + cursorStart = start + 3; 96 + cursorEnd = start + insert.length; 97 + break; 98 + case 'code': 99 + if (selected && selected.indexOf('\n') !== -1) { 100 + insert = '```\n' + selected + '\n```'; 101 + cursorStart = start + 4; 102 + cursorEnd = start + 4 + selected.length; 103 + } else { 104 + insert = '`' + (selected || 'code') + '`'; 105 + cursorStart = start + 1; 106 + cursorEnd = start + insert.length - 1; 107 + } 108 + break; 109 + default: 110 + return; 111 + } 112 + 113 + textarea.value = before + insert + after; 114 + textarea.focus(); 115 + textarea.selectionStart = cursorStart; 116 + textarea.selectionEnd = cursorEnd; 117 + }; 118 + 119 + window.saveOverview = function() { 120 + const btn = document.getElementById('save-overview-btn'); 121 + btn.classList.add('btn-disabled'); 122 + btn.innerHTML = '<span class="loading loading-spinner loading-xs"></span> Saving...'; 123 + 124 + const form = new FormData(); 125 + form.append('did', ownerDID); 126 + form.append('repository', repoName); 127 + form.append('description', textarea.value); 128 + 129 + fetch('/api/repo-page', { 130 + method: 'POST', 131 + body: form, 132 + headers: { 'HX-Request': 'true' } 133 + }) 134 + .then(r => { 135 + if (!r.ok) return r.text().then(t => { throw new Error(t); }); 136 + return r.text(); 137 + }) 138 + .then(html => { 139 + document.getElementById('overview-rendered').innerHTML = html; 140 + window.toggleOverviewEditor(false); 141 + if (typeof window.showToast === 'function') window.showToast('Overview saved', 'success'); 142 + }) 143 + .catch(err => { 144 + if (typeof window.showToast === 'function') window.showToast(err.message || 'Failed to save', 'error'); 145 + }) 146 + .finally(() => { 147 + btn.classList.remove('btn-disabled'); 148 + btn.innerHTML = 'Save'; 149 + }); 150 + }; 151 + 152 + textarea.addEventListener('keydown', e => { 153 + if ((e.ctrlKey || e.metaKey) && e.key === 's') { 154 + e.preventDefault(); 155 + window.saveOverview(); 156 + } 157 + }); 158 + } 159 + 160 + // ---------------------------------------- 161 + // Tag list sort & filter (called from repo-tags.html) 162 + // ---------------------------------------- 163 + window.sortTags = function(method) { 164 + const container = document.getElementById('tags-list'); 165 + if (!container) return; 166 + const entries = Array.from(container.querySelectorAll('.artifact-entry')); 167 + entries.sort((a, b) => { 168 + switch (method) { 169 + case 'oldest': return parseInt(a.dataset.created) - parseInt(b.dataset.created); 170 + case 'az': return a.dataset.tag.localeCompare(b.dataset.tag); 171 + case 'za': return b.dataset.tag.localeCompare(a.dataset.tag); 172 + default: return parseInt(b.dataset.created) - parseInt(a.dataset.created); 173 + } 174 + }); 175 + entries.forEach(el => container.appendChild(el)); 176 + }; 177 + 178 + // Debounced filter. Runs once per animation frame at most so holding down 179 + // a key or pasting doesn't thrash the whole artifact list (can be 100+ 180 + // entries on active repos). Rendering still feels instant. 181 + let filterTagsHandle = 0; 182 + window.filterTags = function(query) { 183 + if (filterTagsHandle) cancelAnimationFrame(filterTagsHandle); 184 + filterTagsHandle = requestAnimationFrame(() => { 185 + filterTagsHandle = 0; 186 + const q = query.toLowerCase(); 187 + document.querySelectorAll('#tags-list .artifact-entry').forEach(el => { 188 + el.style.display = (!q || el.dataset.tag.toLowerCase().includes(q)) ? '' : 'none'; 189 + }); 190 + }); 191 + }; 192 + 193 + // ---------------------------------------- 194 + // Tag-scoped tab controller (reads config from #tag-content data attributes) 195 + // ---------------------------------------- 196 + function initTabController() { 197 + if (!document.getElementById('tag-content')) return; 198 + 199 + const validTabs = ['overview', 'layers', 'vulns', 'sbom', 'artifacts']; 200 + let loaded = {}; 201 + 202 + function lazyLoad(id, url) { 203 + if (loaded[id]) return; 204 + loaded[id] = true; 205 + const target = document.getElementById(id); 206 + if (!target) return; 207 + 208 + // Abort if the request hangs. SBOM/vuln endpoints can stall when a 209 + // hold is overloaded; without a timeout the spinner spins forever. 210 + const controller = new AbortController(); 211 + const timeoutId = setTimeout(() => controller.abort(), 10000); 212 + 213 + fetch(url, { signal: controller.signal }) 214 + .then(r => { 215 + if (!r.ok) throw new Error('HTTP ' + r.status); 216 + return r.text(); 217 + }) 218 + .then(html => { 219 + target.innerHTML = html; 220 + // innerHTML doesn't execute <script> tags — re-create them 221 + target.querySelectorAll('script').forEach(old => { 222 + const s = document.createElement('script'); 223 + s.textContent = old.textContent; 224 + old.parentNode.replaceChild(s, old); 225 + }); 226 + if (typeof window.htmx !== 'undefined') window.htmx.process(target); 227 + }) 228 + .catch(err => { 229 + loaded[id] = false; 230 + const timedOut = err && err.name === 'AbortError'; 231 + const msg = timedOut 232 + ? 'This section took too long to load.' 233 + : 'Couldn\'t load this section.'; 234 + target.innerHTML = 235 + '<div class="py-6 text-sm text-base-content/70">' + 236 + '<p>' + msg + '</p>' + 237 + '<button type="button" class="btn btn-sm btn-ghost mt-2" data-retry-section="' + id + '">Try again</button>' + 238 + '</div>'; 239 + }) 240 + .finally(() => clearTimeout(timeoutId)); 241 + } 242 + 243 + // Retry button for failed section loads (event delegation) 244 + document.body.addEventListener('click', e => { 245 + const btn = e.target.closest('[data-retry-section]'); 246 + if (!btn) return; 247 + const id = btn.getAttribute('data-retry-section'); 248 + // Re-derive URL from current active tab 249 + const map = { 'artifacts-content': tagsUrl, 'layers-content': () => contentUrl('layers'), 250 + 'vulns-content': () => contentUrl('vulns'), 'sbom-content': () => contentUrl('sbom') }; 251 + const fn = map[id]; 252 + if (fn) { const u = fn(); if (u) lazyLoad(id, u); } 253 + }); 254 + 255 + function contentUrl(section) { 256 + const el = document.getElementById('tag-content'); 257 + if (!el) return null; 258 + const digest = el.dataset.digest; 259 + if (!digest) return null; 260 + return '/api/digest-content/' + el.dataset.owner + '/' + el.dataset.repo + 261 + '?digest=' + encodeURIComponent(digest) + '&section=' + section; 262 + } 263 + 264 + function tagsUrl() { 265 + const el = document.getElementById('tag-content'); 266 + if (!el) return null; 267 + return '/api/repo-tags/' + el.dataset.owner + '/' + el.dataset.repo; 268 + } 269 + 270 + window.diffToTag = function(e, link) { 271 + e.preventDefault(); 272 + const to = link.dataset.diffTo; 273 + const content = document.getElementById('tag-content'); 274 + const selector = document.getElementById('tag-selector'); 275 + if (!content || !selector || !to) return; 276 + const fromDigest = content.dataset.digest; 277 + const currentTag = selector.value; 278 + if (!fromDigest || to === currentTag) return; 279 + window.location.href = '/diff/' + content.dataset.owner + '/' + content.dataset.repo + 280 + '?from=' + encodeURIComponent(fromDigest) + '&to=' + encodeURIComponent(to); 281 + }; 282 + 283 + window.switchRepoTab = function(tabId) { 284 + window._activeRepoTab = tabId; 285 + const section = document.getElementById('tag-content'); 286 + if (!section) return; 287 + 288 + section.querySelectorAll('.repo-panel').forEach(p => p.classList.add('hidden')); 289 + const panel = document.getElementById('tab-' + tabId); 290 + if (panel) panel.classList.remove('hidden'); 291 + 292 + // Roving tabindex: only the active tab is in the document tab sequence. 293 + section.querySelectorAll('.repo-tab').forEach(tab => { 294 + const active = tab.dataset.tab === tabId; 295 + tab.classList.toggle('border-primary', active); 296 + tab.classList.toggle('text-primary', active); 297 + tab.classList.toggle('border-transparent', !active); 298 + tab.classList.toggle('text-base-content/60', !active); 299 + tab.setAttribute('aria-selected', active ? 'true' : 'false'); 300 + tab.setAttribute('tabindex', active ? '0' : '-1'); 301 + }); 302 + 303 + const url = new URL(window.location); 304 + url.hash = tabId; 305 + history.replaceState(null, '', url.toString()); 306 + 307 + if (tabId === 'artifacts') { const u = tagsUrl(); if (u) lazyLoad('artifacts-content', u); } 308 + if (tabId === 'layers') { const u = contentUrl('layers'); if (u) lazyLoad('layers-content', u); } 309 + if (tabId === 'vulns') { const u = contentUrl('vulns'); if (u) lazyLoad('vulns-content', u); } 310 + if (tabId === 'sbom') { const u = contentUrl('sbom'); if (u) lazyLoad('sbom-content', u); } 311 + }; 312 + 313 + function initTabs() { 314 + loaded = {}; 315 + 316 + const prefetch = [ 317 + ['artifacts-tab-btn', 'artifacts-content', tagsUrl], 318 + ['layers-tab-btn', 'layers-content', () => contentUrl('layers')], 319 + ['vulns-tab-btn', 'vulns-content', () => contentUrl('vulns')], 320 + ['sbom-tab-btn', 'sbom-content', () => contentUrl('sbom')], 321 + ]; 322 + prefetch.forEach(([btnId, targetId, urlFn]) => { 323 + const btn = document.getElementById(btnId); 324 + if (btn) btn.addEventListener('mouseenter', () => { 325 + const u = urlFn(); 326 + if (u) lazyLoad(targetId, u); 327 + }, { once: true }); 328 + }); 329 + 330 + // WAI-ARIA tablist keyboard nav (manual activation — arrows move focus, 331 + // Enter/Space activates). Avoids triggering 4 lazy loads when cycling. 332 + const tablist = document.querySelector('[role="tablist"][aria-label="Repository sections"]'); 333 + if (tablist && !tablist.dataset.keyboardBound) { 334 + tablist.dataset.keyboardBound = '1'; 335 + tablist.addEventListener('keydown', evt => { 336 + const tabs = Array.from(tablist.querySelectorAll('.repo-tab')); 337 + const currentIdx = tabs.indexOf(document.activeElement); 338 + if (currentIdx === -1) return; 339 + let targetIdx = -1; 340 + switch (evt.key) { 341 + case 'ArrowRight': targetIdx = (currentIdx + 1) % tabs.length; break; 342 + case 'ArrowLeft': targetIdx = (currentIdx - 1 + tabs.length) % tabs.length; break; 343 + case 'Home': targetIdx = 0; break; 344 + case 'End': targetIdx = tabs.length - 1; break; 345 + case 'Enter': 346 + case ' ': 347 + evt.preventDefault(); 348 + window.switchRepoTab(tabs[currentIdx].dataset.tab); 349 + return; 350 + default: return; 351 + } 352 + evt.preventDefault(); 353 + tabs[targetIdx].focus(); 354 + }); 355 + } 356 + 357 + let initTab = window._activeRepoTab || window.location.hash.replace('#', '') || 'overview'; 358 + if (validTabs.indexOf(initTab) === -1) initTab = 'overview'; 359 + window.switchRepoTab(initTab); 360 + } 361 + 362 + initTabs(); 363 + 364 + // Keyboard shortcuts: first letter of each tab name 365 + document.addEventListener('keydown', e => { 366 + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || 367 + e.target.tagName === 'SELECT' || e.target.isContentEditable) return; 368 + if (e.ctrlKey || e.metaKey || e.altKey) return; 369 + const map = { o: 'overview', l: 'layers', v: 'vulns', s: 'sbom', a: 'artifacts' }; 370 + const tab = map[e.key.toLowerCase()]; 371 + if (tab && validTabs.indexOf(tab) !== -1) window.switchRepoTab(tab); 372 + }); 373 + 374 + // Re-run after HTMX swaps tag section content (tag dropdown change) 375 + document.body.addEventListener('htmx:afterSettle', evt => { 376 + if (evt.detail.target && evt.detail.target.id === 'tag-content') { 377 + initTabs(); 378 + } 379 + }); 380 + } 381 + 382 + document.addEventListener('DOMContentLoaded', () => { 383 + initOverviewEditor(); 384 + initTabController(); 385 + });
+268
pkg/appview/src/js/settings.js
··· 1 + // Settings page: tab controller + account deletion modal. 2 + // Both initializers are no-ops when their targets aren't on the page. 3 + 4 + // ---------------------------------------- 5 + // Tab controller 6 + // Mobile horizontal tablist (.settings-tab-mobile) and desktop vertical 7 + // sidebar menu (.menu li[data-tab]) stay in sync via a single switch fn. 8 + // Uses roving tabindex + arrow-key nav per WAI-ARIA tabs pattern. 9 + // ---------------------------------------- 10 + function initSettingsTabs() { 11 + const validTabs = ['user', 'billing', 'storage', 'devices', 'webhooks', 'advanced']; 12 + if (!document.querySelector('.settings-tab-mobile, .menu li[data-tab]')) return; 13 + 14 + function switchSettingsTab(tabId) { 15 + document.querySelectorAll('.settings-panel').forEach(p => p.classList.add('hidden')); 16 + const panel = document.getElementById('tab-' + tabId); 17 + if (panel) panel.classList.remove('hidden'); 18 + 19 + document.querySelectorAll('.menu li[data-tab]').forEach(li => { 20 + const active = li.dataset.tab === tabId; 21 + li.classList.toggle('menu-active', active); 22 + const a = li.querySelector('a[role="tab"]'); 23 + if (a) { 24 + a.setAttribute('aria-selected', active ? 'true' : 'false'); 25 + a.setAttribute('tabindex', active ? '0' : '-1'); 26 + } 27 + }); 28 + 29 + document.querySelectorAll('.settings-tab-mobile').forEach(btn => { 30 + const active = btn.dataset.tab === tabId; 31 + btn.classList.toggle('btn-ghost', !active); 32 + btn.classList.toggle('btn-secondary', active); 33 + btn.setAttribute('aria-selected', active ? 'true' : 'false'); 34 + btn.setAttribute('tabindex', active ? '0' : '-1'); 35 + }); 36 + 37 + history.replaceState(null, '', '#' + tabId); 38 + document.body.dispatchEvent(new CustomEvent('tab:' + tabId)); 39 + } 40 + 41 + // Exposed so HTMX hx-trigger="every 30s[isTabActive('devices')]" can poll. 42 + window.isTabActive = function(tabId) { 43 + const panel = document.getElementById('tab-' + tabId); 44 + return panel && !panel.classList.contains('hidden'); 45 + }; 46 + 47 + // Exposed so inline <a href="#billing"> onclick can still hop tabs. 48 + window.switchSettingsTab = switchSettingsTab; 49 + 50 + function handleTabKeydown(tabs, orientation) { 51 + const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'; 52 + const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'; 53 + return function(e) { 54 + const idx = tabs.indexOf(e.currentTarget); 55 + if (idx === -1) return; 56 + let target = null; 57 + if (e.key === prevKey) target = tabs[(idx - 1 + tabs.length) % tabs.length]; 58 + else if (e.key === nextKey) target = tabs[(idx + 1) % tabs.length]; 59 + else if (e.key === 'Home') target = tabs[0]; 60 + else if (e.key === 'End') target = tabs[tabs.length - 1]; 61 + if (!target) return; 62 + e.preventDefault(); 63 + switchSettingsTab(target.dataset.tab || target.parentElement.dataset.tab); 64 + target.focus(); 65 + }; 66 + } 67 + 68 + const mobileTabs = Array.from(document.querySelectorAll('.settings-tab-mobile')); 69 + const mobileKeydown = handleTabKeydown(mobileTabs, 'horizontal'); 70 + mobileTabs.forEach(btn => { 71 + btn.addEventListener('click', e => { 72 + e.preventDefault(); 73 + switchSettingsTab(btn.dataset.tab); 74 + }); 75 + btn.addEventListener('keydown', mobileKeydown); 76 + }); 77 + 78 + const sidebarTabs = Array.from(document.querySelectorAll('.menu li[data-tab] a[role="tab"]')); 79 + const sidebarKeydown = handleTabKeydown(sidebarTabs, 'vertical'); 80 + sidebarTabs.forEach(link => { 81 + link.addEventListener('click', e => { 82 + e.preventDefault(); 83 + switchSettingsTab(link.parentElement.dataset.tab); 84 + }); 85 + link.addEventListener('keydown', sidebarKeydown); 86 + }); 87 + 88 + let hash = window.location.hash.replace('#', '') || 'user'; 89 + if (validTabs.indexOf(hash) === -1) hash = 'user'; 90 + switchSettingsTab(hash); 91 + 92 + window.addEventListener('hashchange', () => { 93 + let h = window.location.hash.replace('#', '') || 'user'; 94 + if (validTabs.indexOf(h) !== -1) switchSettingsTab(h); 95 + }); 96 + } 97 + 98 + // ---------------------------------------- 99 + // Account deletion modal 100 + // Reads product name + handle from data-attributes on the trigger so the 101 + // module stays pure JS with no server-side string interpolation. 102 + // ---------------------------------------- 103 + function initAccountDeletion() { 104 + const deleteBtn = document.getElementById('delete-account-btn'); 105 + if (!deleteBtn) return; 106 + 107 + const clientShortName = deleteBtn.dataset.clientShortName || 'this account'; 108 + const profileHandle = deleteBtn.dataset.profileHandle || ''; 109 + const expectedConfirmation = 'DELETE ' + profileHandle; 110 + 111 + function escapeHtml(text) { 112 + const div = document.createElement('div'); 113 + div.textContent = text; 114 + return div.innerHTML; 115 + } 116 + 117 + deleteBtn.addEventListener('click', showDeleteConfirmationModal); 118 + 119 + function showDeleteConfirmationModal() { 120 + const deletePDSNow = document.getElementById('delete-pds-records').checked; 121 + 122 + const modal = document.createElement('div'); 123 + modal.className = 'modal modal-open'; 124 + modal.innerHTML = ` 125 + <div class="modal-box bg-base-200 max-w-lg"> 126 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 127 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#alert-triangle"></use></svg> 128 + Delete ${escapeHtml(clientShortName)} Data 129 + </h2> 130 + 131 + <div class="py-4 space-y-4"> 132 + <div class="alert alert-success"> 133 + <svg class="icon size-5" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 134 + <span>Your ATProto account will <strong>NOT</strong> be affected.</span> 135 + </div> 136 + 137 + <p class="text-base-content/80"> 138 + This action <strong>cannot be undone</strong>. This will permanently delete: 139 + </p> 140 + 141 + <ul class="list-disc list-inside text-sm space-y-1 text-base-content/70"> 142 + <li>Your ${escapeHtml(clientShortName)} account and all settings</li> 143 + <li>All authorized devices</li> 144 + <li>Your data from all holds you're a member of</li> 145 + ${deletePDSNow ? '<li>All io.atcr.* records from your ATProto PDS</li>' : ''} 146 + </ul> 147 + 148 + <div class="space-y-2"> 149 + <label for="confirm-delete-input" class="text-sm">Type <strong class="font-mono">${escapeHtml(expectedConfirmation)}</strong> to confirm:</label> 150 + <input type="text" id="confirm-delete-input" class="input input-bordered w-full font-mono" placeholder="${escapeHtml(expectedConfirmation)}" autocomplete="off"> 151 + </div> 152 + </div> 153 + 154 + <div class="modal-action"> 155 + <button type="button" class="btn" id="cancel-delete">Cancel</button> 156 + <button type="button" class="btn btn-error gap-2" id="confirm-delete" disabled> 157 + <svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#trash-2"></use></svg> 158 + Delete My ${escapeHtml(clientShortName)} Data 159 + </button> 160 + </div> 161 + </div> 162 + <div class="modal-backdrop bg-black/50" id="modal-backdrop"></div> 163 + `; 164 + document.body.appendChild(modal); 165 + 166 + const confirmInput = document.getElementById('confirm-delete-input'); 167 + const confirmBtn = document.getElementById('confirm-delete'); 168 + const cancelBtn = document.getElementById('cancel-delete'); 169 + 170 + setTimeout(() => confirmInput.focus(), 100); 171 + 172 + confirmInput.addEventListener('input', function() { 173 + confirmBtn.disabled = this.value !== expectedConfirmation; 174 + }); 175 + 176 + confirmInput.addEventListener('keydown', function(e) { 177 + if (e.key === 'Enter' && this.value === expectedConfirmation) { 178 + performAccountDeletion(); 179 + } 180 + }); 181 + 182 + cancelBtn.addEventListener('click', () => modal.remove()); 183 + document.getElementById('modal-backdrop').addEventListener('click', () => modal.remove()); 184 + 185 + function escHandler(e) { 186 + if (e.key === 'Escape') { 187 + modal.remove(); 188 + document.removeEventListener('keydown', escHandler); 189 + } 190 + } 191 + document.addEventListener('keydown', escHandler); 192 + 193 + confirmBtn.addEventListener('click', performAccountDeletion); 194 + 195 + async function performAccountDeletion() { 196 + const deletePDS = document.getElementById('delete-pds-records').checked; 197 + 198 + confirmBtn.disabled = true; 199 + confirmBtn.innerHTML = '<svg class="icon size-4 animate-spin" aria-hidden="true"><use href="/icons.svg#loader-2"></use></svg> Deleting...'; 200 + cancelBtn.disabled = true; 201 + 202 + try { 203 + const response = await fetch('/api/account', { 204 + method: 'DELETE', 205 + headers: { 'Content-Type': 'application/json' }, 206 + body: JSON.stringify({ 207 + delete_pds_records: deletePDS, 208 + confirmation: expectedConfirmation 209 + }) 210 + }); 211 + 212 + const result = await response.json(); 213 + 214 + if (response.ok && result.success) { 215 + modal.querySelector('.modal-box').innerHTML = ` 216 + <h2 class="text-xl font-bold flex items-center gap-2 text-success"> 217 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 218 + Account Deleted 219 + </h2> 220 + <div class="py-4 space-y-2"> 221 + <p>Your account has been successfully deleted.</p> 222 + <p class="text-base-content/70">Redirecting to home page...</p> 223 + </div> 224 + `; 225 + setTimeout(() => { window.location.href = '/?deleted=true'; }, 2000); 226 + } else { 227 + const errors = result.errors || ['An unknown error occurred']; 228 + modal.querySelector('.modal-box').innerHTML = ` 229 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 230 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg> 231 + Deletion Failed 232 + </h2> 233 + <div class="py-4 space-y-4"> 234 + <p>There were errors during account deletion:</p> 235 + <ul class="list-disc list-inside text-sm space-y-1 text-error"> 236 + ${errors.map(e => '<li>' + escapeHtml(e) + '</li>').join('')} 237 + </ul> 238 + </div> 239 + <div class="modal-action"> 240 + <button type="button" class="btn" data-dismiss-modal>Close</button> 241 + </div> 242 + `; 243 + modal.querySelector('[data-dismiss-modal]').addEventListener('click', () => modal.remove()); 244 + } 245 + } catch (err) { 246 + console.error('Delete account error:', err); 247 + modal.querySelector('.modal-box').innerHTML = ` 248 + <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 249 + <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg> 250 + Error 251 + </h2> 252 + <div class="py-4"> 253 + <p>Failed to delete account: ${escapeHtml(err.message)}</p> 254 + </div> 255 + <div class="modal-action"> 256 + <button type="button" class="btn" data-dismiss-modal>Close</button> 257 + </div> 258 + `; 259 + modal.querySelector('[data-dismiss-modal]').addEventListener('click', () => modal.remove()); 260 + } 261 + } 262 + } 263 + } 264 + 265 + document.addEventListener('DOMContentLoaded', () => { 266 + initSettingsTabs(); 267 + initAccountDeletion(); 268 + });
+1 -1
pkg/appview/templates/components/card-grid.html
··· 41 41 <p class="text-lg">{{ or .EmptyMessage "No repositories found." }}</p> 42 42 </div> 43 43 {{ if .EmptySubtext }} 44 - <p class="text-base-content/40 text-sm">{{ .EmptySubtext }}</p> 44 + <p class="text-base-content/70 text-sm">{{ .EmptySubtext }}</p> 45 45 {{ end }} 46 46 {{ else }} 47 47 <p class="text-base-content/60">{{ or .EmptyMessage "No repositories found." }}</p>
+2 -2
pkg/appview/templates/components/docker-command.html
··· 24 24 - Display: string - short form shown in the UI (e.g. "alice.bsky.social/myapp:v1.2.3") 25 25 - Copy: string - full command copied to clipboard (e.g. "docker pull atcr.io/alice.bsky.social/myapp:v1.2.3") 26 26 */}} 27 - <div class="cmd group !w-full !bg-base-100"> 27 + <div class="cmd group !w-full"> 28 28 {{ icon "terminal" "size-4 shrink-0 text-base-content/60" }} 29 - <code class="flex-1 min-w-0">{{ .Display }}</code> 29 + <code>{{ .Display }}</code> 30 30 <button class="btn btn-ghost btn-xs absolute right-1 top-1/2 -translate-y-1/2 sm:opacity-0 sm:group-hover:opacity-100 focus:opacity-100 transition-opacity" data-cmd="{{ .Copy }}" aria-label="Copy pull command to clipboard"> 31 31 {{ icon "copy" "size-4" }} 32 32 </button>
+3 -3
pkg/appview/templates/components/footer.html
··· 1 1 {{ define "footer" }} 2 2 <footer class="footer footer-center bg-base-200 text-base-content p-6 pt-20 mt-auto relative"> 3 - <img src="/static/wave-pattern.svg" alt="" class="absolute top-0 left-0 w-full h-16 pointer-events-none rotate-180" aria-hidden="true"> 3 + <img src="/static/wave-pattern.svg" alt="" width="1440" height="64" loading="lazy" decoding="async" class="absolute top-0 left-0 w-full h-16 pointer-events-none rotate-180" aria-hidden="true"> 4 4 <nav class="flex flex-wrap justify-center items-center gap-x-2 gap-y-1 text-sm"> 5 5 <a href="/privacy" class="link link-hover">Privacy</a> 6 6 <span class="text-base-content/30">·</span> ··· 12 12 </a> 13 13 <span class="text-base-content/30">·</span> 14 14 <a href="https://tangled.org/evan.jarrett.net/at-container-registry" target="_blank" rel="noopener" class="link link-hover inline-flex items-center gap-1"> 15 - <img src="/static/tangled-black.svg" alt="" class="size-3.5 icon-light"> 16 - <img src="/static/tangled-white.svg" alt="" class="size-3.5 icon-dark"> 15 + <img src="/static/tangled-black.svg" alt="" width="14" height="14" loading="lazy" decoding="async" class="size-3.5 icon-light"> 16 + <img src="/static/tangled-white.svg" alt="" width="14" height="14" loading="lazy" decoding="async" class="size-3.5 icon-dark"> 17 17 Source 18 18 </a> 19 19 </nav>
+3 -1
pkg/appview/templates/components/head.html
··· 14 14 <link rel="preconnect" href="https://imgs.blue" crossorigin> 15 15 <link rel="dns-prefetch" href="https://imgs.blue"> 16 16 17 - <!-- Preload critical assets --> 17 + <!-- Preload critical assets. Onest is the display face used on h1/hero 18 + headings and is the LCP candidate on landing/discovery pages. --> 18 19 <link rel="preload" href="/icons.svg" as="image" type="image/svg+xml"> 20 + <link rel="preload" href="/fonts/onest-latin.woff2" as="font" type="font/woff2" crossorigin> 19 21 <link rel="preload" href="/fonts/figtree-latin.woff2" as="font" type="font/woff2" crossorigin> 20 22 <link rel="preload" href="/fonts/commit-mono-400.woff2" as="font" type="font/woff2" crossorigin> 21 23
+5 -5
pkg/appview/templates/components/hero.html
··· 13 13 <div class="mockup-code bg-base-300 text-base-content text-left w-full max-w-lg text-base mt-8"> 14 14 <pre data-prefix="$"><code>docker login {{ .RegistryURL }}</code></pre> 15 15 <pre data-prefix="$"><code>docker push {{ .RegistryURL }}/you/app</code></pre> 16 - <pre data-prefix="#" class="text-base-content/65"><code>same docker, decentralized</code></pre> 16 + <pre data-prefix="#" class="text-base-content/70"><code>same docker, decentralized</code></pre> 17 17 </div> 18 18 19 19 <div class="flex items-center justify-center gap-4 mt-8"> ··· 23 23 24 24 <!-- Benefit Cards --> 25 25 <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12 w-full max-w-4xl"> 26 - <div class="card bg-base-100 shadow-sm p-6 text-center"> 26 + <div class="card bg-base-200 shadow-sm p-6 text-center"> 27 27 <div class="text-primary mb-4 flex justify-center"> 28 28 {{ icon "ship" "size-8" }} 29 29 </div> 30 30 <h2 class="font-semibold text-lg">Works with Docker</h2> 31 31 <p class="text-base-content/70 mt-2">Use docker push &amp; pull. No new tools to learn.</p> 32 32 </div> 33 - <div class="card bg-base-100 shadow-sm p-6 text-center"> 33 + <div class="card bg-base-200 shadow-sm p-6 text-center"> 34 34 <div class="text-primary mb-4 flex justify-center"> 35 35 {{ icon "anchor" "size-8" }} 36 36 </div> 37 37 <h2 class="font-semibold text-lg">Your Data</h2> 38 38 <p class="text-base-content/70 mt-2">Join shared holds or captain your own storage.</p> 39 39 </div> 40 - <div class="card bg-base-100 shadow-sm p-6 text-center"> 40 + <div class="card bg-base-200 shadow-sm p-6 text-center"> 41 41 <div class="text-primary mb-4 flex justify-center"> 42 42 {{ icon "compass" "size-8" }} 43 43 </div> ··· 46 46 </div> 47 47 </div> 48 48 </div> 49 - <img src="/static/wave-pattern.svg" width="1440" height="128" alt="" class="absolute bottom-0 left-0 w-full h-32 pointer-events-none" aria-hidden="true"> 49 + <img src="/static/wave-pattern.svg" width="1440" height="128" alt="" loading="lazy" decoding="async" class="absolute bottom-0 left-0 w-full h-32 pointer-events-none" aria-hidden="true"> 50 50 </section> 51 51 {{ end }}
+18 -21
pkg/appview/templates/components/modal.html
··· 1 1 {{ define "manifest-modal" }} 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()" aria-label="Close manifest details">✕</button> 2 + <dialog class="modal modal-open" data-action="modal-backdrop-close"> 3 + <div class="modal-box bg-base-200"> 4 + <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" data-action="remove-closest-dialog" aria-label="Close manifest details">✕</button> 5 5 6 6 <h2 class="text-xl font-semibold mb-4">Manifest Details</h2> 7 7 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 - </div> 13 - <div class="flex justify-between items-center"> 14 - <strong class="text-base-content/60 min-w-[150px]">Media Type:</strong> 15 - <span>{{ .MediaType }}</span> 16 - </div> 17 - <div class="flex justify-between items-center"> 18 - <strong class="text-base-content/60 min-w-[150px]">Hold Endpoint:</strong> 19 - <span>{{ .HoldEndpoint }}</span> 20 - </div> 21 - <div class="flex justify-between items-center"> 22 - <strong class="text-base-content/60 min-w-[150px]">Created:</strong> 8 + <dl class="grid grid-cols-[max-content_1fr] gap-x-6 gap-y-3 text-sm"> 9 + <dt class="text-base-content/70 font-medium">Digest</dt> 10 + <dd class="font-mono break-all">{{ .Digest }}</dd> 11 + 12 + <dt class="text-base-content/70 font-medium">Media Type</dt> 13 + <dd class="break-all">{{ .MediaType }}</dd> 14 + 15 + <dt class="text-base-content/70 font-medium">Hold Endpoint</dt> 16 + <dd class="break-all">{{ .HoldEndpoint }}</dd> 17 + 18 + <dt class="text-base-content/70 font-medium">Created</dt> 19 + <dd> 23 20 <time datetime="{{ .CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}"> 24 21 {{ .CreatedAt.Format "2006-01-02 15:04:05 MST" }} 25 22 </time> 26 - </div> 27 - </div> 23 + </dd> 24 + </dl> 28 25 </div> 29 26 <form method="dialog" class="modal-backdrop"> 30 - <button onclick="this.closest('dialog').remove()">close</button> 27 + <button data-action="remove-closest-dialog">close</button> 31 28 </form> 32 29 </dialog> 33 30 {{ end }}
+1 -1
pkg/appview/templates/components/nav-brand.html
··· 1 1 {{ define "nav-brand" }} 2 2 <a href="/" class="flex items-center gap-2 text-2xl font-display font-bold text-secondary no-underline tracking-tight"> 3 - <img src="/favicon-96x96.png" class="h-12 w-auto" alt="{{ .ClientName }} logo"> 3 + <img src="/favicon-96x96.png" width="48" height="48" fetchpriority="high" class="h-12 w-auto" alt="{{ .ClientName }} logo"> 4 4 {{ .ClientName }} 5 5 </a> 6 6 {{ end }}
+3 -4
pkg/appview/templates/components/nav-search.html
··· 1 1 {{ define "nav-search" }} 2 2 <div class="nav-search-wrapper"> 3 - <button onclick="toggleSearch()" class="btn btn-ghost btn-circle" aria-label="Search"> 3 + <button type="button" data-action="toggle-search" class="btn btn-ghost btn-circle" aria-label="Search" aria-expanded="false" aria-controls="nav-search-form"> 4 4 {{ icon "search" "size-5" }} 5 5 </button> 6 - <form action="/search" method="get" class="nav-search-form" role="search"> 6 + <form id="nav-search-form" action="/search" method="get" class="nav-search-form" role="search" inert> 7 7 <label for="nav-search-input" class="sr-only">Search images</label> 8 - <input type="text" id="nav-search-input" name="q" placeholder="Search images..." value="{{ .Query }}" class="input input-sm input-bordered" aria-label="Search images" /> 9 - <button type="submit" class="sr-only">Search</button> 8 + <input type="text" id="nav-search-input" name="q" placeholder="Search images..." value="{{ .Query }}" class="input input-sm input-bordered" /> 10 9 </form> 11 10 </div> 12 11 {{ end }}
+8 -8
pkg/appview/templates/components/nav-theme-toggle.html
··· 1 1 {{ define "nav-theme-toggle" }} 2 2 <details class="dropdown dropdown-end"> 3 - <summary data-theme-toggle class="btn btn-ghost btn-circle list-none" aria-label="Theme settings"> 3 + <summary data-theme-toggle class="btn btn-ghost btn-circle list-none" aria-label="Theme settings" aria-haspopup="menu"> 4 4 <svg class="icon size-5" data-theme-icon aria-hidden="true"><use href="/icons.svg#sun"></use></svg> 5 5 </summary> 6 - <ul data-theme-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"> 6 + <ul data-theme-menu role="menu" class="dropdown-content menu bg-base-200 text-base-content rounded-box z-50 w-40 p-2 shadow-lg"> 7 + <li role="none"> 8 + <button type="button" role="menuitemradio" aria-checked="false" class="theme-option" data-value="system"> 9 9 {{ icon "sun-moon" "size-4" }} 10 10 <span>System</span> 11 11 {{ icon "check" "size-4 ml-auto text-secondary theme-check invisible" }} 12 12 </button> 13 13 </li> 14 - <li> 15 - <button type="button" class="theme-option" data-value="light"> 14 + <li role="none"> 15 + <button type="button" role="menuitemradio" aria-checked="false" class="theme-option" data-value="light"> 16 16 {{ icon "sun" "size-4" }} 17 17 <span>Light</span> 18 18 {{ icon "check" "size-4 ml-auto text-secondary theme-check invisible" }} 19 19 </button> 20 20 </li> 21 - <li> 22 - <button type="button" class="theme-option" data-value="dark"> 21 + <li role="none"> 22 + <button type="button" role="menuitemradio" aria-checked="false" class="theme-option" data-value="dark"> 23 23 {{ icon "moon" "size-4" }} 24 24 <span>Dark</span> 25 25 {{ icon "check" "size-4 ml-auto text-secondary theme-check invisible" }}
+3 -3
pkg/appview/templates/components/nav-user.html
··· 5 5 <div class="avatar{{ if not .User.Avatar }} avatar-placeholder{{ end }}"> 6 6 {{ if .User.Avatar }} 7 7 <div class="w-7 rounded-full"> 8 - <img src="{{ resizeImage .User.Avatar 96 }}" alt="{{ .User.Handle }}" loading="lazy" /> 8 + <img src="{{ resizeImage .User.Avatar 96 }}" alt="{{ .User.Handle }}" width="28" height="28" /> 9 9 </div> 10 10 {{ else }} 11 11 <div class="bg-secondary text-secondary-content w-7 rounded-full"> ··· 16 16 <span class="hidden sm:inline">@{{ .User.Handle }}</span> 17 17 {{ icon "chevron-down" "size-3.5" }} 18 18 </summary> 19 - <ul class="dropdown-content menu bg-base-100 text-base-content rounded-box z-50 w-52 p-2 shadow-lg"> 19 + <ul class="dropdown-content menu bg-base-200 text-base-content rounded-box z-50 w-52 p-2 shadow-lg"> 20 20 <li><a href="/u/{{ .User.Handle }}">Your Repositories</a></li> 21 21 <li><a href="/settings">Settings</a></li> 22 22 <li class="border-t border-base-300 mt-2 pt-2"> ··· 26 26 <form id="logout-form" action="/auth/logout" method="POST" hidden></form> 27 27 </details> 28 28 {{ else }} 29 - <button type="button" onclick="window.location='/auth/oauth/login?return_to=/'" class="btn btn-secondary btn-sm">Login</button> 29 + <a href="/auth/oauth/login?return_to=/" class="btn btn-secondary btn-sm">Login</a> 30 30 {{ end }} 31 31 {{ end }}
+8 -2
pkg/appview/templates/components/nav.html
··· 1 + {{ define "skip-link" }} 2 + <a href="#main-content" class="skip-link">Skip to content</a> 3 + {{ end }} 4 + 1 5 {{ define "nav" }} 2 - <nav class="navbar bg-base-300 text-secondary px-4"> 6 + {{ template "skip-link" }} 7 + <nav class="navbar bg-base-200 text-secondary px-4"> 3 8 <div class="navbar-start"> 4 9 {{ template "nav-brand" . }} 5 10 </div> ··· 12 17 {{ end }} 13 18 14 19 {{ define "nav-simple" }} 15 - <nav class="navbar bg-base-300 text-secondary px-4"> 20 + {{ template "skip-link" }} 21 + <nav class="navbar bg-base-200 text-secondary px-4"> 16 22 <div class="navbar-start"> 17 23 {{ template "nav-brand" . }} 18 24 </div>
+8 -43
pkg/appview/templates/components/pull-command-switcher.html
··· 18 18 {{ end }} 19 19 </div> 20 20 {{ else }} 21 - <div class="space-y-2" id="pull-cmd-container"> 22 - <p class="text-sm font-medium text-base-content/70">Pull this image</p> 21 + <div class="space-y-2" id="pull-cmd-container" 22 + data-registry-url="{{ .RegistryURL }}" 23 + data-owner-handle="{{ .OwnerHandle }}" 24 + data-repo-name="{{ .RepoName }}" 25 + data-tag="{{ if .Tag }}{{ .Tag }}{{ else }}latest{{ end }}" 26 + data-is-logged-in="{{ .IsLoggedIn }}"> 27 + <label for="oci-client-switcher" class="text-sm font-medium text-base-content/70">Pull this image</label> 23 28 <div class="flex items-center gap-2"> 24 - <select id="oci-client-switcher" class="select select-xs select-bordered w-auto" 25 - onchange="updatePullCommand(this.value)"> 29 + <select id="oci-client-switcher" class="select select-xs select-bordered w-auto"> 26 30 <option value="docker"{{ if or (eq .OciClient "") (eq .OciClient "docker") }} selected{{ end }}>docker</option> 27 31 <option value="podman"{{ if eq .OciClient "podman" }} selected{{ end }}>podman</option> 28 32 <option value="nerdctl"{{ if eq .OciClient "nerdctl" }} selected{{ end }}>nerdctl</option> ··· 39 43 </div> 40 44 </div> 41 45 </div> 42 - <script> 43 - (function() { 44 - var registryURL = {{ .RegistryURL }}; 45 - var ownerHandle = {{ .OwnerHandle }}; 46 - var repoName = {{ .RepoName }}; 47 - var tag = {{ if .Tag }}{{ .Tag }}{{ else }}"latest"{{ end }}; 48 - var isLoggedIn = {{ .IsLoggedIn }}; 49 - 50 - // Restore from localStorage for anonymous users 51 - if (!isLoggedIn) { 52 - var saved = localStorage.getItem('oci-client'); 53 - if (saved) { 54 - var sel = document.getElementById('oci-client-switcher'); 55 - if (sel) { 56 - sel.value = saved; 57 - updatePullCommand(saved); 58 - } 59 - } 60 - } 61 - 62 - window.updatePullCommand = function(client) { 63 - var prefix = client === 'none' ? '' : client + ' pull '; 64 - var cmd = prefix + registryURL + '/' + ownerHandle + '/' + repoName + ':' + tag; 65 - var container = document.getElementById('pull-cmd-display'); 66 - if (!container) return; 67 - var code = container.querySelector('code'); 68 - if (code) code.textContent = cmd; 69 - var btn = container.querySelector('[data-cmd]'); 70 - if (btn) btn.dataset.cmd = cmd; 71 - 72 - // Persist preference 73 - if (isLoggedIn) { 74 - htmx.ajax('POST', '/api/profile/oci-client', {values: {oci_client: client}, swap: 'none'}); 75 - } else { 76 - localStorage.setItem('oci-client', client); 77 - } 78 - }; 79 - })(); 80 - </script> 81 46 {{ end }} 82 47 {{ end }}
+1 -1
pkg/appview/templates/components/repo-avatar.html
··· 6 6 */}} 7 7 <div id="repo-avatar" class="relative shrink-0"> 8 8 {{ if .IconURL }} 9 - <img src="{{ resizeImage .IconURL 96 }}" alt="{{ .RepositoryName }}" loading="lazy" class="w-20 rounded-lg object-cover"> 9 + <img src="{{ resizeImage .IconURL 160 }}" alt="{{ .RepositoryName }}" width="80" height="80" fetchpriority="high" class="w-20 rounded-lg object-cover"> 10 10 {{ else }} 11 11 <div class="avatar avatar-placeholder"> 12 12 <div class="bg-neutral text-neutral-content w-20 rounded-lg shadow-sm uppercase">
+8 -7
pkg/appview/templates/components/repo-card.html
··· 17 17 - LastUpdated: time.Time (optional) - Last push time 18 18 - RegistryURL: string - Registry URL for docker commands (e.g., "atcr.io") 19 19 */}} 20 - <div class="card card-interactive bg-base-200 border-2 border-base-300 p-4 flex flex-col justify-between min-h-60 w-full" data-href="/r/{{ .OwnerHandle }}/{{ .Repository }}"> 20 + <article class="card bg-base-200 card-interactive p-4 flex flex-col justify-between min-h-60 w-full"> 21 + <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="card-stretched-link" aria-label="{{ .OwnerHandle }}/{{ .Repository }} repository"></a> 21 22 <div class="flex gap-4 items-start"> 22 23 {{ if .IconURL }} 23 - <img src="{{ resizeImage .IconURL 96 }}" alt="{{ .Repository }}" loading="lazy" width="48" height="48" class="w-12 rounded-lg object-cover shrink-0"> 24 + <img src="{{ resizeImage .IconURL 96 }}" alt="" loading="lazy" width="48" height="48" class="w-12 rounded-lg object-cover shrink-0"> 24 25 {{ else if .OwnerAvatarURL }} 25 - <img src="{{ resizeImage .OwnerAvatarURL 96 }}" alt="{{ .OwnerHandle }}" loading="lazy" width="48" height="48" class="w-12 rounded-lg object-cover shrink-0"> 26 + <img src="{{ resizeImage .OwnerAvatarURL 96 }}" alt="" loading="lazy" width="48" height="48" class="w-12 rounded-lg object-cover shrink-0"> 26 27 {{ else }} 27 28 <div class="avatar avatar-placeholder"> 28 29 <div class="bg-neutral text-neutral-content w-12 rounded-lg shadow-sm uppercase"> ··· 32 33 {{ end }} 33 34 <div class="flex-1 min-w-0"> 34 35 <div class="font-semibold text-sm flex items-baseline gap-1 min-w-0"> 35 - <a href="/u/{{ .OwnerHandle }}" class="link link-primary truncate min-w-0" onclick="event.stopPropagation()">{{ .OwnerHandle }}</a> 36 + <a href="/u/{{ .OwnerHandle }}" class="link link-primary truncate min-w-0">{{ .OwnerHandle }}</a> 36 37 <span class="text-base-content/60 shrink-0">/</span> 37 - <a href="/r/{{ .OwnerHandle }}/{{ .Repository }}" class="link text-base-content hover:underline truncate min-w-0" onclick="event.stopPropagation()">{{ .Repository }}</a> 38 + <span class="text-base-content truncate min-w-0">{{ .Repository }}</span> 38 39 </div> 39 40 {{ if .Tag }} 40 41 <span class="block text-base-content/60 text-sm truncate">Tag: {{ .Tag }}</span> ··· 42 43 </div> 43 44 </div> 44 45 {{ if .Description }} 45 - <p class="text-base-content/60 text-sm line-clamp-3 m-0 my-4">{{ .Description }}</p> 46 + <p class="text-base-content/60 text-sm line-clamp-3 break-words m-0 my-4">{{ .Description }}</p> 46 47 {{ end }} 47 48 <div class="flex-1 flex flex-col justify-end py-2 min-w-0"> 48 49 {{ if eq .ArtifactType "helm-chart" }} ··· 79 80 <span class="text-base-content/60 text-sm flex items-center gap-1">{{ icon "history" "size-4" }}{{ timeAgoShort .LastUpdated }}</span> 80 81 {{ end }} 81 82 </div> 82 - </div> 83 + </article> 83 84 {{ end }}
+2 -2
pkg/appview/templates/pages/404.html
··· 7 7 </head> 8 8 <body> 9 9 {{ template "nav-simple" . }} 10 - <main class="hero min-h-[60vh]"> 10 + <main id="main-content" class="hero min-h-[60vh]"> 11 11 <div class="hero-content text-center"> 12 12 <div class="flex flex-col items-center"> 13 13 {{ icon "anchor" "size-16 text-neutral mb-4" }} 14 - <div class="font-bold text-primary" style="font-size: 150px; line-height: 1;">404</div> 14 + <div class="font-display font-bold text-primary leading-none tabular-nums" style="font-size: clamp(5rem, 18vw, 10rem);">404</div> 15 15 <h1 class="text-2xl font-semibold mt-4">Lost at Sea</h1> 16 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 17 <a href="/" class="btn btn-primary mt-6">Return to Port</a>
+4 -10
pkg/appview/templates/pages/diff.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8"> 11 + <main id="main-content" class="container mx-auto px-4 py-8"> 12 12 <div class="space-y-6"> 13 13 <!-- Breadcrumb --> 14 14 <div class="text-sm breadcrumbs"> ··· 20 20 </div> 21 21 22 22 <!-- Summary Card --> 23 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6"> 23 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6"> 24 24 <div class="flex flex-wrap items-center gap-2 mb-4"> 25 25 <h1 class="text-xl font-bold"> 26 26 <span class="font-mono">{{ .FromTag }}</span> ··· 74 74 </div> 75 75 76 76 {{ if .IsMultiArch }} 77 - <div class="flex items-center gap-3 pt-4 border-t border-base-200"> 77 + <div class="flex items-center gap-3 pt-4 border-t border-base-300"> 78 78 <label for="diff-arch-select" class="text-sm font-medium whitespace-nowrap">{{ icon "cpu" "size-4" }} Platform</label> 79 79 <select id="diff-arch-select" class="select select-sm select-bordered" 80 - onchange="switchDiffPlatform(this.value)"> 80 + data-diff-url="/diff/{{ .Owner.Handle }}/{{ .Repository }}?from={{ .FromDigest }}&to={{ .ToDigest }}&platform=__VALUE__"> 81 81 {{ range .CommonPlatforms }} 82 82 {{ $platKey := printf "%s/%s" .OS .Architecture }}{{ if .Variant }}{{ $platKey = printf "%s/%s/%s" .OS .Architecture .Variant }}{{ end }} 83 83 <option value="{{ $platKey }}"{{ if eq $platKey $.SelectedPlatform }} selected{{ end }}>{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}</option> 84 84 {{ end }} 85 85 </select> 86 86 </div> 87 - <script> 88 - function switchDiffPlatform(platform) { 89 - var url = '/diff/{{ .Owner.Handle }}/{{ .Repository }}?from={{ .FromDigest }}&to={{ .ToDigest }}&platform=' + encodeURIComponent(platform); 90 - window.location.href = url; 91 - } 92 - </script> 93 87 {{ end }} 94 88 </div> 95 89
+15 -22
pkg/appview/templates/pages/digest.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8"> 11 + <main id="main-content" class="container mx-auto px-4 py-8"> 12 12 <div class="space-y-6"> 13 13 <!-- Breadcrumb --> 14 - <div class="text-sm breadcrumbs"> 14 + <div class="text-sm breadcrumbs min-w-0"> 15 15 <ul> 16 - <li><a href="/u/{{ .Owner.Handle }}" class="link link-primary">{{ .Owner.Handle }}</a></li> 17 - <li><a href="/r/{{ .Owner.Handle }}/{{ .Repository }}" class="link link-primary">{{ .Repository }}</a></li> 18 - <li><code class="font-mono text-xs">{{ truncateDigest (trimPrefix "sha256:" .Manifest.Digest) 16 }}</code></li> 16 + <li class="min-w-0"><a href="/u/{{ .Owner.Handle }}" class="link link-primary truncate">{{ .Owner.Handle }}</a></li> 17 + <li class="min-w-0"><a href="/r/{{ .Owner.Handle }}/{{ .Repository }}" class="link link-primary truncate">{{ .Repository }}</a></li> 18 + <li class="min-w-0"><code class="font-mono text-xs truncate">{{ truncateDigest (trimPrefix "sha256:" .Manifest.Digest) 16 }}</code></li> 19 19 </ul> 20 20 </div> 21 21 22 22 <!-- Digest Header --> 23 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-3"> 23 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-3"> 24 24 <div class="flex flex-wrap items-start justify-between gap-4"> 25 25 <div class="space-y-2 min-w-0"> 26 26 <!-- Title: tags or truncated digest --> ··· 42 42 <!-- Digest (small) --> 43 43 <div class="flex items-center gap-2 text-base-content/70"> 44 44 <code class="font-mono text-xs" title="{{ .Manifest.Digest }}">{{ truncateDigest (trimPrefix "sha256:" .Manifest.Digest) 16 }}</code> 45 - <button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')" aria-label="Copy digest">{{ icon "copy" "size-3" }}</button> 45 + <button class="btn btn-ghost btn-xs" data-action="copy" data-copy="{{ .Manifest.Digest }}" aria-label="Copy digest">{{ icon "copy" "size-3" }}</button> 46 46 </div> 47 47 </div> 48 48 <div class="flex items-center gap-2 shrink-0"> ··· 52 52 53 53 {{ if .Manifest.IsManifestList }} 54 54 <!-- Architecture Dropdown --> 55 - <div class="flex items-center gap-3 pt-2 border-t border-base-200"> 55 + <div class="flex items-center gap-3 pt-2 border-t border-base-300"> 56 56 <label for="arch-select" class="text-sm font-medium whitespace-nowrap">{{ icon "cpu" "size-4" }} Architecture</label> 57 57 <select id="arch-select" class="select select-sm select-bordered" 58 - onchange="loadArchContent(this.value)"> 58 + name="digest" 59 + hx-get="/api/digest-content/{{ .Owner.Handle }}/{{ .Repository }}" 60 + hx-trigger="change" 61 + hx-target="#digest-content" 62 + hx-swap="innerHTML" 63 + hx-indicator="#arch-loading"> 59 64 {{ range .Manifest.Platforms }} 60 65 <option value="{{ .Digest }}"{{ if eq .Digest $.SelectedPlatform }} selected{{ end }}>{{ .OS }}/{{ .Architecture }}{{ if .Variant }}/{{ .Variant }}{{ end }}{{ if .CompressedSize }} ({{ humanizeBytes .CompressedSize }}){{ end }}</option> 61 66 {{ end }} 62 67 </select> 63 - <span id="arch-loading" class="hidden"> 68 + <span id="arch-loading" class="htmx-indicator" aria-hidden="true"> 64 69 {{ icon "loader" "size-4 animate-spin" }} 65 70 </span> 66 71 </div> 67 - <script> 68 - function loadArchContent(digest) { 69 - var target = document.getElementById('digest-content'); 70 - var loading = document.getElementById('arch-loading'); 71 - loading.classList.remove('hidden'); 72 - htmx.ajax('GET', '/api/digest-content/{{ .Owner.Handle }}/{{ .Repository }}?digest=' + encodeURIComponent(digest), {target: target, swap: 'innerHTML'}).then(function() { 73 - loading.classList.add('hidden'); 74 - }); 75 - 76 - 77 - } 78 - </script> 79 72 {{ end }} 80 73 </div> 81 74
+2 -8
pkg/appview/templates/pages/home.html
··· 4 4 <head> 5 5 {{ template "head" . }} 6 6 {{ template "meta" .Meta }} 7 - {{ if not .User }} 8 - <!-- Preload hero images --> 9 - {{ end }} 10 7 </head> 11 8 <body> 12 9 {{ template "nav" . }} ··· 15 12 {{ template "hero" . }} 16 13 {{ end }} 17 14 18 - <main class="container mx-auto px-4 py-8"> 15 + <main id="main-content" class="container mx-auto px-4 py-8"> 19 16 <div class="space-y-12"> 20 17 <!-- Featured Repositories Section --> 21 18 {{ if .FeaturedRepos }} ··· 31 28 </button> 32 29 </div> 33 30 </div> 34 - <div id="featured-carousel" class="carousel w-full gap-6 scroll-smooth"> 31 + <div id="featured-carousel" class="carousel w-full gap-3 sm:gap-6 scroll-smooth"> 35 32 {{ range $i, $repo := .FeaturedRepos }} 36 33 <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"> 37 34 {{ template "repo-card" $repo }} ··· 50 47 {{ end }} 51 48 </div> 52 49 </main> 53 - 54 - <!-- Modal container for HTMX --> 55 - <div id="modal"></div> 56 50 57 51 {{ template "footer" . }} 58 52 </body>
+7 -26
pkg/appview/templates/pages/install.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8 max-w-4xl"> 11 + <main id="main-content" class="container mx-auto px-4 py-8 max-w-4xl"> 12 12 <h1 class="text-3xl font-display font-bold tracking-tight mb-2">Install {{ .ClientShortName }} Credential Helper</h1> 13 13 <p class="text-base-content/70 mb-8">The {{ .ClientShortName }} credential helper enables Docker to authenticate with {{ .ClientShortName }} registries using your ATProto identity.</p> 14 14 ··· 17 17 <h2 class="text-xl font-semibold mb-4">Quick Install</h2> 18 18 19 19 <div class="flex gap-2 mb-4"> 20 - <button class="btn btn-sm btn-primary platform-tab" onclick="showPlatform('linux')">Linux / macOS</button> 21 - <button class="btn btn-sm btn-ghost platform-tab" onclick="showPlatform('windows')">Windows</button> 20 + <button type="button" class="btn btn-sm btn-primary platform-tab" data-platform="linux">Linux / macOS</button> 21 + <button type="button" class="btn btn-sm btn-ghost platform-tab" data-platform="windows">Windows</button> 22 22 </div> 23 23 24 24 <div class="platform-content" id="linux-content"> ··· 53 53 <h2 class="text-xl font-semibold mb-4">Configure Docker</h2> 54 54 <p class="mb-4">After installation, configure Docker to use the credential helper:</p> 55 55 56 - <h3 class="text-lg font-medium mb-3">Create or edit <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">~/.docker/config.json</code></h3> 56 + <h3 class="text-lg font-medium mb-3">Create or edit <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">~/.docker/config.json</code></h3> 57 57 <div class="mockup-code bg-base-300 text-base-content mb-4"> 58 58 <pre><code>{</code></pre> 59 59 <pre><code> "credHelpers": {</code></pre> ··· 92 92 93 93 <section> 94 94 <h2 class="text-xl font-semibold mb-4">Alternative: Use docker login</h2> 95 - <p class="mb-4">You can also use <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">docker login</code> with your ATProto app password:</p> 95 + <p class="mb-4">You can also use <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">docker login</code> with your ATProto app password:</p> 96 96 97 97 <ol class="list-decimal list-inside space-y-2 ml-4 mb-6"> 98 98 <li>Generate an app password at <a href="https://bsky.app/settings/app-passwords" target="_blank" class="link link-primary">bsky.app/settings/app-passwords</a></li> 99 - <li>Run: <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">docker login {{ .RegistryURL }}</code></li> 99 + <li>Run: <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">docker login {{ .RegistryURL }}</code></li> 100 100 <li>Enter your handle as username</li> 101 101 <li>Enter your app password</li> 102 102 </ol> ··· 127 127 <section> 128 128 <h2 class="text-xl font-semibold mb-4">Security</h2> 129 129 <ul class="list-disc list-inside space-y-2 ml-4"> 130 - <li>Credentials are stored in <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">~/.atcr/device.json</code> with secure permissions (0600)</li> 130 + <li>Credentials are stored in <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">~/.atcr/device.json</code> with secure permissions (0600)</li> 131 131 <li>Device secrets are issued per-device and can be revoked anytime</li> 132 132 <li>No passwords are stored locally</li> 133 133 <li>Uses ATProto OAuth with device authorization flow</li> ··· 136 136 </div> 137 137 </main> 138 138 139 - <script> 140 - function showPlatform(platform) { 141 - // Update tabs 142 - document.querySelectorAll('.platform-tab').forEach(tab => { 143 - tab.classList.remove('btn-primary'); 144 - tab.classList.add('btn-ghost'); 145 - }); 146 - event.target.classList.remove('btn-ghost'); 147 - event.target.classList.add('btn-primary'); 148 - 149 - // Update content 150 - document.querySelectorAll('.platform-content').forEach(content => { 151 - content.classList.add('hidden'); 152 - }); 153 - document.getElementById(platform + '-content').classList.remove('hidden'); 154 - } 155 - </script> 156 - 157 - <div id="modal"></div> 158 139 159 140 {{ template "footer" . }} 160 141 </body>
+12 -12
pkg/appview/templates/pages/learn-more.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8 max-w-4xl"> 11 + <main id="main-content" class="container mx-auto px-4 py-8 max-w-4xl"> 12 12 <!-- Hero Section --> 13 13 <section class="text-center mb-16"> 14 14 <h1 class="text-4xl md:text-5xl font-display font-bold tracking-tight mb-4">Docker meets the decentralized web</h1> ··· 31 31 </p> 32 32 </div> 33 33 34 - <div class="collapse collapse-arrow bg-base-100 border border-base-300"> 34 + <div class="collapse collapse-arrow bg-base-200 border border-base-300"> 35 35 <input type="checkbox" aria-label="Toggle technical details" /> 36 36 <div class="collapse-title font-medium"> 37 37 Technical Details ··· 51 51 <h2 class="text-2xl font-bold mb-6">Why Decentralized?</h2> 52 52 53 53 <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> 54 - <div class="card bg-base-100 border border-base-300 p-6"> 54 + <div class="card bg-base-200 border border-base-300 p-6"> 55 55 <div class="text-primary mb-3"> 56 56 {{ icon "database" "size-8" }} 57 57 </div> ··· 62 62 </p> 63 63 </div> 64 64 65 - <div class="card bg-base-100 border border-base-300 p-6"> 65 + <div class="card bg-base-200 border border-base-300 p-6"> 66 66 <div class="text-primary mb-3"> 67 67 {{ icon "fingerprint" "size-8" }} 68 68 </div> ··· 73 73 </p> 74 74 </div> 75 75 76 - <div class="card bg-base-100 border border-base-300 p-6"> 76 + <div class="card bg-base-200 border border-base-300 p-6"> 77 77 <div class="text-primary mb-3"> 78 78 {{ icon "hard-drive" "size-8" }} 79 79 </div> ··· 84 84 </p> 85 85 </div> 86 86 87 - <div class="card bg-base-100 border border-base-300 p-6"> 87 + <div class="card bg-base-200 border border-base-300 p-6"> 88 88 <div class="text-primary mb-3"> 89 89 {{ icon "eye" "size-8" }} 90 90 </div> ··· 104 104 <div class="bg-base-200 rounded-lg p-6"> 105 105 <div class="flex flex-col md:flex-row items-center justify-between gap-6 text-center"> 106 106 <div class="flex-1"> 107 - <div class="bg-base-100 rounded-lg p-4 border border-base-300"> 107 + <div class="bg-base-300 rounded-lg p-4 border border-base-300"> 108 108 {{ icon "container" "size-8 text-primary mx-auto mb-2" }} 109 109 <p class="font-semibold">Docker Client</p> 110 110 <p class="text-sm text-base-content/60">Push & Pull</p> ··· 119 119 </div> 120 120 121 121 <div class="flex-1"> 122 - <div class="bg-base-100 rounded-lg p-4 border border-primary"> 122 + <div class="bg-base-300 rounded-lg p-4 border border-primary"> 123 123 {{ icon "server" "size-8 text-primary mx-auto mb-2" }} 124 124 <p class="font-semibold">AppView</p> 125 125 <p class="text-sm text-base-content/60">{{ .SiteURL }}</p> ··· 136 136 </div> 137 137 138 138 <div class="flex-1 flex flex-col gap-4"> 139 - <div class="bg-base-100 rounded-lg p-4 border border-base-300"> 139 + <div class="bg-base-300 rounded-lg p-4 border border-base-300"> 140 140 {{ icon "user" "size-8 text-primary mx-auto mb-2" }} 141 141 <p class="font-semibold">Your PDS</p> 142 142 <p class="text-sm text-base-content/60">Manifests</p> 143 143 </div> 144 - <div class="bg-base-100 rounded-lg p-4 border border-base-300"> 144 + <div class="bg-base-300 rounded-lg p-4 border border-base-300"> 145 145 {{ icon "database" "size-8 text-primary mx-auto mb-2" }} 146 146 <p class="font-semibold">Hold Service</p> 147 147 <p class="text-sm text-base-content/60">Blobs</p> ··· 165 165 <div class="flex flex-col sm:flex-row items-center justify-center gap-4"> 166 166 <a href="/install" class="btn btn-primary btn-lg">Get Started</a> 167 167 <a href="https://tangled.org/evan.jarrett.net/at-container-registry" target="_blank" rel="noopener" class="btn btn-ghost btn-lg"> 168 - {{ icon "github" "size-5 mr-2" }} 168 + <img src="/static/tangled-black.svg" alt="" width="20" height="20" loading="lazy" decoding="async" class="size-5 mr-2 icon-light"> 169 + <img src="/static/tangled-white.svg" alt="" width="20" height="20" loading="lazy" decoding="async" class="size-5 mr-2 icon-dark"> 169 170 View Source 170 171 </a> 171 172 </div> 172 173 </section> 173 174 </main> 174 175 175 - <div id="modal"></div> 176 176 177 177 {{ template "footer" . }} 178 178 </body>
+4 -3
pkg/appview/templates/pages/login.html
··· 8 8 <body> 9 9 {{ template "nav-simple" . }} 10 10 11 - <main class="min-h-[calc(100vh-4rem)] flex items-start justify-center px-4 pt-16 sm:pt-24"> 11 + <main id="main-content" class="min-h-[calc(100vh-4rem)] flex items-start justify-center px-4 pt-16 sm:pt-24"> 12 12 <div class="w-full max-w-2xl"> 13 13 <h1 class="text-3xl sm:text-4xl font-display font-semibold tracking-tight text-center mb-8">Sign in to {{ .ClientName }}</h1> 14 14 15 15 {{ if .Error }} 16 - <div class="alert alert-error mb-6"> 16 + <div id="login-error" role="alert" class="alert alert-error mb-6"> 17 17 {{ icon "circle-x" "size-5" }} 18 18 <span> 19 19 {{ if eq .Error "handle_required" }} ··· 42 42 autocorrect="off" 43 43 spellcheck="false" 44 44 required 45 - autofocus /> 45 + autofocus 46 + {{ if .Error }}aria-invalid="true" aria-describedby="login-error"{{ end }} /> 46 47 </div> 47 48 48 49 <button type="submit" class="btn btn-primary btn-lg w-full mt-6 order-3">
+38 -39
pkg/appview/templates/pages/privacy.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8 max-w-4xl"> 11 + <main id="main-content" class="container mx-auto px-4 py-8 max-w-4xl"> 12 12 <h1 class="text-3xl font-display font-bold tracking-tight mb-2">Privacy Policy - {{ .CompanyName }} ({{ .SiteURL }})</h1> 13 13 <p class="text-base-content/60 mb-8"><em>Last updated: January 2025</em></p> 14 14 ··· 17 17 <h2 class="text-xl font-semibold text-primary">Data We Collect and Store</h2> 18 18 19 19 <h3 class="text-lg font-medium mt-4">Data Stored on Your PDS (Controlled by You)</h3> 20 - <p>When you use {{ .CompanyName }}, records are written to your Personal Data Server (PDS) under the <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> namespace. This data is stored on infrastructure you or your PDS hosting provider controls. We do not control this data, and its retention and deletion is governed by your PDS provider's policies.</p> 20 + <p>When you use {{ .CompanyName }}, records are written to your Personal Data Server (PDS) under the <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> namespace. This data is stored on infrastructure you or your PDS hosting provider controls. We do not control this data, and its retention and deletion is governed by your PDS provider's policies.</p> 21 21 22 22 <h3 class="text-lg font-medium mt-6">Data Stored on Our Infrastructure</h3> 23 23 24 - <p><strong>Layer Records:</strong> Our hold services (e.g., <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">hold01.{{ .SiteURL }}</code>) maintain records in their embedded PDS that reference container image layers you publish. These records are public and link your AT Protocol identity (DID) to content-addressed SHA identifiers.</p> 24 + <p><strong>Layer Records:</strong> Our hold services (e.g., <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">hold01.{{ .SiteURL }}</code>) maintain records in their embedded PDS that reference container image layers you publish. These records are public and link your AT Protocol identity (DID) to content-addressed SHA identifiers.</p> 25 25 26 26 <p><strong>OCI Blobs:</strong> Container image layers are stored in our object storage (S3). These blobs are content-addressed and deduplicated—meaning identical layers uploaded by different users are stored only once.</p> 27 27 ··· 58 58 </ul> 59 59 60 60 <h3 class="text-lg font-medium mt-6">{{ .ClientShortName }}-Hosted Hold Services</h3> 61 - <p>Storage backends we operate (e.g., <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">hold01.{{ .SiteURL }}</code>). Each hold has an embedded PDS and stores:</p> 61 + <p>Storage backends we operate (e.g., <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">hold01.{{ .SiteURL }}</code>). Each hold has an embedded PDS and stores:</p> 62 62 <ul class="list-disc list-inside space-y-1 ml-4"> 63 63 <li>OCI blobs (container image layers) in object storage</li> 64 64 <li>Layer records in the hold's embedded PDS linking your DID to blob references</li> 65 65 <li>Crew membership records for access control</li> 66 66 </ul> 67 - <p class="mt-2">Hold services on <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">*.{{ .SiteURL }}</code> domains are operated by us and covered by this policy.</p> 67 + <p class="mt-2">Hold services on <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">*.{{ .SiteURL }}</code> domains are operated by us and covered by this policy.</p> 68 68 69 69 <h3 class="text-lg font-medium mt-6">User-Deployed Hold Services (BYOS)</h3> 70 70 <p>You may use "Bring Your Own Storage" by deploying your own hold service. Data on user-deployed holds is governed by that operator's privacy policy, not ours. We can request deletion on your behalf but cannot guarantee it for services we do not control.</p> ··· 133 133 </ul> 134 134 135 135 <p class="mt-4"><strong>Optional: Delete AT Protocol Records</strong></p> 136 - <p>When deleting your account, you may optionally authorize us to delete <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> records from your PDS. This requires an active OAuth session and is optional because:</p> 136 + <p>When deleting your account, you may optionally authorize us to delete <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> records from your PDS. This requires an active OAuth session and is optional because:</p> 137 137 <ul class="list-disc list-inside space-y-1 ml-4"> 138 138 <li>Your PDS is controlled by you or your hosting provider, not us</li> 139 139 <li>You may delete these records yourself at any time</li> ··· 186 186 </thead> 187 187 <tbody> 188 188 <tr> 189 - <td data-label="Category">Identifiers</td> 190 - <td data-label="Examples">DID, handle, IP address, device name</td> 191 - <td data-label="Collected">Yes</td> 189 + <td data-label="Category" aria-label="Category: Identifiers">Identifiers</td> 190 + <td data-label="Examples" aria-label="Examples: DID, handle, IP address, device name">DID, handle, IP address, device name</td> 191 + <td data-label="Collected" aria-label="Collected: Yes">Yes</td> 192 192 </tr> 193 193 <tr> 194 - <td data-label="Category">Internet activity</td> 195 - <td data-label="Examples">Access logs, usage data, actions performed</td> 196 - <td data-label="Collected">Yes</td> 194 + <td data-label="Category" aria-label="Category: Internet activity">Internet activity</td> 195 + <td data-label="Examples" aria-label="Examples: Access logs, usage data, actions performed">Access logs, usage data, actions performed</td> 196 + <td data-label="Collected" aria-label="Collected: Yes">Yes</td> 197 197 </tr> 198 198 <tr> 199 - <td data-label="Category">Geolocation</td> 200 - <td data-label="Examples">Approximate location via IP</td> 201 - <td data-label="Collected">Yes</td> 199 + <td data-label="Category" aria-label="Category: Geolocation">Geolocation</td> 200 + <td data-label="Examples" aria-label="Examples: Approximate location via IP">Approximate location via IP</td> 201 + <td data-label="Collected" aria-label="Collected: Yes">Yes</td> 202 202 </tr> 203 203 </tbody> 204 204 </table> ··· 219 219 </thead> 220 220 <tbody> 221 221 <tr> 222 - <td data-label="Data Type">OAuth tokens</td> 223 - <td data-label="Service">AppView</td> 224 - <td data-label="Retention">Until revoked or logout</td> 222 + <td data-label="Data Type" aria-label="Data Type: OAuth tokens">OAuth tokens</td> 223 + <td data-label="Service" aria-label="Service: AppView">AppView</td> 224 + <td data-label="Retention" aria-label="Retention: Until revoked or logout">Until revoked or logout</td> 225 225 </tr> 226 226 <tr> 227 - <td data-label="Data Type">Web UI session tokens</td> 228 - <td data-label="Service">AppView</td> 229 - <td data-label="Retention">Until logout or expiration</td> 227 + <td data-label="Data Type" aria-label="Data Type: Web UI session tokens">Web UI session tokens</td> 228 + <td data-label="Service" aria-label="Service: AppView">AppView</td> 229 + <td data-label="Retention" aria-label="Retention: Until logout or expiration">Until logout or expiration</td> 230 230 </tr> 231 231 <tr> 232 - <td data-label="Data Type">Device tokens (credential helper)</td> 233 - <td data-label="Service">AppView</td> 234 - <td data-label="Retention">Until revoked by user</td> 232 + <td data-label="Data Type" aria-label="Data Type: Device tokens (credential helper)">Device tokens (credential helper)</td> 233 + <td data-label="Service" aria-label="Service: AppView">AppView</td> 234 + <td data-label="Retention" aria-label="Retention: Until revoked by user">Until revoked by user</td> 235 235 </tr> 236 236 <tr> 237 - <td data-label="Data Type">Cached PDS data</td> 238 - <td data-label="Service">AppView</td> 239 - <td data-label="Retention">Refreshed periodically; deleted on account deletion</td> 237 + <td data-label="Data Type" aria-label="Data Type: Cached PDS data">Cached PDS data</td> 238 + <td data-label="Service" aria-label="Service: AppView">AppView</td> 239 + <td data-label="Retention" aria-label="Retention: Refreshed periodically; deleted on account deletion">Refreshed periodically; deleted on account deletion</td> 240 240 </tr> 241 241 <tr> 242 - <td data-label="Data Type">Server logs</td> 243 - <td data-label="Service">AppView</td> 244 - <td data-label="Retention">Currently ephemeral; this policy will be updated if log retention is implemented</td> 242 + <td data-label="Data Type" aria-label="Data Type: Server logs">Server logs</td> 243 + <td data-label="Service" aria-label="Service: AppView">AppView</td> 244 + <td data-label="Retention" aria-label="Retention: Currently ephemeral; this policy will be updated if log retention is implemented">Currently ephemeral; this policy will be updated if log retention is implemented</td> 245 245 </tr> 246 246 <tr> 247 - <td data-label="Data Type">Layer records</td> 248 - <td data-label="Service">Hold PDS</td> 249 - <td data-label="Retention">Until you request deletion</td> 247 + <td data-label="Data Type" aria-label="Data Type: Layer records">Layer records</td> 248 + <td data-label="Service" aria-label="Service: Hold PDS">Hold PDS</td> 249 + <td data-label="Retention" aria-label="Retention: Until you request deletion">Until you request deletion</td> 250 250 </tr> 251 251 <tr> 252 - <td data-label="Data Type">OCI blobs</td> 253 - <td data-label="Service">Hold Storage</td> 254 - <td data-label="Retention">Until no longer referenced (pruned within 30 days)</td> 252 + <td data-label="Data Type" aria-label="Data Type: OCI blobs">OCI blobs</td> 253 + <td data-label="Service" aria-label="Service: Hold Storage">Hold Storage</td> 254 + <td data-label="Retention" aria-label="Retention: Until no longer referenced (pruned within 30 days)">Until no longer referenced (pruned within 30 days)</td> 255 255 </tr> 256 256 </tbody> 257 257 </table> ··· 276 276 <p>{{ .CompanyName }} supports "Bring Your Own Storage" where users can deploy their own hold services to store container image blobs. This section explains how BYOS affects your privacy rights.</p> 277 277 278 278 <h3 class="text-lg font-medium mt-4">{{ .CompanyName }}-Hosted Holds</h3> 279 - <p>Hold services on <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">*.{{ .SiteURL }}</code> domains (e.g., <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">hold01.{{ .SiteURL }}</code>) are operated by us and fully covered by this privacy policy. We can fulfill all data access, export, and deletion requests for these services.</p> 279 + <p>Hold services on <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">*.{{ .SiteURL }}</code> domains (e.g., <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">hold01.{{ .SiteURL }}</code>) are operated by us and fully covered by this privacy policy. We can fulfill all data access, export, and deletion requests for these services.</p> 280 280 281 281 <h3 class="text-lg font-medium mt-6">User-Deployed Holds</h3> 282 282 <p>If you use a hold service not operated by us:</p> ··· 303 303 <p>Most data management can be done directly through your account settings at {{ .SiteURL }}:</p> 304 304 <ul class="list-disc list-inside space-y-1 ml-4"> 305 305 <li><strong>Export your data:</strong> Use the "Export Data" button in settings to download a copy of all personal data we store about you.</li> 306 - <li><strong>Delete your data:</strong> Use the "Delete Account" button in settings. This will remove your layer records, cached data, and authentication tokens. You may also choose to have us delete <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> records from your PDS (requires active OAuth session).</li> 306 + <li><strong>Delete your data:</strong> Use the "Delete Account" button in settings. This will remove your layer records, cached data, and authentication tokens. You may also choose to have us delete <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> records from your PDS (requires active OAuth session).</li> 307 307 <li><strong>Revoke device tokens:</strong> Manage and revoke credential helper devices in settings.</li> 308 308 <li><strong>Update your data:</strong> Corrections happen through normal use of the service.</li> 309 309 </ul> ··· 334 334 </div> 335 335 </main> 336 336 337 - <div id="modal"></div> 338 337 339 338 {{ template "footer" . }} 340 339 </body>
+33 -337
pkg/appview/templates/pages/repository.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8"> 11 + <main id="main-content" class="container mx-auto px-4 py-8"> 12 12 <div class="space-y-6"> 13 13 <!-- Static Header: Identity + Metadata (does not change with tag) --> 14 - <div class="card bg-base-100 shadow-sm p-6 space-y-4 w-full"> 14 + <div class="card bg-base-200 shadow-sm p-6 space-y-4 w-full"> 15 15 <div class="flex gap-4 items-start"> 16 16 {{ template "repo-avatar" (dict "IconURL" .Repository.IconURL "RepositoryName" .Repository.Name "IsOwner" .IsOwner) }} 17 17 <div class="flex-1 min-w-0"> ··· 37 37 </span> 38 38 {{ end }} 39 39 {{ if .Stats.LastPush }} 40 - <span class="text-sm text-base-content/50" title="Last pushed {{ (derefTime .Stats.LastPush).Format "2006-01-02T15:04:05Z07:00" }}"> 40 + <span class="text-sm text-base-content/70" title="Last pushed {{ (derefTime .Stats.LastPush).Format "2006-01-02T15:04:05Z07:00" }}"> 41 41 Updated {{ timeAgoShort (derefTime .Stats.LastPush) }} 42 42 </span> 43 43 {{ end }} ··· 101 101 </label> 102 102 <ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-56 p-2 shadow max-h-60 overflow-y-auto"> 103 103 {{ range .AllTags }} 104 - <li><a href="#" data-diff-to="{{ . }}" onclick="diffToTag(event, this)">{{ . }}</a></li> 104 + <li><a href="#" data-action="diff-to" data-diff-to="{{ . }}">{{ . }}</a></li> 105 105 {{ end }} 106 106 </ul> 107 107 </div> ··· 126 126 hx-get="/api/attestation-details?digest={{ .SelectedTag.Info.Digest | urlquery }}&did={{ .Owner.DID | urlquery }}&repo={{ .Repository.Name | urlquery }}" 127 127 hx-target="#attestation-modal-body" 128 128 hx-swap="innerHTML" 129 - onclick="document.getElementById('attestation-detail-modal').showModal()"> 129 + data-action="show-modal" data-modal-id="attestation-detail-modal"> 130 130 {{ icon "shield-check" "size-3" }} Attested 131 131 </button> 132 132 {{ end }} ··· 138 138 139 139 <!-- Inline Editor (hidden, owner only) — outside HTMX-swapped section so edits survive tag changes --> 140 140 {{ if .IsOwner }} 141 - <div id="overview-edit" class="card bg-base-100 shadow-sm p-6 hidden"> 141 + <div id="overview-edit" class="card bg-base-200 shadow-sm p-6 hidden"> 142 142 <!-- Write/Preview tabs --> 143 143 <div class="border-b border-base-300 mb-4"> 144 144 <nav class="flex gap-0" role="tablist"> 145 145 <button class="editor-tab px-4 py-2 text-sm font-medium border-b-2 border-primary text-primary" 146 - data-tab="write" onclick="switchEditorTab('write')"> 146 + data-tab="write" data-action="switch-editor-tab"> 147 147 Write 148 148 </button> 149 149 <button class="editor-tab px-4 py-2 text-sm font-medium border-b-2 border-transparent text-base-content/60" 150 - data-tab="preview" onclick="switchEditorTab('preview')"> 150 + data-tab="preview" data-action="switch-editor-tab"> 151 151 Preview 152 152 </button> 153 153 </nav> ··· 157 157 <div id="editor-write" class="editor-panel"> 158 158 <!-- Toolbar --> 159 159 <div class="flex flex-wrap gap-1 mb-2 p-1 bg-base-200 rounded-lg"> 160 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('heading')" title="Heading"> 160 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="heading" title="Heading"> 161 161 {{ icon "heading" "size-4" }} 162 162 </button> 163 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('bold')" title="Bold"> 163 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="bold" title="Bold"> 164 164 {{ icon "bold" "size-4" }} 165 165 </button> 166 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('italic')" title="Italic"> 166 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="italic" title="Italic"> 167 167 {{ icon "italic" "size-4" }} 168 168 </button> 169 169 <div class="divider divider-horizontal mx-0"></div> 170 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('link')" title="Link"> 170 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="link" title="Link"> 171 171 {{ icon "link" "size-4" }} 172 172 </button> 173 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('image')" title="Image"> 173 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="image" title="Image"> 174 174 {{ icon "image" "size-4" }} 175 175 </button> 176 176 <div class="divider divider-horizontal mx-0"></div> 177 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('ul')" title="Bulleted list"> 177 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="ul" title="Bulleted list"> 178 178 {{ icon "list" "size-4" }} 179 179 </button> 180 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('ol')" title="Numbered list"> 180 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="ol" title="Numbered list"> 181 181 {{ icon "list-ordered" "size-4" }} 182 182 </button> 183 - <button type="button" class="btn btn-ghost btn-sm md:btn-xs" onclick="insertMd('code')" title="Code"> 183 + <button type="button" class="btn btn-ghost btn-sm md:btn-xs" data-action="insert-md" data-md-type="code" title="Code"> 184 184 {{ icon "code" "size-4" }} 185 185 </button> 186 186 </div> 187 187 <textarea id="md-editor" 188 188 class="textarea textarea-bordered w-full font-mono text-sm leading-relaxed" 189 189 rows="20" 190 + maxlength="102400" 191 + data-owner-did="{{ .Owner.DID }}" 192 + data-repository="{{ .Repository.Name }}" 190 193 placeholder="Write your repository description in Markdown...">{{ .RawDescription }}</textarea> 191 194 </div> 192 195 ··· 199 202 200 203 <!-- Actions --> 201 204 <div class="flex justify-end gap-2 mt-4"> 202 - <button class="btn btn-sm btn-ghost" onclick="toggleOverviewEditor(false)">Cancel</button> 203 - <button class="btn btn-sm btn-primary" id="save-overview-btn" onclick="saveOverview()">Save</button> 205 + <button class="btn btn-sm btn-ghost" data-action="toggle-editor" data-show="false">Cancel</button> 206 + <button class="btn btn-sm btn-primary" id="save-overview-btn" data-action="save-overview">Save</button> 204 207 </div> 205 208 </div> 206 209 {{ end }} 207 210 </div> 208 211 </main> 209 - 210 - <!-- Modal container for HTMX --> 211 - <div id="modal"></div> 212 212 213 213 <!-- Manifest Delete Confirmation Modal --> 214 - <dialog id="manifest-delete-modal" class="modal"> 215 - <div class="modal-box"> 216 - <h3 class="text-lg font-bold">Confirm Deletion</h3> 214 + <dialog id="manifest-delete-modal" class="modal" aria-modal="true" aria-labelledby="manifest-delete-title"> 215 + <div class="modal-box bg-base-200"> 216 + <h3 id="manifest-delete-title" class="text-lg font-bold">Confirm Deletion</h3> 217 217 <p id="manifest-delete-message" class="py-2">This manifest has associated tags that will also be deleted:</p> 218 218 <ul id="manifest-delete-tags" class="list-disc list-inside text-sm space-y-1"></ul> 219 219 <p class="font-bold py-2 text-error">This action cannot be undone.</p> 220 220 <div class="modal-action"> 221 - <button class="btn" onclick="closeManifestDeleteModal()">Cancel</button> 221 + <button class="btn" data-action="close-manifest-delete-modal">Cancel</button> 222 222 <button class="btn btn-error" id="confirm-manifest-delete-btn">Delete All</button> 223 223 </div> 224 224 </div> 225 225 <form method="dialog" class="modal-backdrop"> 226 - <button onclick="closeManifestDeleteModal()">close</button> 226 + <button data-action="close-manifest-delete-modal">close</button> 227 227 </form> 228 228 </dialog> 229 229 230 230 <!-- Untagged Manifests Delete Confirmation Modal --> 231 - <dialog id="untagged-delete-modal" class="modal"> 232 - <div class="modal-box"> 233 - <h3 class="text-lg font-bold">Delete Untagged Manifests</h3> 231 + <dialog id="untagged-delete-modal" class="modal" aria-modal="true" aria-labelledby="untagged-delete-title"> 232 + <div class="modal-box bg-base-200"> 233 + <h3 id="untagged-delete-title" class="text-lg font-bold">Delete Untagged Manifests</h3> 234 234 <p class="py-2">This will delete <strong>all</strong> untagged manifests in this repository, including those not currently visible.</p> 235 235 <p class="font-bold py-2 text-error">This action cannot be undone.</p> 236 236 <div class="modal-action"> 237 237 <form method="dialog"><button class="btn">Cancel</button></form> 238 238 <button class="btn btn-error" id="confirm-untagged-delete-btn" 239 - onclick="deleteUntaggedManifests('{{ .Repository.Name }}')"> 239 + data-action="delete-untagged" data-repo="{{ .Repository.Name }}"> 240 240 Delete All Untagged 241 241 </button> 242 242 </div> ··· 245 245 </dialog> 246 246 247 247 <!-- Attestation Details Modal --> 248 - <dialog id="attestation-detail-modal" class="modal"> 249 - <div class="modal-box max-w-2xl"> 250 - <h3 class="text-lg font-bold">Attestation Details</h3> 248 + <dialog id="attestation-detail-modal" class="modal" aria-modal="true" aria-labelledby="attestation-detail-title"> 249 + <div class="modal-box bg-base-200 max-w-2xl"> 250 + <h3 id="attestation-detail-title" class="text-lg font-bold">Attestation Details</h3> 251 251 <div id="attestation-modal-body" class="py-4"> 252 252 <span class="loading loading-spinner loading-md"></span> 253 253 </div> ··· 257 257 </div> 258 258 <form method="dialog" class="modal-backdrop"><button>close</button></form> 259 259 </dialog> 260 - 261 - {{ if .IsOwner }} 262 - <script> 263 - (function() { 264 - var textarea = document.getElementById('md-editor'); 265 - if (!textarea) return; 266 - 267 - var ownerDID = {{ .Owner.DID }}; 268 - var repoName = {{ .Repository.Name }}; 269 - 270 - window.toggleOverviewEditor = function(show) { 271 - document.getElementById('overview-view').classList.toggle('hidden', show); 272 - document.getElementById('overview-edit').classList.toggle('hidden', !show); 273 - if (show) textarea.focus(); 274 - }; 275 - 276 - window.switchEditorTab = function(tab) { 277 - document.querySelectorAll('.editor-panel').forEach(function(p) { p.classList.add('hidden'); }); 278 - document.getElementById(tab === 'write' ? 'editor-write' : 'editor-preview').classList.remove('hidden'); 279 - 280 - document.querySelectorAll('.editor-tab').forEach(function(t) { 281 - var active = t.dataset.tab === tab; 282 - t.classList.toggle('border-primary', active); 283 - t.classList.toggle('text-primary', active); 284 - t.classList.toggle('border-transparent', !active); 285 - t.classList.toggle('text-base-content/60', !active); 286 - }); 287 - 288 - if (tab === 'preview') { 289 - var content = textarea.value; 290 - var previewEl = document.getElementById('preview-content'); 291 - if (!content.trim()) { 292 - previewEl.innerHTML = '<p class="text-base-content/60">Nothing to preview</p>'; 293 - return; 294 - } 295 - var form = new FormData(); 296 - form.append('markdown', content); 297 - fetch('/api/repo-page/preview', { method: 'POST', body: form }) 298 - .then(function(r) { return r.text(); }) 299 - .then(function(html) { previewEl.innerHTML = html; }); 300 - } 301 - }; 302 - 303 - window.insertMd = function(type) { 304 - var start = textarea.selectionStart; 305 - var end = textarea.selectionEnd; 306 - var selected = textarea.value.substring(start, end); 307 - var before = textarea.value.substring(0, start); 308 - var after = textarea.value.substring(end); 309 - var insert, cursorStart, cursorEnd; 310 - 311 - switch (type) { 312 - case 'heading': 313 - insert = '## ' + (selected || 'Heading'); 314 - cursorStart = start + 3; 315 - cursorEnd = start + insert.length; 316 - break; 317 - case 'bold': 318 - insert = '**' + (selected || 'bold text') + '**'; 319 - cursorStart = start + 2; 320 - cursorEnd = start + insert.length - 2; 321 - break; 322 - case 'italic': 323 - insert = '_' + (selected || 'italic text') + '_'; 324 - cursorStart = start + 1; 325 - cursorEnd = start + insert.length - 1; 326 - break; 327 - case 'link': 328 - insert = '[' + (selected || 'link text') + '](url)'; 329 - cursorStart = start + insert.length - 4; 330 - cursorEnd = start + insert.length - 1; 331 - break; 332 - case 'image': 333 - insert = '![' + (selected || 'alt text') + '](url)'; 334 - cursorStart = start + insert.length - 4; 335 - cursorEnd = start + insert.length - 1; 336 - break; 337 - case 'ul': 338 - insert = '- ' + (selected || 'list item'); 339 - cursorStart = start + 2; 340 - cursorEnd = start + insert.length; 341 - break; 342 - case 'ol': 343 - insert = '1. ' + (selected || 'list item'); 344 - cursorStart = start + 3; 345 - cursorEnd = start + insert.length; 346 - break; 347 - case 'code': 348 - if (selected && selected.indexOf('\n') !== -1) { 349 - insert = '```\n' + selected + '\n```'; 350 - cursorStart = start + 4; 351 - cursorEnd = start + 4 + selected.length; 352 - } else { 353 - insert = '`' + (selected || 'code') + '`'; 354 - cursorStart = start + 1; 355 - cursorEnd = start + insert.length - 1; 356 - } 357 - break; 358 - default: 359 - return; 360 - } 361 - 362 - textarea.value = before + insert + after; 363 - textarea.focus(); 364 - textarea.selectionStart = cursorStart; 365 - textarea.selectionEnd = cursorEnd; 366 - }; 367 - 368 - window.saveOverview = function() { 369 - var btn = document.getElementById('save-overview-btn'); 370 - btn.classList.add('btn-disabled'); 371 - btn.innerHTML = '<span class="loading loading-spinner loading-xs"></span> Saving...'; 372 - 373 - var form = new FormData(); 374 - form.append('did', ownerDID); 375 - form.append('repository', repoName); 376 - form.append('description', textarea.value); 377 - 378 - fetch('/api/repo-page', { 379 - method: 'POST', 380 - body: form, 381 - headers: { 'HX-Request': 'true' } 382 - }) 383 - .then(function(r) { 384 - if (!r.ok) return r.text().then(function(t) { throw new Error(t); }); 385 - return r.text(); 386 - }) 387 - .then(function(html) { 388 - document.getElementById('overview-rendered').innerHTML = html; 389 - toggleOverviewEditor(false); 390 - if (typeof showToast === 'function') showToast('Overview saved', 'success'); 391 - }) 392 - .catch(function(err) { 393 - if (typeof showToast === 'function') showToast(err.message || 'Failed to save', 'error'); 394 - }) 395 - .finally(function() { 396 - btn.classList.remove('btn-disabled'); 397 - btn.innerHTML = 'Save'; 398 - }); 399 - }; 400 - 401 - // Ctrl+S / Cmd+S to save 402 - textarea.addEventListener('keydown', function(e) { 403 - if ((e.ctrlKey || e.metaKey) && e.key === 's') { 404 - e.preventDefault(); 405 - saveOverview(); 406 - } 407 - }); 408 - })(); 409 - </script> 410 - {{ end }} 411 - 412 - <script> 413 - // Global helpers (tags sort/filter) 414 - window.sortTags = function(method) { 415 - var container = document.getElementById('tags-list'); 416 - if (!container) return; 417 - var entries = Array.from(container.querySelectorAll('.artifact-entry')); 418 - entries.sort(function(a, b) { 419 - switch (method) { 420 - case 'oldest': return parseInt(a.dataset.created) - parseInt(b.dataset.created); 421 - case 'az': return a.dataset.tag.localeCompare(b.dataset.tag); 422 - case 'za': return b.dataset.tag.localeCompare(a.dataset.tag); 423 - default: return parseInt(b.dataset.created) - parseInt(a.dataset.created); 424 - } 425 - }); 426 - entries.forEach(function(el) { container.appendChild(el); }); 427 - }; 428 - 429 - window.filterTags = function(query) { 430 - var q = query.toLowerCase(); 431 - document.querySelectorAll('#tags-list .artifact-entry').forEach(function(el) { 432 - el.style.display = (!q || el.dataset.tag.toLowerCase().includes(q)) ? '' : 'none'; 433 - }); 434 - }; 435 - 436 - // Tab controller — reads config from #tag-content data attributes. 437 - // Re-runs on initial load and after every HTMX swap of #tag-content. 438 - (function() { 439 - var validTabs = ['overview', 'layers', 'vulns', 'sbom', 'artifacts']; 440 - var loaded = {}; 441 - 442 - function lazyLoad(id, url) { 443 - if (loaded[id]) return; 444 - loaded[id] = true; 445 - var target = document.getElementById(id); 446 - if (!target) return; 447 - fetch(url) 448 - .then(function(r) { return r.text(); }) 449 - .then(function(html) { 450 - target.innerHTML = html; 451 - // innerHTML doesn't execute <script> tags — re-create them 452 - target.querySelectorAll('script').forEach(function(old) { 453 - var s = document.createElement('script'); 454 - s.textContent = old.textContent; 455 - old.parentNode.replaceChild(s, old); 456 - }); 457 - if (typeof htmx !== 'undefined') htmx.process(target); 458 - }); 459 - } 460 - 461 - function contentUrl(section) { 462 - var el = document.getElementById('tag-content'); 463 - if (!el) return null; 464 - var owner = el.dataset.owner; 465 - var repo = el.dataset.repo; 466 - var digest = el.dataset.digest; 467 - if (!digest) return null; 468 - return '/api/digest-content/' + owner + '/' + repo + '?digest=' + encodeURIComponent(digest) + '&section=' + section; 469 - } 470 - 471 - function tagsUrl() { 472 - var el = document.getElementById('tag-content'); 473 - if (!el) return null; 474 - return '/api/repo-tags/' + el.dataset.owner + '/' + el.dataset.repo; 475 - } 476 - 477 - window.diffToTag = function(e, link) { 478 - e.preventDefault(); 479 - var to = link.dataset.diffTo; 480 - var content = document.getElementById('tag-content'); 481 - var selector = document.getElementById('tag-selector'); 482 - if (!content || !selector || !to) return; 483 - var fromDigest = content.dataset.digest; 484 - var currentTag = selector.value; 485 - if (!fromDigest || to === currentTag) return; 486 - window.location.href = '/diff/' + content.dataset.owner + '/' + content.dataset.repo + 487 - '?from=' + encodeURIComponent(fromDigest) + '&to=' + encodeURIComponent(to); 488 - }; 489 - 490 - window.switchRepoTab = function(tabId) { 491 - window._activeRepoTab = tabId; 492 - var section = document.getElementById('tag-content'); 493 - if (!section) return; 494 - 495 - section.querySelectorAll('.repo-panel').forEach(function(p) { 496 - p.classList.add('hidden'); 497 - }); 498 - var panel = document.getElementById('tab-' + tabId); 499 - if (panel) panel.classList.remove('hidden'); 500 - 501 - section.querySelectorAll('.repo-tab').forEach(function(tab) { 502 - if (tab.dataset.tab === tabId) { 503 - tab.classList.add('border-primary', 'text-primary'); 504 - tab.classList.remove('border-transparent', 'text-base-content/60'); 505 - tab.setAttribute('aria-selected', 'true'); 506 - } else { 507 - tab.classList.remove('border-primary', 'text-primary'); 508 - tab.classList.add('border-transparent', 'text-base-content/60'); 509 - tab.setAttribute('aria-selected', 'false'); 510 - } 511 - }); 512 - 513 - var url = new URL(window.location); 514 - url.hash = tabId; 515 - history.replaceState(null, '', url.toString()); 516 - 517 - if (tabId === 'artifacts') { var u = tagsUrl(); if (u) lazyLoad('artifacts-content', u); } 518 - if (tabId === 'layers') { var u = contentUrl('layers'); if (u) lazyLoad('layers-content', u); } 519 - if (tabId === 'vulns') { var u = contentUrl('vulns'); if (u) lazyLoad('vulns-content', u); } 520 - if (tabId === 'sbom') { var u = contentUrl('sbom'); if (u) lazyLoad('sbom-content', u); } 521 - }; 522 - 523 - function initTabs() { 524 - // Reset lazy-load tracking (new tag = new content) 525 - loaded = {}; 526 - 527 - // Prefetch on hover 528 - var tagsBtn = document.getElementById('artifacts-tab-btn'); 529 - if (tagsBtn) tagsBtn.addEventListener('mouseenter', function() { var u = tagsUrl(); if (u) lazyLoad('artifacts-content', u); }, { once: true }); 530 - var layersBtn = document.getElementById('layers-tab-btn'); 531 - if (layersBtn) layersBtn.addEventListener('mouseenter', function() { var u = contentUrl('layers'); if (u) lazyLoad('layers-content', u); }, { once: true }); 532 - var vulnsBtn = document.getElementById('vulns-tab-btn'); 533 - if (vulnsBtn) vulnsBtn.addEventListener('mouseenter', function() { var u = contentUrl('vulns'); if (u) lazyLoad('vulns-content', u); }, { once: true }); 534 - var sbomBtn = document.getElementById('sbom-tab-btn'); 535 - if (sbomBtn) sbomBtn.addEventListener('mouseenter', function() { var u = contentUrl('sbom'); if (u) lazyLoad('sbom-content', u); }, { once: true }); 536 - 537 - // Pick tab: persisted > hash > default 538 - var initTab = window._activeRepoTab || window.location.hash.replace('#', '') || 'overview'; 539 - if (validTabs.indexOf(initTab) === -1) initTab = 'overview'; 540 - switchRepoTab(initTab); 541 - } 542 - 543 - // Run on initial page load 544 - initTabs(); 545 - 546 - // Keyboard shortcuts: first letter of each tab name 547 - document.addEventListener('keydown', function(e) { 548 - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || 549 - e.target.tagName === 'SELECT' || e.target.isContentEditable) return; 550 - if (e.ctrlKey || e.metaKey || e.altKey) return; 551 - var map = { o: 'overview', l: 'layers', v: 'vulns', s: 'sbom', a: 'artifacts' }; 552 - var tab = map[e.key.toLowerCase()]; 553 - if (tab && validTabs.indexOf(tab) !== -1) switchRepoTab(tab); 554 - }); 555 - 556 - // Re-run after HTMX swaps tag section content (tag dropdown change) 557 - document.body.addEventListener('htmx:afterSettle', function(evt) { 558 - if (evt.detail.target && evt.detail.target.id === 'tag-content') { 559 - initTabs(); 560 - } 561 - }); 562 - })(); 563 - </script> 564 260 565 261 {{ template "footer" . }} 566 262 </body>
+1 -4
pkg/appview/templates/pages/search.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8"> 11 + <main id="main-content" class="container mx-auto px-4 py-8"> 12 12 {{ if .SearchQuery }} 13 13 <h1 class="text-2xl font-bold mb-6">Search Results for "{{ .SearchQuery }}"</h1> 14 14 {{ else }} ··· 26 26 {{ end }} 27 27 </div> 28 28 </main> 29 - 30 - <!-- Modal container for HTMX --> 31 - <div id="modal"></div> 32 29 33 30 {{ template "footer" . }} 34 31 </body>
+19 -299
pkg/appview/templates/pages/settings.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8"> 11 + <main id="main-content" class="container mx-auto px-4 py-8"> 12 12 <h1 class="text-3xl font-display font-bold tracking-tight mb-6">Settings</h1> 13 13 14 14 <!-- Mobile identity info (below lg) --> 15 - <div class="lg:hidden mb-4 space-y-1 text-xs text-base-content/50"> 15 + <div class="lg:hidden mb-4 space-y-1 text-xs text-base-content/70"> 16 16 <div class="break-all"><code>{{ .Profile.DID }}</code></div> 17 17 <div><a href="{{ .Profile.PDSEndpoint }}/account" target="_blank" class="link link-primary inline-flex items-center gap-1">{{ .Profile.PDSEndpoint }} {{ icon "external-link" "size-3" }}</a></div> 18 18 </div> ··· 50 50 <li data-tab="webhooks" role="none"><a href="#webhooks" role="tab" id="tab-desktop-webhooks" aria-controls="tab-webhooks" aria-selected="false" tabindex="-1">{{ icon "webhook" "size-4" }} Webhooks</a></li> 51 51 <li data-tab="advanced" role="none"><a href="#advanced" role="tab" id="tab-desktop-advanced" aria-controls="tab-advanced" aria-selected="false" tabindex="-1">{{ icon "shield-check" "size-4" }} Advanced</a></li> 52 52 </ul> 53 - <div class="mt-4 px-2 space-y-1 text-xs text-base-content/50"> 53 + <div class="mt-4 px-2 space-y-1 text-xs text-base-content/70"> 54 54 <div class="break-all"><code>{{ .Profile.DID }}</code></div> 55 55 <div><a href="{{ .Profile.PDSEndpoint }}/account" target="_blank" class="link link-primary inline-flex items-center gap-1">{{ .Profile.PDSEndpoint }} {{ icon "external-link" "size-3" }}</a></div> 56 56 </div> ··· 61 61 62 62 <!-- USER TAB --> 63 63 <div id="tab-user" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-user" tabindex="0"> 64 - <section class="card bg-base-100 shadow-sm p-6 space-y-6"> 64 + <section class="card bg-base-200 shadow-sm p-6 space-y-6"> 65 65 <div> 66 66 <h2 class="text-xl font-semibold">Preferences</h2> 67 67 <p class="text-base-content/70 mt-1">Customize your experience across the site.</p> ··· 71 71 <div class="flex items-center gap-4"> 72 72 <div> 73 73 <label for="oci-client-select" class="text-sm font-medium">Preferred client</label> 74 - <p id="oci-client-hint" class="text-xs text-base-content/60">Sets the pull command shown on repository pages. Choose <em>Image reference only</em> to copy just the image ref (useful for Kubernetes manifests or docker-compose).</p> 74 + <p id="oci-client-hint" class="text-xs text-base-content/70">Sets the pull command shown on repository pages. Choose <em>Image reference only</em> to copy without a command prefix.</p> 75 75 </div> 76 76 {{ $oci := .Profile.OciClient }} 77 77 <select id="oci-client-select" aria-describedby="oci-client-hint" class="select select-sm select-bordered min-w-40" 78 - onchange="htmx.ajax('POST', '/api/profile/oci-client', {values: {oci_client: this.value}, swap: 'none'})"> 78 + name="oci_client" 79 + hx-post="/api/profile/oci-client" 80 + hx-trigger="change" 81 + hx-swap="none"> 79 82 <option value="docker"{{ if or (eq $oci "") (eq $oci "docker") }} selected{{ end }}>Docker</option> 80 83 <option value="podman"{{ if eq $oci "podman" }} selected{{ end }}>Podman</option> 81 84 <option value="buildah"{{ if eq $oci "buildah" }} selected{{ end }}>Buildah</option> ··· 104 107 {{ else }} 105 108 <div> 106 109 <span class="font-medium text-base-content/50">AI Image Advisor</span> 107 - <p class="text-xs text-base-content/50">Analyze your container images for optimization suggestions using AI.</p> 110 + <p class="text-xs text-base-content/70">Analyze your container images for optimization suggestions using AI.</p> 108 111 <p class="text-xs text-primary mt-1"> 109 - <a href="/settings#billing" onclick="switchSettingsTab('billing')">Upgrade your plan</a> to enable this feature. 112 + <a href="/settings#billing">Upgrade your plan</a> to enable this feature. 110 113 </p> 111 114 </div> 112 115 {{ end }} ··· 125 128 {{ if .ActiveHold }} 126 129 {{ template "hold_card" .ActiveHold }} 127 130 {{ else }} 128 - <div class="card bg-base-100 shadow-sm p-6 text-center text-base-content/60"> 131 + <div class="card bg-base-200 shadow-sm p-6 text-center text-base-content/60"> 129 132 No active hold selected. Choose one above. 130 133 </div> 131 134 {{ end }} ··· 137 140 </div> 138 141 </div> 139 142 {{ else }} 140 - <div class="card bg-base-100 shadow-sm p-6 text-center text-base-content/60"> 143 + <div class="card bg-base-200 shadow-sm p-6 text-center text-base-content/60"> 141 144 No holds configured. Push an image to get started. 142 145 </div> 143 146 {{ end }} ··· 169 172 170 173 <!-- DEVICES TAB --> 171 174 <div id="tab-devices" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-devices" tabindex="0"> 172 - <section class="card bg-base-100 shadow-sm p-6 space-y-6"> 175 + <section class="card bg-base-200 shadow-sm p-6 space-y-6"> 173 176 <div> 174 177 <h2 class="text-xl font-semibold">Authorized Devices</h2> 175 178 <p class="text-base-content/70 mt-1">Devices authorized via <code class="cmd">docker-credential-atcr</code> credential helper.</p> ··· 229 232 230 233 <!-- WEBHOOKS TAB --> 231 234 <div id="tab-webhooks" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-webhooks" tabindex="0"> 232 - <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 235 + <section class="card bg-base-200 shadow-sm p-6 space-y-4"> 233 236 <div> 234 237 <h2 class="text-xl font-semibold">Webhooks</h2> 235 238 <p class="text-base-content/70 mt-1">Get notified when images are pushed or vulnerability scans complete.</p> ··· 243 246 <!-- ADVANCED TAB --> 244 247 <div id="tab-advanced" class="settings-panel hidden space-y-6" role="tabpanel" aria-labelledby="tab-desktop-advanced" tabindex="0"> 245 248 <!-- Data Privacy Section --> 246 - <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 249 + <section class="card bg-base-200 shadow-sm p-6 space-y-4"> 247 250 <h2 class="text-xl font-semibold">Data Privacy</h2> 248 251 <p class="text-base-content/70">Download a copy of all data we store about you.</p> 249 252 ··· 290 293 </p> 291 294 </div> 292 295 293 - <button type="button" id="delete-account-btn" class="btn btn-error btn-lg gap-2"> 296 + <button type="button" id="delete-account-btn" class="btn btn-error btn-lg gap-2" 297 + data-client-short-name="{{ .ClientShortName }}" 298 + data-profile-handle="{{ .Profile.Handle }}"> 294 299 {{ icon "trash-2" "size-5" }} 295 300 Delete My {{ .ClientShortName }} Data 296 301 </button> ··· 301 306 </div> 302 307 </div> 303 308 </main> 304 - 305 - <script> 306 - // OCI client dropdown 307 - // Tab switching 308 - (function() { 309 - var validTabs = ['user', 'billing', 'storage', 'devices', 'webhooks', 'advanced']; 310 - 311 - function switchSettingsTab(tabId) { 312 - // Hide all panels 313 - document.querySelectorAll('.settings-panel').forEach(function(p) { 314 - p.classList.add('hidden'); 315 - }); 316 - // Show selected panel 317 - var panel = document.getElementById('tab-' + tabId); 318 - if (panel) panel.classList.remove('hidden'); 319 - 320 - // Sidebar: toggle menu-active + aria-selected + tabindex 321 - document.querySelectorAll('.menu li[data-tab]').forEach(function(li) { 322 - var active = li.dataset.tab === tabId; 323 - li.classList.toggle('menu-active', active); 324 - var a = li.querySelector('a[role="tab"]'); 325 - if (a) { 326 - a.setAttribute('aria-selected', active ? 'true' : 'false'); 327 - a.setAttribute('tabindex', active ? '0' : '-1'); 328 - } 329 - }); 330 - 331 - // Mobile: toggle btn styles + aria-selected + tabindex 332 - document.querySelectorAll('.settings-tab-mobile').forEach(function(btn) { 333 - var active = btn.dataset.tab === tabId; 334 - btn.classList.toggle('btn-ghost', !active); 335 - btn.classList.toggle('btn-secondary', active); 336 - btn.setAttribute('aria-selected', active ? 'true' : 'false'); 337 - btn.setAttribute('tabindex', active ? '0' : '-1'); 338 - }); 339 - 340 - // Update URL hash without adding history entry 341 - history.replaceState(null, '', '#' + tabId); 342 - // Dispatch custom event for HTMX lazy loading 343 - document.body.dispatchEvent(new CustomEvent('tab:' + tabId)); 344 - } 345 - 346 - // Helper for HTMX conditional polling 347 - window.isTabActive = function(tabId) { 348 - var panel = document.getElementById('tab-' + tabId); 349 - return panel && !panel.classList.contains('hidden'); 350 - }; 351 - 352 - // Roving tab navigation (arrow keys, Home, End) per WAI-ARIA tabs pattern 353 - function handleTabKeydown(tabs, orientation) { 354 - return function(e) { 355 - var prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft'; 356 - var nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight'; 357 - var idx = tabs.indexOf(e.currentTarget); 358 - if (idx === -1) return; 359 - var target = null; 360 - if (e.key === prevKey) target = tabs[(idx - 1 + tabs.length) % tabs.length]; 361 - else if (e.key === nextKey) target = tabs[(idx + 1) % tabs.length]; 362 - else if (e.key === 'Home') target = tabs[0]; 363 - else if (e.key === 'End') target = tabs[tabs.length - 1]; 364 - if (!target) return; 365 - e.preventDefault(); 366 - switchSettingsTab(target.dataset.tab || target.parentElement.dataset.tab); 367 - target.focus(); 368 - }; 369 - } 370 - 371 - document.addEventListener('DOMContentLoaded', function() { 372 - // Read initial tab from hash 373 - var hash = window.location.hash.replace('#', '') || 'user'; 374 - if (validTabs.indexOf(hash) === -1) hash = 'user'; 375 - 376 - // Mobile tab click + keyboard handlers 377 - var mobileTabs = Array.prototype.slice.call(document.querySelectorAll('.settings-tab-mobile')); 378 - var mobileKeydown = handleTabKeydown(mobileTabs, 'horizontal'); 379 - mobileTabs.forEach(function(btn) { 380 - btn.addEventListener('click', function(e) { 381 - e.preventDefault(); 382 - switchSettingsTab(this.dataset.tab); 383 - }); 384 - btn.addEventListener('keydown', mobileKeydown); 385 - }); 386 - 387 - // Sidebar tab click + keyboard handlers 388 - var sidebarTabs = Array.prototype.slice.call(document.querySelectorAll('.menu li[data-tab] a[role="tab"]')); 389 - var sidebarKeydown = handleTabKeydown(sidebarTabs, 'vertical'); 390 - sidebarTabs.forEach(function(link) { 391 - link.addEventListener('click', function(e) { 392 - e.preventDefault(); 393 - switchSettingsTab(this.parentElement.dataset.tab); 394 - }); 395 - link.addEventListener('keydown', sidebarKeydown); 396 - }); 397 - 398 - // Activate initial tab 399 - switchSettingsTab(hash); 400 - }); 401 - 402 - // Handle browser back/forward 403 - window.addEventListener('hashchange', function() { 404 - var hash = window.location.hash.replace('#', '') || 'user'; 405 - if (validTabs.indexOf(hash) !== -1) { 406 - switchSettingsTab(hash); 407 - } 408 - }); 409 - })(); 410 - 411 - // Account Deletion JavaScript 412 - (function() { 413 - const deleteBtn = document.getElementById('delete-account-btn'); 414 - if (!deleteBtn) return; 415 - 416 - deleteBtn.addEventListener('click', function() { 417 - showDeleteConfirmationModal(); 418 - }); 419 - 420 - function showDeleteConfirmationModal() { 421 - // Create modal using DaisyUI structure 422 - const modal = document.createElement('div'); 423 - modal.className = 'modal modal-open'; 424 - modal.innerHTML = ` 425 - <div class="modal-box max-w-lg"> 426 - <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 427 - <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#alert-triangle"></use></svg> 428 - Delete {{ .ClientShortName }} Data 429 - </h2> 430 - 431 - <div class="py-4 space-y-4"> 432 - <div class="alert alert-success"> 433 - <svg class="icon size-5" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 434 - <span>Your ATProto account will <strong>NOT</strong> be affected.</span> 435 - </div> 436 - 437 - <p class="text-base-content/80"> 438 - This action <strong>cannot be undone</strong>. This will permanently delete: 439 - </p> 440 - 441 - <ul class="list-disc list-inside text-sm space-y-1 text-base-content/70"> 442 - <li>Your {{ .ClientShortName }} account and all settings</li> 443 - <li>All authorized devices</li> 444 - <li>Your data from all holds you're a member of</li> 445 - ${document.getElementById('delete-pds-records').checked ? 446 - '<li>All io.atcr.* records from your ATProto PDS</li>' : ''} 447 - </ul> 448 - 449 - <div class="space-y-2"> 450 - <label for="confirm-delete-input" class="text-sm">Type <strong class="font-mono">DELETE {{ .Profile.Handle }}</strong> to confirm:</label> 451 - <input type="text" id="confirm-delete-input" class="input input-bordered w-full font-mono" placeholder="DELETE {{ .Profile.Handle }}" autocomplete="off"> 452 - </div> 453 - </div> 454 - 455 - <div class="modal-action"> 456 - <button type="button" class="btn" id="cancel-delete">Cancel</button> 457 - <button type="button" class="btn btn-error gap-2" id="confirm-delete" disabled> 458 - <svg class="icon size-4" aria-hidden="true"><use href="/icons.svg#trash-2"></use></svg> 459 - Delete My {{ .ClientShortName }} Data 460 - </button> 461 - </div> 462 - </div> 463 - <div class="modal-backdrop bg-black/50" id="modal-backdrop"></div> 464 - `; 465 - document.body.appendChild(modal); 466 - 467 - // Focus the input 468 - const confirmInput = document.getElementById('confirm-delete-input'); 469 - const confirmBtn = document.getElementById('confirm-delete'); 470 - const cancelBtn = document.getElementById('cancel-delete'); 471 - 472 - setTimeout(() => confirmInput.focus(), 100); 473 - 474 - // Expected confirmation string 475 - const expectedConfirmation = 'DELETE {{ .Profile.Handle }}'; 476 - 477 - // Enable button only when full confirmation is typed 478 - confirmInput.addEventListener('input', function() { 479 - confirmBtn.disabled = this.value !== expectedConfirmation; 480 - }); 481 - 482 - // Handle enter key 483 - confirmInput.addEventListener('keydown', function(e) { 484 - if (e.key === 'Enter' && this.value === expectedConfirmation) { 485 - performAccountDeletion(); 486 - } 487 - }); 488 - 489 - // Cancel button 490 - cancelBtn.addEventListener('click', function() { 491 - modal.remove(); 492 - }); 493 - 494 - // Click outside to close (on backdrop) 495 - document.getElementById('modal-backdrop').addEventListener('click', function() { 496 - modal.remove(); 497 - }); 498 - 499 - // Escape key to close 500 - document.addEventListener('keydown', function escHandler(e) { 501 - if (e.key === 'Escape') { 502 - modal.remove(); 503 - document.removeEventListener('keydown', escHandler); 504 - } 505 - }); 506 - 507 - // Confirm delete 508 - confirmBtn.addEventListener('click', performAccountDeletion); 509 - 510 - async function performAccountDeletion() { 511 - const deletePDS = document.getElementById('delete-pds-records').checked; 512 - 513 - // Show loading state 514 - confirmBtn.disabled = true; 515 - confirmBtn.innerHTML = '<svg class="icon size-4 animate-spin" aria-hidden="true"><use href="/icons.svg#loader-2"></use></svg> Deleting...'; 516 - cancelBtn.disabled = true; 517 - 518 - try { 519 - const response = await fetch('/api/account', { 520 - method: 'DELETE', 521 - headers: { 'Content-Type': 'application/json' }, 522 - body: JSON.stringify({ 523 - delete_pds_records: deletePDS, 524 - confirmation: expectedConfirmation 525 - }) 526 - }); 527 - 528 - const result = await response.json(); 529 - 530 - if (response.ok && result.success) { 531 - // Show success and redirect 532 - modal.querySelector('.modal-box').innerHTML = ` 533 - <h2 class="text-xl font-bold flex items-center gap-2 text-success"> 534 - <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#check-circle"></use></svg> 535 - Account Deleted 536 - </h2> 537 - <div class="py-4 space-y-2"> 538 - <p>Your account has been successfully deleted.</p> 539 - <p class="text-base-content/70">Redirecting to home page...</p> 540 - </div> 541 - `; 542 - setTimeout(() => { 543 - window.location.href = '/?deleted=true'; 544 - }, 2000); 545 - } else { 546 - // Show error 547 - const errors = result.errors || ['An unknown error occurred']; 548 - modal.querySelector('.modal-box').innerHTML = ` 549 - <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 550 - <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg> 551 - Deletion Failed 552 - </h2> 553 - <div class="py-4 space-y-4"> 554 - <p>There were errors during account deletion:</p> 555 - <ul class="list-disc list-inside text-sm space-y-1 text-error"> 556 - ${errors.map(e => '<li>' + escapeHtml(e) + '</li>').join('')} 557 - </ul> 558 - </div> 559 - <div class="modal-action"> 560 - <button type="button" class="btn" onclick="this.closest('.modal').remove()">Close</button> 561 - </div> 562 - `; 563 - } 564 - } catch (err) { 565 - console.error('Delete account error:', err); 566 - modal.querySelector('.modal-box').innerHTML = ` 567 - <h2 class="text-xl font-bold flex items-center gap-2 text-error"> 568 - <svg class="icon size-6" aria-hidden="true"><use href="/icons.svg#x-circle"></use></svg> 569 - Error 570 - </h2> 571 - <div class="py-4"> 572 - <p>Failed to delete account: ${escapeHtml(err.message)}</p> 573 - </div> 574 - <div class="modal-action"> 575 - <button type="button" class="btn" onclick="this.closest('.modal').remove()">Close</button> 576 - </div> 577 - `; 578 - } 579 - } 580 - } 581 - 582 - function escapeHtml(text) { 583 - const div = document.createElement('div'); 584 - div.textContent = text; 585 - return div.innerHTML; 586 - } 587 - })(); 588 - </script> 589 309 590 310 {{ template "footer" . }} 591 311 </body>
+2 -3
pkg/appview/templates/pages/terms.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8 max-w-4xl"> 11 + <main id="main-content" class="container mx-auto px-4 py-8 max-w-4xl"> 12 12 <h1 class="text-3xl font-display font-bold tracking-tight mb-2">Terms of Service - {{ .CompanyName }} ({{ .SiteURL }})</h1> 13 13 <p class="text-base-content/60 mb-8"><em>Last updated: January 2025</em></p> 14 14 ··· 104 104 <p>The Service operates on the AT Protocol, a distributed network. Data written to your PDS is controlled by you or your PDS hosting provider, not by us.</p> 105 105 106 106 <h3 class="text-lg font-medium mt-6">7.2 Records on Your PDS</h3> 107 - <p>When you use the Service, records are written to your PDS under the <code class="bg-base-200 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> namespace. We can create, update, and delete these records only while you have granted us OAuth access. Revoking access does not automatically delete existing records.</p> 107 + <p>When you use the Service, records are written to your PDS under the <code class="bg-base-300 px-1.5 py-0.5 rounded text-sm font-mono">io.atcr.*</code> namespace. We can create, update, and delete these records only while you have granted us OAuth access. Revoking access does not automatically delete existing records.</p> 108 108 109 109 <h3 class="text-lg font-medium mt-6">7.3 No Control Over Your PDS</h3> 110 110 <p>We do not control your PDS. If your PDS is offline or your PDS provider terminates your account, we cannot restore your AT Protocol records.</p> ··· 198 198 </div> 199 199 </main> 200 200 201 - <div id="modal"></div> 202 201 203 202 {{ template "footer" . }} 204 203 </body>
+3 -6
pkg/appview/templates/pages/user.html
··· 8 8 <body> 9 9 {{ template "nav" . }} 10 10 11 - <main class="container mx-auto px-4 py-8"> 11 + <main id="main-content" class="container mx-auto px-4 py-8"> 12 12 <div class="flex flex-col items-center gap-8"> 13 13 <!-- User Profile Header --> 14 14 <div class="flex flex-col items-center gap-4"> 15 15 {{ if .ViewedUser.Avatar }} 16 16 <div class="avatar"> 17 17 <div class="w-20 rounded-full shadow"> 18 - <img src="{{ .ViewedUser.Avatar }}" alt="{{ .ViewedUser.Handle }}" loading="lazy" /> 18 + <img src="{{ resizeImage .ViewedUser.Avatar 160 }}" alt="{{ .ViewedUser.Handle }}" width="80" height="80" fetchpriority="high" /> 19 19 </div> 20 20 </div> 21 21 {{ else if .HasProfile }} ··· 26 26 </div> 27 27 {{ else }} 28 28 <div class="avatar avatar-placeholder"> 29 - <div class="bg-base-300 text-base-content/60 w-20 rounded-full shadow"> 29 + <div class="bg-neutral text-neutral-content/60 w-20 rounded-full shadow"> 30 30 <span class="text-3xl">?</span> 31 31 </div> 32 32 </div> ··· 53 53 {{ end }} 54 54 </div> 55 55 </main> 56 - 57 - <!-- Modal container for HTMX --> 58 - <div id="modal"></div> 59 56 60 57 {{ template "footer" . }} 61 58 </body>
+3 -3
pkg/appview/templates/partials/attestation-details.html
··· 12 12 <code class="font-mono text-xs text-base-content/60 truncate max-w-48" title="{{ .Digest }}">{{ .Digest }}</code> 13 13 </div> 14 14 {{ if .NeedsLogin }} 15 - <p class="text-sm text-base-content/50"><a href="{{ $.LoginURL }}" class="link link-primary">Log in</a> to view attestation content</p> 15 + <p class="text-sm text-base-content/70"><a href="{{ $.LoginURL }}" class="link link-primary">Log in</a> to view attestation content</p> 16 16 {{ else if .FetchError }} 17 - <p class="text-sm text-base-content/50">{{ .FetchError }}</p> 17 + <p class="text-sm text-base-content/70">{{ .FetchError }}</p> 18 18 {{ else if .RawJSON }} 19 19 <details> 20 20 <summary class="cursor-pointer text-sm text-base-content/70 hover:text-base-content">View content</summary> ··· 23 23 </div> 24 24 </details> 25 25 {{ else if .Size }} 26 - <p class="text-sm text-base-content/50">Binary content ({{ .Size }} bytes) — cannot display inline</p> 26 + <p class="text-sm text-base-content/70">Binary content ({{ .Size }} bytes) — cannot display inline</p> 27 27 {{ end }} 28 28 </div> 29 29 {{ end }}
+6 -6
pkg/appview/templates/partials/diff-content.html
··· 2 2 <!-- Layers + Vulnerabilities Diff --> 3 3 <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> 4 4 <!-- Layer Diff (Left) --> 5 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 5 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 6 6 <h2 class="text-lg font-semibold">Layers</h2> 7 7 {{ if .LayerDiff }} 8 8 <div class="overflow-x-auto"> ··· 29 29 {{ if not .Layer.EmptyLayer }}{{ humanizeBytes .Layer.Size }}{{ end }} 30 30 {{ if and (eq .Status "rebuilt") .PrevLayer }} 31 31 {{ if ne .Layer.Size .PrevLayer.Size }} 32 - <span class="text-xs text-base-content/50">({{ humanizeByteDelta (sub64 .Layer.Size .PrevLayer.Size) }})</span> 32 + <span class="text-xs text-base-content/70">({{ humanizeByteDelta (sub64 .Layer.Size .PrevLayer.Size) }})</span> 33 33 {{ end }} 34 34 {{ end }} 35 35 </td> ··· 44 44 </div> 45 45 46 46 <!-- Vulnerability Diff (Right) --> 47 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 47 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 48 48 <h2 class="text-lg font-semibold">Vulnerabilities</h2> 49 49 50 50 {{ if not .HasVulnData }} ··· 54 54 <!-- Fixed Vulns --> 55 55 {{ if .FixedVulns }} 56 56 <div class="collapse collapse-arrow bg-success/5 border border-success/20 rounded-lg"> 57 - <input type="checkbox" checked /> 57 + <input type="checkbox" checked aria-label="Toggle fixed vulnerabilities ({{ len .FixedVulns }})" /> 58 58 <div class="collapse-title font-medium text-sm flex items-center gap-2"> 59 59 {{ icon "shield-check" "size-4 text-success" }} 60 60 Fixed ({{ len .FixedVulns }}) ··· 94 94 <!-- New Vulns --> 95 95 {{ if .NewVulns }} 96 96 <div class="collapse collapse-arrow bg-error/5 border border-error/20 rounded-lg"> 97 - <input type="checkbox" checked /> 97 + <input type="checkbox" checked aria-label="Toggle new vulnerabilities ({{ len .NewVulns }})" /> 98 98 <div class="collapse-title font-medium text-sm flex items-center gap-2"> 99 99 {{ icon "alert-triangle" "size-4 text-error" }} 100 100 New ({{ len .NewVulns }}) ··· 136 136 <!-- Unchanged Vulns --> 137 137 {{ if .UnchangedVulns }} 138 138 <div class="collapse collapse-arrow bg-base-200/50 border border-base-300 rounded-lg"> 139 - <input type="checkbox" /> 139 + <input type="checkbox" aria-label="Toggle unchanged vulnerabilities ({{ len .UnchangedVulns }})" /> 140 140 <div class="collapse-title font-medium text-sm text-base-content/60"> 141 141 Unchanged ({{ len .UnchangedVulns }}) 142 142 </div>
+4 -84
pkg/appview/templates/partials/digest-content.html
··· 2 2 <!-- Layers + Vulnerabilities Side by Side --> 3 3 <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> 4 4 <!-- Layers (Left) --> 5 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 5 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 6 6 <div class="flex items-center justify-between"> 7 7 <h2 class="text-lg font-semibold">Layers ({{ len .Layers }})</h2> 8 8 <label class="flex items-center gap-2 text-sm cursor-pointer"> 9 - <input type="checkbox" class="checkbox checkbox-xs show-empty-layers-cb" onchange="toggleEmptyLayers(this.checked)"> 9 + <input type="checkbox" class="checkbox checkbox-xs show-empty-layers-cb" data-toggle-empty-layers> 10 10 <span>Show empty layers</span> 11 11 </label> 12 12 </div> ··· 29 29 <code class="font-mono text-xs break-all line-clamp-2" title="{{ .Command }}">{{ .Command }}</code> 30 30 {{ end }} 31 31 </td> 32 - <td class="text-right text-sm whitespace-nowrap">{{ humanizeBytes .Size }}</td> 32 + <td class="text-right text-sm whitespace-nowrap" data-bytes="{{ .Size }}">{{ humanizeBytes .Size }}</td> 33 33 </tr> 34 34 {{ end }} 35 35 </tbody> 36 36 </table> 37 37 </div> 38 - <script> 39 - (function() { 40 - var showEmpty = localStorage.getItem('showEmptyLayers') === 'true'; 41 - document.querySelectorAll('.show-empty-layers-cb').forEach(function(cb) { cb.checked = showEmpty; }); 42 - 43 - window.toggleEmptyLayers = function(show) { 44 - localStorage.setItem('showEmptyLayers', show); 45 - document.querySelectorAll('.show-empty-layers-cb').forEach(function(cb) { cb.checked = show; }); 46 - applyLayerVisibility(); 47 - }; 48 - 49 - function collapseNoHistoryLayers() { 50 - document.querySelectorAll('.layers-table').forEach(function(table) { 51 - var tbody = table.querySelector('tbody'); 52 - if (!tbody) return; 53 - 54 - var rows = Array.from(tbody.querySelectorAll('tr')); 55 - var i = 0; 56 - while (i < rows.length) { 57 - if (rows[i].dataset.noCommand === 'true') { 58 - var start = i; 59 - while (i < rows.length && rows[i].dataset.noCommand === 'true') { 60 - rows[i].classList.add('no-history-row', 'hidden'); 61 - i++; 62 - } 63 - var count = i - start; 64 - if (count > 1) { 65 - var totalBytes = 0; 66 - for (var k = start; k < start + count; k++) { 67 - var sizeCell = rows[k].querySelector('td:last-child'); 68 - if (sizeCell) { 69 - var txt = sizeCell.textContent.trim(); 70 - var match = txt.match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i); 71 - if (match) { 72 - var val = parseFloat(match[1]); 73 - var unit = match[2].toUpperCase(); 74 - var multipliers = {'B':1,'KB':1024,'MB':1048576,'GB':1073741824,'TB':1099511627776}; 75 - totalBytes += val * (multipliers[unit] || 1); 76 - } 77 - } 78 - } 79 - var sizeStr = ''; 80 - if (totalBytes < 1024) sizeStr = totalBytes + ' B'; 81 - else if (totalBytes < 1048576) sizeStr = (totalBytes/1024).toFixed(1) + ' KB'; 82 - else if (totalBytes < 1073741824) sizeStr = (totalBytes/1048576).toFixed(1) + ' MB'; 83 - else sizeStr = (totalBytes/1073741824).toFixed(1) + ' GB'; 84 - 85 - var startIdx = rows[start].querySelector('td').textContent.trim(); 86 - var endIdx = rows[i - 1].querySelector('td').textContent.trim(); 87 - var summary = document.createElement('tr'); 88 - summary.className = 'no-history-summary cursor-pointer hover:bg-base-200'; 89 - summary.innerHTML = '<td colspan="2" class="text-sm py-2">Layers ' + startIdx + '-' + endIdx + ' contain no history <span class="text-xs ml-2">(' + count + ' layers, click to expand)</span></td><td class="text-right text-sm whitespace-nowrap">' + sizeStr + '</td>'; 90 - summary.onclick = function() { 91 - summary.remove(); 92 - for (var j = start; j < start + count; j++) { 93 - rows[j].classList.remove('hidden'); 94 - } 95 - }; 96 - tbody.insertBefore(summary, rows[start]); 97 - } else { 98 - rows[start].classList.remove('hidden'); 99 - } 100 - } else { 101 - i++; 102 - } 103 - } 104 - }); 105 - } 106 - 107 - function applyLayerVisibility() { 108 - var show = localStorage.getItem('showEmptyLayers') === 'true'; 109 - document.querySelectorAll('.layers-table tr[data-empty="true"]').forEach(function(row) { 110 - row.style.display = show ? '' : 'none'; 111 - }); 112 - } 113 - 114 - collapseNoHistoryLayers(); 115 - applyLayerVisibility(); 116 - })(); 117 - </script> 118 38 {{ else }} 119 39 <p class="text-base-content">No layer information available</p> 120 40 {{ end }} 121 41 </div> 122 42 123 43 <!-- Vulnerabilities + SBOM (Right) --> 124 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 44 + <div class="card bg-base-200 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 125 45 <div role="tablist" class="tabs tabs-bordered"> 126 46 <input type="radio" name="scan-tabs" role="tab" class="tab" aria-label="Vulnerabilities" checked="checked" /> 127 47 <div role="tabpanel" class="tab-content pt-4">
+6 -9
pkg/appview/templates/partials/hold_card.html
··· 1 1 {{ define "hold_card" }} 2 - <div class="card bg-base-100 shadow-sm"> 2 + <div class="card bg-base-200 shadow-sm"> 3 3 <!-- Header --> 4 4 <div class="p-4 flex flex-wrap items-center gap-2"> 5 5 <div class="flex-1 min-w-0"> 6 6 <div class="flex items-center gap-2 flex-wrap"> 7 - <span class="text-warning" title="Active hold">&#9733;</span> 8 7 <h3 class="font-semibold text-lg truncate">{{ .DisplayName }}</h3> 9 - <span class="badge badge-sm badge-warning">Active</span> 8 + <span class="badge badge-sm badge-soft badge-primary">Active</span> 10 9 {{ if eq .Membership "owner" }}<span class="badge badge-sm badge-primary">Owner</span> 11 10 {{ else }}<span class="badge badge-sm badge-secondary">Crew</span>{{ end }} 12 - {{ if eq .Status "online" }}<span class="badge badge-sm badge-success gap-1">&#9679; Online</span> 13 - {{ else if eq .Status "offline" }}<span class="badge badge-sm badge-error gap-1">&#9679; Offline</span> 11 + {{ if eq .Status "online" }}<span class="badge badge-sm badge-success gap-1.5"><span class="inline-block size-1.5 rounded-full bg-current" aria-hidden="true"></span>Online</span> 12 + {{ else if eq .Status "offline" }}<span class="badge badge-sm badge-error gap-1.5"><span class="inline-block size-1.5 rounded-full bg-current" aria-hidden="true"></span>Offline</span> 14 13 {{ end }} 15 14 </div> 16 - <code class="text-xs text-base-content/50 break-all">{{ .DID }}</code> 17 - </div> 18 - <div class="shrink-0"> 15 + <code class="text-xs text-base-content/70 break-all">{{ .DID }}</code> 19 16 </div> 20 17 </div> 21 18 ··· 25 22 hx-get="/api/storage?hold_did={{ .DID | urlquery }}" 26 23 hx-trigger="load, tab:storage from:body once" 27 24 hx-swap="innerHTML"> 28 - <p class="flex items-center gap-2 text-sm text-base-content/50">{{ icon "loader-2" "size-4 animate-spin" }} Loading storage...</p> 25 + <p class="flex items-center gap-2 text-sm text-base-content/70">{{ icon "loader-2" "size-4 animate-spin" }} Loading storage...</p> 29 26 </div> 30 27 </div> 31 28 </div>
+2 -2
pkg/appview/templates/partials/hold_selector.html
··· 1 1 {{ define "hold_selector" }} 2 - <div class="card bg-base-100 shadow-sm p-4"> 2 + <div class="card bg-base-200 shadow-sm p-4"> 3 3 <form hx-post="/api/profile/default-hold" hx-swap="none" class="flex items-center gap-3 flex-wrap"> 4 4 <label class="text-sm font-medium whitespace-nowrap" for="hold-select">Active Hold:</label> 5 5 <select id="hold-select" name="hold_did" class="select select-bordered select-sm flex-1 min-w-0" 6 - onchange="this.form.requestSubmit()"> 6 + data-action="submit-form"> 7 7 {{ if not .ActiveHold }} 8 8 <option value="" selected>-- Select a hold --</option> 9 9 {{ end }}
+3 -3
pkg/appview/templates/partials/image-advisor-results.html
··· 10 10 <span>{{ .Error }}</span> 11 11 </div> 12 12 {{ else if .Suggestions }} 13 - <div class="card bg-base-100 shadow-sm border border-base-300"> 13 + <div class="card bg-base-200 shadow-sm border border-base-300"> 14 14 <div class="card-body p-4 space-y-3"> 15 15 <h3 class="text-sm font-semibold flex items-center gap-2"> 16 16 {{ icon "sparkle" "size-4" }} ··· 66 66 </tbody> 67 67 </table> 68 68 </div> 69 - <p class="text-xs opacity-50">Generated by Claude Haiku. Suggestions are advisory only.</p> 69 + <p class="text-xs text-base-content/70">Generated by Claude Haiku. Suggestions are advisory only.</p> 70 70 </div> 71 71 </div> 72 72 {{ else }} 73 - <p class="text-sm opacity-60">No suggestions generated.</p> 73 + <p class="text-sm text-base-content/60">No suggestions generated.</p> 74 74 {{ end }} 75 75 {{ end }}
+7 -84
pkg/appview/templates/partials/layers-section.html
··· 1 1 {{ define "layers-section" }} 2 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 2 + <div class="space-y-4 min-w-0 pt-6"> 3 3 <div class="flex items-center justify-between"> 4 4 <h2 class="text-lg font-semibold">Layers ({{ len .Layers }})</h2> 5 5 <label class="flex items-center gap-2 text-sm cursor-pointer"> 6 - <input type="checkbox" class="checkbox checkbox-xs show-empty-layers-cb" onchange="toggleEmptyLayers(this.checked)"> 6 + <input type="checkbox" class="checkbox checkbox-xs show-empty-layers-cb" data-toggle-empty-layers> 7 7 <span>Show empty layers</span> 8 8 </label> 9 9 </div> ··· 26 26 <code class="font-mono text-xs break-all line-clamp-2" title="{{ .Command }}">{{ .Command }}</code> 27 27 {{ end }} 28 28 </td> 29 - <td class="text-right text-sm whitespace-nowrap">{{ humanizeBytes .Size }}</td> 29 + <td class="text-right text-sm whitespace-nowrap" data-bytes="{{ .Size }}">{{ humanizeBytes .Size }}</td> 30 30 </tr> 31 31 {{ end }} 32 32 </tbody> 33 33 </table> 34 34 </div> 35 - <script> 36 - (function() { 37 - var showEmpty = localStorage.getItem('showEmptyLayers') === 'true'; 38 - document.querySelectorAll('.show-empty-layers-cb').forEach(function(cb) { cb.checked = showEmpty; }); 39 - 40 - window.toggleEmptyLayers = function(show) { 41 - localStorage.setItem('showEmptyLayers', show); 42 - document.querySelectorAll('.show-empty-layers-cb').forEach(function(cb) { cb.checked = show; }); 43 - applyLayerVisibility(); 44 - }; 45 - 46 - function collapseNoHistoryLayers() { 47 - document.querySelectorAll('.layers-table').forEach(function(table) { 48 - var tbody = table.querySelector('tbody'); 49 - if (!tbody) return; 50 - 51 - var rows = Array.from(tbody.querySelectorAll('tr')); 52 - var i = 0; 53 - while (i < rows.length) { 54 - if (rows[i].dataset.noCommand === 'true') { 55 - var start = i; 56 - while (i < rows.length && rows[i].dataset.noCommand === 'true') { 57 - rows[i].classList.add('no-history-row', 'hidden'); 58 - i++; 59 - } 60 - var count = i - start; 61 - if (count > 1) { 62 - var totalBytes = 0; 63 - for (var k = start; k < start + count; k++) { 64 - var sizeCell = rows[k].querySelector('td:last-child'); 65 - if (sizeCell) { 66 - var txt = sizeCell.textContent.trim(); 67 - var match = txt.match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i); 68 - if (match) { 69 - var val = parseFloat(match[1]); 70 - var unit = match[2].toUpperCase(); 71 - var multipliers = {'B':1,'KB':1024,'MB':1048576,'GB':1073741824,'TB':1099511627776}; 72 - totalBytes += val * (multipliers[unit] || 1); 73 - } 74 - } 75 - } 76 - var sizeStr = ''; 77 - if (totalBytes < 1024) sizeStr = totalBytes + ' B'; 78 - else if (totalBytes < 1048576) sizeStr = (totalBytes/1024).toFixed(1) + ' KB'; 79 - else if (totalBytes < 1073741824) sizeStr = (totalBytes/1048576).toFixed(1) + ' MB'; 80 - else sizeStr = (totalBytes/1073741824).toFixed(1) + ' GB'; 81 - 82 - var startIdx = rows[start].querySelector('td').textContent.trim(); 83 - var endIdx = rows[i - 1].querySelector('td').textContent.trim(); 84 - var summary = document.createElement('tr'); 85 - summary.className = 'no-history-summary cursor-pointer hover:bg-base-200'; 86 - summary.innerHTML = '<td colspan="2" class="text-sm py-2">Layers ' + startIdx + '-' + endIdx + ' contain no history <span class="text-xs ml-2">(' + count + ' layers, click to expand)</span></td><td class="text-right text-sm whitespace-nowrap">' + sizeStr + '</td>'; 87 - summary.onclick = function() { 88 - summary.remove(); 89 - for (var j = start; j < start + count; j++) { 90 - rows[j].classList.remove('hidden'); 91 - } 92 - }; 93 - tbody.insertBefore(summary, rows[start]); 94 - } else { 95 - rows[start].classList.remove('hidden'); 96 - } 97 - } else { 98 - i++; 99 - } 100 - } 101 - }); 102 - } 103 - 104 - function applyLayerVisibility() { 105 - var show = localStorage.getItem('showEmptyLayers') === 'true'; 106 - document.querySelectorAll('.layers-table tr[data-empty="true"]').forEach(function(row) { 107 - row.style.display = show ? '' : 'none'; 108 - }); 109 - } 110 - 111 - collapseNoHistoryLayers(); 112 - applyLayerVisibility(); 113 - })(); 114 - </script> 115 35 {{ else }} 116 - <p class="text-base-content">No layer information available</p> 36 + <div class="py-8 text-sm text-base-content/70 max-w-prose"> 37 + <p class="font-medium text-base-content">No layer history available</p> 38 + <p class="mt-1">This image was built without history metadata — common for squashed images or tarballs imported with <code class="font-mono text-xs bg-base-300 px-1 py-0.5 rounded">docker load</code>.</p> 39 + </div> 117 40 {{ end }} 118 41 </div> 119 42 {{ end }}
+4 -4
pkg/appview/templates/partials/other_holds_table.html
··· 1 1 {{ define "other_holds_table" }} 2 - <div class="card bg-base-100 shadow-sm"> 2 + <div class="card bg-base-200 shadow-sm"> 3 3 <div class="p-4 pb-2"> 4 4 <h3 class="text-sm font-semibold text-base-content/70">Other Holds</h3> 5 5 </div> ··· 25 25 </td> 26 26 <td class="text-center"> 27 27 {{ if eq .Status "online" }} 28 - <span class="text-success" aria-hidden="true">&#9679;</span> 28 + <span class="inline-block size-2 rounded-full bg-success" aria-hidden="true"></span> 29 29 <span class="sr-only">Online</span> 30 30 {{ else if eq .Status "offline" }} 31 - <span class="text-error" aria-hidden="true">&#9679;</span> 31 + <span class="inline-block size-2 rounded-full bg-error" aria-hidden="true"></span> 32 32 <span class="sr-only">Offline</span> 33 33 {{ else }} 34 - <span class="text-base-content/50" aria-hidden="true">&#9679;</span> 34 + <span class="inline-block size-2 rounded-full bg-base-content/40" aria-hidden="true"></span> 35 35 <span class="sr-only">Unknown</span> 36 36 {{ end }} 37 37 </td>
+24 -24
pkg/appview/templates/partials/repo-tag-section.html
··· 11 11 {{ range .NonDefaultHolds }} 12 12 <span class="badge badge-outline badge-sm" title="{{ . }}">{{ displayHoldDID . }}</span> 13 13 {{ end }} 14 - <span class="text-base-content/50">(different from your default hold)</span> 14 + <span class="text-base-content/70">(different from your default hold)</span> 15 15 </div> 16 16 {{ end }} 17 17 18 18 <!-- Stats Cards --> 19 19 <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4"> 20 20 <!-- Size & Layers --> 21 - <div class="card bg-base-100 border border-base-300 p-4"> 21 + <div class="card bg-base-200 border border-base-300 p-4"> 22 22 <div class="flex items-center justify-between mb-2"> 23 - <span class="text-xs font-semibold uppercase tracking-wider text-base-content/50">Image Size</span> 24 - <span class="text-xs font-semibold uppercase tracking-wider text-base-content/50">Layers</span> 23 + <span class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Image Size</span> 24 + <span class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Layers</span> 25 25 </div> 26 26 <div class="flex items-center justify-between"> 27 27 <span class="text-lg font-bold">{{ humanizeBytes .SelectedTag.CompressedSize }}</span> 28 28 <span class="text-lg font-bold">{{ .SelectedTag.LayerCount }}</span> 29 29 </div> 30 - <div class="text-xs text-base-content/50 mt-1"> 30 + <div class="text-xs text-base-content/70 mt-1"> 31 31 Pushed {{ timeAgoShort .SelectedTag.Info.CreatedAt }} 32 32 </div> 33 33 </div> 34 34 35 35 <!-- Vulnerabilities (first platform only for summary) --> 36 - <div class="card bg-base-100 border border-base-300 p-4"> 37 - <div class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-2">Vulnerabilities</div> 36 + <div class="card bg-base-200 border border-base-300 p-4"> 37 + <div class="text-xs font-semibold uppercase tracking-wider text-base-content/70 mb-2">Vulnerabilities</div> 38 38 <div id="vuln-summary-card"> 39 39 {{ $firstPlatform := index .SelectedTag.Info.Platforms 0 }} 40 40 <span id="scan-badge-{{ trimPrefix "sha256:" $firstPlatform.Digest }}"></span> 41 - <span id="vuln-loading-text" class="text-sm text-base-content/40">Loading...</span> 41 + <span id="vuln-loading-text" class="text-sm text-base-content/70">Loading...</span> 42 42 </div> 43 43 </div> 44 44 45 45 <!-- Pulls --> 46 - <div class="card bg-base-100 border border-base-300 p-4"> 46 + <div class="card bg-base-200 border border-base-300 p-4"> 47 47 <div class="flex items-center justify-between mb-2"> 48 - <span class="text-xs font-semibold uppercase tracking-wider text-base-content/50">Pulls</span> 48 + <span class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Pulls</span> 49 49 </div> 50 - <div class="text-lg font-bold">{{ .Stats.PullCount }} <span class="text-sm font-normal text-base-content/50">total</span></div> 51 - <div class="text-xs text-base-content/50 mt-1"> 50 + <div class="text-lg font-bold">{{ .Stats.PullCount }} <span class="text-sm font-normal text-base-content/70">total</span></div> 51 + <div class="text-xs text-base-content/70 mt-1"> 52 52 {{ if .Stats.LastPull }}Last pull {{ timeAgoShort (derefTime .Stats.LastPull) }}{{ else }}No pulls yet{{ end }} 53 53 </div> 54 54 </div> ··· 80 80 aria-selected="false" 81 81 aria-controls="tab-overview" 82 82 id="overview-tab-btn" 83 - onclick="switchRepoTab('overview')"> 83 + data-action="switch-repo-tab"> 84 84 Overview 85 85 </button> 86 86 {{ if .SelectedTag }} ··· 90 90 aria-selected="false" 91 91 aria-controls="tab-layers" 92 92 id="layers-tab-btn" 93 - onclick="switchRepoTab('layers')"> 93 + data-action="switch-repo-tab"> 94 94 Layers 95 95 </button> 96 96 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" ··· 99 99 aria-selected="false" 100 100 aria-controls="tab-vulns" 101 101 id="vulns-tab-btn" 102 - onclick="switchRepoTab('vulns')"> 102 + data-action="switch-repo-tab"> 103 103 Vulnerabilities 104 104 </button> 105 105 <button class="repo-tab shrink-0 whitespace-nowrap px-4 sm:px-6 py-3 text-sm font-medium border-b-2 border-transparent text-base-content/60 transition-colors cursor-pointer" ··· 108 108 aria-selected="false" 109 109 aria-controls="tab-sbom" 110 110 id="sbom-tab-btn" 111 - onclick="switchRepoTab('sbom')"> 111 + data-action="switch-repo-tab"> 112 112 SBOM 113 113 </button> 114 114 {{ end }} ··· 118 118 aria-selected="false" 119 119 aria-controls="tab-artifacts" 120 120 id="artifacts-tab-btn" 121 - onclick="switchRepoTab('artifacts')"> 121 + data-action="switch-repo-tab"> 122 122 Artifacts 123 123 </button> 124 124 </nav> ··· 126 126 127 127 <!-- Tab Panels --> 128 128 <div id="tab-overview" class="repo-panel" role="tabpanel" aria-labelledby="overview-tab-btn" tabindex="0"> 129 - <div id="overview-view" class="card bg-base-100 shadow-sm p-6 space-y-4 min-w-0"> 129 + <div id="overview-view" class="space-y-4 min-w-0 pt-6"> 130 130 {{ if and .AIAdvisorEnabled .User .IsOwner .SelectedTag }} 131 131 <div id="ai-advisor-section"> 132 132 <button id="ai-advisor-btn" class="btn btn-sm btn-outline gap-1" ··· 147 147 148 148 {{ if and .IsOwner .ReadmeHTML }} 149 149 <div class="flex justify-end"> 150 - <button class="btn btn-sm btn-ghost gap-1" onclick="toggleOverviewEditor(true)"> 150 + <button class="btn btn-sm btn-ghost gap-1" data-action="toggle-editor" data-show="true"> 151 151 {{ icon "pencil" "size-4" }} 152 152 Edit 153 153 </button> ··· 160 160 {{ else }} 161 161 {{ if .IsOwner }} 162 162 <div class="text-center py-12"> 163 - {{ icon "file-text" "size-12 text-base-content opacity-40 mx-auto" }} 163 + {{ icon "file-text" "size-12 text-base-content/40 mx-auto" }} 164 164 <p class="text-base-content/60 mt-4">No README provided</p> 165 - <p class="text-base-content/40 text-sm mt-1">Add a README to help users understand this image.</p> 166 - <button class="btn btn-primary btn-sm mt-4" onclick="toggleOverviewEditor(true)"> 165 + <p class="text-base-content/70 text-sm mt-1">Add a README to help users understand this image.</p> 166 + <button class="btn btn-primary btn-sm mt-4" data-action="toggle-editor" data-show="true"> 167 167 {{ icon "pencil" "size-4" }} Add README 168 168 </button> 169 169 </div> 170 170 {{ else }} 171 171 <div class="text-center py-12"> 172 - {{ icon "file-text" "size-12 text-base-content opacity-40 mx-auto" }} 172 + {{ icon "file-text" "size-12 text-base-content/40 mx-auto" }} 173 173 <p class="text-base-content/60 mt-4">No README provided</p> 174 - <p class="text-base-content/40 text-sm mt-1">Image metadata is shown above.</p> 174 + <p class="text-base-content/70 text-sm mt-1">Image metadata is shown above.</p> 175 175 </div> 176 176 {{ end }} 177 177 {{ end }}
+9 -9
pkg/appview/templates/partials/repo-tags.html
··· 14 14 hx-get="/api/attestation-details?digest={{ .Entry.Digest | urlquery }}&did={{ .OwnerDID | urlquery }}&repo={{ .RepoName | urlquery }}" 15 15 hx-target="#attestation-modal-body" 16 16 hx-swap="innerHTML" 17 - onclick="document.getElementById('attestation-detail-modal').showModal()"> 17 + data-action="show-modal" data-modal-id="attestation-detail-modal"> 18 18 {{ icon "shield-check" "size-3" }} Attested 19 19 </button> 20 20 {{ end }} ··· 38 38 </button> 39 39 {{ else }} 40 40 <button class="btn btn-ghost btn-sm text-error" 41 - onclick="deleteManifest('{{ .RepoName }}', '{{ .Entry.Digest }}', '')" 41 + data-action="delete-manifest" data-repo="{{ .RepoName }}" data-digest="{{ .Entry.Digest }}" 42 42 aria-label="Delete manifest"> 43 43 {{ icon "trash-2" "size-4" }} 44 44 </button> ··· 86 86 <td> 87 87 <div class="flex items-center gap-1"> 88 88 <a href="/d/{{ $.OwnerHandle }}/{{ $.RepoName }}/{{ $.Entry.Digest }}?platform={{ .Digest }}" class="font-mono text-xs link link-primary" title="{{ .Digest }}">{{ truncateDigest (trimPrefix "sha256:" .Digest) 16 }}</a> 89 - <button class="btn btn-ghost btn-xs hidden sm:inline-flex" onclick="copyToClipboard('{{ .Digest }}')" aria-label="Copy digest">{{ icon "copy" "size-3" }}</button> 89 + <button class="btn btn-ghost btn-sm sm:btn-xs" data-action="copy" data-copy="{{ .Digest }}" aria-label="Copy digest">{{ icon "copy" "size-3" }}</button> 90 90 </div> 91 91 </td> 92 92 <td> ··· 110 110 111 111 {{ define "load-more-button" }} 112 112 {{ if .HasMore }} 113 - <div id="load-more-container" class="p-6 text-center border-t border-base-200"> 113 + <div id="load-more-container" class="p-6 text-center border-t border-base-300"> 114 114 <button class="btn btn-outline" 115 115 hx-get="/api/repo-tags/{{ .Owner.Handle }}/{{ .Repository.Name }}?offset={{ .NextOffset }}" 116 116 hx-target="#tags-list" ··· 138 138 <div class="flex flex-wrap items-center gap-4"> 139 139 <div class="flex items-center gap-2"> 140 140 <span class="text-sm font-medium whitespace-nowrap">Sort by</span> 141 - <select id="tag-sort" class="select select-sm select-bordered min-w-28" onchange="sortTags(this.value)"> 141 + <select id="tag-sort" class="select select-sm select-bordered min-w-28" data-action="sort-tags"> 142 142 <option value="newest">Newest</option> 143 143 <option value="oldest">Oldest</option> 144 144 <option value="az">A-Z</option> ··· 146 146 </select> 147 147 </div> 148 148 <div class="flex-1 max-w-xs"> 149 - <input type="text" id="tag-filter" class="input input-sm input-bordered w-full" placeholder="Filter artifacts..." oninput="filterTags(this.value)"> 149 + <input type="text" id="tag-filter" class="input input-sm input-bordered w-full" placeholder="Filter artifacts..." data-action="filter-tags"> 150 150 </div> 151 151 {{ if $.IsOwner }} 152 152 <div class="flex-none ml-auto"> 153 153 <button class="btn btn-ghost btn-sm text-error" 154 - onclick="document.getElementById('untagged-delete-modal').showModal()" 154 + data-action="show-modal" data-modal-id="untagged-delete-modal" 155 155 aria-label="Delete all untagged manifests"> 156 156 {{ icon "trash-2" "size-4" }} Delete untagged 157 157 </button> ··· 160 160 </div> 161 161 162 162 <!-- Manifest Entries --> 163 - <div class="card bg-base-100 shadow-sm border border-base-300"> 164 - <div class="divide-y divide-base-200" id="tags-list"> 163 + <div class="pt-6"> 164 + <div class="divide-y divide-base-300" id="tags-list"> 165 165 {{ range .Entries }} 166 166 {{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner "ViewerDefaultHold" $.ViewerDefaultHold) }} 167 167 {{ end }}
+5 -5
pkg/appview/templates/partials/sbom-details.html
··· 1 1 {{ define "sbom-details" }} 2 2 {{ if .Error }} 3 3 <p>{{ .Error }}</p> 4 - {{ if .ScannedAt }}<p class="text-xs opacity-60">Scanned: {{ .ScannedAt }}</p>{{ end }} 4 + {{ if .ScannedAt }}<p class="text-xs text-base-content/60">Scanned: {{ .ScannedAt }}</p>{{ end }} 5 5 {{ else }} 6 6 <div class="space-y-4" data-csv-section data-csv-filename="sbom.csv"> 7 7 <div class="flex flex-wrap items-center gap-3"> ··· 12 12 {{ icon "download" "size-3.5" }} 13 13 {{ icon "chevron-down" "size-3" }} 14 14 </summary> 15 - <ul class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-300"> 15 + <ul class="dropdown-content menu bg-base-200 rounded-box z-50 w-52 p-2 shadow-lg border border-base-300"> 16 16 <li><button type="button" data-copy-csv>{{ icon "copy" "size-4" }} Copy as CSV</button></li> 17 17 <li><a href="/api/scan-download?type=sbom&digest={{ .Digest | urlquery }}&holdEndpoint={{ .HoldEndpoint | urlquery }}" download>{{ icon "download" "size-4" }} SPDX JSON</a></li> 18 18 </ul> ··· 20 20 {{ end }} 21 21 </div> 22 22 23 - {{ if .ScannedAt }}<p class="text-xs opacity-60">Scanned: {{ .ScannedAt }}</p>{{ end }} 23 + {{ if .ScannedAt }}<p class="text-xs text-base-content/60">Scanned: {{ .ScannedAt }}</p>{{ end }} 24 24 25 25 {{ if .Packages }} 26 26 <div class="overflow-y-auto max-h-[32rem]"> ··· 40 40 <td class="font-mono text-xs">{{ .Version }}</td> 41 41 <td class="text-xs"> 42 42 {{ if eq .License "-" }} 43 - <span class="opacity-40">-</span> 43 + <span class="text-base-content/40">-</span> 44 44 {{ else }} 45 45 {{ .License }} 46 46 {{ end }} ··· 49 49 {{ if .Type }} 50 50 <span class="badge badge-xs badge-ghost">{{ .Type }}</span> 51 51 {{ else }} 52 - <span class="opacity-40">-</span> 52 + <span class="text-base-content/40">-</span> 53 53 {{ end }} 54 54 </td> 55 55 </tr>
+1 -1
pkg/appview/templates/partials/sbom-section.html
··· 1 1 {{ define "sbom-section" }} 2 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 2 + <div class="space-y-4 min-w-0 pt-6"> 3 3 {{ if .SbomData }} 4 4 {{ template "sbom-details" .SbomData }} 5 5 {{ else }}
+1 -1
pkg/appview/templates/partials/subscription_info.html
··· 1 1 {{ define "subscription_plans" }} 2 2 {{ if not .HideBilling }} 3 3 {{ if .Tiers }} 4 - <section class="card bg-base-100 shadow-sm p-6 space-y-4"> 4 + <section class="card bg-base-200 shadow-sm p-6 space-y-4"> 5 5 <h3 class="text-xl font-semibold">Available Plans</h3> 6 6 <div class="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-4"> 7 7 {{ range .Tiers }}
+5 -5
pkg/appview/templates/partials/vuln-badge.html
··· 6 6 {{ else if eq .Total 0 }} 7 7 <span class="badge badge-sm badge-success" title="No vulnerabilities found (scanned {{ .ScannedAt }})">{{ icon "shield-check" "size-3" }} Clean</span> 8 8 {{ else }} 9 - <span class="vuln-strip" title="Vulnerabilities: {{ .Critical }} critical, {{ .High }} high, {{ .Medium }} medium, {{ .Low }} low (scanned {{ .ScannedAt }})"> 10 - <span class="tooltip vuln-box-critical" data-tip="Critical">{{ .Critical }}</span> 11 - <span class="tooltip vuln-box-high" data-tip="High">{{ .High }}</span> 12 - <span class="tooltip vuln-box-medium" data-tip="Medium">{{ .Medium }}</span> 13 - <span class="tooltip vuln-box-low" data-tip="Low">{{ .Low }}</span> 9 + <span class="vuln-strip" role="group" aria-label="Vulnerabilities: {{ .Critical }} critical, {{ .High }} high, {{ .Medium }} medium, {{ .Low }} low (scanned {{ .ScannedAt }})" title="Vulnerabilities: {{ .Critical }} critical, {{ .High }} high, {{ .Medium }} medium, {{ .Low }} low (scanned {{ .ScannedAt }})"> 10 + <span class="tooltip vuln-box-critical" data-tip="Critical" aria-label="{{ .Critical }} critical">{{ .Critical }}</span> 11 + <span class="tooltip vuln-box-high" data-tip="High" aria-label="{{ .High }} high">{{ .High }}</span> 12 + <span class="tooltip vuln-box-medium" data-tip="Medium" aria-label="{{ .Medium }} medium">{{ .Medium }}</span> 13 + <span class="tooltip vuln-box-low" data-tip="Low" aria-label="{{ .Low }} low">{{ .Low }}</span> 14 14 </span> 15 15 {{ end }} 16 16 {{ end }}
+15 -15
pkg/appview/templates/partials/vuln-details.html
··· 3 3 {{ if .Summary.Total }} 4 4 <!-- Summary available but no detailed report --> 5 5 <div class="space-y-4"> 6 - <span class="vuln-strip"> 7 - <span class="tooltip vuln-box-critical" data-tip="Critical">{{ .Summary.Critical }}</span> 8 - <span class="tooltip vuln-box-high" data-tip="High">{{ .Summary.High }}</span> 9 - <span class="tooltip vuln-box-medium" data-tip="Medium">{{ .Summary.Medium }}</span> 10 - <span class="tooltip vuln-box-low" data-tip="Low">{{ .Summary.Low }}</span> 6 + <span class="vuln-strip" role="group" aria-label="Vulnerability summary by severity"> 7 + <span class="tooltip vuln-box-critical" data-tip="Critical" aria-label="{{ .Summary.Critical }} critical">{{ .Summary.Critical }}</span> 8 + <span class="tooltip vuln-box-high" data-tip="High" aria-label="{{ .Summary.High }} high">{{ .Summary.High }}</span> 9 + <span class="tooltip vuln-box-medium" data-tip="Medium" aria-label="{{ .Summary.Medium }} medium">{{ .Summary.Medium }}</span> 10 + <span class="tooltip vuln-box-low" data-tip="Low" aria-label="{{ .Summary.Low }} low">{{ .Summary.Low }}</span> 11 11 </span> 12 12 <p class="text-sm">{{ .Error }}</p> 13 - {{ if .ScannedAt }}<p class="text-xs opacity-60">Scanned: {{ .ScannedAt }}</p>{{ end }} 13 + {{ if .ScannedAt }}<p class="text-xs text-base-content/60">Scanned: {{ .ScannedAt }}</p>{{ end }} 14 14 </div> 15 15 {{ else }} 16 16 <p>{{ .Error }}</p> ··· 20 20 <!-- Summary badges --> 21 21 <div class="flex flex-wrap items-center gap-3"> 22 22 <span class="font-semibold text-sm">{{ .Summary.Total }} vulnerabilities</span> 23 - <span class="vuln-strip"> 24 - <span class="tooltip vuln-box-critical" data-tip="Critical">{{ .Summary.Critical }}</span> 25 - <span class="tooltip vuln-box-high" data-tip="High">{{ .Summary.High }}</span> 26 - <span class="tooltip vuln-box-medium" data-tip="Medium">{{ .Summary.Medium }}</span> 27 - <span class="tooltip vuln-box-low" data-tip="Low">{{ .Summary.Low }}</span> 23 + <span class="vuln-strip" role="group" aria-label="Vulnerability summary by severity"> 24 + <span class="tooltip vuln-box-critical" data-tip="Critical" aria-label="{{ .Summary.Critical }} critical">{{ .Summary.Critical }}</span> 25 + <span class="tooltip vuln-box-high" data-tip="High" aria-label="{{ .Summary.High }} high">{{ .Summary.High }}</span> 26 + <span class="tooltip vuln-box-medium" data-tip="Medium" aria-label="{{ .Summary.Medium }} medium">{{ .Summary.Medium }}</span> 27 + <span class="tooltip vuln-box-low" data-tip="Low" aria-label="{{ .Summary.Low }} low">{{ .Summary.Low }}</span> 28 28 </span> 29 29 {{ if .Matches }} 30 30 <details class="dropdown dropdown-end ml-auto"> ··· 32 32 {{ icon "download" "size-3.5" }} 33 33 {{ icon "chevron-down" "size-3" }} 34 34 </summary> 35 - <ul class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-300"> 35 + <ul class="dropdown-content menu bg-base-200 rounded-box z-50 w-52 p-2 shadow-lg border border-base-300"> 36 36 <li><button type="button" data-copy-csv>{{ icon "copy" "size-4" }} Copy as CSV</button></li> 37 37 <li><a href="/api/scan-download?type=vuln&digest={{ .Digest | urlquery }}&holdEndpoint={{ .HoldEndpoint | urlquery }}" download>{{ icon "download" "size-4" }} Grype Report</a></li> 38 38 </ul> ··· 40 40 {{ end }} 41 41 </div> 42 42 43 - {{ if .ScannedAt }}<p class="text-xs opacity-60">Scanned: {{ .ScannedAt }}</p>{{ end }} 43 + {{ if .ScannedAt }}<p class="text-xs text-base-content/60">Scanned: {{ .ScannedAt }}</p>{{ end }} 44 44 45 45 {{ if .Matches }} 46 46 <!-- CVE table --> ··· 79 79 {{ end }} 80 80 </td> 81 81 <td class="text-xs"> 82 - {{ .Package }}{{ if .Type }} <span class="opacity-60">({{ .Type }})</span>{{ end }} 82 + {{ .Package }}{{ if .Type }} <span class="text-base-content/60">({{ .Type }})</span>{{ end }} 83 83 </td> 84 84 <td class="font-mono text-xs">{{ .Version }}</td> 85 85 <td class="font-mono text-xs"> 86 86 {{ if .FixedIn }} 87 87 <span class="text-success">{{ .FixedIn }}</span> 88 88 {{ else }} 89 - <span class="opacity-40">-</span> 89 + <span class="text-base-content/40">-</span> 90 90 {{ end }} 91 91 </td> 92 92 </tr>
+5 -2
pkg/appview/templates/partials/vulns-section.html
··· 1 1 {{ define "vulns-section" }} 2 - <div class="card bg-base-100 shadow-sm border border-base-300 p-6 space-y-4 min-w-0"> 2 + <div class="space-y-4 min-w-0 pt-6"> 3 3 {{ if .VulnData }} 4 4 {{ template "vuln-details" .VulnData }} 5 5 {{ else }} 6 - <p class="text-base-content">No vulnerability scan data available</p> 6 + <div class="py-8 text-sm text-base-content/70 max-w-prose"> 7 + <p class="font-medium text-base-content">No vulnerability scan available yet</p> 8 + <p class="mt-1">Scans run automatically shortly after a push. Check back in a few minutes, or push a new tag to trigger a scan.</p> 9 + </div> 7 10 {{ end }} 8 11 </div> 9 12 {{ end }}
+8 -7
pkg/appview/templates/partials/webhooks_list.html
··· 12 12 <span class="label-text">Webhook URL</span> 13 13 </label> 14 14 <input type="url" id="webhook-url" name="url" placeholder="https://example.com/webhook" 15 - class="input input-bordered w-full" required> 15 + class="input input-bordered w-full" required aria-required="true"> 16 16 </fieldset> 17 17 18 18 <fieldset class="fieldset"> 19 19 <label class="label" for="webhook-secret"> 20 - <span class="label-text">Signing Secret <span class="text-base-content/50">(optional)</span></span> 20 + <span class="label-text">Signing Secret <span class="text-base-content/70">(optional)</span></span> 21 21 </label> 22 22 <input type="password" id="webhook-secret" name="secret" placeholder="HMAC-SHA256 signing secret" 23 - class="input input-bordered w-full" autocomplete="new-password"> 24 - <p class="text-xs text-base-content/60 mt-1">If set, payloads include an <code class="cmd">X-Webhook-Signature-256</code> header</p> 23 + class="input input-bordered w-full" autocomplete="new-password" 24 + aria-describedby="webhook-secret-hint"> 25 + <p id="webhook-secret-hint" class="text-xs text-base-content/60 mt-1">If set, payloads include an <code class="cmd">X-Webhook-Signature-256</code> header</p> 25 26 </fieldset> 26 27 27 28 <fieldset class="fieldset"> ··· 58 59 {{ if .Webhooks }} 59 60 <div class="space-y-3"> 60 61 {{ range .Webhooks }} 61 - <div class="card bg-base-100 border border-base-300 p-4"> 62 + <div class="card bg-base-200 border border-base-300 p-4"> 62 63 <div class="flex items-start justify-between gap-4"> 63 64 <div class="min-w-0 flex-1"> 64 65 <code class="text-sm break-all">{{ .URL }}</code> ··· 82 83 </div> 83 84 <div class="flex gap-2 shrink-0"> 84 85 <button class="btn btn-xs btn-ghost" 85 - onclick="testWebhook('{{ .ID }}')" 86 + data-action="test-webhook" data-webhook-id="{{ .ID }}" 86 87 title="Send test payload"> 87 88 Test 88 89 </button> ··· 100 101 {{ end }} 101 102 </div> 102 103 {{ else }} 103 - <p class="text-base-content/50 text-sm text-center py-4">No webhooks configured yet.</p> 104 + <p class="text-base-content/70 text-sm text-center py-4">No webhooks configured yet.</p> 104 105 {{ end }} 105 106 </div> 106 107 {{ end }}
-1
pkg/hold/admin/public/icons.svg
··· 30 30 <symbol id="fingerprint" viewBox="0 0 24 24"><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M2 12a10 10 0 0 1 18-6"/><path d="M2 16h.01"/><path d="M21.8 16c.2-2 .131-5.354 0-6"/><path d="M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2"/><path d="M8.65 22c.21-.66.45-1.32.57-2"/><path d="M9 6.8a6 6 0 0 1 9 5.2v2"/></symbol> 31 31 <symbol id="git-compare" viewBox="0 0 24 24"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M13 6h3a2 2 0 0 1 2 2v7"/><path d="M11 18H8a2 2 0 0 1-2-2V9"/></symbol> 32 32 <symbol id="git-merge" viewBox="0 0 24 24"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></symbol> 33 - <symbol id="github" viewBox="0 0 24 24"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></symbol> 34 33 <symbol id="hard-drive" viewBox="0 0 24 24"><path d="M10 16h.01"/><path d="M2.212 11.577a2 2 0 0 0-.212.896V18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-5.527a2 2 0 0 0-.212-.896L18.55 5.11A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><path d="M21.946 12.013H2.054"/><path d="M6 16h.01"/></symbol> 35 34 <symbol id="heading" viewBox="0 0 24 24"><path d="M6 12h12"/><path d="M6 20V4"/><path d="M18 20V4"/></symbol> 36 35 <symbol id="history" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></symbol>
+3 -3
themes/seamark/templates/components/hero.html
··· 38 38 39 39 <!-- Benefit Cards --> 40 40 <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12 w-full max-w-4xl"> 41 - <div class="card bg-base-100 shadow-sm p-6 text-center"> 41 + <div class="card bg-base-200 shadow-sm p-6 text-center"> 42 42 <div class="text-primary mb-4 flex justify-center"> 43 43 {{ icon "ship" "size-8" }} 44 44 </div> 45 45 <h2 class="font-semibold text-lg">Works with Docker</h2> 46 46 <p class="text-base-content/70 mt-2">Use docker push &amp; pull. No new tools to learn.</p> 47 47 </div> 48 - <div class="card bg-base-100 shadow-sm p-6 text-center"> 48 + <div class="card bg-base-200 shadow-sm p-6 text-center"> 49 49 <div class="text-primary mb-4 flex justify-center"> 50 50 {{ icon "anchor" "size-8" }} 51 51 </div> 52 52 <h2 class="font-semibold text-lg">Your Data</h2> 53 53 <p class="text-base-content/70 mt-2">Join shared holds or captain your own storage.</p> 54 54 </div> 55 - <div class="card bg-base-100 shadow-sm p-6 text-center"> 55 + <div class="card bg-base-200 shadow-sm p-6 text-center"> 56 56 <div class="text-primary mb-4 flex justify-center"> 57 57 {{ icon "compass" "size-8" }} 58 58 </div>
+14 -4
themes/seamark/theme.css
··· 1 1 /* Seamark color overrides (injected after atcr.io's compiled stylesheet) */ 2 2 [data-theme="dark"] { 3 + --color-primary: oklch(72% 0.17 225); 4 + --color-primary-content: oklch(15% 0.05 240); 3 5 --color-secondary: oklch(90% 0.182 98.111); 4 6 --color-secondary-content: oklch(30% 0.182 98.111); 5 - --color-accent: oklch(66.6% 0.121 28); 6 - --color-accent-content: oklch(28% 0.121 28); 7 + --color-accent: oklch(70% 0.14 345); 8 + --color-accent-content: oklch(98% 0.008 345); 9 + --color-base-100: oklch(20% 0.05 250); 10 + --color-base-200: oklch(25% 0.05 250); 11 + --color-base-300: oklch(30% 0.05 250); 7 12 } 8 13 9 14 [data-theme="light"] { 15 + --color-primary: oklch(48% 0.17 250); 16 + --color-primary-content: oklch(98% 0.01 250); 10 17 --color-secondary: oklch(76% 0.095 76.1); 11 18 --color-secondary-content: oklch(27.1% 0.026 76.4); 12 - --color-accent: oklch(66.6% 0.121 28); 13 - --color-accent-content: oklch(28% 0.121 28); 19 + --color-accent: oklch(56% 0.17 345); 20 + --color-accent-content: oklch(98% 0.008 345); 21 + --color-base-100: oklch(98% 0.01 225); 22 + --color-base-200: oklch(95% 0.02 225); 23 + --color-base-300: oklch(92% 0.03 225); 14 24 } 15 25 16 26 @keyframes rock {