A server-side link shortening service powered by Linkat

refactor(css): convert to TailwindCSS 4

ewancroft.uk 603ce23f 489d706a

verified
+19
README.md
··· 10 10 - ๐ŸŽฏ **Smart Redirects**: Instant HTTP 301 redirects to your target URLs 11 11 - ๐Ÿ” **Automatic PDS Discovery**: Resolves your PDS endpoint via Slingshot 12 12 - โšก **Built-in Cache**: 5-minute cache for optimal performance 13 + - ๐ŸŽจ **Tailwind CSS 4**: Modern styling with the latest Tailwind version 13 14 14 15 ## ๐Ÿš€ Quick Start 15 16 ··· 206 207 npm run lint 207 208 ``` 208 209 210 + ## ๐ŸŽจ Styling with Tailwind CSS 4 211 + 212 + This project uses **Tailwind CSS 4** with the new Vite plugin. See [TAILWIND.md](./TAILWIND.md) for detailed information about: 213 + 214 + - New Tailwind CSS 4 features 215 + - Configuration and customization 216 + - Dark mode support 217 + - Migration from v3 218 + 219 + Key features: 220 + 221 + - โœ… Native CSS imports with `@import 'tailwindcss'` 222 + - โœ… Faster builds with the Vite plugin 223 + - โœ… Automatic dark mode support 224 + - โœ… No config file needed for basic usage 225 + 209 226 ## ๐Ÿ“ฆ Tech Stack 210 227 211 228 - **Framework**: [SvelteKit 2](https://kit.svelte.dev/) 229 + - **Styling**: [Tailwind CSS 4](https://tailwindcss.com/) 212 230 - **Runtime**: Server-side only (no client JavaScript required) 213 231 - **Data Source**: AT Protocol (`blue.linkat.board` collection) 214 232 - **PDS Resolution**: [Slingshot](https://slingshot.microcosm.blue) by Microcosm ··· 239 257 - [Linkat](https://linkat.blue) - The link board service 240 258 - [AT Protocol](https://atproto.com) - The underlying protocol 241 259 - [SvelteKit](https://kit.svelte.dev) - The web framework 260 + - [Tailwind CSS](https://tailwindcss.com) - CSS framework 242 261 - [PDSls](https://pdsls.dev/) - Find your DID 243 262 - [Slingshot](https://slingshot.microcosm.blue) - Identity resolver 244 263
+652 -27
package-lock.json
··· 15 15 "@sveltejs/adapter-auto": "^7.0.0", 16 16 "@sveltejs/kit": "^2.49.0", 17 17 "@sveltejs/vite-plugin-svelte": "^6.2.1", 18 + "@tailwindcss/vite": "^4.0.0", 18 19 "prettier": "^3.6.2", 19 20 "prettier-plugin-svelte": "^3.4.0", 20 21 "svelte": "^5.43.14", 21 22 "svelte-check": "^4.3.4", 23 + "tailwindcss": "^4.0.0", 22 24 "typescript": "^5.9.3", 23 25 "vite": "^7.2.4" 24 26 } 25 27 }, 26 28 "node_modules/@atproto/api": { 27 - "version": "0.18.1", 28 - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.1.tgz", 29 - "integrity": "sha512-eK8Us3kRfK+KjxEq/abF3XL4qtqxh7a5GbKHaUGQqPxNGmLiIdFn4Ve4PkpP/OsDfcRMZF5CK47Jr7SARc7ttg==", 29 + "version": "0.18.3", 30 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.18.3.tgz", 31 + "integrity": "sha512-CBqyZfkcKYsr348KP4CKb9plMlZ5A96HwA/DnYscPBl6fvMZkAezAjniZX+xUILASHQJg5c+NaNw9xP8ZuyyDQ==", 30 32 "license": "MIT", 31 33 "dependencies": { 32 - "@atproto/common-web": "^0.4.3", 33 - "@atproto/lexicon": "^0.5.1", 34 + "@atproto/common-web": "^0.4.5", 35 + "@atproto/lexicon": "^0.5.2", 34 36 "@atproto/syntax": "^0.4.1", 35 - "@atproto/xrpc": "^0.7.5", 37 + "@atproto/xrpc": "^0.7.6", 36 38 "await-lock": "^2.2.2", 37 39 "multiformats": "^9.9.0", 38 40 "tlds": "^1.234.0", ··· 40 42 } 41 43 }, 42 44 "node_modules/@atproto/common-web": { 43 - "version": "0.4.3", 44 - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", 45 - "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 45 + "version": "0.4.5", 46 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.5.tgz", 47 + "integrity": "sha512-Tx0xUafLm3vRvOQpbBl5eb9V8xlC7TaRXs6dAulHRkDG3Kb+P9qn3pkDteq+aeMshbVXbVa1rm3Ok4vFyuoyYA==", 46 48 "license": "MIT", 47 49 "dependencies": { 48 - "graphemer": "^1.4.0", 50 + "@atproto/lex-data": "0.0.1", 51 + "@atproto/lex-json": "0.0.1", 52 + "zod": "^3.23.8" 53 + } 54 + }, 55 + "node_modules/@atproto/lex-data": { 56 + "version": "0.0.1", 57 + "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.1.tgz", 58 + "integrity": "sha512-DrS/8cQcQs3s5t9ELAFNtyDZ8/PdiCx47ALtFEP2GnX2uCBHZRkqWG7xmu6ehjc787nsFzZBvlnz3T/gov5fGA==", 59 + "license": "MIT", 60 + "dependencies": { 61 + "@atproto/syntax": "0.4.1", 49 62 "multiformats": "^9.9.0", 63 + "tslib": "^2.8.1", 50 64 "uint8arrays": "3.0.0", 51 - "zod": "^3.23.8" 65 + "unicode-segmenter": "^0.14.0" 66 + } 67 + }, 68 + "node_modules/@atproto/lex-json": { 69 + "version": "0.0.1", 70 + "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.1.tgz", 71 + "integrity": "sha512-ivcF7+pDRuD/P97IEKQ/9TruunXj0w58Khvwk3M6psaI5eZT6LRsRZ4cWcKaXiFX4SHnjy+x43g0f7pPtIsERg==", 72 + "license": "MIT", 73 + "dependencies": { 74 + "@atproto/lex-data": "0.0.1", 75 + "tslib": "^2.8.1" 52 76 } 53 77 }, 54 78 "node_modules/@atproto/lexicon": { 55 - "version": "0.5.1", 56 - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", 57 - "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 79 + "version": "0.5.2", 80 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.2.tgz", 81 + "integrity": "sha512-lRmJgMA8f5j7VB5Iu5cp188ald5FuI4FlmZ7nn6EBrk1dgOstWVrI5Ft6K3z2vjyLZRG6nzknlsw+tDP63p7bQ==", 58 82 "license": "MIT", 59 83 "dependencies": { 60 - "@atproto/common-web": "^0.4.3", 84 + "@atproto/common-web": "^0.4.4", 61 85 "@atproto/syntax": "^0.4.1", 62 86 "iso-datestring-validator": "^2.2.2", 63 87 "multiformats": "^9.9.0", ··· 71 95 "license": "MIT" 72 96 }, 73 97 "node_modules/@atproto/xrpc": { 74 - "version": "0.7.5", 75 - "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", 76 - "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 98 + "version": "0.7.6", 99 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.6.tgz", 100 + "integrity": "sha512-RvCf4j0JnKYWuz3QzsYCntJi3VuiAAybQsMIUw2wLWcHhchO9F7UaBZINLL2z0qc/cYWPv5NSwcVydMseoCZLA==", 77 101 "license": "MIT", 78 102 "dependencies": { 79 - "@atproto/lexicon": "^0.5.1", 103 + "@atproto/lexicon": "^0.5.2", 80 104 "zod": "^3.23.8" 81 105 } 82 106 }, ··· 994 1018 "vite": "^6.3.0 || ^7.0.0" 995 1019 } 996 1020 }, 1021 + "node_modules/@tailwindcss/node": { 1022 + "version": "4.1.17", 1023 + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", 1024 + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", 1025 + "dev": true, 1026 + "license": "MIT", 1027 + "dependencies": { 1028 + "@jridgewell/remapping": "^2.3.4", 1029 + "enhanced-resolve": "^5.18.3", 1030 + "jiti": "^2.6.1", 1031 + "lightningcss": "1.30.2", 1032 + "magic-string": "^0.30.21", 1033 + "source-map-js": "^1.2.1", 1034 + "tailwindcss": "4.1.17" 1035 + } 1036 + }, 1037 + "node_modules/@tailwindcss/oxide": { 1038 + "version": "4.1.17", 1039 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", 1040 + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", 1041 + "dev": true, 1042 + "license": "MIT", 1043 + "engines": { 1044 + "node": ">= 10" 1045 + }, 1046 + "optionalDependencies": { 1047 + "@tailwindcss/oxide-android-arm64": "4.1.17", 1048 + "@tailwindcss/oxide-darwin-arm64": "4.1.17", 1049 + "@tailwindcss/oxide-darwin-x64": "4.1.17", 1050 + "@tailwindcss/oxide-freebsd-x64": "4.1.17", 1051 + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", 1052 + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", 1053 + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", 1054 + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", 1055 + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", 1056 + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", 1057 + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", 1058 + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" 1059 + } 1060 + }, 1061 + "node_modules/@tailwindcss/oxide-android-arm64": { 1062 + "version": "4.1.17", 1063 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", 1064 + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", 1065 + "cpu": [ 1066 + "arm64" 1067 + ], 1068 + "dev": true, 1069 + "license": "MIT", 1070 + "optional": true, 1071 + "os": [ 1072 + "android" 1073 + ], 1074 + "engines": { 1075 + "node": ">= 10" 1076 + } 1077 + }, 1078 + "node_modules/@tailwindcss/oxide-darwin-arm64": { 1079 + "version": "4.1.17", 1080 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", 1081 + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", 1082 + "cpu": [ 1083 + "arm64" 1084 + ], 1085 + "dev": true, 1086 + "license": "MIT", 1087 + "optional": true, 1088 + "os": [ 1089 + "darwin" 1090 + ], 1091 + "engines": { 1092 + "node": ">= 10" 1093 + } 1094 + }, 1095 + "node_modules/@tailwindcss/oxide-darwin-x64": { 1096 + "version": "4.1.17", 1097 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", 1098 + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", 1099 + "cpu": [ 1100 + "x64" 1101 + ], 1102 + "dev": true, 1103 + "license": "MIT", 1104 + "optional": true, 1105 + "os": [ 1106 + "darwin" 1107 + ], 1108 + "engines": { 1109 + "node": ">= 10" 1110 + } 1111 + }, 1112 + "node_modules/@tailwindcss/oxide-freebsd-x64": { 1113 + "version": "4.1.17", 1114 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", 1115 + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", 1116 + "cpu": [ 1117 + "x64" 1118 + ], 1119 + "dev": true, 1120 + "license": "MIT", 1121 + "optional": true, 1122 + "os": [ 1123 + "freebsd" 1124 + ], 1125 + "engines": { 1126 + "node": ">= 10" 1127 + } 1128 + }, 1129 + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { 1130 + "version": "4.1.17", 1131 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", 1132 + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", 1133 + "cpu": [ 1134 + "arm" 1135 + ], 1136 + "dev": true, 1137 + "license": "MIT", 1138 + "optional": true, 1139 + "os": [ 1140 + "linux" 1141 + ], 1142 + "engines": { 1143 + "node": ">= 10" 1144 + } 1145 + }, 1146 + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { 1147 + "version": "4.1.17", 1148 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", 1149 + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", 1150 + "cpu": [ 1151 + "arm64" 1152 + ], 1153 + "dev": true, 1154 + "license": "MIT", 1155 + "optional": true, 1156 + "os": [ 1157 + "linux" 1158 + ], 1159 + "engines": { 1160 + "node": ">= 10" 1161 + } 1162 + }, 1163 + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { 1164 + "version": "4.1.17", 1165 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", 1166 + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", 1167 + "cpu": [ 1168 + "arm64" 1169 + ], 1170 + "dev": true, 1171 + "license": "MIT", 1172 + "optional": true, 1173 + "os": [ 1174 + "linux" 1175 + ], 1176 + "engines": { 1177 + "node": ">= 10" 1178 + } 1179 + }, 1180 + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { 1181 + "version": "4.1.17", 1182 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", 1183 + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", 1184 + "cpu": [ 1185 + "x64" 1186 + ], 1187 + "dev": true, 1188 + "license": "MIT", 1189 + "optional": true, 1190 + "os": [ 1191 + "linux" 1192 + ], 1193 + "engines": { 1194 + "node": ">= 10" 1195 + } 1196 + }, 1197 + "node_modules/@tailwindcss/oxide-linux-x64-musl": { 1198 + "version": "4.1.17", 1199 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", 1200 + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", 1201 + "cpu": [ 1202 + "x64" 1203 + ], 1204 + "dev": true, 1205 + "license": "MIT", 1206 + "optional": true, 1207 + "os": [ 1208 + "linux" 1209 + ], 1210 + "engines": { 1211 + "node": ">= 10" 1212 + } 1213 + }, 1214 + "node_modules/@tailwindcss/oxide-wasm32-wasi": { 1215 + "version": "4.1.17", 1216 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", 1217 + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", 1218 + "bundleDependencies": [ 1219 + "@napi-rs/wasm-runtime", 1220 + "@emnapi/core", 1221 + "@emnapi/runtime", 1222 + "@tybys/wasm-util", 1223 + "@emnapi/wasi-threads", 1224 + "tslib" 1225 + ], 1226 + "cpu": [ 1227 + "wasm32" 1228 + ], 1229 + "dev": true, 1230 + "license": "MIT", 1231 + "optional": true, 1232 + "dependencies": { 1233 + "@emnapi/core": "^1.6.0", 1234 + "@emnapi/runtime": "^1.6.0", 1235 + "@emnapi/wasi-threads": "^1.1.0", 1236 + "@napi-rs/wasm-runtime": "^1.0.7", 1237 + "@tybys/wasm-util": "^0.10.1", 1238 + "tslib": "^2.4.0" 1239 + }, 1240 + "engines": { 1241 + "node": ">=14.0.0" 1242 + } 1243 + }, 1244 + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { 1245 + "version": "4.1.17", 1246 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", 1247 + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", 1248 + "cpu": [ 1249 + "arm64" 1250 + ], 1251 + "dev": true, 1252 + "license": "MIT", 1253 + "optional": true, 1254 + "os": [ 1255 + "win32" 1256 + ], 1257 + "engines": { 1258 + "node": ">= 10" 1259 + } 1260 + }, 1261 + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { 1262 + "version": "4.1.17", 1263 + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", 1264 + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", 1265 + "cpu": [ 1266 + "x64" 1267 + ], 1268 + "dev": true, 1269 + "license": "MIT", 1270 + "optional": true, 1271 + "os": [ 1272 + "win32" 1273 + ], 1274 + "engines": { 1275 + "node": ">= 10" 1276 + } 1277 + }, 1278 + "node_modules/@tailwindcss/vite": { 1279 + "version": "4.1.17", 1280 + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", 1281 + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", 1282 + "dev": true, 1283 + "license": "MIT", 1284 + "dependencies": { 1285 + "@tailwindcss/node": "4.1.17", 1286 + "@tailwindcss/oxide": "4.1.17", 1287 + "tailwindcss": "4.1.17" 1288 + }, 1289 + "peerDependencies": { 1290 + "vite": "^5.2.0 || ^6 || ^7" 1291 + } 1292 + }, 997 1293 "node_modules/@types/cookie": { 998 1294 "version": "0.6.0", 999 1295 "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", ··· 1112 1408 "node": ">=0.10.0" 1113 1409 } 1114 1410 }, 1411 + "node_modules/detect-libc": { 1412 + "version": "2.1.2", 1413 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", 1414 + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", 1415 + "dev": true, 1416 + "license": "Apache-2.0", 1417 + "engines": { 1418 + "node": ">=8" 1419 + } 1420 + }, 1115 1421 "node_modules/devalue": { 1116 1422 "version": "5.5.0", 1117 1423 "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz", 1118 1424 "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==", 1119 1425 "dev": true, 1120 1426 "license": "MIT" 1427 + }, 1428 + "node_modules/enhanced-resolve": { 1429 + "version": "5.18.3", 1430 + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", 1431 + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", 1432 + "dev": true, 1433 + "license": "MIT", 1434 + "dependencies": { 1435 + "graceful-fs": "^4.2.4", 1436 + "tapable": "^2.2.0" 1437 + }, 1438 + "engines": { 1439 + "node": ">=10.13.0" 1440 + } 1121 1441 }, 1122 1442 "node_modules/esbuild": { 1123 1443 "version": "0.25.12", ··· 1211 1531 "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1212 1532 } 1213 1533 }, 1214 - "node_modules/graphemer": { 1215 - "version": "1.4.0", 1216 - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 1217 - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 1218 - "license": "MIT" 1534 + "node_modules/graceful-fs": { 1535 + "version": "4.2.11", 1536 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 1537 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 1538 + "dev": true, 1539 + "license": "ISC" 1219 1540 }, 1220 1541 "node_modules/is-reference": { 1221 1542 "version": "3.0.3", ··· 1233 1554 "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 1234 1555 "license": "MIT" 1235 1556 }, 1557 + "node_modules/jiti": { 1558 + "version": "2.6.1", 1559 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", 1560 + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", 1561 + "dev": true, 1562 + "license": "MIT", 1563 + "bin": { 1564 + "jiti": "lib/jiti-cli.mjs" 1565 + } 1566 + }, 1236 1567 "node_modules/kleur": { 1237 1568 "version": "4.1.5", 1238 1569 "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", ··· 1243 1574 "node": ">=6" 1244 1575 } 1245 1576 }, 1577 + "node_modules/lightningcss": { 1578 + "version": "1.30.2", 1579 + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", 1580 + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", 1581 + "dev": true, 1582 + "license": "MPL-2.0", 1583 + "dependencies": { 1584 + "detect-libc": "^2.0.3" 1585 + }, 1586 + "engines": { 1587 + "node": ">= 12.0.0" 1588 + }, 1589 + "funding": { 1590 + "type": "opencollective", 1591 + "url": "https://opencollective.com/parcel" 1592 + }, 1593 + "optionalDependencies": { 1594 + "lightningcss-android-arm64": "1.30.2", 1595 + "lightningcss-darwin-arm64": "1.30.2", 1596 + "lightningcss-darwin-x64": "1.30.2", 1597 + "lightningcss-freebsd-x64": "1.30.2", 1598 + "lightningcss-linux-arm-gnueabihf": "1.30.2", 1599 + "lightningcss-linux-arm64-gnu": "1.30.2", 1600 + "lightningcss-linux-arm64-musl": "1.30.2", 1601 + "lightningcss-linux-x64-gnu": "1.30.2", 1602 + "lightningcss-linux-x64-musl": "1.30.2", 1603 + "lightningcss-win32-arm64-msvc": "1.30.2", 1604 + "lightningcss-win32-x64-msvc": "1.30.2" 1605 + } 1606 + }, 1607 + "node_modules/lightningcss-android-arm64": { 1608 + "version": "1.30.2", 1609 + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", 1610 + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", 1611 + "cpu": [ 1612 + "arm64" 1613 + ], 1614 + "dev": true, 1615 + "license": "MPL-2.0", 1616 + "optional": true, 1617 + "os": [ 1618 + "android" 1619 + ], 1620 + "engines": { 1621 + "node": ">= 12.0.0" 1622 + }, 1623 + "funding": { 1624 + "type": "opencollective", 1625 + "url": "https://opencollective.com/parcel" 1626 + } 1627 + }, 1628 + "node_modules/lightningcss-darwin-arm64": { 1629 + "version": "1.30.2", 1630 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", 1631 + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", 1632 + "cpu": [ 1633 + "arm64" 1634 + ], 1635 + "dev": true, 1636 + "license": "MPL-2.0", 1637 + "optional": true, 1638 + "os": [ 1639 + "darwin" 1640 + ], 1641 + "engines": { 1642 + "node": ">= 12.0.0" 1643 + }, 1644 + "funding": { 1645 + "type": "opencollective", 1646 + "url": "https://opencollective.com/parcel" 1647 + } 1648 + }, 1649 + "node_modules/lightningcss-darwin-x64": { 1650 + "version": "1.30.2", 1651 + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", 1652 + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", 1653 + "cpu": [ 1654 + "x64" 1655 + ], 1656 + "dev": true, 1657 + "license": "MPL-2.0", 1658 + "optional": true, 1659 + "os": [ 1660 + "darwin" 1661 + ], 1662 + "engines": { 1663 + "node": ">= 12.0.0" 1664 + }, 1665 + "funding": { 1666 + "type": "opencollective", 1667 + "url": "https://opencollective.com/parcel" 1668 + } 1669 + }, 1670 + "node_modules/lightningcss-freebsd-x64": { 1671 + "version": "1.30.2", 1672 + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", 1673 + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", 1674 + "cpu": [ 1675 + "x64" 1676 + ], 1677 + "dev": true, 1678 + "license": "MPL-2.0", 1679 + "optional": true, 1680 + "os": [ 1681 + "freebsd" 1682 + ], 1683 + "engines": { 1684 + "node": ">= 12.0.0" 1685 + }, 1686 + "funding": { 1687 + "type": "opencollective", 1688 + "url": "https://opencollective.com/parcel" 1689 + } 1690 + }, 1691 + "node_modules/lightningcss-linux-arm-gnueabihf": { 1692 + "version": "1.30.2", 1693 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", 1694 + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", 1695 + "cpu": [ 1696 + "arm" 1697 + ], 1698 + "dev": true, 1699 + "license": "MPL-2.0", 1700 + "optional": true, 1701 + "os": [ 1702 + "linux" 1703 + ], 1704 + "engines": { 1705 + "node": ">= 12.0.0" 1706 + }, 1707 + "funding": { 1708 + "type": "opencollective", 1709 + "url": "https://opencollective.com/parcel" 1710 + } 1711 + }, 1712 + "node_modules/lightningcss-linux-arm64-gnu": { 1713 + "version": "1.30.2", 1714 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", 1715 + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", 1716 + "cpu": [ 1717 + "arm64" 1718 + ], 1719 + "dev": true, 1720 + "license": "MPL-2.0", 1721 + "optional": true, 1722 + "os": [ 1723 + "linux" 1724 + ], 1725 + "engines": { 1726 + "node": ">= 12.0.0" 1727 + }, 1728 + "funding": { 1729 + "type": "opencollective", 1730 + "url": "https://opencollective.com/parcel" 1731 + } 1732 + }, 1733 + "node_modules/lightningcss-linux-arm64-musl": { 1734 + "version": "1.30.2", 1735 + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", 1736 + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", 1737 + "cpu": [ 1738 + "arm64" 1739 + ], 1740 + "dev": true, 1741 + "license": "MPL-2.0", 1742 + "optional": true, 1743 + "os": [ 1744 + "linux" 1745 + ], 1746 + "engines": { 1747 + "node": ">= 12.0.0" 1748 + }, 1749 + "funding": { 1750 + "type": "opencollective", 1751 + "url": "https://opencollective.com/parcel" 1752 + } 1753 + }, 1754 + "node_modules/lightningcss-linux-x64-gnu": { 1755 + "version": "1.30.2", 1756 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", 1757 + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", 1758 + "cpu": [ 1759 + "x64" 1760 + ], 1761 + "dev": true, 1762 + "license": "MPL-2.0", 1763 + "optional": true, 1764 + "os": [ 1765 + "linux" 1766 + ], 1767 + "engines": { 1768 + "node": ">= 12.0.0" 1769 + }, 1770 + "funding": { 1771 + "type": "opencollective", 1772 + "url": "https://opencollective.com/parcel" 1773 + } 1774 + }, 1775 + "node_modules/lightningcss-linux-x64-musl": { 1776 + "version": "1.30.2", 1777 + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", 1778 + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", 1779 + "cpu": [ 1780 + "x64" 1781 + ], 1782 + "dev": true, 1783 + "license": "MPL-2.0", 1784 + "optional": true, 1785 + "os": [ 1786 + "linux" 1787 + ], 1788 + "engines": { 1789 + "node": ">= 12.0.0" 1790 + }, 1791 + "funding": { 1792 + "type": "opencollective", 1793 + "url": "https://opencollective.com/parcel" 1794 + } 1795 + }, 1796 + "node_modules/lightningcss-win32-arm64-msvc": { 1797 + "version": "1.30.2", 1798 + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", 1799 + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", 1800 + "cpu": [ 1801 + "arm64" 1802 + ], 1803 + "dev": true, 1804 + "license": "MPL-2.0", 1805 + "optional": true, 1806 + "os": [ 1807 + "win32" 1808 + ], 1809 + "engines": { 1810 + "node": ">= 12.0.0" 1811 + }, 1812 + "funding": { 1813 + "type": "opencollective", 1814 + "url": "https://opencollective.com/parcel" 1815 + } 1816 + }, 1817 + "node_modules/lightningcss-win32-x64-msvc": { 1818 + "version": "1.30.2", 1819 + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", 1820 + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", 1821 + "cpu": [ 1822 + "x64" 1823 + ], 1824 + "dev": true, 1825 + "license": "MPL-2.0", 1826 + "optional": true, 1827 + "os": [ 1828 + "win32" 1829 + ], 1830 + "engines": { 1831 + "node": ">= 12.0.0" 1832 + }, 1833 + "funding": { 1834 + "type": "opencollective", 1835 + "url": "https://opencollective.com/parcel" 1836 + } 1837 + }, 1246 1838 "node_modules/locate-character": { 1247 1839 "version": "3.0.0", 1248 1840 "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", ··· 1492 2084 } 1493 2085 }, 1494 2086 "node_modules/svelte": { 1495 - "version": "5.43.14", 1496 - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.14.tgz", 1497 - "integrity": "sha512-pHeUrp1A5S6RGaXhJB7PtYjL1VVjbVrJ2EfuAoPu9/1LeoMaJa/pcdCsCSb0gS4eUHAHnhCbUDxORZyvGK6kOQ==", 2087 + "version": "5.43.15", 2088 + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.15.tgz", 2089 + "integrity": "sha512-FYlfm3oyLBNUy2NGqaWfKPiGOamS6YB8BJwAcF9xSXVFUjfcl9Ded1YSMu1vXEf0y0lcmBj45UgnOY2ZxhW0Cw==", 1498 2090 "dev": true, 1499 2091 "license": "MIT", 1500 2092 "peer": true, ··· 1542 2134 "typescript": ">=5.0.0" 1543 2135 } 1544 2136 }, 2137 + "node_modules/tailwindcss": { 2138 + "version": "4.1.17", 2139 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", 2140 + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", 2141 + "dev": true, 2142 + "license": "MIT" 2143 + }, 2144 + "node_modules/tapable": { 2145 + "version": "2.3.0", 2146 + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", 2147 + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", 2148 + "dev": true, 2149 + "license": "MIT", 2150 + "engines": { 2151 + "node": ">=6" 2152 + }, 2153 + "funding": { 2154 + "type": "opencollective", 2155 + "url": "https://opencollective.com/webpack" 2156 + } 2157 + }, 1545 2158 "node_modules/tinyglobby": { 1546 2159 "version": "0.2.15", 1547 2160 "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", ··· 1596 2209 "node": ">=6" 1597 2210 } 1598 2211 }, 2212 + "node_modules/tslib": { 2213 + "version": "2.8.1", 2214 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2215 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2216 + "license": "0BSD" 2217 + }, 1599 2218 "node_modules/typescript": { 1600 2219 "version": "5.9.3", 1601 2220 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", ··· 1619 2238 "dependencies": { 1620 2239 "multiformats": "^9.4.2" 1621 2240 } 2241 + }, 2242 + "node_modules/unicode-segmenter": { 2243 + "version": "0.14.0", 2244 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.0.tgz", 2245 + "integrity": "sha512-AH4lhPCJANUnSLEKnM4byboctePJzltF4xj8b+NbNiYeAkAXGh7px2K/4NANFp7dnr6+zB3e6HLu8Jj8SKyvYg==", 2246 + "license": "MIT" 1622 2247 }, 1623 2248 "node_modules/vite": { 1624 2249 "version": "7.2.4",
+2
package.json
··· 22 22 "@sveltejs/adapter-auto": "^7.0.0", 23 23 "@sveltejs/kit": "^2.49.0", 24 24 "@sveltejs/vite-plugin-svelte": "^6.2.1", 25 + "@tailwindcss/vite": "^4.0.0", 25 26 "prettier": "^3.6.2", 26 27 "prettier-plugin-svelte": "^3.4.0", 27 28 "svelte": "^5.43.14", 28 29 "svelte-check": "^4.3.4", 30 + "tailwindcss": "^4.0.0", 29 31 "typescript": "^5.9.3", 30 32 "vite": "^7.2.4" 31 33 }
+81
src/app.css
··· 1 + @import 'tailwindcss'; 2 + 3 + /* 4 + * Semantic Color System - Green-hued theme optimized for Light/Dark Modes 5 + * Based on forest green palette with excellent contrast and readability 6 + */ 7 + 8 + :root { 9 + /* Light Mode - Darker, more subdued palette */ 10 + --color-background: 237 247 237; /* Soft gray-green background */ 11 + --color-surface: 220 242 220; /* Darker green-tinted surface */ 12 + --color-surface-elevated: 200 230 200; /* Medium green surface */ 13 + 14 + --color-text-primary: 5 46 22; /* green-950 - very dark forest green */ 15 + --color-text-secondary: 22 101 52; /* green-800 */ 16 + --color-text-tertiary: 21 128 61; /* green-700 */ 17 + 18 + --color-border: 160 210 160; /* Darker green border */ 19 + --color-border-strong: 120 190 120; /* Strong green border */ 20 + 21 + /* Primary - Vibrant emerald for links/actions */ 22 + --color-primary: 5 150 105; /* emerald-600 */ 23 + --color-primary-hover: 4 120 87; /* emerald-700 */ 24 + 25 + /* Success - Rich green */ 26 + --color-success: 22 163 74; /* green-600 */ 27 + --color-success-bg: 220 242 220; /* Darker green-50 */ 28 + --color-success-border: 120 190 120; /* green-300 equivalent */ 29 + 30 + /* Error - Strong red for visibility */ 31 + --color-error: 185 28 28; /* red-700 - darker */ 32 + --color-error-bg: 254 226 226; /* Darker red-100 */ 33 + --color-error-border: 248 113 113; /* red-400 */ 34 + 35 + /* Code blocks */ 36 + --color-code-bg: 200 230 200; /* Darker green surface */ 37 + } 38 + 39 + .dark { 40 + /* Dark Mode - Deep forest green with teal accents */ 41 + --color-background: 3 15 10; /* Very dark forest green (almost black) */ 42 + --color-surface: 5 30 20; /* Dark green-tinted */ 43 + --color-surface-elevated: 10 50 35; /* Medium dark green */ 44 + 45 + --color-text-primary: 240 253 244; /* green-50 - bright mint */ 46 + --color-text-secondary: 187 247 208; /* green-200 */ 47 + --color-text-tertiary: 134 239 172; /* green-300 */ 48 + 49 + --color-border: 22 101 52; /* green-800 */ 50 + --color-border-strong: 34 197 94; /* green-500 */ 51 + 52 + /* Primary - Bright emerald for dark mode */ 53 + --color-primary: 52 211 153; /* emerald-400 */ 54 + --color-primary-hover: 110 231 183; /* emerald-300 */ 55 + 56 + /* Success - Bright green for dark mode */ 57 + --color-success: 74 222 128; /* green-400 */ 58 + --color-success-bg: 5 46 22; /* green-950 */ 59 + --color-success-border: 34 197 94; /* green-500 */ 60 + 61 + /* Error - Softer red for dark mode */ 62 + --color-error: 248 113 113; /* red-400 */ 63 + --color-error-bg: 69 10 10; /* red-950 */ 64 + --color-error-border: 220 38 38; /* red-600 */ 65 + 66 + /* Code blocks */ 67 + --color-code-bg: 5 30 20; /* Dark green-tinted */ 68 + } 69 + 70 + /* Apply base background and text colors to html/body */ 71 + html { 72 + background-color: rgb(var(--color-background)); 73 + color: rgb(var(--color-text-primary)); 74 + min-height: 100vh; 75 + } 76 + 77 + body { 78 + background-color: rgb(var(--color-background)); 79 + color: rgb(var(--color-text-primary)); 80 + min-height: 100vh; 81 + }
+104 -96
src/lib/utils/encoding.ts
··· 5 5 const BASE = BASE_CHARS.length; 6 6 7 7 function hashString(text: string): bigint { 8 - let hash = 1469598103934665603n; 9 - for (let i = 0; i < text.length; i++) { 10 - const char = BigInt(text.charCodeAt(i)); 11 - hash = (hash ^ char) * 1099511628211n; 12 - } 13 - return hash < 0n ? -hash : hash; 8 + let hash = 1469598103934665603n; 9 + for (let i = 0; i < text.length; i++) { 10 + const char = BigInt(text.charCodeAt(i)); 11 + hash = (hash ^ char) * 1099511628211n; 12 + } 13 + return hash < 0n ? -hash : hash; 14 14 } 15 15 16 16 function toBase(num: bigint, length: number, seed = ''): string { 17 - let encoded = ''; 18 - let n = num; 19 - for (let i = 0; i < length; i++) { 20 - let rem: bigint; 21 - if (n > 0n) { 22 - rem = n % BigInt(BASE); 23 - n = n / BigInt(BASE); 24 - } else { 25 - const fallback = hashString(num.toString() + '::' + seed + '::' + i.toString()); 26 - rem = fallback % BigInt(BASE); 27 - } 28 - encoded = BASE_CHARS[Number(rem)] + encoded; 29 - } 30 - return encoded; 17 + let encoded = ''; 18 + let n = num; 19 + for (let i = 0; i < length; i++) { 20 + let rem: bigint; 21 + if (n > 0n) { 22 + rem = n % BigInt(BASE); 23 + n = n / BigInt(BASE); 24 + } else { 25 + const fallback = hashString(num.toString() + '::' + seed + '::' + i.toString()); 26 + rem = fallback % BigInt(BASE); 27 + } 28 + encoded = BASE_CHARS[Number(rem)] + encoded; 29 + } 30 + return encoded; 31 31 } 32 32 33 33 function normaliseUrl(url: string): string { 34 - try { 35 - const parsed = new URL(url.startsWith('http') ? url : `https://${url}`); 36 - parsed.hash = ''; 34 + try { 35 + const parsed = new URL(url.startsWith('http') ? url : `https://${url}`); 36 + parsed.hash = ''; 37 37 38 - const sortedParams = [...parsed.searchParams.entries()].sort((a, b) => a[0].localeCompare(b[0])); 39 - parsed.search = ''; 40 - for (const [key, value] of sortedParams) parsed.searchParams.append(key, value); 38 + const sortedParams = [...parsed.searchParams.entries()].sort((a, b) => 39 + a[0].localeCompare(b[0]) 40 + ); 41 + parsed.search = ''; 42 + for (const [key, value] of sortedParams) parsed.searchParams.append(key, value); 41 43 42 - parsed.hostname = parsed.hostname.toLowerCase(); 43 - parsed.protocol = 'https:'; 44 - return parsed.toString(); 45 - } catch (e) { 46 - return url.trim(); 47 - } 44 + parsed.hostname = parsed.hostname.toLowerCase(); 45 + parsed.protocol = 'https:'; 46 + return parsed.toString(); 47 + } catch (e) { 48 + return url.trim(); 49 + } 48 50 } 49 51 50 52 function getBaseDomain(url: string): string { 51 - try { 52 - const domain = getDomain(url, { allowPrivateDomains: false }); 53 - if (domain) return domain.toLowerCase(); 53 + try { 54 + const domain = getDomain(url, { allowPrivateDomains: false }); 55 + if (domain) return domain.toLowerCase(); 54 56 55 - const parsed = parse(url, { extractHostname: true }); 56 - return (parsed.hostname ?? '').toLowerCase(); 57 - } catch (e) { 58 - return ''; 59 - } 57 + const parsed = parse(url, { extractHostname: true }); 58 + return (parsed.hostname ?? '').toLowerCase(); 59 + } catch (e) { 60 + return ''; 61 + } 60 62 } 61 63 62 64 export function encodeUrl(url: string, length: number = SHORTCODE.DEFAULT_LENGTH): string { 63 - if (!Number.isInteger(length) || length < 3) length = SHORTCODE.DEFAULT_LENGTH; 65 + if (!Number.isInteger(length) || length < 3) length = SHORTCODE.DEFAULT_LENGTH; 64 66 65 - const DOMAIN_PREFIX_LENGTH = 2; 67 + const DOMAIN_PREFIX_LENGTH = 2; 66 68 67 - const normalised = normaliseUrl(url); 68 - const apex = getBaseDomain(normalised) || ''; 69 + const normalised = normaliseUrl(url); 70 + const apex = getBaseDomain(normalised) || ''; 69 71 70 - const domainHash = hashString(apex || normalised); 71 - const domainPrefix = toBase(domainHash, DOMAIN_PREFIX_LENGTH, 'domain'); 72 + const domainHash = hashString(apex || normalised); 73 + const domainPrefix = toBase(domainHash, DOMAIN_PREFIX_LENGTH, 'domain'); 72 74 73 - const remaining = Math.max(1, length - DOMAIN_PREFIX_LENGTH); 75 + const remaining = Math.max(1, length - DOMAIN_PREFIX_LENGTH); 74 76 75 - let hostname = ''; 76 - try { 77 - hostname = new URL(normalised).hostname.toLowerCase(); 78 - } catch (e) { 79 - try { hostname = new URL(url.startsWith('http') ? url : `https://${url}`).hostname.toLowerCase(); } catch { hostname = ''; } 80 - } 77 + let hostname = ''; 78 + try { 79 + hostname = new URL(normalised).hostname.toLowerCase(); 80 + } catch (e) { 81 + try { 82 + hostname = new URL(url.startsWith('http') ? url : `https://${url}`).hostname.toLowerCase(); 83 + } catch { 84 + hostname = ''; 85 + } 86 + } 81 87 82 - let subLevels: string[] = []; 83 - if (apex && hostname && hostname !== apex) { 84 - const sub = hostname.replace(new RegExp(`\.${apex}$`), ''); 85 - subLevels = sub.split('.'); 86 - } 88 + let subLevels: string[] = []; 89 + if (apex && hostname && hostname !== apex) { 90 + const sub = hostname.replace(new RegExp(`\.${apex}$`), ''); 91 + subLevels = sub.split('.'); 92 + } 87 93 88 - const MIN_URL_CORE = 1; 89 - const MIN_TAIL = 1; 90 - const tailLength = remaining; 94 + const MIN_URL_CORE = 1; 95 + const MIN_TAIL = 1; 96 + const tailLength = remaining; 91 97 92 - const urlHash = hashString(normalised + '::url'); 93 - const urlCoreLength = remaining - subLevels.length; 94 - const urlCore = toBase(urlHash, Math.max(MIN_URL_CORE, urlCoreLength), 'url'); 98 + const urlHash = hashString(normalised + '::url'); 99 + const urlCoreLength = remaining - subLevels.length; 100 + const urlCore = toBase(urlHash, Math.max(MIN_URL_CORE, urlCoreLength), 'url'); 95 101 96 - const subTail: string[] = []; 97 - const reversedSubLevels = subLevels.slice().reverse(); 98 - for (let i = 0; i < reversedSubLevels.length; i++) { 99 - const h = hashString(reversedSubLevels[i] + '::sub'); 100 - subTail.push(toBase(h, 1, 'sub' + i)); 101 - } 102 + const subTail: string[] = []; 103 + const reversedSubLevels = subLevels.slice().reverse(); 104 + for (let i = 0; i < reversedSubLevels.length; i++) { 105 + const h = hashString(reversedSubLevels[i] + '::sub'); 106 + subTail.push(toBase(h, 1, 'sub' + i)); 107 + } 102 108 103 - let tail = subTail.join(''); 104 - if (!tail) { 105 - const fallbackHash = hashString(normalised + '::fallback'); 106 - tail = toBase(fallbackHash, tailLength, 'sub'); 107 - } 109 + let tail = subTail.join(''); 110 + if (!tail) { 111 + const fallbackHash = hashString(normalised + '::fallback'); 112 + tail = toBase(fallbackHash, tailLength, 'sub'); 113 + } 108 114 109 - let out = domainPrefix + urlCore + tail; 110 - if (out.length > length) out = out.slice(0, length); 111 - if (out.length < length) { 112 - let pad = ''; 113 - let i = 0; 114 - while (out.length + pad.length < length) { 115 - const h = hashString(normalised + '::pad2::' + i); 116 - pad += toBase(h, Math.min(4, length - out.length - pad.length), 'pad2' + i); 117 - i++; 118 - } 119 - out += pad.slice(0, length - out.length); 120 - } 115 + let out = domainPrefix + urlCore + tail; 116 + if (out.length > length) out = out.slice(0, length); 117 + if (out.length < length) { 118 + let pad = ''; 119 + let i = 0; 120 + while (out.length + pad.length < length) { 121 + const h = hashString(normalised + '::pad2::' + i); 122 + pad += toBase(h, Math.min(4, length - out.length - pad.length), 'pad2' + i); 123 + i++; 124 + } 125 + out += pad.slice(0, length - out.length); 126 + } 121 127 122 - // --- LOGGING MAX COMBINATIONS --- 123 - const maxCombinations = BigInt(BASE) ** BigInt(length); 124 - console.log(`[Shortcode Info] URL: ${url}`); 125 - console.log(`[Shortcode Info] Length: ${length}, Charset: ${BASE} chars`); 126 - console.log(`[Shortcode Info] Max possible combinations: ${maxCombinations.toString()}`); 127 - console.log(`[Shortcode Info] Domain prefix: ${domainPrefix}, URL core: ${urlCore}, Subdomain tail: ${tail}`); 128 + // --- LOGGING MAX COMBINATIONS --- 129 + const maxCombinations = BigInt(BASE) ** BigInt(length); 130 + console.log(`[Shortcode Info] URL: ${url}`); 131 + console.log(`[Shortcode Info] Length: ${length}, Charset: ${BASE} chars`); 132 + console.log(`[Shortcode Info] Max possible combinations: ${maxCombinations.toString()}`); 133 + console.log( 134 + `[Shortcode Info] Domain prefix: ${domainPrefix}, URL core: ${urlCore}, Subdomain tail: ${tail}` 135 + ); 128 136 129 - return out; 137 + return out; 130 138 } 131 139 132 140 export function isValidShortcode(code: string): boolean { 133 - return /^[0-9a-zA-Z]+$/.test(code); 141 + return /^[0-9a-zA-Z]+$/.test(code); 134 142 } 135 143 136 144 export function getMaxCombinations(length: number): number { 137 - return Math.pow(BASE, length); 145 + return Math.pow(BASE, length); 138 146 }
+7 -67
src/routes/+layout.svelte
··· 1 1 <script lang="ts"> 2 + import '../app.css'; 3 + 2 4 let { children } = $props(); 3 5 </script> 4 6 ··· 11 13 12 14 if (prefersDark) { 13 15 htmlElement.classList.add('dark'); 14 - htmlElement.style.colorScheme = 'dark'; 15 16 } else { 16 17 htmlElement.classList.remove('dark'); 17 - htmlElement.style.colorScheme = 'light'; 18 18 } 19 19 })(); 20 20 </script> 21 21 </svelte:head> 22 22 23 - {@render children()} 24 - 25 - <style> 26 - :global(*) { 27 - margin: 0; 28 - padding: 0; 29 - box-sizing: border-box; 30 - } 31 - 32 - :global(html) { 33 - min-height: 100vh; 34 - background: #ffffff; 35 - color: #1a1a1a; 36 - transition: 37 - background-color 0.3s, 38 - color 0.3s; 39 - } 40 - 41 - :global(html.dark) { 42 - background: #1a1a1a; 43 - color: #e0e0e0; 44 - } 45 - 46 - :global(html.dark .info) { 47 - background: #1e3a1e; 48 - border-color: #4caf50; 49 - color: #e0e0e0; 50 - } 51 - 52 - :global(html.dark .error) { 53 - background: #3a1e1e; 54 - border-color: #f44336; 55 - color: #e0e0e0; 56 - } 57 - 58 - :global(html.dark code) { 59 - background: #2a2a2a; 60 - color: #e0e0e0; 61 - } 62 - 63 - :global(html.dark .links a), 64 - :global(html.dark .api li) { 65 - background: #2a2a2a; 66 - } 67 - 68 - :global(html.dark .links a:hover) { 69 - background: #333; 70 - } 71 - 72 - :global(html.dark h1), 73 - :global(html.dark h2) { 74 - color: #e0e0e0; 75 - } 76 - 77 - :global(html.dark .title), 78 - :global(html.dark .api span), 79 - :global(html.dark .empty), 80 - :global(html.dark footer) { 81 - color: #b0b0b0; 82 - } 83 - 84 - :global(html.dark footer) { 85 - border-top-color: #333; 86 - } 87 - </style> 23 + <div 24 + style="min-height: 100vh; background-color: rgb(var(--color-background)); color: rgb(var(--color-text-primary))" 25 + > 26 + {@render children()} 27 + </div>
+166 -210
src/routes/+page.svelte
··· 9 9 <meta name="description" content="A server-side link shortening service powered by Linkat" /> 10 10 </svelte:head> 11 11 12 - <main> 13 - <h1>AT Protocol Link Shortener</h1> 12 + <main class="mx-auto max-w-3xl px-4 py-8 font-sans leading-relaxed"> 13 + <h1 class="mb-8 text-3xl font-bold" style="color: rgb(var(--color-text-primary))"> 14 + AT Protocol Link Shortener 15 + </h1> 14 16 15 - <section> 16 - <h2>Service Status</h2> 17 + <section class="mb-12"> 18 + <h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))"> 19 + Service Status 20 + </h2> 17 21 {#if data.error} 18 - <div class="error"> 19 - <p><strong>โš ๏ธ Configuration Error</strong></p> 20 - <p>{data.error}</p> 22 + <div 23 + class="mb-4 rounded-lg border p-4" 24 + style="background-color: rgb(var(--color-error-bg)); border-color: rgb(var(--color-error-border))" 25 + > 26 + <p class="font-bold" style="color: rgb(var(--color-text-primary))"> 27 + โš ๏ธ Configuration Error 28 + </p> 29 + <p style="color: rgb(var(--color-text-primary))">{data.error}</p> 21 30 {#if data.did === 'NOT_CONFIGURED'} 22 - <div class="help"> 23 - <p><strong>Quick Fix:</strong></p> 24 - <ol> 25 - <li>Create a <code>.env</code> file in your project root</li> 31 + <div class="mt-4 border-t pt-4" style="border-color: rgb(var(--color-error-border))"> 32 + <p class="font-bold" style="color: rgb(var(--color-text-primary))">Quick Fix:</p> 33 + <ol 34 + class="ml-6 mt-2 list-decimal space-y-2 leading-8" 35 + style="color: rgb(var(--color-text-primary))" 36 + > 26 37 <li> 27 - Add: <code>ATPROTO_DID=did:plc:your-did-here</code> 38 + Create a <code 39 + class="rounded px-2 py-1 font-mono text-sm" 40 + style="background-color: rgb(var(--color-code-bg))">. env</code 41 + > file in your project root 28 42 </li> 29 43 <li> 30 - Find your DID at <a href="https://pdsls.dev/" target="_blank">pdsls.dev</a> 44 + Add: <code 45 + class="rounded px-2 py-1 font-mono text-sm" 46 + style="background-color: rgb(var(--color-code-bg))" 47 + >ATPROTO_DID=did:plc:your-did-here</code 48 + > 31 49 </li> 32 - <li>Run <code>npm run test:config</code> to verify</li> 50 + <li> 51 + Find your DID at <a 52 + href="https://pdsls.dev/" 53 + target="_blank" 54 + class="underline" 55 + style="color: rgb(var(--color-error))">pdsls.dev</a 56 + > 57 + </li> 58 + <li> 59 + Run <code 60 + class="rounded px-2 py-1 font-mono text-sm" 61 + style="background-color: rgb(var(--color-code-bg))">npm run test:config</code 62 + > to verify 63 + </li> 33 64 <li>Restart the server</li> 34 65 </ol> 35 66 </div> 36 67 {/if} 37 68 </div> 38 69 {:else} 39 - <div class="info"> 40 - <p>โœ“ Service is running</p> 41 - <p>โœ“ Configured DID: <code>{data.did}</code></p> 42 - <p>โœ“ Active links: {data.linkCount}</p> 70 + <div 71 + class="mb-4 rounded-lg border p-4" 72 + style="background-color: rgb(var(--color-success-bg)); border-color: rgb(var(--color-success-border))" 73 + > 74 + <p style="color: rgb(var(--color-text-primary))">โœ“ Service is running</p> 75 + <p style="color: rgb(var(--color-text-primary))"> 76 + โœ“ Configured DID: <code 77 + class="rounded px-2 py-1 font-mono text-sm" 78 + style="background-color: rgb(var(--color-code-bg))">{data.did}</code 79 + > 80 + </p> 81 + <p style="color: rgb(var(--color-text-primary))">โœ“ Active links: {data.linkCount}</p> 43 82 </div> 44 83 {/if} 45 84 </section> 46 85 47 - <section> 48 - <h2>Available Short Links</h2> 86 + <section class="mb-12"> 87 + <h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))"> 88 + Available Short Links 89 + </h2> 49 90 {#if data.links && data.links.length > 0} 50 - <ul class="links"> 91 + <ul class="m-0 list-none p-0"> 51 92 {#each data.links as link} 52 - <li> 53 - <a href="/{link.shortcode}"> 93 + <li class="mb-2"> 94 + <a 95 + href="/{link.shortcode}" 96 + class="flex items-center gap-3 rounded-lg p-3 no-underline transition-colors" 97 + style="background-color: rgb(var(--color-surface)); color: rgb(var(--color-text-primary))" 98 + onmouseenter={(e) => { 99 + e.currentTarget.style.backgroundColor = `rgb(var(--color-surface-elevated))`; 100 + }} 101 + onmouseleave={(e) => { 102 + e.currentTarget.style.backgroundColor = `rgb(var(--color-surface))`; 103 + }} 104 + > 54 105 {#if link.emoji} 55 - <span class="emoji">{link.emoji}</span> 106 + <span class="shrink-0 text-2xl">{link.emoji}</span> 56 107 {/if} 57 - <code>/{link.shortcode}</code> 58 - <span class="title">{link.title}</span> 108 + <code 109 + class="rounded px-2 py-1 font-mono text-sm" 110 + style="background-color: rgb(var(--color-code-bg))">/{link.shortcode}</code 111 + > 112 + <span style="color: rgb(var(--color-text-secondary))">{link.title}</span> 59 113 </a> 60 114 </li> 61 115 {/each} 62 116 </ul> 63 117 {:else if !data.error} 64 - <p class="empty"> 118 + <p class="italic" style="color: rgb(var(--color-text-secondary))"> 65 119 No short links configured yet. Add links to your <a 66 120 href="https://linkat.blue" 67 - target="_blank">Linkat board</a 121 + target="_blank" 122 + class="no-underline hover:underline" 123 + style="color: rgb(var(--color-primary))" 124 + onmouseenter={(e) => { 125 + e.currentTarget.style.color = `rgb(var(--color-primary-hover))`; 126 + }} 127 + onmouseleave={(e) => { 128 + e.currentTarget.style.color = `rgb(var(--color-primary))`; 129 + }}>Linkat board</a 68 130 >! 69 131 </p> 70 132 {/if} 71 133 </section> 72 134 73 - <section> 74 - <h2>API Endpoints</h2> 75 - <ul class="api"> 76 - <li> 77 - <a href="/api/links"> 78 - <code>GET /api/links</code> 79 - <span>List all short links (JSON)</span> 135 + <section class="mb-12"> 136 + <h2 class="mb-4 mt-8 text-2xl font-semibold" style="color: rgb(var(--color-text-primary))"> 137 + API Endpoints 138 + </h2> 139 + <ul class="m-0 list-none p-0"> 140 + <li class="mb-3 rounded-lg p-3" style="background-color: rgb(var(--color-surface))"> 141 + <a 142 + href="/api/links" 143 + class="flex items-center gap-4 no-underline hover:underline" 144 + style="color: rgb(var(--color-text-primary))" 145 + > 146 + <code 147 + class="rounded px-2 py-1 font-mono text-sm" 148 + style="background-color: rgb(var(--color-code-bg))">GET /api/links</code 149 + > 150 + <span style="color: rgb(var(--color-text-secondary))">List all short links (JSON)</span> 80 151 </a> 81 152 </li> 82 - <li> 83 - <code>GET /:shortcode</code> 84 - <span>Redirect to target URL (301)</span> 153 + <li class="mb-3 rounded-lg p-3" style="background-color: rgb(var(--color-surface))"> 154 + <div class="flex items-center gap-4" style="color: rgb(var(--color-text-primary))"> 155 + <code 156 + class="rounded px-2 py-1 font-mono text-sm" 157 + style="background-color: rgb(var(--color-code-bg))">GET /:shortcode</code 158 + > 159 + <span style="color: rgb(var(--color-text-secondary))">Redirect to target URL (301)</span> 160 + </div> 85 161 </li> 86 162 </ul> 87 163 </section> 88 164 89 - <footer> 90 - <p> 91 - Powered by <a href="https://linkat.blue" target="_blank" rel="noopener noreferrer">Linkat</a>, 92 - <a href="https://atproto.com" target="_blank" rel="noopener noreferrer">AT Protocol</a>, and 93 - <a href="https://slingshot.microcosm.blue" target="_blank" rel="noopener noreferrer" 94 - >Slingshot</a 165 + <footer 166 + class="mt-16 border-t pt-8 text-center text-sm" 167 + style="border-color: rgb(var(--color-border)); color: rgb(var(--color-text-secondary))" 168 + > 169 + <p class="mb-2"> 170 + Powered by <a 171 + href="https://linkat.blue" 172 + target="_blank" 173 + rel="noopener noreferrer" 174 + class="no-underline hover:underline" 175 + style="color: rgb(var(--color-primary))" 176 + onmouseenter={(e) => { 177 + e.currentTarget.style.color = `rgb(var(--color-primary-hover))`; 178 + }} 179 + onmouseleave={(e) => { 180 + e.currentTarget.style.color = `rgb(var(--color-primary))`; 181 + }}>Linkat</a 182 + >, 183 + <a 184 + href="https://atproto.com" 185 + target="_blank" 186 + rel="noopener noreferrer" 187 + class="no-underline hover:underline" 188 + style="color: rgb(var(--color-primary))" 189 + onmouseenter={(e) => { 190 + e.currentTarget.style.color = `rgb(var(--color-primary-hover))`; 191 + }} 192 + onmouseleave={(e) => { 193 + e.currentTarget.style.color = `rgb(var(--color-primary))`; 194 + }}>AT Protocol</a 195 + >, and 196 + <a 197 + href="https://slingshot.microcosm.blue" 198 + target="_blank" 199 + rel="noopener noreferrer" 200 + class="no-underline hover:underline" 201 + style="color: rgb(var(--color-primary))" 202 + onmouseenter={(e) => { 203 + e.currentTarget.style.color = `rgb(var(--color-primary-hover))`; 204 + }} 205 + onmouseleave={(e) => { 206 + e.currentTarget.style.color = `rgb(var(--color-primary))`; 207 + }}>Slingshot</a 95 208 > 96 209 </p> 97 210 <p> 98 211 <a 99 212 href="https://github.com/ewanc26/atproto-shortlink" 100 213 target="_blank" 101 - rel="noopener noreferrer">Source Code on GitHub</a 214 + rel="noopener noreferrer" 215 + class="no-underline hover:underline" 216 + style="color: rgb(var(--color-primary))" 217 + onmouseenter={(e) => { 218 + e.currentTarget.style.color = `rgb(var(--color-primary-hover))`; 219 + }} 220 + onmouseleave={(e) => { 221 + e.currentTarget.style.color = `rgb(var(--color-primary))`; 222 + }}>Source Code on GitHub</a 102 223 > 103 224 </p> 104 225 </footer> 105 226 </main> 106 - 107 - <style> 108 - main { 109 - max-width: 800px; 110 - margin: 0 auto; 111 - padding: 2rem 1rem; 112 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 113 - line-height: 1.6; 114 - } 115 - 116 - h1 { 117 - font-size: 2rem; 118 - margin-bottom: 2rem; 119 - color: #1a1a1a; 120 - } 121 - 122 - h2 { 123 - font-size: 1.5rem; 124 - margin-top: 2rem; 125 - margin-bottom: 1rem; 126 - color: #333; 127 - } 128 - 129 - section { 130 - margin-bottom: 3rem; 131 - } 132 - 133 - .info, 134 - .error { 135 - padding: 1rem; 136 - border-radius: 0.5rem; 137 - margin-bottom: 1rem; 138 - } 139 - 140 - .info { 141 - background: #e8f5e9; 142 - border: 1px solid #4caf50; 143 - } 144 - 145 - .error { 146 - background: #ffebee; 147 - border: 1px solid #f44336; 148 - } 149 - 150 - .help { 151 - margin-top: 1rem; 152 - padding-top: 1rem; 153 - border-top: 1px solid rgba(0, 0, 0, 0.1); 154 - } 155 - 156 - .help ol { 157 - margin-left: 1.5rem; 158 - margin-top: 0.5rem; 159 - line-height: 1.8; 160 - } 161 - 162 - .help a { 163 - color: #d32f2f; 164 - text-decoration: underline; 165 - } 166 - 167 - code { 168 - background: #f5f5f5; 169 - padding: 0.2rem 0.4rem; 170 - border-radius: 0.25rem; 171 - font-family: 'Courier New', monospace; 172 - font-size: 0.9rem; 173 - } 174 - 175 - .links { 176 - list-style: none; 177 - padding: 0; 178 - margin: 0; 179 - } 180 - 181 - .links li { 182 - margin-bottom: 0.5rem; 183 - } 184 - 185 - .links a { 186 - display: flex; 187 - align-items: center; 188 - gap: 0.75rem; 189 - padding: 0.75rem; 190 - background: #f8f9fa; 191 - border-radius: 0.5rem; 192 - text-decoration: none; 193 - color: inherit; 194 - transition: background 0.2s; 195 - } 196 - 197 - .links a:hover { 198 - background: #e9ecef; 199 - } 200 - 201 - .emoji { 202 - font-size: 1.5rem; 203 - flex-shrink: 0; 204 - } 205 - 206 - .title { 207 - color: #666; 208 - } 209 - 210 - .empty { 211 - color: #666; 212 - font-style: italic; 213 - } 214 - 215 - .empty a { 216 - color: #0066cc; 217 - text-decoration: none; 218 - } 219 - 220 - .empty a:hover { 221 - text-decoration: underline; 222 - } 223 - 224 - .api { 225 - list-style: none; 226 - padding: 0; 227 - margin: 0; 228 - } 229 - 230 - .api li { 231 - margin-bottom: 0.75rem; 232 - padding: 0.75rem; 233 - background: #f8f9fa; 234 - border-radius: 0.5rem; 235 - } 236 - 237 - .api a { 238 - display: flex; 239 - align-items: center; 240 - gap: 1rem; 241 - text-decoration: none; 242 - color: inherit; 243 - } 244 - 245 - .api a:hover { 246 - text-decoration: underline; 247 - } 248 - 249 - .api span { 250 - color: #666; 251 - } 252 - 253 - footer { 254 - margin-top: 4rem; 255 - padding-top: 2rem; 256 - border-top: 1px solid #e0e0e0; 257 - text-align: center; 258 - color: #666; 259 - font-size: 0.9rem; 260 - } 261 - 262 - footer a { 263 - color: #0066cc; 264 - text-decoration: none; 265 - } 266 - 267 - footer a:hover { 268 - text-decoration: underline; 269 - } 270 - </style>
+74 -11
src/routes/[shortcode]/+server.ts
··· 14 14 console.warn(`[Redirect] Shortcode not found: ${shortcode}`); 15 15 return new Response( 16 16 `<!DOCTYPE html> 17 - <html lang="en"> 17 + <html lang="en" class="h-full"> 18 18 <head> 19 19 <meta charset="UTF-8"> 20 20 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 21 21 <title>Link Not Found - AT Protocol Link Shortener</title> 22 22 <style> 23 + /* Inline critical CSS for semantic colors */ 24 + :root { 25 + --color-bg: 255 255 255; 26 + --color-text: 15 23 42; 27 + --color-text-secondary: 71 85 105; 28 + --color-surface: 248 250 252; 29 + --color-primary: 59 130 246; 30 + } 31 + 32 + @media (prefers-color-scheme: dark) { 33 + :root { 34 + --color-bg: 15 23 42; 35 + --color-text: 248 250 252; 36 + --color-text-secondary: 203 213 225; 37 + --color-surface: 30 41 59; 38 + --color-primary: 96 165 250; 39 + } 40 + } 41 + 23 42 body { 43 + margin: 0; 24 44 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; 45 + background: rgb(var(--color-bg)); 46 + color: rgb(var(--color-text)); 47 + display: flex; 48 + align-items: center; 49 + justify-content: center; 50 + min-height: 100vh; 51 + } 52 + 53 + .container { 25 54 max-width: 600px; 26 - margin: 4rem auto; 27 55 padding: 2rem; 28 56 text-align: center; 29 57 } 30 - h1 { font-size: 3rem; margin-bottom: 1rem; } 31 - p { color: #666; line-height: 1.6; margin-bottom: 1rem; } 32 - code { background: #f5f5f5; padding: 0.2rem 0.4rem; border-radius: 0.25rem; } 33 - a { color: #0066cc; text-decoration: none; } 34 - a:hover { text-decoration: underline; } 58 + 59 + h1 { 60 + font-size: 3rem; 61 + margin-bottom: 1rem; 62 + } 63 + 64 + h2 { 65 + font-size: 1.5rem; 66 + font-weight: 600; 67 + margin-bottom: 1rem; 68 + } 69 + 70 + p { 71 + color: rgb(var(--color-text-secondary)); 72 + line-height: 1.6; 73 + margin-bottom: 1rem; 74 + } 75 + 76 + code { 77 + background: rgb(var(--color-surface)); 78 + padding: 0.2rem 0.5rem; 79 + border-radius: 0.25rem; 80 + font-family: 'Courier New', monospace; 81 + font-size: 0.875rem; 82 + } 83 + 84 + a { 85 + color: rgb(var(--color-primary)); 86 + text-decoration: none; 87 + } 88 + 89 + a:hover { 90 + text-decoration: underline; 91 + } 35 92 </style> 36 93 </head> 37 94 <body> 38 - <h1>๐Ÿ” 404</h1> 39 - <h2>Short Link Not Found</h2> 40 - <p>The short link <code>/${shortcode}</code> doesn't exist.</p> 41 - <p><a href="/">โ† View all available links</a></p> 95 + <div class="container"> 96 + <h1>๐Ÿ” 404</h1> 97 + <h2>Short Link Not Found</h2> 98 + <p> 99 + The short link <code>/${shortcode}</code> doesn't exist. 100 + </p> 101 + <p> 102 + <a href="/">โ† View all available links</a> 103 + </p> 104 + </div> 42 105 </body> 43 106 </html>`, 44 107 {
+2 -1
vite.config.ts
··· 1 1 import { sveltekit } from '@sveltejs/kit/vite'; 2 + import tailwindcss from '@tailwindcss/vite'; 2 3 import { defineConfig } from 'vite'; 3 4 4 5 export default defineConfig({ 5 - plugins: [sveltekit()] 6 + plugins: [sveltekit(), tailwindcss()] 6 7 });