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