dev/agent-skills/cocoindex-diagrams/references/pitfalls.md
A record of mistakes encountered while building the current diagram set. Each entry: the symptom, the cause, and the fix.
Symptom: Diagrams look like black rectangles in screenshots; text is dimly visible but all fills are black.
Cause: The docs site uses base: '/docs' in astro.config.mjs,
so built HTML references CSS at /docs/_astro/*.css. A naïve
python3 -m http.server from dist/ serves CSS at /_astro/...
instead, returning 404. With no stylesheet, var(--cream), var(--coral),
etc. are undefined; SVG fill falls back to black.
Fix: Serve from a parent dir containing a docs/ symlink or
copy of dist/, so /docs/_astro/... resolves. The
scripts/preview.sh script handles this automatically.
Symptom: Multiple rows in a diagram all appear dim; only show at full opacity when the user hovers.
Cause: dg-step + dg-step-N classes. These are designed for
progressive-reveal narratives (e.g., a 3-panel "step 1 → 2 → 3") where
each step should fade in on hover with stagger. If applied to parallel
rows that should all be visible statically, they fade by default.
Fix: Remove dg-step from the <g> wrapping the row. Only use
dg-step when hover-driven reveal is intentional.
Symptom: "Split into chunks" sticks out past the box edges.
Cause: Early versions used SVG <text> which does not wrap. Manual
line-splitting via lines={['Split into', 'chunks']} worked but was
awkward.
Fix: All label-bearing primitives (DataBox, LogicBox,
ProcessingComponent, TargetBullet) now use <foreignObject> with a
flex-centered <div class="dg-fo-label">. The browser wraps against
the box width. Just pass label="...".
Symptom: <MemoMark x={PC_X + 14} y={row.pcY + 4} size={12} /> and
<MemoMark x={embedX + 6} y={mid - EMBED_H / 2 - 2} size={10} /> —
different offsets, different sizes, invisible coupling to container
dimensions.
Cause: Inset values baked into call sites instead of the primitive.
Fix: The primitive owns its inset + size. Callers pass the
container's reference corner (top-right for MemoMark). The primitive
internally does translate(x - INSET_X - w, y). Same for
ProcessingComponent's header and memo mark — the container primitive
knows its own dimensions and handles all internal positioning.
Symptom: Drive Folder in palm-green, Vector Database in coral — suggests a distinction that doesn't exist in CocoIndex.
Cause: Early LogicBox had variant="source" / variant="target"
modifiers with different fills. Kept around from homepage conventions
that don't apply to docs diagrams.
Fix: One neutral cream + maroon fill for all non-container logic
boxes. Shape (not color) carries the semantic distinction —
TargetBullet is visually different from LogicBox because of its
bullet shape, not because of color. Only App containers get the peach
tint for visual grouping.
Symptom: vector1 at y=93 points to Vector Database, but the arrow
line crosses over vector2 at y=147 because both converge to VDB
center.
Cause: Drawing all arrows to the single y-center of the destination box.
Fix: Keep arrows horizontal — draw each to the destination's left edge at the source's y-coordinate. This only works when destinations are tall enough to accept multiple horizontal entries. See layout-patterns.md.
Symptom: Drive Folder → file and vector → Vector Database both have arrowheads and moving dashes, making them feel like causal flow.
Cause: Using FlowArrow everywhere instead of distinguishing flow
from binding.
Fix: Connector (static, dashed, no arrowhead) for bindings;
FlowArrow only for causal flow. A binding is "X is bound to Y" —
Drive Folder → file (the file IS from the folder), vector → Vector
Database (the vector IS written to the DB).
Symptom: Memo marks render as dark solid maroon ribbons that visually dominate small boxes.
Fix: Use coral outline + translucent coral fill
(fill: color-mix(in oklab, var(--coral) 22%, transparent)), not a
solid color. Reads over any background and matches the CocoIndex accent
palette.
Symptom: Content hugs the left side of an App/ProcessingComponent container with a big empty gap on the right. Or the first row hugs the top while the last row has a large gap below.
Cause: Hardcoding APP.w / APP.h to a round number and then
placing content starting at a small APP_PAD. The content fits but
leaves whatever remainder on the right/bottom.
Fix: Derive the container size from the content, not the other way around:
const APP_PAD_X = 24;
const APP_W = FILE_W + GAP + PC_W + APP_PAD_X * 2; // left pad == right pad
const APP = { x: 140, y: TOP_Y, w: APP_W, h: TOP_H };
Any downstream sibling (Target System, Drive Folder right) must then be
positioned relative to APP.x + APP_W, not a hardcoded x. Same
principle vertically: compute row y-centers so top-pad == bottom-pad.
See layout-patterns.md "Balanced padding on all sides" for the full idiom.
Symptom: A ProcessingComponent with pcState='updated' paints
every nested .dg-box (chunks, embeds, vectors) gold — even the
ones explicitly left at state='idle'.
Cause: The state CSS rules were written with the descendant
combinator: .dg-state-updated .dg-box { stroke: gold; }. The
container's state class matches, and its descendant .dg-box
elements inherit the style.
Fix: Use the direct-child combinator > so the rule only applies
to the box element on the same wrapper <g> that owns the state:
.dg-state-new > .dg-box { ... }
.dg-state-updated > .dg-box { ... }
.dg-state-removed > .dg-box { ... }
.dg-state-changed > .dg-box { ... }
Each shape primitive (built on ShapeGroup) has its own wrapper
<g> with its own state class and its own direct-child .dg-box.
With >, states never cascade.
Symptom: A state="new" chunk inside a state="updated" PC
looks identical to its sibling idle chunks — palm-green stroke is
invisible against gold-tinted PC background.
Cause: Default stroke-width (1.4) makes colored state strokes too thin to pop against a tinted parent.
Fix: The dg-state-* rules in diagrams.css bump
stroke-width to 2.2 for all delta states, and the hover
dg-delta-pulse keyframes push it to 4.2 for extra visibility. If
you add a new delta state, match the pattern.
Symptom: A spinning icon (refresh badge) keeps spinning even when the user isn't looking at that diagram — makes the page feel busy.
Fix: Every animation in diagrams.css is idle by default,
active only on .dg-root:hover. Includes dg-flow, dg-pulse,
dg-delta-pulse, dg-spin (refresh badge), dg-check-draw
(cache-ready check). When adding a new animation, gate it the same
way — and register it in the @media (prefers-reduced-motion: reduce)
override at the bottom of the stylesheet.
Symptom: "The build succeeded, diagram done." Then user screenshots show overlaps / black boxes / faded content.
Fix: Always run scripts/preview.sh and Read the PNG before
reporting. A clean npm run build proves only that the Astro
components compile.