summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-04-21-tag-cloud-spiral.md
blob: 808b2cbfa373cc6564be40e149e98dcf32091e6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# Tag Cloud Spiral Layout Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the flat flex-wrap tag cloud with an Archimedean spiral layout where big tags cluster at center and smaller tags scatter outward, deterministically per tag text.

**Architecture:** Vanilla JS module reads `data-weight` attributes rendered by the Hugo partial, sorts tags by weight, places them along an outward Archimedean spiral with AABB collision detection, then sets absolute positions. Falls back to existing centered flex layout when JS is disabled or container is < 400px wide.

**Tech Stack:** Hugo (partial modification), Vanilla JS (ES5-compatible, no build step), Hugo Pipes (minify), existing CSS variables for theming.

---

## File Map

| Action | Path | Responsibility |
|--------|------|----------------|
| Modify | `themes/danix-xyz-hacker/layouts/partials/tag-cloud.html` | Add `data-weight` attr to links, `data-tag-cloud` to container |
| Modify | `themes/danix-xyz-hacker/assets/css/main.css` | Add `overflow: visible` to `.tag-cloud` |
| Create | `themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js` | Spiral placement algorithm |
| Modify | `themes/danix-xyz-hacker/layouts/_default/baseof.html` | Load spiral script via Hugo Pipes |

---

### Task 1: Add data attributes to tag-cloud partial

**Files:**
- Modify: `themes/danix-xyz-hacker/layouts/partials/tag-cloud.html:42,49,57`

- [ ] **Step 1: Add `data-tag-cloud` to both container divs**

In `tag-cloud.html`, both render paths have `<div class="tag-cloud">`. Add `data-tag-cloud` attribute to each:

```html
{{- if $wrapInWidget -}}
<div class="sidebar-widget">
  <p class="sidebar-widget-label"># {{ i18n "topTags" }}</p>
  <nav aria-label="{{ i18n "exploreTopics" }}">
    <div class="tag-cloud" data-tag-cloud>
{{- else -}}
<section aria-labelledby="tag-cloud-heading">
  <{{ $headingLevel }} id="tag-cloud-heading" class="text-lg font-semibold text-accent mb-4">
    {{ $heading }}
  </{{ $headingLevel }}>
  <nav aria-label="{{ i18n "exploreTopics" }}">
    <div class="tag-cloud" data-tag-cloud>
{{- end -}}
```

- [ ] **Step 2: Add `data-weight` to each tag link**

In the `range $orderedTags` block, add `data-weight="{{ printf "%.4f" $ratio }}"` to the `<a>` tag:

```html
        <a
          href="{{ .Page.RelPermalink }}"
          class="tag-cloud-link"
          data-weight="{{ printf "%.4f" $ratio }}"
          {{- if ge $ratio 0.5 }}
          style="font-size: {{ $size }}rem; color: var(--accent); opacity: {{ $opacity }};"
          {{- else }}
          style="font-size: {{ $size }}rem; color: var(--text-dim); opacity: {{ $opacity }};"
          {{- end }}
          aria-label="{{ .Name }}{{- if $showCount }} ({{ i18n "postCount" $count }}){{- end -}}"
        >
```

- [ ] **Step 3: Verify Hugo renders correctly**

Run: `hugo serve` and inspect homepage tag cloud in browser devtools.
Expected: each `<a>` has `data-weight="0.XXXX"`, container `<div>` has `data-tag-cloud` attribute.

- [ ] **Step 4: Commit**

```bash
git add themes/danix-xyz-hacker/layouts/partials/tag-cloud.html
git commit -m "feat: add data-weight and data-tag-cloud attributes to tag cloud partial"
```

---

### Task 2: Add `overflow: visible` to `.tag-cloud` CSS

**Files:**
- Modify: `themes/danix-xyz-hacker/assets/css/main.css:357`

- [ ] **Step 1: Update `.tag-cloud` rule**

Find `.tag-cloud` rule (around line 357) and add `overflow: visible`:

```css
.tag-cloud {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  align-items: baseline;
  overflow: visible;
}
```

- [ ] **Step 2: Rebuild CSS**

Run: `npm run build`
Expected: exits 0, `main.min.css` updated.

- [ ] **Step 3: Commit**

```bash
git add themes/danix-xyz-hacker/assets/css/main.css themes/danix-xyz-hacker/assets/css/main.min.css
git commit -m "feat: add overflow visible to tag-cloud container"
```

---

### Task 3: Create the spiral JS module

**Files:**
- Create: `themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js`

- [ ] **Step 1: Create the file with full algorithm**

