Tools for the Atmosphere tools.slices.network
quickslice atproto html

docs: add rich text facets implementation plan

+517
+517
docs/plans/2025-12-17-rich-text-facets.md
···
··· 1 + # Rich Text Facets Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add Bluesky rich text facet support to render clickable links in bug descriptions, steps, comments, and responses. 6 + 7 + **Architecture:** Two utility functions (`parseFacets` for detection, `renderFacetedText` for display), GraphQL query updates to fetch facet data, mutation updates to save parsed facets, and render call replacements. 8 + 9 + **Tech Stack:** Vanilla JavaScript, GraphQL, TextEncoder/TextDecoder APIs 10 + 11 + --- 12 + 13 + ### Task 1: Add CSS for facet links 14 + 15 + **Files:** 16 + - Modify: `bugs.html:~225` (after `.hidden` utility class) 17 + 18 + **Step 1: Add the facet link styles** 19 + 20 + Add after the `.hidden { display: none !important; }` rule: 21 + 22 + ```css 23 + /* Facet links */ 24 + .facet-link { 25 + color: var(--accent); 26 + text-decoration: underline; 27 + } 28 + 29 + .facet-link:hover { 30 + color: var(--accent-hover); 31 + } 32 + ``` 33 + 34 + **Step 2: Commit** 35 + 36 + ```bash 37 + git add bugs.html 38 + git commit -m "feat(bugs): add CSS for facet links" 39 + ``` 40 + 41 + --- 42 + 43 + ### Task 2: Add parseFacets utility function 44 + 45 + **Files:** 46 + - Modify: `bugs.html:~1179` (after the `esc()` function) 47 + 48 + **Step 1: Add the parseFacets function** 49 + 50 + Add after the `esc()` function closing brace: 51 + 52 + ```javascript 53 + function parseFacets(text) { 54 + if (!text) return { text, facets: null }; 55 + 56 + const urlRegex = /https?:\/\/[^\s<>"\]\)]+/gi; 57 + const facets = []; 58 + 59 + let match; 60 + while ((match = urlRegex.exec(text)) !== null) { 61 + const url = match[0]; 62 + const charStart = match.index; 63 + const charEnd = charStart + url.length; 64 + 65 + const byteStart = new TextEncoder().encode(text.slice(0, charStart)).length; 66 + const byteEnd = new TextEncoder().encode(text.slice(0, charEnd)).length; 67 + 68 + facets.push({ 69 + index: { byteStart, byteEnd }, 70 + features: [{ $type: "app.bsky.richtext.facet#link", uri: url }] 71 + }); 72 + } 73 + 74 + return { 75 + text, 76 + facets: facets.length > 0 ? facets : null 77 + }; 78 + } 79 + ``` 80 + 81 + **Step 2: Verify in browser console** 82 + 83 + Open bugs.html, open DevTools console, run: 84 + ```javascript 85 + parseFacets("Check https://example.com for info") 86 + // Expected: { text: "Check https://example.com for info", facets: [{ index: { byteStart: 6, byteEnd: 25 }, features: [...] }] } 87 + ``` 88 + 89 + **Step 3: Commit** 90 + 91 + ```bash 92 + git add bugs.html 93 + git commit -m "feat(bugs): add parseFacets utility for URL detection" 94 + ``` 95 + 96 + --- 97 + 98 + ### Task 3: Add renderFacetedText utility function 99 + 100 + **Files:** 101 + - Modify: `bugs.html` (after `parseFacets` function) 102 + 103 + **Step 1: Add the renderFacetedText function** 104 + 105 + Add after the `parseFacets()` function: 106 + 107 + ```javascript 108 + function renderFacetedText(text, facets) { 109 + if (!text) return ""; 110 + if (!facets || facets.length === 0) return esc(text); 111 + 112 + const encoder = new TextEncoder(); 113 + const decoder = new TextDecoder(); 114 + const bytes = encoder.encode(text); 115 + 116 + const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart); 117 + 118 + let result = ""; 119 + let lastEnd = 0; 120 + 121 + for (const facet of sorted) { 122 + if (facet.index.byteStart > lastEnd) { 123 + result += esc(decoder.decode(bytes.slice(lastEnd, facet.index.byteStart))); 124 + } 125 + 126 + const facetText = decoder.decode(bytes.slice(facet.index.byteStart, facet.index.byteEnd)); 127 + const link = facet.features.find(f => f.uri); 128 + 129 + if (link) { 130 + result += `<a href="${esc(link.uri)}" target="_blank" rel="noopener noreferrer" class="facet-link">${esc(facetText)}</a>`; 131 + } else { 132 + result += esc(facetText); 133 + } 134 + 135 + lastEnd = facet.index.byteEnd; 136 + } 137 + 138 + if (lastEnd < bytes.length) { 139 + result += esc(decoder.decode(bytes.slice(lastEnd))); 140 + } 141 + 142 + return result; 143 + } 144 + ``` 145 + 146 + **Step 2: Verify in browser console** 147 + 148 + ```javascript 149 + const parsed = parseFacets("Check https://example.com for info"); 150 + renderFacetedText(parsed.text, parsed.facets); 151 + // Expected: 'Check <a href="https://example.com" target="_blank" rel="noopener noreferrer" class="facet-link">https://example.com</a> for info' 152 + ``` 153 + 154 + **Step 3: Commit** 155 + 156 + ```bash 157 + git add bugs.html 158 + git commit -m "feat(bugs): add renderFacetedText utility for displaying links" 159 + ``` 160 + 161 + --- 162 + 163 + ### Task 4: Update BUGS_QUERY to fetch facets 164 + 165 + **Files:** 166 + - Modify: `bugs.html:~1381` (BUGS_QUERY constant) 167 + 168 + **Step 1: Add facet fields to the query** 169 + 170 + In BUGS_QUERY, after `stepsToReproduce` field, add: 171 + 172 + ```graphql 173 + descriptionFacets { 174 + index { byteStart byteEnd } 175 + features { 176 + ... on AppBskyRichtextFacetLink { uri } 177 + } 178 + } 179 + stepsToReproduceFacets { 180 + index { byteStart byteEnd } 181 + features { 182 + ... on AppBskyRichtextFacetLink { uri } 183 + } 184 + } 185 + ``` 186 + 187 + **Step 2: Commit** 188 + 189 + ```bash 190 + git add bugs.html 191 + git commit -m "feat(bugs): fetch description and steps facets in BUGS_QUERY" 192 + ``` 193 + 194 + --- 195 + 196 + ### Task 5: Update COMMENTS_QUERY to fetch facets 197 + 198 + **Files:** 199 + - Modify: `bugs.html:~1512` (COMMENTS_QUERY constant) 200 + 201 + **Step 1: Add facet fields to the query** 202 + 203 + In COMMENTS_QUERY, after `body` field, add: 204 + 205 + ```graphql 206 + bodyFacets { 207 + index { byteStart byteEnd } 208 + features { 209 + ... on AppBskyRichtextFacetLink { uri } 210 + } 211 + } 212 + ``` 213 + 214 + **Step 2: Commit** 215 + 216 + ```bash 217 + git add bugs.html 218 + git commit -m "feat(bugs): fetch body facets in COMMENTS_QUERY" 219 + ``` 220 + 221 + --- 222 + 223 + ### Task 6: Update RESPONSES_QUERY to fetch facets 224 + 225 + **Files:** 226 + - Modify: `bugs.html:~1461` (RESPONSES_QUERY constant) 227 + 228 + **Step 1: Add facet fields to the query** 229 + 230 + In RESPONSES_QUERY, after `message` field, add: 231 + 232 + ```graphql 233 + messageFacets { 234 + index { byteStart byteEnd } 235 + features { 236 + ... on AppBskyRichtextFacetLink { uri } 237 + } 238 + } 239 + ``` 240 + 241 + **Step 2: Commit** 242 + 243 + ```bash 244 + git add bugs.html 245 + git commit -m "feat(bugs): fetch message facets in RESPONSES_QUERY" 246 + ``` 247 + 248 + --- 249 + 250 + ### Task 7: Update bug overlay rendering to use facets 251 + 252 + **Files:** 253 + - Modify: `bugs.html:~1977` and `~2051` (renderOverlay and updateOverlayContent) 254 + 255 + **Step 1: Replace description rendering in renderOverlay** 256 + 257 + Find: 258 + ```javascript 259 + <p>${esc(bug.description)}</p> 260 + ``` 261 + 262 + Replace with: 263 + ```javascript 264 + <p>${renderFacetedText(bug.description, bug.descriptionFacets)}</p> 265 + ``` 266 + 267 + **Step 2: Replace stepsToReproduce rendering in renderOverlay** 268 + 269 + Find: 270 + ```javascript 271 + <p>${esc(bug.stepsToReproduce)}</p> 272 + ``` 273 + 274 + Replace with: 275 + ```javascript 276 + <p>${renderFacetedText(bug.stepsToReproduce, bug.stepsToReproduceFacets)}</p> 277 + ``` 278 + 279 + **Step 3: Repeat for updateOverlayContent function** 280 + 281 + Apply the same two replacements in `updateOverlayContent()` (~lines 2051 and 2056). 282 + 283 + **Step 4: Verify manually** 284 + 285 + Open a bug with a URL in description or steps. The URL should now be a clickable link. 286 + 287 + **Step 5: Commit** 288 + 289 + ```bash 290 + git add bugs.html 291 + git commit -m "feat(bugs): render faceted text in bug overlay" 292 + ``` 293 + 294 + --- 295 + 296 + ### Task 8: Update comment rendering to use facets 297 + 298 + **Files:** 299 + - Modify: `bugs.html:~2519` (renderComment function) 300 + 301 + **Step 1: Replace comment body rendering** 302 + 303 + Find: 304 + ```javascript 305 + <p class="comment-body">${esc(comment.body)}</p> 306 + ``` 307 + 308 + Replace with: 309 + ```javascript 310 + <p class="comment-body">${renderFacetedText(comment.body, comment.bodyFacets)}</p> 311 + ``` 312 + 313 + **Step 2: Commit** 314 + 315 + ```bash 316 + git add bugs.html 317 + git commit -m "feat(bugs): render faceted text in comments" 318 + ``` 319 + 320 + --- 321 + 322 + ### Task 9: Update response rendering to use facets 323 + 324 + **Files:** 325 + - Modify: `bugs.html` (renderResponse function - search for `r.message`) 326 + 327 + **Step 1: Find and replace response message rendering** 328 + 329 + Find where response message is rendered (search for `esc(r.message)` or similar) and replace with: 330 + ```javascript 331 + renderFacetedText(r.message, r.messageFacets) 332 + ``` 333 + 334 + **Step 2: Commit** 335 + 336 + ```bash 337 + git add bugs.html 338 + git commit -m "feat(bugs): render faceted text in responses" 339 + ``` 340 + 341 + --- 342 + 343 + ### Task 10: Update handleSubmitBug to parse facets 344 + 345 + **Files:** 346 + - Modify: `bugs.html:~3206` (handleSubmitBug function) 347 + 348 + **Step 1: Parse description and steps before creating input** 349 + 350 + Before the mutation input is constructed, add: 351 + 352 + ```javascript 353 + const descriptionParsed = parseFacets(description); 354 + const stepsParsed = parseFacets(steps); 355 + ``` 356 + 357 + **Step 2: Update the input object** 358 + 359 + Change the input object to use parsed values: 360 + 361 + ```javascript 362 + description: descriptionParsed.text, 363 + descriptionFacets: descriptionParsed.facets, 364 + stepsToReproduce: stepsParsed.text, 365 + stepsToReproduceFacets: stepsParsed.facets, 366 + ``` 367 + 368 + **Step 3: Verify manually** 369 + 370 + Submit a new bug with a URL in description. After reload, the URL should be clickable. 371 + 372 + **Step 4: Commit** 373 + 374 + ```bash 375 + git add bugs.html 376 + git commit -m "feat(bugs): parse facets when submitting new bugs" 377 + ``` 378 + 379 + --- 380 + 381 + ### Task 11: Update handleUpdateBug to parse facets 382 + 383 + **Files:** 384 + - Modify: `bugs.html:~3465` (handleUpdateBug function) 385 + 386 + **Step 1: Parse description and steps** 387 + 388 + Add before mutation: 389 + 390 + ```javascript 391 + const descriptionParsed = parseFacets(description); 392 + const stepsParsed = parseFacets(steps); 393 + ``` 394 + 395 + **Step 2: Update the input object** 396 + 397 + ```javascript 398 + description: descriptionParsed.text, 399 + descriptionFacets: descriptionParsed.facets, 400 + stepsToReproduce: stepsParsed.text, 401 + stepsToReproduceFacets: stepsParsed.facets, 402 + ``` 403 + 404 + **Step 3: Commit** 405 + 406 + ```bash 407 + git add bugs.html 408 + git commit -m "feat(bugs): parse facets when updating bugs" 409 + ``` 410 + 411 + --- 412 + 413 + ### Task 12: Update handleSubmitComment to parse facets 414 + 415 + **Files:** 416 + - Modify: `bugs.html:~2730` (handleSubmitComment function) 417 + 418 + **Step 1: Parse body before creating input** 419 + 420 + Find where `body` is used in the input and add: 421 + 422 + ```javascript 423 + const bodyParsed = parseFacets(body); 424 + ``` 425 + 426 + **Step 2: Update the input object** 427 + 428 + ```javascript 429 + body: bodyParsed.text, 430 + bodyFacets: bodyParsed.facets, 431 + ``` 432 + 433 + **Step 3: Commit** 434 + 435 + ```bash 436 + git add bugs.html 437 + git commit -m "feat(bugs): parse facets when submitting comments" 438 + ``` 439 + 440 + --- 441 + 442 + ### Task 13: Update handleSaveEditComment to parse facets 443 + 444 + **Files:** 445 + - Modify: `bugs.html:~2696` (handleSaveEditComment function) 446 + 447 + **Step 1: Parse body before updating** 448 + 449 + Find where `newBody` is used and add: 450 + 451 + ```javascript 452 + const bodyParsed = parseFacets(newBody); 453 + ``` 454 + 455 + **Step 2: Update the input object** 456 + 457 + ```javascript 458 + body: bodyParsed.text, 459 + bodyFacets: bodyParsed.facets, 460 + ``` 461 + 462 + **Step 3: Commit** 463 + 464 + ```bash 465 + git add bugs.html 466 + git commit -m "feat(bugs): parse facets when editing comments" 467 + ``` 468 + 469 + --- 470 + 471 + ### Task 14: Update handleSubmitResponse to parse facets 472 + 473 + **Files:** 474 + - Modify: `bugs.html` (handleSubmitResponse function - search for it) 475 + 476 + **Step 1: Parse message before creating input** 477 + 478 + Find where the response message is used and add: 479 + 480 + ```javascript 481 + const messageParsed = parseFacets(message); 482 + ``` 483 + 484 + **Step 2: Update the input object** 485 + 486 + ```javascript 487 + message: messageParsed.text, 488 + messageFacets: messageParsed.facets, 489 + ``` 490 + 491 + **Step 3: Commit** 492 + 493 + ```bash 494 + git add bugs.html 495 + git commit -m "feat(bugs): parse facets when submitting responses" 496 + ``` 497 + 498 + --- 499 + 500 + ### Task 15: Final verification and cleanup commit 501 + 502 + **Step 1: Full manual test** 503 + 504 + 1. Create a new bug with URL in description and steps 505 + 2. Verify links are clickable 506 + 3. Add a comment with a URL 507 + 4. Verify comment link is clickable 508 + 5. Add a response with a URL (if you have permissions) 509 + 6. Verify response link is clickable 510 + 7. Edit a bug/comment - verify links still work 511 + 512 + **Step 2: Final commit** 513 + 514 + ```bash 515 + git add bugs.html 516 + git commit -m "feat(bugs): complete rich text facet support for links" 517 + ```