feat(web): every advertised layout offset gets a control in the style editor

The editor took the whole layout section over but matched offsets to style
rows by exact key. A hand-written schema's two blocks were never named alike --
football styles score_text but positions score -- so of football's eleven
positionable things only status_text had a control. Score, odds, both logos,
timeouts, possession, down-and-distance, date, time and records were options
the schema advertised and the renderer reads, reachable nowhere in the UI.

Core now resolves each style element's layout key through alias_keys, the map
the resolver already reads offsets with, and records it as x-layout-key. The
widget reads that rather than carrying a second copy of the rules, and posts
under the key the schema declares: football's own offset reader looks up
layout.score, so a value saved as layout.score_text would be kept and never
drawn. Layout entries no style element claims get an "Other positions" table
with its own columns, in every mode panel as well as the base one, in the order
the plugin declared them.

Verified in a browser against football's real schema: 92 of 92 layout fields
(23 base, 23 per mode) rendered exactly once under their declared names, none
posted under a style key, no duplicated field names, favorite_result_colors
still editable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-14 12:40:28 -04:00
co-authored by Claude Opus 5
parent 9eede6c0d4
commit 1720c22a28
3 changed files with 230 additions and 21 deletions
+28
View File
@@ -761,6 +761,11 @@ def _adopt_handwritten_block(schema: Dict[str, Any],
customization.setdefault('x-widget', 'style-editor')
props = customization['properties']
layout_block = props.get('layout')
layout_fields = (layout_block.get('properties')
if isinstance(layout_block, dict) else None)
layout_fields = layout_fields if isinstance(layout_fields, dict) else {}
for key in element_keys:
_upgrade_font_property(props[key])
# Marked like a declared element, so consumers can tell the style
@@ -769,12 +774,35 @@ def _adopt_handwritten_block(schema: Dict[str, Any],
# feature with its own fields -- without this the editor treats it
# as an element and every row grows an "enabled"/"win color" column.
props[key]['x-style-managed'] = True
# Where this element's offsets live, resolved through the same alias
# map the renderer reads them with. The two blocks were never named
# alike -- football styles score_text but positions score -- and the
# editor matched them by exact name, so it drew one offset in eleven
# and the rest had no control anywhere. Recording the answer here
# keeps the alias rules in one place instead of a JavaScript copy.
layout_key = next((name for name in alias_keys(key)
if isinstance(layout_fields.get(name), dict)), None)
if layout_key is not None:
props[key]['x-layout-key'] = layout_key
if layout_fields:
# Positions are listed in the order the plugin declared them. Flask's
# JSON provider sorts keys, which would put the logos after the date.
layout_block.setdefault('x-propertyOrder', list(layout_fields))
modes = customization.get('x-style-modes')
if isinstance(modes, list) and modes:
props.setdefault('modes',
_modes_block_from_properties(props, element_keys,
modes))
mode_blocks = props['modes'].get('properties') if isinstance(
props.get('modes'), dict) else None
for mode_block in (mode_blocks or {}).values():
mode_layout = ((mode_block or {}).get('properties') or {}).get('layout')
if (isinstance(mode_layout, dict)
and isinstance(mode_layout.get('properties'), dict)):
mode_layout.setdefault('x-propertyOrder',
list(mode_layout['properties']))
# Stated explicitly because the config form serialises the schema with
# Flask's JSON provider, which sorts keys -- without this the elements