aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio
Commit message (Collapse)AuthorAgeFilesLines
* refactor(theme-studio): extract control factories to controls.js, drop dead ↵Craig Jennings19 hours6-234/+221
| | | | | | | | previewFaceAttrs I split the custom dropdown, detail-editor, and expander factories out of app.js into controls.js (205 lines), spliced back at a CONTROLS_J token by generate.py. The token sits at the exact extraction point, so the assembled page is byte-identical and every gate passes unchanged. app.js drops from 927 to 721 lines. I also removed previewFaceAttrs (function, export, and test). It was test-only dead code whose docstring stalely claimed the gate calls it. The gate uses assertPreviewFaces instead.
* refactor(theme-studio): tier-1 simplification passCraig Jennings20 hours8-82/+81
| | | | | | | | These are behavior-preserving cleanups from the refactor/simplify assessment, all test-verified. I merged syncMockHeight and syncPkgHeight into one syncPaneHeight(tableId, paneId), inlined the two single-use displayHex/displayName closures, dropped a pkgbody guard that buildPkgTable already does, and had paintUI call worstCellHtml instead of rebuilding the covered-contrast cell. I deleted the dead generatorHues "manual" branch (a copy of the fallback) and locateInfoLine (orphaned when I removed the preview info line earlier today). The two nerd-icons loaders now share _load_nerd_icons_artifact, with a sentinel so a null-file edge keeps its exact behavior. face_coverage.classify reads through named locals now, guarded by a new characterization test. Two assessment findings were wrong and skipped after I checked them against the code: LOCATE_REG is live (read by previewSpan), and normalizePaletteEntryCore doesn't exist.
* fix(theme-studio): gold nav arrows for the language and preview dropdownsCraig Jennings20 hours2-4/+10
| | | | The gold viewnav style was scoped to .pkgbar, so the arrows on the .langbar selectors (language and preview) fell back to default gray. I broadened the rule to .langbar and added a dimmed-gold disabled state for the single-pane preview.
* style(theme-studio): unify nav dropdowns to gold-on-darkCraig Jennings21 hours3-6/+12
| | | | The view, language, and preview selects share a navsel class matching their flanking arrow buttons (dark bg, gold bold-mono text), so each select and its arrows read as one control.
* feat(theme-studio): visible size-nav buttons + 48 pt gallery scaleCraig Jennings21 hours4-52/+82
| | | | | | | | | | The preview dropdown gets flanking nav buttons, matching the view selector, so the size steps with a click. Left/Right arrows do the same when the dropdown is focused. Both clamp at the ends and disable on a single-pane app. I extended the size scale to 32 and 48 pt for inspecting a glyph's detail. The cell width scales with the size, so beyond about 48 pt the grid is mostly scrolling. I removed the separate hover info line beside the dropdown. Each glyph's own title tooltip already shows its face and color, so the line was redundant. A new computed-style gate confirms the point size renders to the right pixels (24 pt is 32 px), so the pt label isn't lying.
* chore(theme-studio): update WIP theme snapshotCraig Jennings22 hours1-69/+103
|
* feat(theme-studio): nerd-icons gallery as a hue-ordered icon gridCraig Jennings22 hours12-186/+2260
| | | | | | | | | | The nerd-icons pane is now a grid: one row per color face, the rows ordered by hue so families cluster, distinct icons (deduped within a color) drawn in their color with the icon's nerd-font name beneath. A "preview:" dropdown above the grid picks the glyph size in points, with Left/Right arrows to step it. Single-pane apps show it disabled, naming the preview. This replaces the v1 legend in the pane, whose data is still captured for round-trip. build-nerd-icons-legend.el is now a library. A cj/nerd-icons-write-legend entry point requires nerd-icons only at write time, so the capture logic loads and unit-tests without it. It dedupes icons by name within a face, computes each face's native hue, and orders the groups by hue. Writing the test surfaced a latent bug: face-hsl used (cadr (assoc t spec)), which grabs the first keyword instead of the plist. It only worked because the real faces fall through to the face-foreground branch. I fixed it to a correct t-clause parse. Coverage: 7 ERT capture tests (dedupe, hue order, lightness tiebreak, name sort, skip rules), 4 Python validator edges, and browser gates for the grid and the size dropdown. Locate stays color-level: clicking a color flashes its icons, and clicking an icon flashes its color row. Icons aren't individually editable, so there's nothing per-icon to select.
* fix(theme-studio): render nerd-icon glyphs in previews instead of tofuCraig Jennings24 hours7-12/+151
| | | | | | | | The legend, dashboard, and package previews drew nerd-icon glyphs as empty boxes. The font-family never reached them: PREVIEW_FONT was spliced into inline style="..." attributes with a double-quoted family name, so the inner quote closed the attribute early and the font was silently dropped. Dropping the quotes fixes it. A no-space family name needs none. I embedded the glyph font directly: Symbols Nerd Font Mono, encoded with fontTools (woff2_compress output is rejected by headed Chrome and Firefox), inlined as a data: URI under the unique family name ThemeStudioNerd so it resolves to the embed rather than a system-installed copy of the same name. The page is self-contained and renders on any clone. I added a #fonttest gate that parses previewLines output and asserts the resolved font-family plus glyph coverage, plus a make font target that re-encodes the woff2 with fontTools.
* chore(theme-studio): commit WIP theme locked-face reorderingCraig Jennings31 hours1-223/+229
| | | | Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* test(nerd-icons): dir-precedence probe + legend round-trip (phase 4)Craig Jennings32 hours2-0/+18
| | | | | | Lock the dir-precedence decision with an ERT probe: when a dir icon already carries nerd-icons-completion-dir-face, the advice's prepended nerd-icons-yellow is first in the face list and wins. Extend the #nerdiconstest browser gate with an export/import round-trip over an assigned nerd-icons color, asserting it re-imports to the same state and that the separate nerd-icons-completion dir-face stays out of the nerd-icons app. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* feat(theme-studio): bespoke nerd-icons filetype-legend preview (phase 2)Craig Jennings32 hours7-5/+127
| | | | | | Register nerd-icons as a bespoke app whenever its captured legend is valid: the 34 color faces stay editable rows, and the legend rides APPS['nerd-icons'].legend. A new renderNerdIconsPreview draws each curated filetype's glyph in its mapped face's effective color, read through the same registry the other previews use, so recoloring a face repaints every row mapped to it. When the legend is absent the generic inventory app stands in. The #nerdiconstest browser gate covers the wiring, the dir-row owner, and the recolor-repaint. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* feat(theme-studio): capture the nerd-icons filetype legend (phase 1)Craig Jennings32 hours4-0/+266
| | | | | | Add build-nerd-icons-legend.el, which resolves the curated v1 legend rows (glyph + owner color face per filetype) from the live nerd-icons alists and dumps them to nerd-icons-legend.json, a committed artifact like package-inventory.json. generate.py gains load_nerd_icons_legend, which validates the artifact and returns None — with a warning — when it is absent, malformed, empty, or missing a field, so the page can fall back to the generic nerd-icons app rather than error. Data only; the bespoke preview that renders it lands next. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
* chore(theme): update WIP theme capturesCraig Jennings42 hours1-7/+10
|
* feat(theme-studio): locate preview elements by hover and clickCraig Jennings42 hours7-15/+928
| | | | | | | | Hovering a data-face preview element shows its section, face, and effective value in the preview-label info line, and the element's title carries the full record: effective fg/bg plus a per-attribute source note (direct, inherited-from-X, default, or cleared-rendering-as-default). Clicking an on-pane element scrolls to and flashes its assignment row. Off-pane and cross-surface elements stay hover-only. A single owner-qualified registry keyed by {owner, face} backs both data-face surfaces, package and UI, so the same face name under two owners never collides. The pure helpers in app-core.js take all state as arguments and return data. The one stateful adapter, previewSpan, lives in previews.js and emits the escaped markup. os() stays a package-owner wrapper over previewSpan, and a unified locateClick dispatcher replaces the per-surface click branches. Covered by test-locate.mjs and four new browser gates. Full harness green.
* refactor(theme-studio): restore studio state after mutating gates via ↵Craig Jennings5 days2-28/+70
| | | | | | | | | | | | | | | withSavedState Seven gates (locktest, mocktest, gallerytest, safetest, ndtest, viewlocktest, expandpersisttest) mutated PALETTE/MAP/SYNTAX/UIMAP/PKGMAP/LOCKED and restored nothing, so opening the studio at one of those #hashes left its state corrupted for interactive use. Wrap each in withSavedState(keys, body), scoped to the keys that gate touches: snapshot the named globals, run the body, restore in a finally. Uses a JSON-round-trip clone (the studio objects carry values structuredClone throws on — the same clone the gates' own local saves use). The 14 gates that already restore locally are left as-is. Verified: all 44 gates green, the restore round-trip holds for reassignment + in-place + Set mutation, and a forced assertion in a wrapped gate still reports FAIL.
* refactor(theme-studio): hoist the browser-gate boilerplate into a gate() helperCraig Jennings5 days2-228/+180
| | | | | | | | | | | | The 38 standard gates each repeated the same ok flag, notes list, A(cond,note) collector, and verdict postamble (title + result div), with the note separator drifted across them (' | ' vs ' fails='). gate(id, body) owns all of it and standardizes the note format to ' fails=note1,note2'; the gate name is the hash uppercased, the div id is the hash, matching what each gate set by hand. Each call site keeps its literal location.hash==='#NAMEtest' (run-tests.sh greps that to discover gates). Six custom gates (selftest, cursortest, planetest, oklchtest, readouttest, savetest) stay inline. Verified: all 44 gates green, and a forced A(false) in a converted gate still reports FAIL (gate() can't manufacture greens).
* refactor(theme-studio): extract assertPreviewFaces for the 3 preview-face gatesCraig Jennings5 days2-48/+40
| | | | | | | | | | | The #mdtest, #mupreviewtest, and #gnustest browser gates each copy-pasted the same preview-face validation: render the preview, assert it exercises enough data-faces, that every data-face is real for the package, and that the required faces are present. Lift that into one assertPreviewFaces(A, html, faces, min, name, required) helper. Each gate keeps its literal location.hash==='#NAMEtest' check (run-tests.sh greps that to discover gates) and its own title/result-div. Verified: all 44 gates green, and a deliberately-broken required face still makes its gate report FAIL (the helper can't manufacture greens).
* refactor(theme-studio): defer generate.py's page build behind a lazy _build()Craig Jennings5 days1-63/+85
| | | | | | | | | | Importing generate.py ran the full assembly at module load -- DEFAULTS, the package inventory, palette/syntax/uimap construction, and the HTML fill -- so face_coverage.py, which imports it only for UI_FACES, paid the whole cost. Move that derivation into a cached _build(); expose the built attributes (HTML, MAP, APPS, ...) lazily via PEP 562 __getattr__. UI_FACES/CATS/COLS and the file bodies stay cheap module constants, the file write stays __main__-guarded, and the page is byte-identical (check-generated current; test_generate green via __getattr__).
* refactor(theme-studio): dedup the inline-integrity test scaffoldingCraig Jennings5 days5-40/+37
| | | | | | | | | Two test-DRY cleanups. The seven near-identical test_page_carries_*_verbatim methods in test_generate.py collapse into one subTest loop over the inlined-body names. The strip-exports helper -- reimplemented three times across the colormath, app-core, and app-util inline-integrity tests, each annotated 'same strip generate.py applies' -- moves to one shared inline-strip.mjs (stripInlinedBody), so the three copies can no longer drift from generate.py's strip_exports.
* refactor(theme-studio): drop the orphaned dropdownRowTextColor helperCraig Jennings5 days3-41/+2
| | | | | | | | dropdownRowTextColor was exported and unit-tested but had no runtime caller: it computed text color for a vertical-list dropdown row, a UI replaced by the swatch gallery popup (colored buttons, no per-row text), so its regression rationale is moot. Remove the function, its export, and its four tests; regenerate the page (theme-studio.html staged so check-generated stays green).
* refactor(theme-studio): unify the two condition_matches clause checkersCraig Jennings5 days1-32/+30
| | | | | | | | | condition_matches encoded the same four display-condition rules twice -- once for the dict spec shape, once for the list-of-clauses shape. Normalize both to a single {key: values} mapping and run the rules once in _condition_clauses_pass. Verified byte-identical over 31 representative conditions (dict, list, scalar, and malformed). The pre-existing Pyright complaints in choose_gui_light are unrelated and untouched.
* refactor(theme-studio): extract path_kind from the two face-coverage bucketersCraig Jennings5 days1-15/+23
| | | | | | | bucket_from_source and bucket_of_source each re-derived the elpa/user/builtin/ other origin of a defface path. Lift that into one path_kind(path) classifier; both functions now map its result to their own vocabulary. Verified byte-identical bucket output over a representative path set before and after.
* chore(theme-studio): restyle WIP orderless-match facesCraig Jennings5 days1-8/+12
| | | | New foregrounds plus italic slant on orderless-match-face-0..3; source flips default to user.
* chore(theme-studio): save WIP working stateCraig Jennings5 days1-61/+111
|
* feat(theme-studio): move the box column between style and contrastCraig Jennings5 days4-26/+26
| | | | Box now sits at column 5 in all three tables, after style and before contrast, instead of last. The contrast and example/preview columns shift right by one, and the position-based gates follow.
* feat(theme-studio): add 18 language previewsCraig Jennings5 days4-2/+430
| | | | Add tokenized code samples for Racket, Scheme, Haskell, OCaml, Scala, Kotlin, Swift, Lua, Ruby, Perl, R, Erlang, SQL, PHP, Ada, Fortran, MATLAB, and Assembly, wired into the language dropdown. Each is an idiomatic snippet tagged by syntax category so the studio renders it in the assignment colors. A guard test checks every added language is registered and renders a non-trivial sample.
* fix(theme-studio): keep an expander open across a table rebuildCraig Jennings5 days3-10/+64
| | | | A package edit rebuilds the whole table, which collapsed any open expander under the user mid-edit. Track open rows in a module-level EXPANDED set, keyed by element/face, and reopen them on rebuild. Editing a value inside an open expander now leaves the row open. The expand-all/collapse-all and per-row toggles keep the set in sync.
* feat(theme-studio): expand/collapse-all toggle and disclosure trianglesCraig Jennings5 days4-18/+106
| | | | Each row's expander toggle now shows a disclosure triangle that tracks its state: a right triangle when collapsed, a down triangle when expanded (it was a static ellipsis). A header-level expand-all / collapse-all button per table opens or closes every row's detail at once and follows the aggregate state. The per-row triangles and the header button stay in sync across a table rebuild.
* feat(theme-studio): expander label hovers and a view-dropdown lock indicatorCraig Jennings5 days3-8/+108
| | | | Each label in the expander detail row now carries an explanatory hover (DETAIL_HOVERS), matching the table-header labels. The view dropdown prefixes a lock glyph on any view whose elements are all locked, recomputed on every lock change through updateLockToggles.
* feat(theme-studio): move the lock column to the leftmost positionCraig Jennings5 days4-20/+20
| | | | Put the lock cell first in all three assignment tables, ahead of the element/face name. The lock and name columns swap, and every column from fg onward is unchanged. The element-name sort moves to column 1, and the sort and index gates follow it.
* feat(theme-studio): custom weight/slant dropdowns with previewsCraig Jennings5 days4-42/+128
| | | | | | Replace the native weight and slant selects with a custom dropdown themed like the color dropdown. The values are spelled out (semibold instead of "semi", and an unset control reads "weight"/"slant" rather than "wt"/"sl"), and each popup option renders in its own weight or slant so the choice previews itself. The trigger shows the current value in that style too. mkEnumDropdown mirrors the color dropdown's popup, lock, and outside-click handling, so the new control opens, locks, and closes the same way. The style-cluster gates drive the popup instead of a native select and check the spelled-out range plus the per-option preview.
* feat(theme-studio): tighten the elements-table horizontal layoutCraig Jennings5 days4-8/+8
| | | | Reduce the per-cell horizontal padding from 12px to 8px across the assignment tables, so more of each row fits without horizontal scroll on a narrow screen. Also shorten the mode-line-highlight label to "(hover)", since the face name already carries the "mode-line" part.
* feat(theme-studio): sort the language dropdown and add nav arrowsCraig Jennings5 days4-4/+54
| | | | Sort the language list alphabetically and pin Elisp as the default selection. Add the ‹ › arrows flanking the dropdown that step the selection (clamped, no wrap), reusing the view-dropdown's stepViewIndex so you can walk languages without reopening the menu.
* feat(theme-studio): unify per-row widgets across the assignment tablesCraig Jennings5 days7-63/+162
| | | | | | | | | | | | The color/code, UI, and package tables now share one per-row widget set, so the editing surface reads the same whatever view is selected. Column order is the same in all three: element, lock, fg, bg, style, contrast, example, box. Box moves to last in the color/code table, and the package table's inline inherit and size columns fold into the row expander, matching how UI and color/code already carry them. The UI overlay faces drop the inline PASS/FAIL word and the red FAIL badge on the preview swatch. They show a bare colored worst-case number with the WCAG verdict in the hover, like the other two tables. The orphaned .crerr style goes with it. The height picker now clamps a typed value into [0.1, 2.0]. A number input only enforces min/max on its stepper arrows, so a typed or pasted value reached the model unchecked. 0.1 is Emacs's own floor (a smaller height errors out), and 2.0 is the studio ceiling. Clearing the field still unsets to the inherited default. Tests: clampHeight unit tests plus a #heighttest browser gate. The column and contrast gates move to the new positions and the bare-number readout.
* chore(theme-studio): save WIP working stateCraig Jennings5 days1-0/+106
|
* feat(theme-studio): add ansi-color faces to drive all ANSI consumersCraig Jennings5 days2-1/+17
| | | | | | theme-studio could theme ghostel-color-* but not the base ansi-color-* faces. build-inventory.el skips built-in faces, and ansi-color is part of Emacs core, so the 16 base ANSI faces never reached the dropdown. That left vterm, eshell, and compilation buffers on stock ANSI colors no matter the theme. I added ansi-color as a bespoke app (the same path shr already uses for a built-in library), with the 16 palette faces seeded to match the ghostel colors. Theming ansi-color-* now sets the 16 colors for every ANSI consumer at once. ghostel-color-* inherit these, so clearing a ghostel-color face lets it follow ansi-color rather than holding its own value.
* feat(theme-studio): export through the save-file picker to overwrite in placeCraig Jennings5 days3-2/+56
| | | | | | Re-exporting a theme used to land a "name (1).json" duplicate. The export built a blob and clicked a download link, which routes through the browser's downloads folder, and the browser uniquifies a re-save rather than replacing the file. I switched export to the File System Access API (showSaveFilePicker): it writes straight to the file you pick, so re-exporting the same WIP.json overwrites it. Where the API is absent the old blob download still runs, mirroring importTheme's picker-with-fallback shape. A new #savetest browser gate stubs the picker and checks the written content and the close.
* chore(theme-studio): save WIP.json working stateCraig Jennings5 days1-13/+45
|
* test(theme-studio): cover the face-docs dump helper and syntax mapCraig Jennings5 days2-7/+87
| | | | I added ERT tests for face-docs-dump.el: the pure first-line extractor across Normal/Boundary/Error inputs (multi-line, leading blanks, whitespace collapse, empty, whitespace-only, nil/non-string), and the syntax-category resolution (kw to the keyword-face doc, bg and p to default, the faceless dec absent). run-tests.sh loads the new file alongside the build-theme tests in the same batch.
* feat(theme-studio): show face docstrings in element hoversCraig Jennings5 days9-9/+183
| | | | | | 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.
* chore(theme-studio): save WIP.json working stateCraig Jennings6 days1-472/+472
|
* test(theme-studio): cover defensive branches and the palette generatorCraig Jennings6 days3-1/+140
| | | | Added the uncovered fallback branches in app-core (migrateLegacyFace null input, normalizePkgFace's source fallback chain, mergePackagesInto's null/new-app guards, boxCss shading a relief from the bg when no box color is set) and in colormath (apca's equal-luminance return-0 and low-contrast clamp, isPureEndpointHex). New test-palette-generator-core.mjs drives planPaletteGenerator across every scheme, vibe, source mode, and the fill-gaps intents, since those internals are only reachable through the public planner. colormath branch 96 -> 99%, palette-generator-core funcs 97 -> 100%, node suite 237 tests. The remaining gaps are the deep palette-column edge branches, deferred as diminishing returns on already line-covered code.
* test(theme-studio): cover the promoted faceCss helpersCraig Jennings6 days1-0/+89
| | | | cssWeight, faceDecoration, boxCss, and faceCss moved into app-core in the CSS-builder refactor but had no node tests, leaving app-core at 96% line / 95% funcs. Added Normal/Boundary/Error cases for all four: every weight name plus the fallbacks, underline/strike/both/neither decoration, line/released/pressed boxes with and without color and width plus the no-color fallback, and faceCss's background/noBg/fontSize/box-order assembly. app-core is now 100% line; the node suite is 217 tests.
* refactor(theme-studio): share the spec-from-entries loop in build-themeCraig Jennings6 days1-15/+14
| | | | The UI and package tier builders repeated the same "for each (face . obj) entry, build attrs, emit a non-empty spec" loop. Both now call one build-theme/--specs-from-entries helper; the package builder concatenates each app's specs in order. The syntax builder keeps its own form since it fans one category out to several font-lock faces. The 41 ERT tests stay green and the emitted themes are unchanged.
* refactor(theme-studio): dedup the palette harvest and color-pair walksCraig Jennings6 days2-29/+25
| | | | | | generate.py's add_default_palette_colors repeated the same fg/bg/box-color harvest three times (syntax, ui, package faces); it now calls one _harvest_spec_colors helper, preserving the add order so the palette and generated page are byte-identical. default_faces' _build_color_hex and _build_color_names each walked the same faces -> chosen/effective -> foreground/background/distantForeground nesting; both now consume one _iter_color_pairs generator and only differ in their key choice and filter. The rebuilt color maps match the originals exactly. I left the lower-value generate.py items (a build() wrapper, dict-driven fill_data) and the capture/face-coverage script dedups for later: they touch import-time behavior or scripts the suite doesn't run, so they want their own verification rather than riding this change.
* refactor(theme-studio): single-source the bespoke-app list in face_dataCraig Jennings6 days3-54/+40
| | | | The bespoke apps were listed twice: generate.py's _BESPOKE_APPS (key, label, preview, faces, prefix, seed) and app_inventory's BESPOKE_APPS set of keys, which had drifted (it carried both "org" and "org-mode"). The spec list now lives in face_data.py as BESPOKE_APP_SPECS, beside the FACES/SEED constants it references. generate.py builds APPS from it, and app_inventory derives its exclusion set from the same keys plus an explicit "org" alias of the "org-mode" app. Adding a bespoke app is now one row. APPS order and the generated page are unchanged.
* chore(theme-studio): regenerate page after the face-spec refactorCraig Jennings6 days1-1/+1
| | | | The single-spec seed now emits package-face keys in the spec's order rather than the old hand-written order, so the embedded package JSON reorders some keys. Values are identical (the APPS data parses equal; the page normalizes face key order on import), and check-generated requires the committed page to match the generator output.
* refactor(theme-studio): derive the Python face model from one spec listCraig Jennings6 days3-76/+79
| | | | STYLE_DEFAULTS, default_faces.seed, and capture-default-faces' ATTRS each hand-listed the face attributes, so adding one meant editing three places in step. face_specs.FACE_ATTRS is now one row per attribute carrying the model key, default, capture keyword, snapshot field, and seed transform kind. STYLE_DEFAULTS derives from it (same keys, order, and values), seed iterates it applying the per-kind extraction, and the capture probe map derives its emacs-attr to snapshot-field pairs from it. The snapshot-to-model translation stays genuinely per-attribute (colors prefer the Hex variant, flags become objects, weight/slant are value-narrowed), but the attribute list itself is now single-sourced. Verified byte-identical: STYLE_DEFAULTS and ATTRS match the old literals, the generated page is unchanged, and the suite is green. app-core.js mirrors this list by hand since the page is a separate runtime.
* refactor(theme-studio): table-drive vibe chroma and share the inherit walkCraig Jennings6 days3-56/+36
| | | | | | 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): fold the box control into mkLineStyleControlCraig Jennings6 days2-34/+20
| | | | mkBoxControl duplicated the whole cluster/dropdown/paint/syncLocked machinery that mkLineStyleControl already provides for underline/strike/overline; it differed only in that its state object carries a width. I gave mkLineStyleControl an optional toState builder and reduced mkBoxControl to a wrapper that supplies it. The clear/reset tier functions already delegate their shared loop to clearUnlockedRows, so they were left as is. The buildTable/buildUITable/buildPkgTable skeleton merge is deferred: their shared control/expander/detail-editor/CSS machinery is now all extracted, and what remains is genuine per-tier column divergence that the browser gates pin by exact cell position.