From 021d8b2bc7a06dce951d6d2d6ec5f34b32828ec5 Mon Sep 17 00:00:00 2001 From: Valentin PLANES Date: Mon, 3 Aug 2026 02:44:46 +0200 Subject: [PATCH] frontend: fix elements left unstyled by the primitive rename The selection dialogs rendered their rows as raw text -- "Concrete51,836" with no column, hairline or hover -- because selection.js still built them from .itemLocationRow/.itemLocationLabel/.itemLocationCount, which were replaced by the .row/.row-label/.row-count primitives. Both the "List objects" and "Total inventory" views go through that one renderList, so both were affected. Auditing every class the JS emits against the classes the stylesheets actually define turned up two more of the same mistake: - finditem.js built the vehicle modal's "Fuel loaded" label with .buildingModalSectionLabel; every other call site had become UI.kicker() when that class was removed. - panels.js toggles .is-active on the hamburger to mark "the layers dock is collapsed", but the rule was never renamed from .panelHidden, so the accent that says "click here to get the panel back" was gone. tools/ui_classes.py is that audit, kept. It flags any element built with no styled class at all, which is exactly this failure mode, and ignores the normal case of a feature hook sitting next to a primitive (`el("div", "row row-hover networkRow")`). No browser and no save needed, so it is cheap to run after any rename. Verified it reports the bug above when reintroduced, and is clean otherwise. Co-Authored-By: Claude Opus 5 (1M context) --- CONTRIBUTING.md | 5 ++ map/static/map/finditem.js | 2 +- map/static/map/map.css | 14 +++++ map/static/map/selection.js | 6 +-- tools/ui_classes.py | 101 ++++++++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 tools/ui_classes.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19125b7..10760a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,6 +121,7 @@ tests, so these are what make a CSS or layout change verifiable: py tools/ui_shots.py --serve --out ui_shots/before # record, then make changes py tools/ui_shots.py --serve --out ui_shots/after --baseline ui_shots/before py tools/ui_behaviour.py --serve # dialogs, Escape layers, docks +py tools/ui_classes.py # static: nothing renders unstyled ``` `ui_shots.py` captures 17 UI states at three viewport widths and diffs them @@ -129,6 +130,10 @@ screenshot cannot see (that dialogs are real modals, that the tooltip still paints above one, that opening a category does not resize the map). Both run against `dist/`, so build or copy the changed files there first. +`ui_classes.py` needs no browser at all: it flags any element the JS builds +whose classes are ALL unstyled, which is what a renamed CSS class with a missed +call site looks like. Worth running after any rename in `ui.css`/`map.css`. + Note: `sav_core` embeds `game_data/generated/{docs,world}/*.json` and the icon manifest at compile time, and **nothing generated is committed**, so the Rust crates do not build until you have either run `game_data/extract_all.py` or diff --git a/map/static/map/finditem.js b/map/static/map/finditem.js index 5b42367..ea228cd 100644 --- a/map/static/map/finditem.js +++ b/map/static/map/finditem.js @@ -1084,7 +1084,7 @@ var FindItem = {}; } if (info.fuelInventory && info.fuelInventory.length > 0) { - buildingModalRecipes.appendChild(el("div", "buildingModalSectionLabel", "Fuel loaded")); + buildingModalRecipes.appendChild(UI.kicker("Fuel loaded")); var fuelList = el("div", "list"); renderLocationList(fuelList, info.fuelInventory.map(function(entryRow) { return [entryRow.label, entryRow.count.toLocaleString() + (entryRow.isFluid ? " m³" : ""), "item", entryRow.item]; diff --git a/map/static/map/map.css b/map/static/map/map.css index c624f23..da6e8ae 100644 --- a/map/static/map/map.css +++ b/map/static/map/map.css @@ -2937,6 +2937,20 @@ button:focus-visible, select:focus-visible, input[type="checkbox"]:focus-visible font-weight: 600; } +/* While the layers dock is collapsed the hamburger stays quietly accented, so + it reads as "the way to get the panel back" rather than just a menu icon. + (panels.js toggles .is-active; this is the rule that got lost when the + button became a .btn and the class was renamed from .panelHidden.) */ +#menuButton.is-active { + color: var(--accent-soft); + background: var(--accent-bg); +} + +#menuButton.is-active:hover { + color: var(--text-bright); + background: var(--accent-bg-hover); +} + /* Brand mark (not a control -- see index.html). */ #logoMark { display: flex; diff --git a/map/static/map/selection.js b/map/static/map/selection.js index 579fa8e..57a0715 100644 --- a/map/static/map/selection.js +++ b/map/static/map/selection.js @@ -97,9 +97,9 @@ var SelectionTool = {}; function renderList(rows) { modalList.innerHTML = ""; rows.forEach(function(pair) { - var row = el("div", "itemLocationRow"); - row.appendChild(el("span", "itemLocationLabel", pair[0])); - row.appendChild(el("span", "itemLocationCount", pair[1])); + var row = el("div", "row row-hover"); + row.appendChild(el("span", "row-label", pair[0])); + row.appendChild(el("span", "row-count", pair[1])); modalList.appendChild(row); }); } diff --git a/tools/ui_classes.py b/tools/ui_classes.py new file mode 100644 index 0000000..c4f5e23 --- /dev/null +++ b/tools/ui_classes.py @@ -0,0 +1,101 @@ +"""Find DOM elements the JS builds with no styling behind them at all. + +The frontend styles content through a small set of primitives in ui.css +(.btn, .field, .row, .list, .bar, .kicker, .chip...). A feature adds its own +class next to one of those as a JS/CSS hook -- `el("div", "row row-hover +networkRow")`. That is fine and expected. + +What is NOT fine is an element whose classes are ALL unstyled, which is what a +rename that missed a call site looks like: the element still gets built, no rule +matches it any more, and it renders as raw unstyled text. That shipped once -- +the selection inventory list rendered as "Concrete51,836" for a while, because +selection.js still asked for .itemLocationRow/.itemLocationLabel after those +rules were replaced by .row/.row-label. + + py tools/ui_classes.py + +Exit code is non-zero if any element is completely unstyled. Static check -- +no browser, no save, nothing to serve. +""" + +import glob +import io +import os +import re +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +STATIC = os.path.join(REPO, "map", "static", "map") + +# Classes nothing in our stylesheets defines on purpose: Leaflet's own, and +# state hooks that exist only for JS to query. +EXTERNAL_PREFIXES = ("leaflet-",) + + +def definedClasses(): + """Every class name our stylesheets (and Leaflet's) actually style.""" + defined = set() + for name in ("map.css", "ui.css", os.path.join("vendor", "leaflet.css")): + path = os.path.join(STATIC, name) + if not os.path.isfile(path): + continue + text = io.open(path, encoding="utf-8", errors="replace").read() + text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) + defined.update(re.findall(r"\.(-?[A-Za-z_][\w-]*)", text)) + return defined + + +# Each pattern captures a full class ATTRIBUTE (possibly several classes), so +# the check can ask "did any of these get styled" rather than judging names one +# at a time. +CLASS_PATTERNS = [ + r'\bel\(\s*"[a-zA-Z]+"\s*,\s*"([^"]+)"', # el("div", "a b c") + r'\.className\s*=\s*"([^"]+)"', # node.className = "a b" + r'\bUI\.el\(\s*"[a-zA-Z]+"\s*,\s*"([^"]+)"', +] + + +def emittedClassLists(): + """[(file, line, classList)] for every element the JS builds with classes.""" + out = [] + for path in sorted(glob.glob(os.path.join(STATIC, "*.js"))): + name = os.path.basename(path) + if name in ("ui.js", "worker.js"): + continue # ui.js DEFINES the primitives; worker.js has no DOM. + for lineNo, line in enumerate(io.open(path, encoding="utf-8"), 1): + for pattern in CLASS_PATTERNS: + for match in re.findall(pattern, line): + # Skip concatenations -- the literal is only half the name. + if match.rstrip().endswith(("-", "_")): + continue + classes = [c for c in match.split() if c] + if classes: + out.append((name, lineNo, classes)) + return out + + +def main(): + defined = definedClasses() + problems = [] + for name, lineNo, classes in emittedClassLists(): + if any(c.startswith(EXTERNAL_PREFIXES) for c in classes): + continue + if not any(c in defined for c in classes): + problems.append((name, lineNo, classes)) + + if not problems: + print("every element the JS builds has at least one styled class") + return 0 + + print("%d element(s) built with no styled class at all:\n" % len(problems)) + for name, lineNo, classes in problems: + print(" %s:%d" % (name, lineNo)) + print(" class=\"%s\"" % " ".join(classes)) + print("\nEach of these renders unstyled. Either the class was renamed and " + "this call site was missed,\nor it needs a ui.css primitive " + "(.row / .btn / .field / .list / .kicker / ...) alongside it.") + return 1 + + +if __name__ == "__main__": + sys.exit(main())