Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee

feat: pwa and tailwind move to vite #10

closed opened by pdewey.com targeting main from refactor-svelte
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3mdercwj6xu22
+1367 -127
Diff #0
+2
BACKLOG.md
··· 58 58 - Profile page should show more details, and allow brew entries to take up more vertical space 59 59 60 60 - Show "view" button on brews in profile page (same as on brews list page) 61 + 62 + - Fix nix build, nix run, to build frontend as well
+30 -9
default.nix
··· 1 - { lib, buildGoModule, tailwindcss }: 1 + { lib, buildGoModule, buildNpmPackage }: 2 2 3 - buildGoModule rec { 3 + let 4 + frontend = buildNpmPackage { 5 + pname = "arabica-frontend"; 6 + version = "0.1.0"; 7 + src = ./frontend; 8 + npmDepsHash = "sha256-zCQiB+NV3iIxZtZ/hHKZ23FbLzBDJmmngBJ4s3QPhyk="; 9 + 10 + preBuild = '' 11 + mkdir -p ../static 12 + ''; 13 + 14 + buildPhase = '' 15 + npm run build 16 + ''; 17 + 18 + installPhase = '' 19 + mkdir -p $out 20 + cp -r ../static/app $out/ 21 + ''; 22 + }; 23 + in buildGoModule { 4 24 pname = "arabica"; 5 25 version = "0.1.0"; 6 26 src = ./.; 7 - vendorHash = "sha256-mrIFu5c2EuGvYHyjJVqC8WzlsmUJYCm/6yUpJ0IGPlA="; 8 - 9 - nativeBuildInputs = [ tailwindcss ]; 27 + vendorHash = "sha256-xgxoI2tmT4tVjgy+dv96ptI2YSU8T+Yq+rzApAiJ3yw="; 10 28 11 29 preBuild = '' 12 - tailwindcss -i web/static/css/style.css -o web/static/css/output.css --minify 30 + echo "Copying pre-built frontend..." 31 + mkdir -p static/app 32 + cp -r ${frontend}/app/* static/app/ || true 33 + cp -r ${frontend}/* static/app/ || true 34 + ls -la static/app/ || true 13 35 ''; 14 36 15 37 buildPhase = '' ··· 39 61 mkdir -p $out/bin 40 62 mkdir -p $out/share/arabica 41 63 42 - # Copy static files and templates 43 - cp -r web $out/share/arabica/ 44 - cp -r templates $out/share/arabica/ 64 + # Copy static files 65 + cp -r static $out/share/arabica/ 45 66 cp arabica $out/bin/arabica-unwrapped 46 67 cat > $out/bin/arabica <<'WRAPPER' 47 68 ${wrapperScript}
+1 -8
flake.nix
··· 8 8 (system: function nixpkgs.legacyPackages.${system} system); 9 9 in { 10 10 devShells = forAllSystems (pkgs: system: { 11 - default = pkgs.mkShell { packages = with pkgs; [ go tailwindcss ]; }; 11 + default = pkgs.mkShell { packages = with pkgs; [ go nodejs ]; }; 12 12 }); 13 13 14 14 packages = forAllSystems (pkgs: system: rec { ··· 21 21 type = "app"; 22 22 program = "${self.packages.${system}.arabica}/bin/arabica"; 23 23 }; 24 - tailwind = { 25 - type = "app"; 26 - program = toString (pkgs.writeShellScript "tailwind-build" '' 27 - cd ${./.} 28 - ${pkgs.tailwindcss}/bin/tailwindcss -i web/static/css/style.css -o web/static/css/output.css --minify 29 - ''); 30 - }; 31 24 }); 32 25 33 26 nixosModules.default = import ./module.nix;
+14 -25
frontend/index.html
··· 2 2 <html lang="en"> 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> 6 + <meta name="mobile-web-app-capable" content="yes" /> 7 + <meta name="apple-mobile-web-app-capable" content="yes" /> 8 + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> 9 + <meta name="apple-mobile-web-app-title" content="Arabica" /> 10 + <meta name="theme-color" content="#78350f" /> 11 + <meta name="color-scheme" content="light" /> 12 + 6 13 <title>Arabica - Coffee Brew Tracker</title> 7 14 <meta 8 15 name="description" 9 16 content="Track your coffee brewing journey with detailed logs stored in your Personal Data Server" 10 17 /> 11 18 12 - <!-- Tailwind CSS --> 13 - <link rel="stylesheet" href="/static/css/output.css?v=0.1.4" /> 14 - 15 - <!-- Favicon --> 16 - <link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg" /> 17 - <link 18 - rel="icon" 19 - type="image/png" 20 - sizes="32x32" 21 - href="/static/images/favicon-32x32.png" 22 - /> 23 - <link 24 - rel="icon" 25 - type="image/png" 26 - sizes="16x16" 27 - href="/static/images/favicon-16x16.png" 28 - /> 29 - <link 30 - rel="apple-touch-icon" 31 - sizes="180x180" 32 - href="/static/images/apple-touch-icon.png" 33 - /> 19 + <!-- Favicon & Icons --> 20 + <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" /> 21 + <link rel="apple-touch-icon" href="/static/icon-192.svg" /> 34 22 35 - <!-- Web Manifest --> 23 + <!-- Web Manifest for PWA --> 36 24 <link rel="manifest" href="/static/manifest.json" /> 37 - <meta name="theme-color" content="#78350f" /> 38 25 </head> 39 26 <body class="bg-brown-50 text-brown-900 min-h-screen"> 40 27 <div id="app"></div> 41 28 <script type="module" src="/src/main.js"></script> 29 + <!-- Service Worker Registration (external script for CSP compliance) --> 30 + <script defer src="/static/register-sw.js"></script> 42 31 </body> 43 32 </html>
+888
frontend/package-lock.json
··· 12 12 }, 13 13 "devDependencies": { 14 14 "@sveltejs/vite-plugin-svelte": "^3.0.0", 15 + "postcss": "^8.4.0", 15 16 "svelte": "^4.2.0", 17 + "tailwindcss": "^3.4.0", 16 18 "vite": "^5.0.0" 17 19 } 18 20 }, 21 + "node_modules/@alloc/quick-lru": { 22 + "version": "5.2.0", 23 + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 24 + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 25 + "dev": true, 26 + "license": "MIT", 27 + "engines": { 28 + "node": ">=10" 29 + }, 30 + "funding": { 31 + "url": "https://github.com/sponsors/sindresorhus" 32 + } 33 + }, 19 34 "node_modules/@ampproject/remapping": { 20 35 "version": "2.3.0", 21 36 "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", ··· 460 475 "@jridgewell/sourcemap-codec": "^1.4.14" 461 476 } 462 477 }, 478 + "node_modules/@nodelib/fs.scandir": { 479 + "version": "2.1.5", 480 + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 481 + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 482 + "dev": true, 483 + "license": "MIT", 484 + "dependencies": { 485 + "@nodelib/fs.stat": "2.0.5", 486 + "run-parallel": "^1.1.9" 487 + }, 488 + "engines": { 489 + "node": ">= 8" 490 + } 491 + }, 492 + "node_modules/@nodelib/fs.stat": { 493 + "version": "2.0.5", 494 + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 495 + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 496 + "dev": true, 497 + "license": "MIT", 498 + "engines": { 499 + "node": ">= 8" 500 + } 501 + }, 502 + "node_modules/@nodelib/fs.walk": { 503 + "version": "1.2.8", 504 + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 505 + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 506 + "dev": true, 507 + "license": "MIT", 508 + "dependencies": { 509 + "@nodelib/fs.scandir": "2.1.5", 510 + "fastq": "^1.6.0" 511 + }, 512 + "engines": { 513 + "node": ">= 8" 514 + } 515 + }, 463 516 "node_modules/@rollup/rollup-android-arm-eabi": { 464 517 "version": "4.56.0", 465 518 "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", ··· 872 925 "node": ">=0.4.0" 873 926 } 874 927 }, 928 + "node_modules/any-promise": { 929 + "version": "1.3.0", 930 + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 931 + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", 932 + "dev": true, 933 + "license": "MIT" 934 + }, 935 + "node_modules/anymatch": { 936 + "version": "3.1.3", 937 + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 938 + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 939 + "dev": true, 940 + "license": "ISC", 941 + "dependencies": { 942 + "normalize-path": "^3.0.0", 943 + "picomatch": "^2.0.4" 944 + }, 945 + "engines": { 946 + "node": ">= 8" 947 + } 948 + }, 949 + "node_modules/arg": { 950 + "version": "5.0.2", 951 + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", 952 + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", 953 + "dev": true, 954 + "license": "MIT" 955 + }, 875 956 "node_modules/aria-query": { 876 957 "version": "5.3.2", 877 958 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", ··· 892 973 "node": ">= 0.4" 893 974 } 894 975 }, 976 + "node_modules/binary-extensions": { 977 + "version": "2.3.0", 978 + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", 979 + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", 980 + "dev": true, 981 + "license": "MIT", 982 + "engines": { 983 + "node": ">=8" 984 + }, 985 + "funding": { 986 + "url": "https://github.com/sponsors/sindresorhus" 987 + } 988 + }, 989 + "node_modules/braces": { 990 + "version": "3.0.3", 991 + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 992 + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 993 + "dev": true, 994 + "license": "MIT", 995 + "dependencies": { 996 + "fill-range": "^7.1.1" 997 + }, 998 + "engines": { 999 + "node": ">=8" 1000 + } 1001 + }, 1002 + "node_modules/camelcase-css": { 1003 + "version": "2.0.1", 1004 + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", 1005 + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", 1006 + "dev": true, 1007 + "license": "MIT", 1008 + "engines": { 1009 + "node": ">= 6" 1010 + } 1011 + }, 1012 + "node_modules/chokidar": { 1013 + "version": "3.6.0", 1014 + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", 1015 + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", 1016 + "dev": true, 1017 + "license": "MIT", 1018 + "dependencies": { 1019 + "anymatch": "~3.1.2", 1020 + "braces": "~3.0.2", 1021 + "glob-parent": "~5.1.2", 1022 + "is-binary-path": "~2.1.0", 1023 + "is-glob": "~4.0.1", 1024 + "normalize-path": "~3.0.0", 1025 + "readdirp": "~3.6.0" 1026 + }, 1027 + "engines": { 1028 + "node": ">= 8.10.0" 1029 + }, 1030 + "funding": { 1031 + "url": "https://paulmillr.com/funding/" 1032 + }, 1033 + "optionalDependencies": { 1034 + "fsevents": "~2.3.2" 1035 + } 1036 + }, 1037 + "node_modules/chokidar/node_modules/glob-parent": { 1038 + "version": "5.1.2", 1039 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1040 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1041 + "dev": true, 1042 + "license": "ISC", 1043 + "dependencies": { 1044 + "is-glob": "^4.0.1" 1045 + }, 1046 + "engines": { 1047 + "node": ">= 6" 1048 + } 1049 + }, 895 1050 "node_modules/code-red": { 896 1051 "version": "1.0.4", 897 1052 "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", ··· 906 1061 "periscopic": "^3.1.0" 907 1062 } 908 1063 }, 1064 + "node_modules/commander": { 1065 + "version": "4.1.1", 1066 + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", 1067 + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", 1068 + "dev": true, 1069 + "license": "MIT", 1070 + "engines": { 1071 + "node": ">= 6" 1072 + } 1073 + }, 909 1074 "node_modules/css-tree": { 910 1075 "version": "2.3.1", 911 1076 "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", ··· 920 1085 "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" 921 1086 } 922 1087 }, 1088 + "node_modules/cssesc": { 1089 + "version": "3.0.0", 1090 + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", 1091 + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", 1092 + "dev": true, 1093 + "license": "MIT", 1094 + "bin": { 1095 + "cssesc": "bin/cssesc" 1096 + }, 1097 + "engines": { 1098 + "node": ">=4" 1099 + } 1100 + }, 923 1101 "node_modules/debug": { 924 1102 "version": "4.4.3", 925 1103 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", ··· 948 1126 "node": ">=0.10.0" 949 1127 } 950 1128 }, 1129 + "node_modules/didyoumean": { 1130 + "version": "1.2.2", 1131 + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", 1132 + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", 1133 + "dev": true, 1134 + "license": "Apache-2.0" 1135 + }, 1136 + "node_modules/dlv": { 1137 + "version": "1.1.3", 1138 + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", 1139 + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", 1140 + "dev": true, 1141 + "license": "MIT" 1142 + }, 951 1143 "node_modules/esbuild": { 952 1144 "version": "0.21.5", 953 1145 "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", ··· 997 1189 "@types/estree": "^1.0.0" 998 1190 } 999 1191 }, 1192 + "node_modules/fast-glob": { 1193 + "version": "3.3.3", 1194 + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", 1195 + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", 1196 + "dev": true, 1197 + "license": "MIT", 1198 + "dependencies": { 1199 + "@nodelib/fs.stat": "^2.0.2", 1200 + "@nodelib/fs.walk": "^1.2.3", 1201 + "glob-parent": "^5.1.2", 1202 + "merge2": "^1.3.0", 1203 + "micromatch": "^4.0.8" 1204 + }, 1205 + "engines": { 1206 + "node": ">=8.6.0" 1207 + } 1208 + }, 1209 + "node_modules/fast-glob/node_modules/glob-parent": { 1210 + "version": "5.1.2", 1211 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1212 + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1213 + "dev": true, 1214 + "license": "ISC", 1215 + "dependencies": { 1216 + "is-glob": "^4.0.1" 1217 + }, 1218 + "engines": { 1219 + "node": ">= 6" 1220 + } 1221 + }, 1222 + "node_modules/fastq": { 1223 + "version": "1.20.1", 1224 + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", 1225 + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", 1226 + "dev": true, 1227 + "license": "ISC", 1228 + "dependencies": { 1229 + "reusify": "^1.0.4" 1230 + } 1231 + }, 1232 + "node_modules/fill-range": { 1233 + "version": "7.1.1", 1234 + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 1235 + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 1236 + "dev": true, 1237 + "license": "MIT", 1238 + "dependencies": { 1239 + "to-regex-range": "^5.0.1" 1240 + }, 1241 + "engines": { 1242 + "node": ">=8" 1243 + } 1244 + }, 1000 1245 "node_modules/fsevents": { 1001 1246 "version": "2.3.3", 1002 1247 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 1012 1257 "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1013 1258 } 1014 1259 }, 1260 + "node_modules/function-bind": { 1261 + "version": "1.1.2", 1262 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 1263 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 1264 + "dev": true, 1265 + "license": "MIT", 1266 + "funding": { 1267 + "url": "https://github.com/sponsors/ljharb" 1268 + } 1269 + }, 1270 + "node_modules/glob-parent": { 1271 + "version": "6.0.2", 1272 + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 1273 + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 1274 + "dev": true, 1275 + "license": "ISC", 1276 + "dependencies": { 1277 + "is-glob": "^4.0.3" 1278 + }, 1279 + "engines": { 1280 + "node": ">=10.13.0" 1281 + } 1282 + }, 1283 + "node_modules/hasown": { 1284 + "version": "2.0.2", 1285 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1286 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1287 + "dev": true, 1288 + "license": "MIT", 1289 + "dependencies": { 1290 + "function-bind": "^1.1.2" 1291 + }, 1292 + "engines": { 1293 + "node": ">= 0.4" 1294 + } 1295 + }, 1296 + "node_modules/is-binary-path": { 1297 + "version": "2.1.0", 1298 + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1299 + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1300 + "dev": true, 1301 + "license": "MIT", 1302 + "dependencies": { 1303 + "binary-extensions": "^2.0.0" 1304 + }, 1305 + "engines": { 1306 + "node": ">=8" 1307 + } 1308 + }, 1309 + "node_modules/is-core-module": { 1310 + "version": "2.16.1", 1311 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", 1312 + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", 1313 + "dev": true, 1314 + "license": "MIT", 1315 + "dependencies": { 1316 + "hasown": "^2.0.2" 1317 + }, 1318 + "engines": { 1319 + "node": ">= 0.4" 1320 + }, 1321 + "funding": { 1322 + "url": "https://github.com/sponsors/ljharb" 1323 + } 1324 + }, 1325 + "node_modules/is-extglob": { 1326 + "version": "2.1.1", 1327 + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1328 + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1329 + "dev": true, 1330 + "license": "MIT", 1331 + "engines": { 1332 + "node": ">=0.10.0" 1333 + } 1334 + }, 1335 + "node_modules/is-glob": { 1336 + "version": "4.0.3", 1337 + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1338 + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1339 + "dev": true, 1340 + "license": "MIT", 1341 + "dependencies": { 1342 + "is-extglob": "^2.1.1" 1343 + }, 1344 + "engines": { 1345 + "node": ">=0.10.0" 1346 + } 1347 + }, 1348 + "node_modules/is-number": { 1349 + "version": "7.0.0", 1350 + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1351 + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1352 + "dev": true, 1353 + "license": "MIT", 1354 + "engines": { 1355 + "node": ">=0.12.0" 1356 + } 1357 + }, 1015 1358 "node_modules/is-reference": { 1016 1359 "version": "3.0.3", 1017 1360 "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", ··· 1022 1365 "@types/estree": "^1.0.6" 1023 1366 } 1024 1367 }, 1368 + "node_modules/jiti": { 1369 + "version": "1.21.7", 1370 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", 1371 + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", 1372 + "dev": true, 1373 + "license": "MIT", 1374 + "peer": true, 1375 + "bin": { 1376 + "jiti": "bin/jiti.js" 1377 + } 1378 + }, 1025 1379 "node_modules/kleur": { 1026 1380 "version": "4.1.5", 1027 1381 "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", ··· 1032 1386 "node": ">=6" 1033 1387 } 1034 1388 }, 1389 + "node_modules/lilconfig": { 1390 + "version": "3.1.3", 1391 + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", 1392 + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", 1393 + "dev": true, 1394 + "license": "MIT", 1395 + "engines": { 1396 + "node": ">=14" 1397 + }, 1398 + "funding": { 1399 + "url": "https://github.com/sponsors/antonk52" 1400 + } 1401 + }, 1402 + "node_modules/lines-and-columns": { 1403 + "version": "1.2.4", 1404 + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", 1405 + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", 1406 + "dev": true, 1407 + "license": "MIT" 1408 + }, 1035 1409 "node_modules/locate-character": { 1036 1410 "version": "3.0.0", 1037 1411 "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", ··· 1056 1430 "dev": true, 1057 1431 "license": "CC0-1.0" 1058 1432 }, 1433 + "node_modules/merge2": { 1434 + "version": "1.4.1", 1435 + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", 1436 + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", 1437 + "dev": true, 1438 + "license": "MIT", 1439 + "engines": { 1440 + "node": ">= 8" 1441 + } 1442 + }, 1443 + "node_modules/micromatch": { 1444 + "version": "4.0.8", 1445 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 1446 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 1447 + "dev": true, 1448 + "license": "MIT", 1449 + "dependencies": { 1450 + "braces": "^3.0.3", 1451 + "picomatch": "^2.3.1" 1452 + }, 1453 + "engines": { 1454 + "node": ">=8.6" 1455 + } 1456 + }, 1059 1457 "node_modules/ms": { 1060 1458 "version": "2.1.3", 1061 1459 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", ··· 1063 1461 "dev": true, 1064 1462 "license": "MIT" 1065 1463 }, 1464 + "node_modules/mz": { 1465 + "version": "2.7.0", 1466 + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 1467 + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 1468 + "dev": true, 1469 + "license": "MIT", 1470 + "dependencies": { 1471 + "any-promise": "^1.0.0", 1472 + "object-assign": "^4.0.1", 1473 + "thenify-all": "^1.0.0" 1474 + } 1475 + }, 1066 1476 "node_modules/nanoid": { 1067 1477 "version": "3.3.11", 1068 1478 "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", ··· 1094 1504 "node": ">= 6" 1095 1505 } 1096 1506 }, 1507 + "node_modules/normalize-path": { 1508 + "version": "3.0.0", 1509 + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1510 + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1511 + "dev": true, 1512 + "license": "MIT", 1513 + "engines": { 1514 + "node": ">=0.10.0" 1515 + } 1516 + }, 1517 + "node_modules/object-assign": { 1518 + "version": "4.1.1", 1519 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1520 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1521 + "dev": true, 1522 + "license": "MIT", 1523 + "engines": { 1524 + "node": ">=0.10.0" 1525 + } 1526 + }, 1527 + "node_modules/object-hash": { 1528 + "version": "3.0.0", 1529 + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", 1530 + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", 1531 + "dev": true, 1532 + "license": "MIT", 1533 + "engines": { 1534 + "node": ">= 6" 1535 + } 1536 + }, 1537 + "node_modules/path-parse": { 1538 + "version": "1.0.7", 1539 + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 1540 + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 1541 + "dev": true, 1542 + "license": "MIT" 1543 + }, 1097 1544 "node_modules/periscopic": { 1098 1545 "version": "3.1.0", 1099 1546 "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", ··· 1113 1560 "dev": true, 1114 1561 "license": "ISC" 1115 1562 }, 1563 + "node_modules/picomatch": { 1564 + "version": "2.3.1", 1565 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1566 + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1567 + "dev": true, 1568 + "license": "MIT", 1569 + "engines": { 1570 + "node": ">=8.6" 1571 + }, 1572 + "funding": { 1573 + "url": "https://github.com/sponsors/jonschlinkert" 1574 + } 1575 + }, 1576 + "node_modules/pify": { 1577 + "version": "2.3.0", 1578 + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1579 + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 1580 + "dev": true, 1581 + "license": "MIT", 1582 + "engines": { 1583 + "node": ">=0.10.0" 1584 + } 1585 + }, 1586 + "node_modules/pirates": { 1587 + "version": "4.0.7", 1588 + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", 1589 + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", 1590 + "dev": true, 1591 + "license": "MIT", 1592 + "engines": { 1593 + "node": ">= 6" 1594 + } 1595 + }, 1116 1596 "node_modules/postcss": { 1117 1597 "version": "8.5.6", 1118 1598 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", ··· 1133 1613 } 1134 1614 ], 1135 1615 "license": "MIT", 1616 + "peer": true, 1136 1617 "dependencies": { 1137 1618 "nanoid": "^3.3.11", 1138 1619 "picocolors": "^1.1.1", ··· 1142 1623 "node": "^10 || ^12 || >=14" 1143 1624 } 1144 1625 }, 1626 + "node_modules/postcss-import": { 1627 + "version": "15.1.0", 1628 + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", 1629 + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", 1630 + "dev": true, 1631 + "license": "MIT", 1632 + "dependencies": { 1633 + "postcss-value-parser": "^4.0.0", 1634 + "read-cache": "^1.0.0", 1635 + "resolve": "^1.1.7" 1636 + }, 1637 + "engines": { 1638 + "node": ">=14.0.0" 1639 + }, 1640 + "peerDependencies": { 1641 + "postcss": "^8.0.0" 1642 + } 1643 + }, 1644 + "node_modules/postcss-js": { 1645 + "version": "4.1.0", 1646 + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", 1647 + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", 1648 + "dev": true, 1649 + "funding": [ 1650 + { 1651 + "type": "opencollective", 1652 + "url": "https://opencollective.com/postcss/" 1653 + }, 1654 + { 1655 + "type": "github", 1656 + "url": "https://github.com/sponsors/ai" 1657 + } 1658 + ], 1659 + "license": "MIT", 1660 + "dependencies": { 1661 + "camelcase-css": "^2.0.1" 1662 + }, 1663 + "engines": { 1664 + "node": "^12 || ^14 || >= 16" 1665 + }, 1666 + "peerDependencies": { 1667 + "postcss": "^8.4.21" 1668 + } 1669 + }, 1670 + "node_modules/postcss-load-config": { 1671 + "version": "6.0.1", 1672 + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", 1673 + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", 1674 + "dev": true, 1675 + "funding": [ 1676 + { 1677 + "type": "opencollective", 1678 + "url": "https://opencollective.com/postcss/" 1679 + }, 1680 + { 1681 + "type": "github", 1682 + "url": "https://github.com/sponsors/ai" 1683 + } 1684 + ], 1685 + "license": "MIT", 1686 + "dependencies": { 1687 + "lilconfig": "^3.1.1" 1688 + }, 1689 + "engines": { 1690 + "node": ">= 18" 1691 + }, 1692 + "peerDependencies": { 1693 + "jiti": ">=1.21.0", 1694 + "postcss": ">=8.0.9", 1695 + "tsx": "^4.8.1", 1696 + "yaml": "^2.4.2" 1697 + }, 1698 + "peerDependenciesMeta": { 1699 + "jiti": { 1700 + "optional": true 1701 + }, 1702 + "postcss": { 1703 + "optional": true 1704 + }, 1705 + "tsx": { 1706 + "optional": true 1707 + }, 1708 + "yaml": { 1709 + "optional": true 1710 + } 1711 + } 1712 + }, 1713 + "node_modules/postcss-nested": { 1714 + "version": "6.2.0", 1715 + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", 1716 + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", 1717 + "dev": true, 1718 + "funding": [ 1719 + { 1720 + "type": "opencollective", 1721 + "url": "https://opencollective.com/postcss/" 1722 + }, 1723 + { 1724 + "type": "github", 1725 + "url": "https://github.com/sponsors/ai" 1726 + } 1727 + ], 1728 + "license": "MIT", 1729 + "dependencies": { 1730 + "postcss-selector-parser": "^6.1.1" 1731 + }, 1732 + "engines": { 1733 + "node": ">=12.0" 1734 + }, 1735 + "peerDependencies": { 1736 + "postcss": "^8.2.14" 1737 + } 1738 + }, 1739 + "node_modules/postcss-selector-parser": { 1740 + "version": "6.1.2", 1741 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", 1742 + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", 1743 + "dev": true, 1744 + "license": "MIT", 1745 + "dependencies": { 1746 + "cssesc": "^3.0.0", 1747 + "util-deprecate": "^1.0.2" 1748 + }, 1749 + "engines": { 1750 + "node": ">=4" 1751 + } 1752 + }, 1753 + "node_modules/postcss-value-parser": { 1754 + "version": "4.2.0", 1755 + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", 1756 + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", 1757 + "dev": true, 1758 + "license": "MIT" 1759 + }, 1760 + "node_modules/queue-microtask": { 1761 + "version": "1.2.3", 1762 + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1763 + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1764 + "dev": true, 1765 + "funding": [ 1766 + { 1767 + "type": "github", 1768 + "url": "https://github.com/sponsors/feross" 1769 + }, 1770 + { 1771 + "type": "patreon", 1772 + "url": "https://www.patreon.com/feross" 1773 + }, 1774 + { 1775 + "type": "consulting", 1776 + "url": "https://feross.org/support" 1777 + } 1778 + ], 1779 + "license": "MIT" 1780 + }, 1781 + "node_modules/read-cache": { 1782 + "version": "1.0.0", 1783 + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", 1784 + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", 1785 + "dev": true, 1786 + "license": "MIT", 1787 + "dependencies": { 1788 + "pify": "^2.3.0" 1789 + } 1790 + }, 1791 + "node_modules/readdirp": { 1792 + "version": "3.6.0", 1793 + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1794 + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1795 + "dev": true, 1796 + "license": "MIT", 1797 + "dependencies": { 1798 + "picomatch": "^2.2.1" 1799 + }, 1800 + "engines": { 1801 + "node": ">=8.10.0" 1802 + } 1803 + }, 1145 1804 "node_modules/regexparam": { 1146 1805 "version": "1.3.0", 1147 1806 "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz", ··· 1151 1810 "node": ">=6" 1152 1811 } 1153 1812 }, 1813 + "node_modules/resolve": { 1814 + "version": "1.22.11", 1815 + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", 1816 + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", 1817 + "dev": true, 1818 + "license": "MIT", 1819 + "dependencies": { 1820 + "is-core-module": "^2.16.1", 1821 + "path-parse": "^1.0.7", 1822 + "supports-preserve-symlinks-flag": "^1.0.0" 1823 + }, 1824 + "bin": { 1825 + "resolve": "bin/resolve" 1826 + }, 1827 + "engines": { 1828 + "node": ">= 0.4" 1829 + }, 1830 + "funding": { 1831 + "url": "https://github.com/sponsors/ljharb" 1832 + } 1833 + }, 1834 + "node_modules/reusify": { 1835 + "version": "1.1.0", 1836 + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", 1837 + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", 1838 + "dev": true, 1839 + "license": "MIT", 1840 + "engines": { 1841 + "iojs": ">=1.0.0", 1842 + "node": ">=0.10.0" 1843 + } 1844 + }, 1154 1845 "node_modules/rollup": { 1155 1846 "version": "4.56.0", 1156 1847 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", ··· 1196 1887 "fsevents": "~2.3.2" 1197 1888 } 1198 1889 }, 1890 + "node_modules/run-parallel": { 1891 + "version": "1.2.0", 1892 + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1893 + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1894 + "dev": true, 1895 + "funding": [ 1896 + { 1897 + "type": "github", 1898 + "url": "https://github.com/sponsors/feross" 1899 + }, 1900 + { 1901 + "type": "patreon", 1902 + "url": "https://www.patreon.com/feross" 1903 + }, 1904 + { 1905 + "type": "consulting", 1906 + "url": "https://feross.org/support" 1907 + } 1908 + ], 1909 + "license": "MIT", 1910 + "dependencies": { 1911 + "queue-microtask": "^1.2.2" 1912 + } 1913 + }, 1199 1914 "node_modules/source-map-js": { 1200 1915 "version": "1.2.1", 1201 1916 "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", ··· 1206 1921 "node": ">=0.10.0" 1207 1922 } 1208 1923 }, 1924 + "node_modules/sucrase": { 1925 + "version": "3.35.1", 1926 + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", 1927 + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", 1928 + "dev": true, 1929 + "license": "MIT", 1930 + "dependencies": { 1931 + "@jridgewell/gen-mapping": "^0.3.2", 1932 + "commander": "^4.0.0", 1933 + "lines-and-columns": "^1.1.6", 1934 + "mz": "^2.7.0", 1935 + "pirates": "^4.0.1", 1936 + "tinyglobby": "^0.2.11", 1937 + "ts-interface-checker": "^0.1.9" 1938 + }, 1939 + "bin": { 1940 + "sucrase": "bin/sucrase", 1941 + "sucrase-node": "bin/sucrase-node" 1942 + }, 1943 + "engines": { 1944 + "node": ">=16 || 14 >=14.17" 1945 + } 1946 + }, 1947 + "node_modules/supports-preserve-symlinks-flag": { 1948 + "version": "1.0.0", 1949 + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1950 + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1951 + "dev": true, 1952 + "license": "MIT", 1953 + "engines": { 1954 + "node": ">= 0.4" 1955 + }, 1956 + "funding": { 1957 + "url": "https://github.com/sponsors/ljharb" 1958 + } 1959 + }, 1209 1960 "node_modules/svelte": { 1210 1961 "version": "4.2.20", 1211 1962 "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz", ··· 1246 1997 "svelte": "^3.19.0 || ^4.0.0" 1247 1998 } 1248 1999 }, 2000 + "node_modules/tailwindcss": { 2001 + "version": "3.4.19", 2002 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", 2003 + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", 2004 + "dev": true, 2005 + "license": "MIT", 2006 + "dependencies": { 2007 + "@alloc/quick-lru": "^5.2.0", 2008 + "arg": "^5.0.2", 2009 + "chokidar": "^3.6.0", 2010 + "didyoumean": "^1.2.2", 2011 + "dlv": "^1.1.3", 2012 + "fast-glob": "^3.3.2", 2013 + "glob-parent": "^6.0.2", 2014 + "is-glob": "^4.0.3", 2015 + "jiti": "^1.21.7", 2016 + "lilconfig": "^3.1.3", 2017 + "micromatch": "^4.0.8", 2018 + "normalize-path": "^3.0.0", 2019 + "object-hash": "^3.0.0", 2020 + "picocolors": "^1.1.1", 2021 + "postcss": "^8.4.47", 2022 + "postcss-import": "^15.1.0", 2023 + "postcss-js": "^4.0.1", 2024 + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", 2025 + "postcss-nested": "^6.2.0", 2026 + "postcss-selector-parser": "^6.1.2", 2027 + "resolve": "^1.22.8", 2028 + "sucrase": "^3.35.0" 2029 + }, 2030 + "bin": { 2031 + "tailwind": "lib/cli.js", 2032 + "tailwindcss": "lib/cli.js" 2033 + }, 2034 + "engines": { 2035 + "node": ">=14.0.0" 2036 + } 2037 + }, 2038 + "node_modules/thenify": { 2039 + "version": "3.3.1", 2040 + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 2041 + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 2042 + "dev": true, 2043 + "license": "MIT", 2044 + "dependencies": { 2045 + "any-promise": "^1.0.0" 2046 + } 2047 + }, 2048 + "node_modules/thenify-all": { 2049 + "version": "1.6.0", 2050 + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 2051 + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 2052 + "dev": true, 2053 + "license": "MIT", 2054 + "dependencies": { 2055 + "thenify": ">= 3.1.0 < 4" 2056 + }, 2057 + "engines": { 2058 + "node": ">=0.8" 2059 + } 2060 + }, 2061 + "node_modules/tinyglobby": { 2062 + "version": "0.2.15", 2063 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", 2064 + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 2065 + "dev": true, 2066 + "license": "MIT", 2067 + "dependencies": { 2068 + "fdir": "^6.5.0", 2069 + "picomatch": "^4.0.3" 2070 + }, 2071 + "engines": { 2072 + "node": ">=12.0.0" 2073 + }, 2074 + "funding": { 2075 + "url": "https://github.com/sponsors/SuperchupuDev" 2076 + } 2077 + }, 2078 + "node_modules/tinyglobby/node_modules/fdir": { 2079 + "version": "6.5.0", 2080 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", 2081 + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", 2082 + "dev": true, 2083 + "license": "MIT", 2084 + "engines": { 2085 + "node": ">=12.0.0" 2086 + }, 2087 + "peerDependencies": { 2088 + "picomatch": "^3 || ^4" 2089 + }, 2090 + "peerDependenciesMeta": { 2091 + "picomatch": { 2092 + "optional": true 2093 + } 2094 + } 2095 + }, 2096 + "node_modules/tinyglobby/node_modules/picomatch": { 2097 + "version": "4.0.3", 2098 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", 2099 + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", 2100 + "dev": true, 2101 + "license": "MIT", 2102 + "peer": true, 2103 + "engines": { 2104 + "node": ">=12" 2105 + }, 2106 + "funding": { 2107 + "url": "https://github.com/sponsors/jonschlinkert" 2108 + } 2109 + }, 2110 + "node_modules/to-regex-range": { 2111 + "version": "5.0.1", 2112 + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 2113 + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 2114 + "dev": true, 2115 + "license": "MIT", 2116 + "dependencies": { 2117 + "is-number": "^7.0.0" 2118 + }, 2119 + "engines": { 2120 + "node": ">=8.0" 2121 + } 2122 + }, 2123 + "node_modules/ts-interface-checker": { 2124 + "version": "0.1.13", 2125 + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", 2126 + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", 2127 + "dev": true, 2128 + "license": "Apache-2.0" 2129 + }, 2130 + "node_modules/util-deprecate": { 2131 + "version": "1.0.2", 2132 + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2133 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 2134 + "dev": true, 2135 + "license": "MIT" 2136 + }, 1249 2137 "node_modules/vite": { 1250 2138 "version": "5.4.21", 1251 2139 "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+2
frontend/package.json
··· 9 9 }, 10 10 "devDependencies": { 11 11 "@sveltejs/vite-plugin-svelte": "^3.0.0", 12 + "postcss": "^8.4.0", 12 13 "svelte": "^4.2.0", 14 + "tailwindcss": "^3.4.0", 13 15 "vite": "^5.0.0" 14 16 }, 15 17 "dependencies": {
+5
frontend/postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + tailwindcss: {}, 4 + }, 5 + };
+5
frontend/src/App.svelte
··· 17 17 18 18 import Header from "./components/Header.svelte"; 19 19 import Footer from "./components/Footer.svelte"; 20 + import OfflineIndicator from "./components/OfflineIndicator.svelte"; 21 + import UpdateNotification from "./components/UpdateNotification.svelte"; 20 22 21 23 let currentRoute = null; 22 24 let params = {}; ··· 83 85 </script> 84 86 85 87 <div class="flex flex-col min-h-screen"> 88 + <OfflineIndicator /> 89 + <UpdateNotification /> 90 + 86 91 <Header /> 87 92 88 93 <main class="flex-1 container mx-auto px-3 md:px-4 py-4 md:py-8">
+36
frontend/src/components/OfflineIndicator.svelte
··· 1 + <script> 2 + import { isOnline } from "../stores/pwa.js"; 3 + </script> 4 + 5 + {#if !$isOnline} 6 + <div 7 + class="fixed top-0 left-0 right-0 bg-red-500 text-white px-4 py-2 text-center text-sm font-medium z-50 animate-pulse" 8 + > 9 + <div class="flex items-center justify-center gap-2"> 10 + <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> 11 + <path 12 + fill-rule="evenodd" 13 + d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" 14 + clip-rule="evenodd" 15 + /> 16 + </svg> 17 + <span>You're offline. Some features may be limited.</span> 18 + </div> 19 + </div> 20 + {/if} 21 + 22 + <style> 23 + @keyframes animate-pulse { 24 + 0%, 25 + 100% { 26 + opacity: 1; 27 + } 28 + 50% { 29 + opacity: 0.5; 30 + } 31 + } 32 + 33 + :global(.animate-pulse) { 34 + animation: animate-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 35 + } 36 + </style>
+60
frontend/src/components/UpdateNotification.svelte
··· 1 + <script> 2 + import { updateAvailable, updateServiceWorker } from '../stores/pwa.js'; 3 + 4 + let showNotification = false; 5 + 6 + $: if ($updateAvailable) { 7 + showNotification = true; 8 + } 9 + 10 + function handleUpdate() { 11 + showNotification = false; 12 + updateServiceWorker(); 13 + } 14 + 15 + function handleDismiss() { 16 + showNotification = false; 17 + } 18 + </script> 19 + 20 + {#if showNotification} 21 + <div class="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:max-w-md bg-amber-100 border border-amber-400 rounded-lg shadow-lg p-4"> 22 + <div class="flex items-start gap-3"> 23 + <div class="flex-shrink-0"> 24 + <svg class="h-5 w-5 text-amber-600" fill="currentColor" viewBox="0 0 20 20"> 25 + <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> 26 + </svg> 27 + </div> 28 + <div class="flex-1"> 29 + <h3 class="text-sm font-medium text-amber-900">Update Available</h3> 30 + <p class="mt-1 text-sm text-amber-800"> 31 + A new version of Arabica is available. 32 + </p> 33 + </div> 34 + <div class="flex gap-2 flex-shrink-0"> 35 + <button 36 + on:click={handleUpdate} 37 + class="inline-flex items-center px-2.5 py-1.5 rounded text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 transition-colors" 38 + > 39 + Update 40 + </button> 41 + <button 42 + on:click={handleDismiss} 43 + class="inline-flex items-center px-2.5 py-1.5 rounded text-sm font-medium text-amber-900 bg-transparent hover:bg-amber-200 transition-colors" 44 + > 45 + Dismiss 46 + </button> 47 + </div> 48 + </div> 49 + </div> 50 + {/if} 51 + 52 + <style> 53 + /* Mobile-friendly positioning */ 54 + @media (max-width: 640px) { 55 + div { 56 + margin: 0 1rem; 57 + max-width: calc(100vw - 2rem); 58 + } 59 + } 60 + </style>
+42
frontend/src/main.js
··· 1 + import './styles.css'; 1 2 import App from './App.svelte'; 2 3 4 + // Register service worker for PWA functionality 5 + // Note: Service workers require a secure context (HTTPS) or localhost/127.0.0.1 for development 6 + if ('serviceWorker' in navigator) { 7 + // Wait for page load before registering 8 + window.addEventListener('load', () => { 9 + navigator.serviceWorker 10 + .register('/static/service-worker.js', { scope: '/' }) 11 + .then((registration) => { 12 + console.log('[App] Service Worker registered:', registration); 13 + 14 + // Listen for updates 15 + registration.addEventListener('updatefound', () => { 16 + const newWorker = registration.installing; 17 + newWorker.addEventListener('statechange', () => { 18 + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 19 + // New service worker available - notify user 20 + console.log('[App] New service worker available'); 21 + // Dispatch event to show update notification 22 + window.dispatchEvent(new CustomEvent('sw-update-available')); 23 + } 24 + }); 25 + }); 26 + 27 + // Check for updates periodically (every 60 seconds) 28 + setInterval(() => { 29 + registration.update(); 30 + }, 60000); 31 + }) 32 + .catch((err) => { 33 + console.error('[App] Service Worker registration failed:', err); 34 + // Log additional context for debugging 35 + if (err instanceof DOMException && err.name === 'SecurityError') { 36 + console.warn('[App] Service Worker requires a secure context (HTTPS) or localhost'); 37 + console.warn('[App] For development, access via http://localhost:18910 instead of 127.0.0.1'); 38 + } 39 + }); 40 + }); 41 + } else { 42 + console.warn('[App] Service Workers not supported in this browser'); 43 + } 44 + 3 45 const app = new App({ 4 46 target: document.getElementById('app'), 5 47 });
+47
frontend/src/stores/pwa.js
··· 1 + import { writable } from 'svelte/store'; 2 + 3 + // Track online/offline status 4 + export const isOnline = writable( 5 + typeof navigator !== 'undefined' ? navigator.onLine : true 6 + ); 7 + 8 + // Track service worker update availability 9 + export const updateAvailable = writable(false); 10 + 11 + // Initialize online/offline detection 12 + if (typeof window !== 'undefined') { 13 + window.addEventListener('online', () => isOnline.set(true)); 14 + window.addEventListener('offline', () => isOnline.set(false)); 15 + 16 + // Listen for service worker updates 17 + window.addEventListener('sw-update-available', () => { 18 + console.log('[PWA] Update available'); 19 + updateAvailable.set(true); 20 + }); 21 + } 22 + 23 + // Trigger service worker update 24 + export function updateServiceWorker() { 25 + if ('serviceWorker' in navigator) { 26 + navigator.serviceWorker.getRegistration().then((registration) => { 27 + if (registration) { 28 + registration.unregister().then(() => { 29 + window.location.reload(); 30 + }); 31 + } 32 + }); 33 + } 34 + } 35 + 36 + // Request for notification permission (future use for push notifications) 37 + export function requestNotificationPermission() { 38 + if ('Notification' in window && 'serviceWorker' in navigator) { 39 + if (Notification.permission === 'granted') { 40 + return Promise.resolve(); 41 + } 42 + if (Notification.permission !== 'denied') { 43 + return Notification.requestPermission(); 44 + } 45 + } 46 + return Promise.reject('Notifications not supported'); 47 + }
static/css/style.css frontend/src/styles.css
+5 -2
tailwind.config.js frontend/tailwind.config.js
··· 1 1 /** @type {import('tailwindcss').Config} */ 2 - module.exports = { 3 - content: ["./static/**/*.{html,js}"], 2 + export default { 3 + content: [ 4 + "./src/**/*.{svelte,js,ts}", 5 + "./index.html", 6 + ], 4 7 theme: { 5 8 extend: { 6 9 colors: {
+7 -5
internal/middleware/security.go
··· 26 26 w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()") 27 27 28 28 // Content Security Policy 29 - // Allows: self for scripts/styles, inline styles (for Tailwind), jsdelivr for HTMX/Alpine 30 - // Note: unsafe-eval required for Alpine.js standard build (CSP build has CDN MIME type issues) 29 + // Allows: self for scripts/styles, inline styles (for Tailwind), external scripts from CDN 31 30 // Note: form-action allows https: for OAuth redirects to external authorization servers 31 + // Note: script-src includes https://cdn.jsdelivr.net for external libraries and /static/ for service worker registration 32 32 csp := strings.Join([]string{ 33 33 "default-src 'self'", 34 - "script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net", 35 - "style-src 'self' 'unsafe-inline'", // unsafe-inline needed for Tailwind 36 - "img-src 'self' https: data:", // Allow external images (avatars) and data URIs 34 + "script-src 'self' 'unsafe-eval' https://cdn.jsdelivr.net", // External scripts must be listed explicitly 35 + "script-src-elem 'self' https://cdn.jsdelivr.net", // External script tags (register-sw.js) 36 + "style-src 'self' 'unsafe-inline'", // unsafe-inline needed for Tailwind 37 + "img-src 'self' https: data:", // Allow external images (avatars) and data URIs 37 38 "font-src 'self'", 38 39 "connect-src 'self' https:", // Allow connections to external APIs (OAuth, PDS) 40 + "worker-src 'self'", // Service workers must be same-origin 39 41 "frame-ancestors 'none'", 40 42 "base-uri 'self'", 41 43 "form-action 'self' https:", // Allow form submissions to external OAuth servers
+2 -8
justfile
··· 1 1 run: 2 2 @LOG_LEVEL=debug LOG_FORMAT=console go run cmd/arabica-server/main.go -known-dids known-dids.txt 3 3 4 - run-production: 5 - @LOG_FORMAT=json SERVER_PUBLIC_URL=https://arabica.example.com go run cmd/arabica-server/main.go 4 + build-ui: 5 + @pushd frontend || exit 1 && npm run build && popd || exit 1 6 6 7 7 test: 8 8 @go test ./... -cover -coverprofile=cover.out 9 - 10 - style: 11 - @nix develop --command tailwindcss -i static/css/style.css -o static/css/output.css --minify 12 - 13 - build-ui: 14 - @pushd frontend || exit 1 && npm run build && popd || exit 1
+16 -27
static/app/index.html
··· 2 2 <html lang="en"> 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> 6 + <meta name="mobile-web-app-capable" content="yes" /> 7 + <meta name="apple-mobile-web-app-capable" content="yes" /> 8 + <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> 9 + <meta name="apple-mobile-web-app-title" content="Arabica" /> 10 + <meta name="theme-color" content="#78350f" /> 11 + <meta name="color-scheme" content="light" /> 12 + 6 13 <title>Arabica - Coffee Brew Tracker</title> 7 14 <meta 8 15 name="description" 9 16 content="Track your coffee brewing journey with detailed logs stored in your Personal Data Server" 10 17 /> 11 18 12 - <!-- Tailwind CSS --> 13 - <link rel="stylesheet" href="/static/css/output.css?v=0.1.4" /> 14 - 15 - <!-- Favicon --> 16 - <link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg" /> 17 - <link 18 - rel="icon" 19 - type="image/png" 20 - sizes="32x32" 21 - href="/static/images/favicon-32x32.png" 22 - /> 23 - <link 24 - rel="icon" 25 - type="image/png" 26 - sizes="16x16" 27 - href="/static/images/favicon-16x16.png" 28 - /> 29 - <link 30 - rel="apple-touch-icon" 31 - sizes="180x180" 32 - href="/static/images/apple-touch-icon.png" 33 - /> 19 + <!-- Favicon & Icons --> 20 + <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" /> 21 + <link rel="apple-touch-icon" href="/static/icon-192.svg" /> 34 22 35 - <!-- Web Manifest --> 23 + <!-- Web Manifest for PWA --> 36 24 <link rel="manifest" href="/static/manifest.json" /> 37 - <meta name="theme-color" content="#78350f" /> 38 - <script type="module" crossorigin src="/static/app/assets/index-COWWPLMS.js"></script> 39 - <link rel="stylesheet" crossorigin href="/static/app/assets/index-DUcERGgO.css"> 25 + <script type="module" crossorigin src="/static/app/assets/index-B1vlIgXx.js"></script> 26 + <link rel="stylesheet" crossorigin href="/static/app/assets/index-C7VTH3Mq.css"> 40 27 </head> 41 28 <body class="bg-brown-50 text-brown-900 min-h-screen"> 42 29 <div id="app"></div> 30 + <!-- Service Worker Registration (external script for CSP compliance) --> 31 + <script defer src="/static/register-sw.js"></script> 43 32 </body> 44 33 </html>
-1
static/css/output.css
··· 1 - *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}button,input[type=button],input[type=submit]{min-height:44px;min-width:44px}@media (max-width:768px){input,select,textarea{font-size:16px}}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-50{z-index:50}.-mx-3{margin-left:-.75rem;margin-right:-.75rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.max-h-60{max-height:15rem}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-1\/6{width:16.666667%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-20{width:5rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[50px\]{min-width:50px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-6xl{max-width:72rem}.max-w-full{max-width:100%}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-0\.5{row-gap:.125rem}.gap-y-1{row-gap:.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-brown-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(234 221 215/var(--tw-divide-opacity,1))}.divide-brown-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(224 206 199/var(--tw-divide-opacity,1))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-r-lg{border-bottom-right-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-amber-400{--tw-border-opacity:1;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-brown-100{--tw-border-opacity:1;border-color:rgb(242 232 229/var(--tw-border-opacity,1))}.border-brown-200{--tw-border-opacity:1;border-color:rgb(234 221 215/var(--tw-border-opacity,1))}.border-brown-300{--tw-border-opacity:1;border-color:rgb(224 206 199/var(--tw-border-opacity,1))}.border-brown-600{--tw-border-opacity:1;border-color:rgb(127 85 57/var(--tw-border-opacity,1))}.border-brown-700{--tw-border-opacity:1;border-color:rgb(107 68 35/var(--tw-border-opacity,1))}.border-brown-800{--tw-border-opacity:1;border-color:rgb(74 44 42/var(--tw-border-opacity,1))}.border-brown-900{--tw-border-opacity:1;border-color:rgb(61 35 25/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-red-400{--tw-border-opacity:1;border-color:rgb(248 113 113/var(--tw-border-opacity,1))}.bg-amber-100{--tw-bg-opacity:1;background-color:rgb(254 243 199/var(--tw-bg-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-black\/40{background-color:rgba(0,0,0,.4)}.bg-brown-200{--tw-bg-opacity:1;background-color:rgb(234 221 215/var(--tw-bg-opacity,1))}.bg-brown-200\/80{background-color:hsla(19,31%,88%,.8)}.bg-brown-300{--tw-bg-opacity:1;background-color:rgb(224 206 199/var(--tw-bg-opacity,1))}.bg-brown-50{--tw-bg-opacity:1;background-color:rgb(253 248 246/var(--tw-bg-opacity,1))}.bg-brown-50\/60{background-color:hsla(17,64%,98%,.6)}.bg-brown-600{--tw-bg-opacity:1;background-color:rgb(127 85 57/var(--tw-bg-opacity,1))}.bg-brown-700{--tw-bg-opacity:1;background-color:rgb(107 68 35/var(--tw-bg-opacity,1))}.bg-brown-800{--tw-bg-opacity:1;background-color:rgb(74 44 42/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity,1))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/60{background-color:hsla(0,0%,100%,.6)}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-amber-50{--tw-gradient-from:#fffbeb var(--tw-gradient-from-position);--tw-gradient-to:rgba(255,251,235,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-100{--tw-gradient-from:#f2e8e5 var(--tw-gradient-from-position);--tw-gradient-to:hsla(14,33%,92%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-50{--tw-gradient-from:#fdf8f6 var(--tw-gradient-from-position);--tw-gradient-to:hsla(17,64%,98%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-500{--tw-gradient-from:#bfa094 var(--tw-gradient-from-position);--tw-gradient-to:hsla(17,25%,66%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-700{--tw-gradient-from:#6b4423 var(--tw-gradient-from-position);--tw-gradient-to:rgba(107,68,35,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-brown-800{--tw-gradient-from:#4a2c2a var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,44,42,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-brown-100{--tw-gradient-to:#f2e8e5 var(--tw-gradient-to-position)}.to-brown-200{--tw-gradient-to:#eaddd7 var(--tw-gradient-to-position)}.to-brown-600{--tw-gradient-to:#7f5539 var(--tw-gradient-to-position)}.to-brown-800{--tw-gradient-to:#4a2c2a var(--tw-gradient-to-position)}.to-brown-900{--tw-gradient-to:#3d2319 var(--tw-gradient-to-position)}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pl-3{padding-left:.75rem}.pt-1{padding-top:.25rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wider{letter-spacing:.05em}.text-amber-900{--tw-text-opacity:1;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-brown-100{--tw-text-opacity:1;color:rgb(242 232 229/var(--tw-text-opacity,1))}.text-brown-300{--tw-text-opacity:1;color:rgb(224 206 199/var(--tw-text-opacity,1))}.text-brown-400{--tw-text-opacity:1;color:rgb(210 186 176/var(--tw-text-opacity,1))}.text-brown-500{--tw-text-opacity:1;color:rgb(191 160 148/var(--tw-text-opacity,1))}.text-brown-600{--tw-text-opacity:1;color:rgb(127 85 57/var(--tw-text-opacity,1))}.text-brown-700{--tw-text-opacity:1;color:rgb(107 68 35/var(--tw-text-opacity,1))}.text-brown-800{--tw-text-opacity:1;color:rgb(74 44 42/var(--tw-text-opacity,1))}.text-brown-900{--tw-text-opacity:1;color:rgb(61 35 25/var(--tw-text-opacity,1))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.accent-brown-700{accent-color:#6b4423}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-brown-500{--tw-ring-opacity:1;--tw-ring-color:rgb(191 160 148/var(--tw-ring-opacity,1))}.ring-brown-600{--tw-ring-opacity:1;--tw-ring-color:rgb(127 85 57/var(--tw-ring-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.htmx-swapping{opacity:0;transition:opacity .3s ease-out}.hover\:bg-brown-100:hover{--tw-bg-opacity:1;background-color:rgb(242 232 229/var(--tw-bg-opacity,1))}.hover\:bg-brown-100\/60:hover{background-color:hsla(14,33%,92%,.6)}.hover\:bg-brown-300:hover{--tw-bg-opacity:1;background-color:rgb(224 206 199/var(--tw-bg-opacity,1))}.hover\:bg-brown-400:hover{--tw-bg-opacity:1;background-color:rgb(210 186 176/var(--tw-bg-opacity,1))}.hover\:bg-brown-50:hover{--tw-bg-opacity:1;background-color:rgb(253 248 246/var(--tw-bg-opacity,1))}.hover\:bg-brown-800:hover{--tw-bg-opacity:1;background-color:rgb(74 44 42/var(--tw-bg-opacity,1))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.hover\:from-brown-600:hover{--tw-gradient-from:#7f5539 var(--tw-gradient-from-position);--tw-gradient-to:rgba(127,85,57,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:from-brown-800:hover{--tw-gradient-from:#4a2c2a var(--tw-gradient-from-position);--tw-gradient-to:rgba(74,44,42,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-brown-700:hover{--tw-gradient-to:#6b4423 var(--tw-gradient-to-position)}.hover\:to-brown-900:hover{--tw-gradient-to:#3d2319 var(--tw-gradient-to-position)}.hover\:text-brown-700:hover{--tw-text-opacity:1;color:rgb(107 68 35/var(--tw-text-opacity,1))}.hover\:text-brown-800:hover{--tw-text-opacity:1;color:rgb(74 44 42/var(--tw-text-opacity,1))}.hover\:text-brown-900:hover{--tw-text-opacity:1;color:rgb(61 35 25/var(--tw-text-opacity,1))}.hover\:text-red-800:hover{--tw-text-opacity:1;color:rgb(153 27 27/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-lg:hover,.hover\:shadow-xl:hover{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.hover\:ring-2:hover{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.hover\:ring-brown-600:hover{--tw-ring-opacity:1;--tw-ring-color:rgb(127 85 57/var(--tw-ring-opacity,1))}.focus\:border-brown-600:focus{--tw-border-opacity:1;border-color:rgb(127 85 57/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-brown-600:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(127 85 57/var(--tw-ring-opacity,1))}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:640px){.sm\:inline{display:inline}}@media (min-width:768px){.md\:mx-0{margin-left:0;margin-right:0}.md\:mb-6{margin-bottom:1.5rem}.md\:line-clamp-2{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-20{width:5rem}.md\:min-w-\[60px\]{min-width:60px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:items-start{align-items:flex-start}.md\:items-end{align-items:flex-end}.md\:justify-between{justify-content:space-between}.md\:gap-4{gap:1rem}.md\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.md\:p-3{padding:.75rem}.md\:p-4{padding:1rem}.md\:p-5{padding:1.25rem}.md\:p-6{padding:1.5rem}.md\:p-8{padding:2rem}.md\:px-4{padding-left:1rem;padding-right:1rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:py-3{padding-bottom:.75rem;padding-top:.75rem}.md\:py-8{padding-bottom:2rem;padding-top:2rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-base{font-size:1rem;line-height:1.5rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}
-7
static/js/sw-register.js
··· 1 - // Service Worker Registration 2 - if ("serviceWorker" in navigator) { 3 - navigator.serviceWorker 4 - .register("/static/service-worker.js") 5 - .then(() => console.log("Service Worker registered")) 6 - .catch((err) => console.log("Service Worker registration failed:", err)); 7 - }
+48 -4
static/manifest.json
··· 1 1 { 2 2 "name": "Arabica - Coffee Brew Tracker", 3 3 "short_name": "Arabica", 4 - "description": "Track your coffee brewing journey", 4 + "description": "Track your coffee brewing journey with detailed logs stored in your Personal Data Server", 5 5 "start_url": "/", 6 6 "display": "standalone", 7 - "background_color": "#4a2c2a", 8 - "theme_color": "#4a2c2a", 9 - "orientation": "portrait", 7 + "orientation": "portrait-primary", 8 + "scope": "/", 9 + "background_color": "#faf8f3", 10 + "theme_color": "#78350f", 11 + "categories": ["productivity", "lifestyle"], 12 + "screenshots": [ 13 + { 14 + "src": "/static/icon-192.svg", 15 + "sizes": "192x192", 16 + "type": "image/svg+xml", 17 + "form_factor": "narrow" 18 + }, 19 + { 20 + "src": "/static/icon-512.svg", 21 + "sizes": "512x512", 22 + "type": "image/svg+xml", 23 + "form_factor": "wide" 24 + } 25 + ], 10 26 "icons": [ 11 27 { 12 28 "src": "/static/favicon.svg", ··· 26 42 "type": "image/svg+xml", 27 43 "purpose": "any maskable" 28 44 } 45 + ], 46 + "shortcuts": [ 47 + { 48 + "name": "Log a Brew", 49 + "short_name": "New Brew", 50 + "description": "Create a new brew entry", 51 + "url": "/brews?action=new", 52 + "icons": [ 53 + { 54 + "src": "/static/icon-192.svg", 55 + "sizes": "192x192", 56 + "type": "image/svg+xml" 57 + } 58 + ] 59 + }, 60 + { 61 + "name": "View Brews", 62 + "short_name": "My Brews", 63 + "description": "View your brew history", 64 + "url": "/brews", 65 + "icons": [ 66 + { 67 + "src": "/static/icon-192.svg", 68 + "sizes": "192x192", 69 + "type": "image/svg+xml" 70 + } 71 + ] 72 + } 29 73 ] 30 74 }
+29
static/register-sw.js
··· 1 + // Service Worker Registration 2 + // This is loaded as an external script to comply with strict CSP 3 + // Note: Service workers require a secure context (HTTPS) or localhost/127.0.0.1 for development 4 + 5 + if ('serviceWorker' in navigator) { 6 + window.addEventListener('load', () => { 7 + navigator.serviceWorker 8 + .register('/static/service-worker.js', { scope: '/' }) 9 + .then((registration) => { 10 + console.log('[SW] Service Worker registered:', registration); 11 + 12 + // Check for updates periodically (every 60 seconds) 13 + setInterval(() => { 14 + registration.update(); 15 + }, 60000); 16 + }) 17 + .catch((err) => { 18 + console.error('[SW] Service Worker registration failed:', err); 19 + 20 + // Log additional context for debugging 21 + if (err instanceof DOMException && err.name === 'SecurityError') { 22 + console.warn('[SW] Service Worker requires a secure context (HTTPS) or localhost'); 23 + console.warn('[SW] For development, access via http://localhost:18910 instead of 127.0.0.1'); 24 + } 25 + }); 26 + }); 27 + } else { 28 + console.warn('[SW] Service Workers not supported in this browser'); 29 + }
+128 -31
static/service-worker.js
··· 1 - const CACHE_NAME = 'arabica-v1'; 2 - const urlsToCache = [ 3 - '/', 4 - '/static/css/style.css', 5 - 'https://unpkg.com/htmx.org@1.9.10', 6 - 'https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js' 1 + const CACHE_VERSION = "v1"; 2 + const CACHE_NAMES = { 3 + static: `arabica-static-${CACHE_VERSION}`, 4 + dynamic: `arabica-dynamic-${CACHE_VERSION}`, 5 + api: `arabica-api-${CACHE_VERSION}`, 6 + }; 7 + 8 + // Resources to cache on install 9 + const staticAssets = [ 10 + "/", 11 + "/static/app/index.html", 12 + "/static/manifest.json", 13 + "/static/favicon.svg", 14 + "/static/icon-192.svg", 15 + "/static/icon-512.svg", 7 16 ]; 8 17 9 - // Install service worker and cache resources 10 - self.addEventListener('install', (event) => { 18 + // Install service worker - cache static assets 19 + self.addEventListener("install", (event) => { 20 + console.log("[SW] Installing service worker"); 11 21 event.waitUntil( 12 - caches.open(CACHE_NAME) 13 - .then((cache) => cache.addAll(urlsToCache)) 22 + caches.open(CACHE_NAMES.static).then((cache) => { 23 + console.log("[SW] Caching static assets"); 24 + return cache.addAll(staticAssets).catch((err) => { 25 + console.warn("[SW] Failed to cache some assets:", err); 26 + // Don't fail install if some assets can't be cached 27 + return Promise.resolve(); 28 + }); 29 + }), 14 30 ); 31 + self.skipWaiting(); // Activate new service worker immediately 15 32 }); 16 33 17 - // Fetch from cache, fallback to network 18 - self.addEventListener('fetch', (event) => { 19 - event.respondWith( 20 - caches.match(event.request) 21 - .then((response) => { 22 - // Cache hit - return response 23 - if (response) { 24 - return response; 25 - } 26 - return fetch(event.request); 27 - } 28 - ) 29 - ); 34 + // Fetch event - implement cache strategies 35 + self.addEventListener("fetch", (event) => { 36 + const { request } = event; 37 + const url = new URL(request.url); 38 + 39 + // Skip cross-origin requests 40 + if (url.origin !== self.location.origin) { 41 + return; 42 + } 43 + 44 + // API requests: network-first with cache fallback 45 + if (url.pathname.startsWith("/api/")) { 46 + event.respondWith(networkFirstStrategy(request, CACHE_NAMES.api)); 47 + return; 48 + } 49 + 50 + // Static assets: cache-first with network fallback 51 + if ( 52 + url.pathname.includes("/assets/") || 53 + url.pathname.endsWith(".svg") || 54 + url.pathname.endsWith(".css") || 55 + url.pathname.endsWith(".js") 56 + ) { 57 + event.respondWith(cacheFirstStrategy(request, CACHE_NAMES.static)); 58 + return; 59 + } 60 + 61 + // HTML documents: network-first with cache fallback 62 + if (request.method === "GET" && request.headers.get("accept")?.includes("text/html")) { 63 + event.respondWith(networkFirstStrategy(request, CACHE_NAMES.dynamic)); 64 + return; 65 + } 66 + 67 + // Default: try network, fallback to cache 68 + event.respondWith(networkFirstStrategy(request, CACHE_NAMES.dynamic)); 30 69 }); 31 70 32 - // Update service worker 33 - self.addEventListener('activate', (event) => { 34 - const cacheWhitelist = [CACHE_NAME]; 71 + // Activate service worker - clean up old caches 72 + self.addEventListener("activate", (event) => { 73 + console.log("[SW] Activating service worker"); 35 74 event.waitUntil( 36 75 caches.keys().then((cacheNames) => { 76 + const cacheWhitelist = Object.values(CACHE_NAMES); 37 77 return Promise.all( 38 - cacheNames.map((cacheName) => { 39 - if (cacheWhitelist.indexOf(cacheName) === -1) { 78 + cacheNames 79 + .filter((cacheName) => !cacheWhitelist.includes(cacheName)) 80 + .map((cacheName) => { 81 + console.log("[SW] Deleting old cache:", cacheName); 40 82 return caches.delete(cacheName); 41 - } 42 - }) 83 + }), 43 84 ); 44 - }) 85 + }), 45 86 ); 87 + self.clients.claim(); // Take control of all pages immediately 46 88 }); 89 + 90 + // Cache-first strategy: use cache, fallback to network 91 + async function cacheFirstStrategy(request, cacheName) { 92 + const cache = await caches.open(cacheName); 93 + const cached = await cache.match(request); 94 + 95 + if (cached) { 96 + return cached; 97 + } 98 + 99 + try { 100 + const response = await fetch(request); 101 + if (response.ok) { 102 + cache.put(request, response.clone()); 103 + } 104 + return response; 105 + } catch (error) { 106 + console.warn("[SW] Fetch failed for", request.url, error); 107 + // Return offline page if available 108 + return cache.match("/") || new Response("Offline - content not available", { 109 + status: 503, 110 + statusText: "Service Unavailable", 111 + headers: { "Content-Type": "text/plain" }, 112 + }); 113 + } 114 + } 115 + 116 + // Network-first strategy: try network, fallback to cache 117 + async function networkFirstStrategy(request, cacheName) { 118 + const cache = await caches.open(cacheName); 119 + 120 + try { 121 + const response = await fetch(request); 122 + if (response.ok) { 123 + cache.put(request, response.clone()); 124 + } 125 + return response; 126 + } catch (error) { 127 + console.warn("[SW] Network request failed for", request.url, error); 128 + const cached = await cache.match(request); 129 + if (cached) { 130 + return cached; 131 + } 132 + 133 + // Fallback for failed requests 134 + return new Response( 135 + JSON.stringify({ error: "Offline - unable to fetch data" }), 136 + { 137 + status: 503, 138 + statusText: "Service Unavailable", 139 + headers: { "Content-Type": "application/json" }, 140 + }, 141 + ); 142 + } 143 + }

History

1 round 0 comments
sign up or login to add to the discussion
pdewey.com submitted #0
1 commit
expand
feat: pwa and tailwind move to vite
expand 0 comments
closed without merging