1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
|
#+TITLE: theme-studio — package faces (tier 3), starting with org-mode
#+AUTHOR: Craig Jennings
#+DATE: 2026-06-07
* Status
Spec / Craig's first-round answers folded in (2026-06-07). Proposes a third tier
for the theme-studio (scripts/theme-studio/) that lets a theme colorize
package-specific faces, built one application at a time. v1 apps: org-mode
(incl. org-agenda), magit, elfeed. Codex review incorporated (2026-06-07): added
implementation phases, acceptance criteria, the package-face inventory source
(hybrid, split), and state/export semantics. Rubric now =Ready=.
All opens resolved (Craig, 2026-06-07/08): inheritance is modeled (show each
face's resolved color in the table + preview, override what looks bad); inventory
is hybrid-and-split (org/magit/elfeed bespoke first, generated all-package
inventory as a later phase); the custom color picker is built after tier 3.
Implementation tasks live in =todo.org=.
* Background — the three tiers
The theme-studio 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. v1 ships
org-mode (including org-agenda), magit, and elfeed; the rest of Craig's
packages (calibredb, ghostel, mu4e, IRC, org-drill, dirvish + dired, slack)
follow one at a time.
2. A *face table* for the selected app — one row per face in the app's complete
set, each with a foreground dropdown, a background dropdown, bold / italic
toggles, an optional inherit, and a relative-height stepper, all drawing from
the same palette as the other tables. Grouped, with a text filter for the
large apps.
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, inherit, height, source}}}=, edited live, rendered into
the table and the preview. The =APPS= block above shows ~18 org faces only as a
shape illustration; the real org entry is the complete set below.
** Data model — org face set (complete)
Per the completeness decision, org's table lists org's entire own =defface= set
(org-faces.el + org-agenda.el), ~88 faces, grouped. Seed defaults for the
prominent groups; the long tail seeds to fg or an =inherit= of its group base,
which the user overrides. The groups (face names verbatim from the running
Emacs):
- *Document:* org-document-title, org-document-info, org-document-info-keyword
- *Headings:* org-level-1 .. org-level-8, org-headline-todo, org-headline-done
- *Status / keywords:* org-todo, org-done, org-priority, org-tag, org-tag-group,
org-special-keyword, org-drawer, org-property-value, org-checkbox,
org-checkbox-statistics-todo, org-checkbox-statistics-done, org-warning
- *Links / dates / refs:* org-link, org-footnote, org-date, org-sexp-date,
org-date-selected, org-target, org-macro, org-cite, org-cite-key
- *Blocks / code / quote:* org-block, org-block-begin-line, org-block-end-line,
org-code, org-verbatim, org-inline-src-block, org-quote, org-verse,
org-latex-and-related
- *Tables / columns:* org-table, org-table-header, org-table-row, org-formula,
org-column, org-column-title
- *Lists / meta / structure:* org-list-dt, org-meta-line, org-ellipsis,
org-hide, org-indent, org-archived, org-default, org-dispatcher-highlight
- *Agenda — structure & dates:* org-agenda-structure,
org-agenda-structure-secondary, org-agenda-structure-filter, org-agenda-date,
org-agenda-date-today, org-agenda-date-weekend, org-agenda-date-weekend-today,
org-agenda-current-time, org-agenda-done, org-agenda-dimmed-todo-face
- *Agenda — calendar & filters:* org-agenda-calendar-event,
org-agenda-calendar-sexp, org-agenda-calendar-daterange, org-agenda-diary,
org-agenda-clocking, org-agenda-column-dateline, org-agenda-restriction-lock,
org-agenda-filter-category, org-agenda-filter-effort, org-agenda-filter-regexp,
org-agenda-filter-tags
- *Scheduling / deadlines / clock:* org-scheduled, org-scheduled-today,
org-scheduled-previously, org-upcoming-deadline, org-upcoming-distant-deadline,
org-imminent-deadline, org-time-grid, org-clock-overlay, org-mode-line-clock,
org-mode-line-clock-overrun
The org *preview* below stays a curated document exercising the prominent
faces; the *table* carries the complete set so every face is assignable, even
the ones the preview doesn't draw. magit and elfeed get the same treatment
(complete own-defface set in the table, a bespoke preview for the common faces).
* 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.
org, magit, and elfeed get bespoke preview renderers (magit -> a status buffer
mock, elfeed -> a search-list mock). Every *other* package is still fully
themeable: its face *table* is always present and editable, only the rich
*preview* is replaced by a generic fallback — each face's name rendered in its
own colors on the ground. So a user can theme every package they have the
moment its face list is added; the bespoke preview is a polish layer on top, not
a gate. This is the v1 answer to "some will want to touch every package."
* 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,"inherit":null,"height":1.3},
"org-level-2": {"fg":"#e8bd30","bg":null,"bold":false,"italic":false,"inherit":"org-level-1","height":1.2},
"org-todo": {"fg":"#cb6b4d","bg":null,"bold":true,"italic":false,"inherit":null}
}
}
}
#+end_src
=inherit= is optional and =null= when absent. When set, the converter writes
=:inherit PARENT= plus only the overridden attributes.
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, and the face tables + previews for the
three v1 apps: org-mode (incl. org-agenda), magit, elfeed.
- org's table carries its complete own-defface set (~88 faces, grouped above),
seeded with defaults; the org preview draws the prominent ones.
- Every other installed package is reachable in the dropdown with an editable
face table and the generic fallback preview, so any package can be themed.
- Wire export/import of the =packages= key (with the optional =inherit= and
=height= fields).
- Leave the converter for the separate build-step task (Elisp, per Craig); the
spec only needs the schema to be right.
* Implementation phases
Phased so each step ships without a broken intermediate, and the three bespoke
apps don't wait on the all-package inventory.
1. *State + schema.* Add =PKGMAP=
({app:{face:{fg,bg,bold,italic,inherit,height,source}}}) and the =APPS=
registry. Extend export/import with the =packages= key; old JSON (no
=packages=) still imports cleanly. No UI yet.
2. *Curated app data.* Complete own-defface face lists + seeded defaults for org
(incl. org-agenda), magit, elfeed, in =APPS= — including heading heights and
the fixed-pitch inherits. Pure data.
3. *Package face table UI.* App selector; grouped rows; fg/bg dropdowns + bold /
italic toggles + optional inherit + a relative-height stepper; per-face and
per-app reset; a text filter (org/magit are large); a contrast readout per
fg/bg. Built on a generalized face-control helper shared with the ui-faces
table, not a fork of =uiSelect=.
4. *Org preview.* =renderOrgPreview()=, live, refreshing on palette/face change.
5. *Magit + elfeed previews.* Bespoke mocks (magit status buffer, elfeed search
list).
6. *Generated all-package inventory* (the "theme every package" path). A build
step queries Emacs for installed packages' faces grouped by package, writes a
data file =generate.py= embeds; the dropdown then lists every package with an
editable table + the generic fallback preview. Lands after phases 1-5 without
blocking the three bespoke apps.
7. *Docs + validation.* README =packages= schema + inventory-refresh command;
regenerate HTML; fixtures + manual checklist.
Phases 1-5 deliver the three high-value apps fully; phase 6 opens the long tail;
phase 7 documents.
* Package face inventory source
*Hybrid, split across phases.* Curated app metadata (org/magit/elfeed: complete
face lists, seeded defaults, bespoke previews) is hand-maintained in =APPS= and
ships in phases 2-5. A *generated* =PACKAGE_FACE_INVENTORY= — produced by a build
step that asks the running Emacs for each installed package's faces grouped by
package, written to a JSON/Python data file =generate.py= embeds — supplies the
generic fallback packages and ships in phase 6.
Why hybrid and split: the static generator can't discover packages at runtime in
the browser, so "theme every package" needs a generated inventory; but making the
full inventory a prerequisite for the three bespoke apps invites the scope
explosion the review flagged. Splitting it lets v1's core ship first; the
inventory is additive.
The generated inventory is an *input artifact* to =generate.py= (a committed data
file refreshed by an explicit command), never browser-side discovery. The refresh
command's dependency on a loaded Emacs config is documented.
Decided (Craig, 2026-06-08): hybrid-and-split, as above.
* State and export policy
Each package face object carries a =source= marker so export can tell a seeded
default from a user edit from a deliberate clear:
#+begin_src js
{ fg:"#67809c", bg:null, bold:true, italic:false, underline:false, strike:false, inherit:null, height:1.0, source:"default" }
// underline / strike: booleans -> the converter writes :underline t / :strike-through t
// height: float multiplier off the base font (1.0 = unchanged); see Relative height
// source: "default" (seeded) | "user" (edited) | "cleared" (user removed a default)
#+end_src
Export policy:
- Write =default= and =user= entries.
- Write =cleared= entries — they must suppress a curated default on reload.
- Omit untouched faces that have no default.
- When =inherit= is set, write =inherit= plus only the explicit overrides.
- Write =height= only when it differs from 1.0.
- Preserve package faces present in an imported file but absent from the current
inventory (or warn) — don't silently drop them.
Import tolerates a missing =packages= key, unknown app keys, unknown face keys,
a missing =inherit=, and a missing =height= (defaults 1.0). A deleted palette
color leaves package face references in the same "(gone)" recoverable state
syntax colors use. Inheritance cycles are rejected (treated as no inheritance)
during preview resolution.
* Relative height
Some faces want to be bigger than body text — org headings above all, also
=org-document-title=. A face's =height= field is a *float multiplier* off the
base font (=1.3= = 1.3× the running font, whatever it is), never an absolute
point size, so it stays portable across fonts and machines. =1.0= means
unchanged. The base monospace family is *not* a theme/tool concern — it lives in
=modules/font-config.el=; the tool owns only relative size.
*Height does not cascade through =inherit=.* This is the one attribute resolved
directly off the face, not through its inherit chain. Emacs multiplies float
heights along an inherit chain, so a level-2 that inherits level-1 (1.3) and
also sets 1.1 would render at 1.43 — almost never what's wanted. Headings should
each size off the *body*, so the seeded defaults set =org-level-1= 1.3,
=org-level-2= 1.2, =org-level-3= 1.15, etc., each independent, and the tool reads
=height= from the face while still resolving *color* through inherit.
- *Schema:* the =height= float on the face object (above), default 1.0, omitted
from export when 1.0.
- *UI:* a small numeric stepper in the face row (range ~0.8–2.0, step 0.05);
meaningful only for the size-bearing faces but shown on every row at 1.0.
- *Preview:* the row renders at the scaled =font-size= so a heading visibly
grows in the mock.
- *Converter:* writes =:height 1.3= into the face spec when ≠ 1.0.
Related, same mechanism: org's mixed-pitch faces (=org-block=, =org-code=,
=org-verbatim=, =org-table=, =org-meta-line=, =org-date=) seed =inherit:
"fixed-pitch"= so they stay monospace when a buffer switches to a proportional
font via =variable-pitch-mode= / =mixed-pitch=. The proportional family itself
stays in =font-config.el= (the presets already carry =:variable-pitch-family=);
the tool only carries the fixed-pitch inherit relationship, shown like any other
inherited value.
* Acceptance criteria
- Existing =dupre.json= (no =packages= key) imports cleanly.
- Export includes =packages= once defaults or edits exist;
=fg/bg/bold/italic/inherit/height/source= round-trip through import/export.
- A face =height= renders as a scaled font-size in the preview (heading visibly
grows) and is read off the face, not cascaded through =inherit=.
- org, magit, elfeed appear in the app selector with complete grouped face tables.
- (phase 6) generic inventory packages appear with editable tables + fallback
previews, the fallback visibly labeled as generic.
- A palette color update propagates to package faces the same way it does to
syntax / ui faces.
- =python3 scripts/theme-studio/generate.py= rebuilds =theme-studio.html=.
- README documents the =packages= schema, inheritance, and the inventory source.
* 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=.
* Agreed decisions
Craig's answers to the first review round, baked in (the body sections above
reflect these; this records the decisions):
1. *Curated set is complete, not iterative.* For org, list its *entire* own
defface set (org-faces.el + org-agenda.el), ~88 faces, not a hand-picked
~18. The user wants every choice present, not a set that grows on demand.
See "Data model — org face set" for the full grouped list.
2. *Seed curated defaults.* Seed sensible fg/bg and weight per face (headings,
title, TODO/DONE bold; agenda dates and deadlines colored by role). The user
reassigns from there.
3. *App order: org, magit, elfeed for v1.* Then the rest one at a time, drawn
from the packages Craig actually runs: calibredb, ghostel, mu4e, the IRC
client, org-drill, dirvish + dired, slack. A finite "most-used" list gets
picked later; we do not try to do everything at once.
4. *Generic fallback is real, not display-only.* Any package not given a
bespoke preview still gets a fully editable face table (so a user can theme
*every* package they have); only the rich preview is missing, replaced by a
swatch-in-context fallback. Bespoke previews ship for org, magit, elfeed.
* Inheritance representation (decided)
Each face carries an optional =inherit= field naming another face (or =null=).
The face's own =fg/bg/bold/italic= are *overrides* layered on top of what it
inherits.
#+begin_src js
["org-level-2", "heading 2", {inherit:"org-level-1", fg:"gold"}]
// exports as: (org-level-2 ((t (:inherit org-level-1 :foreground "#e8bd30"))))
["org-agenda-date-today", "agenda today", {inherit:"org-agenda-date", bold:true}]
// exports as: (org-agenda-date-today ((t (:inherit org-agenda-date :weight bold))))
#+end_src
*Decision (Craig, 2026-06-07): model inheritance, show the resolved result,
override what looks bad.* The point is to see what a face ends up looking like
when it inherits, judge it in the preview, and fix only the ones that look
wrong:
- Each face's *effective* color is resolved through its inherit chain and shown
in its table row, visibly marked "inherited from <face>" so it reads as
not-explicitly-set. The face's own =fg/bg/bold/italic= are overrides layered
on top.
- The mock preview on the right renders every face with its effective color, so
inherited faces are judged in context, not in the abstract.
- Overriding is one action: assign a color (or toggle weight) and the row flips
from inherited to explicit (=source: "user"=), shown at once in the table and
preview.
- Export writes =:inherit PARENT= for faces left inherited (carrying the
relationship, so they follow the parent the theme also sets) and explicit
attributes for the ones overridden — never a frozen copy of an inherited
color.
Seeded defaults express the inherit relationships org itself uses out of the box
(heading levels off a base, =org-agenda-date= variants off =org-agenda-date=,
=org-code= / =org-verbatim= off =fixed-pitch=), so the table opens showing
org's real cascade, which the user then tunes. Inheritance cycles resolve to no
inheritance.
* Custom color picker (proposal)
Craig wants a custom in-page color picker to replace the native browser swatch.
The native =<input type=color>= opens the OS color chooser, which the page
cannot size or restyle; a custom picker is the only way to get a larger,
on-theme picker and to show the palette/contrast in the picker itself.
Proposed widget — a popup anchored to the swatch, drawn in-page:
- A *saturation/value square* (click or drag to set S and V) plus a *hue
slider* down the side. Standard HSV picker geometry.
- A *hex field* synced both ways with the square/slider (already exists in the
add-color row; the picker writes to it).
- The current *palette* shown as clickable chips along the bottom, so picking
an existing color is one click and the overlap problem (many roles, one
color) is visible while choosing.
- A live *contrast readout* against the current background (ratio + AAA / AA /
FAIL) updating as the color moves, so a color is judged for legibility at
pick time, not after assignment.
- Sized generously (the native popup's size was the original complaint); opens
on click of the swatch, closes on pick or click-away.
Implementation: ~120 lines of vanilla JS/canvas (or CSS gradients) for the
square + slider, reusing the existing =rl()= / =contrast()= / =rating()=
helpers for the readout and =normHex()= for the field sync. No dependency. It
replaces the =<input type=color>= in the add-color row and, later, becomes the
picker the package-face dropdowns can also invoke.
It stays *off* the tier-3 critical path: a separate task before or after the
package-face build, not folded into it, since folding it in widens the blast
radius for no dependency benefit. Build it only sooner if package-face editing
proves painful with the native swatch.
Decided (Craig, 2026-06-08): after tier 3, as its own task.
* Files touched
- =scripts/theme-studio/generate.py= — the section, =APPS= data, the package
face table, =renderOrgPreview()=, export/import of =packages=.
- =scripts/theme-studio/theme-studio.html= — regenerated.
- (later) the =theme.json= -> =dupre-*.el= converter (Elisp) — consumes
=packages=.
* Review dispositions
Codex review (2026-06-07), =Not ready=. Findings processed:
- *Modified — generated inventory (high, blocking).* Codex recommended a hybrid
inventory so every installed package is reachable. Accepted the hybrid, but
*split* it: the generated all-package inventory is its own phase (6), after the
three bespoke apps (phases 1-5), rather than a v1 prerequisite. Reason: Codex
named scope explosion as the main risk, and gating org/magit/elfeed on a
full-inventory mechanism is exactly that. The split keeps v1's core shippable
and makes "theme every package" additive. Confirm-with-Craig flagged as an
open.
- *Modified — preview depth (UX obs).* Codex suggested level 4-8 examples in the
org preview. The preview stays a curated document drawing the prominent faces
(incl. a couple of deeper levels as representative); the complete level set
lives in the *table*, which is where every face is assignable. A full 8-level
preview block would bloat the mock without adding assignability.
Everything else in the review accepted as written: implementation phases,
acceptance criteria, the =source= state field + export policy, curated-vs-complete
wording, keeping the custom picker off the critical path, unknown-import
preservation, the test-strategy fixtures, and the UX/architecture/robustness
observations (grouping + filter, reset controls, package-fg/bg contrast readout,
generalized face-control helper, package style kept inside the package object,
"(gone)" recoverable state, inheritance-cycle rejection).
* Review and iteration history
** 2026-06-07 Sun @ 18:17:14 -0500 — Claude Code (emacs-d) — author + responder
- *What:* Folded Craig's first-round cj-comment answers into the body. Curated
org set changed from ~18 to org's complete own-defface set (~88, grouped, incl.
org-agenda). v1 apps fixed to org/magit/elfeed with the rest deferred to a
one-at-a-time list. Generic fallback clarified as a fully editable table for
every package (only the rich preview is bespoke). Answered the inheritance
question with an optional =inherit= field (absolute-default, opt-in cascade)
and added it to the schema. Added a custom-color-picker proposal at Craig's
request. Converted Open questions to Agreed decisions; two genuine opens
remain (inheritance confirm, picker timing).
- *Why:* Craig answered the five open questions inline and asked for the
spec-response fold plus a custom-picker proposal. Completeness over iterative
growth was his explicit call on the curated set.
- *Artifacts:* This spec. The converter language (Elisp) confirmed by Craig.
Note: Craig referenced a Codex spec review, but no
=theme-studio-package-faces-review.org= (or any =*-review.org=) exists in the
repo — the review file could not be found, so only the author-side cj-comment
fold was done; the Codex review remains to be processed when its file is
located.
** 2026-06-07 Sun @ 18:20:16 -0500 — Codex — reviewer
- *What changed or was recommended:* Created the missing formal review and
marked the spec =Not ready=. The review recommends adding implementation
phases, acceptance criteria, an explicit package-face inventory source, and
default/user/cleared/inherited export semantics.
- *Why:* The direction is coherent and the first-round decisions are folded in,
but v1 now depends on behavior that is not yet implementable from the current
static generator without a defined inventory and state/export contract.
- *Artifacts:* [[file:theme-studio-package-faces-spec-review.org][theme-studio-package-faces-spec-review.org]]
** 2026-06-07 Sun @ 18:28:02 -0500 — Claude Code (emacs-d) — responder
- *What:* Ran spec-response against the Codex review. Added Implementation
phases (7), Package face inventory source (hybrid, split), State and export
policy (=source= field + export rules), and Acceptance criteria. Wove in the
UX/architecture/robustness observations. Two reasoned modifies (generated
inventory split into its own phase; org preview stays curated rather than
all-8-levels), everything else accepted — see Review dispositions. Status
moved to =Ready with caveats=. Deleted the review file.
- *Why:* The four blocking findings were real implementation-contract gaps; the
inventory split answers Codex's own scope-explosion warning while still
reaching "theme every package."
- *Artifacts:* This spec (Review dispositions section); review file deleted per
the spec-response close-out. Three opens remain for Craig (inheritance confirm,
hybrid-inventory confirm, picker timing).
** 2026-06-08 Mon @ 00:12:38 -0500 — Codex — reviewer
- *What changed or was recommended:* Reran spec-review after the response pass.
Assigned rubric =Ready= and did not create a new review file. The previous
blockers are now addressed: implementation phases, acceptance criteria,
hybrid/split inventory source, package-face state/export semantics, task
tracking, and the open inheritance/inventory/picker decisions are resolved.
- *Why:* The spec now gives an implementer concrete behavior, phase boundaries,
validation criteria, and deferred-work handling without forcing product
decisions during implementation.
- *Artifacts:* This spec; implementation tasks in [[file:../../todo.org][todo.org]].
** 2026-06-08 Mon @ 00:38:23 -0500 — Claude Code (emacs-d) — author
- *What:* Added a relative =height= field to the face schema (float multiplier
off the base font, default 1.0, omitted at 1.0), a new "Relative height"
section, a per-face stepper in the table, preview scaling, and converter
output. Established the rule that =height= is read off the face and does *not*
cascade through =inherit= (Emacs multiplies float heights along the chain).
Noted the mixed-pitch =fixed-pitch= inherits as the same-mechanism companion.
Brought Phase 1's shipped schema plumbing in line with the new field.
- *Why:* Craig asked to fold height in — it matters for org headings above all.
Font *family* stays in =modules/font-config.el=; the theme owns relative size
and the fixed-pitch inherit relationships only.
- *Artifacts:* This spec; =scripts/theme-studio/generate.py= phase-1 plumbing.
|