diff options
Diffstat (limited to 'scripts/theme-studio/README.md')
| -rw-r--r-- | scripts/theme-studio/README.md | 229 |
1 files changed, 201 insertions, 28 deletions
diff --git a/scripts/theme-studio/README.md b/scripts/theme-studio/README.md index 58ddbf811..df3d92607 100644 --- a/scripts/theme-studio/README.md +++ b/scripts/theme-studio/README.md @@ -30,11 +30,69 @@ During color work, disable Hyprland inactive-window dimming so colors read true: hyprctl keyword decoration:dim_inactive false ``` +## Build A Theme + +Convert a Theme Studio JSON export into a loadable Emacs theme: + +```bash +make theme-studio-theme JSON=/path/to/theme.json +``` + +That writes `themes/<name>-theme.el`, where `<name>` comes from the JSON +`name` field. To write somewhere else: + +```bash +make theme-studio-theme JSON=/path/to/theme.json OUT=/tmp/themes +``` + +To apply a generated theme in the current Emacs session after disabling every +enabled custom theme: + +```bash +make theme-studio-theme-load THEME=theme +``` + +To rebuild a JSON export and cleanly reload the theme named by that JSON: + +```bash +make theme-studio-theme-reload JSON=/path/to/theme.json +``` + +## Tests + +```bash +make theme-studio-test # from the repo root, runs the whole pyramid +scripts/theme-studio/run-tests.sh # or call the runner directly +``` + +The runner regenerates the page, runs the Python templating tests +(`test_generate.py`), the Node unit tests for `colormath.js` +(`test-colormath.mjs`, including the inline-integrity check), a syntax check of +the spliced page script, and the browser hash gates in headless Chrome +(`#selftest`, `#cursortest`, `#readouttest`, `#deltatest`, `#oklchtest`, +`#planetest`, `#locktest`, `#sorttest`, `#mocktest`, `#contrasttest`, +`#safetest`, `#healtest`, `#columntest`, `#counttest`, `#baseedittest`, +`#roundtriptest`, `#beveltest`, `#previewlinktest`). It exits non-zero on any failure. The browser gates need a +Chromium-family browser; without one they report SKIPPED rather than passing +silently. The pure color math and the extracted picker logic (`planeCell`, +`paletteWarnings`) live in `colormath.js` so they are unit-tested directly in +Node; palette-column plans and lock-set plans live in `app-core.js` so edge +cases are unit-tested directly. The DOM glue is covered by the browser hash +gates. + ## Files -- `generate.py` — emits the HTML+JS, and embeds the package data. Edit here to - change layout or behavior. -- `samples.py` — the six language code samples and the default syntax +- `generate.py` — assembles the generated page from the source JS/CSS, data, and + template. +- `theme-studio.template.html` — static page shell with placeholders for the + inlined CSS/JS/data. Edit here for layout markup. +- `face_data.py` — bespoke package face lists and seed defaults. +- `palette-actions.js` — stateful palette-panel actions and rendering, inlined + into the generated page. +- `browser-gates.js` — the browser hash-gate test harness, also inlined. +- `app_inventory.py`, `face_specs.py`, `default_faces.py` — generator helpers for + package inventory, face-spec defaults, and captured Emacs defaults. +- `samples.py` — the language code samples and the default syntax category→color map (`COLS`). `generate.py` reads the part before the `cols=` marker. - `package-inventory.json` — generated map of every installed package to the @@ -42,15 +100,21 @@ hyprctl keyword decoration:dim_inactive false - `build-inventory.el` — refreshes `package-inventory.json` from a running Emacs. - `theme-studio.html` — generated output. Regenerate; don't hand-edit. + Use `make check-generated` before review if you want to verify the committed + page matches the generator without leaving the tree dirty. ## What it captures Three tiers of faces, plus the palette: -- **Palette** — named colors. Add by hex or with the in-page color picker +- **Palette** — named colors, shown grouped into stable structural columns. Add + by hex or with the in-page color picker (saturation/value square, hue slider, palette reuse chips, live contrast - readout, and an any / AA+ / AAA legibility mask). Remove, rename, reorder with - arrows or drag. The colors serving as background and foreground are locked. + readout, and an any / AA+ / AAA legibility mask). Remove and rename per chip; + the colors serving as background and foreground are locked. `clear palette` + removes every non-ground color and leaves only the `bg` and `fg` tiles; existing + face assignments remain on their old hexes and show as "(gone)" until a color + with the same name is recreated. The picker also shows perceptual readouts beside the WCAG ratio: the OKLCH coordinates (lightness, chroma, hue°) and the APCA Lc contrast against the @@ -58,22 +122,122 @@ Three tiers of faces, plus the palette: background, negative means light text on a dark background — so a light color on dupre's dark ground reads as a negative Lc. WCAG stays the rating used in the syntax/UI/package tables; APCA and OKLCH are picker-only diagnostics. + + An edit-model toggle switches the picker between HSV and OKLCH, independent of + the contrast mask. In OKLCH mode the L/C/H dials drive the color and the square + becomes a Chroma×Lightness plane at the current hue, with the out-of-gamut + region greyed out; the hue strip selects the hue. Pushing chroma past sRGB + snaps to the reachable color and shows a clamp note. The palette also warns + when two colors fall below a perceptual ΔE threshold, hard to tell apart. - **Syntax** — every font-lock / tree-sitter category (keyword, string, - function, type, comment, and the rest), each with normal/bold/italic and a - contrast rating. Click a category to flash its tokens in the code; click a - token to flash its row. + function, type, comment, and the rest), each with foreground, background, + style, box, and a contrast rating. Click a category to flash its tokens in the + code; click a token to flash its row. `lock all` flips to `unlock all` when + every row in the tier is locked. `reset` restores editable rows to the captured + syntax defaults; `erase` blanks editable rows. Both preserve locked rows. - **UI faces** — cursor, region, mode-line, fringe, line numbers, isearch, paren match, link, error/warning/success, and the rest, foreground and background - per face, shown in a live mock Emacs buffer. + per face, shown in a live mock Emacs buffer. `reset` restores captured UI face + defaults; `erase` blanks editable rows to no explicit fg/bg. Both preserve + locked rows. Box controls include style plus an optional color; raised/pressed + boxes derive their relief edges from that color when set. - **Package faces** — per-package face tables with a live preview (below). +Color dropdowns in the face tables use compact square swatches to save horizontal +space. Hovering a swatch shows the color name and hex; clicking it opens the full +palette dropdown. + +## Color columns + +The palette is displayed as **columns**. The ground column is pinned first: `bg` +at one end, `fg` at the other, with optional `ground+N` span colors between them. +Every other color stays in the column where it was created. Columns are not +derived from hue, chroma, lightness, or the visible color name. + +- **Grouping.** Each palette entry carries a stable column id. New colors start + their own column; generated ramp steps inherit the base color's column id. + Renaming a color only changes its label, so a renamed tile stays in its original + column. Older two-field palette entries still load by falling back to the + generated-name stem (`blue-1`, `blue`, `blue+1` -> `blue`). + Generic Emacs names like `color-22` stay separate base columns unless they + already carry an explicit column id. Numbered named colors such as `blue1`, + `grey80`, `orange3`, and `orchid4` group by their text stem. Imported names + that begin with `bg` or `fg` are normal colors unless they are exact ground + endpoints or explicitly use the `ground` column id. +- **Deleting.** Normal columns have a separated header delete control with a + confirmation prompt. Confirming removes every tile in that column. The ground + column is pinned and cannot be deleted. Face assignments that used a deleted + tile stay on that old hex and appear as recoverable "(gone)" values, matching + individual chip deletion. +- **Tile clicks.** Single-clicking a tile, including its name, selects that + whole color. Double-clicking the name enters name-edit mode with the cursor at + the start of the name. +- **The count control** under each non-ground column sets how many steps sit on + each side of the column's base. Setting N regenerates the column as a symmetric + base ±N span: N interior OKLab steps from black to the base and N interior + OKLab steps from the base to white. Pure black/white endpoint duplicates and + rounded base duplicates are skipped. The current UI caps N at 8; N=0 collapses + to the base alone. +- **Editing a base** recolors the whole column: change a base color and the column + regenerates from it at the same count. +- **References follow.** When a regenerate changes a step's hex, any face assigned + to that step is re-pointed to the new hex. A step *removed* by lowering the count + leaves its references showing "(gone)" — visible and recoverable, never a silent + jump to a different color. +- **Dropdown order.** Color dropdowns show the default entry, then `bg` and `fg`, + then palette columns from left to right. Within each column's dropdown group, + colors are ordered lightest to darkest. +- **Dropdown arrows.** Color dropdowns in the syntax, UI, and package face tables + have left/right arrows. Left steps to the next darker color in the selected + color's column; right steps to the next lighter color. The arrows are disabled + for defaults, gone colors, locked rows, and column ends. + +The standalone ramp generator is gone; fanning a color into a ramp is now "add the +color, then raise its column's count." + +## Background-contrast safety + +Keep background tints readable. Works in OKLCH; the pure math is in `app-core.js` +(`fgSetFor`, `floor`, `lMax`), the DOM in `app.js`. + +**Worst-case contrast.** A background overlay sits behind many foregrounds at +once, so one fg/bg contrast pair is the wrong number. For the covered overlay +faces — `region`, `hl-line`, `highlight`, `lazy-highlight`, `isearch` — the +contrast cell shows the *worst-case floor*: the lowest contrast over the face's +foreground set (the syntax-token colors plus the default foreground), naming the +*limiting foreground* that sets it. A tint that clears the default text but fails +the darkest token reads FAIL, with that token named. Package faces and the other +UI rows keep their single-pair readout. + +The verdict is WCAG: AA (4.5) by default, AAA (7) selectable. APCA Lc stays a +picker-only diagnostic and does not drive PASS/FAIL. + +**Safe lightness.** In the OKLCH picker, the "safe for" selector picks one +covered face. The Chroma×Lightness plane then 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. If even pure black can't satisfy the target (a foreground is +too dark), the whole plane shades; that is a true finding about the foreground, +not a tool bug. + ## Package faces Pick an application from the dropdown to edit its faces. Each row has a foreground and background dropdown, bold/italic toggles, an `inherit` dropdown (base faces like `fixed-pitch`/`link` plus the app's own faces), a relative -height stepper, a contrast readout, and a per-face reset. There's a per-app -reset and a text filter for the large sets. +height stepper, a contrast readout, box style/color controls, and a per-face reset. There's a per-app +reset and a text filter for the large sets. Package `reset` restores editable +rows to the captured package defaults; `erase` blanks editable rows to no +fg/bg/style/inherit override. Both preserve locked rows. Package +`lock all` / `unlock all` applies to the whole currently selected package, not +only the rows visible under the text filter. + +Org TODO keyword colors are normal Org face resolution, not a separate automatic +palette generator. Org checks `org-todo-keyword-faces` for an exact keyword match +first. If no exact face is configured, keywords before the `|` separator in +`org-todo-keywords` use `org-todo`; keywords after `|` use `org-done`. For +example, in `(sequence "TODO" "WAIT" "|" "DONE" "CANCELLED")`, `TODO` and +`WAIT` fall back to `org-todo`, while `DONE` and `CANCELLED` fall back to +`org-done`. Fast-selection keys such as `WAIT(w)` do not affect the face. Twenty applications have bespoke previews that exercise nearly all of their faces: org-mode (a document plus an agenda view), magit (a status buffer plus @@ -121,10 +285,15 @@ The export (and what a build step consumes): ```json { "name": "dupre", - "palette": [["#67809c", "blue"], ["#e8bd30", "gold"]], - "assignments": {"kw": "#67809c", "str": "#5d9b86", "bg": "#000000", "p": "#ffffff"}, - "bold": ["kw", "fnd"], - "italic": [], + "palette": [["#67809c", "blue", "blue"], ["#e8bd30", "gold", "gold"]], + "syntax": { + "bg": {"fg": "#000000", "bg": null, "bold": false, "italic": false, + "underline": false, "strike": false, "box": null}, + "p": {"fg": "#ffffff", "bg": null, "bold": false, "italic": false, + "underline": false, "strike": false, "box": null}, + "kw": {"fg": "#67809c", "bg": null, "bold": true, "italic": false, + "underline": false, "strike": false, "box": null} + }, "ui": {"region": {"fg": null, "bg": "#264364"}, "cursor": {"fg": null, "bg": "#a9b2bb"}}, "packages": { "org-mode": { @@ -136,28 +305,32 @@ The export (and what a build step consumes): } ``` -- `assignments` maps syntax category keys to hexes; `bg` is the `default` face - background, `p` the foreground. +- `syntax` maps syntax category keys to full face specs. `syntax.bg.fg` is the + `default` face background, and `syntax.p.fg` is the `default` face foreground. + Other syntax categories fan out to the font-lock / tree-sitter faces listed in + `build-theme.el`. +- `palette` is a flat list of `[hex, name, columnId]`. `name` is the editable + display label; `columnId` is the durable grouping key that keeps generated + colors in their original column even if they are renamed. - `ui` and `packages` faces carry `fg`/`bg` (hex or `null`), `bold`, `italic`, - `underline`, `strike`, and for package faces `inherit` (a face name or - `null`), `height` (a float, omitted at 1.0), and `source` (`"default"` seeded, - `"user"` edited, `"cleared"`). The converter writes `underline` as - `:underline t` and `strike` as `:strike-through t`. + `underline`, `strike`, and `box`; package faces also carry `inherit` (a face + name or `null`), `height` (a float, omitted at 1.0), and `source` + (`"default"` seeded, `"user"` edited, `"cleared"`). The converter writes + `underline` as `:underline t`, `strike` as `:strike-through t`, and `box` as an + Emacs `:box` plist. - The theme name is both the `name` field and the download filename. Import a `theme.json` to start from a prior theme; a file with no `packages` key still loads. -`export` always downloads a fresh file; `save` (shown once a name is entered) -writes the same file in place via the File System Access API. +`export` downloads the current theme JSON using the theme name as the filename. ## Build step — `build-theme.el` `build-theme.el` converts a `theme.json` into a single self-contained `themes/<name>-theme.el` deftheme. JSON in, valid Emacs faces out, across all -four tiers: `default` from `assignments.bg`/`.p`, the syntax categories mapped -to their font-lock / tree-sitter faces (with the `bold`/`italic` sets applied), -the UI faces passed through, and the package faces with `:inherit`/`:height` -and weight/slant written. +four tiers: `default` from `syntax.bg`/`syntax.p`, the syntax categories mapped +to their font-lock / tree-sitter faces, the UI faces passed through, and the +package faces with `:inherit`/`:height` and weight/slant written. ```bash emacs --batch -l scripts/theme-studio/build-theme.el \ |
