From 630cfddc7060c7019815f8e82f87fb629aefebfa Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 2 Jul 2026 23:01:54 -0400 Subject: feat(theme-studio): explicit absolute-vs-relative face height kind 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. --- scripts/theme-studio/test_generate.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'scripts/theme-studio/test_generate.py') diff --git a/scripts/theme-studio/test_generate.py b/scripts/theme-studio/test_generate.py index 0ba35dcc..28c9b88c 100644 --- a/scripts/theme-studio/test_generate.py +++ b/scripts/theme-studio/test_generate.py @@ -254,6 +254,7 @@ class FaceSpecDefaults(unittest.TestCase): "extend": False, "inherit": None, "height": None, + "heightMode": None, }) def test_ui_face_spec_carries_inherit_and_height(self): @@ -286,6 +287,7 @@ class FaceSpecDefaults(unittest.TestCase): "extend": False, "inherit": "base", "height": 1.2, + "heightMode": "rel", }) def test_generated_color_names_are_base_columns_when_legacy(self): @@ -345,6 +347,30 @@ class GeneratorStateHelpers(unittest.TestCase): generate.apply_modeline_height_default(uimap) self.assertEqual(uimap["mode-line"]["height"], 142) + def test_modeline_height_seed_carries_abs_kind(self): + # the seed is a fixed 1/10pt pin, so its kind is explicit -- never + # left for number-type inference (JSON can't carry the distinction) + self.assertEqual(generate.UIMAP["mode-line"]["heightMode"], "abs") + + def test_migrate_legacy_infers_height_kind(self): + # mirrors app-core.js migrateLegacyFace: integer -> abs, fractional + # -> rel, explicit kind wins, identity/absent/non-number infer none + from face_specs import migrate_legacy + self.assertEqual(migrate_legacy({"height": 130})["heightMode"], "abs") + self.assertEqual(migrate_legacy({"height": 1.2})["heightMode"], "rel") + # python keeps the authored type: an integral float is still relative + self.assertEqual(migrate_legacy({"height": 2.0})["heightMode"], "rel") + self.assertEqual( + migrate_legacy({"height": 2, "heightMode": "rel"})["heightMode"], "rel") + self.assertNotIn("heightMode", migrate_legacy({})) + self.assertNotIn("heightMode", migrate_legacy({"height": 1})) + self.assertNotIn("heightMode", migrate_legacy({"height": None})) + + def test_face_spec_defaults_include_height_kind(self): + # the model row exists with a null default in both spec shapes + self.assertIsNone(ui_face_spec()["heightMode"]) + self.assertIsNone(package_face_spec()["heightMode"]) + def test_build_syntax_uses_map_and_style_fallbacks_without_defaults_snapshot(self): syntax = generate.build_syntax( {"kw": [None, True]}, -- cgit v1.2.3