aboutsummaryrefslogtreecommitdiff
path: root/scripts
Commit message (Collapse)AuthorAgeFilesLines
* feat(theme-studio): mark safe lightness in the OKLCH pickerCraig Jennings13 hours5-7/+81
| | | | | | | | 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.
* feat(theme-studio): show worst-case contrast for overlay facesCraig Jennings13 hours3-5/+99
| | | | | | | | 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.
* feat(theme-studio): add the ramp UI in the paletteCraig Jennings13 hours5-1/+161
| | | | | | | | 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.
* feat(theme-studio): add the background-contrast safety coreCraig Jennings13 hours3-2/+221
| | | | | | | | 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.
* feat(theme-studio): add the ramp generator coreCraig Jennings14 hours4-2/+181
| | | | | | | | 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).
* feat(theme-studio): make sterling the default theme, replacing dupreCraig Jennings18 hours3-4/+5792
| | | | 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.
* fix(theme-studio): drop the contrary shr-sliced-image fg; file bevel + ↵Craig Jennings19 hours2-2/+2
| | | | consistency-check tasks
* fix(theme-studio): apply the package-face audit — dead faces, structural ↵Craig Jennings19 hours2-40/+39
| | | | | | | | | | | | | | 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.
* feat(theme-studio): add a real, exported :box face attributeCraig Jennings20 hours6-40/+92
| | | | | | | | The mode-line box in the preview was hardcoded — it showed a box the generated theme couldn't actually produce, since build-theme.el never emitted :box. Made :box a real face attribute instead: a per-face box object (style line/raised/pressed, width, color) stored on UI and package faces, set from a "box" dropdown in both tables, rendered from the attribute everywhere (the mode-line bars, the package previews via ofs, the UI table preview cells), and exported through build-theme.el's --attrs as a proper :box plist (released/pressed → :style *-button; line → :line-width + optional :color). The hardcoded box is gone; mode-line and mode-line-inactive now default to the released-button box that is the Emacs default, so the preview and the export agree. This also gives the package faces that genuinely use :box a way to represent it — the face audit found several (magit-branch-current/-remote-head, two flycheck list faces, the telega button family, ~15 slack button/dialog faces). Tests: build-theme gains box-conversion + ui-box-emit ERT tests (24/24); the app-core deep-equal tests account for the new box slot; all 9 browser gates, 20 python, and 55 node tests stay green.
* fix(theme-studio): make the live buffer preview render UI faces the way ↵Craig Jennings23 hours4-37/+137
| | | | | | | | | | | | | | | | Emacs does The mock buffer rendered several faces wrong, verified against the running daemon and emacs -Q: - highlight was flat monochrome text. Like region, the highlight face is background-only, so Emacs lets the syntax foreground show through (face merge: highlight over a keyword keeps the keyword color, adds the highlight background). Both now go through one overlay helper that keeps token colors and only overrides the foreground when the face sets one. - cursor was an empty block at end-of-line. Emacs draws a box cursor on the character at point, in the frame background over the cursor color — now it sits on a real glyph. - the overlay faces, the line-number gutter, and the paren faces ignored weight/underline/etc. They now honor bold/italic/underline/strike, matching the table preview. - the fringe showed only its background. It now draws a continuation indicator in the fringe foreground. - the mode line had no box. It now carries the 3D released-button box that is the Emacs default. Defaults seeded to the theme-independent Emacs core styles (from emacs -Q): error/warning/success are bold; lazy-highlight and show-paren-match are underlined (link already was). Added a #mocktest gate pinning these — overlay faces keep token colors, the cursor is on a glyph, styles render, the fringe indicator is present, the mode line has its box. Verified it goes red when the rendering regresses.
* build(theme-studio): add a local Makefile; root delegates test + coverageCraig Jennings24 hours2-1/+68
| | | | | | | | theme-studio is a self-contained Python + JS subproject with its own toolchain (python3, node, uvx, headless Chrome), unrelated to the root Makefile's Elisp/ERT world. Gave it a local Makefile that owns that toolchain — test, check (fast, no browser), coverage, gen, open — so the build logic lives with the code and the short target names don't collide with the root's Elisp-flavored test/coverage. The root keeps the discoverable entry points: theme-studio-test and a new theme-studio-coverage now delegate via make -C. run-tests.sh grows a --no-browser flag so `make check` can skip the headless-Chrome gates for a fast inner loop. gen/open take an optional SEED to view a specific palette. coverage reports both halves: node --experimental-test-coverage for the three JS modules (all 100% line, ~96% branch) and uvx coverage for generate.py (89% lines; the rest is the __main__ writer and the optional seed-env branch).
* test(theme-studio): extract color/slug helpers to importable modules and ↵Craig Jennings24 hours8-21/+199
| | | | | | | | | | cover them The pure helpers that were still stranded in app.js — normHex, ratingColor, textOn, and the filename-slug logic — had no unit tests because app.js can't be imported (it runs its bootstrap and references the data placeholders at load). Moved them into importable modules so they can be tested directly: a new app-util.js holds the color/UI-boundary trio, and slugify joins app-core.js. app.js keeps thin wrappers, so no call site changed and the built DOM is byte-identical. textOn needs rl from colormath, so generate.py's inline strip now drops import lines as well as export lines — app-util.js imports rl for its tests, and the import is stripped on inline where rl is already in the page. _faces in generate.py also gets direct tests for its prefix-strip and label derivation. New: 12 node tests (normHex, ratingColor, textOn, slugify) and 7 python tests (_faces, app-util integrity, the import strip). Coverage: app-util.js 100/100/100, app-core.js 100/94.9/100, colormath.js 100/96/100 (line/branch/func); generate.py 89% lines (the rest is the __main__ writer and the optional seed-env branch). No bugs surfaced — the logic was correct, just untested.
* docs(theme-studio): warn that fill_data tokens are replaced in comments tooCraig Jennings26 hours1-0/+3
|
* test(theme-studio): extract app-core.js and unit-test the app logicCraig Jennings26 hours6-17/+225
| | | | | | | | The refactor's goal was to make the app logic testable; this realizes it. Pulled the pure package-face model and the dropdown option list into app-core.js — nameToHex, buildPkgmap, packagesForExport, mergePackagesInto, effResolve (the inherit-chain resolver behind pkgEffFg/pkgEffBg), and optList — with every dependency passed as a parameter so there is no DOM and no module-global reliance. generate.py inlines it into the page the same way it inlines colormath.js (strip exports, placeholder, integrity check), so the browser runs the same code the tests import. app.js keeps thin wrappers (pname, seedPkgmap, ddList, pkgEffFg, pkgEffBg) that pass the live PALETTE / APPS / PKGMAP into the core, so no call site changed and the built DOM is byte-identical to before. test-app-core.mjs adds 18 Normal/Boundary/Error tests over the extracted logic — name resolution, the seed/export/merge round trip, the inherit chain including a cycle that must terminate at null, and the "(gone)" dropdown entry — plus an inline-integrity check that the page carries the core verbatim. The node suite goes 25 to 43 tests; python templating gains the app-core integrity assertion.
* refactor(theme-studio): parameterize clear-unlocked, add effFg/effBg helpersCraig Jennings27 hours2-32/+46
| | | | | | | | The three clear-unlocked functions (syntax, UI, package) repeated the same loop: walk the tier's rows, skip the locked ones, reset the rest. Pulled that into clearUnlockedRows(items, keyFn, resetFn) — keyFn returns a row's lock key or null to skip it outright (syntax bg and the default fg), resetFn does the tier-specific clearing. #locktest already pins clear-unlocked-skips-locked across all three tiers, so it guards this directly. The "unset foreground reads as the default fg, unset background as the ground" fallback was written inline at nine sites as ||MAP['p'] / ||MAP['bg']. Replaced them with effFg(v) and effBg(v). The syntax, UI, and package render paths now resolve their raw value through the same two helpers. Behavior-preserving: the rendered DOM (script stripped) is byte-identical to before, and every gate stays green.
* refactor(theme-studio): unify the syntax table onto the shared sortCraig Jennings27 hours4-21/+45
| | | | | | | | The syntax table had its own sort (srt + a D{} direction map) that read state directly — MAP[kind] for the color column, cell text for elements. The UI and package tables used a separate, more general system (srtTable / cellVal / applyTableSort) that reads the rendered cells. Pointed the syntax headers at srtTable('legbody', col) and deleted srt, so all three tables share one sort. The mapping is exact: the legtable color cell is a swatch dropdown whose data-val is the hex, which cellVal reads — same key srt sorted on — and the elements cell is text. First-click direction stays ascending. The syntax table sorts on click only; it doesn't opt into the cross-rebuild persistence the UI and package tables get from applyTableSort, which preserves its prior behavior. Added a #sorttest gate: sort was previously untested, and this collapses two systems into one. It checks the syntax table sorts by color ascending, reverses on a second click, sorts by element name, and that the UI and package tables still sort. The asc/desc pair is self-validating — a no-op sort can't pass both.
* refactor(theme-studio): extract crHtml and mkStyleButtons table helpersCraig Jennings27 hours2-14/+42
| | | | | | | | Three tables repeated two scaffolds. The contrast-cell readout (a WCAG ratio colored by its AA/AAA rating, plus the rating word) was copy-pasted at five sites; pulled the shared formatting into crHtml(r) and called it from the syntax, UI, and package cells (the picker readout renders differently and stays as is). The B/I/U/S style-button loop was duplicated near-verbatim in the UI and package tables; pulled it into mkStyleButtons(isOn, onToggle), which returns the button list so the caller still hands them to mkLockCell. Left the syntax table's bold/italic buttons alone — two buttons, a different state model (the BOLD/ITALIC dicts), and an in-place refresh closure make it a poor fit for the same helper. Didn't introduce a shared row scaffold either; the three tables differ enough in columns and order that one would leak. Behavior-preserving: the runtime-rendered tables are byte-identical to before (a DOM dump diff shows only the inline-script source changing, never a built tr/td/button/span). All hash gates, the node suite, and #locktest stay green.
* refactor(theme-studio): unify color dropdowns on the swatch pickerCraig Jennings27 hours2-40/+38
| | | | | | The UI and package tables used a native <select> for fg/bg, while the syntax table used the swatch-div dropdown (mkColorDropdown). The native select rendered its option swatch colors unreliably on Linux Chrome — the reason the swatch div exists. Routed all three tiers through mkColorDropdown and deleted colorDropdown, so every color picker now shows real swatches. The inherit column stays a select; it picks a face name, not a color. Pulled the option-list construction into a shared ddList helper (default entry, palette, plus a "(gone)" entry when the current color left the palette) — the syntax table built that inline, the other tiers now reuse it. To keep fg/bg columns sorting by color value rather than by displayed name, the swatch dropdown exposes its value as data-val and cellVal reads it. Updated #locktest's UI assertion to the div lock mechanism (data-locked) since the UI control is no longer a select.
* refactor(theme-studio): extract CSS and JS to files, inline at generate timeCraig Jennings27 hours4-912/+941
| | | | | | | | generate.py was 1378 lines, ~1300 of them a single triple-quoted string holding the whole app. Moved the <style> block to styles.css and the <script> body to app.js, and generate.py now inlines both through placeholders the same way it already inlines colormath.js, then fills the data placeholders. It drops to ~500 lines (the remaining bulk is the package face-data dicts, a later stage). The generated page is byte-identical to before — every hash gate, the node suite, the spliced-script parse, and the new #locktest stay green. Two integrity tests guard the splice: styles.css inlines verbatim, and app.js reaches the page exactly as fill_data renders it. Both go red if the splice wiring is dropped. Living in real files instead of a Python string kills the backslash-doubling bug class (str.replace is literal, so escapes survive), gives the CSS and JS real editor tooling, and opens the app logic to unit testing — the point of the whole refactor.
* test(theme-studio): pin lock behaviors with a #locktest gateCraig Jennings27 hours3-1/+59
| | | | Adds a browser hash gate covering the two lock behaviors no existing gate touched: locking a row disables its control (syntax swatch div via data-locked, UI select via .disabled, both through the shared mkLockCell), and clear-unlocked wipes unlocked rows to default while leaving locked rows untouched across all three tiers. This is the characterization net for the generate.py extraction refactor — it proves the upcoming CSS/JS move preserves lock behavior. Verified it goes red when a lock guard is removed.
* feat(theme-studio): swatch dropdowns, lock toggles, palette seedingCraig Jennings27 hours2-60/+235
| | | | | | | | | | | | The design session added a batch of tool features. They landed together in the page generator, so this commits them as one. A custom swatch color dropdown replaces the native select in the syntax table, showing a color square, name, and hex per row, because native option backgrounds render unreliably on Linux Chrome. THEME_STUDIO_SEED=<file.json> seeds the tool's starting palette, assignments, bold, italic, and UI from a theme.json at generate time, so opening the tool on a given palette is one regenerate with no import step. Lock toggles sit on all three tables. A closed lock marks a decided element/color pairing and disables that row's editing controls. Clear-unlocked resets the rest to default and leaves the locked rows alone. Locks persist in the saved theme, with keys namespaced (bare syntax, ui:, pkg:) so the tiers don't collide. The UI faces table gained a contrast column, and the three tables now share one column order: name, lock, color, style, contrast. The bg and fg rows are renamed to "bg (ground)" and "fg".
* test(theme-studio): add a one-command test runner and make targetCraig Jennings34 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 Jennings34 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 Jennings34 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 Jennings35 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 Jennings35 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 Jennings35 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 Jennings36 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 Jennings36 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 Jennings36 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 Jennings37 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 Jennings38 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 Jennings44 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 Jennings45 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 Jennings47 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 Jennings47 hours7-20/+20
| | | | 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.
* feat(theme-selector): sortable package and UI face tablesCraig Jennings2 days2-4/+24
| | | | The package table (face, fg, bg, inherit, size, contrast) and the UI table (face, foreground, background) now sort on a header click, like the code/color assignments table. A generic comparator reads a select value, a numeric input, or cell text (numeric when it leads with a number, so size and contrast sort by value). The sort is remembered per table and re-applied after a rebuild, so editing a face no longer resets the order.
* feat(theme-selector): remember the imported/saved file so save overwrites itCraig Jennings2 days2-16/+34
| | | | Import now goes through showOpenFilePicker and keeps the returned file handle, and the rename handler no longer drops the handle on every keystroke. Once a theme has been imported or saved, save writes back to that same file (the first write asks once for permission, then overwrites silently); the save button title reflects whether it will overwrite or prompt for a location. Browsers without the File System Access API fall back to the hidden file input and a download-style save, as before.
* feat(theme-selector): bold/italic/underline/strike controls on the UI face tableCraig Jennings2 days2-15/+21
| | | | The UI face table gains a style column with the same B/I/U/S toggle buttons the package table has, so built-in UI faces reach parity. Each toggle updates the per-face preview cell and the live mock buffer (mode-line, minibuffer-prompt, link, error/warning/success now reflect weight, slant, and decoration). The link face is seeded underlined to match the built-in default, and the converter already reads these fields off the ui objects, so they export and convert without further change.
* docs(theme-selector): document underline and strike in the theme.json contractCraig Jennings2 days1-3/+5
| | | | Record the two new face booleans in the README contract and the spec's state-and-export-policy block, including that the converter writes them as :underline t and :strike-through t.
* feat(theme-selector): converter writes :underline and :strike-throughCraig Jennings2 days1-7/+13
| | | | build-theme/--attrs takes underline and strike flags and emits :underline t and :strike-through t in canonical order (after slant, before height). The UI and package spec builders read the two new fields off each face object; syntax and default faces pass nil since they never carry them. Two new ERT tests plus updated ordering cases; an end-to-end convert confirms a shr-link face round-trips to :underline t and shr-strike-through to :strike-through t. 22/22 green.
* feat(theme-selector): underline and strikethrough on package facesCraig Jennings2 days2-22/+22
| | | | Package faces gain underline and strike toggles alongside bold and italic. Each face object carries the two new booleans, the face table shows U and S buttons (the S button itself renders struck through, U underlined), the preview applies text-decoration so links underline and strike faces strike for real, and theme.json round-trips both fields. Link and strike-through faces across shr, mu4e, erc, slack, and telega are seeded with the right decoration so they look correct by default. The schema self-test still passes, so old exports without the fields import unchanged.
* feat(theme-selector): split C and C++ code samples, add JavaCraig Jennings2 days3-13/+65
| | | | The combined C/C++ preview is now a dedicated C sample and a dedicated C++ sample, plus a new Java sample. Each is tokenized to exercise every syntax category the language actually has: C++ hits all 19 (including [[nodiscard]] attributes, a std::regex raw string, and a Doxygen doc comment); C hits 18 (no regex literal exists in C); Java hits 17 (no preprocessor and no global builtins). C also gained a doc comment and a __attribute__ decorator over the old combined sample.
* feat(theme-selector): bespoke shr preview (themes nov, eww, and HTML mail)Craig Jennings2 days3-8/+37
| | | | nov defines no faces of its own; it renders EPUBs entirely through shr, the built-in HTML renderer also behind eww, elfeed's article view, and HTML mail. Rather than a hollow nov entry, this exposes shr directly: a rendered-document preview (chapter title, section headings, body text, links, inline code, mark, strike-through, superscript, abbreviation, image slice) covering all 15 shr faces. Theming it themes every HTML reader at once. The body font (variable-pitch) stays a font-config concern and body color follows the default face, so nov needs nothing beyond shr.
* feat(theme-selector): bespoke previews for slack and telegaCraig Jennings2 days3-8/+136
| | | | slack covers all 57 faces (a channel buffer with mrkdwn formatting, mentions, reactions, attachments, block-kit elements, dialogs, user and search rows, and modeline indicators). telega covers all 91 (root and chat-list rows, message entities, reactions, the full box-button matrix, describe/enckey/palette samplers, and webpage rendering). README updated to nineteen bespoke previews. Each verified by a headless DOM dump.
* feat(theme-selector): bespoke previews for calibredb, erc, org-drill, ↵Craig Jennings2 days2-5/+121
| | | | | | org-noter, signel, pearl calibredb (28 faces: a search-header bar plus a library listing and a detail view), erc (31: a full IRC channel buffer covering nicks, prefixes, pals/fools/dangerous hosts, actions, DMs, formatting, and the keep-place indicators), org-drill (3: a cloze flashcard), org-noter (2: notes-exist markers), signel (4: a Signal chat), and pearl (6: a ticket with modified-field states). signel and pearl are personal packages absent from the generated inventory, so their face lists come from the package sources. Each preview covers every face, verified by a headless DOM dump.
* feat(theme-selector): bespoke previews for lsp-mode, git-gutter, flycheck, ↵Craig Jennings2 days3-13/+184
| | | | | | dired, dirvish Five more apps leave the generic face-name list for full bespoke previews: lsp-mode (signature, inlay hints, read/write/textual highlights, rename, install) covering all 14 faces; git-gutter all 5 (its face names carry colons, which survive the data-face attribute and flash selectors); flycheck all 20 (a diagnostic line plus an error-list buffer); dired all 12 (built-in, so curated from the daemon rather than the inventory); dirvish all 38 (attribute columns, vc states, media info, proc, narrow). Every face renders a clickable preview element, verified by a headless DOM dump per app.
* feat(theme-selector): bespoke previews for ghostel, mu4e, and dashboardCraig Jennings2 days3-8/+134
| | | | These three no longer fall to the generic face-name list. ghostel renders a mock terminal covering all 19 faces (the 16 ANSI colors, default, and the fake cursor). mu4e renders a headers list, message view, and compose stub covering all 37 of the faces the dupre theme themes; mu4e is absent from the generated inventory, so its face list is curated. dashboard covers all 8. Every face has a real preview element, so the click-to-flash linking works both directions with no fallback.
* feat(theme-selector): near-complete face coverage in org/magit/elfeed previewsCraig Jennings2 days2-42/+142
| | | | The org preview now exercises 83 of 88 org faces (document plus agenda view); the five it skips are non-visual (org-hide, org-indent, org-clock-overlay, org-default, org-date-selected). Magit covers 97 of 98 across a status buffer plus labeled sampler rows for the long tail (diff variants, reflog, rebase sequence, bisect, blame, signatures, cherry). Elfeed now covers all 13. So clicking a face row flashes a real preview element for nearly every face instead of falling back to the row cell.
* feat(theme-selector): bidirectional flash-link for UI and package facesCraig Jennings2 days2-12/+18
| | | | Clicking a face row now flashes that face in the live-buffer and package previews, and clicking an element in either preview flashes its table row. The syntax tier already did this. UI faces had only preview-to-row, and package faces had neither. The package preview spans and table rows now carry data-face, reusing the same delegated-click handler the mock frame uses. When a face isn't shown in a bespoke preview, the row falls back to flashing its own cell. A fuller org preview that gives every org row a real target is filed as a separate task.