aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-07 17:45:54 -0500
committerCraig Jennings <c@cjennings.net>2026-06-07 17:45:54 -0500
commit275604c35df1410e5fae28e13507d332370d0756 (patch)
tree75416600a3fe0ed5d42e1832369d495ea2f0fc8e /docs
parent2ff3c7bad13095964a4eb2b77fa1dcf2d99c7f66 (diff)
downloaddotemacs-275604c35df1410e5fae28e13507d332370d0756.tar.gz
dotemacs-275604c35df1410e5fae28e13507d332370d0756.zip
docs(theme-selector): spec the tier-3 package-faces section
I spec'd a third tier for the tool: package-specific faces, edited one application at a time, org-mode first. It covers the app dropdown, a per-app face table, a bespoke org-document preview, the theme.json packages schema, and how the build step consumes it. It's awaiting review and ends with five open questions.
Diffstat (limited to 'docs')
-rw-r--r--docs/design/theme-selector-package-faces.org220
1 files changed, 220 insertions, 0 deletions
diff --git a/docs/design/theme-selector-package-faces.org b/docs/design/theme-selector-package-faces.org
new file mode 100644
index 00000000..ea1073a0
--- /dev/null
+++ b/docs/design/theme-selector-package-faces.org
@@ -0,0 +1,220 @@
+#+TITLE: theme-selector — package faces (tier 3), starting with org-mode
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-07
+
+* Status
+
+Spec / awaiting review. Proposes a third tier for the theme-selector
+(scripts/theme-selector/) that lets a theme colorize package-specific faces,
+built one application at a time. org-mode is the first application.
+
+* Background — the three tiers
+
+The theme-selector already models two tiers of faces:
+
+1. *Syntax* — the font-lock / tree-sitter categories (keyword, string, type,
+ comment, etc.), in the "code/color assignments" table.
+2. *UI* — Emacs's built-in interface faces (cursor, region, mode-line, fringe,
+ line numbers, isearch, and the rest), in the "ui faces" table with the live
+ mock-frame preview.
+
+Tier 3 is *package faces*: faces a package declares with =defface= so a theme
+can color the package as it wishes. The running config has 1,146 such faces
+across 186 packages (magit 111, lsp-mode 97, telega 91, web-mode 82, org ~30
+core, and a long tail). No theme colors all of them; quality themes hand-pick
+the packages the user actually lives in and theme those.
+
+This spec adds a tier-3 section to the tool, structured so applications are
+added one at a time. org-mode ships first.
+
+* Goal
+
+A new "package faces" section with:
+
+1. An *application dropdown* — pick which package's faces to edit (org-mode
+ first; magit, elfeed, dirvish, telega, marginalia, consult to follow).
+2. A *face table* for the selected app — one row per curated face, each with a
+ foreground dropdown, a background dropdown, and bold / italic toggles, all
+ drawing from the same palette as the other tables.
+3. A *preview pane* for the selected app — a realistic mock of that package
+ rendered with the live theme, the way the ui-faces mock-frame shows the UI
+ faces in a buffer. org-mode gets a mock org document.
+
+The export (=theme.json=) gains a =packages= object so the build step can set
+these faces too.
+
+* UI placement
+
+A new top-level section under the ui-faces row:
+
+#+begin_example
+<h1>package faces</h1>
+[ application: (org-mode v) ]
+<div class="cols stretch">
+ left = the selected app's face table (fg / bg / B / I per face)
+ right = the selected app's preview pane (e.g. the org document mock)
+</div>
+#+end_example
+
+Same two-column stretch layout as the ui-faces row, so the preview matches the
+table's height.
+
+* Data model
+
+A single data structure drives everything, keyed by application:
+
+#+begin_src js
+APPS = {
+ "org-mode": {
+ label: "org-mode",
+ faces: [
+ // face, human label, default {fg, bg, bold, italic}
+ ["org-document-title", "document title", {fg:"gold", bold:true}],
+ ["org-level-1", "heading 1", {fg:"blue", bold:true}],
+ ["org-level-2", "heading 2", {fg:"gold"}],
+ ["org-level-3", "heading 3", {fg:"regal"}],
+ ["org-todo", "TODO keyword", {fg:"terracotta", bold:true}],
+ ["org-done", "DONE keyword", {fg:"sage", bold:true}],
+ ["org-link", "link", {fg:"blue"}], // base `link`
+ ["org-code", "inline code", {fg:"terracotta"}],
+ ["org-verbatim", "verbatim", {fg:"steel"}],
+ ["org-block", "src block body", {fg:"white", bg:"bg-dim"}],
+ ["org-block-begin-line","block delim", {fg:"pewter", bg:"bg-dim"}],
+ ["org-table", "table", {fg:"steel"}],
+ ["org-date", "timestamp", {fg:"steel"}],
+ ["org-tag", "tag", {fg:"tan"}],
+ ["org-special-keyword","keyword/drawer", {fg:"pewter"}],
+ ["org-meta-line", "#+meta line", {fg:"pewter"}],
+ ["org-checkbox", "checkbox", {fg:"gold"}],
+ ["org-headline-done", "done headline", {fg:"pewter"}],
+ ],
+ preview: "org" // names the preview renderer
+ },
+ // magit, elfeed, ... added later with the same shape
+}
+#+end_src
+
+Defaults reference palette *names* (blue, gold, ...) resolved to hexes at load,
+so a curated app seeds sensibly from the current palette. The user reassigns
+any face from the palette dropdowns exactly like the other tables.
+
+State mirrors the other tiers: a =PKGMAP= of
+={app: {face: {fg, bg, bold, italic}}}=, edited live, rendered into the table
+and the preview.
+
+* The org preview
+
+A mock org document painted from PKGMAP["org-mode"] plus the palette ground/fg.
+One bespoke renderer (=renderOrgPreview()=) drawing a representative document:
+
+#+begin_example
+#+TITLE: Project Notes <- org-document-title
+#+AUTHOR: ... <- org-meta-line / document-info
+
+* Inbox :work: <- org-level-1 + org-tag
+** TODO Draft the spec <- org-level-2 + org-todo
+ SCHEDULED: <2026-06-08 Sun> <- org-special-keyword + org-date
+** DONE Ship the tool <- org-level-2 + org-done (headline-done)
+*** Heading three <- org-level-3
+ A line with =inline code=, <- org-code
+ ~verbatim~, and a [[link]]. <- org-verbatim + org-link
+ - [X] a checkbox item <- org-checkbox
+
+ #+begin_src elisp <- org-block-begin-line
+ (message "hi") <- org-block
+ #+end_src <- org-block-end-line
+
+ | name | hex | <- org-table (header row org-table-header)
+ |------+---------|
+ | blue | #67809c |
+#+end_example
+
+Each marked element is a span colored from the corresponding PKGMAP face. The
+preview rebuilds whenever a package face or the palette changes, same as the
+mock frame.
+
+For later apps, each gets its own preview renderer (magit -> a status buffer
+mock, elfeed -> a search-list mock). Until an app has a bespoke preview, it
+falls back to a generic "face name rendered in its own colors" list, so a new
+app is usable the moment its face list is added, and gets a real preview when
+one is written.
+
+* Export schema
+
+=theme.json= gains a =packages= key:
+
+#+begin_src json
+{
+ "name": "dupre",
+ "palette": [...],
+ "assignments": {...},
+ "bold": [...], "italic": [...],
+ "ui": {...},
+ "packages": {
+ "org-mode": {
+ "org-level-1": {"fg":"#67809c","bg":null,"bold":true,"italic":false},
+ "org-todo": {"fg":"#cb6b4d","bg":null,"bold":true,"italic":false}
+ }
+ }
+}
+#+end_src
+
+Only faces the user actually touched (or the curated defaults) are written. The
+build step's converter sets each as a normal face. Backward compatible: a file
+without =packages= loads fine.
+
+* Build-step consumption
+
+The eventual =theme.json= -> =dupre-*.el= converter already owns tiers 1 and 2.
+Tier 3 adds, per package face:
+
+#+begin_src elisp
+(org-level-1 ((t (:foreground "#67809c" :weight bold))))
+(org-todo ((t (:foreground "#cb6b4d" :weight bold))))
+#+end_src
+
+No new converter machinery — package faces are just more faces. This is the
+TDD-worthy part (JSON in, valid faces out), same as the rest of the converter.
+
+* Scope for v1
+
+- Build the section, the app dropdown, the org-mode face table, and the org
+ preview.
+- Seed org's ~18 curated faces (above), not all 104 org-* faces (most are
+ org-roam / superstar / agenda noise the core theme need not touch).
+- Wire export/import of the =packages= key.
+- Leave the converter for the separate build-step task; the spec only needs the
+ schema to be right.
+
+* Extensibility (adding the next app)
+
+1. Add an entry to =APPS= (label, curated face list with palette-name defaults,
+ preview key).
+2. Optionally write a bespoke preview renderer; until then the generic fallback
+ renders.
+3. Nothing else changes — the dropdown, table, export, and import are all
+ data-driven off =APPS= / =PKGMAP=.
+
+* Open questions
+
+1. *Curated set size.* ~18 org faces proposed. Add =org-level-4..8=, quote,
+ verse, drawer, footnote, priority? Or keep it tight and grow on demand?
+2. *Bold/italic source.* org defaults carry weight (headings, todo). Seed those
+ as the curated defaults, or start everything normal and let the user set
+ weight? Proposed: seed the obvious ones (title, levels, todo, done bold).
+3. *Inherit relationships.* Many org faces inherit (org-level-N from outline-N;
+ org-code/verbatim from =shr= / =fixed-pitch=). Model inheritance, or set
+ absolute values per face? Proposed: absolute values, simplest and matches
+ how the converter writes them.
+4. *App order after org.* magit (111 faces, highest payoff), elfeed, dirvish,
+ marginalia, consult, telega — which next, and how many?
+5. *Preview fidelity.* Bespoke per-app previews are the most work. Is the
+ generic fallback acceptable for the long tail, with bespoke previews only
+ for org and magit?
+
+* Files touched
+
+- =scripts/theme-selector/generate.py= — the section, =APPS= data, the package
+ face table, =renderOrgPreview()=, export/import of =packages=.
+- =scripts/theme-selector/theme-selector.html= — regenerated.
+- (later) the =theme.json= -> =dupre-*.el= converter — consumes =packages=.