tangled
alpha
login
or
join now
slices.network
/
tools
7
fork
atom
Tools for the Atmosphere
tools.slices.network
quickslice
atproto
html
7
fork
atom
overview
issues
1
pulls
pipelines
docs: add tangled repo search implementation plan
chadtmiller.com
3 months ago
6f932bf3
4728604f
+981
1 changed file
expand all
collapse all
unified
split
docs
plans
2025-12-18-tangled-repo-search.md
+981
docs/plans/2025-12-18-tangled-repo-search.md
···
1
1
+
# Tangled Repo Search 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:** Build a single-page HTML tool to browse and search Tangled repositories with Catppuccin theming.
6
6
+
7
7
+
**Architecture:** Single HTML file with embedded CSS and JS. Uses quickslice-client SDK to query GraphQL endpoint. Server-side search with OR/contains across name, description, actorHandle. Catppuccin Latte (light) / Mocha (dark) based on system preference.
8
8
+
9
9
+
**Tech Stack:** HTML, CSS (variables + prefers-color-scheme), vanilla JS, quickslice GraphQL API
10
10
+
11
11
+
---
12
12
+
13
13
+
### Task 1: Create HTML Structure
14
14
+
15
15
+
**Files:**
16
16
+
- Create: `tangled-repos.html`
17
17
+
18
18
+
**Step 1: Create the base HTML file with structure**
19
19
+
20
20
+
Create `tangled-repos.html` with the basic HTML structure:
21
21
+
22
22
+
```html
23
23
+
<!doctype html>
24
24
+
<html lang="en">
25
25
+
<head>
26
26
+
<meta charset="UTF-8" />
27
27
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
28
28
+
<meta
29
29
+
http-equiv="Content-Security-Policy"
30
30
+
content="default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; connect-src 'self' https://quickslice-production-ddc3.up.railway.app; img-src 'self' https: data:;"
31
31
+
/>
32
32
+
<title>Tangled Repos</title>
33
33
+
<style>
34
34
+
/* Styles will be added in Task 2 */
35
35
+
</style>
36
36
+
</head>
37
37
+
<body>
38
38
+
<div id="app">
39
39
+
<header>
40
40
+
<h1>Tangled Repos</h1>
41
41
+
<p class="tagline">Browse repositories from the Atmosphere</p>
42
42
+
</header>
43
43
+
<div class="search-container">
44
44
+
<input type="text" id="search-input" placeholder="Search repos..." />
45
45
+
<button id="clear-search" class="hidden" title="Clear search">×</button>
46
46
+
</div>
47
47
+
<div id="result-count"></div>
48
48
+
<main>
49
49
+
<div id="repo-feed"></div>
50
50
+
<div id="load-more"></div>
51
51
+
</main>
52
52
+
<div id="error-banner" class="hidden"></div>
53
53
+
</div>
54
54
+
55
55
+
<!-- Quickslice Client SDK -->
56
56
+
<script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@main/quickslice-client-js/dist/quickslice-client.min.js"></script>
57
57
+
58
58
+
<script>
59
59
+
// JavaScript will be added in Task 3+
60
60
+
</script>
61
61
+
</body>
62
62
+
</html>
63
63
+
```
64
64
+
65
65
+
**Step 2: Verify file opens in browser**
66
66
+
67
67
+
Open in browser and verify basic structure renders.
68
68
+
69
69
+
**Step 3: Commit**
70
70
+
71
71
+
```bash
72
72
+
git add tangled-repos.html
73
73
+
git commit -m "feat(tangled): add base HTML structure"
74
74
+
```
75
75
+
76
76
+
---
77
77
+
78
78
+
### Task 2: Add Catppuccin CSS Theming
79
79
+
80
80
+
**Files:**
81
81
+
- Modify: `tangled-repos.html` (style section)
82
82
+
83
83
+
**Step 1: Add CSS reset and Catppuccin variables**
84
84
+
85
85
+
Replace the `<style>` section with:
86
86
+
87
87
+
```css
88
88
+
/* CSS Reset */
89
89
+
*,
90
90
+
*::before,
91
91
+
*::after {
92
92
+
box-sizing: border-box;
93
93
+
}
94
94
+
* {
95
95
+
margin: 0;
96
96
+
}
97
97
+
body {
98
98
+
line-height: 1.5;
99
99
+
-webkit-font-smoothing: antialiased;
100
100
+
}
101
101
+
input,
102
102
+
button {
103
103
+
font: inherit;
104
104
+
}
105
105
+
106
106
+
/* Catppuccin Latte (Light) */
107
107
+
:root {
108
108
+
--bg-base: #eff1f5;
109
109
+
--bg-mantle: #e6e9ef;
110
110
+
--bg-surface0: #ccd0da;
111
111
+
--bg-surface1: #bcc0cc;
112
112
+
--text-primary: #4c4f69;
113
113
+
--text-secondary: #6c6f85;
114
114
+
--text-subtext: #7c7f93;
115
115
+
--accent: #1e66f5;
116
116
+
--accent-hover: #2a6ff7;
117
117
+
--border: #ccd0da;
118
118
+
--error-bg: #fce4e6;
119
119
+
--error-border: #e64553;
120
120
+
--error-text: #d20f39;
121
121
+
--star-color: #df8e1d;
122
122
+
--topic-bg: #dce0e8;
123
123
+
--topic-text: #5c5f77;
124
124
+
}
125
125
+
126
126
+
/* Catppuccin Mocha (Dark) */
127
127
+
@media (prefers-color-scheme: dark) {
128
128
+
:root {
129
129
+
--bg-base: #1e1e2e;
130
130
+
--bg-mantle: #181825;
131
131
+
--bg-surface0: #313244;
132
132
+
--bg-surface1: #45475a;
133
133
+
--text-primary: #cdd6f4;
134
134
+
--text-secondary: #a6adc8;
135
135
+
--text-subtext: #bac2de;
136
136
+
--accent: #89b4fa;
137
137
+
--accent-hover: #9cc4fc;
138
138
+
--border: #313244;
139
139
+
--error-bg: #45293b;
140
140
+
--error-border: #f38ba8;
141
141
+
--error-text: #f38ba8;
142
142
+
--star-color: #f9e2af;
143
143
+
--topic-bg: #313244;
144
144
+
--topic-text: #bac2de;
145
145
+
}
146
146
+
}
147
147
+
148
148
+
body {
149
149
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
150
150
+
background: var(--bg-base);
151
151
+
color: var(--text-primary);
152
152
+
min-height: 100vh;
153
153
+
padding: 2rem 1rem;
154
154
+
}
155
155
+
156
156
+
#app {
157
157
+
max-width: 700px;
158
158
+
margin: 0 auto;
159
159
+
}
160
160
+
161
161
+
/* Header */
162
162
+
header {
163
163
+
text-align: center;
164
164
+
margin-bottom: 1.5rem;
165
165
+
}
166
166
+
167
167
+
header h1 {
168
168
+
font-size: 2rem;
169
169
+
color: var(--accent);
170
170
+
margin-bottom: 0.25rem;
171
171
+
}
172
172
+
173
173
+
.tagline {
174
174
+
color: var(--text-secondary);
175
175
+
font-size: 0.875rem;
176
176
+
}
177
177
+
178
178
+
/* Search */
179
179
+
.search-container {
180
180
+
position: relative;
181
181
+
margin-bottom: 1rem;
182
182
+
}
183
183
+
184
184
+
#search-input {
185
185
+
width: 100%;
186
186
+
padding: 0.75rem 2.5rem 0.75rem 1rem;
187
187
+
border: 1px solid var(--border);
188
188
+
border-radius: 0.5rem;
189
189
+
background: var(--bg-mantle);
190
190
+
color: var(--text-primary);
191
191
+
font-size: 1rem;
192
192
+
}
193
193
+
194
194
+
#search-input::placeholder {
195
195
+
color: var(--text-secondary);
196
196
+
}
197
197
+
198
198
+
#search-input:focus {
199
199
+
outline: none;
200
200
+
border-color: var(--accent);
201
201
+
}
202
202
+
203
203
+
#clear-search {
204
204
+
position: absolute;
205
205
+
right: 0.5rem;
206
206
+
top: 50%;
207
207
+
transform: translateY(-50%);
208
208
+
background: none;
209
209
+
border: none;
210
210
+
color: var(--text-secondary);
211
211
+
font-size: 1.25rem;
212
212
+
cursor: pointer;
213
213
+
padding: 0.25rem 0.5rem;
214
214
+
line-height: 1;
215
215
+
}
216
216
+
217
217
+
#clear-search:hover {
218
218
+
color: var(--text-primary);
219
219
+
}
220
220
+
221
221
+
#result-count {
222
222
+
color: var(--text-secondary);
223
223
+
font-size: 0.875rem;
224
224
+
margin-bottom: 1rem;
225
225
+
min-height: 1.25rem;
226
226
+
}
227
227
+
228
228
+
/* Cards */
229
229
+
.card {
230
230
+
background: var(--bg-mantle);
231
231
+
border-radius: 0.5rem;
232
232
+
padding: 1rem;
233
233
+
margin-bottom: 0.75rem;
234
234
+
border: 1px solid var(--border);
235
235
+
}
236
236
+
237
237
+
.card:hover {
238
238
+
border-color: var(--bg-surface1);
239
239
+
}
240
240
+
241
241
+
/* Repo Card Header */
242
242
+
.repo-header {
243
243
+
display: flex;
244
244
+
align-items: center;
245
245
+
gap: 0.75rem;
246
246
+
margin-bottom: 0.75rem;
247
247
+
}
248
248
+
249
249
+
.repo-avatar {
250
250
+
width: 36px;
251
251
+
height: 36px;
252
252
+
border-radius: 50%;
253
253
+
background: var(--bg-surface0);
254
254
+
overflow: hidden;
255
255
+
flex-shrink: 0;
256
256
+
}
257
257
+
258
258
+
.repo-avatar img {
259
259
+
width: 100%;
260
260
+
height: 100%;
261
261
+
object-fit: cover;
262
262
+
}
263
263
+
264
264
+
.repo-meta {
265
265
+
flex: 1;
266
266
+
min-width: 0;
267
267
+
}
268
268
+
269
269
+
.repo-owner {
270
270
+
color: var(--accent);
271
271
+
text-decoration: none;
272
272
+
font-weight: 500;
273
273
+
font-size: 0.875rem;
274
274
+
}
275
275
+
276
276
+
.repo-owner:hover {
277
277
+
text-decoration: underline;
278
278
+
}
279
279
+
280
280
+
.repo-time {
281
281
+
color: var(--text-secondary);
282
282
+
font-size: 0.75rem;
283
283
+
}
284
284
+
285
285
+
/* Repo Name & Stars */
286
286
+
.repo-title-row {
287
287
+
display: flex;
288
288
+
justify-content: space-between;
289
289
+
align-items: baseline;
290
290
+
gap: 0.5rem;
291
291
+
margin-bottom: 0.5rem;
292
292
+
}
293
293
+
294
294
+
.repo-name {
295
295
+
font-size: 1.125rem;
296
296
+
font-weight: 600;
297
297
+
color: var(--text-primary);
298
298
+
text-decoration: none;
299
299
+
min-width: 0;
300
300
+
overflow: hidden;
301
301
+
text-overflow: ellipsis;
302
302
+
white-space: nowrap;
303
303
+
}
304
304
+
305
305
+
.repo-name:hover {
306
306
+
color: var(--accent);
307
307
+
}
308
308
+
309
309
+
.repo-stars {
310
310
+
color: var(--star-color);
311
311
+
font-size: 0.875rem;
312
312
+
flex-shrink: 0;
313
313
+
display: flex;
314
314
+
align-items: center;
315
315
+
gap: 0.25rem;
316
316
+
}
317
317
+
318
318
+
/* Description */
319
319
+
.repo-description {
320
320
+
color: var(--text-secondary);
321
321
+
font-size: 0.875rem;
322
322
+
margin-bottom: 0.5rem;
323
323
+
line-height: 1.4;
324
324
+
}
325
325
+
326
326
+
/* Topics */
327
327
+
.repo-topics {
328
328
+
display: flex;
329
329
+
flex-wrap: wrap;
330
330
+
gap: 0.375rem;
331
331
+
margin-bottom: 0.5rem;
332
332
+
}
333
333
+
334
334
+
.topic-tag {
335
335
+
background: var(--topic-bg);
336
336
+
color: var(--topic-text);
337
337
+
font-size: 0.75rem;
338
338
+
padding: 0.125rem 0.5rem;
339
339
+
border-radius: 1rem;
340
340
+
}
341
341
+
342
342
+
/* Footer Links */
343
343
+
.repo-footer {
344
344
+
display: flex;
345
345
+
gap: 1rem;
346
346
+
padding-top: 0.5rem;
347
347
+
border-top: 1px solid var(--border);
348
348
+
margin-top: 0.5rem;
349
349
+
}
350
350
+
351
351
+
.repo-link {
352
352
+
color: var(--text-secondary);
353
353
+
text-decoration: none;
354
354
+
font-size: 0.75rem;
355
355
+
}
356
356
+
357
357
+
.repo-link:hover {
358
358
+
color: var(--accent);
359
359
+
}
360
360
+
361
361
+
/* Status Messages */
362
362
+
.status-msg {
363
363
+
text-align: center;
364
364
+
color: var(--text-secondary);
365
365
+
padding: 2rem;
366
366
+
}
367
367
+
368
368
+
.load-more {
369
369
+
text-align: center;
370
370
+
padding: 1rem;
371
371
+
}
372
372
+
373
373
+
/* Buttons */
374
374
+
.btn {
375
375
+
padding: 0.75rem 1.5rem;
376
376
+
border: none;
377
377
+
border-radius: 0.5rem;
378
378
+
font-size: 0.875rem;
379
379
+
font-weight: 500;
380
380
+
cursor: pointer;
381
381
+
transition: background-color 0.15s, opacity 0.15s;
382
382
+
}
383
383
+
384
384
+
.btn-primary {
385
385
+
background: var(--accent);
386
386
+
color: var(--bg-base);
387
387
+
}
388
388
+
389
389
+
.btn-primary:hover {
390
390
+
background: var(--accent-hover);
391
391
+
}
392
392
+
393
393
+
.btn-primary:disabled {
394
394
+
opacity: 0.5;
395
395
+
cursor: not-allowed;
396
396
+
}
397
397
+
398
398
+
/* Error Banner */
399
399
+
#error-banner {
400
400
+
position: fixed;
401
401
+
top: 1rem;
402
402
+
left: 50%;
403
403
+
transform: translateX(-50%);
404
404
+
background: var(--error-bg);
405
405
+
border: 1px solid var(--error-border);
406
406
+
color: var(--error-text);
407
407
+
padding: 0.75rem 1rem;
408
408
+
border-radius: 0.5rem;
409
409
+
display: flex;
410
410
+
align-items: center;
411
411
+
gap: 0.75rem;
412
412
+
max-width: 90%;
413
413
+
z-index: 100;
414
414
+
}
415
415
+
416
416
+
#error-banner.hidden {
417
417
+
display: none;
418
418
+
}
419
419
+
420
420
+
#error-banner button {
421
421
+
background: none;
422
422
+
border: none;
423
423
+
color: var(--error-text);
424
424
+
cursor: pointer;
425
425
+
font-size: 1.25rem;
426
426
+
line-height: 1;
427
427
+
}
428
428
+
429
429
+
.hidden {
430
430
+
display: none !important;
431
431
+
}
432
432
+
433
433
+
/* Spinner */
434
434
+
.spinner {
435
435
+
width: 32px;
436
436
+
height: 32px;
437
437
+
border: 3px solid var(--border);
438
438
+
border-top-color: var(--accent);
439
439
+
border-radius: 50%;
440
440
+
animation: spin 0.8s linear infinite;
441
441
+
margin: 0 auto;
442
442
+
}
443
443
+
444
444
+
@keyframes spin {
445
445
+
to {
446
446
+
transform: rotate(360deg);
447
447
+
}
448
448
+
}
449
449
+
450
450
+
.loading-container {
451
451
+
display: flex;
452
452
+
flex-direction: column;
453
453
+
align-items: center;
454
454
+
gap: 0.75rem;
455
455
+
padding: 2rem;
456
456
+
color: var(--text-secondary);
457
457
+
}
458
458
+
```
459
459
+
460
460
+
**Step 2: Test light and dark modes**
461
461
+
462
462
+
Open in browser, verify Catppuccin Latte colors. Change system to dark mode, verify Mocha colors.
463
463
+
464
464
+
**Step 3: Commit**
465
465
+
466
466
+
```bash
467
467
+
git add tangled-repos.html
468
468
+
git commit -m "feat(tangled): add Catppuccin theming with light/dark support"
469
469
+
```
470
470
+
471
471
+
---
472
472
+
473
473
+
### Task 3: Add JavaScript Configuration and State
474
474
+
475
475
+
**Files:**
476
476
+
- Modify: `tangled-repos.html` (script section)
477
477
+
478
478
+
**Step 1: Add configuration and state**
479
479
+
480
480
+
Add inside the `<script>` tag:
481
481
+
482
482
+
```javascript
483
483
+
// =============================================================================
484
484
+
// CONFIGURATION
485
485
+
// =============================================================================
486
486
+
487
487
+
const SERVER_URL = "https://quickslice-production-ddc3.up.railway.app";
488
488
+
const PAGE_SIZE = 20;
489
489
+
const DEBOUNCE_MS = 300;
490
490
+
491
491
+
// =============================================================================
492
492
+
// STATE
493
493
+
// =============================================================================
494
494
+
495
495
+
const state = {
496
496
+
repos: [],
497
497
+
cursor: null,
498
498
+
hasMore: true,
499
499
+
isLoading: false,
500
500
+
searchQuery: "",
501
501
+
totalCount: 0,
502
502
+
};
503
503
+
```
504
504
+
505
505
+
**Step 2: Commit**
506
506
+
507
507
+
```bash
508
508
+
git add tangled-repos.html
509
509
+
git commit -m "feat(tangled): add configuration and state management"
510
510
+
```
511
511
+
512
512
+
---
513
513
+
514
514
+
### Task 4: Add GraphQL Query and Data Fetching
515
515
+
516
516
+
**Files:**
517
517
+
- Modify: `tangled-repos.html` (script section)
518
518
+
519
519
+
**Step 1: Add GraphQL query and fetch function**
520
520
+
521
521
+
Add after the state section:
522
522
+
523
523
+
```javascript
524
524
+
// =============================================================================
525
525
+
// GRAPHQL
526
526
+
// =============================================================================
527
527
+
528
528
+
const REPOS_QUERY = `
529
529
+
query GetRepos($first: Int!, $after: String, $where: ShTangledRepoWhereInput) {
530
530
+
shTangledRepo(
531
531
+
first: $first
532
532
+
after: $after
533
533
+
sortBy: [{ field: createdAt, direction: DESC }]
534
534
+
where: $where
535
535
+
) {
536
536
+
totalCount
537
537
+
edges {
538
538
+
node {
539
539
+
uri
540
540
+
name
541
541
+
description
542
542
+
knot
543
543
+
topics
544
544
+
website
545
545
+
actorHandle
546
546
+
createdAt
547
547
+
appBskyActorProfileByDid {
548
548
+
displayName
549
549
+
avatar { url(preset: "avatar") }
550
550
+
}
551
551
+
shTangledFeedStarViaSubject {
552
552
+
totalCount
553
553
+
}
554
554
+
}
555
555
+
}
556
556
+
pageInfo {
557
557
+
hasNextPage
558
558
+
endCursor
559
559
+
}
560
560
+
}
561
561
+
}
562
562
+
`;
563
563
+
564
564
+
// =============================================================================
565
565
+
// DATA FETCHING
566
566
+
// =============================================================================
567
567
+
568
568
+
function buildWhereClause(query) {
569
569
+
if (!query || !query.trim()) return null;
570
570
+
const q = query.trim();
571
571
+
return {
572
572
+
or: [
573
573
+
{ name: { contains: q } },
574
574
+
{ description: { contains: q } },
575
575
+
{ actorHandle: { contains: q } },
576
576
+
],
577
577
+
};
578
578
+
}
579
579
+
580
580
+
async function fetchRepos(cursor = null, searchQuery = "") {
581
581
+
const variables = {
582
582
+
first: PAGE_SIZE,
583
583
+
after: cursor,
584
584
+
where: buildWhereClause(searchQuery),
585
585
+
};
586
586
+
587
587
+
const res = await fetch(`${SERVER_URL}/graphql`, {
588
588
+
method: "POST",
589
589
+
headers: { "Content-Type": "application/json" },
590
590
+
body: JSON.stringify({ query: REPOS_QUERY, variables }),
591
591
+
});
592
592
+
593
593
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
594
594
+
595
595
+
const json = await res.json();
596
596
+
if (json.errors) throw new Error(json.errors[0].message);
597
597
+
598
598
+
return json.data.shTangledRepo;
599
599
+
}
600
600
+
```
601
601
+
602
602
+
**Step 2: Commit**
603
603
+
604
604
+
```bash
605
605
+
git add tangled-repos.html
606
606
+
git commit -m "feat(tangled): add GraphQL query and fetch function"
607
607
+
```
608
608
+
609
609
+
---
610
610
+
611
611
+
### Task 5: Add Helper Functions
612
612
+
613
613
+
**Files:**
614
614
+
- Modify: `tangled-repos.html` (script section)
615
615
+
616
616
+
**Step 1: Add helper functions**
617
617
+
618
618
+
Add after the data fetching section:
619
619
+
620
620
+
```javascript
621
621
+
// =============================================================================
622
622
+
// HELPERS
623
623
+
// =============================================================================
624
624
+
625
625
+
function showError(msg) {
626
626
+
const el = document.getElementById("error-banner");
627
627
+
el.innerHTML = `<span>${esc(msg)}</span><button onclick="hideError()">×</button>`;
628
628
+
el.classList.remove("hidden");
629
629
+
}
630
630
+
631
631
+
function hideError() {
632
632
+
document.getElementById("error-banner").classList.add("hidden");
633
633
+
}
634
634
+
635
635
+
function esc(str) {
636
636
+
if (!str) return "";
637
637
+
const d = document.createElement("div");
638
638
+
d.textContent = str;
639
639
+
return d.innerHTML;
640
640
+
}
641
641
+
642
642
+
function formatTime(iso) {
643
643
+
const d = new Date(iso);
644
644
+
const now = new Date();
645
645
+
const diff = Math.floor((now - d) / 1000);
646
646
+
647
647
+
if (diff < 60) return "just now";
648
648
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
649
649
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
650
650
+
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
651
651
+
652
652
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
653
653
+
}
654
654
+
655
655
+
function debounce(fn, ms) {
656
656
+
let timeout;
657
657
+
return (...args) => {
658
658
+
clearTimeout(timeout);
659
659
+
timeout = setTimeout(() => fn(...args), ms);
660
660
+
};
661
661
+
}
662
662
+
```
663
663
+
664
664
+
**Step 2: Commit**
665
665
+
666
666
+
```bash
667
667
+
git add tangled-repos.html
668
668
+
git commit -m "feat(tangled): add helper functions"
669
669
+
```
670
670
+
671
671
+
---
672
672
+
673
673
+
### Task 6: Add Repo Card Rendering
674
674
+
675
675
+
**Files:**
676
676
+
- Modify: `tangled-repos.html` (script section)
677
677
+
678
678
+
**Step 1: Add render function for repo cards**
679
679
+
680
680
+
Add after helpers:
681
681
+
682
682
+
```javascript
683
683
+
// =============================================================================
684
684
+
// RENDERING
685
685
+
// =============================================================================
686
686
+
687
687
+
function renderRepoCard(repo) {
688
688
+
const profile = repo.appBskyActorProfileByDid;
689
689
+
const handle = repo.actorHandle || "unknown";
690
690
+
const avatar = profile?.avatar?.url || "";
691
691
+
const displayName = profile?.displayName || handle;
692
692
+
const stars = repo.shTangledFeedStarViaSubject?.totalCount || 0;
693
693
+
const topics = repo.topics || [];
694
694
+
const tangledUrl = `https://tangled.org/${handle}/${repo.name}`;
695
695
+
696
696
+
let topicsHtml = "";
697
697
+
if (topics.length > 0) {
698
698
+
topicsHtml = `
699
699
+
<div class="repo-topics">
700
700
+
${topics.map((t) => `<span class="topic-tag">${esc(t)}</span>`).join("")}
701
701
+
</div>
702
702
+
`;
703
703
+
}
704
704
+
705
705
+
let footerLinks = `<a href="${esc(tangledUrl)}" target="_blank" class="repo-link">View on Tangled →</a>`;
706
706
+
if (repo.website) {
707
707
+
const websiteDisplay = repo.website.replace(/^https?:\/\//, "").replace(/\/$/, "");
708
708
+
footerLinks = `<a href="${esc(repo.website)}" target="_blank" class="repo-link">${esc(websiteDisplay)}</a>` + footerLinks;
709
709
+
}
710
710
+
711
711
+
return `
712
712
+
<div class="card" data-uri="${esc(repo.uri)}">
713
713
+
<div class="repo-header">
714
714
+
<div class="repo-avatar">
715
715
+
${avatar ? `<img src="${esc(avatar)}" alt="">` : ""}
716
716
+
</div>
717
717
+
<div class="repo-meta">
718
718
+
<a href="https://bsky.app/profile/${esc(handle)}" target="_blank" class="repo-owner">@${esc(handle)}</a>
719
719
+
<div class="repo-time">${formatTime(repo.createdAt)}</div>
720
720
+
</div>
721
721
+
</div>
722
722
+
<div class="repo-title-row">
723
723
+
<a href="${esc(tangledUrl)}" target="_blank" class="repo-name">${esc(repo.name)}</a>
724
724
+
${stars > 0 ? `<span class="repo-stars">★ ${stars}</span>` : ""}
725
725
+
</div>
726
726
+
${repo.description ? `<div class="repo-description">${esc(repo.description)}</div>` : ""}
727
727
+
${topicsHtml}
728
728
+
<div class="repo-footer">
729
729
+
${footerLinks}
730
730
+
</div>
731
731
+
</div>
732
732
+
`;
733
733
+
}
734
734
+
```
735
735
+
736
736
+
**Step 2: Commit**
737
737
+
738
738
+
```bash
739
739
+
git add tangled-repos.html
740
740
+
git commit -m "feat(tangled): add repo card rendering"
741
741
+
```
742
742
+
743
743
+
---
744
744
+
745
745
+
### Task 7: Add Feed and Load More Rendering
746
746
+
747
747
+
**Files:**
748
748
+
- Modify: `tangled-repos.html` (script section)
749
749
+
750
750
+
**Step 1: Add feed rendering functions**
751
751
+
752
752
+
Add after renderRepoCard:
753
753
+
754
754
+
```javascript
755
755
+
function renderFeed() {
756
756
+
const el = document.getElementById("repo-feed");
757
757
+
758
758
+
if (state.isLoading && state.repos.length === 0) {
759
759
+
el.innerHTML = `<div class="loading-container"><div class="spinner"></div><span>Loading repos...</span></div>`;
760
760
+
return;
761
761
+
}
762
762
+
763
763
+
if (state.repos.length === 0) {
764
764
+
const msg = state.searchQuery
765
765
+
? `No repos found for "${esc(state.searchQuery)}"`
766
766
+
: "No repos yet.";
767
767
+
el.innerHTML = `<div class="status-msg">${msg}</div>`;
768
768
+
return;
769
769
+
}
770
770
+
771
771
+
el.innerHTML = state.repos.map((r) => renderRepoCard(r)).join("");
772
772
+
}
773
773
+
774
774
+
function renderResultCount() {
775
775
+
const el = document.getElementById("result-count");
776
776
+
if (state.searchQuery && state.repos.length > 0) {
777
777
+
el.textContent = `${state.totalCount} results for "${state.searchQuery}"`;
778
778
+
} else if (!state.searchQuery && state.totalCount > 0) {
779
779
+
el.textContent = `${state.totalCount} repos`;
780
780
+
} else {
781
781
+
el.textContent = "";
782
782
+
}
783
783
+
}
784
784
+
785
785
+
function renderLoadMore() {
786
786
+
const el = document.getElementById("load-more");
787
787
+
788
788
+
if (state.repos.length === 0) {
789
789
+
el.innerHTML = "";
790
790
+
return;
791
791
+
}
792
792
+
793
793
+
if (!state.hasMore) {
794
794
+
el.innerHTML = `<div class="status-msg">No more repos</div>`;
795
795
+
return;
796
796
+
}
797
797
+
798
798
+
el.innerHTML = `
799
799
+
<div class="load-more">
800
800
+
<button class="btn btn-primary" onclick="handleLoadMore()" ${state.isLoading ? "disabled" : ""}>
801
801
+
${state.isLoading ? "Loading..." : "Load More"}
802
802
+
</button>
803
803
+
</div>
804
804
+
`;
805
805
+
}
806
806
+
```
807
807
+
808
808
+
**Step 2: Commit**
809
809
+
810
810
+
```bash
811
811
+
git add tangled-repos.html
812
812
+
git commit -m "feat(tangled): add feed and load more rendering"
813
813
+
```
814
814
+
815
815
+
---
816
816
+
817
817
+
### Task 8: Add Load and Search Logic
818
818
+
819
819
+
**Files:**
820
820
+
- Modify: `tangled-repos.html` (script section)
821
821
+
822
822
+
**Step 1: Add load and search functions**
823
823
+
824
824
+
Add after rendering functions:
825
825
+
826
826
+
```javascript
827
827
+
// =============================================================================
828
828
+
// ACTIONS
829
829
+
// =============================================================================
830
830
+
831
831
+
async function loadRepos(append = false) {
832
832
+
if (state.isLoading) return;
833
833
+
state.isLoading = true;
834
834
+
renderFeed();
835
835
+
renderLoadMore();
836
836
+
837
837
+
try {
838
838
+
const data = await fetchRepos(
839
839
+
append ? state.cursor : null,
840
840
+
state.searchQuery
841
841
+
);
842
842
+
const newRepos = data.edges.map((e) => e.node);
843
843
+
844
844
+
state.repos = append ? [...state.repos, ...newRepos] : newRepos;
845
845
+
state.cursor = data.pageInfo.endCursor;
846
846
+
state.hasMore = data.pageInfo.hasNextPage;
847
847
+
state.totalCount = data.totalCount;
848
848
+
849
849
+
renderFeed();
850
850
+
renderResultCount();
851
851
+
} catch (err) {
852
852
+
console.error("Load failed:", err);
853
853
+
showError(`Failed to load: ${err.message}`);
854
854
+
} finally {
855
855
+
state.isLoading = false;
856
856
+
renderLoadMore();
857
857
+
}
858
858
+
}
859
859
+
860
860
+
function handleLoadMore() {
861
861
+
loadRepos(true);
862
862
+
}
863
863
+
864
864
+
function handleSearch(query) {
865
865
+
state.searchQuery = query;
866
866
+
state.cursor = null;
867
867
+
state.repos = [];
868
868
+
state.hasMore = true;
869
869
+
loadRepos();
870
870
+
}
871
871
+
872
872
+
const debouncedSearch = debounce(handleSearch, DEBOUNCE_MS);
873
873
+
874
874
+
function clearSearch() {
875
875
+
const input = document.getElementById("search-input");
876
876
+
input.value = "";
877
877
+
document.getElementById("clear-search").classList.add("hidden");
878
878
+
handleSearch("");
879
879
+
}
880
880
+
```
881
881
+
882
882
+
**Step 2: Commit**
883
883
+
884
884
+
```bash
885
885
+
git add tangled-repos.html
886
886
+
git commit -m "feat(tangled): add load and search logic"
887
887
+
```
888
888
+
889
889
+
---
890
890
+
891
891
+
### Task 9: Add Main Function and Event Listeners
892
892
+
893
893
+
**Files:**
894
894
+
- Modify: `tangled-repos.html` (script section)
895
895
+
896
896
+
**Step 1: Add main function and event setup**
897
897
+
898
898
+
Add at the end of the script:
899
899
+
900
900
+
```javascript
901
901
+
// =============================================================================
902
902
+
// MAIN
903
903
+
// =============================================================================
904
904
+
905
905
+
function main() {
906
906
+
// Set up search input
907
907
+
const searchInput = document.getElementById("search-input");
908
908
+
const clearBtn = document.getElementById("clear-search");
909
909
+
910
910
+
searchInput.addEventListener("input", (e) => {
911
911
+
const value = e.target.value;
912
912
+
clearBtn.classList.toggle("hidden", !value);
913
913
+
debouncedSearch(value);
914
914
+
});
915
915
+
916
916
+
clearBtn.addEventListener("click", clearSearch);
917
917
+
918
918
+
// Initial load
919
919
+
loadRepos();
920
920
+
}
921
921
+
922
922
+
main();
923
923
+
```
924
924
+
925
925
+
**Step 2: Test the complete application**
926
926
+
927
927
+
1. Open `tangled-repos.html` in browser
928
928
+
2. Verify repos load on page load
929
929
+
3. Test search functionality (try "bsky", "tool", etc.)
930
930
+
4. Test clear search button
931
931
+
5. Test Load More button
932
932
+
6. Test light/dark mode switching
933
933
+
7. Verify links work (Bsky profile, Tangled repo, website)
934
934
+
935
935
+
**Step 3: Commit**
936
936
+
937
937
+
```bash
938
938
+
git add tangled-repos.html
939
939
+
git commit -m "feat(tangled): add main function and event listeners"
940
940
+
```
941
941
+
942
942
+
---
943
943
+
944
944
+
### Task 10: Add to Index Page
945
945
+
946
946
+
**Files:**
947
947
+
- Modify: `index.html`
948
948
+
949
949
+
**Step 1: Check current index.html structure**
950
950
+
951
951
+
Read `index.html` to understand the current link structure.
952
952
+
953
953
+
**Step 2: Add link to Tangled Repos tool**
954
954
+
955
955
+
Add a link to `tangled-repos.html` following the existing pattern.
956
956
+
957
957
+
**Step 3: Commit**
958
958
+
959
959
+
```bash
960
960
+
git add index.html
961
961
+
git commit -m "feat(tangled): add link to index page"
962
962
+
```
963
963
+
964
964
+
---
965
965
+
966
966
+
## Final Verification
967
967
+
968
968
+
After all tasks complete:
969
969
+
970
970
+
1. Run through full user flow:
971
971
+
- Page loads with recent repos
972
972
+
- Search filters results
973
973
+
- Clear search returns to default view
974
974
+
- Load More works
975
975
+
- All links open correctly
976
976
+
- Light/dark themes work
977
977
+
978
978
+
2. Test edge cases:
979
979
+
- Empty search results
980
980
+
- Network error handling
981
981
+
- Repos with no description/topics/website