feat(element-style): adopt hand-written customization blocks, and widen the font list

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>
This commit is contained in:
Chuck
2026-09-11 11:01:01 -04:00
co-authored by Claude Opus 5
parent 6cd886cd78
commit e86dc9c27b
4 changed files with 423 additions and 5 deletions
@@ -127,6 +127,27 @@
// ---- individual controls -------------------------------------------
/**
* Fonts this field can actually honour.
*
* A bitmap (BDF) face renders at the one size baked into the file and
* ignores the size setting, so an element that caps size at 16 can
* still be handed a 27px face and overflow a 32px panel. The cap comes
* from the element's own font_size.maximum, so this hides exactly the
* faces that cannot meet the limit the plugin already declared --
* rather than a list curated by hand, which is what these fields used
* to carry and why an uploaded font could never appear in one.
*/
function usableFonts(fonts, maxFixedSize) {
if (!maxFixedSize) { return fonts; }
return fonts.filter(function (f) {
if (f.scalable !== false) { return true; }
// An unknown native size is not evidence it is too big.
if (!f.nativeSize) { return true; }
return f.nativeSize <= maxFixedSize;
});
}
function fontControl(name, current, fonts, optional, onChange) {
var select = el('select', {
name: name,
@@ -334,7 +355,10 @@
var types = Array.isArray(declared) ? declared : [declared];
if (opts.key === 'font' || prop['x-widget'] === 'font-selector') {
return fontControl(opts.name, opts.current, opts.fonts,
var xOptions = prop['x-options'] || prop['x_options'] || {};
var cap = Number(xOptions.maxFixedSize) || opts.maxFixedSize || null;
return fontControl(opts.name, opts.current,
usableFonts(opts.fonts, cap),
opts.optional, opts.onFontChange);
}
if (types.indexOf('boolean') !== -1) {
@@ -411,6 +435,9 @@
current: effective(value, path, prop, optional),
optional: optional,
fonts: opts.fonts,
// The element's declared size ceiling is what a fixed-size
// font has to fit under.
maxFixedSize: (props.font_size || {}).maximum || null,
onFontChange: function () { syncSize(fontSelect); }
});
if (col.key === 'font') { fontSelect = node; }