OverviewFilesCommitsIssuesDocsReleasesPeers

codetree commits / 246b5b0

Groups get their own area: a group is not a card and does not get a card's gap
Reported twice — "the groups still overlap", then "boxes are still stacked on top of each other". Both times I measured rectangle intersection, found zero, and looked no further. That was the wrong question. The boxes never intersected. They were separated by 26px — the same gap used between CARDS INSIDE a box — so two category boxes butted together read as one continuous stack: @noble/hashes users (0, 0) h=87 Crypto & Identity (0, 113) h=172 <- 26px Data Structures (0, 311) h=172 <- 26px File Storage & Dist (0, 509) h=98 <- 26px Layout now has a groupGap (90px), applied when a group sits on either side of a boundary, vertically and between columns. Cards inside a group keep the tight card gap, or every box becomes mostly empty space — both are asserted. Measured on the 56-module scan: smallest gap between stacked groups is now 90px, was 26. Extent 5132x1396 -> 5594x1460, which is the cost and it is small. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
246b5b0fd915bda9a378a10b588dffc27550f583
by plankatron <fleeky@gmail.com> · 2026-09-21T00:19:23.000Z

Changed files (2) +62 -8

lib/layout.js +26 -8
……@@ -25,6 +25,14 @@ const DEFAULTS = {
2525 pad: 16, // container padding around its contents
2626 labelH: 18, // extra headroom inside a container for its label
2727
28 // Separation between GROUPS, as opposed to between cards.
29 //
30 // A group is not a card and must not get a card's gap. With both at 26px, two
31 // category boxes sat 26px apart and read as one stack — reported twice as the
32 // groups overlapping, when in fact they never intersected. Each group needs to
33 // occupy its own area of the page.
34 groupGap: 90,
35
2836 // How tall one layer may grow before it wraps into another column.
2937 //
3038 // Layering decides x, but a layer with many members and no edges to spread
……@@ -173,23 +181,33 @@ export function layout (input, opts = {}) {
173181 // layer starts — so a supplier still sits left of its consumer.
174182 let y = 0
175183 let colW = 0
176 let layerW = 0
184 let holdsGroup = false
185 let prev = null
177186 for (const item of col) {
187 // The gap BEFORE this item: a group on either side of the boundary earns
188 // the group gap, otherwise it is a card gap.
189 if (prev !== null) {
190 const gap = (prev.kind === 'container' || item.kind === 'container')
191 ? O.groupGap
192 : O.rowGap
193 y += gap
194 }
178195 if (y > 0 && y + item.h > O.maxColumnHeight) {
179 x += colW + O.colGap
180 layerW += colW + O.colGap
196 x += colW + (holdsGroup ? O.groupGap : O.colGap)
181197 y = 0
182198 colW = 0
199 holdsGroup = false
183200 }
184201 placed.push({ id: item.id, kind: item.kind, dx: x, dy: y, w: item.w, h: item.h })
185 y += item.h + O.rowGap
202 y += item.h
186203 if (item.w > colW) colW = item.w
187 if (y - O.rowGap > maxH) maxH = y - O.rowGap
204 if (item.kind === 'container') holdsGroup = true
205 if (y > maxH) maxH = y
206 prev = item
188207 }
189 void layerW
190 x += colW + O.colGap
208 x += colW + (holdsGroup ? O.groupGap : O.colGap)
191209 }
192 const contentW = Math.max(0, x - O.colGap)
210 const contentW = Math.max(0, x - Math.min(O.colGap, O.groupGap))
193211
194212 const isRoot = gid === ROOT
195213 const padL = isRoot ? 0 : O.pad
test/layout.test.js +36 -0
……@@ -91,6 +91,42 @@ test('layout: SIBLING CONTAINERS CANNOT OVERLAP — the bug this exists to kill'
9191 t.absent(contains(c.zA, n.loose), 'a node outside every zone is not captured by one')
9292})
9393
94test('layout: sibling containers get their OWN breathing room, not the card gap', (t) => {
95 // Reported twice as "the groups overlap" / "boxes stacked on top of each
96 // other". They never intersected — they were separated by the same 26px used
97 // between cards INSIDE a box, so two groups butted together read as one stack.
98 // A group is not a card and does not get a card's gap.
99 const out = layout({
100 nodes: [node('a', 'zA'), node('b', 'zB'), node('c', 'zC')],
101 links: [],
102 containers: [
103 { id: 'zA', parent: null, label: 'A' },
104 { id: 'zB', parent: null, label: 'B' },
105 { id: 'zC', parent: null, label: 'C' }
106 ]
107 }, { groupGap: 90, rowGap: 26 })
108
109 const boxes = out.containers.slice().sort((p, q) => p.y - q.y)
110 for (let i = 1; i < boxes.length; i++) {
111 const gap = boxes[i].y - (boxes[i - 1].y + boxes[i - 1].h)
112 t.ok(gap >= 90, 'gap between group ' + (i - 1) + ' and ' + i + ' is ' + gap + ', not a card gap')
113 }
114})
115
116test('layout: cards inside a container keep the tight card gap', (t) => {
117 // The separation is for GROUPS. Cards within one group stay close together,
118 // or every box becomes mostly empty space.
119 const out = layout({
120 nodes: [node('a', 'z'), node('b', 'z')],
121 links: [],
122 containers: [{ id: 'z', parent: null, label: 'z' }]
123 }, { groupGap: 90, rowGap: 26 })
124
125 const ns = out.nodes.slice().sort((p, q) => p.y - q.y)
126 const gap = ns[1].y - (ns[0].y + ns[0].h)
127 t.is(gap, 26, 'cards keep the card gap')
128})
129
94130test('layout: nesting — a parent container strictly contains its child', (t) => {
95131 const out = layout({
96132 nodes: [node('outer1', 'zOuter'), node('inner1', 'zInner'), node('inner2', 'zInner')],