summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-04-21-tag-cloud-spiral-design.md
blob: 3d840c9b7cc8c97e61f7defa1b2b8bbf01d4e550 (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
# Tag Cloud: Archimedean Spiral Layout

## Context

The existing tag cloud partial uses centered flex-wrap with continuous font scaling (weight → size/opacity). Tags appear as a flat weighted list. The goal is a visually striking cloud: big tags centered, smaller tags scattered organically around the outside — deterministic per tag, not random per load.

## Approach: Archimedean Spiral + CSS Fallback

Two layers:
- **Hugo partial** (existing) renders tags in DOM with added `data-weight` attribute
- **Vanilla JS module** reads weights, sorts tags, places them via Archimedean spiral

CSS fallback (no JS / narrow viewport): existing centered flex layout unchanged.

## Components

### 1. `tag-cloud.html` partial (modify)

- Add `data-weight="{{ $ratio }}"` to each `.tag-cloud-link` element
- Add `data-tag-cloud` attribute to the `.tag-cloud` container div
- No other changes to markup or existing CSS classes

### 2. `assets/js/tag-cloud-spiral.js` (new, ~60 lines)

**Algorithm:**
1. Find all `[data-tag-cloud]` containers on page
2. For each container: collect tags, sort descending by `data-weight`
3. Per tag: derive deterministic angle seed from tag text hash
4. Place along Archimedean spiral `r = a * θ` stepping `θ` until AABB collision-free
5. Set container `position: relative`, tags `position: absolute` with computed `left/top`
6. Set container explicit height = bounding box of all placed tags + 2rem padding
7. Remove flex layout class from container after placement

**Determinism:** simple string hash (sum of charCodes) → starting `θ` offset. Same tag text always seeds same angle.

**Collision detection:** AABB check against array of already-placed rects. O(n²) — fine for ≤50 tags.

**Responsive guard:** if container `offsetWidth < 400px`, skip spiral entirely, leave flex layout intact.

### 3. Script loading

Inline `<script>` at bottom of `tag-cloud.html` partial using Hugo Pipes:
```
{{ $js := resources.Get "js/tag-cloud-spiral.js" | minify }}
<script src="{{ $js.RelPermalink }}"></script>
```
Or inline if small enough. Loaded only when partial is rendered.

## A11y Compliance (WCAG 2.1 AA)

- **DOM order** = weight order (biggest first) — logical reading sequence
- **Tab order** = DOM order — keyboard navigation follows prominence, acceptable
- **No animation**`prefers-reduced-motion` not affected
- **Container:** `overflow: visible` to prevent clipping at zoom levels
- **Existing** `aria-label` on links preserved unchanged

## CSS Changes

- Container: add `overflow: visible` and `min-height` guard (set by JS, not CSS)
- No color/spacing changes — theming via CSS variables unaffected

## Fallback Behavior

| Condition | Behavior |
|-----------|----------|
| JS disabled | Centered flex wrap (current layout) |
| Container < 400px | Centered flex wrap (JS skips spiral) |
| JS enabled, container ≥ 400px | Archimedean spiral |

## Verification

1. `npm run build` — CSS compiles clean
2. `hugo serve` — tag cloud renders on homepage, sidebar, 404
3. JS enabled wide viewport: spiral layout, big tags centered
4. JS enabled narrow viewport (< 400px): flex fallback
5. JS disabled (devtools): flex fallback
6. Keyboard Tab through tags: logical order, all focusable
7. Dark/light mode toggle: colors correct (CSS vars)
8. Screen reader (or axe DevTools): no new violations