Widget Registry¶
How widgets are discovered and how a placement becomes a resolved config. Lives in app/src/core/registry.ts and app/src/core/widget-types.ts.
Auto-discovery¶
The registry globs widget manifests at build time:
The [!_] excludes folders starting with an underscore (so _template is skipped). For each match it reads the manifest export and indexes it by id. Two boot-time guards throw clear errors:
- A module missing its
manifestexport. - A duplicate
id(names both offending paths).
The result is widgetRegistry: Record<string, WidgetManifest>. getWidgetManifest(id) looks one up, throwing with the known-widget list on a miss.
Surface validation¶
assertSurfaceSupported(manifest, surface) throws if you place a widget on a surface its manifest doesn't declare — so a misconfiguration in settings.ts fails loudly at boot rather than rendering nothing.
Resolution — resolveWidget¶
resolveWidget(placement, surface) turns a WidgetPlacement (from settings.ts) plus the target surface into a ResolvedWidgetConfig the shell renders. It validates the surface, then merges each field:
manifest
defaults←placement
producing concrete values for panelWidth, size, hasUI, chromeless, order, stayOnMobile, and the header. The header merges a third level — the global settings.ui.widgetHeader sits between the kit default and the per-placement override:
DEFAULT_WIDGET_HEADER←settings.ui.widgetHeader←placement.header
This is the same kit-default ← global ← per-item pattern used throughout the framework.
Icons per surface¶
iconForSurface(manifest, surface) returns manifest.icons?.[surface] or falls back to manifest.icon. mobileIconFor(manifest) resolves the mobile-carousel icon, preferring an explicit 'mobile' override, then the 'top-bar' icon (already light for the colored bar), then the base — so a left-panel-only widget (dark base icon) doesn't render an unreadable dark glyph on the blue mobile bar.
Mobile widgets¶
resolveMobileWidgets(names) resolves settings.mobileWidgets (an ordered list of names) for the mobile carousel. Mobile reuses each widget's existing component, so there's no dedicated 'mobile' surface — a name just needs a manifest with at least one UI surface, and it's resolved against the widget's first surface so header/panelWidth defaults still flow through resolveWidget. An unknown name throws at boot.