aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/app-core.js
Commit message (Collapse)AuthorAgeFilesLines
* feat(theme-studio): show face docstrings in element hoversCraig Jennings12 hours1-1/+12
| | | | | | Each table row's category cell now shows the face's Emacs docstring on hover, on top of whatever the cell showed before. The package cell keeps the face name underneath. The syntax and UI cells had no prior tooltip, so they show just the docstring. The label-span hints are left alone. I added face-docs-dump.el, which emits face-docs.json from a live Emacs: a face-name to first-doc-line map for the UI and package tables, and a category to doc map for the syntax table. The category to font-lock-face mapping is read from build-theme.el's own map, so it isn't copied a third time. generate.py inlines both maps. A pure composeHoverTitle helper composes the tooltip, covered by Node, Python, and a new browser gate.
* refactor(theme-studio): table-drive vibe chroma and share the inherit walkCraig Jennings28 hours1-18/+10
| | | | | | vibeChroma was a ten-branch if-ladder of magic chroma constants; it's now a [base, range] lookup table, so a vibe is one row to read or tune. resolveSyntaxFg and resolveUiAttr each hand-rolled the same cycle-guarded inherit walk; both now call one walkInheritChain helper that takes the parent and value functions. effResolve keeps its own recursive form since it double-indexes through the package map. I left the palette-actions splice helpers (replacePaletteEntries, withCfg) and the paletteGroups dedup for a later pass: they mutate the live palette and are only browser-gate covered, so they want their own careful change rather than riding this one.
* refactor(theme-studio): unify the face CSS builders in app-coreCraig Jennings28 hours1-2/+38
| | | | syntaxStyle, uiCss, and ofs each assembled the same color/background/weight/style/text-decoration/box-shadow string by hand, differing only in how they resolved fg/bg and whether they added a font-size. I promoted one faceCss(face, fg, bg, opts) plus cssWeight, boxCss, and a faceDecoration helper into app-core (all pure, no DOM), and reduced the three builders to thin wrappers that resolve fg/bg and call it. styleEx and paintUI now use the promoted cssWeight/boxCss too. udeco keeps its own untrimmed decoration form, so it stays in app.js.
* refactor(theme-studio): share oklchOf and isPureEndpointHex from colormathCraig Jennings28 hours1-3/+1
| | | | oklchOf and isPureEndpointHex were each defined identically in app-core.js and palette-generator-core.js, and hueOfHex inlined oklchOf's body a third time. I moved both helpers into colormath.js, which already owns the primitives they call, and had the two consumers import them. hueOfHex now calls oklchOf instead of re-deriving it.
* refactor(theme-studio): drive the package face model from one attribute tableCraig Jennings29 hours1-2/+37
| | | | | | I replaced the two hand-kept attribute lists in normalizePkgFace and packagesForExport with a single faceAttrs() table. Each row carries the default, whether the value resolves through the palette, and the export rule, so adding a face attribute is one row instead of an edit in two places kept in step by hand. faceAttrs is a hoisted function, not a const. The inlined page calls normalizePkgFace at top level (seedPkgmap) before the table's source position, where a const would sit in its temporal dead zone.
* refactor(theme-studio): polish the expander (underline inside, dynamic ↵Craig Jennings37 hours1-2/+20
| | | | | | | | | | | | | | colspan, nd flag) Three cleanups to the per-row expander from 3B-2. The underline control moves from the in-row style cell into the expander, next to overline. The row keeps weight, slant, and strike inline, so the style cell drops from three wrapped rows to two and the table reads flatter. mkExpander no longer hardcodes each table's colspan. tableColCount reads the column count from the table's header, so a detail row spans correctly even if a column is added later. A collapsed expander now flags itself when it hides an attribute that differs from the face's default, so a non-default value is never invisible. overflowNonDefault (app-core.js, unit-tested) compares the expander's attributes against the default. The toggle re-checks after every edit and gets the gold marker when any differ. faceBoxNonDefaults drops underline from the in-row style box in the same move, since underline is now the expander's concern. The #expandtest gate covers the underline control in its new home, its wavy write, and the flag appearing then clearing. Full suite green: Python 59, Node 201, ERT 41, plus the browser hash gates.
* feat(theme-studio): replace the style toggles with ↵Craig Jennings40 hours1-17/+1
| | | | | | | | | | | | weight/slant/underline/strike controls The B/I/U/S toggle buttons in the syntax, UI, and package tables become a weight selector (light/normal/medium/semibold/bold/heavy), a slant selector (normal/italic/oblique), and box-like underline and strike controls. The underline control sets line or wave plus a color, and the strike control sets a color. A face can now reach the full weight range and a wavy or colored underline, not just bold and italic on-off. All four controls come from one mkStyleControls helper shared across the three tables, and underline and strike share mkLineStyleControl (the box-control pattern, parameterized for a styled line vs a plain toggle). With the real controls in place I dropped the transitional legacyStyleOn/toggleLegacyStyle shim and its tests. The overflow attributes (distant-fg, family, overline, inverse, extend, and inherit/height for ui and syntax) move into a per-row expander next. Verified by screenshot and the browser style gate, which now drives a weight-select change and an underline-wave click through the model. Full suite green: Python 59, Node 198, ERT 41, plus the browser hash gates.
* refactor(theme-studio): cut the face model over to weight/slant/objectsCraig Jennings40 hours1-5/+36
| | | | | | | | | | I replaced the legacy bold/italic/underline/strike booleans with the final model shape across both sides of the tool. weight (light/normal/medium/semibold/bold/heavy) and slant (normal/italic/oblique) replace the bold/italic flags, underline becomes {style: line|wave, color}, strike becomes {color}, and null means unset. A single migration converts a legacy face on the way in, mirrored as migrateLegacyFace in app-core.js and migrate_legacy in face_specs.py so the JS and Python models can't drift. It runs on import (applyImported, mergePackagesInto) and on every seed that face_spec touches. The captured-snapshot seed (default_faces.seed) narrows the same way it did before. Only bold and italic survive, as weight "bold" and slant "italic", so the generated themes stay byte-identical. The B/I/U/S toggle buttons keep working through a transitional bridge (legacyStyleOn / toggleLegacyStyle). The weight/slant dropdowns and underline/strike controls that replace them land next. The live previews read the new shape, with a weight name mapped to a numeric CSS font-weight. The cutover is proven emit-neutral two ways. An ERT test asserts the migrated shapes emit the same attributes as the legacy booleans, and deep-migrating every face in dupre, distinguished, sterling, now, theme, and WIP then running build-theme yields byte-identical output. Full suite green: Python 59, Node 200, ERT 41, plus the browser hash gates.
* feat(theme-studio): widen the face model with the additive attributesCraig Jennings40 hours1-2/+2
| | | | | | | | | | This is Phase 2 of the face-attribute expansion. The model now carries distant-fg, family, overline, inverse, and extend in final shape across all three tiers, and inherit and height are no longer package-only (a ui or syntax face can set them too). I kept bold/italic/underline/strike as the legacy booleans for now. The cutover to weight/slant and the underline/strike object forms lands in the next phase with the editor widgets that force it, so the representation and the controls that drive it move together. face_specs.py holds the canonical defaults. In app-core.js, normalizePkgFace and packagesForExport carry and emit the new attrs: distant-fg resolves through the palette like fg/bg, and each attr exports only when set, so existing presets re-export unchanged. app.js syntaxBlank, uiFaceBlank, and seedFace match the shape. Nothing changed shape, so dupre, distinguished, sterling, now, theme, and WIP all emit byte-identical themes. make check green: Python 58, Node 193, ERT 40.
* feat(theme-studio): prev/next arrows to step the view dropdownCraig Jennings4 days1-1/+8
| | | | I added left and right arrow buttons flanking the view dropdown. They step the selection to the previous or next item and re-render the faces table and preview, so you can walk the list without reopening the dropdown. A pure stepViewIndex helper clamps the index to the option range, no wrap. stepView sets the selection and calls onViewChange.
* feat(theme-studio): mark per-face setting boxes that differ from defaultCraig Jennings4 days1-1/+20
| | | | A non-default height looks identical to the default in the size input, so a stray 1.1 hides in plain sight. I added a small gold corner flag on any per-face setting cell (fg, bg, style, inherit, size, box) whose value differs from the face's seed default. A pure faceBoxNonDefaults helper computes the per-box flags. buildPkgTable resolves fg/bg to hex before comparing, so a palette-name-vs-hex difference doesn't read as a change.
* feat(theme-studio): alphabetize packages in the assignment dropdownCraig Jennings4 days1-1/+11
| | | | The assignment-view dropdown listed package faces in APPS build order (bespoke apps first, then inventory). generate.py builds them that way, so the list wasn't alphabetical. I added a pure appViewKeysSorted helper that orders the app keys by display label, and buildViewSel uses it. The @code and @ui editor entries above the divider are unchanged.
* feat(theme-studio): 2D gallery color picker for the assignment dropdownsCraig Jennings5 days1-1/+24
| | | | | | | | | | - The color dropdown opens a grid, not a long list. - The grid mirrors the palette: ground strip, then a row per family. - Members run dark to light, with the current color outlined. - A default chip clears the assignment. - A (gone) cell shows a color no longer in the palette. - The trigger and step buttons stay the same. - All three tiers share the one dropdown.
* feat(theme-studio): show view-area > element usages on palette tile hoverCraig Jennings5 days1-1/+18
| | | | I added paletteUsages, which enumerates every place a color is assigned, grouped by view area (the view dropdown's names: color/code assignments, ui faces, each package app) and the element within it. renderPalette builds the per-area scopes once and appends the list to each used tile's hover title, under the existing name/hex/nearest-deltaE line. Node tests and a #usagetest gate cover it.
* feat(theme-studio): flag unused palette tiles and columnsCraig Jennings5 days1-1/+16
| | | | | | I added usedPaletteHexes, a reverse lookup over the syntax, ui, and package assignments (plus the ground endpoints) that resolves each reference to a hex. renderPalette outlines a tile whose color is referenced nowhere and outlines a whole column when none of its colors are used, so dead colors stand out for pruning before a theme ships. The check is biased safe: an unresolvable reference marks nothing, so a color that is actually used is never flagged. Node tests cover the lookup. A #unusedtest gate covers the tile and column flags.
* fix(theme-studio): clamp generated palette spans to the bg/fg boundsCraig Jennings5 days1-3/+13
| | | | | | Spanning a color generated steps toward pure black and white, so a column could produce colors darker than bg or lighter than fg. I changed regenColumn to ramp the dark side toward the darker ground endpoint and the light side toward the lighter one, bounded by bg and fg. Pure black/white duplicates are still skipped, and callers that pass no ground fall back to the old black/white ramp. Node tests cover the bounded span and the no-ground fallback. The #counttest gate asserts the regenerated column stays within the bg/fg bounds.
* chore(theme-studio): remove dead code and clear a type warningCraig Jennings6 days1-37/+1
| | | | | | | | | - ramp (app-core.js) and its test-ramp.mjs: superseded by regenColumn, no production caller. - optList (app-core.js) and its tests: superseded by paletteOptionList. - ITALIC in generate.py: computed, never read (ITALIC_MAP is the live one). - a stray empty string in MU4E_FACES that .split() silently dropped. - the dead #familytest alias in the columntest gate, which HASHES never listed. - widen face_rows to Sequence[str], clearing the list-invariance warnings on the APPS calls.
* fix(theme-studio): keep dropdown color names legibleCraig Jennings6 days1-1/+12
| | | | | | The color-picker popup colored each row's name and hex for contrast against the swatch, but the rows sit on the popup's fixed dark background. A mid or dark swatch (the blues past blue-1) got near-black text that vanished on the dark popup. The text now inherits the popup foreground for every real palette color. Only the solid "default" row, whose background is the color itself, still contrasts against its own fill. I moved the decision into dropdownRowTextColor with unit coverage, including a dark-swatch regression case.
* feat(theme-studio): palette generator and preview fidelityCraig Jennings6 days1-4/+49
| | | | | | | | | | Two strands land together because the generated theme-studio.html bundles every source file into one page and can't be split cleanly. The palette generator is a preview-first panel: palette-generator-core.js plans the palette and palette-generator-ui.js draws it. Generated colors stay inspectable and tunable through the existing selector, and committing one creates a normal base column. It adds source-mode and scheme controls, a configurable accent count, and color names from color-names.json. For preview fidelity, syntax and UI colors now resolve through the real Emacs inherit chains, so the preview matches how Emacs renders the theme. resolveSyntaxFg pins dec to ty (Emacs has no decorator face) and otherwise follows comment-delimiter to comment, doc to string, property to variable, function-call to function-name. resolveUiAttr walks mode-line-inactive to mode-line and line-number-current-line to line-number. The decorator label now reads "decorator to type" to match the type face Emacs uses for it. Design recorded in the two theme-studio specs under docs/.
* Update theme studio palette workflowCraig Jennings7 days1-2/+2
|
* Add theme studio face color step arrowsCraig Jennings7 days1-1/+19
|
* Change theme studio spans to endpoint divisionsCraig Jennings7 days1-8/+19
|
* Fix theme studio span endpoint tilesCraig Jennings7 days1-4/+5
|
* Sort theme studio dropdown colors by lightnessCraig Jennings7 days1-4/+6
|
* Add theme studio column deleteCraig Jennings7 days1-1/+10
|
* Refactor theme studio palette testsCraig Jennings7 days1-1/+41
|
* Fix theme studio bg-prefixed span inferenceCraig Jennings7 days1-3/+6
|
* Fix theme studio bg-like imported colorsCraig Jennings7 days1-8/+23
|
* Group numeric color names by stemCraig Jennings7 days1-1/+1
|
* Treat legacy color names as base columnsCraig Jennings7 days1-1/+1
|
* Refactor theme studio face assemblyCraig Jennings7 days1-3/+9
|
* Rename theme studio color model to columnsCraig Jennings7 days1-18/+18
|
* Update theme studio color columns and defaultsCraig Jennings7 days1-80/+45
|
* feat(theme-studio): group families by lightness-conditioned complete linkageCraig Jennings10 days1-15/+42
| | | | | | | | | | Replace the hue-anchor bucketing and the tent neutral threshold with the model two independent reviews of color-sorting.org converged on (Codex and Fable, with Fable's harness measuring pairwise F1 0.63 → 0.96 on the real palette). Chromatic colors now cluster by complete-linkage agglomeration on a lightness-conditioned hue distance: hue must match tightly at equal lightness and may drift across a lightness gap, because a tonal ramp drifts in hue with lightness by design. A low-chroma noise term widens the tolerance where hue is ill-defined, and a chroma clause keeps a vivid accent out of a soft same-hue family. Complete linkage makes single-linkage chaining structurally impossible. The neutral threshold is floored at both ends instead of tapering to zero, which fixes two real defects: pale warm grays (gray+1, gray+2) that leaked into a color column, and pure white (C=0 at L=1) that evaded a zero threshold. On the sterling/distinguished palette this separates the gold and olive ramps (the green/yellow complaint), keeps the red and blue ramps whole including drifted tints, isolates intense-red, and consolidates every gray and steel into the neutral column. The one residual — pale yellow+2 lands on the olive ramp — is geometrically irreducible from the hex (it sits on the olive trajectory by nearest-neighbor, ramp-line fit, and eye); only its name says gold. That needs the deferred per-hex family-hint override. New node tests cover the gold/olive split, blue pale-tint cohesion, gray/white neutrality, intense-red isolation, and palette-order independence. The count gate now asserts the count action adds all ramp colors to the palette rather than that they all display in one family, since a chroma-eased extreme can sit at the neutral boundary.
* feat(theme-studio): add the live per-family count controlCraig Jennings11 days1-3/+3
| | | | | | | | Each chromatic family column gets a count input (0-4) showing its current per-side reach. Setting N regenerates the family as a symmetric base ±N ramp from its most-saturated color, replacing the family's current members. A reference to a surviving step (matched by signed lightness rank) follows the new hex through repointHex; a reference to a step removed by lowering N is left on its old hex, which is no longer in the palette and renders as "(gone)" — never silently reassigned. The neutral and ground strips get no control. I also fixed the neutral threshold curve: it was flat-high through the darks, which pulled a chroma-eased dark ramp step (a dark desaturated blue) into the neutral column and broke the family. The curve now tapers toward both lightness extremes, peaking near mid, so dark and light tints both keep their hue while mid grays stay neutral. This is the symmetric form of the Munsell scaling and a strict improvement. Phase 4 of the color-families spec. A #counttest gate covers count-up adding symmetric steps, count-down dropping the extremes, the surviving-step repoint, and the removed-step "(gone)".
* feat(theme-studio): group families by hue anchor with a lightness-scaled ↵Craig Jennings11 days1-21/+20
| | | | | | | | | | neutral cut Replace gap-based hue clustering and the flat neutral threshold. Chromatic colors now bucket by nearest perceptual hue anchor (red, orange, yellow, green, teal, blue, purple, pink), so adjacent categories stay separate by construction and there's no single-linkage chaining merging them through intermediate tones. The neutral cut is lightness-scaled rather than flat: a color reads as neutral below a chroma that's highest in the mid-tones and tapers toward the light end, so a faint mid gray goes neutral while an equally-faint pale tint keeps its hue. This fixes the two concrete problems: the grays and steels consolidate into one neutral column, and pale tints (light blues) stay with their hue instead of falling into the grays. What it doesn't fix is hue-adjacent warm colors: this palette's olive-greens sit on top of the golds in OKLCH hue, so they still group together, and a ramp that drifts in hue can split across an anchor boundary. That's a real property of the colors, not a bug, and it's filed for research (a writeup of the problem and the four approaches tried lives outside the repo; the task points to it). 20 family node tests including the yellow/green split and the no-chaining case; suite green.
* feat(theme-studio): add color-family sortCraig Jennings11 days1-1/+19
| | | | | | sortFamilies orders the strips for display: neutrals first by lightness, then chromatic families by base hue, ties broken by base lightness then base hex. Each family's members come back sorted dark to light. Hue is compared rounded so a sub-degree hue hair from gamut quantization doesn't outrank lightness. Sorting is display-only; the stored palette order is untouched. Phase 2 of the color-families spec, pure logic. Four node tests cover the hue order, the neutral pin, within-family lightness order, and the (hue, then lightness) ordering invariant. Suite 91 to 95 green.
* feat(theme-studio): add the color-families model coreCraig Jennings11 days1-1/+88
| | | | | | Four pure functions in app-core.js, all derived from the hex so renaming never moves a color. familiesFromPalette groups a flat palette into the ground strip (the bg/fg assignment hexes, pinned, de-duped) plus hue families: near-neutrals split off by a chroma threshold, the rest cluster by hue proximity with a 25-degree gap and a 360 wrap, each family's base its most-saturated member. regenFamily returns a family's symmetric ramp around the base (n=0 is the base alone, handled without ramp()'s 1-4 clamp). rankByLightness gives each current member a signed offset from the base, and stepRepointPlan maps old positions to new ones across a regenerate, listing the positions that drop out so the caller can leave their references a visible "(gone)". Phase 1 of the color-families spec, pure logic, no UI. 13 node tests cover the gap split/merge, neutrals, absent and de-duped ground hexes, n=0, lightness ranking, and the survivor/removed repoint split. Suite 78 to 91 green.
* feat(theme-studio): add the background-contrast safety coreCraig Jennings11 days1-2/+56
| | | | | | | | A background overlay sits behind many foregrounds at once, so its real constraint is the worst-case contrast over the whole set, not the single fg/bg pair the contrast cell shows today. Phase 3 adds three pure functions in app-core.js for that. fgSetFor(face, state) builds a covered face's foreground set: the distinct syntax-token colors plus the default foreground, each labeled by syntax role. It returns a structured reason ('out-of-scope' or 'empty') rather than a bogus set when the face isn't covered or has no syntax assignments. floor(bgHex, fgSet) returns the minimum WCAG contrast over that set with the limiting foreground's hex and label. lMax(hue, chroma, fgSet, target) finds the lightest background that still clears the target, scanning L up from black to bracket the dark-side crossing then binary-searching it, and reports status ok/none/all/clamp. state is passed explicitly (covered set, syntax assignments, default fg) so the functions read no globals and the Node tests stay direct. The closed five-face covered set lives here as COVERED_FACES, shared with app.js. Tests include the sterling keyword-blue worst case as a fixture, plus lMax's none/all/clamp branches.
* feat(theme-studio): add the ramp generator coreCraig Jennings11 days1-1/+39
| | | | | | | | ramp(baseHex, {n, stepL, chromaEase}) in app-core.js turns one base color into a tonal ramp: 2n steps at offsets -n..-1 and +1..+n, ordered darkest to lightest, base excluded. It holds the OKLCH hue, steps lightness by stepL, eases chroma toward the extremes so only the farthest step loses most of its color, and gamut-clamps each step with its own clamped flag. Bad input returns a structured result rather than throwing: an unparseable base gives {steps: [], error: 'bad-hex'}, and out-of-range n/stepL/chromaEase clamp into range with the clamped knob named in adjusted. Defaults are n=2, stepL=0.08, chromaEase=0.5. This is Phase 1 of the palette-ramps spec: pure logic, no UI. Tests cover mid/near-white/near-black bases, hue-hold, chroma easing, knob clamping, and malformed hex. The integrity stripper for app-core.js now drops import lines too, since the core imports normHex and the colormath helpers for the Node tests (stripped on inline, where both are already in scope).
* feat(theme-studio): add a real, exported :box face attributeCraig Jennings11 days1-3/+3
| | | | | | | | The mode-line box in the preview was hardcoded — it showed a box the generated theme couldn't actually produce, since build-theme.el never emitted :box. Made :box a real face attribute instead: a per-face box object (style line/raised/pressed, width, color) stored on UI and package faces, set from a "box" dropdown in both tables, rendered from the attribute everywhere (the mode-line bars, the package previews via ofs, the UI table preview cells), and exported through build-theme.el's --attrs as a proper :box plist (released/pressed → :style *-button; line → :line-width + optional :color). The hardcoded box is gone; mode-line and mode-line-inactive now default to the released-button box that is the Emacs default, so the preview and the export agree. This also gives the package faces that genuinely use :box a way to represent it — the face audit found several (magit-branch-current/-remote-head, two flycheck list faces, the telega button family, ~15 slack button/dialog faces). Tests: build-theme gains box-conversion + ui-box-emit ERT tests (24/24); the app-core deep-equal tests account for the new box slot; all 9 browser gates, 20 python, and 55 node tests stay green.
* test(theme-studio): extract color/slug helpers to importable modules and ↵Craig Jennings11 days1-1/+5
| | | | | | | | | | cover them The pure helpers that were still stranded in app.js — normHex, ratingColor, textOn, and the filename-slug logic — had no unit tests because app.js can't be imported (it runs its bootstrap and references the data placeholders at load). Moved them into importable modules so they can be tested directly: a new app-util.js holds the color/UI-boundary trio, and slugify joins app-core.js. app.js keeps thin wrappers, so no call site changed and the built DOM is byte-identical. textOn needs rl from colormath, so generate.py's inline strip now drops import lines as well as export lines — app-util.js imports rl for its tests, and the import is stripped on inline where rl is already in the page. _faces in generate.py also gets direct tests for its prefix-strip and label derivation. New: 12 node tests (normHex, ratingColor, textOn, slugify) and 7 python tests (_faces, app-util integrity, the import strip). Coverage: app-util.js 100/100/100, app-core.js 100/94.9/100, colormath.js 100/96/100 (line/branch/func); generate.py 89% lines (the rest is the __main__ writer and the optional seed-env branch). No bugs surfaced — the logic was correct, just untested.
* test(theme-studio): extract app-core.js and unit-test the app logicCraig Jennings11 days1-0/+28
The refactor's goal was to make the app logic testable; this realizes it. Pulled the pure package-face model and the dropdown option list into app-core.js — nameToHex, buildPkgmap, packagesForExport, mergePackagesInto, effResolve (the inherit-chain resolver behind pkgEffFg/pkgEffBg), and optList — with every dependency passed as a parameter so there is no DOM and no module-global reliance. generate.py inlines it into the page the same way it inlines colormath.js (strip exports, placeholder, integrity check), so the browser runs the same code the tests import. app.js keeps thin wrappers (pname, seedPkgmap, ddList, pkgEffFg, pkgEffBg) that pass the live PALETTE / APPS / PKGMAP into the core, so no call site changed and the built DOM is byte-identical to before. test-app-core.mjs adds 18 Normal/Boundary/Error tests over the extracted logic — name resolution, the seed/export/merge round trip, the inherit chain including a cycle that must terminate at null, and the "(gone)" dropdown entry — plus an inline-integrity check that the page carries the core verbatim. The node suite goes 25 to 43 tests; python templating gains the app-core integrity assertion.