aboutsummaryrefslogtreecommitdiff
path: root/scripts
Commit message (Collapse)AuthorAgeFilesLines
* test(theme-studio): pin lock behaviors with a #locktest gateCraig Jennings10 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 Jennings10 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 Jennings17 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 Jennings17 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 Jennings17 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 Jennings17 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 Jennings18 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 Jennings18 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 Jennings19 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 Jennings19 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 Jennings19 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 Jennings19 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 Jennings20 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 Jennings26 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 Jennings27 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 Jennings30 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 Jennings30 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 Jennings32 hours2-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 Jennings32 hours2-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 Jennings32 hours2-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 Jennings32 hours1-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 Jennings32 hours1-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 Jennings32 hours2-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 Jennings32 hours3-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 Jennings32 hours3-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 Jennings33 hours3-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 Jennings33 hours2-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 Jennings33 hours3-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 Jennings35 hours3-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 Jennings35 hours2-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 Jennings35 hours2-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.
* style(theme-selector): 12pt previews and assignment-table refinementsCraig Jennings36 hours2-26/+26
| | | | Every preview surface (code samples, live buffer, package previews) now renders at 12pt instead of the earlier mix of 10, 15, and 19px. The code/color assignments table leads with the elements column before color. I dropped the redundant normal style button: toggling bold or italic off already returns a row to normal.
* style(theme-selector): unify all previews to a 10pt point sizeCraig Jennings36 hours2-12/+12
| | | | The code samples rendered at 19px while the live-buffer and package previews sat at 15px. All preview surfaces now share 10pt so sizes can be judged consistently. Package-face height steppers stay em-relative to the new base.
* feat(theme-selector): convert theme.json into a loadable defthemeCraig Jennings36 hours2-6/+262
| | | | | | | | | | build-theme.el is the last link in the theme-selector pipeline: a theme.json export becomes a single self-contained themes/<name>-theme.el. All four tiers convert: default from assignments.bg/.p, the syntax categories to their font-lock/tree-sitter faces with the bold/italic sets applied, UI passthrough, and package faces with :inherit/:height/weight/slant. The output is a flat generated deftheme, not the palette/faces/theme trio the hand-authored dupre ships. A theme.json carries resolved per-face hex, not dupre's semantic-mapping layer, so a flat deftheme is the faithful output and never clobbers the curated dupre files. I omitted the dec (decorator) key: Emacs has no dedicated decorator face and renders decorators with font-lock-type-face, which the type key already owns, so coloring dec independently would clobber types. Decorators follow the type color, as they do in stock Emacs. 20 ERT tests cover the attribute builder, each tier, the dec omission, and an end-to-end convert-and-load with a WCAG-AA assertion on the round-tripped default.
* test(theme-selector): cover the tier-3 acceptance criteria in the self-testCraig Jennings36 hours2-4/+16
| | | | | | I extended the hash-guarded self-test harness to assert the tier-3 acceptance criteria against the real emitted code, run in headless Chrome: old-JSON import with no packages key, the full package round-trip (fg, bg, bold, italic, inherit, height, source), cleared-state export, unknown-package preservation, and inheritance-cycle termination. All pass. The two DOM-coupled regressions are handled structurally rather than in the harness: updateColor remaps PKGMAP when a palette color is edited, and PKGMAP stores hexes so a deleted palette color leaves package references in the recoverable "(gone)" state. This closes the tier-3 package-faces milestone: all seven phases plus the test surface have landed.
* docs(theme-selector): rewrite README for the full tool (tier-3 phase 7)Craig Jennings36 hours1-22/+89
| | | | I rewrote the README to cover what the tool actually is now: the three face tiers (syntax, UI, package) plus the palette, the in-page color picker with its AA/AAA legibility mask, the package-faces section with bespoke org/magit/elfeed previews and the generic fallback, modeled inheritance and relative height (with the note that the font family stays in font-config.el), the theme.json packages schema with inherit/height/source, export-versus-save, and the inventory-refresh command with its loaded-config dependency. It also documents that theme-selector.html is generated.
* feat(theme-selector): generated all-package inventory (tier-3 phase 6)Craig Jennings36 hours4-1/+766
| | | | | | I added the hybrid inventory. build-inventory.el, loaded into a running Emacs, queries every installed package's faces grouped by the package that defines them and writes package-inventory.json. generate.py embeds that file and merges each package into the app dropdown as an editable generic app, leaving the bespoke org, magit, and elfeed untouched. The dropdown now reaches 40 apps: the three bespoke plus 37 inventory packages (643 faces), so any installed package can be themed against the palette with the generic preview. The inventory is a committed data artifact refreshed by reloading the .el, never browser-side discovery, matching the spec's hybrid-and-split decision.
* feat(theme-selector): magit and elfeed bespoke previews (tier-3 phase 5)Craig Jennings36 hours2-4/+68
| | | | I added renderMagitPreview and renderElfeedPreview. The magit one is a status buffer: head and branch lines, an untracked section, a diff hunk with context, removed, and added lines, and recent commits with hashes, authors, and a keyword and tag. The elfeed one is a search list: the filter line, dated entries with feed, unread and read titles, and tags, plus log lines colored by level. The preview pane dispatches to each app's bespoke renderer, the generic fallback covers everything else, and the pane label now names the app and says whether the preview is bespoke or generic.
* feat(theme-selector): org-mode bespoke preview (tier-3 phase 4)Craig Jennings36 hours2-2/+60
| | | | I added renderOrgPreview, a mock org document painted live from the org package faces. It exercises the prominent ones in context: document title, headings with their heights, a TODO and a DONE, tags, a scheduled timestamp, a property drawer, inline code and verbatim, a link, a checkbox, a quote, a src block with begin and end lines, and a header-row table. The preview pane now dispatches on the app's preview key, so org-mode gets this bespoke document and every other app keeps the generic face-name list until its own preview lands. It rebuilds on any package-face or palette edit.
* feat(theme-selector): package-faces table UI (tier-3 phase 3)Craig Jennings36 hours2-14/+112
| | | | | | I added the package faces section: an application selector for org-mode, magit, and elfeed, and a face table for the chosen app with a foreground and background dropdown, bold and italic toggles, an inherit dropdown (base faces plus the app's own faces), a relative-height stepper, a live contrast readout on the effective inherit-resolved color, and a per-face reset, plus a per-app reset and a text filter for the large sets. The right pane shows a generic preview, each face name in its own resolved colors, which the bespoke org/magit/elfeed previews replace in the next phases. The fg/bg dropdown is now a shared colorDropdown helper that the ui-faces table also uses, so there's no forked control. Palette edits propagate to package faces, and import and export carry them through.
* feat(theme-selector): curated org/magit/elfeed face data (tier-3 phase 2)Craig Jennings37 hours2-23/+137
| | | | | | I filled the APPS registry with the complete own-defface sets: 88 org faces including org-agenda, 98 magit, and 13 elfeed, built from embedded face-name lists plus a curated seed-color map. Prominent faces are seeded with palette colors, heading heights, and the fixed-pitch inherits that keep code and tables monospace under variable-pitch prose. The long tail seeds to the default foreground for the user to tune. Org headings carry their heights, agenda dates and deadlines are colored by role, and magit's diff, branch, and section faces map to the palette's greens, reds, and blues. No UI yet. That's phase 3, and the schema self-test still passes while seeding all 199 faces.
* feat(theme-selector): full category coverage, elements rename, visible tile ↵Craig Jennings37 hours3-15/+18
| | | | | | | | borders I renamed the assignments column "category" to "elements" and gave every language sample a token for each category that applies to it, so clicking an element name flashes real code in any language: Python gained a regexp, Elisp/Go/TypeScript/C each a builtin, and Go and Shell an escape. The categories a language genuinely lacks (Elisp has no preprocessor, Shell no types) fall back to flashing the example cell. The save area header is now "export, import, and save", and the palette tiles and the picker swatch carry a visible border so a tile that matches the page background still reads.
* feat(theme-selector): category-flash, full-height mock, contrast-limit maskingCraig Jennings37 hours2-14/+44
| | | | I made three preview and picker fixes. Clicking a category name in the code/color assignments table now flashes the matching tokens in the code sample, the reverse of clicking a token to flash its row, and falls back to flashing the example cell when the language has no token of that category. The live buffer preview now syncs its height to the ui-faces table so the two end together. The picker gains an any / AA+ / AAA limit: the saturation-value square masks the region that fails the chosen contrast against the background, and the reuse chips dim the colors that don't pass, so it's hard to land on an illegible color.
* feat(theme-selector): black/white anchors, locked ground/fg tiles, ↵Craig Jennings37 hours3-21/+41
| | | | | | | | save/export/import I made the ground default pure black and the default text pure white, the two anchors the whole theme is judged against. The palette tiles holding the current background and foreground are now locked, showing a small lock instead of the remove ×, so the contrast reference can't be deleted out from under everything. The save/load row is relabeled and the buttons stay right-aligned: export is always a fresh download, import loads a file, and show toggles the JSON box. A save button appears once a theme name is entered and uses the File System Access API to write the same file in place on repeat saves, falling back to a download where that API isn't available.
* feat(theme-selector): enlarge the color pickerCraig Jennings38 hours2-18/+18
| | | | I doubled the picker's saturation/value square to 400x320 and scaled the hue slider, readout, and chips to match. The cursor positioning now reads the live element dimensions instead of hardcoded sizes, so the markers stay correct at any size and a future resize needs no JS change.
* feat(theme-selector): custom in-page color pickerCraig Jennings38 hours2-22/+92
| | | | | | I replaced the native OS color swatch with an in-page picker, since the native one couldn't be sized or restyled and had no clean way to apply and dismiss. Clicking the swatch opens a popup with a saturation/value square and a hue slider you drag, a live hex plus AAA/AA/FAIL contrast readout, and the current palette as clickable chips for grabbing an exact existing color. It writes to the hex field, so add and update work unchanged, and it closes on click-away or when you commit a color. The HSV math is self-contained and the contrast readout reuses the existing helpers, so there's no dependency. A hash-guarded #pick hook opens the picker for headless screenshot verification, the same pattern as the #selftest harness.
* feat(theme-selector): click-to-move arrows for palette tilesCraig Jennings38 hours2-14/+28
| | | | Dragging the palette tiles with native HTML5 drag was fiddly: small drop targets, no feedback, and the name field inside each tile fought the grab. I added small move-left and move-right arrows on each tile's bottom corners, so reordering is one click and needs no dragging. The first tile drops its left arrow and the last drops its right. Drag still works for anyone who prefers it, now with a dashed outline marking the drop target while you drag.
* feat(theme-selector): add relative height to the package-face schemaCraig Jennings38 hours2-27/+31
| | | | | | I folded a relative height field into the tier-3 spec and brought Phase 1's schema in line. A face's height is a float multiplier off the base font (1.3 is 1.3x the running font, never a point size), so it stays portable across fonts and machines, and it's omitted from export at 1.0. The font family itself stays in font-config.el, where it belongs; the theme owns only relative size. Height is read straight off the face and does not cascade through inherit, because Emacs multiplies float heights along an inherit chain and headings should each size off the body, not compound off the level above. The org starter now seeds heading heights and the fixed-pitch inherits that keep code and tables monospace under variable-pitch prose. The self-test gained a height assertion and still reports PASS.
* feat(theme-selector): show every ui face in the mock frameCraig Jennings38 hours2-20/+36
| | | | The live buffer preview was incomplete. It skipped highlight, isearch-fail, show-paren-mismatch, and vertical-border, and the fringe was painted its ground-colored default so it read as absent. The mock now renders all twenty ui faces: a highlighted line, a failing isearch in the echo area, a mismatched paren, and a vertical-border strip down the buffer's right edge. The fringe column is wider with a hairline edge so the gutter is locatable even at ground color, and the buffer runs a dozen lines to fit everything.
* feat(theme-selector): add package-face state and schema (tier-3 phase 1)Craig Jennings39 hours2-5/+69
| | | | | | I laid the tier-3 foundation: an APPS registry (org starter for now) and a PKGMAP holding {fg,bg,bold,italic,inherit,source} per face. Pure helpers seed PKGMAP from APPS defaults, build the export per the state policy, and merge an import back in. Export gains a packages key when any package face is present, and import reads it while old JSON with no packages key still loads cleanly onto the seeded defaults. No UI yet — that's phase 3. A #selftest harness, guarded by the URL hash so it never shows in normal use, runs seed to export to import and checks the round-trip, old-JSON merge, and inherit/source survival. Headless Chrome reports PASS, which is how I verified the schema end-to-end against the real emitted code rather than a copy.