- Fix _load_font_sized double-wrapping the (font, size) tuple on the
missing-font path, which handed callers a tuple instead of a font.
- Fix _set_nested_value skipping an explicit None when the key already
existed, which silently kept stale overrides when a user cleared a
nullable per-mode field or blanked all channels of an indexed color.
- Preserve BDF scalable/native_size metadata through fetchFontCatalog's
catalog-format mapping so maxFixedSize filtering actually applies.
- Stop caching an empty array on a failed font-catalog fetch so a later
call can retry instead of being stuck with the failed result.
- Keep a saved font selected in the style editor even when it no longer
fits a newly declared maxFixedSize, instead of silently deselecting it.
- Don't drop in-progress user edits to fallback fields when a plugin
widget finishes loading asynchronously and takes over the form.
- Tighten the removed font-override endpoint test to assert 405, not
just != 200.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nineteen plugins spell their style elements out longhand instead of
declaring them -- football's block is 701 lines for seven elements -- and
predate this system entirely. Core now recognises that shape, so they pick
up the row-per-element editor and the real font picker on a core update
rather than on a plugin release. Checked against every published schema:
21 plugins adopt, and the defaults of each still validate against the
schema generated for it.
Detection requires *every* field in a block to be one this system
understands. A looser "has at least one style field" rule sweeps in
baseball's `count`, which carries a text_color beside geometry that means
nothing here. That distinction took three attempts to test: the first two
assertions passed under both rules, because an over-eager rule leaves a
fontless block looking untouched and only surfaces as an extra row in the
editor.
The hardcoded font enum is replaced rather than extended. Football lists
five of the thirty-five installed fonts, which is why a font a user
uploads can never appear in one. It is not a curated safe set -- it omits
some twenty other faces that fit the declared size cap just as well -- it
is the fonts that happened to exist when it was written.
Widening it does need a guard, though, and not the one the schema already
has: a bitmap font ignores font_size and renders at its size baked into
the file, so `maximum: 16` cannot stop a 27px face. The picker now filters
out fixed-size fonts taller than the element's own declared ceiling, which
drops exactly the four that would overflow a 32px panel and keeps the
other thirty.
Per-mode overrides stay opt-in: core cannot invent a plugin's display
modes, so `x-style-modes` remains the one line that unlocks them. Their
layout half covers every positionable element rather than only those with
a style block -- the two namespaces do not line up in a hand-written
schema, and football positions six things (logos, timeouts, possession)
that have no style block at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Completes the customization vocabulary: hide an element, align it, and
resize a logo, alongside the font/size/colour/offset that already existed.
All three per mode.
They resolve to "change nothing" until the user asks for something -- True,
None and 1.0 -- rather than to whatever the schema declares. That is the
same invariant the font fields keep: a caller that honours them still
renders an untouched config exactly as it did before they existed. A
schema default therefore does not count as a choice, which matters because
the save flow writes that default into config either way.
scale sits in the layout block with the offsets rather than in the element
block, because it is geometry: a logo has a scale and no font. The widget's
columns come from the schema, so a logo row shows visibility, offsets and
scale and no empty font cell.
Two bugs found by the tests rather than by reading:
- A nullable enum needs null in its enum list, not just in its type. The
mode copy of `align` defaulted to null and then failed its own schema, so
a plugin declaring any enum field with modes could not save at all. Six
tests failed on this before any of them reached what they were testing.
- defaults_from_schema only ever extracted font/font_size/text_color, so
the schema defaults for the new fields were invisible to the resolver and
a declared default read as a user choice.
Widget: the table scrolls horizontally and pins the element-name column.
Nine columns do not fit the config panel, and clipping them hid the offsets
entirely while scrolling them made every row anonymous.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rendered element by element, a realistic scoreboard's customization block
is 65 nested sections, and reaching one per-mode font size takes five
levels of expanding. The widget collapses that to one compact row per
element -- font, size, colour, X, Y -- with a tab per declared mode.
It emits ordinary inputs under the same dotted names the generic renderer
would produce, so the save/validate/merge pipeline is untouched: no hidden
JSON blob and no new server-side parsing. It is driven entirely by the
schema block it is handed, so fields added to the schema later appear
without editing the widget. If it fails to load or throws, the generic
nested rendering it replaces is left in place.
Fixing two things the save path got wrong for nullable fields, found by
posting what the widget actually emits:
- The indexed-array recombiner (text_color.0/.1/.2 -> one list) compared
the declared type to the string 'array', so a per-mode colour, typed
["array", "null"], was never reassembled and failed validation on save.
_parse_form_value_with_schema had the same comparison.
- A blank nullable field became [] rather than None, which then failed the
minItems the colour array declares. Null is the inherit sentinel, so it
has to survive.
And two things the widget itself got wrong, found by looking at it:
- An unset base control fell back to the select's first option, so an
untouched scoreboard claimed every element used 10x20.bdf -- and the
size box then locked itself to that bitmap font's fixed size. Base
controls now show the schema default; mode controls stay blank, because
blank there means inherit.
- Elements arrived alphabetised (Detail and Odds above Score). Flask's
JSON provider sorts keys, so declaration order has to be stated
explicitly; expand_style_elements now emits x-propertyOrder, which the
generic renderer already honoured too.
Size is disabled and shown as fixed for a bitmap font, using the
scalable/native_size the font catalog now reports.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>