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())