PR: Editorial shape + one-truth design system (render-as-html) #42
SKILL.md
Color subsection — palette migration| 312 | 312 | ``` |
| 313 | 313 | Light: |
| 314 | −--accent #c74438 coral — THE single accent | |
| 315 | −--paper #fafaf7 cool white — page background | |
| 314 | +--paper #faf6ef warm cream — page background | |
| 315 | +--paper-tint #f3ede1 recessed surfaces, hover fills | |
| 316 | +--paper-card #fbf7f0 raised cards | |
| 317 | +--accent #8a3a1a terracotta — THE single accent (links, active state, emphasis) | |
| 318 | +--accent-2 #c2901a ochre/gold — affirmative actions only (primary button, star/select on) | |
| 316 | 319 | --ok #2f7d44 |
| 317 | 320 | --warn #9b641d |
| 318 | 321 | ``` |
#c74438 removed; terracotta #8a3a1a + ochre #c2901a take over. Cool white paper also replaced by warm cream. All existing artifacts using the old hex values will render out-of-palette until regenerated.
index.html
:root — value swap, variable names preserved (no-rename constraint)| 14 | 14 | :root { |
| 15 | − --paper: #fafaf7; /* cool white */ | |
| 15 | + --paper: #faf6ef; /* warm cream */ | |
| 16 | − --paper-tint: #f5f5f2; | |
| 16 | + --paper-tint: #f3ede1; | |
| 17 | − --accent: #c74438; /* coral */ | |
| 17 | + --accent: #8a3a1a; /* terracotta */ | |
| 18 | + --accent-2: #c2901a; /* ochre */ | |
| 18 | 19 | --ink: #1a1815; |
| 19 | 20 | --ink-soft: #4a443c; |
| 20 | 21 | --muted: #8a8378; |
| 21 | 22 | } |
index.html
In-text search — stateful regex bug & fix (Critical)| 412 | 412 | function highlightMatches(q) { |
| 413 | 413 | const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 414 | − const rx = new RegExp(esc(q), 'gi'); | |
| 415 | 414 | nodes.forEach(el => { |
| 416 | − if (rx.test(el.dataset.original)) { | |
| 417 | − el.innerHTML = el.dataset.original.replace(rx, m => | |
| 418 | − `<mark>${m}</mark>`); | |
| 419 | − } | |
| 415 | + const rx = new RegExp(esc(q), 'gi'); // fresh per node — no lastIndex drift | |
| 416 | + el.innerHTML = el.dataset.original.replace(rx, m => | |
| 417 | + `<mark>${m}</mark>`); | |
| 420 | 418 | }); |
| 421 | 419 | } |
RegExp with the g flag, called via .test() then .replace() across multiple DOM nodes, advances lastIndex on each test(). After a match, the next test() starts mid-string and may skip a valid match. Fix: construct a fresh RegExp inside the forEach loop, eliminating shared stateful state. The if (rx.test(…)) guard is also dropped — replace is a no-op on zero matches and the guard was only there to avoid replacing on non-matching nodes, which the stateful regex was causing.
index.html
Cleanup — dead data-sec attributes + deduped .cl-export| 198 | −<section data-sec="color" id="sec-color"> | |
| 198 | +<section id="sec-color"> | |
| 199 | −<section data-sec="type" id="sec-type"> | |
| 199 | +<section id="sec-type"> | |
| 200 | 200 | <section id="sec-layout"> |
data-sec was referenced by an old JS section-filter that was replaced — attributes are now dead weight. Removed across all 14 section elements. No behavior change.
All findings
A single RegExp(esc(q), 'gi') was constructed outside the forEach loop. Because g-flag regexes track lastIndex, calling rx.test(el.dataset.original) advances the cursor. The subsequent rx.replace(…) call then starts mid-string on the next node — silently skipping any match that falls before the advanced offset.
The plan specified a literal diff gate: "variable names must not change." Once the PR added --accent-2 and --paper-card (both new, not renamed), the gate tripped on every CI pass even though the constraint was satisfied — no existing variable was renamed.
After the register table was added (Reading vs Instrument), the introductory paragraph in the Design system section still described a single-palette approach without acknowledging the two registers. A reader skimming the intro would miss that the palette is applied differently by shape.
data-sec="…" was used by an old JS section-filter that was replaced in a prior commit. The attributes remained on all 14 <section> elements — harmless but misleading noise in the DOM.
.cl-export was added to style the copy-prompt textarea, but its declaration block was byte-for-byte identical to the pre-existing .cap-output class. Both set the same font, size, padding, border, background, and resize values.
.cap-output, .cl-export { … }.