** This file was created with an agent. **
Status: PROPOSAL / not implemented — part 1 of 3. As of 2026-07-16,
static REQUIREMENTS,static DEPENDENCY_MAPPINGS, andresolveWidgetSetup()do not exist in the codebase; the snippets below are a proposed API. This is the first document in a three-part series written 2026-02-25, all filed underdocs/planning/:
- this file — proposes
static REQUIREMENTSon widget classes;design-analysis-widget-setup-evaluation.md— critiques this proposal, arguing it treats a symptom and counter-proposing model-declared UI hints;design-analysis-composable-widget-system.md— the revised synthesis (“Beyond REQUIREMENTS”), which supersedes the central theses of both earlier documents and introducesstatic INTERFACE.Read as an evolving discussion; document 3 is the most current view. The analysis of current limitations remains accurate.
In TypeRoof’s current architecture, widget construction knowledge is split between two locations:
widgetBus, ...args)This means every place that builds a widget must redundantly encode:
The require() / InjectDependency pattern in type-driven-ui-basics.mjs
partially addresses this for the type-driven UI pipeline, but it exists only in
the centralized uiElementsMap — not on the widgets themselves.
Widget definition (generic.mjs):
// UIToggleButton knows it needs these args:
constructor(widgetBus, classToken, labelIsOn, labelIsOff, title) { ... }
Type-driven setup (type-driven-ui.mjs, centralized registry):
[BooleanModel, [UIToggleButton
, require('settings:internalPropertyName', 'boolean')
, require('classToken')
, require('label', val=>`turn ${val} off`)
, require('label', val=>`turn ${val} on`)
, require('label', val=>`Toggle ${val}`)
]]
Manual call site (videoproof-array-v2.mjs, since renamed to
videoproof.typeroof.jsx; ad-hoc):
[
{ zone: 'main', rootPath: Path.fromParts('.', 'keyMoments', '0', 'textColor') }
, [] // dependency mappings - caller must know!
, UIColorChooser
, zones
, 'Text Color'
, this._getDefaults.bind(this, ...)
, updateDefaultsDependencies
, requireUpdateDefaults
]
The type-driven path (uiElementsMap → _getArgumentConfig → _defineGenericWidgets)
resolves require() objects at widget-build time. But the ad-hoc path in layout files
has no such resolution — the caller must manually construct the entire setup array.
Move the require()-based setup declarations onto the widget class itself
as a static property. The widget class declares what it needs; the container
resolves those needs at construction time.
export class UIToggleButton extends _BaseComponent {
static REQUIREMENTS = [
require('settings:internalPropertyName', 'boolean')
, require('classToken')
, require('label', val=>`turn ${val} off`)
, require('label', val=>`turn ${val} on`)
, require('label', val=>`Toggle ${val}`)
];
constructor(widgetBus, classToken, labelIsOn, labelIsOff, title) { ... }
}
export class UIColorChooser extends _BaseContainerComponent {
static REQUIREMENTS = [
require('settings:rootPath')
, require('zones')
, require('label')
, require('getDefault')
, require('updateDefaultsDependencies')
, require('raw:requireUpdateDefaults')
];
constructor(widgetBus, zones, label, getDefault,
updateDefaultsDependencies, requireUpdateDefaults) { ... }
}
Instead of manually building [settings, dependencyMappings, Constructor, ...args]:
// Option A: Fully automatic from REQUIREMENTS
const widgetDef = resolveWidgetSetup(UIColorChooser, {
zone: 'main'
, rootPath: Path.fromParts('.', 'keyMoments', '0', 'textColor')
, injectable
, ppsRecord
, fieldName: 'textColor'
});
// Returns: [settings, dependencyMappings, UIColorChooser, ...resolvedArgs]
// Option B: Use REQUIREMENTS as defaults, override specific args
const widgetDef = resolveWidgetSetup(UIColorChooser, {
zone: 'main'
, rootPath: Path.fromParts('.', 'keyMoments', '0', 'textColor')
, overrides: { label: 'Text Color' }
, injectable
, ppsRecord
, fieldName: 'textColor'
});
// Before: uiElementsMap must duplicate what the widget already knows
[BooleanModel, [UIToggleButton
, require('settings:internalPropertyName', 'boolean')
, require('classToken')
, require('label', val=>`turn ${val} off`)
, ...
]]
// After: uiElementsMap can reference the widget's own REQUIREMENTS
// or override specific ones
[BooleanModel, UIToggleButton] // uses static REQUIREMENTS as-is
// Or with overrides for model-specific customizations:
[BooleanModel, [UIToggleButton, { labelIsOn: val=>`turn ${val} off` }]]
InjectDependency (require)The existing require() function creates InjectDependency objects with
name and payload. This can be extended:
export class InjectDependency {
constructor(name, payload=null, typeHint=_NOTDEF) {
this.name = name;
this.payload = payload;
// NEW: type constraint for validation
this.typeHint = typeHint;
}
}
// Existing — still works
require('label')
require('settings:internalPropertyName', 'value')
// Enhanced — with type hints for validation and documentation
require('label', null, String)
require('settings:internalPropertyName', 'value', 'dependencyMapping')
require('zones', null, Map)
REQUIREMENTS Static PropertyEach widget class declares its requirements as a static array:
class MyWidget extends _BaseComponent {
static REQUIREMENTS = [
// Each entry is an InjectDependency or a literal value
require('settings:internalPropertyName', 'value')
, require('label')
];
// Constructor params match REQUIREMENTS order (after widgetBus):
constructor(widgetBus, /* resolved from REQUIREMENTS */) { ... }
}
Convention: REQUIREMENTS entries correspond positionally to constructor
arguments after widgetBus. The resolution system processes each entry
and passes the result as a constructor argument.
DEPENDENCY_MAPPINGS Static PropertyFor widgets that need model-path-to-internal-name mappings:
class MyWidget extends _BaseComponent {
// Admissible model types this widget can consume
static ADMISSIBLE_TYPES = new Set([PadModeModel, _AbstractEnumModel]);
// Default dependency mappings (can be overridden)
static DEFAULT_DEPENDENCY_MAPPINGS = [
// [externalPath, internalName] — or auto-derived from model type
];
}
A new function in basics.mjs or type-driven-ui-basics.mjs:
/**
* Resolve a widget's REQUIREMENTS against a context to produce
* a standard widget setup array.
*
* @param {typeof _BaseComponent} WidgetClass
* @param {Object} context - { zone, rootPath, injectable, ppsRecord,
* fieldName, overrides }
* @returns {Array} [settings, dependencyMappings, WidgetClass, ...args]
*/
export function resolveWidgetSetup(WidgetClass, context) {
const requirements = WidgetClass.REQUIREMENTS || [];
const settings = new Map();
const dependencyMappings = new Map();
const args = [];
if(context.zone)
settings.set('zone', context.zone);
if(context.rootPath)
settings.set('rootPath', context.rootPath);
for(const requirement of requirements) {
if(!(requirement instanceof InjectDependency)) {
// Literal value — pass through
args.push(context.overrides?.[requirement] ?? requirement);
continue;
}
// Delegate to existing _getArgumentConfig logic
const [argSettings, argDeps, argArgs] =
_resolveRequirement(requirement, context);
for(const [k, v] of argSettings) settings.set(k, v);
for(const [k, v] of argDeps) dependencyMappings.set(k, v);
args.push(...argArgs);
}
return [
Object.fromEntries(settings)
, Array.from(dependencyMappings)
, WidgetClass
, ...args
];
}
The key insight is that _resolveRequirement reuses the existing
_getArgumentConfig switch-case logic. No new resolution mechanism
needed — just a new entry point.
Files: lib/js/components/generic.mjs, lib/js/components/ui-color-chooser.mjs,
lib/js/components/ui-char-groups.mjs, etc.
static REQUIREMENTS = [...] to each widget classuiElementsMap currently declaresresolveWidgetSetup FunctionFile: lib/js/components/type-driven-ui-basics.mjs
_getArgumentConfig into a standalone function_getArgumentConfig method can delegate to this function_BaseTypeDrivenContainerComponent._defineGenericWidget can optionally
use WidgetClass.REQUIREMENTS as a fallback when no uiElementsMap entry existsuiElementsMapFile: lib/js/components/type-driven-ui.mjs
REQUIREMENTS matches the uiElementsMap entry exactly,
the map entry can be simplified to just [ModelType, WidgetClass]Files: lib/js/components/layouts/videoproof.typeroof.jsx (formerly
videoproof-array-v2.mjs), lib/js/components/layouts/motion-stage.mjs
(formerly stage-and-actors.mjs), etc.
[settings, deps, Constructor, ...args] arrays
with resolveWidgetSetup(Constructor, context) callsFile: lib/js/components/basics.mjs
ComponentWrapper is the natural place to integrate widget-declared requirements
because it already handles:
absPathDependencies)rootPath managementcreate())The _CommonContainerComponent._initWrapper method (line 805) could be enhanced:
_initWrapper(childrenWidgetBus, settings, dependencyMappings, Constructor, ...args) {
// NEW: If Constructor has REQUIREMENTS and args are empty,
// auto-resolve from REQUIREMENTS
if(Constructor.REQUIREMENTS && args.length === 0) {
const resolved = resolveWidgetSetup(Constructor, {
...settings
, injectable: this._injectable // or however injectable is accessed
});
// Use resolved settings/deps/args
[settings, dependencyMappings, , ...args] = resolved;
}
const hostElement = settings.zone
? this._zones.get(settings.zone)
: null;
// ... rest unchanged
}
Each widget class tells you exactly what it needs. No need to search
uiElementsMap, layout files, or getActorWidgetSetup to understand
a widget’s requirements.
Requirements live on the class — uiElementsMap and call sites become
consumers of that truth rather than independent sources that can diverge.
The most common pattern in layout files is manually constructing the
[settings, deps, Constructor, ...args] tuple. With REQUIREMENTS on
the class, this becomes a single function call.
REQUIREMENTS work exactly as beforeuiElementsMap entries override REQUIREMENTS when presentComponentWrapper doesn’t change its external APIWith requirements declared on classes:
WidgetClass.REQUIREMENTS to understand what to provideThe require() / InjectDependency system already exists. This proposal
just moves where the require() calls live — from the registry to the class.
Some widgets need different setups depending on context. For example,
UIColorChooser in a key moment editor needs updateDefaultsDependencies
resolved from the parent’s injectable, but the same class used in a
standalone color picker might have different defaults.
Mitigation: REQUIREMENTS define the shape of what’s needed (via
require() names), not the values. The resolution context provides
values. Overrides handle special cases.
REQUIREMENTS must match constructor parameter order exactly. If a constructor is refactored, REQUIREMENTS must change too.
Mitigation: This is already true for uiElementsMap — it’s just
relocating the coupling. TypeScript types could enforce the contract.
Alternatively, a named-parameter pattern (Map or object) could replace
positional arguments in the future.
Some requirements have compound effects — require('requireUpdateDefaults')
both adds dependency mappings AND injects an argument. This compound
behavior is currently handled by the switch-case in _getArgumentConfig.
Moving requirements to the class doesn’t simplify this complexity.
Mitigation: The resolution function delegates to the same _getArgumentConfig
logic. The complexity is contained, not duplicated.
If class SpecialSelect extends UISelectInput, should REQUIREMENTS
be inherited? Overridden? Merged?
Mitigation: Use standard JavaScript static property semantics:
subclass defines its own REQUIREMENTS or inherits from parent.
The createClass pattern (used for UISelectInput) would need to
propagate REQUIREMENTS from the wrapped class.
Until all widgets have REQUIREMENTS, the system must support both
uiElementsMap-driven and REQUIREMENTS-driven resolution.
Mitigation: uiElementsMap takes precedence. REQUIREMENTS is
the fallback. This is a transitional cost, not a permanent one.
createClass Wrapper PatternMany UI widgets are created via _UIAbstractPlainInputWrapper.createClass():
UISelectInput = _UIAbstractPlainInputWrapper.createClass(
'UISelectInput', _AbstractPlainSelectInput);
These wrapper classes would need a way to derive REQUIREMENTS from their
wrapped inner class, or declare their own. This adds complexity to the
createClass factory.
Mitigation: The createClass factory can copy/adapt REQUIREMENTS from
the inner class, similar to how it already wraps the constructor.
| Aspect | Current | Proposed |
|---|---|---|
| Widget setup knowledge | Split: class + registry + call site | Primarily on class |
| Adding new widget type | Edit class + registry + all call sites | Edit class (REQUIREMENTS), registry optional |
| Adding new field to model | Edit model + REGISTERED_FIELDS + maybe registry | Edit model + (auto-discovered if REQUIREMENTS exist) |
| Call site complexity | Full tuple: [settings, deps, Class, ...args] |
resolveWidgetSetup(Class, context) |
| Discoverability | Search multiple files | Read WidgetClass.REQUIREMENTS |
| Validation | Runtime errors when args mismatch | Could be checked statically |
| Migration cost | N/A | Gradual: add REQUIREMENTS to classes over time |
REGISTERED_GENERIC_KEYMOMENT_FIELDSThe gatekeeper set REGISTERED_GENERIC_KEYMOMENT_FIELDS in motion-stage.mjs
(formerly stage-and-actors.mjs)
could eventually become unnecessary if the type-driven UI system can automatically
determine which fields have valid UI mappings.
With REQUIREMENTS on widget classes and types mapped in uiElementsMap, the
_defineGenericWidgets method could check:
// Instead of:
fieldName => REGISTERED_GENERIC_KEYMOMENT_FIELDS.has(fieldName)
// Could become:
fieldName => {
const FieldType = TypeClass.fields.get(fieldName);
return genericTypeToUIElement(FieldType, false) !== false;
}
This would eliminate the “invisible gate” problem entirely — if a type has a UI mapping, its fields automatically get editors.
static REQUIREMENTS to the most commonly used widgets
(UIToggleButton, UIColorChooser, UICharGroupContainer)resolveWidgetSetup() to type-driven-ui-basics.mjsThis validates the design with minimal risk. The existing system continues to work unchanged. New code can opt into the simpler pattern.
Once the pattern proves itself:
uiElementsMap entriesComponentWrapper._initWrapperREGISTERED_GENERIC_KEYMOMENT_FIELDSGenerated: 2026-02-25, based on analysis of TypeRoof widget architecture Key files studied: basics.mjs (ComponentWrapper, _CommonContainerComponent), type-driven-ui-basics.mjs (require, InjectDependency, _getArgumentConfig, _defineGenericWidgets, createTypeToUIElementFunction), type-driven-ui.mjs (uiElementsMap), generic.mjs (UI widget classes), videoproof-array-v2.mjs, stage-and-actors.mjs (layout widget setup patterns) Note: videoproof-array-v2.mjs → videoproof.typeroof.jsx and stage-and-actors.mjs → motion-stage.mjs since this was written. Filed under docs/planning/ 2026-07-16 as part 1 of the widget-setup series.