https://domlink.deployments.hotsocket.fyi/

Compare changes

Choose any two refs to compare.

+12
.envrc
··· 1 + #!/usr/bin/env bash 2 + # ^ added for shellcheck and file-type detection 3 + 4 + # Watch & reload direnv on change 5 + watch_file devshell.toml 6 + 7 + if [[ $(type -t use_flake) != function ]]; then 8 + echo "ERROR: use_flake function missing." 9 + echo "Please update direnv to v2.30.0 or later." 10 + exit 1 11 + fi 12 + use flake
+1
.gitignore
··· 1 1 .vscode 2 + .direnv 2 3 out 3 4 node_modules 4 5 certs
+14
deno.json
··· 1 + { 2 + "compilerOptions": { 3 + "lib": [ 4 + "dom", 5 + "es2017", 6 + "dom.iterable", 7 + "deno.ns" 8 + ] 9 + }, 10 + "exclude": [ 11 + "node_modules", 12 + "out" 13 + ] 14 + }
+757
deno.lock
··· 1 + { 2 + "version": "5", 3 + "specifiers": { 4 + "npm:@atcute/atproto@^3.1.1": "3.1.2", 5 + "npm:@atcute/bluesky@^3.2.0": "3.2.1", 6 + "npm:@atcute/client@^4.0.3": "4.0.3", 7 + "npm:@atcute/identity-resolver@^1.1.3": "1.1.3_@atcute+identity@1.1.0", 8 + "npm:@atcute/identity@^1.0.2": "1.1.0", 9 + "npm:copyfiles@^2.4.1": "2.4.1", 10 + "npm:esbuild@~0.25.8": "0.25.9", 11 + "npm:http-server@^14.1.1": "14.1.1", 12 + "npm:typescript@^5.8.3": "5.9.2" 13 + }, 14 + "npm": { 15 + "@atcute/atproto@3.1.2": { 16 + "integrity": "sha512-m6OWoGTeL8Wlw7cm8Nrf+NU24rlx5A0DzxEQQPPu1wqYNHw6S33nOU90W+U09vVN0aZGZUrLFfWWhAaSWTp1ZQ==", 17 + "dependencies": [ 18 + "@atcute/lexicons" 19 + ] 20 + }, 21 + "@atcute/bluesky@3.2.1": { 22 + "integrity": "sha512-WehZuGNEC9NNB2y7Jg/i4ANzbMEQzs2EwrLYKWDuvJT3YnPHEol3vkc4DK47TODs3TiacQJp4MWhsQyfktsZ6g==", 23 + "dependencies": [ 24 + "@atcute/atproto", 25 + "@atcute/lexicons" 26 + ] 27 + }, 28 + "@atcute/client@4.0.3": { 29 + "integrity": "sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==", 30 + "dependencies": [ 31 + "@atcute/identity", 32 + "@atcute/lexicons" 33 + ] 34 + }, 35 + "@atcute/identity-resolver@1.1.3_@atcute+identity@1.1.0": { 36 + "integrity": "sha512-KZgGgg99CWaV7Df3+h3X/WMrDzTPQVfsaoIVbTNLx2B56BvCL2EmaxPSVw/7BFUJMZHlVU4rtoEB4lyvNyMswA==", 37 + "dependencies": [ 38 + "@atcute/identity", 39 + "@atcute/lexicons", 40 + "@atcute/util-fetch", 41 + "@badrap/valita" 42 + ] 43 + }, 44 + "@atcute/identity@1.1.0": { 45 + "integrity": "sha512-6vRvRqJatDB+JUQsb+UswYmtBGQnSZcqC3a2y6H5DB/v5KcIh+6nFFtc17G0+3W9rxdk7k9M4KkgkdKf/YDNoQ==", 46 + "dependencies": [ 47 + "@atcute/lexicons", 48 + "@badrap/valita" 49 + ] 50 + }, 51 + "@atcute/lexicons@1.1.1": { 52 + "integrity": "sha512-k6qy5p3j9fJJ6ekaMPfEfp3ni4TW/XNuH9ZmsuwC0fi0tOjp+Fa8ZQakHwnqOzFt/cVBfGcmYE/lKNAbeTjgUg==", 53 + "dependencies": [ 54 + "esm-env" 55 + ] 56 + }, 57 + "@atcute/util-fetch@1.0.1": { 58 + "integrity": "sha512-Clc0E/5ufyGBVfYBUwWNlHONlZCoblSr4Ho50l1LhmRPGB1Wu/AQ9Sz+rsBg7fdaW/auve8ulmwhRhnX2cGRow==", 59 + "dependencies": [ 60 + "@badrap/valita" 61 + ] 62 + }, 63 + "@badrap/valita@0.4.6": { 64 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==" 65 + }, 66 + "@esbuild/aix-ppc64@0.25.9": { 67 + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", 68 + "os": ["aix"], 69 + "cpu": ["ppc64"] 70 + }, 71 + "@esbuild/android-arm64@0.25.9": { 72 + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", 73 + "os": ["android"], 74 + "cpu": ["arm64"] 75 + }, 76 + "@esbuild/android-arm@0.25.9": { 77 + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", 78 + "os": ["android"], 79 + "cpu": ["arm"] 80 + }, 81 + "@esbuild/android-x64@0.25.9": { 82 + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", 83 + "os": ["android"], 84 + "cpu": ["x64"] 85 + }, 86 + "@esbuild/darwin-arm64@0.25.9": { 87 + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", 88 + "os": ["darwin"], 89 + "cpu": ["arm64"] 90 + }, 91 + "@esbuild/darwin-x64@0.25.9": { 92 + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", 93 + "os": ["darwin"], 94 + "cpu": ["x64"] 95 + }, 96 + "@esbuild/freebsd-arm64@0.25.9": { 97 + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", 98 + "os": ["freebsd"], 99 + "cpu": ["arm64"] 100 + }, 101 + "@esbuild/freebsd-x64@0.25.9": { 102 + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", 103 + "os": ["freebsd"], 104 + "cpu": ["x64"] 105 + }, 106 + "@esbuild/linux-arm64@0.25.9": { 107 + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", 108 + "os": ["linux"], 109 + "cpu": ["arm64"] 110 + }, 111 + "@esbuild/linux-arm@0.25.9": { 112 + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", 113 + "os": ["linux"], 114 + "cpu": ["arm"] 115 + }, 116 + "@esbuild/linux-ia32@0.25.9": { 117 + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", 118 + "os": ["linux"], 119 + "cpu": ["ia32"] 120 + }, 121 + "@esbuild/linux-loong64@0.25.9": { 122 + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", 123 + "os": ["linux"], 124 + "cpu": ["loong64"] 125 + }, 126 + "@esbuild/linux-mips64el@0.25.9": { 127 + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", 128 + "os": ["linux"], 129 + "cpu": ["mips64el"] 130 + }, 131 + "@esbuild/linux-ppc64@0.25.9": { 132 + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", 133 + "os": ["linux"], 134 + "cpu": ["ppc64"] 135 + }, 136 + "@esbuild/linux-riscv64@0.25.9": { 137 + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", 138 + "os": ["linux"], 139 + "cpu": ["riscv64"] 140 + }, 141 + "@esbuild/linux-s390x@0.25.9": { 142 + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", 143 + "os": ["linux"], 144 + "cpu": ["s390x"] 145 + }, 146 + "@esbuild/linux-x64@0.25.9": { 147 + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", 148 + "os": ["linux"], 149 + "cpu": ["x64"] 150 + }, 151 + "@esbuild/netbsd-arm64@0.25.9": { 152 + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", 153 + "os": ["netbsd"], 154 + "cpu": ["arm64"] 155 + }, 156 + "@esbuild/netbsd-x64@0.25.9": { 157 + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", 158 + "os": ["netbsd"], 159 + "cpu": ["x64"] 160 + }, 161 + "@esbuild/openbsd-arm64@0.25.9": { 162 + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", 163 + "os": ["openbsd"], 164 + "cpu": ["arm64"] 165 + }, 166 + "@esbuild/openbsd-x64@0.25.9": { 167 + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", 168 + "os": ["openbsd"], 169 + "cpu": ["x64"] 170 + }, 171 + "@esbuild/openharmony-arm64@0.25.9": { 172 + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", 173 + "os": ["openharmony"], 174 + "cpu": ["arm64"] 175 + }, 176 + "@esbuild/sunos-x64@0.25.9": { 177 + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", 178 + "os": ["sunos"], 179 + "cpu": ["x64"] 180 + }, 181 + "@esbuild/win32-arm64@0.25.9": { 182 + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", 183 + "os": ["win32"], 184 + "cpu": ["arm64"] 185 + }, 186 + "@esbuild/win32-ia32@0.25.9": { 187 + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", 188 + "os": ["win32"], 189 + "cpu": ["ia32"] 190 + }, 191 + "@esbuild/win32-x64@0.25.9": { 192 + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", 193 + "os": ["win32"], 194 + "cpu": ["x64"] 195 + }, 196 + "ansi-regex@5.0.1": { 197 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" 198 + }, 199 + "ansi-styles@4.3.0": { 200 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 201 + "dependencies": [ 202 + "color-convert" 203 + ] 204 + }, 205 + "async@3.2.6": { 206 + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" 207 + }, 208 + "balanced-match@1.0.2": { 209 + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 210 + }, 211 + "basic-auth@2.0.1": { 212 + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", 213 + "dependencies": [ 214 + "safe-buffer" 215 + ] 216 + }, 217 + "brace-expansion@1.1.12": { 218 + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 219 + "dependencies": [ 220 + "balanced-match", 221 + "concat-map" 222 + ] 223 + }, 224 + "call-bind-apply-helpers@1.0.2": { 225 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 226 + "dependencies": [ 227 + "es-errors", 228 + "function-bind" 229 + ] 230 + }, 231 + "call-bound@1.0.4": { 232 + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 233 + "dependencies": [ 234 + "call-bind-apply-helpers", 235 + "get-intrinsic" 236 + ] 237 + }, 238 + "chalk@4.1.2": { 239 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 240 + "dependencies": [ 241 + "ansi-styles", 242 + "supports-color" 243 + ] 244 + }, 245 + "cliui@7.0.4": { 246 + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 247 + "dependencies": [ 248 + "string-width", 249 + "strip-ansi", 250 + "wrap-ansi" 251 + ] 252 + }, 253 + "color-convert@2.0.1": { 254 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 255 + "dependencies": [ 256 + "color-name" 257 + ] 258 + }, 259 + "color-name@1.1.4": { 260 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 261 + }, 262 + "concat-map@0.0.1": { 263 + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 264 + }, 265 + "copyfiles@2.4.1": { 266 + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", 267 + "dependencies": [ 268 + "glob", 269 + "minimatch", 270 + "mkdirp", 271 + "noms", 272 + "through2", 273 + "untildify", 274 + "yargs" 275 + ], 276 + "bin": true 277 + }, 278 + "core-util-is@1.0.3": { 279 + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 280 + }, 281 + "corser@2.0.1": { 282 + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==" 283 + }, 284 + "debug@4.4.1": { 285 + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 286 + "dependencies": [ 287 + "ms" 288 + ] 289 + }, 290 + "dunder-proto@1.0.1": { 291 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 292 + "dependencies": [ 293 + "call-bind-apply-helpers", 294 + "es-errors", 295 + "gopd" 296 + ] 297 + }, 298 + "emoji-regex@8.0.0": { 299 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 300 + }, 301 + "es-define-property@1.0.1": { 302 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" 303 + }, 304 + "es-errors@1.3.0": { 305 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" 306 + }, 307 + "es-object-atoms@1.1.1": { 308 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 309 + "dependencies": [ 310 + "es-errors" 311 + ] 312 + }, 313 + "esbuild@0.25.9": { 314 + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", 315 + "optionalDependencies": [ 316 + "@esbuild/aix-ppc64", 317 + "@esbuild/android-arm", 318 + "@esbuild/android-arm64", 319 + "@esbuild/android-x64", 320 + "@esbuild/darwin-arm64", 321 + "@esbuild/darwin-x64", 322 + "@esbuild/freebsd-arm64", 323 + "@esbuild/freebsd-x64", 324 + "@esbuild/linux-arm", 325 + "@esbuild/linux-arm64", 326 + "@esbuild/linux-ia32", 327 + "@esbuild/linux-loong64", 328 + "@esbuild/linux-mips64el", 329 + "@esbuild/linux-ppc64", 330 + "@esbuild/linux-riscv64", 331 + "@esbuild/linux-s390x", 332 + "@esbuild/linux-x64", 333 + "@esbuild/netbsd-arm64", 334 + "@esbuild/netbsd-x64", 335 + "@esbuild/openbsd-arm64", 336 + "@esbuild/openbsd-x64", 337 + "@esbuild/openharmony-arm64", 338 + "@esbuild/sunos-x64", 339 + "@esbuild/win32-arm64", 340 + "@esbuild/win32-ia32", 341 + "@esbuild/win32-x64" 342 + ], 343 + "scripts": true, 344 + "bin": true 345 + }, 346 + "escalade@3.2.0": { 347 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" 348 + }, 349 + "esm-env@1.2.2": { 350 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" 351 + }, 352 + "eventemitter3@4.0.7": { 353 + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" 354 + }, 355 + "follow-redirects@1.15.11": { 356 + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==" 357 + }, 358 + "fs.realpath@1.0.0": { 359 + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" 360 + }, 361 + "function-bind@1.1.2": { 362 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" 363 + }, 364 + "get-caller-file@2.0.5": { 365 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 366 + }, 367 + "get-intrinsic@1.3.0": { 368 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 369 + "dependencies": [ 370 + "call-bind-apply-helpers", 371 + "es-define-property", 372 + "es-errors", 373 + "es-object-atoms", 374 + "function-bind", 375 + "get-proto", 376 + "gopd", 377 + "has-symbols", 378 + "hasown", 379 + "math-intrinsics" 380 + ] 381 + }, 382 + "get-proto@1.0.1": { 383 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 384 + "dependencies": [ 385 + "dunder-proto", 386 + "es-object-atoms" 387 + ] 388 + }, 389 + "glob@7.2.3": { 390 + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 391 + "dependencies": [ 392 + "fs.realpath", 393 + "inflight", 394 + "inherits", 395 + "minimatch", 396 + "once", 397 + "path-is-absolute" 398 + ], 399 + "deprecated": true 400 + }, 401 + "gopd@1.2.0": { 402 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" 403 + }, 404 + "has-flag@4.0.0": { 405 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 406 + }, 407 + "has-symbols@1.1.0": { 408 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" 409 + }, 410 + "hasown@2.0.2": { 411 + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 412 + "dependencies": [ 413 + "function-bind" 414 + ] 415 + }, 416 + "he@1.2.0": { 417 + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 418 + "bin": true 419 + }, 420 + "html-encoding-sniffer@3.0.0": { 421 + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 422 + "dependencies": [ 423 + "whatwg-encoding" 424 + ] 425 + }, 426 + "http-proxy@1.18.1": { 427 + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", 428 + "dependencies": [ 429 + "eventemitter3", 430 + "follow-redirects", 431 + "requires-port" 432 + ] 433 + }, 434 + "http-server@14.1.1": { 435 + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", 436 + "dependencies": [ 437 + "basic-auth", 438 + "chalk", 439 + "corser", 440 + "he", 441 + "html-encoding-sniffer", 442 + "http-proxy", 443 + "mime", 444 + "minimist", 445 + "opener", 446 + "portfinder", 447 + "secure-compare", 448 + "union", 449 + "url-join" 450 + ], 451 + "bin": true 452 + }, 453 + "iconv-lite@0.6.3": { 454 + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 455 + "dependencies": [ 456 + "safer-buffer" 457 + ] 458 + }, 459 + "inflight@1.0.6": { 460 + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 461 + "dependencies": [ 462 + "once", 463 + "wrappy" 464 + ], 465 + "deprecated": true 466 + }, 467 + "inherits@2.0.4": { 468 + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 469 + }, 470 + "is-fullwidth-code-point@3.0.0": { 471 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 472 + }, 473 + "isarray@0.0.1": { 474 + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" 475 + }, 476 + "isarray@1.0.0": { 477 + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 478 + }, 479 + "math-intrinsics@1.1.0": { 480 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" 481 + }, 482 + "mime@1.6.0": { 483 + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 484 + "bin": true 485 + }, 486 + "minimatch@3.1.2": { 487 + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 488 + "dependencies": [ 489 + "brace-expansion" 490 + ] 491 + }, 492 + "minimist@1.2.8": { 493 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" 494 + }, 495 + "mkdirp@1.0.4": { 496 + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", 497 + "bin": true 498 + }, 499 + "ms@2.1.3": { 500 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 501 + }, 502 + "noms@0.0.0": { 503 + "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", 504 + "dependencies": [ 505 + "inherits", 506 + "readable-stream@1.0.34" 507 + ] 508 + }, 509 + "object-inspect@1.13.4": { 510 + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" 511 + }, 512 + "once@1.4.0": { 513 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 514 + "dependencies": [ 515 + "wrappy" 516 + ] 517 + }, 518 + "opener@1.5.2": { 519 + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", 520 + "bin": true 521 + }, 522 + "path-is-absolute@1.0.1": { 523 + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" 524 + }, 525 + "portfinder@1.0.37": { 526 + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", 527 + "dependencies": [ 528 + "async", 529 + "debug" 530 + ] 531 + }, 532 + "process-nextick-args@2.0.1": { 533 + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 534 + }, 535 + "qs@6.14.0": { 536 + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 537 + "dependencies": [ 538 + "side-channel" 539 + ] 540 + }, 541 + "readable-stream@1.0.34": { 542 + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", 543 + "dependencies": [ 544 + "core-util-is", 545 + "inherits", 546 + "isarray@0.0.1", 547 + "string_decoder@0.10.31" 548 + ] 549 + }, 550 + "readable-stream@2.3.8": { 551 + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 552 + "dependencies": [ 553 + "core-util-is", 554 + "inherits", 555 + "isarray@1.0.0", 556 + "process-nextick-args", 557 + "safe-buffer", 558 + "string_decoder@1.1.1", 559 + "util-deprecate" 560 + ] 561 + }, 562 + "require-directory@2.1.1": { 563 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" 564 + }, 565 + "requires-port@1.0.0": { 566 + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" 567 + }, 568 + "safe-buffer@5.1.2": { 569 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 570 + }, 571 + "safer-buffer@2.1.2": { 572 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 573 + }, 574 + "secure-compare@3.0.1": { 575 + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" 576 + }, 577 + "side-channel-list@1.0.0": { 578 + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 579 + "dependencies": [ 580 + "es-errors", 581 + "object-inspect" 582 + ] 583 + }, 584 + "side-channel-map@1.0.1": { 585 + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 586 + "dependencies": [ 587 + "call-bound", 588 + "es-errors", 589 + "get-intrinsic", 590 + "object-inspect" 591 + ] 592 + }, 593 + "side-channel-weakmap@1.0.2": { 594 + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 595 + "dependencies": [ 596 + "call-bound", 597 + "es-errors", 598 + "get-intrinsic", 599 + "object-inspect", 600 + "side-channel-map" 601 + ] 602 + }, 603 + "side-channel@1.1.0": { 604 + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 605 + "dependencies": [ 606 + "es-errors", 607 + "object-inspect", 608 + "side-channel-list", 609 + "side-channel-map", 610 + "side-channel-weakmap" 611 + ] 612 + }, 613 + "string-width@4.2.3": { 614 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 615 + "dependencies": [ 616 + "emoji-regex", 617 + "is-fullwidth-code-point", 618 + "strip-ansi" 619 + ] 620 + }, 621 + "string_decoder@0.10.31": { 622 + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" 623 + }, 624 + "string_decoder@1.1.1": { 625 + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 626 + "dependencies": [ 627 + "safe-buffer" 628 + ] 629 + }, 630 + "strip-ansi@6.0.1": { 631 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 632 + "dependencies": [ 633 + "ansi-regex" 634 + ] 635 + }, 636 + "supports-color@7.2.0": { 637 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 638 + "dependencies": [ 639 + "has-flag" 640 + ] 641 + }, 642 + "through2@2.0.5": { 643 + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", 644 + "dependencies": [ 645 + "readable-stream@2.3.8", 646 + "xtend" 647 + ] 648 + }, 649 + "typescript@5.9.2": { 650 + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", 651 + "bin": true 652 + }, 653 + "union@0.5.0": { 654 + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", 655 + "dependencies": [ 656 + "qs" 657 + ] 658 + }, 659 + "untildify@4.0.0": { 660 + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==" 661 + }, 662 + "url-join@4.0.1": { 663 + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" 664 + }, 665 + "util-deprecate@1.0.2": { 666 + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 667 + }, 668 + "whatwg-encoding@2.0.0": { 669 + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 670 + "dependencies": [ 671 + "iconv-lite" 672 + ] 673 + }, 674 + "wrap-ansi@7.0.0": { 675 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 676 + "dependencies": [ 677 + "ansi-styles", 678 + "string-width", 679 + "strip-ansi" 680 + ] 681 + }, 682 + "wrappy@1.0.2": { 683 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 684 + }, 685 + "xtend@4.0.2": { 686 + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 687 + }, 688 + "y18n@5.0.8": { 689 + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" 690 + }, 691 + "yargs-parser@20.2.9": { 692 + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" 693 + }, 694 + "yargs@16.2.0": { 695 + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 696 + "dependencies": [ 697 + "cliui", 698 + "escalade", 699 + "get-caller-file", 700 + "require-directory", 701 + "string-width", 702 + "y18n", 703 + "yargs-parser" 704 + ] 705 + } 706 + }, 707 + "remote": { 708 + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", 709 + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", 710 + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", 711 + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", 712 + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", 713 + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", 714 + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", 715 + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", 716 + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", 717 + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", 718 + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", 719 + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", 720 + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", 721 + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", 722 + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", 723 + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", 724 + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", 725 + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", 726 + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", 727 + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", 728 + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", 729 + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", 730 + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", 731 + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", 732 + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", 733 + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", 734 + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", 735 + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", 736 + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", 737 + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", 738 + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", 739 + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", 740 + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e" 741 + }, 742 + "workspace": { 743 + "packageJson": { 744 + "dependencies": [ 745 + "npm:@atcute/atproto@^3.1.1", 746 + "npm:@atcute/bluesky@^3.2.0", 747 + "npm:@atcute/client@^4.0.3", 748 + "npm:@atcute/identity-resolver@^1.1.3", 749 + "npm:@atcute/identity@^1.0.2", 750 + "npm:copyfiles@^2.4.1", 751 + "npm:esbuild@~0.25.8", 752 + "npm:http-server@^14.1.1", 753 + "npm:typescript@^5.8.3" 754 + ] 755 + } 756 + } 757 + }
+61
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-utils": { 4 + "inputs": { 5 + "systems": "systems" 6 + }, 7 + "locked": { 8 + "lastModified": 1731533236, 9 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 + "owner": "numtide", 11 + "repo": "flake-utils", 12 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 + "type": "github" 14 + }, 15 + "original": { 16 + "owner": "numtide", 17 + "repo": "flake-utils", 18 + "type": "github" 19 + } 20 + }, 21 + "nixpkgs": { 22 + "locked": { 23 + "lastModified": 1755615617, 24 + "narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=", 25 + "owner": "NixOS", 26 + "repo": "nixpkgs", 27 + "rev": "20075955deac2583bb12f07151c2df830ef346b4", 28 + "type": "github" 29 + }, 30 + "original": { 31 + "owner": "NixOS", 32 + "ref": "nixos-unstable", 33 + "repo": "nixpkgs", 34 + "type": "github" 35 + } 36 + }, 37 + "root": { 38 + "inputs": { 39 + "flake-utils": "flake-utils", 40 + "nixpkgs": "nixpkgs" 41 + } 42 + }, 43 + "systems": { 44 + "locked": { 45 + "lastModified": 1681028828, 46 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 + "owner": "nix-systems", 48 + "repo": "default", 49 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 + "type": "github" 51 + }, 52 + "original": { 53 + "owner": "nix-systems", 54 + "repo": "default", 55 + "type": "github" 56 + } 57 + } 58 + }, 59 + "root": "root", 60 + "version": 7 61 + }
+19
flake.nix
··· 1 + { 2 + inputs = { 3 + flake-utils.url = "github:numtide/flake-utils"; 4 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 5 + }; 6 + 7 + outputs = inputs: 8 + inputs.flake-utils.lib.eachDefaultSystem (system: 9 + let 10 + pkgs = (import (inputs.nixpkgs) { inherit system; }); 11 + in { 12 + devShell = pkgs.mkShell { 13 + buildInputs = with pkgs; [ 14 + deno rsync 15 + ]; 16 + }; 17 + } 18 + ); 19 + }
+4 -4
package.json
··· 14 14 "@atcute/identity-resolver": "^1.1.3" 15 15 }, 16 16 "scripts": { 17 - "cert": "mkdir certs && cd certs && mkcert 127.0.0.1", 18 - "serve": "http-server ./out -C ./certs/127.0.0.1.pem -K ./certs/127.0.0.1-key.pem", 19 - "build": "esbuild src/* --bundle --outdir=out && rsync -av static/ out/", 20 - "build-prod": "esbuild src/* --minify --tree-shaking=true --bundle --outdir=out && rsync -av static/ out/" 17 + "serve": "http-server ./out -C", 18 + "build": "deno bundle --platform=browser src/*.ts --outdir=out --sourcemap=inline && rsync -av static/ out/", 19 + "build-prod": "deno bundle --platform=browser src/*.ts --outdir=out --minify && rsync -av static/ out/", 20 + "test": "deno test test/**/*.ts" 21 21 } 22 22 }
+12
readme
··· 18 18 hey! heads up! im just thinking that as i add things someone might go "wow i want to use that" 19 19 i dont know how packaging works (yet). if you can somehow pull my code into your project as it stands, expect it to break. 20 20 its not good for your health. just dont. not yet? 21 + 22 + 23 + > I have learned that the name you give something when you don't know 24 + > what you should call it should always end up being the final name and 25 + > that's also the case if objectively, it sucks. 26 + - @poggers.website 27 + at://did:plc:7ggpsji45es4auo5pqjmnarr/app.bsky.feed.post/3lx5d3p4oos2n 28 + 29 + bro idk what this is anymore but im a fan of it and will keep going 30 + 31 + i really gotta stop using Node.wraps... 32 + that literally means that my implementation around it is inadequate
-8
shell.nix
··· 1 - { 2 - pkgs ? import <nixpkgs> {} 3 - }: pkgs.mkShell { 4 - packages = [ 5 - pkgs.nodejs 6 - pkgs.mkcert 7 - ]; 8 - }
+31
src/desktop.ts
··· 1 + import { Body, Button, Column, Input, Link, Row } from "./domlink.ts"; 2 + import { resolveMiniDoc } from "./support/slingshot.ts"; 3 + import { Window } from "./windowing_mod.ts"; 4 + import { Feed } from "./windows/bluesky/feed.ts"; 5 + 6 + const about = new Window("About", 150).with( 7 + new Column().with( 8 + new Row().with(`"Domlink"/toybox project by `, new Link("@hotsocket.fyi").to("https://bsky.app/profile/hotsocket.fyi")), 9 + new Link("Source code here!").to("https://tangled.sh/@hotsocket.fyi/domlink"), 10 + "Copyrighted by default(?), no license chosen or anything atp." 11 + ) 12 + ).closable(false); // haha 13 + 14 + const userInput = new Input(); 15 + const authorFeedWindowCreator = new Row().with( 16 + userInput, 17 + new Button("Get Author Feed", async () => { 18 + const doc = await resolveMiniDoc(userInput.value); 19 + const feed = new Feed(); 20 + Body.with(new Window("Posts by @"+doc.handle).with(feed)); 21 + await feed.loadFeed(Feed.createAuthorGenerator(doc.did)); // separated out to allow for updating 22 + }) 23 + ); 24 + const instantiator = new Column().with( 25 + authorFeedWindowCreator, 26 + ).style((x) => x.maxWidth = "100ch"); 27 + 28 + Body.with( 29 + instantiator, 30 + about 31 + );
+61 -23
src/domlink.ts
··· 1 - let _LCounter = 0; // is there a point to this? idk. might be unnecessary overhead. 1 + let _LCounter = 0; 2 + const NodeMap = new WeakMap<HTMLElement, Node>(); 3 + 4 + /** Core wrapper for {@link HTMLElement DOM elements}. */ 2 5 export class Node { 3 6 wraps: HTMLElement; 4 7 children: Node[] = []; 5 - id: string = "L" + _LCounter++;; 6 - constructor(wrap: HTMLElement) { 7 - this.wraps = wrap; 8 + id: string = "L" + _LCounter++; 9 + /** 10 + * Creates a node, wrapping an HTML element. 11 + * @example new Node(document.createElement("h1")); 12 + */ 13 + constructor(wraps: HTMLElement) { 14 + this.wraps = wraps; 8 15 this.wraps.id = this.id; 9 16 } 10 - // could i make add and with one function? yeah. 11 - // will i do that? no. 12 - // why not? because it feels nice to have them separate. 13 - add(node: Node | string) { 17 + /** Adds a child node. Should not be overridden. */ 18 + add(node: NodeEquivalent) { 14 19 if (node != undefined) { 15 20 let xnode: Node; 16 21 if (typeof node === "string") { ··· 25 30 } 26 31 return this; 27 32 } 28 - remove(node: Node) { 33 + /** Works similarly to {@link add}, but takes multiple nodes and is the one you would want to override. */ 34 + with(...nodes: NodeEquivalent[]) { 35 + nodes.forEach(x=>{this.add(x)}); 36 + return this; 37 + } 38 + /** Removes a child node in the same way {@link add} does. */ 39 + delete(node: Node) { 29 40 let idx = this.children.indexOf(node); 30 41 if (idx < 0) { 31 - throw new Error("node not a child of this node"); 42 + throw new Error(`Node ${node.id} not found.`); 32 43 } 33 44 this.children.splice(idx, 1); 34 45 this.wraps.removeChild(node.wraps); 35 46 } 36 - with(...nodes: (Node | string)[]) { 37 - nodes.forEach(x=>{this.add(x)}); 38 - return this 39 - } 40 - style(fn: (style: CSSStyleDeclaration) => void) { 47 + /** Removes one or more child nodes, like {@link with} does adding them. */ 48 + remove(...nodes: Node[]) { 49 + nodes.forEach(x=>{this.delete(x)}); 50 + return this; 51 + } 52 + /** Conveniently chain-able callback thing for styling your nodes. 53 + * @example new Label("This is a label!").style((s)=>{s.color="maroon";}) */ 54 + style(fn: (style: CSSStyleDeclaration) => void) { 41 55 fn(this.wraps.style); 42 56 return this; 43 57 } 58 + /** Adds a CSS class to your node. 59 + * @example new Container().class("my-class") */ 44 60 class(cls: string) { 45 61 this.wraps.classList.add(cls); 46 62 return this; 47 63 } 64 + /** Hopefully, this points at the parent element. */ 65 + get parent(): Node | undefined{ 66 + if (this.wraps.parentElement == null) return undefined; 67 + return NodeMap.get(this.wraps.parentElement); 68 + } 48 69 } 49 70 type NodeEquivalent = (Node | string); 50 71 enum LinkTarget { ··· 52 73 NEW_TAB = "_blank", 53 74 OUT_FRAME = "_parent", 54 75 NOT_FRAME = "_top" 55 - }; 76 + } 77 + /** Wrapper for {@link HTMLAnchorElement `<a>`} */ 56 78 export class Link extends Node { 57 79 constructor(around: NodeEquivalent | null) { 58 80 let a = document.createElement("a"); ··· 65 87 set destination(x: string) { 66 88 (this.wraps as HTMLAnchorElement).href = x; 67 89 } 68 - get destination(): string { 90 + // noinspection JSUnusedGlobalSymbols 91 + get destination(): string { 69 92 return (this.wraps as HTMLAnchorElement).href; 70 93 } 71 94 set target(x: string) { ··· 81 104 } 82 105 83 106 } 107 + /** Wrapper for {@link HTMLDivElement `<div>`} */ 84 108 export class Container extends Node { 85 109 constructor() { 86 110 let div = document.createElement("div"); ··· 88 112 super(div); 89 113 } 90 114 } 115 + /** Vertical Flexbox container */ 91 116 export class Column extends Container { 92 117 constructor() { 93 118 super(); 94 119 this.wraps.classList.add("LColumn"); 95 120 } 96 121 } 122 + /** Horizontal Flexbox container */ 97 123 export class Row extends Container { 98 124 constructor() { 99 125 super(); 100 126 this.wraps.classList.add("LRow"); 101 127 } 102 128 } 129 + /** Wrapper for {@link HTMLTableElement `<table>`} */ 103 130 export class Table extends Node { 104 131 constructor() { 105 132 let table = document.createElement("table"); ··· 107 134 super(table); 108 135 } 109 136 } 137 + /** Wrapper for {@link HTMLTableRowElement `<tr>`} */ 110 138 export class TableRow extends Node { 111 139 constructor() { 112 140 let tr = document.createElement("tr"); ··· 114 142 super(tr); 115 143 } 116 144 } 145 + /** Wrapper for {@link HTMLTableCellElement `<td>`} */ 117 146 export class TableCell extends Node { 118 147 constructor() { 119 148 let td = document.createElement("td"); ··· 121 150 super(td); 122 151 } 123 152 } 153 + /** An "abstract" class of sorts for {@link HTMLElement}s that have a textContent member. */ 124 154 export class Text extends Node { 125 155 private _text: string = ""; 126 156 get text() { ··· 131 161 this.wraps.textContent = x; 132 162 } 133 163 } 164 + /** Wrapper for {@link HTMLSpanElement `<span>`} */ 134 165 export class Label extends Text { 135 166 constructor(text: string = "") { 136 167 let el = document.createElement("span"); ··· 139 170 this.text = text; 140 171 } 141 172 } 173 + /** Wrapper for {@link HTMLButtonElement `<button>`} */ 142 174 export class Button extends Text { 143 175 constructor(label: string, action: EventListener) { 144 176 let btn = document.createElement("button"); ··· 149 181 this.text = label; 150 182 } 151 183 } 152 - interface Updatable { 153 - watch: (watcher: (newValue: string)=>void) => void; 184 + /** Interface providing {@link Updatable.watch} */ 185 + interface Updatable<T> { 186 + /** Takes a callback `watcher` that is called when something like a text input is updated. */ 187 + watch: (watcher: (newValue: T)=>void) => void; 154 188 } 155 - export class Input extends Text implements Updatable { 189 + export class Input extends Text implements Updatable<string> { 156 190 constructor(type: HTMLInputElement["type"] = "text") { 157 191 let el = document.createElement("input"); 158 192 el.type = type; ··· 172 206 173 207 174 208 175 - // Implementation for Updatable 176 209 watching: ((newValue: string) => void)[] = []; 210 + /** Implementation for {@link Updatable.watch} */ 177 211 watch(watcher: (newValue: string)=>void) { 178 212 this.watching.push(watcher); 179 - this.wraps.oninput = (e)=>{watcher(this.value)}; 213 + this.wraps.oninput = ()=>{watcher(this.value)}; 180 214 watcher(this.value); 181 215 } 182 216 } 217 + /** Wrapper for {@link HTMLImageElement `<img>`} */ 183 218 export class Image extends Node { 184 219 constructor(src: string) { 185 220 let img = document.createElement("img"); ··· 188 223 super(img); 189 224 } 190 225 } 226 + /** Wrapper for {@link HTMLDialogElement `<dialog>`} with methods to show/hide it as a modal. */ 191 227 export class Modal extends Node { 192 228 constructor() { 193 229 let dlg: HTMLDialogElement = document.createElement("dialog"); 194 230 dlg.classList.add("LModal"); 195 231 super(dlg); 196 232 } 233 + /** Shows the dialog as a modal. */ 197 234 show() { 198 235 Body.add(this); 199 236 (this.wraps as HTMLDialogElement).showModal(); 200 237 } 238 + /** Hides the dialog. */ 201 239 hide() { 202 240 (this.wraps as HTMLDialogElement).close(); 203 241 Body.wraps.removeChild(this.wraps); 204 242 } 205 243 } 206 244 207 - // user helper shit 245 + /** Convenience wrapper for the {@link HTMLBodyElement body} of the document. */ 208 246 export const Body = new Node(document.body);
+29
src/extras.ts
··· 1 + import { Row, Button, Column, Node } from "./domlink.ts"; 2 + 3 + // extra little handy things i guess. 4 + export async function timeout<T>(time: number, promise: Promise<T>): Promise<T> { 5 + const result = await Promise.race([new Promise<null>(r=>setTimeout(r, time, null)), promise]); 6 + if (result == null) { 7 + throw new Error("timeout"); 8 + } 9 + return result; 10 + } 11 + 12 + function webFrame(url: string) { 13 + const frame = document.createElement("iframe"); 14 + frame.src = url; 15 + frame.style.width = "100%"; 16 + frame.style.height = "100%"; 17 + const controls = new Row().with( 18 + "Web Frame Controls:", 19 + new Button("reload", ()=>{frame.contentWindow?.location.reload();}) 20 + ); 21 + 22 + return new Column().with( 23 + controls, 24 + new Node(frame) 25 + ).style((s)=>{ 26 + s.width = "100%"; 27 + s.height = "100%"; 28 + }); 29 + }
-199
src/issuesearch.ts
··· 1 - import { Body, Button, Column, Input, Label, Link, Row, Table, TableCell, TableRow } from "./domlink.js"; 2 - Body.with(new Link("TypeScript source here!").to("https://tangled.sh/@hotsocket.fyi/domlink/blob/main/src/issuesearch.ts")); 3 - type issueRecord = { 4 - body: string; 5 - createdAt: string; 6 - issueId: number; 7 - owner: string; 8 - repo: string; 9 - title: string; 10 - }; 11 - type constellationRecordItem = { 12 - did: string; 13 - collection: string; 14 - rkey: string; 15 - }; 16 - type constellationResponse = { 17 - total: number; 18 - linking_records: [constellationRecordItem]; 19 - cursor: string; 20 - }; 21 - type slingshotResponse<T> = { 22 - cid: string; 23 - uri: string; 24 - value: T; 25 - }; 26 - type miniDoc = { 27 - did: string; 28 - handle: string; 29 - pds: string; 30 - signing_key: string; 31 - }; 32 - type repoRecord = { 33 - knot: string; 34 - name: string; 35 - owner: string; 36 - createdAt: string; 37 - }; 38 - type recordListingItem<T> = { 39 - uri: string; 40 - cid: string; 41 - value: T; 42 - }; 43 - type recordListing<T> = { 44 - cursor: string; 45 - records: [recordListingItem<T>]; 46 - }; 47 - 48 - async function xcall(host: string, method: string, params: Record<string, string | number> | null = null): Promise<unknown> { 49 - let url = `${host}/xrpc/${method}`; 50 - if (params) { 51 - let usp = new URLSearchParams(); 52 - for (let key in params) { 53 - usp.append(key, params[key].toString()); 54 - } 55 - url += "?" + usp.toString(); 56 - } 57 - return await (await fetch(url)).json(); 58 - } 59 - 60 - type issueItem = ({handle: string;issue: issueRecord}); 61 - let allIssues:issueItem[] = []; 62 - const SLINGSHOT = "https://slingshot.microcosm.blue"; 63 - let currentRepoLink = ""; // essentially for issue links. ensures any extras are stripped off. 64 - async function getIssues(repo: string) { 65 - let repoUrl = new URL(repo); 66 - let repoUrlSplat = repoUrl.pathname.split("/"); 67 - let repoOwnerHandle = repoUrlSplat[1].substring(1); 68 - let repoName = repoUrlSplat[2]; 69 - currentRepoLink = `https://tangled.sh/@${repoOwnerHandle}/${repoName}`; 70 - statusText.text = "resolving owner did"; 71 - let repoOwnerDoc = await xcall(SLINGSHOT, "com.bad-example.identity.resolveMiniDoc", {identifier: repoOwnerHandle}) as miniDoc; 72 - statusText.text = "finding repository"; 73 - let ownedRepos = await xcall(repoOwnerDoc.pds, "com.atproto.repo.listRecords", { repo: repoOwnerDoc.did, collection: "sh.tangled.repo" }) as recordListing<repoRecord>; 74 - let repoRecord = ownedRepos.records.find(x=>x.value.name == repoName)!; 75 - let encodedRepoUri = encodeURIComponent(repoRecord.uri); 76 - statusText.text = `finding issues...`; 77 - let irsp = await (await fetch(`https://constellation.microcosm.blue/links?target=${encodedRepoUri}&collection=sh.tangled.repo.issue&path=.repo`)).json() as constellationResponse; 78 - let allIssueRefs: constellationRecordItem[] = irsp.linking_records; 79 - let nextCursor = irsp.cursor; 80 - while (allIssueRefs.length < irsp.total && nextCursor != null) { 81 - statusText.text = `finding issues... (${allIssueRefs.length}/${irsp.total})`; 82 - let rsp = await (await fetch(`https://constellation.microcosm.blue/links?target=${encodedRepoUri}&collection=sh.tangled.repo.issue&path=.repo&cursor=${nextCursor}`, {headers:{Accept:"application/json"}})).json() as constellationResponse; 83 - nextCursor = rsp.cursor; 84 - allIssueRefs = allIssueRefs.concat(rsp.linking_records); 85 - if (nextCursor == null) break; 86 - } 87 - allIssues = (await Promise.all(allIssueRefs.map(async (issueRef) => { 88 - let rsp = await xcall(SLINGSHOT, "com.atproto.repo.getRecord", { 89 - repo: issueRef.did, 90 - collection: issueRef.collection, 91 - rkey: issueRef.rkey 92 - }) as slingshotResponse<issueRecord>; 93 - let issue = rsp.value; 94 - let doc; 95 - try { 96 - doc = await xcall(SLINGSHOT, "com.bad-example.identity.resolveMiniDoc", { identifier: issue.owner }) as miniDoc; 97 - } catch (error) { 98 - return null; 99 - } 100 - statusText.text = `retrieved issue #${issue.issueId} (not in order!)`; 101 - let handle = doc.handle; 102 - return { 103 - handle: handle, 104 - issue: issue 105 - }; 106 - }))).filter(x=>x as issueItem).sort((a,b)=>{ 107 - let x = 0; 108 - if (a && b) { 109 - x = b.issue.issueId-a.issue.issueId 110 - } 111 - return x; 112 - }) as issueItem[]; 113 - statusText.text = `got ${allIssues.length} issues!`; 114 - } 115 - function renderIssues(issues:issueItem[]) { 116 - let view_issues_list = new Table().add( 117 - new TableRow().with( 118 - new TableCell().add("Handle"), 119 - new TableCell().add("Issue#"), 120 - new TableCell().add("Title") 121 - ) 122 - ); 123 - issues.forEach(issue =>{ 124 - if (issue) { 125 - let view_issue_row = new TableRow(); 126 - view_issue_row.with( 127 - new TableCell().add(new Link(issue.handle).to(`https://tangled.sh/@${issue.handle}`)), 128 - new TableCell().add(new Link(issue.issue.issueId.toString()).to(`${currentRepoLink}/issues/${issue.issue.issueId}`)), 129 - new TableCell().add(issue.issue.title) 130 - ); 131 - view_issues_list.add(view_issue_row); 132 - } 133 - }); 134 - return view_issues_list; 135 - } 136 - 137 - 138 - let repoUrl = new Input(); 139 - let last: any; 140 - let statusText = new Label("waiting"); 141 - let runButton = new Button("GO!",async ()=>{ 142 - (runButton.wraps as HTMLButtonElement).disabled = true; 143 - if (last) { 144 - try { 145 - Body.remove(last); 146 - } catch {} 147 - last = null; 148 - } 149 - allIssues = []; 150 - searchRow.style(x=>x.display="none"); 151 - try { 152 - await getIssues(repoUrl.value); 153 - console.log(allIssues); 154 - last = renderIssues(allIssues); 155 - searchRow.style(x=>x.removeProperty("display")); 156 - } catch (e) { 157 - last = new Label("Failed"); 158 - console.log(e); 159 - } 160 - Body.add(last); 161 - searchInput.value = ""; 162 - console.log(last); 163 - (runButton.wraps as HTMLButtonElement).disabled = false; 164 - }); 165 - 166 - 167 - 168 - let searchInput = new Input(); 169 - searchInput.watch((search)=>{ 170 - if (allIssues.length > 0) { 171 - if (last) { 172 - Body.remove(last); 173 - } 174 - search = search.toLowerCase(); 175 - last = renderIssues(allIssues.filter((item)=>{ 176 - return item.issue.issueId == Number(item) || 177 - search.split(" ").every((word)=> ( 178 - item.handle.toLowerCase().includes(word) || 179 - item.issue.title.toLowerCase().includes(word) || 180 - item.issue.body.toLowerCase().includes(word))) 181 - })); 182 - Body.add(last); 183 - } 184 - }); 185 - let searchRow = new Row().with( 186 - "search", 187 - searchInput 188 - ); 189 - searchRow.style(x=>x.display="none"); 190 - 191 - 192 - 193 - Body.add(new Row().with( 194 - "List issues for repo: ", 195 - repoUrl, 196 - runButton, 197 - statusText 198 - )); 199 - Body.add(searchRow);
-107
src/main.ts
··· 1 - // yeah this is mildly busted so dont get too excited 2 - 3 - import { Client, ok, simpleFetchHandler, AtpSessionData, CredentialManager } from "@atcute/client"; 4 - import { Body, Button, Column, Container, Image, Input, Label, Modal, Row } from "./domlink.js"; 5 - import { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/bluesky'; 6 - import { CompositeDidDocumentResolver, CompositeHandleResolver, DohJsonHandleResolver, PlcDidDocumentResolver, WebDidDocumentResolver, WellKnownHandleResolver } from "@atcute/identity-resolver"; 7 - 8 - const handleResolver = new CompositeHandleResolver({ 9 - strategy: 'race', 10 - methods: { 11 - dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }), 12 - http: new WellKnownHandleResolver(), 13 - }, 14 - }); 15 - const docResolver = new CompositeDidDocumentResolver({ 16 - methods: { 17 - plc: new PlcDidDocumentResolver(), 18 - web: new WebDidDocumentResolver(), 19 - }, 20 - }); 21 - const handler = simpleFetchHandler({service: "https://public.api.zeppelin.social"}); 22 - const client = new Client({handler}); 23 - let manager: CredentialManager | undefined; 24 - const MY_DID = "did:plc:jlplwn5pi4dqrls7i6dx2me7"; 25 - 26 - class PostView extends Container { 27 - constructor(post: AppBskyFeedDefs.PostView) { 28 - super(); 29 - let record: AppBskyFeedPost.Main = post.record as AppBskyFeedPost.Main; 30 - this.with( 31 - new Row().with( 32 - new Image(post.author.avatar ?? "/media/default-avatar.png").class("Avatar"), 33 - new Column().with( 34 - post.author.displayName ?? post.author.handle, 35 - record.text, 36 - new Row().with( 37 - `๐Ÿ—จ๏ธ ${post.replyCount} `, 38 - `๐Ÿ” ${post.repostCount} `, 39 - `๐Ÿ”ƒ ${post.quoteCount} `, 40 - `โค๏ธ ${post.likeCount} `, 41 - ) 42 - ), 43 - ) 44 - ); 45 - this.wraps.classList.add("PostView"); 46 - } 47 - } 48 - async function login() { 49 - let mdl = new Modal(); 50 - let handle = new Input("text"); 51 - handle.placeholder = "Handle"; 52 - let pass = new Input("password"); 53 - pass.placeholder = "Password"; 54 - let col = new Column().with( 55 - "log in here", 56 - handle,pass, 57 - new Row().with( 58 - new Button("cancel",()=>{mdl.hide();}), 59 - new Button("log in",()=>{ 60 - 61 - }), 62 - ) 63 - ); 64 - mdl.add(col).show(); 65 - } 66 - 67 - class AccountPanel extends Container { 68 - constructor(client: Client) { 69 - super(); 70 - this.wraps.classList.add("AccountPanel"); 71 - 72 - const sessionJson = localStorage.getItem('atp-session'); 73 - if (sessionJson) { 74 - const session: AtpSessionData = JSON.parse(sessionJson); 75 - this.with( 76 - new Label(`Logged in as ${session.handle}`), 77 - new Button("Log out", () => { 78 - localStorage.removeItem('atp-session'); 79 - window.location.reload(); 80 - }) 81 - ); 82 - } else { 83 - this.add(new Button("Log in", () => { 84 - login(); 85 - })); 86 - } 87 - } 88 - } 89 - 90 - async function main() { 91 - const postsColumn = new Column().class("PostsColumn"); 92 - const mainColumn = new Column().with( 93 - new AccountPanel(client), 94 - new Label("who up skeeting they deck"), 95 - postsColumn 96 - ); 97 - Body.add(mainColumn); 98 - 99 - // Fetch and display the feed 100 - let data = await ok(client.get("app.bsky.feed.getAuthorFeed", { 101 - params: {actor: MY_DID} 102 - })); 103 - const postViews = data.feed.map(post => new PostView(post.post)); 104 - postsColumn.with(...postViews); 105 - } 106 - 107 - main();
+2 -2
src/pds.ts
··· 1 - import { Body, Button, Column, Input, Label, Row } from "./domlink.js"; 1 + import { Body, Button, Column, Input, Label, Row } from "./domlink.ts"; 2 2 import { DidDocument } from "@atcute/identity"; 3 3 4 4 let input_source_pds = [ ··· 65 65 let runButton = new Button("GO!",async ()=>{ 66 66 (runButton.wraps as HTMLButtonElement).disabled = true; 67 67 if (last) { 68 - Body.remove(last); 68 + Body.delete(last); 69 69 } 70 70 try { 71 71 last = await listActiveUsers(hostInput.value);
+106
src/support/atproto.ts
··· 1 + // skinny atproto types file for supporting constellation.ts 2 + 3 + const DEFAULT_SERVICE = "https://api.bsky.app"; 4 + 5 + /** Type used to imply that a parameter will be run through {@link ValidateNSID} */ 6 + export type NSID = string; 7 + const NSIDExpression = /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$/; 8 + export function ValidateNSID(nsid: string): string | null { 9 + return NSIDExpression.test(nsid) ? nsid : null; 10 + } 11 + 12 + export type DID = `did:${"web"|"plc"}:${string}`; 13 + export function ValidateDID(did: string): string | null { 14 + const parts = did.split(":"); 15 + const isValid = parts.length == 3 && parts[0] == "did" && (parts[1] == "plc" || parts[1] == "web") && parts[2].length > 0; 16 + return isValid ? did : null; 17 + } 18 + 19 + export type AtURIString = `at://${string}/${string}/${string}`; 20 + export class AtURI { 21 + readonly authority: string | null; 22 + readonly collection: string | null; 23 + readonly rkey: string | null; 24 + static fromString(uri: AtURIString): AtURI { 25 + const parts = uri.split("/").slice(2); 26 + return new AtURI(ValidateDID(parts[0]), ValidateNSID(parts[1]), parts[2]); 27 + } 28 + constructor(authority: string | null, collection: string | null, rkey: string | null) { 29 + this.authority = authority; 30 + this.collection = collection; 31 + this.rkey = rkey; 32 + } 33 + /** 34 + * Converts URI to at:// URI. 35 + * @returns The string form of this URI, unless if any parts are specified without any preceding elements. 36 + * @example ``` 37 + * // Invalid collection NSID, returns null. 38 + * new AtURI("at://did:web:example.com/cheese/abc123").toString() 39 + * // Invalid 'authority' DID, returns null. 40 + * new AtURI("at://not-a-did/com.example.nsid").toString() 41 + * // All good and happy, returns the string fed in. 42 + * new AtURI("at://did:web:example.com/com.example.nsid/abc123").toString() 43 + * ``` 44 + */ 45 + toString(): string | null { 46 + const ret: (string|null)[] = ["at://"]; 47 + // using `?? ""` to have a "bad" value to find 48 + if (this.authority) { 49 + ret.push(this.authority ?? ""); 50 + } else ret.push(null); 51 + if (this.collection) { 52 + if (ret.indexOf(null) != -1) {return null;} 53 + ret.push("/"); 54 + ret.push(this.collection ?? ""); 55 + } else ret.push(null); 56 + if (this.rkey) { 57 + if (ret.indexOf(null) != -1) {return null;} 58 + ret.push("/"); 59 + ret.push(this.rkey ?? ""); 60 + } 61 + return ret.join(""); 62 + } 63 + } 64 + 65 + export type RecordResponse<T> = { 66 + cid: string; 67 + uri: AtURIString; 68 + value: T; 69 + } 70 + 71 + // technically you can cast it to whatever you want but i feel like using a generic(?) makes it cleaner 72 + /** Calls an XRPC "query" method (HTTP GET) 73 + * @param service Defaults to {@link https://slingshot.microcosm.blue Slingshot}. 74 + */ 75 + export async function XQuery<T>(method: string, params: Record<string, string | number | null> | null = null, service: string = DEFAULT_SERVICE) { 76 + let QueryURL = `${service}/xrpc/${method}`; 77 + if (params) { 78 + const usp = new URLSearchParams(); 79 + for (const key in params) { 80 + if (params[key]) { 81 + usp.append(key, params[key].toString()); 82 + } 83 + } 84 + QueryURL += "?" + usp.toString(); 85 + } 86 + return (await (await fetch(QueryURL)).json()) as T; 87 + } 88 + /** Calls com.atproto.repo.getRecord with XQuery */ 89 + export async function GetRecord<T>(uri: AtURI, service: string = "https://slingshot.microcosm.blue") { 90 + return await XQuery<RecordResponse<T>>("com.atproto.repo.getRecord", { 91 + repo: uri.authority!, 92 + collection: uri.collection!, 93 + rkey: uri.rkey! 94 + }, service); 95 + } 96 + type ListRecordsResponse<T> = { 97 + cursor: string; 98 + records: RecordResponse<T>[]; 99 + }; 100 + export async function ListRecords<T>(repo: string, collection: NSID, service: string, limit: number = 50) { 101 + return await XQuery<ListRecordsResponse<T>>("com.atproto.repo.listRecords", { 102 + repo: repo, 103 + collection: collection, 104 + limit: limit 105 + }, service); 106 + }
+6
src/support/bluesky.ts
··· 1 + import { AppBskyFeedDefs } from "@atcute/bluesky"; 2 + 3 + export type FeedResponse = { 4 + feed: AppBskyFeedDefs.FeedViewPost[]; 5 + cursor: string; 6 + };
+39
src/support/constellation.ts
··· 1 + import { AtURI, DID, NSID, ValidateNSID } from "./atproto.ts"; 2 + 3 + const BASEURL = "https://constellation.microcosm.blue"; 4 + 5 + /** Essentially an AtURI, fed back by Constellation. */ 6 + export type Reference = { 7 + did: string; 8 + collection: string; 9 + rkey: string; 10 + } 11 + 12 + /** Raw response type from /links */ 13 + type LinksResponse = { 14 + total: number; 15 + linking_records: Reference[]; 16 + cursor: string; 17 + }; 18 + 19 + type Target = AtURI | DID; 20 + /** 21 + * Retrieves an array of record references to records containing links to the specified target. 22 + * @throws When the provided NSID is invalid. 23 + */ 24 + export async function links(target: Target, collection: NSID, path: string): Promise<AtURI[]> { 25 + if (ValidateNSID(collection) == null) { 26 + throw new Error("invalid NSID for collection parameter"); 27 + } 28 + const _target = encodeURIComponent(target.toString()!); 29 + const _path = encodeURIComponent(path); 30 + let cursor = ""; 31 + let records: AtURI[] = []; 32 + while (cursor != null) { 33 + const rsp = await fetch(`${BASEURL}/links?target=${_target.toString()}&collection=${collection}&path=${_path}${cursor ? `&cursor=${cursor}` : ""}`); 34 + const data = await rsp.json() as LinksResponse; 35 + records = records.concat(data.linking_records.map(x=>new AtURI(x.did, x.collection, x.rkey))); 36 + cursor = data.cursor; 37 + } 38 + return records; 39 + }
+21
src/support/slingshot.ts
··· 1 + import { AtURI, DID, RecordResponse, XQuery } from "./atproto.ts"; 2 + 3 + const SLINGSHOT = "https://slingshot.microcosm.blue"; 4 + 5 + export async function resolveHandle(handle: string) { 6 + const res = await XQuery("com.atproto.identity.resolveHandle", {handle: handle}, SLINGSHOT) as {did: DID}; 7 + return res.did; 8 + } 9 + 10 + export type MiniDoc = { 11 + did: string; 12 + handle: string; 13 + pds: string; 14 + signing_key: string; 15 + }; 16 + export async function resolveMiniDoc(identifier: string) { 17 + return await XQuery<MiniDoc>("com.bad-example.identity.resolveMiniDoc", {identifier: identifier}, SLINGSHOT); 18 + } 19 + export async function getUriRecord<T>(at_uri: AtURI, cid: string | null = null) { 20 + return await XQuery<RecordResponse<T>>("com.bad-example.repo.getRecord", {at_uri: at_uri.toString()!, cid: cid}, SLINGSHOT); 21 + }
+80
src/support/tangled.ts
··· 1 + import * as x from "../extras.ts"; 2 + import { AtURI, ListRecords, XQuery } from "./atproto.ts"; 3 + import { getUriRecord, MiniDoc } from "./slingshot.ts"; 4 + import * as Constellation from "./constellation.ts"; 5 + 6 + export type Issue = { 7 + body: string; 8 + createdAt: string; 9 + issueId: number; 10 + owner: string; 11 + repo: string; 12 + title: string; 13 + }; 14 + export type RepoRef = { 15 + readonly owner: string; 16 + readonly name: string; 17 + }; 18 + type RepoCommon = { 19 + readonly description: string; 20 + readonly name: string; 21 + readonly knot: string; 22 + readonly spindle: string; 23 + readonly createdAt: string; 24 + } 25 + type RawRepo = RepoCommon & { 26 + readonly owner: string; 27 + }; 28 + export type Repo = RepoCommon & { 29 + readonly owner: MiniDoc; // The record itself returns a DID, but a MiniDoc is awfully handy. 30 + readonly uri: AtURI; 31 + }; 32 + 33 + export async function GetRepo(ref: RepoRef): Promise<Repo> { 34 + const repoOwnerDoc = await XQuery<MiniDoc>("com.bad-example.identity.resolveMiniDoc", {identifier: ref.owner}); 35 + const ownedRepos = await ListRecords<RawRepo>(repoOwnerDoc.did, "sh.tangled.repo", repoOwnerDoc.pds); 36 + const repoRecord = ownedRepos.records.find(x=>x.value.name == ref.name)!; 37 + return { 38 + ...repoRecord.value, 39 + owner: repoOwnerDoc, 40 + uri: AtURI.fromString(repoRecord.uri) 41 + }; 42 + } 43 + export async function GetIssues(repo: Repo, timeout: number = 10000) { 44 + const allIssueRefs = await x.timeout(timeout, Constellation.links(repo.uri, "sh.tangled.repo.issue", ".repo")); 45 + const issues = allIssueRefs.map(async (uri)=>{ 46 + try { 47 + return (await x.timeout(timeout/5,getUriRecord<Issue>(uri))).value; 48 + } catch { 49 + console.log(`issue timed out: ${uri.toString()}`); 50 + return null; 51 + } 52 + }).filter(x=>x!=null) as unknown as Issue[]; // despite filtering them out, typescript still thinks there could be nulls 53 + return issues.sort((a,b)=>b.issueId-a.issueId); 54 + } 55 + 56 + /** Takes a string like `tangled.sh/core` or `https://tangled.sh/@tangled.sh/core` and returns a {@link RepoRef} */ 57 + export function StringRepoRef(repo: string): RepoRef { 58 + // pick out repo info 59 + const repoSplat = repo.split("//"); 60 + let repoOwner: string; 61 + let repoName: string; 62 + if (repoSplat.length == 1) { 63 + const repoHalves = repo.split("/"); 64 + if (repoHalves.length == 1) throw new Error("invalid repo string"); 65 + repoOwner = repoHalves[0]; 66 + repoName = repoHalves[1]; 67 + } else { 68 + if (repoSplat[0] == "https:") { 69 + const repoUrlSplat = new URL(repo).pathname.split("/"); 70 + // [ "", "@tangled.sh", "core" ] 71 + if (repoUrlSplat.length < 2) throw new Error("invalid repo url"); 72 + repoOwner = repoUrlSplat[1]; 73 + repoName = repoUrlSplat[2]; 74 + } else { 75 + throw new Error("unknown repo uri scheme"); 76 + } 77 + } 78 + if (repoOwner.startsWith("@")) repoOwner = repoOwner.substring(1); 79 + return {owner: repoOwner, name: repoName}; 80 + }
+38
src/windowing.ts
··· 1 + import { Button, Container, Body, Node, Row, Column, Image } from "./domlink.ts"; 2 + import { Window } from "./windowing_mod.ts"; 3 + 4 + function webFrame(url: string) { 5 + const frame = document.createElement("iframe"); 6 + frame.src = url; 7 + frame.style.width = "100%"; 8 + frame.style.height = "100%"; 9 + const controls = new Row().with( 10 + "Web Frame Controls:", 11 + new Button("reload", ()=>{frame.contentWindow?.location.reload();}) 12 + ); 13 + 14 + return new Column().with( 15 + controls, 16 + new Node(frame) 17 + ).style((s)=>{ 18 + s.width = "100%"; 19 + s.height = "100%"; 20 + }); 21 + } 22 + const userListWindow = new Window("User List").with(webFrame("./pds.html")); 23 + const issuesWindow = new Window("Tangled Issues").with(webFrame("./issuesearch.html")); 24 + const feedWindow = new Window("My Posts").with(webFrame("./main.html")); 25 + 26 + // Example usage to demonstrate the new Window class 27 + Body.with( 28 + "you know, i really should check if adding elements normally works", 29 + userListWindow, 30 + issuesWindow, 31 + feedWindow, 32 + new Window('"native" window').with( 33 + new Column().with( 34 + "this doesnt have a web frame in it, but it does have a dog", 35 + new Image("./media/dog.jpeg") 36 + ) 37 + ) 38 + );
+116
src/windowing_mod.ts
··· 1 + import { string } from "../out/main.js"; 2 + import { Button, Container, Label, Node } from "./domlink.ts"; 3 + 4 + 5 + class TitleBar extends Container { 6 + label: Label; 7 + closeButton: Button; 8 + constructor(title: string, closeCallback: EventListener) { 9 + super(); 10 + this.class("LWindowHandle"); 11 + this.label = new Label(title); 12 + this.closeButton = new Button("x", closeCallback); 13 + this.with(this.label,this.closeButton); 14 + } 15 + get closable(): boolean { 16 + return this.closeButton.wraps.style.display != "none"; 17 + } 18 + set closable(x: boolean) { 19 + this.closeButton.wraps.style.display = x ? "inline-block" : "none"; 20 + } 21 + } 22 + export class Window extends Container { 23 + private static currentlyDragged: Window | null = null; 24 + private static mouseRelX = 0; 25 + private static mouseRelY = 0; 26 + private static zIndexCounter = 100; 27 + titleBar: TitleBar; 28 + content = new Container().class("LWindowContent"); 29 + 30 + constructor(title: string = "New Window", height: number = 300, width: number = 400) { 31 + super(); 32 + this.class("LWindow"); 33 + this.titleBar = new TitleBar(title, ()=>{this.wraps.remove();}); 34 + this.add(this.titleBar); 35 + this.titleBar.wraps.addEventListener("mousedown", this.titleGrabHandler.bind(this)); 36 + this.wraps.addEventListener("mousedown", ()=>{this.wraps.style.zIndex = `${Window.zIndexCounter++}`;}); 37 + this.add(this.content); 38 + this.style((s)=>{ 39 + s.width = `${width}px`; 40 + s.height = `${height}px`; 41 + s.top = "0px"; 42 + s.left = "0px"; 43 + }); 44 + } 45 + override with(...nodes: (Node | string)[]): this { 46 + const w = this.wraps.style.width; 47 + const h = this.wraps.style.height; 48 + this.content.with(...nodes); 49 + this.wraps.style.width = w; 50 + this.wraps.style.height = h; 51 + return this; 52 + } 53 + 54 + get title() { 55 + return (this.titleBar.children[0] as Label).text; 56 + } 57 + set title(newTitle: string) { 58 + (this.titleBar.children[0] as Label).text = newTitle; 59 + } 60 + public closable(can: boolean = true) { 61 + this.titleBar.closable = can; 62 + return this; 63 + } 64 + 65 + private titleGrabHandler(ev: MouseEvent) { 66 + if (ev.button !== 0) return; 67 + 68 + Window.currentlyDragged = this; 69 + this.titleBar.wraps.style.cursor = 'grabbing'; 70 + 71 + const rect = this.wraps.getBoundingClientRect(); 72 + Window.mouseRelX = ev.clientX - rect.left; 73 + Window.mouseRelY = ev.clientY - rect.top; 74 + 75 + ev.preventDefault(); 76 + } 77 + 78 + private static onMouseMove(ev: MouseEvent) { 79 + const draggedWindow = Window.currentlyDragged; 80 + if (draggedWindow) { 81 + const viewportWidth = document.documentElement.clientWidth; 82 + const viewportHeight = document.documentElement.clientHeight; 83 + 84 + const newLeft = ev.clientX - Window.mouseRelX; 85 + const newTop = ev.clientY - Window.mouseRelY; 86 + 87 + const clampedLeft = Math.min(Math.max(0, newLeft), viewportWidth - draggedWindow.wraps.offsetWidth); 88 + const clampedTop = Math.min(Math.max(0, newTop), viewportHeight - draggedWindow.wraps.offsetHeight); 89 + 90 + draggedWindow.wraps.style.left = `${clampedLeft}px`; 91 + draggedWindow.wraps.style.top = `${clampedTop}px`; 92 + } 93 + } 94 + 95 + private static onMouseUp(_ev: MouseEvent) { 96 + if (Window.currentlyDragged) { 97 + Window.currentlyDragged.titleBar.wraps.style.cursor = 'grab'; 98 + Window.currentlyDragged = null; 99 + } 100 + } 101 + 102 + // A single place to initialize global listeners 103 + private static _initialized = false; 104 + private static initializeGlobalListeners() { 105 + if (this._initialized) return; 106 + globalThis.addEventListener("mousemove", this.onMouseMove); 107 + globalThis.addEventListener("mouseup", this.onMouseUp); 108 + this._initialized = true; 109 + } 110 + 111 + // Static initializer block to set up listeners once 112 + static { 113 + this.initializeGlobalListeners(); 114 + } 115 + } 116 +
+52
src/windows/bluesky/feed.ts
··· 1 + // formerly src/main.ts. how far we've come. 2 + 3 + import { Column, Container, Image, Row } from "../../domlink.ts"; 4 + import { AppBskyFeedDefs, AppBskyFeedPost } from '@atcute/bluesky'; 5 + import { XQuery } from "../../support/atproto.ts"; 6 + import { FeedResponse as RawFeedResponse } from "../../support/bluesky.ts"; 7 + 8 + class PostView extends Container { 9 + constructor(fvp: AppBskyFeedDefs.FeedViewPost) { 10 + super(); 11 + const post = fvp.post; 12 + const record = post.record as AppBskyFeedPost.Main; 13 + this.with( 14 + new Row().with( 15 + new Image(post.author.avatar ?? "/media/default-avatar.png").class("Avatar"), 16 + new Column().with( 17 + post.author.displayName ?? post.author.handle, 18 + record.text, 19 + new Row().with( 20 + `๐Ÿ—จ๏ธ ${post.replyCount} `, 21 + `๐Ÿ” ${post.repostCount} `, 22 + `๐Ÿ”ƒ ${post.quoteCount} `, 23 + `โค๏ธ ${post.likeCount} `, 24 + ) 25 + ), 26 + ) 27 + ); 28 + this.wraps.classList.add("PostView"); 29 + } 30 + } 31 + 32 + type FeedGenerator = (cursor: string | null) => Promise<RawFeedResponse>; 33 + export class Feed extends Column { 34 + generator: FeedGenerator | null = null; 35 + cursor: string | null = null; 36 + constructor() { 37 + super(); 38 + this.class("Feed"); 39 + } 40 + async loadFeed(generator: FeedGenerator) { 41 + this.generator = generator; 42 + this.children.forEach(x=>this.delete(x)); 43 + const data = await this.generator(this.cursor); 44 + this.cursor = data.cursor; 45 + const postViews = data.feed.map(post => new PostView(post)); 46 + this.with(...postViews); 47 + return this; 48 + } 49 + static createAuthorGenerator(did: string): FeedGenerator { 50 + return (cursor) => XQuery<RawFeedResponse>("app.bsky.feed.getAuthorFeed", {actor: did, cursor: cursor}); 51 + } 52 + }
+120
src/windows/tangled/issuesearch.ts
··· 1 + import { Body, Button, Column, Input, Label, Link, Node, Row, Table, TableCell, TableRow } from "../../domlink.ts"; 2 + import { AtURI, AtURIString, GetRecord, ListRecords, XQuery } from "../../support/atproto.ts"; 3 + import { GetIssues, GetRepo, Issue, StringRepoRef } from "../../support/tangled.ts"; 4 + import * as Constellation from "../../support/constellation.ts"; 5 + import { MiniDoc } from "../../support/slingshot.ts"; 6 + Body.with(new Link("TypeScript source here!").to("https://tangled.sh/@hotsocket.fyi/domlink/blob/main/src/issuesearch.ts")); 7 + 8 + type recordListingItem<T> = { 9 + uri: string; 10 + cid: string; 11 + value: T; 12 + }; 13 + 14 + async function timeout<T>(time: number, promise: Promise<T>): Promise<T> { 15 + const result = await Promise.race([new Promise<null>(r => setTimeout(r, time, null)), promise]); 16 + if (result == null) { 17 + throw new Error("timeout"); 18 + } 19 + return result; 20 + } 21 + 22 + type issueItem = ({ handle: string; issue: Issue }); 23 + let allIssues: issueItem[] = []; 24 + let currentRepoLink = ""; // used to build issue links 25 + 26 + function renderIssues(issues: issueItem[]) { 27 + const view_issues_list = new Table().add( 28 + new TableRow().with( 29 + new TableCell().add("Handle"), 30 + new TableCell().add("Issue#"), 31 + new TableCell().add("Title") 32 + ) 33 + ); 34 + issues.forEach(issue => { 35 + if (issue) { 36 + const view_issue_row = new TableRow(); 37 + view_issue_row.with( 38 + new TableCell().add(new Link(issue.handle).to(`https://tangled.sh/@${issue.handle}`)), 39 + new TableCell().add(new Link(issue.issue.issueId.toString()).to(`${currentRepoLink}/issues/${issue.issue.issueId}`)), 40 + new TableCell().add(issue.issue.title) 41 + ); 42 + view_issues_list.add(view_issue_row); 43 + } 44 + }); 45 + return view_issues_list; 46 + } 47 + 48 + 49 + const repoUrl = new Input(); 50 + let last: Node | null = null; 51 + const statusText = new Label("waiting"); 52 + const runButton = new Button("GO!", async () => { 53 + (runButton.wraps as HTMLButtonElement).disabled = true; 54 + if (last) { 55 + try { 56 + Body.delete(last); 57 + } finally { 58 + last = null; 59 + } 60 + } 61 + allIssues = []; 62 + searchRow.style(x => x.display = "none"); 63 + try { 64 + await getIssues(repoUrl.value); 65 + last = renderIssues(allIssues); 66 + searchRow.style(x => x.removeProperty("display")); 67 + } catch (e) { 68 + last = new Label("Failed"); 69 + console.error(e); 70 + } 71 + Body.add(last); 72 + searchInput.value = ""; 73 + (runButton.wraps as HTMLButtonElement).disabled = false; 74 + }); 75 + 76 + 77 + 78 + const searchInput = new Input(); 79 + searchInput.watch((search) => { 80 + if (allIssues.length > 0) { 81 + if (last) { 82 + Body.delete(last); 83 + } 84 + search = search.toLowerCase(); 85 + last = renderIssues(allIssues.filter((item) => { 86 + return item.issue.issueId == Number(item) || 87 + search.split(" ").every((word) => ( 88 + item.handle.toLowerCase().includes(word) || 89 + item.issue.title.toLowerCase().includes(word) || 90 + item.issue.body.toLowerCase().includes(word))) 91 + })); 92 + Body.add(last); 93 + } 94 + }); 95 + const searchRow = new Row().with( 96 + "search", 97 + searchInput 98 + ); 99 + searchRow.style(x => x.display = "none"); 100 + 101 + 102 + 103 + Body.add(new Row().with( 104 + "List issues for repo: ", 105 + repoUrl, 106 + runButton, 107 + statusText 108 + )); 109 + Body.add(searchRow); 110 + export class IssueList extends Column { 111 + issues: Issue[] | null = null; 112 + constructor() { 113 + super(); 114 + this.class("IssueList"); 115 + } 116 + async getIssues(repo: string) { 117 + const rp = await GetRepo(StringRepoRef(repo)); 118 + this.issues = await GetIssues(rp); 119 + } 120 + }
+14
static/desktop.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>desktop</title> 7 + <link rel="stylesheet" href="styles/domlink.css"> 8 + <link rel="stylesheet" href="mine.css"> 9 + </head> 10 + <body> 11 + <noscript>enable js or fukc off</noscript> 12 + <script src="desktop.js" type="module"></script> 13 + </body> 14 + </html>
static/media/dog.jpeg

This is a binary file and will not be displayed.

+3
static/styles/domlink.css
··· 1 + /* fancy separation */ 1 2 @import url("reset.css"); 2 3 @import url("domlink-visual.css"); 3 4 @import url("domlink-functional.css"); 5 + /* modules idk */ 6 + @import url("domlink-windowing.css");
+13
static/windowing.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>windowing</title> 7 + <link rel="stylesheet" href="styles/domlink.css"> 8 + </head> 9 + <body> 10 + <noscript>enable js or fukc off</noscript> 11 + <script src="windowing.js" type="module"></script> 12 + </body> 13 + </html>
+58
test/support/atproto.ts
··· 1 + import { ValidateNSID, ValidateDID, AtURI } from "../../src/support/atproto.ts"; 2 + import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; 3 + 4 + Deno.test("ValidateNSID returns NSID for valid input", () => { 5 + assertEquals(ValidateNSID("com.example.app"), "com.example.app"); 6 + assertEquals(ValidateNSID("com.example.app.v2"), "com.example.app.v2"); 7 + }); 8 + 9 + Deno.test("ValidateNSID returns null for invalid input", () => { 10 + assertEquals(ValidateNSID("invalid-nsid"), null); 11 + assertEquals(ValidateNSID("com.example..app"), null); 12 + assertEquals(ValidateNSID(".com.example.app"), null); 13 + assertEquals(ValidateNSID("com.example.app."), null); 14 + assertEquals(ValidateNSID("com.example.app_name"), null); 15 + assertEquals(ValidateNSID("com.example.app-name"), null); 16 + assertEquals(ValidateNSID(""), null); 17 + }); 18 + 19 + Deno.test("ValidateDID returns DID for valid input", () => { 20 + assertEquals(ValidateDID("did:plc:example"), "did:plc:example"); 21 + assertEquals(ValidateDID("did:web:example.com"), "did:web:example.com"); 22 + }); 23 + 24 + Deno.test("ValidateDID returns null for invalid input", () => { 25 + assertEquals(ValidateDID("invalid-did"), null); 26 + assertEquals(ValidateDID("did:other:example"), null); 27 + assertEquals(ValidateDID("did:plc"), null); 28 + assertEquals(ValidateDID("did:web:"), null); 29 + assertEquals(ValidateDID(""), null); 30 + }); 31 + 32 + Deno.test("AtURI.fromString parses valid URI", () => { 33 + const uri = AtURI.fromString("at://did:plc:example/com.example.collection/rkey123"); 34 + assertEquals(uri.authority, "did:plc:example"); 35 + assertEquals(uri.collection, "com.example.collection"); 36 + assertEquals(uri.rkey, "rkey123"); 37 + }); 38 + 39 + Deno.test("AtURI.fromString sets nulls for invalid URI", () => { 40 + const uri = AtURI.fromString("at://invalid-did/invalid-collection/actually-fine-rkey"); 41 + assertEquals(uri.authority, null); 42 + assertEquals(uri.collection, null); 43 + assertEquals(uri.rkey, "actually-fine-rkey"); 44 + }); 45 + 46 + Deno.test("AtURI toString returns string form of URI properly", () => { 47 + const uri = AtURI.fromString("at://did:plc:example/com.example.collection/rkey123"); 48 + assertEquals(uri.toString(), "at://did:plc:example/com.example.collection/rkey123"); 49 + }); 50 + 51 + Deno.test("AtURI toString returns null for invalid URI parts", () => { 52 + let uri = AtURI.fromString("at://invalid-did/com.example.collection/rkey123"); 53 + assertEquals(uri.toString(), null); 54 + 55 + uri = AtURI.fromString("at://did:plc:example/invalid-collection/rkey123"); 56 + assertEquals(uri.toString(), null); 57 + 58 + });
-14
tsconfig.json
··· 1 - { 2 - "compilerOptions": { 3 - "target": "es2017", 4 - "module": "nodenext", 5 - "moduleResolution": "nodenext", 6 - "esModuleInterop": true, 7 - "forceConsistentCasingInFileNames": true, 8 - "strict": true, 9 - "skipLibCheck": true, 10 - "rootDir": "src", 11 - "outDir": "out", 12 - "resolveJsonModule": true 13 - } 14 - }