+19
README.md
+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
+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
+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
+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
+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
+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
+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
+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
{