aboutsummaryrefslogtreecommitdiff
path: root/scripts/theme-studio/theme-studio.html
Commit message (Collapse)AuthorAgeFilesLines
* feat(theme-studio): previews render the chosen face heightCraig Jennings35 hours1-4/+13
| | | | | | heightCssValue maps a face's height to CSS from its stored kind: a relative multiplier renders as em, an absolute 1/10pt value as true pt, with legacy objects falling back to integer/fractional inference. uiCss feeds it to the mock editor, so the mode-line, mode-line-inactive, and line-number bars visibly thicken with an absolute height while the buffer text stays put; paintUI scales the UI row's sample text; the package-preview span builder swaps its em-only sizing for the same kind-aware value. faceCss now accepts a unit-carrying string for fontSize alongside the existing em number.
* feat(theme-studio): inline height control on the exposed face rowsCraig Jennings35 hours1-36/+174
| | | | | | | | A new size column in the UI and package tables carries one numeric field plus an abs/rel toggle, exposed per the editable-height spec: chrome faces (mode-line family, line-number family, and header-line/tab-bar/tab-line when they arrive) default to absolute 1/10pt entry with a computed pt hint; the seeded heading faces (org-level-*, document title/info, agenda structure/dates, shr headings, and friends) default to a relative multiplier. Any other face carrying a live height exposes the control dynamically; the long tail gets none. Absolute entry takes a positive integer only; relative entry clamps into the 0.1-2.0 range the old field used; garbage never reaches the model. The toggle writes heightMode explicitly and clears the number on a flip, since 130 tenth-points and 1.3x mean different things. The kind-unaware height field in the row expander is retired, and a non-default height now marks the size cell instead of the expander toggle. The seeded set is named statically in app-core.js because the per-row default comes from the captured Emacs snapshot, which carries no heights for those faces. The #preview screenshot hash now accepts @ui/@code view keys so the harness can shoot the UI table.
* feat(theme-studio): explicit absolute-vs-relative face height kindCraig Jennings36 hours1-8/+18
| | | | | | JSON collapses 2.0 to 2 on save, so a height's number type can't say whether it's a fixed 1/10pt value or a relative multiplier. The face model now carries an explicit heightMode field (abs/rel) through seed, save/load, and export. build-theme.el coerces :height from the kind: abs exports an integer, rel a float, so a relative 2.0 renders as 2.0, never 2. Faces saved before the field existed infer the kind once on load (JS: integer to abs, fractional to rel; Python keeps the authored type, so a float 2.0 seed stays relative) and persist it on the next save. The mode-line seed carries abs explicitly, and WIP.json's eight seeded heights are stamped with their kinds. Regenerating the theme from the stamped WIP.json produces an identical WIP-theme.el, so the round-trip holds.
* fix(theme-studio): pin mode-line at an absolute heightCraig Jennings37 hours1-1/+1
| | | | | | | | | | | mode-line's :height was unspecified, so a buffer that remaps its default face larger (the nov reading view) inflated its modeline with it. Seed an absolute 130 (1/10pt) on mode-line — build_uimap gains apply_modeline_height_default, mirroring the hover-box default — and set it in WIP.json. Also drop the stray :height 2 from mode-line-inactive (a JSON integral-float collapse that rendered inactive bars at 0.2pt); inactive now inherits mode-line's height. Theme regenerated and loaded live; the editable-height spec covers making this tunable in the studio.
* feat(theme-studio): wire the ansi-color scene, closing its dangling preview keyCraig Jennings45 hours1-0/+30
| | | | ansi-color carried a bespoke spec and a full seed palette but its renderer was never written, so it silently fell back to the generic face list. Added renderAnsiColorPreview — a compilation/shell buffer exercising all 16 SGR palette faces, the faces eat and vterm color faces inherit from — and registered it. Last generic app in the ecosystem-coverage scope.
* feat(theme-studio): scenes for pinned ghostel and all-the-iconsCraig Jennings45 hours1-1/+68
| | | | ghostel (the terminal behind the agent buffers) gets an eat-style scene through its own 16 ANSI color faces plus the default surface and the two fake-cursor faces; all-the-icons a dired listing where each file's leading marker carries its color face across the 8 hues, their light/dark variants, and the -alt accents. Both are pinned not-loaded apps whose only visibility is the studio preview, so they carry the highest realism bar. Face names are literal, not constructed, to satisfy the coverage gate.
* feat(theme-studio): scenes for tmr, wttrin, alert, org-superstar, ↵Craig Jennings45 hours1-1/+54
| | | | | | nerd-icons-completion tmr shows the list view and tmr-tabulated-view with every column face; wttrin the weather buffer with staleness header, key hints, and the stale modeline lighter; alert one line per severity; org-superstar the bullet swap across levels plus list items; nerd-icons-completion folder icons in the dir face. Phase 4 of the preview run — all 13 small-scene apps now sit in the face-coverage gate.
* feat(theme-studio): scenes for indent-guides, yasnippet, prescient, ↵Craig Jennings45 hours1-1/+49
| | | | | | flyspell-correct highlight-indent-guides shows both methods (character glyphs, column bands) plus the responsive top/stack variants at a marked point; yasnippet a mid-expansion snippet with the active field and the debug overlay; prescient primary/secondary match highlights; flyspell-correct the word under correction with its menu. All four join the face-coverage gate.
* feat(theme-studio): scenes for emms, org-roam, hl-todo, symbol-overlayCraig Jennings45 hours1-1/+66
| | | | emms shows its three UIs (browser tree, playlist, metaplaylist); org-roam the backlinks buffer plus the dailies calendar mark; hl-todo comment keywords and the flymake diagnostic; symbol-overlay all eight pin faces on one defun plus the at-point default. All four join the face-coverage gate.
* feat(theme-studio): minibuffer-stack scenes on one shared sessionCraig Jennings45 hours1-1/+104
| | | | One C-x b consult-buffer scene drawn jointly by vertico, marginalia, consult, embark, and orderless; each app's preview shows the shared session with its own faces at work, then extends it with the states only it owns (vertico multiline, marginalia's full annotation catalog, consult line/grep/async, embark act + collect, orderless component cycling). All five join the face-coverage gate, so every one of their 72 faces appears verbatim in the renderers.
* feat(theme-studio): web-mode scene covering all 81 faces + HTML sampleCraig Jennings46 hours1-1/+49
| | | | The web-mode preview is one mixed document: markup with every tag/attr variant, an inline CSS part, a generic template block (engine-agnostic on purpose), and a script part carrying a JSON island, JSX depths, nested template literals, SQL-in-a-string, a PHP preprocessor island, and JSDoc annotations. The realism gate now covers it, so all 81 faces are exercised. SAMPLES gains an HTML language, which also enriches the syntax and auto-dim previews.
* feat(theme-studio): realistic scenes for company, transient, and friendsCraig Jennings46 hours1-1/+95
| | | | Five daily-driver apps leave the generic face-name list: company (a completion popup with every row variant, inline ghost preview, tooltip search, and the echo frontend), company-box (the icons variant), transient (a magit-commit-style panel exercising every key class), magit-section (status-buffer headings with child counts and the highlighted section), and rainbow-delimiters (buffer-honest nesting depths 1-9 plus the cycle past 9 and both error faces). A new test gates realism: every face of each covered app must appear verbatim in its renderer, so a scene can't silently skip faces.
* feat(theme-studio): pin retired packages so themes keep covering themCraig Jennings46 hours1-1/+1
| | | | PINNED_PACKAGE_FACES is the curated record of packages retired from the config (ghostel, all-the-icons). A pinned package survives inventory regeneration, shows a 'not loaded' label + hover in the app dropdown, and keeps refreshing its face list from the live inventory while that still carries it. An attempted live regen made the fragility concrete: today's daemon session was missing 142 faces from lazily-loaded packages, so regen-from-live is inherently lossy and the pin is the only durable record.
* feat(theme-studio): screenshot harness + ecosystem coverage policyCraig Jennings46 hours1-0/+23
| | | | Two speedrun-enabling pieces. A #preview=<app>&theme=<json> hash handler plus screenshot-previews.sh shoot any app's face table and live preview headlessly under a real theme (WIP.json by default), so preview work can be verified without a human clicking through the studio. The README gains the coverage policy: the studio themes popular packages even when uninstalled, pinning their faces rather than dropping them, and unloaded packages' previews matter more, not less.
* feat(theme-studio): register the ai-term agent-color facesCraig Jennings2 days1-2/+15
| | | | The nine ai-term faces (the bypass-banner accent plus one per Claude Code /color name) join the studio as a bespoke app, seeded with their dupre hues. The preview mirrors what they paint: the banner line and a mock input box per session color. Tuning one here recolors live agents on their next redraw.
* feat(theme-studio): surface the five remaining font-lock facesCraig Jennings4 days1-2/+2
| | | | | | Add the font-lock faces the syntax tier didn't cover (warning, doc-markup, negation-char, and the two regexp-grouping faces) as their own editable categories: warn, dmark, neg, rxgb, rxgc. Each maps 1:1 to its face, seeds from the stock defaults, and is exercised in the code-sample preview via a TODO comment, a docstring substitution, a string regexp, and a C negation. Injected only the five new faces into the default-faces snapshot rather than regenerating it wholesale. A full recapture pulls in unrelated package-inventory drift.
* feat(theme-studio): register nov-reading structural faces and color the previewCraig Jennings4 days1-4/+13
| | | | The nov reading view app gains six faces: a heading and link accent for each of the sepia, dark, and light palettes, seeded to match the module defaults. The book-page preview now draws each page's title in its palette's heading face and an inline phrase in its link face, iterating only the base palette faces so the structural faces color a page rather than getting blank pages of their own.
* feat(theme-studio): render nov-reading preview as a book pageCraig Jennings5 days1-0/+24
| | | | The nov-reading app fell back to the generic preview (face names in their own colors), which doesn't show what a palette looks like. I added a renderer that draws a real book page per palette: a mock page of Hawthorne's "Dr. Heidegger's Experiment" in sepia, dark, and light, with a small-caps byline, a drop cap, and justified serif prose. Each page takes its background and foreground from its palette face, so tuning a palette repaints its page.
* feat(nov): reading-view theme layer with palettes and font sizingCraig Jennings5 days1-1/+1
| | | | | | | | EPUB reading prefs were scattered: a hardcoded Merriweather/180 font-remap in calibredb-epub-config's nov hook, no color control (the old sepia foreground had been stripped), and a frame-global EBook fontaine preset as the only way to size up. That preset resized the font in every buffer in the frame, not just the book. I pulled the reading view into its own layer, modules/nov-reading.el, on top of stock nov (no fork). It owns three things, all buffer-local: a reading palette (sepia/dark/light, each a face the dupre theme owns, sepia the default), the serif typography (family plus a defcustom base height replacing the hardcoded 180), and page font sizing (+/- bump the size live, = resets to the base). Width moves to { }. calibredb-epub-config keeps the library and width/centering layout. Its nov hook now calls into the layer. The three palette faces register as a nov-reading app in theme-studio (face_data.py), so they're tunable there like any other app. I dropped the EBook fontaine preset, since reading size is buffer-local now.
* feat(theme-studio): make wttrin themeable in the package inventoryCraig Jennings6 days1-1/+1
| | | | wttrin loads from an unversioned local checkout (elpa/wttrin/), which the inventory regex already learned to capture (ffa458a6). This long-lived daemon's wttrin face attribution was scrambled by the session's theme reloading, so rather than regenerate the whole inventory from a disturbed daemon I added wttrin's four file-loaded faces from a clean batch load and regenerated the studio. wttrin is now a previewable app, and all studio gates stay green.
* feat(theme-studio): rebuild the dirvish preview as a realistic two-paneCraig Jennings7 days1-12/+53
| | | | The preview was a flat catalog dumping every face on labeled lines. It's now a believable two-pane dirvish: an active directory listing (the real nerd-icon glyph and color per file type, dir-entry counts, file sizes, the hl-line on the selected row, a dimmed backup) beside an ls-l preview of the selected dir. Faces that don't fit a calm listing (vc, git, subtree, media, proc, narrow, emerge) moved to a labeled extras strip below, so all 38 dirvish faces stay covered. Glyphs and colors mirror what nerd-icons actually emits per type.
* refactor(term): finish ghostel retirement (phase 5)Craig Jennings8 days1-11/+3
| | | | Remove the dead ghostel app from theme-studio: the GHOSTEL_FACES/SEED data, the registry row, the renderGhostelPreview previewer, and the package_seed test, then regenerate the tool. ansi-color stays since eat inherits it. Rename testutil-ghostel-buffers to testutil-terminal-buffers and drop make-fake-ghostel-buffer; the toggle-filter test now uses the eat fixture, since agents are eat. Fix the comments that still called the agent buffers ghostel (they're eat now) in eat-config and the ai-term and auto-dim test docstrings. I also package-deleted the unused ghostel ELPA package. Full suite green; the remaining ghostel mentions are accurate migration history.
* feat(theme-studio): make the EAT preview blocks representativeCraig Jennings9 days1-15/+39
| | | | Expand each command's output to realistic length. The eza listing now shows a full directory of nine entries, git status shows staged, unstaged, deleted, and untracked files, git log shows nine commits with a merge graph, and make test shows seven results plus a failure detail and a totals line. One-line results read as toy output; this looks like a real terminal.
* feat(theme-studio): richer EAT preview with realistic terminal activityCraig Jennings9 days1-8/+27
| | | | Replace the thin EAT preview with four realistic blocks above a reference key: an eza --color listing (the widest dircolors palette), git status, git log --oneline --decorate, and a make test run. Together they exercise all 16 ANSI colors in context plus the bold, faint, and prompt-annotation faces. The palette swatch, attribute, and annotation rows sit below as a key. All 24 exposed EAT faces now appear in the preview.
* chore(theme-studio): label the EMMS app and lowercase the expanded labelsCraig Jennings9 days1-1/+1
| | | | Add a display-label override for the emms inventory app (emacs multimedia system), and lowercase all the acronym-expansion labels (eat, lsp, shr, emms) so they match the lowercase package-name labels and the package ids. The hovers keep normal capitalization since they are descriptions, not labels.
* feat(theme-studio): move reuse context from app labels into a hoverCraig Jennings9 days1-3/+4
| | | | Clean the app labels and move the "what reuses this" context into the app dropdown's tooltip, so the labels stay short. The foundational face sets name their consumers on hover: ansi-color (vterm, eshell, compilation, ghostel, eat), shr (eww, nov, mu4e/message), gnus (mu4e article view), and dired (dirvish). Labels now carry only the name plus any acronym expansion. A small APP_HOVERS dict in face_data.py feeds an app "hover" field that sets the dropdown's title on selection.
* chore(theme-studio): spell out the LSP and SHR app namesCraig Jennings9 days1-1/+1
| | | | Match the EAT format: lsp-mode -> Language Server Protocol (LSP), shr -> Simple HTML Renderer (SHR).
* feat(theme-studio): add EAT face docstrings to the studio hoversCraig Jennings9 days1-1/+1
| | | | The EAT faces had no entries in face-docs.json, so their studio hovers showed nothing. Merge in the 24 exposed EAT faces' docstrings. The named color faces are aliases whose first docstring line is alias boilerplate, so this uses the descriptive line ("Face used to render red color text") instead. A full re-dump was avoided because it would drop 85 unrelated erc background-color entries not currently loaded; this is a targeted merge that only adds.
* chore(theme-studio): label the EAT app with its full name (EAT)Craig Jennings9 days1-1/+1
|
* feat(theme-studio): expose EAT terminal faces with a previewCraig Jennings9 days1-2/+14
| | | | Add EAT as a theme-studio app so its faces become editable in the studio: the 16 named ANSI palette faces, the SGR attribute faces (bold, faint, italic, slow/fast blink), and the three shell-prompt annotation faces. renderEatPreview draws a sample terminal with the palette rows, colored ls output, and the prompt annotations, so editing a face updates the preview live. No seed colors are set, so the faces sit at their own defaults until themed, consistent with the vanilla pass. Mirrors the existing ghostel terminal app.
* feat(theme-studio): bind dashboard preview list items to dashboard-items-faceCraig Jennings9 days1-11/+11
| | | | The dashboard preview rendered the project/bookmark/recent rows as plain text, so editing dashboard-items-face didn't recolor them. Wrap those rows in dashboard-items-face so the preview reflects the face. It inherits widget-button (unspecified) while unset, so the rows stay bare until the face is themed -- this only makes the preview responsive. Completes the dashboard preview's face coverage.
* fix(theme-studio): restrict the cursor UI row to fg + bgCraig Jennings9 days1-6/+28
| | | | Emacs draws the cursor as a rectangle: its foreground colors the glyph sitting on it and its background is the cursor color, but weight/slant/underline/strike and box are no-ops on it. The UI table now shows only the fg and bg swatches for the cursor row and mutes the style and box cells to a dash, so the studio stops presenting controls Emacs drops. New #cursorrowtest gate; styletest/boxtest retargeted off cursor (it was UI_FACES[0], their generic subject) onto the first styled face.
* refactor(theme-studio): extract control factories to controls.js, drop dead ↵Craig Jennings10 days1-9/+5
| | | | | | | | 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 Jennings10 days1-22/+11
| | | | | | | | 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 Jennings10 days1-2/+5
| | | | 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 Jennings10 days1-3/+6
| | | | 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 Jennings10 days1-26/+41
| | | | | | | | | | 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.
* feat(theme-studio): nerd-icons gallery as a hue-ordered icon gridCraig Jennings10 days1-31/+144
| | | | | | | | | | 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 Jennings10 days1-5/+58
| | | | | | | | 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.
* test(nerd-icons): dir-precedence probe + legend round-trip (phase 4)Craig Jennings10 days1-0/+9
| | | | | | 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 Jennings10 days1-3/+42
| | | | | | 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): locate preview elements by hover and clickCraig Jennings11 days1-7/+366
| | | | | | | | 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 Jennings14 days1-14/+35
| | | | | | | | | | | | | | | 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 Jennings14 days1-114/+90
| | | | | | | | | | | | 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 Jennings14 days1-24/+20
| | | | | | | | | | | 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): drop the orphaned dropdownRowTextColor helperCraig Jennings14 days1-11/+0
| | | | | | | | 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).
* feat(theme-studio): move the box column between style and contrastCraig Jennings2026-06-201-13/+13
| | | | 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 Jennings2026-06-201-1/+1
| | | | 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 Jennings2026-06-201-5/+32
| | | | 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 Jennings2026-06-201-9/+53
| | | | 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.