aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio
Commit message (Collapse)AuthorAgeFilesLines
* test(theme-studio): add a one-command test runner and make targetCraig Jennings10 hours2-0/+91
| | | | | | The browser hash gates were run by hand through headless Chrome, so a picker-JS regression only surfaced if someone remembered to run them. run-tests.sh now drives the whole pyramid in one command: regenerate the page, the Python templating tests, the Node unit tests plus inline-integrity, a syntax check of the spliced page script, and the six browser hash gates. It exits non-zero on any failure, and make theme-studio-test calls it. The browser gates need a Chromium-family browser. When none is found they report SKIPPED rather than passing, so a machine without Chrome can't turn the gates silently green.
* test(theme-studio): make generate.py importable and test the templatingCraig Jennings10 hours2-9/+102
| | | | | | The page generator's risky logic, the export-strip that inlines colormath.js and the placeholder substitution that fills in the sample and palette data, had no tests. A bug there ships a broken theme-studio.html the JS tests can't see, since they import colormath.js directly and never look at the assembled page. I pulled the strip into a strip_exports function and guarded the file write behind __main__, so importing generate.py builds the page in memory without writing it. test_generate.py asserts the strip removes export lines, preserves the body, and rstrips. It asserts the assembled page has every placeholder filled and carries the colormath body verbatim. And it guards that colormath.js keeps its export on a single line, since the line-based strip would leave a multi-line export's continuation behind. That was the exact failure that bit during the first integration.
* refactor(theme-studio): extract plane and palette-ΔE logic into the tested coreCraig Jennings10 hours4-40/+140
| | | | | | | | The picker's two heaviest pieces of pure logic lived as strings inside generate.py, reachable only through the single-scenario browser hash tests. I moved them into colormath.js, where they get the same direct Node testing the color math has: planeCell(L,C,H) returns a C×L plane cell's color or flags it out of gamut, and paletteWarnings(palette, threshold, cap) does the pairwise ΔE analysis and returns the too-close pairs, the overflow count, and each color's nearest neighbor. The page now calls both. The inline copies are gone. The new Node tests cover what the hash tests never could: empty, single, and identical-color palettes; the strict threshold boundary; the cap and overflow count; closest-first ordering; the C=0 achromatic case; and a plane cell pinned to oklch2hex's clamped flag so the plane and the commit path agree on the gamut edge. The refactor preserves behavior: the page renders identically, guarded by the existing #deltatest and #planetest characterization gates.
* feat(theme-studio): render a Chroma×Lightness plane in OKLCH modeCraig Jennings10 hours5-12/+126
| | | | | | | | Perceptual-metrics Phase 4b, the last piece of the OKLCH editor. In OKLCH mode the picker's square becomes a Chroma (x) by Lightness (y) plane at the current hue. The crosshair maps to (C, L) and the hue strip selects H. The unreachable region is greyed out so the sRGB gamut boundary at that hue is visible, and the AA/AAA contrast mask overlays on top of the reachable colors. The per-cell in-gamut test is forward-only: oklch to oklab to linear-rgb plus a channel-range check, never the binary search, which stays in oklch2hex for committing a chosen color. colormath.js now exports oklab2lrgb, inGamut, and lrgb2hex (with direct Node tests, including one that pins inGamut to oklch2hex's clamped flag so the plane and the commit path agree on the boundary). The rendered bitmap caches on hue, dimensions, mask, and background, so dragging C and L at a fixed hue reuses it. HSV stays untouched: the square keeps its saturation/value gradient and the existing contrast mask. A #planetest headless guard asserts the crosshair lands at the color's (C, L), an out-of-gamut cell renders as the grey fill, and an in-gamut cell renders as a real color.
* feat(theme-studio): add an OKLCH edit mode to the pickerCraig Jennings11 hours2-8/+126
| | | | | | | | Perceptual-metrics Phase 4a. The picker gains an edit-model toggle (HSV / OKLCH) held in its own pkModel state, orthogonal to the existing AA/AAA contrast mask (pkMode). The two never share state: one is how you edit the color, the other is what constraint you mask. In OKLCH mode the picker shows L, C, and H as paired range and number inputs that drive the color through oklch2hex, updating the hex field, swatch, readouts, and the HSV cursor. When the requested chroma is out of sRGB gamut the dials snap to the reachable color and a "chroma clamped to sRGB" line appears. HSV stays the default, and the SV square keeps editing in HSV (the C×L plane is Phase 4b). An SV drag in OKLCH mode refreshes the dials so the two surfaces stay consistent. A #oklchtest headless guard asserts switching to OKLCH preserves the color, toggling the mask leaves pkModel alone, switching the model leaves pkMode alone, the dials drive the color to a known OKLCH target, and an out-of-gamut chroma raises the clamp status.
* feat(theme-studio): warn on too-similar palette colors by ΔECraig Jennings11 hours2-0/+78
| | | | | | Perceptual-metrics Phase 3. renderPalette now runs a pairwise OKLab ΔE over the palette and warns on any pair below the named DELTAE_MIN threshold (0.02). The warning lists the closest pairs first, caps at five, and appends "and N more" so a noisy palette never hides the count. Each chip's tooltip gains its nearest-neighbor ΔE. paletteDeltas computes the pairs and the per-color nearest distance in one pass, feeding both the chip titles and the warning list. Palette names go through esc before they reach the warning markup. A #deltatest headless guard asserts a near-identical pair fires and names itself, a spread palette stays quiet, and a tight cluster caps at five in ascending order with the overflow suffix.
* fix(theme-studio): lay out picker OKLCH/APCA readouts in two columnsCraig Jennings12 hours2-2/+2
| | | | The .pinfo2 row had the font and margin but not the flex space-between the .pinfo row above it uses, so the OKLCH and APCA spans ran together as "90°APCA Lc -39". With space-between the OKLCH coordinates sit under the hex and the APCA Lc sits under the WCAG ratio, matching the row above.
* feat(theme-studio): show OKLCH and APCA readouts in the pickerCraig Jennings12 hours3-4/+41
| | | | | | | | Perceptual-metrics Phase 2. The picker now shows a second readout row under the WCAG ratio: the OKLCH coordinates (L, C, hue°) and the signed APCA Lc against the ground color. WCAG and all three contrast tables are untouched, so APCA stays picker-only for v1. APCA Lc carries its sign convention in a tooltip and in the README: positive is dark text on a light background, negative is light text on a dark background, so a light color on dupre's dark ground reads negative. pkReadout drives the new spans from the inlined colormath functions, and a #readouttest headless guard loads dupre-blue and asserts the spans match both the live computation and the known OKLCH reference, with WCAG unchanged.
* feat(theme-studio): inline colormath.js, migrate WCAG/HSV helpersCraig Jennings12 hours4-19/+317
| | | | | | | | Perceptual-metrics Phase 1. generate.py inlines the colormath.js body into the page script, stripping the ES-module export so one source feeds both the browser and the Node tests. The page's own lin, rl, contrast, rating, hsv2rgb, rgb2hsv, hex2rgb, and rgb2hex copies move into colormath.js. normHex, textOn, and ratingColor stay in the page as UI-boundary helpers. rl now reuses colormath's canonical lin (0.04045 cutoff) instead of the old 0.03928 form. The two are byte-identical on every #rrggbb: no 8-bit channel falls between the cutoffs (10/255 = 0.0392, 11/255 = 0.0431), confirmed over 200k random pairs with zero contrast change and no AA/AAA flips. test-colormath.mjs adds Normal/Boundary/Error cases for the migrated helpers, a seeded hsv-rgb round-trip property test, and an inline-integrity check that the generated page carries the colormath.js body verbatim, so the inlined copy and the tested module can't drift.
* feat(theme-studio): add colormath.js perceptual color coreCraig Jennings12 hours2-0/+189
| | | | colormath.js is the pure color-math module both theme-studio features need: OKLab/OKLCH conversions, oklch2hex with a binary-search gamut clamp, APCA (APCA-W3 0.1.9), and deltaE-OK. It's tested directly in Node (test-colormath.mjs under node --test) against the spec's fixtures (OKLab anchors, the red and dupre-blue OKLCH values, APCA at 106.0 and -107.9, the clamp invariants), at 100% line and 90% branch coverage. Next: generate.py inlines it and the existing rl/contrast/hsv helpers move in.
* docs(theme-studio): add color-assignment guideCraig Jennings13 hours2-0/+477
| | | | theme-coloring-guide.org is the design philosophy behind the tool, organized from principles out: seven laws, a role-to-treatment seed table, and three tiers (syntax, UI faces, org packages) as that table projected onto Emacs faces. It documents a shade budget per hue family, a functional signal-color convention table (not an emotion table), color-vision rules, and Emacs face specifics, and closes with canonical references. The README points to it, and dupre is the worked example.
* fix(theme-studio): open the picker crosshair on the current colorCraig Jennings19 hours2-2/+4
| | | | openPicker positioned the crosshair while the picker was still display:none, so its client dimensions read 0 and the crosshair pinned to the top-left corner instead of the shown color. I moved display:block ahead of paintPicker so the element has layout when its size is read, then added a headless #cursortest that opens the picker on a saturated color and checks the crosshair lands off-corner.
* feat(theme-studio): add dupre theme design exportsCraig Jennings20 hours2-0/+10586
| | | | Saved theme-studio exports for the dupre palette. dupre.json holds the base palette, UI, and syntax assignments; dupre-revised.json adds the full per-package face set across 51 packages. build-theme.el converts either into a loadable deftheme.
* fix(theme-studio): make a face-row click visibly flash its preview elementCraig Jennings23 hours2-10/+16
| | | | Clicking a UI or package face already called the flash, but two things hid it: the flashtok animation was scoped to #codepre and .ex cells, so the class landed on the mock-frame and package-preview spans without ever animating; and the flash never scrolled its target into view, so an element below the fold of a preview's scroll box flashed unseen. The animation is now global, and the flash scrolls its element into view (the first of several, via a new flashEls helper). Clicking a face in either tier now lands the viewport on the matching preview element and lights it up.
* refactor(theme-studio): rename theme-selector to theme-studioCraig Jennings23 hours7-0/+3287
The tool authors themes from scratch -- palette, faces across every tier, live preview, export to a loadable deftheme. It never selects among existing themes, so "selector" mis-described it. Renamed the directory, the generated HTML and its title, the design spec, and every reference in the code, README, tests, and todo. No behavior change.