```javascript
document.addEventListener('DOMContentLoaded', function () {
  var containers = document.querySelectorAll('[data-tag-cloud]');
  if (!containers.length) return;

  Array.prototype.forEach.call(containers, function (container) {
    if (container.offsetWidth < 400) return;

    var links = Array.prototype.slice.call(
      container.querySelectorAll('.tag-cloud-link')
    );
    if (!links.length) return;

    // Sort descending by weight (biggest first = placed near center)
    links.sort(function (a, b) {
      return parseFloat(b.dataset.weight) - parseFloat(a.dataset.weight);
    });

    // String hash → deterministic angle seed (0..2π)
    function hashAngle(str) {
      var h = 0;
      for (var i = 0; i < str.length; i++) {
        h = (h * 31 + str.charCodeAt(i)) & 0xffffffff;
      }
      return ((h >>> 0) / 0xffffffff) * 2 * Math.PI;
    }

    // AABB collision check
    function overlaps(a, b) {
      return !(
        a.right  < b.left  ||
        a.left   > b.right ||
        a.bottom < b.top   ||
        a.top    > b.bottom
      );
    }

    var placed = [];
    var containerWidth = container.offsetWidth;
    var cx = containerWidth / 2;

    // Measure each tag before repositioning
    var sizes = links.map(function (link) {
      var rect = link.getBoundingClientRect();
      return { w: rect.width, h: rect.height };
    });

    // Switch container to relative positioning
    container.style.position = 'relative';
    container.style.display = 'block';

    var padding = 8; // px gap between tags
    var aStep = 0.3; // radians per spiral step
    var rScale = (containerWidth * 0.018); // spiral tightness

    var minTop = 0, maxBottom = 0;

    links.forEach(function (link, i) {
      var w = sizes[i].w;
      var h = sizes[i].h;
      var seed = hashAngle(link.textContent.trim());
      var theta = seed;
      var placed_rect;

      // Step along spiral until no collision
      for (var attempt = 0; attempt < 2000; attempt++) {
        var r = rScale * theta;
        var x = cx + r * Math.cos(theta) - w / 2;
        var y = r * Math.sin(theta) - h / 2;

        var candidate = { left: x, top: y, right: x + w, bottom: y + h };
        var collision = false;

        for (var j = 0; j < placed.length; j++) {
          var p = placed[j];
          var padded = {
            left: p.left - padding,
            top: p.top - padding,
            right: p.right + padding,
            bottom: p.bottom + padding
          };
          if (overlaps(candidate, padded)) {
            collision = true;
            break;
          }
        }

        if (!collision) {
          placed_rect = candidate;
          break;
        }
        theta += aStep;
      }

      if (!placed_rect) {
        // Fallback: just append to flow if spiral exhausted
        link.style.position = 'static';
        return;
      }

      placed.push(placed_rect);

      link.style.position = 'absolute';
      link.style.left = Math.round(placed_rect.left) + 'px';
      link.style.top = Math.round(placed_rect.top) + 'px';

      if (placed_rect.top < minTop) minTop = placed_rect.top;
      if (placed_rect.bottom > maxBottom) maxBottom = placed_rect.bottom;
    });

    // Normalize: shift all tags so topmost is at y=16px
    var offset = 16 - minTop;
    links.forEach(function (link) {
      if (link.style.position === 'absolute') {
        link.style.top = (parseInt(link.style.top) + offset) + 'px';
      }
    });

    // Set container height to fit all tags + 2rem bottom padding
    container.style.height = (maxBottom - minTop + 48) + 'px';
  });
});
```

- [ ] **Step 2: Verify file saved correctly**

Run: `wc -l themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js`
Expected: ~100 lines.

- [ ] **Step 3: Commit**

```bash
git add themes/danix-xyz-hacker/assets/js/tag-cloud-spiral.js
git commit -m "feat: add Archimedean spiral layout engine for tag cloud"
```

---

### Task 4: Load the script via Hugo Pipes in baseof.html

**Files:**
- Modify: `themes/danix-xyz-hacker/layouts/_default/baseof.html`

- [ ] **Step 1: Add script block after existing JS entries**

In `baseof.html`, find the block of script tags (around line 83–120). Add after the last existing script block (before `</body>`):

```html
  <!-- Tag cloud spiral layout -->
  {{ $tagCloudScript := resources.Get "js/tag-cloud-spiral.js" | minify }}
  <script src="{{ $tagCloudScript.RelPermalink }}"></script>
```

- [ ] **Step 2: Verify Hugo builds without error**

Run: `hugo` (build, not serve)
Expected: exits 0, no errors about missing resources.

- [ ] **Step 3: Commit**

```bash
git add themes/danix-xyz-hacker/layouts/_default/baseof.html
git commit -m "feat: load tag-cloud-spiral.js via Hugo Pipes"
```

---

### Task 5: End-to-end verification

**Files:** None modified — verification only.

- [ ] **Step 1: Start dev server and open homepage**

Run: `hugo serve`
Open: `http://localhost:1313`
Expected: tag cloud at bottom of homepage shows spiral layout. Big tags near center, smaller tags scattered outward.

- [ ] **Step 2: Test narrow viewport fallback**

In browser devtools, set viewport width to 375px (iPhone SE).
Expected: tag cloud falls back to flex-wrap layout (no absolute positioning applied).

- [ ] **Step 3: Test JS-disabled fallback**

In devtools → Settings → Debugger → "Disable JavaScript". Reload.
Expected: tag cloud shows centered flex-wrap (current layout). No broken positioning.

- [ ] **Step 4: Test sidebar tag cloud**

Open any article page with sidebar.
Expected: sidebar tag cloud (≤15 tags) also renders in spiral if container ≥ 400px; flex fallback on narrow.

- [ ] **Step 5: Test 404 page**

Open: `http://localhost:1313/404.html`
Expected: tag cloud renders spiral layout.

- [ ] **Step 6: Keyboard navigation**

Tab through tags on homepage. Expected: all tags focusable in DOM order (weight descending), visible focus ring (accent outline).

- [ ] **Step 7: Dark/light mode**

Toggle theme. Expected: tag colors correct in both modes (CSS variables, not hardcoded).

- [ ] **Step 8: Determinism check**

Hard-reload page 3 times. Expected: tag positions identical each load.

- [ ] **Step 9: Final commit if any fixups made**

```bash
git add -p
git commit -m "fix: tag cloud spiral fixups from e2e verification"
```