diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-15 11:15:58 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-15 11:15:58 -0500 |
| commit | 7bcf6dad2b1078994346b61531bce281a75519f9 (patch) | |
| tree | 3bb7f507c339fc73343d22c54a303786a3585408 /docs/specs | |
| parent | 45e0f6e896b2c34de25d5c3aa18474c79d6a1e72 (diff) | |
| download | dotemacs-7bcf6dad2b1078994346b61531bce281a75519f9.tar.gz dotemacs-7bcf6dad2b1078994346b61531bce281a75519f9.zip | |
docs: move the two docs-root specs into docs/specs/
Finish the reorg: theme-studio-palette-generator (doing — core planner, UI, and generatortest gate shipped, one refinement open) and theme-studio-semantic-theme-architecture (not-started) lived in docs/ root. Moved both into docs/specs/ with status filenames, ID and STATUS drawers, and id-linked references. Fixed their Related links from ../todo.org to ../../todo.org for the deeper path.
Diffstat (limited to 'docs/specs')
| -rw-r--r-- | docs/specs/theme-studio-palette-generator-spec-doing.org | 298 | ||||
| -rw-r--r-- | docs/specs/theme-studio-semantic-theme-architecture-spec.org | 266 |
2 files changed, 564 insertions, 0 deletions
diff --git a/docs/specs/theme-studio-palette-generator-spec-doing.org b/docs/specs/theme-studio-palette-generator-spec-doing.org new file mode 100644 index 000000000..b98e10789 --- /dev/null +++ b/docs/specs/theme-studio-palette-generator-spec-doing.org @@ -0,0 +1,298 @@ +:PROPERTIES: +:ID: 2df157b8-c7c1-47a9-b080-d9586c6f424c +:STATUS: doing +:END: +#+TITLE: Theme Studio Palette Generator -- Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-14 +#+TODO: TODO | DONE SUPERSEDED CANCELLED + +* Metadata +| Status | doing | +|----------+-------| +| Owner | Craig | +|----------+-------| +| Reviewer | Craig | +|----------+-------| +| Related | [[file:../../todo.org::*theme-studio palette generator][theme-studio palette generator task]] | +|----------+-------| + +* Summary +Theme Studio should grow a constraint-first palette generator that creates preview palette columns from the current theme context, then lets the user explicitly commit individual colors or whole columns. It should reuse the existing color selector as the single-color workbench instead of adding a second picker. + +The v1 feature generates palette columns only. It does not assign faces automatically; once generated colors are applied, they become normal editable palette colors with stable column ids. + +* Problem / Context +Theme Studio now has stable color columns, span controls, OKLCH editing, contrast metrics, DeltaE warnings, locks, package faces, and a live preview. The missing workflow is generating a coherent set of candidate base colors without manually choosing each hue, checking contrast, spanning, and then adjusting by eye. + +Generic palette generators are not a good fit by themselves. They optimize visual harmony before text readability, while Emacs themes need dense syntax colors, UI overlays, selections, search hits, diagnostics, and package faces to remain legible over a fixed ground. + +There is also UI overlap with the existing color selector. The selector already edits one active color, shows a swatch, drives the hex field and OKLCH picker, and adds or updates palette colors. A generator should feed that selector, not duplicate it. + +* Goals and Non-Goals +** Goals +- Generate candidate palette columns from explicit source modes and current theme constraints. +- Default to OKLCH generation so lightness and chroma are predictable. +- Default to a syntax-balanced scheme designed for readable code themes. +- Preview generated columns without mutating the real palette. +- Reuse the current color selector to inspect and tune one generated tile at a time. +- Allow adding a generated tile as a new base color column. +- Allow appending a whole generated column. +- Preserve stable column ids and existing assignments where possible when applying proposals. +- Expose diagnostics for contrast, DeltaE, gamut clamp, and rejected candidates. + +** Non-Goals +- Automatically assign generated colors to syntax/UI/package faces in v1. +- Replace the existing manual picker, swatch, or per-column span controls. +- Import external palettes, CSS colors, image colors, or theme files. Import organization remains separate. +- Generate terminal/ANSI palettes in v1. +- Add OKHSL/OKHSV generation modes in v1. +- Rewrite bg/fg automatically in v1. + +** Scope tiers +- v1: generator panel, pure planner, preview columns, existing color selector integration, add generated tile as base column, append whole generated column, diagnostics, tests, README docs. +- Out of scope: automatic face assignment, external imports, image extraction, terminal colors, rewriting bg/fg. +- vNext: replace selected columns, regenerate selected spans, regenerate generator-owned columns, OKHSL/OKHSV controls, whole-palette harmonization, CVD-aware scoring, named style presets, terminal palette derivation. + +* Design +The generator is a panel above the real palette columns. It contains generation controls and a preview area, but no separate color picker. The existing color selector remains the place where one color is inspected, tuned, named, and committed. + +For the user, the workflow is: + +1. Choose a generator source mode and scheme from compact controls in the generator panel. +2. Click preview. +3. Inspect temporary generated columns above the real palette. +4. Click a generated tile to load it into the existing color selector. +5. Optionally tune that color with the existing hex/OKLCH picker. +6. Commit either one generated tile as a new base column, or a whole generated column as normal palette entries. + +The preview layer is separate from =PALETTE=. Rendering preview columns must not create real palette entries, move existing columns, or change assignments. Preview tiles should look like palette tiles, but have a distinct preview treatment such as a dashed border/header or subtle "preview" label. + +The generator controls should be compact and explicit: + +- =Source= is a segmented control in v1: =bg/fg= and, if implemented in v1, =selected=. =bg/fg= is the default. Each option has a short tooltip: =bg/fg= means "generate from the current ground/foreground constraints"; =selected= means "harmonize around selected palette columns." +- =Scheme= can be a select/dropdown because it has more choices. A scheme is the hue-placement strategy used to propose candidate accents. The choices are not final color decisions; they are starting layouts that the planner filters through contrast, lightness, chroma, gamut, and DeltaE constraints. +- The scheme dropdown should include one-line help in the option title or adjacent help text. For example: =syntax-balanced= means "spread readable code accents across the hue wheel"; =analogous= means "nearby hues"; =triadic= means "three evenly-spaced hue families"; =manual= means "use the hues entered by the user." +- =Accent count= controls how many base columns the proposal tries to generate. The default is 8, with a v1 range of 3-12. +- =Span count= controls how many generated span steps each proposed column includes. The default should be conservative, likely 0 or the current preferred span setting, with a v1 range of 0-4. + +Therefore, the number of generated columns is configurable: it is the =accent count=, subject to rejected candidates. If the user asks for 8 accents and 2 are rejected by constraints, the preview should show 6 generated columns and report 2 rejected candidates in the summary. It should not silently backfill unrelated colors unless that behavior is explicitly added later. + +The current color selector gains a third selection mode: + +- No selection: =+ add color= creates a new manual base column from the selector value. +- Real palette tile selected: =update selected= changes that palette tile or recolors its column as today. +- Generated preview tile selected: the selector shows the generated hex/name, and =+ add color= commits that generated color as a new base color column. =update selected= is disabled unless a real palette tile is selected. + +A small status label near the selector should make this state explicit: + +#+begin_example +editing: new color +editing: palette color blue +editing: generated color blue-2 +#+end_example + +Generated columns are proposed from a source mode: + +- =bg/fg only= is the v1 default. It uses the ground and foreground as constraints, plus the chosen scheme/base hue. +- =selected columns= is v1 optional if cheap; otherwise vNext. It harmonizes around columns the user explicitly selected. +- =whole palette= is vNext. It should not be automatic in v1 because imported, experimental, or throwaway colors could make generation unpredictable. + +Generation is constraint-first. The planner chooses hue candidates from the scheme, then searches for useful OKLCH lightness/chroma values that satisfy the current background, contrast target, DeltaE separation, and gamut constraints. Classical harmony schemes are input layouts, not the final authority. + +For implementers, the feature is a pure planning layer plus DOM rendering/application: + +- The planner receives current palette, ground, source mode, generator config, and locks. +- The planner returns a proposal object and never mutates global state. +- The DOM layer renders proposal columns as preview columns. +- Applying one tile or one column converts proposal members into normal palette entries using existing column-id, span, name-collision, and gone/repoint behavior. + +** Generator config +V1 config fields: + +- sourceMode: bg-fg, selected-columns +- scheme: syntax-balanced, analogous, split-complementary, triadic, tetradic, warm-cool, manual +- baseHue: degrees, used by non-manual schemes +- manualHues: list of degrees, used by manual mode +- accentCount: integer, default 8, range 3-12 +- spanCount: integer, default 0 or current preferred span, range 0-4 in v1 +- textLightnessBand: min/max OKLCH L for text accents +- chromaBias: subdued, balanced, vivid +- contrastTarget: none, WCAG AA, WCAG AAA +- deltaEMin: default to the existing palette warning threshold +- locks: respect locked columns and assignments where apply modes touch existing data + +** Proposal object +The planner returns a proposal object with the generator config, proposed columns, rejected candidates, and a summary. Each proposed column carries a stable column id, display name, base hex, member colors with offsets and clamp/metric data, and column-level diagnostics. Rejected candidates carry the attempted hue, rejection reason, and nearest conflicting column when relevant. The summary includes generated count, clamped count, rejected count, minimum contrast, and minimum DeltaE. + +This shape is intentionally close to the existing palette-column model. Preview rendering should not need a second color model. + +** Applying generated colors +The first v1 apply actions are deliberately small: + +- =Add generated tile as base column=: creates a new normal palette column from the selected preview tile. The new column id is derived from the tile name and suffixes on collision. +- =Append generated column=: adds every member of that preview column after existing real columns. Members keep a stable shared column id. +- =Clear preview=: discards proposal state without changing the real palette. + +The following apply actions are deferred unless v1 implementation is already straightforward: + +- replace selected columns +- regenerate spans only +- regenerate generator-owned columns + +When a generated tile is committed as a base column, it starts as a one-tile column. The user can then span it using the existing column span widget. This keeps the one-color action easy to understand and avoids surprising multi-tile commits from a tile-level button. + +** Display +The panel sits between the color selector row and the committed palette columns. It has: + +- source mode segmented control +- scheme segmented control or select +- base hue/manual hue controls +- accent count and span count numeric controls +- chroma bias control +- contrast target control +- preview and clear-preview buttons +- summary row: min contrast, min DeltaE, clamped count, rejected count + +Preview columns render below the controls and above committed columns. A generated tile click loads the existing selector. A generated column header click loads the column base into the selector. A column-level =append column= button commits the whole preview column. + +* Alternatives Considered +** Generic harmony wheel that writes directly into the palette +- Good, because it is familiar and visually compact. +- Bad, because it mutates real palette state before the user can inspect results, and it optimizes hue harmony before readability. +- Neutral, because a hue wheel can still be useful as an input control inside a preview-first generator. + +** Separate generator-specific color picker +- Good, because preview tuning could be isolated from committed palette editing. +- Bad, because Theme Studio already has a capable single-color selector, and a second picker would duplicate hex, OKLCH, contrast, swatch, and add/update semantics. +- Neutral, because a future advanced generator could add a small detail panel, but v1 should not. + +** Generate from the whole palette by default +- Good, because it can harmonize with everything already on screen. +- Bad, because the palette may contain experiments, imported colors, temporary colors, or intentionally clashing accents; using all of them makes results hard to predict. +- Neutral, because whole-palette harmonization is valuable as an explicit vNext mode. + +** Full automatic face assignment +- Good, because it could produce a near-complete theme in one action. +- Bad, because it crosses into seeding, locks, role maps, and package face behavior that already have separate product decisions. +- Neutral, because the palette generator can feed a later seeding workflow. + +** Add OKHSL/OKHSV now +- Good, because those controls may feel friendlier than raw OKLCH. +- Bad, because v1 already has OKLCH math and the feature risk is workflow/state, not another color model. +- Neutral, because OKHSL/OKHSV remain good vNext candidates. + +* Decisions [5/5] +** DONE Default to preview-first generation +- Context: Generator output can disrupt a carefully tuned palette if it mutates immediately. +- Decision: We will render generated colors as temporary preview columns and require explicit add/append actions. +- Consequences: Easier to inspect and avoid accidental data loss; harder because the UI needs proposal state and apply semantics. + +** DONE Reuse the current color selector +- Context: The existing selector already edits one color, shows metrics, opens the picker, and adds/updates palette colors. +- Decision: We will make generated tile clicks load the existing selector instead of adding a second generator picker. +- Consequences: Easier to keep editing behavior consistent; harder because the selector now needs clear state for new, palette, and generated selections. + +** DONE Keep v1 palette-only +- Context: Automatic assignment would touch syntax, UI, package faces, locks, and seeding rules. +- Decision: We will generate palette columns only in v1 and leave face assignment to existing/manual workflows. +- Consequences: Easier to ship a focused generator; harder because the user still maps colors onto faces. + +** DONE Default generation source to bg/fg only +- Context: Existing palette colors may be experimental or imported; using all of them by default makes generation unpredictable. +- Decision: We will default to bg/fg constraints plus explicit scheme/base hue. Selected-column source can be included if scoped; whole-palette source is vNext. +- Consequences: Easier to understand why a proposal was generated; harder because matching an existing palette requires an explicit source mode. + +** DONE Defer OKHSL/OKHSV to vNext +- Context: OKHSL/OKHSV may be friendlier interaction models, but OKLCH already supports the required perceptual generation math. +- Decision: We will ship OKLCH generation first and consider OKHSL/OKHSV after v1 is usable. +- Consequences: Easier to keep v1 small and rigorous; harder because some users may find OKLCH controls less familiar. + +* Implementation phases +** Phase 1 -- Planner core +Add pure generator functions in =app-core.js= or a new generator module. Inputs are current palette, ground, generator config, source selection, and locks. Outputs are proposal objects. Unit tests cover scheme hue placement, OKLCH candidate generation, gamut clamp reporting, name/id collision handling, and no mutation of input state. + +** Phase 2 -- Candidate scoring +Add bounded scoring/adjustment for contrast target, DeltaE separation, chroma bias, and text lightness band. Unit tests cover rejected candidates, clamped colors, low-chroma distinguishability, and deterministic output for fixed config. + +** Phase 3 -- Generator panel and preview rendering +Add the panel, controls, proposal state, preview columns, summary metrics, and clear-preview behavior. Browser gate: preview creates temporary columns without changing committed =PALETTE=. + +** Phase 4 -- Existing selector integration +Add generated-preview selection state to the color selector. Clicking a preview tile loads its hex/name. The selector status label shows generated-vs-palette-vs-new. =+ add color= commits the selected preview tile as a new one-tile base column. Browser gates cover generated tile selection and add-as-column behavior. + +** Phase 5 -- Append generated column +Add column-level append. The generated column becomes normal palette entries with one stable column id. Collisions suffix names/ids deterministically. Browser gates cover append, collision suffixing, and unchanged existing assignments. + +** Phase 6 -- Persistence and metadata +Round-trip optional generator metadata for applied columns without requiring it for normal palette behavior. Existing palettes without metadata continue to work. Browser gate extends roundtrip coverage. + +** Phase 7 -- Documentation and cleanup +Document generator controls, source modes, preview behavior, selector integration, and limits in =scripts/theme-studio/README.md=. Remove prototype code and keep =make -C scripts/theme-studio test= green. + +* Acceptance criteria +- [ ] Previewing a generated palette does not mutate committed =PALETTE=. +- [ ] Preview columns appear above committed columns and are visually distinct. +- [ ] Clicking a generated tile loads that color into the existing selector. +- [ ] The selector clearly says whether it is editing a new color, palette color, or generated color. +- [ ] =+ add color= on a selected generated tile creates a new one-tile base column. +- [ ] Appending a generated column creates normal editable palette entries with one stable column id. +- [ ] Name and column-id collisions are suffixed deterministically. +- [ ] Generated colors report clamp, contrast, and DeltaE diagnostics. +- [ ] Existing manual palette workflows still work without opening the generator panel. +- [ ] Theme Studio tests cover planner functions, preview rendering, selector integration, apply behavior, and round-trip metadata. + +* Readiness dimensions +- Data model & ownership: Proposal state is transient and generator-owned. Applied colors become normal user-owned palette entries. Optional generator metadata is advisory and must not override manual edits. +- Errors, empty states & failure: Invalid config disables preview with an inline message naming the bad field. No preview shows an empty-state line. Rejected candidates appear in the summary. Apply failures must not partially mutate committed palette state. +- Security & privacy: N/A because generation is local deterministic color math with no credentials, network calls, or private external data. +- Observability: The preview summary shows generated, clamped, rejected, min contrast, and min DeltaE. Tile titles or details expose per-color diagnostics. +- Performance & scale: Expected accent counts are 3-12 bases with 0-4 span steps. Candidate search should remain synchronous and bounded. Broader search, if added later, needs progress/cancel. +- Reuse & lost opportunities: Reuse OKLCH, gamut clamp, contrast, DeltaE, palette columns, span generation, locks, existing selector, and existing browser gates. Do not add a second color math stack or picker. +- Architecture fit & weak points: The weak point is proposal/apply state in the DOM app. Keep planner pure and DOM code limited to rendering/applying proposal objects. +- Config surface: Public knobs are source mode, scheme, base/manual hue, accent count, span count, chroma bias, contrast target, DeltaE threshold, and lightness band. Defaults favor readable dark-theme syntax. +- Documentation plan: Update the Theme Studio README with generator controls, source modes, selector integration, preview/apply behavior, and v1 limits. +- Dev tooling: Use =make -C scripts/theme-studio test= as the primary gate and =make -C scripts/theme-studio coverage= for instrumented JS/generator coverage. +- Rollout, compatibility & rollback: The generator is additive. Existing palettes load unchanged. Preview can be cleared. Applied generated columns can be deleted manually. +- External APIs & deps: N/A because v1 has no external APIs or new dependencies. + +* Risks, Rabbit Holes, and Drawbacks +- Candidate search can become an optimization rabbit hole. V1 should use deterministic bounded search around target OKLCH bands. +- "Syntax-balanced" is subjective. Keep it documented as a heuristic, not a claim of universal taste. +- Selector state can become confusing. The status label and disabled update button are required, not polish. +- Whole-palette harmonization is tempting but should wait until preview/apply basics feel good. +- Optional metadata can drift after manual edits. Treat it as advisory only. + +* Testing / Verification / Rollout +Use the existing Theme Studio test stack: + +- Node tests for planner/scoring/collision/immutability. +- Browser hash gate for preview-only non-mutation. +- Browser hash gate for generated tile -> selector state. +- Browser hash gate for add-generated-tile-as-column. +- Browser hash gate for append generated column. +- Round-trip gate for optional generator metadata. +- Manual Chrome pass on at least one dark palette and one light palette. + +* References / Appendix +- [[file:design/theme-studio-color-harmony.org][theme-studio color harmony explainer]] +- [[id:15db8ae3-fc14-49f3-9ed5-d5ff59790904][perceptual color metrics spec]] +- [[file:theme-studio-palette-ramps-spec.org][palette ramps and contrast safety spec]] +- [[file:theme-studio-palette-columns-spec.org][palette columns spec]] +- [[file:../../todo.org::*theme-studio import organization workflow needs a spec][import organization task]] + +* Review and iteration history +** 2026-06-13 Saturday @ 16:31:01 -0500 -- Craig -- author +- What: Initial draft using the spec-create workflow. +- Why: Palette generation has real design trade-offs around color space, preview/apply behavior, assignment boundaries, and how much generator state should persist. +- Artifacts: [[file:../../todo.org::*theme-studio palette generator][theme-studio palette generator task]]. + +** 2026-06-14 Sunday @ 00:44:00 -0500 -- Craig -- author +- What: Reworked the draft around preview-first generation, existing color selector integration, generated tile add-as-column behavior, and source-mode defaults. +- Why: Craig clarified the desired UX: generated colors should be inspectable/tunable through the existing selector, and committing one generated color should create a normal base column. +- Artifacts: [[file:../../todo.org::*theme-studio palette generator][theme-studio palette generator task]]. + +** 2026-06-14 Sunday @ 01:07:00 -0500 -- Craig -- author +- What: Folded Craig's inline comments into the design, clarifying source/scheme controls, guidance text, the meaning of schemes, configurable accent count, and rejected-candidate behavior. Removed the comment/source blocks. +- Why: The generator UI needed to say what the user actually sees before implementation can proceed. +- Artifacts: [[file:../../todo.org::*theme-studio palette generator][theme-studio palette generator task]]. diff --git a/docs/specs/theme-studio-semantic-theme-architecture-spec.org b/docs/specs/theme-studio-semantic-theme-architecture-spec.org new file mode 100644 index 000000000..01ef1902c --- /dev/null +++ b/docs/specs/theme-studio-semantic-theme-architecture-spec.org @@ -0,0 +1,266 @@ +:PROPERTIES: +:ID: fe980b12-451a-4d8b-a550-d99f9ec49f45 +:STATUS: not-started +:END: +#+TITLE: Theme Studio Semantic Theme Architecture -- Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-14 +#+TODO: TODO | DONE SUPERSEDED CANCELLED + +* Metadata +| Status | not-started | +|----------+-------| +| Owner | Craig | +|----------+-------| +| Reviewer | Craig | +|----------+-------| +| Related | [[file:../../todo.org::*theme-studio semantic theme architecture][theme-studio semantic theme architecture task]] | +|----------+-------| + +* Summary +Theme Studio currently exports JSON into a flat generated Emacs theme: every face receives resolved hex colors and attributes directly. That is faithful to the current UI state, but it loses the middle layer that makes themes like Modus easy to reason about: named palette entries, semantic color roles, reusable face templates, and a thin generated wrapper. + +This spec proposes a future Theme Studio output architecture that can preserve those layers. The v1 goal is not to replace the current flat exporter immediately; it is to define the model, converter shape, migration path, and test surface needed to support a more maintainable theme system. + +* Problem / Context +Theme Studio has grown from a color picker into a full theme workbench: palette columns, syntax assignments, UI faces, package faces, generated candidates, contrast checks, locks, and a JSON-to-Emacs-theme converter. The converter currently emits direct =custom-theme-set-faces= specs. That works, but it makes the generated theme hard to maintain outside Theme Studio because the meaning of each color is already flattened away. + +The design discussion around Modus showed a different structure. Modus keeps concrete theme files small and pushes the durable logic into a shared engine: palettes define available colors, semantic mappings define what those colors mean, face specs use semantic names, and a theme constructor resolves everything when the theme loads. + +Theme Studio does not need to become Modus, but it can borrow the architecture. This would make generated themes easier to inspect, easier to customize by hand, and more able to support rules such as "comments and comment delimiters should normally share a color" without forcing every rule into the UI. + +* Goals and Non-Goals +** Goals +- Define a layered Theme Studio theme architecture: palette data, semantic role mapping, face templates, and generated theme wrapper. +- Preserve the current flat exporter as a compatibility path until the layered output is proven. +- Allow semantic roles to group multiple faces under one design decision. +- Allow advisory semantic rules that detect inconsistent mappings, such as comment and comment delimiter diverging. +- Keep the generated output loadable by normal Emacs =load-theme=. +- Make the design testable with pure converter tests and real Emacs theme-load tests. +- Make future Theme Studio features, such as face-seeding and role-aware palette generation, easier to build. + +** Non-Goals +- Do not enforce any specific semantic design rule in v1. The comment/comment-delim rule is an example of what the layer can express, not a required policy. +- Do not replace Theme Studio's current JSON format in one step. +- Do not require users to edit generated Elisp by hand. +- Do not implement a full Modus-compatible theme engine. +- Do not automatically reassign faces based on semantic rules. +- Do not remove direct per-face overrides from Theme Studio. + +** Scope tiers +- v1: Specify the layered model, add optional export fields or an intermediate converter model, generate a loadable layered theme file, preserve current flat output, add tests, document the architecture. +- Out of scope: UI for editing all semantic roles, automatic role inference from arbitrary imported themes, rule enforcement, Modus compatibility. +- vNext: Role editor UI, advisory rule panel, role-aware palette generator integration, semantic imports, user-defined rule packs, derivative theme wrappers. + +* Design +The architecture has four layers. + +Palette data is the color inventory. It answers "what named colors exist?" A Theme Studio palette entry already has most of this information: hex, display name, and stable column id. The layered exporter should preserve that as named Elisp data rather than immediately substituting every hex into every face. + +Semantic role mapping assigns design meaning to palette entries. It answers "what does this color do?" Examples include =syntax-keyword=, =syntax-comment=, =ui-region-bg=, =org-todo-fg=, and =mode-line-bg=. A role may point to a palette color, a literal hex, or another role. The important shift is that faces stop depending directly on "blue" or "#67809c"; they depend on "keyword" or "org-todo foreground". + +Face templates map Emacs faces to semantic roles and structural attributes. They answer "which faces use which roles?" A template can say =font-lock-comment-face= uses =syntax-comment=, =font-lock-comment-delimiter-face= uses =syntax-comment-delimiter=, and =org-todo= uses =org-todo-fg= plus =org-todo-bg= with bold and box attributes. Templates are where syntax, UI, and package face coverage live. + +The generated theme wrapper ties the layers together. It declares the theme, binds palette and semantic role values, expands the face templates, calls =custom-theme-set-faces= and =custom-theme-set-variables= where needed, then provides the theme. For users, the result is still a normal loadable Emacs theme. + +For the user, this does not need to introduce a new workflow immediately. Theme Studio can still export JSON and build a theme. The difference is that the generated Elisp becomes more intelligible: + +#+begin_src emacs-lisp +(defconst theme-palette + '((bg "#000000") + (fg "#e0e0e0") + (blue "#67809c"))) + +(defconst theme-semantics + '((syntax-keyword blue) + (syntax-comment fg-muted) + (ui-region-bg blue))) + +(custom-theme-set-faces + 'theme + `(font-lock-keyword-face ((,c :foreground ,syntax-keyword))) + `(region ((,c :background ,ui-region-bg)))) +#+end_src + +For implementers, the converter gains an intermediate representation: + +#+begin_src text +theme.json + -> palette model + -> semantic role model + -> face template model + -> generated theme file +#+end_src + +The current JSON can be projected into this model conservatively. If a face has no semantic role, the converter can synthesize a private role for that face, or fall back to a direct literal. That lets the layered exporter coexist with the current state format while the UI catches up. + +** Palette data +Palette data should be owned by the palette panel. It persists user-authored color names, hexes, and column ids. Generated span members remain palette entries, but the exporter may choose whether to expose every span member as a public named color or treat some as generated/private names. + +The palette layer should avoid face knowledge. It should not know that =blue= is a keyword or that =gold= is an Org title. That meaning belongs to the semantic layer. + +** Semantic role mapping +Semantic roles are owned by the theme design layer. Some roles can be generated from existing Theme Studio assignments: + +- syntax rows become roles such as =syntax-keyword= and =syntax-string=. +- UI rows become roles such as =ui-region-fg=, =ui-region-bg=, and =ui-mode-line-bg=. +- package rows become roles such as =org-todo-fg= or =magit-branch-fg= when those roles are worth naming. + +The role map should support three value kinds: + +- palette reference: =syntax-keyword -> blue= +- role reference: =syntax-comment-delimiter -> syntax-comment= +- literal: =warning-underline -> "#ff0000"= + +Role references are what make rules and shared intent useful. If comment and comment delimiter should move together, =syntax-comment-delimiter= can point at =syntax-comment=. If the user intentionally separates them, the mapping can become explicit and the advisory can report that it was intentionally diverged. + +** Semantic rules +The semantic layer can support rules because it knows relationships that the flat face table does not. A rule can be advisory, enforced, or ignored, but v1 should only define the mechanism and ship rules as documentation/test fixtures unless a specific rule is agreed later. + +Example advisory rules: + +- =syntax-comment= and =syntax-comment-delimiter= normally use the same foreground. +- =org-headline-done= should normally be no brighter than active headline faces. +- =region= with explicit foreground/background should be checked as its own pair, while bg-only region should be checked against covered text. +- warning/error/success roles should stay distinguishable by DeltaE and not only by text label. + +The comment/comment-delim rule is a note, not a v1 requirement. The spec deliberately does not mandate it because Theme Studio should preserve deliberate design choices even when they differ from the guide. + +** Face templates +Face templates should be data, not ad hoc emitted strings. They need to represent: + +- target face symbol +- foreground role or literal +- background role or literal +- inherit face or role +- structural attributes: bold, italic, underline, strike, height, box +- optional conditions such as display class, if the project later supports them + +The first implementation can keep one template table inside =build-theme.el=. Later, this can move into generated files or a shared Theme Studio runtime. + +Templates should support both semantic and direct output. This keeps migration safe: + +#+begin_src emacs-lisp +(font-lock-keyword-face :foreground syntax-keyword) +(some-face :foreground "#aabbcc") ; direct fallback +#+end_src + +** Generated theme wrapper +The wrapper is the loadable artifact. It should be self-contained unless we intentionally introduce a shared runtime. + +Two wrapper styles are viable: + +- Single-file layered output: every generated theme contains its palette, semantic map, resolver, templates, and =custom-theme-set-faces= call. +- Shared runtime plus thin generated wrapper: a common =theme-studio-theme.el= library resolves roles and expands templates, while each generated theme mostly supplies data. + +V1 should prefer single-file layered output. It is easier to test, easier to share, and avoids requiring a generated theme user to install Theme Studio runtime files. A shared runtime can be introduced later if duplication becomes painful. + +* Alternatives Considered +** Keep only the flat exporter +- Good, because it is simple, already works, and exactly reflects the current UI state. +- Bad, because it loses role intent and makes hand inspection or downstream customization harder. +- Neutral, because it should remain the compatibility and debugging baseline. + +** Generate a full Modus-style engine +- Good, because it is a proven architecture for serious Emacs themes. +- Bad, because Modus solves a broader package-quality problem than Theme Studio needs, and copying its engine would add complexity before we know the required seams. +- Neutral, because Modus remains the reference for structuring palette, semantics, and wrappers. + +** Add semantic roles only in JSON, still emit flat Elisp +- Good, because it lets Theme Studio reason about roles without changing the generated theme format. +- Bad, because users inspecting the generated theme still see only flattened direct specs. +- Neutral, because this may be a useful intermediate phase. + +** Make semantic rules mandatory +- Good, because it can preserve consistency and prevent accidental drift. +- Bad, because theme design often has deliberate exceptions, and hard rules can fight the user's eye. +- Neutral, because some rules may later become opt-in enforcement once proven. + +* Decisions [3/3] +** DONE Output layered themes without replacing flat output immediately +- Context: The current flat exporter is useful and working. The layered architecture is broader and should not block current Theme Studio iteration. +- Decision: We will add layered output as an additional converter path or mode before considering it the default. +- Consequences: Easier rollback and comparison; harder because tests must cover two output paths during the transition. + +** DONE Treat semantic rules as advisory in v1 +- Context: The semantic layer can express design relationships, but not every relationship should be forced. +- Decision: We will model rules as advisory validation data in v1 and avoid enforcement unless a separate product decision promotes a rule. +- Consequences: Easier to preserve user intent; harder because inconsistent themes can still be exported. + +** DONE Prefer self-contained generated layered themes for v1 +- Context: A shared runtime would reduce generated file size, but it creates another dependency for loading a generated theme. +- Decision: We will prefer a self-contained layered theme file in v1. +- Consequences: Easier sharing and loading; harder because each generated theme duplicates resolver/template code. + +* Implementation phases +** Phase 1 -- Intermediate model +Add a pure intermediate representation in the converter: palette entries, semantic roles, face templates, and resolved face specs. Keep existing flat output unchanged. + +** Phase 2 -- Semantic projection +Project current Theme Studio JSON into semantic roles. Start with syntax and core UI roles, then package roles only where names are clear and useful. Preserve direct literals as fallback. + +** Phase 3 -- Layered renderer +Generate a self-contained layered =*-theme.el= from the intermediate model. The generated file should still load with normal =load-theme= and should produce the same effective faces as the flat renderer for covered data. + +** Phase 4 -- Advisory rule surface +Add pure rule checks over the semantic map. Output diagnostics in tests or converter warnings first; defer UI display. + +** Phase 5 -- Tests and comparison tooling +Add tests that convert the same JSON through flat and layered paths, load both themes in isolated Emacs sessions, and compare representative effective face attributes. + +** Phase 6 -- Documentation and rollout +Document the architecture, Makefile target, and compatibility story. Keep flat output as the default until manual inspection and tests show the layered output is trustworthy. + +* Acceptance criteria +- [ ] The current flat JSON-to-theme converter still works. +- [ ] A layered converter can generate a self-contained loadable =*-theme.el=. +- [ ] The layered generated theme preserves default, syntax, UI, and package face attributes for representative fixtures. +- [ ] The layered generated file exposes palette data and semantic mappings in readable Elisp. +- [ ] Semantic role references resolve deterministically, including role-to-role references. +- [ ] Cycles in semantic role references fail with an actionable converter error. +- [ ] Advisory rules can report findings without blocking output. +- [ ] Documentation explains the four layers and how they relate to the current flat exporter. + +* Readiness dimensions +- Data model & ownership: Palette data remains owned by Theme Studio's palette model. Semantic roles are owned by the theme design layer. Face templates are owned by the converter/runtime. Generated theme wrappers are build artifacts. +- Errors, empty states & failure: Missing roles, invalid palette references, and role cycles must name the role and input file. Unknown faces should fall back to direct specs or fail only when a strict mode is explicitly requested. +- Security & privacy: N/A because theme files contain colors and face names, not credentials or private content. +- Observability: Converter output should say whether it wrote flat or layered output and list advisory rule findings. Later UI can display semantic findings. +- Performance & scale: Expected scale is hundreds to low thousands of faces. Role resolution should be linear with cycle detection; tests should include a large package-face fixture. +- Reuse & lost opportunities: Reuse =build-theme.el= parsing, face attr construction, box conversion, and Makefile targets. Do not duplicate Emacs face attribute rules. +- Architecture fit & weak points: The converter is the right first integration point because it can prove the model without changing the browser UI. Weak point: projecting semantics from flat assignments may create artificial roles; mitigate by marking generated/private roles distinctly. +- Config surface: New converter mode or Make target, likely =theme-studio-theme-layered= or =MODE=layered=. Defaults keep current flat output. +- Documentation plan: Update Theme Studio README and add a short architecture note. Cross-link this spec from the theme converter task. +- Dev tooling: Extend existing =make theme-studio-theme= or add a sibling target. Add ERT coverage for converter behavior and a comparison test between flat/layered outputs. +- Rollout, compatibility & rollback: Keep flat output as default. Layered output is opt-in until proven. Rollback is switching the Make target/mode back to flat. +- External APIs & deps: No external APIs. Emacs =custom-theme-set-faces= and =load-theme= semantics are the only runtime dependency. + +* Risks, Rabbit Holes, and Drawbacks +- Role projection may invent names that look meaningful but are only direct mappings from one face. Keep generated/private role names visibly distinct. +- A role editor UI could become a second Theme Studio. Defer UI until the converter model proves useful. +- Advisory rules can become noisy. Start with a small list, no enforcement, and clear wording. +- Self-contained output duplicates helper code. Accept that cost until sharing/runtime needs are clearer. + +* Testing / Verification / Rollout +The test surface should start in =tests/test-build-theme.el= or a sibling converter test. Required tests: + +- role reference resolution +- cycle detection +- layered output loads in Emacs +- flat/layered equivalence for representative fixture faces +- advisory rule returns findings but does not block output +- generated file remains self-contained + +Rollout should keep the current flat output path as the default and add a separate layered target. Manual verification should compare a real Theme Studio JSON through both outputs in a current Emacs session. + +* References / Appendix +- Modus Themes source: [[https://github.com/protesilaos/modus-themes][github.com/protesilaos/modus-themes]] +- Current converter: [[file:../scripts/theme-studio/build-theme.el][scripts/theme-studio/build-theme.el]] +- Current Theme Studio README: [[file:../scripts/theme-studio/README.md][scripts/theme-studio/README.md]] +- Package-face model spec: [[id:8f37a1fd-cfd3-4b25-92e5-772468092bdc][theme-studio-package-faces-spec-doing.org]] + +* Review and iteration history +** 2026-06-14 Sunday @ 14:37:00 -0500 -- Craig -- author +- What: Initial draft for a Modus-inspired layered Theme Studio output architecture. +- Why: Craig asked how palette data, semantic role mappings, face templates, and generated wrappers could work together, including whether the semantic layer can support advisory consistency rules. +- Artifacts: [[file:theme-studio-semantic-theme-architecture-spec.org][this spec]]. |
