Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

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

various ux improvements/style updates, codegen syntax highlighting, view lexicon modal, consolidate redirect logic

Chad Miller 4f06e552 3a2ce6e2

+525 -75
+265
frontend/deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@shikijs/shiki@*": "3.7.0", 4 5 "jsr:@slices/oauth@0.3": "0.3.0", 5 6 "jsr:@std/assert@^1.0.14": "1.0.14", 6 7 "jsr:@std/cli@^1.0.21": "1.0.21", ··· 14 15 "jsr:@std/net@^1.0.4": "1.0.5", 15 16 "jsr:@std/path@^1.1.1": "1.1.2", 16 17 "jsr:@std/streams@^1.0.10": "1.0.11", 18 + "npm:@shikijs/core@^3.7.0": "3.11.0", 19 + "npm:@shikijs/engine-oniguruma@^3.7.0": "3.11.0", 17 20 "npm:preact-render-to-string@^6.5.13": "6.5.13_preact@10.27.1", 18 21 "npm:preact@^10.27.1": "10.27.1", 22 + "npm:shiki@^3.7.0": "3.11.0", 19 23 "npm:typed-htmx@~0.3.1": "0.3.1" 20 24 }, 21 25 "jsr": { 26 + "@shikijs/shiki@3.7.0": { 27 + "integrity": "6afb828d7d26efc521ef4ca16a7ef7245aca8e83dceaf58cc5cc64d3a4a4a895", 28 + "dependencies": [ 29 + "npm:@shikijs/core", 30 + "npm:@shikijs/engine-oniguruma", 31 + "npm:shiki" 32 + ] 33 + }, 22 34 "@slices/oauth@0.3.0": { 23 35 "integrity": "a6f3296e701291f14b4c8491a7f7a86bd3c8d5caf006eb1d371627558439e3b5" 24 36 }, ··· 77 89 } 78 90 }, 79 91 "npm": { 92 + "@shikijs/core@3.11.0": { 93 + "integrity": "sha512-oJwU+DxGqp6lUZpvtQgVOXNZcVsirN76tihOLBmwILkKuRuwHteApP8oTXmL4tF5vS5FbOY0+8seXmiCoslk4g==", 94 + "dependencies": [ 95 + "@shikijs/types", 96 + "@shikijs/vscode-textmate", 97 + "@types/hast", 98 + "hast-util-to-html" 99 + ] 100 + }, 101 + "@shikijs/engine-javascript@3.11.0": { 102 + "integrity": "sha512-6/ov6pxrSvew13k9ztIOnSBOytXeKs5kfIR7vbhdtVRg+KPzvp2HctYGeWkqv7V6YIoLicnig/QF3iajqyElZA==", 103 + "dependencies": [ 104 + "@shikijs/types", 105 + "@shikijs/vscode-textmate", 106 + "oniguruma-to-es" 107 + ] 108 + }, 109 + "@shikijs/engine-oniguruma@3.11.0": { 110 + "integrity": "sha512-4DwIjIgETK04VneKbfOE4WNm4Q7WC1wo95wv82PoHKdqX4/9qLRUwrfKlmhf0gAuvT6GHy0uc7t9cailk6Tbhw==", 111 + "dependencies": [ 112 + "@shikijs/types", 113 + "@shikijs/vscode-textmate" 114 + ] 115 + }, 116 + "@shikijs/langs@3.11.0": { 117 + "integrity": "sha512-Njg/nFL4HDcf/ObxcK2VeyidIq61EeLmocrwTHGGpOQx0BzrPWM1j55XtKQ1LvvDWH15cjQy7rg96aJ1/l63uw==", 118 + "dependencies": [ 119 + "@shikijs/types" 120 + ] 121 + }, 122 + "@shikijs/themes@3.11.0": { 123 + "integrity": "sha512-BhhWRzCTEk2CtWt4S4bgsOqPJRkapvxdsifAwqP+6mk5uxboAQchc0etiJ0iIasxnMsb764qGD24DK9albcU9Q==", 124 + "dependencies": [ 125 + "@shikijs/types" 126 + ] 127 + }, 128 + "@shikijs/types@3.11.0": { 129 + "integrity": "sha512-RB7IMo2E7NZHyfkqAuaf4CofyY8bPzjWPjJRzn6SEak3b46fIQyG6Vx5fG/obqkfppQ+g8vEsiD7Uc6lqQt32Q==", 130 + "dependencies": [ 131 + "@shikijs/vscode-textmate", 132 + "@types/hast" 133 + ] 134 + }, 135 + "@shikijs/vscode-textmate@10.0.2": { 136 + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==" 137 + }, 138 + "@types/hast@3.0.4": { 139 + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", 140 + "dependencies": [ 141 + "@types/unist" 142 + ] 143 + }, 144 + "@types/mdast@4.0.4": { 145 + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 146 + "dependencies": [ 147 + "@types/unist" 148 + ] 149 + }, 150 + "@types/unist@3.0.3": { 151 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" 152 + }, 153 + "@ungap/structured-clone@1.3.0": { 154 + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" 155 + }, 156 + "ccount@2.0.1": { 157 + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==" 158 + }, 159 + "character-entities-html4@2.1.0": { 160 + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==" 161 + }, 162 + "character-entities-legacy@3.0.0": { 163 + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" 164 + }, 165 + "comma-separated-tokens@2.0.3": { 166 + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==" 167 + }, 168 + "dequal@2.0.3": { 169 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" 170 + }, 171 + "devlop@1.1.0": { 172 + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 173 + "dependencies": [ 174 + "dequal" 175 + ] 176 + }, 177 + "hast-util-to-html@9.0.5": { 178 + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", 179 + "dependencies": [ 180 + "@types/hast", 181 + "@types/unist", 182 + "ccount", 183 + "comma-separated-tokens", 184 + "hast-util-whitespace", 185 + "html-void-elements", 186 + "mdast-util-to-hast", 187 + "property-information", 188 + "space-separated-tokens", 189 + "stringify-entities", 190 + "zwitch" 191 + ] 192 + }, 193 + "hast-util-whitespace@3.0.0": { 194 + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", 195 + "dependencies": [ 196 + "@types/hast" 197 + ] 198 + }, 199 + "html-void-elements@3.0.0": { 200 + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==" 201 + }, 202 + "mdast-util-to-hast@13.2.0": { 203 + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", 204 + "dependencies": [ 205 + "@types/hast", 206 + "@types/mdast", 207 + "@ungap/structured-clone", 208 + "devlop", 209 + "micromark-util-sanitize-uri", 210 + "trim-lines", 211 + "unist-util-position", 212 + "unist-util-visit", 213 + "vfile" 214 + ] 215 + }, 216 + "micromark-util-character@2.1.1": { 217 + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 218 + "dependencies": [ 219 + "micromark-util-symbol", 220 + "micromark-util-types" 221 + ] 222 + }, 223 + "micromark-util-encode@2.0.1": { 224 + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==" 225 + }, 226 + "micromark-util-sanitize-uri@2.0.1": { 227 + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 228 + "dependencies": [ 229 + "micromark-util-character", 230 + "micromark-util-encode", 231 + "micromark-util-symbol" 232 + ] 233 + }, 234 + "micromark-util-symbol@2.0.1": { 235 + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==" 236 + }, 237 + "micromark-util-types@2.0.2": { 238 + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" 239 + }, 240 + "oniguruma-parser@0.12.1": { 241 + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==" 242 + }, 243 + "oniguruma-to-es@4.3.3": { 244 + "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", 245 + "dependencies": [ 246 + "oniguruma-parser", 247 + "regex", 248 + "regex-recursion" 249 + ] 250 + }, 80 251 "preact-render-to-string@6.5.13_preact@10.27.1": { 81 252 "integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==", 82 253 "dependencies": [ ··· 86 257 "preact@10.27.1": { 87 258 "integrity": "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==" 88 259 }, 260 + "property-information@7.1.0": { 261 + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==" 262 + }, 263 + "regex-recursion@6.0.2": { 264 + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", 265 + "dependencies": [ 266 + "regex-utilities" 267 + ] 268 + }, 269 + "regex-utilities@2.3.0": { 270 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==" 271 + }, 272 + "regex@6.0.1": { 273 + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", 274 + "dependencies": [ 275 + "regex-utilities" 276 + ] 277 + }, 278 + "shiki@3.11.0": { 279 + "integrity": "sha512-VgKumh/ib38I1i3QkMn6mAQA6XjjQubqaAYhfge71glAll0/4xnt8L2oSuC45Qcr/G5Kbskj4RliMQddGmy/Og==", 280 + "dependencies": [ 281 + "@shikijs/core", 282 + "@shikijs/engine-javascript", 283 + "@shikijs/engine-oniguruma", 284 + "@shikijs/langs", 285 + "@shikijs/themes", 286 + "@shikijs/types", 287 + "@shikijs/vscode-textmate", 288 + "@types/hast" 289 + ] 290 + }, 291 + "space-separated-tokens@2.0.2": { 292 + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==" 293 + }, 294 + "stringify-entities@4.0.4": { 295 + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", 296 + "dependencies": [ 297 + "character-entities-html4", 298 + "character-entities-legacy" 299 + ] 300 + }, 301 + "trim-lines@3.0.1": { 302 + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" 303 + }, 89 304 "typed-html@3.0.1": { 90 305 "integrity": "sha512-JKCM9zTfPDuPqQqdGZBWSEiItShliKkBFg5c6yOR8zth43v763XkAzTWaOlVqc0Y6p9ee8AaAbipGfUnCsYZUA==" 91 306 }, ··· 94 309 "dependencies": [ 95 310 "typed-html" 96 311 ] 312 + }, 313 + "unist-util-is@6.0.0": { 314 + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", 315 + "dependencies": [ 316 + "@types/unist" 317 + ] 318 + }, 319 + "unist-util-position@5.0.0": { 320 + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", 321 + "dependencies": [ 322 + "@types/unist" 323 + ] 324 + }, 325 + "unist-util-stringify-position@4.0.0": { 326 + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 327 + "dependencies": [ 328 + "@types/unist" 329 + ] 330 + }, 331 + "unist-util-visit-parents@6.0.1": { 332 + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", 333 + "dependencies": [ 334 + "@types/unist", 335 + "unist-util-is" 336 + ] 337 + }, 338 + "unist-util-visit@5.0.0": { 339 + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", 340 + "dependencies": [ 341 + "@types/unist", 342 + "unist-util-is", 343 + "unist-util-visit-parents" 344 + ] 345 + }, 346 + "vfile-message@4.0.3": { 347 + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", 348 + "dependencies": [ 349 + "@types/unist", 350 + "unist-util-stringify-position" 351 + ] 352 + }, 353 + "vfile@6.0.3": { 354 + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 355 + "dependencies": [ 356 + "@types/unist", 357 + "vfile-message" 358 + ] 359 + }, 360 + "zwitch@2.0.4": { 361 + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" 97 362 } 98 363 }, 99 364 "workspace": {
+8 -10
frontend/src/components/CodegenForm.tsx
··· 33 33 34 34 <button 35 35 type="submit" 36 - className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-md" 37 - hx-indicator="#loading-spinner" 36 + className="bg-orange-500 hover:bg-orange-600 text-white px-6 py-2 rounded-md flex items-center justify-center" 38 37 > 39 - <span 40 - className="htmx-indicator" 41 - id="loading-spinner" 42 - style="display:none;" 43 - > 44 - Generating... 45 - </span> 46 - <span>Generate Client</span> 38 + <i 39 + data-lucide="loader-2" 40 + className="htmx-indicator animate-spin mr-2 h-4 w-4" 41 + _="on load js lucide.createIcons() end" 42 + ></i> 43 + <span className="htmx-indicator">Generating Client...</span> 44 + <span className="default-text">Generate Client</span> 47 45 </button> 48 46 </form> 49 47
+13 -5
frontend/src/components/CodegenResult.tsx
··· 1 + import { codeToHtml } from "jsr:@shikijs/shiki"; 2 + 1 3 interface CodegenResultProps { 2 4 success: boolean; 3 5 generatedCode?: string; 4 6 error?: string; 5 7 } 6 8 7 - export function CodegenResult({ 9 + export async function CodegenResult({ 8 10 success, 9 11 generatedCode, 10 12 error, 11 13 }: CodegenResultProps) { 12 14 if (success && generatedCode) { 15 + const highlightedCode = await codeToHtml(generatedCode, { 16 + lang: "typescript", 17 + theme: "catppuccin-latte", 18 + }); 19 + 13 20 return ( 14 21 <div className="bg-green-50 border border-green-200 rounded-lg p-4"> 15 22 <div className="flex items-center justify-between mb-2"> ··· 24 31 Copy to Clipboard 25 32 </button> 26 33 </div> 27 - <div className="bg-white border rounded p-4"> 28 - <pre className="text-sm overflow-x-auto whitespace-pre-wrap"> 29 - {generatedCode} 30 - </pre> 34 + <div className="rounded overflow-hidden"> 35 + <div 36 + className="text-sm overflow-x-auto [&_pre]:p-4" 37 + dangerouslySetInnerHTML={{ __html: highlightedCode }} 38 + /> 31 39 </div> 32 40 </div> 33 41 );
+2 -1
frontend/src/components/Layout.tsx
··· 20 20 <script src="https://unpkg.com/htmx.org@1.9.10"></script> 21 21 <script src="https://unpkg.com/hyperscript.org@0.9.12"></script> 22 22 <script src="https://cdn.tailwindcss.com/3.4.1"></script> 23 + <script src="https://unpkg.com/lucide@latest"></script> 23 24 <style 24 25 dangerouslySetInnerHTML={{ 25 26 __html: ` ··· 49 50 {currentUser?.isAuthenticated ? ( 50 51 <div className="flex items-center space-x-3"> 51 52 <span className="text-sm text-gray-600"> 52 - {currentUser.handle || "Authenticated User"} 53 + {currentUser.handle ? `@${currentUser.handle}` : "Authenticated User"} 53 54 </span> 54 55 <a 55 56 href="/settings"
+28 -3
frontend/src/components/LexiconListItem.tsx
··· 29 29 <p className="text-xs text-gray-400 font-mono">{uri}</p> 30 30 </div> 31 31 <div className="flex items-center space-x-2"> 32 - <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> 33 - Active 34 - </span> 32 + <button 33 + type="button" 34 + hx-get={`/api/slices/${sliceId}/lexicons/${rkey}/view`} 35 + hx-target="#lexicon-modal" 36 + hx-swap="innerHTML" 37 + className="inline-flex items-center px-2 py-1 border border-blue-300 rounded text-xs font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" 38 + > 39 + <svg 40 + className="h-3 w-3 mr-1" 41 + fill="none" 42 + viewBox="0 0 24 24" 43 + stroke="currentColor" 44 + > 45 + <path 46 + strokeLinecap="round" 47 + strokeLinejoin="round" 48 + strokeWidth={2} 49 + d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" 50 + /> 51 + <path 52 + strokeLinecap="round" 53 + strokeLinejoin="round" 54 + strokeWidth={2} 55 + d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" 56 + /> 57 + </svg> 58 + View 59 + </button> 35 60 <button 36 61 type="button" 37 62 hx-delete={`/api/slices/${sliceId}/lexicons/${rkey}`}
+112
frontend/src/components/LexiconViewModal.tsx
··· 1 + import { codeToHtml } from "jsr:@shikijs/shiki"; 2 + 3 + interface LexiconViewModalProps { 4 + nsid: string; 5 + definitions: string; 6 + uri: string; 7 + createdAt: string; 8 + } 9 + 10 + export async function LexiconViewModal({ 11 + nsid, 12 + definitions, 13 + uri, 14 + createdAt, 15 + }: LexiconViewModalProps) { 16 + // Parse and format the definitions JSON 17 + let formattedDefinitions = definitions; 18 + try { 19 + const parsed = JSON.parse(definitions); 20 + formattedDefinitions = JSON.stringify(parsed, null, 2); 21 + } catch (_e) { 22 + // Keep original if parsing fails 23 + } 24 + 25 + // Apply Shiki syntax highlighting 26 + const highlightedJson = await codeToHtml(formattedDefinitions, { 27 + lang: "json", 28 + theme: "catppuccin-latte", 29 + }); 30 + 31 + return ( 32 + <div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"> 33 + <div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden"> 34 + {/* Header */} 35 + <div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center"> 36 + <div> 37 + <h3 className="text-lg font-semibold text-gray-900 font-mono"> 38 + {nsid} 39 + </h3> 40 + <p className="text-sm text-gray-500 mt-1"> 41 + Created: {new Date(createdAt).toLocaleString()} 42 + </p> 43 + <p className="text-xs text-gray-400 font-mono mt-1">{uri}</p> 44 + </div> 45 + <button 46 + type="button" 47 + _="on click set #lexicon-modal's innerHTML to ''" 48 + className="text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 rounded-full p-1" 49 + > 50 + <svg 51 + className="h-6 w-6" 52 + fill="none" 53 + viewBox="0 0 24 24" 54 + stroke="currentColor" 55 + > 56 + <path 57 + strokeLinecap="round" 58 + strokeLinejoin="round" 59 + strokeWidth={2} 60 + d="M6 18L18 6M6 6l12 12" 61 + /> 62 + </svg> 63 + </button> 64 + </div> 65 + 66 + {/* Content */} 67 + <div className="px-6 py-4 overflow-y-auto max-h-[calc(90vh-120px)]"> 68 + <div className="mb-4"> 69 + <label className="block text-sm font-medium text-gray-700 mb-2"> 70 + Lexicon Definition 71 + </label> 72 + <div className="border border-gray-200 rounded-md overflow-x-auto [&_pre]:p-4"> 73 + <div dangerouslySetInnerHTML={{ __html: highlightedJson }} /> 74 + </div> 75 + </div> 76 + </div> 77 + 78 + {/* Footer */} 79 + <div className="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3"> 80 + <button 81 + type="button" 82 + _="on click js navigator.clipboard.writeText(me.previousElementSibling.textContent) then set my textContent to 'Copied!' then wait 2s then set my textContent to 'Copy JSON' end" 83 + className="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" 84 + > 85 + <svg 86 + className="h-4 w-4 mr-2" 87 + fill="none" 88 + viewBox="0 0 24 24" 89 + stroke="currentColor" 90 + > 91 + <path 92 + strokeLinecap="round" 93 + strokeLinejoin="round" 94 + strokeWidth={2} 95 + d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" 96 + /> 97 + </svg> 98 + Copy JSON 99 + </button> 100 + <span className="hidden">{formattedDefinitions}</span> 101 + <button 102 + type="button" 103 + _="on click set #lexicon-modal's innerHTML to ''" 104 + className="inline-flex items-center px-4 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" 105 + > 106 + Close 107 + </button> 108 + </div> 109 + </div> 110 + </div> 111 + ); 112 + }
+1 -1
frontend/src/pages/LoginPage.tsx
··· 12 12 <div className="bg-white rounded-lg shadow-md p-8"> 13 13 <div className="text-center mb-8"> 14 14 <h1 className="text-3xl font-bold text-gray-800 mb-2"> 15 - Welcome to Slice 15 + Welcome to Slices 16 16 </h1> 17 17 <p className="text-gray-600"> 18 18 Sign in with your AT Protocol handle
+3
frontend/src/pages/SliceLexiconPage.tsx
··· 165 165 <li>• Custom collection types</li> 166 166 </ul> 167 167 </div> 168 + 169 + {/* Modal container for viewing lexicons */} 170 + <div id="lexicon-modal"></div> 168 171 </div> 169 172 </Layout> 170 173 );
+8 -10
frontend/src/pages/SliceSyncPage.tsx
··· 82 82 <div className="flex space-x-4"> 83 83 <button 84 84 type="submit" 85 - className="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-md" 86 - hx-indicator="#sync-loading" 85 + className="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-md flex items-center justify-center" 87 86 > 88 - <span 89 - className="htmx-indicator" 90 - id="sync-loading" 91 - style="display:none;" 92 - > 93 - Syncing... 94 - </span> 95 - <span className="default-text">Start Bulk Sync</span> 87 + <i 88 + data-lucide="loader-2" 89 + className="htmx-indicator animate-spin mr-2 h-4 w-4" 90 + _="on load js lucide.createIcons() end" 91 + ></i> 92 + <span className="htmx-indicator">Syncing...</span> 93 + <span className="default-text">Start Sync</span> 96 94 </button> 97 95 </div> 98 96 </form>
+1 -1
frontend/src/routes/dialogs.tsx
··· 5 5 6 6 async function handleCreateSliceDialog(req: Request): Promise<Response> { 7 7 const context = await withAuth(req); 8 - const authResponse = requireAuth(context, req); 8 + const authResponse = requireAuth(context); 9 9 if (authResponse) return authResponse; 10 10 11 11 const dialogHtml = render(<CreateSliceDialog />);
+6 -15
frontend/src/routes/middleware.ts
··· 18 18 }; 19 19 } 20 20 21 - export function requireAuth( 22 - context: RouteContext, 23 - req: Request 24 - ): Response | null { 21 + export function requireAuth(context: RouteContext): Response | null { 25 22 if (!context.currentUser.isAuthenticated) { 26 - return Response.redirect(new URL("/login", req.url), 302); 27 - } 28 - return null; 29 - } 30 - 31 - export function requireApiAuth( 32 - context: RouteContext 33 - ): Response | null { 34 - if (!context.currentUser.isAuthenticated) { 35 - return new Response(JSON.stringify({ error: "Authentication required" }), { 23 + return new Response("", { 36 24 status: 401, 37 - headers: { "Content-Type": "application/json" } 25 + headers: { 26 + "HX-Redirect": "/login", 27 + "Location": "/login" 28 + }, 38 29 }); 39 30 } 40 31 return null;
+1 -1
frontend/src/routes/pages.tsx
··· 15 15 16 16 async function handleIndexPage(req: Request): Promise<Response> { 17 17 const context = await withAuth(req); 18 - const authResponse = requireAuth(context, req); 18 + const authResponse = requireAuth(context); 19 19 if (authResponse) return authResponse; 20 20 21 21 // Slice list page - get real slices from AT Protocol
+77 -28
frontend/src/routes/slices.tsx
··· 1 1 import type { Route } from "@std/http/unstable-route"; 2 2 import { render } from "preact-render-to-string"; 3 - import { withAuth, requireAuth, requireApiAuth } from "./middleware.ts"; 3 + import { withAuth, requireAuth } from "./middleware.ts"; 4 4 import { atprotoClient } from "../config.ts"; 5 5 import { getSliceClient } from "../utils/client.ts"; 6 6 import { buildSliceUri } from "../utils/at-uri.ts"; ··· 9 9 import { EmptyLexiconState } from "../components/EmptyLexiconState.tsx"; 10 10 import { LexiconSuccessMessage } from "../components/LexiconSuccessMessage.tsx"; 11 11 import { LexiconErrorMessage } from "../components/LexiconErrorMessage.tsx"; 12 + import { LexiconViewModal } from "../components/LexiconViewModal.tsx"; 12 13 import { LexiconListItem } from "../components/LexiconListItem.tsx"; 13 14 import { CodegenResult } from "../components/CodegenResult.tsx"; 14 15 import { SettingsResult } from "../components/SettingsResult.tsx"; ··· 17 18 18 19 async function handleCreateSlice(req: Request): Promise<Response> { 19 20 const context = await withAuth(req); 20 - const authResponse = requireAuth(context, req); 21 + const authResponse = requireAuth(context); 21 22 if (authResponse) return authResponse; 22 23 23 24 // Ensure client has tokens before attempting API calls ··· 91 92 params?: URLPatternResult 92 93 ): Promise<Response> { 93 94 const context = await withAuth(req); 94 - const authResponse = requireApiAuth(context); 95 + const authResponse = requireAuth(context); 95 96 if (authResponse) return authResponse; 96 97 97 98 const sliceId = params?.pathname.groups.id; ··· 162 163 params?: URLPatternResult 163 164 ): Promise<Response> { 164 165 const context = await withAuth(req); 165 - const authResponse = requireApiAuth(context); 166 + const authResponse = requireAuth(context); 166 167 if (authResponse) return authResponse; 167 168 168 169 const sliceId = params?.pathname.groups.id; ··· 191 192 params?: URLPatternResult 192 193 ): Promise<Response> { 193 194 const context = await withAuth(req); 194 - const authResponse = requireApiAuth(context); 195 + const authResponse = requireAuth(context); 195 196 if (authResponse) return authResponse; 196 197 197 198 const sliceId = params?.pathname.groups.id; ··· 247 248 248 249 async function handleCreateLexicon(req: Request): Promise<Response> { 249 250 const context = await withAuth(req); 250 - const authResponse = requireApiAuth(context); 251 + const authResponse = requireAuth(context); 251 252 if (authResponse) return authResponse; 252 253 253 254 try { ··· 342 343 } 343 344 } 344 345 346 + async function handleViewLexicon( 347 + req: Request, 348 + params?: URLPatternResult 349 + ): Promise<Response> { 350 + const context = await withAuth(req); 351 + const authResponse = requireAuth(context); 352 + if (authResponse) return authResponse; 353 + 354 + const sliceId = params?.pathname.groups.id; 355 + const rkey = params?.pathname.groups.rkey; 356 + if (!sliceId || !rkey) { 357 + return new Response("Invalid slice ID or lexicon key", { status: 400 }); 358 + } 359 + 360 + try { 361 + // Get slice-specific client and fetch the specific lexicon 362 + const sliceClient = getSliceClient(context, sliceId); 363 + const lexiconRecords = await sliceClient.social.slices.lexicon.listRecords(); 364 + 365 + // Find the lexicon with matching rkey 366 + const lexicon = lexiconRecords.records.find(record => 367 + record.uri.endsWith(`/${rkey}`) 368 + ); 369 + 370 + if (!lexicon) { 371 + return new Response("Lexicon not found", { status: 404 }); 372 + } 373 + 374 + const component = await LexiconViewModal({ 375 + nsid: lexicon.value.nsid, 376 + definitions: lexicon.value.definitions, 377 + uri: lexicon.uri, 378 + createdAt: lexicon.indexed_at 379 + }); 380 + const html = render(component); 381 + 382 + return new Response(html, { 383 + status: 200, 384 + headers: { "content-type": "text/html" }, 385 + }); 386 + } catch (error) { 387 + console.error("Error viewing lexicon:", error); 388 + return new Response("Failed to load lexicon", { status: 500 }); 389 + } 390 + } 391 + 345 392 async function handleDeleteLexicon( 346 393 req: Request, 347 394 params?: URLPatternResult 348 395 ): Promise<Response> { 349 396 const context = await withAuth(req); 350 - const authResponse = requireApiAuth(context); 397 + const authResponse = requireAuth(context); 351 398 if (authResponse) return authResponse; 352 399 353 400 const sliceId = params?.pathname.groups.id; ··· 402 449 params?: URLPatternResult 403 450 ): Promise<Response> { 404 451 const context = await withAuth(req); 405 - const authResponse = requireApiAuth(context); 452 + const authResponse = requireAuth(context); 406 453 if (authResponse) return authResponse; 407 454 408 455 const sliceId = params?.pathname.groups.id; 409 456 if (!sliceId) { 410 - const html = render( 411 - <CodegenResult success={false} error="Invalid slice ID" /> 412 - ); 457 + const component = await CodegenResult({ success: false, error: "Invalid slice ID" }); 458 + const html = render(component); 413 459 return new Response(html, { 414 460 status: 400, 415 461 headers: { "content-type": "text/html" }, ··· 434 480 slice: sliceUri, 435 481 }); 436 482 437 - const html = render( 438 - <CodegenResult 439 - success={result.success} 440 - generatedCode={result.generated_code} 441 - error={result.error} 442 - /> 443 - ); 483 + const component = await CodegenResult({ 484 + success: result.success, 485 + generatedCode: result.generated_code, 486 + error: result.error 487 + }); 488 + const html = render(component); 444 489 445 490 return new Response(html, { 446 491 headers: { "content-type": "text/html; charset=utf-8" }, 447 492 }); 448 493 } catch (error) { 449 494 console.error("Codegen error:", error); 450 - const html = render( 451 - <CodegenResult 452 - success={false} 453 - error={`Error: ${ 454 - error instanceof Error ? error.message : String(error) 455 - }`} 456 - /> 457 - ); 495 + const component = await CodegenResult({ 496 + success: false, 497 + error: `Error: ${ 498 + error instanceof Error ? error.message : String(error) 499 + }` 500 + }); 501 + const html = render(component); 458 502 459 503 return new Response(html, { 460 504 headers: { "content-type": "text/html; charset=utf-8" }, ··· 464 508 465 509 async function handleUpdateProfile(req: Request): Promise<Response> { 466 510 const context = await withAuth(req); 467 - const authResponse = requireApiAuth(context); 511 + const authResponse = requireAuth(context); 468 512 if (authResponse) return authResponse; 469 513 470 514 try { ··· 575 619 params?: URLPatternResult 576 620 ): Promise<Response> { 577 621 const context = await withAuth(req); 578 - const authResponse = requireApiAuth(context); 622 + const authResponse = requireAuth(context); 579 623 if (authResponse) return authResponse; 580 624 581 625 const sliceId = params?.pathname.groups.id; ··· 687 731 method: "GET", 688 732 pattern: new URLPattern({ pathname: "/api/slices/:id/lexicons/list" }), 689 733 handler: handleListLexicons, 734 + }, 735 + { 736 + method: "GET", 737 + pattern: new URLPattern({ pathname: "/api/slices/:id/lexicons/:rkey/view" }), 738 + handler: handleViewLexicon, 690 739 }, 691 740 { 692 741 method: "DELETE",