Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion map/static/map/finditem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
14 changes: 14 additions & 0 deletions map/static/map/map.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions map/static/map/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down
101 changes: 101 additions & 0 deletions tools/ui_classes.py
Original file line number Diff line number Diff line change
@@ -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())
Loading