| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
| |
The released/pressed bevel was a flat translucent white/black overlay, which reads weaker than the box Emacs draws. reliefColors in colormath.js now ports Emacs 30's x_alloc_lighter_color: highlight = bg x1.2, shadow = bg x0.6, an additive boost for dark backgrounds, and the same-color fallback for pure black and white. boxCss takes the face's effective bg and derives both edges from it. Pressed swaps the pair, and the translucent pair remains only when no bg is known. Width stays 1px because dupre's :line-width -1 draws 1px lines in Emacs too. The gap was color strength, not width. Five node tests pin hand-computed fixtures from the C source, and a new #beveltest gate pins the wiring.
|
| | |
|
| | |
|
| |
|
|
| |
A default-fg (p) change only re-rated the covered overlay faces. UI-face and package ratios that fall back to the default fg kept their old number until something else rebuilt the tables. The p branch now runs the same repaint as a ground-bg change. The new #contrasttest assertion drives the real syntax dropdown (unlocking a locked p row for the test) so the handler wiring is pinned, not just the repaint helper.
|
| | |
|
| |
|
|
| |
The contrast cells already rated a two-color face's own fg-on-bg. They read wrong because applyGround blanketed every .ex cell (the per-face preview cells included) with the ground bg, and a ground-bg change never repainted the UI or package tables. The preview showed fg on the ground bg next to a correct fg-on-face-bg ratio, and ground-dependent ratios went stale. applyGround now blankets only the code panes and syntax example cells and repaints UI faces through paintUI. The ground-bg handler also rebuilds the package table and preview. New #contrasttest assertions pin the two-color pair in both tables, preview-bg survival, and ground-change re-rating.
|
| | |
|
| |
|
|
| |
org-lint's misplaced-heading checker flags any mid-line "** " in a paragraph. The quoted "** TODO" examples in two manual-test Expected lines tripped it.
|
| |
|
|
| |
A headless render with an empty --user-data-dir falls back to the real Chrome profile and takes its SingletonLock. A hung render held that lock for 18 hours on 2026-06-09, blocking every interactive Chrome launch. The runner now refuses to run the browser gates when mktemp fails, and each render runs under timeout --kill-after so a wedged Chrome dies in seconds.
|
| | |
|
| |
|
|
| |
The two color-sorting reviews resolved the warm-color grouping problem; LCCL is implemented (04b82bbe). The only remaining piece, a per-hex family-hint override for the irreducible ramp-collision case, is filed as its own task.
|
| |
|
|
|
|
|
|
|
|
| |
Replace the hue-anchor bucketing and the tent neutral threshold with the model two independent reviews of color-sorting.org converged on (Codex and Fable, with Fable's harness measuring pairwise F1 0.63 → 0.96 on the real palette).
Chromatic colors now cluster by complete-linkage agglomeration on a lightness-conditioned hue distance: hue must match tightly at equal lightness and may drift across a lightness gap, because a tonal ramp drifts in hue with lightness by design. A low-chroma noise term widens the tolerance where hue is ill-defined, and a chroma clause keeps a vivid accent out of a soft same-hue family. Complete linkage makes single-linkage chaining structurally impossible. The neutral threshold is floored at both ends instead of tapering to zero, which fixes two real defects: pale warm grays (gray+1, gray+2) that leaked into a color column, and pure white (C=0 at L=1) that evaded a zero threshold.
On the sterling/distinguished palette this separates the gold and olive ramps (the green/yellow complaint), keeps the red and blue ramps whole including drifted tints, isolates intense-red, and consolidates every gray and steel into the neutral column. The one residual — pale yellow+2 lands on the olive ramp — is geometrically irreducible from the hex (it sits on the olive trajectory by nearest-neighbor, ramp-line fit, and eye); only its name says gold. That needs the deferred per-hex family-hint override.
New node tests cover the gold/olive split, blue pale-tint cohesion, gray/white neutrality, intense-red isolation, and palette-order independence. The count gate now asserts the count action adds all ramp colors to the palette rather than that they all display in one family, since a chroma-eased extreme can sit at the neutral boundary.
|
| | |
|
| |
|
|
|
|
|
|
| |
Export stays a flat palette and import needs no reconstruction, because families are derived from the hex every render rather than stored. A #roundtriptest gate confirms export to import to export is byte-identical, and that the exported palette is still a flat [hex, name] list. Package seeding is unaffected since it reads the same flat palette.
The spec's planned ramp-step warning exemption is dropped after analysis: a generated ramp's steps are a stepL apart, well above the too-similar ΔE threshold, so they never trigger the warning, and exempting same-family pairs would hide genuine near-duplicates that should be flagged (the case #deltatest checks). So the warning stays on the full palette.
README documents color families: the hue grouping and its limitation, the ground strip, the per-column count control and regenerate, removed-step references reading "(gone)", and the removal of the standalone ramp panel. Phase 6, the last phase; the color-families v1 build is code-complete.
|
| |
|
|
|
|
|
|
| |
Editing a family's base now recolors the whole family: update-selected on a base with a ramp regenerates the family from the new base at the same count, so references follow the new hexes (shared regenFamilyInPlace with the count control). Editing a ground swatch already writes the bg/fg assignment through the existing repoint, and the gate confirms it.
The standalone ramp panel is gone — its button, panel, JS, CSS, and the #ramptest gate are removed. Fanning a color into a ramp now happens from its strip: add a color, then raise its column's count. The ramp() math stays in app-core; only the duplicate UI is retired.
Phase 5 of the color-families spec. A #baseedittest gate covers the base-edit recolor (family follows, references repoint, count preserved) and the bg-swatch edit writing the assignment.
|
| |
|
|
|
|
|
|
| |
Each chromatic family column gets a count input (0-4) showing its current per-side reach. Setting N regenerates the family as a symmetric base ±N ramp from its most-saturated color, replacing the family's current members. A reference to a surviving step (matched by signed lightness rank) follows the new hex through repointHex; a reference to a step removed by lowering N is left on its old hex, which is no longer in the palette and renders as "(gone)" — never silently reassigned. The neutral and ground strips get no control.
I also fixed the neutral threshold curve: it was flat-high through the darks, which pulled a chroma-eased dark ramp step (a dark desaturated blue) into the neutral column and broke the family. The curve now tapers toward both lightness extremes, peaking near mid, so dark and light tints both keep their hue while mid grays stay neutral. This is the symmetric form of the Munsell scaling and a strict improvement.
Phase 4 of the color-families spec. A #counttest gate covers count-up adding symmetric steps, count-down dropping the extremes, the surviving-step repoint, and the removed-step "(gone)".
|
| |
|
|
|
|
|
|
|
|
| |
neutral cut
Replace gap-based hue clustering and the flat neutral threshold. Chromatic colors now bucket by nearest perceptual hue anchor (red, orange, yellow, green, teal, blue, purple, pink), so adjacent categories stay separate by construction and there's no single-linkage chaining merging them through intermediate tones. The neutral cut is lightness-scaled rather than flat: a color reads as neutral below a chroma that's highest in the mid-tones and tapers toward the light end, so a faint mid gray goes neutral while an equally-faint pale tint keeps its hue.
This fixes the two concrete problems: the grays and steels consolidate into one neutral column, and pale tints (light blues) stay with their hue instead of falling into the grays. What it doesn't fix is hue-adjacent warm colors: this palette's olive-greens sit on top of the golds in OKLCH hue, so they still group together, and a ramp that drifts in hue can split across an anchor boundary. That's a real property of the colors, not a bug, and it's filed for research (a writeup of the problem and the four approaches tried lives outside the repo; the task points to it).
20 family node tests including the yellow/green split and the no-chaining case; suite green.
|
| |
|
|
| |
Each family now reads top to bottom (dark to light) as a column, with families arranged left to right, rather than horizontal rows stacked down the panel.
|
| |
|
|
|
|
| |
The palette panel is now a stack of strips: the pinned ground strip (bg, fg) first, then hue-sorted family strips, each dark to light. Grouping comes from familiesFromPalette off the hex every render, so renaming a color never moves it. The flat PALETTE stays the editable truth and chips keep their per-chip remove / rename / select; the move-arrow and drag reordering are gone since the sort is deterministic now (moveColor and the drag state with them).
Phase 3 of the color-families spec. A #familytest gate checks the ground strip pins first, families render, chips keep their controls, and a color renamed to anything stays in the same strip. Existing palette flows (delta, heal, ramp gates) stay green.
|
| |
|
|
|
|
| |
sortFamilies orders the strips for display: neutrals first by lightness, then chromatic families by base hue, ties broken by base lightness then base hex. Each family's members come back sorted dark to light. Hue is compared rounded so a sub-degree hue hair from gamut quantization doesn't outrank lightness. Sorting is display-only; the stored palette order is untouched.
Phase 2 of the color-families spec, pure logic. Four node tests cover the hue order, the neutral pin, within-family lightness order, and the (hue, then lightness) ordering invariant. Suite 91 to 95 green.
|
| |
|
|
|
|
| |
Four pure functions in app-core.js, all derived from the hex so renaming never moves a color. familiesFromPalette groups a flat palette into the ground strip (the bg/fg assignment hexes, pinned, de-duped) plus hue families: near-neutrals split off by a chroma threshold, the rest cluster by hue proximity with a 25-degree gap and a 360 wrap, each family's base its most-saturated member. regenFamily returns a family's symmetric ramp around the base (n=0 is the base alone, handled without ramp()'s 1-4 clamp). rankByLightness gives each current member a signed offset from the base, and stepRepointPlan maps old positions to new ones across a regenerate, listing the positions that drop out so the caller can leave their references a visible "(gone)".
Phase 1 of the color-families spec, pure logic, no UI. 13 node tests cover the gap split/merge, neutrals, absent and de-duped ground hexes, n=0, lightness ranking, and the survivor/removed repoint split. Suite 78 to 91 green.
|
| |
|
|
| |
Craig confirmed the spec Ready. The six phases become tracked tasks under the color-families parent — family model and sort cores, strip rendering, the count control, the ground strip plus ramp-panel retirement, and the warnings/seeding/export/README close-out — all :solo:. Three manual aesthetic checks (hue grouping matches the eye, regenerate-replace reads as deliberate, removed-step references read as "(gone)") go under the Manual testing parent.
|
| |
|
|
| |
Codex re-reviewed the updated palette-ramps spec and confirmed it implementation-ready; the history gets a Ready-verification entry and the metadata table is realigned. The spec was already built and shipped, so this just closes its review trail.
|
| |
|
|
|
|
|
|
| |
Resolved both open decisions per Craig: theme.json stays flat, and the standalone ramp panel goes away in favor of the per-strip control. The bigger change is the grouping mechanism: families are now derived from OKLCH hue off the hex, never from a name convention, so renaming a color to anything never moves it between strips.
That pivot designs out the Codex review's two hardest blockers. There's no step-name grammar and no import inference, because grouping isn't name-based. And the palette stays a flat, individually-editable list rather than transferring ownership to family objects, so per-chip rename/remove/edit keep working and there's no ownership contract to invent. Families are a display view over the existing palette.
The rest of the review is folded as written, adapted to the flat model: the ground strip is synthesized from the bg/fg assignments (pinned, editable, de-duped by hex), removed-step references degrade to a visible "(gone)" rather than a silent jump, n=0 is handled without ramp(), and the neutral (0.02) and hue-gap (25) thresholds and sort tie-breakers are pinned. Review file consumed and deleted; dispositions and a responder entry are in the spec.
|
| |
|
|
|
|
| |
The palette panel becomes color families instead of a flat row of chips: a base color plus its tonal ramp, shown as a dark-to-light strip with a per-family symmetric step count (N gives base ±N). Families are live, so changing the count or the base regenerates the strip; regeneration is authoritative. fg and bg share a fixed ground strip, other standalone colors are singletons, and strips sort by hue across the panel with steps sorted by lightness within.
The spec keeps theme.json a flat palette (families derived from the ramp naming on import) and reuses the shipped ramp() and assignment re-point. Two decisions are left open for Craig: flat-vs-structured persistence, and whether the per-family count control should replace the standalone ramp panel. Six phases, each leaving the tree green; the v1 build it extends is cross-linked from the task.
|
| |
|
|
|
|
| |
Add-all inserted every step after the source, giving base, -2, -1, +1, +2. Now the darker steps go before the base and the lighter ones after, so the palette reads -2 -1 base +1 +2, matching the preview row and how a ramp reads left to right.
Inserting a darker step before the base shifted the base's index, so I bump the selected index to keep the selection (and the next preview's base) on the base color rather than drifting onto an inserted step. The #ramptest gate now checks the steps surround the base in order.
|
| |
|
|
|
|
| |
Deleting a palette color leaves any assignment pointing at it showing "(gone)". Recreating a color with the same deleted name now re-points those stranded assignments to the new color, even when its hex differs, instead of leaving them stuck on the old hex forever.
Delete records the removed name and hex; the next add of that name re-points every reference (syntax map, UI faces, package faces) to the new hex and consumes the record. The registry clears on import so a stale name from a previous theme can't re-bind across a load. I pulled the re-point loop that update-selected already used into a shared helper. A #healtest gate covers delete-then-recreate-with-a-new-hex.
|
| |
|
|
| |
A ramp step whose name already exists in the palette is skipped on add, but the only signal was a count. Now preview marks each colliding tile with a dashed outline and a badge, and the message names every collision, so you can see which steps won't add before you add them. Add-all reports the skipped names too, not just how many. The single-tile add already named its one collision; this extends the same warning to preview and add-all.
|
| |
|
|
| |
The ramp preview tiles were narrower than the palette chips, so longer hexes and names had less room. They now match the palette tile width (128px) and hold the same column count.
|
| |
|
|
| |
Each ramp step tile showed only its derived name. It now shows the hex underneath too, matching the palette chips, so you can read the generated value without hovering for the title. The #ramptest gate asserts every tile carries its hex line.
|
| |
|
|
| |
Preview captured the base only when the ramp panel first opened, so selecting a different palette color and pressing preview kept ramping the old one. Now preview re-reads the color-selection tile each time (the selected palette color, or a typed hex and name), so selecting a color then pressing preview just works, the same as reopening the panel. The #ramptest gate now asserts the base follows the tile.
|
| |
|
|
| |
All five build phases plus the README close-out landed; the parent stays open pending the manual aesthetic and real-Emacs-fidelity sign-off.
|
| |
|
|
|
|
| |
The README now covers the ramp generator (the controls and their defaults, naming, and collision behavior), the worst-case-floor readout and its limiting foreground, the five covered overlay faces, the safe-lightness picker guidance, and that WCAG drives PASS/FAIL with APCA as a diagnostic. The browser-gate list is updated with the full set, including #ramptest, #contrasttest, and #safetest.
Closes the README and test-surface acceptance criteria for the palette-ramps v1 build.
|
| |
|
|
|
|
|
|
| |
The OKLCH picker gets a "safe for" selector listing the covered overlay faces. Pick one and the C×L plane shades the lightness band too light to keep that face readable over its foreground set, with the L_max ceiling as the band's lower edge. The ceiling is one marker computed via lMax at the current chroma, not a per-pixel foreground-set mask over the plane, so the existing AA/AAA mask stays single-foreground.
When no foreground is dark enough to fail, the band hides; when even black can't satisfy the target, the whole plane shades. The band only shows in OKLCH mode and clears in HSV. The cursor moved above the band so it stays visible through the shade.
Phase 5 of the palette-ramps spec, the last build phase. A #safetest browser gate pins that the band appears for a selected covered face with a positive height and hides when none is selected.
|
| |
|
|
|
|
|
|
| |
The five covered overlay faces (region, hl-line, highlight, lazy-highlight, isearch) now show the worst-case floor over their foreground set instead of one fg/bg pair. The cell starts with "worst:", names the limiting foreground by role and hex, then gives the ratio and a PASS/FAIL verdict, so a tint that clears the default text but fails the darkest token can't hide. The foreground set is the live syntax-token colors plus the default fg; the verdict is WCAG AA by default. Package and other UI rows keep their single-pair readout.
A syntax-color edit now repaints the covered faces, since their floors depend on the whole token palette, not just their own fg/bg. An out-of-scope face falls through to the single-pair cell, and a face whose set resolves empty reads "no fg set" rather than a bogus ratio.
Phase 4 of the palette-ramps spec. A #contrasttest browser gate pins the readout, the keyword-blue limiting case, the single-pair fallback, and the no-set message.
|
| |
|
|
|
|
|
|
| |
A ramp button on the palette controls opens a panel that generates a tonal ramp from the current color and previews the steps. Each step is a swatch labeled with its derived name (blue, blue+1, blue-1) and a clamp badge when the color left the sRGB gamut, so an out-of-gamut step is visible before it's added. The n, stepL, and chroma-ease controls default to 2 / 0.08 / 0.5 and re-preview live.
Clicking a step adds it to the palette; "add all" adds the lot. Steps insert adjacent to the source swatch in -n..+n order. A name collision is flagged and skipped rather than overwriting an existing color, and a generated hex that already matches another entry is added but flagged as a duplicate.
This is Phase 2, the DOM around the pure ramp() from Phase 1. A new #ramptest browser gate pins the step count, the ordered insertion after the source, the collision skip, and the clamp badge on an out-of-gamut step.
|
| |
|
|
|
|
|
|
| |
A background overlay sits behind many foregrounds at once, so its real constraint is the worst-case contrast over the whole set, not the single fg/bg pair the contrast cell shows today. Phase 3 adds three pure functions in app-core.js for that.
fgSetFor(face, state) builds a covered face's foreground set: the distinct syntax-token colors plus the default foreground, each labeled by syntax role. It returns a structured reason ('out-of-scope' or 'empty') rather than a bogus set when the face isn't covered or has no syntax assignments. floor(bgHex, fgSet) returns the minimum WCAG contrast over that set with the limiting foreground's hex and label. lMax(hue, chroma, fgSet, target) finds the lightest background that still clears the target, scanning L up from black to bracket the dark-side crossing then binary-searching it, and reports status ok/none/all/clamp.
state is passed explicitly (covered set, syntax assignments, default fg) so the functions read no globals and the Node tests stay direct. The closed five-face covered set lives here as COVERED_FACES, shared with app.js. Tests include the sterling keyword-blue worst case as a fixture, plus lMax's none/all/clamp branches.
|
| | |
|
| |
|
|
|
|
|
|
| |
ramp(baseHex, {n, stepL, chromaEase}) in app-core.js turns one base color into a tonal ramp: 2n steps at offsets -n..-1 and +1..+n, ordered darkest to lightest, base excluded. It holds the OKLCH hue, steps lightness by stepL, eases chroma toward the extremes so only the farthest step loses most of its color, and gamut-clamps each step with its own clamped flag.
Bad input returns a structured result rather than throwing: an unparseable base gives {steps: [], error: 'bad-hex'}, and out-of-range n/stepL/chromaEase clamp into range with the clamped knob named in adjusted. Defaults are n=2, stepL=0.08, chromaEase=0.5.
This is Phase 1 of the palette-ramps spec: pure logic, no UI. Tests cover mid/near-white/near-black bases, hue-hold, chroma easing, knob clamping, and malformed hex. The integrity stripper for app-core.js now drops import lines too, since the core imports normHex and the colormath helpers for the Node tests (stripped on inline, where both are already in scope).
|
| |
|
|
|
|
| |
The palette-ramps spec is Ready, so its six phases become tracked tasks under a v1 build parent: ramp core, ramp UI, foreground-set/floor/lMax core, worst-case readout, safe-lightness picker, and a README plus test-surface close-out. All six are :solo: — each verifies through node tests or a headless hash-gate plus a screenshot read.
The aesthetic and real-Emacs-fidelity checks that need Craig's eye go under the existing Manual testing parent: does a generated ramp harmonize, does the safe-lightness guidance read clearly, and does a tint the tool calls safe actually keep every token readable behind real syntax.
|
| |
|
|
|
|
|
|
|
|
| |
Resolved both open decisions. The contrast target is WCAG AA by default, with AAA selectable and APCA shown as a diagnostic only. The v1 foreground set is the distinct syntax-assignment hexes plus the default foreground, with locked background-only roles excluded.
Pinned what the review flagged as underspecified: a closed five-face covered set (region, hl-line, highlight, lazy-highlight, isearch), ramp defaults and palette-insertion rules (n/stepL/chromaEase, naming, collisions, clamp display), and explicit-state structured-error contracts for ramp, fgSetFor, floor, and lMax. Package and non-overlay UI cells stay single-pair in v1.
Closed the v1 face set rather than keeping the review's open-ended "any face the buffer renders text over" clause, since an open set reintroduces the gap the foreground-set decision exists to close. Kept throwing for genuine programmer error while returning structured results for bad user input, matching colormath.js.
The spec is implementation-ready. The six implementation tasks get created once Craig confirms the go. Review file consumed and deleted.
|
| |
|
|
| |
Design doc (spec-create workflow) for two coupled v1 features: generating a harmonized tonal ramp from a base color, and a worst-case-contrast readout that caps a background tint at the lightness where every foreground on it stays readable. Cross-linked from the color-harmony task; harmonic fill deferred to vNext.
|
| | |
|
| | |
|
| |
|
|
| |
sterling.json becomes the canonical default: merged sterling's palette/colors with the audited face structure (today's Fidelity fixes + boxes) and pointed generate.py's default at it. The seed reader now also applies the theme's package overrides, so the no-seed page is fully sterling. THEME_STUDIO_SEED still overrides to view another palette.
|
| | |
|
| |
|
|
| |
consistency-check tasks
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
| |
defaults, boxes
Acting on the per-package face audit (every package's faces checked against the live daemon and source):
Removed 11 nonexistent mu4e faces — mu4e-cited-1-face through -7-face, mu4e-attach-number-face, mu4e-compose-header-face, mu4e-moved-face. Newer mu4e dropped these (citations now inherit message-cited-text-N), so they themed nothing and showed phantom samples in the preview. Confirmed absent in the installed mu4e source before removing.
Boxed the faces that actually use :box, now that the attribute exists: magit-branch-current and magit-branch-remote-head (flat line box, matching :box t), and the released-button buttons — two flycheck list faces, the telega box-button family, and the slack button/dialog/reaction faces.
Restored the structural defaults the seeds had dropped relative to each package's own defface: underline on org-link/-cite/-cite-key, lsp-face-highlight-read/-rename, flycheck-error/-warning/-info, mu4e-header-highlight-face, calibredb-search-header-highlight-face, telega-describe-section-title, slack-message-output-header/-channel-button; bold on org-special-keyword, lsp-face-highlight-write, all four git-gutter faces, flycheck-error-list-highlight, the erc nick cluster (nick-default, button-nick-default, nick-prefix, my-nick-prefix, nick-msg, notice, pal, button), org-noter's two faces, and three slack label faces; italic on dirvish-media-info-property-key; height on lsp-details-face (0.8), shr-sup (0.8), calibredb-current-page-button (1.1).
Left the taste-call contradictions for review (shr-h3/h4-6, telega-msg-user-title, erc-action-face) — these are theme-design choices, not clear defects. dashboard, dired, org-drill, elfeed, signel, and pearl were clean.
|
| | |
|