Repository navigation
fix(capytone): review fixes — the generate card honours the picked colour (P0) - #108
Merged
Merged
Conversation
…ld (P0) Generate mode kept only the base colour's hue — lightness and chroma were discarded and every role re-drawn from fixed bands, so picking #1a1a2e produced a pale lavender card. The picked colour now passes into harmonyPalette whole: it IS the field (lightness and chroma verbatim, gamut-clamped at its own (L, H)), role lightness keeps the band's internal spacing relative to it, and the Field pill follows dark and light picks — overriding it pulls the colour into the chosen regime (L windows [0.55, 0.98] light / [0.08, 0.45] deep) so the composition stays coherent. Mid/accent hues still land exactly on the harmony offsets; the ink-vs-bg contrast ≥ 4.5 contract is unchanged and still pinned. A partial or unparseable hex no longer silently falls back to hue 0 (red): the text box keeps the draft, the engine keeps the last valid colour, and the existing "Not a colour" note explains the frozen card. Verified: full vitest suite (2053 passed, incl. 5 new tests pinning the colour-fidelity contract), lint, next typegen + tsc, and a live page check (#1a1a2e renders rgb(26,26,46) with the field pill on Deep; invalid input freezes the card). Review: docs/research/capytone-review-2026-10-11.md, finding 1. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
families.ts derived each anchor's hue family from a naive arithmetic mean of its hueAnchors, so any anchor straddling the 0° wrap mis-filed: cherry-blossom's [20°, 350°] averaged to 185° (teal) and the pink pastel was listed under the Cool filter. The circular mean — already used by this module's own blessed-hue check, with a comment naming exactly this trap — puts the same anchors at 5°, squarely pink, and cherry-blossom now files under Warm. carnival-night keeps its existing override (now consistent with the derivation rather than masking it); no other anchor's family changes. Review: docs/research/capytone-review-2026-10-11.md, finding 2. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…chroma (P1) The "popping" filter's blurb is "festival-grade chroma", but the tag keyed on chromaRange's TOP (>= 0.115), so any quiet mood whose accent may spike filed in beside carnival and confetti: fireside-night, cinnamon-cafe, honeyed-afternoon, harvest-fair, brass-band-evening, moss-carpet and fern-gully all appeared under Popping. Keying on the range FLOOR (>= 0.085) keeps exactly the anchors that are always loud — the floors separate the festival trio (diwali 0.09, carnival 0.10, confetti 0.11) from every other anchor (<= 0.08). A floor is the honest reading of the blurb: chroma this mood is guaranteed to wear, not chroma it may reach. Every demoted anchor stays reachable from its other filters (warm/dusk), pinned by test. Review: docs/research/capytone-review-2026-10-11.md, finding 3. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
For radial gradients the fallback head carried no direction clause, and the stop list was joined with an unconditional comma: the fallback emitted "radial-gradient(, #121212 0%, ..." - a comma directly after the paren is invalid CSS, and Chrome drops the whole declaration (verified via computed style during the review). The exact engines the hex fallback exists for painted nothing at all. Linear and conic keep their comma (their heads carry the angle / from-clause); the radial fallback now joins its stop list directly. The radial golden gains the fallback shape, and a sweep over all 3 shapes x 4 interpolation spaces pins that no fallback ever opens with a bare comma. Review: docs/research/capytone-review-2026-10-11.md, finding 4. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…e 0 (P1) The page-change effect skipped page 0 (a mount double-draw guard), so when the 8-page cycle wrapped from the short last page back to page 0 nothing repainted: the persistent canvases kept the previous page's art under page-0 labels and links, and the two canvases the short page had unmounted came back never drawn - blank. Every full loop reproduced it for the whole 4.2 s dwell (caught live during the review). One effect now draws on mount AND every page change, which is strictly simpler than the guard it replaces. Review: docs/research/capytone-review-2026-10-11.md, finding 5. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
The feel-mode spacebar shortcut exempted only HTMLInputElement, so with a mood submitted and a mood pill focused, keydown Space ran preventDefault - which cancels a button's space activation - and remixed instead. A keyboard user could not activate mood pills at all; Space silently re-rolled the card (caught live during the review: seed changed with a pill focused). The shortcut now fires only when the page itself has focus (body/documentElement); on any focused control Space keeps its native job. The Remix button remains for pointer and keyboard users alike. Review: docs/research/capytone-review-2026-10-11.md, finding 6. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
The hub passed only the FEEL palette to CheckMode and BlendMode, so a palette just built in Generate was unreachable there: switching to Check after tuning a harmony card silently showed the last mood card - or the bare defaults when no mood was up. The hub now tracks which card-owning mode was visited last (cardSource) and hands Check and Blend that card, so the mode you left is the pair you get to interrogate. Extract's jump-to-feel routes through the same tracking. Image and Extract palettes still live inside their modes; their bridge out lands in a later commit. Review: docs/research/capytone-review-2026-10-11.md, finding 7. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…(P1) Feel and Generate keep their state across mode switches (it lives in the hub) while Check, Blend and Extract destroyed yours on unmount - build a gradient, glance at Feel, come back to defaults. That asymmetry is what made the hub feel arbitrary. Their state is lifted to the hub now: - Check: the text/background pair (plus its card-role loaders) - Blend: stops, shape, angle and interpolation space - Extract: the url and the whole extraction status, result included Untouched state follows the current card - the seeded view the components used to initialise with, and what a ?mood= share link expects - and once anything is edited, it persists per field. Image keeps its own state for now (a decoded photo is not serialisable hub state); its palette gets a bridge out in the next commit. Review: docs/research/capytone-review-2026-10-11.md, finding 8. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
Extract's swatches could deep-jump into Feel, but Image's could reach nothing: every hex out of a photo went through the clipboard by hand. The hub now hands ImageMode two bridges - each swatch and the picked pixel gain Check (the hex lands in Check mode as the text colour) and Generate (the hex becomes Generate's base colour, field auto-following as everywhere else). The swatch card restructures from a bare copy button into a card with the copy button plus the two bridge pills, since a button cannot nest a button. The token-export dialect (numeric role names 1..12 under the "image" slug) stays as-is on purpose: a photo palette has no bg/accent semantics, and the names already follow the shared cssVariables convention. Review: docs/research/capytone-review-2026-10-11.md, finding 9. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…(P1) check.ts's APCA path trimmed its input and measured 0-255 clamped channels, while the WCAG path (engine/color.ts contrastRatio) parsed the string raw and linearised whatever culori returned. Two consequences: a pasted leading space failed the WCAG parse and read as a 1:1 Fail beside a healthy APCA Lc - contradictory verdicts on the same string - and a wide-gamut input measured unclamped, able to exceed the 21:1 ceiling WCAG defines as impossible. Both parsers trim now, and contrastRatio clamps channels to 0-1 before linearising, matching APCA. toOklchOrNull trims too, so ColorPick's own gate agrees with both engines. In-gamut hexes are untouched - ensureContrast, the guardrails and every golden keep their values. Review: docs/research/capytone-review-2026-10-11.md, finding 10. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
The pill dots showed each anchor's hand-tuned BLESSED hexes, but the synthesizer never uses them - it re-draws hue, chroma and lightness from the anchor's ranges at the phrase's seed. Worst drift: coastal-fog's pill showed pale fog #C9CFCC over a mid-grey #7b8a86 card (deltaE 21); cozy-autumn-morning drifted 24 degrees into maroon. The pill now renders the five roles of the SAME deterministic call the card makes (generatePalette(phrase) at the derived seed), so what you tap is byte-for-byte what you get and the two can never disagree again. The blessed hexes stay in the lexicon as the tuning ground truth the family tests measure against. Review: docs/research/capytone-review-2026-10-11.md, finding 11. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
The stop hex was a promise the synthesizer only partly kept: the pinned hue wandered +/-18 degrees (Poppy #E0576B landed pink at #c9688e, Festival Gold khaki) and the field-chroma ceiling sat at the mood engine's 0.15, pastelising vivid stops. Stops now wander just +/-8 degrees (harmony gates still hold: 8 + the accent's 14 < 30), the chroma ceiling is the stop's OWN chroma (a hex is in-gamut by construction; an explicitly tapped vivid color is the user's call), and the chroma draw floor rises from 0.75x to 0.85x so the seed lottery cannot wash the stop out. Measured worsts across all 30 stops x derived seed: deltaE 11.6 -> 6.7, hue drift 18 -> 8 degrees, chroma drop 32% -> 24%. The fidelity test pins the tighter bounds; the guardrail test still passes unchanged (the ceiling only exceeds 0.15 for poppy, within the measured epsilon). Review: docs/research/capytone-review-2026-10-11.md, finding 12. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai> Port-pin note: the "start from warm orange" literal is regenerated with the new stop behaviour (the anchor pins are untouched - the mood engine is).
Check mode's five 'From the card' role swatches all loaded into the TEXT slot - tapping 'Field' put the field colour where the text goes, and the natural ink-vs-field check required typing the field hex by hand. A second row loads the same roles as the background; the first row is relabeled 'as text' to say which slot it feeds. The mode's lifted state makes this a two-line change. Review: docs/research/capytone-review-2026-10-11.md, finding 13. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…0/15 (P2) The review flagged the band ladder as polarity-blind; checking the published source says otherwise - APCA in a Nutshell (Myndex/SAPC-APCA) publishes ONE polarity-neutral ladder, and the tool had TWO levels of it wrong: 75 was labelled 'preferred body text' when the published preferred level for fluent body text is 90 (75 is the minimum for columns of body text), and the 60 tier appears in no current published table. Re-keyed to the published ladder (90/75/45/30/15), labels updated, band-table test pinned to the boundaries including 60 landing in large-text. The APCA readme cross-check confirms no polarity-specific thresholds - the metric is polarity-aware (the Lc sign), the guidance table is not; the UI's polarity note stays as is. Review: docs/research/capytone-review-2026-10-11.md, finding 14 (its polarity premise does not survive the source check; the mislabeled ladder it pointed at did). Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…lias (P2) cssTokens appended '/* camelCase alias: rainyTuesday */' to its output - a comment promising an alias the snippet does not contain. The variables already carry the slug, and nothing consumes a camelCase form, so the honest fix is removing the line (and with it the now-unused slug computation); the byte-for-byte pin in the image tests loses the line with it. Review: docs/research/capytone-review-2026-10-11.md, finding 15. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…k (P2)
A proxy 502 HTML page or a truncated body made response.json() throw,
which landed in the catch and reported 'The wire went quiet' - a network
drop - for what is really the server answering with something unreadable.
The body is parsed defensively now: unparseable or lacking a palette
resolves to the 'upstream' failure kind ('The site said no.'), and only a
true fetch-level throw reports network. JSON error kinds from the route
are untouched.
Review: docs/research/capytone-review-2026-10-11.md, finding 16.
Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
Every error in the whole pipeline - decode refusals included - surfaced as
'The browser's decoder refused it', which would be wrong for a palette-
build or canvas failure. The catch now distinguishes DecodeFailedError
(the decoder copy, unchanged) from everything else ('That file wouldn't
read' - a tool fault, not a file fault), each with its own error card.
Review: docs/research/capytone-review-2026-10-11.md, finding 17.
Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
ColorPick pushed every keystroke to the parent, so typing a hex made Check's verdicts and Blend's ramp vanish and reappear character by character, and the native picker silently displayed black for any half-typed text. Invalid text now lives in a local draft: the note shows beside the field while the parent keeps its last valid colour, the picker shows that last valid colour too, valid text pushes through immediately, and blur reverts the draft. All three consumers (check, blend, generate) benefit; their state-gating from the earlier fixes is untouched. Review: docs/research/capytone-review-2026-10-11.md, finding 18. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
Canvas text never triggers @font-face loads, so document.fonts.ready - which resolves when currently-pending loads finish - could settle before Fraunces or Albert Sans had ever fetched, and the card silently painted fallbacks. ensureFontsReady now asks for each face first (fonts.load at the weights the renderers use: 300 display, 500 mono, 700 sans), then awaits ready; a refused load falls through honestly. ExampleCards routes through it instead of its own bare ready. Pattern seconded from capystamp/fonts.ts. Review: docs/research/capytone-review-2026-10-11.md, finding 19. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
31 picks over a 2x2-by-4 grid meant the cycle's last page showed three cards and one empty slot, every loop. A 32nd entry - 'sakura', a real keyword route to cherry-blossom, rendered editorial where 'cherry blossom' itself is minimal - makes 8 pages of exactly 4. No anchor is duplicated in the visible set; the deep link resolves like any pill. Review: docs/research/capytone-review-2026-10-11.md, finding 20. Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
…ield and ink Translating every role band by (field - band middle) slid the accent onto the ink on fields far from the band's middle (#2f4a80 deep: accent/ink 1.01:1). Roles now map proportionally between the field and the ink pole; a test pins accent/ink >= 1.35 and accent/field >= 1.6 across picks, fields, rules and seeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/30/15) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… focus ring) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes finding 1 (P0) of the CapyTone comprehensive review (2026-10-11,
docs/research/capytone-review-2026-10-11.md, owner-local): Generate mode now honours the selected colour instead of keeping only its hue. This PR collects the review fixes — each lands as its own commit; the P1s follow.The defect
CapyTone.tsxfed onlytoOklchOrNull(genHex)?.hintoharmonyPalette— the picked colour's lightness and chroma were discarded and every role re-drawn from fixed bands. Picking dark navy#1a1a2erendered a pale lavender card (field L 0.88); a partial or unparseable hex silently became hue 0 (red family) mid-typing.The fix
harmony.ts—harmonyPaletteaccepts the picked colour asbaseColor: it is the field (lightness and chroma verbatim, gamut-clamped at its own (L, H)), and role lightness keeps each band's internal spacing relative to it, so mid-tone picks don't wash out. NewfieldForBaseLightness()maps a pick to Light (L ≥ 0.55) or Deep (L ≤ 0.45) and defers mid-tones. When the Field pill is set against the colour, the field's lightness is pulled into the regime (windows [0.55, 0.98] light / [0.08, 0.45] deep) so the composition and the ≥ 4.5 ink contract hold. Harmony hue offsets are untouched and still pinned.CapyTone.tsx— the hex box keeps a draft state; the engine only ever sees parseable colours, so mid-typing freezes the last valid card (with the existing "Not a colour this can read" note) instead of flipping red. The Field pill auto-follows the pick.tests/capytone-hub.test.tspin the colour-fidelity contract (within one 8-bit step of the picked hex), the forced-regime coherence (roles in physical range, contrast ≥ 4.5), hue offsets with a base colour, determinism, and the field mapping.Verification
npx vitest run— 2053/2053 passed (94 files), including the 5 new.npm run lint— clean.npx next typegen && npx tsc --noEmit— clean.#1a1a2e→ card field pixel rgb(26,26,46) with the Field pill auto-flipped to Deep;#8e9b7e→ rgb(141,155,125); typingzzzor#1a→ card stays put with the note shown.Co-Authored-By: GLM-5.3-Flash noreply@z.ai
Review follow-ups (4 commits, Claude Opus 5.5)
The PR also carries the review's P1/P2 fixes (19 commits after the P0 — see the commit list). A review of the whole branch found:
b234fdb) — translating every role band by (field − band middle) slid the accent onto the ink:#2f4a80deep → accent/ink 1.01:1;#8a3b2aforced light → near-black accent beside black ink. Roles now keep their proportional place between the field and the ink pole. Worst case across 9 picks × 2 fields × 6 rules × 3 seeds: accent/ink 1.38, accent/field 1.67 (the no-pick engine's own floor is 1.57); pinned by a new test that fails on the previous code.cb42f4d) — the branch dropped Lc 60; restored as "Other content text minimum (60+)", so the ladder is the published 90/75/60/45/30/15.9e14df2) — image-swatch Check/Generate pills were hand-styled (no hover fill, no focus ring); nowBTN.6604a62) — typing#6b6b6bpassed through#6b6(light) and left a mid-tone on the light field; 3/4-digit shorthands no longer move the pill.Verified: vitest 2059/2059, eslint and
next typegen && tsc --noEmitclean; in the dev server#2f4a80gives a navy field with a gold accent distinct from the pale ink, and the field pill follows to Deep.Known, left as is: Space no longer remixes while a mood pill has focus (correct for keyboard users).