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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
```